From 0e3943df9862301553ed6a49de9fe5c9403914ef Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 7 Apr 2021 00:11:00 -0400 Subject: [PATCH] Creating a Labs service for Presentation Solutions (#95435) (#96373) Co-authored-by: Clint Andrew Hall --- .../server/collectors/management/schema.ts | 6 +- .../server/collectors/management/types.ts | 1 + src/plugins/presentation_util/common/index.ts | 2 + src/plugins/presentation_util/common/labs.ts | 68 +++++++++ src/plugins/presentation_util/kibana.json | 6 +- .../public/components/index.tsx | 6 + .../components/labs/environment_switch.tsx | 62 ++++++++ .../public/components/labs/labs.stories.tsx | 32 ++++ .../components/labs/labs_beaker_button.tsx | 48 ++++++ .../public/components/labs/labs_flyout.tsx | 138 ++++++++++++++++++ .../public/components/labs/project_list.tsx | 50 +++++++ .../components/labs/project_list_item.scss | 46 ++++++ .../labs/project_list_item.stories.tsx | 79 ++++++++++ .../components/labs/project_list_item.tsx | 102 +++++++++++++ .../presentation_util/public/i18n/index.ts | 9 ++ .../presentation_util/public/i18n/labs.tsx | 94 ++++++++++++ src/plugins/presentation_util/public/index.ts | 14 +- .../presentation_util/public/plugin.ts | 2 +- .../public/services/capabilities.ts | 13 ++ .../public/services/create/index.ts | 28 +++- .../public/services/create/provider.tsx | 6 +- .../public/services/create/registry.tsx | 24 ++- .../public/services/dashboards.ts | 20 +++ .../public/services/index.ts | 23 +-- .../public/services/kibana/capabilities.ts | 2 +- .../public/services/kibana/dashboards.ts | 2 +- .../public/services/kibana/index.ts | 9 +- .../public/services/kibana/labs.ts | 85 +++++++++++ .../presentation_util/public/services/labs.ts | 82 +++++++++++ .../public/services/storybook/capabilities.ts | 2 +- .../public/services/storybook/index.ts | 4 +- .../public/services/storybook/labs.ts | 53 +++++++ .../public/services/stub/capabilities.ts | 2 +- .../public/services/stub/dashboards.ts | 2 +- .../public/services/stub/index.ts | 4 +- .../public/services/stub/labs.ts | 70 +++++++++ src/plugins/presentation_util/public/types.ts | 3 + src/plugins/presentation_util/server/index.ts | 11 ++ .../presentation_util/server/plugin.ts | 23 +++ .../presentation_util/server/ui_settings.ts | 40 +++++ src/plugins/presentation_util/tsconfig.json | 16 +- src/plugins/telemetry/schema/oss_plugins.json | 6 + x-pack/plugins/canvas/kibana.json | 1 + x-pack/plugins/canvas/public/application.tsx | 13 +- x-pack/plugins/canvas/public/plugin.tsx | 2 + .../canvas/public/services/context.tsx | 2 + .../plugins/canvas/public/services/index.ts | 3 + x-pack/plugins/canvas/public/services/labs.ts | 29 ++++ .../canvas/public/services/stubs/index.ts | 2 + .../canvas/public/services/stubs/labs.ts | 15 ++ 50 files changed, 1294 insertions(+), 68 deletions(-) create mode 100644 src/plugins/presentation_util/common/labs.ts create mode 100644 src/plugins/presentation_util/public/components/labs/environment_switch.tsx create mode 100644 src/plugins/presentation_util/public/components/labs/labs.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx create mode 100644 src/plugins/presentation_util/public/components/labs/labs_flyout.tsx create mode 100644 src/plugins/presentation_util/public/components/labs/project_list.tsx create mode 100644 src/plugins/presentation_util/public/components/labs/project_list_item.scss create mode 100644 src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/labs/project_list_item.tsx create mode 100644 src/plugins/presentation_util/public/i18n/index.ts create mode 100644 src/plugins/presentation_util/public/i18n/labs.tsx create mode 100644 src/plugins/presentation_util/public/services/capabilities.ts create mode 100644 src/plugins/presentation_util/public/services/dashboards.ts create mode 100644 src/plugins/presentation_util/public/services/kibana/labs.ts create mode 100644 src/plugins/presentation_util/public/services/labs.ts create mode 100644 src/plugins/presentation_util/public/services/storybook/labs.ts create mode 100644 src/plugins/presentation_util/public/services/stub/labs.ts create mode 100644 src/plugins/presentation_util/server/index.ts create mode 100644 src/plugins/presentation_util/server/plugin.ts create mode 100644 src/plugins/presentation_util/server/ui_settings.ts create mode 100644 x-pack/plugins/canvas/public/services/labs.ts create mode 100644 x-pack/plugins/canvas/public/services/stubs/labs.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 0d0c43d225380..8c34538cc6865 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -416,6 +416,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:enableInspectEsQueries': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'banners:placement': { type: 'keyword', _meta: { description: 'Non-default value of setting.' }, @@ -432,7 +436,7 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, - 'observability:enableInspectEsQueries': { + 'labs:presentation:unifiedToolbar': { 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 a77e4ce874172..10ba4674b3659 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -119,4 +119,5 @@ export interface UsageStats { 'banners:placement': string; 'banners:textColor': string; 'banners:backgroundColor': string; + 'labs:presentation:unifiedToolbar': boolean; } diff --git a/src/plugins/presentation_util/common/index.ts b/src/plugins/presentation_util/common/index.ts index 8b556af07dd62..bf8819b13a92d 100644 --- a/src/plugins/presentation_util/common/index.ts +++ b/src/plugins/presentation_util/common/index.ts @@ -8,3 +8,5 @@ export const PLUGIN_ID = 'presentationUtil'; export const PLUGIN_NAME = 'presentationUtil'; + +export * from './labs'; diff --git a/src/plugins/presentation_util/common/labs.ts b/src/plugins/presentation_util/common/labs.ts new file mode 100644 index 0000000000000..65e42996ae910 --- /dev/null +++ b/src/plugins/presentation_util/common/labs.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const UNIFIED_TOOLBAR = 'labs:presentation:unifiedToolbar'; + +export const projectIDs = [UNIFIED_TOOLBAR] as const; +export const environmentNames = ['kibana', 'browser', 'session'] as const; +export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; + +/** + * This is a list of active Labs Projects for the Presentation Team. It is the "source of truth" for all projects + * provided to users of our solutions in Kibana. + */ +export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = { + [UNIFIED_TOOLBAR]: { + id: UNIFIED_TOOLBAR, + isActive: false, + environments: ['kibana', 'browser', 'session'], + name: i18n.translate('presentationUtil.labs.enableUnifiedToolbarProjectName', { + defaultMessage: 'Unified Toolbar', + }), + description: i18n.translate('presentationUtil.labs.enableUnifiedToolbarProjectDescription', { + defaultMessage: 'Enable the new unified toolbar design for Presentation solutions', + }), + solutions: ['dashboard', 'canvas'], + }, +}; + +export type ProjectID = typeof projectIDs[number]; +export type EnvironmentName = typeof environmentNames[number]; +export type SolutionName = typeof solutionNames[number]; + +export type EnvironmentStatus = { + [env in EnvironmentName]?: boolean; +}; + +export type ProjectStatus = { + defaultValue: boolean; + isEnabled: boolean; + isOverride: boolean; +} & EnvironmentStatus; + +export interface ProjectConfig { + id: ProjectID; + name: string; + isActive: boolean; + environments: EnvironmentName[]; + description: string; + solutions: SolutionName[]; +} + +export type Project = ProjectConfig & { status: ProjectStatus }; + +export const getProjectIDs = () => projectIDs; + +export const isProjectEnabledByStatus = (active: boolean, status: EnvironmentStatus): boolean => { + // If the project is enabled by default, then any false flag will flip the switch, and vice-versa. + return active + ? Object.values(status).every((value) => value === true) + : Object.values(status).some((value) => value === true); +}; diff --git a/src/plugins/presentation_util/kibana.json b/src/plugins/presentation_util/kibana.json index b1b3d768c3e76..c7d272dcd02a1 100644 --- a/src/plugins/presentation_util/kibana.json +++ b/src/plugins/presentation_util/kibana.json @@ -2,8 +2,10 @@ "id": "presentationUtil", "version": "1.0.0", "kibanaVersion": "kibana", - "server": false, + "server": true, "ui": true, - "requiredPlugins": ["savedObjects"], + "requiredPlugins": [ + "savedObjects" + ], "optionalPlugins": [] } diff --git a/src/plugins/presentation_util/public/components/index.tsx b/src/plugins/presentation_util/public/components/index.tsx index 1aff029bae846..af806e1c22f1a 100644 --- a/src/plugins/presentation_util/public/components/index.tsx +++ b/src/plugins/presentation_util/public/components/index.tsx @@ -25,6 +25,12 @@ export const withSuspense =

( ); +export const LazyLabsBeakerButton = withSuspense( + React.lazy(() => import('./labs/labs_beaker_button')) +); + +export const LazyLabsFlyout = withSuspense(React.lazy(() => import('./labs/labs_flyout'))); + export const LazyDashboardPicker = React.lazy(() => import('./dashboard_picker')); export const LazySavedObjectSaveModalDashboard = React.lazy( diff --git a/src/plugins/presentation_util/public/components/labs/environment_switch.tsx b/src/plugins/presentation_util/public/components/labs/environment_switch.tsx new file mode 100644 index 0000000000000..0acdd433cbac8 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/environment_switch.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSwitch, + EuiIconTip, + EuiSpacer, + EuiScreenReaderOnly, +} from '@elastic/eui'; + +import { EnvironmentName } from '../../../common/labs'; +import { LabsStrings } from '../../i18n'; + +const { Switch: strings } = LabsStrings.Components; + +const switchText: { [env in EnvironmentName]: { name: string; help: string } } = { + kibana: strings.getKibanaSwitchText(), + browser: strings.getBrowserSwitchText(), + session: strings.getSessionSwitchText(), +}; + +export interface Props { + env: EnvironmentName; + isChecked: boolean; + onChange: (checked: boolean) => void; + name: string; +} + +export const EnvironmentSwitch = ({ env, isChecked, onChange, name }: Props) => ( + + + + + + {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 new file mode 100644 index 0000000000000..a9a1a0753d24b --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/labs.stories.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import { LabsBeakerButton } from './labs_beaker_button'; +import { LabsFlyout } from './labs_flyout'; + +export default { + title: 'Labs/Flyout', + description: + 'A set of components used for providing Labs controls and projects in another solution.', + argTypes: {}, +}; + +export function BeakerButton() { + return ; +} + +export function Flyout() { + return ; +} + +export function EmptyFlyout() { + return ; +} diff --git a/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx b/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx new file mode 100644 index 0000000000000..6d7fd4afdac68 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/labs_beaker_button.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { EuiButton, EuiIcon, EuiNotificationBadge, EuiButtonProps } from '@elastic/eui'; + +import { pluginServices } from '../../services'; +import { LabsFlyout, Props as FlyoutProps } from './labs_flyout'; + +export type Props = EuiButtonProps & Pick; + +export const LabsBeakerButton = ({ solutions, ...props }: Props) => { + const { labs: labsService } = pluginServices.getHooks(); + const { getProjects } = labsService.useService(); + const [isOpen, setIsOpen] = useState(false); + + const projects = getProjects(); + + const [overrideCount, onEnabledCountChange] = useState( + Object.values(projects).filter((project) => project.status.isOverride).length + ); + + const onButtonClick = () => setIsOpen((open) => !open); + const onClose = () => setIsOpen(false); + + return ( + <> + + + {overrideCount > 0 ? ( + + {overrideCount} + + ) : null} + + {isOpen ? : null} + + ); +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default LabsBeakerButton; diff --git a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx new file mode 100644 index 0000000000000..562d3b291a4b3 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { ReactNode, useRef, useState, useEffect } from 'react'; +import { + EuiFlyout, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiButton, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, + EuiIcon, +} from '@elastic/eui'; + +import { SolutionName, ProjectStatus, ProjectID, Project, EnvironmentName } from '../../../common'; +import { pluginServices } from '../../services'; +import { LabsStrings } from '../../i18n'; + +import { ProjectList } from './project_list'; + +const { Flyout: strings } = LabsStrings.Components; + +export interface Props { + onClose: () => void; + solutions?: SolutionName[]; + onEnabledCountChange?: (overrideCount: number) => void; +} + +const hasStatusChanged = ( + original: Record, + current: Record +): boolean => { + for (const id of Object.keys(original) as ProjectID[]) { + for (const key of Object.keys(original[id].status) as Array) { + if (original[id].status[key] !== current[id].status[key]) { + return true; + } + } + } + return false; +}; + +export const getOverridenCount = (projects: Record) => + Object.values(projects).filter((project) => project.status.isOverride).length; + +export const LabsFlyout = (props: Props) => { + const { solutions, onEnabledCountChange = () => {}, onClose } = props; + const { labs: labsService } = pluginServices.getHooks(); + const { getProjects, setProjectStatus, reset } = labsService.useService(); + + const [projects, setProjects] = useState(getProjects()); + const [overrideCount, setOverrideCount] = useState(getOverridenCount(projects)); + const initialStatus = useRef(getProjects()); + + const isChanged = hasStatusChanged(initialStatus.current, projects); + + useEffect(() => { + setOverrideCount(getOverridenCount(projects)); + }, [projects]); + + useEffect(() => { + onEnabledCountChange(overrideCount); + }, [onEnabledCountChange, overrideCount]); + + const onStatusChange = (id: ProjectID, env: EnvironmentName, enabled: boolean) => { + setProjectStatus(id, env, enabled); + setProjects(getProjects()); + }; + + let footer: ReactNode = null; + + const resetButton = ( + { + reset(); + setProjects(getProjects()); + }} + isDisabled={!overrideCount} + > + {strings.getResetToDefaultLabel()} + + ); + + const refreshButton = ( + { + window.location.reload(); + }} + isDisabled={!isChanged} + > + {strings.getRefreshLabel()} + + ); + + footer = ( + + + {resetButton} + {refreshButton} + + + ); + + return ( + + + +

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

+ + + + + + {footer} + + ); +}; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default LabsFlyout; diff --git a/src/plugins/presentation_util/public/components/labs/project_list.tsx b/src/plugins/presentation_util/public/components/labs/project_list.tsx new file mode 100644 index 0000000000000..4ecf45409b02c --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list.tsx @@ -0,0 +1,50 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiCallOut } from '@elastic/eui'; + +import { SolutionName, ProjectID, Project } from '../../../common'; +import { ProjectListItem, Props as ProjectListItemProps } from './project_list_item'; + +import { LabsStrings } from '../../i18n'; + +const { List: strings } = LabsStrings.Components; + +export interface Props { + solutions?: SolutionName[]; + projects: Record; + onStatusChange: ProjectListItemProps['onStatusChange']; +} + +const EmptyList = () => ; + +export const ProjectList = (props: Props) => { + const { solutions, projects, onStatusChange } = props; + + const items = Object.values(projects) + .map((project) => { + // Filter out any panels that don't match the solutions filter, (if provided). + if (solutions && !solutions.some((solution) => project.solutions.includes(solution))) { + return null; + } + + return ( +
  • + +
  • + ); + }) + .filter((item) => item !== null); + + return ( + + {items.length > 0 ?
      {items}
    : } +
    + ); +}; 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 new file mode 100644 index 0000000000000..c91a07576b314 --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.scss @@ -0,0 +1,46 @@ +.projectListItem { + position: relative; + background: $euiColorEmptyShade; + padding: $euiSizeL; + min-width: 500px; + + &--isOverridden:before { + position: absolute; + top: $euiSizeL; + left: 4px; + bottom: $euiSizeL; + width: 4px; + background: $euiColorPrimary; + content: ''; + } + + .euiSwitch__label { + width: 100%; + } +} + +.projectListItem + .projectListItem:after { + position: absolute; + top: 0; + right: 0; + left: 0; + height: 1px; + background: $euiColorLightShade; + content: ''; +} + +.euiFlyout .projectListItem { + padding: $euiSizeL $euiSizeXS; + + &:first-child { + padding-top: 0; + } + + &--isOverridden:before { + left: -12px; + } + + &--isOverridden:first-child:before { + top: 0; + } +} 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 new file mode 100644 index 0000000000000..ce93abded521e --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.stories.tsx @@ -0,0 +1,79 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { mapValues } from 'lodash'; + +import { EnvironmentStatus, ProjectConfig, ProjectID, ProjectStatus } from '../../../common'; +import { applyProjectStatus } from '../../services/labs'; +import { ProjectListItem, Props } from './project_list_item'; + +import { projects as projectConfigs } from '../../../common'; +import { ProjectList } from './project_list'; + +export default { + title: 'Labs/ProjectList', + description: 'A set of controls for displaying and manipulating projects.', +}; + +const projects = mapValues(projectConfigs, (project) => + applyProjectStatus(project, { kibana: false, session: false, browser: false }) +); + +export function List() { + return ; +} + +export function EmptyList() { + return ; +} + +export const ListItem = ( + props: Pick< + Props['project'], + 'description' | 'isActive' | 'name' | 'solutions' | 'environments' + > & + Omit +) => { + const { kibana, browser, session, ...rest } = props; + const status: EnvironmentStatus = { kibana, browser, session }; + const projectConfig: ProjectConfig = { + ...rest, + id: 'storybook:component' as ProjectID, + }; + + return ( +
    + ({ ...status, [env]: enabled })} + /> +
    + ); +}; + +ListItem.args = { + isActive: false, + name: 'Demo Project', + description: 'This is a demo project, and this is the description of the demo project.', + kibana: false, + browser: false, + session: false, + solutions: ['dashboard', 'canvas'], + environments: ['kibana', 'browser', 'session'], +}; + +ListItem.argTypes = { + environments: { + control: { + type: 'check', + options: ['kibana', 'browser', 'session'], + }, + }, +}; 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 new file mode 100644 index 0000000000000..e4aa1abd3693c --- /dev/null +++ b/src/plugins/presentation_util/public/components/labs/project_list_item.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiTitle, + EuiText, + EuiFormFieldset, + EuiScreenReaderOnly, +} from '@elastic/eui'; +import classnames from 'classnames'; + +import { ProjectID, EnvironmentName, Project, environmentNames } from '../../../common/labs'; +import { EnvironmentSwitch } from './environment_switch'; + +import { LabsStrings } from '../../i18n'; +const { ListItem: strings } = LabsStrings.Components; + +import './project_list_item.scss'; + +export interface Props { + project: Project; + onStatusChange: (id: ProjectID, env: EnvironmentName, enabled: boolean) => void; +} + +export const ProjectListItem = ({ project, onStatusChange }: Props) => { + const { id, status, isActive, name, description, solutions } = project; + const { isEnabled, isOverride } = status; + + return ( + + + + + + +

    {name}

    +
    +
    + +
    + {solutions.map((solution) => ( + {solution} + ))} +
    +
    + + {description} + + + + {isActive ? strings.getEnabledStatusMessage() : strings.getDisabledStatusMessage()} + + +
    +
    + + + + {name} + + {strings.getOverrideLegend()} + + ), + }} + > + {environmentNames.map((env) => { + const envStatus = status[env]; + if (envStatus !== undefined) { + return ( + onStatusChange(id, env, checked)} + {...{ env, name }} + /> + ); + } + })} + + +
    +
    + ); +}; diff --git a/src/plugins/presentation_util/public/i18n/index.ts b/src/plugins/presentation_util/public/i18n/index.ts new file mode 100644 index 0000000000000..cf2f2c111ad58 --- /dev/null +++ b/src/plugins/presentation_util/public/i18n/index.ts @@ -0,0 +1,9 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './labs'; diff --git a/src/plugins/presentation_util/public/i18n/labs.tsx b/src/plugins/presentation_util/public/i18n/labs.tsx new file mode 100644 index 0000000000000..ddf6346bd68ca --- /dev/null +++ b/src/plugins/presentation_util/public/i18n/labs.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const LabsStrings = { + Components: { + Switch: { + getKibanaSwitchText: () => ({ + name: i18n.translate('presentationUtil.labs.components.kibanaSwitchName', { + defaultMessage: 'Kibana', + }), + help: i18n.translate('presentationUtil.labs.components.kibanaSwitchHelp', { + defaultMessage: 'Sets the corresponding Advanced Setting for this lab project in Kibana', + }), + }), + getBrowserSwitchText: () => ({ + name: i18n.translate('presentationUtil.labs.components.browserSwitchName', { + defaultMessage: 'Browser', + }), + help: i18n.translate('presentationUtil.labs.components.browserSwitchHelp', { + defaultMessage: + 'Enables or disables the lab project for the browser; persists between browser instances', + }), + }), + getSessionSwitchText: () => ({ + name: i18n.translate('presentationUtil.labs.components.sessionSwitchName', { + defaultMessage: 'Session', + }), + help: i18n.translate('presentationUtil.labs.components.sessionSwitchHelp', { + defaultMessage: + 'Enables or disables the lab project for this tab; resets when the browser tab is closed', + }), + }), + }, + List: { + getNoProjectsMessage: () => + i18n.translate('presentationUtil.labs.components.noProjectsMessage', { + defaultMessage: 'No available lab projects', + }), + }, + ListItem: { + getOverrideLegend: () => + i18n.translate('presentationUtil.labs.components.overrideFlagsLabel', { + defaultMessage: 'Override flags', + }), + getEnabledStatusMessage: () => ( + Enabled, + }} + description="Displays the current status of a lab project" + /> + ), + getDisabledStatusMessage: () => ( + Disabled, + }} + description="Displays the current status of a lab project" + /> + ), + }, + Flyout: { + getTitleLabel: () => + i18n.translate('presentationUtil.labs.components.titleLabel', { + defaultMessage: 'Lab projects', + }), + getResetToDefaultLabel: () => + i18n.translate('presentationUtil.labs.components.resetToDefaultLabel', { + defaultMessage: 'Reset to defaults', + }), + getLabFlagsLabel: () => + i18n.translate('presentationUtil.labs.components.labFlagsLabel', { + defaultMessage: 'Lab flags', + }), + getRefreshLabel: () => + i18n.translate('presentationUtil.labs.components.calloutHelp', { + defaultMessage: 'Refresh to apply changes', + }), + }, + }, +}; diff --git a/src/plugins/presentation_util/public/index.ts b/src/plugins/presentation_util/public/index.ts index 457f167dd8228..1cbf4b5a4f334 100644 --- a/src/plugins/presentation_util/public/index.ts +++ b/src/plugins/presentation_util/public/index.ts @@ -8,12 +8,18 @@ import { PresentationUtilPlugin } from './plugin'; -export { LazyDashboardPicker, LazySavedObjectSaveModalDashboard, withSuspense } from './components'; - +export { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; export { SaveModalDashboardProps } from './components/types'; +export { projectIDs, ProjectID, Project } from '../common/labs'; + +export { + LazyLabsBeakerButton, + LazyLabsFlyout, + LazyDashboardPicker, + LazySavedObjectSaveModalDashboard, + withSuspense, +} from './components'; export function plugin() { return new PresentationUtilPlugin(); } - -export { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; diff --git a/src/plugins/presentation_util/public/plugin.ts b/src/plugins/presentation_util/public/plugin.ts index 6f74198bb56ab..00931c5730fe3 100644 --- a/src/plugins/presentation_util/public/plugin.ts +++ b/src/plugins/presentation_util/public/plugin.ts @@ -36,9 +36,9 @@ export class PresentationUtilPlugin startPlugins: PresentationUtilPluginStartDeps ): PresentationUtilPluginStart { pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); - return { ContextProvider: pluginServices.getContextProvider(), + labsService: pluginServices.getServices().labs, }; } diff --git a/src/plugins/presentation_util/public/services/capabilities.ts b/src/plugins/presentation_util/public/services/capabilities.ts new file mode 100644 index 0000000000000..58d56d1a4d81d --- /dev/null +++ b/src/plugins/presentation_util/public/services/capabilities.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface PresentationCapabilitiesService { + canAccessDashboards: () => boolean; + canCreateNewDashboards: () => boolean; + canSaveVisualizations: () => boolean; +} diff --git a/src/plugins/presentation_util/public/services/create/index.ts b/src/plugins/presentation_util/public/services/create/index.ts index 66f7185913323..163e25e26babf 100644 --- a/src/plugins/presentation_util/public/services/create/index.ts +++ b/src/plugins/presentation_util/public/services/create/index.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import { mapValues } from 'lodash'; - import { PluginServiceRegistry } from './registry'; export { PluginServiceRegistry } from './registry'; @@ -18,6 +16,8 @@ export { KibanaPluginServiceParams, } from './factory'; +type ServiceHooks = { [K in keyof Services]: { useService: () => Services[K] } }; + /** * `PluginServices` is a top-level class for specifying and accessing services within a plugin. * @@ -70,13 +70,27 @@ export class PluginServices { /** * Return a map of React Hooks that can be used in React components. */ - getHooks(): { [K in keyof Services]: { useService: () => Services[K] } } { + getHooks(): ServiceHooks { const registry = this.getRegistry(); const providers = registry.getServiceProviders(); - // @ts-expect-error Need to fix this; the type isn't fully understood when inferred. - return mapValues(providers, (provider) => ({ - useService: provider.getUseServiceHook(), - })); + const providerNames = Object.keys(providers) as Array; + + return providerNames.reduce((acc, providerName) => { + acc[providerName] = { useService: providers[providerName].getServiceReactHook() }; + return acc; + }, {} as ServiceHooks); + } + + getServices(): Services { + const registry = this.getRegistry(); + const providers = registry.getServiceProviders(); + + const providerNames = Object.keys(providers) as Array; + + return providerNames.reduce((acc, providerName) => { + acc[providerName] = providers[providerName].getService(); + return acc; + }, {} as Services); } } diff --git a/src/plugins/presentation_util/public/services/create/provider.tsx b/src/plugins/presentation_util/public/services/create/provider.tsx index fa16e291a656d..06590bcfbb3d0 100644 --- a/src/plugins/presentation_util/public/services/create/provider.tsx +++ b/src/plugins/presentation_util/public/services/create/provider.tsx @@ -41,9 +41,9 @@ export class PluginServiceProvider { } /** - * Private getter that will enforce proper setup throughout the class. + * Getter that will enforce proper setup throughout the class. */ - private getService() { + public getService() { if (!this.pluginService) { throw new Error('Service not started'); } @@ -62,7 +62,7 @@ export class PluginServiceProvider { /** * Returns a function for providing a Context hook for the service. */ - getUseServiceHook() { + getServiceReactHook() { return () => { const service = useContext(this.context); diff --git a/src/plugins/presentation_util/public/services/create/registry.tsx b/src/plugins/presentation_util/public/services/create/registry.tsx index 61ada16e241a5..e8f85666bcac4 100644 --- a/src/plugins/presentation_util/public/services/create/registry.tsx +++ b/src/plugins/presentation_util/public/services/create/registry.tsx @@ -7,7 +7,6 @@ */ import React from 'react'; -import { values } from 'lodash'; import { PluginServiceProvider, PluginServiceProviders } from './provider'; /** @@ -47,16 +46,17 @@ export class PluginServiceRegistry { * Returns a React Context Provider for use in consuming applications. */ getContextProvider() { + const values = Object.values(this.getServiceProviders()) as Array< + PluginServiceProvider + >; + // Collect and combine Context.Provider elements from each Service Provider into a single // Functional Component. const provider: React.FC = ({ children }) => ( <> - {values>(this.getServiceProviders()).reduceRight( - (acc, serviceProvider) => { - return {acc}; - }, - children - )} + {values.reduceRight((acc, serviceProvider) => { + return {acc}; + }, children)} ); @@ -69,9 +69,8 @@ export class PluginServiceRegistry { * @param params Parameters used to start the registry. */ start(params: StartParameters) { - values>(this.providers).map((serviceProvider) => - serviceProvider.start(params) - ); + const providerNames = Object.keys(this.providers) as Array; + providerNames.forEach((providerName) => this.providers[providerName].start(params)); this._isStarted = true; return this; } @@ -80,9 +79,8 @@ export class PluginServiceRegistry { * Stop the registry. */ stop() { - values>(this.providers).map((serviceProvider) => - serviceProvider.stop() - ); + const providerNames = Object.keys(this.providers) as Array; + providerNames.forEach((providerName) => this.providers[providerName].stop()); this._isStarted = false; return this; } diff --git a/src/plugins/presentation_util/public/services/dashboards.ts b/src/plugins/presentation_util/public/services/dashboards.ts new file mode 100644 index 0000000000000..cbca79223063b --- /dev/null +++ b/src/plugins/presentation_util/public/services/dashboards.ts @@ -0,0 +1,20 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SimpleSavedObject } from 'src/core/public'; +import { PartialDashboardAttributes } from './kibana/dashboards'; + +export interface PresentationDashboardsService { + findDashboards: ( + query: string, + fields: string[] + ) => Promise>>; + findDashboardsByTitle: ( + title: string + ) => Promise>>; +} diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts index 39dae92aa2ba9..c01a95f64619c 100644 --- a/src/plugins/presentation_util/public/services/index.ts +++ b/src/plugins/presentation_util/public/services/index.ts @@ -6,29 +6,14 @@ * Side Public License, v 1. */ -import { SimpleSavedObject } from 'src/core/public'; import { PluginServices } from './create'; -import { PartialDashboardAttributes } from './kibana/dashboards'; - -export interface PresentationDashboardsService { - findDashboards: ( - query: string, - fields: string[] - ) => Promise>>; - findDashboardsByTitle: ( - title: string - ) => Promise>>; -} - -export interface PresentationCapabilitiesService { - canAccessDashboards: () => boolean; - canCreateNewDashboards: () => boolean; - canSaveVisualizations: () => boolean; -} - +import { PresentationCapabilitiesService } from './capabilities'; +import { PresentationDashboardsService } from './dashboards'; +import { PresentationLabsService } from './labs'; export interface PresentationUtilServices { dashboards: PresentationDashboardsService; capabilities: PresentationCapabilitiesService; + labs: PresentationLabsService; } export const pluginServices = new PluginServices(); diff --git a/src/plugins/presentation_util/public/services/kibana/capabilities.ts b/src/plugins/presentation_util/public/services/kibana/capabilities.ts index 6949fba00c65a..d46af31b30667 100644 --- a/src/plugins/presentation_util/public/services/kibana/capabilities.ts +++ b/src/plugins/presentation_util/public/services/kibana/capabilities.ts @@ -8,7 +8,7 @@ import { PresentationUtilPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../create'; -import { PresentationCapabilitiesService } from '..'; +import { PresentationCapabilitiesService } from '../capabilities'; export type CapabilitiesServiceFactory = KibanaPluginServiceFactory< PresentationCapabilitiesService, diff --git a/src/plugins/presentation_util/public/services/kibana/dashboards.ts b/src/plugins/presentation_util/public/services/kibana/dashboards.ts index 8735fe7fe2668..59e3ada10a869 100644 --- a/src/plugins/presentation_util/public/services/kibana/dashboards.ts +++ b/src/plugins/presentation_util/public/services/kibana/dashboards.ts @@ -8,7 +8,7 @@ import { PresentationUtilPluginStartDeps } from '../../types'; import { KibanaPluginServiceFactory } from '../create'; -import { PresentationDashboardsService } from '..'; +import { PresentationDashboardsService } from '../dashboards'; export type DashboardsServiceFactory = KibanaPluginServiceFactory< PresentationDashboardsService, diff --git a/src/plugins/presentation_util/public/services/kibana/index.ts b/src/plugins/presentation_util/public/services/kibana/index.ts index 75388a71d14ca..880f0f8b49c76 100644 --- a/src/plugins/presentation_util/public/services/kibana/index.ts +++ b/src/plugins/presentation_util/public/services/kibana/index.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { dashboardsServiceFactory } from './dashboards'; import { capabilitiesServiceFactory } from './capabilities'; +import { dashboardsServiceFactory } from './dashboards'; +import { labsServiceFactory } from './labs'; import { PluginServiceProviders, KibanaPluginServiceParams, @@ -17,15 +18,17 @@ import { import { PresentationUtilPluginStartDeps } from '../../types'; import { PresentationUtilServices } from '..'; -export { dashboardsServiceFactory } from './dashboards'; export { capabilitiesServiceFactory } from './capabilities'; +export { dashboardsServiceFactory } from './dashboards'; +export { labsServiceFactory } from './labs'; export const providers: PluginServiceProviders< PresentationUtilServices, KibanaPluginServiceParams > = { - dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + labs: new PluginServiceProvider(labsServiceFactory), + dashboards: new PluginServiceProvider(dashboardsServiceFactory), }; export const registry = new PluginServiceRegistry< diff --git a/src/plugins/presentation_util/public/services/kibana/labs.ts b/src/plugins/presentation_util/public/services/kibana/labs.ts new file mode 100644 index 0000000000000..d2c0735c76eeb --- /dev/null +++ b/src/plugins/presentation_util/public/services/kibana/labs.ts @@ -0,0 +1,85 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + environmentNames, + EnvironmentName, + projectIDs, + projects, + ProjectID, + Project, + getProjectIDs, +} from '../../../common'; +import { PresentationUtilPluginStartDeps } from '../../types'; +import { KibanaPluginServiceFactory } from '../create'; +import { + PresentationLabsService, + isEnabledByStorageValue, + setStorageStatus, + setUISettingsStatus, + applyProjectStatus, +} from '../labs'; + +export type LabsServiceFactory = KibanaPluginServiceFactory< + PresentationLabsService, + PresentationUtilPluginStartDeps +>; + +export const labsServiceFactory: LabsServiceFactory = ({ coreStart }) => { + const { uiSettings } = coreStart; + const localStorage = window.localStorage; + const sessionStorage = window.sessionStorage; + + const getProjects = () => + projectIDs.reduce((acc, id) => { + acc[id] = getProject(id); + return acc; + }, {} as { [id in ProjectID]: Project }); + + const getProject = (id: ProjectID) => { + const project = projects[id]; + + const status = { + session: isEnabledByStorageValue(project, 'session', sessionStorage.getItem(id)), + browser: isEnabledByStorageValue(project, 'browser', localStorage.getItem(id)), + kibana: isEnabledByStorageValue(project, 'kibana', uiSettings.get(id, project.isActive)), + }; + + return applyProjectStatus(project, status); + }; + + const setProjectStatus = (name: ProjectID, env: EnvironmentName, status: boolean) => { + switch (env) { + case 'session': + setStorageStatus(sessionStorage, name, status); + break; + case 'browser': + setStorageStatus(localStorage, name, status); + break; + case 'kibana': + setUISettingsStatus(uiSettings, name, status); + break; + } + }; + + const reset = () => { + localStorage.clear(); + sessionStorage.clear(); + environmentNames.forEach((env) => + projectIDs.forEach((id) => setProjectStatus(id, env, projects[id].isActive)) + ); + }; + + return { + getProjectIDs, + getProjects, + getProject, + reset, + setProjectStatus, + }; +}; diff --git a/src/plugins/presentation_util/public/services/labs.ts b/src/plugins/presentation_util/public/services/labs.ts new file mode 100644 index 0000000000000..72e9a232ea976 --- /dev/null +++ b/src/plugins/presentation_util/public/services/labs.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IUiSettingsClient } from 'kibana/public'; +import { + EnvironmentName, + projectIDs, + Project, + ProjectConfig, + ProjectID, + EnvironmentStatus, + environmentNames, + isProjectEnabledByStatus, +} from '../../common'; + +export interface PresentationLabsService { + getProjectIDs: () => typeof projectIDs; + getProject: (id: ProjectID) => Project; + getProjects: () => Record; + setProjectStatus: (id: ProjectID, env: EnvironmentName, status: boolean) => void; + reset: () => void; +} + +export const isEnabledByStorageValue = ( + project: ProjectConfig, + environment: EnvironmentName, + value: string | boolean | null +): boolean => { + const defaultValue = project.isActive; + + if (!project.environments.includes(environment)) { + return defaultValue; + } + + if (value === true || value === false) { + return value; + } + + if (value === 'enabled') { + return true; + } + + if (value === 'disabled') { + return false; + } + + return defaultValue; +}; + +export const setStorageStatus = (storage: Storage, id: ProjectID, enabled: boolean) => + storage.setItem(id, enabled ? 'enabled' : 'disabled'); + +export const applyProjectStatus = (project: ProjectConfig, status: EnvironmentStatus): Project => { + const { isActive, environments } = project; + + environmentNames.forEach((name) => { + if (!environments.includes(name)) { + delete status[name]; + } + }); + + const isEnabled = isProjectEnabledByStatus(isActive, status); + const isOverride = isEnabled !== isActive; + + return { + ...project, + status: { + ...status, + defaultValue: isActive, + isEnabled, + isOverride, + }, + }; +}; + +export const setUISettingsStatus = (client: IUiSettingsClient, id: ProjectID, enabled: boolean) => + client.set(id, enabled); diff --git a/src/plugins/presentation_util/public/services/storybook/capabilities.ts b/src/plugins/presentation_util/public/services/storybook/capabilities.ts index 16fbe3baf488f..60285f00993ab 100644 --- a/src/plugins/presentation_util/public/services/storybook/capabilities.ts +++ b/src/plugins/presentation_util/public/services/storybook/capabilities.ts @@ -8,7 +8,7 @@ import { PluginServiceFactory } from '../create'; import { StorybookParams } from '.'; -import { PresentationCapabilitiesService } from '..'; +import { PresentationCapabilitiesService } from '../capabilities'; type CapabilitiesServiceFactory = PluginServiceFactory< PresentationCapabilitiesService, diff --git a/src/plugins/presentation_util/public/services/storybook/index.ts b/src/plugins/presentation_util/public/services/storybook/index.ts index dd7de54264062..37669d52c0096 100644 --- a/src/plugins/presentation_util/public/services/storybook/index.ts +++ b/src/plugins/presentation_util/public/services/storybook/index.ts @@ -8,6 +8,7 @@ import { PluginServices, PluginServiceProviders, PluginServiceProvider } from '../create'; import { dashboardsServiceFactory } from '../stub/dashboards'; +import { labsServiceFactory } from './labs'; import { capabilitiesServiceFactory } from './capabilities'; import { PresentationUtilServices } from '..'; @@ -22,8 +23,9 @@ export interface StorybookParams { } export const providers: PluginServiceProviders = { - dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + dashboards: new PluginServiceProvider(dashboardsServiceFactory), + labs: new PluginServiceProvider(labsServiceFactory), }; export const pluginServices = new PluginServices(); diff --git a/src/plugins/presentation_util/public/services/storybook/labs.ts b/src/plugins/presentation_util/public/services/storybook/labs.ts new file mode 100644 index 0000000000000..8878e218f19e8 --- /dev/null +++ b/src/plugins/presentation_util/public/services/storybook/labs.ts @@ -0,0 +1,53 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EnvironmentName, projectIDs, Project } from '../../../common'; +import { PluginServiceFactory } from '../create'; +import { projects, ProjectID, getProjectIDs } from '../../../common'; +import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } from '../labs'; + +export type LabsServiceFactory = PluginServiceFactory; + +export const labsServiceFactory: LabsServiceFactory = () => { + const storage = window.sessionStorage; + + const getProjects = () => + projectIDs.reduce((acc, id) => { + acc[id] = getProject(id); + return acc; + }, {} as { [id in ProjectID]: Project }); + + const getProject = (id: ProjectID) => { + const project = projects[id]; + const { isActive } = project; + const status = { + session: isEnabledByStorageValue(project, 'session', sessionStorage.getItem(id)), + browser: isEnabledByStorageValue(project, 'browser', localStorage.getItem(id)), + kibana: isActive, + }; + return applyProjectStatus(project, status); + }; + + const setProjectStatus = (name: ProjectID, env: EnvironmentName, enabled: boolean) => { + if (env === 'session') { + storage.setItem(name, enabled ? 'enabled' : 'disabled'); + } + }; + + const reset = () => { + storage.clear(); + }; + + return { + getProjectIDs, + getProjects, + getProject, + reset, + setProjectStatus, + }; +}; diff --git a/src/plugins/presentation_util/public/services/stub/capabilities.ts b/src/plugins/presentation_util/public/services/stub/capabilities.ts index 4154fa65a0cd7..80b913c4f0856 100644 --- a/src/plugins/presentation_util/public/services/stub/capabilities.ts +++ b/src/plugins/presentation_util/public/services/stub/capabilities.ts @@ -7,7 +7,7 @@ */ import { PluginServiceFactory } from '../create'; -import { PresentationCapabilitiesService } from '..'; +import { PresentationCapabilitiesService } from '../capabilities'; type CapabilitiesServiceFactory = PluginServiceFactory; diff --git a/src/plugins/presentation_util/public/services/stub/dashboards.ts b/src/plugins/presentation_util/public/services/stub/dashboards.ts index 280ae9582b815..047176836896b 100644 --- a/src/plugins/presentation_util/public/services/stub/dashboards.ts +++ b/src/plugins/presentation_util/public/services/stub/dashboards.ts @@ -7,7 +7,7 @@ */ import { PluginServiceFactory } from '../create'; -import { PresentationDashboardsService } from '..'; +import { PresentationDashboardsService } from '../dashboards'; // TODO (clint): Create set of dashboards to stub and return. diff --git a/src/plugins/presentation_util/public/services/stub/index.ts b/src/plugins/presentation_util/public/services/stub/index.ts index d1a8147f8fb8c..6bf32bba00a3e 100644 --- a/src/plugins/presentation_util/public/services/stub/index.ts +++ b/src/plugins/presentation_util/public/services/stub/index.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { dashboardsServiceFactory } from './dashboards'; import { capabilitiesServiceFactory } from './capabilities'; +import { dashboardsServiceFactory } from './dashboards'; +import { labsServiceFactory } from './labs'; import { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create'; import { PresentationUtilServices } from '..'; @@ -17,6 +18,7 @@ export { capabilitiesServiceFactory } from './capabilities'; export const providers: PluginServiceProviders = { dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + labs: new PluginServiceProvider(labsServiceFactory), }; export const registry = new PluginServiceRegistry(providers); diff --git a/src/plugins/presentation_util/public/services/stub/labs.ts b/src/plugins/presentation_util/public/services/stub/labs.ts new file mode 100644 index 0000000000000..c83bb68b5d072 --- /dev/null +++ b/src/plugins/presentation_util/public/services/stub/labs.ts @@ -0,0 +1,70 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + projects, + projectIDs, + ProjectID, + EnvironmentName, + getProjectIDs, + Project, +} from '../../../common'; +import { PluginServiceFactory } from '../create'; +import { PresentationLabsService, isEnabledByStorageValue, applyProjectStatus } from '../labs'; + +export type LabsServiceFactory = PluginServiceFactory; + +export const labsServiceFactory: LabsServiceFactory = () => { + const reset = () => + projectIDs.reduce((acc, id) => { + const project = getProject(id); + const defaultValue = project.isActive; + + acc[id] = { + defaultValue, + session: null, + browser: null, + kibana: defaultValue, + }; + return acc; + }, {} as { [id in ProjectID]: { defaultValue: boolean; session: boolean | null; browser: boolean | null; kibana: boolean } }); + + let statuses = reset(); + + const getProjects = () => + projectIDs.reduce((acc, id) => { + acc[id] = getProject(id); + return acc; + }, {} as { [id in ProjectID]: Project }); + + const getProject = (id: ProjectID) => { + const project = projects[id]; + const value = statuses[id]; + const status = { + session: isEnabledByStorageValue(project, 'session', value.session), + browser: isEnabledByStorageValue(project, 'browser', value.browser), + kibana: isEnabledByStorageValue(project, 'kibana', value.kibana), + }; + + return applyProjectStatus(project, status); + }; + + const setProjectStatus = (id: ProjectID, env: EnvironmentName, value: boolean) => { + statuses[id] = { ...statuses[id], [env]: value }; + }; + + return { + getProjectIDs, + getProject, + getProjects, + setProjectStatus, + reset: () => { + statuses = reset(); + }, + }; +}; diff --git a/src/plugins/presentation_util/public/types.ts b/src/plugins/presentation_util/public/types.ts index f1bd6c1b747eb..05779ffb206c4 100644 --- a/src/plugins/presentation_util/public/types.ts +++ b/src/plugins/presentation_util/public/types.ts @@ -6,11 +6,14 @@ * Side Public License, v 1. */ +import { PresentationLabsService } from './services/labs'; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PresentationUtilPluginSetup {} export interface PresentationUtilPluginStart { ContextProvider: React.FC; + labsService: PresentationLabsService; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/plugins/presentation_util/server/index.ts b/src/plugins/presentation_util/server/index.ts new file mode 100644 index 0000000000000..de7e8de405442 --- /dev/null +++ b/src/plugins/presentation_util/server/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PresentationUtilPlugin } from './plugin'; + +export const plugin = () => new PresentationUtilPlugin(); diff --git a/src/plugins/presentation_util/server/plugin.ts b/src/plugins/presentation_util/server/plugin.ts new file mode 100644 index 0000000000000..eb55373920625 --- /dev/null +++ b/src/plugins/presentation_util/server/plugin.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup, Plugin } from 'kibana/server'; +import { getUISettings } from './ui_settings'; + +export class PresentationUtilPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register(getUISettings()); + return {}; + } + + public start() { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/presentation_util/server/ui_settings.ts b/src/plugins/presentation_util/server/ui_settings.ts new file mode 100644 index 0000000000000..450354832c3ac --- /dev/null +++ b/src/plugins/presentation_util/server/ui_settings.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '../../../../src/core/types'; +import { projects, projectIDs, ProjectID } from '../common'; + +export const SETTING_CATEGORY = 'Presentation Labs'; + +const labsProjectSettings: Record> = projectIDs.reduce( + (acc, id) => { + const project = projects[id]; + const { name, description, isActive: value } = project; + acc[id] = { + name, + value, + type: 'boolean', + description, + schema: schema.boolean(), + requiresPageReload: true, + category: [SETTING_CATEGORY], + }; + return acc; + }, + {} as { + [id in ProjectID]: UiSettingsParams; + } +); + +/** + * uiSettings definitions for Presentation Util. + */ +export const getUISettings = (): Record> => ({ + ...labsProjectSettings, +}); diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index 37b9380f6f2b9..63d136cf9445a 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -7,9 +7,19 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "public/**/*", "storybook/**/*", "../../../typings/**/*"], + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "storybook/**/*", + "../../../typings/**/*" + ], "references": [ - { "path": "../../core/tsconfig.json" }, - { "path": "../saved_objects/tsconfig.json" }, + { + "path": "../../core/tsconfig.json" + }, + { + "path": "../saved_objects/tsconfig.json" + }, ] } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 6d291a2d5cc0e..ddfb8f421f85d 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8143,6 +8143,12 @@ "_meta": { "description": "Non-default value of setting." } + }, + "labs:presentation:unifiedToolbar": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } } } }, diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index c14e8340957ad..cff1a3e7fa8b7 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -13,6 +13,7 @@ "expressions", "features", "inspector", + "presentationUtil", "uiActions" ], "optionalPlugins": [ diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 66b02bdc16408..f910aff9a83fe 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -56,17 +56,20 @@ export const renderApp = ( { element }: AppMountParameters, canvasStore: Store ) => { + const { presentationUtil } = plugins; element.classList.add('canvas'); element.classList.add('canvasContainerWrapper'); ReactDOM.render( - - - - - + + + + + + + , element diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 6871c8d98b8f5..486cd03eb9dd6 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -27,6 +27,7 @@ import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; +import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { getPluginApi, CanvasApi } from './plugin_api'; import { CanvasSrcPlugin } from '../canvas_plugin_src/plugin'; export { CoreStart, CoreSetup }; @@ -51,6 +52,7 @@ export interface CanvasStartDeps { inspector: InspectorStart; uiActions: UiActionsStart; charts: ChartsPluginStart; + presentationUtil: PresentationUtilPluginStart; } /** diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/context.tsx index 6e74b5ac98621..3865d98caf2b3 100644 --- a/x-pack/plugins/canvas/public/services/context.tsx +++ b/x-pack/plugins/canvas/public/services/context.tsx @@ -35,6 +35,7 @@ export const useEmbeddablesService = () => useServices().embeddables; export const useExpressionsService = () => useServices().expressions; export const useNotifyService = () => useServices().notify; export const useNavLinkService = () => useServices().navLink; +export const useLabsService = () => useServices().labs; export const withServices = (type: ComponentType) => { const EnhancedType: FC = (props) => @@ -53,6 +54,7 @@ export const ServicesProvider: FC<{ notify: specifiedProviders.notify.getService(), platform: specifiedProviders.platform.getService(), navLink: specifiedProviders.navLink.getService(), + labs: specifiedProviders.labs.getService(), }; return {children}; }; diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index 7452352fc0ef4..9bfc41a782edc 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -13,6 +13,7 @@ import { platformServiceFactory } from './platform'; import { navLinkServiceFactory } from './nav_link'; import { embeddablesServiceFactory } from './embeddables'; import { expressionsServiceFactory } from './expressions'; +import { labsServiceFactory } from './labs'; export { NotifyService } from './notify'; export { PlatformService } from './platform'; @@ -78,6 +79,7 @@ export const services = { notify: new CanvasServiceProvider(notifyServiceFactory), platform: new CanvasServiceProvider(platformServiceFactory), navLink: new CanvasServiceProvider(navLinkServiceFactory), + labs: new CanvasServiceProvider(labsServiceFactory), }; export type CanvasServiceProviders = typeof services; @@ -88,6 +90,7 @@ export interface CanvasServices { notify: ServiceFromProvider; platform: ServiceFromProvider; navLink: ServiceFromProvider; + labs: ServiceFromProvider; } export const startServices = async ( diff --git a/x-pack/plugins/canvas/public/services/labs.ts b/x-pack/plugins/canvas/public/services/labs.ts new file mode 100644 index 0000000000000..9bc4bea3e35c3 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/labs.ts @@ -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 { + projectIDs, + Project, + ProjectID, +} from '../../../../../src/plugins/presentation_util/public'; + +import { CanvasServiceFactory } from '.'; + +export interface CanvasLabsService { + getProject: (id: ProjectID) => Project; + getProjects: () => Record; +} + +export const labsServiceFactory: CanvasServiceFactory = async ( + _coreSetup, + _coreStart, + _setupPlugins, + startPlugins +) => ({ + projectIDs, + ...startPlugins.presentationUtil.labsService, +}); diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 2565445af2db2..91bda2556284e 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -10,6 +10,7 @@ import { embeddablesService } from './embeddables'; import { expressionsService } from './expressions'; import { navLinkService } from './nav_link'; import { notifyService } from './notify'; +import { labsService } from './labs'; import { platformService } from './platform'; export const stubs: CanvasServices = { @@ -18,6 +19,7 @@ export const stubs: CanvasServices = { navLink: navLinkService, notify: notifyService, platform: platformService, + labs: labsService, }; export const startServices = async (providedServices: Partial = {}) => { diff --git a/x-pack/plugins/canvas/public/services/stubs/labs.ts b/x-pack/plugins/canvas/public/services/stubs/labs.ts new file mode 100644 index 0000000000000..52168ebeb6f80 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/labs.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. + */ + +import { CanvasLabsService } from '../labs'; + +const noop = (..._args: any[]): any => {}; + +export const labsService: CanvasLabsService = { + getProject: noop, + getProjects: noop, +};