From 261cb25cdf432e220472d6229b38a8c257bfcbc7 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Thu, 10 Jun 2021 15:42:30 -0500 Subject: [PATCH 1/3] [canvas] Refactor Storybook from bespoke to standard configuration --- packages/kbn-storybook/package.json | 2 +- test/scripts/jenkins_storybook.sh | 3 - x-pack/package.json | 1 - x-pack/plugins/canvas/CONTRIBUTING.md | 25 +-- x-pack/plugins/canvas/scripts/storybook.js | 124 ----------- .../canvas/shareable_runtime/README.mdx | 2 +- x-pack/plugins/canvas/storybook/.babelrc | 9 - .../canvas/storybook/addon/src/register.tsx | 15 ++ x-pack/plugins/canvas/storybook/constants.js | 18 -- x-pack/plugins/canvas/storybook/constants.ts | 10 + .../plugins/canvas/storybook/dll_contexts.js | 13 -- x-pack/plugins/canvas/storybook/main.ts | 67 ++++-- x-pack/plugins/canvas/storybook/manager.ts | 23 -- x-pack/plugins/canvas/storybook/middleware.ts | 18 -- .../canvas/storybook/preview-head.html | 6 - x-pack/plugins/canvas/storybook/preview.ts | 3 - .../canvas/storybook/webpack.config.js | 210 ------------------ .../canvas/storybook/webpack.dll.config.js | 110 --------- 18 files changed, 79 insertions(+), 580 deletions(-) delete mode 100644 x-pack/plugins/canvas/scripts/storybook.js delete mode 100644 x-pack/plugins/canvas/storybook/.babelrc delete mode 100644 x-pack/plugins/canvas/storybook/constants.js create mode 100644 x-pack/plugins/canvas/storybook/constants.ts delete mode 100644 x-pack/plugins/canvas/storybook/dll_contexts.js delete mode 100644 x-pack/plugins/canvas/storybook/manager.ts delete mode 100644 x-pack/plugins/canvas/storybook/middleware.ts delete mode 100644 x-pack/plugins/canvas/storybook/preview-head.html delete mode 100644 x-pack/plugins/canvas/storybook/webpack.config.js delete mode 100644 x-pack/plugins/canvas/storybook/webpack.dll.config.js diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index 0e70f7c340a90..f2e4c9b3418b1 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -11,6 +11,6 @@ "scripts": { "build": "../../node_modules/.bin/tsc", "kbn:bootstrap": "yarn build", - "watch": "yarn build --watch" + "kbn:watch": "yarn build --watch" } } \ No newline at end of file diff --git a/test/scripts/jenkins_storybook.sh b/test/scripts/jenkins_storybook.sh index 5c99654f16cbe..73ab43fd02eba 100755 --- a/test/scripts/jenkins_storybook.sh +++ b/test/scripts/jenkins_storybook.sh @@ -2,9 +2,6 @@ source src/dev/ci_setup/setup_env.sh -cd "$XPACK_DIR/plugins/canvas" -node scripts/storybook --dll - cd "$KIBANA_DIR" yarn storybook --site apm diff --git a/x-pack/package.json b/x-pack/package.json index 04f808c89764d..84fd5ba081d8f 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -7,7 +7,6 @@ "scripts": { "github-checks-reporter": "../node_modules/.bin/github-checks-reporter", "kbn": "node ../scripts/kbn", - "kbn:bootstrap": "node plugins/canvas/scripts/storybook --clean", "start": "node ../scripts/kibana --dev", "build": "node --preserve-symlinks ../node_modules/.bin/gulp build", "test:jest": "node ../scripts/jest" diff --git a/x-pack/plugins/canvas/CONTRIBUTING.md b/x-pack/plugins/canvas/CONTRIBUTING.md index 538cc1592c3bc..d3bff67771244 100644 --- a/x-pack/plugins/canvas/CONTRIBUTING.md +++ b/x-pack/plugins/canvas/CONTRIBUTING.md @@ -113,27 +113,4 @@ Canvas uses [Storybook](https://storybook.js.org) to test and develop components ### Using Storybook -The Canvas Storybook instance can be started by running `node scripts/storybook` from the Canvas root directory. It has a number of options: - -``` -node scripts/storybook - - Storybook runner for Canvas. - - Options: - --clean Forces a clean of the Storybook DLL and exits. - --dll Cleans and builds the Storybook dependency DLL and exits. - --stats Produces a Webpack stats file. - --site Produces a site deployment of this Storybook. - --verbose, -v Log verbosely - --debug Log debug messages (less than verbose) - --quiet Only log errors - --silent Don't log anything - --help Show this message -``` - -### What about `kbn-storybook`? - -Canvas wants to move to the Kibana Storybook instance as soon as feasible. There are few tweaks Canvas makes to Storybook, so we're actively working with the maintainers to make that migration successful. - -In the meantime, people can test our progress by running `node scripts/storybook_new` from the Canvas root. +The Canvas Storybook instance can be started by running `yarn storybook canvas` from the Kibana root directory. diff --git a/x-pack/plugins/canvas/scripts/storybook.js b/x-pack/plugins/canvas/scripts/storybook.js deleted file mode 100644 index e6b8a66b9026f..0000000000000 --- a/x-pack/plugins/canvas/scripts/storybook.js +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -const path = require('path'); -const fs = require('fs'); -const del = require('del'); -const { run } = require('@kbn/dev-utils'); -// This is included in the main Kibana package.json -// eslint-disable-next-line import/no-extraneous-dependencies -const storybook = require('@storybook/react/standalone'); -const execa = require('execa'); -const { DLL_OUTPUT } = require('./../storybook/constants'); - -const storybookOptions = { - configDir: path.resolve(__dirname, './../storybook'), - mode: 'dev', -}; - -run( - ({ log, flags }) => { - const { addon, dll, clean, stats, site } = flags; - - // Delete the existing DLL if we're cleaning or building. - if (clean || dll) { - del.sync([DLL_OUTPUT], { force: true }); - - if (clean) { - return; - } - } - - // Build the DLL if necessary. - if (fs.existsSync(DLL_OUTPUT)) { - log.info('storybook: DLL exists from previous build; skipping'); - } else { - log.info('storybook: Building DLL'); - execa.sync( - 'yarn', - [ - 'webpack', - '--config', - 'x-pack/plugins/canvas/storybook/webpack.dll.config.js', - ...(process.stdout.isTTY && !process.env.CI ? ['--progress'] : []), - '--hide-modules', - '--display-entrypoints', - 'false', - ], - { - cwd: path.resolve(__dirname, '../../../..'), - stdio: ['ignore', 'inherit', 'inherit'], - buffer: false, - } - ); - log.success('storybook: DLL built'); - } - - // If we're only building the DLL, we're done. - if (dll) { - return; - } - - // Build statistics and exit - if (stats) { - log.success('storybook: Generating Storybook statistics'); - storybook({ - ...storybookOptions, - smokeTest: true, - }); - return; - } - - // Build the addon - execa.sync('node', ['scripts/build'], { - cwd: path.resolve(__dirname, '../storybook/addon'), - stdio: ['ignore', 'inherit', 'inherit'], - buffer: false, - }); - - // Build site and exit - if (site) { - log.success('storybook: Generating Storybook site'); - storybook({ - ...storybookOptions, - mode: 'static', - outputDir: path.resolve(__dirname, './../storybook/build'), - }); - return; - } - - log.info('storybook: Starting Storybook'); - - if (addon) { - execa('node', ['scripts/build', '--watch'], { - cwd: path.resolve(__dirname, '../storybook/addon'), - stdio: ['ignore', 'inherit', 'inherit'], - buffer: false, - }); - } - - storybook({ - ...storybookOptions, - port: 9001, - }); - }, - { - description: ` - Storybook runner for Canvas. - `, - flags: { - boolean: ['addon', 'dll', 'clean', 'stats', 'site'], - help: ` - --addon Watch the addon source code for changes. - --clean Forces a clean of the Storybook DLL and exits. - --dll Cleans and builds the Storybook dependency DLL and exits. - --stats Produces a Webpack stats file. - --site Produces a site deployment of this Storybook. - `, - }, - } -); diff --git a/x-pack/plugins/canvas/shareable_runtime/README.mdx b/x-pack/plugins/canvas/shareable_runtime/README.mdx index 8f1c9e06ca788..a9d58bb3833d9 100644 --- a/x-pack/plugins/canvas/shareable_runtime/README.mdx +++ b/x-pack/plugins/canvas/shareable_runtime/README.mdx @@ -153,7 +153,7 @@ You can test this functionality in a number of ways. The easiest would be: ### Run the Canvas Storybook -From `/canvas`: `node scripts/storybook` +From `/kibana`: `yarn storybook canvas` ### Run the Jest Tests diff --git a/x-pack/plugins/canvas/storybook/.babelrc b/x-pack/plugins/canvas/storybook/.babelrc deleted file mode 100644 index c8f2d480f3a09..0000000000000 --- a/x-pack/plugins/canvas/storybook/.babelrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "env": { - "test": { - "plugins": [ - "require-context-hook" - ] - } - } -} diff --git a/x-pack/plugins/canvas/storybook/addon/src/register.tsx b/x-pack/plugins/canvas/storybook/addon/src/register.tsx index 2a0df578ff22c..1e99d6bdb74eb 100644 --- a/x-pack/plugins/canvas/storybook/addon/src/register.tsx +++ b/x-pack/plugins/canvas/storybook/addon/src/register.tsx @@ -9,6 +9,8 @@ import React from 'react'; import { addons, types } from '@storybook/addons'; import { AddonPanel } from '@storybook/components'; import { STORY_CHANGED } from '@storybook/core-events'; +import { create } from '@storybook/theming'; +import { PANEL_ID } from '@storybook/addon-actions'; import { ADDON_ID, EVENTS, ACTIONS_PANEL_ID } from './constants'; import { Panel } from './panel'; @@ -32,3 +34,16 @@ addons.register(ADDON_ID, (api) => { }, }); }); + +addons.setConfig({ + theme: create({ + base: 'light', + brandTitle: 'Canvas Storybook', + brandUrl: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/canvas', + }), + showPanel: true, + isFullscreen: false, + panelPosition: 'bottom', + isToolshown: true, + selectedPanel: PANEL_ID, +}); diff --git a/x-pack/plugins/canvas/storybook/constants.js b/x-pack/plugins/canvas/storybook/constants.js deleted file mode 100644 index c2a4f414588d4..0000000000000 --- a/x-pack/plugins/canvas/storybook/constants.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -const path = require('path'); - -const DLL_NAME = 'canvas_storybook_dll'; -const KIBANA_ROOT = path.resolve(__dirname, '../../../..'); -const DLL_OUTPUT = path.resolve(KIBANA_ROOT, 'built_assets', DLL_NAME); - -module.exports = { - DLL_NAME, - KIBANA_ROOT, - DLL_OUTPUT, -}; diff --git a/x-pack/plugins/canvas/storybook/constants.ts b/x-pack/plugins/canvas/storybook/constants.ts new file mode 100644 index 0000000000000..711b939179452 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import path from 'path'; + +export const KIBANA_ROOT = path.resolve(__dirname, '../../../..'); diff --git a/x-pack/plugins/canvas/storybook/dll_contexts.js b/x-pack/plugins/canvas/storybook/dll_contexts.js deleted file mode 100644 index 12f0723ae8b77..0000000000000 --- a/x-pack/plugins/canvas/storybook/dll_contexts.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// This file defines CSS and Legacy style contexts for use in the DLL. This file -// is also require'd in the Storybook config so that the Storybook Webpack instance -// is aware of them, and can load them from the DLL. - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -require('../../../../src/core/server/core_app/assets/legacy_light_theme.css'); diff --git a/x-pack/plugins/canvas/storybook/main.ts b/x-pack/plugins/canvas/storybook/main.ts index 79e64caffd3dc..80a8aeb14a804 100644 --- a/x-pack/plugins/canvas/storybook/main.ts +++ b/x-pack/plugins/canvas/storybook/main.ts @@ -5,23 +5,58 @@ * 2.0. */ -/* eslint-disable @typescript-eslint/no-var-requires */ -const { existsSync } = require('fs'); -const { join } = require('path'); +import { resolve } from 'path'; +import webpackMerge from 'webpack-merge'; +import { defaultConfig } from '@kbn/storybook'; -// Check for DLL if we're not running in Jest -if ( - !existsSync(join(__dirname, '../../../../built_assets/canvas_storybook_dll/manifest.json')) && - !process.env.JEST_WORKER_ID -) { - // eslint-disable-next-line no-console - console.error( - 'No DLL found. Run `node scripts/storybook --dll` from the Canvas plugin directory.' - ); - process.exit(1); -} +import type { Configuration } from 'webpack'; + +import { KIBANA_ROOT } from './constants'; + +const canvasWebpack = { + module: { + rules: [ + // Enable CSS Modules in Storybook (Shareable Runtime) + { + test: /\.module\.s(a|c)ss$/, + loader: [ + 'style-loader', + { + loader: 'css-loader', + options: { + importLoaders: 2, + modules: { + localIdentName: '[name]__[local]___[hash:base64:5]', + }, + }, + }, + { + loader: 'postcss-loader', + options: { + path: resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, + }, + { + loader: 'sass-loader', + }, + ], + }, + // Exclude large-dependency, troublesome or irrelevant modules. + { + test: [ + resolve(KIBANA_ROOT, 'x-pack/plugins/canvas/public/components/embeddable_flyout'), + resolve(KIBANA_ROOT, 'x-pack/plugins/reporting/public'), + resolve(KIBANA_ROOT, 'src/plugins/kibana_legacy/public/angular'), + resolve(KIBANA_ROOT, 'src/plugins/kibana_legacy/public/paginate'), + ], + use: 'null-loader', + }, + ], + }, +}; module.exports = { - stories: ['../**/*.stories.tsx'], - addons: ['@storybook/addon-actions', '@storybook/addon-knobs', './addon/target/register'], + ...defaultConfig, + addons: [...(defaultConfig.addons || []), './addon/target/register'], + webpackFinal: (config: Configuration) => webpackMerge(config, canvasWebpack), }; diff --git a/x-pack/plugins/canvas/storybook/manager.ts b/x-pack/plugins/canvas/storybook/manager.ts deleted file mode 100644 index 27abc9eba3854..0000000000000 --- a/x-pack/plugins/canvas/storybook/manager.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { addons } from '@storybook/addons'; -import { create } from '@storybook/theming'; -import { PANEL_ID } from '@storybook/addon-actions'; - -addons.setConfig({ - theme: create({ - base: 'light', - brandTitle: 'Canvas Storybook', - brandUrl: 'https://github.com/elastic/kibana/tree/master/x-pack/plugins/canvas', - }), - showPanel: true, - isFullscreen: false, - panelPosition: 'bottom', - isToolshown: true, - selectedPanel: PANEL_ID, -}); diff --git a/x-pack/plugins/canvas/storybook/middleware.ts b/x-pack/plugins/canvas/storybook/middleware.ts deleted file mode 100644 index 1662c28b238d7..0000000000000 --- a/x-pack/plugins/canvas/storybook/middleware.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import path from 'path'; -// @ts-expect-error -import serve from 'serve-static'; - -// Extend the Storybook Middleware to include a route to access Legacy UI assets -module.exports = function (router: { get: (...args: any[]) => void }) { - router.get( - '/ui', - serve(path.resolve(__dirname, '../../../../../src/core/server/core_app/assets')) - ); -}; diff --git a/x-pack/plugins/canvas/storybook/preview-head.html b/x-pack/plugins/canvas/storybook/preview-head.html deleted file mode 100644 index f8a7de6ddbaf1..0000000000000 --- a/x-pack/plugins/canvas/storybook/preview-head.html +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/x-pack/plugins/canvas/storybook/preview.ts b/x-pack/plugins/canvas/storybook/preview.ts index 3c38f4ed5c682..f885a654cdab8 100644 --- a/x-pack/plugins/canvas/storybook/preview.ts +++ b/x-pack/plugins/canvas/storybook/preview.ts @@ -10,9 +10,6 @@ import { action } from '@storybook/addon-actions'; import { startServices } from '../public/services/stubs'; import { addDecorators } from './decorators'; -// Import the modules from the DLL. -import './dll_contexts'; - // Import Canvas CSS import '../public/style/index.scss'; diff --git a/x-pack/plugins/canvas/storybook/webpack.config.js b/x-pack/plugins/canvas/storybook/webpack.config.js deleted file mode 100644 index 77b8d343a2bea..0000000000000 --- a/x-pack/plugins/canvas/storybook/webpack.config.js +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -const path = require('path'); -const webpack = require('webpack'); -const webpackMerge = require('webpack-merge'); -const { stringifyRequest } = require('loader-utils'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const { DLL_OUTPUT, KIBANA_ROOT } = require('./constants'); - -// Extend the Storybook Webpack config with some customizations -module.exports = async ({ config: storybookConfig }) => { - const config = { - module: { - rules: [ - // Include the React preset from Kibana for JS(X) and TS(X) - { - test: /\.(j|t)sx?$/, - exclude: /node_modules/, - loaders: 'babel-loader', - options: { - presets: [require.resolve('@kbn/babel-preset/webpack_preset')], - }, - }, - // Parse props data for .tsx files - // This is notoriously slow, and is making Storybook unusable. Disabling for now. - // See: https://github.com/storybookjs/storybook/issues/7998 - // - // { - // test: /\.tsx$/, - // // Exclude example files, as we don't display props info for them - // exclude: /\.examples.tsx$/, - // use: [ - // // Parse TS comments to create Props tables in the UI - // require.resolve('react-docgen-typescript-loader'), - // ], - // }, - // Enable SASS, but exclude CSS Modules in Storybook - { - test: /\.scss$/, - exclude: /\.module.(s(a|c)ss)$/, - use: [ - { loader: 'style-loader' }, - { loader: 'css-loader', options: { importLoaders: 2 } }, - { - loader: 'postcss-loader', - options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), - }, - }, - { - loader: 'sass-loader', - options: { - prependData(loaderContext) { - return `@import ${stringifyRequest( - loaderContext, - path.resolve( - KIBANA_ROOT, - 'src/core/public/core_app/styles/_globals_v7light.scss' - ) - )};\n`; - }, - sassOptions: { - includePaths: [path.resolve(KIBANA_ROOT, 'node_modules')], - }, - }, - }, - ], - }, - // Enable CSS Modules in Storybook (Shareable Runtime) - { - test: /\.module\.s(a|c)ss$/, - loader: [ - 'style-loader', - { - loader: 'css-loader', - options: { - importLoaders: 2, - modules: { - localIdentName: '[name]__[local]___[hash:base64:5]', - }, - }, - }, - { - loader: 'postcss-loader', - options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), - }, - }, - { - loader: 'sass-loader', - }, - ], - }, - { - test: /\.mjs$/, - include: /node_modules/, - type: 'javascript/auto', - }, - // Exclude large-dependency, troublesome or irrelevant modules. - { - test: [ - path.resolve(__dirname, '../public/components/embeddable_flyout'), - path.resolve(__dirname, '../../reporting/public'), - path.resolve(__dirname, '../../../../src/plugins/kibana_legacy/public/angular'), - path.resolve(__dirname, '../../../../src/plugins/kibana_legacy/public/paginate'), - ], - use: 'null-loader', - }, - ], - }, - plugins: [ - // Reference the built DLL file of static(ish) dependencies, which are removed - // during kbn:bootstrap and rebuilt if missing. - new webpack.DllReferencePlugin({ - manifest: path.resolve(DLL_OUTPUT, 'manifest.json'), - context: KIBANA_ROOT, - }), - // Ensure jQuery is global for Storybook, specifically for the runtime. - new webpack.ProvidePlugin({ - $: 'jquery', - jQuery: 'jquery', - }), - // Copy the DLL files to the Webpack build for use in the Storybook UI - new CopyWebpackPlugin({ - patterns: [ - { - from: path.resolve(DLL_OUTPUT, 'dll.js'), - to: 'dll.js', - }, - { - from: path.resolve(DLL_OUTPUT, 'dll.css'), - to: 'dll.css', - }, - ], - }), - // replace imports for `uiExports/*` modules with a synthetic module - // created by create_ui_exports_module.js - new webpack.NormalModuleReplacementPlugin(/^uiExports\//, (resource) => { - // uiExports used by Canvas - const extensions = { - hacks: [], - chromeNavControls: [], - }; - - // everything following the first / in the request is - // treated as a type of appExtension - const type = resource.request.slice(resource.request.indexOf('/') + 1); - - resource.request = [ - // the "val-loader" is used to execute create_ui_exports_module - // and use its return value as the source for the module in the - // bundle. This allows us to bypass writing to the file system - require.resolve('val-loader'), - '!', - require.resolve(KIBANA_ROOT + '/src/optimize/create_ui_exports_module'), - '?', - // this JSON is parsed by create_ui_exports_module and determines - // what require() calls it will execute within the bundle - JSON.stringify({ type, modules: extensions[type] || [] }), - ].join(''); - }), - - new webpack.NormalModuleReplacementPlugin( - /lib\/download_workpad/, - path.resolve(__dirname, '../tasks/mocks/downloadWorkpad') - ), - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/custom_element_service/, - path.resolve(__dirname, '../tasks/mocks/customElementService') - ), - new webpack.NormalModuleReplacementPlugin( - /(lib)?\/ui_metric/, - path.resolve(__dirname, '../tasks/mocks/uiMetric') - ), - new webpack.NormalModuleReplacementPlugin( - /lib\/es_service/, - path.resolve(__dirname, '../tasks/mocks/esService') - ), - ], - resolve: { - extensions: ['.ts', '.tsx', '.scss', '.mjs', '.html'], - alias: { - 'ui/url/absolute_to_parsed_url': path.resolve( - __dirname, - '../tasks/mocks/uiAbsoluteToParsedUrl' - ), - }, - symlinks: false, - }, - }; - - // Find and alter the CSS rule to replace the Kibana public path string with a path - // to the route we've added in middleware.js - const cssRule = storybookConfig.module.rules.find((rule) => rule.test.source.includes('.css$')); - cssRule.use.push({ - loader: 'string-replace-loader', - options: { - search: '__REPLACE_WITH_PUBLIC_PATH__', - replace: '/', - flags: 'g', - }, - }); - - return webpackMerge(storybookConfig, config); -}; diff --git a/x-pack/plugins/canvas/storybook/webpack.dll.config.js b/x-pack/plugins/canvas/storybook/webpack.dll.config.js deleted file mode 100644 index c13fabe998921..0000000000000 --- a/x-pack/plugins/canvas/storybook/webpack.dll.config.js +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -const path = require('path'); -const webpack = require('webpack'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); - -const { DLL_NAME, DLL_OUTPUT, KIBANA_ROOT } = require('./constants'); - -// This is the Webpack config for the DLL of CSS and JS assets that are -// not expected to change during development. This saves compile and run -// times considerably. -module.exports = { - context: KIBANA_ROOT, - mode: 'development', - - // This is a (potentially growing) list of modules that can be safely - // included in the DLL. Only add to this list modules or other code - // which Storybook stories and their components would require, but don't - // change during development. - entry: [ - '@elastic/eui/dist/eui_theme_light.css', - '@kbn/ui-framework/dist/kui_light.css', - '@storybook/addon-actions/register', - '@storybook/core', - '@storybook/core/dist/server/common/polyfills.js', - '@storybook/react', - '@storybook/theming', - 'angular-mocks', - 'angular', - 'brace', - 'chroma-js', - 'highlight.js', - 'html-entities', - 'jsondiffpatch', - 'jquery', - 'lodash', - 'markdown-it', - 'monaco-editor', - 'prop-types', - 'react-ace', - 'react-beautiful-dnd', - 'react-dom', - 'react-focus-lock', - 'react-markdown', - 'react-monaco-editor', - 'react-resize-detector', - 'react-virtualized', - 'react', - 'recompose', - 'redux-actions', - 'remark-parse', - 'rxjs', - 'sinon', - 'tinycolor2', - // Include the DLL UI contexts from Kibana - require.resolve('./dll_contexts'), - ], - plugins: [ - // Produce the DLL and its manifest - new webpack.DllPlugin({ - name: DLL_NAME, - path: path.resolve(DLL_OUTPUT, 'manifest.json'), - }), - // Produce the DLL CSS file - new MiniCssExtractPlugin({ - filename: 'dll.css', - }), - ], - // Output the DLL JS file - output: { - path: DLL_OUTPUT, - filename: 'dll.js', - library: DLL_NAME, - }, - module: { - rules: [ - { - test: /\.css$/, - use: [ - { - loader: MiniCssExtractPlugin.loader, - options: {}, - }, - { loader: 'css-loader' }, - { - loader: 'string-replace-loader', - options: { - search: '__REPLACE_WITH_PUBLIC_PATH__', - replace: '/', - flags: 'g', - }, - }, - ], - }, - { - test: /\.(woff|woff2|ttf|eot|svg|ico)(\?|$)/, - loader: 'file-loader', - }, - ], - }, - node: { - fs: 'empty', - child_process: 'empty', - }, -}; From d39ed057c6bff63553298a81e4118f48dbbd220f Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Wed, 16 Jun 2021 23:45:22 -0500 Subject: [PATCH 2/3] [canvas] New Home Page --- x-pack/plugins/canvas/i18n/components.ts | 225 ++++--- x-pack/plugins/canvas/i18n/errors.ts | 44 +- .../public/components/home/home.component.tsx | 56 ++ .../public/components/home/home.stories.tsx | 29 + .../public/components/home/hooks/index.ts | 15 + .../home/hooks/use_clone_workpad.ts | 43 ++ .../home/hooks/use_create_from_template.ts | 32 + .../home/hooks/use_create_workpad.ts | 41 ++ .../home/hooks/use_delete_workpad.ts | 58 ++ .../home/hooks/use_download_workpad.ts | 12 + .../home/hooks/use_find_templates.ts | 38 ++ .../components/home/hooks/use_find_workpad.ts | 52 ++ .../home/hooks/use_upload_workpad.ts | 68 +++ .../canvas/public/components/home/index.tsx | 32 + .../home/my_workpads/empty_prompt.stories.tsx | 17 + .../home/my_workpads/empty_prompt.tsx | 40 ++ .../index.js => home/my_workpads/index.ts} | 4 +- .../components/home/my_workpads/loading.tsx | 17 + .../my_workpads/my_workpads.component.tsx | 38 ++ .../home/my_workpads/my_workpads.stories.tsx | 56 ++ .../home/my_workpads/my_workpads.tsx | 42 ++ .../my_workpads/upload_dropzone.component.tsx | 30 + .../home/my_workpads/upload_dropzone.scss | 8 + .../home/my_workpads/upload_dropzone.tsx | 55 ++ .../my_workpads/workpad_import.component.tsx | 32 + .../home/my_workpads/workpad_import.tsx | 35 ++ .../my_workpads/workpad_table.component.tsx | 156 +++++ .../my_workpads/workpad_table.stories.tsx | 83 +++ .../home/my_workpads/workpad_table.tsx | 38 ++ .../workpad_table_tools.component.tsx | 97 +++ .../home/my_workpads/workpad_table_tools.tsx | 51 ++ .../home/workpad_create.component.tsx | 32 + .../public/components/home/workpad_create.tsx | 31 + .../home/workpad_templates/index.ts | 10 + .../workpad_templates.component.tsx | 123 ++++ .../workpad_templates.stories.tsx | 61 ++ .../workpad_templates/workpad_templates.tsx | 35 ++ .../home_app/home_app.component.tsx | 18 +- .../toolbar/__stories__/toolbar.stories.tsx | 2 - .../components/toolbar/toolbar.component.tsx | 36 +- .../components/workpad_loader/index.tsx | 173 ------ .../workpad_loader/upload_workpad.js | 52 -- .../workpad_loader/workpad_create.js | 31 - .../workpad_loader/workpad_dropzone/index.js | 31 - .../workpad_dropzone/workpad_dropzone.js | 31 - .../workpad_dropzone/workpad_dropzone.scss | 22 - .../workpad_loader/workpad_loader.js | 426 ------------- .../workpad_loader/workpad_loader.scss | 25 - .../workpad_loader/workpad_search.js | 44 -- .../workpad_manager/workpad_manager.js | 69 --- .../workpad_templates.stories.storyshot | 564 ------------------ .../examples/workpad_templates.stories.tsx | 45 -- .../components/workpad_templates/index.tsx | 86 --- .../workpad_templates/workpad_templates.tsx | 215 ------- .../plugins/canvas/public/services/index.ts | 2 +- .../canvas/public/services/stubs/platform.ts | 8 +- .../canvas/public/services/stubs/workpad.ts | 94 ++- .../plugins/canvas/public/services/workpad.ts | 21 +- x-pack/plugins/canvas/public/style/index.scss | 2 - .../canvas/storybook/decorators/index.ts | 3 +- .../storybook/decorators/redux_decorator.tsx | 2 +- .../decorators/services_decorator.tsx | 40 +- x-pack/plugins/canvas/storybook/index.ts | 5 + x-pack/plugins/canvas/storybook/main.ts | 5 + 64 files changed, 1860 insertions(+), 2028 deletions(-) create mode 100644 x-pack/plugins/canvas/public/components/home/home.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/home.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/index.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts create mode 100644 x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts create mode 100644 x-pack/plugins/canvas/public/components/home/index.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx rename x-pack/plugins/canvas/public/components/{workpad_manager/index.js => home/my_workpads/index.ts} (71%) create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_create.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/index.tsx delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js delete mode 100644 x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot delete mode 100644 x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx delete mode 100644 x-pack/plugins/canvas/public/components/workpad_templates/index.tsx delete mode 100644 x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index a797a8bda061b..4975d8080e2d0 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -504,6 +504,17 @@ export const ComponentStrings = { defaultMessage: 'Keyboard shortcuts', }), }, + Home: { + getMyWorkpadsTabLabel: () => + i18n.translate('xpack.canvas.workpadManager.myWorkpadsTabLabel', { + defaultMessage: 'My workpads', + }), + getWorkpadTemplatesTabLabel: () => + i18n.translate('xpack.canvas.workpadManager.workpadTemplatesTabLabel', { + defaultMessage: 'Templates', + description: 'The label for the tab that displays a list of designed workpad templates.', + }), + }, KeyboardShortcutsDoc: { getFlyoutCloseButtonAriaLabel: () => i18n.translate('xpack.canvas.keyboardShortcutsDoc.flyout.closeButtonAriaLabel', { @@ -1631,9 +1642,18 @@ export const ComponentStrings = { defaultMessage: 'Reset', }), }, - WorkpadLoader: { + WorkpadImport: { + getFilePickerPlaceholder: () => + i18n.translate('xpack.canvas.workpadImport.filePickerPlaceholder', { + defaultMessage: 'Import workpad {JSON} file', + values: { + JSON, + }, + }), + }, + useCloneWorkpad: { getClonedWorkpadName: (workpadName: string) => - i18n.translate('xpack.canvas.workpadLoader.clonedWorkpadName', { + i18n.translate('xpack.canvas.useCloneWorkpad.clonedWorkpadName', { defaultMessage: 'Copy of {workpadName}', values: { workpadName, @@ -1642,178 +1662,124 @@ export const ComponentStrings = { 'This suffix is added to the end of the name of a cloned workpad to indicate that this ' + 'new workpad is a copy of the original workpad. Example: "Copy of Sales Pitch"', }), + }, + WorkpadTable: { getCloneToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.cloneTooltip', { + i18n.translate('xpack.canvas.workpadTable.cloneTooltip', { defaultMessage: 'Clone workpad', }), - getCreateWorkpadLoadingDescription: () => - i18n.translate('xpack.canvas.workpadLoader.createWorkpadLoadingDescription', { - defaultMessage: 'Creating workpad...', + getExportToolTip: () => + i18n.translate('xpack.canvas.workpadTable.exportTooltip', { + defaultMessage: 'Export workpad', + }), + getLoadWorkpadArialLabel: (workpadName: string) => + i18n.translate('xpack.canvas.workpadTable.loadWorkpadArialLabel', { + defaultMessage: `Load workpad '{workpadName}'`, + values: { + workpadName, + }, + }), + getNoPermissionToCloneToolTip: () => + i18n.translate('xpack.canvas.workpadTable.noPermissionToCloneToolTip', { + defaultMessage: `You don't have permission to clone workpads`, + }), + getNoWorkpadsFoundMessage: () => + i18n.translate('xpack.canvas.workpadTable.noWorkpadsFoundMessage', { + defaultMessage: 'No workpads matched your search.', + }), + getWorkpadSearchPlaceholder: () => + i18n.translate('xpack.canvas.workpadTable.searchPlaceholder', { + defaultMessage: 'Find workpad', + }), + getTableCreatedColumnTitle: () => + i18n.translate('xpack.canvas.workpadTable.table.createdColumnTitle', { + defaultMessage: 'Created', + description: 'This column in the table contains the date/time the workpad was created.', + }), + getTableNameColumnTitle: () => + i18n.translate('xpack.canvas.workpadTable.table.nameColumnTitle', { + defaultMessage: 'Workpad name', + }), + getTableUpdatedColumnTitle: () => + i18n.translate('xpack.canvas.workpadTable.table.updatedColumnTitle', { + defaultMessage: 'Updated', + description: + 'This column in the table contains the date/time the workpad was last updated.', + }), + getTableActionsColumnTitle: () => + i18n.translate('xpack.canvas.workpadTable.table.actionsColumnTitle', { + defaultMessage: 'Actions', description: - 'This message appears while the user is waiting for a new workpad to be created', + 'This column in the table contains the actions that can be taken on a workpad.', }), + }, + WorkpadTableTools: { getDeleteButtonAriaLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadLoader.deleteButtonAriaLabel', { + i18n.translate('xpack.canvas.workpadTableTools.deleteButtonAriaLabel', { defaultMessage: 'Delete {numberOfWorkpads} workpads', values: { numberOfWorkpads, }, }), getDeleteButtonLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadLoader.deleteButtonLabel', { + i18n.translate('xpack.canvas.workpadTableTools.deleteButtonLabel', { defaultMessage: 'Delete ({numberOfWorkpads})', values: { numberOfWorkpads, }, }), getDeleteModalConfirmButtonLabel: () => - i18n.translate('xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel', { + i18n.translate('xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel', { defaultMessage: 'Delete', }), getDeleteModalDescription: () => - i18n.translate('xpack.canvas.workpadLoader.deleteModalDescription', { + i18n.translate('xpack.canvas.workpadTableTools.deleteModalDescription', { defaultMessage: `You can't recover deleted workpads.`, }), getDeleteMultipleWorkpadModalTitle: (numberOfWorkpads: string) => - i18n.translate('xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle', { + i18n.translate('xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle', { defaultMessage: 'Delete {numberOfWorkpads} workpads?', values: { numberOfWorkpads, }, }), getDeleteSingleWorkpadModalTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle', { + i18n.translate('xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle', { defaultMessage: `Delete workpad '{workpadName}'?`, values: { workpadName, }, }), - getEmptyPromptGettingStartedDescription: () => - i18n.translate('xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription', { - defaultMessage: - 'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.', - values: { - JSON, - }, - }), - getEmptyPromptNewUserDescription: () => - i18n.translate('xpack.canvas.workpadLoader.emptyPromptNewUserDescription', { - defaultMessage: 'New to {CANVAS}?', - values: { - CANVAS, - }, - }), - getEmptyPromptTitle: () => - i18n.translate('xpack.canvas.workpadLoader.emptyPromptTitle', { - defaultMessage: 'Add your first workpad', - }), getExportButtonAriaLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadLoader.exportButtonAriaLabel', { + i18n.translate('xpack.canvas.workpadTableTools.exportButtonAriaLabel', { defaultMessage: 'Export {numberOfWorkpads} workpads', values: { numberOfWorkpads, }, }), getExportButtonLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadLoader.exportButtonLabel', { + i18n.translate('xpack.canvas.workpadTableTools.exportButtonLabel', { defaultMessage: 'Export ({numberOfWorkpads})', values: { numberOfWorkpads, }, }), - getExportToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.exportTooltip', { - defaultMessage: 'Export workpad', - }), - getFetchLoadingDescription: () => - i18n.translate('xpack.canvas.workpadLoader.fetchLoadingDescription', { - defaultMessage: 'Fetching workpads...', - description: - 'This message appears while the user is waiting for their list of workpads to load', - }), - getFilePickerPlaceholder: () => - i18n.translate('xpack.canvas.workpadLoader.filePickerPlaceholder', { - defaultMessage: 'Import workpad {JSON} file', - values: { - JSON, - }, - }), - getLoadWorkpadArialLabel: (workpadName: string) => - i18n.translate('xpack.canvas.workpadLoader.loadWorkpadArialLabel', { - defaultMessage: `Load workpad '{workpadName}'`, - values: { - workpadName, - }, - }), - getNoPermissionToCloneToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.noPermissionToCloneToolTip', { - defaultMessage: `You don't have permission to clone workpads`, - }), getNoPermissionToCreateToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.noPermissionToCreateToolTip', { + i18n.translate('xpack.canvas.workpadTableTools.noPermissionToCreateToolTip', { defaultMessage: `You don't have permission to create workpads`, }), getNoPermissionToDeleteToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.noPermissionToDeleteToolTip', { + i18n.translate('xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip', { defaultMessage: `You don't have permission to delete workpads`, }), getNoPermissionToUploadToolTip: () => - i18n.translate('xpack.canvas.workpadLoader.noPermissionToUploadToolTip', { + i18n.translate('xpack.canvas.workpadTableTools.noPermissionToUploadToolTip', { defaultMessage: `You don't have permission to upload workpads`, }), - getSampleDataLinkLabel: () => - i18n.translate('xpack.canvas.workpadLoader.sampleDataLinkLabel', { - defaultMessage: 'Add your first workpad', - }), - getTableCreatedColumnTitle: () => - i18n.translate('xpack.canvas.workpadLoader.table.createdColumnTitle', { - defaultMessage: 'Created', - description: 'This column in the table contains the date/time the workpad was created.', - }), - getTableNameColumnTitle: () => - i18n.translate('xpack.canvas.workpadLoader.table.nameColumnTitle', { - defaultMessage: 'Workpad name', - }), - getTableUpdatedColumnTitle: () => - i18n.translate('xpack.canvas.workpadLoader.table.updatedColumnTitle', { - defaultMessage: 'Updated', - description: - 'This column in the table contains the date/time the workpad was last updated.', - }), - getTableActionsColumnTitle: () => - i18n.translate('xpack.canvas.workpadLoader.table.actionsColumnTitle', { - defaultMessage: 'Actions', - description: - 'This column in the table contains the actions that can be taken on a workpad.', - }), - }, - WorkpadManager: { - getModalTitle: () => - i18n.translate('xpack.canvas.workpadManager.modalTitle', { - defaultMessage: '{CANVAS} workpads', - values: { - CANVAS, - }, - }), - getMyWorkpadsTabLabel: () => - i18n.translate('xpack.canvas.workpadManager.myWorkpadsTabLabel', { - defaultMessage: 'My workpads', - }), - getWorkpadTemplatesTabLabel: () => - i18n.translate('xpack.canvas.workpadManager.workpadTemplatesTabLabel', { - defaultMessage: 'Templates', - description: 'The label for the tab that displays a list of designed workpad templates.', - }), - }, - WorkpadSearch: { - getWorkpadSearchPlaceholder: () => - i18n.translate('xpack.canvas.workpadSearch.searchPlaceholder', { - defaultMessage: 'Find workpad', - }), }, WorkpadTemplates: { getCloneTemplateLinkAriaLabel: (templateName: string) => - i18n.translate('xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel', { + i18n.translate('xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel', { defaultMessage: `Clone workpad template '{templateName}'`, values: { templateName, @@ -1835,15 +1801,40 @@ export const ComponentStrings = { 'is displayed. For example: "report", "presentation", etc.', }), getTemplateSearchPlaceholder: () => - i18n.translate('xpack.canvas.workpadTemplate.searchPlaceholder', { + i18n.translate('xpack.canvas.workpadTemplates.searchPlaceholder', { defaultMessage: 'Find template', }), getCreatingTemplateLabel: (templateName: string) => - i18n.translate('xpack.canvas.workpadTemplate.creatingTemplateLabel', { + i18n.translate('xpack.canvas.workpadTemplates.creatingTemplateLabel', { defaultMessage: `Creating from template '{templateName}'`, values: { templateName, }, }), }, + WorkpadUploadPrompt: { + getEmptyPromptGettingStartedDescription: () => + i18n.translate('xpack.canvas.workpadUploadPrompt.emptyPromptGettingStartedDescription', { + defaultMessage: + 'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.', + values: { + JSON, + }, + }), + getEmptyPromptNewUserDescription: () => + i18n.translate('xpack.canvas.workpadUploadPrompt.emptyPromptNewUserDescription', { + defaultMessage: 'New to {CANVAS}?', + values: { + CANVAS, + }, + }), + getEmptyPromptTitle: () => + i18n.translate('xpack.canvas.workpadUploadPrompt.emptyPromptTitle', { + defaultMessage: 'Add your first workpad', + }), + getSampleDataLinkLabel: () => + i18n.translate('xpack.canvas.workpadUploadPrompt.sampleDataLinkLabel', { + defaultMessage: 'Add your first workpad', + }), + }, }; diff --git a/x-pack/plugins/canvas/i18n/errors.ts b/x-pack/plugins/canvas/i18n/errors.ts index 0928045119234..902fc0acd256a 100644 --- a/x-pack/plugins/canvas/i18n/errors.ts +++ b/x-pack/plugins/canvas/i18n/errors.ts @@ -93,7 +93,13 @@ export const ErrorStrings = { }, }), }, - WorkpadFileUpload: { + WorkpadDropzone: { + getTooManyFilesErrorMessage: () => + i18n.translate('xpack.canvas.error.workpadDropzone.tooManyFilesErrorMessage', { + defaultMessage: 'One one file can be uploaded at a time', + }), + }, + workpadLoader: { getAcceptJSONOnlyErrorMessage: () => i18n.translate('xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage', { defaultMessage: 'Only {JSON} files are accepted', @@ -101,11 +107,13 @@ export const ErrorStrings = { JSON, }, }), - getFileUploadFailureWithFileNameErrorMessage: (fileName: string) => - i18n.translate('xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage', { - defaultMessage: `Couldn't upload '{fileName}'`, + getMissingPropertiesErrorMessage: () => + i18n.translate('xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage', { + defaultMessage: + 'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.', values: { - fileName, + CANVAS, + JSON, }, }), getFileUploadFailureWithoutFileNameErrorMessage: () => @@ -115,33 +123,29 @@ export const ErrorStrings = { defaultMessage: `Couldn't upload file`, } ), - getMissingPropertiesErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage', { - defaultMessage: - 'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.', + getFileUploadFailureWithFileNameErrorMessage: (fileName: string) => + i18n.translate('xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage', { + defaultMessage: `Couldn't upload '{fileName}'`, values: { - CANVAS, - JSON, + fileName, }, }), - }, - WorkpadLoader: { - getCloneFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.cloneFailureErrorMessage', { - defaultMessage: `Couldn't clone workpad`, + getUploadFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.workpadLoader.uploadFailureErrorMessage', { + defaultMessage: `Couldn't upload workpad`, }), getDeleteFailureErrorMessage: () => i18n.translate('xpack.canvas.error.workpadLoader.deleteFailureErrorMessage', { defaultMessage: `Couldn't delete all workpads`, }), + getCloneFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.workpadLoader.cloneFailureErrorMessage', { + defaultMessage: `Couldn't clone workpad`, + }), getFindFailureErrorMessage: () => i18n.translate('xpack.canvas.error.workpadLoader.findFailureErrorMessage', { defaultMessage: `Couldn't find workpad`, }), - getUploadFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.uploadFailureErrorMessage', { - defaultMessage: `Couldn't upload workpad`, - }), }, workpadRoutes: { getCreateFailureErrorMessage: () => diff --git a/x-pack/plugins/canvas/public/components/home/home.component.tsx b/x-pack/plugins/canvas/public/components/home/home.component.tsx new file mode 100644 index 0000000000000..5d62cd0516610 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/home.component.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { EuiPageTemplate } from '@elastic/eui'; +import { withSuspense } from '../../../../../../src/plugins/presentation_util/public'; + +import { ComponentStrings } from '../../../i18n'; +import { WorkpadCreate } from './workpad_create'; +import { LazyWorkpadTemplates } from './workpad_templates'; +import { LazyMyWorkpads } from './my_workpads'; + +const { Home: strings } = ComponentStrings; + +export type HomePageTab = 'workpads' | 'templates'; + +export interface Props { + activeTab?: HomePageTab; +} + +const WorkpadTemplates = withSuspense(LazyWorkpadTemplates); +const MyWorkpads = withSuspense(LazyMyWorkpads); + +export const Home = ({ activeTab = 'workpads' }: Props) => { + const [tab, setTab] = useState(activeTab); + + return ( + ], + tabs: [ + { + label: strings.getMyWorkpadsTabLabel(), + id: 'myWorkpads', + isSelected: tab === 'workpads', + onClick: () => setTab('workpads'), + }, + { + label: strings.getWorkpadTemplatesTabLabel(), + id: 'workpadTemplates', + 'data-test-subj': 'workpadTemplates', + isSelected: tab === 'templates', + onClick: () => setTab('templates'), + }, + ], + }} + > + {tab === 'workpads' ? : } + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/home.stories.tsx b/x-pack/plugins/canvas/public/components/home/home.stories.tsx new file mode 100644 index 0000000000000..1f442e2dd9bfc --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/home.stories.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + reduxDecorator, + getAddonPanelParameters, + servicesContextDecorator, +} from '../../../storybook'; + +import { Home } from './home.component'; + +export default { + title: 'Home/Home Page', + argTypes: {}, + decorators: [reduxDecorator()], + parameters: [getAddonPanelParameters()], +}; + +export const NoContent = () => ; +export const HasContent = () => ; + +NoContent.decorators = [servicesContextDecorator()]; +HasContent.decorators = [servicesContextDecorator({ findWorkpads: 5, findTemplates: true })]; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/index.ts b/x-pack/plugins/canvas/public/components/home/hooks/index.ts new file mode 100644 index 0000000000000..91e52948a7ba6 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { useCloneWorkpad } from './use_clone_workpad'; +export { useCreateWorkpad } from './use_create_workpad'; +export { useDeleteWorkpads } from './use_delete_workpad'; +export { useDownloadWorkpad } from './use_download_workpad'; +export { useFindTemplates, useFindTemplatesOnMount } from './use_find_templates'; +export { useFindWorkpads, useFindWorkpadsOnMount } from './use_find_workpad'; +export { useImportWorkpad } from './use_upload_workpad'; +export { useCreateFromTemplate } from './use_create_from_template'; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts new file mode 100644 index 0000000000000..15e397da4f603 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { useNotifyService, useWorkpadService } from '../../../services'; +import { ErrorStrings, ComponentStrings } from '../../../../i18n'; +import { getId } from '../../../lib/get_id'; + +const { workpadLoader: errors } = ErrorStrings; +const { useCloneWorkpad: strings } = ComponentStrings; + +export const useCloneWorkpad = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + const history = useHistory(); + + return useCallback( + async (workpadId: string) => { + try { + let workpad = await workpadService.get(workpadId); + + workpad = { + ...workpad, + name: strings.getClonedWorkpadName(workpad.name), + id: getId('workpad'), + }; + + await workpadService.create(workpad); + + history.push(`/workpad/${workpad.id}/page/1`); + } catch (err) { + notifyService.error(err, { title: errors.getCloneFailureErrorMessage() }); + } + }, + [notifyService, workpadService, history] + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts new file mode 100644 index 0000000000000..968f9398ba857 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_from_template.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { CanvasTemplate } from '../../../../types'; +import { useNotifyService, useWorkpadService } from '../../../services'; + +export const useCreateFromTemplate = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + const history = useHistory(); + + return useCallback( + async (template: CanvasTemplate) => { + try { + const result = await workpadService.createFromTemplate(template.id); + history.push(`/workpad/${result.id}/page/1`); + } catch (e) { + notifyService.error(e, { + title: `Couldn't create workpad from template`, + }); + } + }, + [workpadService, notifyService, history] + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts new file mode 100644 index 0000000000000..b388edd346fd6 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; + +// @ts-expect-error +import { getDefaultWorkpad } from '../../../state/defaults'; +import { useNotifyService, useWorkpadService } from '../../../services'; +import { ErrorStrings } from '../../../../i18n'; + +import type { CanvasWorkpad } from '../../../../types'; + +const { workpadLoader: errors } = ErrorStrings; + +export const useCreateWorkpad = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + const history = useHistory(); + + return useCallback( + async (_workpad?: CanvasWorkpad | null) => { + const workpad = _workpad || (getDefaultWorkpad() as CanvasWorkpad); + + try { + await workpadService.create(workpad); + history.push(`/workpad/${workpad.id}/page/1`); + } catch (err) { + notifyService.error(err, { + title: errors.getUploadFailureErrorMessage(), + }); + } + return; + }, + [notifyService, history, workpadService] + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts new file mode 100644 index 0000000000000..26bf7889e9014 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; + +import { useNotifyService, useWorkpadService } from '../../../services'; +import { ErrorStrings } from '../../../../i18n'; + +const { workpadLoader: errors } = ErrorStrings; + +export const useDeleteWorkpads = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + + return useCallback( + async (workpadIds: string[]) => { + const removedWorkpads = workpadIds.map(async (id) => { + try { + await workpadService.remove(id); + return { id, err: null }; + } catch (err) { + return { id, err }; + } + }); + + return Promise.all(removedWorkpads).then((results) => { + const [passes, errored] = results.reduce<[string[], string[]]>( + ([passesArr, errorsArr], result) => { + if (result.err) { + errorsArr.push(result.id); + } else { + passesArr.push(result.id); + } + + return [passesArr, errorsArr]; + }, + [[], []] + ); + + const removedIds = workpadIds.filter((id) => passes.includes(id)); + + if (errored.length > 0) { + notifyService.error(errors.getDeleteFailureErrorMessage()); + } + + return { + removedIds, + errored, + }; + }); + }, + [workpadService, notifyService] + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts new file mode 100644 index 0000000000000..b875e08c2a230 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_download_workpad.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { downloadWorkpad as downloadWorkpadFn } from '../../../lib/download_workpad'; + +export const useDownloadWorkpad = () => + useCallback((workpadId: string) => downloadWorkpadFn(workpadId), []); diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts new file mode 100644 index 0000000000000..13ee289fe9867 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_templates.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useCallback } from 'react'; +import useMount from 'react-use/lib/useMount'; + +import { useWorkpadService } from '../../../services'; +import { TemplateFindResponse } from '../../../services/workpad'; + +const emptyResponse = { templates: [] }; + +export const useFindTemplates = () => { + const workpadService = useWorkpadService(); + return useCallback(async () => await workpadService.findTemplates(), [workpadService]); +}; + +export const useFindTemplatesOnMount = (): [boolean, TemplateFindResponse] => { + const [isMounted, setIsMounted] = useState(false); + const findTemplates = useFindTemplates(); + const [templateResponse, setTemplateResponse] = useState(emptyResponse); + + const fetchTemplates = useCallback(async () => { + const foundTemplates = await findTemplates(); + setTemplateResponse(foundTemplates || emptyResponse); + setIsMounted(true); + }, [findTemplates]); + + useMount(() => { + fetchTemplates(); + return () => setIsMounted(false); + }); + + return [isMounted, templateResponse]; +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts new file mode 100644 index 0000000000000..23099ae7de45b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useCallback } from 'react'; +import useMount from 'react-use/lib/useMount'; + +import { WorkpadFindResponse } from '../../../services/workpad'; + +import { useNotifyService, useWorkpadService } from '../../../services'; +import { ErrorStrings } from '../../../../i18n'; + +const { workpadLoader: errors } = ErrorStrings; +const emptyResponse = { total: 0, workpads: [] }; + +export const useFindWorkpads = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + + return useCallback( + async (text = '') => { + try { + return await workpadService.find(text); + } catch (err) { + notifyService.error(err, { title: errors.getFindFailureErrorMessage() }); + } + }, + [notifyService, workpadService] + ); +}; + +export const useFindWorkpadsOnMount = (): [boolean, WorkpadFindResponse] => { + const [isMounted, setIsMounted] = useState(false); + const findWorkpads = useFindWorkpads(); + const [workpadResponse, setWorkpadResponse] = useState(emptyResponse); + + const fetchWorkpads = useCallback(async () => { + const foundWorkpads = await findWorkpads(); + setWorkpadResponse(foundWorkpads || emptyResponse); + setIsMounted(true); + }, [findWorkpads]); + + useMount(() => { + fetchWorkpads(); + return () => setIsMounted(false); + }); + + return [isMounted, workpadResponse]; +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts new file mode 100644 index 0000000000000..1891d5df4a218 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { get } from 'lodash'; + +import { useNotifyService } from '../../../services'; +import { ErrorStrings } from '../../../../i18n'; +import { getId } from '../../../lib/get_id'; +import type { CanvasWorkpad } from '../../../../types'; + +const { workpadLoader: errors } = ErrorStrings; + +export const useImportWorkpad = () => { + const notifyService = useNotifyService(); + + return useCallback( + (file?: File, onComplete: (workpad?: CanvasWorkpad) => void = () => {}) => { + if (!file) { + onComplete(); + return; + } + + if (get(file, 'type') !== 'application/json') { + notifyService.warning(errors.getAcceptJSONOnlyErrorMessage(), { + title: file.name + ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name) + : errors.getFileUploadFailureWithoutFileNameErrorMessage(), + }); + onComplete(); + } + + // TODO: Clean up this file, this loading stuff can, and should be, abstracted + const reader = new FileReader(); + + // handle reading the uploaded file + reader.onload = () => { + try { + const workpad = JSON.parse(reader.result as string); // Type-casting because we catch below. + workpad.id = getId('workpad'); + + // sanity check for workpad object + if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) { + onComplete(); + throw new Error(errors.getMissingPropertiesErrorMessage()); + } + + onComplete(workpad); + } catch (e) { + notifyService.error(e, { + title: file.name + ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name) + : errors.getFileUploadFailureWithoutFileNameErrorMessage(), + }); + onComplete(); + } + }; + + // read the uploaded file + reader.readAsText(file); + }, + [notifyService] + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/index.tsx b/x-pack/plugins/canvas/public/components/home/index.tsx new file mode 100644 index 0000000000000..afb0ef1ca979c --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/index.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { getBaseBreadcrumb } from '../../lib/breadcrumbs'; +import { resetWorkpad } from '../../state/actions/workpad'; +import { Home as Component } from './home.component'; +import { usePlatformService } from '../../services'; + +export const Home = () => { + const { setBreadcrumbs } = usePlatformService(); + const [isMounted, setIsMounted] = useState(false); + const dispatch = useDispatch(); + + useEffect(() => { + if (!isMounted) { + dispatch(resetWorkpad()); + setIsMounted(true); + } + }, [dispatch, isMounted, setIsMounted]); + + useEffect(() => { + setBreadcrumbs([getBaseBreadcrumb()]); + }, [setBreadcrumbs]); + + return ; +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx new file mode 100644 index 0000000000000..5f8310f8d3f10 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { HomeEmptyPrompt } from './empty_prompt'; + +export default { + title: 'Home/Empty Prompt', + argTypes: {}, +}; + +export const EmptyPrompt = () => ; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx new file mode 100644 index 0000000000000..0e506636d0580 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment } from 'react'; +import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { ComponentStrings } from '../../../../i18n'; + +const { WorkpadUploadPrompt: strings } = ComponentStrings; + +export const HomeEmptyPrompt = () => ( + + + + {strings.getEmptyPromptTitle()}} + titleSize="m" + body={ + +

{strings.getEmptyPromptGettingStartedDescription()}

+

+ {strings.getEmptyPromptNewUserDescription()}{' '} + + {strings.getSampleDataLinkLabel()} + + . +

+
+ } + /> +
+
+
+); diff --git a/x-pack/plugins/canvas/public/components/workpad_manager/index.js b/x-pack/plugins/canvas/public/components/home/my_workpads/index.ts similarity index 71% rename from x-pack/plugins/canvas/public/components/workpad_manager/index.js rename to x-pack/plugins/canvas/public/components/home/my_workpads/index.ts index e1f5855e762af..79b1519df90fe 100644 --- a/x-pack/plugins/canvas/public/components/workpad_manager/index.js +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/index.ts @@ -5,4 +5,6 @@ * 2.0. */ -export { WorkpadManager } from './workpad_manager'; +import React from 'react'; + +export const LazyMyWorkpads = React.lazy(() => import('./my_workpads')); diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx new file mode 100644 index 0000000000000..28edfea7c36ca --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/loading.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +export const Loading = () => ( + + + + + +); diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx new file mode 100644 index 0000000000000..d9e3f0e4e2c99 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.component.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { FoundWorkpad } from '../../../services/workpad'; +import { UploadDropzone } from './upload_dropzone'; +import { HomeEmptyPrompt } from './empty_prompt'; +import { WorkpadTable } from './workpad_table'; + +export interface Props { + workpads: FoundWorkpad[]; +} + +export const MyWorkpads = ({ workpads }: Props) => { + if (workpads.length === 0) { + return ( + + + + + + + + ); + } + + return ( + + + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx new file mode 100644 index 0000000000000..7aedce57814f6 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { EuiPanel } from '@elastic/eui'; + +import { + reduxDecorator, + getAddonPanelParameters, + servicesContextDecorator, + getDisableStoryshotsParameter, +} from '../../../../storybook'; +import { getSomeWorkpads } from '../../../services/stubs/workpad'; + +import { MyWorkpads, WorkpadsContext } from './my_workpads'; +import { MyWorkpads as MyWorkpadsComponent } from './my_workpads.component'; + +export default { + title: 'Home/My Workpads', + argTypes: {}, + decorators: [reduxDecorator()], + parameters: [getAddonPanelParameters(), getDisableStoryshotsParameter()], +}; + +export const NoWorkpads = () => { + return ; +}; + +export const HasWorkpads = () => { + return ( + + + + ); +}; + +NoWorkpads.decorators = [servicesContextDecorator()]; +HasWorkpads.decorators = [servicesContextDecorator({ findWorkpads: 5 })]; + +export const Component = ({ workpadCount }: { workpadCount: number }) => { + const [workpads, setWorkpads] = useState(getSomeWorkpads(workpadCount)); + + return ( + + + + + + ); +}; + +Component.args = { workpadCount: 5 }; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx new file mode 100644 index 0000000000000..4242e2e9d130f --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useEffect, createContext, Dispatch, SetStateAction } from 'react'; +import { useFindWorkpadsOnMount } from './../hooks'; +import { FoundWorkpad } from '../../../services/workpad'; +import { Loading } from './loading'; +import { MyWorkpads as Component } from './my_workpads.component'; + +interface Context { + workpads: FoundWorkpad[]; + setWorkpads: Dispatch>; +} + +export const WorkpadsContext = createContext(null); + +export const MyWorkpads = () => { + const [isMounted, workpadResponse] = useFindWorkpadsOnMount(); + const [workpads, setWorkpads] = useState(workpadResponse.workpads); + + useEffect(() => { + setWorkpads(workpadResponse.workpads); + }, [workpadResponse]); + + if (!isMounted) { + return ; + } + + return ( + + + + ); +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default MyWorkpads; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx new file mode 100644 index 0000000000000..603f4679a9e95 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.component.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +// @ts-expect-error untyped library +import Dropzone from 'react-dropzone'; + +import './upload_dropzone.scss'; + +export interface Props { + disabled?: boolean; + onDrop?: (files: FileList) => void; +} + +export const UploadDropzone: FC = ({ onDrop = () => {}, disabled, children }) => { + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss new file mode 100644 index 0000000000000..e4ee284c72dee --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.scss @@ -0,0 +1,8 @@ +.canvasWorkpad__dropzone { + border: 2px dashed transparent; +} + +.canvasWorkpad__dropzone--active { + background-color: $euiColorLightestShade; + border-color: $euiColorLightShade; +} diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx new file mode 100644 index 0000000000000..8ee0ae108392e --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/upload_dropzone.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useState } from 'react'; +// @ts-expect-error untyped library +import Dropzone from 'react-dropzone'; + +import { useNotifyService } from '../../../services'; +import { ErrorStrings } from '../../../../i18n'; +import { useImportWorkpad, useCreateWorkpad } from '../hooks'; +import { CanvasWorkpad } from '../../../../types'; + +import { UploadDropzone as Component } from './upload_dropzone.component'; + +const { WorkpadDropzone: errors } = ErrorStrings; + +export const UploadDropzone: FC = ({ children }) => { + const notify = useNotifyService(); + const uploadWorkpad = useImportWorkpad(); + const createWorkpad = useCreateWorkpad(); + const [isDisabled, setIsDisabled] = useState(false); + + const onComplete = async (workpad?: CanvasWorkpad) => { + if (!workpad) { + setIsDisabled(false); + return; + } + + await createWorkpad(workpad); + }; + + const onDrop = (files: FileList) => { + if (!files) { + return; + } + + if (files.length > 1) { + notify.warning(errors.getTooManyFilesErrorMessage()); + return; + } + + setIsDisabled(true); + uploadWorkpad(files[0], onComplete); + }; + + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx new file mode 100644 index 0000000000000..b4a8467d4d703 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFilePicker, EuiFilePickerProps } from '@elastic/eui'; + +import { ComponentStrings } from '../../../../i18n'; + +const { WorkpadImport: strings } = ComponentStrings; + +export interface Props { + canUserWrite: boolean; + onImportWorkpad?: EuiFilePickerProps['onChange']; + uniqueKey?: string | number; +} + +export const WorkpadImport = ({ uniqueKey, canUserWrite, onImportWorkpad = () => {} }: Props) => ( + +); diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx new file mode 100644 index 0000000000000..0f1ba621e14d7 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app'; +import type { State } from '../../../../types'; + +import { useImportWorkpad } from '../hooks'; +import { WorkpadImport as Component, Props as ComponentProps } from './workpad_import.component'; + +type Props = Omit; + +export const WorkpadImport = (props: Props) => { + const importWorkpad = useImportWorkpad(); + const [uniqueKey, setUniqueKey] = useState(Date.now()); + + const { canUserWrite } = useSelector((state: State) => ({ + canUserWrite: canUserWriteSelector(state), + })); + + const onImportWorkpad: ComponentProps['onImportWorkpad'] = (files) => { + if (files) { + importWorkpad(files[0]); + } + setUniqueKey(Date.now()); + }; + + return ; +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx new file mode 100644 index 0000000000000..58a6b50f8ad39 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { + EuiInMemoryTable, + EuiInMemoryTableProps, + EuiTableActionsColumnType, + EuiBasicTableColumn, + EuiToolTip, + EuiButtonIcon, + EuiTableSelectionType, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import moment from 'moment'; + +import { RoutingLink } from '../../routing'; +import { ComponentStrings } from '../../../../i18n'; +import { FoundWorkpad } from '../../../services/workpad'; +import { WorkpadTableTools } from './workpad_table_tools'; +import { WorkpadImport } from './workpad_import'; + +export interface Props { + workpads: FoundWorkpad[]; + canUserWrite: boolean; + dateFormat: string; + onExportWorkpad: (ids: string) => void; + onCloneWorkpad: (id: string) => void; +} + +const { WorkpadTable: strings } = ComponentStrings; + +const getDisplayName = (name: string, workpadId: string, loadedWorkpadId?: string) => { + const workpadName = name.length ? {name} : {workpadId}; + return workpadId === loadedWorkpadId ? {workpadName} : workpadName; +}; + +export const WorkpadTable = ({ + workpads, + canUserWrite, + dateFormat, + onExportWorkpad: onExport, + onCloneWorkpad, +}: Props) => { + const [selectedIds, setSelectedIds] = useState([]); + const formatDate = (date: string) => date && moment(date).format(dateFormat); + + const selection: EuiTableSelectionType = { + onSelectionChange: (selectedWorkpads) => { + setSelectedIds(selectedWorkpads.map((workpad) => workpad.id).filter((id) => !!id)); + }, + }; + + const actions: EuiTableActionsColumnType['actions'] = [ + { + render: (workpad: FoundWorkpad) => ( + + + + onExport(workpad.id)} + aria-label={strings.getExportToolTip()} + /> + + + + + onCloneWorkpad(workpad.id)} + aria-label={strings.getCloneToolTip()} + disabled={!canUserWrite} + /> + + + + ), + }, + ]; + + const search: EuiInMemoryTableProps['search'] = { + toolsLeft: + selectedIds.length > 0 ? : undefined, + toolsRight: , + box: { + schema: true, + incremental: true, + placeholder: strings.getWorkpadSearchPlaceholder(), + 'data-test-subj': 'tableListSearchBox', + }, + }; + + const columns: Array> = [ + { + field: 'name', + name: strings.getTableNameColumnTitle(), + sortable: true, + dataType: 'string', + render: (name, workpad) => ( + + {getDisplayName(name, workpad.id)} + + ), + }, + { + field: '@created', + name: strings.getTableCreatedColumnTitle(), + sortable: true, + dataType: 'date', + width: '20%', + render: (date: string) => formatDate(date), + }, + { + field: '@timestamp', + name: strings.getTableUpdatedColumnTitle(), + sortable: true, + dataType: 'date', + width: '20%', + render: (date: string) => formatDate(date), + }, + { name: strings.getTableActionsColumnTitle(), actions, width: '100px' }, + ]; + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx new file mode 100644 index 0000000000000..1cbd3b842ba6d --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useEffect } from 'react'; +import { EuiPanel } from '@elastic/eui'; + +import { action } from '@storybook/addon-actions'; +import { + reduxDecorator, + getAddonPanelParameters, + getDisableStoryshotsParameter, +} from '../../../../storybook'; +import { getSomeWorkpads } from '../../../services/stubs/workpad'; + +import { WorkpadTable } from './workpad_table'; +import { WorkpadTable as WorkpadTableComponent } from './workpad_table.component'; +import { WorkpadsContext } from './my_workpads'; + +export default { + title: 'Home/Workpad Table', + argTypes: {}, + decorators: [reduxDecorator()], + parameters: [getAddonPanelParameters(), getDisableStoryshotsParameter()], +}; + +export const NoWorkpads = () => { + const [workpads, setWorkpads] = useState(getSomeWorkpads(0)); + + return ( + + + + + + ); +}; + +export const HasWorkpads = () => { + const [workpads, setWorkpads] = useState(getSomeWorkpads(5)); + + return ( + + + + + + ); +}; + +export const Component = ({ + workpadCount, + canUserWrite, + dateFormat, +}: { + workpadCount: number; + canUserWrite: boolean; + dateFormat: string; +}) => { + const [workpads, setWorkpads] = useState(getSomeWorkpads(workpadCount)); + + useEffect(() => { + setWorkpads(getSomeWorkpads(workpadCount)); + }, [workpadCount]); + + return ( + + + + + + ); +}; + +Component.args = { workpadCount: 5, canUserWrite: true, dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS' }; +Component.argTypes = {}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx new file mode 100644 index 0000000000000..e5d83039a87eb --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext } from 'react'; +import { useSelector } from 'react-redux'; + +import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app'; +import type { State } from '../../../../types'; +import { usePlatformService } from '../../../services'; +import { useCloneWorkpad, useDownloadWorkpad } from '../hooks'; + +import { WorkpadTable as Component } from './workpad_table.component'; +import { WorkpadsContext } from './my_workpads'; + +export const WorkpadTable = () => { + const platformService = usePlatformService(); + const onCloneWorkpad = useCloneWorkpad(); + const onExportWorkpad = useDownloadWorkpad(); + const context = useContext(WorkpadsContext); + + const { canUserWrite } = useSelector((state: State) => ({ + canUserWrite: canUserWriteSelector(state), + })); + + if (!context) { + return null; + } + + const { workpads } = context; + + const dateFormat = platformService.getUISetting('dateFormat'); + + return ; +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx new file mode 100644 index 0000000000000..256447d0df200 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, Fragment } from 'react'; +import { EuiButton, EuiToolTip, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; + +import { ComponentStrings } from '../../../../i18n'; +import { ConfirmModal } from '../../confirm_modal'; +import { FoundWorkpad } from '../../../services/workpad'; + +const { WorkpadTableTools: strings } = ComponentStrings; + +export interface Props { + workpads: FoundWorkpad[]; + canUserWrite: boolean; + selectedWorkpadIds: string[]; + onDeleteWorkpads: (ids: string[]) => void; + onExportWorkpads: (ids: string[]) => void; +} + +export const WorkpadTableTools = ({ + workpads, + canUserWrite, + selectedWorkpadIds, + onDeleteWorkpads, + onExportWorkpads, +}: Props) => { + const [isDeletePending, setIsDeletePending] = useState(false); + + const openRemoveConfirm = () => setIsDeletePending(true); + const closeRemoveConfirm = () => setIsDeletePending(false); + + let deleteButton = ( + + {strings.getDeleteButtonLabel(selectedWorkpadIds.length)} + + ); + + const downloadButton = ( + onExportWorkpads(selectedWorkpadIds)} + iconType="exportAction" + aria-label={strings.getExportButtonAriaLabel(selectedWorkpadIds.length)} + > + {strings.getExportButtonLabel(selectedWorkpadIds.length)} + + ); + + if (!canUserWrite) { + deleteButton = ( + {deleteButton} + ); + } + + const modalTitle = + selectedWorkpadIds.length === 1 + ? strings.getDeleteSingleWorkpadModalTitle( + workpads.find((workpad) => workpad.id === selectedWorkpadIds[0])?.name || '' + ) + : strings.getDeleteMultipleWorkpadModalTitle(selectedWorkpadIds.length + ''); + + const confirmModal = ( + { + onDeleteWorkpads(selectedWorkpadIds); + closeRemoveConfirm(); + }} + onCancel={closeRemoveConfirm} + /> + ); + + return ( + + + {downloadButton} + {deleteButton} + + {confirmModal} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx new file mode 100644 index 0000000000000..62d84adfc2649 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext } from 'react'; +import { useSelector } from 'react-redux'; + +import { canUserWrite as canUserWriteSelector } from '../../../state/selectors/app'; +import type { State } from '../../../../types'; +import { useDeleteWorkpads, useDownloadWorkpad } from '../hooks'; + +import { + WorkpadTableTools as Component, + Props as ComponentProps, +} from './workpad_table_tools.component'; +import { WorkpadsContext } from './my_workpads'; + +export type Props = Pick; + +export const WorkpadTableTools = ({ selectedWorkpadIds }: Props) => { + const deleteWorkpads = useDeleteWorkpads(); + const downloadWorkpad = useDownloadWorkpad(); + const context = useContext(WorkpadsContext); + + const { canUserWrite } = useSelector((state: State) => ({ + canUserWrite: canUserWriteSelector(state), + })); + + if (context === null || selectedWorkpadIds.length <= 0) { + return null; + } + + const { workpads, setWorkpads } = context; + + const onExport = () => selectedWorkpadIds.map((id) => downloadWorkpad(id)); + const onDelete = async () => { + const { removedIds } = await deleteWorkpads(selectedWorkpadIds); + setWorkpads(workpads.filter((workpad) => !removedIds.includes(workpad.id))); + }; + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx b/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx new file mode 100644 index 0000000000000..4c00306ac73ce --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { EuiButtonPropsForButton } from '@elastic/eui/src/components/button/button'; +import { ComponentStrings } from '../../../i18n'; + +const { WorkpadCreate: strings } = ComponentStrings; + +export interface Props + extends Omit { + canUserWrite: boolean; +} + +export const WorkpadCreate = ({ canUserWrite, disabled, ...rest }: Props) => { + return ( + + {strings.getWorkpadCreateButtonLabel()} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_create.tsx b/x-pack/plugins/canvas/public/components/home/workpad_create.tsx new file mode 100644 index 0000000000000..adb73a6bb8896 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/workpad_create.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { canUserWrite as canUserWriteSelector } from '../../state/selectors/app'; +import type { State } from '../../../types'; + +import { useCreateWorkpad } from './hooks'; +import { WorkpadCreate as Component, Props as ComponentProps } from './workpad_create.component'; + +type Props = Omit; + +export const WorkpadCreate = (props: Props) => { + const createWorkpad = useCreateWorkpad(); + + const { canUserWrite } = useSelector((state: State) => ({ + canUserWrite: canUserWriteSelector(state), + })); + + const onClick: ComponentProps['onClick'] = async () => { + await createWorkpad(); + }; + + return ; +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts b/x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts new file mode 100644 index 0000000000000..4c45dbff38377 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +export const LazyWorkpadTemplates = React.lazy(() => import('./workpad_templates')); diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx new file mode 100644 index 0000000000000..f9ecf7a2bb5cc --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { uniq } from 'lodash'; +import { + EuiInMemoryTable, + EuiBasicTableColumn, + EuiButtonEmpty, + EuiSearchBarProps, + SearchFilterConfig, +} from '@elastic/eui'; + +import { CanvasTemplate } from '../../../../types'; +import { ComponentStrings } from '../../../../i18n'; +import { tagsRegistry } from '../../../lib/tags_registry'; +import { TagList } from '../../tag_list'; + +export interface Props { + templates: CanvasTemplate[]; + onCreateWorkpad: (template: CanvasTemplate) => void; +} + +const { WorkpadTemplates: strings } = ComponentStrings; + +export const WorkpadTemplates = ({ templates, onCreateWorkpad }: Props) => { + const columns: Array> = [ + { + field: 'name', + name: strings.getTableNameColumnTitle(), + sortable: true, + width: '30%', + dataType: 'string', + render: (name: string, template) => { + const templateName = name.length ? name : 'Unnamed Template'; + + return ( + onCreateWorkpad(template)} + aria-label={strings.getCloneTemplateLinkAriaLabel(templateName)} + type="button" + > + {templateName} + + ); + }, + }, + { + field: 'help', + name: strings.getTableDescriptionColumnTitle(), + sortable: false, + dataType: 'string', + width: '30%', + }, + { + field: 'tags', + name: strings.getTableTagsColumnTitle(), + sortable: false, + dataType: 'string', + width: '30%', + render: (tags: string[]) => , + }, + ]; + + let uniqueTagNames: string[] = []; + + templates.forEach((template) => { + const { tags } = template; + tags.forEach((tag) => uniqueTagNames.push(tag)); + uniqueTagNames = uniq(uniqueTagNames); + }); + + const uniqueTags = uniqueTagNames.map( + (name) => + tagsRegistry.get(name) || { + color: undefined, + name, + } + ); + + const filters: SearchFilterConfig[] = [ + { + type: 'field_value_selection', + field: 'tags', + name: 'Tags', + multiSelect: true, + options: uniqueTags.map((tag) => ({ + value: tag.name, + name: tag.name, + view: , + })), + }, + ]; + + const search: EuiSearchBarProps = { + box: { + incremental: true, + schema: true, + }, + filters, + }; + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx new file mode 100644 index 0000000000000..8201ef55bf5a6 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiPanel } from '@elastic/eui'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; + +import { + reduxDecorator, + getAddonPanelParameters, + servicesContextDecorator, +} from '../../../../storybook'; +import { getSomeTemplates } from '../../../services/stubs/workpad'; + +import { WorkpadTemplates } from './workpad_templates'; +import { WorkpadTemplates as WorkpadTemplatesComponent } from './workpad_templates.component'; + +export default { + title: 'Home/Workpad Templates', + argTypes: {}, + decorators: [reduxDecorator()], + parameters: [getAddonPanelParameters()], +}; + +export const NoTemplates = () => { + return ( + + + + ); +}; + +export const HasTemplates = () => { + return ( + + + + ); +}; + +NoTemplates.decorators = [servicesContextDecorator()]; +HasTemplates.decorators = [servicesContextDecorator({ findTemplates: true })]; + +export const Component = ({ hasTemplates }: { hasTemplates: boolean }) => { + return ( + + + + ); +}; + +Component.args = { + hasTemplates: true, +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx new file mode 100644 index 0000000000000..352285e66424b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; + +import { useCreateFromTemplate, useFindTemplatesOnMount } from '../hooks'; + +import { WorkpadTemplates as Component } from './workpad_templates.component'; + +export const WorkpadTemplates = () => { + const [isMounted, templateResponse] = useFindTemplatesOnMount(); + const onCreateWorkpad = useCreateFromTemplate(); + + if (!isMounted) { + return ( + + + + + + ); + } + const { templates } = templateResponse; + + return ; +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default WorkpadTemplates; diff --git a/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx b/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx index 712b06cb39299..2e3e826cc32b5 100644 --- a/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx +++ b/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx @@ -6,9 +6,7 @@ */ import React, { FC } from 'react'; -import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; -// @ts-expect-error untyped local -import { WorkpadManager } from '../workpad_manager'; +import { Home } from '../home'; // @ts-expect-error untyped local import { setDocTitle } from '../../lib/doc_title'; @@ -19,17 +17,5 @@ export interface Props { export const HomeApp: FC = ({ onLoad = () => {} }) => { onLoad(); setDocTitle('Canvas'); - return ( - - - - {}} /> - - - - ); + return ; }; diff --git a/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx index e4f297446701c..bd47bb52e0030 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/__stories__/toolbar.stories.tsx @@ -18,7 +18,6 @@ storiesOf('components/Toolbar', module) isWriteable={true} selectedPageNumber={1} totalPages={1} - workpadId={'abc'} workpadName={'My Canvas Workpad'} /> )) @@ -28,7 +27,6 @@ storiesOf('components/Toolbar', module) selectedElement={getDefaultElement()} selectedPageNumber={1} totalPages={1} - workpadId={'abc'} workpadName={'My Canvas Workpad'} /> )); diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index baafbdafcc549..9e89ad4c4f27b 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -7,17 +7,8 @@ import React, { FC, useState, useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiModal, - EuiModalFooter, - EuiButton, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -// @ts-expect-error untyped local -import { WorkpadManager } from '../workpad_manager'; import { PageManager } from '../page_manager'; import { Expression } from '../expression'; import { Tray } from './tray'; @@ -37,7 +28,6 @@ export interface Props { selectedElement?: CanvasElement; selectedPageNumber: number; totalPages: number; - workpadId: string; workpadName: string; } @@ -46,11 +36,9 @@ export const Toolbar: FC = ({ selectedElement, selectedPageNumber, totalPages, - workpadId, workpadName, }) => { const [activeTray, setActiveTray] = useState(null); - const [showWorkpadManager, setShowWorkpadManager] = useState(false); const { getUrl, previousPage } = useContext(WorkpadRoutingContext); // While the tray doesn't get activated if the workpad isn't writeable, @@ -75,20 +63,6 @@ export const Toolbar: FC = ({ } }; - const closeWorkpadManager = () => setShowWorkpadManager(false); - const openWorkpadManager = () => setShowWorkpadManager(true); - - const workpadManager = ( - - - - - {strings.getWorkpadManagerCloseButtonLabel()} - - - - ); - const trays = { pageManager: , expression: !elementIsSelected ? null : setActiveTray(null)} />, @@ -99,12 +73,6 @@ export const Toolbar: FC = ({ {activeTray !== null && setActiveTray(null)}>{trays[activeTray]}}
- - openWorkpadManager()}> - {workpadName} - - - = ({ )}
- {showWorkpadManager && workpadManager} ); }; @@ -153,6 +120,5 @@ Toolbar.propTypes = { selectedElement: PropTypes.object, selectedPageNumber: PropTypes.number.isRequired, totalPages: PropTypes.number.isRequired, - workpadId: PropTypes.string.isRequired, workpadName: PropTypes.string.isRequired, }; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx b/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx deleted file mode 100644 index 2afd5fe70abe1..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FC, useState, useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; -import { useSelector } from 'react-redux'; -import moment from 'moment'; -// @ts-expect-error -import { getDefaultWorkpad } from '../../state/defaults'; -import { canUserWrite as canUserWriteSelector } from '../../state/selectors/app'; -import { getWorkpad } from '../../state/selectors/workpad'; -import { getId } from '../../lib/get_id'; -import { downloadWorkpad } from '../../lib/download_workpad'; -import { ComponentStrings, ErrorStrings } from '../../../i18n'; -import { State, CanvasWorkpad } from '../../../types'; -import { useNotifyService, useWorkpadService, usePlatformService } from '../../services'; -// @ts-expect-error -import { WorkpadLoader as Component } from './workpad_loader'; - -const { WorkpadLoader: strings } = ComponentStrings; -const { WorkpadLoader: errors } = ErrorStrings; - -type WorkpadStatePromise = ReturnType['find']>; -type WorkpadState = WorkpadStatePromise extends PromiseLike ? U : never; - -export const WorkpadLoader: FC<{ onClose: () => void }> = ({ onClose }) => { - const fromState = useSelector((state: State) => ({ - workpadId: getWorkpad(state).id, - canUserWrite: canUserWriteSelector(state), - })); - - const [workpadsState, setWorkpadsState] = useState(null); - const workpadService = useWorkpadService(); - const notifyService = useNotifyService(); - const platformService = usePlatformService(); - const history = useHistory(); - - const createWorkpad = useCallback( - async (_workpad: CanvasWorkpad | null | undefined) => { - const workpad = _workpad || getDefaultWorkpad(); - if (workpad != null) { - try { - await workpadService.create(workpad); - history.push(`/workpad/${workpad.id}/page/1`); - } catch (err) { - notifyService.error(err, { - title: errors.getUploadFailureErrorMessage(), - }); - } - return; - } - }, - [workpadService, notifyService, history] - ); - - const findWorkpads = useCallback( - async (text) => { - try { - const fetchedWorkpads = await workpadService.find(text); - setWorkpadsState(fetchedWorkpads); - } catch (err) { - notifyService.error(err, { title: errors.getFindFailureErrorMessage() }); - } - }, - [notifyService, workpadService] - ); - - const onDownloadWorkpad = useCallback((workpadId: string) => downloadWorkpad(workpadId), []); - - const cloneWorkpad = useCallback( - async (workpadId: string) => { - try { - const workpad = await workpadService.get(workpadId); - workpad.name = strings.getClonedWorkpadName(workpad.name); - workpad.id = getId('workpad'); - await workpadService.create(workpad); - history.push(`/workpad/${workpad.id}/page/1`); - } catch (err) { - notifyService.error(err, { title: errors.getCloneFailureErrorMessage() }); - } - }, - [notifyService, workpadService, history] - ); - - const removeWorkpads = useCallback( - (workpadIds: string[]) => { - if (workpadsState === null) { - return; - } - - const removedWorkpads = workpadIds.map(async (id) => { - try { - await workpadService.remove(id); - return { id, err: null }; - } catch (err) { - return { id, err }; - } - }); - - return Promise.all(removedWorkpads).then((results) => { - let redirectHome = false; - - const [passes, errored] = results.reduce<[string[], string[]]>( - ([passesArr, errorsArr], result) => { - if (result.id === fromState.workpadId && !result.err) { - redirectHome = true; - } - - if (result.err) { - errorsArr.push(result.id); - } else { - passesArr.push(result.id); - } - - return [passesArr, errorsArr]; - }, - [[], []] - ); - - const remainingWorkpads = workpadsState.workpads.filter(({ id }) => !passes.includes(id)); - - const workpadState = { - total: remainingWorkpads.length, - workpads: remainingWorkpads, - }; - - if (errored.length > 0) { - notifyService.error(errors.getDeleteFailureErrorMessage()); - } - - setWorkpadsState(workpadState); - - if (redirectHome) { - history.push('/'); - } - - return errored; - }); - }, - [history, workpadService, fromState.workpadId, workpadsState, notifyService] - ); - - const formatDate = useCallback( - (date: any) => { - const dateFormat = platformService.getUISetting('dateFormat'); - return date && moment(date).format(dateFormat); - }, - [platformService] - ); - - const { workpadId, canUserWrite } = fromState; - - return ( - - ); -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js b/x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js deleted file mode 100644 index 24a694268e4ee..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/upload_workpad.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { get } from 'lodash'; -import { getId } from '../../lib/get_id'; -import { ErrorStrings } from '../../../i18n'; - -const { WorkpadFileUpload: errors } = ErrorStrings; - -export const uploadWorkpad = (file, onUpload, notify) => { - if (!file) { - return; - } - - if (get(file, 'type') !== 'application/json') { - return notify.warning(errors.getAcceptJSONOnlyErrorMessage(), { - title: file.name - ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name) - : errors.getFileUploadFailureWithoutFileNameErrorMessage(), - }); - } - // TODO: Clean up this file, this loading stuff can, and should be, abstracted - const reader = new FileReader(); - - // handle reading the uploaded file - reader.onload = () => { - try { - const workpad = JSON.parse(reader.result); - workpad.id = getId('workpad'); - - // sanity check for workpad object - if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) { - throw new Error(errors.getMissingPropertiesErrorMessage()); - } - - onUpload(workpad); - } catch (e) { - notify.error(e, { - title: file.name - ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name) - : errors.getFileUploadFailureWithoutFileNameErrorMessage(), - }); - } - }; - - // read the uploaded file - reader.readAsText(file); -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js deleted file mode 100644 index 51733dad5b377..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { EuiButton } from '@elastic/eui'; -import { ComponentStrings } from '../../../i18n'; - -const { WorkpadCreate: strings } = ComponentStrings; - -export const WorkpadCreate = ({ createPending, onCreate, ...rest }) => ( - - {strings.getWorkpadCreateButtonLabel()} - -); - -WorkpadCreate.propTypes = { - onCreate: PropTypes.func.isRequired, - createPending: PropTypes.bool, -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js deleted file mode 100644 index 7c34837771c6f..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import PropTypes from 'prop-types'; -import { compose, withHandlers } from 'recompose'; -import { uploadWorkpad } from '../upload_workpad'; -import { ErrorStrings } from '../../../../i18n'; -import { WorkpadDropzone as Component } from './workpad_dropzone'; - -const { WorkpadFileUpload: errors } = ErrorStrings; - -export const WorkpadDropzone = compose( - withHandlers(({ notify }) => ({ - onDropAccepted: ({ onUpload }) => ([file]) => uploadWorkpad(file, onUpload), - onDropRejected: () => ([file]) => { - notify.warning(errors.getAcceptJSONOnlyErrorMessage(), { - title: file.name - ? errors.getFileUploadFailureWithFileNameErrorMessage(file.name) - : errors.getFileUploadFailureWithoutFileNameErrorMessage(), - }); - }, - })) -)(Component); - -WorkpadDropzone.propTypes = { - onUpload: PropTypes.func.isRequired, -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js deleted file mode 100644 index f77929e1feb76..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import Dropzone from 'react-dropzone'; - -export const WorkpadDropzone = ({ onDropAccepted, onDropRejected, disabled, children }) => ( - - {children} - -); - -WorkpadDropzone.propTypes = { - onDropAccepted: PropTypes.func.isRequired, - onDropRejected: PropTypes.func.isRequired, - disabled: PropTypes.bool.isRequired, - children: PropTypes.node.isRequired, -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss deleted file mode 100644 index ac6838da97fbd..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_dropzone/workpad_dropzone.scss +++ /dev/null @@ -1,22 +0,0 @@ -.canvasWorkpad__dropzone { - border: 2px dashed transparent; -} - -.canvasWorkpad__dropzone--active { - background-color: $euiColorLightestShade; - border-color: $euiColorLightShade; -} - -.canvasWorkpad__dropzoneTable .euiTable { - background-color: transparent; -} - -.canvasWorkpad__dropzoneTable--tags { - .euiTableCellContent { - flex-wrap: wrap; - } - - .euiHealth { - width: 100%; - } -} \ No newline at end of file diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js deleted file mode 100644 index 9c232ab43ec8d..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiBasicTable, - EuiButtonIcon, - EuiPagination, - EuiSpacer, - EuiButton, - EuiToolTip, - EuiEmptyPrompt, - EuiFilePicker, - EuiLink, -} from '@elastic/eui'; -import { orderBy } from 'lodash'; -import { ConfirmModal } from '../confirm_modal'; -import { RoutingLink } from '../routing'; -import { Paginate } from '../paginate'; -import { ComponentStrings } from '../../../i18n'; -import { WorkpadDropzone } from './workpad_dropzone'; -import { WorkpadCreate } from './workpad_create'; -import { WorkpadSearch } from './workpad_search'; -import { uploadWorkpad } from './upload_workpad'; - -const { WorkpadLoader: strings } = ComponentStrings; - -const getDisplayName = (name, workpad, loadedWorkpad) => { - const workpadName = name.length ? name : {workpad.id}; - return workpad.id === loadedWorkpad ? {workpadName} : workpadName; -}; - -export class WorkpadLoader extends React.PureComponent { - static propTypes = { - workpadId: PropTypes.string.isRequired, - canUserWrite: PropTypes.bool.isRequired, - createWorkpad: PropTypes.func.isRequired, - findWorkpads: PropTypes.func.isRequired, - downloadWorkpad: PropTypes.func.isRequired, - cloneWorkpad: PropTypes.func.isRequired, - removeWorkpads: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - workpads: PropTypes.object, - formatDate: PropTypes.func.isRequired, - }; - - state = { - createPending: false, - deletingWorkpad: false, - sortField: '@timestamp', - sortDirection: 'desc', - selectedWorkpads: [], - pageSize: 10, - }; - - async componentDidMount() { - // on component load, kick off the workpad search - this.props.findWorkpads(); - - // keep track of whether or not the component is mounted, to prevent rogue setState calls - this._isMounted = true; - } - - UNSAFE_componentWillReceiveProps(newProps) { - // the workpadId prop will change when a is created or loaded, close the toolbar when it does - const { workpadId, onClose } = this.props; - if (workpadId !== newProps.workpadId) { - onClose(); - } - } - - componentWillUnmount() { - this._isMounted = false; - } - - // create new empty workpad - createWorkpad = async () => { - this.setState({ createPending: true }); - await this.props.createWorkpad(); - this._isMounted && this.setState({ createPending: false }); - }; - - // create new workpad from uploaded JSON - onUpload = async (workpad) => { - this.setState({ createPending: true }); - await this.props.createWorkpad(workpad); - this._isMounted && this.setState({ createPending: false }); - }; - - // clone existing workpad - cloneWorkpad = async (workpad) => { - this.setState({ createPending: true }); - await this.props.cloneWorkpad(workpad.id); - this._isMounted && this.setState({ createPending: false }); - }; - - // Workpad remove methods - openRemoveConfirm = () => this.setState({ deletingWorkpad: true }); - - closeRemoveConfirm = () => this.setState({ deletingWorkpad: false }); - - removeWorkpads = () => { - const { selectedWorkpads } = this.state; - - this.props.removeWorkpads(selectedWorkpads.map(({ id }) => id)).then((remainingIds) => { - const remainingWorkpads = - remainingIds.length > 0 - ? selectedWorkpads.filter(({ id }) => remainingIds.includes(id)) - : []; - - this._isMounted && - this.setState({ - deletingWorkpad: false, - selectedWorkpads: remainingWorkpads, - }); - }); - }; - - // downloads selected workpads as JSON files - downloadWorkpads = () => { - this.state.selectedWorkpads.forEach(({ id }) => this.props.downloadWorkpad(id)); - }; - - onSelectionChange = (selectedWorkpads) => { - this.setState({ selectedWorkpads }); - }; - - onTableChange = ({ sort = {} }) => { - const { field: sortField, direction: sortDirection } = sort; - this.setState({ - sortField, - sortDirection, - }); - }; - - renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }) => { - const { sortField, sortDirection } = this.state; - const { canUserWrite, createPending, workpadId: loadedWorkpad } = this.props; - - const actions = [ - { - render: (workpad) => ( - - - - this.props.downloadWorkpad(workpad.id)} - aria-label={strings.getExportToolTip()} - /> - - - - - this.cloneWorkpad(workpad)} - aria-label={strings.getCloneToolTip()} - disabled={!canUserWrite} - /> - - - - ), - }, - ]; - - const columns = [ - { - field: 'name', - name: strings.getTableNameColumnTitle(), - sortable: true, - dataType: 'string', - render: (name, workpad) => { - const workpadName = getDisplayName(name, workpad, loadedWorkpad); - - return ( - - {workpadName} - - ); - }, - }, - { - field: '@created', - name: strings.getTableCreatedColumnTitle(), - sortable: true, - dataType: 'date', - width: '20%', - render: (date) => this.props.formatDate(date), - }, - { - field: '@timestamp', - name: strings.getTableUpdatedColumnTitle(), - sortable: true, - dataType: 'date', - width: '20%', - render: (date) => this.props.formatDate(date), - }, - { name: strings.getTableActionsColumnTitle(), actions, width: '100px' }, - ]; - - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - const selection = { - itemId: 'id', - onSelectionChange: this.onSelectionChange, - }; - - const emptyTable = ( - {strings.getEmptyPromptTitle()}} - titleSize="s" - body={ - -

{strings.getEmptyPromptGettingStartedDescription()}

-

- {strings.getEmptyPromptNewUserDescription()}{' '} - - {strings.getSampleDataLinkLabel()} - - . -

-
- } - /> - ); - - return ( - - - - - {rows.length > 0 && ( - - - - - - )} - - - ); - }; - - render() { - const { - deletingWorkpad, - createPending, - selectedWorkpads, - sortField, - sortDirection, - } = this.state; - const { canUserWrite } = this.props; - const isLoading = this.props.workpads == null; - - let createButton = ( - - ); - - let deleteButton = ( - - {strings.getDeleteButtonLabel(selectedWorkpads.length)} - - ); - - const downloadButton = ( - - {strings.getExportButtonLabel(selectedWorkpads.length)} - - ); - - let uploadButton = ( - uploadWorkpad(file, this.onUpload, this.props.notify)} - accept="application/json" - disabled={createPending || !canUserWrite} - /> - ); - - if (!canUserWrite) { - createButton = ( - {createButton} - ); - deleteButton = ( - {deleteButton} - ); - uploadButton = ( - {uploadButton} - ); - } - - const modalTitle = - selectedWorkpads.length === 1 - ? strings.getDeleteSingleWorkpadModalTitle(selectedWorkpads[0].name) - : strings.getDeleteMultipleWorkpadModalTitle(selectedWorkpads.length); - - const confirmModal = ( - - ); - - let sortedWorkpads = []; - - if (!createPending && !isLoading) { - const { workpads } = this.props.workpads; - sortedWorkpads = orderBy(workpads, [sortField, '@timestamp'], [sortDirection, 'desc']); - } - - return ( - - {(pagination) => ( - - - - - {selectedWorkpads.length > 0 && ( - - {downloadButton} - {deleteButton} - - )} - - { - pagination.setPage(0); - this.props.findWorkpads(text); - }} - /> - - - - - - {uploadButton} - {createButton} - - - - - - - {createPending && ( -
{strings.getCreateWorkpadLoadingDescription()}
- )} - - {!createPending && isLoading && ( -
{strings.getFetchLoadingDescription()}
- )} - - {!createPending && !isLoading && this.renderWorkpadTable(pagination)} - - {confirmModal} -
- )} -
- ); - } -} diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss deleted file mode 100644 index 3b2c8eae9e542..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.scss +++ /dev/null @@ -1,25 +0,0 @@ -.canvasWorkpad__upload--compressed { - - &.euiFilePicker--compressed.euiFilePicker { - .euiFilePicker__prompt { - height: $euiSizeXXL; - padding: $euiSizeM; - padding-left: $euiSizeXXL; - } - - .euiFilePicker__icon { - top: $euiSizeM; - } - } - - // The file picker input is being used moreso as a button, outside of a form, - // and thus the need to override the default max-width of form inputs. - // An issue has been opened in EUI to consider creating a button - // version of the file picker - https://github.com/elastic/eui/issues/1987 - - .euiFilePicker__wrap { - @include euiBreakpoint('xs', 's') { - max-width: none; - } - } -} \ No newline at end of file diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js deleted file mode 100644 index 8bf8bbae8ced4..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_search.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { EuiFieldSearch } from '@elastic/eui'; -import { debounce } from 'lodash'; -import { ComponentStrings } from '../../../i18n'; - -const { WorkpadSearch: strings } = ComponentStrings; -export class WorkpadSearch extends React.PureComponent { - static propTypes = { - onChange: PropTypes.func.isRequired, - initialText: PropTypes.string, - }; - - state = { - searchText: this.props.initialText || '', - }; - - triggerChange = debounce(this.props.onChange, 150); - - setSearchText = (ev) => { - const text = ev.target.value; - this.setState({ searchText: text }); - this.triggerChange(text); - }; - - render() { - return ( - - ); - } -} diff --git a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js b/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js deleted file mode 100644 index 8055be32ac481..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiTabbedContent, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { WorkpadLoader } from '../workpad_loader'; -import { WorkpadTemplates } from '../workpad_templates'; -import { ComponentStrings } from '../../../i18n'; - -const { WorkpadManager: strings } = ComponentStrings; - -export const WorkpadManager = ({ onClose }) => { - const tabs = [ - { - id: 'workpadLoader', - name: strings.getMyWorkpadsTabLabel(), - content: ( - - - - - ), - }, - { - id: 'workpadTemplates', - name: strings.getWorkpadTemplatesTabLabel(), - 'data-test-subj': 'workpadTemplates', - content: ( - - - - - ), - }, - ]; - return ( - - - - - -

{strings.getModalTitle()}

-
-
-
-
- - - -
- ); -}; - -WorkpadManager.propTypes = { - onClose: PropTypes.func, -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot deleted file mode 100644 index cab6e8fd9b5f5..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot +++ /dev/null @@ -1,564 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots components/WorkpadTemplates default 1`] = ` -
-
-
-
-
- -
- - -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - -
-
- - - - - Description - - - - - - Tags - - -
-
- Template name -
-
- -
-
-
- Description -
-
- - This is a test template - -
-
-
- Tags -
-
-
-
-
- -
-
- tag1 -
-
-
-
-
-
- -
-
- tag2 -
-
-
-
-
-
- Template name -
-
- -
-
-
- Description -
-
- - This is a second test template - -
-
-
- Tags -
-
-
-
-
- -
-
- tag2 -
-
-
-
-
-
- -
-
- tag3 -
-
-
-
-
-
-
-
-
-
- -
-
-
-`; diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx deleted file mode 100644 index 8e6c055478ca2..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/workpad_templates.stories.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import { WorkpadTemplates } from '../workpad_templates'; -import { CanvasTemplate } from '../../../../types'; - -const templates: Record = { - test1: { - id: 'test1-id', - name: 'test1', - help: 'This is a test template', - tags: ['tag1', 'tag2'], - template_key: 'test1-key', - }, - test2: { - id: 'test2-id', - name: 'test2', - help: 'This is a second test template', - tags: ['tag2', 'tag3'], - template_key: 'test2-key', - }, -}; - -storiesOf('components/WorkpadTemplates', module) - .addDecorator((story) =>
{story()}
) - .add('default', () => { - const onCreateFromTemplateAction = action('onCreateFromTemplate'); - return ( - { - onCreateFromTemplateAction(template); - return Promise.resolve(); - }} - /> - ); - }); diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx deleted file mode 100644 index 7e007b1253464..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useState, useEffect, FunctionComponent } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; -import { useHistory } from 'react-router-dom'; - -import { ComponentStrings } from '../../../i18n/components'; -// @ts-expect-error -import * as workpadService from '../../lib/workpad_service'; -import { WorkpadTemplates as Component } from './workpad_templates'; -import { CanvasTemplate } from '../../../types'; -import { list } from '../../lib/template_service'; -import { applyTemplateStrings } from '../../../i18n/templates/apply_strings'; -import { useNotifyService, useServices } from '../../services'; - -interface WorkpadTemplatesProps { - onClose: () => void; -} - -const Creating: FunctionComponent<{ name: string }> = ({ name }) => ( -
- {' '} - {ComponentStrings.WorkpadTemplates.getCreatingTemplateLabel(name)} -
-); -export const WorkpadTemplates: FunctionComponent = ({ onClose }) => { - const history = useHistory(); - const services = useServices(); - - const [templates, setTemplates] = useState(undefined); - const [creatingFromTemplateName, setCreatingFromTemplateName] = useState( - undefined - ); - const { error } = useNotifyService(); - - useEffect(() => { - if (!templates) { - (async () => { - const fetchedTemplates = await list(); - setTemplates(applyTemplateStrings(fetchedTemplates)); - })(); - } - }, [templates]); - - let templateProp: Record = {}; - - if (templates) { - templateProp = templates.reduce>((reduction, template) => { - reduction[template.name] = template; - return reduction; - }, {}); - } - - const createFromTemplate = useCallback( - async (template: CanvasTemplate) => { - setCreatingFromTemplateName(template.name); - try { - const result = await services.workpad.createFromTemplate(template.id); - history.push(`/workpad/${result.id}/page/1`); - } catch (e) { - setCreatingFromTemplateName(undefined); - error(e, { - title: `Couldn't create workpad from template`, - }); - } - }, - [services.workpad, error, history] - ); - - if (creatingFromTemplateName) { - return ; - } - - return ( - - ); -}; diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx deleted file mode 100644 index 72871b93c1735..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_templates/workpad_templates.tsx +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiBasicTable, - EuiPagination, - EuiSpacer, - EuiButtonEmpty, - EuiSearchBar, - EuiTableSortingType, - Direction, - SortDirection, -} from '@elastic/eui'; -import { orderBy } from 'lodash'; -// @ts-ignore untyped local -import { EuiBasicTableColumn } from '@elastic/eui'; -import { Paginate, PaginateChildProps } from '../paginate'; -import { TagList } from '../tag_list'; -import { getTagsFilter } from '../../lib/get_tags_filter'; -// @ts-expect-error -import { extractSearch } from '../../lib/extract_search'; -import { ComponentStrings } from '../../../i18n'; -import { CanvasTemplate } from '../../../types'; - -interface TableChange { - page?: { - index: number; - size: number; - }; - sort?: { - field: keyof T; - direction: Direction; - }; -} - -const { WorkpadTemplates: strings } = ComponentStrings; - -interface WorkpadTemplatesProps { - onCreateFromTemplate: (template: CanvasTemplate) => Promise; - onClose: () => void; - templates: Record; -} - -interface WorkpadTemplatesState { - sortField: string; - sortDirection: Direction; - pageSize: number; - searchTerm: string; - filterTags: string[]; -} - -export class WorkpadTemplates extends React.PureComponent< - WorkpadTemplatesProps, - WorkpadTemplatesState -> { - static propTypes = { - onCreateFromTemplate: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - templates: PropTypes.object, - }; - - state = { - sortField: 'name', - sortDirection: SortDirection.ASC, - pageSize: 10, - searchTerm: '', - filterTags: [], - }; - - tagType: 'health' = 'health'; - - onTableChange = (tableChange: TableChange) => { - if (tableChange.sort) { - const { field: sortField, direction: sortDirection } = tableChange.sort; - this.setState({ - sortField, - sortDirection, - }); - } - }; - - onSearch = ({ queryText = '' }) => this.setState(extractSearch(queryText)); - - cloneTemplate = (template: CanvasTemplate) => - this.props.onCreateFromTemplate(template).then(() => this.props.onClose()); - - renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }: PaginateChildProps) => { - const { sortField, sortDirection } = this.state; - - const columns: Array> = [ - { - field: 'name', - name: strings.getTableNameColumnTitle(), - sortable: true, - width: '30%', - dataType: 'string', - render: (name: string, template) => { - const templateName = name.length ? name : 'Unnamed Template'; - - return ( - this.cloneTemplate(template)} - aria-label={strings.getCloneTemplateLinkAriaLabel(templateName)} - type="button" - > - {templateName} - - ); - }, - }, - { - field: 'help', - name: strings.getTableDescriptionColumnTitle(), - sortable: false, - dataType: 'string', - width: '30%', - }, - { - field: 'tags', - name: strings.getTableTagsColumnTitle(), - sortable: false, - dataType: 'string', - width: '30%', - render: (tags: string[]) => , - }, - ]; - - const sorting: EuiTableSortingType = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - return ( - - - - {rows.length > 0 && ( - - - - - - )} - - ); - }; - - renderSearch = () => { - const { searchTerm } = this.state; - const filters = [getTagsFilter(this.tagType)]; - - return ( - - ); - }; - - render() { - const { templates } = this.props; - const { sortField, sortDirection, searchTerm, filterTags } = this.state; - const sortedTemplates = orderBy(templates, [sortField, 'name'], [sortDirection, 'asc']); - - const filteredTemplates = sortedTemplates.filter(({ name = '', help = '', tags = [] }) => { - const tagMatch = filterTags.length - ? filterTags.every((filterTag) => tags.indexOf(filterTag) > -1) - : true; - - const lowercaseSearch = searchTerm.toLowerCase(); - const textMatch = lowercaseSearch - ? name.toLowerCase().indexOf(lowercaseSearch) > -1 || - help.toLowerCase().indexOf(lowercaseSearch) > -1 - : true; - - return tagMatch && textMatch; - }); - - return ( - - {(pagination: PaginateChildProps) => ( - - {this.renderSearch()} - - {this.renderWorkpadTable(pagination)} - - )} - - ); - } -} diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index 6c039660c64c7..3f8f58367171a 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -34,7 +34,7 @@ export type CanvasServiceFactory = ( appUpdater: BehaviorSubject ) => Service | Promise; -class CanvasServiceProvider { +export class CanvasServiceProvider { private factory: CanvasServiceFactory; private service: Service | undefined; diff --git a/x-pack/plugins/canvas/public/services/stubs/platform.ts b/x-pack/plugins/canvas/public/services/stubs/platform.ts index ea80a5a7c26b9..5776a1d0d6983 100644 --- a/x-pack/plugins/canvas/public/services/stubs/platform.ts +++ b/x-pack/plugins/canvas/public/services/stubs/platform.ts @@ -9,13 +9,19 @@ import { PlatformService } from '../platform'; const noop = (..._args: any[]): any => {}; +const uiSettings: Record = { + dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', +}; + +const getUISetting = (setting: string) => uiSettings[setting]; + export const platformService: PlatformService = { getBasePath: () => '/base/path', getBasePathInterface: noop, getDocLinkVersion: () => 'dockLinkVersion', getElasticWebsiteUrl: () => 'https://elastic.co', getHasWriteAccess: () => true, - getUISetting: noop, + getUISetting, setBreadcrumbs: noop, setRecentlyAccessed: noop, getSavedObjects: noop, diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts index 857831c92a8a6..d9cd2ff8e0611 100644 --- a/x-pack/plugins/canvas/public/services/stubs/workpad.ts +++ b/x-pack/plugins/canvas/public/services/stubs/workpad.ts @@ -5,17 +5,93 @@ * 2.0. */ +import moment from 'moment'; + +// @ts-expect-error +import { getDefaultWorkpad } from '../../state/defaults'; import { WorkpadService } from '../workpad'; -import { CanvasWorkpad } from '../../../types'; +import { getId } from '../../lib/get_id'; +import { CanvasTemplate } from '../../../types'; -export const workpadService: WorkpadService = { - get: (id: string) => Promise.resolve({} as CanvasWorkpad), - create: (workpad) => Promise.resolve({} as CanvasWorkpad), - createFromTemplate: (templateId: string) => Promise.resolve({} as CanvasWorkpad), - find: (term: string) => - Promise.resolve({ +const promiseTimeout = (time: number) => () => new Promise((resolve) => setTimeout(resolve, time)); +const getName = () => { + const lorem = 'Lorem ipsum dolor sit amet consectetur adipiscing elit Fusce lobortis aliquet arcu ut turpis duis'.split( + ' ' + ); + return [1, 2, 3].map(() => lorem[Math.floor(Math.random() * lorem.length)]).join(' '); +}; + +const randomDate = ( + start: Date = moment().toDate(), + end: Date = moment().subtract(7, 'days').toDate() +) => new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toISOString(); + +const templates: CanvasTemplate[] = [ + { + id: 'test1-id', + name: 'test1', + help: 'This is a test template', + tags: ['tag1', 'tag2'], + template_key: 'test1-key', + }, + { + id: 'test2-id', + name: 'test2', + help: 'This is a second test template', + tags: ['tag2', 'tag3'], + template_key: 'test2-key', + }, +]; + +export const getSomeWorkpads = (count = 3) => + Array.from({ length: count }, () => ({ + '@created': randomDate( + moment().subtract(3, 'days').toDate(), + moment().subtract(10, 'days').toDate() + ), + '@timestamp': randomDate(), + id: getId('workpad'), + name: getName(), + })); + +export const findSomeWorkpads = (count = 3, timeout = 2000) => (_term: string) => { + return Promise.resolve() + .then(promiseTimeout(timeout)) + .then(() => ({ + total: count, + workpads: getSomeWorkpads(count), + })); +}; + +export const findNoWorkpads = (timeout = 2000) => (_term: string) => { + return Promise.resolve() + .then(promiseTimeout(timeout)) + .then(() => ({ total: 0, workpads: [], - }), - remove: (id: string) => Promise.resolve(undefined), + })); +}; + +export const findSomeTemplates = (timeout = 2000) => () => { + return Promise.resolve() + .then(promiseTimeout(timeout)) + .then(() => getSomeTemplates()); +}; + +export const findNoTemplates = (timeout = 2000) => () => { + return Promise.resolve() + .then(promiseTimeout(timeout)) + .then(() => getNoTemplates()); +}; + +export const getNoTemplates = () => ({ templates: [] }); +export const getSomeTemplates = () => ({ templates }); + +export const workpadService: WorkpadService = { + get: (id: string) => Promise.resolve({ ...getDefaultWorkpad(), id }), + findTemplates: findNoTemplates(), + create: (workpad) => Promise.resolve(workpad), + createFromTemplate: (_templateId: string) => Promise.resolve(getDefaultWorkpad()), + find: findNoWorkpads(), + remove: (id: string) => Promise.resolve(), }; diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts index 11690ca4c0c45..7d2f1550a312f 100644 --- a/x-pack/plugins/canvas/public/services/workpad.ts +++ b/x-pack/plugins/canvas/public/services/workpad.ts @@ -5,8 +5,12 @@ * 2.0. */ -import { API_ROUTE_WORKPAD, DEFAULT_WORKPAD_CSS } from '../../common/lib/constants'; -import { CanvasWorkpad } from '../../types'; +import { + API_ROUTE_WORKPAD, + DEFAULT_WORKPAD_CSS, + API_ROUTE_TEMPLATES, +} from '../../common/lib/constants'; +import { CanvasWorkpad, CanvasTemplate } from '../../types'; import { CanvasServiceFactory } from './'; /* @@ -40,9 +44,15 @@ const sanitizeWorkpad = function (workpad: CanvasWorkpad) { return workpad; }; -interface WorkpadFindResponse { +export type FoundWorkpads = Array>; +export type FoundWorkpad = FoundWorkpads[number]; +export interface WorkpadFindResponse { total: number; - workpads: Array>; + workpads: FoundWorkpads; +} + +export interface TemplateFindResponse { + templates: CanvasTemplate[]; } export interface WorkpadService { @@ -51,6 +61,7 @@ export interface WorkpadService { createFromTemplate: (templateId: string) => Promise; find: (term: string) => Promise; remove: (id: string) => Promise; + findTemplates: () => Promise; } export const workpadServiceFactory: CanvasServiceFactory = ( @@ -82,7 +93,9 @@ export const workpadServiceFactory: CanvasServiceFactory = ( body: JSON.stringify({ templateId }), }); }, + findTemplates: async () => coreStart.http.get(API_ROUTE_TEMPLATES), find: (searchTerm: string) => { + // TODO: this shouldn't be necessary. Check for usage. const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0; return coreStart.http.get(`${getApiPath()}/find`, { diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index a79e07a7d0016..d9592d5c0be5f 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -40,8 +40,6 @@ @import '../components/workpad_header/element_menu/element_menu'; @import '../components/workpad_header/share_menu/share_menu'; @import '../components/workpad_header/view_menu/view_menu'; -@import '../components/workpad_loader/workpad_loader'; -@import '../components/workpad_loader/workpad_dropzone/workpad_dropzone'; @import '../components/workpad_page/workpad_page'; @import '../components/workpad_page/workpad_interactive_page/workpad_interactive_page'; @import '../components/workpad_page/workpad_static_page/workpad_static_page'; diff --git a/x-pack/plugins/canvas/storybook/decorators/index.ts b/x-pack/plugins/canvas/storybook/decorators/index.ts index a674eaad576a7..598a2333be554 100644 --- a/x-pack/plugins/canvas/storybook/decorators/index.ts +++ b/x-pack/plugins/canvas/storybook/decorators/index.ts @@ -11,6 +11,7 @@ import { kibanaContextDecorator } from './kibana_decorator'; import { servicesContextDecorator } from './services_decorator'; export { reduxDecorator } from './redux_decorator'; +export { servicesContextDecorator } from './services_decorator'; export const addDecorators = () => { if (process.env.NODE_ENV === 'test') { @@ -20,5 +21,5 @@ export const addDecorators = () => { addDecorator(kibanaContextDecorator); addDecorator(routerContextDecorator); - addDecorator(servicesContextDecorator); + addDecorator(servicesContextDecorator()); }; diff --git a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx index 01d96cb0c70e6..289171f136ab5 100644 --- a/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx +++ b/x-pack/plugins/canvas/storybook/decorators/redux_decorator.tsx @@ -25,7 +25,7 @@ elementsRegistry.register(image); import { getInitialState, getReducer, getMiddleware, patchDispatch } from '../addon/src/state'; export { ADDON_ID, ACTIONS_PANEL_ID } from '../addon/src/constants'; -interface Params { +export interface Params { workpad?: CanvasWorkpad; elements?: CanvasElement[]; assets?: CanvasAsset[]; diff --git a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx index a11492387ea7f..def5a5681a8c4 100644 --- a/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx +++ b/x-pack/plugins/canvas/storybook/decorators/services_decorator.tsx @@ -7,8 +7,40 @@ import React from 'react'; -import { ServicesProvider } from '../../public/services'; +import { + CanvasServiceFactory, + CanvasServiceProvider, + ServicesProvider, +} from '../../public/services'; +import { + findNoWorkpads, + findSomeWorkpads, + workpadService, + findSomeTemplates, + findNoTemplates, +} from '../../public/services/stubs/workpad'; +import { WorkpadService } from '../../public/services/workpad'; -export const servicesContextDecorator = (story: Function) => ( - {story()} -); +interface Params { + findWorkpads?: number; + findTemplates?: boolean; +} + +export const servicesContextDecorator = ({ + findWorkpads = 0, + findTemplates: findTemplatesOption = false, +}: Params = {}) => { + const workpadServiceFactory: CanvasServiceFactory = (): WorkpadService => ({ + ...workpadService, + find: findWorkpads > 0 ? findSomeWorkpads(findWorkpads) : findNoWorkpads(), + findTemplates: findTemplatesOption ? findSomeTemplates() : findNoTemplates(), + }); + + const workpad = new CanvasServiceProvider(workpadServiceFactory); + // @ts-expect-error This is a hack at the moment, until we can get Canvas moved over to the new services architecture. + workpad.start(); + + return (story: Function) => ( + {story()} + ); +}; diff --git a/x-pack/plugins/canvas/storybook/index.ts b/x-pack/plugins/canvas/storybook/index.ts index 148af337d7720..ff60b84c88a69 100644 --- a/x-pack/plugins/canvas/storybook/index.ts +++ b/x-pack/plugins/canvas/storybook/index.ts @@ -10,3 +10,8 @@ import { ACTIONS_PANEL_ID } from './addon/src/constants'; export * from './decorators'; export { ACTIONS_PANEL_ID } from './addon/src/constants'; export const getAddonPanelParameters = () => ({ options: { selectedPanel: ACTIONS_PANEL_ID } }); +export const getDisableStoryshotsParameter = () => ({ + storyshots: { + disable: true, + }, +}); diff --git a/x-pack/plugins/canvas/storybook/main.ts b/x-pack/plugins/canvas/storybook/main.ts index 80a8aeb14a804..69c05322cf3f0 100644 --- a/x-pack/plugins/canvas/storybook/main.ts +++ b/x-pack/plugins/canvas/storybook/main.ts @@ -53,6 +53,11 @@ const canvasWebpack = { }, ], }, + resolve: { + alias: { + 'src/plugins': resolve(KIBANA_ROOT, 'src/plugins'), + }, + }, }; module.exports = { From ec956496b3e539010be1977676583beec402e38c Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Thu, 17 Jun 2021 23:45:15 -0500 Subject: [PATCH 3/3] Fix i18n and other tests --- x-pack/plugins/canvas/i18n/components.ts | 212 ------------------ x-pack/plugins/canvas/i18n/errors.ts | 49 ---- .../public/components/home/home.component.tsx | 23 +- .../public/components/home/home.stories.tsx | 3 +- .../components/home/{index.tsx => home.tsx} | 1 + .../home/hooks/use_clone_workpad.ts | 25 ++- .../home/hooks/use_create_workpad.ts | 11 +- .../home/hooks/use_delete_workpad.ts | 11 +- .../components/home/hooks/use_find_workpad.ts | 11 +- .../home/hooks/use_upload_workpad.ts | 38 +++- .../canvas/public/components/home/index.ts | 8 + .../home/my_workpads/empty_prompt.stories.tsx | 2 + .../home/my_workpads/empty_prompt.tsx | 31 ++- .../home/my_workpads/my_workpads.stories.tsx | 2 +- .../my_workpads/workpad_import.component.tsx | 16 +- .../my_workpads/workpad_table.component.tsx | 57 ++++- .../my_workpads/workpad_table.stories.tsx | 2 +- .../workpad_table_tools.component.tsx | 69 +++++- .../home/workpad_create.component.tsx | 11 +- .../workpad_templates.component.tsx | 42 +++- .../workpad_templates.stories.tsx | 3 +- .../canvas/public/lib/get_tags_filter.tsx | 39 ---- .../canvas/public/services/stubs/workpad.ts | 10 +- .../empty_prompt.stories.storyshot | 65 ++++++ .../canvas/storybook/storyshots.test.tsx | 7 +- .../translations/translations/ja-JP.json | 81 ++++--- .../translations/translations/zh-CN.json | 81 ++++--- x-pack/test/accessibility/apps/canvas.ts | 2 +- .../test/functional/apps/canvas/smoke_test.js | 2 +- .../functional/page_objects/canvas_page.ts | 2 +- 30 files changed, 476 insertions(+), 440 deletions(-) rename x-pack/plugins/canvas/public/components/home/{index.tsx => home.tsx} (99%) create mode 100644 x-pack/plugins/canvas/public/components/home/index.ts delete mode 100644 x-pack/plugins/canvas/public/lib/get_tags_filter.tsx create mode 100644 x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index d6165057d5acd..6f011bb73e3b0 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -504,17 +504,6 @@ export const ComponentStrings = { defaultMessage: 'Keyboard shortcuts', }), }, - Home: { - getMyWorkpadsTabLabel: () => - i18n.translate('xpack.canvas.workpadManager.myWorkpadsTabLabel', { - defaultMessage: 'My workpads', - }), - getWorkpadTemplatesTabLabel: () => - i18n.translate('xpack.canvas.workpadManager.workpadTemplatesTabLabel', { - defaultMessage: 'Templates', - description: 'The label for the tab that displays a list of designed workpad templates.', - }), - }, KeyboardShortcutsDoc: { getFlyoutCloseButtonAriaLabel: () => i18n.translate('xpack.canvas.keyboardShortcutsDoc.flyout.closeButtonAriaLabel', { @@ -1177,12 +1166,6 @@ export const ComponentStrings = { description: 'This is referring to the dimensions of U.S. standard letter paper.', }), }, - WorkpadCreate: { - getWorkpadCreateButtonLabel: () => - i18n.translate('xpack.canvas.workpadCreate.createButtonLabel', { - defaultMessage: 'Create workpad', - }), - }, WorkpadHeader: { getAddElementButtonLabel: () => i18n.translate('xpack.canvas.workpadHeader.addElementButtonLabel', { @@ -1557,199 +1540,4 @@ export const ComponentStrings = { defaultMessage: 'Reset', }), }, - WorkpadImport: { - getFilePickerPlaceholder: () => - i18n.translate('xpack.canvas.workpadImport.filePickerPlaceholder', { - defaultMessage: 'Import workpad {JSON} file', - values: { - JSON, - }, - }), - }, - useCloneWorkpad: { - getClonedWorkpadName: (workpadName: string) => - i18n.translate('xpack.canvas.useCloneWorkpad.clonedWorkpadName', { - defaultMessage: 'Copy of {workpadName}', - values: { - workpadName, - }, - description: - 'This suffix is added to the end of the name of a cloned workpad to indicate that this ' + - 'new workpad is a copy of the original workpad. Example: "Copy of Sales Pitch"', - }), - }, - WorkpadTable: { - getCloneToolTip: () => - i18n.translate('xpack.canvas.workpadTable.cloneTooltip', { - defaultMessage: 'Clone workpad', - }), - getExportToolTip: () => - i18n.translate('xpack.canvas.workpadTable.exportTooltip', { - defaultMessage: 'Export workpad', - }), - getLoadWorkpadArialLabel: (workpadName: string) => - i18n.translate('xpack.canvas.workpadTable.loadWorkpadArialLabel', { - defaultMessage: `Load workpad '{workpadName}'`, - values: { - workpadName, - }, - }), - getNoPermissionToCloneToolTip: () => - i18n.translate('xpack.canvas.workpadTable.noPermissionToCloneToolTip', { - defaultMessage: `You don't have permission to clone workpads`, - }), - getNoWorkpadsFoundMessage: () => - i18n.translate('xpack.canvas.workpadTable.noWorkpadsFoundMessage', { - defaultMessage: 'No workpads matched your search.', - }), - getWorkpadSearchPlaceholder: () => - i18n.translate('xpack.canvas.workpadTable.searchPlaceholder', { - defaultMessage: 'Find workpad', - }), - getTableCreatedColumnTitle: () => - i18n.translate('xpack.canvas.workpadTable.table.createdColumnTitle', { - defaultMessage: 'Created', - description: 'This column in the table contains the date/time the workpad was created.', - }), - getTableNameColumnTitle: () => - i18n.translate('xpack.canvas.workpadTable.table.nameColumnTitle', { - defaultMessage: 'Workpad name', - }), - getTableUpdatedColumnTitle: () => - i18n.translate('xpack.canvas.workpadTable.table.updatedColumnTitle', { - defaultMessage: 'Updated', - description: - 'This column in the table contains the date/time the workpad was last updated.', - }), - getTableActionsColumnTitle: () => - i18n.translate('xpack.canvas.workpadTable.table.actionsColumnTitle', { - defaultMessage: 'Actions', - description: - 'This column in the table contains the actions that can be taken on a workpad.', - }), - }, - WorkpadTableTools: { - getDeleteButtonAriaLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadTableTools.deleteButtonAriaLabel', { - defaultMessage: 'Delete {numberOfWorkpads} workpads', - values: { - numberOfWorkpads, - }, - }), - getDeleteButtonLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadTableTools.deleteButtonLabel', { - defaultMessage: 'Delete ({numberOfWorkpads})', - values: { - numberOfWorkpads, - }, - }), - getDeleteModalConfirmButtonLabel: () => - i18n.translate('xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel', { - defaultMessage: 'Delete', - }), - getDeleteModalDescription: () => - i18n.translate('xpack.canvas.workpadTableTools.deleteModalDescription', { - defaultMessage: `You can't recover deleted workpads.`, - }), - getDeleteMultipleWorkpadModalTitle: (numberOfWorkpads: string) => - i18n.translate('xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle', { - defaultMessage: 'Delete {numberOfWorkpads} workpads?', - values: { - numberOfWorkpads, - }, - }), - getDeleteSingleWorkpadModalTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle', { - defaultMessage: `Delete workpad '{workpadName}'?`, - values: { - workpadName, - }, - }), - getExportButtonAriaLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadTableTools.exportButtonAriaLabel', { - defaultMessage: 'Export {numberOfWorkpads} workpads', - values: { - numberOfWorkpads, - }, - }), - getExportButtonLabel: (numberOfWorkpads: number) => - i18n.translate('xpack.canvas.workpadTableTools.exportButtonLabel', { - defaultMessage: 'Export ({numberOfWorkpads})', - values: { - numberOfWorkpads, - }, - }), - getNoPermissionToCreateToolTip: () => - i18n.translate('xpack.canvas.workpadTableTools.noPermissionToCreateToolTip', { - defaultMessage: `You don't have permission to create workpads`, - }), - getNoPermissionToDeleteToolTip: () => - i18n.translate('xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip', { - defaultMessage: `You don't have permission to delete workpads`, - }), - getNoPermissionToUploadToolTip: () => - i18n.translate('xpack.canvas.workpadTableTools.noPermissionToUploadToolTip', { - defaultMessage: `You don't have permission to upload workpads`, - }), - }, - WorkpadTemplates: { - getCloneTemplateLinkAriaLabel: (templateName: string) => - i18n.translate('xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel', { - defaultMessage: `Clone workpad template '{templateName}'`, - values: { - templateName, - }, - }), - getTableDescriptionColumnTitle: () => - i18n.translate('xpack.canvas.workpadTemplates.table.descriptionColumnTitle', { - defaultMessage: 'Description', - }), - getTableNameColumnTitle: () => - i18n.translate('xpack.canvas.workpadTemplates.table.nameColumnTitle', { - defaultMessage: 'Template name', - }), - getTableTagsColumnTitle: () => - i18n.translate('xpack.canvas.workpadTemplates.table.tagsColumnTitle', { - defaultMessage: 'Tags', - description: - 'This column contains relevant tags that indicate what type of template ' + - 'is displayed. For example: "report", "presentation", etc.', - }), - getTemplateSearchPlaceholder: () => - i18n.translate('xpack.canvas.workpadTemplates.searchPlaceholder', { - defaultMessage: 'Find template', - }), - getCreatingTemplateLabel: (templateName: string) => - i18n.translate('xpack.canvas.workpadTemplates.creatingTemplateLabel', { - defaultMessage: `Creating from template '{templateName}'`, - values: { - templateName, - }, - }), - }, - WorkpadUploadPrompt: { - getEmptyPromptGettingStartedDescription: () => - i18n.translate('xpack.canvas.workpadUploadPrompt.emptyPromptGettingStartedDescription', { - defaultMessage: - 'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.', - values: { - JSON, - }, - }), - getEmptyPromptNewUserDescription: () => - i18n.translate('xpack.canvas.workpadUploadPrompt.emptyPromptNewUserDescription', { - defaultMessage: 'New to {CANVAS}?', - values: { - CANVAS, - }, - }), - getEmptyPromptTitle: () => - i18n.translate('xpack.canvas.workpadUploadPrompt.emptyPromptTitle', { - defaultMessage: 'Add your first workpad', - }), - getSampleDataLinkLabel: () => - i18n.translate('xpack.canvas.workpadUploadPrompt.sampleDataLinkLabel', { - defaultMessage: 'Add your first workpad', - }), - }, }; diff --git a/x-pack/plugins/canvas/i18n/errors.ts b/x-pack/plugins/canvas/i18n/errors.ts index 902fc0acd256a..a55762dce2d20 100644 --- a/x-pack/plugins/canvas/i18n/errors.ts +++ b/x-pack/plugins/canvas/i18n/errors.ts @@ -6,7 +6,6 @@ */ import { i18n } from '@kbn/i18n'; -import { CANVAS, JSON } from './constants'; export const ErrorStrings = { actionsElements: { @@ -99,54 +98,6 @@ export const ErrorStrings = { defaultMessage: 'One one file can be uploaded at a time', }), }, - workpadLoader: { - getAcceptJSONOnlyErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage', { - defaultMessage: 'Only {JSON} files are accepted', - values: { - JSON, - }, - }), - getMissingPropertiesErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage', { - defaultMessage: - 'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.', - values: { - CANVAS, - JSON, - }, - }), - getFileUploadFailureWithoutFileNameErrorMessage: () => - i18n.translate( - 'xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage', - { - defaultMessage: `Couldn't upload file`, - } - ), - getFileUploadFailureWithFileNameErrorMessage: (fileName: string) => - i18n.translate('xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage', { - defaultMessage: `Couldn't upload '{fileName}'`, - values: { - fileName, - }, - }), - getUploadFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.uploadFailureErrorMessage', { - defaultMessage: `Couldn't upload workpad`, - }), - getDeleteFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.deleteFailureErrorMessage', { - defaultMessage: `Couldn't delete all workpads`, - }), - getCloneFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.cloneFailureErrorMessage', { - defaultMessage: `Couldn't clone workpad`, - }), - getFindFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.workpadLoader.findFailureErrorMessage', { - defaultMessage: `Couldn't find workpad`, - }), - }, workpadRoutes: { getCreateFailureErrorMessage: () => i18n.translate('xpack.canvas.error.workpadRoutes.createFailureErrorMessage', { diff --git a/x-pack/plugins/canvas/public/components/home/home.component.tsx b/x-pack/plugins/canvas/public/components/home/home.component.tsx index 5d62cd0516610..96a773186da2b 100644 --- a/x-pack/plugins/canvas/public/components/home/home.component.tsx +++ b/x-pack/plugins/canvas/public/components/home/home.component.tsx @@ -6,16 +6,14 @@ */ import React, { useState } from 'react'; -import { EuiPageTemplate } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { KibanaPageTemplate } from '../../../../../../src/plugins/kibana_react/public'; import { withSuspense } from '../../../../../../src/plugins/presentation_util/public'; -import { ComponentStrings } from '../../../i18n'; import { WorkpadCreate } from './workpad_create'; import { LazyWorkpadTemplates } from './workpad_templates'; import { LazyMyWorkpads } from './my_workpads'; -const { Home: strings } = ComponentStrings; - export type HomePageTab = 'workpads' | 'templates'; export interface Props { @@ -29,10 +27,11 @@ export const Home = ({ activeTab = 'workpads' }: Props) => { const [tab, setTab] = useState(activeTab); return ( - ], + bottomBorder: true, tabs: [ { label: strings.getMyWorkpadsTabLabel(), @@ -51,6 +50,18 @@ export const Home = ({ activeTab = 'workpads' }: Props) => { }} > {tab === 'workpads' ? : } - + ); }; + +const strings = { + getMyWorkpadsTabLabel: () => + i18n.translate('xpack.canvas.home.myWorkpadsTabLabel', { + defaultMessage: 'My workpads', + }), + getWorkpadTemplatesTabLabel: () => + i18n.translate('xpack.canvas.home.workpadTemplatesTabLabel', { + defaultMessage: 'Templates', + description: 'The label for the tab that displays a list of designed workpad templates.', + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/home.stories.tsx b/x-pack/plugins/canvas/public/components/home/home.stories.tsx index 1f442e2dd9bfc..186b916afa003 100644 --- a/x-pack/plugins/canvas/public/components/home/home.stories.tsx +++ b/x-pack/plugins/canvas/public/components/home/home.stories.tsx @@ -11,6 +11,7 @@ import { reduxDecorator, getAddonPanelParameters, servicesContextDecorator, + getDisableStoryshotsParameter, } from '../../../storybook'; import { Home } from './home.component'; @@ -19,7 +20,7 @@ export default { title: 'Home/Home Page', argTypes: {}, decorators: [reduxDecorator()], - parameters: [getAddonPanelParameters()], + parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() }, }; export const NoContent = () => ; diff --git a/x-pack/plugins/canvas/public/components/home/index.tsx b/x-pack/plugins/canvas/public/components/home/home.tsx similarity index 99% rename from x-pack/plugins/canvas/public/components/home/index.tsx rename to x-pack/plugins/canvas/public/components/home/home.tsx index afb0ef1ca979c..6b356ada8681e 100644 --- a/x-pack/plugins/canvas/public/components/home/index.tsx +++ b/x-pack/plugins/canvas/public/components/home/home.tsx @@ -7,6 +7,7 @@ import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; + import { getBaseBreadcrumb } from '../../lib/breadcrumbs'; import { resetWorkpad } from '../../state/actions/workpad'; import { Home as Component } from './home.component'; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts index 15e397da4f603..001a711a58a72 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_clone_workpad.ts @@ -7,14 +7,11 @@ import { useCallback } from 'react'; import { useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; import { useNotifyService, useWorkpadService } from '../../../services'; -import { ErrorStrings, ComponentStrings } from '../../../../i18n'; import { getId } from '../../../lib/get_id'; -const { workpadLoader: errors } = ErrorStrings; -const { useCloneWorkpad: strings } = ComponentStrings; - export const useCloneWorkpad = () => { const workpadService = useWorkpadService(); const notifyService = useNotifyService(); @@ -41,3 +38,23 @@ export const useCloneWorkpad = () => { [notifyService, workpadService, history] ); }; + +const strings = { + getClonedWorkpadName: (workpadName: string) => + i18n.translate('xpack.canvas.useCloneWorkpad.clonedWorkpadName', { + defaultMessage: 'Copy of {workpadName}', + values: { + workpadName, + }, + description: + 'This suffix is added to the end of the name of a cloned workpad to indicate that this ' + + 'new workpad is a copy of the original workpad. Example: "Copy of Sales Pitch"', + }), +}; + +const errors = { + getCloneFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage', { + defaultMessage: `Couldn't clone workpad`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts index b388edd346fd6..eb87f4720deec 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts @@ -7,16 +7,14 @@ import { useCallback } from 'react'; import { useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; // @ts-expect-error import { getDefaultWorkpad } from '../../../state/defaults'; import { useNotifyService, useWorkpadService } from '../../../services'; -import { ErrorStrings } from '../../../../i18n'; import type { CanvasWorkpad } from '../../../../types'; -const { workpadLoader: errors } = ErrorStrings; - export const useCreateWorkpad = () => { const workpadService = useWorkpadService(); const notifyService = useNotifyService(); @@ -39,3 +37,10 @@ export const useCreateWorkpad = () => { [notifyService, history, workpadService] ); }; + +const errors = { + getUploadFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage', { + defaultMessage: `Couldn't upload workpad`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts index 26bf7889e9014..722ddae7411c9 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_delete_workpad.ts @@ -6,11 +6,9 @@ */ import { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; import { useNotifyService, useWorkpadService } from '../../../services'; -import { ErrorStrings } from '../../../../i18n'; - -const { workpadLoader: errors } = ErrorStrings; export const useDeleteWorkpads = () => { const workpadService = useWorkpadService(); @@ -56,3 +54,10 @@ export const useDeleteWorkpads = () => { [workpadService, notifyService] ); }; + +const errors = { + getDeleteFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage', { + defaultMessage: `Couldn't delete all workpads`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts index 23099ae7de45b..3f8b0e6f630f5 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_find_workpad.ts @@ -7,13 +7,11 @@ import { useState, useCallback } from 'react'; import useMount from 'react-use/lib/useMount'; +import { i18n } from '@kbn/i18n'; import { WorkpadFindResponse } from '../../../services/workpad'; import { useNotifyService, useWorkpadService } from '../../../services'; -import { ErrorStrings } from '../../../../i18n'; - -const { workpadLoader: errors } = ErrorStrings; const emptyResponse = { total: 0, workpads: [] }; export const useFindWorkpads = () => { @@ -50,3 +48,10 @@ export const useFindWorkpadsOnMount = (): [boolean, WorkpadFindResponse] => { return [isMounted, workpadResponse]; }; + +const errors = { + getFindFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useFindWorkpads.findFailureErrorMessage', { + defaultMessage: `Couldn't find workpad`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts index 1891d5df4a218..7934a469bb7a2 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts @@ -7,14 +7,13 @@ import { useCallback } from 'react'; import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { CANVAS, JSON as JSONString } from '../../../../i18n/constants'; import { useNotifyService } from '../../../services'; -import { ErrorStrings } from '../../../../i18n'; import { getId } from '../../../lib/get_id'; import type { CanvasWorkpad } from '../../../../types'; -const { workpadLoader: errors } = ErrorStrings; - export const useImportWorkpad = () => { const notifyService = useNotifyService(); @@ -66,3 +65,36 @@ export const useImportWorkpad = () => { [notifyService] ); }; + +const errors = { + getFileUploadFailureWithoutFileNameErrorMessage: () => + i18n.translate( + 'xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage', + { + defaultMessage: `Couldn't upload file`, + } + ), + getFileUploadFailureWithFileNameErrorMessage: (fileName: string) => + i18n.translate('xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage', { + defaultMessage: `Couldn't upload '{fileName}'`, + values: { + fileName, + }, + }), + getMissingPropertiesErrorMessage: () => + i18n.translate('xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage', { + defaultMessage: + 'Some properties required for a {CANVAS} workpad are missing. Edit your {JSON} file to provide the correct property values, and try again.', + values: { + CANVAS, + JSON: JSONString, + }, + }), + getAcceptJSONOnlyErrorMessage: () => + i18n.translate('xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage', { + defaultMessage: 'Only {JSON} files are accepted', + values: { + JSON: JSONString, + }, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/index.ts b/x-pack/plugins/canvas/public/components/home/index.ts new file mode 100644 index 0000000000000..aeb62c3a8de78 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/index.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { Home } from './home'; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx index 5f8310f8d3f10..aef1b0625b585 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.stories.tsx @@ -8,10 +8,12 @@ import React from 'react'; import { HomeEmptyPrompt } from './empty_prompt'; +import { getDisableStoryshotsParameter } from '../../../../storybook'; export default { title: 'Home/Empty Prompt', argTypes: {}, + parameters: { ...getDisableStoryshotsParameter() }, }; export const EmptyPrompt = () => ; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx index 0e506636d0580..797f50ac112d0 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/empty_prompt.tsx @@ -6,11 +6,10 @@ */ import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { ComponentStrings } from '../../../../i18n'; - -const { WorkpadUploadPrompt: strings } = ComponentStrings; +import { CANVAS, JSON } from '../../../../i18n/constants'; export const HomeEmptyPrompt = () => ( @@ -38,3 +37,29 @@ export const HomeEmptyPrompt = () => ( ); + +const strings = { + getEmptyPromptGettingStartedDescription: () => + i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription', { + defaultMessage: + 'Create a new workpad, start from a template, or import a workpad {JSON} file by dropping it here.', + values: { + JSON, + }, + }), + getEmptyPromptNewUserDescription: () => + i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription', { + defaultMessage: 'New to {CANVAS}?', + values: { + CANVAS, + }, + }), + getEmptyPromptTitle: () => + i18n.translate('xpack.canvas.homeEmptyPrompt.emptyPromptTitle', { + defaultMessage: 'Add your first workpad', + }), + getSampleDataLinkLabel: () => + i18n.translate('xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel', { + defaultMessage: 'Add your first workpad', + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx index 7aedce57814f6..0d5d6ca16f614 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/my_workpads.stories.tsx @@ -23,7 +23,7 @@ export default { title: 'Home/My Workpads', argTypes: {}, decorators: [reduxDecorator()], - parameters: [getAddonPanelParameters(), getDisableStoryshotsParameter()], + parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() }, }; export const NoWorkpads = () => { diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx index b4a8467d4d703..28e2aa0449d46 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_import.component.tsx @@ -6,12 +6,10 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiFilePicker, EuiFilePickerProps } from '@elastic/eui'; -import { ComponentStrings } from '../../../../i18n'; - -const { WorkpadImport: strings } = ComponentStrings; - +import { JSON } from '../../../../i18n/constants'; export interface Props { canUserWrite: boolean; onImportWorkpad?: EuiFilePickerProps['onChange']; @@ -30,3 +28,13 @@ export const WorkpadImport = ({ uniqueKey, canUserWrite, onImportWorkpad = () => disabled={!canUserWrite} /> ); + +const strings = { + getFilePickerPlaceholder: () => + i18n.translate('xpack.canvas.workpadImport.filePickerPlaceholder', { + defaultMessage: 'Import workpad {JSON} file', + values: { + JSON, + }, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx index 58a6b50f8ad39..5301a88844369 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.component.tsx @@ -6,6 +6,7 @@ */ import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiInMemoryTable, EuiInMemoryTableProps, @@ -20,7 +21,6 @@ import { import moment from 'moment'; import { RoutingLink } from '../../routing'; -import { ComponentStrings } from '../../../../i18n'; import { FoundWorkpad } from '../../../services/workpad'; import { WorkpadTableTools } from './workpad_table_tools'; import { WorkpadImport } from './workpad_import'; @@ -33,8 +33,6 @@ export interface Props { onCloneWorkpad: (id: string) => void; } -const { WorkpadTable: strings } = ComponentStrings; - const getDisplayName = (name: string, workpadId: string, loadedWorkpadId?: string) => { const workpadName = name.length ? {name} : {workpadId}; return workpadId === loadedWorkpadId ? {workpadName} : workpadName; @@ -108,7 +106,7 @@ export const WorkpadTable = ({ dataType: 'string', render: (name, workpad) => ( @@ -150,7 +148,56 @@ export const WorkpadTable = ({ }} pagination={true} selection={selection} - data-test-subj="workpadTable" + data-test-subj="canvasWorkpadTable" /> ); }; + +const strings = { + getCloneToolTip: () => + i18n.translate('xpack.canvas.workpadTable.cloneTooltip', { + defaultMessage: 'Clone workpad', + }), + getExportToolTip: () => + i18n.translate('xpack.canvas.workpadTable.exportTooltip', { + defaultMessage: 'Export workpad', + }), + getLoadWorkpadArialLabel: (workpadName: string) => + i18n.translate('xpack.canvas.workpadTable.loadWorkpadArialLabel', { + defaultMessage: `Load workpad '{workpadName}'`, + values: { + workpadName, + }, + }), + getNoPermissionToCloneToolTip: () => + i18n.translate('xpack.canvas.workpadTable.noPermissionToCloneToolTip', { + defaultMessage: `You don't have permission to clone workpads`, + }), + getNoWorkpadsFoundMessage: () => + i18n.translate('xpack.canvas.workpadTable.noWorkpadsFoundMessage', { + defaultMessage: 'No workpads matched your search.', + }), + getWorkpadSearchPlaceholder: () => + i18n.translate('xpack.canvas.workpadTable.searchPlaceholder', { + defaultMessage: 'Find workpad', + }), + getTableCreatedColumnTitle: () => + i18n.translate('xpack.canvas.workpadTable.table.createdColumnTitle', { + defaultMessage: 'Created', + description: 'This column in the table contains the date/time the workpad was created.', + }), + getTableNameColumnTitle: () => + i18n.translate('xpack.canvas.workpadTable.table.nameColumnTitle', { + defaultMessage: 'Workpad name', + }), + getTableUpdatedColumnTitle: () => + i18n.translate('xpack.canvas.workpadTable.table.updatedColumnTitle', { + defaultMessage: 'Updated', + description: 'This column in the table contains the date/time the workpad was last updated.', + }), + getTableActionsColumnTitle: () => + i18n.translate('xpack.canvas.workpadTable.table.actionsColumnTitle', { + defaultMessage: 'Actions', + description: 'This column in the table contains the actions that can be taken on a workpad.', + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx index 1cbd3b842ba6d..501a0a76a8589 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table.stories.tsx @@ -24,7 +24,7 @@ export default { title: 'Home/Workpad Table', argTypes: {}, decorators: [reduxDecorator()], - parameters: [getAddonPanelParameters(), getDisableStoryshotsParameter()], + parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() }, }; export const NoWorkpads = () => { diff --git a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx index 256447d0df200..ae6ff9c3cc910 100644 --- a/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx +++ b/x-pack/plugins/canvas/public/components/home/my_workpads/workpad_table_tools.component.tsx @@ -6,14 +6,12 @@ */ import React, { useState, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiButton, EuiToolTip, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import { ComponentStrings } from '../../../../i18n'; import { ConfirmModal } from '../../confirm_modal'; import { FoundWorkpad } from '../../../services/workpad'; -const { WorkpadTableTools: strings } = ComponentStrings; - export interface Props { workpads: FoundWorkpad[]; canUserWrite: boolean; @@ -95,3 +93,68 @@ export const WorkpadTableTools = ({ ); }; + +const strings = { + getDeleteButtonAriaLabel: (numberOfWorkpads: number) => + i18n.translate('xpack.canvas.workpadTableTools.deleteButtonAriaLabel', { + defaultMessage: 'Delete {numberOfWorkpads} workpads', + values: { + numberOfWorkpads, + }, + }), + getDeleteButtonLabel: (numberOfWorkpads: number) => + i18n.translate('xpack.canvas.workpadTableTools.deleteButtonLabel', { + defaultMessage: 'Delete ({numberOfWorkpads})', + values: { + numberOfWorkpads, + }, + }), + getDeleteModalConfirmButtonLabel: () => + i18n.translate('xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel', { + defaultMessage: 'Delete', + }), + getDeleteModalDescription: () => + i18n.translate('xpack.canvas.workpadTableTools.deleteModalDescription', { + defaultMessage: `You can't recover deleted workpads.`, + }), + getDeleteMultipleWorkpadModalTitle: (numberOfWorkpads: string) => + i18n.translate('xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle', { + defaultMessage: 'Delete {numberOfWorkpads} workpads?', + values: { + numberOfWorkpads, + }, + }), + getDeleteSingleWorkpadModalTitle: (workpadName: string) => + i18n.translate('xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle', { + defaultMessage: `Delete workpad '{workpadName}'?`, + values: { + workpadName, + }, + }), + getExportButtonAriaLabel: (numberOfWorkpads: number) => + i18n.translate('xpack.canvas.workpadTableTools.exportButtonAriaLabel', { + defaultMessage: 'Export {numberOfWorkpads} workpads', + values: { + numberOfWorkpads, + }, + }), + getExportButtonLabel: (numberOfWorkpads: number) => + i18n.translate('xpack.canvas.workpadTableTools.exportButtonLabel', { + defaultMessage: 'Export ({numberOfWorkpads})', + values: { + numberOfWorkpads, + }, + }), + getNoPermissionToCreateToolTip: () => + i18n.translate('xpack.canvas.workpadTableTools.noPermissionToCreateToolTip', { + defaultMessage: `You don't have permission to create workpads`, + }), + getNoPermissionToDeleteToolTip: () => + i18n.translate('xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip', { + defaultMessage: `You don't have permission to delete workpads`, + }), + getNoPermissionToUploadToolTip: () => + i18n.translate('xpack.canvas.workpadTableTools.noPermissionToUploadToolTip', { + defaultMessage: `You don't have permission to upload workpads`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx b/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx index 4c00306ac73ce..18bdb97683194 100644 --- a/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx +++ b/x-pack/plugins/canvas/public/components/home/workpad_create.component.tsx @@ -6,11 +6,9 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiButton } from '@elastic/eui'; import { EuiButtonPropsForButton } from '@elastic/eui/src/components/button/button'; -import { ComponentStrings } from '../../../i18n'; - -const { WorkpadCreate: strings } = ComponentStrings; export interface Props extends Omit { @@ -30,3 +28,10 @@ export const WorkpadCreate = ({ canUserWrite, disabled, ...rest }: Props) => { ); }; + +const strings = { + getWorkpadCreateButtonLabel: () => + i18n.translate('xpack.canvas.workpadCreate.createButtonLabel', { + defaultMessage: 'Create workpad', + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx index f9ecf7a2bb5cc..d974c70b05cf2 100644 --- a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx +++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.component.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { uniq } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { EuiInMemoryTable, EuiBasicTableColumn, @@ -16,7 +17,6 @@ import { } from '@elastic/eui'; import { CanvasTemplate } from '../../../../types'; -import { ComponentStrings } from '../../../../i18n'; import { tagsRegistry } from '../../../lib/tags_registry'; import { TagList } from '../../tag_list'; @@ -25,8 +25,6 @@ export interface Props { onCreateWorkpad: (template: CanvasTemplate) => void; } -const { WorkpadTemplates: strings } = ComponentStrings; - export const WorkpadTemplates = ({ templates, onCreateWorkpad }: Props) => { const columns: Array> = [ { @@ -117,7 +115,43 @@ export const WorkpadTemplates = ({ templates, onCreateWorkpad }: Props) => { }, }} pagination={true} - data-test-subj="workpadTable" + data-test-subj="canvasTemplatesTable" /> ); }; + +const strings = { + getCloneTemplateLinkAriaLabel: (templateName: string) => + i18n.translate('xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel', { + defaultMessage: `Clone workpad template '{templateName}'`, + values: { + templateName, + }, + }), + getTableDescriptionColumnTitle: () => + i18n.translate('xpack.canvas.workpadTemplates.table.descriptionColumnTitle', { + defaultMessage: 'Description', + }), + getTableNameColumnTitle: () => + i18n.translate('xpack.canvas.workpadTemplates.table.nameColumnTitle', { + defaultMessage: 'Template name', + }), + getTableTagsColumnTitle: () => + i18n.translate('xpack.canvas.workpadTemplates.table.tagsColumnTitle', { + defaultMessage: 'Tags', + description: + 'This column contains relevant tags that indicate what type of template ' + + 'is displayed. For example: "report", "presentation", etc.', + }), + getTemplateSearchPlaceholder: () => + i18n.translate('xpack.canvas.workpadTemplates.searchPlaceholder', { + defaultMessage: 'Find template', + }), + getCreatingTemplateLabel: (templateName: string) => + i18n.translate('xpack.canvas.workpadTemplates.creatingTemplateLabel', { + defaultMessage: `Creating from template '{templateName}'`, + values: { + templateName, + }, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx index 8201ef55bf5a6..cb2b872ea15f9 100644 --- a/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx +++ b/x-pack/plugins/canvas/public/components/home/workpad_templates/workpad_templates.stories.tsx @@ -13,6 +13,7 @@ import { reduxDecorator, getAddonPanelParameters, servicesContextDecorator, + getDisableStoryshotsParameter, } from '../../../../storybook'; import { getSomeTemplates } from '../../../services/stubs/workpad'; @@ -23,7 +24,7 @@ export default { title: 'Home/Workpad Templates', argTypes: {}, decorators: [reduxDecorator()], - parameters: [getAddonPanelParameters()], + parameters: { ...getAddonPanelParameters(), ...getDisableStoryshotsParameter() }, }; export const NoTemplates = () => { diff --git a/x-pack/plugins/canvas/public/lib/get_tags_filter.tsx b/x-pack/plugins/canvas/public/lib/get_tags_filter.tsx deleted file mode 100644 index 12d77c9c7f0c0..0000000000000 --- a/x-pack/plugins/canvas/public/lib/get_tags_filter.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { sortBy } from 'lodash'; -import { SearchFilterConfig } from '@elastic/eui'; -import { Tag } from '../components/tag'; -import { getId } from './get_id'; -import { tagsRegistry } from './tags_registry'; -import { ComponentStrings } from '../../i18n'; - -const { WorkpadTemplates: strings } = ComponentStrings; - -// EUI helper function -// generates the FieldValueSelectionFilter object for EuiSearchBar for tag filtering -export const getTagsFilter = (type: 'health' | 'badge'): SearchFilterConfig => { - const uniqueTags = sortBy(Object.values(tagsRegistry.toJS()), 'name'); - const filterType = 'field_value_selection'; - - return { - type: filterType, - field: 'tag', - name: strings.getTableTagsColumnTitle(), - multiSelect: true, - options: uniqueTags.map(({ name, color }) => ({ - value: name, - name, - view: ( -
- -
- ), - })), - }; -}; diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts index d9cd2ff8e0611..4e3612feb67c8 100644 --- a/x-pack/plugins/canvas/public/services/stubs/workpad.ts +++ b/x-pack/plugins/canvas/public/services/stubs/workpad.ts @@ -13,6 +13,8 @@ import { WorkpadService } from '../workpad'; import { getId } from '../../lib/get_id'; import { CanvasTemplate } from '../../../types'; +const TIMEOUT = 500; + const promiseTimeout = (time: number) => () => new Promise((resolve) => setTimeout(resolve, time)); const getName = () => { const lorem = 'Lorem ipsum dolor sit amet consectetur adipiscing elit Fusce lobortis aliquet arcu ut turpis duis'.split( @@ -54,7 +56,7 @@ export const getSomeWorkpads = (count = 3) => name: getName(), })); -export const findSomeWorkpads = (count = 3, timeout = 2000) => (_term: string) => { +export const findSomeWorkpads = (count = 3, timeout = TIMEOUT) => (_term: string) => { return Promise.resolve() .then(promiseTimeout(timeout)) .then(() => ({ @@ -63,7 +65,7 @@ export const findSomeWorkpads = (count = 3, timeout = 2000) => (_term: string) = })); }; -export const findNoWorkpads = (timeout = 2000) => (_term: string) => { +export const findNoWorkpads = (timeout = TIMEOUT) => (_term: string) => { return Promise.resolve() .then(promiseTimeout(timeout)) .then(() => ({ @@ -72,13 +74,13 @@ export const findNoWorkpads = (timeout = 2000) => (_term: string) => { })); }; -export const findSomeTemplates = (timeout = 2000) => () => { +export const findSomeTemplates = (timeout = TIMEOUT) => () => { return Promise.resolve() .then(promiseTimeout(timeout)) .then(() => getSomeTemplates()); }; -export const findNoTemplates = (timeout = 2000) => () => { +export const findNoTemplates = (timeout = TIMEOUT) => () => { return Promise.resolve() .then(promiseTimeout(timeout)) .then(() => getNoTemplates()); diff --git a/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot b/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot new file mode 100644 index 0000000000000..39ec1e234ead5 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/public/components/home/my_workpads/__snapshots__/empty_prompt.stories.storyshot @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Home/Empty Prompt Empty Prompt 1`] = ` +
+
+
+
+ +
+ +

+ Add your first workpad +

+
+
+

+ Create a new workpad, start from a template, or import a workpad JSON file by dropping it here. +

+

+ New to Canvas? + + + Add your first workpad + + . +

+
+ +
+
+
+
+`; diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx index 0c3765812066e..7f0ea077c7569 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -90,6 +90,11 @@ import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer' jest.mock('@elastic/eui/test-env/components/observer/observer'); EuiObserver.mockImplementation(() => 'EuiObserver'); +// @ts-expect-error untyped library +import Dropzone from 'react-dropzone'; +jest.mock('react-dropzone'); +Dropzone.mockImplementation(() => 'Dropzone'); + // 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'); @@ -111,7 +116,7 @@ addSerializer(styleSheetSerializer); // Initialize Storyshots and build the Jest Snapshots initStoryshots({ - configPath: path.resolve(__dirname, './../storybook'), + configPath: path.resolve(__dirname), framework: 'react', test: multiSnapshotWithOptions({}), // Don't snapshot tests that start with 'redux' diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6b1b8091ec926..728fc8848d29d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6111,18 +6111,18 @@ "xpack.canvas.error.esService.indicesFetchErrorMessage": "Elasticsearch インデックスを取得できませんでした", "xpack.canvas.error.RenderWithFn.renderErrorMessage": "「{functionName}」のレンダリングが失敗しました", "xpack.canvas.error.repeatImage.missingMaxArgument": "{emptyImageArgument} を指定する場合は、{maxArgument} を設定する必要があります", - "xpack.canvas.error.workpadLoader.cloneFailureErrorMessage": "ワークパッドのクローンを作成できませんでした", - "xpack.canvas.error.workpadLoader.deleteFailureErrorMessage": "すべてのワークパッドを削除できませんでした", - "xpack.canvas.error.workpadLoader.findFailureErrorMessage": "ワークパッドが見つかりませんでした", - "xpack.canvas.error.workpadLoader.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした", + "xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "ワークパッドのクローンを作成できませんでした", + "xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした", + "xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "すべてのワークパッドを削除できませんでした", + "xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "ワークパッドが見つかりませんでした", + "xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "{JSON} 個のファイルしか受け付けられませんでした", + "xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage": "ファイルをアップロードできませんでした", + "xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage": "{CANVAS} ワークパッドに必要なプロパティの一部が欠けています。 {JSON} ファイルを編集して正しいプロパティ値を入力し、再試行してください。", "xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "ワークパッドを作成できませんでした", "xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "ID でワークパッドを読み込めませんでした", - "xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage": "{JSON} 個のファイルしか受け付けられませんでした", - "xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage": "ファイルをアップロードできませんでした", - "xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage": "{CANVAS} ワークパッドに必要なプロパティの一部が欠けています。 {JSON} ファイルを編集して正しいプロパティ値を入力し、再試行してください。", "xpack.canvas.errorComponent.description": "表現が失敗し次のメッセージが返されました:", "xpack.canvas.errorComponent.title": "おっと!表現が失敗しました", - "xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage": "「{fileName}」をアップロードできませんでした", + "xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage": "「{fileName}」をアップロードできませんでした", "xpack.canvas.expression.cancelButtonLabel": "キャンセル", "xpack.canvas.expression.closeButtonLabel": "閉じる", "xpack.canvas.expression.learnLinkText": "表現構文の詳細", @@ -6456,6 +6456,12 @@ "xpack.canvas.helpMenu.description": "{CANVAS} に関する情報", "xpack.canvas.helpMenu.documentationLinkLabel": "{CANVAS} ドキュメント", "xpack.canvas.helpMenu.keyboardShortcutsLinkLabel": "キーボードショートカット", + "xpack.canvas.home.myWorkpadsTabLabel": "マイワークパッド", + "xpack.canvas.home.workpadTemplatesTabLabel": "テンプレート", + "xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription": "新規ワークパッドを作成、テンプレートで開始、またはワークパッド {JSON} ファイルをここにドロップしてインポートします。", + "xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription": "{CANVAS} を初めて使用する場合", + "xpack.canvas.homeEmptyPrompt.emptyPromptTitle": "初の’ワークパッドを追加しましょう", + "xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel": "初の’ワークパッドを追加しましょう", "xpack.canvas.keyboardShortcuts.bringFowardShortcutHelpText": "前に移動", "xpack.canvas.keyboardShortcuts.bringToFrontShortcutHelpText": "表面に移動", "xpack.canvas.keyboardShortcuts.cloneShortcutHelpText": "クローンを作成", @@ -6902,6 +6908,7 @@ "xpack.canvas.units.quickRange.last90Days": "過去90日間", "xpack.canvas.units.quickRange.today": "今日", "xpack.canvas.units.quickRange.yesterday": "昨日", + "xpack.canvas.useCloneWorkpad.clonedWorkpadName": "{workpadName} のコピー", "xpack.canvas.varConfig.addButtonLabel": "変数の追加", "xpack.canvas.varConfig.addTooltipLabel": "変数の追加", "xpack.canvas.varConfig.copyActionButtonLabel": "スニペットをコピー", @@ -7028,40 +7035,30 @@ "xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "ズーム", "xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "リセット", "xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%", - "xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} のコピー", - "xpack.canvas.workpadLoader.cloneTooltip": "ワークパッドのクローンを作成します", - "xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "ワークパッドを作成中...", - "xpack.canvas.workpadLoader.deleteButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドを削除", - "xpack.canvas.workpadLoader.deleteButtonLabel": " ({numberOfWorkpads}) ワークパッドを削除", - "xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel": "削除", - "xpack.canvas.workpadLoader.deleteModalDescription": "削除されたワークパッドは復元できません。", - "xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle": "{numberOfWorkpads} 個のワークパッドを削除しますか?", - "xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle": "ワークパッド「{workpadName}」削除しますか?", - "xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription": "新規ワークパッドを作成、テンプレートで開始、またはワークパッド {JSON} ファイルをここにドロップしてインポートします。", - "xpack.canvas.workpadLoader.emptyPromptNewUserDescription": "{CANVAS} を初めて使用する場合", - "xpack.canvas.workpadLoader.emptyPromptTitle": "初の’ワークパッドを追加しましょう", - "xpack.canvas.workpadLoader.exportButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドをエクスポート", - "xpack.canvas.workpadLoader.exportButtonLabel": "エクスポート ({numberOfWorkpads}) ", - "xpack.canvas.workpadLoader.exportTooltip": "ワークパッドをエクスポート", - "xpack.canvas.workpadLoader.fetchLoadingDescription": "ワークパッドを取得中...", - "xpack.canvas.workpadLoader.filePickerPlaceholder": "ワークパッド {JSON} ファイルをインポート", - "xpack.canvas.workpadLoader.loadWorkpadArialLabel": "ワークパッド「{workpadName}」を読み込む", - "xpack.canvas.workpadLoader.noPermissionToCloneToolTip": "ワークパッドのクローンを作成するパーミッションがありません", - "xpack.canvas.workpadLoader.noPermissionToCreateToolTip": "ワークパッドを作成するパーミッションがありません", - "xpack.canvas.workpadLoader.noPermissionToDeleteToolTip": "ワークパッドを削除するパーミッションがありません", - "xpack.canvas.workpadLoader.noPermissionToUploadToolTip": "ワークパッドを更新するパーミッションがありません", - "xpack.canvas.workpadLoader.sampleDataLinkLabel": "初の’ワークパッドを追加しましょう", - "xpack.canvas.workpadLoader.table.actionsColumnTitle": "アクション", - "xpack.canvas.workpadLoader.table.createdColumnTitle": "作成済み", - "xpack.canvas.workpadLoader.table.nameColumnTitle": "ワークパッド名", - "xpack.canvas.workpadLoader.table.updatedColumnTitle": "更新しました", - "xpack.canvas.workpadManager.modalTitle": "{CANVAS} ワークパッド", - "xpack.canvas.workpadManager.myWorkpadsTabLabel": "マイワークパッド", - "xpack.canvas.workpadManager.workpadTemplatesTabLabel": "テンプレート", - "xpack.canvas.workpadSearch.searchPlaceholder": "ワークパッドを検索", - "xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel": "ワークパッドテンプレート「{templateName}」のクローンを作成", - "xpack.canvas.workpadTemplate.creatingTemplateLabel": "テンプレート「{templateName}」から作成しています", - "xpack.canvas.workpadTemplate.searchPlaceholder": "テンプレートを検索", + "xpack.canvas.workpadImport.filePickerPlaceholder": "ワークパッド {JSON} ファイルをインポート", + "xpack.canvas.workpadTable.cloneTooltip": "ワークパッドのクローンを作成します", + "xpack.canvas.workpadTable.exportTooltip": "ワークパッドをエクスポート", + "xpack.canvas.workpadTable.loadWorkpadArialLabel": "ワークパッド「{workpadName}」を読み込む", + "xpack.canvas.workpadTable.noPermissionToCloneToolTip": "ワークパッドのクローンを作成するパーミッションがありません", + "xpack.canvas.workpadTable.searchPlaceholder": "ワークパッドを検索", + "xpack.canvas.workpadTable.table.actionsColumnTitle": "アクション", + "xpack.canvas.workpadTable.table.createdColumnTitle": "作成済み", + "xpack.canvas.workpadTable.table.nameColumnTitle": "ワークパッド名", + "xpack.canvas.workpadTable.table.updatedColumnTitle": "更新しました", + "xpack.canvas.workpadTableTools.deleteButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドを削除", + "xpack.canvas.workpadTableTools.deleteButtonLabel": " ({numberOfWorkpads}) ワークパッドを削除", + "xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel": "削除", + "xpack.canvas.workpadTableTools.deleteModalDescription": "削除されたワークパッドは復元できません。", + "xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle": "{numberOfWorkpads} 個のワークパッドを削除しますか?", + "xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle": "ワークパッド「{workpadName}」削除しますか?", + "xpack.canvas.workpadTableTools.exportButtonAriaLabel": "{numberOfWorkpads} 個のワークパッドをエクスポート", + "xpack.canvas.workpadTableTools.exportButtonLabel": "エクスポート ({numberOfWorkpads}) ", + "xpack.canvas.workpadTableTools.noPermissionToCreateToolTip": "ワークパッドを作成するパーミッションがありません", + "xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip": "ワークパッドを削除するパーミッションがありません", + "xpack.canvas.workpadTableTools.noPermissionToUploadToolTip": "ワークパッドを更新するパーミッションがありません", + "xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel": "ワークパッドテンプレート「{templateName}」のクローンを作成", + "xpack.canvas.workpadTemplates.creatingTemplateLabel": "テンプレート「{templateName}」から作成しています", + "xpack.canvas.workpadTemplates.searchPlaceholder": "テンプレートを検索", "xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "説明", "xpack.canvas.workpadTemplates.table.nameColumnTitle": "テンプレート名", "xpack.canvas.workpadTemplates.table.tagsColumnTitle": "タグ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 014ebabbe783f..c4d730cbdc8a5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6150,18 +6150,18 @@ "xpack.canvas.error.esService.indicesFetchErrorMessage": "无法提取 Elasticsearch 索引", "xpack.canvas.error.RenderWithFn.renderErrorMessage": "呈现“{functionName}”失败。", "xpack.canvas.error.repeatImage.missingMaxArgument": "如果提供 {emptyImageArgument},则必须设置 {maxArgument}", - "xpack.canvas.error.workpadLoader.cloneFailureErrorMessage": "无法克隆 Workpad", - "xpack.canvas.error.workpadLoader.deleteFailureErrorMessage": "无法删除所有 Workpad", - "xpack.canvas.error.workpadLoader.findFailureErrorMessage": "无法查找 Workpad", - "xpack.canvas.error.workpadLoader.uploadFailureErrorMessage": "无法上传 Workpad", + "xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "无法克隆 Workpad", + "xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "无法上传 Workpad", + "xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "无法删除所有 Workpad", + "xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "无法查找 Workpad", + "xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "仅接受 {JSON} 文件", + "xpack.canvas.error.useImportWorkpad.fileUploadFailureWithoutFileNameErrorMessage": "无法上传文件", + "xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage": "{CANVAS} Workpad 所需的某些属性缺失。 编辑 {JSON} 文件以提供正确的属性值,然后重试。", "xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "无法创建 Workpad", "xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "无法加载具有以下 ID 的 Workpad", - "xpack.canvas.error.workpadUpload.acceptJSONOnlyErrorMessage": "仅接受 {JSON} 文件", - "xpack.canvas.error.workpadUpload.fileUploadFailureWithoutFileNameErrorMessage": "无法上传文件", - "xpack.canvas.error.workpadUpload.missingPropertiesErrorMessage": "{CANVAS} Workpad 所需的某些属性缺失。 编辑 {JSON} 文件以提供正确的属性值,然后重试。", "xpack.canvas.errorComponent.description": "表达式失败,并显示消息:", "xpack.canvas.errorComponent.title": "哎哟!表达式失败", - "xpack.canvas.errors.workpadUpload.fileUploadFileWithFileNameErrorMessage": "无法上传“{fileName}”", + "xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage": "无法上传“{fileName}”", "xpack.canvas.expression.cancelButtonLabel": "取消", "xpack.canvas.expression.closeButtonLabel": "关闭", "xpack.canvas.expression.learnLinkText": "学习表达式语法", @@ -6496,6 +6496,12 @@ "xpack.canvas.helpMenu.description": "有关 {CANVAS} 特定信息", "xpack.canvas.helpMenu.documentationLinkLabel": "{CANVAS} 文档", "xpack.canvas.helpMenu.keyboardShortcutsLinkLabel": "快捷键", + "xpack.canvas.home.myWorkpadsTabLabel": "我的 Workpad", + "xpack.canvas.home.workpadTemplatesTabLabel": "模板", + "xpack.canvas.homeEmptyPrompt.emptyPromptGettingStartedDescription": "创建新的 Workpad、从模板入手或通过将 Workpad {JSON} 文件拖放到此处来导入。", + "xpack.canvas.homeEmptyPrompt.emptyPromptNewUserDescription": "{CANVAS} 新手?", + "xpack.canvas.homeEmptyPrompt.emptyPromptTitle": "添加您的首个 Workpad", + "xpack.canvas.homeEmptyPrompt.sampleDataLinkLabel": "添加您的首个 Workpad", "xpack.canvas.keyboardShortcuts.bringFowardShortcutHelpText": "前移", "xpack.canvas.keyboardShortcuts.bringToFrontShortcutHelpText": "置前", "xpack.canvas.keyboardShortcuts.cloneShortcutHelpText": "克隆", @@ -6946,6 +6952,7 @@ "xpack.canvas.units.time.hours": "{hours, plural, other {# 小时}}", "xpack.canvas.units.time.minutes": "{minutes, plural, other {# 分钟}}", "xpack.canvas.units.time.seconds": "{seconds, plural, other {# 秒}}", + "xpack.canvas.useCloneWorkpad.clonedWorkpadName": "{workpadName} 副本", "xpack.canvas.varConfig.addButtonLabel": "添加变量", "xpack.canvas.varConfig.addTooltipLabel": "添加变量", "xpack.canvas.varConfig.copyActionButtonLabel": "复制代码片段", @@ -7076,40 +7083,30 @@ "xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "缩放", "xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "重置", "xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%", - "xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} 副本", - "xpack.canvas.workpadLoader.cloneTooltip": "克隆 Workpad", - "xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "正在创建 Workpad......", - "xpack.canvas.workpadLoader.deleteButtonAriaLabel": "删除 {numberOfWorkpads} 个 Workpad", - "xpack.canvas.workpadLoader.deleteButtonLabel": "删除 ({numberOfWorkpads})", - "xpack.canvas.workpadLoader.deleteModalConfirmButtonLabel": "删除", - "xpack.canvas.workpadLoader.deleteModalDescription": "您无法恢复删除的 Workpad。", - "xpack.canvas.workpadLoader.deleteMultipleWorkpadsModalTitle": "删除 {numberOfWorkpads} 个 Workpad?", - "xpack.canvas.workpadLoader.deleteSingleWorkpadModalTitle": "删除 Workpad“{workpadName}”?", - "xpack.canvas.workpadLoader.emptyPromptGettingStartedDescription": "创建新的 Workpad、从模板入手或通过将 Workpad {JSON} 文件拖放到此处来导入。", - "xpack.canvas.workpadLoader.emptyPromptNewUserDescription": "{CANVAS} 新手?", - "xpack.canvas.workpadLoader.emptyPromptTitle": "添加您的首个 Workpad", - "xpack.canvas.workpadLoader.exportButtonAriaLabel": "导出 {numberOfWorkpads} 个 Workpad", - "xpack.canvas.workpadLoader.exportButtonLabel": "导出 ({numberOfWorkpads})", - "xpack.canvas.workpadLoader.exportTooltip": "导出 Workpad", - "xpack.canvas.workpadLoader.fetchLoadingDescription": "正在获取 Workpad......", - "xpack.canvas.workpadLoader.filePickerPlaceholder": "导入 Workpad {JSON} 文件", - "xpack.canvas.workpadLoader.loadWorkpadArialLabel": "加载 Workpad“{workpadName}”", - "xpack.canvas.workpadLoader.noPermissionToCloneToolTip": "您无权克隆 Workpad", - "xpack.canvas.workpadLoader.noPermissionToCreateToolTip": "您无权创建 Workpad", - "xpack.canvas.workpadLoader.noPermissionToDeleteToolTip": "您无权删除 Workpad", - "xpack.canvas.workpadLoader.noPermissionToUploadToolTip": "您无权上传 Workpad", - "xpack.canvas.workpadLoader.sampleDataLinkLabel": "添加您的首个 Workpad", - "xpack.canvas.workpadLoader.table.actionsColumnTitle": "操作", - "xpack.canvas.workpadLoader.table.createdColumnTitle": "创建时间", - "xpack.canvas.workpadLoader.table.nameColumnTitle": "Workpad 名称", - "xpack.canvas.workpadLoader.table.updatedColumnTitle": "更新时间", - "xpack.canvas.workpadManager.modalTitle": "{CANVAS} Workpad", - "xpack.canvas.workpadManager.myWorkpadsTabLabel": "我的 Workpad", - "xpack.canvas.workpadManager.workpadTemplatesTabLabel": "模板", - "xpack.canvas.workpadSearch.searchPlaceholder": "查找 Workpad", - "xpack.canvas.workpadTemplate.cloneTemplateLinkAriaLabel": "克隆 Workpad 模板“{templateName}”", - "xpack.canvas.workpadTemplate.creatingTemplateLabel": "正在从模板“{templateName}”创建", - "xpack.canvas.workpadTemplate.searchPlaceholder": "查找模板", + "xpack.canvas.workpadImport.filePickerPlaceholder": "导入 Workpad {JSON} 文件", + "xpack.canvas.workpadTable.searchPlaceholder": "查找 Workpad", + "xpack.canvas.workpadTable.cloneTooltip": "克隆 Workpad", + "xpack.canvas.workpadTable.exportTooltip": "导出 Workpad", + "xpack.canvas.workpadTable.loadWorkpadArialLabel": "加载 Workpad“{workpadName}”", + "xpack.canvas.workpadTable.noPermissionToCloneToolTip": "您无权克隆 Workpad", + "xpack.canvas.workpadTable.table.actionsColumnTitle": "操作", + "xpack.canvas.workpadTable.table.createdColumnTitle": "创建时间", + "xpack.canvas.workpadTable.table.nameColumnTitle": "Workpad 名称", + "xpack.canvas.workpadTable.table.updatedColumnTitle": "更新时间", + "xpack.canvas.workpadTableTools.deleteButtonAriaLabel": "删除 {numberOfWorkpads} 个 Workpad", + "xpack.canvas.workpadTableTools.deleteButtonLabel": "删除 ({numberOfWorkpads})", + "xpack.canvas.workpadTableTools.deleteModalConfirmButtonLabel": "删除", + "xpack.canvas.workpadTableTools.deleteModalDescription": "您无法恢复删除的 Workpad。", + "xpack.canvas.workpadTableTools.deleteMultipleWorkpadsModalTitle": "删除 {numberOfWorkpads} 个 Workpad?", + "xpack.canvas.workpadTableTools.deleteSingleWorkpadModalTitle": "删除 Workpad“{workpadName}”?", + "xpack.canvas.workpadTableTools.exportButtonAriaLabel": "导出 {numberOfWorkpads} 个 Workpad", + "xpack.canvas.workpadTableTools.exportButtonLabel": "导出 ({numberOfWorkpads})", + "xpack.canvas.workpadTableTools.noPermissionToCreateToolTip": "您无权创建 Workpad", + "xpack.canvas.workpadTableTools.noPermissionToDeleteToolTip": "您无权删除 Workpad", + "xpack.canvas.workpadTableTools.noPermissionToUploadToolTip": "您无权上传 Workpad", + "xpack.canvas.workpadTemplates.cloneTemplateLinkAriaLabel": "克隆 Workpad 模板“{templateName}”", + "xpack.canvas.workpadTemplates.creatingTemplateLabel": "正在从模板“{templateName}”创建", + "xpack.canvas.workpadTemplates.searchPlaceholder": "查找模板", "xpack.canvas.workpadTemplates.table.descriptionColumnTitle": "描述", "xpack.canvas.workpadTemplates.table.nameColumnTitle": "模板名称", "xpack.canvas.workpadTemplates.table.tagsColumnTitle": "标签", diff --git a/x-pack/test/accessibility/apps/canvas.ts b/x-pack/test/accessibility/apps/canvas.ts index a79fb7b60e76a..609c8bf5bb1ae 100644 --- a/x-pack/test/accessibility/apps/canvas.ts +++ b/x-pack/test/accessibility/apps/canvas.ts @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('loads workpads', async function () { await retry.waitFor( 'canvas workpads visible', - async () => await testSubjects.exists('canvasWorkpadLoaderTable') + async () => await testSubjects.exists('canvasWorkpadTable') ); await a11y.testAppSnapshot(); }); diff --git a/x-pack/test/functional/apps/canvas/smoke_test.js b/x-pack/test/functional/apps/canvas/smoke_test.js index 5280ad0118fba..fcc04aafdbcd8 100644 --- a/x-pack/test/functional/apps/canvas/smoke_test.js +++ b/x-pack/test/functional/apps/canvas/smoke_test.js @@ -17,7 +17,7 @@ export default function canvasSmokeTest({ getService, getPageObjects }) { describe('smoke test', function () { this.tags('includeFirefox'); - const workpadListSelector = 'canvasWorkpadLoaderTable > canvasWorkpadLoaderWorkpad'; + const workpadListSelector = 'canvasWorkpadTable > canvasWorkpadTableWorkpad'; const testWorkpadId = 'workpad-1705f884-6224-47de-ba49-ca224fe6ec31'; before(async () => { diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index 0e0203046fd16..df92c1c398d93 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -39,7 +39,7 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo * to load the workpad. Resolves once the workpad is in the DOM */ async loadFirstWorkpad(workpadName: string) { - const elem = await testSubjects.find('canvasWorkpadLoaderWorkpad'); + const elem = await testSubjects.find('canvasWorkpadTableWorkpad'); const text = await elem.getVisibleText(); expect(text).to.be(workpadName); await elem.click();