diff --git a/.sass-lint.yml b/.sass-lint.yml index fac5e4a04c1d65..64309aaae6cc3e 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -7,6 +7,8 @@ files: - 'x-pack/legacy/plugins/rollup/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss' + ignore: + - 'x-pack/legacy/plugins/canvas/shareable_runtime/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/lens/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss' rules: diff --git a/renovate.json5 b/renovate.json5 index 22682fa3d13610..4bada2f9ecc38f 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -623,6 +623,14 @@ '@types/zen-observable', ], }, + { + groupSlug: 'archiver', + groupName: 'archiver related packages', + packageNames: [ + 'archiver', + '@types/archiver', + ], + }, { groupSlug: 'base64-js', groupName: 'base64-js related packages', diff --git a/src/dev/build/build_distributables.js b/src/dev/build/build_distributables.js index 3f39faae5a4d00..e3fcfd9536c47c 100644 --- a/src/dev/build/build_distributables.js +++ b/src/dev/build/build_distributables.js @@ -86,7 +86,7 @@ export async function buildDistributables(options) { const config = await getConfig({ isRelease, versionQualifier, - targetAllPlatforms + targetAllPlatforms, }); const run = createRunner({ @@ -143,16 +143,20 @@ export async function buildDistributables(options) { * package platform-specific builds into archives * or os-specific packages in the target directory */ - if (createArchives) { // control w/ --skip-archives + if (createArchives) { + // control w/ --skip-archives await run(CreateArchivesTask); } - if (createDebPackage) { // control w/ --deb or --skip-os-packages + if (createDebPackage) { + // control w/ --deb or --skip-os-packages await run(CreateDebPackageTask); } - if (createRpmPackage) { // control w/ --rpm or --skip-os-packages + if (createRpmPackage) { + // control w/ --rpm or --skip-os-packages await run(CreateRpmPackageTask); } - if (createDockerPackage) { // control w/ --docker or --skip-os-packages + if (createDockerPackage) { + // control w/ --docker or --skip-os-packages await run(CreateDockerPackageTask); } diff --git a/src/dev/build/tasks/clean_tasks.js b/src/dev/build/tasks/clean_tasks.js index e5c52f663d0564..3ec1d6b6967e9d 100644 --- a/src/dev/build/tasks/clean_tasks.js +++ b/src/dev/build/tasks/clean_tasks.js @@ -29,39 +29,36 @@ export const CleanTask = { description: 'Cleaning artifacts from previous builds', async run(config, log) { - await deleteAll([ - config.resolveFromRepo('build'), - config.resolveFromRepo('target'), - config.resolveFromRepo('.node_binaries'), - ], log); + await deleteAll( + [ + config.resolveFromRepo('build'), + config.resolveFromRepo('target'), + config.resolveFromRepo('.node_binaries'), + ], + log + ); }, }; export const CleanPackagesTask = { - description: - 'Cleaning source for packages that are now installed in node_modules', + description: 'Cleaning source for packages that are now installed in node_modules', async run(config, log, build) { - await deleteAll([ - build.resolvePath('packages'), - build.resolvePath('yarn.lock'), - ], log); + await deleteAll([build.resolvePath('packages'), build.resolvePath('yarn.lock')], log); }, }; - export const CleanTypescriptTask = { - description: - 'Cleaning typescript source files that have been transpiled to JS', + description: 'Cleaning typescript source files that have been transpiled to JS', async run(config, log, build) { - log.info('Deleted %d files', await scanDelete({ - directory: build.resolvePath(), - regularExpressions: [ - /\.(ts|tsx|d\.ts)$/, - /tsconfig.*\.json$/ - ] - })); + log.info( + 'Deleted %d files', + await scanDelete({ + directory: build.resolvePath(), + regularExpressions: [/\.(ts|tsx|d\.ts)$/, /tsconfig.*\.json$/], + }) + ); }, }; @@ -70,9 +67,7 @@ export const CleanExtraFilesFromModulesTask = { async run(config, log, build) { const makeRegexps = patterns => - patterns.map(pattern => - minimatch.makeRe(pattern, { nocase: true }) - ); + patterns.map(pattern => minimatch.makeRe(pattern, { nocase: true })); const regularExpressions = makeRegexps([ // tests @@ -169,19 +164,23 @@ export const CleanExtraFilesFromModulesTask = { '**/docker-compose.yml', ]); - log.info('Deleted %d files', await scanDelete({ - directory: build.resolvePath('node_modules'), - regularExpressions, - excludePaths: [ - build.resolvePath('node_modules/@elastic/ctags-langserver/vendor') - ] - })); + log.info( + 'Deleted %d files', + await scanDelete({ + directory: build.resolvePath('node_modules'), + regularExpressions, + excludePaths: [build.resolvePath('node_modules/@elastic/ctags-langserver/vendor')], + }) + ); if (!build.isOss()) { - log.info('Deleted %d files', await scanDelete({ - directory: build.resolvePath('x-pack/node_modules'), - regularExpressions - })); + log.info( + 'Deleted %d files', + await scanDelete({ + directory: build.resolvePath('x-pack/node_modules'), + regularExpressions, + }) + ); } }, }; @@ -192,14 +191,15 @@ export const CleanExtraBinScriptsTask = { async run(config, log, build) { for (const platform of config.getNodePlatforms()) { if (platform.isWindows()) { - await deleteAll([ - build.resolvePathForPlatform(platform, 'bin', '*'), - `!${build.resolvePathForPlatform(platform, 'bin', '*.bat')}`, - ], log); + await deleteAll( + [ + build.resolvePathForPlatform(platform, 'bin', '*'), + `!${build.resolvePathForPlatform(platform, 'bin', '*.bat')}`, + ], + log + ); } else { - await deleteAll([ - build.resolvePathForPlatform(platform, 'bin', '*.bat'), - ], log); + await deleteAll([build.resolvePathForPlatform(platform, 'bin', '*.bat')], log); } } }, @@ -251,14 +251,10 @@ export const CleanEmptyFoldersTask = { // Delete every single empty folder from // the distributable except the plugins // and data folder. - await deleteEmptyFolders( - log, - build.resolvePath('.'), - [ - build.resolvePath('plugins'), - build.resolvePath('data') - ] - ); + await deleteEmptyFolders(log, build.resolvePath('.'), [ + build.resolvePath('plugins'), + build.resolvePath('data'), + ]); }, }; @@ -266,7 +262,7 @@ export const CleanCtagBuildTask = { description: 'Cleaning extra platform-specific files from @elastic/node-ctag build dir', async run(config, log, build) { - const getPlatformId = (platform) => { + const getPlatformId = platform => { if (platform.isWindows()) { return 'win32'; } else if (platform.isLinux()) { @@ -283,11 +279,17 @@ export const CleanCtagBuildTask = { } const ctagsBuildDir = build.resolvePathForPlatform(platform, RELATIVE_CTAGS_BUILD_DIR); - await deleteAll([ - resolve(ctagsBuildDir, '*'), - `!${resolve(ctagsBuildDir, `ctags-node-v${process.versions.modules}-${getPlatformId(platform)}-x64`)}` - ], log); + await deleteAll( + [ + resolve(ctagsBuildDir, '*'), + `!${resolve( + ctagsBuildDir, + `ctags-node-v${process.versions.modules}-${getPlatformId(platform)}-x64` + )}`, + ], + log + ); }) ); - } + }, }; diff --git a/src/dev/jest/mocks/css_module_mock.js b/src/dev/jest/mocks/css_module_mock.js new file mode 100644 index 00000000000000..a8018154e39197 --- /dev/null +++ b/src/dev/jest/mocks/css_module_mock.js @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * This proxy allows for CSS Modules to be interpreted properly by + * Jest. Given a CSS Module class `thisClass`, we'd expect it to + * be obfuscated at runtime. With this mock, `thisClass` will be + * returned. This allows for consistent enzyme and snapshot tests. + */ +module.exports = new Proxy( + {}, + { + get: function getter(target, key) { + if (key === '__esModule') { + return false; + } + return key; + }, + } +); diff --git a/x-pack/.kibana-plugin-helpers.json b/x-pack/.kibana-plugin-helpers.json index b4f0915a27b9d6..032783d9309f1a 100644 --- a/x-pack/.kibana-plugin-helpers.json +++ b/x-pack/.kibana-plugin-helpers.json @@ -28,7 +28,9 @@ "!legacy/plugins/**/*.test.{js,ts}", "!legacy/plugins/**/__snapshots__", "!legacy/plugins/**/__snapshots__/*", - "!legacy/plugins/**/__mocks__/*" + "!legacy/plugins/**/__mocks__/*", + "!legacy/plugins/canvas/shareable_runtime/test", + "!legacy/plugins/canvas/shareable_runtime/test/**/*" ], "skipInstallDependencies": true } diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index e9ed43e81780bd..0fba7d2cefbd52 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -24,14 +24,13 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '^plugins/([^/.]*)(.*)': `${kibanaDirectory}/src/legacy/core_plugins/$1/public$2`, '^legacy/plugins/xpack_main/(.*);': `${xPackKibanaDirectory}/legacy/plugins/xpack_main/public/$1`, '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath, + '\\.module.(css|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/css_module_mock.js`, '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, '^test_utils/enzyme_helpers': `${xPackKibanaDirectory}/test_utils/enzyme_helpers.tsx`, '^test_utils/find_test_subject': `${xPackKibanaDirectory}/test_utils/find_test_subject.ts`, }, coverageDirectory: '/../target/kibana-coverage/jest', - coverageReporters: [ - 'html', - ], + coverageReporters: ['html'], setupFiles: [ `${kibanaDirectory}/src/dev/jest/setup/babel_polyfill.js`, `/dev-tools/jest/setup/polyfills.js`, diff --git a/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js b/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js index 333d8e3c05beb7..b7924486c34cb6 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js +++ b/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js @@ -12,6 +12,11 @@ import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-story 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'; +global.$ = jquery; +global.jQuery = jquery; + // Set our default timezone to UTC for tests so we can generate predictable snapshots moment.tz.setDefault('UTC'); @@ -53,6 +58,19 @@ jest.mock( } ); +// Disabling this test due to https://github.com/elastic/eui/issues/2242 +jest.mock( + '../public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.examples', + () => { + return 'Disabled Panel'; + } +); + +// 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'); +RenderedElement.mockImplementation(() => 'RenderedElement'); + addSerializer(styleSheetSerializer); // Initialize Storyshots and build the Jest Snapshots diff --git a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js index 6d28d9d97b23f8..662078585422f3 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js +++ b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js @@ -57,6 +57,56 @@ module.exports = async ({ config }) => { ], }); + // 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: { + path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, + }, + { loader: 'sass-loader' }, + ], + }); + + // 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: true, + localIdentName: '[name]__[local]___[hash:base64:5]', + }, + }, + { + loader: 'postcss-loader', + options: { + path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, + }, + { + loader: 'sass-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( @@ -109,8 +159,8 @@ module.exports = async ({ config }) => { }) ); - // Tell Webpack about the ts/x extensions - config.resolve.extensions.push('.ts', '.tsx'); + // 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 diff --git a/x-pack/legacy/plugins/canvas/common/lib/constants.ts b/x-pack/legacy/plugins/canvas/common/lib/constants.ts index 381d5c2900a0c3..d4b092c830f72c 100644 --- a/x-pack/legacy/plugins/canvas/common/lib/constants.ts +++ b/x-pack/legacy/plugins/canvas/common/lib/constants.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SHAREABLE_RUNTIME_NAME } from '../../shareable_runtime/constants'; + export const CANVAS_TYPE = 'canvas-workpad'; export const CUSTOM_ELEMENT_TYPE = 'canvas-element'; export const CANVAS_APP = 'canvas'; @@ -33,3 +35,7 @@ export const CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR = `canvasLayout__stageContent` export const DATATABLE_COLUMN_TYPES = ['string', 'number', 'null', 'boolean', 'date']; export const LAUNCHED_FULLSCREEN = 'workpad-full-screen-launch'; export const LAUNCHED_FULLSCREEN_AUTOPLAY = 'workpad-full-screen-launch-with-autoplay'; +export const API_ROUTE_SHAREABLE_BASE = '/public/canvas'; +export const API_ROUTE_SHAREABLE_ZIP = `${API_ROUTE_SHAREABLE_BASE}/zip`; +export const API_ROUTE_SHAREABLE_RUNTIME = `${API_ROUTE_SHAREABLE_BASE}/runtime`; +export const API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD = `${API_ROUTE_SHAREABLE_BASE}/${SHAREABLE_RUNTIME_NAME}.js`; diff --git a/x-pack/legacy/plugins/canvas/common/lib/fetch.js b/x-pack/legacy/plugins/canvas/common/lib/fetch.ts similarity index 67% rename from x-pack/legacy/plugins/canvas/common/lib/fetch.js rename to x-pack/legacy/plugins/canvas/common/lib/fetch.ts index 8af72b9aef25b4..dd975eb4c2023c 100644 --- a/x-pack/legacy/plugins/canvas/common/lib/fetch.js +++ b/x-pack/legacy/plugins/canvas/common/lib/fetch.ts @@ -15,3 +15,13 @@ export const fetch = axios.create({ }, timeout: FETCH_TIMEOUT, }); + +export const arrayBufferFetch = axios.create({ + responseType: 'arraybuffer', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'kbn-xsrf': 'professionally-crafted-string-of-text', + }, + timeout: FETCH_TIMEOUT, +}); diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts index f9cf90bd328573..23725f3be6a120 100644 --- a/x-pack/legacy/plugins/canvas/i18n/components.ts +++ b/x-pack/legacy/plugins/canvas/i18n/components.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { CANVAS, JSON, KIBANA, PDF, POST, URL } from './constants'; +import { CANVAS, HTML, JSON, KIBANA, PDF, POST, URL, ZIP } from './constants'; export const ComponentStrings = { AddEmbeddableFlyout: { @@ -463,6 +463,144 @@ export const ComponentStrings = { defaultMessage: 'Delete', }), }, + ShareWebsiteFlyout: { + getRuntimeStepTitle: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadRuntimeTitle', { + defaultMessage: 'Download runtime', + }), + getSnippentsStepTitle: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.addSnippetsTitle', { + defaultMessage: 'Add snippets to website', + }), + getStepsDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.description', { + defaultMessage: + 'Follow these steps to share a static version of this workpad on an external website. It will be a visual snapshot of the current workpad, and will not have access to live data.', + }), + getTitle: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.flyoutTitle', { + defaultMessage: 'Share on a website', + }), + getUnsupportedRendererWarning: () => + i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.unsupportedRendererWarning', { + defaultMessage: + 'This workpad contains render functions that are not supported by the {CANVAS} Shareable Workpad Runtime. These elements will not be rendered:', + values: { + CANVAS, + }, + }), + getWorkpadStepTitle: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadWorkpadTitle', { + defaultMessage: 'Download workpad', + }), + }, + ShareWebsiteRuntimeStep: { + getDownloadLabel: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.runtimeStep.downloadLabel', { + defaultMessage: 'Download runtime', + }), + getStepDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.runtimeStep.description', { + defaultMessage: + 'In order to render a Shareable Workpad, you also need to include the {CANVAS} Shareable Workpad Runtime. You can skip this step if the runtime is already included on your website.', + values: { + CANVAS, + }, + }), + }, + ShareWebsiteSnippetsStep: { + getAutoplayParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.autoplayParameterDescription', { + defaultMessage: 'Should the runtime automatically move through the pages of the workpad?', + }), + getCallRuntimeLabel: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.callRuntimeLabel', { + defaultMessage: 'Call Runtime', + }), + getHeightParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.heightParameterDescription', { + defaultMessage: 'The height of the Workpad. Defaults to the Workpad height.', + }), + getIncludeRuntimeLabel: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.includeRuntimeLabel', { + defaultMessage: 'Include Runtime', + }), + getIntervalParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.intervalParameterDescription', { + defaultMessage: + 'The interval upon which the pages will advance in time format, (e.g. {twoSeconds}, {oneMinute})', + values: { + twoSeconds: '2s', + oneMinute: '1m', + }, + }), + getPageParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.pageParameterDescription', { + defaultMessage: 'The page to display. Defaults to the page specified by the Workpad.', + }), + getParametersDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.parametersDescription', { + defaultMessage: + 'There are a number of inline parameters to configure the Shareable Workpad.', + }), + getParametersTitle: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.parametersLabel', { + defaultMessage: 'Parameters', + }), + getPlaceholderLabel: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.placeholderLabel', { + defaultMessage: 'Placeholder', + }), + getRequiredLabel: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.requiredLabel', { + defaultMessage: 'required', + }), + getShareableParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.shareableParameterDescription', { + defaultMessage: 'The type of shareable. In this case, a {CANVAS} Workpad.', + values: { + CANVAS, + }, + }), + getSnippetsStepDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.description', { + defaultMessage: + 'The Workpad is placed within the {HTML} of the site by using an {HTML} placeholder. Parameters for the runtime are included inline. See the full list of parameters below. You can include more than one workpad on the page.', + values: { + HTML, + }, + }), + getToolbarParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.toolbarParameterDescription', { + defaultMessage: 'Should the toolbar be hidden?', + }), + getUrlParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.urlParameterDescription', { + defaultMessage: 'The {URL} of the Shareable Workpad {JSON} file.', + values: { + URL, + JSON, + }, + }), + getWidthParameterDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.widthParameterDescription', { + defaultMessage: 'The width of the Workpad. Defaults to the Workpad width.', + }), + }, + ShareWebsiteWorkpadStep: { + getDownloadLabel: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.workpadStep.downloadLabel', { + defaultMessage: 'Download workpad', + }), + getStepDescription: () => + i18n.translate('xpack.canvas.shareWebsiteFlyout.workpadStep.description', { + defaultMessage: + 'The workpad will be exported as a single {JSON} file for sharing in another site.', + values: { + JSON, + }, + }), + }, SidebarContent: { getGroupedElementSidebarTitle: () => i18n.translate('xpack.canvas.sidebarContent.groupedElementSidebarTitle', { @@ -816,6 +954,10 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.copyReportingConfigMessage', { defaultMessage: 'Copied reporting configuration to clipboard', }), + getCopyShareConfigMessage: () => + i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.copyShareConfigMessage', { + defaultMessage: 'Copied share markup to clipboard', + }), getExportPDFErrorTitle: (workpadName: string) => i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.exportPDFErrorMessage', { defaultMessage: "Failed to create {PDF} for '{workpadName}'", @@ -881,6 +1023,15 @@ export const ComponentStrings = { PDF, }, }), + getShareableZipErrorTitle: (workpadName: string) => + i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteErrorTitle', { + defaultMessage: + "Failed to create {ZIP} file for '{workpadName}'. The workpad may be too large. You'll need to download the files separately.", + values: { + ZIP, + workpadName, + }, + }), getShareDownloadJSONTitle: () => i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareDownloadJSONTitle', { defaultMessage: 'Download as {JSON}', @@ -895,6 +1046,10 @@ export const ComponentStrings = { PDF, }, }), + getShareWebsiteTitle: () => + i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteTitle', { + defaultMessage: 'Share on a website', + }), getShareWorkpadMessage: () => i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareWorkpadMessage', { defaultMessage: 'Share this workpad', diff --git a/x-pack/legacy/plugins/canvas/i18n/constants.ts b/x-pack/legacy/plugins/canvas/i18n/constants.ts index 3cc583f6af8a88..865775047ca67b 100644 --- a/x-pack/legacy/plugins/canvas/i18n/constants.ts +++ b/x-pack/legacy/plugins/canvas/i18n/constants.ts @@ -41,3 +41,4 @@ export const TYPE_NUMBER = '`number`'; export const TYPE_STRING = '`string`'; export const URL = 'URL'; export const UTC = 'UTC'; +export const ZIP = 'ZIP'; diff --git a/x-pack/legacy/plugins/canvas/i18n/errors.ts b/x-pack/legacy/plugins/canvas/i18n/errors.ts index c61fa54154e0ed..4c32622105c040 100644 --- a/x-pack/legacy/plugins/canvas/i18n/errors.ts +++ b/x-pack/legacy/plugins/canvas/i18n/errors.ts @@ -22,6 +22,24 @@ export const ErrorStrings = { i18n.translate('xpack.canvas.error.downloadWorkpad.downloadFailureErrorMessage', { defaultMessage: "Couldn't download workpad", }), + getDownloadRenderedWorkpadFailureErrorMessage: () => + i18n.translate( + 'xpack.canvas.error.downloadWorkpad.downloadRenderedWorkpadFailureErrorMessage', + { + defaultMessage: "Couldn't download rendered workpad", + } + ), + getDownloadRuntimeFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.downloadWorkpad.downloadRuntimeFailureErrorMessage', { + defaultMessage: "Couldn't download Shareable Runtime", + }), + getDownloadZippedRuntimeFailureErrorMessage: () => + i18n.translate( + 'xpack.canvas.error.downloadWorkpad.downloadZippedRuntimeFailureErrorMessage', + { + defaultMessage: "Couldn't download ZIP file", + } + ), }, esPersist: { getSaveFailureTitle: () => diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx index f021f4724bc52b..0bcb0c89ba1ccb 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx @@ -17,7 +17,7 @@ const { WorkpadHeaderCustomInterval: strings } = ComponentStrings; interface Props { gutterSize: FlexGroupGutterSize; buttonSize: ButtonSize; - onSubmit: (interval: number | undefined) => void; + onSubmit: (interval: number) => void; defaultValue: any; } @@ -32,8 +32,9 @@ export const CustomInterval = ({ gutterSize, buttonSize, onSubmit, defaultValue
{ ev.preventDefault(); - - onSubmit(refreshInterval); + if (!isInvalid && refreshInterval) { + onSubmit(refreshInterval); + } }} > diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot index 4b05517d15de6e..c9f5cf67f734ab 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot @@ -1,85 +1,89 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Storyshots components/Export/WorkpadExport disabled 1`] = ` -
+
- - - + + +
`; exports[`Storyshots components/Export/WorkpadExport enabled 1`] = ` -
+
- - - + + +
`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.examples.tsx new file mode 100644 index 00000000000000..af30d8d4fc20b5 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.examples.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ShareWebsiteFlyout } from '../share_website_flyout'; + +storiesOf('components/Export/ShareWebsiteFlyout', module) + .addParameters({ + info: { + inline: true, + styles: { + infoBody: { + margin: 20, + }, + infoStory: { + margin: '20px 30px', + width: '620px', + }, + }, + }, + }) + .add('default', () => ( + + )) + .add('unsupported renderers', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/index.ts new file mode 100644 index 00000000000000..756e84baffe547 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/index.ts @@ -0,0 +1,94 @@ +/* + * 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 chrome from 'ui/chrome'; +import { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +// @ts-ignore Untyped local +import { + getWorkpad, + getRenderedWorkpad, + getRenderedWorkpadExpressions, +} from '../../../../state/selectors/workpad'; +// @ts-ignore Untyped local +import { notify } from '../../../../lib/notify'; +// @ts-ignore Untyped local +import { + downloadRenderedWorkpad, + downloadRuntime, + downloadZippedRuntime, + // @ts-ignore Untyped local +} from '../../../../lib/download_workpad'; +import { ShareWebsiteFlyout as Component, Props as ComponentProps } from './share_website_flyout'; +import { State, CanvasWorkpad } from '../../../../../types'; +import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types'; +// @ts-ignore Untyped local. +import { fetch, arrayBufferFetch } from '../../../../../common/lib/fetch'; +import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants'; +import { renderFunctionNames } from '../../../../../shareable_runtime/supported_renderers'; + +import { ComponentStrings } from '../../../../../i18n'; +import { OnCloseFn } from '../workpad_export'; +const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; + +const getUnsupportedRenderers = (state: State) => { + const renderers: string[] = []; + const expressions = getRenderedWorkpadExpressions(state); + expressions.forEach(expression => { + if (!renderFunctionNames.includes(expression)) { + renderers.push(expression); + } + }); + + return renderers; +}; + +const mapStateToProps = (state: State) => ({ + renderedWorkpad: getRenderedWorkpad(state), + unsupportedRenderers: getUnsupportedRenderers(state), + workpad: getWorkpad(state), +}); + +interface Props { + onClose: OnCloseFn; + renderedWorkpad: CanvasRenderedWorkpad; + unsupportedRenderers: string[]; + workpad: CanvasWorkpad; +} + +export const ShareWebsiteFlyout = compose>( + connect(mapStateToProps), + withProps( + ({ unsupportedRenderers, renderedWorkpad, onClose, workpad }: Props): ComponentProps => ({ + unsupportedRenderers, + onClose, + onCopy: () => { + notify.info(strings.getCopyShareConfigMessage()); + }, + onDownload: type => { + switch (type) { + case 'share': + downloadRenderedWorkpad(renderedWorkpad); + return; + case 'shareRuntime': + downloadRuntime(); + return; + case 'shareZip': + const basePath = chrome.getBasePath(); + arrayBufferFetch + .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) + .then(blob => downloadZippedRuntime(blob.data)) + .catch((err: Error) => { + notify.error(err, { title: strings.getShareableZipErrorTitle(workpad.name) }); + }); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/runtime_step.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/runtime_step.tsx new file mode 100644 index 00000000000000..dafca394a5037a --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/runtime_step.tsx @@ -0,0 +1,29 @@ +/* + * 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 { EuiText, EuiSpacer, EuiButton } from '@elastic/eui'; + +import { ComponentStrings } from '../../../../../i18n'; + +import { OnDownloadFn } from './share_website_flyout'; + +const { ShareWebsiteRuntimeStep: strings } = ComponentStrings; + +export const RuntimeStep: FC<{ onDownload: OnDownloadFn }> = ({ onDownload }) => ( + +

{strings.getStepDescription()}

+ + { + onDownload('shareRuntime'); + }} + size="s" + > + {strings.getDownloadLabel()} + +
+); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/share_website_flyout.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/share_website_flyout.tsx new file mode 100644 index 00000000000000..f5730ea2eab843 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/share_website_flyout.tsx @@ -0,0 +1,138 @@ +/* + * 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 { + EuiText, + EuiSpacer, + EuiCallOut, + EuiSteps, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiLink, + EuiCode, + EuiBetaBadge, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { ComponentStrings, ZIP, CANVAS, HTML } from '../../../../../i18n'; +import { OnCloseFn } from '../workpad_export'; +import { WorkpadStep } from './workpad_step'; +import { RuntimeStep } from './runtime_step'; +import { SnippetsStep } from './snippets_step'; + +const { ShareWebsiteFlyout: strings } = ComponentStrings; + +export type OnDownloadFn = (type: 'share' | 'shareRuntime' | 'shareZip') => void; +export type OnCopyFn = () => void; + +export interface Props { + onCopy: OnCopyFn; + onDownload: OnDownloadFn; + onClose: OnCloseFn; + unsupportedRenderers?: string[]; +} + +const steps = (onDownload: OnDownloadFn, onCopy: OnCopyFn) => [ + { + title: strings.getWorkpadStepTitle(), + children: , + }, + { + title: strings.getRuntimeStepTitle(), + children: , + }, + { + title: strings.getSnippentsStepTitle(), + children: , + }, +]; + +export const ShareWebsiteFlyout: FC = ({ + onCopy, + onDownload, + onClose, + unsupportedRenderers, +}) => { + const link = ( + { + onDownload('shareZip'); + }} + > + + + ); + + const title = ( +
+ +
+ ); + + let warningText = null; + + if (unsupportedRenderers && unsupportedRenderers.length > 0) { + const warning = [ + + {strings.getUnsupportedRendererWarning()} + {unsupportedRenderers.map((fn, index) => [ + {fn}, + index < unsupportedRenderers.length - 1 ? ', ' : '', + ])} + , + , + ]; + warningText = [ + , + , + ]; + } + + return ( + onClose('share')} maxWidth> + + + +

+ {strings.getTitle()} +

+ + + +
+
+
+ + +

{strings.getStepsDescription()}

+
+ + + + {warningText} + +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/snippets_step.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/snippets_step.tsx new file mode 100644 index 00000000000000..f933d9009d3674 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/snippets_step.tsx @@ -0,0 +1,110 @@ +/* + * 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 { + EuiText, + EuiSpacer, + EuiCode, + EuiCodeBlock, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiHorizontalRule, +} from '@elastic/eui'; + +import { ComponentStrings } from '../../../../../i18n'; + +import { Clipboard } from '../../../clipboard'; +import { OnCopyFn } from './share_website_flyout'; + +const { ShareWebsiteSnippetsStep: strings } = ComponentStrings; + +const HTML = ` + + + +
+ + +`; + +export const SnippetsStep: FC<{ onCopy: OnCopyFn }> = ({ onCopy }) => ( +
+ +

{strings.getSnippetsStepDescription()}

+
+ + + + {HTML} + + + + +

{strings.getParametersTitle()}

+

{strings.getParametersDescription()}

+
+ + + + kbn-canvas-shareable="canvas" ({strings.getRequiredLabel()}) + + + {strings.getShareableParameterDescription()} + + + kbn-canvas-url ({strings.getRequiredLabel()}) + + + {strings.getUrlParameterDescription()} + + + kbn-canvas-height + + + {strings.getHeightParameterDescription()} + + + kbn-canvas-width + + + {strings.getWidthParameterDescription()} + + + kbn-canvas-page + + + {strings.getPageParameterDescription()} + + + kbn-canvas-autoplay + + + {strings.getAutoplayParameterDescription()} + + + kbn-canvas-interval + + + {strings.getIntervalParameterDescription()} + + + kbn-canvas-toolbar + + + {strings.getToolbarParameterDescription()} + + +
+); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/workpad_step.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/workpad_step.tsx new file mode 100644 index 00000000000000..6874090702b2d5 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/workpad_step.tsx @@ -0,0 +1,29 @@ +/* + * 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 { EuiText, EuiSpacer, EuiButton } from '@elastic/eui'; + +import { ComponentStrings } from '../../../../../i18n'; + +import { OnDownloadFn } from './share_website_flyout'; + +const { ShareWebsiteWorkpadStep: strings } = ComponentStrings; + +export const WorkpadStep: FC<{ onDownload: OnDownloadFn }> = ({ onDownload }) => ( + +

{strings.getStepDescription()}

+ + { + onDownload('share'); + }} + size="s" + > + {strings.getDownloadLabel()} + +
+); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts index 7eca74381d9563..f677f84362da7f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts @@ -15,10 +15,15 @@ import { getReportingBrowserType } from '../../../state/selectors/app'; import { notify } from '../../../lib/notify'; import { getWindow } from '../../../lib/get_window'; // @ts-ignore Untyped local -import { downloadWorkpad } from '../../../lib/download_workpad'; +import { + downloadWorkpad, + // @ts-ignore Untyped local +} from '../../../lib/download_workpad'; import { WorkpadExport as Component, Props as ComponentProps } from './workpad_export'; import { getPdfUrl, createPdf } from './utils'; import { State, CanvasWorkpad } from '../../../../types'; +// @ts-ignore Untyped local. +import { fetch, arrayBufferFetch } from '../../../../common/lib/fetch'; import { ComponentStrings } from '../../../../i18n'; const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; @@ -88,7 +93,7 @@ export const WorkpadExport = compose( }); case 'json': downloadWorkpad(workpad.id); - break; + return; default: throw new Error(strings.getUnknownExportErrorMessage(type)); } diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx index cd07142d11d57f..20f48c20927668 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent, MouseEvent } from 'react'; +import React, { FunctionComponent, useState, MouseEvent } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonIcon, EuiContextMenu, EuiIcon } from '@elastic/eui'; // @ts-ignore Untyped local import { Popover } from '../../popover'; import { DisabledPanel } from './disabled_panel'; import { PDFPanel } from './pdf_panel'; +import { ShareWebsiteFlyout } from './flyout'; import { ComponentStrings } from '../../../../i18n'; const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; @@ -20,9 +21,11 @@ type ClosePopoverFn = () => void; type CopyTypes = 'pdf' | 'reportingConfig'; type ExportTypes = 'pdf' | 'json'; type ExportUrlTypes = 'pdf'; +type CloseTypes = 'share'; export type OnCopyFn = (type: CopyTypes) => void; export type OnExportFn = (type: ExportTypes) => void; +export type OnCloseFn = (type: CloseTypes) => void; export type GetExportUrlFn = (type: ExportUrlTypes) => string; export interface Props { @@ -45,6 +48,12 @@ export const WorkpadExport: FunctionComponent = ({ onExport, getExportUrl, }) => { + const [showFlyout, setShowFlyout] = useState(false); + + const onClose = () => { + setShowFlyout(false); + }; + // TODO: Fix all of this magic from EUI; this code is boilerplate from // EUI examples and isn't easily typed. const flattenPanelTree = (tree: any, array: any[] = []) => { @@ -109,6 +118,14 @@ export const WorkpadExport: FunctionComponent = ({ ), }, }, + { + name: strings.getShareWebsiteTitle(), + icon: , + onClick: () => { + setShowFlyout(true); + closePopover(); + }, + }, ], }); @@ -120,17 +137,25 @@ export const WorkpadExport: FunctionComponent = ({ /> ); + const flyout = showFlyout ? : null; + return ( - - {({ closePopover }: { closePopover: ClosePopoverFn }) => ( - - )} - +
+ + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + + )} + + {flyout} +
); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_page/integration_utils.js b/x-pack/legacy/plugins/canvas/public/components/workpad_page/integration_utils.js index 51a897d3c6d9a9..34c3d446c5ca7d 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_page/integration_utils.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_page/integration_utils.js @@ -8,12 +8,11 @@ import { shallowEqual } from 'recompose'; import { getNodes, getSelectedPage } from '../../state/selectors/workpad'; import { addElement, removeElements, setMultiplePositions } from '../../state/actions/elements'; import { selectToplevelNodes } from '../../state/actions/transient'; -import { matrixToAngle } from '../../lib/aeroelastic/matrix'; import { arrayToMap, flatten, identity } from '../../lib/aeroelastic/functional'; import { getLocalTransformMatrix } from '../../lib/aeroelastic/layout_functions'; -import { isGroupId, elementToShape } from './positioning_utils'; - -export * from './positioning_utils'; +import { matrixToAngle } from '../../lib/aeroelastic/matrix'; +import { isGroupId, elementToShape } from './utils'; +export * from './utils'; const shapeToElement = shape => ({ left: shape.transformMatrix[12] - shape.a, diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_page/utils.js b/x-pack/legacy/plugins/canvas/public/components/workpad_page/utils.js new file mode 100644 index 00000000000000..04b1e02d422dec --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_page/utils.js @@ -0,0 +1,58 @@ +/* + * 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 { multiply, rotateZ, translate } from '../../lib/aeroelastic/matrix'; + +export const isGroupId = id => id.startsWith('group'); + +const headerData = id => + isGroupId(id) + ? { id, type: 'group', subtype: 'persistentGroup' } + : { id, type: 'rectangleElement', subtype: '' }; + +const transformData = ({ top, left, width, height, angle }, z) => + multiply( + translate(left + width / 2, top + height / 2, z), // painter's algo: latest item (highest z) goes to top + rotateZ((-angle / 180) * Math.PI) // minus angle as transform:matrix3d uses a left-handed coordinate system + ); + +/** + * elementToShape + * + * converts a `kibana-canvas` element to an `aeroelastic` shape. + * + * Shape: the layout algorithms need to deal with objects through their geometric properties, excluding other aspects, + * such as what's inside the element, eg. image or scatter plot. This representation is, at its core, a transform matrix + * that establishes a new local coordinate system https://drafts.csswg.org/css-transforms/#local-coordinate-system plus a + * size descriptor. There are two versions of the transform matrix: + * - `transformMatrix` is analogous to the SVG https://drafts.csswg.org/css-transforms/#current-transformation-matrix + * - `localTransformMatrix` is analogous to the SVG https://drafts.csswg.org/css-transforms/#transformation-matrix + * + * Element: it also needs to represent the geometry, primarily because of the need to persist it in `redux` and on the + * server, and to accept such data from the server. The redux and server representations will need to change as more general + * projections such as 3D are added. The element also needs to maintain its content, such as an image or a plot. + * + * While all elements on the current page also exist as shapes, there are shapes that are not elements: annotations. + * For example, `rotation_handle`, `border_resize_handle` and `border_connection` are modeled as shapes by the layout + * library, simply for generality. + */ +export const elementToShape = ({ id, position }, z) => ({ + ...headerData(id), + parent: (position && position.parent) || null, + transformMatrix: transformData(position, z), + a: position.width / 2, // we currently specify half-width, half-height as it leads to + b: position.height / 2, // more regular math (like ellipsis radii rather than diameters) +}); + +const simplePosition = ({ id, position, filter }, z) => ({ + ...headerData(id), + width: position.width, + height: position.height, + transformMatrix: transformData(position, z), + filter, +}); + +export const simplePositioning = ({ elements }) => ({ elements: elements.map(simplePosition) }); diff --git a/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts b/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts index bdadb224482fac..7bcf0ac948613b 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import fileSaver from 'file-saver'; +import chrome from 'ui/chrome'; +import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../common/lib/constants'; import { ErrorStrings } from '../../i18n'; // @ts-ignore untyped local import { notify } from './notify'; // @ts-ignore untyped local import * as workpadService from './workpad_service'; +import { CanvasRenderedWorkpad } from '../../shareable_runtime/types'; const { downloadWorkpad: strings } = ErrorStrings; @@ -21,3 +24,35 @@ export const downloadWorkpad = async (workpadId: string) => { notify.error(err, { title: strings.getDownloadFailureErrorMessage() }); } }; + +export const downloadRenderedWorkpad = async (renderedWorkpad: CanvasRenderedWorkpad) => { + try { + const jsonBlob = new Blob([JSON.stringify(renderedWorkpad)], { type: 'application/json' }); + fileSaver.saveAs( + jsonBlob, + `canvas-embed-workpad-${renderedWorkpad.name}-${renderedWorkpad.id}.json` + ); + } catch (err) { + notify.error(err, { title: strings.getDownloadRenderedWorkpadFailureErrorMessage() }); + } +}; + +export const downloadRuntime = async () => { + try { + const basePath = chrome.getBasePath(); + const path = `${basePath}${API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD}`; + window.open(path); + return; + } catch (err) { + notify.error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() }); + } +}; + +export const downloadZippedRuntime = async (data: any) => { + try { + const zip = new Blob([data], { type: 'octet/stream' }); + fileSaver.saveAs(zip, 'canvas-workpad-embed.zip'); + } catch (err) { + notify.error(err, { title: strings.getDownloadZippedRuntimeFailureErrorMessage() }); + } +}; diff --git a/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts index b41e4b3b4a1eac..d47a339cf8afe2 100644 --- a/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts @@ -421,3 +421,56 @@ export function getRefreshInterval(state: State): number { export function getAutoplay(state: State): State['transient']['autoplay'] { return get(state, 'transient.autoplay'); } + +export function getRenderedWorkpad(state: State) { + const currentPages = getPages(state); + const args = state.transient.resolvedArgs; + const renderedPages = currentPages.map(page => { + const { elements, ...rest } = page; + return { + ...rest, + elements: elements.map(element => { + const { id, position } = element; + const arg = args[id]; + if (!arg) { + return null; + } + const { expressionRenderable } = arg; + + return { id, position, expressionRenderable }; + }), + }; + }); + + const workpad = getWorkpad(state); + + // eslint-disable-next-line no-unused-vars + const { pages, ...rest } = workpad; + + return { + pages: renderedPages, + ...rest, + }; +} + +export function getRenderedWorkpadExpressions(state: State) { + const workpad = getRenderedWorkpad(state); + const { pages } = workpad; + const expressions: string[] = []; + + pages.forEach(page => + page.elements.forEach(element => { + if (element && element.expressionRenderable) { + const { value } = element.expressionRenderable; + if (value) { + const { as } = value; + if (!expressions.includes(as)) { + expressions.push(as); + } + } + } + }) + ); + + return expressions; +} diff --git a/x-pack/legacy/plugins/canvas/scripts/jest.js b/x-pack/legacy/plugins/canvas/scripts/jest.js index 7d1c8cf20362e8..cce1b8d3558461 100644 --- a/x-pack/legacy/plugins/canvas/scripts/jest.js +++ b/x-pack/legacy/plugins/canvas/scripts/jest.js @@ -43,26 +43,24 @@ run( '--coverageDirectory', // Output to canvas/coverage 'legacy/plugins/canvas/coverage', ]; - } else { - // Mitigation for https://github.com/facebook/jest/issues/7267 - if (all || storybook || update) { - options = options.concat(['--no-cache', '--no-watchman']); - } + } + // Mitigation for https://github.com/facebook/jest/issues/7267 + if (all || storybook) { + options = options.concat(['--no-cache', '--no-watchman']); + } - if (all) { - log.info('Running all available tests. This will take a while...'); - } else if (storybook || update) { - path = 'legacy/plugins/canvas/.storybook'; + if (all) { + log.info('Running all available tests. This will take a while...'); + } else if (storybook) { + path = 'legacy/plugins/canvas/.storybook'; + log.info('Running Storybook Snapshot tests...'); + } else { + log.info('Running tests. This does not include Storybook Snapshots...'); + } - if (update) { - log.info('Updating Storybook Snapshot tests...'); - options.push('-u'); - } else { - log.info('Running Storybook Snapshot tests...'); - } - } else { - log.info('Running tests. This does not include Storybook Snapshots...'); - } + if (update) { + log.info('Updating any Jest snapshots...'); + options.push('-u'); } runXPackScript('jest', [path].concat(options)); diff --git a/x-pack/legacy/plugins/canvas/scripts/shareable_runtime.js b/x-pack/legacy/plugins/canvas/scripts/shareable_runtime.js new file mode 100644 index 00000000000000..d4d7276ebbc185 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/scripts/shareable_runtime.js @@ -0,0 +1,114 @@ +/* + * 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 fs = require('fs'); +const path = require('path'); +const { pipeline } = require('stream'); +const { promisify } = require('util'); + +const del = require('del'); +const { run } = require('@kbn/dev-utils'); +const execa = require('execa'); + +const asyncPipeline = promisify(pipeline); + +const { + SHAREABLE_RUNTIME_SRC: RUNTIME_SRC, + KIBANA_ROOT, + STATS_OUTPUT, + SHAREABLE_RUNTIME_FILE: RUNTIME_FILE, +} = require('../shareable_runtime/constants'); + +run( + async ({ log, flags }) => { + const options = { + cwd: KIBANA_ROOT, + stdio: ['ignore', 'inherit', 'inherit'], + buffer: false, + }; + + log.info('pre-req: Ensuring Kibana SCSS is built.'); + // Ensure SASS has been built completely before building the runtime. + execa.sync(process.execPath, ['scripts/build_sass'], { + ...options, + }); + + const webpackConfig = path.resolve(RUNTIME_SRC, 'webpack.config.js'); + + const clean = () => { + log.info('Deleting previous build.'); + del.sync([RUNTIME_FILE], { force: true }); + }; + + if (flags.clean) { + clean(); + } + + const env = {}; + + if (!flags.dev) { + env.NODE_ENV = 'production'; + } + + if (flags.run) { + log.info('Starting Webpack Dev Server...'); + execa.sync( + 'yarn', + [ + 'webpack-dev-server', + '--config', + webpackConfig, + '--progress', + '--hide-modules', + '--display-entrypoints', + 'false', + '--content-base', + RUNTIME_SRC, + ], + options + ); + return; + } + + if (flags.stats) { + log.info('Writing Webpack stats...'); + const output = execa( + require.resolve('webpack/bin/webpack'), + ['--config', webpackConfig, '--profile', '--json'], + { + ...options, + env, + stdio: ['ignore', 'pipe', 'inherit'], + } + ); + await asyncPipeline(output.stdout, fs.createWriteStream(STATS_OUTPUT)); + log.success('...output written to', STATS_OUTPUT); + return; + } + + clean(); + log.info('Building Canvas Shareable Workpad Runtime...'); + execa.sync('yarn', ['webpack', '--config', webpackConfig, '--hide-modules', '--progress'], { + ...options, + env, + }); + log.success('...runtime built!'); + }, + { + description: ` + Build script for the Canvas Shareable Workpad Runtime. + `, + flags: { + boolean: ['run', 'clean', 'help', 'stats', 'dev'], + help: ` + --run Run a server with the runtime + --dev Build and/or create stats in development mode. + --stats Output Webpack statistics to a stats.json file. + --clean Delete the existing build + `, + }, + } +); diff --git a/x-pack/legacy/plugins/canvas/server/routes/index.js b/x-pack/legacy/plugins/canvas/server/routes/index.js index 209851ee3f204d..7d0bccb8f5fd64 100644 --- a/x-pack/legacy/plugins/canvas/server/routes/index.js +++ b/x-pack/legacy/plugins/canvas/server/routes/index.js @@ -7,9 +7,11 @@ import { workpad } from './workpad'; import { esFields } from './es_fields'; import { customElements } from './custom_elements'; +import { shareableWorkpads } from './shareables'; export function routes(server) { customElements(server); esFields(server); workpad(server); + shareableWorkpads(server); } diff --git a/x-pack/legacy/plugins/canvas/server/routes/shareables.ts b/x-pack/legacy/plugins/canvas/server/routes/shareables.ts new file mode 100644 index 00000000000000..f48cd78dc80f80 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/server/routes/shareables.ts @@ -0,0 +1,62 @@ +/* + * 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 { Server } from 'hapi'; +import archiver from 'archiver'; + +import { + API_ROUTE_SHAREABLE_RUNTIME, + API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, + API_ROUTE_SHAREABLE_ZIP, +} from '../../common/lib/constants'; + +import { + SHAREABLE_RUNTIME_FILE, + SHAREABLE_RUNTIME_NAME, + SHAREABLE_RUNTIME_SRC, +} from '../../shareable_runtime/constants'; + +export function shareableWorkpads(server: Server) { + // get runtime + server.route({ + method: 'GET', + path: API_ROUTE_SHAREABLE_RUNTIME, + handler: { + file: SHAREABLE_RUNTIME_FILE, + }, + }); + + // download runtime + server.route({ + method: 'GET', + path: API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, + handler(_request, handler) { + // @ts-ignore No type for inert Hapi handler + const file = handler.file(SHAREABLE_RUNTIME_FILE); + file.type('application/octet-stream'); + return file; + }, + }); + + server.route({ + method: 'POST', + path: API_ROUTE_SHAREABLE_ZIP, + handler(request, handler) { + const workpad = request.payload; + + const archive = archiver('zip'); + archive.append(JSON.stringify(workpad), { name: 'workpad.json' }); + archive.file(`${SHAREABLE_RUNTIME_SRC}/template.html`, { name: 'index.html' }); + archive.file(SHAREABLE_RUNTIME_FILE, { name: `${SHAREABLE_RUNTIME_NAME}.js` }); + + const response = handler.response(archive); + response.header('content-type', 'application/zip'); + archive.finalize(); + + return response; + }, + }); +} diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/README.md b/x-pack/legacy/plugins/canvas/shareable_runtime/README.md new file mode 100644 index 00000000000000..8fdeb6ca6258e6 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/README.md @@ -0,0 +1,276 @@ +# Canvas Shareable Workpads + +- [Introduction](#introduction) +- [Quick Start](#quick-start) +- [Using the Runtime](#using-the-runtime) + - [Assumptions](#assumptions) + - [Restrictions](#restrictions) + - [JS](#js) + - [HTML](#html) + - [Options](#options) +- [Testing](#testing) + - [Download a ZIP from Canvas](#download-a-zip-from-canvas) + - [Test the Runtime Directly from Webpack](#test-the-runtime-directly-from-webpack) + - [Run the Canvas Storybook](#run-the-canvas-storybook) + - [Run the Jest Tests](#run-the-jest-tests) + - [Gathering Test Coverage](#gathering-test-coverage) +- [Building](#building) + - [Build Options](#build-options) +- [Development](#development) + - [Prerequisite](#prerequisite) + - [Webpack Dev Server](#webpack-dev-server) + - [Gathering Statistics](#gathering-statistics) +- [Architecture](#architecture) + - [The Build](#the-build) + - [Supported Expressions](#supported-expressions) + - [Expression Interpreter](#expression-interpreter) + - [Build Size](#build-size) + - [The App](#the-app) + - [App State](#app-state) + - [CSS](#css) + +## Introduction + +The Canvas Shareable Runtime is designed to render Shareable Canvas Workpads outside of Kibana in a different website or application. It uses the intermediate, "transient" state of a workpad, which is a JSON-blob state after element expressions are initially evaluated against their data sources, but before the elements are rendered to the screen. This "transient" state, therefore, has no dependency or access to ES/Kibana data, making it lightweight and portable. + +This directory contains the code necessary to build and test this runtime. + +## Quick Start + +- Load a workpad in Canvas. +- Click "Export" -> "Share on a website" -> "download a ZIP file" +- Extract and change to the extracted directory. +- On your local machine: + - Start a web server, like: `python -m SimpleHTTPServer 9001` + - Open a web browser to `http://localhost:9001` +- On a remote webserver: + - Add `kbn_canvas.js` and your Shared Workpad file to your web page: + ``` + + ``` + - Add the HTML snippet to your webpage: + ``` +
+ ``` + - Execute the JS method: + ``` + + ``` + +## Using the Runtime + +### Assumptions + +- The runtime is added to a web page using a standard ` +``` + +### HTML + +The Canvas Shareable Runtime will scan the DOM of a given web page looking for any element with `kbn-canvas-shareable="canvas"` as an attribute. This DOM node will be the host in which the workpad will be rendered. The node will also be sized and manipulated as necessary, but all other attributes, (such as `id`) will remain unaltered. A class name, `kbnCanvas`, will be _added_ to the DOM node. + +> Note: Any content within this DOM node will be replaced. + +Options to configure the runtime are included on the DOM node. The only required attribute is `kbn-canvas-url`, the URL from which the shared workpad can be loaded. + +> Note: the workpad is loaded by `fetch`, therefore the runtime cannot be initialized on the local file system. Relative URLs are allowed. + +Each attribute on the node that is correctly parsed will be removed. For example: + +```html + +
+ + +
+``` + +A sure sign that there was an error, or that an attribute was included that is not recognized, would be any attributes remaining: + +```html + +
+ + +
+``` + +### Options + +The [`api/shareable.tsx`]('./api/shareable') component file contains the base class with available options to configure the Shareable Workpad. Each of these would be prefixed with `kbn-canvas-`: + +```typescript + /** + * The preferred height to scale the Shareable Canvas Workpad. If only `height` is + * specified, `width` will be calculated by the workpad ratio. If both are + * specified, the ratio will be overriden by an absolute size. + */ + height?: number; + + /** + * The preferred width to scale the Shareable Canvas Workpad. If only `width` is + * specified, `height` will be calculated by the workpad ratio. If both are + * specified, the ratio will be overriden by an absolute size. + */ + width?: number; + + /** + * The initial page to display. + */ + page?: number; + + /** + * Should the runtime automatically move through the pages of the workpad? + * @default false + */ + autoplay?: boolean; + + /** + * The interval upon which the pages will advance in time format, (e.g. 2s, 1m) + * @default '5s' + * */ + interval?: string; + + /** + * Should the toolbar be hidden? + * @default false + */ + toolbar?: boolean; +``` + +## Testing + +You can test this functionality in a number of ways. The easiest would be: + +### Download a ZIP from Canvas + +- Load a workpad in Canvas. +- Click "Export" -> "Share on a website" -> "download a ZIP file" +- Extract and change to the extracted directory. +- Start a web server, like: `python -m SimpleHTTPServer 9001` +- Open a web browser to `http://localhost:9001` + +### Test the Runtime Directly from Webpack + +- Load a workpad in Canvas. +- Click "Export" -> "Share on a website" -> "Download Workpad" +- Copy the workpad to `canvas/shareable_runtime/test`. +- Edit `canvas/shareable_runtime/index.html` to include your workpad. +- From `/canvas`, run `node scripts/shareable_runtime --run` +- Open a web browser to `http://localhost:8080` + +### Run the Canvas Storybook + +From `/canvas`: `node scripts/storybook` + +### Run the Jest Tests + +The Jest tests utilize Enzyme to test interactions within the runtime, as well. + +From `/canvas`: `node scripts/jest --path shareable_runtime` + +#### Gathering Test Coverage + +From `/canvas`: `node scripts/jest --path shareable_runtime --coverage` + +## Building + +Run `node scripts/shareable_runtime`. The runtime will be built and stored `shareable_runtime/build`. + +### Build Options + +By default, `scripts/shareable_runtime` will build a production-ready JS library. This takes a bit longer and produces a single file. + +There are a number of options for the build script: + +- `--dev` - allow Webpack to chunk the runtime into several files. This is helpful when developing the runtime itself. +- `--run` - run the Webpack Dev Server to develop and test the runtime. It will use HMR to incorporate changes. +- `--clean` - clean the runtime from the build directory. +- `--stats` - output Webpack statistics for the runtime. + +## Development + +### Prerequisite + +Before testing or running this PR locally, you **must** run `node scripts/runtime` from `/canvas` _after_ `yarn kbn bootstrap` and _before_ starting Kibana. It is only built automatically when Kibana is built to avoid slowing down other development activities. + +### Webpack Dev Server + +To start the `webpack-dev-server` and test a workpad, simply run: + +`/canvas`: `node scripts/shareable_runtime --dev --run` + +A browser window should automatically open. If not, open a browser to [`http://localhost:8080/`](http://localhost:8080). + +The `index.html` file contains a call to the `CanvasShareable` runtime. Currently, you can share by object or by url: + +```html + +... +
+ +``` + +There are three workpads available, in `test/workpads`: + +- `hello.json` - A simple 'Hello, Canvas' workpad. +- `austin.json` - A workpad from an Elastic{ON} talk in Austin, TX. +- `test.json` - A couple of pages with customized CSS animations and charts. + +### Gathering Statistics + +Webpack will output a `stats.json` file for analysis. This allows us to know how large the runtime is, where the largest dependencies are coming from, and how we might prune down its size. Two popular sites are: + +- Official Webpack Analysis tool: http://webpack.github.io/analyse/ +- Webpack Visualizer: https://chrisbateman.github.io/webpack-visualizer/ + +## Architecture + +The Shareable Runtime is an independently-built artifact for use outside of Kibana. It consists of two parts: the Build and the App. + +### The Build + +A custom Webpack build is used to incorporate code from Canvas, Kibana and EUI into a single file for distribution. This code interprets the shared JSON workpad file and renders the pages of elements within the area provided. + +#### Supported Expressions + +Because Shareable Workpads are not connected to any data source in Kibana or otherwise, the runtime simply renders the transient state of the workpad at the time it was shared from within Canvas. So elements that are used to manipulate data, (e.g. filtering controls like `time_filter` or `dropdown_filter`) are not included in the runtime. This lowers the runtime size. Any element that uses an excluded renderer will render nothing in their place. Users are warned within Canvas as they download the Shared Workpad if their workpad contains any of these non-rendered controls. + +> Note: Since the runtime is statically built with the Kibana release, renderers provided by plugins are not supported. Functions that use standard renderers, provided they are not data-manipulating, will still work as expected. + +#### Expression Interpreter + +Kibana and Canvas use an interpreter to register expressions and then eventually evaluate them at run time. Most of the code within the interpreter is not needed for the Shareable Runtime. As a result, a bespoke interpreter is used instead. + +#### Build Size + +At the moment, the resulting library is relatively large, (5.6M). This is due to the bundling of dependencies like EUI. By trading off file size, we're able to keep the library contained without a need to download other external dependencies, (like React). We're working to reduce that size through further tree-shaking or compression. + +### The App + +The App refers to the user interface that is embedded on the consuming web page and displays the workpad. + +#### App State + +To minimize the distribution size, we opted to avoid as many libraries as possible in the UI. So while Canvas uses Redux to maintain state, we opted for a React Context + Hooks-based approach with a custom reducer. This code can be found in `shareable_runtime/context`. + +#### CSS + +All CSS in the runtime UI uses CSS Modules to sandbox and obfuscate class names. In addition, the Webpack build uses `postcss-prefix-selector` to prefix all public class names from Kibana and EUI with `.kbnCanvas`. As a result, all class names should be sandboxed and not interfere with the host page in any way. diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/__mocks__/supported_renderers.js b/x-pack/legacy/plugins/canvas/shareable_runtime/__mocks__/supported_renderers.js new file mode 100644 index 00000000000000..051082107a2e61 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/__mocks__/supported_renderers.js @@ -0,0 +1,38 @@ +/* + * 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 ReactDOM from 'react-dom'; +import React from 'react'; + +const renderers = [ + 'debug', + 'error', + 'image', + 'repeatImage', + 'revealImage', + 'markdown', + 'metric', + 'pie', + 'plot', + 'progress', + 'shape', + 'table', + 'text', +]; + +/** + * Mock all of the render functions to return a `div` containing + * a predictable string. + */ +export const renderFunctions = renderers.map(fn => () => ({ + name: fn, + displayName: fn, + help: fn, + reuseDomNode: true, + render: domNode => { + ReactDOM.render(
{fn} mock
, domNode); + }, +})); diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap b/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap new file mode 100644 index 00000000000000..01a8337d2d313b --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Canvas Shareable Workpad API Placed successfully with default properties 1`] = `"
"`; + +exports[`Canvas Shareable Workpad API Placed successfully with default properties 2`] = ` +"
markdown mock
markdown mock
My Canvas Workpad
" +`; + +exports[`Canvas Shareable Workpad API Placed successfully with height specified 1`] = `"
"`; + +exports[`Canvas Shareable Workpad API Placed successfully with height specified 2`] = ` +"
markdown mock
markdown mock
My Canvas Workpad
" +`; + +exports[`Canvas Shareable Workpad API Placed successfully with page specified 1`] = `"
"`; + +exports[`Canvas Shareable Workpad API Placed successfully with page specified 2`] = ` +"
markdown mock
markdown mock
My Canvas Workpad
" +`; + +exports[`Canvas Shareable Workpad API Placed successfully with width and height specified 1`] = `"
"`; + +exports[`Canvas Shareable Workpad API Placed successfully with width and height specified 2`] = ` +"
markdown mock
markdown mock
My Canvas Workpad
" +`; + +exports[`Canvas Shareable Workpad API Placed successfully with width specified 1`] = `"
"`; + +exports[`Canvas Shareable Workpad API Placed successfully with width specified 2`] = ` +"
markdown mock
markdown mock
My Canvas Workpad
" +`; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx new file mode 100644 index 00000000000000..ea944bd30e9b83 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx @@ -0,0 +1,130 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { sharedWorkpads, tick } from '../../test'; +import { share } from '../shareable'; + +// Mock the renderers within this test. +jest.mock('../../supported_renderers'); + +describe('Canvas Shareable Workpad API', () => { + // Mock the AJAX load of the workpad. + beforeEach(function() { + // @ts-ignore Applying a global in Jest is alright. + global.fetch = jest.fn().mockImplementation(() => { + const p = new Promise((resolve, _reject) => { + resolve({ + ok: true, + json: () => { + return sharedWorkpads.hello; + }, + }); + }); + return p; + }); + }); + + test('Placed successfully with default properties', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const wrapper = mount(
, { + attachTo: container, + }); + + expect(wrapper.html()).toMatchSnapshot(); + share(); + await tick(); + expect(wrapper.html()).toMatchSnapshot(); + }); + + test('Placed successfully with height specified', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const wrapper = mount( +
, + { + attachTo: container, + } + ); + + expect(wrapper.html()).toMatchSnapshot(); + share(); + await tick(); + expect(wrapper.html()).toMatch( + /
/ + ); + expect(wrapper.html()).toMatchSnapshot(); + }); + + test('Placed successfully with width specified', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const wrapper = mount( +
, + { + attachTo: container, + } + ); + + expect(wrapper.html()).toMatchSnapshot(); + share(); + await tick(); + expect(wrapper.html()).toMatch( + /
/ + ); + expect(wrapper.html()).toMatchSnapshot(); + }); + + test('Placed successfully with width and height specified', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const wrapper = mount( +
, + { + attachTo: container, + } + ); + + expect(wrapper.html()).toMatchSnapshot(); + share(); + await tick(); + expect(wrapper.html()).toMatch( + /
/ + ); + expect(wrapper.html()).toMatchSnapshot(); + }); + + test('Placed successfully with page specified', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const wrapper = mount( +
, + { + attachTo: container, + } + ); + + expect(wrapper.html()).toMatchSnapshot(); + share(); + await tick(); + expect(wrapper.html()).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts b/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts new file mode 100644 index 00000000000000..b05379df6b0b1f --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts @@ -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. + */ + +import 'whatwg-fetch'; +import 'babel-polyfill'; + +export * from './shareable'; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/api/shareable.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/api/shareable.tsx new file mode 100644 index 00000000000000..11289d3c379702 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/shareable.tsx @@ -0,0 +1,157 @@ +/* + * 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 { render } from 'react-dom'; +import { App } from '../components/app'; +import { CanvasRenderedWorkpad } from '../types'; + +export interface Options { + /** + * The preferred height to scale the shared workpad. If only `height` is + * specified, `width` will be calculated by the workpad ratio. If both are + * specified, the ratio will be overriden by an absolute size. + * @default The height provided by the workpad. + */ + height?: number; + + /** + * The preferred width to scale the shared workpad. If only `width` is + * specified, `height` will be calculated by the workpad ratio. If both are + * specified, the ratio will be overriden by an absolute size. + * @default The width provided by the workpad. + */ + width?: number; + + /** + * The initial page to display. + * @default The page provided by the workpad. + */ + page?: number; + + /** + * Should the runtime automatically move through the pages of the workpad? + * @default false + */ + autoplay?: boolean; + + /** + * The interval upon which the pages will advance in time format, (e.g. 2s, 1m) + * @default '5s' + * */ + interval?: string; + + /** + * Should the toolbar be hidden? + * @default false + */ + toolbar?: boolean; +} + +// All data attributes start with this prefix. +const PREFIX = 'kbn-canvas'; + +// The identifying data attribute for all shareable workpads. +const SHAREABLE = `${PREFIX}-shareable`; + +// Valid option attributes, preceded by `PREFIX` in markup. +const VALID_ATTRIBUTES = ['url', 'page', 'height', 'width', 'autoplay', 'interval', 'toolbar']; + +// Collect and then remove valid data attributes. +const getAttributes = (element: Element, attributes: string[]) => { + const result: { [key: string]: string } = {}; + attributes.forEach(attribute => { + const key = `${PREFIX}-${attribute}`; + const value = element.getAttribute(key); + + if (value) { + result[attribute] = value; + element.removeAttribute(key); + } + }); + + return result; +}; + +const getWorkpad = async (url: string): Promise => { + const workpadResponse = await fetch(url); + + if (workpadResponse.ok) { + return await workpadResponse.json(); + } + + return null; +}; + +const updateArea = async (area: Element) => { + const { + url, + page: pageAttr, + height: heightAttr, + width: widthAttr, + autoplay, + interval, + toolbar, + } = getAttributes(area, VALID_ATTRIBUTES); + + if (url) { + const workpad = await getWorkpad(url); + + if (workpad) { + const page = pageAttr ? parseInt(pageAttr, 10) : null; + let height = heightAttr ? parseInt(heightAttr, 10) : null; + let width = widthAttr ? parseInt(widthAttr, 10) : null; + + if (height && !width) { + // If we have a height but no width, the width should honor the workpad ratio. + width = Math.round(workpad.width * (height / workpad.height)); + } else if (width && !height) { + // If we have a width but no height, the height should honor the workpad ratio. + height = Math.round(workpad.height * (width / workpad.width)); + } + + const stage = { + height: height || workpad.height, + width: width || workpad.width, + page: page !== null ? page : workpad.page, + }; + + const settings = { + autoplay: { + isEnabled: !!autoplay, + interval: interval || '5s', + }, + toolbar: { + isAutohide: !!toolbar, + }, + }; + + area.classList.add('kbnCanvas'); + area.removeAttribute(SHAREABLE); + + render( + [ + , + , + ], + area + ); + } + } +}; + +/** + * This function processes all elements that have a valid share data attribute and + * attempts to place the designated workpad within them. + */ +export const share = () => { + const shareAreas = document.querySelectorAll(`[${SHAREABLE}]`); + const validAreas = Array.from(shareAreas).filter( + area => area.getAttribute(SHAREABLE) === 'canvas' + ); + + validAreas.forEach(updateArea); +}; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot new file mode 100644 index 00000000000000..1b351e9ac0f6ab --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot @@ -0,0 +1,3298 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots shareables/Canvas component 1`] = ` +
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ Elastic{ON} - Austin from Clint Andrew Hall with a title that just goes and goes and goes +
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` +
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ Elastic{ON} - Austin from Clint Andrew Hall with a title that just goes and goes and goes +
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Storyshots shareables/Canvas contextual: hello 1`] = ` +
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ My Canvas Workpad +
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.examples.storyshot new file mode 100644 index 00000000000000..fefe6c6b428586 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.examples.storyshot @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots shareables/Page component 1`] = ` +
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+`; + +exports[`Storyshots shareables/Page contextual: austin 1`] = ` +
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+`; + +exports[`Storyshots shareables/Page contextual: hello 1`] = ` +
+
+ RenderedElement +
+
+`; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.examples.storyshot new file mode 100644 index 00000000000000..0131b61b6b93c4 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.examples.storyshot @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots shareables/RenderedElement component 1`] = ` +
+`; + +exports[`Storyshots shareables/RenderedElement contextual: austin 1`] = ` +
+ RenderedElement +
+`; + +exports[`Storyshots shareables/RenderedElement contextual: hello 1`] = ` +
+ RenderedElement +
+`; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/canvas.examples.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/canvas.examples.tsx new file mode 100644 index 00000000000000..ff764e6dc2330c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/canvas.examples.tsx @@ -0,0 +1,43 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ExampleContext } from '../../test/context_example'; + +import { Canvas, CanvasComponent } from '../canvas'; +import { sharedWorkpads } from '../../test'; +import { initialCanvasShareableState } from '../../context/state'; +const { austin } = sharedWorkpads; + +storiesOf('shareables/Canvas', module) + .add('contextual: austin', () => ( + + + + )) + .add('contextual: hello', () => ( + + + + )) + .add('component', () => ( + + + + )); diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/page.examples.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/page.examples.tsx new file mode 100644 index 00000000000000..ece2d1a8542259 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/page.examples.tsx @@ -0,0 +1,30 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import React from 'react'; +import { ExampleContext } from '../../test/context_example'; + +import { Page, PageComponent } from '../page'; +import { sharedWorkpads } from '../../test'; +const { austin } = sharedWorkpads; + +storiesOf('shareables/Page', module) + .add('contextual: austin', () => ( + + + + )) + .add('contextual: hello', () => ( + + + + )) + .add('component', () => ( + + + + )); diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx new file mode 100644 index 00000000000000..7b5a5080ae790b --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx @@ -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. + */ +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { ExampleContext } from '../../test/context_example'; + +// @ts-ignore +import { image } from '../../../canvas_plugin_src/renderers/image'; +import { sharedWorkpads } from '../../test'; +import { RenderedElement, RenderedElementComponent } from '../rendered_element'; + +const { austin, hello } = sharedWorkpads; + +storiesOf('shareables/RenderedElement', module) + .add('contextual: hello', () => ( + + + + )) + .add('contextual: austin', () => ( + + + + )) + .add('component', () => ( + + + + )); diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/__snapshots__/app.test.tsx.snap b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/__snapshots__/app.test.tsx.snap new file mode 100644 index 00000000000000..c287fe96c05848 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/__snapshots__/app.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` App renders properly 1`] = ` +"
markdown mock
markdown mock
My Canvas Workpad
" +`; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx new file mode 100644 index 00000000000000..db0aac34336eab --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + One test relies on react-dom at a version of 16.9... it can be enabled + once renovate completes the upgrade. Relevant code has been commented out + in the meantime. +*/ + +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +// import { act } from 'react-dom/test-utils'; +import { App } from '../app'; +import { sharedWorkpads, WorkpadNames, tick } from '../../test'; +import { + getScrubber as scrubber, + getScrubberSlideContainer as scrubberContainer, + getPageControlsCenter as center, + getSettingsTrigger as trigger, + getContextMenuItems as menuItems, + // getAutoplayTextField as autoplayText, + // getAutoplayCheckbox as autoplayCheck, + // getAutoplaySubmit as autoplaySubmit, + getToolbarCheckbox as toolbarCheck, + getCanvas as canvas, + getFooter as footer, + getPageControlsPrevious as previous, + getPageControlsNext as next, +} from '../../test/selectors'; + +// Mock the renderers +jest.mock('../../supported_renderers'); + +// Mock the EuiPortal - `insertAdjacentElement is not supported in +// `jsdom` 12. We're just going to render a `div` with the children +// so the `enzyme` tests will be accurate. +jest.mock('@elastic/eui/lib/components/portal/portal', () => { + // Local constants are not supported in Jest mocks-- they must be + // imported within the mock. + // eslint-disable-next-line no-shadow + const React = require.requireActual('react'); + return { + EuiPortal: (props: any) =>
{props.children}
, + }; +}); + +const getWrapper: (name?: WorkpadNames) => ReactWrapper = (name = 'hello') => { + const workpad = sharedWorkpads[name]; + const { height, width } = workpad; + const stage = { + height, + width, + page: 0, + }; + + return mount(); +}; + +describe('', () => { + test('App renders properly', () => { + expect(getWrapper().html()).toMatchSnapshot(); + }); + + test('App can be navigated', () => { + const wrapper = getWrapper('austin'); + next(wrapper).simulate('click'); + expect(center(wrapper).text()).toEqual('Page 2 of 28'); + previous(wrapper).simulate('click'); + }); + + test('scrubber opens and closes', () => { + const wrapper = getWrapper('austin'); + expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(false); + center(wrapper).simulate('click'); + expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(true); + }); + + test('can open scrubber and set page', () => { + const wrapper = getWrapper('austin'); + expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(false); + center(wrapper).simulate('click'); + expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(true); + + // Click a page preview + scrubberContainer(wrapper) + .childAt(3) // Get the fourth page preview + .childAt(0) // Get the click-responding element + .simulate('click'); + expect(center(wrapper).text()).toEqual('Page 4 of 28'); + + // Focus and key press a page preview + scrubberContainer(wrapper) + .childAt(5) // Get the sixth page preview + .childAt(0) // Get the click-responding element + .simulate('focus') + .simulate('keyPress'); + expect(center(wrapper).text()).toEqual('Page 6 of 28'); + }); + + test('autohide footer functions on mouseEnter + Leave', async () => { + const wrapper = getWrapper(); + trigger(wrapper).simulate('click'); + await tick(20); + menuItems(wrapper) + .at(1) + .simulate('click'); + await tick(20); + wrapper.update(); + expect(footer(wrapper).prop('isHidden')).toEqual(false); + expect(footer(wrapper).prop('isAutohide')).toEqual(false); + toolbarCheck(wrapper).simulate('change'); + expect(footer(wrapper).prop('isAutohide')).toEqual(true); + canvas(wrapper).simulate('mouseEnter'); + expect(footer(wrapper).prop('isHidden')).toEqual(false); + canvas(wrapper).simulate('mouseLeave'); + expect(footer(wrapper).prop('isHidden')).toEqual(true); + }); + + test('scrubber hides if open when autohide is activated', async () => { + const wrapper = getWrapper('austin'); + center(wrapper).simulate('click'); + expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(true); + + // Open the menu and activate toolbar hiding. + trigger(wrapper).simulate('click'); + await tick(20); + menuItems(wrapper) + .at(1) + .simulate('click'); + await tick(20); + wrapper.update(); + toolbarCheck(wrapper).simulate('change'); + await tick(20); + + // Simulate the mouse leaving the container + canvas(wrapper).simulate('mouseLeave'); + expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(false); + }); + + /* + test('autoplay starts when triggered', async () => { + const wrapper = getWrapper('austin'); + trigger(wrapper).simulate('click'); + await tick(20); + menuItems(wrapper) + .at(0) + .simulate('click'); + await tick(20); + wrapper.update(); + autoplayText(wrapper).simulate('change', { target: { value: '1s' } }); + autoplaySubmit(wrapper).simulate('submit'); + autoplayCheck(wrapper).simulate('change'); + expect(center(wrapper).text()).toEqual('Page 1 of 28'); + + await act(async () => { + await tick(1500); + }); + + wrapper.update(); + expect(center(wrapper).text()).not.toEqual('Page 1 of 28'); + }); +*/ +}); diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/canvas.test.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/canvas.test.tsx new file mode 100644 index 00000000000000..deb524ed56bc59 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/canvas.test.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. + */ + +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { JestContext } from '../../test/context_jest'; +import { getScrubber as scrubber, getPageControlsCenter as center } from '../../test/selectors'; +import { Canvas } from '../canvas'; + +jest.mock('../../supported_renderers'); + +describe('', () => { + test('null workpad renders nothing', () => { + expect(mount().isEmptyRender()); + }); + + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + + + + ); + }); + + test('scrubber opens and closes', () => { + expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(false); + center(wrapper).simulate('click'); + expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(true); + }); +}); diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/page.test.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/page.test.tsx new file mode 100644 index 00000000000000..7e3f5b1cd35554 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/page.test.tsx @@ -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. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { Page } from '../page'; + +describe('', () => { + test('null workpad renders nothing', () => { + expect(mount().isEmptyRender()); + }); +}); diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/app.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/app.tsx new file mode 100644 index 00000000000000..1779ec4846cb78 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/app.tsx @@ -0,0 +1,49 @@ +/* + * 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 { CanvasRenderedWorkpad, CanvasShareableState, Stage } from '../types'; +import { RendererSpec } from '../../types'; +import { initialCanvasShareableState, CanvasShareableStateProvider } from '../context'; +import { Canvas } from './canvas'; +import { renderFunctions } from '../supported_renderers'; + +interface Props { + /** + * An object describing the state of the workpad container. + */ + stage: Stage; + + /** + * The workpad being rendered within the shareable area. + */ + workpad: CanvasRenderedWorkpad; +} + +/** + * The overall Canvas Shareable Workpad app; the highest-layer component. + */ +export const App: FC = ({ workpad, stage }) => { + const renderers: { [key: string]: RendererSpec } = {}; + + renderFunctions.forEach(fn => { + const func = fn(); + renderers[func.name] = func; + }); + + const initialState: CanvasShareableState = { + ...initialCanvasShareableState, + stage, + renderers, + workpad, + }; + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.module.scss b/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.module.scss new file mode 100644 index 00000000000000..88619c150c1f5c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.module.scss @@ -0,0 +1,22 @@ +:global .kbnCanvas :local .root { + position: relative; + overflow: hidden; + transition: height 1s; +} + +.container { + composes: canvas from global; + composes: canvasContainer from global; +} + +:global .kbnCanvas :local .container { + align-items: center; + display: flex; + justify-content: center; + pointer-events: none; +} + +:global .kbnCanvas :local .page { + position: absolute; + transform-origin: center center; +} diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.tsx new file mode 100644 index 00000000000000..7d7067da09ee63 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.tsx @@ -0,0 +1,153 @@ +/* + * 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 { useCanvasShareableState, setPageAction, setScrubberVisibleAction } from '../context'; +import { Page } from './page'; +import { Footer, FOOTER_HEIGHT } from './footer'; +import { getTimeInterval } from '../../public/lib/time_interval'; + +import css from './canvas.module.scss'; +import { CanvasRenderedWorkpad, Stage, Settings, Refs } from '../types'; + +let timeout: number = 0; + +export type onSetPageFn = (page: number) => void; +export type onSetScrubberVisibleFn = (visible: boolean) => void; +type Workpad = Pick; + +interface Props { + /** + * The handler to invoke when a page is selected. + */ + onSetPage: onSetPageFn; + + /** + * The handler to invoke when the Scrubber is shown or hidden. + */ + onSetScrubberVisible: onSetScrubberVisibleFn; + + /** + * The `react` `ref` objects pertaining to the stage. + */ + refs: Pick; + + /** + * An object describing the current settings of the Shareable. + */ + settings: Settings; + + /** + * An object describing the state of the workpad container. + */ + stage: Stage; + + /** + * The workpad being rendered within the Shareable area. + */ + workpad: Workpad; +} + +/** + * The "canvas" for a workpad, which composes the toolbar and other components. + */ +export const CanvasComponent = ({ + onSetPage, + onSetScrubberVisible, + refs, + settings, + stage, + workpad, +}: Props) => { + const { toolbar, autoplay } = settings; + const { height: stageHeight, width: stageWidth, page } = stage; + const { height: workpadHeight, width: workpadWidth } = workpad; + const ratio = Math.max(workpadWidth / stageWidth, workpadHeight / stageHeight); + const transform = `scale3d(${stageHeight / (stageHeight * ratio)}, ${stageWidth / + (stageWidth * ratio)}, 1)`; + + const pageStyle = { + height: workpadHeight, + transform, + width: workpadWidth, + }; + + if (autoplay.isEnabled && autoplay.interval) { + // We need to clear the timeout every time, even if it doesn't need to be or + // it's null. Since one could select a different page from the scrubber at + // any point, or change the interval, we need to make sure the interval is + // killed on React re-render-- otherwise the pages will start bouncing around + // as timeouts are accumulated. + clearTimeout(timeout); + + timeout = setTimeout( + () => onSetPage(page >= workpad.pages.length - 1 ? 0 : page + 1), + getTimeInterval(autoplay.interval) + ); + } + + const [toolbarHidden, setToolbarHidden] = useState(toolbar.isAutohide); + const rootHeight = stageHeight + (toolbar.isAutohide ? 0 : FOOTER_HEIGHT); + + const hideToolbar = (hidden: boolean) => { + if (toolbar.isAutohide) { + if (hidden) { + // Hide the scrubber if we hide the toolbar. + onSetScrubberVisible(false); + } + setToolbarHidden(hidden); + } + }; + + return ( +
hideToolbar(false)} + onMouseLeave={() => hideToolbar(true)} + ref={refs.stage} + > +
+
+ +
+
+
+
+ ); +}; + +/** + * A store-connected container for the `Canvas` component. + */ +export const Canvas = () => { + const [{ workpad, stage, settings, refs }, dispatch] = useCanvasShareableState(); + + if (!workpad) { + return null; + } + + const onSetPage: onSetPageFn = (page: number) => { + dispatch(setPageAction(page)); + }; + + const onSetScrubberVisible: onSetScrubberVisibleFn = (visible: boolean) => { + dispatch(setScrubberVisibleAction(visible)); + }; + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.components.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.components.examples.storyshot new file mode 100644 index 00000000000000..6d783a26d84242 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.components.examples.storyshot @@ -0,0 +1,140 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots shareables/Footer/components PageControls 1`] = ` +
+
+ +
+
+ +
+
+ +
+
+`; + +exports[`Storyshots shareables/Footer/components Title 1`] = ` +
+
+
+ +
+
+
+
+
+ This is a test title. +
+
+
+
+
+
+`; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot new file mode 100644 index 00000000000000..bad95ca4fb5b50 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot @@ -0,0 +1,1710 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots shareables/Footer contextual: austin 1`] = ` +
+
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ Elastic{ON} - Austin from Clint Andrew Hall with a title that just goes and goes and goes +
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+`; + +exports[`Storyshots shareables/Footer contextual: hello 1`] = ` +
+
+
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ My Canvas Workpad +
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+`; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.examples.storyshot new file mode 100644 index 00000000000000..bb807062a6baac --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.examples.storyshot @@ -0,0 +1,302 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots shareables/Footer/PageControls component 1`] = ` +
+
+
+ +
+
+ +
+
+ +
+
+
+`; + +exports[`Storyshots shareables/Footer/PageControls contextual: austin 1`] = ` +
+
+
+ +
+
+ +
+
+ +
+
+
+`; + +exports[`Storyshots shareables/Footer/PageControls contextual: hello 1`] = ` +
+
+
+ +
+
+ +
+
+ +
+
+
+`; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.examples.storyshot new file mode 100644 index 00000000000000..cc27e42e3fe651 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.examples.storyshot @@ -0,0 +1,1360 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots shareables/Footer/Scrubber component 1`] = ` +
+
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+`; + +exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+ RenderedElement + RenderedElement + RenderedElement +
+
+
+
+
+
+`; + +exports[`Storyshots shareables/Footer/Scrubber contextual: hello 1`] = ` +
+
+
+
+
+
+ RenderedElement +
+
+
+
+
+
+`; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot new file mode 100644 index 00000000000000..6bb60b8e574a23 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot @@ -0,0 +1,193 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots shareables/Footer/Title component 1`] = ` +
+
+
+ + + +
+
+
+
+
+ This is a test title. +
+
+
+
+
+
+`; + +exports[`Storyshots shareables/Footer/Title contextual: austin 1`] = ` +
+
+
+ + + +
+
+
+
+
+ Elastic{ON} - Austin from Clint Andrew Hall with a title that just goes and goes and goes +
+
+
+
+
+
+`; + +exports[`Storyshots shareables/Footer/Title contextual: hello 1`] = ` +
+
+
+ + + +
+
+
+
+
+ My Canvas Workpad +
+
+
+
+
+
+`; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.examples.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.examples.tsx new file mode 100644 index 00000000000000..0b1d1f9019ac65 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.examples.tsx @@ -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 { storiesOf } from '@storybook/react'; +import React from 'react'; +import { ExampleContext } from '../../../test/context_example'; + +import { Footer } from '../footer'; + +storiesOf('shareables/Footer', module) + .add('contextual: hello', () => ( + +