From 854a32e4cd16acbe1a7cadf2414e0b6b0ebafc61 Mon Sep 17 00:00:00 2001 From: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> Date: Mon, 18 Mar 2024 23:37:04 +0000 Subject: [PATCH 1/8] Add data_source_name to sample data install Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --- src/core/server/index.ts | 2 ++ src/core/server/saved_objects/import/index.ts | 1 + src/core/server/saved_objects/import/utils.ts | 5 +++- .../data_sets/logs/saved_objects.ts | 2 +- .../services/sample_data/data_sets/util.ts | 28 +++++++++++++++++++ 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/core/server/index.ts b/src/core/server/index.ts index d3df420710e5..713b1494940d 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -322,6 +322,8 @@ export { importSavedObjectsFromStream, resolveSavedObjectsImportErrors, SavedObjectsDeleteByWorkspaceOptions, + updateDataSourceNameInVegaSpec, + extractVegaSpecFromSavedObject, } from './saved_objects'; export { diff --git a/src/core/server/saved_objects/import/index.ts b/src/core/server/saved_objects/import/index.ts index eafab25aebd4..f265f714cac9 100644 --- a/src/core/server/saved_objects/import/index.ts +++ b/src/core/server/saved_objects/import/index.ts @@ -43,3 +43,4 @@ export { SavedObjectsResolveImportErrorsOptions, SavedObjectsImportRetry, } from './types'; +export { updateDataSourceNameInVegaSpec, extractVegaSpecFromSavedObject } from './utils'; diff --git a/src/core/server/saved_objects/import/utils.ts b/src/core/server/saved_objects/import/utils.ts index 9bb1d10cd0eb..133fe8af415d 100644 --- a/src/core/server/saved_objects/import/utils.ts +++ b/src/core/server/saved_objects/import/utils.ts @@ -9,12 +9,14 @@ import { SavedObject, SavedObjectsClientContract } from '../types'; export interface UpdateDataSourceNameInVegaSpecProps { spec: string; newDataSourceName: string; + spacing?: number; } export const updateDataSourceNameInVegaSpec = ( props: UpdateDataSourceNameInVegaSpecProps ): string => { - const { spec } = props; + const { spec, spacing } = props; + const stringifiedSpacing = !!spacing ? spacing : 2; let parsedSpec = parseJSONSpec(spec); const isJSONString = !!parsedSpec; @@ -39,6 +41,7 @@ export const updateDataSourceNameInVegaSpec = ( : stringify(parsedSpec, { bracesSameLine: true, keepWsc: true, + space: stringifiedSpacing, }); }; diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts index 943c3599a2cb..d2699128abd5 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts @@ -174,7 +174,7 @@ export const getSavedObjects = (): SavedObject[] => [ defaultMessage: '[Logs] Source and Destination Sankey Chart', }), visState: - '{"title":"[Logs] Source and Destination Sankey Chart","type":"vega","params":{"spec":"{ \\n $schema: https://vega.github.io/schema/vega/v5.json\\n data: [\\n\\t{\\n \\t// query OpenSearch based on the currently selected time range and filter string\\n \\tname: rawData\\n \\turl: {\\n \\t%context%: true\\n \\t%timefield%: timestamp\\n \\tindex: opensearch_dashboards_sample_data_logs\\n \\tbody: {\\n \\tsize: 0\\n \\taggs: {\\n \\ttable: {\\n \\tcomposite: {\\n \\tsize: 10000\\n \\tsources: [\\n \\t{\\n \\tstk1: {\\n \\tterms: {field: \\"geo.src\\"}\\n \\t}\\n \\t}\\n \\t{\\n \\tstk2: {\\n \\tterms: {field: \\"geo.dest\\"}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t// From the result, take just the data we are interested in\\n \\tformat: {property: \\"aggregations.table.buckets\\"}\\n \\t// Convert key.stk1 -> stk1 for simpler access below\\n \\ttransform: [\\n \\t{type: \\"formula\\", expr: \\"datum.key.stk1\\", as: \\"stk1\\"}\\n \\t{type: \\"formula\\", expr: \\"datum.key.stk2\\", as: \\"stk2\\"}\\n \\t{type: \\"formula\\", expr: \\"datum.doc_count\\", as: \\"size\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: nodes\\n \\tsource: rawData\\n \\ttransform: [\\n \\t// when a country is selected, filter out unrelated data\\n \\t{\\n \\ttype: filter\\n \\texpr: !groupSelector || groupSelector.stk1 == datum.stk1 || groupSelector.stk2 == datum.stk2\\n \\t}\\n \\t// Set new key for later lookups - identifies each node\\n \\t{type: \\"formula\\", expr: \\"datum.stk1+datum.stk2\\", as: \\"key\\"}\\n \\t// instead of each table row, create two new rows,\\n \\t// one for the source (stack=stk1) and one for destination node (stack=stk2).\\n \\t// The country code stored in stk1 and stk2 fields is placed into grpId field.\\n \\t{\\n \\ttype: fold\\n \\tfields: [\\"stk1\\", \\"stk2\\"]\\n \\tas: [\\"stack\\", \\"grpId\\"]\\n \\t}\\n \\t// Create a sortkey, different for stk1 and stk2 stacks.\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.stack == \'stk1\' ? datum.stk1+datum.stk2 : datum.stk2+datum.stk1\\n \\tas: sortField\\n \\t}\\n \\t// Calculate y0 and y1 positions for stacking nodes one on top of the other,\\n \\t// independently for each stack, and ensuring they are in the proper order,\\n \\t// alphabetical from the top (reversed on the y axis)\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\"stack\\"]\\n \\tsort: {field: \\"sortField\\", order: \\"descending\\"}\\n \\tfield: size\\n \\t}\\n \\t// calculate vertical center point for each node, used to draw edges\\n \\t{type: \\"formula\\", expr: \\"(datum.y0+datum.y1)/2\\", as: \\"yc\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: groups\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// combine all nodes into country groups, summing up the doc counts\\n \\t{\\n \\ttype: aggregate\\n \\tgroupby: [\\"stack\\", \\"grpId\\"]\\n \\tfields: [\\"size\\"]\\n \\tops: [\\"sum\\"]\\n \\tas: [\\"total\\"]\\n \\t}\\n \\t// re-calculate the stacking y0,y1 values\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\"stack\\"]\\n \\tsort: {field: \\"grpId\\", order: \\"descending\\"}\\n \\tfield: total\\n \\t}\\n \\t// project y0 and y1 values to screen coordinates\\n \\t// doing it once here instead of doing it several times in marks\\n \\t{type: \\"formula\\", expr: \\"scale(\'y\', datum.y0)\\", as: \\"scaledY0\\"}\\n \\t{type: \\"formula\\", expr: \\"scale(\'y\', datum.y1)\\", as: \\"scaledY1\\"}\\n \\t// boolean flag if the label should be on the right of the stack\\n \\t{type: \\"formula\\", expr: \\"datum.stack == \'stk1\'\\", as: \\"rightLabel\\"}\\n \\t// Calculate traffic percentage for this country using \\"y\\" scale\\n \\t// domain upper bound, which represents the total traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.total/domain(\'y\')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n\\t{\\n \\t// This is a temp lookup table with all the \'stk2\' stack nodes\\n \\tname: destinationNodes\\n \\tsource: nodes\\n \\ttransform: [\\n \\t{type: \\"filter\\", expr: \\"datum.stack == \'stk2\'\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: edges\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// we only want nodes from the left stack\\n \\t{type: \\"filter\\", expr: \\"datum.stack == \'stk1\'\\"}\\n \\t// find corresponding node from the right stack, keep it as \\"target\\"\\n \\t{\\n \\ttype: lookup\\n \\tfrom: destinationNodes\\n \\tkey: key\\n \\tfields: [\\"key\\"]\\n \\tas: [\\"target\\"]\\n \\t}\\n \\t// calculate SVG link path between stk1 and stk2 stacks for the node pair\\n \\t{\\n \\ttype: linkpath\\n \\torient: horizontal\\n \\tshape: diagonal\\n \\tsourceY: {expr: \\"scale(\'y\', datum.yc)\\"}\\n \\tsourceX: {expr: \\"scale(\'x\', \'stk1\') + bandwidth(\'x\')\\"}\\n \\ttargetY: {expr: \\"scale(\'y\', datum.target.yc)\\"}\\n \\ttargetX: {expr: \\"scale(\'x\', \'stk2\')\\"}\\n \\t}\\n \\t// A little trick to calculate the thickness of the line.\\n \\t// The value needs to be the same as the hight of the node, but scaling\\n \\t// size to screen\'s height gives inversed value because screen\'s Y\\n \\t// coordinate goes from the top to the bottom, whereas the graph\'s Y=0\\n \\t// is at the bottom. So subtracting scaled doc count from screen height\\n \\t// (which is the \\"lower\\" bound of the \\"y\\" scale) gives us the right value\\n \\t{\\n \\ttype: formula\\n \\texpr: range(\'y\')[0]-scale(\'y\', datum.size)\\n \\tas: strokeWidth\\n \\t}\\n \\t// Tooltip needs individual link\'s percentage of all traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.size/domain(\'y\')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n ]\\n scales: [\\n\\t{\\n \\t// calculates horizontal stack positioning\\n \\tname: x\\n \\ttype: band\\n \\trange: width\\n \\tdomain: [\\"stk1\\", \\"stk2\\"]\\n \\tpaddingOuter: 0.05\\n \\tpaddingInner: 0.95\\n\\t}\\n\\t{\\n \\t// this scale goes up as high as the highest y1 value of all nodes\\n \\tname: y\\n \\ttype: linear\\n \\trange: height\\n \\tdomain: {data: \\"nodes\\", field: \\"y1\\"}\\n\\t}\\n\\t{\\n \\t// use rawData to ensure the colors stay the same when clicking.\\n \\tname: color\\n \\ttype: ordinal\\n \\trange: category\\n \\tdomain: {data: \\"rawData\\", field: \\"stk1\\"}\\n\\t}\\n\\t{\\n \\t// this scale is used to map internal ids (stk1, stk2) to stack names\\n \\tname: stackNames\\n \\ttype: ordinal\\n \\trange: [\\"Source\\", \\"Destination\\"]\\n \\tdomain: [\\"stk1\\", \\"stk2\\"]\\n\\t}\\n ]\\n axes: [\\n\\t{\\n \\t// x axis should use custom label formatting to print proper stack names\\n \\torient: bottom\\n \\tscale: x\\n \\tencode: {\\n \\tlabels: {\\n \\tupdate: {\\n \\ttext: {scale: \\"stackNames\\", field: \\"value\\"}\\n \\t}\\n \\t}\\n \\t}\\n\\t}\\n\\t{orient: \\"left\\", scale: \\"y\\"}\\n ]\\n marks: [\\n\\t{\\n \\t// draw the connecting line between stacks\\n \\ttype: path\\n \\tname: edgeMark\\n \\tfrom: {data: \\"edges\\"}\\n \\t// this prevents some autosizing issues with large strokeWidth for paths\\n \\tclip: true\\n \\tencode: {\\n \\tupdate: {\\n \\t// By default use color of the left node, except when showing traffic\\n \\t// from just one country, in which case use destination color.\\n \\tstroke: [\\n \\t{\\n \\ttest: groupSelector && groupSelector.stack==\'stk1\'\\n \\tscale: color\\n \\tfield: stk2\\n \\t}\\n \\t{scale: \\"color\\", field: \\"stk1\\"}\\n \\t]\\n \\tstrokeWidth: {field: \\"strokeWidth\\"}\\n \\tpath: {field: \\"path\\"}\\n \\t// when showing all traffic, and hovering over a country,\\n \\t// highlight the traffic from that country.\\n \\tstrokeOpacity: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 0.9 : 0.3\\n \\t}\\n \\t// Ensure that the hover-selected edges show on top\\n \\tzindex: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 1 : 0\\n \\t}\\n \\t// format tooltip string\\n \\ttooltip: {\\n \\tsignal: datum.stk1 + \' → \' + datum.stk2 + \'\\t\' + format(datum.size, \',.0f\') + \' (\' + format(datum.percentage, \'.1%\') + \')\'\\n \\t}\\n \\t}\\n \\t// Simple mouseover highlighting of a single line\\n \\thover: {\\n \\tstrokeOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw stack groups (countries)\\n \\ttype: rect\\n \\tname: groupMark\\n \\tfrom: {data: \\"groups\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tfill: {scale: \\"color\\", field: \\"grpId\\"}\\n \\twidth: {scale: \\"x\\", band: 1}\\n \\t}\\n \\tupdate: {\\n \\tx: {scale: \\"x\\", field: \\"stack\\"}\\n \\ty: {field: \\"scaledY0\\"}\\n \\ty2: {field: \\"scaledY1\\"}\\n \\tfillOpacity: {value: 0.6}\\n \\ttooltip: {\\n \\tsignal: datum.grpId + \' \' + format(datum.total, \',.0f\') + \' (\' + format(datum.percentage, \'.1%\') + \')\'\\n \\t}\\n \\t}\\n \\thover: {\\n \\tfillOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw country code labels on the inner side of the stack\\n \\ttype: text\\n \\tfrom: {data: \\"groups\\"}\\n \\t// don\'t process events for the labels - otherwise line mouseover is unclean\\n \\tinteractive: false\\n \\tencode: {\\n \\tupdate: {\\n \\t// depending on which stack it is, position x with some padding\\n \\tx: {\\n \\tsignal: scale(\'x\', datum.stack) + (datum.rightLabel ? bandwidth(\'x\') + 8 : -8)\\n \\t}\\n \\t// middle of the group\\n \\tyc: {signal: \\"(datum.scaledY0 + datum.scaledY1)/2\\"}\\n \\talign: {signal: \\"datum.rightLabel ? \'left\' : \'right\'\\"}\\n \\tbaseline: {value: \\"middle\\"}\\n \\tfontWeight: {value: \\"bold\\"}\\n \\t// only show text label if the group\'s height is large enough\\n \\ttext: {signal: \\"abs(datum.scaledY0-datum.scaledY1) > 13 ? datum.grpId : \'\'\\"}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// Create a \\"show all\\" button. Shown only when a country is selected.\\n \\ttype: group\\n \\tdata: [\\n \\t// We need to make the button show only when groupSelector signal is true.\\n \\t// Each mark is drawn as many times as there are elements in the backing data.\\n \\t// Which means that if values list is empty, it will not be drawn.\\n \\t// Here I create a data source with one empty object, and filter that list\\n \\t// based on the signal value. This can only be done in a group.\\n \\t{\\n \\tname: dataForShowAll\\n \\tvalues: [{}]\\n \\ttransform: [{type: \\"filter\\", expr: \\"groupSelector\\"}]\\n \\t}\\n \\t]\\n \\t// Set button size and positioning\\n \\tencode: {\\n \\tenter: {\\n \\txc: {signal: \\"width/2\\"}\\n \\ty: {value: 30}\\n \\twidth: {value: 80}\\n \\theight: {value: 30}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\t// This group is shown as a button with rounded corners.\\n \\ttype: group\\n \\t// mark name allows signal capturing\\n \\tname: groupReset\\n \\t// Only shows button if dataForShowAll has values.\\n \\tfrom: {data: \\"dataForShowAll\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tcornerRadius: {value: 6}\\n \\tfill: {value: \\"#F5F7FA\\"}\\n \\tstroke: {value: \\"#c1c1c1\\"}\\n \\tstrokeWidth: {value: 2}\\n \\t// use parent group\'s size\\n \\theight: {\\n \\tfield: {group: \\"height\\"}\\n \\t}\\n \\twidth: {\\n \\tfield: {group: \\"width\\"}\\n \\t}\\n \\t}\\n \\tupdate: {\\n \\t// groups are transparent by default\\n \\topacity: {value: 1}\\n \\t}\\n \\thover: {\\n \\topacity: {value: 0.7}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\ttype: text\\n \\t// if true, it will prevent clicking on the button when over text.\\n \\tinteractive: false\\n \\tencode: {\\n \\tenter: {\\n \\t// center text in the paren group\\n \\txc: {\\n \\tfield: {group: \\"width\\"}\\n \\tmult: 0.5\\n \\t}\\n \\tyc: {\\n \\tfield: {group: \\"height\\"}\\n \\tmult: 0.5\\n \\toffset: 2\\n \\t}\\n \\talign: {value: \\"center\\"}\\n \\tbaseline: {value: \\"middle\\"}\\n \\tfontWeight: {value: \\"bold\\"}\\n \\ttext: {value: \\"Show All\\"}\\n \\t}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t]\\n\\t}\\n ]\\n signals: [\\n\\t{\\n \\t// used to highlight traffic to/from the same country\\n \\tname: groupHover\\n \\tvalue: {}\\n \\ton: [\\n \\t{\\n \\tevents: @groupMark:mouseover\\n \\tupdate: \\"{stk1:datum.stack==\'stk1\' && datum.grpId, stk2:datum.stack==\'stk2\' && datum.grpId}\\"\\n \\t}\\n \\t{events: \\"mouseout\\", update: \\"{}\\"}\\n \\t]\\n\\t}\\n\\t// used to filter only the data related to the selected country\\n\\t{\\n \\tname: groupSelector\\n \\tvalue: false\\n \\ton: [\\n \\t{\\n \\t// Clicking groupMark sets this signal to the filter values\\n \\tevents: @groupMark:click!\\n \\tupdate: \\"{stack:datum.stack, stk1:datum.stack==\'stk1\' && datum.grpId, stk2:datum.stack==\'stk2\' && datum.grpId}\\"\\n \\t}\\n \\t{\\n \\t// Clicking \\"show all\\" button, or double-clicking anywhere resets it\\n \\tevents: [\\n \\t{type: \\"click\\", markname: \\"groupReset\\"}\\n \\t{type: \\"dblclick\\"}\\n \\t]\\n \\tupdate: \\"false\\"\\n \\t}\\n \\t]\\n\\t}\\n ]\\n}\\n"},"aggs":[]}', + '{"title":"[Logs] Source and Destination Sankey Chart","type":"vega","params":{"spec":"{ \\n $schema: https://vega.github.io/schema/vega/v5.json\\n data: [\\n\\t{\\n \\t// query OpenSearch based on the currently selected time range and filter string\\n \\tname: rawData\\n \\turl: {\\n \\t%context%: true\\n \\t%timefield%: timestamp\\n \\tindex: opensearch_dashboards_sample_data_logs\\n \\tbody: {\\n \\tsize: 0\\n \\taggs: {\\n \\ttable: {\\n \\tcomposite: {\\n \\tsize: 10000\\n \\tsources: [\\n \\t{\\n \\tstk1: {\\n \\tterms: {field: \\"geo.src\\"}\\n \\t}\\n \\t}\\n \\t{\\n \\tstk2: {\\n \\tterms: {field: \\"geo.dest\\"}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t}\\n \\t// From the result, take just the data we are interested in\\n \\tformat: {property: \\"aggregations.table.buckets\\"}\\n \\t// Convert key.stk1 -> stk1 for simpler access below\\n \\ttransform: [\\n \\t{type: \\"formula\\", expr: \\"datum.key.stk1\\", as: \\"stk1\\"}\\n \\t{type: \\"formula\\", expr: \\"datum.key.stk2\\", as: \\"stk2\\"}\\n \\t{type: \\"formula\\", expr: \\"datum.doc_count\\", as: \\"size\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: nodes\\n \\tsource: rawData\\n \\ttransform: [\\n \\t// when a country is selected, filter out unrelated data\\n \\t{\\n \\ttype: filter\\n \\texpr: !groupSelector || groupSelector.stk1 == datum.stk1 || groupSelector.stk2 == datum.stk2\\n \\t}\\n \\t// Set new key for later lookups - identifies each node\\n \\t{type: \\"formula\\", expr: \\"datum.stk1+datum.stk2\\", as: \\"key\\"}\\n \\t// instead of each table row, create two new rows,\\n \\t// one for the source (stack=stk1) and one for destination node (stack=stk2).\\n \\t// The country code stored in stk1 and stk2 fields is placed into grpId field.\\n \\t{\\n \\ttype: fold\\n \\tfields: [\\"stk1\\", \\"stk2\\"]\\n \\tas: [\\"stack\\", \\"grpId\\"]\\n \\t}\\n \\t// Create a sortkey, different for stk1 and stk2 stacks.\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.stack == \'stk1\' ? datum.stk1+datum.stk2 : datum.stk2+datum.stk1\\n \\tas: sortField\\n \\t}\\n \\t// Calculate y0 and y1 positions for stacking nodes one on top of the other,\\n \\t// independently for each stack, and ensuring they are in the proper order,\\n \\t// alphabetical from the top (reversed on the y axis)\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\"stack\\"]\\n \\tsort: {field: \\"sortField\\", order: \\"descending\\"}\\n \\tfield: size\\n \\t}\\n \\t// calculate vertical center point for each node, used to draw edges\\n \\t{type: \\"formula\\", expr: \\"(datum.y0+datum.y1)/2\\", as: \\"yc\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: groups\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// combine all nodes into country groups, summing up the doc counts\\n \\t{\\n \\ttype: aggregate\\n \\tgroupby: [\\"stack\\", \\"grpId\\"]\\n \\tfields: [\\"size\\"]\\n \\tops: [\\"sum\\"]\\n \\tas: [\\"total\\"]\\n \\t}\\n \\t// re-calculate the stacking y0,y1 values\\n \\t{\\n \\ttype: stack\\n \\tgroupby: [\\"stack\\"]\\n \\tsort: {field: \\"grpId\\", order: \\"descending\\"}\\n \\tfield: total\\n \\t}\\n \\t// project y0 and y1 values to screen coordinates\\n \\t// doing it once here instead of doing it several times in marks\\n \\t{type: \\"formula\\", expr: \\"scale(\'y\', datum.y0)\\", as: \\"scaledY0\\"}\\n \\t{type: \\"formula\\", expr: \\"scale(\'y\', datum.y1)\\", as: \\"scaledY1\\"}\\n \\t// boolean flag if the label should be on the right of the stack\\n \\t{type: \\"formula\\", expr: \\"datum.stack == \'stk1\'\\", as: \\"rightLabel\\"}\\n \\t// Calculate traffic percentage for this country using \\"y\\" scale\\n \\t// domain upper bound, which represents the total traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.total/domain(\'y\')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n\\t{\\n \\t// This is a temp lookup table with all the \'stk2\' stack nodes\\n \\tname: destinationNodes\\n \\tsource: nodes\\n \\ttransform: [\\n \\t{type: \\"filter\\", expr: \\"datum.stack == \'stk2\'\\"}\\n \\t]\\n\\t}\\n\\t{\\n \\tname: edges\\n \\tsource: nodes\\n \\ttransform: [\\n \\t// we only want nodes from the left stack\\n \\t{type: \\"filter\\", expr: \\"datum.stack == \'stk1\'\\"}\\n \\t// find corresponding node from the right stack, keep it as \\"target\\"\\n \\t{\\n \\ttype: lookup\\n \\tfrom: destinationNodes\\n \\tkey: key\\n \\tfields: [\\"key\\"]\\n \\tas: [\\"target\\"]\\n \\t}\\n \\t// calculate SVG link path between stk1 and stk2 stacks for the node pair\\n \\t{\\n \\ttype: linkpath\\n \\torient: horizontal\\n \\tshape: diagonal\\n \\tsourceY: {expr: \\"scale(\'y\', datum.yc)\\"}\\n \\tsourceX: {expr: \\"scale(\'x\', \'stk1\') + bandwidth(\'x\')\\"}\\n \\ttargetY: {expr: \\"scale(\'y\', datum.target.yc)\\"}\\n \\ttargetX: {expr: \\"scale(\'x\', \'stk2\')\\"}\\n \\t}\\n \\t// A little trick to calculate the thickness of the line.\\n \\t// The value needs to be the same as the hight of the node, but scaling\\n \\t// size to screen\'s height gives inversed value because screen\'s Y\\n \\t// coordinate goes from the top to the bottom, whereas the graph\'s Y=0\\n \\t// is at the bottom. So subtracting scaled doc count from screen height\\n \\t// (which is the \\"lower\\" bound of the \\"y\\" scale) gives us the right value\\n \\t{\\n \\ttype: formula\\n \\texpr: range(\'y\')[0]-scale(\'y\', datum.size)\\n \\tas: strokeWidth\\n \\t}\\n \\t// Tooltip needs individual link\'s percentage of all traffic\\n \\t{\\n \\ttype: formula\\n \\texpr: datum.size/domain(\'y\')[1]\\n \\tas: percentage\\n \\t}\\n \\t]\\n\\t}\\n ]\\n scales: [\\n\\t{\\n \\t// calculates horizontal stack positioning\\n \\tname: x\\n \\ttype: band\\n \\trange: width\\n \\tdomain: [\\"stk1\\", \\"stk2\\"]\\n \\tpaddingOuter: 0.05\\n \\tpaddingInner: 0.95\\n\\t}\\n\\t{\\n \\t// this scale goes up as high as the highest y1 value of all nodes\\n \\tname: y\\n \\ttype: linear\\n \\trange: height\\n \\tdomain: {data: \\"nodes\\", field: \\"y1\\"}\\n\\t}\\n\\t{\\n \\t// use rawData to ensure the colors stay the same when clicking.\\n \\tname: color\\n \\ttype: ordinal\\n \\trange: category\\n \\tdomain: {data: \\"rawData\\", field: \\"stk1\\"}\\n\\t}\\n\\t{\\n \\t// this scale is used to map internal ids (stk1, stk2) to stack names\\n \\tname: stackNames\\n \\ttype: ordinal\\n \\trange: [\\"Source\\", \\"Destination\\"]\\n \\tdomain: [\\"stk1\\", \\"stk2\\"]\\n\\t}\\n ]\\n axes: [\\n\\t{\\n \\t// x axis should use custom label formatting to print proper stack names\\n \\torient: bottom\\n \\tscale: x\\n \\tencode: {\\n \\tlabels: {\\n \\tupdate: {\\n \\ttext: {scale: \\"stackNames\\", field: \\"value\\"}\\n \\t}\\n \\t}\\n \\t}\\n\\t}\\n\\t{orient: \\"left\\", scale: \\"y\\"}\\n ]\\n marks: [\\n\\t{\\n \\t// draw the connecting line between stacks\\n \\ttype: path\\n \\tname: edgeMark\\n \\tfrom: {data: \\"edges\\"}\\n \\t// this prevents some autosizing issues with large strokeWidth for paths\\n \\tclip: true\\n \\tencode: {\\n \\tupdate: {\\n \\t// By default use color of the left node, except when showing traffic\\n \\t// from just one country, in which case use destination color.\\n \\tstroke: [\\n \\t{\\n \\ttest: groupSelector && groupSelector.stack==\'stk1\'\\n \\tscale: color\\n \\tfield: stk2\\n \\t}\\n \\t{scale: \\"color\\", field: \\"stk1\\"}\\n \\t]\\n \\tstrokeWidth: {field: \\"strokeWidth\\"}\\n \\tpath: {field: \\"path\\"}\\n \\t// when showing all traffic, and hovering over a country,\\n \\t// highlight the traffic from that country.\\n \\tstrokeOpacity: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 0.9 : 0.3\\n \\t}\\n \\t// Ensure that the hover-selected edges show on top\\n \\tzindex: {\\n \\tsignal: !groupSelector && (groupHover.stk1 == datum.stk1 || groupHover.stk2 == datum.stk2) ? 1 : 0\\n \\t}\\n \\t// format tooltip string\\n \\ttooltip: {\\n \\tsignal: datum.stk1 + \\" → \\" + datum.stk2 + \\"\\t\\" + format(datum.size, \\",.0f\\") + \\" (\\" + format(datum.percentage, \\".1%\\") + \\")\\"\\n \\t}\\n \\t}\\n \\t// Simple mouseover highlighting of a single line\\n \\thover: {\\n \\tstrokeOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw stack groups (countries)\\n \\ttype: rect\\n \\tname: groupMark\\n \\tfrom: {data: \\"groups\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tfill: {scale: \\"color\\", field: \\"grpId\\"}\\n \\twidth: {scale: \\"x\\", band: 1}\\n \\t}\\n \\tupdate: {\\n \\tx: {scale: \\"x\\", field: \\"stack\\"}\\n \\ty: {field: \\"scaledY0\\"}\\n \\ty2: {field: \\"scaledY1\\"}\\n \\tfillOpacity: {value: 0.6}\\n \\ttooltip: {\\n \\tsignal: datum.grpId + \' \' + format(datum.total, \',.0f\') + \' (\' + format(datum.percentage, \'.1%\') + \')\'\\n \\t}\\n \\t}\\n \\thover: {\\n \\tfillOpacity: {value: 1}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// draw country code labels on the inner side of the stack\\n \\ttype: text\\n \\tfrom: {data: \\"groups\\"}\\n \\t// don\'t process events for the labels - otherwise line mouseover is unclean\\n \\tinteractive: false\\n \\tencode: {\\n \\tupdate: {\\n \\t// depending on which stack it is, position x with some padding\\n \\tx: {\\n \\tsignal: scale(\'x\', datum.stack) + (datum.rightLabel ? bandwidth(\'x\') + 8 : -8)\\n \\t}\\n \\t// middle of the group\\n \\tyc: {signal: \\"(datum.scaledY0 + datum.scaledY1)/2\\"}\\n \\talign: {signal: \\"datum.rightLabel ? \'left\' : \'right\'\\"}\\n \\tbaseline: {value: \\"middle\\"}\\n \\tfontWeight: {value: \\"bold\\"}\\n \\t// only show text label if the group\'s height is large enough\\n \\ttext: {signal: \\"abs(datum.scaledY0-datum.scaledY1) > 13 ? datum.grpId : \'\'\\"}\\n \\t}\\n \\t}\\n\\t}\\n\\t{\\n \\t// Create a \\"show all\\" button. Shown only when a country is selected.\\n \\ttype: group\\n \\tdata: [\\n \\t// We need to make the button show only when groupSelector signal is true.\\n \\t// Each mark is drawn as many times as there are elements in the backing data.\\n \\t// Which means that if values list is empty, it will not be drawn.\\n \\t// Here I create a data source with one empty object, and filter that list\\n \\t// based on the signal value. This can only be done in a group.\\n \\t{\\n \\tname: dataForShowAll\\n \\tvalues: [{}]\\n \\ttransform: [{type: \\"filter\\", expr: \\"groupSelector\\"}]\\n \\t}\\n \\t]\\n \\t// Set button size and positioning\\n \\tencode: {\\n \\tenter: {\\n \\txc: {signal: \\"width/2\\"}\\n \\ty: {value: 30}\\n \\twidth: {value: 80}\\n \\theight: {value: 30}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\t// This group is shown as a button with rounded corners.\\n \\ttype: group\\n \\t// mark name allows signal capturing\\n \\tname: groupReset\\n \\t// Only shows button if dataForShowAll has values.\\n \\tfrom: {data: \\"dataForShowAll\\"}\\n \\tencode: {\\n \\tenter: {\\n \\tcornerRadius: {value: 6}\\n \\tfill: {value: \\"#F5F7FA\\"}\\n \\tstroke: {value: \\"#c1c1c1\\"}\\n \\tstrokeWidth: {value: 2}\\n \\t// use parent group\'s size\\n \\theight: {\\n \\tfield: {group: \\"height\\"}\\n \\t}\\n \\twidth: {\\n \\tfield: {group: \\"width\\"}\\n \\t}\\n \\t}\\n \\tupdate: {\\n \\t// groups are transparent by default\\n \\topacity: {value: 1}\\n \\t}\\n \\thover: {\\n \\topacity: {value: 0.7}\\n \\t}\\n \\t}\\n \\tmarks: [\\n \\t{\\n \\ttype: text\\n \\t// if true, it will prevent clicking on the button when over text.\\n \\tinteractive: false\\n \\tencode: {\\n \\tenter: {\\n \\t// center text in the paren group\\n \\txc: {\\n \\tfield: {group: \\"width\\"}\\n \\tmult: 0.5\\n \\t}\\n \\tyc: {\\n \\tfield: {group: \\"height\\"}\\n \\tmult: 0.5\\n \\toffset: 2\\n \\t}\\n \\talign: {value: \\"center\\"}\\n \\tbaseline: {value: \\"middle\\"}\\n \\tfontWeight: {value: \\"bold\\"}\\n \\ttext: {value: \\"Show All\\"}\\n \\t}\\n \\t}\\n \\t}\\n \\t]\\n \\t}\\n \\t]\\n\\t}\\n ]\\n signals: [\\n\\t{\\n \\t// used to highlight traffic to/from the same country\\n \\tname: groupHover\\n \\tvalue: {}\\n \\ton: [\\n \\t{\\n \\tevents: @groupMark:mouseover\\n \\tupdate: \\"{stk1:datum.stack==\'stk1\' && datum.grpId, stk2:datum.stack==\'stk2\' && datum.grpId}\\"\\n \\t}\\n \\t{events: \\"mouseout\\", update: \\"{}\\"}\\n \\t]\\n\\t}\\n\\t// used to filter only the data related to the selected country\\n\\t{\\n \\tname: groupSelector\\n \\tvalue: false\\n \\ton: [\\n \\t{\\n \\t// Clicking groupMark sets this signal to the filter values\\n \\tevents: @groupMark:click!\\n \\tupdate: \\"{stack:datum.stack, stk1:datum.stack==\'stk1\' && datum.grpId, stk2:datum.stack==\'stk2\' && datum.grpId}\\"\\n \\t}\\n \\t{\\n \\t// Clicking \\"show all\\" button, or double-clicking anywhere resets it\\n \\tevents: [\\n \\t{type: \\"click\\", markname: \\"groupReset\\"}\\n \\t{type: \\"dblclick\\"}\\n \\t]\\n \\tupdate: \\"false\\"\\n \\t}\\n \\t]\\n\\t}\\n ]\\n}\\n"},"aggs":[]}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/home/server/services/sample_data/data_sets/util.ts b/src/plugins/home/server/services/sample_data/data_sets/util.ts index 46022f1c22d3..d84a4d0b2f20 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/util.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/util.ts @@ -4,6 +4,10 @@ */ import { SavedObject } from 'opensearch-dashboards/server'; +import { + extractVegaSpecFromSavedObject, + updateDataSourceNameInVegaSpec, +} from '../../../../../../core/server'; export const appendDataSourceId = (id: string) => { return (dataSourceId?: string) => (dataSourceId ? `${dataSourceId}_` + id : id); @@ -74,6 +78,30 @@ export const getSavedObjectsWithDataSource = ( ) { saveObject.attributes.title = saveObject.attributes.title + `_${dataSourceTitle}`; } + + if (saveObject.type === 'visualization') { + const vegaSpec = extractVegaSpecFromSavedObject(saveObject); + + if (!!vegaSpec) { + const updatedVegaSpec = updateDataSourceNameInVegaSpec({ + spec: vegaSpec, + newDataSourceName: dataSourceTitle, + spacing: 1, + }); + + // @ts-expect-error + const visStateObject = JSON.parse(saveObject.attributes?.visState); + visStateObject.params.spec = updatedVegaSpec; + + // @ts-expect-error + saveObject.attributes.visState = JSON.stringify(visStateObject); + saveObject.references.push({ + id: `${dataSourceId}`, + type: 'data-source', + name: 'dataSource', + }); + } + } } return saveObject; From 001d499786a89740cd997af8a149ffd688bb46d6 Mon Sep 17 00:00:00 2001 From: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:12:13 +0000 Subject: [PATCH 2/8] Add unit test coverage Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --- .../sample_data/data_sets/util.test.ts | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/plugins/home/server/services/sample_data/data_sets/util.test.ts diff --git a/src/plugins/home/server/services/sample_data/data_sets/util.test.ts b/src/plugins/home/server/services/sample_data/data_sets/util.test.ts new file mode 100644 index 000000000000..ba02f770d582 --- /dev/null +++ b/src/plugins/home/server/services/sample_data/data_sets/util.test.ts @@ -0,0 +1,137 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getSavedObjectsWithDataSource } from './util'; +import { SavedObject, updateDataSourceNameInVegaSpec } from '../../../../../../core/server'; + +describe('getSavedObjectsWithDataSource()', () => { + const getVisualizationSavedObjects = (): Array> => { + return [ + { + id: '935afa20-e0cd-11e7-9d07-1398ccfcefa3', + type: 'visualization', + updated_at: '2018-08-29T13:22:17.617Z', + version: '1', + migrationVersion: {}, + attributes: { + title: '[Logs] Heatmap', + visState: + '{"title":"[Logs] Heatmap","type":"heatmap","params":{"type":"heatmap","addTooltip":true,"addLegend":true,"enableHover":true,"legendPosition":"right","times":[],"colorsNumber":10,"colorSchema":"Reds","setColorRange":false,"colorsRange":[],"invertColors":false,"percentageMode":false,"valueAxes":[{"show":false,"id":"ValueAxis-1","type":"value","scale":{"type":"linear","defaultYExtents":false},"labels":{"show":false,"rotate":0,"color":"#555","overwriteColor":false}}]},"aggs":[{"id":"1","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"clientip"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"geo.src","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Country Source"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"hour_of_day","size":25,"order":"asc","orderBy":"_key","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Hour of Day"}}]}', + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"90943e30-9a47-11e8-b64d-95841ca0b247","filter":[],"query":{"query":"","language":"kuery"}}', + }, + }, + references: [], + }, + { + id: 'some-id', + type: 'visualization', + references: [], + attributes: { + title: 'Some non-OpenSearch query Vega visualization', + description: 'This is a sample Vega visualization without an OpenSearch query.', + kibanaSavedObjectMeta: {}, + visState: + '{"title":"Vega Visualization without OpenSearch Query","type":"vega","params":{"spec":"{\\"$schema\\":\\"https://vega.github.io/schema/vega-lite/v5.json\\",\\"description\\":\\"A simple scatter plot.\\",\\"data\\":{\\"values\\":[{\\"x\\":1,\\"y\\":2},{\\"x\\":2,\\"y\\":3},{\\"x\\":3,\\"y\\":4},{\\"x\\":4,\\"y\\":5},{\\"x\\":5,\\"y\\":6},{\\"x\\":6,\\"y\\":7},{\\"x\\":7,\\"y\\":8},{\\"x\\":8,\\"y\\":9},{\\"x\\":9,\\"y\\":10}],\\"mark\\":\\"point\\",\\"encoding\\":{\\"x\\":{\\"field\\":\\"x\\",\\"type\\":\\"quantitative\\"},\\"y\\":{\\"field\\":\\"y\\",\\"type\\":\\"quantitative\\"}}},\\"width\\":400,\\"height\\":200}"},"aggs":[]}', + uiStateJSON: '{}', + version: 1, + }, + }, + { + attributes: { + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', + }, + title: '(Vega) Top destination count', + uiStateJSON: '{}', + version: 1, + visState: + '{"title":"(Vega) Top destination count","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega-lite/v5.json\\n data: {\\n url: {\\n %context%: true\\n %timefield%: @timestamp\\n index: opensearch_dashboards_sample_data_logs\\n body: {\\n aggs: {\\n 1: {\\n terms: {\\n field: geo.dest\\n order: {\\n _count: desc\\n }\\n size: 5\\n }\\n }\\n }\\n }\\n }\\n format: {\\n property: aggregations.1.buckets\\n }\\n }\\n transform: [\\n {\\n calculate: datum.key\\n as: dest\\n }\\n {\\n calculate: datum.doc_count\\n as: count\\n }\\n ]\\n layer: [\\n {\\n mark: {\\n type: bar\\n tooltip: true\\n }\\n }\\n ]\\n encoding: {\\n x: {\\n field: count\\n type: quantitative\\n axis: {\\n title: Count\\n }\\n }\\n y: {\\n field: dest\\n type: nominal\\n axis: {\\n title: Dest\\n }\\n sort: -x\\n }\\n }\\n}"}}', + }, + id: 'f0d162c0-227b-11ee-b88b-47a93b5c527c', + migrationVersion: { visualization: '7.10.0' }, + references: [], + type: 'visualization', + updated_at: '2023-07-25T19:39:50.773Z', + version: 'WzgxLDFd', + }, + { + id: '06cf9c40-9ee8-11e7-8711-e7a007dcef99', + type: 'visualization', + updated_at: '2018-08-29T13:22:17.617Z', + version: '1', + migrationVersion: {}, + attributes: { + title: '[Logs] Visitors Map', + visState: + '{"title":"[Logs] Visitors Map","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 30, longitude: -120, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: opensearch_dashboards_sample_data_logs\\n %context%: true\\n %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\"geo.coordinates\\", precision: 5, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\"geo.coordinates\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.gridSplit.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n {\\n name: bubbleColor\\n type: linear\\n domain: {\\n data: table\\n field: doc_count\\n }\\n range: [\\"rgb(255, 255, 255)\\",\\"rgb(249, 212, 204)\\",\\"rgb(238, 170, 156)\\", \\"rgb(223, 129, 110)\\"]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n fill: {\\n scale: bubbleColor\\n field: doc_count\\n }\\n size: {scale: \\"gridSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n tooltip: {\\n signal: \\"{flights: datum.doc_count}\\"\\n }\\n }\\n }\\n }\\n ]\\n}"}}', + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"90943e30-9a47-11e8-b64d-95841ca0b247","filter":[],"query":{"query":"","language":"kuery"}}', + }, + }, + references: [], + }, + ]; + }; + + test('when processing Vega Visualization saved objects, it should attach data_source_name to each OpenSearch query', () => { + const dataSourceId = 'some-datasource-id'; + const dataSourceName = 'Data Source Name'; + const expectedUpdatedFields = getVisualizationSavedObjects().map((object) => { + const visState = JSON.parse(object.attributes.visState); + if (visState.type !== 'vega') { + return { + vegaSpec: undefined, + references: object.references, + }; + } + const spec = visState.params.spec; + return { + vegaSpec: updateDataSourceNameInVegaSpec({ + newDataSourceName: dataSourceName, + spec, + spacing: 1, + }), + references: [ + { + id: dataSourceId, + type: 'data-source', + name: 'dataSource', + }, + ], + }; + }); + const updatedVegaVisualizationsFields = getSavedObjectsWithDataSource( + getVisualizationSavedObjects(), + dataSourceId, + dataSourceName + ).map((object) => { + // @ts-expect-error + const visState = JSON.parse(object.attributes.visState); + if (visState.type !== 'vega') { + return { + vegaSpec: undefined, + references: object.references, + }; + } + const spec = visState.params.spec; + return { + vegaSpec: spec, + references: object.references, + }; + }); + + expect(updatedVegaVisualizationsFields).toEqual(expect.arrayContaining(expectedUpdatedFields)); + }); +}); From 32b05754d937c50bf3e96ce0475eb8b9559bbbb5 Mon Sep 17 00:00:00 2001 From: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:24:08 +0000 Subject: [PATCH 3/8] Resolve CHANGELOG.md conflicts Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c18a0f81d40f..89299cd00193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,7 +71,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [Workspace] Add create workspace page ([#6179](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6179)) - [Multiple Datasource] Make sure customer always have a default datasource ([#6237](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6237)) - [Workspace] Add workspace list page ([#6182](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6182)) - +- [Multiple Datasource] Add multi data source support to sample vega visualizations ([#6218](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6218)) ### 🐛 Bug Fixes From 09dbbdfae02755aa57cc43141b8302f7a1e4e856 Mon Sep 17 00:00:00 2001 From: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:12:40 +0000 Subject: [PATCH 4/8] Refactor logic Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --- src/core/server/saved_objects/import/utils.ts | 2 +- .../test_utils/visualization_objects.json | 73 +++++++++++++++++ .../sample_data/data_sets/util.test.ts | 78 +------------------ 3 files changed, 77 insertions(+), 76 deletions(-) create mode 100644 src/plugins/home/server/services/sample_data/data_sets/test_utils/visualization_objects.json diff --git a/src/core/server/saved_objects/import/utils.ts b/src/core/server/saved_objects/import/utils.ts index 133fe8af415d..c9c3fc2e6927 100644 --- a/src/core/server/saved_objects/import/utils.ts +++ b/src/core/server/saved_objects/import/utils.ts @@ -16,7 +16,7 @@ export const updateDataSourceNameInVegaSpec = ( props: UpdateDataSourceNameInVegaSpecProps ): string => { const { spec, spacing } = props; - const stringifiedSpacing = !!spacing ? spacing : 2; + const stringifiedSpacing = spacing || 2; let parsedSpec = parseJSONSpec(spec); const isJSONString = !!parsedSpec; diff --git a/src/plugins/home/server/services/sample_data/data_sets/test_utils/visualization_objects.json b/src/plugins/home/server/services/sample_data/data_sets/test_utils/visualization_objects.json new file mode 100644 index 000000000000..eb4080e0671c --- /dev/null +++ b/src/plugins/home/server/services/sample_data/data_sets/test_utils/visualization_objects.json @@ -0,0 +1,73 @@ +{ + "saved_objects": [ + { + "id": "935afa20-e0cd-11e7-9d07-1398ccfcefa3", + "type": "visualization", + "updated_at": "2018-08-29T13:22:17.617Z", + "version": "1", + "migrationVersion": {}, + "attributes": { + "title": "[Logs] Heatmap", + "visState": "{\"title\":\"[Logs] Heatmap\",\"type\":\"heatmap\",\"params\":{\"type\":\"heatmap\",\"addTooltip\":true,\"addLegend\":true,\"enableHover\":true,\"legendPosition\":\"right\",\"times\":[],\"colorsNumber\":10,\"colorSchema\":\"Reds\",\"setColorRange\":false,\"colorsRange\":[],\"invertColors\":false,\"percentageMode\":false,\"valueAxes\":[{\"show\":false,\"id\":\"ValueAxis-1\",\"type\":\"value\",\"scale\":{\"type\":\"linear\",\"defaultYExtents\":false},\"labels\":{\"show\":false,\"rotate\":0,\"color\":\"#555\",\"overwriteColor\":false}}]},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"clientip\"}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Country Source\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"hour_of_day\",\"size\":25,\"order\":\"asc\",\"orderBy\":\"_key\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Hour of Day\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"kuery\"}}" + } + }, + "references": [] + }, + { + "id": "some-id", + "type": "visualization", + "references": [], + "attributes": { + "title": "Some non-OpenSearch query Vega visualization", + "description": "This is a sample Vega visualization without an OpenSearch query.", + "kibanaSavedObjectMeta": {}, + "visState": "{\"title\":\"Vega Visualization without OpenSearch Query\",\"type\":\"vega\",\"params\":{\"spec\":\"{\\\"$schema\\\":\\\"https://vega.github.io/schema/vega-lite/v5.json\\\",\\\"description\\\":\\\"A simple scatter plot.\\\",\\\"data\\\":{\\\"values\\\":[{\\\"x\\\":1,\\\"y\\\":2},{\\\"x\\\":2,\\\"y\\\":3},{\\\"x\\\":3,\\\"y\\\":4},{\\\"x\\\":4,\\\"y\\\":5},{\\\"x\\\":5,\\\"y\\\":6},{\\\"x\\\":6,\\\"y\\\":7},{\\\"x\\\":7,\\\"y\\\":8},{\\\"x\\\":8,\\\"y\\\":9},{\\\"x\\\":9,\\\"y\\\":10}],\\\"mark\\\":\\\"point\\\",\\\"encoding\\\":{\\\"x\\\":{\\\"field\\\":\\\"x\\\",\\\"type\\\":\\\"quantitative\\\"},\\\"y\\\":{\\\"field\\\":\\\"y\\\",\\\"type\\\":\\\"quantitative\\\"}}},\\\"width\\\":400,\\\"height\\\":200}\"},\"aggs\":[]}", + "uiStateJSON": "{}", + "version": 1 + } + }, + { + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}" + }, + "title": "(Vega) Top destination count", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"(Vega) Top destination count\",\"type\":\"vega\",\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega-lite/v5.json\\n data: {\\n url: {\\n %context%: true\\n %timefield%: @timestamp\\n index: opensearch_dashboards_sample_data_logs\\n body: {\\n aggs: {\\n 1: {\\n terms: {\\n field: geo.dest\\n order: {\\n _count: desc\\n }\\n size: 5\\n }\\n }\\n }\\n }\\n }\\n format: {\\n property: aggregations.1.buckets\\n }\\n }\\n transform: [\\n {\\n calculate: datum.key\\n as: dest\\n }\\n {\\n calculate: datum.doc_count\\n as: count\\n }\\n ]\\n layer: [\\n {\\n mark: {\\n type: bar\\n tooltip: true\\n }\\n }\\n ]\\n encoding: {\\n x: {\\n field: count\\n type: quantitative\\n axis: {\\n title: Count\\n }\\n }\\n y: {\\n field: dest\\n type: nominal\\n axis: {\\n title: Dest\\n }\\n sort: -x\\n }\\n }\\n}\"}}" + }, + "id": "f0d162c0-227b-11ee-b88b-47a93b5c527c", + "migrationVersion": { + "visualization": "7.10.0" + }, + "references": [], + "type": "visualization", + "updated_at": "2023-07-25T19:39:50.773Z", + "version": "WzgxLDFd" + }, + { + "id": "06cf9c40-9ee8-11e7-8711-e7a007dcef99", + "type": "visualization", + "updated_at": "2018-08-29T13:22:17.617Z", + "version": "1", + "migrationVersion": {}, + "attributes": { + "title": "[Logs] Visitors Map", + "visState": "{\"title\":\"[Logs] Visitors Map\",\"type\":\"vega\",\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 30, longitude: -120, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: opensearch_dashboards_sample_data_logs\\n %context%: true\\n %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\\"geo.coordinates\\\", precision: 5, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\\"geo.coordinates\\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.gridSplit.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n {\\n name: bubbleColor\\n type: linear\\n domain: {\\n data: table\\n field: doc_count\\n }\\n range: [\\\"rgb(255, 255, 255)\\\",\\\"rgb(249, 212, 204)\\\",\\\"rgb(238, 170, 156)\\\", \\\"rgb(223, 129, 110)\\\"]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n fill: {\\n scale: bubbleColor\\n field: doc_count\\n }\\n size: {scale: \\\"gridSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"}}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"kuery\"}}" + } + }, + "references": [] + } + ] +} diff --git a/src/plugins/home/server/services/sample_data/data_sets/util.test.ts b/src/plugins/home/server/services/sample_data/data_sets/util.test.ts index ba02f770d582..56f2f15bca64 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/util.test.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/util.test.ts @@ -5,84 +5,12 @@ import { getSavedObjectsWithDataSource } from './util'; import { SavedObject, updateDataSourceNameInVegaSpec } from '../../../../../../core/server'; +import visualizationObjects from './test_utils/visualization_objects.json'; describe('getSavedObjectsWithDataSource()', () => { const getVisualizationSavedObjects = (): Array> => { - return [ - { - id: '935afa20-e0cd-11e7-9d07-1398ccfcefa3', - type: 'visualization', - updated_at: '2018-08-29T13:22:17.617Z', - version: '1', - migrationVersion: {}, - attributes: { - title: '[Logs] Heatmap', - visState: - '{"title":"[Logs] Heatmap","type":"heatmap","params":{"type":"heatmap","addTooltip":true,"addLegend":true,"enableHover":true,"legendPosition":"right","times":[],"colorsNumber":10,"colorSchema":"Reds","setColorRange":false,"colorsRange":[],"invertColors":false,"percentageMode":false,"valueAxes":[{"show":false,"id":"ValueAxis-1","type":"value","scale":{"type":"linear","defaultYExtents":false},"labels":{"show":false,"rotate":0,"color":"#555","overwriteColor":false}}]},"aggs":[{"id":"1","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"clientip"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"geo.src","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Country Source"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"hour_of_day","size":25,"order":"asc","orderBy":"_key","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Hour of Day"}}]}', - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"90943e30-9a47-11e8-b64d-95841ca0b247","filter":[],"query":{"query":"","language":"kuery"}}', - }, - }, - references: [], - }, - { - id: 'some-id', - type: 'visualization', - references: [], - attributes: { - title: 'Some non-OpenSearch query Vega visualization', - description: 'This is a sample Vega visualization without an OpenSearch query.', - kibanaSavedObjectMeta: {}, - visState: - '{"title":"Vega Visualization without OpenSearch Query","type":"vega","params":{"spec":"{\\"$schema\\":\\"https://vega.github.io/schema/vega-lite/v5.json\\",\\"description\\":\\"A simple scatter plot.\\",\\"data\\":{\\"values\\":[{\\"x\\":1,\\"y\\":2},{\\"x\\":2,\\"y\\":3},{\\"x\\":3,\\"y\\":4},{\\"x\\":4,\\"y\\":5},{\\"x\\":5,\\"y\\":6},{\\"x\\":6,\\"y\\":7},{\\"x\\":7,\\"y\\":8},{\\"x\\":8,\\"y\\":9},{\\"x\\":9,\\"y\\":10}],\\"mark\\":\\"point\\",\\"encoding\\":{\\"x\\":{\\"field\\":\\"x\\",\\"type\\":\\"quantitative\\"},\\"y\\":{\\"field\\":\\"y\\",\\"type\\":\\"quantitative\\"}}},\\"width\\":400,\\"height\\":200}"},"aggs":[]}', - uiStateJSON: '{}', - version: 1, - }, - }, - { - attributes: { - description: '', - kibanaSavedObjectMeta: { - searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', - }, - title: '(Vega) Top destination count', - uiStateJSON: '{}', - version: 1, - visState: - '{"title":"(Vega) Top destination count","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega-lite/v5.json\\n data: {\\n url: {\\n %context%: true\\n %timefield%: @timestamp\\n index: opensearch_dashboards_sample_data_logs\\n body: {\\n aggs: {\\n 1: {\\n terms: {\\n field: geo.dest\\n order: {\\n _count: desc\\n }\\n size: 5\\n }\\n }\\n }\\n }\\n }\\n format: {\\n property: aggregations.1.buckets\\n }\\n }\\n transform: [\\n {\\n calculate: datum.key\\n as: dest\\n }\\n {\\n calculate: datum.doc_count\\n as: count\\n }\\n ]\\n layer: [\\n {\\n mark: {\\n type: bar\\n tooltip: true\\n }\\n }\\n ]\\n encoding: {\\n x: {\\n field: count\\n type: quantitative\\n axis: {\\n title: Count\\n }\\n }\\n y: {\\n field: dest\\n type: nominal\\n axis: {\\n title: Dest\\n }\\n sort: -x\\n }\\n }\\n}"}}', - }, - id: 'f0d162c0-227b-11ee-b88b-47a93b5c527c', - migrationVersion: { visualization: '7.10.0' }, - references: [], - type: 'visualization', - updated_at: '2023-07-25T19:39:50.773Z', - version: 'WzgxLDFd', - }, - { - id: '06cf9c40-9ee8-11e7-8711-e7a007dcef99', - type: 'visualization', - updated_at: '2018-08-29T13:22:17.617Z', - version: '1', - migrationVersion: {}, - attributes: { - title: '[Logs] Visitors Map', - visState: - '{"title":"[Logs] Visitors Map","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 30, longitude: -120, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: opensearch_dashboards_sample_data_logs\\n %context%: true\\n %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\"geo.coordinates\\", precision: 5, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\"geo.coordinates\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.gridSplit.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n {\\n name: bubbleColor\\n type: linear\\n domain: {\\n data: table\\n field: doc_count\\n }\\n range: [\\"rgb(255, 255, 255)\\",\\"rgb(249, 212, 204)\\",\\"rgb(238, 170, 156)\\", \\"rgb(223, 129, 110)\\"]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n fill: {\\n scale: bubbleColor\\n field: doc_count\\n }\\n size: {scale: \\"gridSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n tooltip: {\\n signal: \\"{flights: datum.doc_count}\\"\\n }\\n }\\n }\\n }\\n ]\\n}"}}', - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"90943e30-9a47-11e8-b64d-95841ca0b247","filter":[],"query":{"query":"","language":"kuery"}}', - }, - }, - references: [], - }, - ]; + // @ts-expect-error + return visualizationObjects.saved_objects; }; test('when processing Vega Visualization saved objects, it should attach data_source_name to each OpenSearch query', () => { From 9435b6c7d7964d2de60d4157f06b0315ee2e38a7 Mon Sep 17 00:00:00 2001 From: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:30:17 +0000 Subject: [PATCH 5/8] Remove newline Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89299cd00193..e0176c50e208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ + # CHANGELOG Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) From 3dcf3a03eebbe82863d31f3c4e5d42781acfbbad Mon Sep 17 00:00:00 2001 From: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:31:55 +0000 Subject: [PATCH 6/8] Remove newline Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0176c50e208..89299cd00193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,3 @@ - # CHANGELOG Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) From c000459c066fd7259b3162011acc35b26577d15f Mon Sep 17 00:00:00 2001 From: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> Date: Wed, 27 Mar 2024 20:24:12 +0000 Subject: [PATCH 7/8] Add comments for spacing Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --- src/core/server/saved_objects/import/utils.ts | 7 +++++++ .../home/server/services/sample_data/data_sets/util.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/src/core/server/saved_objects/import/utils.ts b/src/core/server/saved_objects/import/utils.ts index c9c3fc2e6927..5ef955ab8ad0 100644 --- a/src/core/server/saved_objects/import/utils.ts +++ b/src/core/server/saved_objects/import/utils.ts @@ -6,6 +6,13 @@ import { parse, stringify } from 'hjson'; import { SavedObject, SavedObjectsClientContract } from '../types'; +/** + * Given a Vega spec, the new datasource (by name), and spacing, + * + * @param {string} spec - the stringified Vega spec (HJSON or JSON) + * @param {string} newDataSourceName - the datasource name to append + * @param {number} [spacing=2] - how large the indenting should be after updating the spec (should be set to > 0 for a readable spec) + */ export interface UpdateDataSourceNameInVegaSpecProps { spec: string; newDataSourceName: string; diff --git a/src/plugins/home/server/services/sample_data/data_sets/util.ts b/src/plugins/home/server/services/sample_data/data_sets/util.ts index d84a4d0b2f20..c58cb2302f00 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/util.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/util.ts @@ -86,6 +86,7 @@ export const getSavedObjectsWithDataSource = ( const updatedVegaSpec = updateDataSourceNameInVegaSpec({ spec: vegaSpec, newDataSourceName: dataSourceTitle, + // Spacing of 1 prevents the Sankey visualization in logs data from exceeding the default url length and breaking spacing: 1, }); From 3ac41a02cd04b798c480c5e62a0c27a74d42bf19 Mon Sep 17 00:00:00 2001 From: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> Date: Fri, 29 Mar 2024 00:31:28 +0000 Subject: [PATCH 8/8] Update jsdoc Signed-off-by: Huy Nguyen <73027756+huyaboo@users.noreply.github.com> --- src/core/server/saved_objects/import/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/import/utils.ts b/src/core/server/saved_objects/import/utils.ts index 5ef955ab8ad0..10481c8227b6 100644 --- a/src/core/server/saved_objects/import/utils.ts +++ b/src/core/server/saved_objects/import/utils.ts @@ -7,7 +7,7 @@ import { parse, stringify } from 'hjson'; import { SavedObject, SavedObjectsClientContract } from '../types'; /** - * Given a Vega spec, the new datasource (by name), and spacing, + * Given a Vega spec, the new datasource (by name), and spacing, update the Vega spec to add the new datasource name to each local cluster query * * @param {string} spec - the stringified Vega spec (HJSON or JSON) * @param {string} newDataSourceName - the datasource name to append