[feature][Canvas] Share Workpads in other Websites (#46278)

* [Canvas] Embedding Workpads in other Websites (#42545)

* Testing for Workpad Snapshots

* Rename Snapshots to Shareables; update documentation. (#46513)

* [canvas][shareables] Add Localization + Tweaks (#46632)

* Add localization + tweak naming

* Fix duplicate key

* Update storyshots

* [shareables] Unsupported Renderer Warning (#46862)

* [shareables] Unsupported Renderer Warning

* Update snapshots; add test

* Addressing Feedback

* [canvas][shareables] Simplify and complete testing (#47328)

* Simplify

* Updates

* Finishing up

* A few tweaks

* Fix eslint errors; how would those happen??

* Fix CI build of runtime; assorted visual tweaks

* Update x-pack/legacy/plugins/canvas/shareable_runtime/test/index.ts

Co-Authored-By: Spencer <email@spalger.com>

* Addressing feedback

* Remove null-loader from root package

* re-add null-loader until mitigation is found

* [perf] Fix unsupported renderers performance issue (#47769)

* [perf] Fix perf issue with unsupported renderers

* Fixing snapshots

* Addressing review feedback (#47775)

* Addressing feedback

* Addressing feedback (#47883)

* Branding Changes (#47913)

* Branding Changes

* Update snapshots
This commit is contained in:
Clint Andrew Hall 2019-10-11 12:15:41 -05:00 committed by GitHub
parent 0a85478d41
commit 27cbdf5f50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
121 changed files with 46883 additions and 188 deletions

View file

@ -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:

View file

@ -625,6 +625,14 @@
'@types/zen-observable',
],
},
{
groupSlug: 'archiver',
groupName: 'archiver related packages',
packageNames: [
'archiver',
'@types/archiver',
],
},
{
groupSlug: 'base64-js',
groupName: 'base64-js related packages',

View file

@ -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);
}

View file

@ -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
);
})
);
}
},
};

View file

@ -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;
},
}
);

View file

@ -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
}

View file

@ -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: '<rootDir>/../target/kibana-coverage/jest',
coverageReporters: [
'html',
],
coverageReporters: ['html'],
setupFiles: [
`${kibanaDirectory}/src/dev/jest/setup/babel_polyfill.js`,
`<rootDir>/dev-tools/jest/setup/polyfills.js`,

View file

@ -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

View file

@ -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

View file

@ -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`;

View file

@ -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,
});

View file

@ -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',

View file

@ -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';

View file

@ -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: () =>

View file

@ -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
<form
onSubmit={ev => {
ev.preventDefault();
onSubmit(refreshInterval);
if (!isInvalid && refreshInterval) {
onSubmit(refreshInterval);
}
}}
>
<EuiFlexGroup gutterSize={gutterSize}>

View file

@ -1,85 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/Export/WorkpadExport disabled 1`] = `
<div
className="euiPopover euiPopover--anchorDownCenter"
container={null}
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div>
<div
className="euiPopover__anchor"
className="euiPopover euiPopover--anchorDownCenter"
container={null}
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
<div
className="euiPopover__anchor"
>
<button
aria-label="Share this workpad"
className="euiButtonIcon euiButtonIcon--primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</span>
<button
aria-label="Share this workpad"
className="euiButtonIcon euiButtonIcon--primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</span>
</div>
</div>
</div>
`;
exports[`Storyshots components/Export/WorkpadExport enabled 1`] = `
<div
className="euiPopover euiPopover--anchorDownCenter"
container={null}
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div>
<div
className="euiPopover__anchor"
className="euiPopover euiPopover--anchorDownCenter"
container={null}
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
<div
className="euiPopover__anchor"
>
<button
aria-label="Share this workpad"
className="euiButtonIcon euiButtonIcon--primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
<span
className="euiToolTipAnchor"
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</span>
<button
aria-label="Share this workpad"
className="euiButtonIcon euiButtonIcon--primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</span>
</div>
</div>
</div>
`;

View file

@ -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', () => (
<ShareWebsiteFlyout
onCopy={action('onCopy')}
onDownload={action('onDownload')}
onClose={action('onClose')}
/>
))
.add('unsupported renderers', () => (
<ShareWebsiteFlyout
onCopy={action('onCopy')}
onDownload={action('onDownload')}
onClose={action('onClose')}
unsupportedRenderers={['rendererOne', 'rendererTwo']}
/>
));

View file

@ -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<ComponentProps, Pick<Props, 'onClose'>>(
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);

View file

@ -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 }) => (
<EuiText size="s">
<p>{strings.getStepDescription()}</p>
<EuiSpacer size="s" />
<EuiButton
onClick={() => {
onDownload('shareRuntime');
}}
size="s"
>
{strings.getDownloadLabel()}
</EuiButton>
</EuiText>
);

View file

@ -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: <WorkpadStep {...{ onDownload }} />,
},
{
title: strings.getRuntimeStepTitle(),
children: <RuntimeStep {...{ onDownload }} />,
},
{
title: strings.getSnippentsStepTitle(),
children: <SnippetsStep {...{ onCopy }} />,
},
];
export const ShareWebsiteFlyout: FC<Props> = ({
onCopy,
onDownload,
onClose,
unsupportedRenderers,
}) => {
const link = (
<EuiLink
style={{ textDecoration: 'underline' }}
onClick={() => {
onDownload('shareZip');
}}
>
<FormattedMessage
id="xpack.canvas.shareWebsiteFlyout.zipDownloadLinkLabel"
defaultMessage="download an example {ZIP} file"
values={{ ZIP }}
/>
</EuiLink>
);
const title = (
<div>
<FormattedMessage
id="xpack.canvas.shareWebsiteFlyout.flyoutCalloutDescription"
defaultMessage="To try sharing, you can {link} containing this workpad, the {CANVAS} Shareable Workpad runtime, and a sample {HTML} file."
values={{
CANVAS,
HTML,
link,
}}
/>
</div>
);
let warningText = null;
if (unsupportedRenderers && unsupportedRenderers.length > 0) {
const warning = [
<EuiText size="s" key="text">
<span>{strings.getUnsupportedRendererWarning()}</span>
{unsupportedRenderers.map((fn, index) => [
<EuiCode key={`item-${index}`}>{fn}</EuiCode>,
index < unsupportedRenderers.length - 1 ? ', ' : '',
])}
</EuiText>,
<EuiSpacer size="xs" key="spacer" />,
];
warningText = [
<EuiCallOut title={warning} color="warning" size="s" iconType="alert" key="callout" />,
<EuiSpacer key="spacer" />,
];
}
return (
<EuiFlyout onClose={() => onClose('share')} maxWidth>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<EuiFlexGroup alignItems="center">
<h2 id="flyoutTitle">
<EuiFlexItem grow={false}>{strings.getTitle()}</EuiFlexItem>
</h2>
<EuiFlexItem grow={false}>
<EuiBetaBadge label="Beta" color="accent" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText size="s">
<p>{strings.getStepsDescription()}</p>
</EuiText>
<EuiSpacer />
<EuiCallOut size="s" title={title} iconType="iInCircle" />
<EuiSpacer />
{warningText}
<EuiSteps steps={steps(onDownload, onCopy)} />
</EuiFlyoutBody>
</EuiFlyout>
);
};

View file

@ -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 = `<!-- ${strings.getIncludeRuntimeLabel()} -->
<script src="kbn_canvas.js"></script>
<!-- ${strings.getPlaceholderLabel()} -->
<div kbn-canvas-shareable="canvas" kbn-canvas-url="workpad.json" />
<!-- ${strings.getCallRuntimeLabel()} -->
<script type="text/javascript">
KbnCanvas.share();
</script>`;
export const SnippetsStep: FC<{ onCopy: OnCopyFn }> = ({ onCopy }) => (
<div>
<EuiText size="s">
<p>{strings.getSnippetsStepDescription()}</p>
</EuiText>
<EuiSpacer size="s" />
<Clipboard content={HTML} onCopy={onCopy}>
<EuiCodeBlock
className="canvasWorkpadExport__reportingConfig"
paddingSize="s"
fontSize="s"
language="html"
>
{HTML}
</EuiCodeBlock>
</Clipboard>
<EuiSpacer />
<EuiText>
<h4>{strings.getParametersTitle()}</h4>
<p>{strings.getParametersDescription()}</p>
</EuiText>
<EuiHorizontalRule />
<EuiDescriptionList>
<EuiDescriptionListTitle>
<EuiCode>kbn-canvas-shareable="canvas"</EuiCode> ({strings.getRequiredLabel()})
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{strings.getShareableParameterDescription()}
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>
<EuiCode>kbn-canvas-url</EuiCode> ({strings.getRequiredLabel()})
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{strings.getUrlParameterDescription()}
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>
<EuiCode>kbn-canvas-height</EuiCode>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{strings.getHeightParameterDescription()}
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>
<EuiCode>kbn-canvas-width</EuiCode>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{strings.getWidthParameterDescription()}
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>
<EuiCode>kbn-canvas-page</EuiCode>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{strings.getPageParameterDescription()}
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>
<EuiCode>kbn-canvas-autoplay</EuiCode>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{strings.getAutoplayParameterDescription()}
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>
<EuiCode>kbn-canvas-interval</EuiCode>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{strings.getIntervalParameterDescription()}
</EuiDescriptionListDescription>
<EuiDescriptionListTitle>
<EuiCode>kbn-canvas-toolbar</EuiCode>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{strings.getToolbarParameterDescription()}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</div>
);

View file

@ -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 }) => (
<EuiText size="s">
<p>{strings.getStepDescription()}</p>
<EuiSpacer size="s" />
<EuiButton
onClick={() => {
onDownload('share');
}}
size="s"
>
{strings.getDownloadLabel()}
</EuiButton>
</EuiText>
);

View file

@ -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<ComponentProps, {}>(
});
case 'json':
downloadWorkpad(workpad.id);
break;
return;
default:
throw new Error(strings.getUnknownExportErrorMessage(type));
}

View file

@ -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<Props> = ({
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<Props> = ({
),
},
},
{
name: strings.getShareWebsiteTitle(),
icon: <EuiIcon type="globe" size="m" />,
onClick: () => {
setShowFlyout(true);
closePopover();
},
},
],
});
@ -120,17 +137,25 @@ export const WorkpadExport: FunctionComponent<Props> = ({
/>
);
const flyout = showFlyout ? <ShareWebsiteFlyout onClose={onClose} /> : null;
return (
<Popover
button={exportControl}
panelPaddingSize="none"
tooltip={strings.getShareWorkpadMessage()}
tooltipPosition="bottom"
>
{({ closePopover }: { closePopover: ClosePopoverFn }) => (
<EuiContextMenu initialPanelId={0} panels={flattenPanelTree(getPanelTree(closePopover))} />
)}
</Popover>
<div>
<Popover
button={exportControl}
panelPaddingSize="none"
tooltip={strings.getShareWorkpadMessage()}
tooltipPosition="bottom"
>
{({ closePopover }: { closePopover: ClosePopoverFn }) => (
<EuiContextMenu
initialPanelId={0}
panels={flattenPanelTree(getPanelTree(closePopover))}
/>
)}
</Popover>
{flyout}
</div>
);
};

View file

@ -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,

View file

@ -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) });

View file

@ -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() });
}
};

View file

@ -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;
}

View file

@ -43,26 +43,24 @@ run(
'--coverageDirectory', // Output to canvas/coverage
'legacy/plugins/canvas/coverage',
];
}
// 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) {
path = 'legacy/plugins/canvas/.storybook';
log.info('Running Storybook Snapshot tests...');
} else {
// Mitigation for https://github.com/facebook/jest/issues/7267
if (all || storybook || update) {
options = options.concat(['--no-cache', '--no-watchman']);
}
log.info('Running tests. This does not include Storybook Snapshots...');
}
if (all) {
log.info('Running all available tests. This will take a while...');
} else if (storybook || update) {
path = 'legacy/plugins/canvas/.storybook';
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));

View file

@ -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
`,
},
}
);

View file

@ -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);
}

View file

@ -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;
},
});
}

View file

@ -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:
```
<script src="kbn_canvas.js"></script>
```
- Add the HTML snippet to your webpage:
```
<div kbn-canvas-shareable="canvas" kbn-canvas-url="[WORKPAD URL]" />
```
- Execute the JS method:
```
<script type="text/javascript">
KbnCanvas.share();
</script>
```
## Using the Runtime
### Assumptions
- The runtime is added to a web page using a standard `<script>` tag.
- A Shared Workpad JSON file (see: [Testing](#Testing)) is available via some known URL.
### Restrictions
Not all elements from a workpad may render in the runtime. See [Supported Expressions](#supported-expressions) for more details.
### JS
The runtime is a global library with `KbnCanvas` as the namespace. When executed, the `share` method will interpret any and all nodes that match the API. This function can be called from anywhere, in a script block at the bottom of the page, or after any other initialization.
```html
<script type="text/javascript">
KbnCanvas.share();
</script>
```
### 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
<!-- Markup added to the source file. -->
<div kbn-canvas-shareable="canvas" kbn-canvas-height="400" kbn-canvas-url="workpad.json" />
<!-- Markup in the DOM after runtime processes it. -->
<div class="kbnCanvas" />
```
A sure sign that there was an error, or that an attribute was included that is not recognized, would be any attributes remaining:
```html
<!-- Markup added to the source file. -->
<div kbn-canvas-shareable="canvas" kbn-canvas-hieght="400" kbn-canvas-url="workpad.json" />
<!-- Markup in the DOM after runtime processes it. -->
<div class="kbnCanvas" kbn-canvas-hieght="400" />
```
### 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
<script src="kbn_canvas.js"></script>
...
<div kbn-canvas-shareable="canvas" kbn-canvas-height="400" kbn-canvas-url="workpad.json"></div>
<script type="text/javascript">
KbnCanvas.share();
</script>
```
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.

View file

@ -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(<div>{fn} mock</div>, domNode);
},
}));

File diff suppressed because one or more lines are too long

View file

@ -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(<div kbn-canvas-shareable="canvas" kbn-canvas-url="workpad.json"></div>, {
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(
<div
kbn-canvas-shareable="canvas"
kbn-canvas-height="350"
kbn-canvas-url="workpad.json"
></div>,
{
attachTo: container,
}
);
expect(wrapper.html()).toMatchSnapshot();
share();
await tick();
expect(wrapper.html()).toMatch(
/<div class=\"container\" style="height: 350px; width: 525px;\">/
);
expect(wrapper.html()).toMatchSnapshot();
});
test('Placed successfully with width specified', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const wrapper = mount(
<div
kbn-canvas-shareable="canvas"
kbn-canvas-width="400"
kbn-canvas-url="workpad.json"
></div>,
{
attachTo: container,
}
);
expect(wrapper.html()).toMatchSnapshot();
share();
await tick();
expect(wrapper.html()).toMatch(
/<div class=\"container\" style="height: 267px; width: 400px;\">/
);
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(
<div
kbn-canvas-shareable="canvas"
kbn-canvas-width="350"
kbn-canvas-height="350"
kbn-canvas-url="workpad.json"
></div>,
{
attachTo: container,
}
);
expect(wrapper.html()).toMatchSnapshot();
share();
await tick();
expect(wrapper.html()).toMatch(
/<div class=\"container\" style="height: 350px; width: 350px;\">/
);
expect(wrapper.html()).toMatchSnapshot();
});
test('Placed successfully with page specified', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const wrapper = mount(
<div kbn-canvas-shareable="canvas" kbn-canvas-page="0" kbn-canvas-url="workpad.json"></div>,
{
attachTo: container,
}
);
expect(wrapper.html()).toMatchSnapshot();
share();
await tick();
expect(wrapper.html()).toMatchSnapshot();
});
});

View file

@ -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';

View file

@ -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<CanvasRenderedWorkpad | null> => {
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(
[
<style key="style">{`html body .kbnCanvas { height: ${stage.height}px; width: ${stage.width}px; }`}</style>,
<App key="app" workpad={workpad} {...{ stage, settings }} />,
],
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);
};

View file

@ -0,0 +1,101 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots shareables/Page component 1`] = `
<div
className="kbnCanvas"
style={
Object {
"height": 720,
"overflow": "hidden",
"position": "relative",
"width": undefined,
}
}
>
<div
className="root"
id="page-d8b39380-a8f5-4a52-afe7-c8f6a4eade7b"
style={
Object {
"background": "#b83c6f",
"height": 720,
"width": 1280,
}
}
>
RenderedElement
RenderedElement
RenderedElement
RenderedElement
RenderedElement
RenderedElement
RenderedElement
RenderedElement
RenderedElement
</div>
</div>
`;
exports[`Storyshots shareables/Page contextual: austin 1`] = `
<div
className="kbnCanvas"
style={
Object {
"height": 720,
"overflow": "hidden",
"position": "relative",
"width": undefined,
}
}
>
<div
className="root"
id="page-d8b39380-a8f5-4a52-afe7-c8f6a4eade7b"
style={
Object {
"background": "#b83c6f",
"height": 720,
"width": 1280,
}
}
>
RenderedElement
RenderedElement
RenderedElement
RenderedElement
RenderedElement
RenderedElement
RenderedElement
RenderedElement
RenderedElement
</div>
</div>
`;
exports[`Storyshots shareables/Page contextual: hello 1`] = `
<div
className="kbnCanvas"
style={
Object {
"height": 720,
"overflow": "hidden",
"position": "relative",
"width": undefined,
}
}
>
<div
className="root"
id="page-7186b301-f8a7-4c65-8b89-38d68d31cfc4"
style={
Object {
"background": "#777777",
"height": 720,
"width": 1080,
}
}
>
RenderedElement
</div>
</div>
`;

View file

@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots shareables/RenderedElement component 1`] = `
<div
className="kbnCanvas"
style={
Object {
"height": 100,
"overflow": "hidden",
"position": "relative",
"width": 100,
}
}
/>
`;
exports[`Storyshots shareables/RenderedElement contextual: austin 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#000",
"height": 720,
"overflow": "hidden",
"position": "relative",
"width": undefined,
}
}
>
RenderedElement
</div>
`;
exports[`Storyshots shareables/RenderedElement contextual: hello 1`] = `
<div
className="kbnCanvas"
style={
Object {
"height": 720,
"overflow": "hidden",
"position": "relative",
"width": undefined,
}
}
>
RenderedElement
</div>
`;

View file

@ -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', () => (
<ExampleContext source="austin">
<Canvas />
</ExampleContext>
))
.add('contextual: hello', () => (
<ExampleContext source="hello">
<Canvas />
</ExampleContext>
))
.add('component', () => (
<ExampleContext source="austin">
<CanvasComponent
onSetPage={action('onSetPage')}
onSetScrubberVisible={action('onSetScrubberVisible')}
refs={initialCanvasShareableState.refs}
settings={initialCanvasShareableState.settings}
stage={{
height: 338,
page: 0,
width: 600,
}}
workpad={austin}
/>
</ExampleContext>
));

View file

@ -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', () => (
<ExampleContext source="austin" style={{ height: 720 }}>
<Page index={3} />
</ExampleContext>
))
.add('contextual: hello', () => (
<ExampleContext source="hello" style={{ height: 720 }}>
<Page index={0} />
</ExampleContext>
))
.add('component', () => (
<ExampleContext source="austin" style={{ height: 720 }}>
<PageComponent height={720} width={1280} page={austin.pages[3]} />
</ExampleContext>
));

View file

@ -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', () => (
<ExampleContext style={{ height: 720 }}>
<RenderedElement element={hello.pages[0].elements[0]} index={0} />
</ExampleContext>
))
.add('contextual: austin', () => (
<ExampleContext style={{ height: 720, background: '#000' }}>
<RenderedElement element={austin.pages[0].elements[0]} index={0} />
</ExampleContext>
))
.add('component', () => (
<ExampleContext style={{ height: 100, width: 100 }}>
<RenderedElementComponent
index={0}
fn={image()}
element={{
id: '123',
position: {
left: 0,
top: 0,
height: 100,
width: 100,
angle: 0,
parent: null,
},
expressionRenderable: {
state: 'ready',
value: {
type: 'render',
as: 'image',
value: {
type: 'image',
mode: 'contain',
dataurl:
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NC42MiA1MS4wMyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7fS5jbHMtMSwuY2xzLTJ7c3Ryb2tlOiNmMzY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9LmNscy0ye2ZpbGw6bm9uZTt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkZsYWcgSWNvbjwvdGl0bGU+PGcgaWQ9IkxheWVyXzIiIGRhdGEtbmFtZT0iTGF5ZXIgMiI+PGcgaWQ9IkxheWVyXzEtMiIgZGF0YS1uYW1lPSJMYXllciAxIj48cG9seWdvbiBjbGFzcz0iY2xzLTEiIHBvaW50cz0iNDIuOTMgMjguMTUgMSAyOC4xNSAxIDEgNDIuOTMgMSAzNS40NyAxNC41OCA0Mi45MyAyOC4xNSIvPjxsaW5lIGNsYXNzPSJjbHMtMiIgeDE9IjEiIHkxPSIxIiB4Mj0iMSIgeTI9IjUxLjAzIi8+PC9nPjwvZz48L3N2Zz4=',
},
css: '.canvasRenderEl{\n\n}',
containerStyle: {
type: 'containerStyle',
overflow: 'hidden',
},
},
error: null,
},
}}
/>
</ExampleContext>
));

View file

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<App /> App renders properly 1`] = `
"<div class=\\"root\\" style=\\"height: 768px; width: 1080px;\\"><div class=\\"container\\" style=\\"height: 720px; width: 1080px;\\"><div class=\\"page\\" style=\\"height: 720px; width: 1080px;\\"><div id=\\"page-7186b301-f8a7-4c65-8b89-38d68d31cfc4\\" class=\\"root\\" style=\\"height: 720px; width: 1080px; background: rgb(119, 119, 119);\\"><div class=\\"canvasPositionable canvasInteractable\\" style=\\"width: 1082px; height: 205.37748344370857px; margin-left: -541px; margin-top: -102.68874172185429px; position: absolute;\\"><div class=\\"root\\"><div class=\\"container s2042575598\\" style=\\"overflow: hidden;\\"><style type=\\"text/css\\">.s2042575598 .canvasRenderEl h1 {
font-size: 150px; text-align: center; color: #d3d3d3;
}
</style><div class=\\"content\\"><div class=\\"renderContainer\\"><div data-renderer=\\"markdown\\" class=\\"render\\"><div>markdown mock</div></div></div></div></div></div></div></div></div></div><div class=\\"root\\" style=\\"height: 48px;\\"><div class=\\"root\\"><div class=\\"slideContainer\\"><div class=\\"root\\" style=\\"height: 100px; width: 150px;\\"><div class=\\"preview\\" style=\\"height: 100px; width: 150px;\\"><div id=\\"page-7186b301-f8a7-4c65-8b89-38d68d31cfc4\\" class=\\"root\\" style=\\"height: 720px; width: 1080px; background: rgb(119, 119, 119);\\"><div class=\\"canvasPositionable canvasInteractable\\" style=\\"width: 1082px; height: 205.37748344370857px; margin-left: -541px; margin-top: -102.68874172185429px; position: absolute;\\"><div class=\\"root\\"><div class=\\"container s2042575598\\" style=\\"overflow: hidden;\\"><style type=\\"text/css\\">.s2042575598 .canvasRenderEl h1 {
font-size: 150px; text-align: center; color: #d3d3d3;
}
</style><div class=\\"content\\"><div class=\\"renderContainer\\"><div data-renderer=\\"markdown\\" class=\\"render\\"><div>markdown mock</div></div></div></div></div></div></div></div></div></div></div></div><div class=\\"bar\\" style=\\"bottom: 0px;\\"><div class=\\"euiFlexGroup euiFlexGroup--directionRow euiFlexGroup--responsive\\"><div class=\\"euiFlexItem title\\"><div class=\\"euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><a class=\\"euiLink euiLink--primary\\" href=\\"https://www.elastic.co\\" rel=\\"\\" title=\\"Powered by Elastic.co\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--large euiIcon-isLoading\\" focusable=\\"false\\"></svg></a></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\" style=\\"min-width: 0; cursor: default;\\"><div class=\\"euiText euiText--small\\"><div class=\\"euiTextColor euiTextColor--ghost\\"><div class=\\"eui-textTruncate\\">My Canvas Workpad</div></div></div></div></div></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><div class=\\"euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive\\"><div class=\\"euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive\\" style=\\"margin: 0px 12px;\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><button disabled=\\"\\" class=\\"euiButtonIcon euiButtonIcon--ghost\\" type=\\"button\\" data-test-subj=\\"pageControlsPrevPage\\" aria-label=\\"Previous Page\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon\\" focusable=\\"false\\" aria-hidden=\\"true\\"></svg></button></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><button class=\\"euiButtonEmpty euiButtonEmpty--ghost euiButtonEmpty--small\\" type=\\"button\\" data-test-subj=\\"pageControlsCurrentPage\\"><span class=\\"euiButtonEmpty__content\\"><span class=\\"euiButtonEmpty__text\\"><div class=\\"euiText euiText--small\\"><div class=\\"euiTextColor euiTextColor--ghost\\">Page 1</div></div></span></span></button></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><button disabled=\\"\\" class=\\"euiButtonIcon euiButtonIcon--ghost\\" type=\\"button\\" data-test-subj=\\"pageControlsNextPage\\" aria-label=\\"Next Page\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon\\" focusable=\\"false\\" aria-hidden=\\"true\\"></svg></button></div></div><div class=\\"euiFlexGroup euiFlexGroup--alignItemsFlexEnd euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><div class=\\"euiPopover euiPopover--anchorUpRight euiPopover--withTitle\\" id=\\"settings\\"><div class=\\"euiPopover__anchor\\"><button class=\\"euiButtonIcon euiButtonIcon--ghost\\" type=\\"button\\" aria-label=\\"Settings\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon\\" focusable=\\"false\\" aria-hidden=\\"true\\"></svg></button></div></div></div></div></div></div></div></div></div></div>"
`;

View file

@ -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) => <div>{props.children}</div>,
};
});
const getWrapper: (name?: WorkpadNames) => ReactWrapper = (name = 'hello') => {
const workpad = sharedWorkpads[name];
const { height, width } = workpad;
const stage = {
height,
width,
page: 0,
};
return mount(<App {...{ stage, workpad }} />);
};
describe('<App />', () => {
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');
});
*/
});

View file

@ -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('<Canvas />', () => {
test('null workpad renders nothing', () => {
expect(mount(<Canvas />).isEmptyRender());
});
let wrapper: ReactWrapper;
beforeEach(() => {
wrapper = mount(
<JestContext source="austin">
<Canvas />
</JestContext>
);
});
test('scrubber opens and closes', () => {
expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(false);
center(wrapper).simulate('click');
expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(true);
});
});

View file

@ -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('<Page />', () => {
test('null workpad renders nothing', () => {
expect(mount(<Page index={0} />).isEmptyRender());
});
});

View file

@ -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<Props> = ({ 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 (
<CanvasShareableStateProvider initialState={initialState}>
<Canvas />
</CanvasShareableStateProvider>
);
};

View file

@ -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;
}

View file

@ -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<CanvasRenderedWorkpad, 'height' | 'width' | 'pages'>;
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<Refs, 'stage'>;
/**
* 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 (
<div
className={css.root}
style={{ height: rootHeight, width: stageWidth }}
onMouseEnter={() => hideToolbar(false)}
onMouseLeave={() => hideToolbar(true)}
ref={refs.stage}
>
<div className={css.container} style={{ height: stageHeight, width: stageWidth }}>
<div className={css.page} style={pageStyle}>
<Page index={page} />
</div>
</div>
<Footer isHidden={toolbarHidden} />
</div>
);
};
/**
* 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 (
<CanvasComponent
{...{
onSetPage,
onSetScrubberVisible,
refs,
settings,
stage,
workpad,
}}
/>
);
};

View file

@ -0,0 +1,140 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots shareables/Footer/components PageControls 1`] = `
<div
className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
style={
Object {
"margin": "0 12px",
}
}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Previous Page"
className="euiButtonIcon euiButtonIcon--ghost"
data-test-subj="pageControlsPrevPage"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
className="euiButtonEmpty euiButtonEmpty--ghost euiButtonEmpty--small"
data-test-subj="pageControlsCurrentPage"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
<div
className="euiText euiText--small"
>
<div
className="euiTextColor euiTextColor--ghost"
>
Page
1
</div>
</div>
</span>
</span>
</button>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Next Page"
className="euiButtonIcon euiButtonIcon--ghost"
data-test-subj="pageControlsNextPage"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</div>
</div>
`;
exports[`Storyshots shareables/Footer/components Title 1`] = `
<div
style={
Object {
"background": "#333",
"padding": 10,
}
}
>
<div
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
style={
Object {
"cursor": "default",
"minWidth": 0,
}
}
>
<div
className="euiText euiText--small"
>
<div
className="euiTextColor euiTextColor--ghost"
>
<div
className="eui-textTruncate"
>
This is a test title.
</div>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,302 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots shareables/Footer/PageControls component 1`] = `
<div
style={
Object {
"background": "#333",
"padding": 10,
}
}
>
<div
className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
style={
Object {
"margin": "0 12px",
}
}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Previous Page"
className="euiButtonIcon euiButtonIcon--ghost"
data-test-subj="pageControlsPrevPage"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
className="euiButtonEmpty euiButtonEmpty--ghost euiButtonEmpty--small"
data-test-subj="pageControlsCurrentPage"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
<div
className="euiText euiText--small"
>
<div
className="euiTextColor euiTextColor--ghost"
>
Page
1
of 10
</div>
</div>
</span>
</span>
</button>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Next Page"
className="euiButtonIcon euiButtonIcon--ghost"
data-test-subj="pageControlsNextPage"
disabled={false}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</div>
</div>
</div>
`;
exports[`Storyshots shareables/Footer/PageControls contextual: austin 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#333",
"height": undefined,
"overflow": "hidden",
"padding": 10,
"position": "relative",
"width": undefined,
}
}
>
<div
className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
style={
Object {
"margin": "0 12px",
}
}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Previous Page"
className="euiButtonIcon euiButtonIcon--ghost"
data-test-subj="pageControlsPrevPage"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
className="euiButtonEmpty euiButtonEmpty--ghost euiButtonEmpty--small"
data-test-subj="pageControlsCurrentPage"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
<div
className="euiText euiText--small"
>
<div
className="euiTextColor euiTextColor--ghost"
>
Page
1
of 28
</div>
</div>
</span>
</span>
</button>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Next Page"
className="euiButtonIcon euiButtonIcon--ghost"
data-test-subj="pageControlsNextPage"
disabled={false}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</div>
</div>
</div>
`;
exports[`Storyshots shareables/Footer/PageControls contextual: hello 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#333",
"height": undefined,
"overflow": "hidden",
"padding": 10,
"position": "relative",
"width": undefined,
}
}
>
<div
className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
style={
Object {
"margin": "0 12px",
}
}
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Previous Page"
className="euiButtonIcon euiButtonIcon--ghost"
data-test-subj="pageControlsPrevPage"
disabled={true}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
className="euiButtonEmpty euiButtonEmpty--ghost euiButtonEmpty--small"
data-test-subj="pageControlsCurrentPage"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
<div
className="euiText euiText--small"
>
<div
className="euiTextColor euiTextColor--ghost"
>
Page
1
of 28
</div>
</div>
</span>
</span>
</button>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-label="Next Page"
className="euiButtonIcon euiButtonIcon--ghost"
data-test-subj="pageControlsNextPage"
disabled={false}
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,193 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots shareables/Footer/Title component 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#333",
"height": undefined,
"overflow": "hidden",
"padding": 10,
"position": "relative",
"width": undefined,
}
}
>
<div
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<a
className="euiLink euiLink--primary"
href="https://www.elastic.co"
rel=""
title="Powered by Elastic.co"
>
<svg
className="euiIcon euiIcon--large euiIcon-isLoading"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</a>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
style={
Object {
"cursor": "default",
"minWidth": 0,
}
}
>
<div
className="euiText euiText--small"
>
<div
className="euiTextColor euiTextColor--ghost"
>
<div
className="eui-textTruncate"
>
This is a test title.
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots shareables/Footer/Title contextual: austin 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#333",
"height": undefined,
"overflow": "hidden",
"padding": 10,
"position": "relative",
"width": undefined,
}
}
>
<div
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<a
className="euiLink euiLink--primary"
href="https://www.elastic.co"
rel=""
title="Powered by Elastic.co"
>
<svg
className="euiIcon euiIcon--large euiIcon-isLoading"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</a>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
style={
Object {
"cursor": "default",
"minWidth": 0,
}
}
>
<div
className="euiText euiText--small"
>
<div
className="euiTextColor euiTextColor--ghost"
>
<div
className="eui-textTruncate"
>
Elastic{ON} - Austin from Clint Andrew Hall with a title that just goes and goes and goes
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots shareables/Footer/Title contextual: hello 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#333",
"height": undefined,
"overflow": "hidden",
"padding": 10,
"position": "relative",
"width": undefined,
}
}
>
<div
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<a
className="euiLink euiLink--primary"
href="https://www.elastic.co"
rel=""
title="Powered by Elastic.co"
>
<svg
className="euiIcon euiIcon--large euiIcon-isLoading"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</a>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
style={
Object {
"cursor": "default",
"minWidth": 0,
}
}
>
<div
className="euiText euiText--small"
>
<div
className="euiTextColor euiTextColor--ghost"
>
<div
className="eui-textTruncate"
>
My Canvas Workpad
</div>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -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', () => (
<ExampleContext height={172} source="hello">
<Footer />
</ExampleContext>
))
.add('contextual: austin', () => (
<ExampleContext height={172} source="austin">
<Footer />
</ExampleContext>
));

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { ExampleContext } from '../../../test/context_example';
import { PageControls, PageControlsComponent } from '../page_controls';
const style = { background: '#333', padding: 10 };
storiesOf('shareables/Footer/PageControls', module)
.add('contextual: hello', () => (
<ExampleContext source="austin" {...{ style }}>
<PageControls />
</ExampleContext>
))
.add('contextual: austin', () => (
<ExampleContext source="austin" {...{ style }}>
<PageControls />
</ExampleContext>
))
.add('component', () => (
<div {...{ style }}>
<PageControlsComponent
page={0}
totalPages={10}
onSetPageNumber={action('onSetPageNumber')}
onToggleScrubber={action('onToggleScrubber')}
/>
</div>
));

View file

@ -0,0 +1,33 @@
/*
* 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 { storiesOf } from '@storybook/react';
import { CanvasRenderedPage } from '../../../types';
import { ExampleContext } from '../../../test/context_example';
import { Scrubber, ScrubberComponent } from '../scrubber';
import { workpads } from '../../../../__tests__/fixtures/workpads';
storiesOf('shareables/Footer/Scrubber', module)
.add('contextual: hello', () => (
<ExampleContext source="hello" style={{ height: 172 }} isScrubberVisible={true}>
<Scrubber />
</ExampleContext>
))
.add('contextual: austin', () => (
<ExampleContext source="austin" style={{ height: 172 }} isScrubberVisible={true}>
<Scrubber />
</ExampleContext>
))
.add('component', () => (
<ExampleContext style={{ height: 172 }}>
<ScrubberComponent
isScrubberVisible={true}
pages={(workpads[0].pages as unknown) as CanvasRenderedPage[]}
/>
</ExampleContext>
));

View file

@ -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 React from 'react';
import { storiesOf } from '@storybook/react';
import { ExampleContext } from '../../../test/context_example';
import { Title, TitleComponent } from '../title';
const style = { background: '#333', padding: 10 };
storiesOf('shareables/Footer/Title', module)
.add('contextual: hello', () => (
<ExampleContext source="hello" {...{ style }}>
<Title />
</ExampleContext>
))
.add('contextual: austin', () => (
<ExampleContext source="austin" {...{ style }}>
<Title />
</ExampleContext>
))
.add('component', () => (
<ExampleContext {...{ style }}>
<TitleComponent title="This is a test title." />
</ExampleContext>
));

View file

@ -0,0 +1,31 @@
/*
* 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 { JestContext } from '../../../test/context_jest';
import { getScrubber as scrubber, getPageControlsCenter as center } from '../../../test/selectors';
import { Footer } from '../footer';
jest.mock('../../../supported_renderers');
describe('<Footer />', () => {
test('null workpad renders nothing', () => {
expect(mount(<Footer />).isEmptyRender());
});
const wrapper = mount(
<JestContext>
<Footer />
</JestContext>
);
test('scrubber functions properly', () => {
expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(false);
center(wrapper).simulate('click');
expect(scrubber(wrapper).prop('isScrubberVisible')).toEqual(true);
});
});

View file

@ -0,0 +1,55 @@
/*
* 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 { JestContext } from '../../../test/context_jest';
import {
getPageControlsPrevious as previous,
getPageControlsCenter as current,
getPageControlsNext as next,
} from '../../../test/selectors';
import { PageControls } from '../page_controls';
jest.mock('../../../supported_renderers');
describe('<PageControls />', () => {
test('null workpad renders nothing', () => {
expect(mount(<PageControls />).isEmptyRender());
});
const hello = mount(
<JestContext source="hello">
<PageControls />
</JestContext>
);
const austin = mount(
<JestContext source="austin">
<PageControls />
</JestContext>
);
test('hello: renders as expected', () => {
expect(previous(hello).props().disabled).toEqual(true);
expect(next(hello).props().disabled).toEqual(true);
expect(current(hello).text()).toEqual('Page 1');
});
test('austin: renders as expected', () => {
expect(previous(austin).props().disabled).toEqual(true);
expect(next(austin).props().disabled).toEqual(false);
expect(current(austin).text()).toEqual('Page 1 of 28');
});
test('austin: moves between pages', () => {
next(austin).simulate('click');
expect(current(austin).text()).toEqual('Page 2 of 28');
next(austin).simulate('click');
expect(current(austin).text()).toEqual('Page 3 of 28');
previous(austin).simulate('click');
expect(current(austin).text()).toEqual('Page 2 of 28');
});
});

View file

@ -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 { mount } from 'enzyme';
import React from 'react';
import { JestContext } from '../../../test/context_jest';
import { PagePreview } from '../page_preview';
import { getRenderedElement as element } from '../../../test/selectors';
jest.mock('../../../supported_renderers');
describe('<PagePreview />', () => {
test('null workpad renders nothing', () => {
expect(mount(<PagePreview height={100} index={0} />).isEmptyRender());
});
const wrapper = mount(
<JestContext>
<PagePreview height={100} index={0} />
</JestContext>
);
test('renders as expected', () => {
expect(element(wrapper).text()).toEqual('markdown mock');
});
});

View file

@ -0,0 +1,33 @@
/*
* 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 { JestContext } from '../../../test/context_jest';
import { Scrubber } from '../scrubber';
import {
getScrubberSlideContainer as container,
getRenderedElement as element,
} from '../../../test/selectors';
jest.mock('../../../supported_renderers');
describe('<Scrubber />', () => {
test('null workpad renders nothing', () => {
expect(mount(<Scrubber />).isEmptyRender());
});
const wrapper = mount(
<JestContext>
<Scrubber />
</JestContext>
);
test('renders as expected', () => {
expect(container(wrapper).children().length === 1);
expect(element(wrapper).text()).toEqual('markdown mock');
});
});

View file

@ -0,0 +1,28 @@
/*
* 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 { JestContext } from '../../../test/context_jest';
import { Title } from '../title';
jest.mock('../../../supported_renderers');
describe('<Title />', () => {
test('null workpad renders nothing', () => {
expect(mount(<Title />).isEmptyRender());
});
const wrapper = mount(
<JestContext>
<Title />
</JestContext>
);
test('renders as expected', () => {
expect(wrapper.text()).toEqual('My Canvas Workpad');
});
});

View file

@ -0,0 +1,27 @@
@import '@elastic/eui/src/global_styling/variables/_size.scss';
@import '@elastic/eui/src/global_styling/variables/_colors.scss';
:global .kbnCanvas :local .root .bar {
position: absolute;
}
.bar {
composes: euiBottomBar from global;
}
:global .kbnCanvas :local .bar {
transition: bottom 0.25s;
padding: $euiSizeM;
}
:global .kbnCanvas :local .title {
overflow: hidden;
min-width: 0;
text-overflow: ellipsis;
white-space: nowrap;
flex-grow: 1;
}
:global .kbnCanvas .euiIcon__fillNegative {
fill: $euiColorGhost !important;
}

View file

@ -0,0 +1,71 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { useCanvasShareableState } from '../../context';
import { Scrubber } from './scrubber';
import { Title } from './title';
import { PageControls } from './page_controls';
import { Settings } from './settings';
import css from './footer.module.scss';
export const FOOTER_HEIGHT = 48;
export interface Props {
/**
* True if the footer should be hidden when not interacted with, false otherwise.
*/
isAutohide?: boolean;
/**
* True if the footer should be hidden, false otherwise.
*/
isHidden?: boolean;
}
/**
* The Footer of the Shareable Canvas Workpad.
*/
export const FooterComponent: FC<Props> = ({ isAutohide = false, isHidden = false }) => {
const { root, bar, title } = css;
return (
<div className={root} style={{ height: FOOTER_HEIGHT }}>
<Scrubber />
<div className={bar} style={{ bottom: isAutohide && isHidden ? -FOOTER_HEIGHT : 0 }}>
<EuiFlexGroup gutterSize="none">
<EuiFlexItem className={title}>
<Title />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<PageControls />
<Settings />
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</div>
</div>
);
};
/**
* A store-connected container for the `Footer` component.
*/
export const Footer: FC<Pick<Props, 'isHidden'>> = ({ isHidden = false }) => {
const [{ workpad, settings }] = useCanvasShareableState();
if (!workpad) {
return null;
}
const { toolbar } = settings;
const { isAutohide } = toolbar;
return <FooterComponent {...{ isHidden, isAutohide }} />;
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './footer';

View file

@ -0,0 +1,113 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiButtonEmpty, EuiText } from '@elastic/eui';
import {
useCanvasShareableState,
setScrubberVisibleAction,
setPageAction,
setAutoplayAction,
} from '../../context';
type onSetPageNumberFn = (page: number) => void;
type onToggleScrubberFn = () => void;
interface Props {
/**
* The handler to invoke when the current page number is set.
*/
onSetPageNumber: onSetPageNumberFn;
/**
* The handler to invoke when the scrubber visibility is toggled.
*/
onToggleScrubber: onToggleScrubberFn;
/**
* The current page number.
*/
page: number;
/**
* The total number of pages in the worpad.
*/
totalPages: number;
}
/**
* The page count and paging controls within the footer of the Shareable Canvas Workpad.
*/
export const PageControlsComponent: FC<Props> = ({
onSetPageNumber,
page,
totalPages,
onToggleScrubber,
}) => {
const currentPage = page + 1;
return (
<EuiFlexGroup alignItems="center" gutterSize="none" style={{ margin: '0 12px' }}>
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="ghost"
data-test-subj="pageControlsPrevPage"
onClick={() => onSetPageNumber(page - 1)}
iconType="arrowLeft"
disabled={currentPage <= 1}
aria-label="Previous Page"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
color="ghost"
size="s"
onClick={onToggleScrubber}
data-test-subj="pageControlsCurrentPage"
>
<EuiText color="ghost" size="s">
Page {currentPage}
{totalPages > 1 ? ` of ${totalPages}` : null}
</EuiText>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="ghost"
data-test-subj="pageControlsNextPage"
onClick={() => onSetPageNumber(page + 1)}
iconType="arrowRight"
disabled={currentPage >= totalPages}
aria-label="Next Page"
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
/**
* A store-connected container for the `PageControls` component.
*/
export const PageControls: FC<{}> = () => {
const [{ workpad, footer, stage }, dispatch] = useCanvasShareableState();
if (!workpad) {
return null;
}
const { isScrubberVisible } = footer;
const { page } = stage;
const totalPages = workpad.pages.length;
const onToggleScrubber = () => {
dispatch(setAutoplayAction(false));
dispatch(setScrubberVisibleAction(!isScrubberVisible));
};
const onSetPageNumber = (number: number) => dispatch(setPageAction(number));
return <PageControlsComponent {...{ onToggleScrubber, onSetPageNumber, page, totalPages }} />;
};

View file

@ -0,0 +1,15 @@
@import '@elastic/eui/src/global_styling/variables/_size.scss';
:global .kbnCanvas :local .root {
margin: 0 $euiSizeS;
cursor: pointer;
}
:global .kbnCanvas :local .root :global .canvasPage {
position: relative;
}
:global .kbnCanvas :local .preview {
pointer-events: none;
transform-origin: top left;
}

View file

@ -0,0 +1,102 @@
/*
* 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 { PageComponent } from '../page';
import { CanvasRenderedPage } from '../../types';
import { useCanvasShareableState } from '../../context';
import { setPageAction } from '../../context/actions';
import css from './page_preview.module.scss';
type onClickFn = (index: number) => void;
export interface Props {
/**
* The height of the preview container.
*/
height: number;
/**
* The index of the preview relative to other pages in the workpad.
*/
index: number;
/**
* The handler to invoke if the preview is clicked.
*/
onClick: onClickFn;
/**
* An object describing the page.
*/
page: CanvasRenderedPage;
/**
* The height of the workpad.
*/
workpadHeight: number;
/**
* The width of the workpad.
*/
workpadWidth: number;
}
/**
* The small preview of the page shown within the `Scrubber`.
*/
export const PagePreviewComponent: FC<Props> = ({
height,
index,
onClick,
page,
workpadHeight,
workpadWidth,
}) => {
const scale = height / workpadHeight;
const style = {
height: workpadHeight * scale,
width: workpadWidth * scale,
};
const transform = {
...style,
transform: `scale3d(${scale}, ${scale}, 1)`,
};
return (
<div
className={css.root}
onClick={() => onClick(index)}
onKeyPress={() => onClick(index)}
style={style}
>
<div className={css.preview} style={transform}>
<PageComponent {...{ page }} height={workpadHeight} width={workpadWidth} />
</div>
</div>
);
};
/**
* A store-connected container for the `PagePreview` component.
*/
export const PagePreview: FC<Pick<Props, 'index' | 'height'>> = ({ index, height }) => {
const [{ workpad }, dispatch] = useCanvasShareableState();
if (!workpad) {
return null;
}
const page = workpad.pages[index];
const onClick = (pageIndex: number) => dispatch(setPageAction(pageIndex));
const { height: workpadHeight, width: workpadWidth } = workpad;
return (
<PagePreviewComponent {...{ onClick, height, workpadHeight, workpadWidth, page, index }} />
);
};

View file

@ -0,0 +1,26 @@
@import '@elastic/eui/src/global_styling/variables/_size.scss';
@import '@elastic/eui/src/global_styling/variables/_colors.scss';
@import '@elastic/eui/src/global_styling/mixins/_helpers.scss';
:global .kbnCanvas :local .root {
background: $euiColorGhost;
position: absolute;
bottom: -172px;
left: 0;
right: 0;
padding: $euiSizeS 0 ($euiSizeL * 2) 0;
transition: bottom 0.25s;
height: 172px;
}
:global .kbnCanvas :local .visible {
bottom: 0;
}
:global .kbnCanvas :local .slideContainer {
@include euiScrollBar;
display: flex;
overflow-x: auto;
overflow-y: hidden;
width: 100%;
}

View file

@ -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 React, { FC } from 'react';
import classnames from 'classnames';
import { PagePreview } from './page_preview';
import { useCanvasShareableState } from '../../context';
import css from './scrubber.module.scss';
import { CanvasRenderedPage } from '../../types';
interface Props {
/**
* True if the scrubber is currently visible, false otherwise.
*/
isScrubberVisible: boolean;
/**
* A collection of objects describing the pages within the workpad to be
* displayed in the Scrubber.
*/
pages: CanvasRenderedPage[];
}
const THUMBNAIL_HEIGHT = 100;
/**
* The panel of previews of the pages in the workpad, allowing one to select and
* navigate to a specific page.
*/
export const ScrubberComponent: FC<Props> = ({ isScrubberVisible, pages }) => {
const className = isScrubberVisible ? classnames(css.root, css.visible) : css.root;
const slides = pages.map((page, index) => (
<PagePreview key={page.id} height={THUMBNAIL_HEIGHT} {...{ index }} />
));
return (
<div className={className}>
<div className={css.slideContainer}>{slides}</div>
</div>
);
};
/**
* A store-connected container for the `Scrubber` component.
*/
export const Scrubber: FC<{}> = () => {
const [{ workpad, footer }] = useCanvasShareableState();
if (!workpad) {
return null;
}
const { pages } = workpad;
const { isScrubberVisible } = footer;
return <ScrubberComponent {...{ pages, isScrubberVisible }} />;
};

View file

@ -0,0 +1,532 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: off, 2s 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#fff",
"border": "1px solid #ccc",
"height": 228,
"overflow": "hidden",
"padding": 16,
"position": "relative",
"width": 256,
}
}
>
<div
style={
Object {
"padding": 16,
}
}
>
<div
className="euiSwitch"
>
<input
checked={false}
className="euiSwitch__input"
id="cycle"
name="cycle"
onChange={[Function]}
type="checkbox"
/>
<span
className="euiSwitch__body"
>
<span
className="euiSwitch__thumb"
/>
<span
className="euiSwitch__track"
>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon euiSwitch__icon--checked"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</span>
</span>
<label
className="euiSwitch__label"
htmlFor="cycle"
>
Cycle Slides
</label>
</div>
<hr
className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium"
/>
<form
onSubmit={[Function]}
>
<div
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem"
>
<div
className="euiFormRow euiFormRow--compressed"
id="generated-id-row"
>
<div
className="euiFormRow__labelWrapper"
>
<label
className="euiFormLabel euiFormRow__label"
htmlFor="generated-id"
>
Set a custom interval
</label>
</div>
<div
className="euiFormRow__fieldWrapper"
>
<div
className="euiFormControlLayout euiFormControlLayout--compressed"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
aria-describedby="generated-id-help"
className="euiFieldText euiFieldText--compressed"
id="generated-id"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="text"
value="2s"
/>
</div>
</div>
<div
className="euiFormHelpText euiFormRow__text"
id="generated-id-help"
>
Use shorthand notation, like 30s, 10m, or 1h
</div>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiFormRow"
id="generated-id-row"
>
<div
className="euiFormRow__labelWrapper"
>
<label
className="euiFormLabel euiFormRow__label"
htmlFor="generated-id"
>
 
</label>
</div>
<div
className="euiFormRow__fieldWrapper"
>
<button
className="euiButton euiButton--primary euiButton--small"
disabled={false}
id="generated-id"
onBlur={[Function]}
onFocus={[Function]}
style={
Object {
"minWidth": "auto",
}
}
type="submit"
>
<span
className="euiButton__content"
>
<span
className="euiButton__text"
>
Set
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
`;
exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: on, 5s 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#fff",
"border": "1px solid #ccc",
"height": 228,
"overflow": "hidden",
"padding": 16,
"position": "relative",
"width": 256,
}
}
>
<div
style={
Object {
"padding": 16,
}
}
>
<div
className="euiSwitch"
>
<input
checked={true}
className="euiSwitch__input"
id="cycle"
name="cycle"
onChange={[Function]}
type="checkbox"
/>
<span
className="euiSwitch__body"
>
<span
className="euiSwitch__thumb"
/>
<span
className="euiSwitch__track"
>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon euiSwitch__icon--checked"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</span>
</span>
<label
className="euiSwitch__label"
htmlFor="cycle"
>
Cycle Slides
</label>
</div>
<hr
className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium"
/>
<form
onSubmit={[Function]}
>
<div
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem"
>
<div
className="euiFormRow euiFormRow--compressed"
id="generated-id-row"
>
<div
className="euiFormRow__labelWrapper"
>
<label
className="euiFormLabel euiFormRow__label"
htmlFor="generated-id"
>
Set a custom interval
</label>
</div>
<div
className="euiFormRow__fieldWrapper"
>
<div
className="euiFormControlLayout euiFormControlLayout--compressed"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
aria-describedby="generated-id-help"
className="euiFieldText euiFieldText--compressed"
id="generated-id"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="text"
value="5s"
/>
</div>
</div>
<div
className="euiFormHelpText euiFormRow__text"
id="generated-id-help"
>
Use shorthand notation, like 30s, 10m, or 1h
</div>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiFormRow"
id="generated-id-row"
>
<div
className="euiFormRow__labelWrapper"
>
<label
className="euiFormLabel euiFormRow__label"
htmlFor="generated-id"
>
 
</label>
</div>
<div
className="euiFormRow__fieldWrapper"
>
<button
className="euiButton euiButton--primary euiButton--small"
disabled={false}
id="generated-id"
onBlur={[Function]}
onFocus={[Function]}
style={
Object {
"minWidth": "auto",
}
}
type="submit"
>
<span
className="euiButton__content"
>
<span
className="euiButton__text"
>
Set
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
`;
exports[`Storyshots shareables/Footer/Settings/AutoplaySettings contextual 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#fff",
"border": "1px solid #ccc",
"height": 228,
"overflow": "hidden",
"padding": 16,
"position": "relative",
"width": 256,
}
}
>
<div
style={
Object {
"padding": 16,
}
}
>
<div
className="euiSwitch"
>
<input
checked={false}
className="euiSwitch__input"
id="cycle"
name="cycle"
onChange={[Function]}
type="checkbox"
/>
<span
className="euiSwitch__body"
>
<span
className="euiSwitch__thumb"
/>
<span
className="euiSwitch__track"
>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon euiSwitch__icon--checked"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</span>
</span>
<label
className="euiSwitch__label"
htmlFor="cycle"
>
Cycle Slides
</label>
</div>
<hr
className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium"
/>
<form
onSubmit={[Function]}
>
<div
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem"
>
<div
className="euiFormRow euiFormRow--compressed"
id="generated-id-row"
>
<div
className="euiFormRow__labelWrapper"
>
<label
className="euiFormLabel euiFormRow__label"
htmlFor="generated-id"
>
Set a custom interval
</label>
</div>
<div
className="euiFormRow__fieldWrapper"
>
<div
className="euiFormControlLayout euiFormControlLayout--compressed"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
aria-describedby="generated-id-help"
className="euiFieldText euiFieldText--compressed"
id="generated-id"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="text"
value="5s"
/>
</div>
</div>
<div
className="euiFormHelpText euiFormRow__text"
id="generated-id-help"
>
Use shorthand notation, like 30s, 10m, or 1h
</div>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiFormRow"
id="generated-id-row"
>
<div
className="euiFormRow__labelWrapper"
>
<label
className="euiFormLabel euiFormRow__label"
htmlFor="generated-id"
>
 
</label>
</div>
<div
className="euiFormRow__fieldWrapper"
>
<button
className="euiButton euiButton--primary euiButton--small"
disabled={false}
id="generated-id"
onBlur={[Function]}
onFocus={[Function]}
style={
Object {
"minWidth": "auto",
}
}
type="submit"
>
<span
className="euiButton__content"
>
<span
className="euiButton__text"
>
Set
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
`;

View file

@ -0,0 +1,479 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots shareables/Settings/components AutoplaySettings, autoplay disabled 1`] = `
<div
style={
Object {
"padding": 16,
}
}
>
<div
className="euiSwitch"
>
<input
checked={false}
className="euiSwitch__input"
id="cycle"
name="cycle"
onChange={[Function]}
type="checkbox"
/>
<span
className="euiSwitch__body"
>
<span
className="euiSwitch__thumb"
/>
<span
className="euiSwitch__track"
>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon euiSwitch__icon--checked"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</span>
</span>
<label
className="euiSwitch__label"
htmlFor="cycle"
>
Cycle Slides
</label>
</div>
<hr
className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium"
/>
<form
onSubmit={[Function]}
>
<div
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem"
>
<div
className="euiFormRow euiFormRow--compressed"
id="generated-id-row"
>
<div
className="euiFormRow__labelWrapper"
>
<label
className="euiFormLabel euiFormRow__label"
htmlFor="generated-id"
>
Set a custom interval
</label>
</div>
<div
className="euiFormRow__fieldWrapper"
>
<div
className="euiFormControlLayout euiFormControlLayout--compressed"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
aria-describedby="generated-id-help"
className="euiFieldText euiFieldText--compressed"
id="generated-id"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="text"
value="5s"
/>
</div>
</div>
<div
className="euiFormHelpText euiFormRow__text"
id="generated-id-help"
>
Use shorthand notation, like 30s, 10m, or 1h
</div>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiFormRow"
id="generated-id-row"
>
<div
className="euiFormRow__labelWrapper"
>
<label
className="euiFormLabel euiFormRow__label"
htmlFor="generated-id"
>
 
</label>
</div>
<div
className="euiFormRow__fieldWrapper"
>
<button
className="euiButton euiButton--primary euiButton--small"
disabled={false}
id="generated-id"
onBlur={[Function]}
onFocus={[Function]}
style={
Object {
"minWidth": "auto",
}
}
type="submit"
>
<span
className="euiButton__content"
>
<span
className="euiButton__text"
>
Set
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</form>
</div>
`;
exports[`Storyshots shareables/Settings/components AutoplaySettings, autoplay enabled 1`] = `
<div
style={
Object {
"padding": 16,
}
}
>
<div
className="euiSwitch"
>
<input
checked={true}
className="euiSwitch__input"
id="cycle"
name="cycle"
onChange={[Function]}
type="checkbox"
/>
<span
className="euiSwitch__body"
>
<span
className="euiSwitch__thumb"
/>
<span
className="euiSwitch__track"
>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon euiSwitch__icon--checked"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</span>
</span>
<label
className="euiSwitch__label"
htmlFor="cycle"
>
Cycle Slides
</label>
</div>
<hr
className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium"
/>
<form
onSubmit={[Function]}
>
<div
className="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem"
>
<div
className="euiFormRow euiFormRow--compressed"
id="generated-id-row"
>
<div
className="euiFormRow__labelWrapper"
>
<label
className="euiFormLabel euiFormRow__label"
htmlFor="generated-id"
>
Set a custom interval
</label>
</div>
<div
className="euiFormRow__fieldWrapper"
>
<div
className="euiFormControlLayout euiFormControlLayout--compressed"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
aria-describedby="generated-id-help"
className="euiFieldText euiFieldText--compressed"
id="generated-id"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="text"
value="5s"
/>
</div>
</div>
<div
className="euiFormHelpText euiFormRow__text"
id="generated-id-help"
>
Use shorthand notation, like 30s, 10m, or 1h
</div>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiFormRow"
id="generated-id-row"
>
<div
className="euiFormRow__labelWrapper"
>
<label
className="euiFormLabel euiFormRow__label"
htmlFor="generated-id"
>
 
</label>
</div>
<div
className="euiFormRow__fieldWrapper"
>
<button
className="euiButton euiButton--primary euiButton--small"
disabled={false}
id="generated-id"
onBlur={[Function]}
onFocus={[Function]}
style={
Object {
"minWidth": "auto",
}
}
type="submit"
>
<span
className="euiButton__content"
>
<span
className="euiButton__text"
>
Set
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</form>
</div>
`;
exports[`Storyshots shareables/Settings/components ToolbarSettings, autohide disabled 1`] = `
<div
style={
Object {
"padding": 16,
}
}
>
<div
className="euiFormRow"
id="generated-id-row"
>
<div
className="euiFormRow__fieldWrapper"
>
<div
className="euiSwitch"
>
<input
aria-describedby="generated-id-help"
checked={false}
className="euiSwitch__input"
data-test-subj="hideToolbarSwitch"
id="generated-id"
name="toolbarHide"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="checkbox"
/>
<span
className="euiSwitch__body"
>
<span
className="euiSwitch__thumb"
/>
<span
className="euiSwitch__track"
>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon euiSwitch__icon--checked"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</span>
</span>
<label
className="euiSwitch__label"
htmlFor="generated-id"
>
Hide Toolbar
</label>
</div>
<div
className="euiFormHelpText euiFormRow__text"
id="generated-id-help"
>
Hide the toolbar when the mouse is not within the Canvas?
</div>
</div>
</div>
</div>
`;
exports[`Storyshots shareables/Settings/components ToolbarSettings, autohide enabled 1`] = `
<div
style={
Object {
"padding": 16,
}
}
>
<div
className="euiFormRow"
id="generated-id-row"
>
<div
className="euiFormRow__fieldWrapper"
>
<div
className="euiSwitch"
>
<input
aria-describedby="generated-id-help"
checked={true}
className="euiSwitch__input"
data-test-subj="hideToolbarSwitch"
id="generated-id"
name="toolbarHide"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="checkbox"
/>
<span
className="euiSwitch__body"
>
<span
className="euiSwitch__thumb"
/>
<span
className="euiSwitch__track"
>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon euiSwitch__icon--checked"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</span>
</span>
<label
className="euiSwitch__label"
htmlFor="generated-id"
>
Hide Toolbar
</label>
</div>
<div
className="euiFormHelpText euiFormRow__text"
id="generated-id-help"
>
Hide the toolbar when the mouse is not within the Canvas?
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,113 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots shareables/Footer/Settings component 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#333",
"height": undefined,
"overflow": "hidden",
"padding": 10,
"position": "relative",
"width": undefined,
}
}
>
<div
className="euiFlexGroup euiFlexGroup--alignItemsFlexEnd euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiPopover euiPopover--anchorUpRight euiPopover--withTitle"
id="settings"
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<button
aria-label="Settings"
className="euiButtonIcon euiButtonIcon--ghost"
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots shareables/Footer/Settings contextual 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#333",
"height": undefined,
"overflow": "hidden",
"padding": 10,
"position": "relative",
"width": undefined,
}
}
>
<div
className="euiFlexGroup euiFlexGroup--alignItemsFlexEnd euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiPopover euiPopover--anchorUpRight euiPopover--withTitle"
id="settings"
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<button
aria-label="Settings"
className="euiButtonIcon euiButtonIcon--ghost"
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</button>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,277 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: off 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#fff",
"border": "1px solid #ccc",
"height": 124,
"overflow": "hidden",
"padding": 16,
"position": "relative",
"width": 256,
}
}
>
<div
style={
Object {
"padding": 16,
}
}
>
<div
className="euiFormRow"
id="generated-id-row"
>
<div
className="euiFormRow__fieldWrapper"
>
<div
className="euiSwitch"
>
<input
aria-describedby="generated-id-help"
checked={false}
className="euiSwitch__input"
data-test-subj="hideToolbarSwitch"
id="generated-id"
name="toolbarHide"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="checkbox"
/>
<span
className="euiSwitch__body"
>
<span
className="euiSwitch__thumb"
/>
<span
className="euiSwitch__track"
>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon euiSwitch__icon--checked"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</span>
</span>
<label
className="euiSwitch__label"
htmlFor="generated-id"
>
Hide Toolbar
</label>
</div>
<div
className="euiFormHelpText euiFormRow__text"
id="generated-id-help"
>
Hide the toolbar when the mouse is not within the Canvas?
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: on 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#fff",
"border": "1px solid #ccc",
"height": 124,
"overflow": "hidden",
"padding": 16,
"position": "relative",
"width": 256,
}
}
>
<div
style={
Object {
"padding": 16,
}
}
>
<div
className="euiFormRow"
id="generated-id-row"
>
<div
className="euiFormRow__fieldWrapper"
>
<div
className="euiSwitch"
>
<input
aria-describedby="generated-id-help"
checked={true}
className="euiSwitch__input"
data-test-subj="hideToolbarSwitch"
id="generated-id"
name="toolbarHide"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="checkbox"
/>
<span
className="euiSwitch__body"
>
<span
className="euiSwitch__thumb"
/>
<span
className="euiSwitch__track"
>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon euiSwitch__icon--checked"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</span>
</span>
<label
className="euiSwitch__label"
htmlFor="generated-id"
>
Hide Toolbar
</label>
</div>
<div
className="euiFormHelpText euiFormRow__text"
id="generated-id-help"
>
Hide the toolbar when the mouse is not within the Canvas?
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots shareables/Footer/Settings/ToolbarSettings contextual 1`] = `
<div
className="kbnCanvas"
style={
Object {
"background": "#fff",
"border": "1px solid #ccc",
"height": 124,
"overflow": "hidden",
"padding": 16,
"position": "relative",
"width": 256,
}
}
>
<div
style={
Object {
"padding": 16,
}
}
>
<div
className="euiFormRow"
id="generated-id-row"
>
<div
className="euiFormRow__fieldWrapper"
>
<div
className="euiSwitch"
>
<input
aria-describedby="generated-id-help"
checked={false}
className="euiSwitch__input"
data-test-subj="hideToolbarSwitch"
id="generated-id"
name="toolbarHide"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="checkbox"
/>
<span
className="euiSwitch__body"
>
<span
className="euiSwitch__thumb"
/>
<span
className="euiSwitch__track"
>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
<svg
className="euiIcon euiIcon--medium euiIcon-isLoading euiSwitch__icon euiSwitch__icon--checked"
focusable="false"
height={16}
style={null}
viewBox="0 0 16 16"
width={16}
xmlns="http://www.w3.org/2000/svg"
/>
</span>
</span>
<label
className="euiSwitch__label"
htmlFor="generated-id"
>
Hide Toolbar
</label>
</div>
<div
className="euiFormHelpText euiFormRow__text"
id="generated-id-help"
>
Hide the toolbar when the mouse is not within the Canvas?
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,46 @@
/*
* 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 { AutoplaySettings, AutoplaySettingsComponent } from '../autoplay_settings';
const style = {
width: 256,
height: 228,
padding: 16,
border: '1px solid #ccc',
background: '#fff',
};
storiesOf('shareables/Footer/Settings/AutoplaySettings', module)
.add('contextual', () => (
<ExampleContext {...{ style }}>
<AutoplaySettings />
</ExampleContext>
))
.add('component: off, 2s', () => (
<ExampleContext {...{ style }}>
<AutoplaySettingsComponent
isEnabled={false}
interval="2s"
onSetAutoplay={action('onSetAutoplay')}
onSetInterval={action('onSetInterval')}
/>
</ExampleContext>
))
.add('component: on, 5s', () => (
<ExampleContext {...{ style }}>
<AutoplaySettingsComponent
isEnabled={true}
interval="5s"
onSetAutoplay={action('onSetAutoplay')}
onSetInterval={action('onSetInterval')}
/>
</ExampleContext>
));

View file

@ -0,0 +1,23 @@
/*
* 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 { Settings, SettingsComponent } from '../settings';
import { initialCanvasShareableState } from '../../../../context';
storiesOf('shareables/Footer/Settings', module)
.add('contextual', () => (
<ExampleContext style={{ background: '#333', padding: 10 }}>
<Settings />
</ExampleContext>
))
.add('component', () => (
<ExampleContext style={{ background: '#333', padding: 10 }}>
<SettingsComponent refs={initialCanvasShareableState.refs} />
</ExampleContext>
));

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import React from 'react';
import { ExampleContext } from '../../../../test/context_example';
import { ToolbarSettings, ToolbarSettingsComponent } from '../toolbar_settings';
const style = {
width: 256,
height: 124,
padding: 16,
border: '1px solid #ccc',
background: '#fff',
};
storiesOf('shareables/Footer/Settings/ToolbarSettings', module)
.add('contextual', () => (
<ExampleContext {...{ style }}>
<ToolbarSettings onSetAutohide={action('onSetAutohide')} />
</ExampleContext>
))
.add('component: on', () => (
<ExampleContext {...{ style }}>
<ToolbarSettingsComponent isAutohide={true} onSetAutohide={action('onSetAutohide')} />
</ExampleContext>
))
.add('component: off', () => (
<ExampleContext {...{ style }}>
<ToolbarSettingsComponent isAutohide={false} onSetAutohide={action('onSetAutohide')} />
</ExampleContext>
));

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,48 @@
/*
* 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 { JestContext } from '../../../../test/context_jest';
import {
getAutoplayTextField as input,
getAutoplayCheckbox as checkbox,
getAutoplaySubmit as submit,
} from '../../../../test/selectors';
import { AutoplaySettings } from '../autoplay_settings';
jest.mock('../../../../supported_renderers');
describe('<AutoplaySettings />', () => {
const wrapper = mount(
<JestContext>
<AutoplaySettings />
</JestContext>
);
test('renders as expected', () => {
expect(checkbox(wrapper).props().checked).toEqual(false);
expect(input(wrapper).props().value).toBe('5s');
});
test('activates and deactivates', () => {
checkbox(wrapper).simulate('change');
expect(checkbox(wrapper).props().checked).toEqual(true);
checkbox(wrapper).simulate('change');
expect(checkbox(wrapper).props().checked).toEqual(false);
});
test('changes properly with input', () => {
input(wrapper).simulate('change', { target: { value: '2asd' } });
expect(submit(wrapper).props().disabled).toEqual(true);
input(wrapper).simulate('change', { target: { value: '2s' } });
expect(submit(wrapper).props().disabled).toEqual(false);
expect(input(wrapper).props().value === '2s');
submit(wrapper).simulate('submit');
expect(input(wrapper).props().value === '2s');
expect(submit(wrapper).props().disabled).toEqual(false);
});
});

View file

@ -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 { mount, ReactWrapper } from 'enzyme';
import React from 'react';
import { JestContext } from '../../../../test/context_jest';
import { takeMountedSnapshot, tick } from '../../../../test';
import {
getSettingsTrigger as trigger,
getPopover as popover,
getPortal as portal,
getContextMenuItems as menuItems,
} from '../../../../test/selectors';
import { Settings } from '../settings';
jest.mock('../../../../supported_renderers');
jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`);
jest.mock('@elastic/eui/lib/components/portal/portal', () => {
// eslint-disable-next-line no-shadow
const React = require.requireActual('react');
return {
EuiPortal: (props: any) => <div>{props.children}</div>,
};
});
describe('<Settings />', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
const ref = React.createRef<HTMLDivElement>();
wrapper = mount(
<JestContext stageRef={ref}>
<div ref={ref}>
<Settings />
</div>
</JestContext>
);
});
test('renders as expected', () => {
expect(trigger(wrapper).exists()).toEqual(true);
expect(portal(wrapper).exists()).toEqual(false);
});
test('clicking settings opens and closes the menu', () => {
trigger(wrapper).simulate('click');
expect(portal(wrapper).exists()).toEqual(true);
expect(popover(wrapper).prop('isOpen')).toEqual(true);
expect(menuItems(wrapper).length).toEqual(2);
expect(portal(wrapper).text()).toEqual('SettingsAuto PlayToolbar');
trigger(wrapper).simulate('click');
expect(popover(wrapper).prop('isOpen')).toEqual(false);
});
test('can navigate Autoplay Settings', async () => {
trigger(wrapper).simulate('click');
expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot();
await tick(20);
menuItems(wrapper)
.at(0)
.simulate('click');
await tick(20);
expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot();
});
test('can navigate Toolbar Settings, closes when activated', async () => {
trigger(wrapper).simulate('click');
expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot();
menuItems(wrapper)
.at(1)
.simulate('click');
// Wait for the animation and DOM update
await tick(20);
portal(wrapper).update();
expect(portal(wrapper).html()).toMatchSnapshot();
// Click the Hide Toolbar switch
portal(wrapper)
.find('input[data-test-subj="hideToolbarSwitch"]')
.simulate('change');
// Wait for the animation and DOM update
await tick(20);
portal(wrapper).update();
// The Portal should not be open.
expect(popover(wrapper).prop('isOpen')).toEqual(false);
expect(portal(wrapper).html()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,32 @@
/*
* 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 { JestContext } from '../../../../test/context_jest';
import { getToolbarCheckbox as checkbox } from '../../../../test/selectors';
import { ToolbarSettings } from '../toolbar_settings';
jest.mock('../../../../supported_renderers');
describe('<ToolbarSettings />', () => {
const wrapper = mount(
<JestContext>
<ToolbarSettings onSetAutohide={() => {}} />
</JestContext>
);
test('renders as expected', () => {
expect(checkbox(wrapper).props().checked).toEqual(false);
});
test('activates and deactivates', () => {
checkbox(wrapper).simulate('change');
expect(checkbox(wrapper).props().checked).toEqual(true);
checkbox(wrapper).simulate('change');
expect(checkbox(wrapper).props().checked).toEqual(false);
});
});

View file

@ -0,0 +1,83 @@
/*
* 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 { EuiHorizontalRule, EuiSwitch } from '@elastic/eui';
import {
useCanvasShareableState,
setAutoplayAction,
setAutoplayIntervalAction,
} from '../../../context';
import { createTimeInterval } from '../../../../public/lib/time_interval';
// @ts-ignore Untyped local
import { CustomInterval } from '../../../../public/components/workpad_header/control_settings/custom_interval';
export type onSetAutoplayFn = (autoplay: boolean) => void;
export type onSetIntervalFn = (interval: string) => void;
export interface Props {
/**
* True if autoplay is currently enabled, false otherwise.
*/
isEnabled: boolean;
/**
* The interval with which to move between pages.
*/
interval: string;
/**
* The handler to invoke when Autoplay is enabled or disabled.
*/
onSetAutoplay: onSetAutoplayFn;
/**
* The handler to invoke when the autoplay interval is set.
*/
onSetInterval: onSetIntervalFn;
}
/**
* The panel used to configure Autolay in Shareable Canvas Workpads.
*/
export const AutoplaySettingsComponent: FC<Props> = ({
isEnabled,
interval,
onSetAutoplay,
onSetInterval,
}: Props) => (
<div style={{ padding: 16 }}>
<EuiSwitch
name="cycle"
id="cycle"
label="Cycle Slides"
checked={isEnabled}
onChange={() => onSetAutoplay(!isEnabled)}
/>
<EuiHorizontalRule margin="m" />
<CustomInterval
defaultValue={interval}
onSubmit={(value: number) => onSetInterval(createTimeInterval(value))}
/>
</div>
);
/**
* A store-connected container for the `AutoplaySettings` component.
*/
export const AutoplaySettings = () => {
const [{ settings }, dispatch] = useCanvasShareableState();
const { autoplay } = settings;
const { isEnabled, interval } = autoplay;
const onSetInterval: onSetIntervalFn = (newInterval: string) =>
dispatch(setAutoplayIntervalAction(newInterval));
const onSetAutoplay: onSetAutoplayFn = (enabled: boolean) => dispatch(setAutoplayAction(enabled));
return <AutoplaySettingsComponent {...{ isEnabled, interval, onSetAutoplay, onSetInterval }} />;
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './settings';

View file

@ -0,0 +1,104 @@
/*
* 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, FC } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiPopover, EuiContextMenu } from '@elastic/eui';
import { useCanvasShareableState } from '../../../context';
import { Refs } from '../../../types';
import { ToolbarSettings } from './toolbar_settings';
import { AutoplaySettings } from './autoplay_settings';
interface Props {
/**
* A collection of React `ref` objects for the Shareable Runtime.
*/
refs: Refs;
}
/**
* The Settings Popover for Canvas Shareable Workpads.
*/
export const SettingsComponent: FC<Props> = ({ refs }) => {
const [isPopoverOpen, setPopoverOpen] = useState(false);
const button = (
<EuiButtonIcon
color="ghost"
iconType="gear"
aria-label="Settings"
onClick={() => setPopoverOpen(!isPopoverOpen)}
/>
);
const flattenPanelTree = (tree: any, array: any[] = []) => {
array.push(tree);
if (tree.items) {
tree.items.forEach((item: any) => {
if (item.panel) {
flattenPanelTree(item.panel, array);
item.panel = item.panel.id;
}
});
}
return array;
};
const panels = flattenPanelTree({
id: 0,
title: 'Settings',
items: [
{
name: 'Auto Play',
icon: 'play',
panel: {
id: 1,
title: 'Auto Play',
content: <AutoplaySettings />,
},
},
{
name: 'Toolbar',
icon: 'boxesHorizontal',
panel: {
id: 2,
title: 'Toolbar',
content: <ToolbarSettings onSetAutohide={() => setPopoverOpen(false)} />,
},
},
],
});
return (
<EuiFlexGroup alignItems="flexEnd" justifyContent="center" direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiPopover
closePopover={() => setPopoverOpen(false)}
id="settings"
isOpen={isPopoverOpen}
button={button}
panelPaddingSize="none"
withTitle
anchorPosition="upRight"
insert={
refs.stage.current ? { sibling: refs.stage.current, position: 'after' } : undefined
}
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
);
};
/**
* A store-connected container for the `Settings` component.
*/
export const Settings: FC<{}> = () => {
const [{ refs }] = useCanvasShareableState();
return <SettingsComponent refs={refs} />;
};

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { EuiSwitch, EuiFormRow } from '@elastic/eui';
import { useCanvasShareableState, setToolbarAutohideAction } from '../../../context';
export type onSetAutohideFn = (isAutohide: boolean) => void;
export interface Props {
/**
* True if the toolbar should be hidden when the mouse is not within the workpad,
* false otherwise.
*/
isAutohide: boolean;
/**
* The handler to invoke when autohide is set.
*/
onSetAutohide: onSetAutohideFn;
}
/**
* The settings panel for the Toolbar of a Shareable Canvas Workpad.
*/
export const ToolbarSettingsComponent = ({ isAutohide, onSetAutohide }: Props) => {
return (
<div style={{ padding: 16 }}>
<EuiFormRow helpText="Hide the toolbar when the mouse is not within the Canvas?">
<EuiSwitch
data-test-subj="hideToolbarSwitch"
name="toolbarHide"
id="toolbarHide"
label="Hide Toolbar"
checked={isAutohide}
onChange={() => onSetAutohide(!isAutohide)}
/>
</EuiFormRow>
</div>
);
};
/**
* A store-connected container for the `ToolbarSettings` component.
*/
export const ToolbarSettings: FC<Pick<Props, 'onSetAutohide'>> = ({ onSetAutohide }) => {
const [{ settings }, dispatch] = useCanvasShareableState();
const { toolbar } = settings;
const { isAutohide } = toolbar;
const onSetAutohideFn: onSetAutohideFn = (autohide: boolean) => {
onSetAutohide(autohide);
dispatch(setToolbarAutohideAction(autohide));
};
return <ToolbarSettingsComponent onSetAutohide={onSetAutohideFn} {...{ isAutohide }} />;
};

View file

@ -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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiLink } from '@elastic/eui';
import { useCanvasShareableState } from '../../context';
interface Props {
/**
* The title of the workpad being shared.
*/
title: string;
}
/**
* The title of the workpad displayed in the left-hand of the footer.
*/
export const TitleComponent: FC<Props> = ({ title }) => (
<EuiFlexGroup gutterSize="s" justifyContent="flexStart" alignItems="center">
<EuiFlexItem grow={false}>
<EuiLink href="https://www.elastic.co" title="Powered by Elastic.co">
<EuiIcon type="logoElastic" size="l" />
</EuiLink>
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ minWidth: 0, cursor: 'default' }}>
<EuiText color="ghost" size="s">
<div className="eui-textTruncate">{title}</div>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
/**
* A store-connected container for the `Title` component.
*/
export const Title: FC<{}> = () => {
const [{ workpad }] = useCanvasShareableState();
if (!workpad) {
return null;
}
const { name: title } = workpad;
return <TitleComponent {...{ title }} />;
};

View file

@ -0,0 +1,6 @@
.root {
composes: canvasPage from global;
composes: canvasInteractivePage from global;
composes: kbn-resetFocusState from global;
overflow: 'hidden';
}

View file

@ -0,0 +1,67 @@
/*
* 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 { RenderedElement } from './rendered_element';
import { CanvasRenderedPage, CanvasRenderedElement } from '../types';
import { useCanvasShareableState } from '../context';
import css from './page.module.scss';
interface ComponentProps {
/**
* The height of the page, in pixels.
*/
height: number;
/**
* The width of the page, in pixels.
*/
width: number;
/**
* An object describing the Page, taken from a Shareable Workpad.
*/
page: CanvasRenderedPage;
}
/**
* A Page in the Shareable Workpad is conceptually identical to a Page in a Workpad.
*/
export const PageComponent: FC<ComponentProps> = ({ page, height, width }) => {
const { elements, style, id } = page;
const output = elements.map((element: CanvasRenderedElement, i) => (
<RenderedElement key={element.id} element={element} index={i + 1} />
));
return (
<div {...{ id }} className={css.root} style={{ height, width, ...style }}>
{output}
</div>
);
};
interface Props {
/**
* The zero-based index of the page relative others within the workpad.
*/
index: number;
}
/**
* A store-connected container for the `Page` component.
*/
export const Page: FC<Props> = ({ index }) => {
const [{ workpad }] = useCanvasShareableState();
if (!workpad) {
return null;
}
const { height, width, pages } = workpad;
const page = pages[index];
return <PageComponent {...{ page, height, width }} />;
};

View file

@ -0,0 +1,21 @@
:global .kbnCanvas :local .root,
:global .kbnCanvas :local .render {
height: 100%;
width: 100%;
}
.container {
composes: canvas__element from global;
composes: canvasElement from global;
}
.content {
composes: canvasElement__content from global;
}
.renderContainer {
composes: canvasWorkpad--element_render from global;
composes: canvasRenderEl from global;
height: 100%;
width: 100%;
}

View file

@ -0,0 +1,118 @@
/*
* 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, PureComponent } from 'react';
// @ts-ignore Untyped library
import Style from 'style-it';
// @ts-ignore Untyped local
import { Positionable } from '../../public/components/positionable/positionable';
// @ts-ignore Untyped local
import { elementToShape } from '../../public/components/workpad_page/utils';
import { CanvasRenderedElement } from '../types';
import { CanvasShareableContext, useCanvasShareableState } from '../context';
import { RendererSpec } from '../../types';
import css from './rendered_element.module.scss';
export interface Props {
/**
* An object describing the transient, independently renderable Element.
*/
element: CanvasRenderedElement;
/**
* The index of the Element relative to other Elements on the Page. This is
* primarily used for z-indexing.
*/
index: number;
/**
* The Expression function that evaluates the state of the Element and renders
* it to the Page.
*/
fn: RendererSpec;
}
/**
* A Rendered Element is different from an Element added to a Canvas Workpad. A
* Rendered Element has actually be evaluated already to gather any data from
* datasources, and is just a simple expression to render the result. This
* component renders that "transient" element state.
*/
export class RenderedElementComponent extends PureComponent<Props> {
static contextType = CanvasShareableContext;
protected ref: React.RefObject<HTMLDivElement>;
constructor(props: Props) {
super(props);
this.ref = React.createRef();
}
componentDidMount() {
const { element, fn } = this.props;
const { expressionRenderable } = element;
const { value } = expressionRenderable;
const { as } = value;
if (!this.ref.current) {
return null;
}
try {
// TODO: These are stubbed, but may need implementation.
fn.render(this.ref.current, value.value, {
done: () => {},
onDestroy: () => {},
onResize: () => {},
setFilter: () => {},
getFilter: () => '',
});
} catch (e) {
// eslint-disable-next-line no-console
console.log(as, e.message);
}
}
render() {
const { element, index } = this.props;
const shape = elementToShape(element, index || 1);
const { id, expressionRenderable, position } = element;
const { value } = expressionRenderable;
const { as, css: elementCSS, containerStyle } = value;
const { height, width } = position;
return (
<Positionable height={height} width={width} transformMatrix={shape.transformMatrix}>
<div className={css.root}>
{Style.it(
elementCSS,
<div className={css.container} style={{ ...containerStyle }}>
<div className={css.content}>
<div className={css.renderContainer}>
<div key={id} ref={this.ref} data-renderer={as} className={css.render} />
</div>
</div>
</div>
)}
</div>
</Positionable>
);
}
}
/**
* A store-connected container for the `RenderedElement` component.
*/
export const RenderedElement: FC<Pick<Props, 'element' | 'index'>> = ({ index, element }) => {
const [{ renderers }] = useCanvasShareableState();
const { expressionRenderable } = element;
const { value } = expressionRenderable;
const { as } = value;
const fn = renderers[as];
return <RenderedElementComponent {...{ element, fn, index }} />;
};

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const KIBANA_ROOT: string;
export const LIBRARY_NAME: string;
export const SHAREABLE_RUNTIME_FILE: string;
export const SHAREABLE_RUNTIME_NAME: string;
export const SHAREABLE_RUNTIME_OUTPUT: string;
export const SHAREABLE_RUNTIME_SRC: string;
export const STATS_OUTPUT: string;

View file

@ -0,0 +1,33 @@
/*
* 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 path = require('path');
const LIBRARY_NAME = 'KbnCanvas';
const SHAREABLE_RUNTIME_NAME = 'kbn_canvas';
const SHAREABLE_RUNTIME_CSS_NAME = 'kbn_canvas_css';
const KIBANA_ROOT = path.resolve(__dirname, '../../../../..');
const SHAREABLE_RUNTIME_SRC = path.resolve(
KIBANA_ROOT,
'x-pack/legacy/plugins/canvas/shareable_runtime'
);
const SHAREABLE_RUNTIME_OUTPUT = path.resolve(SHAREABLE_RUNTIME_SRC, 'build');
const SHAREABLE_RUNTIME_FILE = path.resolve(
SHAREABLE_RUNTIME_OUTPUT,
SHAREABLE_RUNTIME_NAME + '.js'
);
const STATS_OUTPUT = path.resolve(SHAREABLE_RUNTIME_OUTPUT, 'webpack_stats.json');
module.exports = {
KIBANA_ROOT,
LIBRARY_NAME,
SHAREABLE_RUNTIME_CSS_NAME,
SHAREABLE_RUNTIME_FILE,
SHAREABLE_RUNTIME_NAME,
SHAREABLE_RUNTIME_OUTPUT,
SHAREABLE_RUNTIME_SRC,
STATS_OUTPUT,
};

View file

@ -0,0 +1,82 @@
/*
* 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.
*/
/**
* This enumeration applies a strong type to all of the actions that can be
* triggered from the interface.
*/
export enum CanvasShareableActions {
SET_WORKPAD = 'SET_WORKPAD',
SET_PAGE = 'SET_PAGE',
SET_SCRUBBER_VISIBLE = 'SET_SCRUBBER_VISIBLE',
SET_AUTOPLAY = 'SET_AUTOPLAY',
SET_AUTOPLAY_INTERVAL = 'SET_AUTOPLAY_INTERVAL',
SET_TOOLBAR_AUTOHIDE = 'SET_TOOLBAR_AUTOHIDE',
}
interface FluxAction<T, P> {
type: T;
payload: P;
}
const createAction = <T extends CanvasShareableActions, P>(
type: T,
payload: P
): FluxAction<T, P> => ({
type,
payload,
});
/**
* Set the current page to display
* @param page The zero-indexed page to display.
*/
export const setPageAction = (page: number) =>
createAction(CanvasShareableActions.SET_PAGE, { page });
/**
* Set the visibility of the page scrubber.
* @param visible True if it should be visible, false otherwise.
*/
export const setScrubberVisibleAction = (visible: boolean) => {
return createAction(CanvasShareableActions.SET_SCRUBBER_VISIBLE, { visible });
};
/**
* Set whether the slides should automatically advance.
* @param autoplay True if it should automatically advance, false otherwise.
*/
export const setAutoplayAction = (isEnabled: boolean) =>
createAction(CanvasShareableActions.SET_AUTOPLAY, { isEnabled });
/**
* Set the interval in which slide will advance. This is a `string` identical to
* that used in Canvas proper: `1m`, `2s`, etc.
* @param autoplay The interval in which slides should advance.
*/
export const setAutoplayIntervalAction = (interval: string) =>
createAction(CanvasShareableActions.SET_AUTOPLAY_INTERVAL, { interval });
/**
* Set if the toolbar should be hidden if the mouse is not within the bounds of the
* Canvas Shareable Workpad.
* @param autohide True if the toolbar should hide, false otherwise.
*/
export const setToolbarAutohideAction = (isAutohide: boolean) =>
createAction(CanvasShareableActions.SET_TOOLBAR_AUTOHIDE, { isAutohide });
const actions = {
setPageAction,
setScrubberVisibleAction,
setAutoplayAction,
setAutoplayIntervalAction,
setToolbarAutohideAction,
};
/**
* Strongly-types the correlation between an `action` and its return.
*/
export type CanvasShareableAction = ReturnType<typeof actions[keyof typeof actions]>;

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './state';
export * from './actions';

View file

@ -0,0 +1,96 @@
/*
* 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 { CanvasShareableState } from '../types';
import { CanvasShareableAction, CanvasShareableActions } from './actions';
/**
* The Action Reducer for the Shareable Canvas Workpad interface.
*/
export const reducer = (
state: CanvasShareableState,
action: CanvasShareableAction
): CanvasShareableState => {
switch (action.type) {
case CanvasShareableActions.SET_PAGE: {
const { stage } = state;
return {
...state,
stage: {
...stage,
page: action.payload.page,
},
};
}
case CanvasShareableActions.SET_SCRUBBER_VISIBLE: {
const { footer } = state;
return {
...state,
footer: {
...footer,
isScrubberVisible: action.payload.visible,
},
};
}
case CanvasShareableActions.SET_AUTOPLAY: {
const { settings } = state;
const { autoplay } = settings;
const { isEnabled } = action.payload;
return {
...state,
settings: {
...settings,
autoplay: {
...autoplay,
isEnabled,
},
},
};
}
case CanvasShareableActions.SET_AUTOPLAY_INTERVAL: {
const { settings } = state;
const { autoplay } = settings;
const { interval } = action.payload;
return {
...state,
settings: {
...settings,
autoplay: {
...autoplay,
interval,
},
},
};
}
case CanvasShareableActions.SET_TOOLBAR_AUTOHIDE: {
const { settings } = state;
const { toolbar } = settings;
const { isAutohide } = action.payload;
return {
...state,
settings: {
...settings,
toolbar: {
...toolbar,
isAutohide,
},
},
};
}
default: {
return state;
}
}
};

View file

@ -0,0 +1,59 @@
/*
* 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, { createContext, useContext, Dispatch, useReducer, ReactChild } from 'react';
import { CanvasShareableState } from '../types';
import { reducer } from './reducer';
import { CanvasShareableAction } from './actions';
type StateType = [CanvasShareableState, Dispatch<CanvasShareableAction>];
/**
* The initial state for the Canvas Shareable Runtime.
*/
export const initialCanvasShareableState: CanvasShareableState = {
renderers: {},
workpad: null,
stage: {
page: 0,
height: 400,
width: 600,
},
footer: {
isScrubberVisible: false,
},
settings: {
autoplay: {
isEnabled: false,
interval: '5s',
},
toolbar: {
isAutohide: false,
},
},
refs: {
stage: React.createRef(),
},
};
export const CanvasShareableContext = createContext<StateType>([
initialCanvasShareableState,
() => {},
]);
export const CanvasShareableStateProvider = ({
initialState,
children,
}: {
initialState: CanvasShareableState;
children: ReactChild;
}) => (
<CanvasShareableContext.Provider value={useReducer(reducer, initialState)}>
{children}
</CanvasShareableContext.Provider>
);
export const useCanvasShareableState = () => useContext<StateType>(CanvasShareableContext);

View file

@ -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.
*/
declare module '*.module.scss' {
const styles: { [className: string]: string };
// eslint-disable-next-line
export default styles;
}

Some files were not shown because too many files have changed in this diff Show more