[Canvas] Add simple variables to workpads (#66139)

* Add simple variables to Canvas workpads

* Fix type for workpad variable action and clarify comment

* Fix types in fixtures and templates

* Fixing type check errors on actions

* Addressing pr feedback and refactoring canvas sidebar accordions

* Render true/false instead of Yes/no on variables

* add warning callout when editing a variable

* Address review feedback

* More feedback

* updating storyshot with new edit mode callout

* Some animation tweaks for the panel

* one more panel tweak

* Removing the slide transition for now

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Poff Poffenberger 2020-07-13 15:45:36 -05:00 committed by GitHub
parent 85d42535ea
commit 4d6ad89194
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 2698 additions and 170 deletions

View file

@ -84,6 +84,10 @@ import { RenderedElement } from '../shareable_runtime/components/rendered_elemen
jest.mock('../shareable_runtime/components/rendered_element');
RenderedElement.mockImplementation(() => 'RenderedElement');
import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer';
jest.mock('@elastic/eui/test-env/components/observer/observer');
EuiObserver.mockImplementation(() => 'EuiObserver');
addSerializer(styleSheetSerializer);
// Initialize Storyshots and build the Jest Snapshots

View file

@ -25,6 +25,7 @@ const BaseWorkpad: CanvasWorkpad = {
pages: [],
colors: [],
isWriteable: true,
variables: [],
};
const BasePage: CanvasPage = {

View file

@ -107,7 +107,7 @@ const EsdocsDatasource = ({ args, updateArgs, defaultIndex }) => {
<EuiAccordion
id="accordionAdvancedSettings"
buttonContent="Advanced settings"
className="canvasArg__accordion"
className="canvasSidebar__accordion"
>
<EuiSpacer size="s" />
<EuiFormRow label={strings.getSortFieldTitle()} display="columnCompressed">

View file

@ -545,7 +545,7 @@ export const ComponentStrings = {
}),
getTitle: () =>
i18n.translate('xpack.canvas.pageConfig.title', {
defaultMessage: 'Page styles',
defaultMessage: 'Page settings',
}),
getTransitionLabel: () =>
i18n.translate('xpack.canvas.pageConfig.transitionLabel', {
@ -899,6 +899,144 @@ export const ComponentStrings = {
defaultMessage: 'Close tray',
}),
},
VarConfig: {
getAddButtonLabel: () =>
i18n.translate('xpack.canvas.varConfig.addButtonLabel', {
defaultMessage: 'Add a variable',
}),
getAddTooltipLabel: () =>
i18n.translate('xpack.canvas.varConfig.addTooltipLabel', {
defaultMessage: 'Add a variable',
}),
getCopyActionButtonLabel: () =>
i18n.translate('xpack.canvas.varConfig.copyActionButtonLabel', {
defaultMessage: 'Copy snippet',
}),
getCopyActionTooltipLabel: () =>
i18n.translate('xpack.canvas.varConfig.copyActionTooltipLabel', {
defaultMessage: 'Copy variable syntax to clipboard',
}),
getCopyNotificationDescription: () =>
i18n.translate('xpack.canvas.varConfig.copyNotificationDescription', {
defaultMessage: 'Variable syntax copied to clipboard',
}),
getDeleteActionButtonLabel: () =>
i18n.translate('xpack.canvas.varConfig.deleteActionButtonLabel', {
defaultMessage: 'Delete variable',
}),
getDeleteNotificationDescription: () =>
i18n.translate('xpack.canvas.varConfig.deleteNotificationDescription', {
defaultMessage: 'Variable successfully deleted',
}),
getEditActionButtonLabel: () =>
i18n.translate('xpack.canvas.varConfig.editActionButtonLabel', {
defaultMessage: 'Edit variable',
}),
getEmptyDescription: () =>
i18n.translate('xpack.canvas.varConfig.emptyDescription', {
defaultMessage:
'This workpad has no variables currently. You may add variables to store and edit common values. These variables can then be used in elements or within the expression editor.',
}),
getTableNameLabel: () =>
i18n.translate('xpack.canvas.varConfig.tableNameLabel', {
defaultMessage: 'Name',
}),
getTableTypeLabel: () =>
i18n.translate('xpack.canvas.varConfig.tableTypeLabel', {
defaultMessage: 'Type',
}),
getTableValueLabel: () =>
i18n.translate('xpack.canvas.varConfig.tableValueLabel', {
defaultMessage: 'Value',
}),
getTitle: () =>
i18n.translate('xpack.canvas.varConfig.titleLabel', {
defaultMessage: 'Variables',
}),
getTitleTooltip: () =>
i18n.translate('xpack.canvas.varConfig.titleTooltip', {
defaultMessage: 'Add variables to store and edit common values',
}),
},
VarConfigDeleteVar: {
getCancelButtonLabel: () =>
i18n.translate('xpack.canvas.varConfigDeleteVar.cancelButtonLabel', {
defaultMessage: 'Cancel',
}),
getDeleteButtonLabel: () =>
i18n.translate('xpack.canvas.varConfigDeleteVar.deleteButtonLabel', {
defaultMessage: 'Delete variable',
}),
getTitle: () =>
i18n.translate('xpack.canvas.varConfigDeleteVar.titleLabel', {
defaultMessage: 'Delete variable?',
}),
getWarningDescription: () =>
i18n.translate('xpack.canvas.varConfigDeleteVar.warningDescription', {
defaultMessage:
'Deleting this variable may adversely affect the workpad. Are you sure you wish to continue?',
}),
},
VarConfigEditVar: {
getAddTitle: () =>
i18n.translate('xpack.canvas.varConfigEditVar.addTitleLabel', {
defaultMessage: 'Add variable',
}),
getCancelButtonLabel: () =>
i18n.translate('xpack.canvas.varConfigEditVar.cancelButtonLabel', {
defaultMessage: 'Cancel',
}),
getDuplicateNameError: () =>
i18n.translate('xpack.canvas.varConfigEditVar.duplicateNameError', {
defaultMessage: 'Variable name already in use',
}),
getEditTitle: () =>
i18n.translate('xpack.canvas.varConfigEditVar.editTitleLabel', {
defaultMessage: 'Edit variable',
}),
getEditWarning: () =>
i18n.translate('xpack.canvas.varConfigEditVar.editWarning', {
defaultMessage: 'Editing a variable in use may adversely affect your workpad',
}),
getNameFieldLabel: () =>
i18n.translate('xpack.canvas.varConfigEditVar.nameFieldLabel', {
defaultMessage: 'Name',
}),
getSaveButtonLabel: () =>
i18n.translate('xpack.canvas.varConfigEditVar.saveButtonLabel', {
defaultMessage: 'Save changes',
}),
getTypeBooleanLabel: () =>
i18n.translate('xpack.canvas.varConfigEditVar.typeBooleanLabel', {
defaultMessage: 'Boolean',
}),
getTypeFieldLabel: () =>
i18n.translate('xpack.canvas.varConfigEditVar.typeFieldLabel', {
defaultMessage: 'Type',
}),
getTypeNumberLabel: () =>
i18n.translate('xpack.canvas.varConfigEditVar.typeNumberLabel', {
defaultMessage: 'Number',
}),
getTypeStringLabel: () =>
i18n.translate('xpack.canvas.varConfigEditVar.typeStringLabel', {
defaultMessage: 'String',
}),
getValueFieldLabel: () =>
i18n.translate('xpack.canvas.varConfigEditVar.valueFieldLabel', {
defaultMessage: 'Value',
}),
},
VarConfigVarValueField: {
getFalseOption: () =>
i18n.translate('xpack.canvas.varConfigVarValueField.falseOption', {
defaultMessage: 'False',
}),
getTrueOption: () =>
i18n.translate('xpack.canvas.varConfigVarValueField.trueOption', {
defaultMessage: 'True',
}),
},
WorkpadConfig: {
getApplyStylesheetButtonLabel: () =>
i18n.translate('xpack.canvas.workpadConfig.applyStylesheetButtonLabel', {

View file

@ -120,7 +120,7 @@ class ArgFormComponent extends PureComponent {
);
return (
<div className={`canvasArg ${expandableLabel ? 'canvasArg--expandable' : null}`}>
<div className={`canvasArg ${expandableLabel ? 'canvasSidebar__expandable' : null}`}>
<ArgLabel
className="resolved"
argId={argId}

View file

@ -1,27 +1,3 @@
.canvasArg--expandable + .canvasArg--expandable {
margin-top: 0;
.canvasArg__accordion:before {
display: none;
}
}
.canvasSidebar__panel {
.canvasArg--expandable:last-child {
.canvasArg__accordion {
margin-bottom: (-$euiSizeS);
}
.canvasArg__accordion:after {
content: none;
}
.canvasArg__accordion.euiAccordion-isOpen:after {
display: none;
}
}
}
.canvasArg {
margin-top: $euiSizeS;
}
@ -31,10 +7,6 @@
padding: $euiSizeXS 0;
}
.canvasArg__content {
padding-top: $euiSizeS;
}
.canvasArg__form {
position: relative;
}
@ -43,38 +15,11 @@
margin-left: -$euiSizeXL;
}
.canvasArg__accordion {
padding: $euiSizeS $euiSizeM;
margin: 0 (-$euiSizeM);
background: $euiColorLightestShade;
position: relative;
.canvasSidebar__accordion {
// don't let remove button position here if this is nested in an accordion
.canvasArg__form {
position: static;
}
&.euiAccordion-isOpen {
background: transparent;
}
&:before,
&:after {
content: '';
height: 1px;
position: absolute;
left: 0;
width: 100%;
background: $euiColorLightShade;
}
&:before {
top: 0;
}
&:after {
bottom: 0;
}
}
// this is a workaround since an EuiFormRow label cannot be passed in toggle.js

View file

@ -6,7 +6,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EuiFormRow, EuiAccordion, EuiText, EuiToolTip, EuiIcon } from '@elastic/eui';
import { EuiFormRow, EuiAccordion, EuiToolTip, EuiIcon } from '@elastic/eui';
// This is what is being generated by render() from the Arg class. It is called in FunctionForm
export const ArgLabel = (props) => {
@ -17,18 +17,16 @@ export const ArgLabel = (props) => {
{expandable ? (
<EuiAccordion
id={`accordion-${argId}`}
className="canvasArg__accordion"
className="canvasSidebar__accordion"
buttonContent={
<EuiToolTip content={help} position="left" className="canvasArg__tooltip">
<EuiText size="s" color="subdued" htmlFor={`accordion-${argId}`}>
{label}
</EuiText>
<span>{label}</span>
</EuiToolTip>
}
extraAction={simpleArg}
initialIsOpen={initialIsOpen}
>
<div className="canvasArg__content">{children}</div>
<div className="canvasSidebar__accordionContent">{children}</div>
</EuiAccordion>
) : (
simpleArg && (

View file

@ -15,10 +15,13 @@ export const DatasourcePreview = compose(
withState('datatable', 'setDatatable'),
lifecycle({
componentDidMount() {
interpretAst({
type: 'expression',
chain: [this.props.function],
}).then(this.props.setDatatable);
interpretAst(
{
type: 'expression',
chain: [this.props.function],
},
{}
).then(this.props.setDatatable);
},
}),
branch(({ datatable }) => !datatable, renderComponent(Loading))

View file

@ -1,73 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiAccordion, EuiText, EuiSpacer } from '@elastic/eui';
import PropTypes from 'prop-types';
import React from 'react';
import { ComponentStrings } from '../../../i18n';
const { ElementConfig: strings } = ComponentStrings;
export const ElementConfig = ({ elementStats }) => {
if (!elementStats) {
return null;
}
const { total, ready, error } = elementStats;
const progress = total > 0 ? Math.round(((ready + error) / total) * 100) : 100;
return (
<EuiAccordion
id="canvas-element-stats"
buttonContent={
<EuiText size="s" color="subdued">
{strings.getTitle()}
</EuiText>
}
initialIsOpen={false}
>
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<EuiStat
title={total}
description={strings.getTotalLabel()}
titleSize="xs"
textAlign="center"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiStat
title={ready}
description={strings.getLoadedLabel()}
titleSize="xs"
textAlign="center"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiStat
title={error}
description={strings.getFailedLabel()}
titleSize="xs"
textAlign="center"
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiStat
title={progress + '%'}
description={strings.getProgressLabel()}
titleSize="xs"
textAlign="center"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
);
};
ElementConfig.propTypes = {
elementStats: PropTypes.object,
};

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiAccordion } from '@elastic/eui';
import PropTypes from 'prop-types';
import React from 'react';
import { ComponentStrings } from '../../../i18n';
import { State } from '../../../types';
const { ElementConfig: strings } = ComponentStrings;
interface Props {
elementStats: State['transient']['elementStats'];
}
export const ElementConfig = ({ elementStats }: Props) => {
if (!elementStats) {
return null;
}
const { total, ready, error } = elementStats;
const progress = total > 0 ? Math.round(((ready + error) / total) * 100) : 100;
return (
<div className="canvasSidebar__expandable">
<EuiAccordion
id="canvas-element-stats"
buttonContent={strings.getTitle()}
initialIsOpen={false}
className="canvasSidebar__accordion"
>
<div className="canvasSidebar__accordionContent">
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<EuiStat title={total} description={strings.getTotalLabel()} titleSize="xs" />
</EuiFlexItem>
<EuiFlexItem>
<EuiStat title={ready} description={strings.getLoadedLabel()} titleSize="xs" />
</EuiFlexItem>
<EuiFlexItem>
<EuiStat title={error} description={strings.getFailedLabel()} titleSize="xs" />
</EuiFlexItem>
<EuiFlexItem>
<EuiStat
title={progress + '%'}
description={strings.getProgressLabel()}
titleSize="xs"
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
</EuiAccordion>
</div>
);
};
ElementConfig.propTypes = {
elementStats: PropTypes.object,
};

View file

@ -30,7 +30,7 @@ export const PageConfig = ({
}) => {
return (
<Fragment>
<EuiTitle size="xxxs" className="canvasSidebar__panelTitleHeading">
<EuiTitle size="xs" className="canvasSidebar__panelTitleHeading">
<h4>{strings.getTitle()}</h4>
</EuiTitle>
<EuiSpacer size="s" />

View file

@ -17,8 +17,6 @@ export const GlobalConfig: FunctionComponent = () => (
<Fragment>
<SidebarSection>
<WorkpadConfig />
</SidebarSection>
<SidebarSection>
<ElementConfig />
</SidebarSection>
<SidebarSection>

View file

@ -31,12 +31,68 @@
&--isEmpty {
border-bottom: none;
}
.canvasSidebar__expandable:last-child {
.canvasSidebar__accordion {
margin-bottom: (-$euiSizeS);
}
.canvasSidebar__accordion:after {
content: none;
}
.canvasSidebar__accordion.euiAccordion-isOpen:after {
display: none;
}
}
}
.canvasSidebar__panel-noMinWidth .euiButton {
min-width: 0;
}
.canvasSidebar__expandable + .canvasSidebar__expandable {
margin-top: 0;
.canvasSidebar__accordion:before {
display: none;
}
}
.canvasSidebar__accordion {
padding: $euiSizeM;
margin: 0 (-$euiSizeM);
background: $euiColorLightestShade;
position: relative;
&.euiAccordion-isOpen {
background: transparent;
}
&:before,
&:after {
content: '';
height: 1px;
position: absolute;
left: 0;
width: 100%;
background: $euiColorLightShade;
}
&:before {
top: 0;
}
&:after {
bottom: 0;
}
}
.canvasSidebar__accordionContent {
padding-top: $euiSize;
padding-left: $euiSizeXS + $euiSizeS + $euiSize;
}
@keyframes sidebarPop {
0% {
opacity: 0;

View file

@ -0,0 +1,109 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/Variables/DeleteVar default 1`] = `
Array [
<div
className="canvasVarHeader__triggerWrapper"
>
<button
className="canvasVarHeader__button"
onClick={[Function]}
type="button"
>
<span
className="canvasVarHeader__iconWrapper"
>
<div
data-euiicon-type="sortLeft"
style={
Object {
"verticalAlign": "top",
}
}
/>
</span>
<span>
<span
className="canvasVarHeader__anchor"
>
Delete variable?
</span>
</span>
</button>
</div>,
<div
className="canvasSidebar__accordionContent"
>
<div>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiText euiText--small"
>
<div
className="euiTextColor euiTextColor--subdued"
>
Deleting this variable may adversely affect the workpad. Are you sure you wish to continue?
</div>
</div>
</div>
</div>
<div
className="euiSpacer euiSpacer--m"
/>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
className="euiButton euiButton--danger euiButton--small euiButton--fill"
onClick={[Function]}
type="button"
>
<span
className="euiButton__content"
>
<div
aria-hidden="true"
className="euiButton__icon"
data-euiicon-type="trash"
size="m"
/>
<span
className="euiButton__text"
>
Delete variable
</span>
</span>
</button>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--small"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
Cancel
</span>
</span>
</button>
</div>
</div>
</div>
</div>,
]
`;

View file

@ -0,0 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/Variables/VarConfig default 1`] = `
<div
className="canvasSidebar__expandable canvasVarConfig__container "
>
<div
className="canvasVarConfig__innerContainer"
>
<div
className="euiAccordion canvasVarConfig__listView canvasSidebar__accordion"
>
<div
className="euiAccordion__triggerWrapper"
>
<button
aria-controls="accordion-variables"
aria-expanded={false}
className="euiAccordion__button"
onClick={[Function]}
type="button"
>
<span
className="euiAccordion__iconWrapper"
>
<div
className="euiAccordion__icon"
data-euiicon-type="arrowRight"
size="m"
/>
</span>
<span
className="euiIEFlexWrapFix"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
onBlur={[Function]}
onFocus={[Function]}
>
Variables
</span>
</span>
</span>
</button>
<div
className="euiAccordion__optionalAction"
>
<span
className="euiToolTipAnchor"
onKeyUp={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Add a variable"
className="euiButtonIcon euiButtonIcon--primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="plusInCircle"
size="m"
/>
</button>
</span>
</div>
</div>
<div
className="euiAccordion__childWrapper"
id="accordion-variables"
/>
</div>
<div
className="canvasVarConfig__editView canvasSidebar__accordion"
/>
</div>
</div>
`;

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { CanvasVariable } from '../../../../types';
import { DeleteVar } from '../delete_var';
const variable: CanvasVariable = {
name: 'homeUrl',
value: 'https://elastic.co',
type: 'string',
};
storiesOf('components/Variables/DeleteVar', module).add('default', () => (
<DeleteVar selectedVar={variable} onDelete={action('onDelete')} onCancel={action('onCancel')} />
));

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { CanvasVariable } from '../../../../types';
import { EditVar } from '../edit_var';
const variables: CanvasVariable[] = [
{
name: 'homeUrl',
value: 'https://elastic.co',
type: 'string',
},
{
name: 'bigNumber',
value: 1000,
type: 'number',
},
{
name: 'zenMode',
value: true,
type: 'boolean',
},
];
storiesOf('components/Variables/EditVar', module)
.add('new variable', () => (
<EditVar
variables={variables}
selectedVar={null}
onSave={action('onSave')}
onCancel={action('onCancel')}
/>
))
.add('edit variable (string)', () => (
<EditVar
variables={variables}
selectedVar={variables[0]}
onSave={action('onSave')}
onCancel={action('onCancel')}
/>
))
.add('edit variable (number)', () => (
<EditVar
variables={variables}
selectedVar={variables[1]}
onSave={action('onSave')}
onCancel={action('onCancel')}
/>
))
.add('edit variable (boolean)', () => (
<EditVar
variables={variables}
selectedVar={variables[2]}
onSave={action('onSave')}
onCancel={action('onCancel')}
/>
));

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import React from 'react';
import { CanvasVariable } from '../../../../types';
import { VarConfig } from '../var_config';
const variables: CanvasVariable[] = [
{
name: 'homeUrl',
value: 'https://elastic.co',
type: 'string',
},
{
name: 'bigNumber',
value: 1000,
type: 'number',
},
{
name: 'zenMode',
value: true,
type: 'boolean',
},
];
storiesOf('components/Variables/VarConfig', module).add('default', () => (
<VarConfig
variables={variables}
onCopyVar={action('onCopyVar')}
onDeleteVar={action('onDeleteVar')}
onAddVar={action('onAddVar')}
onEditVar={action('onEditVar')}
/>
));

View file

@ -0,0 +1,77 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import {
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiButtonEmpty,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { CanvasVariable } from '../../../types';
import { ComponentStrings } from '../../../i18n';
const { VarConfigDeleteVar: strings } = ComponentStrings;
import './var_panel.scss';
interface Props {
selectedVar: CanvasVariable;
onDelete: (v: CanvasVariable) => void;
onCancel: () => void;
}
export const DeleteVar: FC<Props> = ({ selectedVar, onCancel, onDelete }) => {
return (
<React.Fragment>
<div className="canvasVarHeader__triggerWrapper">
<button className="canvasVarHeader__button" type="button" onClick={() => onCancel()}>
<span className="canvasVarHeader__iconWrapper">
<EuiIcon type="sortLeft" style={{ verticalAlign: 'top' }} />
</span>
<span>
<span className="canvasVarHeader__anchor">{strings.getTitle()}</span>
</span>
</button>
</div>
<div className="canvasSidebar__accordionContent">
<div>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="s">
{strings.getWarningDescription()}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton
color="danger"
size="s"
fill
onClick={() => onDelete(selectedVar)}
iconType="trash"
>
{strings.getDeleteButtonLabel()}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty size="s" onClick={() => onCancel()}>
{strings.getCancelButtonLabel()}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</div>
</div>
</React.Fragment>
);
};

View file

@ -0,0 +1,8 @@
.canvasEditVar__typeOption {
display: flex;
align-items: center;
.canvasEditVar__tokenIcon {
margin-right: 15px;
}
}

View file

@ -0,0 +1,189 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, FC } from 'react';
import {
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
EuiToken,
EuiSuperSelect,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiButton,
EuiButtonEmpty,
EuiSpacer,
EuiCallOut,
} from '@elastic/eui';
import { CanvasVariable } from '../../../types';
import { VarValueField } from './var_value_field';
import { ComponentStrings } from '../../../i18n';
const { VarConfigEditVar: strings } = ComponentStrings;
import './edit_var.scss';
import './var_panel.scss';
interface Props {
selectedVar: CanvasVariable | null;
variables: CanvasVariable[];
onSave: (v: CanvasVariable) => void;
onCancel: () => void;
}
const checkDupeName = (newName: string, oldName: string | null, variables: CanvasVariable[]) => {
const match = variables.find((v) => {
// If the new name matches an existing variable and that
// matched variable name isn't the old name, then there
// is a duplicate
return newName === v.name && (!oldName || v.name !== oldName);
});
return !!match;
};
export const EditVar: FC<Props> = ({ variables, selectedVar, onCancel, onSave }) => {
// If there isn't a selected variable, we're creating a new var
const isNew = selectedVar === null;
const [type, setType] = useState(isNew ? 'string' : selectedVar!.type);
const [name, setName] = useState(isNew ? '' : selectedVar!.name);
const [value, setValue] = useState(isNew ? '' : selectedVar!.value);
const hasDupeName = checkDupeName(name, selectedVar && selectedVar.name, variables);
const typeOptions = [
{
value: 'string',
inputDisplay: (
<div className="canvasEditVar__typeOption">
<EuiToken iconType="tokenString" className="canvasEditVar__tokenIcon" />{' '}
<span>{strings.getTypeStringLabel()}</span>
</div>
),
},
{
value: 'number',
inputDisplay: (
<div className="canvasEditVar__typeOption">
<EuiToken iconType="tokenNumber" className="canvasEditVar__tokenIcon" />{' '}
<span>{strings.getTypeNumberLabel()}</span>
</div>
),
},
{
value: 'boolean',
inputDisplay: (
<div className="canvasEditVar__typeOption">
<EuiToken iconType="tokenBoolean" className="canvasEditVar__tokenIcon" />{' '}
<span>{strings.getTypeBooleanLabel()}</span>
</div>
),
},
];
return (
<>
<div className="canvasVarHeader__triggerWrapper">
<button className="canvasVarHeader__button" type="button" onClick={() => onCancel()}>
<span className="canvasVarHeader__iconWrapper">
<EuiIcon type="sortLeft" style={{ verticalAlign: 'top' }} />
</span>
<span>
<span className="canvasVarHeader__anchor">
{isNew ? strings.getAddTitle() : strings.getEditTitle()}
</span>
</span>
</button>
</div>
<div className="canvasSidebar__accordionContent">
{!isNew && (
<div>
<EuiCallOut
title={strings.getEditWarning()}
color="warning"
iconType="alert"
size="s"
/>
<EuiSpacer size="m" />
</div>
)}
<EuiForm component="form">
<EuiFormRow label={strings.getTypeFieldLabel()} display="rowCompressed">
<EuiSuperSelect
options={typeOptions}
valueOfSelected={type}
onChange={(v) => {
// Only have these types possible in the dropdown
setType(v as CanvasVariable['type']);
// Reset default value
if (v === 'boolean') {
// Just setting a default value
setValue(true);
} else if (v === 'number') {
// Setting default number
setValue(0);
} else {
setValue('');
}
}}
compressed={true}
/>
</EuiFormRow>
<EuiFormRow
label={strings.getNameFieldLabel()}
display="rowCompressed"
isInvalid={hasDupeName}
error={hasDupeName && strings.getDuplicateNameError()}
>
<EuiFieldText
name="name"
value={name}
compressed={true}
onChange={(e) => setName(e.target.value)}
isInvalid={hasDupeName}
/>
</EuiFormRow>
<EuiFormRow label={strings.getValueFieldLabel()} display="rowCompressed">
<VarValueField type={type} value={value} onChange={(v) => setValue(v)} />
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton
color="secondary"
size="s"
fill
onClick={() =>
onSave({
name,
value,
type,
})
}
disabled={hasDupeName || !name}
iconType="save"
>
{strings.getSaveButtonLabel()}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty size="s" onClick={() => onCancel()}>
{strings.getCancelButtonLabel()}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
</div>
</>
);
};

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import copy from 'copy-to-clipboard';
import { VarConfig as ChildComponent } from './var_config';
import {
withKibana,
KibanaReactContextValue,
KibanaServices,
} from '../../../../../../src/plugins/kibana_react/public';
import { CanvasServices } from '../../services';
import { ComponentStrings } from '../../../i18n';
import { CanvasVariable } from '../../../types';
const { VarConfig: strings } = ComponentStrings;
interface Props {
kibana: KibanaReactContextValue<{ canvas: CanvasServices } & KibanaServices>;
variables: CanvasVariable[];
setVariables: (variables: CanvasVariable[]) => void;
}
const WrappedComponent: FC<Props> = ({ kibana, variables, setVariables }) => {
const onDeleteVar = (v: CanvasVariable) => {
const index = variables.findIndex((targetVar: CanvasVariable) => {
return targetVar.name === v.name;
});
if (index !== -1) {
const newVars = [...variables];
newVars.splice(index, 1);
setVariables(newVars);
kibana.services.canvas.notify.success(strings.getDeleteNotificationDescription());
}
};
const onCopyVar = (v: CanvasVariable) => {
const snippetStr = `{var "${v.name}"}`;
copy(snippetStr, { debug: true });
kibana.services.canvas.notify.success(strings.getCopyNotificationDescription());
};
const onAddVar = (v: CanvasVariable) => {
setVariables([...variables, v]);
};
const onEditVar = (oldVar: CanvasVariable, newVar: CanvasVariable) => {
const existingVarIndex = variables.findIndex((v) => oldVar.name === v.name);
const newVars = [...variables];
newVars[existingVarIndex] = newVar;
setVariables(newVars);
};
return <ChildComponent {...{ variables, onCopyVar, onDeleteVar, onAddVar, onEditVar }} />;
};
export const VarConfig = withKibana(WrappedComponent);

View file

@ -0,0 +1,66 @@
.canvasVarConfig__container {
width: 100%;
position: relative;
&.canvasVarConfig-isEditMode {
.canvasVarConfig__innerContainer {
transform: translateX(-50%);
}
}
}
.canvasVarConfig__list {
table {
background-color: transparent;
}
thead tr th,
thead tr td {
border-bottom: none;
border-top: none;
}
tbody tr td {
border-top: none;
border-bottom: none;
}
tbody tr:hover {
background-color: transparent;
}
tbody tr:last-child td {
border-bottom: none;
}
}
.canvasVarConfig__innerContainer {
width: calc(200% + 48px); // Account for the extra padding
position: relative;
display: flex;
flex-direction: row;
align-content: stretch;
.canvasVarConfig__editView {
margin-left: 0;
}
.canvasVarConfig__listView {
margin-right: 0;
}
}
.canvasVarConfig__editView {
width: 50%;
height: 100%;
flex-shrink: 0;
}
.canvasVarConfig__listView {
width: 50%;
flex-shrink: 0;
}

View file

@ -0,0 +1,230 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, FC } from 'react';
import {
EuiAccordion,
EuiButtonIcon,
EuiToken,
EuiToolTip,
EuiText,
EuiInMemoryTable,
EuiBasicTableColumn,
EuiTableActionsColumnType,
EuiSpacer,
EuiButton,
} from '@elastic/eui';
import { CanvasVariable } from '../../../types';
import { ComponentStrings } from '../../../i18n';
import { EditVar } from './edit_var';
import { DeleteVar } from './delete_var';
import './var_config.scss';
const { VarConfig: strings } = ComponentStrings;
enum PanelMode {
List,
Edit,
Delete,
}
const typeToToken = {
number: 'tokenNumber',
boolean: 'tokenBoolean',
string: 'tokenString',
};
interface Props {
variables: CanvasVariable[];
onCopyVar: (v: CanvasVariable) => void;
onDeleteVar: (v: CanvasVariable) => void;
onAddVar: (v: CanvasVariable) => void;
onEditVar: (oldVar: CanvasVariable, newVar: CanvasVariable) => void;
}
export const VarConfig: FC<Props> = ({
variables,
onCopyVar,
onDeleteVar,
onAddVar,
onEditVar,
}) => {
const [panelMode, setPanelMode] = useState<PanelMode>(PanelMode.List);
const [selectedVar, setSelectedVar] = useState<CanvasVariable | null>(null);
const selectAndEditVar = (v: CanvasVariable) => {
setSelectedVar(v);
setPanelMode(PanelMode.Edit);
};
const selectAndDeleteVar = (v: CanvasVariable) => {
setSelectedVar(v);
setPanelMode(PanelMode.Delete);
};
const actions: EuiTableActionsColumnType<CanvasVariable>['actions'] = [
{
type: 'icon',
name: strings.getCopyActionButtonLabel(),
description: strings.getCopyActionTooltipLabel(),
icon: 'copyClipboard',
onClick: onCopyVar,
isPrimary: true,
},
{
type: 'icon',
name: strings.getEditActionButtonLabel(),
description: '',
icon: 'pencil',
onClick: selectAndEditVar,
},
{
type: 'icon',
name: strings.getDeleteActionButtonLabel(),
description: '',
icon: 'trash',
color: 'danger',
onClick: selectAndDeleteVar,
},
];
const varColumns: Array<EuiBasicTableColumn<CanvasVariable>> = [
{
field: 'type',
name: strings.getTableTypeLabel(),
sortable: true,
render: (varType: CanvasVariable['type'], _v: CanvasVariable) => {
return <EuiToken iconType={typeToToken[varType]} />;
},
width: '50px',
},
{
field: 'name',
name: strings.getTableNameLabel(),
sortable: true,
},
{
field: 'value',
name: strings.getTableValueLabel(),
sortable: true,
truncateText: true,
render: (value: CanvasVariable['value'], _v: CanvasVariable) => {
return '' + value;
},
},
{
actions,
width: '60px',
},
];
return (
<div
className={`canvasSidebar__expandable canvasVarConfig__container ${
panelMode !== PanelMode.List ? 'canvasVarConfig-isEditMode' : ''
}`}
>
<div className="canvasVarConfig__innerContainer">
<EuiAccordion
id="accordion-variables"
className="canvasVarConfig__listView canvasSidebar__accordion"
buttonContent={
<EuiToolTip
content={strings.getTitleTooltip()}
position="left"
className="canvasArg__tooltip"
>
<span>{strings.getTitle()}</span>
</EuiToolTip>
}
extraAction={
<EuiToolTip position="top" content={strings.getAddTooltipLabel()}>
<EuiButtonIcon
color="primary"
iconType="plusInCircle"
aria-label={strings.getAddTooltipLabel()}
onClick={() => {
setSelectedVar(null);
setPanelMode(PanelMode.Edit);
}}
/>
</EuiToolTip>
}
>
{variables.length !== 0 && (
<div className="canvasSidebar__accordionContent">
<EuiInMemoryTable
className="canvasVarConfig__list"
items={variables}
columns={varColumns}
hasActions={true}
pagination={false}
sorting={true}
compressed
/>
</div>
)}
{variables.length === 0 && (
<div className="canvasSidebar__accordionContent">
<EuiText color="subdued" size="s">
{strings.getEmptyDescription()}
</EuiText>
<EuiSpacer size="m" />
<EuiButton
size="s"
iconType="plusInCircle"
onClick={() => setPanelMode(PanelMode.Edit)}
>
{strings.getAddButtonLabel()}
</EuiButton>
</div>
)}
</EuiAccordion>
<div className="canvasVarConfig__editView canvasSidebar__accordion">
{panelMode === PanelMode.Edit && (
<EditVar
variables={variables}
selectedVar={selectedVar}
onSave={(newVar: CanvasVariable) => {
if (!selectedVar) {
onAddVar(newVar);
} else {
onEditVar(selectedVar, newVar);
}
setSelectedVar(null);
setPanelMode(PanelMode.List);
}}
onCancel={() => {
setSelectedVar(null);
setPanelMode(PanelMode.List);
}}
/>
)}
{panelMode === PanelMode.Delete && selectedVar && (
<DeleteVar
selectedVar={selectedVar}
onDelete={(v: CanvasVariable) => {
onDeleteVar(v);
setSelectedVar(null);
setPanelMode(PanelMode.List);
}}
onCancel={() => {
setSelectedVar(null);
setPanelMode(PanelMode.List);
}}
/>
)}
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,31 @@
.canvasVarHeader__triggerWrapper {
display: flex;
align-items: center;
}
.canvasVarHeader__button {
@include euiFontSize;
text-align: left;
width: 100%;
flex-grow: 1;
display: flex;
align-items: center;
}
.canvasVarHeader__iconWrapper {
width: $euiSize;
height: $euiSize;
border-radius: $euiBorderRadius;
margin-right: $euiSizeS;
margin-left: $euiSizeXS;
flex-shrink: 0;
}
.canvasVarHeader__anchor {
display: inline-block;
}

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC } from 'react';
import { EuiFieldText, EuiFieldNumber, EuiButtonGroup } from '@elastic/eui';
import { htmlIdGenerator } from '@elastic/eui';
import { CanvasVariable } from '../../../types';
import { ComponentStrings } from '../../../i18n';
const { VarConfigVarValueField: strings } = ComponentStrings;
interface Props {
type: CanvasVariable['type'];
value: CanvasVariable['value'];
onChange: (v: CanvasVariable['value']) => void;
}
export const VarValueField: FC<Props> = ({ type, value, onChange }) => {
const idPrefix = htmlIdGenerator()();
const options = [
{
id: `${idPrefix}-true`,
label: strings.getTrueOption(),
},
{
id: `${idPrefix}-false`,
label: strings.getFalseOption(),
},
];
if (type === 'number') {
return (
<EuiFieldNumber
compressed
name="value"
value={value as number}
onChange={(e) => onChange(e.target.value)}
/>
);
} else if (type === 'boolean') {
return (
<EuiButtonGroup
name="value"
options={options}
idSelected={`${idPrefix}-${value}`}
onChange={(id) => {
const val = id.replace(`${idPrefix}-`, '') === 'true';
onChange(val);
}}
buttonSize="compressed"
isFullWidth
/>
);
}
return (
<EuiFieldText
compressed
name="value"
value={String(value)}
onChange={(e) => onChange(e.target.value)}
/>
);
};

View file

@ -7,11 +7,17 @@
import { connect } from 'react-redux';
import { get } from 'lodash';
import { sizeWorkpad as setSize, setName, setWorkpadCSS } from '../../state/actions/workpad';
import {
sizeWorkpad as setSize,
setName,
setWorkpadCSS,
updateWorkpadVariables,
} from '../../state/actions/workpad';
import { getWorkpad } from '../../state/selectors/workpad';
import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants';
import { WorkpadConfig as Component } from './workpad_config';
import { State } from '../../../types';
import { State, CanvasVariable } from '../../../types';
const mapStateToProps = (state: State) => {
const workpad = getWorkpad(state);
@ -23,6 +29,7 @@ const mapStateToProps = (state: State) => {
height: get(workpad, 'height'),
},
css: get(workpad, 'css', DEFAULT_WORKPAD_CSS),
variables: get(workpad, 'variables', []),
};
};
@ -30,6 +37,7 @@ const mapDispatchToProps = {
setSize,
setName,
setWorkpadCSS,
setWorkpadVariables: (vars: CanvasVariable[]) => updateWorkpadVariables(vars),
};
export const WorkpadConfig = connect(mapStateToProps, mapDispatchToProps)(Component);

View file

@ -19,10 +19,13 @@ import {
EuiToolTip,
EuiTextArea,
EuiAccordion,
EuiText,
EuiButton,
} from '@elastic/eui';
import { VarConfig } from '../var_config';
import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants';
import { CanvasVariable } from '../../../types';
import { ComponentStrings } from '../../../i18n';
const { WorkpadConfig: strings } = ComponentStrings;
@ -34,14 +37,16 @@ interface Props {
};
name: string;
css?: string;
variables: CanvasVariable[];
setSize: ({ height, width }: { height: number; width: number }) => void;
setName: (name: string) => void;
setWorkpadCSS: (css: string) => void;
setWorkpadVariables: (vars: CanvasVariable[]) => void;
}
export const WorkpadConfig: FunctionComponent<Props> = (props) => {
const [css, setCSS] = useState(props.css);
const { size, name, setSize, setName, setWorkpadCSS } = props;
const { size, name, setSize, setName, setWorkpadCSS, variables, setWorkpadVariables } = props;
const rotate = () => setSize({ width: size.height, height: size.width });
const badges = [
@ -129,23 +134,25 @@ export const WorkpadConfig: FunctionComponent<Props> = (props) => {
</div>
<EuiSpacer size="m" />
<div className="canvasArg--expandable">
<VarConfig variables={variables} setVariables={setWorkpadVariables} />
<div className="canvasSidebar__expandable">
<EuiAccordion
id="accordion-global-css"
className="canvasArg__accordion"
className="canvasSidebar__accordion"
style={{ marginBottom: 0 }}
buttonContent={
<EuiToolTip
content={strings.getGlobalCSSTooltip()}
position="left"
className="canvasArg__tooltip"
>
<EuiText size="s" color="subdued">
{strings.getGlobalCSSLabel()}
</EuiText>
<span>{strings.getGlobalCSSLabel()}</span>
</EuiToolTip>
}
>
<div className="canvasArg__content">
<div className="canvasSidebar__accordionContent">
<EuiTextArea
aria-label={strings.getGlobalCSSTooltip()}
value={css}
@ -169,7 +176,9 @@ WorkpadConfig.propTypes = {
size: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
css: PropTypes.string,
variables: PropTypes.array,
setSize: PropTypes.func.isRequired,
setName: PropTypes.func.isRequired,
setWorkpadCSS: PropTypes.func.isRequired,
setWorkpadVariables: PropTypes.func.isRequired,
};

View file

@ -10,7 +10,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public';
import { interpretAst } from '../lib/run_interpreter';
// @ts-expect-error untyped local
import { getState } from '../state/store';
import { getGlobalFilters } from '../state/selectors/workpad';
import { getGlobalFilters, getWorkpadVariablesAsObject } from '../state/selectors/workpad';
import { ExpressionValueFilter } from '../../types';
import { getFunctionHelp } from '../../i18n';
import { InitializeArguments } from '.';
@ -79,7 +79,7 @@ export function filtersFunctionFactory(initialize: InitializeArguments): () => F
if (filterList && filterList.length) {
const filterExpression = filterList.join(' | ');
const filterAST = fromExpression(filterExpression);
return interpretAst(filterAST);
return interpretAst(filterAST, getWorkpadVariablesAsObject(getState()));
} else {
const filterType = initialize.typesRegistry.get('filter');
return filterType?.from(null, {});

View file

@ -15,8 +15,12 @@ interface Options {
/**
* Meant to be a replacement for plugins/interpreter/interpretAST
*/
export async function interpretAst(ast: ExpressionAstExpression): Promise<ExpressionValue> {
return await expressionsService.getService().execute(ast).getData();
export async function interpretAst(
ast: ExpressionAstExpression,
variables: Record<string, any>
): Promise<ExpressionValue> {
const context = { variables };
return await expressionsService.getService().execute(ast, null, context).getData();
}
/**
@ -24,6 +28,7 @@ export async function interpretAst(ast: ExpressionAstExpression): Promise<Expres
*
* @param {object} ast - Executable AST
* @param {any} input - Initial input for AST execution
* @param {object} variables - Variables to pass in to the intrepreter context
* @param {object} options
* @param {boolean} options.castToRender - try to cast to a type: render object?
* @returns {promise}
@ -31,17 +36,20 @@ export async function interpretAst(ast: ExpressionAstExpression): Promise<Expres
export async function runInterpreter(
ast: ExpressionAstExpression,
input: ExpressionValue,
variables: Record<string, any>,
options: Options = {}
): Promise<ExpressionValue> {
const context = { variables };
try {
const renderable = await expressionsService.getService().execute(ast, input).getData();
const renderable = await expressionsService.getService().execute(ast, input, context).getData();
if (getType(renderable) === 'render') {
return renderable;
}
if (options.castToRender) {
return runInterpreter(fromExpression('render'), renderable, {
return runInterpreter(fromExpression('render'), renderable, variables, {
castToRender: false,
});
}

View file

@ -21,6 +21,7 @@ const validKeys = [
'assets',
'colors',
'css',
'variables',
'height',
'id',
'isWriteable',
@ -61,6 +62,7 @@ export function create(workpad) {
return fetch.post(getApiPath(), {
...sanitizeWorkpad({ ...workpad }),
assets: workpad.assets || {},
variables: workpad.variables || [],
});
}
@ -73,7 +75,7 @@ export async function createFromTemplate(templateId) {
export function get(workpadId) {
return fetch.get(`${getApiPath()}/${workpadId}`).then(({ data: workpad }) => {
// shim old workpads with new properties
return { css: DEFAULT_WORKPAD_CSS, ...workpad };
return { css: DEFAULT_WORKPAD_CSS, variables: [], ...workpad };
});
}

View file

@ -9,7 +9,13 @@ import immutable from 'object-path-immutable';
import { get, pick, cloneDeep, without } from 'lodash';
import { toExpression, safeElementFromExpression } from '@kbn/interpreter/common';
import { createThunk } from '../../lib/create_thunk';
import { getPages, getNodeById, getNodes, getSelectedPageIndex } from '../selectors/workpad';
import {
getPages,
getWorkpadVariablesAsObject,
getNodeById,
getNodes,
getSelectedPageIndex,
} from '../selectors/workpad';
import { getValue as getResolvedArgsValue } from '../selectors/resolved_args';
import { getDefaultElement } from '../defaults';
import { ErrorStrings } from '../../../i18n';
@ -96,13 +102,15 @@ export const fetchContext = createThunk(
return i < index;
});
const variables = getWorkpadVariablesAsObject(getState());
// get context data from a partial AST
return interpretAst(
{
...element.ast,
chain: astChain,
},
prevContextValue
variables
).then((value) => {
dispatch(
args.setValue({
@ -114,7 +122,7 @@ export const fetchContext = createThunk(
}
);
const fetchRenderableWithContextFn = ({ dispatch }, element, ast, context) => {
const fetchRenderableWithContextFn = ({ dispatch, getState }, element, ast, context) => {
const argumentPath = [element.id, 'expressionRenderable'];
dispatch(
args.setLoading({
@ -128,7 +136,9 @@ const fetchRenderableWithContextFn = ({ dispatch }, element, ast, context) => {
value: renderable,
});
return runInterpreter(ast, context, { castToRender: true })
const variables = getWorkpadVariablesAsObject(getState());
return runInterpreter(ast, context, variables, { castToRender: true })
.then((renderable) => {
dispatch(getAction(renderable));
})
@ -172,7 +182,9 @@ export const fetchAllRenderables = createThunk(
const ast = element.ast || safeElementFromExpression(element.expression);
const argumentPath = [element.id, 'expressionRenderable'];
return runInterpreter(ast, null, { castToRender: true })
const variables = getWorkpadVariablesAsObject(getState());
return runInterpreter(ast, null, variables, { castToRender: true })
.then((renderable) => ({ path: argumentPath, value: renderable }))
.catch((err) => {
services.notify.getService().error(err);

View file

@ -10,7 +10,7 @@ import { createThunk } from '../../lib/create_thunk';
import { getWorkpadColors } from '../selectors/workpad';
// @ts-expect-error
import { fetchAllRenderables } from './elements';
import { CanvasWorkpad } from '../../../types';
import { CanvasWorkpad, CanvasVariable } from '../../../types';
export const sizeWorkpad = createAction<{ height: number; width: number }>('sizeWorkpad');
export const setName = createAction<string>('setName');
@ -18,6 +18,7 @@ export const setWriteable = createAction<boolean>('setWriteable');
export const setColors = createAction<string[]>('setColors');
export const setRefreshInterval = createAction<number>('setRefreshInterval');
export const setWorkpadCSS = createAction<string>('setWorkpadCSS');
export const setWorkpadVariables = createAction<CanvasVariable[]>('setWorkpadVariables');
export const enableAutoplay = createAction<boolean>('enableAutoplay');
export const setAutoplayInterval = createAction<number>('setAutoplayInterval');
export const resetWorkpad = createAction<void>('resetWorkpad');
@ -38,6 +39,14 @@ export const removeColor = createThunk('removeColor', ({ dispatch, getState }, c
dispatch(setColors(without(getWorkpadColors(getState()), color)));
});
export const updateWorkpadVariables = createThunk(
'updateWorkpadVariables',
({ dispatch }, vars) => {
dispatch(setWorkpadVariables(vars));
dispatch(fetchAllRenderables());
}
);
export const setWorkpad = createThunk(
'setWorkpad',
(

View file

@ -81,6 +81,7 @@ export const getDefaultWorkpad = () => {
'#FFFFFF',
'rgba(255,255,255,0)', // 'transparent'
],
variables: [],
isWriteable: true,
};
};

View file

@ -14,6 +14,7 @@ import {
setName,
setWriteable,
setWorkpadCSS,
setWorkpadVariables,
resetWorkpad,
} from '../actions/workpad';
@ -59,6 +60,10 @@ export const workpadReducer = handleActions(
return { ...workpadState, css: payload };
},
[setWorkpadVariables]: (workpadState, { payload }) => {
return { ...workpadState, variables: payload };
},
[resetWorkpad]: () => ({ ...getDefaultWorkpad() }),
},
{}

View file

@ -10,7 +10,14 @@ import { safeElementFromExpression, fromExpression } from '@kbn/interpreter/comm
// @ts-expect-error untyped local
import { append } from '../../lib/modify_path';
import { getAssets } from './assets';
import { State, CanvasWorkpad, CanvasPage, CanvasElement, ResolvedArgType } from '../../../types';
import {
State,
CanvasWorkpad,
CanvasPage,
CanvasElement,
CanvasVariable,
ResolvedArgType,
} from '../../../types';
import {
ExpressionContext,
CanvasGroup,
@ -49,6 +56,23 @@ export function getWorkpadPersisted(state: State) {
return getWorkpad(state);
}
export function getWorkpadVariables(state: State) {
const workpad = getWorkpad(state);
return get(workpad, 'variables', []);
}
export function getWorkpadVariablesAsObject(state: State) {
const variables = getWorkpadVariables(state);
if (variables.length === 0) {
return {};
}
return (variables as CanvasVariable[]).reduce(
(vars: Record<string, any>, v: CanvasVariable) => ({ ...vars, [v.name]: v.value }),
{}
);
}
export function getWorkpadInfo(state: State): WorkpadInfo {
return {
...getWorkpad(state),
@ -326,7 +350,9 @@ export function getElements(
return elements.map((el) => omit(el, ['ast']));
}
return elements.map(appendAst);
const elementAppendAst = (elem: CanvasElement) => appendAst(elem);
return elements.map(elementAppendAst);
}
const augment = (type: string) => <T extends CanvasElement | CanvasGroup>(n: T): T => ({

View file

@ -51,12 +51,19 @@ export const WorkpadAssetSchema = schema.object({
value: schema.string(),
});
export const WorkpadVariable = schema.object({
name: schema.string(),
value: schema.oneOf([schema.string(), schema.number(), schema.boolean()]),
type: schema.string(),
});
export const WorkpadSchema = schema.object({
'@created': schema.maybe(schema.string()),
'@timestamp': schema.maybe(schema.string()),
assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)),
colors: schema.arrayOf(schema.string()),
css: schema.string(),
variables: schema.arrayOf(WorkpadVariable),
height: schema.number(),
id: schema.string(),
isWriteable: schema.maybe(schema.boolean()),

View file

@ -1644,5 +1644,6 @@ export const pitch: CanvasTemplate = {
},
css:
".canvasPage h1, .canvasPage h2, .canvasPage h3, .canvasPage h4, .canvasPage h5 {\nfont-family: 'Futura';\ncolor: #444444;\n}\n\n.canvasPage h1 {\nfont-size: 112px;\nfont-weight: bold;\ncolor: #FFFFFF;\n}\n\n.canvasPage h2 {\nfont-size: 48px;\nfont-weight: bold;\n}\n\n.canvasPage h3 {\nfont-size: 30px;\nfont-weight: 300;\ntext-transform: uppercase;\ncolor: #FFFFFF;\n}\n\n.canvasPage h5 {\nfont-size: 24px;\nfont-style: italic;\n}",
variables: [],
},
};

View file

@ -17,6 +17,7 @@ export const status: CanvasTemplate = {
height: 792,
css:
'.canvasPage h1, .canvasPage h2, .canvasPage h3, .canvasPage h4, .canvasPage h5, .canvasPage h6, .canvasPage li, .canvasPage p, .canvasPage th, .canvasPage td {\nfont-family: "Gill Sans" !important;\ncolor: #333333;\n}\n\n.canvasPage h1, .canvasPage h2 {\nfont-weight: 400;\n}\n\n.canvasPage h2 {\ntext-transform: uppercase;\ncolor: #1785B0;\n}\n\n.canvasMarkdown p,\n.canvasMarkdown li {\nfont-size: 18px;\n}\n\n.canvasMarkdown li {\nmargin-bottom: .75em;\n}\n\n.canvasMarkdown h3:not(:first-child) {\nmargin-top: 2em;\n}\n\n.canvasMarkdown a {\ncolor: #1785B0;\n}\n\n.canvasMarkdown th,\n.canvasMarkdown td {\npadding: .5em 1em;\n}\n\n.canvasMarkdown th {\nbackground-color: #FAFBFD;\n}\n\n.canvasMarkdown table,\n.canvasMarkdown th,\n.canvasMarkdown td {\nborder: 1px solid #e4e9f2;\n}',
variables: [],
page: 0,
pages: [
{

View file

@ -493,5 +493,6 @@ export const summary: CanvasTemplate = {
'@created': '2019-05-31T16:01:45.751Z',
assets: {},
css: 'h3 {\ncolor: #343741;\nfont-weight: 400;\n}\n\nh5 {\ncolor: #69707D;\n}',
variables: [],
},
};

View file

@ -17,6 +17,7 @@ export const dark: CanvasTemplate = {
height: 720,
page: 0,
css: '',
variables: [],
pages: [
{
id: 'page-fda26a1f-c096-44e4-a149-cb99e1038a34',

View file

@ -14,6 +14,7 @@ export const light: CanvasTemplate = {
template: {
name: 'Light',
css: '',
variables: [],
width: 1080,
height: 720,
page: 0,

View file

@ -37,12 +37,19 @@ export interface CanvasPage {
groups: CanvasGroup[];
}
export interface CanvasVariable {
name: string;
value: boolean | number | string;
type: 'boolean' | 'number' | 'string';
}
export interface CanvasWorkpad {
'@created': string;
'@timestamp': string;
assets: { [id: string]: CanvasAsset };
colors: string[];
css: string;
variables: CanvasVariable[];
height: number;
id: string;
isWriteable: boolean;