diff --git a/docker/grafana/dashboards/config_from_query_issue_449.json b/docker/grafana/dashboards/config_from_query_issue_449.json new file mode 100644 index 000000000..64de8c313 --- /dev/null +++ b/docker/grafana/dashboards/config_from_query_issue_449.json @@ -0,0 +1,404 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 11, + "links": [], + "panels": [ + { + "datasource": { + "type": "vertamedia-clickhouse-datasource", + "uid": "P7E099F39B84EA795" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "adHocFilters": [ + { + "condition": "", + "key": "default.test_grafana.country", + "operator": "=", + "value": "NL" + } + ], + "adHocValuesQuery": "", + "add_metadata": true, + "contextWindowSize": "10", + "database": "default", + "datasource": { + "type": "vertamedia-clickhouse-datasource", + "uid": "P7E099F39B84EA795" + }, + "dateColDataType": "", + "dateLoading": false, + "dateTimeColDataType": "event_time", + "dateTimeType": "DATETIME", + "datetimeLoading": false, + "editorMode": "sql", + "extrapolate": true, + "format": "time_series", + "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", + "interval": "", + "intervalFactor": 1, + "query": "WITH topx AS (\n SELECT DISTINCT CASE WHEN ${split:text} = '' THEN 'other' ELSE ${split:text} END AS filter, count() AS cnt \n FROM $table WHERE $timeFilter AND $adhoc GROUP BY ${split:text} \n ORDER BY cnt DESC LIMIT 10\n)\n\nSELECT\n $timeSeries as t,\n CASE WHEN ${split:text} IN (SELECT filter FROM topx) THEN ${split:text} ELSE 'other' END AS spl,\n count()\nFROM $table\n\nWHERE $timeFilter AND $adhoc\nGROUP BY t, spl\n\nORDER BY t, spl\n", + "rawQuery": " /* grafana dashboard=Config from query result, Issue 449, user=0 */\n\nWITH topx AS(SELECT DISTINCT CASE WHEN service_name = '' THEN 'other' ELSE service_name END AS filter, count() AS cnt FROM default.test_grafana WHERE event_time >= toDateTime(1731695619) AND event_time <= toDateTime(1735232147) AND (country = 'NL') GROUP BY service_name ORDER BY cnt DESC LIMIT 10)\nSELECT\n (intDiv(toUInt32(event_time), 3600) * 3600) * 1000 as t,\n CASE WHEN service_name IN (\n SELECT filter\n\n FROM topx\n) THEN service_name ELSE 'other' END AS spl,\n count()\nFROM default.test_grafana\n\nWHERE\n event_time >= toDateTime(1731695619) AND event_time <= toDateTime(1735232147)\n AND (country = 'NL')\nGROUP BY\n t,\n spl\nORDER BY\n t,\n spl", + "refId": "A", + "round": "0s", + "skip_comments": true, + "table": "test_grafana", + "tableLoading": false, + "useWindowFuncForMacros": true + } + ], + "title": "Timeseries", + "transformations": [ + { + "id": "configFromData", + "options": { + "configRefId": "A - postgresql", + "mappings": [] + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "vertamedia-clickhouse-datasource", + "uid": "P7E099F39B84EA795" + }, + "description": "", + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "adHocFilters": [ + { + "condition": "", + "key": "default.test_grafana.country", + "operator": "=", + "value": "NL" + } + ], + "adHocValuesQuery": "", + "add_metadata": true, + "contextWindowSize": "10", + "database": "default", + "datasource": { + "type": "vertamedia-clickhouse-datasource", + "uid": "P7E099F39B84EA795" + }, + "dateColDataType": "", + "dateLoading": false, + "dateTimeColDataType": "event_time", + "dateTimeType": "DATETIME", + "datetimeLoading": false, + "editorMode": "sql", + "extrapolate": true, + "format": "logs", + "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", + "interval": "", + "intervalFactor": 1, + "query": "SELECT *\nFROM $table\n\nWHERE $timeFilter AND $adhoc", + "rawQuery": " /* grafana dashboard=Config from query result, Issue 449 Copy, user=0 */\n\nSELECT *\n\nFROM default.test_logs\n\nWHERE\n event_time >= toDateTime(1731695619) AND event_time <= toDateTime(1735232147)\n AND 1", + "refId": "A", + "round": "0s", + "skip_comments": true, + "table": "test_logs", + "tableLoading": false, + "useWindowFuncForMacros": true + } + ], + "title": "Logs", + "transformations": [ + { + "id": "configFromData", + "options": { + "applyTo": { + "id": "byName" + }, + "configRefId": "A", + "mappings": [] + } + } + ], + "type": "logs" + }, + { + "datasource": { + "type": "vertamedia-clickhouse-datasource", + "uid": "P7E099F39B84EA795" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 4, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "adHocFilters": [ + { + "condition": "", + "key": "default.test_grafana.country", + "operator": "=", + "value": "NL" + } + ], + "adHocValuesQuery": "", + "add_metadata": true, + "contextWindowSize": "10", + "database": "default", + "datasource": { + "type": "vertamedia-clickhouse-datasource", + "uid": "P7E099F39B84EA795" + }, + "dateColDataType": "", + "dateLoading": false, + "dateTimeColDataType": "event_time", + "dateTimeType": "DATETIME", + "datetimeLoading": false, + "editorMode": "sql", + "extrapolate": true, + "format": "table", + "formattedQuery": "SELECT $timeSeries as t, count() FROM $table WHERE $timeFilter GROUP BY t ORDER BY t", + "interval": "", + "intervalFactor": 1, + "query": "WITH topx AS (\n SELECT DISTINCT CASE WHEN ${split:text} = '' THEN 'other' ELSE ${split:text} END AS filter, count() AS cnt \n FROM $table WHERE $timeFilter AND $adhoc GROUP BY ${split:text} \n ORDER BY cnt DESC LIMIT 10\n)\n\nSELECT\n $timeSeries as t,\n CASE WHEN ${split:text} IN (SELECT filter FROM topx) THEN ${split:text} ELSE 'other' END AS spl,\n count()\nFROM $table\n\nWHERE $timeFilter AND $adhoc\nGROUP BY t, spl\n\nORDER BY t, spl\n", + "rawQuery": " /* grafana dashboard=Config from query result, Issue 449, user=0 */\n\nWITH topx AS(SELECT DISTINCT CASE WHEN service_name = '' THEN 'other' ELSE service_name END AS filter, count() AS cnt FROM default.test_grafana WHERE event_time >= toDateTime(1731695619) AND event_time <= toDateTime(1735232147) AND (country = 'NL') GROUP BY service_name ORDER BY cnt DESC LIMIT 10)\nSELECT\n (intDiv(toUInt32(event_time), 3600) * 3600) * 1000 as t,\n CASE WHEN service_name IN (\n SELECT filter\n\n FROM topx\n) THEN service_name ELSE 'other' END AS spl,\n count()\nFROM default.test_grafana\n\nWHERE\n event_time >= toDateTime(1731695619) AND event_time <= toDateTime(1735232147)\n AND (country = 'NL')\nGROUP BY\n t,\n spl\nORDER BY\n t,\n spl", + "refId": "A", + "round": "0s", + "skip_comments": true, + "table": "test_grafana", + "tableLoading": false, + "useWindowFuncForMacros": true + } + ], + "title": "Timeseries", + "transformations": [ + { + "id": "configFromData", + "options": { + "configRefId": "A - postgresql", + "mappings": [] + } + } + ], + "type": "table" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [ + { + "baseFilters": [], + "datasource": { + "type": "vertamedia-clickhouse-datasource", + "uid": "P7E099F39B84EA795" + }, + "filters": [ + { + "condition": "", + "key": "default.test_grafana.country", + "operator": "=", + "value": "NL" + } + ], + "name": "adhoc_variable", + "type": "adhoc" + }, + { + "current": { + "text": "service_name", + "value": "service_name" + }, + "datasource": { + "type": "vertamedia-clickhouse-datasource", + "uid": "P7E099F39B84EA795" + }, + "definition": "SELECT name FROM system.columns WHERE database='default' AND table='test_grafana' AND type ILIKE '%String%'", + "includeAll": false, + "name": "split", + "options": [], + "query": "SELECT name FROM system.columns WHERE database='default' AND table='test_grafana' AND type ILIKE '%String%'", + "refresh": 1, + "regex": "", + "type": "query" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Config from query result, Issue 449", + "uid": "be81n7eenft34b", + "version": 1, + "weekStart": "" +} diff --git a/src/datasource/sql-series/toLogs.ts b/src/datasource/sql-series/toLogs.ts index 7bcd4b629..c9dd862a2 100644 --- a/src/datasource/sql-series/toLogs.ts +++ b/src/datasource/sql-series/toLogs.ts @@ -1,6 +1,32 @@ -import { DataFrame, FieldType, MutableDataFrame } from '@grafana/data'; -import { each, find, omitBy, pickBy } from 'lodash'; -import { convertTimezonedDateToUTC } from './sql_series'; +import {createDataFrame, DataFrame, DataFrameType, FieldType} from '@grafana/data'; +import {each, find, omitBy, pickBy} from 'lodash'; +import {convertTimezonedDateToUTC} from './sql_series'; + +const transformObject = (obj) => { + // Check if the input is an object and not null + if (obj && typeof obj === 'object') { + // Create a new object to store the transformed properties + const result = Array.isArray(obj) ? [] : {}; + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const value = obj[key]; + + // If the value is an object (and not null), convert it to a string + if (value && typeof value === 'object') { + result[key] = JSON.stringify(value); + } else { + // Otherwise, keep the primitive value as it is + result[key] = value; + } + } + } + + return result; + } + // Return the original value if it's not an object + return obj; +} const _toFieldType = (type: string, index?: number): FieldType | Object => { if (type.startsWith('Nullable(')) { @@ -38,109 +64,113 @@ const _toFieldType = (type: string, index?: number): FieldType | Object => { }; export const toLogs = (self: any): DataFrame[] => { - const dataFrame: DataFrame[] = []; - const reservedFields = ['level', 'id']; + const reservedFields = ['severity', 'level', 'id']; if (self.series.length === 0) { - return dataFrame; + return []; } let types: { [key: string]: any } = {}; let labelFields: any[] = []; - // Trying to find message field - // If we have a "content" field - take it + const labelFieldsList: any[] = [] + let timestampKey; + // Trying to find message field, If we have a "content" field - take it, If not - take the first string field let messageField = find(self.meta, ['name', 'content'])?.name; - // If not - take the first string field if (messageField === undefined) { messageField = find(self.meta, (o: any) => _toFieldType(o.type) === FieldType.string)?.name; } + // If no string fields - this query is unusable for logs, because Grafana requires at least one text field if (messageField === undefined) { - return dataFrame; + return []; } each(self.meta, function (col: any, index: number) { let type = _toFieldType(col.type, index); - if (type === FieldType.string && col.name !== messageField && !reservedFields.includes(col.name)) { + if ((type === FieldType.number || type === FieldType.string) && col.name !== messageField && !reservedFields.includes(col.name)) { labelFields.push(col.name); } types[col.name] = type; }); - each(self.series, function (ser: any) { - const frame = new MutableDataFrame({ - refId: self.refId, - meta: { - preferredVisualisationType: 'logs', - }, - fields: [], - }); - const labels = pickBy(ser, (_value: any, key: string) => labelFields.includes(key)); + const dataObjectValues = Object.entries(self.series[0]).reduce((acc, [key, value]) => { + acc[key] = { + type: types[key], + values: [], + name: key, + }; - each(ser, function (_value: any, key: string) { - // Skip unknown keys for in case - if (!(key in types)) { - return; - } + return acc; + },{}); - if (key === messageField) { - const transformObject = (obj) => { - // Check if the input is an object and not null - if (obj && typeof obj === 'object') { - // Create a new object to store the transformed properties - const result = Array.isArray(obj) ? [] : {}; - - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - const value = obj[key]; - - // If the value is an object (and not null), convert it to a string - if (value && typeof value === 'object') { - result[key] = JSON.stringify(value); - } else { - // Otherwise, keep the primitive value as it is - result[key] = value; - } - } - } - - return result; - } - // Return the original value if it's not an object - return obj; - } + each(self.series, function (ser: any) { + const labels = pickBy(ser, (_value: any, key: string) => labelFields.includes(key)); - frame.addField({ name: key, type: types[key], labels: transformObject(labels), config: { filterable: false } }); - } else if (!labelFields.includes(key) && types[key].fieldType === FieldType.time) { - frame.addField({ name: key, type: FieldType.time }); - } else if (!labelFields.includes(key)) { - frame.addField({ name: key, type: types[key] }); - } - }); + if (Object.keys(labels).length > 0) { + labelFieldsList.push(transformObject(labels)) + } const data = omitBy(ser, (_value: any, key: string) => { labelFields.includes(key); }); - const frameData = Object.entries(data).reduce((acc, [key, value]) => { + + const timestampObject = Object.entries(types)?.find(object => object[1] === 'time') + timestampKey = timestampObject? timestampObject[0] : null; + + Object.entries(data)?.forEach(([key, value]) => { if ( types[key] && types[key] instanceof Object && 'fieldType' in types[key] && types[key].fieldType === FieldType.time ) { - acc[key] = convertTimezonedDateToUTC(value, types[key].timezone); + timestampKey = key; + dataObjectValues[key].values.push(convertTimezonedDateToUTC(value, types[key].timezone)); } else { - acc[key] = value; + dataObjectValues[key].values.push(value); } - return acc; - }, {}); + }); + }); - frame.add(frameData); - dataFrame.push(frame); + const result = createDataFrame({ + fields: [ + dataObjectValues[timestampKey]?.values.length && { + name: 'timestamp', + type: FieldType.time, + values: dataObjectValues[timestampKey]?.values, + }, + (dataObjectValues['level']?.values?.length || dataObjectValues['severity']?.values?.length) && { + name: 'severity', + type: (dataObjectValues['level'] || dataObjectValues['severity'])?.type, + values: (dataObjectValues['level'] || dataObjectValues['severity'])?.values, + }, + dataObjectValues[messageField] && { + name: 'body', + type: dataObjectValues[messageField].type, + values: dataObjectValues[messageField].values, + config: { filterable: false } + }, + labelFieldsList.length && { + name: 'labels', + values: labelFieldsList, + type: FieldType.other, + }, + dataObjectValues['id']?.values?.length && + { + name: 'id', + type: (dataObjectValues['id'])?.type, + values: (dataObjectValues['id'])?.values, + }, + ].filter(Boolean), + meta: { + type: DataFrameType.LogLines, + preferredVisualisationType: 'logs' + }, + refId: self.refId, }); - return dataFrame; + return [result] }; diff --git a/src/datasource/sql-series/toTimeSeries.ts b/src/datasource/sql-series/toTimeSeries.ts index d65e95669..b00152973 100644 --- a/src/datasource/sql-series/toTimeSeries.ts +++ b/src/datasource/sql-series/toTimeSeries.ts @@ -160,9 +160,9 @@ export const toTimeSeries = (extrapolate = true, self): any => { each(metrics, function (dataPoints, seriesName) { if (extrapolate) { - timeSeries.push({ target: seriesName, datapoints: extrapolateDataPoints(dataPoints, self) }); + timeSeries.push({ target: seriesName, datapoints: extrapolateDataPoints(dataPoints, self), refId: seriesName && self.refId ? `${self.refId} - ${seriesName}` : undefined}); } else { - timeSeries.push({ target: seriesName, datapoints: dataPoints }); + timeSeries.push({ target: seriesName, datapoints: dataPoints, refId: seriesName && self.refId ? `${self.refId} - ${seriesName}` : undefined}); } }); diff --git a/src/spec/datasource.jest.ts b/src/spec/datasource.jest.ts index 986e7bf42..648f2cedf 100644 --- a/src/spec/datasource.jest.ts +++ b/src/spec/datasource.jest.ts @@ -2,7 +2,7 @@ import { size } from 'lodash'; import SqlSeries from '../datasource/sql-series/sql_series'; import AdhocCtrl from '../datasource/adhoc'; import ResponseParser from '../datasource/response_parser'; -import { FieldType, MutableDataFrame } from '@grafana/data'; +import { FieldType } from '@grafana/data'; describe('clickhouse sql series:', () => { describe('SELECT $timeseries response WHERE $adhoc = 1', () => { @@ -267,11 +267,6 @@ describe('clickhouse sql series:', () => { }); let logs = sqlSeries.toLogs(); - it('expects array of MutableDataFrames', () => { - expect(size(logs)).toBe(4); - expect(logs[0]).toBeInstanceOf(MutableDataFrame); - }); - it('should have refId', () => { expect(logs[0].refId).toBe('A'); }); @@ -281,7 +276,7 @@ describe('clickhouse sql series:', () => { }); it('should get four fields in DataFrame', () => { - expect(size(logs[0].fields)).toBe(4); + expect(logs[0].fields.length).toBe(5); }); it('should get first field in DataFrame as time', () => { @@ -289,15 +284,15 @@ describe('clickhouse sql series:', () => { }); it('should get second field in DataFrame as content with labels', () => { - expect(logs[0].fields[1]).toHaveProperty('labels'); - expect(logs[0].fields[1].labels).toHaveProperty('host'); + expect(logs[0].fields[3].name).toEqual('labels'); + expect(logs[0].fields[3].values[0]).toHaveProperty('host'); }); it('should get one datapoints for each field in each DataFrame', () => { - expect(size(logs[0].fields[0].values)).toBe(1); - expect(size(logs[0].fields[1].values)).toBe(1); - expect(size(logs[0].fields[2].values)).toBe(1); - expect(size(logs[0].fields[3].values)).toBe(1); + expect(size(logs[0].fields)).toBe(5); + expect(size(logs[0].fields[1].values)).toBe(4); + expect(size(logs[0].fields[2].values)).toBe(4); + expect(size(logs[0].fields[3].values)).toBe(4); }); }); }); diff --git a/src/spec/sql_series_specs.jest.ts b/src/spec/sql_series_specs.jest.ts index a5219e176..de1e62dea 100644 --- a/src/spec/sql_series_specs.jest.ts +++ b/src/spec/sql_series_specs.jest.ts @@ -1,8 +1,6 @@ import { toFlamegraph } from '../datasource/sql-series/toFlamegraph'; import { toLogs } from '../datasource/sql-series/toLogs'; import { toTable } from '../datasource/sql-series/toTable'; - -import { MutableDataFrame } from '@grafana/data'; import { toTimeSeries } from '../datasource/sql-series/toTimeSeries'; import { toTraces } from '../datasource/sql-series/toTraces'; @@ -98,25 +96,6 @@ describe('sql-series. toLogs unit tests', () => { expect(result).toEqual([]); }); - it('should return an empty array of object if no message field is found', () => { - const input = { - series: [{ id: 1 }], - meta: [{ name: 'level', type: 'FixedString' }], - }; - const result = toLogs(input); - - const expected = new MutableDataFrame({ - fields: [], - meta: { - preferredVisualisationType: 'logs', - }, - }); - - expect(result[0] instanceof MutableDataFrame).toBeTruthy(); - expect(result[0].fields).toEqual(expected.fields); - expect(result[0].meta).toEqual(expected.meta); - }); - it('should correctly identify the message field', () => { const input = { series: [{ id: 1, content: 'Log message' }], @@ -126,8 +105,8 @@ describe('sql-series. toLogs unit tests', () => { ], }; const result = toLogs(input); - expect(result.length).toBe(1); - expect(result[0].fields[0].name).toBe('content'); + expect(result[0].fields.length).toBe(2); + expect(result[0].fields[0].name).toBe('body'); }); it('should handle Nullable types correctly', () => { @@ -140,9 +119,9 @@ describe('sql-series. toLogs unit tests', () => { }; const result = toLogs(input); expect(result.length).toBe(1); - expect(result[0].fields.length).toBe(2); + expect(result[0].fields.length).toBe(4); expect(result[0].fields[0].name).toBe('timestamp'); - expect(result[0].fields[1].name).toBe('level'); + expect(result[0].fields[1].name).toBe('severity'); }); it('should add label fields correctly', () => { @@ -157,11 +136,11 @@ describe('sql-series. toLogs unit tests', () => { }; const result = toLogs(input); expect(result.length).toBe(1); - expect(result[0].fields.length).toBe(3); // [message + labels], level, timestamp + expect(result[0].fields.length).toBe(4); // [message + labels], level, timestamp const message = result[0].fields.find((field) => { - return field.name === 'message'; + return field.name === 'labels'; }); - expect(message?.labels?.user).toBe('user1'); // user + expect(message?.values[0].user).toBe('user1'); // user }); it('should convert time with time zone to UTC', () => { diff --git a/tests/testflows/steps/dashboards/view.py b/tests/testflows/steps/dashboards/view.py index 90cac6da3..7fb0d097b 100644 --- a/tests/testflows/steps/dashboards/view.py +++ b/tests/testflows/steps/dashboards/view.py @@ -118,12 +118,13 @@ def delete_dashboard(self, dashboard_name): def open_dashboard(self, dashboard_name): """Open dashboard view.""" - with delay(): - with When("I go to dashboards view"): + with When("I go to dashboards view"): + with delay(): open_dashboards_view() with And(f"I go to {dashboard_name}"): - open_dashboard_view(dashboard_name=dashboard_name) + with delay(): + open_dashboard_view(dashboard_name=dashboard_name) @TestStep(Then)