[Canvas] Feat: static tags for elements (#28779)

* Extract Tags component and tag filter config object from workpad templates

    Added tag filtering to element selection modal

    Added tags to element

    Added type definitions to Registry in @kbn/interpreter

    Added stories for Tag and TagList components

    Changed graphic tag color
This commit is contained in:
Catherine Liu 2019-05-08 15:46:09 -05:00 committed by GitHub
parent 860977694b
commit 9c6a58cfb6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1442 additions and 117 deletions

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { Registry } from './lib/registry';

View file

@ -0,0 +1,36 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class Registry<ItemSpec, Item> {
constructor(prop?: string);
public wrapper(obj: ItemSpec): Item;
public register(fn: () => ItemSpec): void;
public toJS(): { [key: string]: any };
public toArray(): Item[];
public get(name: string): Item;
public getProp(): string;
public reset(): void;
}

View file

@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["index.d.ts", "src/**/*.d.ts"]
}

View file

@ -11,6 +11,7 @@ export const areaChart: ElementFactory = () => ({
name: 'areaChart',
displayName: 'Area chart',
help: 'A line chart with a filled body',
tags: ['chart'],
image: header,
expression: `filters
| demodata

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const bubbleChart: ElementFactory = () => ({
name: 'bubbleChart',
displayName: 'Bubble chart',
tags: ['chart'],
help: 'A customizable bubble chart',
width: 700,
height: 300,

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const debug: ElementFactory = () => ({
name: 'debug',
displayName: 'Debug',
tags: ['text'],
help: 'Just dumps the configuration of the element',
image: header,
expression: `demodata

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const donut: ElementFactory = () => ({
name: 'donut',
displayName: 'Donut chart',
tags: ['chart', 'proportion'],
help: 'A customizable donut chart',
image: header,
expression: `filters

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const dropdownFilter: ElementFactory = () => ({
name: 'dropdown_filter',
displayName: 'Dropdown filter',
tags: ['filter'],
help: 'A dropdown from which you can select values for an "exactly" filter',
image: header,
height: 50,

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const horizontalBarChart: ElementFactory = () => ({
name: 'horizontalBarChart',
displayName: 'Horizontal bar chart',
tags: ['chart'],
help: 'A customizable horizontal bar chart',
image: header,
expression: `filters

View file

@ -11,6 +11,7 @@ import header from './header.png';
export const horizontalProgressBar: ElementFactory = () => ({
name: 'horizontalProgressBar',
displayName: 'Horizontal progress bar',
tags: ['chart', 'proportion'],
help: 'Displays progress as a portion of a horizontal bar',
width: 400,
height: 30,

View file

@ -11,6 +11,7 @@ import header from './header.png';
export const horizontalProgressPill: ElementFactory = () => ({
name: 'horizontalProgressPill',
displayName: 'Horizontal progress pill',
tags: ['chart', 'proportion'],
help: 'Displays progress as a portion of a horizontal pill',
width: 400,
height: 30,

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const image: ElementFactory = () => ({
name: 'image',
displayName: 'Image',
tags: ['graphic'],
help: 'A static image',
image: header,
expression: `image dataurl=null mode="contain"

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const lineChart: ElementFactory = () => ({
name: 'lineChart',
displayName: 'Line chart',
tags: ['chart'],
help: 'A customizable line chart',
image: header,
expression: `filters

View file

@ -10,6 +10,7 @@ import { ElementFactory } from '../types';
export const markdown: ElementFactory = () => ({
name: 'markdown',
displayName: 'Markdown',
tags: ['text'],
help: 'Markup from Markdown',
image: header,
expression: `filters

View file

@ -11,6 +11,7 @@ import { ElementFactory } from '../types';
export const metric: ElementFactory = () => ({
name: 'metric',
displayName: 'Metric',
tags: ['text'],
help: 'A number with a label',
width: 200,
height: 100,

View file

@ -10,6 +10,7 @@ import { ElementFactory } from '../types';
export const pie: ElementFactory = () => ({
name: 'pie',
displayName: 'Pie chart',
tags: ['chart', 'proportion'],
width: 300,
height: 300,
help: 'A simple pie chart',

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const plot: ElementFactory = () => ({
name: 'plot',
displayName: 'Coordinate plot',
tags: ['chart'],
help: 'Mixed line, bar or dot charts',
image: header,
expression: `filters

View file

@ -11,6 +11,7 @@ import header from './header.png';
export const progressGauge: ElementFactory = () => ({
name: 'progressGauge',
displayName: 'Progress gauge',
tags: ['chart', 'proportion'],
help: 'Displays progress as a portion of a gauge',
width: 200,
height: 200,

View file

@ -11,6 +11,7 @@ import header from './header.png';
export const progressSemicircle: ElementFactory = () => ({
name: 'progressSemicircle',
displayName: 'Progress semicircle',
tags: ['chart', 'proportion'],
help: 'Displays progress as a portion of a semicircle',
width: 200,
height: 100,

View file

@ -11,6 +11,7 @@ import header from './header.png';
export const progressWheel: ElementFactory = () => ({
name: 'progressWheel',
displayName: 'Progress wheel',
tags: ['chart', 'proportion'],
help: 'Displays progress as a portion of a wheel',
width: 200,
height: 200,

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const repeatImage: ElementFactory = () => ({
name: 'repeatImage',
displayName: 'Image repeat',
tags: ['graphic', 'proportion'],
help: 'Repeats an image N times',
image: header,
expression: `filters

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const revealImage: ElementFactory = () => ({
name: 'revealImage',
displayName: 'Image reveal',
tags: ['graphic', 'proportion'],
help: 'Reveals a percentage of an image',
image: header,
expression: `filters

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const shape: ElementFactory = () => ({
name: 'shape',
displayName: 'Shape',
tags: ['graphic'],
help: 'A customizable shape',
width: 200,
height: 200,

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const table: ElementFactory = () => ({
name: 'table',
displayName: 'Data table',
tags: ['text'],
help: 'A scrollable grid for displaying data in a tabular format',
image: header,
expression: `filters

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const tiltedPie: ElementFactory = () => ({
name: 'tiltedPie',
displayName: 'Tilted pie chart',
tags: ['chart', 'proportion'],
width: 500,
height: 250,
help: 'A customizable tilted pie chart',

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const timeFilter: ElementFactory = () => ({
name: 'time_filter',
displayName: 'Time filter',
tags: ['filter'],
help: 'Set a time window',
image: header,
height: 50,

View file

@ -9,6 +9,7 @@ export interface ElementSpec {
image: string;
expression: string;
displayName?: string;
tags?: string[];
help?: string;
filter?: string;
width?: number;

View file

@ -10,6 +10,7 @@ import header from './header.png';
export const verticalBarChart: ElementFactory = () => ({
name: 'verticalBarChart',
displayName: 'Vertical bar chart',
tags: ['chart'],
help: 'A customizable vertical bar chart',
image: header,
expression: `filters

View file

@ -11,6 +11,7 @@ import header from './header.png';
export const verticalProgressBar: ElementFactory = () => ({
name: 'verticalProgressBar',
displayName: 'Vertical progress bar',
tags: ['chart', 'proportion'],
help: 'Displays progress as a portion of a vertical bar',
width: 80,
height: 400,

View file

@ -11,6 +11,7 @@ import header from './header.png';
export const verticalProgressPill: ElementFactory = () => ({
name: 'verticalProgressPill',
displayName: 'Vertical progress pill',
tags: ['chart', 'proportion'],
help: 'Displays progress as a portion of a vertical pill',
width: 80,
height: 400,

View file

@ -4,10 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export function Tag(config) {
// The name of the tag
this.name = config.name;
import { TagFactory } from '../../../public/lib/tag';
// color of the tag to display in a list
this.color = config.color;
}
export const chart: TagFactory = () => ({ name: 'chart', color: '#FEB6DB' });

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { TagFactory } from '../../../public/lib/tag';
export const filter: TagFactory = () => ({ name: 'filter', color: '#3185FC' });

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { TagFactory } from '../../../public/lib/tag';
export const graphic: TagFactory = () => ({ name: 'graphic', color: '#E6C220' });

View file

@ -4,8 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { chart } from './chart';
import { filter } from './filter';
import { graphic } from './graphic';
import { presentation } from './presentation';
import { proportion } from './proportion';
import { report } from './report';
import { text } from './text';
// Registry expects a function that returns a spec object
export const tagSpecs = [presentation, report];
export const tagSpecs = [chart, filter, graphic, presentation, proportion, report, text];

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { TagFactory } from '../../../public/lib/tag';
export const presentation: TagFactory = () => ({ name: 'presentation', color: '#017D73' });

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const presentation = () => ({ name: 'presentation', color: '#1EA593' });
export const proportion = () => ({ name: 'proportion', color: '#490092' });

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { TagFactory } from '../../../public/lib/tag';
export const proportion: TagFactory = () => ({ name: 'proportion', color: '#490092' });

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { TagFactory } from '../../../public/lib/tag';
export const report: TagFactory = () => ({ name: 'report', color: '#DB1374' });

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { TagFactory } from '../../../public/lib/tag';
export const text: TagFactory = () => ({ name: 'text', color: '#D3DAE6' });

View file

@ -103,6 +103,179 @@ exports[`Storyshots components/ElementCard with image 1`] = `
</div>
`;
exports[`Storyshots components/ElementCard with tags 1`] = `
<div
style={
Object {
"width": "210px",
}
}
>
<button
className="euiCard euiCard--leftAligned euiCard--isClickable euiCard--hasIcon canvasElementCard canvasElementCard--hasIcon"
onClick={[Function]}
>
<span
className="euiCard__top"
>
<svg
className="euiIcon euiIcon--xxLarge euiIcon--app euiCard__icon"
focusable="false"
height="32"
style={null}
viewBox="0 0 32 32"
width="32"
xmlns="http://www.w3.org/2000/svg"
>
<path
className="euiIcon__fillSecondary"
d="M7 17h2v7H7zM12 14h2v10h-2zM17 16h2v8h-2zM22 14h3v2h-3zM22 18h3v2h-3zM22 22h3v2h-3z"
/>
<path
d="M30.73 24a6.47 6.47 0 0 1 .45-2.19c.337-.9.52-1.85.54-2.81a8.55 8.55 0 0 0-.54-2.81 6.47 6.47 0 0 1-.45-2.19 9.2 9.2 0 0 1 .62-2.49c.53-1.57 1.08-3.19.08-4.2-1-1.01-2.41-.44-3.52.05a5.59 5.59 0 0 1-2.09.64 5.3 5.3 0 0 1-.59 0L16 .28 6.77 8a5.3 5.3 0 0 1-.59 0 5.59 5.59 0 0 1-2.09-.65C3 6.87 1.6 6.25.57 7.31c-1.03 1.06-.45 2.63.08 4.2A9.2 9.2 0 0 1 1.27 14a6.47 6.47 0 0 1-.45 2.19A8.55 8.55 0 0 0 .28 19c.02.96.203 1.91.54 2.81A6.47 6.47 0 0 1 1.27 24a9.2 9.2 0 0 1-.62 2.49c-.53 1.57-1.08 3.19-.08 4.2.353.38.852.59 1.37.58a5.67 5.67 0 0 0 2.15-.63A5.59 5.59 0 0 1 6.18 30a7.13 7.13 0 0 1 2.29.47 8 8 0 0 0 2.62.53 7.37 7.37 0 0 0 2.47-.51A7.14 7.14 0 0 1 16 30a6.24 6.24 0 0 1 2.14.45 8 8 0 0 0 2.77.55 8.08 8.08 0 0 0 2.77-.55 6.24 6.24 0 0 1 2.14-.45 5.59 5.59 0 0 1 2.09.65c1.11.49 2.49 1.11 3.52.05 1.03-1.06.45-2.63-.08-4.2a9.2 9.2 0 0 1-.62-2.5zM21.17 7h-.26a8 8 0 0 0-2.77.55A6.24 6.24 0 0 1 16 8a6.24 6.24 0 0 1-2.14-.45A8 8 0 0 0 11.09 7h-.26L16 2.72 21.17 7zm8.89 22.27a4.42 4.42 0 0 1-1.34-.46 7.08 7.08 0 0 0-2.9-.82 8.14 8.14 0 0 0-2.78.55 6.13 6.13 0 0 1-2.13.45 6.24 6.24 0 0 1-2.14-.45A8 8 0 0 0 16 28a9 9 0 0 0-3.08.6 5.74 5.74 0 0 1-1.83.4 6.36 6.36 0 0 1-2-.43A8.72 8.72 0 0 0 6.18 28a7.08 7.08 0 0 0-2.9.82 9.65 9.65 0 0 1-1.28.52 6.08 6.08 0 0 1 .52-2.21c.403-1 .65-2.055.73-3.13a8.55 8.55 0 0 0-.54-2.81A6.47 6.47 0 0 1 2.27 19a6.47 6.47 0 0 1 .44-2.19c.337-.9.52-1.85.54-2.81a10.48 10.48 0 0 0-.72-3.13 9 9 0 0 1-.59-2.16H2c.447.1.88.255 1.29.46a7.08 7.08 0 0 0 2.9.82A8.14 8.14 0 0 0 9 9.44 6.13 6.13 0 0 1 11.09 9a6.13 6.13 0 0 1 2.13.45A8.14 8.14 0 0 0 16 10a8.14 8.14 0 0 0 2.78-.55A6.13 6.13 0 0 1 20.91 9a6.13 6.13 0 0 1 2.09.44 8.14 8.14 0 0 0 2.78.55 7.08 7.08 0 0 0 2.9-.82A9.65 9.65 0 0 1 30 8.66a6.08 6.08 0 0 1-.52 2.21c-.403 1-.65 2.055-.73 3.13.02.96.203 1.91.54 2.81a6.47 6.47 0 0 1 .44 2.19 6.47 6.47 0 0 1-.44 2.19 8.55 8.55 0 0 0-.54 2.81c.078 1.074.32 2.13.72 3.13a9 9 0 0 1 .59 2.16v-.02z"
/>
</svg>
</span>
<span
className="euiCard__content"
>
<span
className="euiTitle euiTitle--medium euiCard__title"
id="generated-idTitle"
>
Element 1
</span>
<div
className="euiText euiText--small euiCard__description"
id="generated-idDescription"
>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis.
</p>
</div>
</span>
<span
className="euiCard__footer"
>
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#666666",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag1
</span>
</span>
</span>
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#666666",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag2
</span>
</span>
</span>
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#666666",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag3
</span>
</span>
</span>
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#666666",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag4
</span>
</span>
</span>
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#666666",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag5
</span>
</span>
</span>
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#666666",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag6
</span>
</span>
</span>
</span>
</button>
</div>
`;
exports[`Storyshots components/ElementCard with title and description 1`] = `
<div
style={

View file

@ -33,6 +33,14 @@ storiesOf('components/ElementCard', module)
image={elasticLogo}
/>
))
.add('with tags', () => (
<ElementCard
title="Element 1"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis."
tags={['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6']}
onClick={action('onClick')}
/>
))
.add('with click handler', () => (
<ElementCard
title="Element 1"

View file

@ -10,6 +10,7 @@ import {
EuiCard,
EuiIcon,
} from '@elastic/eui';
import { TagList } from '../tag_list/';
export interface Props {
/**
@ -24,18 +25,25 @@ export interface Props {
* preview image of the element
*/
image?: string;
/**
* tags associated with the element
*/
tags?: string[];
/**
* handler when clicking the card
*/
onClick?: () => void;
}
export const ElementCard = ({ title, description, image, onClick, ...rest }: Props) => (
const tagType = 'badge';
export const ElementCard = ({ title, description, image, tags = [], onClick, ...rest }: Props) => (
<EuiCard
className={image ? 'canvasElementCard' : 'canvasElementCard canvasElementCard--hasIcon'}
textAlign="left"
title={title}
description={description}
footer={<TagList tags={tags} tagType={tagType} />}
image={image}
icon={image ? null : <EuiIcon type="canvasApp" size="xxl" />}
onClick={onClick}

View file

@ -530,7 +530,81 @@ exports[`Storyshots components/ElementTypes/ElementGrid with controls and filter
</div>
`;
exports[`Storyshots components/ElementTypes/ElementGrid with filter 1`] = `
exports[`Storyshots components/ElementTypes/ElementGrid with tags filter 1`] = `
<div
style={
Object {
"width": "1000px",
}
}
>
<div
className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive"
>
<div
className="euiFlexItem canvasElementCard__wrapper"
>
<button
className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard"
onClick={[Function]}
>
<span
className="euiCard__top"
>
<img
alt=""
className="euiCard__image"
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+"
/>
</span>
<span
className="euiCard__content"
>
<span
className="euiTitle euiTitle--medium euiCard__title"
id="generated-idTitle"
>
Image
</span>
<div
className="euiText euiText--small euiCard__description"
id="generated-idDescription"
>
<p>
A static image
</p>
</div>
</span>
<span
className="euiCard__footer"
>
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#666666",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
graphic
</span>
</span>
</span>
</span>
</button>
</div>
</div>
</div>
`;
exports[`Storyshots components/ElementTypes/ElementGrid with text filter 1`] = `
<div
style={
Object {
@ -577,7 +651,27 @@ exports[`Storyshots components/ElementTypes/ElementGrid with filter 1`] = `
</span>
<span
className="euiCard__footer"
/>
>
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#666666",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
text
</span>
</span>
</span>
</span>
</button>
</div>
</div>
@ -631,7 +725,27 @@ exports[`Storyshots components/ElementTypes/ElementGrid without controls 1`] = `
</span>
<span
className="euiCard__footer"
/>
>
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#666666",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
chart
</span>
</span>
</span>
</span>
</button>
</div>
<div
@ -670,7 +784,27 @@ exports[`Storyshots components/ElementTypes/ElementGrid without controls 1`] = `
</span>
<span
className="euiCard__footer"
/>
>
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#666666",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
graphic
</span>
</span>
</span>
</span>
</button>
</div>
<div
@ -709,7 +843,27 @@ exports[`Storyshots components/ElementTypes/ElementGrid without controls 1`] = `
</span>
<span
className="euiCard__footer"
/>
>
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#666666",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
text
</span>
</span>
</span>
</span>
</button>
</div>
</div>

View file

@ -32,14 +32,25 @@ storiesOf('components/ElementTypes/ElementGrid', module)
onEdit={action('onEdit')}
/>
))
.add('with filter', () => (
<ElementGrid elements={testElements} handleClick={action('addCustomElement')} filter="table" />
.add('with text filter', () => (
<ElementGrid
elements={testElements}
handleClick={action('addCustomElement')}
filterText="table"
/>
))
.add('with tags filter', () => (
<ElementGrid
elements={testElements}
handleClick={action('addCustomElement')}
filterTags={['graphic']}
/>
))
.add('with controls and filter', () => (
<ElementGrid
elements={testCustomElements}
handleClick={action('addCustomElement')}
filter="Lorem"
filterText="Lorem"
showControls
onDelete={action('onDelete')}
onEdit={action('onEdit')}

View file

@ -11,6 +11,7 @@ export const testElements = [
name: 'areaChart',
displayName: 'Area chart',
help: 'A line chart with a filled body',
tags: ['chart'],
image: elasticLogo,
expression: `filters
| demodata
@ -22,6 +23,7 @@ export const testElements = [
name: 'image',
displayName: 'Image',
help: 'A static image',
tags: ['graphic'],
image: elasticLogo,
expression: `image dataurl=null mode="contain"
| render`,
@ -29,6 +31,7 @@ export const testElements = [
{
name: 'table',
displayName: 'Data table',
tags: ['text'],
help: 'A scrollable grid for displaying data in a tabular format',
image: elasticLogo,
expression: `filters
@ -37,6 +40,7 @@ export const testElements = [
| render`,
},
];
export const testCustomElements = [
{
id: 'custom-element-10d625f5-1342-47c9-8f19-d174ea6b65d5',

View file

@ -18,9 +18,13 @@ export interface Props {
*/
elements: Array<ElementSpec | CustomElement>;
/**
* text filter to filter out cards
* text to filter out cards
*/
filter: string;
filterText: string;
/**
* tags to filter out cards
*/
filterTags: string[];
/**
* indicate whether or not edit/delete controls should be displayed
*/
@ -41,31 +45,37 @@ export interface Props {
export const ElementGrid = ({
elements,
filter,
filterText,
filterTags,
handleClick,
onEdit,
onDelete,
showControls,
}: Props) => {
filter = filter.toLowerCase();
filterText = filterText.toLowerCase();
return (
<EuiFlexGrid gutterSize="l" columns={4}>
{map(elements, (element: ElementSpec | CustomElement, index) => {
const { help = '', name, displayName = '', image } = element;
const { name, displayName = '', help = '', image, tags = [] } = element;
const whenClicked = () => handleClick(element);
let textMatch = false;
let tagsMatch = false;
if (
!filter.length ||
name.toLowerCase().includes(filter) ||
displayName.toLowerCase().includes(filter) ||
help.toLowerCase().includes(filter)
!filterText.length ||
name.toLowerCase().includes(filterText) ||
displayName.toLowerCase().includes(filterText) ||
help.toLowerCase().includes(filterText)
) {
textMatch = true;
}
if (!textMatch) {
if (!filterTags.length || filterTags.every(tag => tags.includes(tag))) {
tagsMatch = true;
}
if (!textMatch || !tagsMatch) {
return null;
}
@ -75,6 +85,7 @@ export const ElementGrid = ({
title={displayName || name}
description={help}
image={image}
tags={tags}
onClick={whenClicked}
/>
{showControls && onEdit && onDelete && (
@ -95,5 +106,6 @@ ElementGrid.propTypes = {
ElementGrid.defaultProps = {
showControls: false,
filter: '',
filterTags: [],
filterText: '',
};

View file

@ -7,33 +7,35 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import {
EuiFieldSearch,
EuiFlexGroup,
EuiFlexItem,
EuiModalHeader,
EuiModalBody,
EuiTabbedContent,
EuiEmptyPrompt,
EuiSearchBar,
EuiSpacer,
EuiOverlayMask,
} from '@elastic/eui';
import { map, sortBy } from 'lodash';
import { ConfirmModal } from '../confirm_modal/confirm_modal';
import { CustomElementModal } from '../custom_element_modal';
import { getTagsFilter } from '../../lib/get_tags_filter';
import { extractSearch } from '../../lib/extract_search';
import { ElementGrid } from './element_grid';
const tagType = 'badge';
export class ElementTypes extends Component {
static propTypes = {
addCustomElement: PropTypes.func.isRequired,
addElement: PropTypes.func.isRequired,
customElements: PropTypes.array.isRequired,
elements: PropTypes.object,
filterTags: PropTypes.arrayOf(PropTypes.string).isRequired,
findCustomElements: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
removeCustomElement: PropTypes.func.isRequired,
search: PropTypes.string,
setCustomElements: PropTypes.func.isRequired,
setSearch: PropTypes.func.isRequired,
setFilterTags: PropTypes.func.isRequired,
updateCustomElement: PropTypes.func.isRequired,
};
@ -113,7 +115,14 @@ export class ElementTypes extends Component {
sortBy(map(elements, (element, name) => ({ name, ...element })), 'displayName');
render() {
const { search, setSearch, addElement, addCustomElement } = this.props;
const {
search,
setSearch,
addElement,
addCustomElement,
filterTags,
setFilterTags,
} = this.props;
let { elements, customElements } = this.props;
elements = this._sortElements(elements);
@ -140,6 +149,13 @@ export class ElementTypes extends Component {
);
}
const filters = [getTagsFilter(tagType)];
const onSearch = ({ queryText }) => {
const { searchTerm, filterTags } = extractSearch(queryText);
setSearch(searchTerm);
setFilterTags(filterTags);
};
const tabs = [
{
id: 'elements',
@ -147,7 +163,22 @@ export class ElementTypes extends Component {
content: (
<Fragment>
<EuiSpacer />
<ElementGrid elements={elements} filter={search} handleClick={addElement} />
<EuiSearchBar
defaultQuery={search}
box={{
placeholder: 'Find element',
incremental: true,
}}
filters={filters}
onChange={onSearch}
/>
<EuiSpacer />
<ElementGrid
elements={elements}
filterText={search}
filterTags={filterTags}
handleClick={addElement}
/>
</Fragment>
),
},
@ -156,6 +187,15 @@ export class ElementTypes extends Component {
name: 'My elements',
content: (
<Fragment>
<EuiSpacer />
<EuiSearchBar
defaultQuery={search}
box={{
placeholder: 'Find element',
incremental: true,
}}
onChange={onSearch}
/>
<EuiSpacer />
{customElementContent}
</Fragment>
@ -165,19 +205,7 @@ export class ElementTypes extends Component {
return (
<Fragment>
<EuiModalHeader>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFieldSearch
className="canvasElements__filter"
placeholder="Filter elements"
onChange={e => setSearch(e.target.value)}
value={search}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalHeader>
<EuiModalBody>
<EuiModalBody style={{ paddingRight: '1px' }}>
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} />
</EuiModalBody>

View file

@ -17,10 +17,6 @@ import { insertNodes, addElement } from '../../state/actions/elements';
import { getSelectedPage } from '../../state/selectors/workpad';
import { ElementTypes as Component } from './element_types';
const elementTypesState = withState('search', 'setSearch', '');
const customElementsState = withState('customElements', 'setCustomElements', []);
const elementTypeProps = withProps(() => ({ elements: elementsRegistry.toJS() }));
const mapStateToProps = state => ({ pageId: getSelectedPage(state) });
const mapDispatchToProps = dispatch => ({
@ -91,9 +87,10 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
};
export const ElementTypes = compose(
elementTypesState,
elementTypeProps,
customElementsState,
withState('search', 'setSearch', ''),
withState('customElements', 'setCustomElements', []),
withState('filterTags', 'setFilterTags', []),
withProps(() => ({ elements: elementsRegistry.toJS() })),
connect(
mapStateToProps,
mapDispatchToProps,

View file

@ -0,0 +1,137 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/Tag as badge 1`] = `
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#666666",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag
</span>
</span>
</span>
`;
exports[`Storyshots components/Tag as badge with color 1`] = `
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#327b53",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag
</span>
</span>
</span>
`;
exports[`Storyshots components/Tag as health 1`] = `
<div
className="euiHealth"
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={
Object {
"fill": "#666666",
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<circle
cx="8"
cy="8"
id="dot-a"
r="4"
/>
</defs>
<use
xlinkHref="#dot-a"
/>
</svg>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
tag
</div>
</div>
</div>
`;
exports[`Storyshots components/Tag as health with color 1`] = `
<div
className="euiHealth"
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={
Object {
"fill": "#9b3067",
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<circle
cx="8"
cy="8"
id="dot-a"
r="4"
/>
</defs>
<use
xlinkHref="#dot-a"
/>
</svg>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
tag
</div>
</div>
</div>
`;

View file

@ -0,0 +1,14 @@
/*
* 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 { storiesOf } from '@storybook/react';
import React from 'react';
import { Tag } from '../tag';
storiesOf('components/Tag', module)
.add('as health', () => <Tag name="tag" />)
.add('as health with color', () => <Tag name="tag" color="#9b3067" />)
.add('as badge', () => <Tag name="tag" type="badge" />)
.add('as badge with color', () => <Tag name="tag" type="badge" color="#327b53" />);

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const report = () => ({ name: 'report', color: '#DB1374' });
export { Tag } from './tag';

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import { EuiBadge, EuiHealth } from '@elastic/eui';
interface Props {
/**
* name of the tag
*/
name: string;
/**
* color of the tag
*/
color?: string;
/**
* type of tag to display, i.e. EuiHealth or EuiBadge
*/
type?: 'health' | 'badge';
}
export const Tag: FunctionComponent<Props> = ({
name,
color = '#666666',
type = 'health',
...rest
}) => {
switch (type) {
case 'health':
return (
<EuiHealth color={color} {...rest}>
{name}
</EuiHealth>
);
case 'badge':
return (
<EuiBadge color={color} {...rest}>
{name}
</EuiBadge>
);
}
};
Tag.propTypes = {
name: PropTypes.string.isRequired,
color: PropTypes.string,
type: PropTypes.string,
};

View file

@ -0,0 +1,394 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/TagList empty list 1`] = `null`;
exports[`Storyshots components/TagList with badge tags 1`] = `
Array [
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#cc3b54",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag1
</span>
</span>
</span>,
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#5bc149",
"color": "#000000",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag2
</span>
</span>
</span>,
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#fbc545",
"color": "#000000",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag3
</span>
</span>
</span>,
]
`;
exports[`Storyshots components/TagList with health tags 1`] = `
Array [
<div
className="euiHealth"
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={
Object {
"fill": "#cc3b54",
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<circle
cx="8"
cy="8"
id="dot-a"
r="4"
/>
</defs>
<use
xlinkHref="#dot-a"
/>
</svg>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
tag1
</div>
</div>
</div>,
<div
className="euiHealth"
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={
Object {
"fill": "#9b3067",
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<circle
cx="8"
cy="8"
id="dot-a"
r="4"
/>
</defs>
<use
xlinkHref="#dot-a"
/>
</svg>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
tag4
</div>
</div>
</div>,
<div
className="euiHealth"
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<svg
className="euiIcon euiIcon--medium"
focusable="false"
height="16"
style={
Object {
"fill": "#d41e93",
}
}
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<circle
cx="8"
cy="8"
id="dot-a"
r="4"
/>
</defs>
<use
xlinkHref="#dot-a"
/>
</svg>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
tag6
</div>
</div>
</div>,
]
`;
exports[`Storyshots components/TagList with lots of tags 1`] = `
Array [
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#cc3b54",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag1
</span>
</span>
</span>,
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#5bc149",
"color": "#000000",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag2
</span>
</span>
</span>,
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#fbc545",
"color": "#000000",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag3
</span>
</span>
</span>,
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#9b3067",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag4
</span>
</span>
</span>,
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#1819bd",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag5
</span>
</span>
</span>,
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#d41e93",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag6
</span>
</span>
</span>,
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#3486d2",
"color": "#000000",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag7
</span>
</span>
</span>,
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#b870d8",
"color": "#000000",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag8
</span>
</span>
</span>,
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#f4a4a7",
"color": "#000000",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag9
</span>
</span>
</span>,
<span
className="euiBadge"
style={
Object {
"backgroundColor": "#072d6d",
"color": "#FFFFFF",
}
}
>
<span
className="euiBadge__content"
>
<span
className="euiBadge__text"
>
tag10
</span>
</span>
</span>,
]
`;

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
import { storiesOf } from '@storybook/react';
import React from 'react';
import { TagList } from '../tag_list';
import { TagSpec } from '../../../lib/tag';
const mockTagRegistry: { [tag: string]: TagSpec } = {
tag1: {
name: 'tag1',
color: '#cc3b54',
},
tag2: {
name: 'tag2',
color: '#5bc149',
},
tag3: {
name: 'tag3',
color: '#fbc545',
},
tag4: {
name: 'tag4',
color: '#9b3067',
},
tag5: {
name: 'tag5',
color: '#1819bd',
},
tag6: {
name: 'tag6',
color: '#d41e93',
},
tag7: {
name: 'tag7',
color: '#3486d2',
},
tag8: {
name: 'tag8',
color: '#b870d8',
},
tag9: {
name: 'tag9',
color: '#f4a4a7',
},
tag10: {
name: 'tag10',
color: '#072d6d',
},
};
const getTag = (name: string): TagSpec => mockTagRegistry[name] || { name, color: '#666666' };
storiesOf('components/TagList', module)
.add('empty list', () => <TagList getTag={getTag} />)
.add('with health tags', () => <TagList tags={['tag1', 'tag4', 'tag6']} getTag={getTag} />)
.add('with badge tags', () => (
<TagList tags={['tag1', 'tag2', 'tag3']} getTag={getTag} tagType="badge" />
))
.add('with lots of tags', () => (
<TagList
tags={['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7', 'tag8', 'tag9', 'tag10']}
getTag={getTag}
tagType="badge"
/>
));

View file

@ -0,0 +1,27 @@
/*
* 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 { compose, withProps } from 'recompose';
import { tagsRegistry } from '../../lib/tags_registry';
import { TagList as Component, Props as ComponentProps } from './tag_list';
import { TagSpec } from '../../lib/tag';
interface Props {
/**
* list of tags to display in the list
*/
tags: string[];
/**
* choose EuiHealth or EuiBadge
*/
tagType: 'health' | 'badge';
}
export const TagList = compose<ComponentProps, Props>(
withProps(() => ({
getTag: (tag: string): TagSpec => tagsRegistry.get(tag) || { name: tag, color: undefined },
}))
)(Component);

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import { getId } from '../../lib/get_id';
import { Tag } from '../tag';
import { TagSpec } from '../../lib/tag';
export interface Props {
/**
* list of tags to display in the list
*/
tags?: string[];
/**
* choose EuiHealth or EuiBadge
*/
tagType?: 'health' | 'badge';
/**
* gets the tag from the tag registry
*/
getTag: (tagName: string) => TagSpec;
}
export const TagList: FunctionComponent<Props> = ({ tags = [], tagType = 'health', getTag }) => (
<Fragment>
{tags.length
? tags.map((tag: string) => {
const { color, name } = getTag(tag);
const id = getId('tag');
return <Tag key={id} color={color} name={name} type={tagType} />;
})
: null}
</Fragment>
);
TagList.propTypes = {
tags: PropTypes.array,
tagType: PropTypes.oneOf(['health', 'badge']),
getTag: PropTypes.func.isRequired,
};

View file

@ -57,7 +57,7 @@ export class WorkpadHeader extends React.PureComponent {
onClose={this._hideElementModal}
className="canvasModal--fixedSize"
maxWidth="1000px"
initialFocus=".canvasElements__filter"
initialFocus=".canvasElements__filter input"
>
<ElementTypes onClose={this._hideElementModal} />
<EuiModalFooter>

View file

@ -10,7 +10,6 @@ import * as workpadService from '../../lib/workpad_service';
import { notify } from '../../lib/notify';
import { getId } from '../../lib/get_id';
import { templatesRegistry } from '../../lib/templates_registry';
import { tagsRegistry } from '../../lib/tags_registry';
import { WorkpadTemplates as Component } from './workpad_templates';
export const WorkpadTemplates = compose(
@ -19,7 +18,6 @@ export const WorkpadTemplates = compose(
}),
withProps(() => ({
templates: templatesRegistry.toJS(),
uniqueTags: tagsRegistry.toJS(),
})),
withHandlers({
// Clone workpad given an id

View file

@ -12,13 +12,14 @@ import {
EuiBasicTable,
EuiPagination,
EuiSpacer,
EuiHealth,
EuiButtonEmpty,
EuiSearchBar,
} from '@elastic/eui';
import { get, sortByOrder } from 'lodash';
import { getId } from '../../lib/get_id';
import { sortByOrder } from 'lodash';
import { Paginate } from '../paginate';
import { TagList } from '../tag_list';
import { getTagsFilter } from '../../lib/get_tags_filter';
import { extractSearch } from '../../lib/extract_search';
export class WorkpadTemplates extends React.PureComponent {
static propTypes = {
@ -35,6 +36,8 @@ export class WorkpadTemplates extends React.PureComponent {
filterTags: [],
};
tagType = 'health';
onTableChange = ({ sort = {} }) => {
const { field: sortField, direction: sortDirection } = sort;
this.setState({
@ -43,31 +46,11 @@ export class WorkpadTemplates extends React.PureComponent {
});
};
onSearch = ({ query }) => {
const clauses = get(query, 'ast._clauses', []);
const filterTags = [];
const searchTerms = [];
clauses.forEach(clause => {
const { type, field, value } = clause;
// extract terms from the query AST
if (type === 'term') {
searchTerms.push(value);
}
// extracts tags from the query AST
else if (field === 'tags') {
filterTags.push(value);
}
});
this.setState({ searchTerm: searchTerms.join(' '), filterTags });
};
onSearch = ({ queryText }) => this.setState(extractSearch(queryText));
cloneTemplate = template => this.props.cloneWorkpad(template).then(() => this.props.onClose());
renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }) => {
const { uniqueTags } = this.props;
const { sortField, sortDirection } = this.state;
const columns = [
@ -104,16 +87,7 @@ export class WorkpadTemplates extends React.PureComponent {
sortable: false,
dataType: 'string',
width: '30%',
render: tags => {
if (!tags) {
return 'No tags';
}
return tags.map(tag => (
<EuiHealth key={getId('tag')} color={get(uniqueTags, `${tag}.color`, '#666666')}>
{tag}
</EuiHealth>
));
},
render: tags => <TagList tags={tags} tagType={this.tagType} />,
},
];
@ -146,28 +120,8 @@ export class WorkpadTemplates extends React.PureComponent {
};
renderSearch = () => {
let { uniqueTags } = this.props;
const { searchTerm } = this.state;
uniqueTags = Object.values(uniqueTags);
const filters = [
{
type: 'field_value_selection',
field: 'tags',
name: 'Tags',
multiSelect: true,
options: uniqueTags.map(({ name, color }) => ({
value: name,
name: name,
view: (
<EuiHealth key={getId('tag')} color={color}>
{name}
</EuiHealth>
),
})),
},
];
const filters = [getTagsFilter(this.tagType)];
return (
<EuiSearchBar

View file

@ -25,6 +25,10 @@ export interface CustomElement {
* base 64 data URL string of the preview image
*/
image?: string;
/**
* tags associated with the element
*/
tags?: string[];
/**
* the element object stringified
*/

View file

@ -6,12 +6,15 @@
import { ElementSpec } from '../../canvas_plugin_src/elements/types';
import defaultHeader from './default_header.png';
import { tagsRegistry } from './tags_registry';
export class Element {
/** The name of the Element. This must match the name of the function that is used to create the `type: render` object */
public name: string;
/** A more friendly name for the Element */
public displayName: string;
/** Relevant labels to help identify the elements */
public tags: string[];
/** An image to use in the Element type selector */
public image: string;
/** A sentence or few about what this Element does */
@ -25,7 +28,7 @@ export class Element {
public height?: number;
constructor(config: ElementSpec) {
const { name, image, displayName, expression, filter, help, width, height } = config;
const { name, image, displayName, tags, expression, filter, help, width, height } = config;
this.name = name;
this.displayName = displayName || name;
this.image = image || defaultHeader;
@ -35,6 +38,13 @@ export class Element {
throw new Error('Element types must have a default expression');
}
this.tags = tags || [];
this.tags.forEach(tag => {
if (!tagsRegistry.get(tag)) {
tagsRegistry.register(() => ({ name: tag, color: '#666666' }));
}
});
this.expression = expression;
this.filter = filter;
this.width = width || 500;

View file

@ -0,0 +1,27 @@
/*
* 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.
*/
// EUI helper
// extracts search text and array of selected tags from EuiSearchBar
export const extractSearch = queryText => {
const filterTags = [];
const searchTerms = [];
const parts = queryText.split(' ');
parts.forEach(part => {
if (part.indexOf(':') >= 0) {
const [key, value] = part.split(':');
if (key === 'tag') {
filterTags.push(value);
return;
}
}
searchTerms.push(part);
});
return { searchTerm: searchTerms.join(' '), filterTags };
};

View file

@ -0,0 +1,33 @@
/*
* 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 from 'react';
import { sortBy } from 'lodash';
import { Tag } from '../components/tag';
import { getId } from './get_id';
import { tagsRegistry } from './tags_registry';
// EUI helper function
// generates the FieldValueSelectionFilter object for EuiSearchBar for tag filtering
export const getTagsFilter = (type: 'health' | 'badge') => {
const uniqueTags = sortBy(Object.values(tagsRegistry.toJS()), 'name');
return {
type: 'field_value_selection',
field: 'tag',
name: 'Tags',
multiSelect: true,
options: uniqueTags.map(({ name, color }) => ({
value: name,
name,
view: (
<div>
<Tag key={getId('tag')} color={color} name={name} type={type} />
</div>
),
})),
};
};

View file

@ -0,0 +1,27 @@
/*
* 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.
*/
export interface TagSpec {
/**
* name of the tag
*/
name: string;
/**
* color of the tag
*/
color: string;
}
export class Tag implements TagSpec {
public name: string;
public color: string;
constructor(config: TagSpec) {
this.name = config.name;
this.color = config.color;
}
}
export type TagFactory = () => TagSpec;

View file

@ -5,10 +5,10 @@
*/
import { Registry } from '@kbn/interpreter/common';
import { Tag } from './tag';
import { Tag, TagSpec } from './tag';
class TagRegistry extends Registry {
wrapper(obj) {
class TagRegistry extends Registry<TagSpec, Tag> {
public wrapper(obj: TagSpec) {
return new Tag(obj);
}
}