diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 3163c4bde4704..c9694ad7b9423 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -4,7 +4,6 @@ "server": false, "ui": true, "requiredPlugins": [ - "data", "inspector", "uiActions" ], diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 48e5483124704..6b451e71522c5 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -102,8 +102,6 @@ const createStartContract = (): Start => { getAttributeService: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), - filtersAndTimeRangeFromContext: jest.fn(), - filtersFromContext: jest.fn(), }; return startContract; }; diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 508c82c4247ed..319cbf8ec44b4 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -17,13 +17,7 @@ * under the License. */ import React from 'react'; -import { - DataPublicPluginSetup, - DataPublicPluginStart, - Filter, - TimeRange, - esFilters, -} from '../../data/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; import { getSavedObjectFinder } from '../../saved_objects/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; import { Start as InspectorStart } from '../../inspector/public'; @@ -44,9 +38,6 @@ import { IEmbeddable, EmbeddablePanel, SavedObjectEmbeddableInput, - ChartActionContext, - isRangeSelectTriggerContext, - isValueClickTriggerContext, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { AttributeService } from './lib/embeddables/attribute_service'; @@ -92,18 +83,6 @@ export interface EmbeddableStart { type: string ) => AttributeService; - /** - * Given {@link ChartActionContext} returns a list of `data` plugin {@link Filter} entries. - */ - filtersFromContext: (context: ChartActionContext) => Promise; - - /** - * Returns possible time range and filters that can be constructed from {@link ChartActionContext} object. - */ - filtersAndTimeRangeFromContext: ( - context: ChartActionContext - ) => Promise<{ filters: Filter[]; timeRange?: TimeRange }>; - EmbeddablePanel: EmbeddablePanelHOC; getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC; getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; @@ -155,41 +134,6 @@ export class EmbeddablePublicPlugin implements Plugin { - try { - if (isRangeSelectTriggerContext(context)) - return await data.actions.createFiltersFromRangeSelectAction(context.data); - if (isValueClickTriggerContext(context)) - return await data.actions.createFiltersFromValueClickAction(context.data); - // eslint-disable-next-line no-console - console.warn("Can't extract filters from action.", context); - } catch (error) { - // eslint-disable-next-line no-console - console.warn('Error extracting filters from action. Returning empty filter list.', error); - } - return []; - }; - - const filtersAndTimeRangeFromContext: EmbeddableStart['filtersAndTimeRangeFromContext'] = async ( - context - ) => { - const filters = await filtersFromContext(context); - - if (!context.data.timeFieldName) return { filters }; - - const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - context.data.timeFieldName, - filters - ); - - return { - filters: restOfFilters, - timeRange: timeRangeFilter - ? esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter) - : undefined, - }; - }; - const getEmbeddablePanelHoc = (stateTransfer?: EmbeddableStateTransfer) => ({ embeddable, hideHeader, @@ -216,8 +160,6 @@ export class EmbeddablePublicPlugin implements Plugin new AttributeService(type, core.savedObjects.client), - filtersFromContext, - filtersAndTimeRangeFromContext, getStateTransfer: (history?: ScopedHistory) => { return history ? new EmbeddableStateTransfer(core.application.navigateToApp, history) diff --git a/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts b/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts index 7a95709ac28ba..fa9ace1a36c69 100644 --- a/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts @@ -22,6 +22,6 @@ import { Trigger } from '.'; export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; export const applyFilterTrigger: Trigger<'FILTER_TRIGGER'> = { id: APPLY_FILTER_TRIGGER, - title: 'Filter click', + title: 'Apply filter', description: 'Triggered when user applies filter to an embeddable.', }; diff --git a/x-pack/package.json b/x-pack/package.json index e3104aabbb02b..dcba01a771fd5 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -75,6 +75,7 @@ "@types/hoist-non-react-statics": "^3.3.1", "@types/history": "^4.7.3", "@types/jest": "^25.2.3", + "@types/jest-specific-snapshot": "^0.5.4", "@types/joi": "^13.4.2", "@types/js-search": "^1.4.0", "@types/js-yaml": "^3.11.1", @@ -119,6 +120,7 @@ "@types/xml2js": "^0.4.5", "@types/stats-lite": "^2.2.0", "@types/pretty-ms": "^5.0.0", + "@types/webpack-env": "^1.15.2", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", "autoprefixer": "^9.7.4", @@ -158,6 +160,7 @@ "jest-cli": "^25.5.4", "jest-styled-components": "^7.0.2", "jsdom": "13.1.0", + "jsondiffpatch": "0.4.1", "loader-utils": "^1.2.3", "madge": "3.4.4", "marge": "^1.0.1", @@ -394,4 +397,4 @@ "cypress-multi-reporters" ] } -} +} \ No newline at end of file diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset.stories.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset.stories.tsx new file mode 100644 index 0000000000000..0b99bbce50288 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset.stories.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { reduxDecorator, getAddonPanelParameters } from '../../../../storybook'; +import { Asset, AssetComponent } from '../'; +import { AIRPLANE, MARKER, assets } from './assets'; + +storiesOf('components/Assets/Asset', module) + .addDecorator((story) =>
{story()}
) + .addDecorator(reduxDecorator({ assets })) + .addParameters(getAddonPanelParameters()) + .add('redux: Asset', () => { + return ; + }) + .add('airplane', () => ( + + )) + .add('marker', () => ( + + )); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset_manager.stories.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset_manager.stories.tsx index 1434ef60cf0d8..673c66734b39a 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset_manager.stories.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/asset_manager.stories.tsx @@ -4,35 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; -import React from 'react'; +import { reduxDecorator, getAddonPanelParameters } from '../../../../storybook'; import { AssetManager, AssetManagerComponent } from '../'; - -import { Provider, AIRPLANE, MARKER } from './provider'; +import { assets } from './assets'; storiesOf('components/Assets/AssetManager', module) - .add('redux: AssetManager', () => ( - - - - )) + .addDecorator(reduxDecorator({ assets })) + .addParameters(getAddonPanelParameters()) + .add('redux: AssetManager', () => ) .add('no assets', () => ( - - - + )) .add('two assets', () => ( - - - + )); diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/assets.ts b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/assets.ts new file mode 100644 index 0000000000000..3b5576667ed26 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/assets.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AssetType } from '../../../../types'; + +export const AIRPLANE: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'airplane', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +export const MARKER: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'marker', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +export const assets = [AIRPLANE, MARKER]; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/provider.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/provider.tsx deleted file mode 100644 index 1cd7562b59c47..0000000000000 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/provider.tsx +++ /dev/null @@ -1,110 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable no-console */ - -/* - This Provider is temporary. See https://github.com/elastic/kibana/pull/69357 -*/ - -import React, { FC } from 'react'; -import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; -import thunkMiddleware from 'redux-thunk'; -import { Provider as ReduxProvider } from 'react-redux'; - -// @ts-expect-error untyped local -import { appReady } from '../../../../public/state/middleware/app_ready'; -// @ts-expect-error untyped local -import { resolvedArgs } from '../../../../public/state/middleware/resolved_args'; - -// @ts-expect-error untyped local -import { getRootReducer } from '../../../../public/state/reducers'; - -// @ts-expect-error Untyped local -import { getDefaultWorkpad } from '../../../../public/state/defaults'; -import { State, AssetType } from '../../../../types'; - -export const AIRPLANE: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'airplane', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', -}; - -export const MARKER: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'marker', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', -}; - -export const state: State = { - app: { - basePath: '/', - ready: true, - serverFunctions: [], - }, - assets: { - AIRPLANE, - MARKER, - }, - transient: { - canUserWrite: true, - zoomScale: 1, - elementStats: { - total: 0, - ready: 0, - pending: 0, - error: 0, - }, - inFlight: false, - fullScreen: false, - selectedTopLevelNodes: [], - resolvedArgs: {}, - refresh: { - interval: 0, - }, - autoplay: { - enabled: false, - interval: 10000, - }, - }, - persistent: { - schemaVersion: 2, - workpad: getDefaultWorkpad(), - }, -}; - -// @ts-expect-error untyped local -import { elementsRegistry } from '../../../lib/elements_registry'; -import { image } from '../../../../canvas_plugin_src/elements/image'; -elementsRegistry.register(image); - -export const patchDispatch: (store: Store, dispatch: Dispatch) => Dispatch = (store, dispatch) => ( - action -) => { - const previousState = store.getState(); - const returnValue = dispatch(action); - const newState = store.getState(); - - console.group(action.type || '(thunk)'); - console.log('Previous State', previousState); - console.log('New State', newState); - console.groupEnd(); - - return returnValue; -}; - -export const Provider: FC = ({ children }) => { - const middleware = applyMiddleware(thunkMiddleware); - const reducer = getRootReducer(state); - const store = createStore(reducer, state, middleware); - store.dispatch = patchDispatch(store, store.dispatch); - - return {children}; -}; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx index a04d37cf7f9fc..ed000741bc542 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset.component.tsx @@ -17,7 +17,7 @@ import { EuiToolTip, } from '@elastic/eui'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useNotifyService } from '../../services'; import { ConfirmModal } from '../confirm_modal'; import { Clipboard } from '../clipboard'; @@ -38,11 +38,10 @@ interface Props { } export const Asset: FC = ({ asset, onCreate, onDelete }) => { - const { services } = useKibana(); + const { success } = useNotifyService(); const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); - const onCopy = (result: boolean) => - result && services.canvas.notify.success(`Copied '${asset.id}' to clipboard`); + const onCopy = (result: boolean) => result && success(`Copied '${asset.id}' to clipboard`); const confirmModal = ( new CanvasPlugin(); +export const plugin = (_initializerContext: PluginInitializerContext) => new CanvasPlugin(); diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/context.tsx index 9bd86ef98f1e3..9f79e81369b6b 100644 --- a/x-pack/plugins/canvas/public/services/context.tsx +++ b/x-pack/plugins/canvas/public/services/context.tsx @@ -12,7 +12,7 @@ import React, { FC, ReactElement, } from 'react'; -import { CanvasServices, CanvasServiceProviders } from '.'; +import { CanvasServices, CanvasServiceProviders, services } from '.'; export interface WithServicesProps { services: CanvasServices; @@ -36,23 +36,22 @@ export const useNotifyService = () => useServices().notify; export const useNavLinkService = () => useServices().navLink; export const withServices = (type: ComponentType) => { - const EnhancedType: FC = (props) => { - const services = useServices(); - return createElement(type, { ...props, services }); - }; + const EnhancedType: FC = (props) => + createElement(type, { ...props, services: useServices() }); return EnhancedType; }; export const ServicesProvider: FC<{ - providers: CanvasServiceProviders; + providers?: Partial; children: ReactElement; -}> = ({ providers, children }) => { +}> = ({ providers = {}, children }) => { + const specifiedProviders: CanvasServiceProviders = { ...services, ...providers }; const value = { - embeddables: providers.embeddables.getService(), - expressions: providers.expressions.getService(), - notify: providers.notify.getService(), - platform: providers.platform.getService(), - navLink: providers.navLink.getService(), + embeddables: specifiedProviders.embeddables.getService(), + expressions: specifiedProviders.expressions.getService(), + notify: specifiedProviders.notify.getService(), + platform: specifiedProviders.platform.getService(), + navLink: specifiedProviders.navLink.getService(), }; return {children}; }; diff --git a/x-pack/plugins/canvas/scripts/storybook.js b/x-pack/plugins/canvas/scripts/storybook.js index beea1814b54d2..671de53d74407 100644 --- a/x-pack/plugins/canvas/scripts/storybook.js +++ b/x-pack/plugins/canvas/scripts/storybook.js @@ -24,7 +24,7 @@ const storybookOptions = { run( ({ log, flags }) => { - const { dll, clean, stats, site } = flags; + const { addon, dll, clean, stats, site } = flags; // Delete the existing DLL if we're cleaning or building. if (clean || dll) { @@ -81,13 +81,20 @@ run( return; } + // Build the addon + execa.sync('node', ['scripts/build'], { + cwd: path.resolve(__dirname, '../storybook/addon'), + stdio: ['ignore', 'inherit', 'inherit'], + buffer: false, + }); + // Build site and exit if (site) { log.success('storybook: Generating Storybook site'); storybook({ ...storybookOptions, mode: 'static', - outputDir: path.resolve(__dirname, './../storybook'), + outputDir: path.resolve(__dirname, './../storybook/build'), }); return; } @@ -100,6 +107,14 @@ run( ...options, }); + if (addon) { + execa('node', ['scripts/build', '--watch'], { + cwd: path.resolve(__dirname, '../storybook/addon'), + stdio: ['ignore', 'inherit', 'inherit'], + buffer: false, + }); + } + storybook({ ...storybookOptions, port: 9001, @@ -110,8 +125,9 @@ run( Storybook runner for Canvas. `, flags: { - boolean: ['dll', 'clean', 'stats', 'site'], + boolean: ['addon', 'dll', 'clean', 'stats', 'site'], help: ` + --addon Watch the addon source code for changes. --clean Forces a clean of the Storybook DLL and exits. --dll Cleans and builds the Storybook dependency DLL and exits. --stats Produces a Webpack stats file. diff --git a/x-pack/plugins/canvas/storybook/addon/babel.config.js b/x-pack/plugins/canvas/storybook/addon/babel.config.js new file mode 100644 index 0000000000000..5081cf455906f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/babel.config.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + presets: ['@kbn/babel-preset/webpack_preset'], + plugins: ['@babel/plugin-proposal-class-properties'], +}; diff --git a/x-pack/plugins/canvas/storybook/addon/scripts/build.js b/x-pack/plugins/canvas/storybook/addon/scripts/build.js new file mode 100644 index 0000000000000..b3525244fad25 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/scripts/build.js @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const { resolve } = require('path'); + +const del = require('del'); +const supportsColor = require('supports-color'); +const { run, withProcRunner } = require('@kbn/dev-utils'); + +const ROOT_DIR = resolve(__dirname, '..'); +const BUILD_DIR = resolve(ROOT_DIR, 'target'); + +const padRight = (width, str) => + str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; + +run( + async ({ log, flags }) => { + await withProcRunner(log, async (proc) => { + if (!flags.watch) { + log.info('Deleting old output'); + await del(BUILD_DIR); + } + + const cwd = ROOT_DIR; + const env = { process }; + + if (supportsColor.stdout) { + env.FORCE_COLOR = 'true'; + } + + log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); + await proc.run(padRight(10, `babel`), { + cmd: 'babel', + args: [ + 'src', + '--config-file', + require.resolve('../babel.config.js'), + '--out-dir', + BUILD_DIR, + '--extensions', + '.ts,.js,.tsx', + '--copy-files', + ...(flags.watch ? ['--watch'] : ['--quiet']), + ], + wait: true, + env, + cwd, + }); + + log.success('Complete'); + }); + }, + { + description: 'Simple build tool for Canvas Storybook addon', + flags: { + boolean: ['watch'], + help: ` + --watch Run in watch mode + `, + }, + } +); diff --git a/x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx b/x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx new file mode 100644 index 0000000000000..9c29a44a67318 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/components/action_list.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useState } from 'react'; +import { EuiSelectable, EuiSelectableOption } from '@elastic/eui'; +import addons from '@storybook/addons'; +import uuid from 'uuid/v4'; + +import { EVENTS } from '../constants'; +import { RecordedAction, RecordedPayload } from '../types'; + +export const ActionList: FC<{ + onSelect: (action: RecordedAction | null) => void; +}> = ({ onSelect }) => { + const [recordedActions, setRecordedActions] = useState>({}); + const [selectedAction, setSelectedAction] = useState(null); + + useEffect(() => { + onSelect(selectedAction); + }, [onSelect, selectedAction]); + + useEffect(() => { + const actionListener = (newAction: RecordedPayload) => { + const id = uuid(); + setRecordedActions({ ...recordedActions, [id]: { ...newAction, id } }); + }; + + const resetListener = () => { + setSelectedAction(null); + setRecordedActions({}); + }; + + const channel = addons.getChannel(); + channel.addListener(EVENTS.ACTION, actionListener); + channel.addListener(EVENTS.RESET, resetListener); + + return () => { + channel.removeListener(EVENTS.ACTION, actionListener); + channel.removeListener(EVENTS.RESET, resetListener); + }; + }); + + useEffect(() => { + const values = Object.values(recordedActions); + if (values.length > 0) { + setSelectedAction(values[values.length - 1]); + } + }, [recordedActions]); + + const options: EuiSelectableOption[] = Object.values(recordedActions).map((recordedAction) => ({ + id: recordedAction.id, + key: recordedAction.id, + label: recordedAction.action.type, + checked: recordedAction.id === selectedAction?.id ? 'on' : undefined, + })); + + const onChange: (selectedOptions: EuiSelectableOption[]) => void = (selectedOptions) => { + selectedOptions.forEach((option) => { + if (option && option.checked && option.id) { + const selected = recordedActions[option.id]; + + if (selected) { + setSelectedAction(selected); + } + } + }); + }; + + return ( + + {(list) => list} + + ); +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx b/x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx new file mode 100644 index 0000000000000..351b94edb351f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/components/action_tree.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { isObject, isDate } from 'lodash'; +import uuid from 'uuid/v4'; +import { EuiTreeView } from '@elastic/eui'; + +import { Node } from '@elastic/eui/src/components/tree_view/tree_view'; + +import { RecordedAction } from '../types'; + +const actionToTree = (recordedAction: RecordedAction) => { + const { action, newState, previousState } = recordedAction; + + return [ + { + label: 'Action', + id: uuid(), + children: jsonToTree(action), + }, + { + label: 'Previous State', + id: uuid(), + children: jsonToTree(previousState), + }, + { + label: 'Current State', + id: uuid(), + children: jsonToTree(newState), + }, + ]; +}; + +const jsonToTree: (obj: Record) => Node[] = (obj) => { + const keys = Object.keys(obj); + + const values = keys.map((label) => { + const value = obj[label]; + + if (!value) { + return null; + } + + const id = uuid(); + + if (isDate(value)) { + return { label: `${label}: ${(value as Date).toDateString()}` }; + } + + if (isObject(value)) { + const children = jsonToTree(value); + + if (children !== null && Object.keys(children).length > 0) { + return { label, id, children }; + } else { + return { label, id }; + } + } + + return { label: `${label}: ${value.toString().slice(0, 100)}`, id }; + }); + + return values.filter((value) => value !== null) as Node[]; +}; + +export const ActionTree: FC<{ action: RecordedAction | null }> = ({ action }) => { + const items = action ? actionToTree(action) : null; + let tree = <>; + + if (action && items) { + tree = ( + + ); + } else if (action) { + tree =
No change
; + } + + return tree; +}; diff --git a/x-pack/plugins/canvas/storybook/addons.js b/x-pack/plugins/canvas/storybook/addon/src/components/index.ts similarity index 66% rename from x-pack/plugins/canvas/storybook/addons.js rename to x-pack/plugins/canvas/storybook/addon/src/components/index.ts index 75bbe620c9e7b..5acb1acf3b459 100644 --- a/x-pack/plugins/canvas/storybook/addons.js +++ b/x-pack/plugins/canvas/storybook/addon/src/components/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import '@storybook/addon-actions/register'; -import '@storybook/addon-knobs/register'; -import '@storybook/addon-console'; +export { ActionList } from './action_list'; +export { ActionTree } from './action_tree'; diff --git a/x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx b/x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx new file mode 100644 index 0000000000000..4db3c23c93843 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/components/state_change.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiAccordion } from '@elastic/eui'; +import { formatters } from 'jsondiffpatch'; + +import { RecordedAction } from '../types'; + +interface Props { + action: RecordedAction | null; +} + +export const StateChange: FC = ({ action }) => { + if (!action) { + return null; + } + + const { change, previousState } = action; + const html = formatters.html.format(change, previousState); + formatters.html.hideUnchanged(); + + return ( + + {/* eslint-disable-next-line react/no-danger */} +
+ + ); +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/constants.ts b/x-pack/plugins/canvas/storybook/addon/src/constants.ts new file mode 100644 index 0000000000000..fb2646ef3ba8f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ADDON_ID = 'kbn-canvas/redux-actions'; +export const ACTIONS_PANEL_ID = `${ADDON_ID}/panel`; + +const RESULT = `${ADDON_ID}/result`; +const REQUEST = `${ADDON_ID}/request`; +const ACTION = `${ADDON_ID}/action`; +const RESET = `${ADDON_ID}/reset`; + +export const EVENTS = { ACTION, RESULT, REQUEST, RESET }; diff --git a/x-pack/plugins/canvas/storybook/addon/src/panel.css b/x-pack/plugins/canvas/storybook/addon/src/panel.css new file mode 100644 index 0000000000000..b2b6591343b5f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/panel.css @@ -0,0 +1,171 @@ +.panel__tree { + font-family: monospace; + font-size: 85%; +} + +.panel__tree .euiTreeView { + padding-left: 12px; + font-size: 85%; +} + +.panel__resizeableContainer { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; +} + +.panel__stateChange .euiAccordion__button { + font-size: 12px; + font-family: monospace; +} + +.panel__stateChange .euiAccordion__iconWrapper { + transform: scale(.80); + transform-origin: top left; + margin: 8px 0px 4px 7px; +} + +.jsondiffpatch-delta { + font-family: monospace; + font-size: 12px; + line-height: 20px; + margin: 0; + padding: 0 0 0 12px; + display: inline-block; +} +.jsondiffpatch-delta pre { + font-size: 12px; + margin: 0; + padding: 0; + display: inline-block; +} +ul.jsondiffpatch-delta { + list-style-type: none; + padding: 0 0 0 20px; + margin: 0; +} +.jsondiffpatch-delta ul { + list-style-type: none; + padding: 0 0 0 20px; + margin: 0; +} + +.jsondiffpatch-added .jsondiffpatch-property-name, +.jsondiffpatch-added .jsondiffpatch-value pre, +.jsondiffpatch-modified .jsondiffpatch-right-value pre, +.jsondiffpatch-textdiff-added { + background: #bbffbb; +} + +.jsondiffpatch-deleted .jsondiffpatch-property-name, +.jsondiffpatch-deleted pre, +.jsondiffpatch-modified .jsondiffpatch-left-value pre, +.jsondiffpatch-textdiff-deleted { + background: #ffbbbb; + text-decoration: line-through; +} + +.jsondiffpatch-unchanged { display: none; } + +.jsondiffpatch-value { + display: inline-block; +} + +.jsondiffpatch-property-name { + display: inline-block; + padding-right: 5px; + vertical-align: top; +} + +.jsondiffpatch-property-name:after { + content: ': '; +} + +.jsondiffpatch-child-node-type-array > .jsondiffpatch-property-name:after { + content: ': ['; +} + +.jsondiffpatch-child-node-type-array:after { + content: '],'; +} + +div.jsondiffpatch-child-node-type-array:before { + content: '['; +} + +div.jsondiffpatch-child-node-type-array:after { + content: ']'; +} + +.jsondiffpatch-child-node-type-object > .jsondiffpatch-property-name:after { + content: ': {'; +} + +.jsondiffpatch-child-node-type-object:after { + content: '},'; +} + +div.jsondiffpatch-child-node-type-object:before { + content: '{'; +} + +div.jsondiffpatch-child-node-type-object:after { + content: '}'; +} + +.jsondiffpatch-value pre:after { + content: ','; +} + +li:last-child > .jsondiffpatch-value pre:after, +.jsondiffpatch-modified > .jsondiffpatch-left-value pre:after { + content: ''; +} + +.jsondiffpatch-modified .jsondiffpatch-value { + display: inline-block; +} + +.jsondiffpatch-modified .jsondiffpatch-right-value { + margin-left: 5px; +} + +.jsondiffpatch-moved .jsondiffpatch-value { + display: none; +} + +.jsondiffpatch-moved .jsondiffpatch-moved-destination { + display: inline-block; + background: #ffffbb; + color: #888; +} + +.jsondiffpatch-moved .jsondiffpatch-moved-destination:before { + content: ' => '; +} + +ul.jsondiffpatch-textdiff { + padding: 0; +} + +.jsondiffpatch-textdiff-location { + color: #bbb; + display: inline-block; + min-width: 60px; +} + +.jsondiffpatch-textdiff-line { + display: inline-block; +} + +.jsondiffpatch-textdiff-line-number:after { + content: ','; +} + +.jsondiffpatch-error { + background: red; + color: white; + font-weight: bold; +} diff --git a/x-pack/plugins/canvas/storybook/addon/src/panel.tsx b/x-pack/plugins/canvas/storybook/addon/src/panel.tsx new file mode 100644 index 0000000000000..adf6e8555c00a --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/panel.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiResizableContainer } from '@elastic/eui'; +import { StateChange } from './components/state_change'; + +import '@elastic/eui/dist/eui_theme_light.css'; +import './panel.css'; + +import { RecordedAction } from './types'; +import { ActionList, ActionTree } from './components'; + +export const Panel = () => { + const [selectedAction, setSelectedAction] = useState(null); + + return ( + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/register.tsx b/x-pack/plugins/canvas/storybook/addon/src/register.tsx new file mode 100644 index 0000000000000..3a5c4a6818ac1 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/register.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable import/no-extraneous-dependencies */ + +import React from 'react'; +import { addons, types } from '@storybook/addons'; +import { AddonPanel } from '@storybook/components'; +import { STORY_CHANGED } from '@storybook/core-events'; + +import { ADDON_ID, EVENTS, ACTIONS_PANEL_ID } from './constants'; +import { Panel } from './panel'; + +addons.register(ADDON_ID, (api) => { + const channel = addons.getChannel(); + + api.on(STORY_CHANGED, (storyId) => { + channel.emit(EVENTS.RESET, storyId); + }); + + addons.add(ACTIONS_PANEL_ID, { + title: 'Redux Actions', + type: types.PANEL, + render: ({ active, key }) => { + return ( + + + + ); + }, + }); +}); diff --git a/x-pack/plugins/canvas/storybook/addon/src/state.ts b/x-pack/plugins/canvas/storybook/addon/src/state.ts new file mode 100644 index 0000000000000..6d601fff7184a --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/state.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* es-lint-disable import/no-extraneous-dependencies */ +import { applyMiddleware, Dispatch, Store } from 'redux'; +import thunkMiddleware from 'redux-thunk'; +import addons from '@storybook/addons'; +import { diff } from 'jsondiffpatch'; +import { isFunction } from 'lodash'; + +import { EVENTS } from './constants'; + +// @ts-expect-error untyped local +import { appReady } from '../../../public/state/middleware/app_ready'; +// @ts-expect-error untyped local +import { resolvedArgs } from '../../../public/state/middleware/resolved_args'; + +// @ts-expect-error untyped local +import { getRootReducer } from '../../../public/state/reducers'; + +// @ts-expect-error Untyped local +import { getDefaultWorkpad } from '../../../public/state/defaults'; +// @ts-expect-error Untyped local +import { getInitialState as getState } from '../../../public/state/initial_state'; +import { State } from '../../../types'; + +export const getInitialState: () => State = () => getState(); +export const getMiddleware = () => applyMiddleware(thunkMiddleware); +export const getReducer = () => getRootReducer(getInitialState()); + +export const patchDispatch: (store: Store, dispatch: Dispatch) => Dispatch = (store, dispatch) => ( + action +) => { + const channel = addons.getChannel(); + + const previousState = store.getState(); + const returnValue = dispatch(action); + const newState = store.getState(); + const change = diff(previousState, newState) || {}; + + channel.emit(EVENTS.ACTION, { + previousState, + newState, + change, + action: isFunction(action) ? { type: '(thunk)' } : action, + }); + + return returnValue; +}; diff --git a/x-pack/plugins/canvas/storybook/addon/src/types.ts b/x-pack/plugins/canvas/storybook/addon/src/types.ts new file mode 100644 index 0000000000000..e8a2cb70c89ff --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/src/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux'; +import { State } from '../../../types'; + +export interface RecordedPayload { + previousState: State; + newState: State; + change: Partial; + action: Action; +} + +export interface RecordedAction extends RecordedPayload { + id: string; +} diff --git a/x-pack/plugins/canvas/storybook/addon/tsconfig.json b/x-pack/plugins/canvas/storybook/addon/tsconfig.json new file mode 100644 index 0000000000000..9cab0af235f2e --- /dev/null +++ b/x-pack/plugins/canvas/storybook/addon/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.json", + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ], + "exclude": [ + "target" + ], + "compilerOptions": { + "declaration": false, + } +} diff --git a/x-pack/plugins/canvas/storybook/config.js b/x-pack/plugins/canvas/storybook/config.js deleted file mode 100644 index dc16d6c46084d..0000000000000 --- a/x-pack/plugins/canvas/storybook/config.js +++ /dev/null @@ -1,73 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { configure, addDecorator, addParameters } from '@storybook/react'; -import { withInfo } from '@storybook/addon-info'; -import { create } from '@storybook/theming'; - -import { startServices } from '../public/services/stubs'; -import { addDecorators } from './decorators'; - -// If we're running Storyshots, be sure to register the require context hook. -// Otherwise, add the other decorators. -if (process.env.NODE_ENV === 'test') { - require('babel-plugin-require-context-hook/register')(); -} else { - // Customize the info for each story. - addDecorator( - withInfo({ - inline: true, - styles: { - infoBody: { - margin: 20, - }, - infoStory: { - margin: '40px 60px', - }, - }, - }) - ); -} - -addDecorators(); -startServices(); - -function loadStories() { - require('./dll_contexts'); - - // Only gather and require CSS files related to Canvas. The other CSS files - // are built into the DLL. - const css = require.context( - '../../../../built_assets/css', - true, - /plugins\/(?=canvas).*light\.css/ - ); - css.keys().forEach((filename) => css(filename)); - - // Find all files ending in *.stories.tsx - const req = require.context('./..', true, /.(stories).tsx$/); - req.keys().forEach((filename) => req(filename)); - - // Import Canvas CSS - require('../public/style/index.scss'); -} - -// Set up the Storybook environment with custom settings. -addParameters({ - options: { - theme: create({ - base: 'light', - brandTitle: 'Canvas Storybook', - brandUrl: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/canvas', - }), - showPanel: true, - isFullscreen: false, - panelPosition: 'bottom', - isToolshown: true, - }, -}); - -configure(loadStories, module); diff --git a/x-pack/plugins/canvas/storybook/decorators/index.ts b/x-pack/plugins/canvas/storybook/decorators/index.ts index aa1e958a410f5..8cd716cf7e3f1 100644 --- a/x-pack/plugins/canvas/storybook/decorators/index.ts +++ b/x-pack/plugins/canvas/storybook/decorators/index.ts @@ -5,15 +5,43 @@ */ import { addDecorator } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs'; // @ts-expect-error import { withInfo } from '@storybook/addon-info'; +import { Provider as ReduxProvider } from 'react-redux'; + +import { ServicesProvider } from '../../public/services'; +import { RouterContext } from '../../public/components/router'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { routerContextDecorator } from './router_decorator'; import { kibanaContextDecorator } from './kibana_decorator'; +import { servicesContextDecorator } from './services_decorator'; + +export { reduxDecorator } from './redux_decorator'; export const addDecorators = () => { - addDecorator(withKnobs); + if (process.env.NODE_ENV === 'test') { + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('babel-plugin-require-context-hook/register')(); + } else { + // Customize the info for each story. + addDecorator( + withInfo({ + inline: true, + styles: { + infoBody: { + margin: 20, + }, + infoStory: { + margin: '40px 60px', + }, + }, + propTablesExclude: [ReduxProvider, ServicesProvider, RouterContext, KibanaContextProvider], + }) + ); + } + addDecorator(kibanaContextDecorator); addDecorator(routerContextDecorator); + addDecorator(servicesContextDecorator); }; diff --git a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx new file mode 100644 index 0000000000000..e35b065a61764 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +/* es-lint-disable import/no-extraneous-dependencies */ + +import React from 'react'; +import { createStore } from 'redux'; +import { Provider as ReduxProvider } from 'react-redux'; +import { cloneDeep } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; + +// @ts-expect-error Untyped local +import { getDefaultWorkpad } from '../../public/state/defaults'; +import { CanvasWorkpad, CanvasElement, CanvasAsset } from '../../types'; + +// @ts-expect-error untyped local +import { elementsRegistry } from '../../public/lib/elements_registry'; +import { image } from '../../canvas_plugin_src/elements/image'; +elementsRegistry.register(image); + +import { getInitialState, getReducer, getMiddleware, patchDispatch } from '../addon/src/state'; +export { ADDON_ID, ACTIONS_PANEL_ID } from '../addon/src/constants'; + +interface Params { + workpad?: CanvasWorkpad; + elements?: CanvasElement[]; + assets?: CanvasAsset[]; +} + +export const reduxDecorator = (params: Params = {}) => { + const state = cloneDeep(getInitialState()); + const { workpad, elements, assets } = params; + + if (workpad) { + set(state, 'persistent.workpad', workpad); + } + + if (elements) { + set(state, 'persistent.workpad.pages.0.elements', elements); + } + + if (assets) { + set( + state, + 'assets', + assets.reduce((obj: Record, item) => { + obj[item.id] = item; + return obj; + }, {}) + ); + } + + return (story: Function) => { + const store = createStore(getReducer(), state, getMiddleware()); + store.dispatch = patchDispatch(store, store.dispatch); + return {story()}; + }; +}; diff --git a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx index db775b697d248..464577b1f7c1e 100644 --- a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx +++ b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx @@ -5,32 +5,9 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; -import { RouterContext } from '../../public/components/router'; - -const context = { - router: { - getFullPath: () => 'path', - create: () => '', - }, - navigateTo: () => {}, -}; - -class RouterProvider extends React.Component { - static childContextTypes = { - router: PropTypes.object.isRequired, - navigateTo: PropTypes.func, - }; - getChildContext() { - return context; - } - - render() { - return {this.props.children}; - } -} +import { RouterContext } from '../../public/components/router'; -export function routerContextDecorator(story: Function) { - return {story()}; -} +export const routerContextDecorator = (story: Function) => ( + {} }}>{story()} +); diff --git a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx new file mode 100644 index 0000000000000..918eaffb47d77 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { ServicesProvider } from '../../public/services'; + +export const servicesContextDecorator = (story: Function) => ( + {story()} +); diff --git a/x-pack/plugins/canvas/storybook/index.ts b/x-pack/plugins/canvas/storybook/index.ts new file mode 100644 index 0000000000000..5cad89eb614e5 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ACTIONS_PANEL_ID } from './addon/src/constants'; + +export * from './decorators'; +export { ACTIONS_PANEL_ID } from './addon/src/constants'; +export const getAddonPanelParameters = () => ({ options: { selectedPanel: ACTIONS_PANEL_ID } }); diff --git a/x-pack/plugins/canvas/storybook/main.ts b/x-pack/plugins/canvas/storybook/main.ts new file mode 100644 index 0000000000000..ad6d10f9bc75f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/main.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +module.exports = { + stories: ['../**/*.stories.tsx'], + addons: [ + '@storybook/addon-actions', + '@storybook/addon-knobs', + './storybook/addon/target/register', + ], +}; diff --git a/x-pack/plugins/canvas/storybook/manager.ts b/x-pack/plugins/canvas/storybook/manager.ts new file mode 100644 index 0000000000000..6727040c9b27f --- /dev/null +++ b/x-pack/plugins/canvas/storybook/manager.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { addons } from '@storybook/addons'; +import { create } from '@storybook/theming'; +import { PANEL_ID } from '@storybook/addon-actions'; + +addons.setConfig({ + theme: create({ + base: 'light', + brandTitle: 'Canvas Storybook', + brandUrl: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/canvas', + }), + showPanel: true, + isFullscreen: false, + panelPosition: 'bottom', + isToolshown: true, + selectedPanel: PANEL_ID, +}); diff --git a/x-pack/plugins/canvas/storybook/middleware.js b/x-pack/plugins/canvas/storybook/middleware.ts similarity index 74% rename from x-pack/plugins/canvas/storybook/middleware.js rename to x-pack/plugins/canvas/storybook/middleware.ts index baa524aefa709..d319a6918a02a 100644 --- a/x-pack/plugins/canvas/storybook/middleware.js +++ b/x-pack/plugins/canvas/storybook/middleware.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -const path = require('path'); -const serve = require('serve-static'); +import path from 'path'; +// @ts-expect-error +import serve from 'serve-static'; // Extend the Storybook Middleware to include a route to access Legacy UI assets -module.exports = function (router) { +module.exports = function (router: { get: (...args: any[]) => void }) { router.get( '/ui', serve(path.resolve(__dirname, '../../../../../src/core/server/core_app/assets')) diff --git a/x-pack/plugins/canvas/storybook/preview.ts b/x-pack/plugins/canvas/storybook/preview.ts new file mode 100644 index 0000000000000..fc194664c84b8 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/preview.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { action } from '@storybook/addon-actions'; + +import { startServices } from '../public/services/stubs'; +import { addDecorators } from './decorators'; + +// Import the modules from the DLL. +import './dll_contexts'; + +// Import Canvas CSS +import '../public/style/index.scss'; + +startServices({ + notify: { + success: (message) => action(`success: ${message}`)(), + error: (message) => action(`error: ${message}`)(), + info: (message) => action(`info: ${message}`)(), + warning: (message) => action(`warning: ${message}`)(), + }, +}); + +addDecorators(); + +// Only gather and require CSS files related to Canvas. The other CSS files +// are built into the DLL. +const css = require.context( + '../../../../built_assets/css', + true, + /plugins\/(?=canvas).*light\.css/ +); +css.keys().forEach((filename) => css(filename)); diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.js b/x-pack/plugins/canvas/storybook/storyshots.test.tsx similarity index 85% rename from x-pack/plugins/canvas/storybook/storyshots.test.js rename to x-pack/plugins/canvas/storybook/storyshots.test.tsx index 6a275f3edfd2a..b51a85edaa67b 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.js +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -4,18 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ReactChildren } from 'react'; import path from 'path'; import moment from 'moment'; import 'moment-timezone'; import ReactDOM from 'react-dom'; import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'; +// @ts-expect-error untyped library import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer'; import { addSerializer } from 'jest-specific-snapshot'; // Several of the renderers, used by the runtime, use jQuery. import jquery from 'jquery'; +// @ts-expect-error jQuery global global.$ = jquery; +// @ts-expect-error jQuery global global.jQuery = jquery; // Set our default timezone to UTC for tests so we can generate predictable snapshots @@ -23,7 +27,7 @@ moment.tz.setDefault('UTC'); // Freeze time for the tests for predictable snapshots const testTime = new Date(Date.UTC(2019, 5, 1)); // June 1 2019 -Date.now = jest.fn(() => testTime); +Date.now = jest.fn(() => testTime.getTime()); // Mock telemetry service jest.mock('../public/lib/ui_metric', () => ({ trackCanvasUiMetric: () => {} })); @@ -53,10 +57,10 @@ jest.mock('@elastic/eui/packages/react-datepicker', () => { }); // Mock React Portal for components that use modals, tooltips, etc -ReactDOM.createPortal = jest.fn((element) => { - return element; -}); +// @ts-expect-error Portal mocks are notoriously difficult to type +ReactDOM.createPortal = jest.fn((element) => element); +// Mock the EUI HTML ID Generator so elements have a predictable ID in snapshots jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { htmlIdGenerator: () => () => `generated-id`, @@ -67,7 +71,7 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { // https://github.com/elastic/eui/issues/3712 jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => { return { - EuiOverlayMask: ({ children }) => children, + EuiOverlayMask: ({ children }: { children: ReactChildren }) => children, }; }); @@ -79,6 +83,7 @@ jest.mock( } ); +// @ts-expect-error untyped library import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'; jest.mock('@elastic/eui/test-env/components/observer/observer'); EuiObserver.mockImplementation(() => 'EuiObserver'); @@ -86,6 +91,7 @@ EuiObserver.mockImplementation(() => 'EuiObserver'); // This element uses a `ref` and cannot be rendered by Jest snapshots. import { RenderedElement } from '../shareable_runtime/components/rendered_element'; jest.mock('../shareable_runtime/components/rendered_element'); +// @ts-expect-error RenderedElement.mockImplementation(() => 'RenderedElement'); addSerializer(styleSheetSerializer); @@ -94,5 +100,6 @@ addSerializer(styleSheetSerializer); initStoryshots({ configPath: path.resolve(__dirname, './../storybook'), test: multiSnapshotWithOptions({}), + // Don't snapshot tests that start with 'redux' storyNameRegex: /^((?!.*?redux).)*$/, }); diff --git a/x-pack/plugins/canvas/storybook/webpack.config.js b/x-pack/plugins/canvas/storybook/webpack.config.js index 982185a731b14..1321ade30bbde 100644 --- a/x-pack/plugins/canvas/storybook/webpack.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.config.js @@ -6,236 +6,198 @@ const path = require('path'); const webpack = require('webpack'); +const webpackMerge = require('webpack-merge'); const { stringifyRequest } = require('loader-utils'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { DLL_OUTPUT, KIBANA_ROOT } = require('./constants'); // Extend the Storybook Webpack config with some customizations -module.exports = async ({ config }) => { - // Find and alter the CSS rule to replace the Kibana public path string with a path - // to the route we've added in middleware.js - const cssRule = config.module.rules.find((rule) => rule.test.source.includes('.css$')); - cssRule.use.push({ - loader: 'string-replace-loader', - options: { - search: '__REPLACE_WITH_PUBLIC_PATH__', - replace: '/', - flags: 'g', - }, - }); - - // Include the React preset from Kibana for Storybook JS files. - config.module.rules.push({ - test: /\.js$/, - exclude: /node_modules/, - loaders: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }); - - // Handle Typescript files - config.module.rules.push({ - test: /\.tsx?$/, - use: [ - { - loader: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }, - ], - }); - - config.module.rules.push({ - test: /\.mjs$/, - include: /node_modules/, - type: 'javascript/auto', - }); - - // Parse props data for .tsx files - // This is notoriously slow, and is making Storybook unusable. Disabling for now. - // See: https://github.com/storybookjs/storybook/issues/7998 - // - // config.module.rules.push({ - // test: /\.tsx$/, - // // Exclude example files, as we don't display props info for them - // exclude: /\.examples.tsx$/, - // use: [ - // // Parse TS comments to create Props tables in the UI - // require.resolve('react-docgen-typescript-loader'), - // ], - // }); - - // Enable SASS, but exclude CSS Modules in Storybook - config.module.rules.push({ - test: /\.scss$/, - exclude: /\.module.(s(a|c)ss)$/, - use: [ - { loader: 'style-loader' }, - { loader: 'css-loader', options: { importLoaders: 2 } }, - { - loader: 'postcss-loader', - options: { - config: { - path: require.resolve('@kbn/optimizer/postcss.config.js'), - }, - }, - }, - { - loader: 'sass-loader', - options: { - prependData(loaderContext) { - return `@import ${stringifyRequest( - loaderContext, - path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') - )};\n`; - }, - sassOptions: { - includePaths: [path.resolve(KIBANA_ROOT, 'node_modules')], +module.exports = async ({ config: storybookConfig }) => { + const config = { + module: { + rules: [ + // Include the React preset from Kibana for JS(X) and TS(X) + { + test: /\.(j|t)sx?$/, + exclude: /node_modules/, + loaders: 'babel-loader', + options: { + presets: [require.resolve('@kbn/babel-preset/webpack_preset')], }, }, - }, - ], - }); - - // Enable CSS Modules in Storybook - config.module.rules.push({ - test: /\.module\.s(a|c)ss$/, - loader: [ - 'style-loader', - { - loader: 'css-loader', - options: { - importLoaders: 2, - modules: { - localIdentName: '[name]__[local]___[hash:base64:5]', - }, + // Parse props data for .tsx files + // This is notoriously slow, and is making Storybook unusable. Disabling for now. + // See: https://github.com/storybookjs/storybook/issues/7998 + // + // { + // test: /\.tsx$/, + // // Exclude example files, as we don't display props info for them + // exclude: /\.examples.tsx$/, + // use: [ + // // Parse TS comments to create Props tables in the UI + // require.resolve('react-docgen-typescript-loader'), + // ], + // }, + // Enable SASS, but exclude CSS Modules in Storybook + { + test: /\.scss$/, + exclude: /\.module.(s(a|c)ss)$/, + use: [ + { loader: 'style-loader' }, + { loader: 'css-loader', options: { importLoaders: 2 } }, + { + loader: 'postcss-loader', + options: { + path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, + }, + { + loader: 'sass-loader', + options: { + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss') + )};\n`; + }, + sassOptions: { + includePaths: [path.resolve(KIBANA_ROOT, 'node_modules')], + }, + }, + }, + ], }, - }, - { - loader: 'postcss-loader', - options: { - config: { - path: require.resolve('@kbn/optimizer/postcss.config.js'), - }, + // Enable CSS Modules in Storybook (Shareable Runtime) + { + test: /\.module\.s(a|c)ss$/, + loader: [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 2, + modules: { + localIdentName: '[name]__[local]___[hash:base64:5]', + }, + }, + }, + { + loader: 'postcss-loader', + options: { + path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, + }, + { + loader: 'sass-loader', + }, + ], }, - }, - { - loader: 'sass-loader', - }, - ], - }); - - // Exclude large-dependency modules that need not be included in Storybook. - config.module.rules.push({ - test: [ - path.resolve(__dirname, '../public/components/embeddable_flyout'), - path.resolve(__dirname, '../../reporting/public'), - ], - use: 'null-loader', - }); - - // Ensure jQuery is global for Storybook, specifically for the runtime. - config.plugins.push( - new webpack.ProvidePlugin({ - $: 'jquery', - jQuery: 'jquery', - }) - ); - - // Reference the built DLL file of static(ish) dependencies, which are removed - // during kbn:bootstrap and rebuilt if missing. - config.plugins.push( - new webpack.DllReferencePlugin({ - manifest: path.resolve(DLL_OUTPUT, 'manifest.json'), - context: KIBANA_ROOT, - }) - ); - - // Copy the DLL files to the Webpack build for use in the Storybook UI - config.plugins.push( - new CopyWebpackPlugin({ - patterns: [ { - from: path.resolve(DLL_OUTPUT, 'dll.js'), - to: 'dll.js', + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto', }, + // Exclude large-dependency, troublesome or irrelevant modules. { - from: path.resolve(DLL_OUTPUT, 'dll.css'), - to: 'dll.css', + test: [ + path.resolve(__dirname, '../public/components/embeddable_flyout'), + path.resolve(__dirname, '../../reporting/public'), + path.resolve(__dirname, '../../../../src/plugins/kibana_legacy/public/angular'), + path.resolve(__dirname, '../../../../src/plugins/kibana_legacy/public/paginate'), + ], + use: 'null-loader', }, ], - }) - ); - - config.plugins.push( - // replace imports for `uiExports/*` modules with a synthetic module - // created by create_ui_exports_module.js - new webpack.NormalModuleReplacementPlugin(/^uiExports\//, (resource) => { - // uiExports used by Canvas - const extensions = { - hacks: [], - chromeNavControls: [], - }; - - // everything following the first / in the request is - // treated as a type of appExtension - const type = resource.request.slice(resource.request.indexOf('/') + 1); - - resource.request = [ - // the "val-loader" is used to execute create_ui_exports_module - // and use its return value as the source for the module in the - // bundle. This allows us to bypass writing to the file system - require.resolve('val-loader'), - '!', - require.resolve(KIBANA_ROOT + '/src/optimize/create_ui_exports_module'), - '?', - // this JSON is parsed by create_ui_exports_module and determines - // what require() calls it will execute within the bundle - JSON.stringify({ type, modules: extensions[type] || [] }), - ].join(''); - }), - - // Mock out libs used by a few componets to avoid loading in kibana_legacy and platform - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/notify/, - path.resolve(__dirname, '../tasks/mocks/uiNotify') - ), - new webpack.NormalModuleReplacementPlugin( - /lib\/download_workpad/, - path.resolve(__dirname, '../tasks/mocks/downloadWorkpad') - ), - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/custom_element_service/, - path.resolve(__dirname, '../tasks/mocks/customElementService') - ), - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/ui_metric/, - path.resolve(__dirname, '../tasks/mocks/uiMetric') - ) - ); - - // Tell Webpack about relevant extensions - config.resolve.extensions.push('.ts', '.tsx', '.scss'); - - // Alias imports to either a mock or the proper module or directory. - // NOTE: order is important here - `ui/notify` will override `ui/notify/foo` if it - // is added first. - config.resolve.alias['ui/notify/lib/format_msg'] = path.resolve( - __dirname, - '../tasks/mocks/uiNotifyFormatMsg' - ); - config.resolve.alias['ui/notify'] = path.resolve(__dirname, '../tasks/mocks/uiNotify'); - config.resolve.alias['ui/url/absolute_to_parsed_url'] = path.resolve( - __dirname, - '../tasks/mocks/uiAbsoluteToParsedUrl' - ); - config.resolve.alias['ui/chrome'] = path.resolve(__dirname, '../tasks/mocks/uiChrome'); - config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'); - config.resolve.alias.ng_mock$ = path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'); + }, + plugins: [ + // Reference the built DLL file of static(ish) dependencies, which are removed + // during kbn:bootstrap and rebuilt if missing. + new webpack.DllReferencePlugin({ + manifest: path.resolve(DLL_OUTPUT, 'manifest.json'), + context: KIBANA_ROOT, + }), + // Ensure jQuery is global for Storybook, specifically for the runtime. + new webpack.ProvidePlugin({ + $: 'jquery', + jQuery: 'jquery', + }), + // Copy the DLL files to the Webpack build for use in the Storybook UI + new CopyWebpackPlugin({ + patterns: [ + { + from: path.resolve(DLL_OUTPUT, 'dll.js'), + to: 'dll.js', + }, + { + from: path.resolve(DLL_OUTPUT, 'dll.css'), + to: 'dll.css', + }, + ], + }), + // replace imports for `uiExports/*` modules with a synthetic module + // created by create_ui_exports_module.js + new webpack.NormalModuleReplacementPlugin(/^uiExports\//, (resource) => { + // uiExports used by Canvas + const extensions = { + hacks: [], + chromeNavControls: [], + }; + + // everything following the first / in the request is + // treated as a type of appExtension + const type = resource.request.slice(resource.request.indexOf('/') + 1); + + resource.request = [ + // the "val-loader" is used to execute create_ui_exports_module + // and use its return value as the source for the module in the + // bundle. This allows us to bypass writing to the file system + require.resolve('val-loader'), + '!', + require.resolve(KIBANA_ROOT + '/src/optimize/create_ui_exports_module'), + '?', + // this JSON is parsed by create_ui_exports_module and determines + // what require() calls it will execute within the bundle + JSON.stringify({ type, modules: extensions[type] || [] }), + ].join(''); + }), + + new webpack.NormalModuleReplacementPlugin( + /lib\/download_workpad/, + path.resolve(__dirname, '../tasks/mocks/downloadWorkpad') + ), + new webpack.NormalModuleReplacementPlugin( + /(lib)?\/custom_element_service/, + path.resolve(__dirname, '../tasks/mocks/customElementService') + ), + new webpack.NormalModuleReplacementPlugin( + /(lib)?\/ui_metric/, + path.resolve(__dirname, '../tasks/mocks/uiMetric') + ), + ], + resolve: { + extensions: ['.ts', '.tsx', '.scss', '.mjs', '.html'], + alias: { + 'ui/url/absolute_to_parsed_url': path.resolve( + __dirname, + '../tasks/mocks/uiAbsoluteToParsedUrl' + ), + ui: path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'), + ng_mock$: path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'), + }, + }, + }; - config.resolve.extensions.push('.mjs'); + // Find and alter the CSS rule to replace the Kibana public path string with a path + // to the route we've added in middleware.js + const cssRule = storybookConfig.module.rules.find((rule) => rule.test.source.includes('.css$')); + cssRule.use.push({ + loader: 'string-replace-loader', + options: { + search: '__REPLACE_WITH_PUBLIC_PATH__', + replace: '/', + flags: 'g', + }, + }); - return config; + return webpackMerge(storybookConfig, config); }; diff --git a/x-pack/plugins/canvas/storybook/webpack.dll.config.js b/x-pack/plugins/canvas/storybook/webpack.dll.config.js index 81d19c035075f..4e54750f08eea 100644 --- a/x-pack/plugins/canvas/storybook/webpack.dll.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.dll.config.js @@ -25,9 +25,6 @@ module.exports = { '@elastic/eui/dist/eui_theme_light.css', '@kbn/ui-framework/dist/kui_light.css', '@storybook/addon-actions/register', - '@storybook/addon-knobs', - '@storybook/addon-knobs/react', - '@storybook/addon-knobs/register', '@storybook/core', '@storybook/core/dist/server/common/polyfills.js', '@storybook/react', @@ -38,6 +35,7 @@ module.exports = { 'chroma-js', 'highlight.js', 'html-entities', + 'jsondiffpatch', 'jquery', 'lodash', 'markdown-it', diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts index 434d38c76d428..4ddcb3386f314 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { DiscoverStart } from '../../../../../../src/plugins/discover/public'; -import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; import { KibanaLegacyStart } from '../../../../../../src/plugins/kibana_legacy/public'; @@ -18,7 +17,6 @@ export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; export interface PluginDeps { discover: Pick; - embeddable: Pick; kibanaLegacy?: { dashboardConfig: { getHideWriteControls: KibanaLegacyStart['dashboardConfig']['getHideWriteControls']; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts index 14cd48ae1f509..b6bdafc26b445 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -8,19 +8,14 @@ import { ExploreDataChartAction } from './explore_data_chart_action'; import { Params, PluginDeps } from './abstract_explore_data_action'; import { coreMock } from '../../../../../../src/core/public/mocks'; import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; -import { - EmbeddableStart, - RangeSelectContext, - ValueClickContext, - ChartActionContext, -} from '../../../../../../src/plugins/embeddable/public'; +import { ExploreDataChartActionContext } from './explore_data_chart_action'; import { i18n } from '@kbn/i18n'; import { VisualizeEmbeddableContract, VISUALIZE_EMBEDDABLE_TYPE, } from '../../../../../../src/plugins/visualizations/public'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; -import { Filter, TimeRange } from '../../../../../../src/plugins/data/public'; +import { Filter, RangeFilter } from '../../../../../../src/plugins/data/public'; const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance; @@ -34,10 +29,19 @@ afterEach(() => { i18nTranslateSpy.mockClear(); }); -const setup = ({ - useRangeEvent = false, - dashboardOnlyMode = false, -}: { useRangeEvent?: boolean; dashboardOnlyMode?: boolean } = {}) => { +const setup = ( + { + useRangeEvent = false, + timeFieldName, + filters = [], + dashboardOnlyMode = false, + }: { + useRangeEvent?: boolean; + filters?: Filter[]; + timeFieldName?: string; + dashboardOnlyMode?: boolean; + } = { filters: [] } +) => { type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; const core = coreMock.createStart(); @@ -46,17 +50,10 @@ const setup = ({ createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), } as unknown) as UrlGenerator; - const filtersAndTimeRangeFromContext = jest.fn((async () => ({ - filters: [], - })) as EmbeddableStart['filtersAndTimeRangeFromContext']); - const plugins: PluginDeps = { discover: { urlGenerator, }, - embeddable: { - filtersAndTimeRangeFromContext, - }, kibanaLegacy: { dashboardConfig: { getHideWriteControls: () => dashboardOnlyMode, @@ -91,19 +88,13 @@ const setup = ({ getOutput: () => output, } as unknown) as VisualizeEmbeddableContract; - const data: ChartActionContext['data'] = { - ...(useRangeEvent - ? ({ range: {} } as RangeSelectContext['data']) - : ({ data: [] } as ValueClickContext['data'])), - timeFieldName: 'order_date', - }; - const context = { + filters, + timeFieldName, embeddable, - data, - } as ChartActionContext; + } as ExploreDataChartActionContext; - return { core, plugins, urlGenerator, params, action, input, output, embeddable, data, context }; + return { core, plugins, urlGenerator, params, action, input, output, embeddable, context }; }; describe('"Explore underlying data" panel action', () => { @@ -236,32 +227,41 @@ describe('"Explore underlying data" panel action', () => { }); test('applies chart event filters', async () => { - const { action, context, urlGenerator, plugins } = setup(); - - ((plugins.embeddable - .filtersAndTimeRangeFromContext as unknown) as jest.SpyInstance).mockImplementation(() => { - const filters: Filter[] = [ - { - meta: { - alias: 'alias', - disabled: false, - negate: false, + const timeFieldName = 'timeField'; + const from = '2020-07-13T13:40:43.583Z'; + const to = '2020-07-13T13:44:43.583Z'; + const filters: Array = [ + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + field: timeFieldName, + params: { + gte: from, + lte: to, }, }, - ]; - const timeRange: TimeRange = { - from: 'from', - to: 'to', - }; - return { filters, timeRange }; - }); + range: { + [timeFieldName]: { + gte: from, + lte: to, + }, + }, + }, + ]; - expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(0); + const { action, context, urlGenerator } = setup({ filters, timeFieldName }); await action.getHref(context); - expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(1); - expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledWith(context); expect(urlGenerator.createUrl).toHaveBeenCalledWith({ filters: [ { @@ -274,8 +274,8 @@ describe('"Explore underlying data" panel action', () => { ], indexPatternId: 'index-ptr-foo', timeRange: { - from: 'from', - to: 'to', + from, + to, }, }); }); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts index 658a6bcb3cf4d..a89fe3cd12a19 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -5,17 +5,19 @@ */ import { Action } from '../../../../../../src/plugins/ui_actions/public'; -import { - ValueClickContext, - RangeSelectContext, -} from '../../../../../../src/plugins/embeddable/public'; import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; -import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public'; +import { + isTimeRange, + isQuery, + isFilters, + ApplyGlobalFilterActionContext, + esFilters, +} from '../../../../../../src/plugins/data/public'; import { KibanaURL } from './kibana_url'; import * as shared from './shared'; import { AbstractExploreDataAction } from './abstract_explore_data_action'; -export type ExploreDataChartActionContext = ValueClickContext | RangeSelectContext; +export type ExploreDataChartActionContext = ApplyGlobalFilterActionContext; export const ACTION_EXPLORE_DATA_CHART = 'ACTION_EXPLORE_DATA_CHART'; @@ -31,6 +33,11 @@ export class ExploreDataChartAction extends AbstractExploreDataAction { + if (context.embeddable?.type === 'map') return false; // TODO: https://github.com/elastic/kibana/issues/73043 + return super.isCompatible(context); + } + protected readonly getUrl = async ( context: ExploreDataChartActionContext ): Promise => { @@ -42,7 +49,11 @@ export class ExploreDataChartAction extends AbstractExploreDataAction Promise.resolve('/xyz/app/discover/foo#bar')), } as unknown) as UrlGenerator; - const filtersAndTimeRangeFromContext = jest.fn((async () => ({ - filters: [], - })) as EmbeddableStart['filtersAndTimeRangeFromContext']); - const plugins: PluginDeps = { discover: { urlGenerator, }, - embeddable: { - filtersAndTimeRangeFromContext, - }, kibanaLegacy: { dashboardConfig: { getHideWriteControls: () => dashboardOnlyMode, diff --git a/x-pack/plugins/discover_enhanced/public/plugin.ts b/x-pack/plugins/discover_enhanced/public/plugin.ts index 4b018354aa092..9e66925132a7d 100644 --- a/x-pack/plugins/discover_enhanced/public/plugin.ts +++ b/x-pack/plugins/discover_enhanced/public/plugin.ts @@ -9,8 +9,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { UiActionsSetup, UiActionsStart, - SELECT_RANGE_TRIGGER, - VALUE_CLICK_TRIGGER, + APPLY_FILTER_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public'; @@ -77,8 +76,7 @@ export class DiscoverEnhancedPlugin if (this.config.actions.exploreDataInChart.enabled) { const exploreDataChartAction = new ExploreDataChartAction(params); - uiActions.addTriggerAction(SELECT_RANGE_TRIGGER, exploreDataChartAction); - uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, exploreDataChartAction); + uiActions.addTriggerAction(APPLY_FILTER_TRIGGER, exploreDataChartAction); } } } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index 23889bdca2dd7..f5f5071bab158 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -158,7 +158,7 @@ export class VectorLayer extends AbstractLayer { async getBounds({ startLoading, stopLoading, registerCancelCallback, dataFilters }) { const isStaticLayer = !this.getSource().isBoundsAware(); - if (isStaticLayer) { + if (isStaticLayer || this.hasJoins()) { return getFeatureCollectionBounds(this._getSourceFeatureCollection(), this.hasJoins()); } diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js deleted file mode 100644 index 4692bb1db3477..0000000000000 --- a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js +++ /dev/null @@ -1,50 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { MapsTopNavMenu } from './top_nav_menu'; -import { - enableFullScreen, - openMapSettings, - removePreviewLayers, - setSelectedLayer, - updateFlyout, -} from '../../../actions'; -import { FLYOUT_STATE } from '../../../reducers/ui'; -import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; -import { getFlyoutDisplay } from '../../../selectors/ui_selectors'; -import { - getQuery, - getRefreshConfig, - getTimeFilters, - hasDirtyState, -} from '../../../selectors/map_selectors'; - -function mapStateToProps(state = {}) { - return { - isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, - inspectorAdapters: getInspectorAdapters(state), - isSaveDisabled: hasDirtyState(state), - query: getQuery(state), - refreshConfig: getRefreshConfig(state), - timeFilters: getTimeFilters(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - closeFlyout: () => { - dispatch(setSelectedLayer(null)); - dispatch(updateFlyout(FLYOUT_STATE.NONE)); - dispatch(removePreviewLayers()); - }, - enableFullScreen: () => dispatch(enableFullScreen()), - openMapSettings: () => dispatch(openMapSettings()), - }; -} - -const connectedMapsTopNavMenu = connect(mapStateToProps, mapDispatchToProps)(MapsTopNavMenu); -export { connectedMapsTopNavMenu as MapsTopNavMenu }; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js index d7c754c91b89a..c5f959c54fb66 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js @@ -13,6 +13,7 @@ import { getQueryableUniqueIndexPatternIds, getRefreshConfig, getTimeFilters, + hasDirtyState, hasUnsavedChanges, } from '../../../selectors/map_selectors'; import { @@ -26,13 +27,20 @@ import { setRefreshConfig, setSelectedLayer, updateFlyout, + enableFullScreen, + openMapSettings, + removePreviewLayers, } from '../../../actions'; import { FLYOUT_STATE } from '../../../reducers/ui'; import { getMapsCapabilities } from '../../../kibana_services'; +import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; function mapStateToProps(state = {}) { return { isFullScreen: getIsFullScreen(state), + isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, + isSaveDisabled: hasDirtyState(state), + inspectorAdapters: getInspectorAdapters(state), nextIndexPatternIds: getQueryableUniqueIndexPatternIds(state), flyoutDisplay: getFlyoutDisplay(state), refreshConfig: getRefreshConfig(state), @@ -68,6 +76,13 @@ function mapDispatchToProps(dispatch) { dispatch(updateFlyout(FLYOUT_STATE.NONE)); dispatch(setReadOnly(!getMapsCapabilities().save)); }, + closeFlyout: () => { + dispatch(setSelectedLayer(null)); + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + dispatch(removePreviewLayers()); + }, + enableFullScreen: () => dispatch(enableFullScreen()), + openMapSettings: () => dispatch(openMapSettings()), }; } diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index d945aa9623b21..97a08f11a6757 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -9,13 +9,17 @@ import { i18n } from '@kbn/i18n'; import 'mapbox-gl/dist/mapbox-gl.css'; import _ from 'lodash'; import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui'; -import { getData, getCoreChrome } from '../../../kibana_services'; +import { + getData, + getCoreChrome, + getMapsCapabilities, + getNavigation, +} from '../../../kibana_services'; import { copyPersistentState } from '../../../reducers/util'; import { getInitialLayers, getInitialLayersFromUrlParam } from '../../bootstrap/get_initial_layers'; import { getInitialTimeFilters } from '../../bootstrap/get_initial_time_filters'; import { getInitialRefreshConfig } from '../../bootstrap/get_initial_refresh_config'; import { getInitialQuery } from '../../bootstrap/get_initial_query'; -import { MapsTopNavMenu } from '../../page_elements/top_nav_menu'; import { getGlobalState, updateGlobalState, @@ -27,6 +31,7 @@ import { esFilters } from '../../../../../../../src/plugins/data/public'; import { MapContainer } from '../../../connected_components/map_container'; import { goToSpecifiedPath } from '../../maps_router'; import { getIndexPatternsFromIds } from '../../../index_pattern_util'; +import { getTopNavConfig } from './top_nav_config'; const unsavedChangesWarning = i18n.translate('xpack.maps.breadCrumbs.unsavedChangesWarning', { defaultMessage: 'Your map has unsaved changes. Are you sure you want to leave?', @@ -58,7 +63,10 @@ export class MapsAppView extends React.Component { this._updateFromGlobalState ); - this._updateStateFromSavedQuery(this._appStateManager.getAppState().savedQuery); + const initialSavedQuery = this._appStateManager.getAppState().savedQuery; + if (initialSavedQuery) { + this._updateStateFromSavedQuery(initialSavedQuery); + } this._initMap(); @@ -237,18 +245,10 @@ export class MapsAppView extends React.Component { ); } - _onTopNavRefreshConfig = ({ isPaused, refreshInterval }) => { - this._onRefreshConfigChange({ - isPaused, - interval: refreshInterval, - }); - }; + _updateStateFromSavedQuery = (savedQuery) => { + this.setState({ savedQuery: { ...savedQuery } }); + this._appStateManager.setQueryAndFilters({ savedQuery }); - _updateStateFromSavedQuery(savedQuery) { - if (!savedQuery) { - this.setState({ savedQuery: '' }); - return; - } const { filterManager } = getData().query; const savedQueryFilters = savedQuery.attributes.filters || []; const globalFilters = filterManager.getGlobalFilters(); @@ -266,7 +266,7 @@ export class MapsAppView extends React.Component { query: savedQuery.attributes.query, time: savedQuery.attributes.timefilter, }); - } + }; _initMap() { this._initMapAndLayerSettings(); @@ -295,27 +295,65 @@ export class MapsAppView extends React.Component { } _renderTopNav() { - return !this.props.isFullScreen ? ( - { - this.setState({ savedQuery: query }); - this._appStateManager.setQueryAndFilters({ savedQuery: query }); - this._updateStateFromSavedQuery(query); + filters={this.props.filters} + query={this.props.query} + onQuerySubmit={({ dateRange, query }) => { + this._onQueryChange({ + query, + time: dateRange, + refresh: true, + }); }} - onSavedQueryUpdated={(query) => { - this.setState({ savedQuery: { ...query } }); - this._appStateManager.setQueryAndFilters({ savedQuery: query }); - this._updateStateFromSavedQuery(query); + onFiltersUpdated={this._onFiltersChange} + dateRangeFrom={this.props.timeFilters.from} + dateRangeTo={this.props.timeFilters.to} + isRefreshPaused={this.props.refreshConfig.isPaused} + refreshInterval={this.props.refreshConfig.interval} + onRefreshChange={({ isPaused, refreshInterval }) => { + this._onRefreshConfigChange({ + isPaused, + interval: refreshInterval, + }); + }} + showSearchBar={true} + showFilterBar={true} + showDatePicker={true} + showSaveQuery={getMapsCapabilities().saveQuery} + savedQuery={this.state.savedQuery} + onSaved={this._updateStateFromSavedQuery} + onSavedQueryUpdated={this._updateStateFromSavedQuery} + onClearSavedQuery={() => { + const { filterManager, queryString } = getData().query; + this.setState({ savedQuery: '' }); + this._appStateManager.setQueryAndFilters({ savedQuery: '' }); + this._onQueryChange({ + filters: filterManager.getGlobalFilters(), + query: queryString.getDefaultQuery(), + }); }} - setBreadcrumbs={this._setBreadcrumbs} /> - ) : null; + ); } render() { diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx similarity index 72% rename from x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js rename to x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx index be474b43da81a..46d662b28a82f 100644 --- a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx @@ -6,109 +6,44 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { Adapters } from 'src/plugins/inspector/public'; +import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public'; import { - getNavigation, getCoreChrome, getMapsCapabilities, getInspector, getToasts, getCoreI18n, - getData, } from '../../../kibana_services'; import { SavedObjectSaveModal, + OnSaveProps, showSaveModal, } from '../../../../../../../src/plugins/saved_objects/public'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +// @ts-expect-error import { goToSpecifiedPath } from '../../maps_router'; +import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map'; -export function MapsTopNavMenu({ +export function getTopNavConfig({ savedMap, - query, - onQueryChange, - onQuerySaved, - onSavedQueryUpdated, - savedQuery, - timeFilters, - refreshConfig, - onRefreshConfigChange, - indexPatterns, - onFiltersChange, + isOpenSettingsDisabled, isSaveDisabled, closeFlyout, enableFullScreen, openMapSettings, inspectorAdapters, setBreadcrumbs, - isOpenSettingsDisabled, +}: { + savedMap: ISavedGisMap; + isOpenSettingsDisabled: boolean; + isSaveDisabled: boolean; + closeFlyout: () => void; + enableFullScreen: () => void; + openMapSettings: () => void; + inspectorAdapters: Adapters; + setBreadcrumbs: () => void; }) { - const { TopNavMenu } = getNavigation().ui; - const { filterManager, queryString } = getData().query; - const showSaveQuery = getMapsCapabilities().saveQuery; - const onClearSavedQuery = () => { - onQuerySaved(undefined); - onQueryChange({ - filters: filterManager.getGlobalFilters(), - query: queryString.getDefaultQuery(), - }); - }; - - // Nav settings - const config = getTopNavConfig( - savedMap, - isOpenSettingsDisabled, - isSaveDisabled, - closeFlyout, - enableFullScreen, - openMapSettings, - inspectorAdapters, - setBreadcrumbs - ); - - const submitQuery = function ({ dateRange, query }) { - onQueryChange({ - query, - time: dateRange, - refresh: true, - }); - }; - - return ( - - ); -} - -function getTopNavConfig( - savedMap, - isOpenSettingsDisabled, - isSaveDisabled, - closeFlyout, - enableFullScreen, - openMapSettings, - inspectorAdapters, - setBreadcrumbs -) { return [ { id: 'full-screen', @@ -180,11 +115,11 @@ function getTopNavConfig( newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate, - }) => { + }: OnSaveProps) => { const currentTitle = savedMap.title; savedMap.title = newTitle; savedMap.copyOnSave = newCopyOnSave; - const saveOptions = { + const saveOptions: SavedObjectSaveOpts = { confirmOverwrite: false, isTitleDuplicateConfirmed, onTitleDuplicate, @@ -218,7 +153,12 @@ function getTopNavConfig( ]; } -async function doSave(savedMap, saveOptions, closeFlyout, setBreadcrumbs) { +async function doSave( + savedMap: ISavedGisMap, + saveOptions: SavedObjectSaveOpts, + closeFlyout: () => void, + setBreadcrumbs: () => void +) { closeFlyout(); savedMap.syncWithStore(); let id; diff --git a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js index 64c07273c9ccf..c8e8db84df96f 100644 --- a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js +++ b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js @@ -25,10 +25,30 @@ export default function ({ getPageObjects }) { await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "ios"'); await PageObjects.maps.waitForMapPanAndZoom(origView); - const { lat, lon, zoom } = await PageObjects.maps.getView(); + const { lat, lon } = await PageObjects.maps.getView(); expect(Math.round(lat)).to.equal(43); expect(Math.round(lon)).to.equal(-102); - expect(Math.round(zoom)).to.equal(5); + }); + }); + + describe('with joins', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('join example'); + await PageObjects.maps.enableAutoFitToBounds(); + }); + + it('should automatically fit to bounds when query is applied', async () => { + // Set view to other side of world so no matching results + await PageObjects.maps.setView(0, 0, 6); + + // Setting query should trigger fit to bounds and move map + const origView = await PageObjects.maps.getView(); + await PageObjects.maps.setAndSubmitQuery('prop1 >= 11'); + await PageObjects.maps.waitForMapPanAndZoom(origView); + + const { lat, lon } = await PageObjects.maps.getView(); + expect(Math.round(lat)).to.equal(0); + expect(Math.round(lon)).to.equal(60); }); }); }); diff --git a/yarn.lock b/yarn.lock index 4e9f732f1e0a0..a70f04e030447 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4808,7 +4808,7 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest-specific-snapshot@^0.5.3": +"@types/jest-specific-snapshot@^0.5.3", "@types/jest-specific-snapshot@^0.5.4": version "0.5.4" resolved "https://registry.yarnpkg.com/@types/jest-specific-snapshot/-/jest-specific-snapshot-0.5.4.tgz#997364c39a59ddeff0ee790a19415e79dd061d1e" integrity sha512-1qISn4fH8wkOOPFEx+uWRRjw6m/pP/It3OHLm8Ee1KQpO7Z9ZGYDtWPU5AgK05UXsNTAgOK+dPQvJKGdy9E/1g== @@ -5726,7 +5726,7 @@ "@types/node" "*" chokidar "^2.1.2" -"@types/webpack-env@^1.15.0": +"@types/webpack-env@^1.15.0", "@types/webpack-env@^1.15.2": version "1.15.2" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a" integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ== @@ -11933,6 +11933,11 @@ diagnostics@^1.1.1: enabled "1.0.x" kuler "1.0.x" +diff-match-patch@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== + diff-match-patch@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1" @@ -19267,6 +19272,14 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +jsondiffpatch@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/jsondiffpatch/-/jsondiffpatch-0.4.1.tgz#9fb085036767f03534ebd46dcd841df6070c5773" + integrity sha512-t0etAxTUk1w5MYdNOkZBZ8rvYYN5iL+2dHCCx/DpkFm/bW28M6y5nUS83D4XdZiHy35Fpaw6LBb+F88fHZnVCw== + dependencies: + chalk "^2.3.0" + diff-match-patch "^1.0.0" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"