From 552764c0068ca5b6a7848e30ec924fd3efcfd06f Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Mon, 26 Apr 2021 19:56:33 -0500 Subject: [PATCH] [Canvas][Labs] Integrate Labs Service into Canvas (#96920) --- .../server/collectors/management/schema.ts | 6 +- .../server/collectors/management/types.ts | 3 +- src/plugins/presentation_util/common/labs.ts | 18 +++--- .../public/components/index.tsx | 6 +- .../components/labs/environment_switch.tsx | 60 +++++++++++-------- .../public/components/labs/labs.stories.tsx | 7 ++- .../public/components/labs/labs_flyout.tsx | 60 ++++++++++++------- .../public/components/labs/project_list.tsx | 4 ++ .../components/labs/project_list_item.scss | 14 ++++- .../labs/project_list_item.stories.tsx | 2 +- .../components/labs/project_list_item.tsx | 26 ++++++-- .../presentation_util/public/i18n/labs.tsx | 33 +++++++--- src/plugins/presentation_util/public/index.ts | 6 ++ .../public/services/capabilities.ts | 1 + .../public/services/index.ts | 4 ++ .../public/services/kibana/capabilities.ts | 3 +- .../public/services/kibana/labs.ts | 11 +++- .../presentation_util/public/services/labs.ts | 3 +- .../public/services/storybook/capabilities.ts | 4 +- .../public/services/storybook/index.ts | 2 +- .../public/services/storybook/labs.ts | 12 +++- .../public/services/stub/capabilities.ts | 2 +- .../public/services/stub/labs.ts | 11 +++- src/plugins/presentation_util/server/index.ts | 1 + src/plugins/telemetry/schema/oss_plugins.json | 8 ++- x-pack/plugins/canvas/common/index.ts | 10 ++++ x-pack/plugins/canvas/i18n/components.ts | 14 +++++ .../workpad_header/labs_control/index.ts | 8 +++ .../labs_control/labs_control.tsx | 47 +++++++++++++++ .../workpad_header.component.tsx | 4 ++ x-pack/plugins/canvas/public/services/labs.ts | 14 ++--- .../canvas/public/services/stubs/labs.ts | 6 ++ x-pack/plugins/canvas/server/plugin.ts | 2 + x-pack/plugins/canvas/server/ui_settings.ts | 32 ++++++++++ 34 files changed, 345 insertions(+), 99 deletions(-) create mode 100644 x-pack/plugins/canvas/common/index.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/labs_control/index.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx create mode 100644 x-pack/plugins/canvas/server/ui_settings.ts diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 68ebdbb3f35f2..55f9e9cc6257d 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -436,7 +436,11 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'text', _meta: { description: 'Non-default value of setting.' }, }, - 'labs:presentation:unifiedToolbar': { + 'labs:presentation:timeToPresent': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, + 'labs:canvas:enable_ui': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 7fe3746630c9e..b6da4c79b49a3 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -119,5 +119,6 @@ export interface UsageStats { 'banners:placement': string; 'banners:textColor': string; 'banners:backgroundColor': string; - 'labs:presentation:unifiedToolbar': boolean; + 'labs:canvas:enable_ui': boolean; + 'labs:presentation:timeToPresent': boolean; } diff --git a/src/plugins/presentation_util/common/labs.ts b/src/plugins/presentation_util/common/labs.ts index 65e42996ae910..ce7855c516c8b 100644 --- a/src/plugins/presentation_util/common/labs.ts +++ b/src/plugins/presentation_util/common/labs.ts @@ -8,9 +8,9 @@ import { i18n } from '@kbn/i18n'; -export const UNIFIED_TOOLBAR = 'labs:presentation:unifiedToolbar'; +export const TIME_TO_PRESENT = 'labs:presentation:timeToPresent'; -export const projectIDs = [UNIFIED_TOOLBAR] as const; +export const projectIDs = [TIME_TO_PRESENT] as const; export const environmentNames = ['kibana', 'browser', 'session'] as const; export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; @@ -19,17 +19,18 @@ export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; * provided to users of our solutions in Kibana. */ export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = { - [UNIFIED_TOOLBAR]: { - id: UNIFIED_TOOLBAR, + [TIME_TO_PRESENT]: { + id: TIME_TO_PRESENT, isActive: false, + isDisplayed: false, environments: ['kibana', 'browser', 'session'], - name: i18n.translate('presentationUtil.labs.enableUnifiedToolbarProjectName', { - defaultMessage: 'Unified Toolbar', + name: i18n.translate('presentationUtil.labs.enableTimeToPresentProjectName', { + defaultMessage: 'Canvas Presentation UI', }), description: i18n.translate('presentationUtil.labs.enableUnifiedToolbarProjectDescription', { - defaultMessage: 'Enable the new unified toolbar design for Presentation solutions', + defaultMessage: 'Enable the new presentation-oriented UI for Canvas.', }), - solutions: ['dashboard', 'canvas'], + solutions: ['canvas'], }, }; @@ -51,6 +52,7 @@ export interface ProjectConfig { id: ProjectID; name: string; isActive: boolean; + isDisplayed: boolean; environments: EnvironmentName[]; description: string; solutions: SolutionName[]; diff --git a/src/plugins/presentation_util/public/components/index.tsx b/src/plugins/presentation_util/public/components/index.tsx index af806e1c22f1a..508a1f4983031 100644 --- a/src/plugins/presentation_util/public/components/index.tsx +++ b/src/plugins/presentation_util/public/components/index.tsx @@ -25,11 +25,9 @@ export const withSuspense =

( ); -export const LazyLabsBeakerButton = withSuspense( - React.lazy(() => import('./labs/labs_beaker_button')) -); +export const LazyLabsBeakerButton = React.lazy(() => import('./labs/labs_beaker_button')); -export const LazyLabsFlyout = withSuspense(React.lazy(() => import('./labs/labs_flyout'))); +export const LazyLabsFlyout = React.lazy(() => import('./labs/labs_flyout')); export const LazyDashboardPicker = React.lazy(() => import('./dashboard_picker')); diff --git a/src/plugins/presentation_util/public/components/labs/environment_switch.tsx b/src/plugins/presentation_util/public/components/labs/environment_switch.tsx index 0acdd433cbac8..9b48bacf3780a 100644 --- a/src/plugins/presentation_util/public/components/labs/environment_switch.tsx +++ b/src/plugins/presentation_util/public/components/labs/environment_switch.tsx @@ -16,6 +16,7 @@ import { EuiScreenReaderOnly, } from '@elastic/eui'; +import { pluginServices } from '../../services'; import { EnvironmentName } from '../../../common/labs'; import { LabsStrings } from '../../i18n'; @@ -34,29 +35,36 @@ export interface Props { name: string; } -export const EnvironmentSwitch = ({ env, isChecked, onChange, name }: Props) => ( - - - - - - {name} - - - {switchText[env].name} - - } - onChange={(e) => onChange(e.target.checked)} - compressed - /> - - - - - - - -); +export const EnvironmentSwitch = ({ env, isChecked, onChange, name }: Props) => { + const { capabilities } = pluginServices.getHooks(); + + const canSet = env === 'kibana' ? capabilities.useService().canSetAdvancedSettings() : true; + + return ( + + + + + + {name} - + + {switchText[env].name} + + } + onChange={(e) => onChange(e.target.checked)} + compressed + /> + + + + + + + + ); +}; diff --git a/src/plugins/presentation_util/public/components/labs/labs.stories.tsx b/src/plugins/presentation_util/public/components/labs/labs.stories.tsx index a9a1a0753d24b..e8dd2abb0c5b8 100644 --- a/src/plugins/presentation_util/public/components/labs/labs.stories.tsx +++ b/src/plugins/presentation_util/public/components/labs/labs.stories.tsx @@ -16,7 +16,12 @@ export default { title: 'Labs/Flyout', description: 'A set of components used for providing Labs controls and projects in another solution.', - argTypes: {}, + argTypes: { + canSetAdvancedSettings: { + control: 'boolean', + defaultValue: true, + }, + }, }; export function BeakerButton() { diff --git a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx index 562d3b291a4b3..5b424c7e95f18 100644 --- a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx +++ b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx @@ -10,6 +10,8 @@ import React, { ReactNode, useRef, useState, useEffect } from 'react'; import { EuiFlyout, EuiTitle, + EuiSpacer, + EuiText, EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, @@ -18,6 +20,7 @@ import { EuiFlexItem, EuiFlexGroup, EuiIcon, + EuiOverlayMask, } from '@elastic/eui'; import { SolutionName, ProjectStatus, ProjectID, Project, EnvironmentName } from '../../../common'; @@ -104,32 +107,47 @@ export const LabsFlyout = (props: Props) => { footer = ( - - {resetButton} - {refreshButton} + + + onClose()} flush="left"> + {strings.getCloseButtonLabel()} + + + + + {resetButton} + {refreshButton} + + ); return ( - - - -

- - - - - {strings.getTitleLabel()} - -

- - - - - - {footer} - + onClose()} headerZindexLocation="below"> + + + +

+ + + + + {strings.getTitleLabel()} + +

+
+ + +

{strings.getDescriptionMessage()}

+
+
+ + + + {footer} +
+
); }; diff --git a/src/plugins/presentation_util/public/components/labs/project_list.tsx b/src/plugins/presentation_util/public/components/labs/project_list.tsx index 4ecf45409b02c..301fd1aa6414f 100644 --- a/src/plugins/presentation_util/public/components/labs/project_list.tsx +++ b/src/plugins/presentation_util/public/components/labs/project_list.tsx @@ -29,6 +29,10 @@ export const ProjectList = (props: Props) => { const items = Object.values(projects) .map((project) => { + if (!project.isDisplayed) { + return null; + } + // Filter out any panels that don't match the solutions filter, (if provided). if (solutions && !solutions.some((solution) => project.solutions.includes(solution))) { return null; diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.scss b/src/plugins/presentation_util/public/components/labs/project_list_item.scss index c91a07576b314..898770f7811a1 100644 --- a/src/plugins/presentation_util/public/components/labs/project_list_item.scss +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.scss @@ -10,7 +10,7 @@ left: 4px; bottom: $euiSizeL; width: 4px; - background: $euiColorPrimary; + background: $euiColorSecondary; content: ''; } @@ -37,10 +37,20 @@ } &--isOverridden:before { - left: -12px; + left: -$euiSizeS; } &--isOverridden:first-child:before { top: 0; } } + +.projectListItem__titlePendingChangesIndicator { + margin-left: $euiSizeS; + position: relative; + top: -1px; +} + +.projectListItem__solutions { + text-transform: capitalize; +} diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx b/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx index ce93abded521e..bc6c123c21f34 100644 --- a/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx @@ -37,7 +37,7 @@ export function EmptyList() { export const ListItem = ( props: Pick< Props['project'], - 'description' | 'isActive' | 'name' | 'solutions' | 'environments' + 'description' | 'isActive' | 'name' | 'solutions' | 'environments' | 'isDisplayed' > & Omit ) => { diff --git a/src/plugins/presentation_util/public/components/labs/project_list_item.tsx b/src/plugins/presentation_util/public/components/labs/project_list_item.tsx index e4aa1abd3693c..994059c9789ec 100644 --- a/src/plugins/presentation_util/public/components/labs/project_list_item.tsx +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.tsx @@ -15,6 +15,8 @@ import { EuiText, EuiFormFieldset, EuiScreenReaderOnly, + EuiSpacer, + EuiIconTip, } from '@elastic/eui'; import classnames from 'classnames'; @@ -47,8 +49,20 @@ export const ProjectListItem = ({ project, onStatusChange }: Props) => { - -

{name}

+ +

+ {name} + {isOverride ? ( + + + + ) : null} +

@@ -59,10 +73,14 @@ export const ProjectListItem = ({ project, onStatusChange }: Props) => { - {description} + + + {description} + - + + {isActive ? strings.getEnabledStatusMessage() : strings.getDisabledStatusMessage()} diff --git a/src/plugins/presentation_util/public/i18n/labs.tsx b/src/plugins/presentation_util/public/i18n/labs.tsx index ddf6346bd68ca..d9e34fa43ebb7 100644 --- a/src/plugins/presentation_util/public/i18n/labs.tsx +++ b/src/plugins/presentation_util/public/i18n/labs.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; export const LabsStrings = { Components: { @@ -18,7 +19,8 @@ export const LabsStrings = { defaultMessage: 'Kibana', }), help: i18n.translate('presentationUtil.labs.components.kibanaSwitchHelp', { - defaultMessage: 'Sets the corresponding Advanced Setting for this lab project in Kibana', + defaultMessage: + 'Sets the corresponding Advanced Setting for this lab project; affects all Kibana users', }), }), getBrowserSwitchText: () => ({ @@ -51,24 +53,28 @@ export const LabsStrings = { i18n.translate('presentationUtil.labs.components.overrideFlagsLabel', { defaultMessage: 'Override flags', }), + getOverriddenIconTipLabel: () => + i18n.translate('presentationUtil.labs.components.overridenIconTipLabel', { + defaultMessage: 'Default overridden', + }), getEnabledStatusMessage: () => ( Enabled, + status: Enabled, }} - description="Displays the current status of a lab project" + description="Displays the enabled status of a lab project" /> ), getDisabledStatusMessage: () => ( Disabled, + status: Disabled, }} - description="Displays the current status of a lab project" + description="Displays the disabled status of a lab project" /> ), }, @@ -77,6 +83,11 @@ export const LabsStrings = { i18n.translate('presentationUtil.labs.components.titleLabel', { defaultMessage: 'Lab projects', }), + getDescriptionMessage: () => + i18n.translate('presentationUtil.labs.components.descriptionMessage', { + defaultMessage: + 'Lab projects are features and functionality that are in-progress or experimental in nature. They can be enabled and disabled locally for your browser or tab, or in Kibana.', + }), getResetToDefaultLabel: () => i18n.translate('presentationUtil.labs.components.resetToDefaultLabel', { defaultMessage: 'Reset to defaults', @@ -89,6 +100,10 @@ export const LabsStrings = { i18n.translate('presentationUtil.labs.components.calloutHelp', { defaultMessage: 'Refresh to apply changes', }), + getCloseButtonLabel: () => + i18n.translate('presentationUtil.labs.components.closeButtonLabel', { + defaultMessage: 'Close', + }), }, }, }; diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index fd3ae89419297..aee3cff92438b 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -8,6 +8,12 @@ import { PresentationUtilPlugin } from './plugin'; +export { + PresentationCapabilitiesService, + PresentationDashboardsService, + PresentationLabsService, +} from './services'; + export { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; export { SaveModalDashboardProps } from './components/types'; export { projectIDs, ProjectID, Project } from '../common/labs'; diff --git a/src/plugins/presentation_util/public/services/capabilities.ts b/src/plugins/presentation_util/public/services/capabilities.ts index 58d56d1a4d81d..421e3e672b328 100644 --- a/src/plugins/presentation_util/public/services/capabilities.ts +++ b/src/plugins/presentation_util/public/services/capabilities.ts @@ -10,4 +10,5 @@ export interface PresentationCapabilitiesService { canAccessDashboards: () => boolean; canCreateNewDashboards: () => boolean; canSaveVisualizations: () => boolean; + canSetAdvancedSettings: () => boolean; } diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts index c01a95f64619c..30bab78aeb27b 100644 --- a/src/plugins/presentation_util/public/services/index.ts +++ b/src/plugins/presentation_util/public/services/index.ts @@ -10,6 +10,10 @@ import { PluginServices } from './create'; import { PresentationCapabilitiesService } from './capabilities'; import { PresentationDashboardsService } from './dashboards'; import { PresentationLabsService } from './labs'; + +export { PresentationCapabilitiesService } from './capabilities'; +export { PresentationDashboardsService } from './dashboards'; +export { PresentationLabsService } from './labs'; export interface PresentationUtilServices { dashboards: PresentationDashboardsService; capabilities: PresentationCapabilitiesService; diff --git a/src/plugins/presentation_util/public/services/kibana/capabilities.ts b/src/plugins/presentation_util/public/services/kibana/capabilities.ts index d46af31b30667..7b12a9a3cc618 100644 --- a/src/plugins/presentation_util/public/services/kibana/capabilities.ts +++ b/src/plugins/presentation_util/public/services/kibana/capabilities.ts @@ -16,11 +16,12 @@ export type CapabilitiesServiceFactory = KibanaPluginServiceFactory< >; export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ coreStart }) => { - const { dashboard, visualize } = coreStart.application.capabilities; + const { dashboard, visualize, advancedSettings } = coreStart.application.capabilities; return { canAccessDashboards: () => Boolean(dashboard.show), canCreateNewDashboards: () => Boolean(dashboard.createNew), canSaveVisualizations: () => Boolean(visualize.save), + canSetAdvancedSettings: () => Boolean(advancedSettings.save), }; }; diff --git a/src/plugins/presentation_util/public/services/kibana/labs.ts b/src/plugins/presentation_util/public/services/kibana/labs.ts index d2c0735c76eeb..db78103469880 100644 --- a/src/plugins/presentation_util/public/services/kibana/labs.ts +++ b/src/plugins/presentation_util/public/services/kibana/labs.ts @@ -14,6 +14,7 @@ import { ProjectID, Project, getProjectIDs, + SolutionName, } from '../../../common'; import { PresentationUtilPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../create'; @@ -35,9 +36,15 @@ export const labsServiceFactory: LabsServiceFactory = ({ coreStart }) => { const localStorage = window.localStorage; const sessionStorage = window.sessionStorage; - const getProjects = () => + const getProjects = (solutions: SolutionName[] = []) => projectIDs.reduce((acc, id) => { - acc[id] = getProject(id); + const project = getProject(id); + if ( + solutions.length === 0 || + solutions.some((solution) => project.solutions.includes(solution)) + ) { + acc[id] = project; + } return acc; }, {} as { [id in ProjectID]: Project }); diff --git a/src/plugins/presentation_util/public/services/labs.ts b/src/plugins/presentation_util/public/services/labs.ts index 72e9a232ea976..ef583bd4189a9 100644 --- a/src/plugins/presentation_util/public/services/labs.ts +++ b/src/plugins/presentation_util/public/services/labs.ts @@ -16,12 +16,13 @@ import { EnvironmentStatus, environmentNames, isProjectEnabledByStatus, + SolutionName, } from '../../common'; export interface PresentationLabsService { getProjectIDs: () => typeof projectIDs; getProject: (id: ProjectID) => Project; - getProjects: () => Record; + getProjects: (solutions?: SolutionName[]) => Record; setProjectStatus: (id: ProjectID, env: EnvironmentName, status: boolean) => void; reset: () => void; } diff --git a/src/plugins/presentation_util/public/services/storybook/capabilities.ts b/src/plugins/presentation_util/public/services/storybook/capabilities.ts index 60285f00993ab..1dd8cfd571e5c 100644 --- a/src/plugins/presentation_util/public/services/storybook/capabilities.ts +++ b/src/plugins/presentation_util/public/services/storybook/capabilities.ts @@ -18,14 +18,14 @@ type CapabilitiesServiceFactory = PluginServiceFactory< export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ canAccessDashboards, canCreateNewDashboards, - canEditDashboards, canSaveVisualizations, + canSetAdvancedSettings, }) => { const check = (value: boolean = true) => value; return { canAccessDashboards: () => check(canAccessDashboards), canCreateNewDashboards: () => check(canCreateNewDashboards), - canEditDashboards: () => check(canEditDashboards), canSaveVisualizations: () => check(canSaveVisualizations), + canSetAdvancedSettings: () => check(canSetAdvancedSettings), }; }; diff --git a/src/plugins/presentation_util/public/services/storybook/index.ts b/src/plugins/presentation_util/public/services/storybook/index.ts index 37669d52c0096..40fdc40a4632e 100644 --- a/src/plugins/presentation_util/public/services/storybook/index.ts +++ b/src/plugins/presentation_util/public/services/storybook/index.ts @@ -18,8 +18,8 @@ export { PresentationUtilServices } from '..'; export interface StorybookParams { canAccessDashboards?: boolean; canCreateNewDashboards?: boolean; - canEditDashboards?: boolean; canSaveVisualizations?: boolean; + canSetAdvancedSettings?: boolean; } export const providers: PluginServiceProviders = { diff --git a/src/plugins/presentation_util/public/services/storybook/labs.ts b/src/plugins/presentation_util/public/services/storybook/labs.ts index 8878e218f19e8..396db52460053 100644 --- a/src/plugins/presentation_util/public/services/storybook/labs.ts +++ b/src/plugins/presentation_util/public/services/storybook/labs.ts @@ -8,7 +8,7 @@ import { EnvironmentName, projectIDs, Project } from '../../../common'; import { PluginServiceFactory } from '../create'; -import { projects, ProjectID, getProjectIDs } from '../../../common'; +import { projects, ProjectID, getProjectIDs, SolutionName } from '../../../common'; import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } from '../labs'; export type LabsServiceFactory = PluginServiceFactory; @@ -16,9 +16,15 @@ export type LabsServiceFactory = PluginServiceFactory; export const labsServiceFactory: LabsServiceFactory = () => { const storage = window.sessionStorage; - const getProjects = () => + const getProjects = (solutions: SolutionName[] = []) => projectIDs.reduce((acc, id) => { - acc[id] = getProject(id); + const project = getProject(id); + if ( + solutions.length === 0 || + solutions.some((solution) => project.solutions.includes(solution)) + ) { + acc[id] = project; + } return acc; }, {} as { [id in ProjectID]: Project }); diff --git a/src/plugins/presentation_util/public/services/stub/capabilities.ts b/src/plugins/presentation_util/public/services/stub/capabilities.ts index 80b913c4f0856..be1be966285f7 100644 --- a/src/plugins/presentation_util/public/services/stub/capabilities.ts +++ b/src/plugins/presentation_util/public/services/stub/capabilities.ts @@ -14,6 +14,6 @@ type CapabilitiesServiceFactory = PluginServiceFactory ({ canAccessDashboards: () => true, canCreateNewDashboards: () => true, - canEditDashboards: () => true, canSaveVisualizations: () => true, + canSetAdvancedSettings: () => true, }); diff --git a/src/plugins/presentation_util/public/services/stub/labs.ts b/src/plugins/presentation_util/public/services/stub/labs.ts index c83bb68b5d072..c511ed26ef32e 100644 --- a/src/plugins/presentation_util/public/services/stub/labs.ts +++ b/src/plugins/presentation_util/public/services/stub/labs.ts @@ -13,6 +13,7 @@ import { EnvironmentName, getProjectIDs, Project, + SolutionName, } from '../../../common'; import { PluginServiceFactory } from '../create'; import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } from '../labs'; @@ -36,9 +37,15 @@ export const labsServiceFactory: LabsServiceFactory = () => { let statuses = reset(); - const getProjects = () => + const getProjects = (solutions: SolutionName[] = []) => projectIDs.reduce((acc, id) => { - acc[id] = getProject(id); + const project = getProject(id); + if ( + solutions.length === 0 || + solutions.some((solution) => project.solutions.includes(solution)) + ) { + acc[id] = project; + } return acc; }, {} as { [id in ProjectID]: Project }); diff --git a/src/plugins/presentation_util/server/index.ts b/src/plugins/presentation_util/server/index.ts index de7e8de405442..d1f9ef6da760a 100644 --- a/src/plugins/presentation_util/server/index.ts +++ b/src/plugins/presentation_util/server/index.ts @@ -8,4 +8,5 @@ import { PresentationUtilPlugin } from './plugin'; +export { SETTING_CATEGORY } from './ui_settings'; export const plugin = () => new PresentationUtilPlugin(); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 670068def2225..203409733a14b 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8337,7 +8337,13 @@ "description": "Non-default value of setting." } }, - "labs:presentation:unifiedToolbar": { + "labs:presentation:timeToPresent": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, + "labs:canvas:enable_ui": { "type": "boolean", "_meta": { "description": "Non-default value of setting." diff --git a/x-pack/plugins/canvas/common/index.ts b/x-pack/plugins/canvas/common/index.ts new file mode 100644 index 0000000000000..51a53586dee3c --- /dev/null +++ b/x-pack/plugins/canvas/common/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. + */ + +export const UI_SETTINGS = { + ENABLE_LABS_UI: 'labs:canvas:enable_ui', +}; diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index afd3d1408e1f1..b60f8db5b25b4 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -514,6 +514,20 @@ export const ComponentStrings = { defaultMessage: 'Keyboard shortcuts', }), }, + LabsControl: { + getLabsButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderLabsControlSettings.labsButtonLabel', { + defaultMessage: 'Labs', + }), + getAriaLabel: () => + i18n.translate('xpack.canvas.workpadHeaderLabsControlSettings.labsAriaLabel', { + defaultMessage: 'View labs projects', + }), + getTooltip: () => + i18n.translate('xpack.canvas.workpadHeaderLabsControlSettings.labsTooltip', { + defaultMessage: 'View labs projects', + }), + }, Link: { getErrorMessage: (message: string) => i18n.translate('xpack.canvas.link.errorMessage', { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/labs_control/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/labs_control/index.ts new file mode 100644 index 0000000000000..fde077e88f86f --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/labs_control/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 { LabsControl } from './labs_control'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx b/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx new file mode 100644 index 0000000000000..eea59e6aa49f3 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/labs_control/labs_control.tsx @@ -0,0 +1,47 @@ +/* + * 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 { EuiButtonEmpty, EuiNotificationBadge } from '@elastic/eui'; + +import { + LazyLabsFlyout, + withSuspense, +} from '../../../../../../../src/plugins/presentation_util/public'; + +import { ComponentStrings } from '../../../../i18n'; +import { useLabsService } from '../../../services'; +const { LabsControl: strings } = ComponentStrings; + +const Flyout = withSuspense(LazyLabsFlyout, null); + +export const LabsControl = () => { + const { isLabsEnabled, getProjects } = useLabsService(); + const [isShown, setIsShown] = useState(false); + + if (!isLabsEnabled()) { + return null; + } + + const projects = getProjects(['canvas']); + const overrideCount = Object.values(projects).filter((project) => project.status.isOverride) + .length; + + return ( + <> + setIsShown(!isShown)} size="xs"> + {strings.getLabsButtonLabel()} + {overrideCount > 0 ? ( + + {overrideCount} + + ) : null} + + {isShown ? setIsShown(false)} /> : null} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx index dc9b7a670846b..415d3ddf46709 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -19,6 +19,7 @@ import { EditMenu } from './edit_menu'; import { ElementMenu } from './element_menu'; import { ShareMenu } from './share_menu'; import { ViewMenu } from './view_menu'; +import { LabsControl } from './labs_control'; import { CommitFn } from '../../../types'; const { WorkpadHeader: strings } = ComponentStrings; @@ -111,6 +112,9 @@ export const WorkpadHeader: FunctionComponent = ({ + + +
diff --git a/x-pack/plugins/canvas/public/services/labs.ts b/x-pack/plugins/canvas/public/services/labs.ts index 9bc4bea3e35c3..7f5de8d1e6570 100644 --- a/x-pack/plugins/canvas/public/services/labs.ts +++ b/x-pack/plugins/canvas/public/services/labs.ts @@ -7,23 +7,23 @@ import { projectIDs, - Project, - ProjectID, + PresentationLabsService, } from '../../../../../src/plugins/presentation_util/public'; import { CanvasServiceFactory } from '.'; - -export interface CanvasLabsService { - getProject: (id: ProjectID) => Project; - getProjects: () => Record; +import { UI_SETTINGS } from '../../common'; +export interface CanvasLabsService extends PresentationLabsService { + projectIDs: typeof projectIDs; + isLabsEnabled: () => boolean; } export const labsServiceFactory: CanvasServiceFactory = async ( _coreSetup, - _coreStart, + coreStart, _setupPlugins, startPlugins ) => ({ projectIDs, + isLabsEnabled: () => coreStart.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI), ...startPlugins.presentationUtil.labsService, }); diff --git a/x-pack/plugins/canvas/public/services/stubs/labs.ts b/x-pack/plugins/canvas/public/services/stubs/labs.ts index 52168ebeb6f80..7caa1d0139a70 100644 --- a/x-pack/plugins/canvas/public/services/stubs/labs.ts +++ b/x-pack/plugins/canvas/public/services/stubs/labs.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { projectIDs } from '../../../../../../src/plugins/presentation_util/public'; import { CanvasLabsService } from '../labs'; const noop = (..._args: any[]): any => {}; @@ -12,4 +13,9 @@ const noop = (..._args: any[]): any => {}; export const labsService: CanvasLabsService = { getProject: noop, getProjects: noop, + getProjectIDs: () => projectIDs, + isLabsEnabled: () => true, + projectIDs, + reset: noop, + setProjectStatus: noop, }; diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index c95d825fb9b0b..9360825830e56 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -19,6 +19,7 @@ import { loadSampleData } from './sample_data'; import { setupInterpreter } from './setup_interpreter'; import { customElementType, workpadType, workpadTemplateType } from './saved_objects'; import { initializeTemplates } from './templates'; +import { getUISettings } from './ui_settings'; interface PluginsSetup { expressions: ExpressionsServerSetup; @@ -36,6 +37,7 @@ export class CanvasPlugin implements Plugin { } public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { + coreSetup.uiSettings.register(getUISettings()); coreSetup.savedObjects.registerType(customElementType); coreSetup.savedObjects.registerType(workpadType); coreSetup.savedObjects.registerType(workpadTemplateType); diff --git a/x-pack/plugins/canvas/server/ui_settings.ts b/x-pack/plugins/canvas/server/ui_settings.ts new file mode 100644 index 0000000000000..75c4cc082c557 --- /dev/null +++ b/x-pack/plugins/canvas/server/ui_settings.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 { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { SETTING_CATEGORY } from '../../../../src/plugins/presentation_util/server'; +import { UiSettingsParams } from '../../../../src/core/types'; +import { UI_SETTINGS } from '../common'; + +/** + * uiSettings definitions for Presentation Util. + */ +export const getUISettings = (): Record> => ({ + [UI_SETTINGS.ENABLE_LABS_UI]: { + name: i18n.translate('xpack.canvas.labs.enableUI', { + defaultMessage: 'Enable labs button in Canvas', + }), + description: i18n.translate('xpack.canvas.labs.enableUnifiedToolbarProjectDescription', { + defaultMessage: + 'This flag determines if the viewer has access to the Labs button, a quick way to enable and disable experimental features in Canvas.', + }), + value: false, + type: 'boolean', + schema: schema.boolean(), + category: [SETTING_CATEGORY], + requiresPageReload: true, + }, +});