[Canvas] Move Templates to be stored as Saved Objects (#69438)

* Moves Canvas templates to live server side

* Adds Clone from template test

* Fix url

* Clean up

* PR Feedback

* i18n
This commit is contained in:
Corey Robertson 2020-06-30 14:21:01 -04:00 committed by GitHub
parent 4784686978
commit a9f72bc5e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 5018 additions and 3568 deletions

View file

@ -24,7 +24,6 @@ import { modelSpecs } from './uis/models';
import { initializeViews } from './uis/views';
import { initializeArgs } from './uis/arguments';
import { tagSpecs } from './uis/tags';
import { templateSpecs } from './templates';
interface SetupDeps {
canvas: CanvasSetup;
@ -59,7 +58,6 @@ export class CanvasSrcPlugin implements Plugin<void, void, SetupDeps, StartDeps>
plugins.canvas.addViewUIs(initializeViews(core, plugins));
plugins.canvas.addArgumentUIs(initializeArgs(core, plugins));
plugins.canvas.addTagUIs(tagSpecs);
plugins.canvas.addTemplates(templateSpecs);
plugins.canvas.addTransformUIs(transformSpecs);
}

View file

@ -1,21 +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 { applyTemplateStrings } from '../../i18n/templates';
import darkTemplate from './theme_dark.json';
import lightTemplate from './theme_light.json';
// import pitchTemplate from './pitch_presentation.json';
import statusTemplate from './status_report.json';
import summaryTemplate from './summary_report.json';
// Registry expects a function that returns a spec object
export const templateSpecs = applyTemplateStrings([
darkTemplate,
lightTemplate,
// pitchTemplate,
statusTemplate,
summaryTemplate,
]);

File diff suppressed because one or more lines are too long

View file

@ -1,455 +0,0 @@
{
"name": "Summary",
"id": "workpad-6181471b-147d-4397-a0d3-1c0f1600fa12",
"displayName": "Summary",
"help": "Infographic-style report with live charts",
"tags": ["report"],
"width": 1100,
"height": 2570,
"page": 0,
"pages": [
{
"id": "page-28d2523e-aa4d-4134-8092-b849835b620f",
"style": {
"background": "#FFF"
},
"transition": {},
"elements": [
{
"id": "element-7e937714-3a57-4d41-bcc7-859b2d2db497",
"position": {
"left": -1.375,
"top": -2.5,
"width": 1101.75,
"height": 115,
"angle": 0,
"parent": null
},
"expression": "shape \"square\" fill=\"#69707D\" border=\"rgba(255,255,255,0)\" borderWidth=0 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" containerStyle={containerStyle}"
},
{
"id": "element-8cbe96d4-f555-4891-8f23-ef6cd679d9cf",
"position": {
"left": 31.75,
"top": 1186,
"width": 1034.5,
"height": 421,
"angle": 0,
"parent": null
},
"expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}"
},
{
"id": "element-9c467f5e-3594-41db-8602-ec45e4f3fe8f",
"position": {
"left": 566.25,
"top": 1650,
"width": 500,
"height": 386,
"angle": 0,
"parent": null
},
"expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}"
},
{
"id": "element-a07f8a00-d3da-470c-aea1-b88407900ba5",
"position": {
"left": 30.75,
"top": 1650,
"width": 508.25,
"height": 386,
"angle": 0,
"parent": null
},
"expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}"
},
{
"id": "element-80c70a23-12d9-4282-a68e-5d98ceb5a31f",
"position": {
"left": 31.75,
"top": 2084.5,
"width": 1034.5,
"height": 413,
"angle": 0,
"parent": null
},
"expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}"
},
{
"id": "element-105a0788-e347-4fa0-afff-0a6b80633b80",
"position": {
"left": 31.75,
"top": 707,
"width": 1034.5,
"height": 437,
"angle": 0,
"parent": null
},
"expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}"
},
{
"id": "element-f1d3d480-8aba-48cb-b5f0-2f6a62e64f3a",
"position": {
"left": 566.25,
"top": 158,
"width": 500,
"height": 508.5,
"angle": 0,
"parent": null
},
"expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}"
},
{
"id": "element-58634438-d8c7-4368-8e41-640d858374c3",
"position": {
"left": 31.75,
"top": 158,
"width": 507.25,
"height": 508.5,
"angle": 0,
"parent": null
},
"expression": "shape \"square\" fill=\"rgba(255,255,255,0)\" border=\"rgba(255,255,255,0)\" borderWidth=2 maintainAspect=false\n| render css=\".canvasRenderEl {\n\n}\" \n containerStyle={containerStyle borderRadius=\"6px\" border=\"2px solid #D3DAE6\" backgroundColor=\"rgba(255,255,255,0)\"}"
},
{
"id": "element-9f76c74a-28d9-4ceb-bd7d-b1b34999a11e",
"position": {
"left": 52,
"top": 178,
"width": 500,
"height": 38,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"### Total cost by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-3b6345a5-16ea-4828-beec-425458e758a7",
"position": {
"left": 591.25,
"top": 240,
"width": 455,
"height": 403,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| pointseries x=\"size(project)\" y=\"project\" color=\"project\"\n| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} legend=false seriesStyle={seriesStyle label=\"elasticsearch\" color=\"#882e72\"}\n seriesStyle={seriesStyle label=\"machine-learning\" color=\"#d6c1de\"}\n seriesStyle={seriesStyle label=\"apm\" color=\"#5289c7\"}\n seriesStyle={seriesStyle label=\"kibana\" color=\"#7bafde\"}\n seriesStyle={seriesStyle label=\"beats\" color=\"#b178a6\"}\n seriesStyle={seriesStyle label=\"logstash\" color=\"#1965b0\"}\n seriesStyle={seriesStyle label=\"x-pack\" color=\"#4eb265\"}\n seriesStyle={seriesStyle label=\"swiftype\" color=\"#90c987\"}\n| render \n css=\".flot-y-axis {\n left: 14px !important;\n}\n\n.flot-x-axis>div {\n top: 380px !important;\n}\""
},
{
"id": "element-bdfb3910-5f65-4c24-9bbe-e62feb9e5e11",
"position": {
"left": 585.75,
"top": 178,
"width": 378,
"height": 38,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"### Number of projects by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-161aafca-ba71-43e1-b2a2-dab96a78d717",
"position": {
"left": 53,
"top": 211,
"width": 500,
"height": 38,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"##### Global cost distribution\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-d0c43968-cdcd-4a25-980f-83d6f0adf68e",
"position": {
"left": 586,
"top": 211,
"width": 500,
"height": 38,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"##### Project type distribution\n\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-ea1f3942-066f-4032-a9d0-125072d353d9",
"position": {
"left": 61.75,
"top": 793,
"width": 643,
"height": 300,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| pointseries x=\"project\" y=\"mean(percent_uptime)\" color=\"project\"\n| plot defaultStyle={seriesStyle bars=0.75} legend=false seriesStyle={seriesStyle label=\"elasticsearch\" color=\"#882e72\"}\n seriesStyle={seriesStyle label=\"machine-learning\" color=\"#d6c1de\"}\n seriesStyle={seriesStyle label=\"apm\" color=\"#5289c7\"}\n seriesStyle={seriesStyle label=\"logstash\" color=\"#1965b0\"}\n seriesStyle={seriesStyle label=\"x-pack\" color=\"#4eb265\"}\n seriesStyle={seriesStyle label=\"kibana\" color=\"#7bafde\"}\n seriesStyle={seriesStyle label=\"swiftype\" color=\"#90c987\"}\n seriesStyle={seriesStyle label=\"beats\" color=\"#b178a6\"}\n| render css=\".flot-x-axis>div {\n top: 258px !important;\n}\""
},
{
"id": "element-5a891ee6-5cb8-4b8a-9c01-302ed42e6a8f",
"position": {
"left": 53,
"top": 726,
"width": 500,
"height": 38,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"### Average uptime\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-09713339-044e-4084-b4e4-553dbc939d8a",
"position": {
"left": 729,
"top": 757,
"width": 301,
"height": 38,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"##### Global average uptime\n\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-bd806eff-400b-4816-b728-b28a0390352d",
"position": {
"left": 764,
"top": 833.5,
"width": 200,
"height": 200,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| math \"mean(percent_uptime)\"\n| progress shape=\"wheel\" label={formatnumber \"0%\"} \n font={font size=24 family=\"'Open Sans', Helvetica, Arial, sans-serif\" color=\"#000000\" align=\"center\"} valueColor=\"#4eb265\"\n| render containerStyle={containerStyle}"
},
{
"id": "element-ccd76ddc-2c03-458d-a0eb-09fcd1e2455f",
"position": {
"left": 53,
"top": 1212,
"width": 500,
"height": 38,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"### Average price by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-ef88de44-1629-4a66-abc5-3764b03342e5",
"position": {
"left": 55.5,
"top": 2110,
"width": 500,
"height": 38,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"### Raw data\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-1dbb5050-7b7c-4dd2-ab83-95913d15cc91",
"position": {
"left": 62.75,
"top": 273.75,
"width": 434.625,
"height": 285,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| pointseries color=\"project\" size=\"sum(cost)\"\n| pie hole=50 labels=false legend=\"ne\"\n| render \n css=\"table {\n right: -16px !important;\n}\n\n\ntr {\n height: 36px;\n}\n\n.legendColorBox div {\n margin-right: 7px;\n}\n\n.legendColorBox div div {\n width: 24px !important;\n height: 24px !important;\nborder-width: 4px !important;\n}\n\ntd {\n vertical-align: middle;\n}\" containerStyle={containerStyle overflow=\"visible\"}"
},
{
"id": "element-8ca58ae7-2091-491f-996f-4256dfd5f4e1",
"position": {
"left": 51.875,
"top": 2162,
"width": 994.25,
"height": 300,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| table\n| render containerStyle={containerStyle overflow=\"hidden\"}"
},
{
"id": "element-64db6690-dd39-4591-973d-d880e068de74",
"position": {
"left": 88,
"top": 1259.5,
"width": 902,
"height": 300,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| pointseries x=\"time\" y=\"mean(price)\" color=\"project\"\n| plot defaultStyle={seriesStyle lines=3} \n palette={palette \"#882E72\" \"#B178A6\" \"#D6C1DE\" \"#1965B0\" \"#5289C7\" \"#7BAFDE\" \"#4EB265\" \"#90C987\" \"#CAE0AB\" \"#F7EE55\" \"#F6C141\" \"#F1932D\" \"#E8601C\" \"#DC050C\" gradient=false} legend=\"ne\" seriesStyle={seriesStyle label=\"elasticsearch\" color=\"#882e72\"}\n seriesStyle={seriesStyle color=\"#b178a6\" label=\"beats\"}\n seriesStyle={seriesStyle label=\"machine-learning\" color=\"#d6c1de\"}\n seriesStyle={seriesStyle label=\"logstash\" color=\"#1965b0\"}\n seriesStyle={seriesStyle label=\"apm\" color=\"#5289c7\"}\n seriesStyle={seriesStyle label=\"kibana\" color=\"#7bafde\"}\n seriesStyle={seriesStyle label=\"x-pack\" color=\"#4eb265\"}\n seriesStyle={seriesStyle label=\"swiftype\" color=\"#90c987\"}\n| render containerStyle={containerStyle overflow=\"visible\"} \n css=\".legend table {\n top: 266px !important;\n width: 100%;\n left: 80px;\n}\n\n.legend td {\nvertical-align: middle;\n}\n\ntr {\n padding-left: 14px;\n}\n\n.legendLabel {\n padding-left: 4px;\n}\n\ntbody {\n display: flex;\n}\n\n.flot-x-axis {\n top: 16px !important;\n}\""
},
{
"id": "element-28fdc851-17bf-4a78-84f1-944fbf508d50",
"position": {
"left": 861.25,
"top": 44.75,
"width": 205,
"height": 36,
"angle": 0,
"parent": null
},
"expression": "timefilterControl compact=true column=\"@timestamp\"\n| render css=\".canvasTimePickerPopover__button {\n border: none !important;\n}\"",
"filter": "timefilter from=\"now-14d\" to=now column=@timestamp"
},
{
"id": "element-bf025bbc-7109-45a1-b954-bab851bc80df",
"position": {
"left": 764,
"top": 44.75,
"width": 89,
"height": 25,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"#### Time period\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#FFFFFF\" weight=\"normal\" underline=false italic=false}\n| render css=\"h4 {\n font-weight: 400;\n}\""
},
{
"id": "element-120f58cd-3ef0-40b6-99fd-32cc1480b9aa",
"position": {
"left": 53,
"top": 757,
"width": 500,
"height": 38,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"##### Average uptime by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-c30023e3-5df6-4b54-8286-544811ce7b6a",
"position": {
"left": 51.875,
"top": 1670,
"width": 500,
"height": 38,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"### Total cost by project type\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-137409de-6f24-4234-9c5a-024054d0632a",
"position": {
"left": 593.25,
"top": 1665.5,
"width": 446,
"height": 38,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"### Average price over time\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-b90b71f0-139b-419f-b43b-b2057abf777b",
"position": {
"left": 595.75,
"top": 1698.5,
"width": 223,
"height": 19,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"##### Price trend over time\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-a9b94f64-5336-4e39-ac69-5c9dacfbe129",
"position": {
"left": 53,
"top": 1703.5,
"width": 500,
"height": 38,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"##### State distribution\n\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#000000\" weight=\"normal\" underline=false italic=false}\n| render css=\"\""
},
{
"id": "element-8777dd63-fbe7-446f-a23a-74cf55dc0a7c",
"position": {
"left": 109.75,
"top": 37.75,
"width": 500,
"height": 39,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| markdown \"## Monitoring Elastic projects\" \"\" \n font={font family=\"'Open Sans', Helvetica, Arial, sans-serif\" size=14 align=\"left\" color=\"#FFFFFF\" weight=\"bold\" underline=false italic=false}\n| render css=\".canvasRenderEl {\n\n}\""
},
{
"id": "element-5e85d913-fb4b-41d5-9caf-ca2de9970cc7",
"position": {
"left": 13.75,
"top": 29.8125,
"width": 92,
"height": 54.875,
"angle": 0,
"parent": null
},
"expression": "image dataurl=null mode=\"contain\"\n| render"
},
{
"id": "element-896f3043-4036-45f4-9e84-8aa6d870f215",
"position": {
"left": 53,
"top": 1729,
"width": 417.375,
"height": 290,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| pointseries x=\"sum(cost)\" y=\"project\" color=\"state\"\n| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} legend=\"ne\"\n| render containerStyle={containerStyle overflow=\"visible\"} \n css=\".legend table {\n top: 100px !important;\n right: -46px !important;\n}\n\n.legendColorBox>div{\nmargin-right: 3px !important;\n}\n\n.legend td {\n\nvertical-align: middle;\n}\n\n.legend tr {\n height: 20px;\n}\n\n.flot-x-axis {\n top: -15px !important;\n}\n\n.flot-y-axis {\n left: 10px !important;\n}\""
},
{
"id": "element-13888369-9dac-4948-90b1-0ae42fa8fa53",
"position": {
"left": 593.75,
"top": 1733,
"width": 441,
"height": 282,
"angle": 0,
"parent": null
},
"expression": "filters\n| demodata\n| pointseries x=\"time\" y=\"mean(price)\"\n| plot defaultStyle={seriesStyle bars=0.75} legend=false \n palette={palette \"#882E72\" \"#B178A6\" \"#D6C1DE\" \"#1965B0\" \"#5289C7\" \"#7BAFDE\" \"#4EB265\" \"#90C987\" \"#CAE0AB\" \"#F7EE55\" \"#F6C141\" \"#F1932D\" \"#E8601C\" \"#DC050C\" gradient=false}\n| render \n css=\".flot-x-axis {\n top: -15px !important;\n}\n\n.flot-y-axis {\n left: 10px !important;\n}\""
}
],
"groups": []
}
],
"colors": [
"#37988d",
"#c19628",
"#b83c6f",
"#3f9939",
"#1785b0",
"#ca5f35",
"#45bdb0",
"#f2bc33",
"#e74b8b",
"#4fbf48",
"#1ea6dc",
"#fd7643",
"#72cec3",
"#f5cc5d",
"#ec77a8",
"#7acf74",
"#4cbce4",
"#fd986f",
"#a1ded7",
"#f8dd91",
"#f2a4c5",
"#a6dfa2",
"#86d2ed",
"#fdba9f",
"#000000",
"#444444",
"#777777",
"#BBBBBB",
"#FFFFFF",
"rgba(255,255,255,0)"
],
"@timestamp": "2019-05-31T16:02:40.420Z",
"@created": "2019-05-31T16:01:45.751Z",
"assets": {},
"css": "h3 {\ncolor: #343741;\nfont-weight: 400;\n}\n\nh5 {\ncolor: #69707D;\n}"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -8,6 +8,7 @@ import { SHAREABLE_RUNTIME_NAME } from '../../shareable_runtime/constants_static
export const CANVAS_TYPE = 'canvas-workpad';
export const CUSTOM_ELEMENT_TYPE = 'canvas-element';
export const TEMPLATE_TYPE = `${CANVAS_TYPE}-template`;
export const CANVAS_APP = 'canvas';
export const APP_ROUTE = '/app/canvas';
export const APP_ROUTE_WORKPAD = `${APP_ROUTE}#/workpad`;
@ -16,6 +17,7 @@ export const API_ROUTE_WORKPAD = `${API_ROUTE}/workpad`;
export const API_ROUTE_WORKPAD_ASSETS = `${API_ROUTE}/workpad-assets`;
export const API_ROUTE_WORKPAD_STRUCTURES = `${API_ROUTE}/workpad-structures`;
export const API_ROUTE_CUSTOM_ELEMENT = `${API_ROUTE}/custom-element`;
export const API_ROUTE_TEMPLATES = `${API_ROUTE}/templates`;
export const LOCALSTORAGE_PREFIX = `kibana.canvas`;
export const LOCALSTORAGE_CLIPBOARD = `${LOCALSTORAGE_PREFIX}.clipboard`;
export const SESSIONSTORAGE_LASTPATH = 'lastPath:canvas';

View file

@ -1604,5 +1604,12 @@ export const ComponentStrings = {
i18n.translate('xpack.canvas.workpadTemplate.searchPlaceholder', {
defaultMessage: 'Find template',
}),
getCreatingTemplateLabel: (templateName: string) =>
i18n.translate('xpack.canvas.workpadTemplate.creatingTemplateLabel', {
defaultMessage: `Creating from template '{templateName}'`,
values: {
templateName,
},
}),
},
};

View file

@ -45,6 +45,6 @@ export const applyTemplateStrings = (templates: CanvasTemplate[]) => {
});
}
return () => template;
return template;
});
};

View file

@ -5,13 +5,13 @@
*/
import { getTemplateStrings } from './template_strings';
import { templateSpecs } from '../../canvas_plugin_src/templates';
import { templates } from '../../server/templates'; // eslint-disable-line
import { TagStrings } from '../tags';
describe('TemplateStrings', () => {
const templateStrings = getTemplateStrings();
const templateNames = templateSpecs.map((template) => template().name);
const templateNames = templates.map((template) => template.name);
const stringKeys = Object.keys(templateStrings);
test('All template names should exist in the strings definition', () => {
@ -39,8 +39,8 @@ describe('TemplateStrings', () => {
test('All templates should have tags that are defined', () => {
const tagNames = Object.keys(TagStrings);
templateSpecs.forEach((template) => {
template().tags.forEach((tagName: string) => expect(tagNames).toContain(tagName));
templates.forEach((template) => {
template.tags.forEach((tagName: string) => expect(tagNames).toContain(tagName));
});
});
});

View file

@ -53,9 +53,6 @@ export const getTemplateStrings = (): TemplateStringDict => ({
defaultMessage: 'Infographic-style report with live charts',
}),
},
});
export const getUnusedTemplateStrings = (): TemplateStringDict => ({
Pitch: {
name: i18n.translate('xpack.canvas.templates.pitchName', {
defaultMessage: 'Pitch',

View file

@ -1,57 +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 PropTypes from 'prop-types';
import { compose, withState, withProps, withHandlers, lifecycle } from 'recompose';
import { Paginate as Component } from './paginate';
export const Paginate = compose(
withProps(({ rows, perPage }) => ({
perPage: Number(perPage),
totalPages: Math.ceil(rows.length / (perPage || 10)),
})),
withState('currentPage', 'setPage', ({ startPage, totalPages }) => {
if (totalPages > 0) {
return Math.min(startPage, totalPages - 1);
}
return 0;
}),
withProps(({ rows, totalPages, currentPage, perPage }) => {
const maxPage = totalPages - 1;
const start = currentPage * perPage;
const end = currentPage === 0 ? perPage : perPage * (currentPage + 1);
return {
pageNumber: currentPage,
nextPageEnabled: currentPage < maxPage,
prevPageEnabled: currentPage > 0,
partialRows: rows.slice(start, end),
};
}),
withHandlers({
nextPage: ({ currentPage, nextPageEnabled, setPage }) => () =>
nextPageEnabled && setPage(currentPage + 1),
prevPage: ({ currentPage, prevPageEnabled, setPage }) => () =>
prevPageEnabled && setPage(currentPage - 1),
}),
lifecycle({
componentDidUpdate(prevProps) {
if (prevProps.perPage !== this.props.perPage) {
this.props.setPage(0);
}
},
})
)(Component);
Paginate.propTypes = {
rows: PropTypes.array.isRequired,
perPage: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
startPage: PropTypes.number,
};
Paginate.defaultProps = {
perPage: 10,
startPage: 0,
};

View file

@ -0,0 +1,76 @@
/*
* 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, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { Paginate as Component, PaginateProps, PaginateChildProps } from './paginate';
export { PaginateProps, PaginateChildProps };
export interface InPaginateProps {
perPage?: number;
startPage?: number;
rows: any[];
children: (props: PaginateChildProps) => React.ReactNode;
}
export const Paginate: React.FunctionComponent<InPaginateProps> = ({
perPage = 10,
startPage = 0,
rows,
children,
}) => {
const totalPages = Math.ceil(rows.length / perPage);
const initialCurrentPage = totalPages > 0 ? Math.min(startPage, totalPages - 1) : 0;
const [currentPage, setPage] = useState(initialCurrentPage);
const hasRenderedRef = useRef<boolean>(false);
const maxPage = totalPages - 1;
const start = currentPage * perPage;
const end = currentPage === 0 ? perPage : perPage * (currentPage + 1);
const nextPageEnabled = currentPage < maxPage;
const prevPageEnabled = currentPage > 0;
const partialRows = rows.slice(start, end);
const nextPage = () => {
if (nextPageEnabled) {
setPage(currentPage + 1);
}
};
const prevPage = () => {
if (prevPageEnabled) {
setPage(currentPage - 1);
}
};
useEffect(() => {
if (!hasRenderedRef.current) {
hasRenderedRef.current = true;
} else {
setPage(0);
}
}, [perPage, hasRenderedRef]);
return (
<Component
rows={partialRows}
perPage={perPage}
pageNumber={currentPage}
totalPages={totalPages}
setPage={setPage}
nextPage={nextPage}
prevPage={prevPage}
nextPageEnabled={nextPageEnabled}
prevPageEnabled={prevPageEnabled}
children={children}
/>
);
};
Paginate.propTypes = {
rows: PropTypes.array.isRequired,
perPage: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
startPage: PropTypes.number,
};

View file

@ -4,25 +4,32 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { InPaginateProps } from './';
export const Paginate = (props) => {
return props.children({
rows: props.partialRows,
perPage: props.perPage,
pageNumber: props.pageNumber,
totalPages: props.totalPages,
nextPageEnabled: props.nextPageEnabled,
prevPageEnabled: props.prevPageEnabled,
setPage: (num) => props.setPage(num),
nextPage: props.nextPage,
prevPage: props.prevPage,
});
export type PaginateProps = Omit<InPaginateProps, 'startPage'> & {
pageNumber: number;
totalPages: number;
nextPageEnabled: boolean;
prevPageEnabled: boolean;
setPage: (num: number) => void;
nextPage: () => void;
prevPage: () => void;
};
export type PaginateChildProps = Omit<PaginateProps, 'children'>;
export const Paginate: React.FunctionComponent<PaginateProps> = ({
children,
...childrenProps
}) => {
return <React.Fragment>{children(childrenProps)}</React.Fragment>;
};
Paginate.propTypes = {
children: PropTypes.func.isRequired,
partialRows: PropTypes.array.isRequired,
rows: PropTypes.array.isRequired,
perPage: PropTypes.number.isRequired,
pageNumber: PropTypes.number.isRequired,
totalPages: PropTypes.number.isRequired,

View file

@ -0,0 +1,19 @@
/*
* 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';
// TODO: We should fully build out this interface for our router
// or switch to a different router that is already typed
interface Router {
navigateTo: (
name: string,
params: Record<string, number | string>,
state?: Record<string, string>
) => void;
}
export const RouterContext = React.createContext<Router | undefined>(undefined);

View file

@ -15,6 +15,7 @@ import {
// @ts-expect-error untyped local
import { Router as Component } from './router';
import { State } from '../../../types';
export * from './context';
const mapDispatchToProps = {
enableAutoplay,

View file

@ -10,6 +10,7 @@ import { routerProvider } from '../../lib/router_provider';
import { getAppState } from '../../lib/app_state';
import { getTimeInterval } from '../../lib/time_interval';
import { CanvasLoading } from './canvas_loading';
import { RouterContext } from './';
export class Router extends React.PureComponent {
static propTypes = {
@ -97,6 +98,10 @@ export class Router extends React.PureComponent {
return React.createElement(CanvasLoading, { msg: this.props.loadingMessage });
}
return <this.state.activeComponent />;
return (
<RouterContext.Provider value={this.state.router}>
<this.state.activeComponent />
</RouterContext.Provider>
);
}
}

View file

@ -0,0 +1,566 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/WorkpadTemplates default 1`] = `
<div
style={
Object {
"width": "500px",
}
}
>
<div
className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap"
>
<div
className="euiFlexItem euiSearchBar__searchHolder"
>
<div
className="euiFormControlLayout euiFormControlLayout--fullWidth"
>
<div
className="euiFormControlLayout__childrenWrapper"
>
<input
aria-label="This is a search bar. As you type, the results lower in the page will automatically filter."
className="euiFieldSearch euiFieldSearch--fullWidth"
defaultValue=""
onKeyUp={[Function]}
placeholder="Find template"
type="search"
/>
<div
className="euiFormControlLayoutIcons"
>
<span
className="euiFormControlLayoutCustomIcon"
>
<div
aria-hidden="true"
className="euiFormControlLayoutCustomIcon__icon"
data-euiicon-type="search"
/>
</span>
</div>
</div>
</div>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero euiSearchBar__filtersHolder"
>
<div
className="euiFilterGroup"
>
<div
className="euiPopover euiPopover--anchorDownCenter"
id="field_value_selection_0"
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<button
className="euiButtonEmpty euiButtonEmpty--text euiButtonEmpty--iconRight euiFilterButton euiFilterButton--hasIcon"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<div
aria-hidden="true"
className="euiButtonEmpty__icon"
data-euiicon-type="arrowDown"
size="m"
/>
<span
className="euiButtonEmpty__text"
>
<span
className="euiFilterButton__textShift"
data-text="Tags"
title="Tags"
>
Tags
</span>
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
className="euiSpacer euiSpacer--l"
/>
<div
className="euiBasicTable canvasWorkpad__dropzoneTable canvasWorkpad__dropzoneTable--tags"
>
<div>
<div
className="euiTableHeaderMobile"
>
<div
className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
/>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiTableSortMobile"
>
<div
className="euiPopover euiPopover--anchorDownRight"
onKeyDown={[Function]}
onMouseDown={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
>
<div
className="euiPopover__anchor"
>
<button
className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiButtonEmpty--iconRight euiButtonEmpty--flushRight"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<div
aria-hidden="true"
className="euiButtonEmpty__icon"
data-euiicon-type="arrowDown"
size="m"
/>
<span
className="euiButtonEmpty__text"
>
Sorting
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<table
className="euiTable euiTable--compressed euiTable--responsive"
>
<caption
className="euiScreenReaderOnly euiTableCaption"
/>
<thead>
<tr>
<th
aria-live="polite"
aria-sort="ascending"
className="euiTableHeaderCell"
data-test-subj="tableHeaderCell_name_0"
role="columnheader"
scope="col"
style={
Object {
"width": "30%",
}
}
>
<button
className="euiTableHeaderButton euiTableHeaderButton-isSorted"
data-test-subj="tableHeaderSortButton"
onClick={[Function]}
type="button"
>
<span
className="euiTableCellContent"
>
<span
className="euiTableCellContent__text"
title={
<Component
ariaSortValue="ascending"
/>
}
>
Template name
</span>
<div
aria-label="Sorted in ascending order"
className="euiTableSortIcon"
data-euiicon-type="sortUp"
size="m"
/>
<span
className="euiScreenReaderOnly"
>
Click to sort in descending order
</span>
</span>
</button>
</th>
<th
className="euiTableHeaderCell"
data-test-subj="tableHeaderCell_help_1"
role="columnheader"
scope="col"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableCellContent"
>
<span
className="euiTableCellContent__text"
>
Description
</span>
</div>
</th>
<th
className="euiTableHeaderCell"
data-test-subj="tableHeaderCell_tags_2"
role="columnheader"
scope="col"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableCellContent"
>
<span
className="euiTableCellContent__text"
>
Tags
</span>
</div>
</th>
</tr>
</thead>
<tbody>
<tr
className="euiTableRow"
>
<td
className="euiTableRowCell"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Template name
</div>
<div
className="euiTableCellContent euiTableCellContent--overflowingContent"
>
<button
aria-label="Clone workpad template 'test1'"
className="euiButtonEmpty euiButtonEmpty--primary"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
test1
</span>
</span>
</button>
</div>
</td>
<td
className="euiTableRowCell"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Description
</div>
<div
className="euiTableCellContent"
>
<span
className="euiTableCellContent__text"
>
This is a test template
</span>
</div>
</td>
<td
className="euiTableRowCell"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Tags
</div>
<div
className="euiTableCellContent euiTableCellContent--overflowingContent"
>
<div
className="euiHealth"
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
color="#666666"
data-euiicon-type="dot"
/>
</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"
>
<div
color="#666666"
data-euiicon-type="dot"
/>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
tag2
</div>
</div>
</div>
</div>
</td>
</tr>
<tr
className="euiTableRow"
>
<td
className="euiTableRowCell"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Template name
</div>
<div
className="euiTableCellContent euiTableCellContent--overflowingContent"
>
<button
aria-label="Clone workpad template 'test2'"
className="euiButtonEmpty euiButtonEmpty--primary"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
test2
</span>
</span>
</button>
</div>
</td>
<td
className="euiTableRowCell"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Description
</div>
<div
className="euiTableCellContent"
>
<span
className="euiTableCellContent__text"
>
This is a second test template
</span>
</div>
</td>
<td
className="euiTableRowCell"
style={
Object {
"width": "30%",
}
}
>
<div
className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
>
Tags
</div>
<div
className="euiTableCellContent euiTableCellContent--overflowingContent"
>
<div
className="euiHealth"
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
color="#666666"
data-euiicon-type="dot"
/>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
tag2
</div>
</div>
</div>
<div
className="euiHealth"
>
<div
className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
color="#666666"
data-euiicon-type="dot"
/>
</div>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
tag3
</div>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div
className="euiSpacer euiSpacer--l"
/>
<div
className="euiFlexGroup euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
className="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
className="euiPagination"
role="group"
>
<button
aria-label="Previous page"
className="euiButtonIcon euiButtonIcon--text"
data-test-subj="pagination-button-previous"
disabled={true}
onClick={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="arrowLeft"
size="m"
/>
</button>
<button
aria-label="Page 1 of 1"
className="euiButtonEmpty euiButtonEmpty--text euiButtonEmpty--xSmall euiPaginationButton euiPaginationButton-isActive euiPaginationButton--hideOnMobile"
data-test-subj="pagination-button-0"
onClick={[Function]}
type="button"
>
<span
className="euiButtonEmpty__content"
>
<span
className="euiButtonEmpty__text"
>
1
</span>
</span>
</button>
<button
aria-label="Next page"
className="euiButtonIcon euiButtonIcon--text"
data-test-subj="pagination-button-next"
disabled={true}
onClick={[Function]}
type="button"
>
<div
aria-hidden="true"
className="euiButtonIcon__icon"
data-euiicon-type="arrowRight"
size="m"
/>
</button>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,44 @@
/*
* 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 { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { WorkpadTemplates } from '../workpad_templates';
import { CanvasTemplate } from '../../../../types';
const templates: Record<string, CanvasTemplate> = {
test1: {
id: 'test1-id',
name: 'test1',
help: 'This is a test template',
tags: ['tag1', 'tag2'],
template_key: 'test1-key',
},
test2: {
id: 'test2-id',
name: 'test2',
help: 'This is a second test template',
tags: ['tag2', 'tag3'],
template_key: 'test2-key',
},
};
storiesOf('components/WorkpadTemplates', module)
.addDecorator((story) => <div style={{ width: '500px' }}>{story()}</div>)
.add('default', () => {
const onCreateFromTemplateAction = action('onCreateFromTemplate');
return (
<WorkpadTemplates
templates={templates}
onClose={action('onClose')}
onCreateFromTemplate={(template) => {
onCreateFromTemplateAction(template);
return Promise.resolve();
}}
/>
);
});

View file

@ -1,40 +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 PropTypes from 'prop-types';
import { compose, getContext, withHandlers, withProps } from 'recompose';
import * as workpadService from '../../lib/workpad_service';
import { getId } from '../../lib/get_id';
import { templatesRegistry } from '../../lib/templates_registry';
import { withKibana } from '../../../../../../src/plugins/kibana_react/public';
import { WorkpadTemplates as Component } from './workpad_templates';
export const WorkpadTemplates = compose(
getContext({
router: PropTypes.object,
}),
withProps(() => ({
templates: templatesRegistry.toJS(),
})),
withKibana,
withHandlers(({ kibana }) => ({
// Clone workpad given an id
cloneWorkpad: (props) => (workpad) => {
workpad.id = getId('workpad');
workpad.name = `My Canvas Workpad - ${workpad.name}`;
// Remove unneeded fields
workpad.tags = undefined;
workpad.displayName = undefined;
workpad.help = undefined;
return workpadService
.create(workpad)
.then(() => props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }))
.catch((err) =>
kibana.services.canvas.notify.error(err, { title: `Couldn't clone workpad template` })
);
},
}))
)(Component);

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useContext, useState, useEffect, FunctionComponent } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import { RouterContext } from '../router';
import { ComponentStrings } from '../../../i18n/components';
// @ts-expect-error
import * as workpadService from '../../lib/workpad_service';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { WorkpadTemplates as Component } from './workpad_templates';
import { CanvasTemplate } from '../../../types';
import { UseKibanaProps } from '../../';
import { list } from '../../lib/template_service';
import { applyTemplateStrings } from '../../../i18n/templates/apply_strings';
interface WorkpadTemplatesProps {
onClose: () => void;
}
const Creating: FunctionComponent<{ name: string }> = ({ name }) => (
<div>
<EuiLoadingSpinner size="l" />{' '}
{ComponentStrings.WorkpadTemplates.getCreatingTemplateLabel(name)}
</div>
);
export const WorkpadTemplates: FunctionComponent<WorkpadTemplatesProps> = ({ onClose }) => {
const router = useContext(RouterContext);
const [templates, setTemplates] = useState<CanvasTemplate[] | undefined>(undefined);
const [creatingFromTemplateName, setCreatingFromTemplateName] = useState<string | undefined>(
undefined
);
const kibana = useKibana<UseKibanaProps>();
useEffect(() => {
if (!templates) {
(async () => {
const fetchedTemplates = await list();
setTemplates(applyTemplateStrings(fetchedTemplates));
})();
}
}, [templates]);
let templateProp: Record<string, CanvasTemplate> = {};
if (templates) {
templateProp = templates.reduce<Record<string, any>>((reduction, template) => {
reduction[template.name] = template;
return reduction;
}, {});
}
const createFromTemplate = async (template: CanvasTemplate) => {
setCreatingFromTemplateName(template.name);
try {
const result = await workpadService.createFromTemplate(template.id);
if (router) {
router.navigateTo('loadWorkpad', { id: result.data.id, page: 1 });
}
} catch (error) {
setCreatingFromTemplateName(undefined);
kibana.services.canvas.notify.error(error, {
title: `Couldn't create workpad from template`,
});
}
};
if (creatingFromTemplateName) {
return <Creating name={creatingFromTemplateName} />;
}
return (
<Component
onClose={onClose}
templates={templateProp}
onCreateFromTemplate={createFromTemplate}
/>
);
};

View file

@ -14,63 +14,101 @@ import {
EuiSpacer,
EuiButtonEmpty,
EuiSearchBar,
EuiTableSortingType,
Direction,
SortDirection,
} from '@elastic/eui';
import { sortByOrder } from 'lodash';
import { Paginate } from '../paginate';
// @ts-ignore untyped local
import { EuiBasicTableColumn } from '@elastic/eui';
import { Paginate, PaginateChildProps } from '../paginate';
import { TagList } from '../tag_list';
import { getTagsFilter } from '../../lib/get_tags_filter';
// @ts-ignore untyped local
import { extractSearch } from '../../lib/extract_search';
import { ComponentStrings } from '../../../i18n';
import { CanvasTemplate } from '../../../types';
interface TableChange<T> {
page?: {
index: number;
size: number;
};
sort?: {
field: keyof T;
direction: Direction;
};
}
const { WorkpadTemplates: strings } = ComponentStrings;
export class WorkpadTemplates extends React.PureComponent {
interface WorkpadTemplatesProps {
onCreateFromTemplate: (template: CanvasTemplate) => Promise<void>;
onClose: () => void;
templates: Record<string, CanvasTemplate>;
}
interface WorkpadTemplatesState {
sortField: string;
sortDirection: Direction;
pageSize: number;
searchTerm: string;
filterTags: string[];
}
export class WorkpadTemplates extends React.PureComponent<
WorkpadTemplatesProps,
WorkpadTemplatesState
> {
static propTypes = {
cloneWorkpad: PropTypes.func.isRequired,
createFromTemplate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
templates: PropTypes.object,
uniqueTags: PropTypes.object,
};
state = {
sortField: 'name',
sortDirection: 'asc',
sortDirection: SortDirection.ASC,
pageSize: 10,
searchTerm: '',
filterTags: [],
};
tagType = 'health';
tagType: 'health' = 'health';
onTableChange = ({ sort = {} }) => {
const { field: sortField, direction: sortDirection } = sort;
this.setState({
sortField,
sortDirection,
});
onTableChange = (tableChange: TableChange<CanvasTemplate>) => {
if (tableChange.sort) {
const { field: sortField, direction: sortDirection } = tableChange.sort;
this.setState({
sortField,
sortDirection,
});
}
};
onSearch = ({ queryText }) => this.setState(extractSearch(queryText));
onSearch = ({ queryText = '' }) => this.setState(extractSearch(queryText));
cloneTemplate = (template) => this.props.cloneWorkpad(template).then(() => this.props.onClose());
cloneTemplate = (template: CanvasTemplate) =>
this.props.onCreateFromTemplate(template).then(() => this.props.onClose());
renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }) => {
renderWorkpadTable = ({ rows, pageNumber, totalPages, setPage }: PaginateChildProps) => {
const { sortField, sortDirection } = this.state;
const columns = [
const columns: Array<EuiBasicTableColumn<CanvasTemplate>> = [
{
field: 'name',
name: strings.getTableNameColumnTitle(),
sortable: true,
width: '30%',
dataType: 'string',
render: (name, template) => {
const templateName = name.length ? name : <em>{template.id}</em>;
render: (name: string, template) => {
const templateName = name.length ? name : 'Unnamed Template';
return (
<EuiButtonEmpty
onClick={() => this.cloneTemplate(template)}
aria-label={strings.getCloneTemplateLinkAriaLabel(templateName)}
type="link"
type="button"
>
{templateName}
</EuiButtonEmpty>
@ -90,11 +128,11 @@ export class WorkpadTemplates extends React.PureComponent {
sortable: false,
dataType: 'string',
width: '30%',
render: (tags) => <TagList tags={tags} tagType={this.tagType} />,
render: (tags: string[]) => <TagList tags={tags} tagType={this.tagType} />,
},
];
const sorting = {
const sorting: EuiTableSortingType<any> = {
sort: {
field: sortField,
direction: sortDirection,
@ -162,7 +200,7 @@ export class WorkpadTemplates extends React.PureComponent {
return (
<Paginate rows={filteredTemplates}>
{(pagination) => (
{(pagination: PaginateChildProps) => (
<Fragment>
{this.renderSearch()}
<EuiSpacer />

View file

@ -17,4 +17,8 @@ export interface WithKibanaProps {
};
}
export interface UseKibanaProps {
canvas: CanvasServices;
}
export const plugin = (initializerContext: PluginInitializerContext) => new CanvasPlugin();

View file

@ -6,6 +6,7 @@
import React from 'react';
import { sortBy } from 'lodash';
import { SearchFilterConfig } from '@elastic/eui';
import { Tag } from '../components/tag';
import { getId } from './get_id';
import { tagsRegistry } from './tags_registry';
@ -15,11 +16,12 @@ const { WorkpadTemplates: strings } = ComponentStrings;
// EUI helper function
// generates the FieldValueSelectionFilter object for EuiSearchBar for tag filtering
export const getTagsFilter = (type: 'health' | 'badge') => {
export const getTagsFilter = (type: 'health' | 'badge'): SearchFilterConfig => {
const uniqueTags = sortBy(Object.values(tagsRegistry.toJS()), 'name');
const filterType = 'field_value_selection';
return {
type: 'field_value_selection',
type: filterType,
field: 'tag',
name: strings.getTableTagsColumnTitle(),
multiSelect: true,

View file

@ -0,0 +1,25 @@
/*
* 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 { API_ROUTE_TEMPLATES } from '../../common/lib/constants';
import { fetch } from '../../common/lib/fetch';
import { platformService } from '../services';
import { CanvasTemplate } from '../../types';
const getApiPath = function () {
const basePath = platformService.getService().coreStart.http.basePath.get();
return `${basePath}${API_ROUTE_TEMPLATES}`;
};
interface ListResponse {
templates: CanvasTemplate[];
}
export async function list() {
const templateResponse = await fetch.get<ListResponse>(`${getApiPath()}`);
return templateResponse.data.templates;
}

View file

@ -64,6 +64,12 @@ export function create(workpad) {
});
}
export async function createFromTemplate(templateId) {
return fetch.post(getApiPath(), {
templateId,
});
}
export function get(workpadId) {
return fetch.get(`${getApiPath()}/${workpadId}`).then(({ data: workpad }) => {
// shim old workpads with new properties

View file

@ -21,7 +21,6 @@ export interface CanvasApi {
addModelUIs: AddToRegistry<any>;
addRenderers: AddToRegistry<RendererFactory>;
addTagUIs: AddToRegistry<any>;
addTemplates: AddToRegistry<any>;
addTransformUIs: AddToRegistry<any>;
addTransitions: AddToRegistry<any>;
addTypes: AddToRegistry<() => AnyExpressionTypeDefinition>;
@ -35,7 +34,6 @@ export interface SetupRegistries {
modelUIs: any[];
viewUIs: any[];
argumentUIs: any[];
templates: any[];
tagUIs: any[];
transitions: any[];
}
@ -50,7 +48,6 @@ export function getPluginApi(
modelUIs: [],
viewUIs: [],
argumentUIs: [],
templates: [],
tagUIs: [],
transitions: [],
};
@ -80,7 +77,6 @@ export function getPluginApi(
addModelUIs: (models) => registries.modelUIs.push(...models),
addViewUIs: (views) => registries.viewUIs.push(...views),
addArgumentUIs: (args) => registries.argumentUIs.push(...args),
addTemplates: (templates) => registries.templates.push(...templates),
addTagUIs: (tags) => registries.tagUIs.push(...tags),
addTransitions: (transitions) => registries.transitions.push(...transitions),
};

View file

@ -5,7 +5,7 @@
*/
import { first } from 'rxjs/operators';
import { CoreSetup, PluginInitializerContext, Plugin, Logger } from 'src/core/server';
import { CoreSetup, PluginInitializerContext, Plugin, Logger, CoreStart } from 'src/core/server';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { HomeServerPluginSetup } from 'src/plugins/home/server';
@ -14,7 +14,8 @@ import { initRoutes } from './routes';
import { registerCanvasUsageCollector } from './collectors';
import { loadSampleData } from './sample_data';
import { setupInterpreter } from './setup_interpreter';
import { customElementType, workpadType } from './saved_objects';
import { customElementType, workpadType, workpadTemplateType } from './saved_objects';
import { initializeTemplates } from './templates';
interface PluginsSetup {
expressions: ExpressionsServerSetup;
@ -32,6 +33,7 @@ export class CanvasPlugin implements Plugin {
public async setup(coreSetup: CoreSetup, plugins: PluginsSetup) {
coreSetup.savedObjects.registerType(customElementType);
coreSetup.savedObjects.registerType(workpadType);
coreSetup.savedObjects.registerType(workpadTemplateType);
plugins.features.registerFeature({
id: 'canvas',
@ -81,7 +83,10 @@ export class CanvasPlugin implements Plugin {
setupInterpreter(plugins.expressions);
}
public start() {}
public start(coreStart: CoreStart) {
const client = coreStart.savedObjects.createInternalRepository();
initializeTemplates(client);
}
public stop() {}
}

View file

@ -9,6 +9,7 @@ import { initCustomElementsRoutes } from './custom_elements';
import { initESFieldsRoutes } from './es_fields';
import { initShareablesRoutes } from './shareables';
import { initWorkpadRoutes } from './workpad';
import { initTemplateRoutes } from './templates';
export interface RouteInitializerDeps {
router: IRouter;
@ -20,4 +21,5 @@ export function initRoutes(deps: RouteInitializerDeps) {
initESFieldsRoutes(deps);
initShareablesRoutes(deps);
initWorkpadRoutes(deps);
initTemplateRoutes(deps);
}

View file

@ -0,0 +1,12 @@
/*
* 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 { RouteInitializerDeps } from '../';
import { initializeListTemplates } from './list';
export function initTemplateRoutes(deps: RouteInitializerDeps) {
initializeListTemplates(deps);
}

View file

@ -0,0 +1,103 @@
/*
* 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 { badRequest } from 'boom';
import { initializeListTemplates } from './list';
import {
IRouter,
kibanaResponseFactory,
RequestHandlerContext,
RequestHandler,
} from 'src/core/server';
import {
savedObjectsClientMock,
httpServiceMock,
httpServerMock,
loggingSystemMock,
} from 'src/core/server/mocks';
const mockRouteContext = ({
core: {
savedObjects: {
client: savedObjectsClientMock.create(),
},
},
} as unknown) as RequestHandlerContext;
describe('Find workpad', () => {
let routeHandler: RequestHandler<any, any, any>;
beforeEach(() => {
const httpService = httpServiceMock.createSetupContract();
const router = httpService.createRouter() as jest.Mocked<IRouter>;
initializeListTemplates({
router,
logger: loggingSystemMock.create().get(),
});
routeHandler = router.get.mock.calls[0][1];
});
it(`returns 200 with the found templates`, async () => {
const template1 = { name: 'template1' };
const template2 = { name: 'template2' };
const mockResults = {
total: 2,
saved_objects: [
{ id: 1, attributes: template1 },
{ id: 2, attributes: template2 },
],
};
const findMock = mockRouteContext.core.savedObjects.client.find as jest.Mock;
findMock.mockResolvedValueOnce(mockResults);
const request = httpServerMock.createKibanaRequest({
method: 'get',
path: `api/canvas/templates/list`,
});
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(200);
expect(response.payload).toMatchInlineSnapshot(`
Object {
"templates": Array [
Object {
"name": "template1",
},
Object {
"name": "template2",
},
],
}
`);
});
it(`returns appropriate error on error`, async () => {
(mockRouteContext.core.savedObjects.client.find as jest.Mock).mockImplementationOnce(() => {
throw badRequest('generic error');
});
const request = httpServerMock.createKibanaRequest({
method: 'get',
path: `api/canvas/templates/list`,
});
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(400);
expect(response.payload).toMatchInlineSnapshot(`
Object {
"error": "Bad Request",
"message": "generic error",
"statusCode": 400,
}
`);
});
});

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 { schema } from '@kbn/config-schema';
import { RouteInitializerDeps } from '../';
import { TEMPLATE_TYPE, API_ROUTE_TEMPLATES } from '../../../common/lib/constants';
import { catchErrorHandler } from '../catch_error_handler';
import { CanvasTemplate } from '../../../types';
export function initializeListTemplates(deps: RouteInitializerDeps) {
const { router } = deps;
router.get(
{
path: `${API_ROUTE_TEMPLATES}`,
validate: {
params: schema.object({}),
},
},
catchErrorHandler(async (context, request, response) => {
const savedObjectsClient = context.core.savedObjects.client;
const templates = await savedObjectsClient.find<CanvasTemplate>({
type: TEMPLATE_TYPE,
sortField: 'name.keyword',
sortOrder: 'desc',
search: '*',
searchFields: ['name', 'help'],
fields: ['id', 'name', 'help', 'tags'],
});
return response.ok({
body: {
templates: templates.saved_objects.map((hit) => ({
...hit.attributes,
})),
},
});
})
);
}

View file

@ -15,7 +15,7 @@ import { CANVAS_TYPE } from '../../../common/lib/constants';
import { initializeCreateWorkpadRoute } from './create';
import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server';
const mockRouteContext = ({
let mockRouteContext = ({
core: {
savedObjects: {
client: savedObjectsClientMock.create(),
@ -34,6 +34,14 @@ describe('POST workpad', () => {
let clock: sinon.SinonFakeTimers;
beforeEach(() => {
mockRouteContext = ({
core: {
savedObjects: {
client: savedObjectsClientMock.create(),
},
},
} as unknown) as RequestHandlerContext;
clock = sinon.useFakeTimers(now);
const httpService = httpServiceMock.createSetupContract();
@ -65,7 +73,7 @@ describe('POST workpad', () => {
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(200);
expect(response.payload).toEqual({ ok: true });
expect(response.payload).toEqual({ ok: true, id: `workpad-${mockedUUID}` });
expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith(
CANVAS_TYPE,
{
@ -94,4 +102,45 @@ describe('POST workpad', () => {
expect(response.status).toBe(400);
});
it(`returns 200 when a template is cloned`, async () => {
const cloneFromTemplateBody = {
templateId: 'template-id',
};
const mockTemplateResponse = {
attributes: {
id: 'template-id',
template: {
pages: [],
},
},
};
(mockRouteContext.core.savedObjects.client.get as jest.Mock).mockResolvedValue(
mockTemplateResponse
);
const request = httpServerMock.createKibanaRequest({
method: 'post',
path: 'api/canvas/workpad',
body: cloneFromTemplateBody,
});
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory);
expect(response.status).toBe(200);
expect(response.payload).toEqual({ ok: true, id: `workpad-${mockedUUID}` });
expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith(
CANVAS_TYPE,
{
...mockTemplateResponse.attributes.template,
'@timestamp': nowIso,
'@created': nowIso,
},
{
id: `workpad-${mockedUUID}`,
}
);
});
});

View file

@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { schema } from '@kbn/config-schema';
import { RouteInitializerDeps } from '../';
import { CANVAS_TYPE, API_ROUTE_WORKPAD } from '../../../common/lib/constants';
import { CANVAS_TYPE, API_ROUTE_WORKPAD, TEMPLATE_TYPE } from '../../../common/lib/constants';
import { CanvasWorkpad } from '../../../types';
import { getId } from '../../../common/lib/get_id';
import { WorkpadAttributes } from './workpad_attributes';
@ -13,13 +14,31 @@ import { WorkpadSchema } from './workpad_schema';
import { okResponse } from '../ok_response';
import { catchErrorHandler } from '../catch_error_handler';
interface TemplateAttributes {
template: CanvasWorkpad;
}
const WorkpadFromTemplateSchema = schema.object({
templateId: schema.string(),
});
const createRequestBodySchema = schema.oneOf([WorkpadSchema, WorkpadFromTemplateSchema]);
function isCreateFromTemplate(
maybeCreateFromTemplate: typeof createRequestBodySchema.type
): maybeCreateFromTemplate is typeof WorkpadFromTemplateSchema.type {
return (
(maybeCreateFromTemplate as typeof WorkpadFromTemplateSchema.type).templateId !== undefined
);
}
export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) {
const { router } = deps;
router.post(
{
path: `${API_ROUTE_WORKPAD}`,
validate: {
body: WorkpadSchema,
body: createRequestBodySchema,
},
options: {
body: {
@ -29,14 +48,20 @@ export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) {
},
},
catchErrorHandler(async (context, request, response) => {
if (!request.body) {
return response.badRequest({ body: 'A workpad payload is required' });
let workpad = request.body as CanvasWorkpad;
if (isCreateFromTemplate(request.body)) {
const templateSavedObject = await context.core.savedObjects.client.get<TemplateAttributes>(
TEMPLATE_TYPE,
request.body.templateId
);
workpad = templateSavedObject.attributes.template;
}
const workpad = request.body as CanvasWorkpad;
const now = new Date().toISOString();
const { id, ...payload } = workpad;
const { id: maybeId, ...payload } = workpad;
const id = maybeId ? maybeId : getId('workpad');
await context.core.savedObjects.client.create<WorkpadAttributes>(
CANVAS_TYPE,
@ -45,11 +70,11 @@ export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) {
'@timestamp': now,
'@created': now,
},
{ id: id || getId('workpad') }
{ id }
);
return response.ok({
body: okResponse,
body: { ...okResponse, id },
});
})
);

View file

@ -6,5 +6,6 @@
import { workpadType } from './workpad';
import { customElementType } from './custom_element';
import { workpadTemplateType } from './workpad_template';
export { customElementType, workpadType };
export { customElementType, workpadType, workpadTemplateType };

View file

@ -0,0 +1,55 @@
/*
* 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 { SavedObjectsType } from 'src/core/server';
import { TEMPLATE_TYPE } from '../../common/lib/constants';
export const workpadTemplateType: SavedObjectsType = {
name: TEMPLATE_TYPE,
hidden: false,
namespaceType: 'agnostic',
mappings: {
dynamic: false,
properties: {
name: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
},
},
},
help: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
},
},
},
tags: {
type: 'text',
fields: {
keyword: {
type: 'keyword',
},
},
},
template_key: {
type: 'keyword',
},
},
},
migrations: {},
management: {
importableAndExportable: true,
icon: 'canvasApp',
defaultSearchField: 'name',
getTitle(obj) {
return obj.attributes.name;
},
},
};

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectsRepository } from 'src/core/server';
import { pitch } from './pitch_presentation';
import { status } from './status_report';
import { summary } from './summary_report';
import { dark } from './theme_dark';
import { light } from './theme_light';
import { TEMPLATE_TYPE } from '../../common/lib/constants';
export const templates = [pitch, status, summary, dark, light];
export async function initializeTemplates(
client: Pick<SavedObjectsRepository, 'bulkCreate' | 'find'>
) {
const existingTemplates = await client.find({ type: TEMPLATE_TYPE, perPage: 1 });
if (existingTemplates.total === 0) {
const templateObjects = templates.map((template) => ({
id: template.id,
type: TEMPLATE_TYPE,
attributes: template,
}));
client.bulkCreate(templateObjects);
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,497 @@
/*
* 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 { CanvasTemplate } from '../../types';
export const summary: CanvasTemplate = {
id: 'workpad-template-6181471b-147d-4397-a0d3-1c0f1600fa12',
name: 'Summary',
help: 'Infographic-style report with live charts',
tags: ['report'],
template_key: 'summary-report',
template: {
name: 'Summary',
width: 1100,
height: 2570,
page: 0,
pages: [
{
id: 'page-28d2523e-aa4d-4134-8092-b849835b620f',
style: {
background: '#FFF',
},
transition: {},
elements: [
{
id: 'element-7e937714-3a57-4d41-bcc7-859b2d2db497',
position: {
left: -1.375,
top: -2.5,
width: 1101.75,
height: 115,
angle: 0,
parent: null,
},
expression:
'shape "square" fill="#69707D" border="rgba(255,255,255,0)" borderWidth=0 maintainAspect=false\n| render css=".canvasRenderEl {\n\n}" containerStyle={containerStyle}',
},
{
id: 'element-8cbe96d4-f555-4891-8f23-ef6cd679d9cf',
position: {
left: 31.75,
top: 1186,
width: 1034.5,
height: 421,
angle: 0,
parent: null,
},
expression:
'shape "square" fill="rgba(255,255,255,0)" border="rgba(255,255,255,0)" borderWidth=2 maintainAspect=false\n| render css=".canvasRenderEl {\n\n}" \n containerStyle={containerStyle borderRadius="6px" border="2px solid #D3DAE6" backgroundColor="rgba(255,255,255,0)"}',
},
{
id: 'element-9c467f5e-3594-41db-8602-ec45e4f3fe8f',
position: {
left: 566.25,
top: 1650,
width: 500,
height: 386,
angle: 0,
parent: null,
},
expression:
'shape "square" fill="rgba(255,255,255,0)" border="rgba(255,255,255,0)" borderWidth=2 maintainAspect=false\n| render css=".canvasRenderEl {\n\n}" \n containerStyle={containerStyle borderRadius="6px" border="2px solid #D3DAE6" backgroundColor="rgba(255,255,255,0)"}',
},
{
id: 'element-a07f8a00-d3da-470c-aea1-b88407900ba5',
position: {
left: 30.75,
top: 1650,
width: 508.25,
height: 386,
angle: 0,
parent: null,
},
expression:
'shape "square" fill="rgba(255,255,255,0)" border="rgba(255,255,255,0)" borderWidth=2 maintainAspect=false\n| render css=".canvasRenderEl {\n\n}" \n containerStyle={containerStyle borderRadius="6px" border="2px solid #D3DAE6" backgroundColor="rgba(255,255,255,0)"}',
},
{
id: 'element-80c70a23-12d9-4282-a68e-5d98ceb5a31f',
position: {
left: 31.75,
top: 2084.5,
width: 1034.5,
height: 413,
angle: 0,
parent: null,
},
expression:
'shape "square" fill="rgba(255,255,255,0)" border="rgba(255,255,255,0)" borderWidth=2 maintainAspect=false\n| render css=".canvasRenderEl {\n\n}" \n containerStyle={containerStyle borderRadius="6px" border="2px solid #D3DAE6" backgroundColor="rgba(255,255,255,0)"}',
},
{
id: 'element-105a0788-e347-4fa0-afff-0a6b80633b80',
position: {
left: 31.75,
top: 707,
width: 1034.5,
height: 437,
angle: 0,
parent: null,
},
expression:
'shape "square" fill="rgba(255,255,255,0)" border="rgba(255,255,255,0)" borderWidth=2 maintainAspect=false\n| render css=".canvasRenderEl {\n\n}" \n containerStyle={containerStyle borderRadius="6px" border="2px solid #D3DAE6" backgroundColor="rgba(255,255,255,0)"}',
},
{
id: 'element-f1d3d480-8aba-48cb-b5f0-2f6a62e64f3a',
position: {
left: 566.25,
top: 158,
width: 500,
height: 508.5,
angle: 0,
parent: null,
},
expression:
'shape "square" fill="rgba(255,255,255,0)" border="rgba(255,255,255,0)" borderWidth=2 maintainAspect=false\n| render css=".canvasRenderEl {\n\n}" \n containerStyle={containerStyle borderRadius="6px" border="2px solid #D3DAE6" backgroundColor="rgba(255,255,255,0)"}',
},
{
id: 'element-58634438-d8c7-4368-8e41-640d858374c3',
position: {
left: 31.75,
top: 158,
width: 507.25,
height: 508.5,
angle: 0,
parent: null,
},
expression:
'shape "square" fill="rgba(255,255,255,0)" border="rgba(255,255,255,0)" borderWidth=2 maintainAspect=false\n| render css=".canvasRenderEl {\n\n}" \n containerStyle={containerStyle borderRadius="6px" border="2px solid #D3DAE6" backgroundColor="rgba(255,255,255,0)"}',
},
{
id: 'element-9f76c74a-28d9-4ceb-bd7d-b1b34999a11e',
position: {
left: 52,
top: 178,
width: 500,
height: 38,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "### Total cost by project type" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-3b6345a5-16ea-4828-beec-425458e758a7',
position: {
left: 591.25,
top: 240,
width: 455,
height: 403,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| pointseries x="size(project)" y="project" color="project"\n| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} legend=false seriesStyle={seriesStyle label="elasticsearch" color="#882e72"}\n seriesStyle={seriesStyle label="machine-learning" color="#d6c1de"}\n seriesStyle={seriesStyle label="apm" color="#5289c7"}\n seriesStyle={seriesStyle label="kibana" color="#7bafde"}\n seriesStyle={seriesStyle label="beats" color="#b178a6"}\n seriesStyle={seriesStyle label="logstash" color="#1965b0"}\n seriesStyle={seriesStyle label="x-pack" color="#4eb265"}\n seriesStyle={seriesStyle label="swiftype" color="#90c987"}\n| render \n css=".flot-y-axis {\n left: 14px !important;\n}\n\n.flot-x-axis>div {\n top: 380px !important;\n}"',
},
{
id: 'element-bdfb3910-5f65-4c24-9bbe-e62feb9e5e11',
position: {
left: 585.75,
top: 178,
width: 378,
height: 38,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "### Number of projects by project type" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-161aafca-ba71-43e1-b2a2-dab96a78d717',
position: {
left: 53,
top: 211,
width: 500,
height: 38,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "##### Global cost distribution" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-d0c43968-cdcd-4a25-980f-83d6f0adf68e',
position: {
left: 586,
top: 211,
width: 500,
height: 38,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "##### Project type distribution\n" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-ea1f3942-066f-4032-a9d0-125072d353d9',
position: {
left: 61.75,
top: 793,
width: 643,
height: 300,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| pointseries x="project" y="mean(percent_uptime)" color="project"\n| plot defaultStyle={seriesStyle bars=0.75} legend=false seriesStyle={seriesStyle label="elasticsearch" color="#882e72"}\n seriesStyle={seriesStyle label="machine-learning" color="#d6c1de"}\n seriesStyle={seriesStyle label="apm" color="#5289c7"}\n seriesStyle={seriesStyle label="logstash" color="#1965b0"}\n seriesStyle={seriesStyle label="x-pack" color="#4eb265"}\n seriesStyle={seriesStyle label="kibana" color="#7bafde"}\n seriesStyle={seriesStyle label="swiftype" color="#90c987"}\n seriesStyle={seriesStyle label="beats" color="#b178a6"}\n| render css=".flot-x-axis>div {\n top: 258px !important;\n}"',
},
{
id: 'element-5a891ee6-5cb8-4b8a-9c01-302ed42e6a8f',
position: {
left: 53,
top: 726,
width: 500,
height: 38,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "### Average uptime" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-09713339-044e-4084-b4e4-553dbc939d8a',
position: {
left: 729,
top: 757,
width: 301,
height: 38,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "##### Global average uptime\n" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-bd806eff-400b-4816-b728-b28a0390352d',
position: {
left: 764,
top: 833.5,
width: 200,
height: 200,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font size=24 family="\'Open Sans\', Helvetica, Arial, sans-serif" color="#000000" align="center"} valueColor="#4eb265"\n| render containerStyle={containerStyle}',
},
{
id: 'element-ccd76ddc-2c03-458d-a0eb-09fcd1e2455f',
position: {
left: 53,
top: 1212,
width: 500,
height: 38,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "### Average price by project type" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-ef88de44-1629-4a66-abc5-3764b03342e5',
position: {
left: 55.5,
top: 2110,
width: 500,
height: 38,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "### Raw data" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-1dbb5050-7b7c-4dd2-ab83-95913d15cc91',
position: {
left: 62.75,
top: 273.75,
width: 434.625,
height: 285,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| pointseries color="project" size="sum(cost)"\n| pie hole=50 labels=false legend="ne"\n| render \n css="table {\n right: -16px !important;\n}\n\n\ntr {\n height: 36px;\n}\n\n.legendColorBox div {\n margin-right: 7px;\n}\n\n.legendColorBox div div {\n width: 24px !important;\n height: 24px !important;\nborder-width: 4px !important;\n}\n\ntd {\n vertical-align: middle;\n}" containerStyle={containerStyle overflow="visible"}',
},
{
id: 'element-8ca58ae7-2091-491f-996f-4256dfd5f4e1',
position: {
left: 51.875,
top: 2162,
width: 994.25,
height: 300,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| table\n| render containerStyle={containerStyle overflow="hidden"}',
},
{
id: 'element-64db6690-dd39-4591-973d-d880e068de74',
position: {
left: 88,
top: 1259.5,
width: 902,
height: 300,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| pointseries x="time" y="mean(price)" color="project"\n| plot defaultStyle={seriesStyle lines=3} \n palette={palette "#882E72" "#B178A6" "#D6C1DE" "#1965B0" "#5289C7" "#7BAFDE" "#4EB265" "#90C987" "#CAE0AB" "#F7EE55" "#F6C141" "#F1932D" "#E8601C" "#DC050C" gradient=false} legend="ne" seriesStyle={seriesStyle label="elasticsearch" color="#882e72"}\n seriesStyle={seriesStyle color="#b178a6" label="beats"}\n seriesStyle={seriesStyle label="machine-learning" color="#d6c1de"}\n seriesStyle={seriesStyle label="logstash" color="#1965b0"}\n seriesStyle={seriesStyle label="apm" color="#5289c7"}\n seriesStyle={seriesStyle label="kibana" color="#7bafde"}\n seriesStyle={seriesStyle label="x-pack" color="#4eb265"}\n seriesStyle={seriesStyle label="swiftype" color="#90c987"}\n| render containerStyle={containerStyle overflow="visible"} \n css=".legend table {\n top: 266px !important;\n width: 100%;\n left: 80px;\n}\n\n.legend td {\nvertical-align: middle;\n}\n\ntr {\n padding-left: 14px;\n}\n\n.legendLabel {\n padding-left: 4px;\n}\n\ntbody {\n display: flex;\n}\n\n.flot-x-axis {\n top: 16px !important;\n}"',
},
{
id: 'element-28fdc851-17bf-4a78-84f1-944fbf508d50',
position: {
left: 861.25,
top: 44.75,
width: 205,
height: 36,
angle: 0,
parent: null,
},
expression:
'timefilterControl compact=true column="@timestamp"\n| render css=".canvasTimePickerPopover__button {\n border: none !important;\n}"',
filter: 'timefilter from="now-14d" to=now column=@timestamp',
},
{
id: 'element-bf025bbc-7109-45a1-b954-bab851bc80df',
position: {
left: 764,
top: 44.75,
width: 89,
height: 25,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "#### Time period" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#FFFFFF" weight="normal" underline=false italic=false}\n| render css="h4 {\n font-weight: 400;\n}"',
},
{
id: 'element-120f58cd-3ef0-40b6-99fd-32cc1480b9aa',
position: {
left: 53,
top: 757,
width: 500,
height: 38,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "##### Average uptime by project type" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-c30023e3-5df6-4b54-8286-544811ce7b6a',
position: {
left: 51.875,
top: 1670,
width: 500,
height: 38,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "### Total cost by project type" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-137409de-6f24-4234-9c5a-024054d0632a',
position: {
left: 593.25,
top: 1665.5,
width: 446,
height: 38,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "### Average price over time" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-b90b71f0-139b-419f-b43b-b2057abf777b',
position: {
left: 595.75,
top: 1698.5,
width: 223,
height: 19,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "##### Price trend over time" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-a9b94f64-5336-4e39-ac69-5c9dacfbe129',
position: {
left: 53,
top: 1703.5,
width: 500,
height: 38,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "##### State distribution\n" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=false}\n| render css=""',
},
{
id: 'element-8777dd63-fbe7-446f-a23a-74cf55dc0a7c',
position: {
left: 109.75,
top: 37.75,
width: 500,
height: 39,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| markdown "## Monitoring Elastic projects" "" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#FFFFFF" weight="bold" underline=false italic=false}\n| render css=".canvasRenderEl {\n\n}"',
},
{
id: 'element-5e85d913-fb4b-41d5-9caf-ca2de9970cc7',
position: {
left: 13.75,
top: 29.8125,
width: 92,
height: 54.875,
angle: 0,
parent: null,
},
expression: 'image dataurl=null mode="contain"\n| render',
},
{
id: 'element-896f3043-4036-45f4-9e84-8aa6d870f215',
position: {
left: 53,
top: 1729,
width: 417.375,
height: 290,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| pointseries x="sum(cost)" y="project" color="state"\n| plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} legend="ne"\n| render containerStyle={containerStyle overflow="visible"} \n css=".legend table {\n top: 100px !important;\n right: -46px !important;\n}\n\n.legendColorBox>div{\nmargin-right: 3px !important;\n}\n\n.legend td {\n\nvertical-align: middle;\n}\n\n.legend tr {\n height: 20px;\n}\n\n.flot-x-axis {\n top: -15px !important;\n}\n\n.flot-y-axis {\n left: 10px !important;\n}"',
},
{
id: 'element-13888369-9dac-4948-90b1-0ae42fa8fa53',
position: {
left: 593.75,
top: 1733,
width: 441,
height: 282,
angle: 0,
parent: null,
},
expression:
'filters\n| demodata\n| pointseries x="time" y="mean(price)"\n| plot defaultStyle={seriesStyle bars=0.75} legend=false \n palette={palette "#882E72" "#B178A6" "#D6C1DE" "#1965B0" "#5289C7" "#7BAFDE" "#4EB265" "#90C987" "#CAE0AB" "#F7EE55" "#F6C141" "#F1932D" "#E8601C" "#DC050C" gradient=false}\n| render \n css=".flot-x-axis {\n top: -15px !important;\n}\n\n.flot-y-axis {\n left: 10px !important;\n}"',
},
],
groups: [],
},
],
colors: [
'#37988d',
'#c19628',
'#b83c6f',
'#3f9939',
'#1785b0',
'#ca5f35',
'#45bdb0',
'#f2bc33',
'#e74b8b',
'#4fbf48',
'#1ea6dc',
'#fd7643',
'#72cec3',
'#f5cc5d',
'#ec77a8',
'#7acf74',
'#4cbce4',
'#fd986f',
'#a1ded7',
'#f8dd91',
'#f2a4c5',
'#a6dfa2',
'#86d2ed',
'#fdba9f',
'#000000',
'#444444',
'#777777',
'#BBBBBB',
'#FFFFFF',
'rgba(255,255,255,0)',
],
'@timestamp': '2019-05-31T16:02:40.420Z',
'@created': '2019-05-31T16:01:45.751Z',
assets: {},
css: 'h3 {\ncolor: #343741;\nfont-weight: 400;\n}\n\nh5 {\ncolor: #69707D;\n}',
},
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -52,10 +52,16 @@ export interface CanvasWorkpad {
width: number;
}
export type CanvasTemplate = CanvasWorkpad & {
type CanvasTemplateElement = Omit<CanvasElement, 'filter' | 'type'>;
type CanvasTemplatePage = Omit<CanvasPage, 'elements'> & { elements: CanvasTemplateElement[] };
export interface CanvasTemplate {
id: string;
name: string;
help: string;
tags: string[];
};
template_key: string;
template?: Omit<CanvasWorkpad, 'id' | 'isWriteable' | 'pages'> & { pages: CanvasTemplatePage[] };
}
export interface CanvasWorkpadBoundingBox {
left: number;