diff --git a/CHANGELOG.md b/CHANGELOG.md index 67ba3f1a..aee4e09b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Removed DatasourceResponseError moved to external Components (#535) - Added HTML, Markdown to supported Code Editor languages (#543) - Updated options to use datasource ID instead of name (#539) +- Updated E2E tests (#538) ## 4.8.0 (2024-10-25) diff --git a/README.md b/README.md index 70a7fbcb..bed24dac 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The Business Forms panel is a conceptually new plugin for Grafana. It is the fir ## Requirements -- Business Forms panel 4.X requires **Grafana 10** or **Grafana 11**. +- Business Forms panel 4.X requires **Grafana 10.3** or **Grafana 11**. - Data Manipulation panel 3.X requires **Grafana 9** or **Grafana 10**. - Data Manipulation panel 2.X requires **Grafana 9** or **Grafana 8.5**. - Data Manipulation panel 1.X requires **Grafana 8**. diff --git a/docker-compose.yml b/docker-compose.yml index 927be309..48967dd1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -69,7 +69,7 @@ services: - main grafana-dep: - image: grafana/grafana:10.0.0 + image: grafana/grafana:10.3.0 ports: - 3000:3000/tcp environment: diff --git a/package-lock.json b/package-lock.json index cf92c6ce..4b45a2ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "business-forms", - "version": "4.8.0", + "version": "4.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "business-forms", - "version": "4.8.0", + "version": "4.9.0", "license": "Apache-2.0", "dependencies": { "@emotion/css": "^11.13.0", diff --git a/package.json b/package.json index cc689e26..6c9c2f7d 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "stop": "docker compose down", "test": "jest --watch --onlyChanged", "test:e2e": "npx playwright test", + "test:e2e:dev": "npx playwright test --ui", "test:e2e:docker": "docker compose --profile e2e up --exit-code-from test", "test:ci": "jest --maxWorkers 4 --coverage", "upgrade": "npm upgrade --save" diff --git a/provisioning/dashboards/datasource.json b/provisioning/dashboards/datasource.json index 1f4253e1..30da6fd2 100644 --- a/provisioning/dashboards/datasource.json +++ b/provisioning/dashboards/datasource.json @@ -23,17 +23,89 @@ "panels": [ { "datasource": { - "default": true, "type": "marcusolsson-static-datasource", "uid": "P1D2C73DC01F2359B" }, + "fieldConfig": { + "defaults": { + "custom": { + "thresholdsStyle": { + "mode": "color", + "thresholds": [] + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, "gridPos": { - "h": 8, - "w": 17, + "h": 35, + "w": 4, "x": 0, "y": 0 }, - "id": 4, + "id": 1, + "options": { + "alwaysVisibleFilter": false, + "autoScroll": true, + "collapsedByDefault": false, + "customValue": false, + "displayMode": "table", + "emptyValue": false, + "favorites": { + "addQuery": {}, + "datasource": "", + "deleteQuery": {}, + "enabled": true, + "getQuery": {}, + "storage": "browser" + }, + "filter": true, + "groupSelection": false, + "header": true, + "isUseLocalTime": false, + "minimizeOutputFormat": "text", + "padding": 10, + "persistent": false, + "saveSelectedGroup": false, + "saveSelectedGroupKey": "", + "showGroupTotal": false, + "showLabel": false, + "showName": false, + "showResetButton": false, + "showTotal": false, + "statusSort": false, + "sticky": true, + "tabsInOrder": true, + "variable": "device" + }, + "pluginVersion": "3.6.0", + "type": "volkovlabs-variable-panel" + }, + { + "datasource": { + "type": "postgres", + "uid": "PCC52D03280B7034C" + }, + "gridPos": { + "h": 9, + "w": 10, + "x": 4, + "y": 0 + }, + "id": 2, "options": { "buttonGroup": { "orientation": "center", @@ -48,34 +120,423 @@ "newValue": "New Value", "oldValue": "Old Value" }, - "confirm": "Confirm", - "elementDisplayMode": "modified", - "title": "Confirm update request" - }, - "elementValueChanged": "", - "elements": [ + "confirm": "Confirm", + "elementDisplayMode": "modified", + "title": "Confirm update request" + }, + "elementValueChanged": "", + "elements": [ + { + "fieldName": "max", + "id": "max", + "labelWidth": 10, + "options": [], + "queryField": { + "label": "A:max", + "refId": "A", + "value": "max" + }, + "section": "current", + "title": "Max", + "tooltip": "", + "type": "disabled", + "uid": "8285e945-b1cf-4dd6-9efd-8e322196222a", + "unit": "", + "value": 0, + "width": 20 + }, + { + "fieldName": "min", + "id": "min", + "labelWidth": 10, + "options": [], + "queryField": { + "label": "A:min", + "refId": "A", + "value": "min" + }, + "section": "current", + "title": "Min", + "tooltip": "", + "type": "disabled", + "uid": "6d3ca353-178b-4a7a-b23d-e548cf3d2437", + "unit": "", + "value": 0, + "width": 20 + }, + { + "fieldName": "speed", + "id": "speed", + "labelWidth": 10, + "options": [], + "queryField": { + "label": "A:speed", + "refId": "A", + "value": "speed" + }, + "section": "current", + "title": "Speed", + "tooltip": "", + "type": "disabled", + "uid": "b7c38a6c-6c59-41db-a439-1964d20e660b", + "unit": "", + "value": 0, + "width": 20 + }, + { + "fieldName": "max", + "id": "max", + "labelWidth": 10, + "queryField": { + "label": "A:max", + "refId": "A", + "value": "max" + }, + "section": "new", + "title": "Max", + "tooltip": "", + "type": "number", + "uid": "3d3cf24f-7611-482d-b27b-b55621745dda", + "unit": "", + "value": 0, + "width": 20 + }, + { + "fieldName": "min", + "id": "min", + "labelWidth": 10, + "queryField": { + "label": "A:min", + "refId": "A", + "value": "min" + }, + "section": "new", + "title": "Min", + "tooltip": "", + "type": "number", + "uid": "bc2c6772-5bbd-456d-9fe2-f1824c373c68", + "unit": "", + "value": 0, + "width": 20 + }, + { + "fieldName": "speed", + "id": "speed", + "labelWidth": 10, + "max": 100, + "min": 0, + "queryField": { + "label": "A:speed", + "refId": "A", + "value": "speed" + }, + "section": "new", + "step": 1, + "title": "Speed", + "tooltip": "", + "type": "slider", + "uid": "1918dc47-f686-4ca2-aeb3-7f21c360f621", + "unit": "", + "value": 0 + } + ], + "initial": { + "code": "const payload = {};\n\ncontext.panel.elements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\nconsole.log(payload);\ncontext.panel.setInitial(payload);\n\nreturn;", + "contentType": "application/json", + "datasource": "PostgreSQL", + "getPayload": "", + "highlight": true, + "highlightColor": "red", + "method": "datasource", + "payload": { + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "select * from configuration where name ='$device';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + }, + "layout": { + "orientation": "horizontal", + "padding": 10, + "sectionVariant": "default", + "sections": [ + { + "id": "current", + "name": "Current Values" + }, + { + "id": "new", + "name": "New Values" + } + ], + "variant": "split" + }, + "reset": { + "backgroundColor": "purple", + "foregroundColor": "yellow", + "icon": "process", + "text": "Reset", + "variant": "secondary" + }, + "resetAction": { + "code": "console.log(context.panel.data, context.panel.response, context.panel.initial, context.panel.elements);", + "confirm": false, + "getPayload": "return {\n rawSql: '',\n format: 'table',\n}", + "mode": "initial", + "payload": {} + }, + "saveDefault": { + "icon": "save", + "text": "Save Default", + "variant": "hidden" + }, + "submit": { + "backgroundColor": "purple", + "foregroundColor": "yellow", + "icon": "cloud-upload", + "text": "Submit", + "variant": "primary" + }, + "sync": true, + "update": { + "code": "if (context.panel.response && context.panel.response.state === 'Done') {\n context.grafana.notifySuccess(['Update', 'Values updated successfully.']);\n context.panel.initialRequest();\n} else {\n context.grafana.notifyError(['Update', 'An error occurred updating values.']);\n}", + "confirm": true, + "contentType": "application/json", + "datasource": "PostgreSQL", + "getPayload": "const payload = {};\n\ncontext.panel.elements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\n/**\n * Data Source payload\n */\nreturn payload;", + "method": "datasource", + "payload": { + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "update configuration set min=${payload.min}, max=${payload.max}, speed=${payload.speed} where name='$device'", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + }, + "payloadMode": "custom" + }, + "updateEnabled": "auto" + }, + "pluginVersion": "4.9.0", + "targets": [ + { + "datasource": { + "type": "postgres", + "uid": "PCC52D03280B7034C" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "select * from configuration where name ='$device';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Data Source", + "type": "volkovlabs-form-panel" + }, + { + "datasource": { + "type": "postgres", + "uid": "PCC52D03280B7034C" + }, + "gridPos": { + "h": 8, + "w": 10, + "x": 14, + "y": 0 + }, + "id": 3, + "options": { + "buttonGroup": { + "orientation": "center", + "size": "md" + }, + "confirmModal": { + "body": "Please confirm to update changed values", + "cancel": "Cancel", + "columns": { + "include": ["name", "oldValue", "newValue"], + "name": "Label", + "newValue": "New Value", + "oldValue": "Old Value" + }, + "confirm": "Confirm", + "elementDisplayMode": "modified", + "title": "Confirm update request" + }, + "elementValueChanged": "", + "elements": [ + { + "fieldName": "max", + "id": "max", + "labelWidth": 10, + "options": [], + "queryField": { + "label": "A:max", + "refId": "A", + "value": "max" + }, + "section": "current", + "title": "Max", + "tooltip": "", + "type": "disabled", + "uid": "8285e945-b1cf-4dd6-9efd-8e322196222a", + "unit": "", + "value": 0, + "width": 20 + }, + { + "fieldName": "min", + "id": "min", + "labelWidth": 10, + "options": [], + "queryField": { + "label": "A:min", + "refId": "A", + "value": "min" + }, + "section": "current", + "title": "Min", + "tooltip": "", + "type": "disabled", + "uid": "6d3ca353-178b-4a7a-b23d-e548cf3d2437", + "unit": "", + "value": 0, + "width": 20 + }, + { + "fieldName": "speed", + "id": "speed", + "labelWidth": 10, + "options": [], + "queryField": { + "label": "A:speed", + "refId": "A", + "value": "speed" + }, + "section": "current", + "title": "Speed", + "tooltip": "", + "type": "disabled", + "uid": "b7c38a6c-6c59-41db-a439-1964d20e660b", + "unit": "", + "value": 0, + "width": 20 + }, + { + "fieldName": "max", + "id": "max", + "labelWidth": 10, + "queryField": { + "label": "A:max", + "refId": "A", + "value": "max" + }, + "section": "new", + "title": "Max", + "tooltip": "", + "type": "number", + "uid": "3d3cf24f-7611-482d-b27b-b55621745dda", + "unit": "", + "value": 0, + "width": 20 + }, { - "hidden": false, - "id": "String", + "fieldName": "min", + "id": "min", "labelWidth": 10, "queryField": { - "label": "undefined:val-1 category", - "value": "category" + "label": "A:min", + "refId": "A", + "value": "min" }, - "section": "", - "title": "String", + "section": "new", + "title": "Min", "tooltip": "", - "type": "string", - "uid": "0a15f927-e435-4678-8341-10f0e5caa3fb", + "type": "number", + "uid": "bc2c6772-5bbd-456d-9fe2-f1824c373c68", "unit": "", - "value": "" + "value": 0, + "width": 20 + }, + { + "fieldName": "speed", + "id": "speed", + "labelWidth": 10, + "max": 100, + "min": 0, + "queryField": { + "label": "A:speed", + "refId": "A", + "value": "speed" + }, + "section": "new", + "step": 1, + "title": "Speed", + "tooltip": "", + "type": "slider", + "uid": "1918dc47-f686-4ca2-aeb3-7f21c360f621", + "unit": "", + "value": 0 } ], "initial": { "code": "", "contentType": "application/json", - "getPayload": "return {}", - "highlight": false, + "datasource": "PostgreSQL", + "getPayload": "return {\n rawSql: \"select * from configuration where name ='$device';\",\n format: 'table',\n}", + "highlight": true, "highlightColor": "red", "method": "query", "payload": {} @@ -84,19 +545,29 @@ "orientation": "horizontal", "padding": 10, "sectionVariant": "default", - "variant": "single" + "sections": [ + { + "id": "current", + "name": "Current Values" + }, + { + "id": "new", + "name": "New Values" + } + ], + "variant": "split" }, "reset": { "backgroundColor": "purple", "foregroundColor": "yellow", "icon": "process", "text": "Reset", - "variant": "hidden" + "variant": "secondary" }, "resetAction": { - "code": "if (context.panel.response) {\n context.grafana.notifySuccess(['Update', 'Values updated successfully.']);\n context.grafana.locationService.reload();\n} else {\n context.grafana.notifyError(['Update', 'An error occurred updating values.']);\n}", + "code": "console.log(context.panel.data, context.panel.response, context.panel.initial, context.panel.elements);", "confirm": false, - "getPayload": "return {}", + "getPayload": "return {\n rawSql: '',\n format: 'table',\n}", "mode": "initial", "payload": {} }, @@ -114,57 +585,72 @@ }, "sync": true, "update": { - "code": "", - "confirm": false, + "code": "if (context.panel.response && context.panel.response.state === 'Done') {\n context.grafana.notifySuccess(['Update', 'Values updated successfully.']);\n context.panel.initialRequest();\n} else {\n context.grafana.notifyError(['Update', 'An error occured updating values.']);\n}", + "confirm": true, "contentType": "application/json", - "getPayload": "const payload = {};\n\ncontext.panel.elements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\nreturn payload;", - "method": "-", - "payload": {}, - "payloadMode": "all" + "datasource": "PostgreSQL", + "getPayload": "const payload = {};\n\ncontext.panel.elements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\n/**\n * Data Source payload\n */\nreturn payload;", + "method": "datasource", + "payload": { + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "update configuration set min=${payload.min}, max=${payload.max}, speed=${payload.speed} where name='$device'", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + }, + "payloadMode": "custom" }, "updateEnabled": "auto" }, - "pluginVersion": "4.7.0", + "pluginVersion": "4.9.0", "targets": [ { "datasource": { - "type": "marcusolsson-static-datasource", - "uid": "P1D2C73DC01F2359B" + "type": "postgres", + "uid": "PCC52D03280B7034C" }, - "frame": { - "fields": [ + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "select * from configuration where name ='$device';", + "refId": "A", + "sql": { + "columns": [ { - "config": {}, - "name": "value", - "type": "string", - "values": ["val-1", "val-2", "val-3"] - }, + "parameters": [], + "type": "function" + } + ], + "groupBy": [ { - "config": {}, - "name": "category", - "type": "string", - "values": ["cat-1", "cat-2", "cat-3"] + "property": { + "type": "string" + }, + "type": "groupBy" } ], - "meta": {}, - "name": "A" - }, - "refId": "A" - } - ], - "title": "Transform data - for Query Fields", - "transformations": [ - { - "id": "partitionByValues", - "options": { - "fields": ["value"], - "keepFields": true, - "naming": { - "asLabels": false - } + "limit": 50 } } ], + "title": "Query", "type": "volkovlabs-form-panel" }, { @@ -173,10 +659,10 @@ "uid": "P1D2C73DC01F2359B" }, "gridPos": { - "h": 25, - "w": 7, - "x": 17, - "y": 0 + "h": 17, + "w": 10, + "x": 14, + "y": 8 }, "id": 5, "options": { @@ -302,96 +788,25 @@ "backgroundColor": "purple", "foregroundColor": "yellow", "icon": "cloud-upload", - "text": "Submit", - "variant": "primary" - }, - "sync": true, - "update": { - "code": "if (context.panel.response && context.panel.response.state === 'Done') {\n context.grafana.notifySuccess(['Update', 'Values updated successfully.']);\n context.panel.initialRequest();\n} else {\n context.grafana.notifyError(['Update', 'An error occurred updating values.']);\n}", - "confirm": true, - "contentType": "application/json", - "datasource": "PostgreSQL", - "getPayload": "const payload = {};\n\ncontext.panel.elements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\n/**\n * Data Source payload\n */\nreturn payload;", - "method": "datasource", - "payload": {}, - "payloadMode": "custom" - }, - "updateEnabled": "auto" - }, - "pluginVersion": "4.7.0", - "title": "Initial patchFormValue", - "type": "volkovlabs-form-panel" - }, - { - "datasource": { - "type": "marcusolsson-static-datasource", - "uid": "P1D2C73DC01F2359B" - }, - "fieldConfig": { - "defaults": { - "custom": { - "thresholdsStyle": { - "mode": "color", - "thresholds": [] - } - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 35, - "w": 4, - "x": 0, - "y": 8 - }, - "id": 1, - "options": { - "alwaysVisibleFilter": false, - "autoScroll": true, - "collapsedByDefault": false, - "customValue": false, - "displayMode": "table", - "emptyValue": false, - "favorites": { - "addQuery": {}, - "datasource": "", - "deleteQuery": {}, - "enabled": true, - "getQuery": {}, - "storage": "browser" - }, - "filter": true, - "groupSelection": false, - "header": true, - "padding": 10, - "persistent": false, - "saveSelectedGroup": false, - "saveSelectedGroupKey": "", - "showGroupTotal": false, - "showLabel": false, - "showName": false, - "showResetButton": false, - "showTotal": false, - "statusSort": false, - "sticky": true, - "tabsInOrder": true, - "variable": "device" + "text": "Submit", + "variant": "primary" + }, + "sync": true, + "update": { + "code": "if (context.panel.response && context.panel.response.state === 'Done') {\n context.grafana.notifySuccess(['Update', 'Values updated successfully.']);\n context.panel.initialRequest();\n} else {\n context.grafana.notifyError(['Update', 'An error occurred updating values.']);\n}", + "confirm": true, + "contentType": "application/json", + "datasource": "PostgreSQL", + "getPayload": "const payload = {};\n\ncontext.panel.elements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\n/**\n * Data Source payload\n */\nreturn payload;", + "method": "datasource", + "payload": {}, + "payloadMode": "custom" + }, + "updateEnabled": "auto" }, - "pluginVersion": "3.5.0", - "type": "volkovlabs-variable-panel" + "pluginVersion": "4.9.0", + "title": "Initial patchFormValue", + "type": "volkovlabs-form-panel" }, { "datasource": { @@ -399,12 +814,12 @@ "uid": "PCC52D03280B7034C" }, "gridPos": { - "h": 9, - "w": 13, + "h": 8, + "w": 10, "x": 4, - "y": 8 + "y": 9 }, - "id": 2, + "id": 8, "options": { "buttonGroup": { "orientation": "center", @@ -626,7 +1041,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "update configuration set min=${payload.min}, max=${payload.max}, speed=${payload.speed} where name='$device'", + "rawSql": "update\n configuration\nset\n min = ${payload.min},\n max = ${payload.max},\n speed = ${payload.speed}\n test = ${payload.speed}\nwhere\n name = '$device'", "refId": "A", "sql": { "columns": [ @@ -650,7 +1065,7 @@ }, "updateEnabled": "auto" }, - "pluginVersion": "4.7.0", + "pluginVersion": "4.9.0", "targets": [ { "datasource": { @@ -681,21 +1096,22 @@ } } ], - "title": "Data Source", + "title": "Invalid Payload", "type": "volkovlabs-form-panel" }, { "datasource": { - "type": "postgres", - "uid": "PCC52D03280B7034C" + "default": true, + "type": "marcusolsson-static-datasource", + "uid": "P1D2C73DC01F2359B" }, "gridPos": { "h": 8, - "w": 13, + "w": 10, "x": 4, "y": 17 }, - "id": 3, + "id": 4, "options": { "buttonGroup": { "orientation": "center", @@ -717,125 +1133,27 @@ "elementValueChanged": "", "elements": [ { - "fieldName": "max", - "id": "max", - "labelWidth": 10, - "options": [], - "queryField": { - "label": "A:max", - "refId": "A", - "value": "max" - }, - "section": "current", - "title": "Max", - "tooltip": "", - "type": "disabled", - "uid": "8285e945-b1cf-4dd6-9efd-8e322196222a", - "unit": "", - "value": 0, - "width": 20 - }, - { - "fieldName": "min", - "id": "min", - "labelWidth": 10, - "options": [], - "queryField": { - "label": "A:min", - "refId": "A", - "value": "min" - }, - "section": "current", - "title": "Min", - "tooltip": "", - "type": "disabled", - "uid": "6d3ca353-178b-4a7a-b23d-e548cf3d2437", - "unit": "", - "value": 0, - "width": 20 - }, - { - "fieldName": "speed", - "id": "speed", - "labelWidth": 10, - "options": [], - "queryField": { - "label": "A:speed", - "refId": "A", - "value": "speed" - }, - "section": "current", - "title": "Speed", - "tooltip": "", - "type": "disabled", - "uid": "b7c38a6c-6c59-41db-a439-1964d20e660b", - "unit": "", - "value": 0, - "width": 20 - }, - { - "fieldName": "max", - "id": "max", - "labelWidth": 10, - "queryField": { - "label": "A:max", - "refId": "A", - "value": "max" - }, - "section": "new", - "title": "Max", - "tooltip": "", - "type": "number", - "uid": "3d3cf24f-7611-482d-b27b-b55621745dda", - "unit": "", - "value": 0, - "width": 20 - }, - { - "fieldName": "min", - "id": "min", - "labelWidth": 10, - "queryField": { - "label": "A:min", - "refId": "A", - "value": "min" - }, - "section": "new", - "title": "Min", - "tooltip": "", - "type": "number", - "uid": "bc2c6772-5bbd-456d-9fe2-f1824c373c68", - "unit": "", - "value": 0, - "width": 20 - }, - { - "fieldName": "speed", - "id": "speed", + "hidden": false, + "id": "String", "labelWidth": 10, - "max": 100, - "min": 0, "queryField": { - "label": "A:speed", - "refId": "A", - "value": "speed" + "label": "undefined:val-1 category", + "value": "category" }, - "section": "new", - "step": 1, - "title": "Speed", + "section": "", + "title": "String", "tooltip": "", - "type": "slider", - "uid": "1918dc47-f686-4ca2-aeb3-7f21c360f621", + "type": "string", + "uid": "0a15f927-e435-4678-8341-10f0e5caa3fb", "unit": "", - "value": 0 + "value": "" } ], "initial": { "code": "", "contentType": "application/json", - "datasource": "PostgreSQL", - "getPayload": "return {\n rawSql: \"select * from configuration where name ='$device';\",\n format: 'table',\n}", - "highlight": true, + "getPayload": "return {}", + "highlight": false, "highlightColor": "red", "method": "query", "payload": {} @@ -844,29 +1162,19 @@ "orientation": "horizontal", "padding": 10, "sectionVariant": "default", - "sections": [ - { - "id": "current", - "name": "Current Values" - }, - { - "id": "new", - "name": "New Values" - } - ], - "variant": "split" + "variant": "single" }, "reset": { "backgroundColor": "purple", "foregroundColor": "yellow", "icon": "process", "text": "Reset", - "variant": "secondary" + "variant": "hidden" }, "resetAction": { - "code": "console.log(context.panel.data, context.panel.response, context.panel.initial, context.panel.elements);", + "code": "if (context.panel.response) {\n context.grafana.notifySuccess(['Update', 'Values updated successfully.']);\n context.grafana.locationService.reload();\n} else {\n context.grafana.notifyError(['Update', 'An error occurred updating values.']);\n}", "confirm": false, - "getPayload": "return {\n rawSql: '',\n format: 'table',\n}", + "getPayload": "return {}", "mode": "initial", "payload": {} }, @@ -884,72 +1192,57 @@ }, "sync": true, "update": { - "code": "if (context.panel.response && context.panel.response.state === 'Done') {\n context.grafana.notifySuccess(['Update', 'Values updated successfully.']);\n context.panel.initialRequest();\n} else {\n context.grafana.notifyError(['Update', 'An error occured updating values.']);\n}", - "confirm": true, + "code": "", + "confirm": false, "contentType": "application/json", - "datasource": "PostgreSQL", - "getPayload": "const payload = {};\n\ncontext.panel.elements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\n/**\n * Data Source payload\n */\nreturn payload;", - "method": "datasource", - "payload": { - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "update configuration set min=${payload.min}, max=${payload.max}, speed=${payload.speed} where name='$device'", - "refId": "A", - "sql": { - "columns": [ - { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ - { - "property": { - "type": "string" - }, - "type": "groupBy" - } - ], - "limit": 50 - } - }, - "payloadMode": "custom" + "getPayload": "const payload = {};\n\ncontext.panel.elements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\nreturn payload;", + "method": "-", + "payload": {}, + "payloadMode": "all" }, "updateEnabled": "auto" }, - "pluginVersion": "4.7.0", + "pluginVersion": "4.9.0", "targets": [ { "datasource": { - "type": "postgres", - "uid": "PCC52D03280B7034C" + "type": "marcusolsson-static-datasource", + "uid": "P1D2C73DC01F2359B" }, - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "select * from configuration where name ='$device';", - "refId": "A", - "sql": { - "columns": [ + "frame": { + "fields": [ { - "parameters": [], - "type": "function" - } - ], - "groupBy": [ + "config": {}, + "name": "value", + "type": "string", + "values": ["val-1", "val-2", "val-3"] + }, { - "property": { - "type": "string" - }, - "type": "groupBy" + "config": {}, + "name": "category", + "type": "string", + "values": ["cat-1", "cat-2", "cat-3"] } ], - "limit": 50 + "meta": {}, + "name": "A" + }, + "refId": "A" + } + ], + "title": "Transform data - for Query Fields", + "transformations": [ + { + "id": "partitionByValues", + "options": { + "fields": ["value"], + "keepFields": true, + "naming": { + "asLabels": false + } } } ], - "title": "Query", "type": "volkovlabs-form-panel" }, { @@ -1235,7 +1528,7 @@ }, "updateEnabled": "auto" }, - "pluginVersion": "4.7.0", + "pluginVersion": "4.9.0", "targets": [ { "datasource": { @@ -1266,7 +1559,7 @@ } } ], - "title": "Data Source", + "title": "Data Source Options", "type": "volkovlabs-form-panel" }, { @@ -1566,7 +1859,7 @@ }, "updateEnabled": "auto" }, - "pluginVersion": "4.7.0", + "pluginVersion": "4.9.0", "targets": [ { "datasource": { @@ -1597,7 +1890,7 @@ } } ], - "title": "Data Source", + "title": "Data Source Options 2", "type": "volkovlabs-form-panel" } ], @@ -1639,6 +1932,6 @@ "timezone": "", "title": "Configuration", "uid": "d185bf58-9e0a-4373-befa-3905a18af42e", - "version": 2, + "version": 1, "weekStart": "" } diff --git a/provisioning/dashboards/e2e.json b/provisioning/dashboards/e2e.json index 7d2950b8..d68b8416 100644 --- a/provisioning/dashboards/e2e.json +++ b/provisioning/dashboards/e2e.json @@ -27,8 +27,8 @@ "uid": "grafana" }, "gridPos": { - "h": 21, - "w": 17, + "h": 16, + "w": 12, "x": 0, "y": 0 }, @@ -55,7 +55,7 @@ "elements": [ { "id": "dateTime", - "labelWidth": 10, + "labelWidth": 15, "title": "DateTime", "type": "datetime", "uid": "69baa91c-9674-455e-896b-83e866a68b36", @@ -64,7 +64,7 @@ { "id": "time", "isUseLocalTime": false, - "labelWidth": 10, + "labelWidth": 15, "section": "", "title": "Time", "tooltip": "", @@ -75,7 +75,7 @@ }, { "id": "date", - "labelWidth": 10, + "labelWidth": 15, "section": "", "title": "Date", "tooltip": "", @@ -86,7 +86,7 @@ }, { "id": "amount", - "labelWidth": 10, + "labelWidth": 15, "title": "Amount", "type": "number", "uid": "5ec96b3b-1360-48fb-aea0-6aecfd5a6bfb", @@ -95,7 +95,7 @@ }, { "id": "updated", - "labelWidth": 10, + "labelWidth": 15, "title": "Updated", "type": "boolean", "uid": "251b9dc7-7fed-4df1-981f-122cd141c51c", @@ -103,7 +103,7 @@ }, { "id": "name", - "labelWidth": 10, + "labelWidth": 15, "title": "Name", "type": "string", "uid": "67e7fa0e-8038-4b14-80d0-9e2ccc3336ff", @@ -111,7 +111,7 @@ }, { "id": "step", - "labelWidth": 10, + "labelWidth": 15, "max": 6, "min": 0, "step": 1, @@ -124,7 +124,7 @@ { "allowCustomValue": false, "id": "select", - "labelWidth": 10, + "labelWidth": 15, "options": [ { "id": "max", @@ -149,7 +149,7 @@ }, { "id": "radio", - "labelWidth": 10, + "labelWidth": 15, "options": [ { "id": "plus", @@ -166,28 +166,320 @@ "type": "radio", "uid": "9dbab8b8-92ca-4bd0-b7b4-9e5f47911de8" }, + { + "id": "password", + "labelWidth": 15, + "title": "Password", + "type": "password", + "uid": "0b021eb5-f8b3-4e8d-9387-9693d6a7df8c" + }, { "id": "disabled", - "labelWidth": 10, + "labelWidth": 15, "options": [], "title": "Disabled", "type": "disabled", "uid": "79758a94-6654-4dbe-a44e-a311cba6a4e5" }, { - "id": "password", + "id": "link", + "labelWidth": 15, + "linkText": "https://volkovlabs.io/", + "section": "", + "target": "_blank", + "title": "link", + "tooltip": "", + "type": "link", + "uid": "69a27982-9e3d-47c1-afe2-7c825d5fa12d", + "unit": "", + "value": "" + }, + { + "allowCustomValue": false, + "id": "checkbox", + "labelWidth": 15, + "options": [ + { + "id": "1", + "label": "1", + "type": "number", + "value": 1 + }, + { + "id": "2", + "label": "2", + "type": "number", + "value": 2 + } + ], + "optionsSource": "Custom", + "section": "", + "title": "checkbox", + "tooltip": "", + "type": "checkboxList", + "uid": "4722ca3f-aca3-4fb2-976c-71b3ddc31797", + "unit": "", + "value": [] + } + ], + "height": 0, + "heightMode": "auto", + "initial": { + "code": "console.log(context.panel.response)\nconsole.log(context)", + "contentType": "application/json", + "getPayload": "return {\n rawSql: '',\n format: 'table',\n}", + "header": [ + { + "name": "X-Key", + "value": "123" + } + ], + "highlight": true, + "highlightColor": "red", + "method": "GET", + "payload": {}, + "payloadMode": "all", + "url": "http://localhost:3001/${var}" + }, + "layout": { + "orientation": "horizontal", + "padding": 10, + "sectionVariant": "default", + "variant": "single" + }, + "name": "data", + "reset": { + "backgroundColor": "purple", + "foregroundColor": "yellow", + "icon": "process", + "text": "Reset", + "variant": "secondary" + }, + "resetAction": { + "code": "if (response && response.ok) {\n notifySuccess(['Update', 'Values updated successfully.']);\n locationService.reload();\n} else {\n notifyError(['Update', 'An error occured updating values.']);\n}", + "confirm": false, + "getPayload": "return {\n rawSql: '',\n format: 'table',\n}", + "mode": "initial", + "payload": {} + }, + "saveDefault": { + "icon": "save", + "text": "Save Default", + "variant": "hidden" + }, + "submit": { + "backgroundColor": "purple", + "foregroundColor": "yellow", + "icon": "cloud-upload", + "orientation": "center", + "size": "md", + "text": "Update", + "variant": "primary" + }, + "sync": true, + "update": { + "code": "if (context.panel.response && context.panel.response.ok) {\n context.grafana.notifySuccess(['Update', 'Values updated successfully.']);\n context.panel.initialRequest();\n} else {\n context.grafana.notifyError(['Update', `An error occured updating values: ${response.status}`]);\n}", + "confirm": true, + "contentType": "application/json", + "getPayload": "const payload = {};\n\nelements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\nreturn payload;\n\n/**\n * Data Source payload\n */ \nreturn {\n rawSql: '',\n format: 'table',\n};", + "header": [ + { + "name": "X-Key", + "value": "123" + } + ], + "method": "POST", + "payload": {}, + "payloadMode": "all", + "url": "http://localhost:3001/${var}" + }, + "updateEnabled": "auto", + "width": 0, + "widthMode": "auto" + }, + "pluginVersion": "4.9.0", + "title": "Single Form", + "type": "volkovlabs-form-panel" + }, + { + "datasource": { + "type": "marcusolsson-static-datasource", + "uid": "P1D2C73DC01F2359B" + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 4, + "options": { + "buttonGroup": { + "orientation": "center", + "size": "md" + }, + "confirmModal": { + "body": "Please confirm to update changed values", + "cancel": "Cancel", + "columns": { + "include": ["name", "oldValue", "newValue"], + "name": "Label", + "newValue": "New Value", + "oldValue": "Old Value" + }, + "confirm": "Confirm", + "elementDisplayMode": "modified", + "title": "Confirm update request" + }, + "elementValueChanged": "", + "elements": [ + { + "hidden": false, + "id": "Name", "labelWidth": 10, - "title": "Password", - "type": "password", - "uid": "0b021eb5-f8b3-4e8d-9387-9693d6a7df8c" + "section": "", + "title": "Name", + "tooltip": "", + "type": "string", + "uid": "86564314-5234-4cc3-8e0c-f29d0c4c4bf7", + "unit": "", + "value": "" + } + ], + "initial": { + "code": "console.log(context.panel.data, context.panel.response, context.panel.initial, context.panel.elements);\n\nreturn;\n\n/**\n * Data Source\n * Requires form elements to be defined\n */\nconst dataQuery = context.utils.toDataQueryResponse(context.panel.response);\nconsole.log(dataQuery);", + "contentType": "application/json", + "getPayload": "return {}", + "highlight": false, + "highlightColor": "red", + "method": "GET", + "payload": {} + }, + "layout": { + "orientation": "horizontal", + "padding": 10, + "sectionVariant": "default", + "variant": "single" + }, + "reset": { + "backgroundColor": "purple", + "foregroundColor": "yellow", + "icon": "process", + "text": "Reset", + "variant": "hidden" + }, + "resetAction": { + "code": "if (context.panel.response) {\n context.grafana.notifySuccess(['Update', 'Values updated successfully.']);\n context.grafana.locationService.reload();\n} else {\n context.grafana.notifyError(['Update', 'An error occurred updating values.']);\n}", + "confirm": false, + "getPayload": "return {}", + "mode": "initial", + "payload": {} + }, + "saveDefault": { + "icon": "save", + "text": "Save Default", + "variant": "hidden" + }, + "submit": { + "backgroundColor": "purple", + "foregroundColor": "yellow", + "icon": "cloud-upload", + "text": "Submit", + "variant": "primary" + }, + "sync": true, + "update": { + "code": "if (context.panel.response) {\n context.grafana.notifySuccess(['Update', 'Values updated successfully.']);\n context.grafana.locationService.reload();\n} else {\n context.grafana.notifyError(['Update', 'An error occurred updating values.']);\n}", + "confirm": false, + "contentType": "application/json", + "getPayload": "const payload = {};\n\ncontext.panel.elements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\nreturn payload;", + "method": "-", + "payload": "const payload = {};\n\ncontext.panel.elements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\nreturn payload;", + "payloadMode": "all" + }, + "updateEnabled": "auto" + }, + "pluginVersion": "4.9.0", + "title": "Empty URL", + "type": "volkovlabs-form-panel" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 21, + "w": 12, + "x": 12, + "y": 6 + }, + "id": 3, + "options": { + "buttonGroup": { + "orientation": "center", + "size": "md" + }, + "confirmModal": { + "body": "Please confirm to update changed values", + "cancel": "Cancel", + "columns": { + "include": ["name", "oldValue", "newValue"], + "name": "Label", + "newValue": "New Value", + "oldValue": "Old Value" + }, + "confirm": "Confirm", + "elementDisplayMode": "modified", + "title": "Confirm update request" + }, + "elementValueChanged": "", + "elements": [ + { + "accept": "", + "id": "file", + "labelWidth": 15, + "multiple": true, + "section": "", + "title": "file", + "tooltip": "", + "type": "file", + "uid": "d2c7fbe1-e246-40ff-8470-b2a2ff7af762", + "unit": "", + "value": [] }, { "id": "text", - "labelWidth": 10, + "labelWidth": 15, "rows": 4, "title": "Text", "type": "textarea", "uid": "ff544458-ce27-4e30-bac5-58a7f8fbe955" + }, + { + "id": "readOnlyTextArea", + "labelWidth": 15, + "rows": 10, + "section": "", + "title": "Disbled Textbox", + "tooltip": "", + "type": "disabledTextarea", + "uid": "7da514d1-8989-45b6-b42d-6919149573bf", + "unit": "", + "value": "" + }, + { + "height": 200, + "id": "code", + "labelWidth": 15, + "language": "javascript", + "section": "", + "title": "code", + "tooltip": "", + "type": "code", + "uid": "29fd6236-b227-4a10-abfb-dd47a5c66798", + "unit": "", + "value": "" } ], "height": 0, @@ -265,8 +557,8 @@ "width": 0, "widthMode": "auto" }, - "pluginVersion": "4.7.0", - "title": "Single Form", + "pluginVersion": "4.9.0", + "title": "Boxes", "type": "volkovlabs-form-panel" } ], @@ -277,7 +569,6 @@ "list": [ { "current": { - "selected": false, "text": "test", "value": "test" }, diff --git a/provisioning/dashboards/panels.json b/provisioning/dashboards/panels.json index bc5a29ba..1d17da64 100644 --- a/provisioning/dashboards/panels.json +++ b/provisioning/dashboards/panels.json @@ -24,15 +24,18 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 4, + "id": 3, "links": [], - "liveNow": false, "panels": [ { "datasource": { "type": "datasource", "uid": "grafana" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, "gridPos": { "h": 6, "w": 9, @@ -117,7 +120,12 @@ }, "updateEnabled": "auto" }, - "pluginVersion": "4.5.0", + "pluginVersion": "4.9.0", + "targets": [ + { + "refId": "A" + } + ], "title": "No Elements", "type": "volkovlabs-form-panel" }, @@ -126,6 +134,10 @@ "type": "datasource", "uid": "grafana" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, "gridPos": { "h": 12, "w": 15, @@ -233,7 +245,12 @@ }, "updateEnabled": "auto" }, - "pluginVersion": "4.5.0", + "pluginVersion": "4.9.0", + "targets": [ + { + "refId": "A" + } + ], "title": "Data Manipulation Form", "type": "volkovlabs-form-panel" }, @@ -242,6 +259,10 @@ "type": "datasource", "uid": "grafana" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, "gridPos": { "h": 6, "w": 9, @@ -325,7 +346,12 @@ }, "updateEnabled": "auto" }, - "pluginVersion": "4.5.0", + "pluginVersion": "4.9.0", + "targets": [ + { + "refId": "A" + } + ], "title": "Buttons only", "type": "volkovlabs-form-panel" }, @@ -334,6 +360,10 @@ "type": "datasource", "uid": "grafana" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, "gridPos": { "h": 13, "w": 9, @@ -410,7 +440,7 @@ "layout": { "orientation": "vertical", "padding": 10, - "sectionVariant": "default", + "sectionVariant": "collapsable", "sections": [ { "id": "Section 1", @@ -464,8 +494,13 @@ }, "updateEnabled": "auto" }, - "pluginVersion": "4.5.0", - "title": "Replace Variables", + "pluginVersion": "4.9.0", + "targets": [ + { + "refId": "A" + } + ], + "title": "Sections", "type": "volkovlabs-form-panel" }, { @@ -473,6 +508,10 @@ "type": "marcusolsson-static-datasource", "uid": "P1D2C73DC01F2359B" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, "gridPos": { "h": 7, "w": 15, @@ -602,7 +641,12 @@ }, "updateEnabled": "auto" }, - "pluginVersion": "4.5.0", + "pluginVersion": "4.9.0", + "targets": [ + { + "refId": "A" + } + ], "title": "Read Only Updated", "type": "volkovlabs-form-panel" }, @@ -611,6 +655,10 @@ "type": "marcusolsson-static-datasource", "uid": "P1D2C73DC01F2359B" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, "gridPos": { "h": 8, "w": 15, @@ -681,6 +729,7 @@ "width": 0 }, { + "allowCustomValue": false, "id": "type", "labelWidth": 10, "options": [ @@ -773,7 +822,12 @@ }, "updateEnabled": "auto" }, - "pluginVersion": "4.5.0", + "pluginVersion": "4.9.0", + "targets": [ + { + "refId": "A" + } + ], "title": "Control Panel", "type": "volkovlabs-form-panel" }, @@ -782,6 +836,10 @@ "type": "datasource", "uid": "grafana" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, "gridPos": { "h": 13, "w": 9, @@ -843,6 +901,7 @@ "value": 4 }, { + "allowCustomValue": false, "id": "select", "options": [ { @@ -980,7 +1039,12 @@ "width": 0, "widthMode": "auto" }, - "pluginVersion": "4.5.0", + "pluginVersion": "4.9.0", + "targets": [ + { + "refId": "A" + } + ], "title": "Single Form", "type": "volkovlabs-form-panel" }, @@ -989,6 +1053,10 @@ "type": "datasource", "uid": "grafana" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, "gridPos": { "h": 9, "w": 15, @@ -1046,6 +1114,7 @@ "unit": "" }, { + "allowCustomValue": false, "id": "select", "options": [ { @@ -1182,7 +1251,12 @@ "width": 0, "widthMode": "auto" }, - "pluginVersion": "4.5.0", + "pluginVersion": "4.9.0", + "targets": [ + { + "refId": "A" + } + ], "title": "Split Form", "type": "volkovlabs-form-panel" }, @@ -1191,6 +1265,10 @@ "type": "datasource", "uid": "grafana" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, "gridPos": { "h": 4, "w": 9, @@ -1280,23 +1358,27 @@ }, "updateEnabled": "auto" }, - "pluginVersion": "4.5.0", + "pluginVersion": "4.9.0", + "targets": [ + { + "refId": "A" + } + ], "title": "No Initial Request and Custom Button", "type": "volkovlabs-form-panel" } ], + "preload": false, "refresh": "", - "schemaVersion": 39, + "schemaVersion": 40, "tags": [], "templating": { "list": [ { "current": { - "selected": false, "text": "test2", "value": "test2" }, - "hide": 0, "label": "Var", "name": "var", "options": [ @@ -1307,7 +1389,6 @@ } ], "query": "test2", - "skipUrlSync": false, "type": "textbox" } ] @@ -1320,6 +1401,6 @@ "timezone": "", "title": "Panels", "uid": "O4tc_E6Gz", - "version": 4, + "version": 1, "weekStart": "" } diff --git a/provisioning/dashboards/validation.json b/provisioning/dashboards/validation.json index 7f2be9b9..c9d08615 100644 --- a/provisioning/dashboards/validation.json +++ b/provisioning/dashboards/validation.json @@ -18,15 +18,18 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 5, + "id": 7, "links": [], - "liveNow": false, "panels": [ { "datasource": { "type": "marcusolsson-static-datasource", "uid": "P1D2C73DC01F2359B" }, + "fieldConfig": { + "defaults": {}, + "overrides": [] + }, "gridPos": { "h": 8, "w": 12, @@ -55,6 +58,7 @@ "elementValueChanged": "let elements = context.panel.elements;\n\n/**\n * Reset room and bench values on facility change\n */\nif (context.element.id === 'facility') {\n elements = elements.map((element) => {\n if (element.id === 'room' || element.id === 'bench') {\n return {\n ...element,\n value: null,\n }\n }\n\n return element;\n })\n}\n\n/**\n * Reset bench value on room change\n */\nif (context.element.id === 'room') {\n elements = elements.map((element) => {\n if (element.id === 'bench') {\n return {\n ...element,\n value: null,\n }\n }\n\n return element;\n })\n}\n\n/**\n * Update form elements\n */\ncontext.panel.onChangeElements(elements);\n\n/**\n * Caclulate form validation state\n */\nconst isFormValid = elements.every((element) => !!element.value);\n\n/**\n * Enable/Disable submit button\n */\nif (isFormValid) {\n context.panel.enableSubmit();\n} else {\n context.panel.disableSubmit();\n}", "elements": [ { + "allowCustomValue": false, "disableIf": "", "id": "facility", "labelWidth": 10, @@ -83,6 +87,20 @@ "value": "" }, { + "disableIf": "const facility = context.panel.elements.find((element) => element.id === 'facility')\n\nreturn !facility?.value", + "id": "number", + "labelWidth": 10, + "section": "", + "showIf": "", + "title": "Number", + "tooltip": "", + "type": "number", + "uid": "fc06941b-31fa-4d09-95ca-89a43e543ea9", + "unit": "", + "value": 0 + }, + { + "allowCustomValue": false, "disableIf": "const facility = context.panel.elements.find((element) => element.id === 'facility')\n\nreturn !facility?.value", "id": "room", "labelWidth": 10, @@ -110,6 +128,7 @@ "value": "" }, { + "allowCustomValue": false, "disableIf": "const facility = context.panel.elements.find((element) => element.id === 'facility')\nconst room = context.panel.elements.find((element) => element.id === 'room')\n\nreturn !facility?.value || !room?.value", "id": "bench", "labelWidth": 10, @@ -190,7 +209,7 @@ }, "updateEnabled": "manual" }, - "pluginVersion": "4.1.0", + "pluginVersion": "4.9.0", "targets": [ { "datasource": { @@ -204,8 +223,9 @@ "type": "volkovlabs-form-panel" } ], + "preload": false, "refresh": "", - "schemaVersion": 39, + "schemaVersion": 40, "tags": [], "templating": { "list": [] diff --git a/src/components/CustomButtonsRow/CustomButtonsRow.tsx b/src/components/CustomButtonsRow/CustomButtonsRow.tsx index 05b66f98..ebb7e81b 100644 --- a/src/components/CustomButtonsRow/CustomButtonsRow.tsx +++ b/src/components/CustomButtonsRow/CustomButtonsRow.tsx @@ -1,8 +1,8 @@ import { InterpolateFunction, PanelData } from '@grafana/data'; import React, { useMemo } from 'react'; -import { FormElementType } from '../../constants'; -import { CustomButtonShow, ExecuteCustomCodeParams, LocalFormElement } from '../../types'; +import { CustomButtonShow, ExecuteCustomCodeParams, FormElementType, LocalFormElement } from '@/types'; + import { CustomButtonElement } from '../FormElement/components'; /** diff --git a/src/components/ElementEditor/ElementEditor.tsx b/src/components/ElementEditor/ElementEditor.tsx index 41641a45..0277d753 100644 --- a/src/components/ElementEditor/ElementEditor.tsx +++ b/src/components/ElementEditor/ElementEditor.tsx @@ -22,7 +22,6 @@ import { CUSTOM_BUTTON_VARIANT_OPTIONS, CUSTOM_VALUE_OPTIONS, FORM_ELEMENT_TYPE_OPTIONS, - FormElementType, LINK_TARGET_OPTIONS, OPTIONS_SOURCE_OPTIONS, OptionsSource, @@ -30,15 +29,16 @@ import { STRING_ELEMENT_OPTIONS, TEST_IDS, TIME_TRANSFORMATION_OPTIONS, -} from '../../constants'; -import { ButtonVariant, CodeLanguage, LocalFormElement } from '../../types'; +} from '@/constants'; +import { ButtonVariant, CodeLanguage, FormElementType, LocalFormElement } from '@/types'; import { formatNumberValue, getElementWithNewType, getOptionsWithTestId, isFormElementType, toNumberValue, -} from '../../utils'; +} from '@/utils'; + import { ElementDateEditor } from '../ElementDateEditor'; import { ElementOptionsEditor } from '../ElementOptionsEditor'; import { ElementQueryOptionsEditor } from '../ElementQueryOptionsEditor'; diff --git a/src/components/ElementOptionsEditor/ElementOptionsEditor.tsx b/src/components/ElementOptionsEditor/ElementOptionsEditor.tsx index 3a88e18c..265bda31 100644 --- a/src/components/ElementOptionsEditor/ElementOptionsEditor.tsx +++ b/src/components/ElementOptionsEditor/ElementOptionsEditor.tsx @@ -4,14 +4,10 @@ import { DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDr import { Collapse } from '@volkovlabs/components'; import React, { ChangeEvent, useCallback, useState } from 'react'; -import { - FORM_ELEMENT_OPTION_DEFAULT, - FormElementType, - ICON_OPTIONS, - SELECT_ELEMENT_OPTIONS, - TEST_IDS, -} from '../../constants'; -import { reorder } from '../../utils'; +import { FORM_ELEMENT_OPTION_DEFAULT, ICON_OPTIONS, SELECT_ELEMENT_OPTIONS, TEST_IDS } from '@/constants'; +import { FormElementType } from '@/types'; +import { reorder } from '@/utils'; + import { getStyles } from './ElementOptionsEditor.styles'; /** diff --git a/src/components/ElementSections/ElementSections.test.tsx b/src/components/ElementSections/ElementSections.test.tsx index 851bf855..30d30fcf 100644 --- a/src/components/ElementSections/ElementSections.test.tsx +++ b/src/components/ElementSections/ElementSections.test.tsx @@ -1,15 +1,10 @@ import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; -import { - FORM_ELEMENT_DEFAULT, - FormElementType, - LayoutOrientation, - LayoutVariant, - SectionVariant, -} from '../../constants'; -import { PanelOptions, UpdateEnabledMode } from '../../types'; -import { getFormElementsSectionSelectors, normalizeElementsForLocalState } from '../../utils'; +import { FORM_ELEMENT_DEFAULT, LayoutOrientation, LayoutVariant, SectionVariant } from '@/constants'; +import { FormElementType, PanelOptions, UpdateEnabledMode } from '@/types'; +import { getFormElementsSectionSelectors, normalizeElementsForLocalState } from '@/utils'; + import { ElementSections } from './ElementSections'; /** diff --git a/src/components/FormElement/FormElement.tsx b/src/components/FormElement/FormElement.tsx index 48ea516f..10b2a045 100644 --- a/src/components/FormElement/FormElement.tsx +++ b/src/components/FormElement/FormElement.tsx @@ -3,8 +3,8 @@ import { InterpolateFunction } from '@grafana/data'; import { InlineFieldRow, InlineLabel, useStyles2 } from '@grafana/ui'; import React from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { CustomButtonShow, ExecuteCustomCodeParams, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { CustomButtonShow, ExecuteCustomCodeParams, FormElementType, LocalFormElement } from '@/types'; import { BooleanElement, diff --git a/src/components/FormElement/components/BooleanElement/BooleanElement.tsx b/src/components/FormElement/components/BooleanElement/BooleanElement.tsx index ea18e8f2..4e329137 100644 --- a/src/components/FormElement/components/BooleanElement/BooleanElement.tsx +++ b/src/components/FormElement/components/BooleanElement/BooleanElement.tsx @@ -1,8 +1,8 @@ import { InlineField, RadioButtonGroup } from '@grafana/ui'; import React from 'react'; -import { BOOLEAN_ELEMENT_OPTIONS, FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { BOOLEAN_ELEMENT_OPTIONS, TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; /** diff --git a/src/components/FormElement/components/CheckboxListElement/CheckboxListElement.tsx b/src/components/FormElement/components/CheckboxListElement/CheckboxListElement.tsx index 9f71c17c..bb2cb248 100644 --- a/src/components/FormElement/components/CheckboxListElement/CheckboxListElement.tsx +++ b/src/components/FormElement/components/CheckboxListElement/CheckboxListElement.tsx @@ -2,8 +2,8 @@ import { cx } from '@emotion/css'; import { Checkbox, InlineField, useStyles2, useTheme2 } from '@grafana/ui'; import React from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; import { getStyles } from './CheckboxListElement.style'; diff --git a/src/components/FormElement/components/CodeElement/CodeElement.tsx b/src/components/FormElement/components/CodeElement/CodeElement.tsx index bb131fd4..73dd3d50 100644 --- a/src/components/FormElement/components/CodeElement/CodeElement.tsx +++ b/src/components/FormElement/components/CodeElement/CodeElement.tsx @@ -2,8 +2,8 @@ import { InlineField } from '@grafana/ui'; import { AutosizeCodeEditor } from '@volkovlabs/components'; import React, { useMemo } from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { CodeLanguage, FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { CodeLanguage, FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; /** diff --git a/src/components/FormElement/components/CustomButtonElement/CustomButtonElement.tsx b/src/components/FormElement/components/CustomButtonElement/CustomButtonElement.tsx index c2fa76f7..5d7b5be2 100644 --- a/src/components/FormElement/components/CustomButtonElement/CustomButtonElement.tsx +++ b/src/components/FormElement/components/CustomButtonElement/CustomButtonElement.tsx @@ -2,8 +2,15 @@ import { InterpolateFunction } from '@grafana/data'; import { Button, InlineField, useTheme2 } from '@grafana/ui'; import React, { useCallback } from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { ButtonVariant, CustomButtonShow, ExecuteCustomCodeParams, FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { + ButtonVariant, + CustomButtonShow, + ExecuteCustomCodeParams, + FormElementByType, + FormElementType, + LocalFormElement, +} from '@/types'; import { applyLabelStyles, applyWidth, getButtonVariant } from '@/utils'; /** diff --git a/src/components/FormElement/components/DateTimeElement/DateTimeElement.tsx b/src/components/FormElement/components/DateTimeElement/DateTimeElement.tsx index 1e0256cf..b4808938 100644 --- a/src/components/FormElement/components/DateTimeElement/DateTimeElement.tsx +++ b/src/components/FormElement/components/DateTimeElement/DateTimeElement.tsx @@ -2,8 +2,8 @@ import { DateTime, dateTime } from '@grafana/data'; import { DatePickerWithInput, DateTimePicker, InlineField } from '@grafana/ui'; import React from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; /** diff --git a/src/components/FormElement/components/DisabledElement/DisabledElement.tsx b/src/components/FormElement/components/DisabledElement/DisabledElement.tsx index fa576735..4a46b909 100644 --- a/src/components/FormElement/components/DisabledElement/DisabledElement.tsx +++ b/src/components/FormElement/components/DisabledElement/DisabledElement.tsx @@ -1,8 +1,8 @@ import { InlineField, Input } from '@grafana/ui'; import React from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; /** diff --git a/src/components/FormElement/components/DisabledTextAreaElement/DisabledTextAreaElement.tsx b/src/components/FormElement/components/DisabledTextAreaElement/DisabledTextAreaElement.tsx index 5b13f7cb..9364c348 100644 --- a/src/components/FormElement/components/DisabledTextAreaElement/DisabledTextAreaElement.tsx +++ b/src/components/FormElement/components/DisabledTextAreaElement/DisabledTextAreaElement.tsx @@ -1,8 +1,8 @@ import { InlineField, TextArea } from '@grafana/ui'; import React from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; /** diff --git a/src/components/FormElement/components/FileElement/FileElement.tsx b/src/components/FormElement/components/FileElement/FileElement.tsx index d87605a9..ba20e872 100644 --- a/src/components/FormElement/components/FileElement/FileElement.tsx +++ b/src/components/FormElement/components/FileElement/FileElement.tsx @@ -1,8 +1,8 @@ import { FileDropzone, InlineField } from '@grafana/ui'; import React from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyAcceptedFiles, applyLabelStyles, applyWidth } from '@/utils'; /** diff --git a/src/components/FormElement/components/LinkElement/LinkElement.tsx b/src/components/FormElement/components/LinkElement/LinkElement.tsx index d7e0ab53..1482e513 100644 --- a/src/components/FormElement/components/LinkElement/LinkElement.tsx +++ b/src/components/FormElement/components/LinkElement/LinkElement.tsx @@ -1,8 +1,8 @@ import { InlineField, TextLink, useStyles2, useTheme2 } from '@grafana/ui'; import React from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LinkTarget, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LinkTarget, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; import { getStyles } from './LinkElement.style'; diff --git a/src/components/FormElement/components/NumberElement/NumberElement.tsx b/src/components/FormElement/components/NumberElement/NumberElement.tsx index d90a5892..1d1065e9 100644 --- a/src/components/FormElement/components/NumberElement/NumberElement.tsx +++ b/src/components/FormElement/components/NumberElement/NumberElement.tsx @@ -2,8 +2,8 @@ import { InlineField } from '@grafana/ui'; import { NumberInput } from '@volkovlabs/components'; import React from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth, formatNumberValue } from '@/utils'; /** diff --git a/src/components/FormElement/components/PasswordElement/PasswordElement.tsx b/src/components/FormElement/components/PasswordElement/PasswordElement.tsx index 5cbcb944..98630647 100644 --- a/src/components/FormElement/components/PasswordElement/PasswordElement.tsx +++ b/src/components/FormElement/components/PasswordElement/PasswordElement.tsx @@ -1,8 +1,8 @@ import { InlineField, Input } from '@grafana/ui'; import React, { ChangeEvent } from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; /** diff --git a/src/components/FormElement/components/RadioElement/RadioElement.tsx b/src/components/FormElement/components/RadioElement/RadioElement.tsx index 3bcf6b35..5ce195b2 100644 --- a/src/components/FormElement/components/RadioElement/RadioElement.tsx +++ b/src/components/FormElement/components/RadioElement/RadioElement.tsx @@ -1,8 +1,8 @@ import { InlineField, RadioButtonGroup } from '@grafana/ui'; import React from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; /** diff --git a/src/components/FormElement/components/SelectElement/SelectElement.tsx b/src/components/FormElement/components/SelectElement/SelectElement.tsx index fbf254d2..5cc6e94c 100644 --- a/src/components/FormElement/components/SelectElement/SelectElement.tsx +++ b/src/components/FormElement/components/SelectElement/SelectElement.tsx @@ -1,8 +1,8 @@ import { InlineField, Select } from '@grafana/ui'; import React from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; /** @@ -47,6 +47,7 @@ export const SelectElement: React.FC = ({ element, onChange, highlightCla key={element.id} isMulti={element.type === FormElementType.MULTISELECT} aria-label={TEST_IDS.formElements.fieldSelect} + data-testid={TEST_IDS.formElements.fieldSelect} value={element.value !== undefined ? element.value : null} allowCustomValue={element.allowCustomValue} onChange={(event) => { diff --git a/src/components/FormElement/components/SliderElement/SliderElement.tsx b/src/components/FormElement/components/SliderElement/SliderElement.tsx index 8ba0db40..6e2ac21d 100644 --- a/src/components/FormElement/components/SliderElement/SliderElement.tsx +++ b/src/components/FormElement/components/SliderElement/SliderElement.tsx @@ -3,8 +3,8 @@ import { InlineField, Input, useStyles2 } from '@grafana/ui'; import Slider from 'rc-slider'; import React from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; import { getStyles } from './SliderElement.style'; diff --git a/src/components/FormElement/components/StringElement/StringElement.tsx b/src/components/FormElement/components/StringElement/StringElement.tsx index 3ae7787d..d69198fc 100644 --- a/src/components/FormElement/components/StringElement/StringElement.tsx +++ b/src/components/FormElement/components/StringElement/StringElement.tsx @@ -2,8 +2,8 @@ import { cx } from '@emotion/css'; import { InlineField, Input, useStyles2 } from '@grafana/ui'; import React, { ChangeEvent } from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; import { getStyles } from './StringElement.style'; diff --git a/src/components/FormElement/components/TextAreaElement/TextAreaElement.tsx b/src/components/FormElement/components/TextAreaElement/TextAreaElement.tsx index 44cc5692..0f031c66 100644 --- a/src/components/FormElement/components/TextAreaElement/TextAreaElement.tsx +++ b/src/components/FormElement/components/TextAreaElement/TextAreaElement.tsx @@ -1,8 +1,8 @@ import { InlineField, TextArea } from '@grafana/ui'; import React, { ChangeEvent } from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyLabelStyles, applyWidth } from '@/utils'; /** diff --git a/src/components/FormElement/components/TimeElement/TimeElement.tsx b/src/components/FormElement/components/TimeElement/TimeElement.tsx index 6ebba517..85c2234e 100644 --- a/src/components/FormElement/components/TimeElement/TimeElement.tsx +++ b/src/components/FormElement/components/TimeElement/TimeElement.tsx @@ -2,8 +2,8 @@ import { DateTime, dateTimeForTimeZone, getTimeZone } from '@grafana/data'; import { InlineField, TimeOfDayPicker } from '@grafana/ui'; import React, { useCallback } from 'react'; -import { FormElementType, TEST_IDS } from '@/constants'; -import { FormElementByType, LocalFormElement } from '@/types'; +import { TEST_IDS } from '@/constants'; +import { FormElementByType, FormElementType, LocalFormElement } from '@/types'; import { applyWidth } from '@/utils'; /** diff --git a/src/components/FormElements/FormElements.test.tsx b/src/components/FormElements/FormElements.test.tsx index ac08176f..cf68d138 100644 --- a/src/components/FormElements/FormElements.test.tsx +++ b/src/components/FormElements/FormElements.test.tsx @@ -2,9 +2,10 @@ import { toDataFrame } from '@grafana/data'; import { act, fireEvent, render, screen, within } from '@testing-library/react'; import React from 'react'; -import { FORM_ELEMENT_DEFAULT, FormElementType, OptionsSource } from '../../constants'; -import { ButtonVariant, CustomButtonShow, LinkTarget } from '../../types'; -import { getFormElementsSelectors, normalizeElementsForLocalState } from '../../utils'; +import { FORM_ELEMENT_DEFAULT, OptionsSource } from '@/constants'; +import { ButtonVariant, CustomButtonShow, FormElementType, LinkTarget } from '@/types'; +import { getFormElementsSelectors, normalizeElementsForLocalState } from '@/utils'; + import { FormElements } from './FormElements'; /** diff --git a/src/components/FormElementsEditor/FormElementsEditor.test.tsx b/src/components/FormElementsEditor/FormElementsEditor.test.tsx index b534e3c0..0cea780c 100644 --- a/src/components/FormElementsEditor/FormElementsEditor.test.tsx +++ b/src/components/FormElementsEditor/FormElementsEditor.test.tsx @@ -8,15 +8,15 @@ import { CUSTOM_BUTTON_DEFAULT, FORM_ELEMENT_DEFAULT, FORM_ELEMENT_OPTION_DEFAULT, - FormElementType, NUMBER_DEFAULT, OptionsSource, RequestMethod, SELECT_DEFAULT, SLIDER_DEFAULT, -} from '../../constants'; -import { ButtonVariant, CodeLanguage, LinkTarget } from '../../types'; -import { getFormElementsEditorSelectors } from '../../utils'; +} from '@/constants'; +import { ButtonVariant, CodeLanguage, FormElementType, LinkTarget } from '@/types'; +import { getFormElementsEditorSelectors } from '@/utils'; + import { FormElementsEditor } from './FormElementsEditor'; /** diff --git a/src/components/FormPanel/FormPanel.test.tsx b/src/components/FormPanel/FormPanel.test.tsx index c910c58a..0781f733 100644 --- a/src/components/FormPanel/FormPanel.test.tsx +++ b/src/components/FormPanel/FormPanel.test.tsx @@ -10,7 +10,6 @@ import { CONFIRM_MODAL_DEFAULT, ContentType, FORM_ELEMENT_DEFAULT, - FormElementType, LayoutOrientation, LayoutVariant, PayloadMode, @@ -24,6 +23,7 @@ import { ButtonVariant, CustomButtonShow, FormElement, + FormElementType, LocalFormElement, ModalColumnName, PanelOptions, diff --git a/src/components/FormPanel/FormPanel.tsx b/src/components/FormPanel/FormPanel.tsx index d350ba53..ec0b8aaa 100644 --- a/src/components/FormPanel/FormPanel.tsx +++ b/src/components/FormPanel/FormPanel.tsx @@ -29,7 +29,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ConfirmationElementDisplayMode, ContentType, - FormElementType, LayoutVariant, LoadingMode, PayloadMode, @@ -41,6 +40,7 @@ import { useFormElements, useMutableState } from '@/hooks'; import { ButtonVariant, FormElement, + FormElementType, LocalFormElement, ModalColumnName, PanelOptions, diff --git a/src/constants/default.ts b/src/constants/default.ts index bc97aa9c..062fbfa9 100644 --- a/src/constants/default.ts +++ b/src/constants/default.ts @@ -7,6 +7,7 @@ import { CustomButtonOptions, CustomButtonShow, FormElement, + FormElementType, ModalColumnName, ModalOptions, NumberOptions, @@ -15,7 +16,7 @@ import { TextareaOptions, } from '../types'; import { CODE_EDITOR_CONFIG } from './code-editor'; -import { FormElementType, OptionsSource } from './form-element'; +import { OptionsSource } from './form-element'; import { ConfirmationElementDisplayMode } from './request'; /** diff --git a/src/constants/form-element.ts b/src/constants/form-element.ts index 429d0f8f..f389b82c 100644 --- a/src/constants/form-element.ts +++ b/src/constants/form-element.ts @@ -1,35 +1,9 @@ import { SelectableValue } from '@grafana/data'; import { getAvailableIcons } from '@grafana/ui'; -import { ButtonSize, ButtonVariant, CustomButtonShow, LinkTarget } from '../types'; +import { ButtonSize, ButtonVariant, CustomButtonShow, FormElementType, LinkTarget } from '../types'; import { TEST_IDS } from './tests'; -/** - * Form Element Type - */ -export const enum FormElementType { - BOOLEAN = 'boolean', - CODE = 'code', - BUTTON = 'button', - DATE = 'date', - DATETIME = 'datetime', - DISABLED = 'disabled', - DISABLED_TEXTAREA = 'disabledTextarea', - FILE = 'file', - LINK = 'link', - MULTISELECT = 'multiselect', - NUMBER = 'number', - PASSWORD = 'password', - RADIO = 'radio', - SECRET = 'secret', - SELECT = 'select', - SLIDER = 'slider', - STRING = 'string', - TEXTAREA = 'textarea', - TIME = 'time', - CHECKBOX_LIST = 'checkboxList', -} - /** * Form Element Type Options */ diff --git a/src/migration.test.ts b/src/migration.test.ts index 9195a27b..282b29cd 100644 --- a/src/migration.test.ts +++ b/src/migration.test.ts @@ -1,8 +1,8 @@ import { getBackendSrv } from '@grafana/runtime'; -import { FormElementType, PayloadMode } from './constants'; +import { PayloadMode } from './constants'; import { getMigratedOptions } from './migration'; -import { PanelOptions } from './types'; +import { FormElementType, PanelOptions } from './types'; /** * Mock @grafana/runtime diff --git a/src/migration.ts b/src/migration.ts index a34cfbd8..6fbce179 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -3,8 +3,8 @@ import { getBackendSrv } from '@grafana/runtime'; import { set } from 'lodash'; import semver from 'semver'; -import { FormElementType, PayloadMode } from './constants'; -import { LayoutOptions, LayoutSection, PanelOptions, RequestOptions } from './types'; +import { PayloadMode } from './constants'; +import { FormElementType, LayoutOptions, LayoutSection, PanelOptions, RequestOptions } from './types'; /** * Outdated Request Options diff --git a/src/plugin.json b/src/plugin.json index 9ea673bb..f04afabf 100644 --- a/src/plugin.json +++ b/src/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json", "dependencies": { - "grafanaDependency": ">=10.0.0", + "grafanaDependency": ">=10.3.0", "plugins": [] }, "id": "volkovlabs-form-panel", diff --git a/src/types/form-element.ts b/src/types/form-element.ts index 2d5111a3..5c6b00cf 100644 --- a/src/types/form-element.ts +++ b/src/types/form-element.ts @@ -1,9 +1,35 @@ import { DataQueryResponse, IconName, InterpolateFunction, PanelData, SelectableValue } from '@grafana/data'; import { FetchResponse } from '@grafana/runtime'; -import { FormElementType, OptionsSource } from '../constants'; +import { OptionsSource } from '../constants'; import { ButtonSize, ButtonVariant, CodeLanguage } from '../types'; +/** + * Form Element Type + */ +export const enum FormElementType { + BOOLEAN = 'boolean', + CODE = 'code', + BUTTON = 'button', + DATE = 'date', + DATETIME = 'datetime', + DISABLED = 'disabled', + DISABLED_TEXTAREA = 'disabledTextarea', + FILE = 'file', + LINK = 'link', + MULTISELECT = 'multiselect', + NUMBER = 'number', + PASSWORD = 'password', + RADIO = 'radio', + SECRET = 'secret', + SELECT = 'select', + SLIDER = 'slider', + STRING = 'string', + TEXTAREA = 'textarea', + TIME = 'time', + CHECKBOX_LIST = 'checkboxList', +} + /** * Query Field */ diff --git a/src/utils/form-element.test.ts b/src/utils/form-element.test.ts index 3dfd2422..053637a1 100644 --- a/src/utils/form-element.test.ts +++ b/src/utils/form-element.test.ts @@ -1,8 +1,8 @@ /* eslint-disable no-console */ import { css, keyframes } from '@emotion/css'; -import { FormElementType, OptionsSource } from '../constants'; -import { ButtonVariant } from '../types'; +import { OptionsSource } from '../constants'; +import { ButtonVariant, FormElementType } from '../types'; import { applyAcceptedFiles, applyLabelStyles, diff --git a/src/utils/form-element.ts b/src/utils/form-element.ts index b310c1d9..0f6a2c78 100644 --- a/src/utils/form-element.ts +++ b/src/utils/form-element.ts @@ -6,7 +6,6 @@ import { v4 as uuidv4 } from 'uuid'; import { CODE_DEFAULT, CUSTOM_BUTTON_DEFAULT, - FormElementType, NUMBER_DEFAULT, OptionsSource, SELECT_DEFAULT, @@ -18,6 +17,7 @@ import { DisableIfHelper, FormElement, FormElementByType, + FormElementType, GetOptionsHelper, LayoutSection, LinkTarget, diff --git a/src/utils/request.test.ts b/src/utils/request.test.ts index 76480d57..f1b5dbc9 100644 --- a/src/utils/request.test.ts +++ b/src/utils/request.test.ts @@ -1,4 +1,5 @@ -import { FormElementType, PayloadMode } from '@/constants'; +import { PayloadMode } from '@/constants'; +import { FormElementType } from '@/types'; import { getPayloadForRequest, toFormData, toJson } from './request'; diff --git a/src/utils/request.ts b/src/utils/request.ts index 3d8dc6da..69e166e8 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,7 +1,7 @@ import { InterpolateFunction } from '@grafana/data'; -import { FormElementType, PayloadMode } from '../constants'; -import { LocalFormElement, RequestOptions } from '../types'; +import { PayloadMode } from '../constants'; +import { FormElementType, LocalFormElement, RequestOptions } from '../types'; import { getPayloadCodeParameters } from './code-parameters'; /** diff --git a/test/panel.spec.ts b/test/panel.spec.ts index c7465ec9..840f216a 100644 --- a/test/panel.spec.ts +++ b/test/panel.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from '@grafana/plugin-e2e'; -import { TEST_IDS } from '../src/constants/tests'; +import { ModalHelper, PanelHelper } from './utils'; +import { FormElementType } from '../src/types/form-element'; test.describe('Data Manipulation Panel', () => { test('Check grafana version', async ({ grafanaVersion }) => { @@ -7,25 +8,441 @@ test.describe('Data Manipulation Panel', () => { expect(grafanaVersion).toEqual(grafanaVersion); }); - test('Should display a Form', async ({ gotoDashboardPage, dashboardPage, page }) => { - /** - * Go To E2E dashboard - * return dashboardPage - */ - await gotoDashboardPage({ uid: 'ddab9faf-2c15-431a-a047-9a7a6a0aed71' }); - - /** - * Find panel by title with chart - * Should be visible - */ - await expect(dashboardPage.getPanelByTitle('Single Form').locator).toBeVisible(); - - await page.waitForTimeout(2500); - /** - * Check and compare image - */ - await expect( - dashboardPage.getPanelByTitle('Single Form').locator.getByTestId(TEST_IDS.panel.root) - ).toHaveScreenshot('actual-screenshot.png', { maxDiffPixelRatio: 0.1 }); + test.describe('Base', () => { + test('Should display a Form panels with all elements', async ({ gotoDashboardPage, readProvisionedDashboard }) => { + /** + * Go To Panels dashboard e2e.json + * return dashboardPage + */ + const dashboard = await readProvisionedDashboard({ fileName: 'e2e.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Single Form'); + + await panel.checkIfNoErrors(); + await panel.checkPresence(); + + const elements = panel.getElements(); + + /** + * Check all elements Presence + */ + await elements.checkPresence(); + await elements.checkElementPresence('dateTime', FormElementType.DATETIME); + await elements.checkElementPresence('time', FormElementType.TIME); + await elements.checkElementPresence('date', FormElementType.DATE); + await elements.checkElementPresence('amount', FormElementType.NUMBER); + await elements.checkElementPresence('updated', FormElementType.BOOLEAN); + await elements.checkElementPresence('name', FormElementType.STRING); + await elements.checkElementPresence('step', FormElementType.SLIDER); + await elements.checkElementPresence('select', FormElementType.SELECT); + await elements.checkElementPresence('radio', FormElementType.RADIO); + await elements.checkElementPresence('password', FormElementType.PASSWORD); + await elements.checkElementPresence('disabled', FormElementType.DISABLED); + await elements.checkElementPresence('link', FormElementType.LINK); + await elements.checkElementPresence('checkbox', FormElementType.CHECKBOX_LIST); + + const panelBoxes = new PanelHelper(dashboardPage, 'Boxes'); + await panelBoxes.checkIfNoErrors(); + await panelBoxes.checkPresence(); + + const boxesElements = panelBoxes.getElements(); + await boxesElements.checkPresence(); + await boxesElements.checkElementPresence('file', FormElementType.FILE); + await boxesElements.checkElementPresence('text', FormElementType.TEXTAREA); + await boxesElements.checkElementPresence('readOnlyTextArea', FormElementType.DISABLED_TEXTAREA); + await boxesElements.checkElementPresence('code', FormElementType.CODE); + }); + + test('Should add empty Form panel', async ({ gotoDashboardPage, readProvisionedDashboard }) => { + /** + * Go To Panels dashboard e2e.json + * return dashboardPage + */ + const dashboard = await readProvisionedDashboard({ fileName: 'e2e.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Add new visualization + */ + const editPage = await dashboardPage.addPanel(); + await editPage.setVisualization('Business Forms'); + await editPage.setPanelTitle('Business Forms Empty'); + await editPage.backToDashboard(); + + /** + * Should add empty visualization without errors + */ + const panel = new PanelHelper(dashboardPage, 'Business Forms Empty'); + await panel.checkIfNoErrors(); + await panel.checkPresence(); + await panel.checkAlertMessage(); + }); + + test('Should add Form panel with base element', async ({ gotoDashboardPage, readProvisionedDashboard, page }) => { + /** + * Go To Panels dashboard e2e.json + * return dashboardPage + */ + const dashboard = await readProvisionedDashboard({ fileName: 'e2e.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Add new visualization + */ + const editPage = await dashboardPage.addPanel(); + await editPage.setVisualization('Business Forms'); + await editPage.setPanelTitle('Business Form New'); + + const panel = new PanelHelper(dashboardPage, 'Business Form New'); + const editor = panel.getPanelEditor(page.locator('body'), editPage); + + await editor.addElement('string', 'Name', 'String input'); + + /** + * Apply changes and return to dashboard + */ + await editPage.backToDashboard(); + + /** + * Check Presence + */ + await panel.checkPresence(); + await panel.checkIfNoErrors(); + + const elements = panel.getElements(); + + await elements.checkPresence(); + await elements.checkElementPresence('string', FormElementType.STRING); + }); + }); + + test.describe('Initial request', () => { + test('Should set initial values from Datasource', async ({ gotoDashboardPage, readProvisionedDashboard }) => { + /** + * Go To Panels dashboard datasource.json + * return dashboardPage + */ + const dashboard = await readProvisionedDashboard({ fileName: 'datasource.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Data Source'); + + await panel.checkIfNoErrors(); + await panel.checkPresence(); + + const sections = panel.getSections(); + await sections.checkSectionPresence('Current Values'); + await sections.checkElementsCountInSection('Current Values', 3); + + const elements = panel.getElements(); + const disabledMaxElement = await elements.getDisabledElement('max', 'disabled'); + const disabledMinElement = await elements.getDisabledElement('min', 'disabled'); + const disabledSpeedElement = await elements.getDisabledElement('speed', 'disabled'); + + await disabledMaxElement.checkValue('100'); + await disabledMinElement.checkValue('10'); + await disabledSpeedElement.checkValue('54'); + }); + + test('Should set initial values from Query', async ({ gotoDashboardPage, readProvisionedDashboard }) => { + /** + * Go To Panels dashboard datasource.json + * return dashboardPage + */ + const variableParams = new URLSearchParams(); + variableParams.set('var-device', 'device2'); + + const dashboard = await readProvisionedDashboard({ fileName: 'datasource.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid, queryParams: variableParams }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Query'); + + await panel.checkIfNoErrors(); + await panel.checkPresence(); + + const sections = panel.getSections(); + await sections.checkSectionPresence('Current Values'); + await sections.checkElementsCountInSection('Current Values', 3); + + const elements = panel.getElements(); + const disabledMaxElement = await elements.getDisabledElement('max', 'disabled'); + const disabledMinElement = await elements.getDisabledElement('min', 'disabled'); + const disabledSpeedElement = await elements.getDisabledElement('speed', 'disabled'); + + await disabledMaxElement.checkValue('60'); + await disabledMinElement.checkValue('0'); + await disabledSpeedElement.checkValue('10'); + + variableParams.set('var-device', 'device1'); + await gotoDashboardPage({ uid: dashboard.uid, queryParams: variableParams }); + + await disabledMaxElement.checkValue('100'); + await disabledMinElement.checkValue('10'); + await disabledSpeedElement.checkValue('54'); + }); + + test('Should display error for get initial request without URL', async ({ + gotoDashboardPage, + readProvisionedDashboard, + }) => { + /** + * Go To Panels dashboard e2e.json + * return dashboardPage + */ + const dashboard = await readProvisionedDashboard({ fileName: 'e2e.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Empty URL'); + await panel.checkPresence(); + await panel.checkErrorMessage(); + }); + }); + + test.describe('Update', () => { + test('Should update values', async ({ gotoDashboardPage, readProvisionedDashboard }) => { + /** + * Go To Panels dashboard datasource.json + * return dashboardPage + */ + + const dashboard = await readProvisionedDashboard({ fileName: 'datasource.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Data Source'); + + await panel.checkIfNoErrors(); + await panel.checkPresence(); + + const sections = panel.getSections(); + await sections.checkSectionPresence('Current Values'); + await sections.checkSectionPresence('New Values'); + await sections.checkElementsCountInSection('Current Values', 3); + await sections.checkElementsCountInSection('New Values', 3); + + const elements = panel.getElements(); + const disabledMaxElement = await elements.getDisabledElement('max', 'disabled'); + const buttons = panel.getButtons(); + + await disabledMaxElement.checkValue('100'); + await buttons.checkSubmitButtonPresence(); + await buttons.checkSubmitButtonIsDisabled(); + + const numberMaxElement = await elements.getNumberElement('max', 'number'); + await numberMaxElement.checkValue('100'); + await numberMaxElement.setValue('125'); + + await buttons.checkSubmitButtonIsNotDisabled(); + await buttons.submit(); + + const confirmModal = new ModalHelper(dashboardPage); + await confirmModal.checkPresence(); + await confirmModal.confirmButtonCheckPresence(); + await confirmModal.cancelButtonCheckPresence(); + await confirmModal.updateValues(); + + await buttons.checkSubmitButtonPresence(); + await numberMaxElement.checkValue('125'); + await disabledMaxElement.checkValue('125'); + + /** + * Return to initial + */ + await numberMaxElement.setValue('100'); + await buttons.submit(); + await confirmModal.updateValues(); + }); + + test('Should not update values if cancel', async ({ gotoDashboardPage, readProvisionedDashboard }) => { + /** + * Go To Panels dashboard datasource.json + * return dashboardPage + */ + + const dashboard = await readProvisionedDashboard({ fileName: 'datasource.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Data Source'); + + await panel.checkIfNoErrors(); + await panel.checkPresence(); + + const elements = panel.getElements(); + const disabledMaxElement = await elements.getDisabledElement('max', 'disabled'); + + const buttons = panel.getButtons(); + + await disabledMaxElement.checkValue('100'); + await buttons.checkSubmitButtonPresence(); + await buttons.checkSubmitButtonIsDisabled(); + + const numberMaxElement = await elements.getNumberElement('max', 'number'); + await numberMaxElement.setValue('125'); + + await buttons.submit(); + + const confirmModal = new ModalHelper(dashboardPage); + + await confirmModal.cancelButtonCheckPresence(); + await confirmModal.cancelUpdateValues(); + await confirmModal.checkNotPresence(); + await disabledMaxElement.checkValue('100'); + }); + + test('Should reset values without update it', async ({ gotoDashboardPage, readProvisionedDashboard }) => { + /** + * Go To Panels dashboard datasource.json + * return dashboardPage + */ + + const dashboard = await readProvisionedDashboard({ fileName: 'datasource.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Data Source'); + + await panel.checkIfNoErrors(); + await panel.checkPresence(); + + const elements = panel.getElements(); + const disabledMaxElement = await elements.getDisabledElement('max', 'disabled'); + + const buttons = panel.getButtons(); + + await disabledMaxElement.checkValue('100'); + + const numberMaxElement = await elements.getNumberElement('max', 'number'); + const numberMinElement = await elements.getNumberElement('min', 'number'); + await numberMaxElement.checkValue('100'); + await numberMinElement.checkValue('10'); + + await numberMaxElement.setValue('115'); + await numberMinElement.setValue('15'); + + await buttons.reset(); + + await numberMaxElement.checkValue('100'); + await numberMinElement.checkValue('10'); + }); + + test('Should display error for invalid update request', async ({ gotoDashboardPage, readProvisionedDashboard }) => { + /** + * Go To Panels dashboard datasource.json + * return dashboardPage + */ + const dashboard = await readProvisionedDashboard({ fileName: 'datasource.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Invalid Payload'); + + await panel.checkPresence(); + + const elements = panel.getElements(); + const numberMaxElement = await elements.getNumberElement('max', 'number'); + await numberMaxElement.setValue('125'); + + const buttons = panel.getButtons(); + await buttons.submit(); + + const confirmModal = new ModalHelper(dashboardPage); + await confirmModal.updateValues(); + + await panel.checkErrorMessage(); + }); + }); + + test.describe('Element change', () => { + test('Should call element change function', async ({ gotoDashboardPage, readProvisionedDashboard }) => { + /** + * Go To Panels dashboard validation.json + * return dashboardPage + */ + + const dashboard = await readProvisionedDashboard({ fileName: 'validation.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Reset and Validate Elements'); + + await panel.checkIfNoErrors(); + await panel.checkPresence(); + + const elements = panel.getElements(); + const facilityElement = await elements.getSelectElement('facility', 'select', dashboardPage); + const numberElement = await elements.getNumberElement('number', 'number'); + + await numberElement.isDisabled(); + + await facilityElement.setValue(1); + + await numberElement.isNotDisabled(); + }); + }); + + test.describe('Sections', () => { + test('Should keep section state after refresh dashboard', async ({ + gotoDashboardPage, + readProvisionedDashboard, + }) => { + /** + * Go To Panels dashboard panels.json + * return dashboardPage + */ + const dashboard = await readProvisionedDashboard({ fileName: 'panels.json' }); + const dashboardPage = await gotoDashboardPage({ uid: dashboard.uid }); + + /** + * Check Presence + */ + const panel = new PanelHelper(dashboardPage, 'Sections'); + + await panel.checkIfNoErrors(); + await panel.checkPresence(); + + const elements = panel.getElements(); + await elements.checkElementNotPresence('element1', 'string'); + + const sections = panel.getSections(); + await sections.checkSectionPresence('Section 1'); + + /** + * Open section + */ + await sections.openSection('Section 1'); + await sections.checkElementsCountInSection('Section 1', 1); + await elements.checkElementPresence('element1', FormElementType.STRING); + + await dashboardPage.refreshDashboard(); + + /** + * Element in open section should be visible + */ + await elements.checkElementPresence('element1', FormElementType.STRING); + }); }); }); diff --git a/test/panel.spec.ts-snapshots/actual-screenshot-run-tests-linux.png b/test/panel.spec.ts-snapshots/actual-screenshot-run-tests-linux.png deleted file mode 100644 index 829535f8..00000000 Binary files a/test/panel.spec.ts-snapshots/actual-screenshot-run-tests-linux.png and /dev/null differ diff --git a/test/utils/form.ts b/test/utils/form.ts new file mode 100644 index 00000000..62b484fd --- /dev/null +++ b/test/utils/form.ts @@ -0,0 +1,292 @@ +import { Locator } from '@playwright/test'; +import { DashboardPage, expect, Panel, PanelEditPage } from '@grafana/plugin-e2e'; +import { getLocatorSelectors, LocatorSelectors } from './selectors'; +import { TEST_IDS } from '../../src/constants/tests'; + +import { FormElementType } from '../../src/types/form-element'; + +const getElementsSelector = getLocatorSelectors(TEST_IDS.formElements); + +/** + * Disabled Element Helper + */ +class BaseElementHelper { + protected readonly locator: Locator; + + constructor(locator: Locator) { + this.locator = locator; + } + + private getMsg(message: string): string { + return `Element: ${message}`; + } + + public get() { + return this.locator; + } + + public async setValue(value: string) { + await this.get().fill(value); + return this.get().blur(); + } + + public async checkValue(text: string) { + return expect(this.get(), this.getMsg('Check value')).toHaveValue(text); + } + + public async isDisabled() { + return expect(this.get(), this.getMsg('Element is Disabled')).toBeDisabled(); + } + + public async isNotDisabled() { + return expect(this.get(), this.getMsg('Element is not Disabled')).not.toBeDisabled(); + } +} + +/** + * Disabled Element Helper + */ +class DisabledElementHelper extends BaseElementHelper { + constructor(parentLocator: Locator) { + super(parentLocator.getByTestId(TEST_IDS.formElements.fieldDisabled)); + } +} + +/** + * Number Element Helper + */ +class NumberElementHelper extends BaseElementHelper { + constructor(parentLocator: Locator) { + super(parentLocator.getByTestId(TEST_IDS.formElements.fieldNumber)); + } +} + +/** + * Select Element Helper + */ +class SelectElementHelper extends BaseElementHelper { + private readonly page: DashboardPage; + + constructor(parentLocator: Locator, page: DashboardPage) { + super(parentLocator.getByTestId(TEST_IDS.formElements.fieldSelect)); + this.page = page; + } + + public async setValue(fieldKey) { + await this.get().click(); + return this.page.getByGrafanaSelector(this.page.ctx.selectors.components.Select.option).getByText(fieldKey).click(); + } +} + +/** + * Elements Helper + */ +class ElementsHelper { + public selectors: LocatorSelectors; + + constructor(public readonly locator: Locator) { + this.selectors = this.getSelectors(locator); + } + + private getMsg(msg: string): string { + return `Elements: ${msg}`; + } + + private getSelectors(locator: Locator) { + return getElementsSelector(locator); + } + + public async checkPresence() { + return expect(this.selectors.root(), this.getMsg('Elements Container Presence')).toBeVisible(); + } + + public async checkElementPresence(elementId: string, elementType: FormElementType) { + return expect( + this.selectors.element(elementId, elementType), + this.getMsg(`Element ${elementId} Presence`) + ).toBeVisible(); + } + + public async checkElementNotPresence(elementId: string, elementType: string) { + return expect( + this.selectors.element(elementId, elementType), + this.getMsg(`Element ${elementId} not Presence`) + ).not.toBeVisible(); + } + + public async getElement(elementId: string, elementType: string) { + return this.selectors.element(elementId, elementType); + } + + public async getDisabledElement(elementId: string, elementType: string) { + const element = await this.getElement(elementId, elementType); + return new DisabledElementHelper(element); + } + + public async getNumberElement(elementId: string, elementType: string) { + const element = await this.getElement(elementId, elementType); + return new NumberElementHelper(element); + } + + public async getSelectElement(elementId: string, elementType: string, dashboardPage: DashboardPage) { + const element = await this.getElement(elementId, elementType); + return new SelectElementHelper(element, dashboardPage); + } +} + +/** + * Buttons Helper + */ +class ButtonsHelper { + private readonly locator: Locator; + private readonly selectors: LocatorSelectors; + + constructor(parentLocator: Locator) { + this.locator = parentLocator; + this.selectors = getLocatorSelectors(TEST_IDS.panel)(this.locator); + } + + private getMsg(message: string): string { + return `Buttons: ${message}`; + } + + public async checkSubmitButtonPresence() { + return expect(this.selectors.buttonSubmit(), this.getMsg(`Check Submit button Presence`)).toBeVisible(); + } + + public async checkSubmitButtonIsDisabled() { + return expect(this.selectors.buttonSubmit(), this.getMsg(`Check Submit button disabled`)).toBeDisabled(); + } + + public async checkSubmitButtonIsNotDisabled() { + return expect(this.selectors.buttonSubmit(), this.getMsg(`Check Submit button not disabled`)).not.toBeDisabled(); + } + + public async submit() { + return this.selectors.buttonSubmit().click(); + } + + public async checkResetButtonPresence() { + return expect(this.selectors.buttonReset(), this.getMsg(`Check Reset button Presence`)).toBeVisible(); + } + + public async reset() { + return this.selectors.buttonReset().click(); + } +} + +/** + * Buttons Helper + */ +class SectionsHelper { + private readonly locator: Locator; + private readonly selectors: LocatorSelectors; + + constructor(parentLocator: Locator) { + this.locator = parentLocator; + this.selectors = getLocatorSelectors(TEST_IDS.panel)(this.locator); + } + + private getMsg(message: string): string { + return `Sections: ${message}`; + } + + public async checkSectionPresence(name: string) { + return expect(this.selectors.splitLayoutContent(name), this.getMsg(`Check ${name} Section`)).toBeVisible(); + } + + public async checkElementsCountInSection(name: string, count: number) { + const section = this.selectors.splitLayoutContent(name); + const elementsContainer = getElementsSelector(section); + const elements = await elementsContainer.root().locator('label').all(); + + return expect(elements, this.getMsg('Check Body Rows Count')).toHaveLength(count); + } + + public async openSection(name: string) { + return this.selectors.splitLayoutContent(name).click(); + } +} + +/** + * Panel Editor Helper + */ +class PanelEditorHelper { + private readonly elementsEditorSelectors: LocatorSelectors; + + constructor( + private readonly locator: Locator, + private readonly editPage: PanelEditPage + ) { + this.elementsEditorSelectors = getLocatorSelectors(TEST_IDS.formElementsEditor)(this.locator); + } + + public async addElement(id: string, label: string, type: string) { + await this.elementsEditorSelectors.newElementId().fill(id); + await this.elementsEditorSelectors.newElementLabel().fill(label); + await this.elementsEditorSelectors.newElementType().click(); + await this.editPage + .getByGrafanaSelector(this.editPage.ctx.selectors.components.Select.option) + .getByText(type) + .click(); + await this.elementsEditorSelectors.buttonAddElement().click(); + await this.elementsEditorSelectors.buttonSaveChanges().click(); + } +} + +/** + * Panel Helper + */ +export class PanelHelper { + private readonly locator: Locator; + private readonly panel: Panel; + private readonly title: string; + private readonly selectors: LocatorSelectors; + + constructor(dashboardPage: DashboardPage, panelTitle: string) { + this.panel = dashboardPage.getPanelByTitle(panelTitle); + this.title = panelTitle; + this.locator = this.panel.locator; + this.selectors = getLocatorSelectors(TEST_IDS.panel)(this.locator); + } + + private getMsg(msg: string): string { + return `Panel: ${msg}`; + } + + public get() { + return this.locator; + } + + public getElements() { + return new ElementsHelper(this.locator); + } + + public getPanelEditor(locator: Locator, editPage: PanelEditPage) { + return new PanelEditorHelper(locator, editPage); + } + + public getButtons() { + return new ButtonsHelper(this.locator); + } + + public getSections() { + return new SectionsHelper(this.locator); + } + + public async checkIfNoErrors() { + return expect(this.panel.getErrorIcon(), this.getMsg('Check If No Errors')).not.toBeVisible(); + } + + public async checkPresence() { + return expect(this.selectors.root(), this.getMsg(`Check ${this.title} Presence`)).toBeVisible(); + } + + public async checkAlertMessage() { + return expect(this.selectors.infoMessage(), this.getMsg(`Check Alert Message`)).toBeVisible(); + } + + public async checkErrorMessage() { + return expect(this.selectors.errorMessage(), this.getMsg(`Check Error Message`)).toBeVisible(); + } +} diff --git a/test/utils/index.ts b/test/utils/index.ts new file mode 100644 index 00000000..6b66b53c --- /dev/null +++ b/test/utils/index.ts @@ -0,0 +1,3 @@ +export * from './selectors'; +export * from './form'; +export * from './modal'; diff --git a/test/utils/modal.ts b/test/utils/modal.ts new file mode 100644 index 00000000..b326ba16 --- /dev/null +++ b/test/utils/modal.ts @@ -0,0 +1,55 @@ +import { Locator } from '@playwright/test'; +import { DashboardPage, expect } from '@grafana/plugin-e2e'; +import { getLocatorSelectors, LocatorSelectors } from './selectors'; +import { TEST_IDS } from '../../src/constants/tests'; + +/** + * Modal Helper + */ +export class ModalHelper { + private readonly locator: Locator; + private readonly selectors: LocatorSelectors; + + constructor(dashboardPage: DashboardPage) { + this.locator = dashboardPage.ctx.page.getByRole('dialog'); + this.selectors = getLocatorSelectors(TEST_IDS.panel)(this.locator); + } + + private getMsg(msg: string): string { + return `Panel: ${msg}`; + } + + public get() { + return this.locator; + } + + public async checkPresence() { + return expect(this.get(), this.getMsg(`Check Modal Presence`)).toBeVisible(); + } + + public async checkNotPresence() { + return expect(this.get(), this.getMsg(`Check Modal Not Presence`)).not.toBeVisible(); + } + + public async confirmButtonCheckPresence() { + return expect( + this.get().getByRole('button', { name: 'Confirm' }), + this.getMsg(`Check Confirm button Presence`) + ).toBeVisible(); + } + + public async cancelButtonCheckPresence() { + return expect( + this.get().getByRole('button', { name: 'Cancel' }), + this.getMsg(`Check Cancel button Presence`) + ).toBeVisible(); + } + + public async updateValues() { + return this.get().getByRole('button', { name: 'Confirm' }).click(); + } + + public async cancelUpdateValues() { + return this.get().getByRole('button', { name: 'Cancel' }).click(); + } +} diff --git a/test/utils/selectors.ts b/test/utils/selectors.ts new file mode 100644 index 00000000..0da35dc4 --- /dev/null +++ b/test/utils/selectors.ts @@ -0,0 +1,51 @@ +import { Locator } from '@playwright/test'; + +/** + * Selector + */ +type LocatorSelector = (...args: TArgs) => ReturnType<() => Locator>; + +/** + * Check If Selector Object + */ +type IsSelectorObject = TCandidate extends { + selector: (...args: unknown[]) => void; + apply: (...args: unknown[]) => void; +} + ? TCandidate & { selector: TCandidate['selector']; apply: TCandidate['apply'] } + : never; + +/** + * Selectors + */ +export type LocatorSelectors = { + [K in keyof T]: T[K] extends (...args: infer Args) => void + ? LocatorSelector + : T[K] extends IsSelectorObject + ? LocatorSelector> + : LocatorSelector<[]>; +}; + +export const getLocatorSelectors = + >( + selectors: TSelectors + ): ((locator: Locator) => LocatorSelectors) => + (locator) => { + return Object.entries(selectors).reduce((acc, [key, selector]) => { + const getElement = (...args: unknown[]): Locator => { + const getValue = typeof selector === 'object' && 'selector' in selector! ? selector.selector : selector; + const value = typeof getValue === 'function' ? getValue(...args) : getValue; + + if (value.startsWith('data-testid')) { + return locator.getByTestId(value); + } + + return locator.getByLabel(value); + }; + + return { + ...acc, + [key]: getElement, + }; + }, {} as LocatorSelectors); + };