[Canvas][Labs] Integrate Labs Service into Canvas (#96920)

This commit is contained in:
Clint Andrew Hall 2021-04-26 19:56:33 -05:00 committed by GitHub
parent 23b84d6a52
commit e2933b5141
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 345 additions and 99 deletions

View file

@ -432,7 +432,11 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
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.' },
},

View file

@ -118,5 +118,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;
}

View file

@ -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[];

View file

@ -25,11 +25,9 @@ export const withSuspense = <P extends {}>(
</EuiErrorBoundary>
);
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'));

View file

@ -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) => (
<EuiFlexItem grow={false} style={{ marginBottom: '.25rem' }}>
<EuiFlexGroup gutterSize="xs" alignItems="flexEnd" responsive={false}>
<EuiFlexItem grow={false}>
<EuiSwitch
checked={isChecked}
style={{ marginTop: 1 }}
label={
<EuiFlexItem grow={false}>
<EuiScreenReaderOnly>
<span>{name} - </span>
</EuiScreenReaderOnly>
{switchText[env].name}
</EuiFlexItem>
}
onChange={(e) => onChange(e.target.checked)}
compressed
/>
</EuiFlexItem>
<EuiFlexItem style={{ textAlign: 'right' }}>
<EuiIconTip content={switchText[env].help} position="left" />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
</EuiFlexItem>
);
export const EnvironmentSwitch = ({ env, isChecked, onChange, name }: Props) => {
const { capabilities } = pluginServices.getHooks();
const canSet = env === 'kibana' ? capabilities.useService().canSetAdvancedSettings() : true;
return (
<EuiFlexItem grow={false} style={{ marginBottom: '.25rem' }}>
<EuiFlexGroup gutterSize="xs" alignItems="flexEnd" responsive={false}>
<EuiFlexItem grow={false}>
<EuiSwitch
disabled={!canSet}
checked={isChecked}
style={{ marginTop: 1 }}
label={
<EuiFlexItem grow={false}>
<EuiScreenReaderOnly>
<span>{name} - </span>
</EuiScreenReaderOnly>
{switchText[env].name}
</EuiFlexItem>
}
onChange={(e) => onChange(e.target.checked)}
compressed
/>
</EuiFlexItem>
<EuiFlexItem style={{ textAlign: 'right' }}>
<EuiIconTip content={switchText[env].help} position="left" />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
</EuiFlexItem>
);
};

View file

@ -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() {

View file

@ -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 = (
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>{resetButton}</EuiFlexItem>
<EuiFlexItem grow={false}>{refreshButton}</EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={() => onClose()} flush="left">
{strings.getCloseButtonLabel()}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>{resetButton}</EuiFlexItem>
<EuiFlexItem grow={false}>{refreshButton}</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
);
return (
<EuiFlyout onClose={onClose}>
<EuiFlyoutHeader>
<EuiTitle size="m">
<h2>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiIcon type="beaker" size="l" />
</EuiFlexItem>
<EuiFlexItem>{strings.getTitleLabel()}</EuiFlexItem>
</EuiFlexGroup>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<ProjectList {...{ projects, solutions, onStatusChange }} />
</EuiFlyoutBody>
{footer}
</EuiFlyout>
<EuiOverlayMask onClick={() => onClose()} headerZindexLocation="below">
<EuiFlyout onClose={onClose} hideCloseButton={true}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiIcon type="beaker" size="l" />
</EuiFlexItem>
<EuiFlexItem>{strings.getTitleLabel()}</EuiFlexItem>
</EuiFlexGroup>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
<p>{strings.getDescriptionMessage()}</p>
</EuiText>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<ProjectList {...{ projects, solutions, onStatusChange }} />
</EuiFlyoutBody>
{footer}
</EuiFlyout>
</EuiOverlayMask>
);
};

View file

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

View file

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

View file

@ -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<ProjectStatus, 'defaultValue'>
) => {

View file

@ -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) => {
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={false}>
<EuiTitle className="projectListItem__title" size="s">
<h2>{name}</h2>
<EuiTitle className="projectListItem__title" size="xs">
<h2>
{name}
{isOverride ? (
<span className="projectListItem__titlePendingChangesIndicator">
<EuiIconTip
content={strings.getOverriddenIconTipLabel()}
position="top"
type="dot"
color="secondary"
/>
</span>
) : null}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
@ -59,10 +73,14 @@ export const ProjectListItem = ({ project, onStatusChange }: Props) => {
</div>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s">{description}</EuiText>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
{description}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" color="subdued">
<EuiSpacer size="xs" />
<EuiText size="xs" color="subdued">
{isActive ? strings.getEnabledStatusMessage() : strings.getDisabledStatusMessage()}
</EuiText>
</EuiFlexItem>

View file

@ -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: () => (
<FormattedMessage
id="presentationUtil.labs.components.defaultStatusMessage"
defaultMessage="{status} by default"
id="presentationUtil.labs.components.enabledStatusMessage"
defaultMessage="Default: {status}"
values={{
status: <strong>Enabled</strong>,
status: <EuiCode>Enabled</EuiCode>,
}}
description="Displays the current status of a lab project"
description="Displays the enabled status of a lab project"
/>
),
getDisabledStatusMessage: () => (
<FormattedMessage
id="presentationUtil.labs.components.defaultStatusMessage"
defaultMessage="{status} by default"
id="presentationUtil.labs.components.disabledStatusMessage"
defaultMessage="Default: {status}"
values={{
status: <strong>Disabled</strong>,
status: <EuiCode>Disabled</EuiCode>,
}}
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',
}),
},
},
};

View file

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

View file

@ -10,4 +10,5 @@ export interface PresentationCapabilitiesService {
canAccessDashboards: () => boolean;
canCreateNewDashboards: () => boolean;
canSaveVisualizations: () => boolean;
canSetAdvancedSettings: () => boolean;
}

View file

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

View file

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

View file

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

View file

@ -16,12 +16,13 @@ import {
EnvironmentStatus,
environmentNames,
isProjectEnabledByStatus,
SolutionName,
} from '../../common';
export interface PresentationLabsService {
getProjectIDs: () => typeof projectIDs;
getProject: (id: ProjectID) => Project;
getProjects: () => Record<ProjectID, Project>;
getProjects: (solutions?: SolutionName[]) => Record<ProjectID, Project>;
setProjectStatus: (id: ProjectID, env: EnvironmentName, status: boolean) => void;
reset: () => void;
}

View file

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

View file

@ -18,8 +18,8 @@ export { PresentationUtilServices } from '..';
export interface StorybookParams {
canAccessDashboards?: boolean;
canCreateNewDashboards?: boolean;
canEditDashboards?: boolean;
canSaveVisualizations?: boolean;
canSetAdvancedSettings?: boolean;
}
export const providers: PluginServiceProviders<PresentationUtilServices, StorybookParams> = {

View file

@ -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<PresentationLabsService>;
@ -16,9 +16,15 @@ export type LabsServiceFactory = PluginServiceFactory<PresentationLabsService>;
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 });

View file

@ -14,6 +14,6 @@ type CapabilitiesServiceFactory = PluginServiceFactory<PresentationCapabilitiesS
export const capabilitiesServiceFactory: CapabilitiesServiceFactory = () => ({
canAccessDashboards: () => true,
canCreateNewDashboards: () => true,
canEditDashboards: () => true,
canSaveVisualizations: () => true,
canSetAdvancedSettings: () => true,
});

View file

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

View file

@ -8,4 +8,5 @@
import { PresentationUtilPlugin } from './plugin';
export { SETTING_CATEGORY } from './ui_settings';
export const plugin = () => new PresentationUtilPlugin();

View file

@ -8331,7 +8331,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."

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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',
};

View file

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

View file

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

View file

@ -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 (
<>
<EuiButtonEmpty onClick={() => setIsShown(!isShown)} size="xs">
{strings.getLabsButtonLabel()}
{overrideCount > 0 ? (
<EuiNotificationBadge color="subdued" style={{ marginLeft: 4 }}>
{overrideCount}
</EuiNotificationBadge>
) : null}
</EuiButtonEmpty>
{isShown ? <Flyout solutions={['canvas']} onClose={() => setIsShown(false)} /> : null}
</>
);
};

View file

@ -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<Props> = ({
<EuiFlexItem grow={false}>
<ShareMenu />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LabsControl />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>

View file

@ -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<ProjectID, Project>;
import { UI_SETTINGS } from '../../common';
export interface CanvasLabsService extends PresentationLabsService {
projectIDs: typeof projectIDs;
isLabsEnabled: () => boolean;
}
export const labsServiceFactory: CanvasServiceFactory<CanvasLabsService> = async (
_coreSetup,
_coreStart,
coreStart,
_setupPlugins,
startPlugins
) => ({
projectIDs,
isLabsEnabled: () => coreStart.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI),
...startPlugins.presentationUtil.labsService,
});

View file

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

View file

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

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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<string, UiSettingsParams<boolean>> => ({
[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,
},
});