From ee518cfb1fef7ac0d89448d0187255175f869e4e Mon Sep 17 00:00:00 2001 From: Alex Simonok Date: Fri, 8 Mar 2024 08:15:25 +0300 Subject: [PATCH] Add files upload examples and fix form data header (#357) * Add files upload examples and fix form data header * Clear name variable default selection * Updates * Update Postgres Server --------- Co-authored-by: Mikhail Volkov Co-authored-by: Mikhail Volkov <47795110+mikhail-vl@users.noreply.github.com> --- CHANGELOG.md | 6 + docker-compose.yml | 2 +- package-lock.json | 12 +- package.json | 4 +- provisioning/dashboards/files.json | 336 +++++++++++++++++++++++++ server-pg/Dockerfile | 7 +- server-pg/{feedback.sql => init.sql} | 7 +- server-pg/server.ts | 40 ++- server-pg/tsconfig.json | 4 + src/components/FormPanel/FormPanel.tsx | 8 +- 10 files changed, 410 insertions(+), 16 deletions(-) create mode 100644 provisioning/dashboards/files.json rename server-pg/{feedback.sql => init.sql} (81%) create mode 100644 server-pg/tsconfig.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 61fbe474..1fde892a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 3.7.0 (IN PROGRESS) + +### Features / Enhancements + +- Add files upload examples and fix form data header (#357) + ## 3.6.0 (2023-01-10) ### Features / Enhancements diff --git a/docker-compose.yml b/docker-compose.yml index a0f09b99..16fe6e6f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: environment: - GF_DEFAULT_APP_MODE=development - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/provisioning/dashboards/panels.json - - GF_INSTALL_PLUGINS=marcusolsson-static-datasource,marcusolsson-json-datasource,volkovlabs-variable-panel + - GF_INSTALL_PLUGINS=marcusolsson-static-datasource,marcusolsson-json-datasource,volkovlabs-variable-panel, volkovlabs-image-panel volumes: - ./dist:/var/lib/grafana/plugins/volkovlabs-form-panel - ./provisioning:/etc/grafana/provisioning diff --git a/package-lock.json b/package-lock.json index fafba8f1..a8c1ccc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "volkovlabs-form-panel", - "version": "3.6.0", + "version": "3.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "volkovlabs-form-panel", - "version": "3.6.0", + "version": "3.7.0", "license": "Apache-2.0", "dependencies": { "@emotion/css": "^11.11.2", @@ -33,7 +33,7 @@ "@testing-library/react": "^14.1.2", "@types/jest": "^29.5.11", "@types/lodash": "^4.14.202", - "@types/node": "^20.10.6", + "@types/node": "^20.11.21", "@types/uuid": "^9.0.7", "@volkovlabs/eslint-config": "^1.2.2", "@volkovlabs/jest-selectors": "^1.2.0", @@ -5920,9 +5920,9 @@ } }, "node_modules/@types/node": { - "version": "20.10.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", - "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", + "version": "20.11.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.21.tgz", + "integrity": "sha512-/ySDLGscFPNasfqStUuWWPfL78jompfIoVzLJPVVAHBh6rpG68+pI2Gk+fNLeI8/f1yPYL4s46EleVIc20F1Ow==", "dev": true, "dependencies": { "undici-types": "~5.26.4" diff --git a/package.json b/package.json index 667237d8..a996925d 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@testing-library/react": "^14.1.2", "@types/jest": "^29.5.11", "@types/lodash": "^4.14.202", - "@types/node": "^20.10.6", + "@types/node": "^20.11.21", "@types/uuid": "^9.0.7", "@volkovlabs/eslint-config": "^1.2.2", "@volkovlabs/jest-selectors": "^1.2.0", @@ -73,5 +73,5 @@ "test:ci": "jest --maxWorkers 4 --coverage", "upgrade": "npm upgrade --save" }, - "version": "3.6.0" + "version": "3.7.0" } diff --git a/provisioning/dashboards/files.json b/provisioning/dashboards/files.json new file mode 100644 index 00000000..c197a817 --- /dev/null +++ b/provisioning/dashboards/files.json @@ -0,0 +1,336 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PCC52D03280B7034C" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "buttonGroup": { + "orientation": "center", + "size": "md" + }, + "confirmModal": { + "body": "Please confirm to update changed values", + "cancel": "Cancel", + "columns": { + "name": "Label", + "newValue": "New Value", + "oldValue": "Old Value" + }, + "confirm": "Confirm", + "title": "Confirm update request" + }, + "elementValueChanged": "", + "elements": [ + { + "accept": "", + "id": "file", + "labelWidth": 10, + "section": "", + "title": "File", + "tooltip": "", + "type": "file", + "uid": "c63ec810-e7f7-471f-9f9d-a8fd58330982", + "unit": "", + "value": [] + } + ], + "initial": { + "code": "console.log(data, response, initial, elements);\n\nreturn;\n\n/**\n * Data Source\n * Requires form elements to be defined\n */\nconst dataQuery = toDataQueryResponse(response);\nconsole.log(dataQuery);", + "contentType": "application/json", + "getPayload": "return {\n rawSql: '',\n format: 'table',\n}", + "highlight": false, + "highlightColor": "red", + "method": "-" + }, + "layout": { + "orientation": "horizontal", + "padding": 10, + "variant": "single" + }, + "reset": { + "backgroundColor": "purple", + "foregroundColor": "yellow", + "icon": "process", + "text": "Reset", + "variant": "hidden" + }, + "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" + }, + "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 (response && response.ok) {\n notifySuccess(['Update', 'Values updated successfully.']);\n} else {\n notifyError(['Update', 'An error occured updating values.']);\n}", + "confirm": false, + "contentType": "application/json", + "datasource": "PostgreSQL", + "getPayload": "const payload = {};\n\nelements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\nconst toBase64 = (file) =>\n new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.readAsDataURL(file);\n reader.onload = () => resolve(reader.result);\n reader.onerror = reject;\n });\n\n\n/**\n * Data Source payload\n */\nconst getPayload = async () => {\n const file = payload.file[0];\n const base64 = await toBase64(file);\n\n return {\n rawSql: `INSERT INTO files (name, file)\nVALUES ('${file.name}', '${base64}');`,\n format: 'table',\n }\n}\n\nreturn getPayload();", + "method": "datasource", + "payloadMode": "custom" + } + }, + "pluginVersion": "3.6.0", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PCC52D03280B7034C" + }, + "refId": "A" + } + ], + "title": "Datasource Upload as Base64", + "type": "volkovlabs-form-panel" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PCC52D03280B7034C" + }, + "gridPos": { + "h": 16, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "autoPlay": true, + "buttons": [], + "controls": true, + "height": 0, + "heightMode": "original", + "infinityPlay": false, + "name": "file", + "scale": "auto", + "toolbar": true, + "width": 0, + "widthMode": "auto", + "zoomType": "default" + }, + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PCC52D03280B7034C" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT file FROM files WHERE name='$name';", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Current File", + "type": "volkovlabs-image-panel" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PCC52D03280B7034C" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "buttonGroup": { + "orientation": "center", + "size": "md" + }, + "confirmModal": { + "body": "Please confirm to update changed values", + "cancel": "Cancel", + "columns": { + "name": "Label", + "newValue": "New Value", + "oldValue": "Old Value" + }, + "confirm": "Confirm", + "title": "Confirm update request" + }, + "elementValueChanged": "", + "elements": [ + { + "accept": "", + "id": "file", + "labelWidth": 10, + "section": "", + "title": "File", + "tooltip": "", + "type": "file", + "uid": "c63ec810-e7f7-471f-9f9d-a8fd58330982", + "unit": "", + "value": [] + } + ], + "initial": { + "code": "console.log(data, response, initial, elements);\n\nreturn;\n\n/**\n * Data Source\n * Requires form elements to be defined\n */\nconst dataQuery = toDataQueryResponse(response);\nconsole.log(dataQuery);", + "contentType": "application/json", + "getPayload": "return {\n rawSql: '',\n format: 'table',\n}", + "highlight": false, + "highlightColor": "red", + "method": "-" + }, + "layout": { + "orientation": "horizontal", + "padding": 10, + "variant": "single" + }, + "reset": { + "backgroundColor": "purple", + "foregroundColor": "yellow", + "icon": "process", + "text": "Reset", + "variant": "hidden" + }, + "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" + }, + "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 (response && response.ok) {\n notifySuccess(['Update', 'Values updated successfully.']);\n} else {\n notifyError(['Update', 'An error occured updating values.']);\n}", + "confirm": false, + "contentType": "multipart/form-data", + "datasource": "PostgreSQL", + "getPayload": "const payload = {};\n\nelements.forEach((element) => {\n if (!element.value) {\n return;\n }\n\n payload[element.id] = element.value;\n})\n\nconst toBase64 = (file) =>\n new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.readAsDataURL(file);\n reader.onload = () => resolve(reader.result);\n reader.onerror = reject;\n });\n\n\n/**\n * Data Source payload\n */\nconst getPayload = async () => {\n const file = payload.file[0];\n const base64 = await toBase64(file);\n\n return {\n rawSql: `INSERT INTO files (name, file)\nVALUES ('${file.name}', '${base64}');`,\n format: 'table',\n }\n}\n\nreturn getPayload();", + "method": "POST", + "payloadMode": "all", + "url": "http://localhost:3002/upload" + } + }, + "pluginVersion": "3.6.0", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PCC52D03280B7034C" + }, + "refId": "A" + } + ], + "title": "HTTP Upload as Form Data", + "type": "volkovlabs-form-panel" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "isNone": true, + "selected": false, + "text": "None", + "value": "" + }, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "PCC52D03280B7034C" + }, + "definition": "select name from files", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "name", + "options": [], + "query": "select name from files", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Files", + "uid": "eaad38e5-abc9-4388-b061-97ef9f6d600b", + "version": 8, + "weekStart": "" +} diff --git a/server-pg/Dockerfile b/server-pg/Dockerfile index 773ed463..0d720f0f 100644 --- a/server-pg/Dockerfile +++ b/server-pg/Dockerfile @@ -6,8 +6,9 @@ WORKDIR /app ## Copy Server COPY server.ts . -## Add Postgres package -RUN yarn add pg +## Add packages +RUN npm install -g ts-node +RUN npm add pg multiparty ## Add the wait script to the image ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.9.0/wait /wait @@ -15,4 +16,4 @@ RUN chmod +x /wait EXPOSE 3001 -CMD /wait && node server.ts +CMD /wait && ts-node server.ts diff --git a/server-pg/feedback.sql b/server-pg/init.sql similarity index 81% rename from server-pg/feedback.sql rename to server-pg/init.sql index f2496bdb..307d711f 100644 --- a/server-pg/feedback.sql +++ b/server-pg/init.sql @@ -17,4 +17,9 @@ CREATE TABLE configuration ( insert into configuration values ('device1', 100, 10, 54); insert into configuration values ('device2', 60, 0, 10); insert into configuration values ('device3', 60, 30, 40); -insert into configuration values ('device4', 34, 10, 20); \ No newline at end of file +insert into configuration values ('device4', 34, 10, 20); + +CREATE TABLE files ( + name text, + file text +); diff --git a/server-pg/server.ts b/server-pg/server.ts index 3ffb912f..27be4f00 100644 --- a/server-pg/server.ts +++ b/server-pg/server.ts @@ -1,5 +1,7 @@ const http = require('http'); +const fs = require('fs'); const { Client } = require('pg'); +const multiparty = require('multiparty'); /** * Server Port @@ -19,7 +21,7 @@ client.connect(); /** * Create Server */ -const server = http.createServer(async function (req, res) { +const server = http.createServer(async function (req: any, res: any) { /** * Set CORS headers */ @@ -53,12 +55,46 @@ const server = http.createServer(async function (req, res) { return; } + /** + * Upload File + */ + if (req.url === '/upload' && req.method === 'POST') { + const form = new multiparty.Form({ autoFiles: true }); + + form.parse(req, async function (err: any, fields: any, files: any) { + if (!files) { + res.writeHead(200, { 'content-type': 'text/plain' }); + res.write('Incorrect request'); + res.end(); + return; + } + + const filesArray = Object.values(files).reduce((acc: any[], files) => acc.concat(files), []); + + /** + * Insert files to database + */ + await Promise.all( + filesArray.map(async (file) => { + const base64 = await fs.readFileSync(file.path, { encoding: 'base64' }); + await client.query('INSERT INTO files(name, file) VALUES($1, $2)', [file.originalFilename, base64]); + }) + ); + + res.writeHead(200, { 'content-type': 'text/plain' }); + res.write('Files uploaded'); + res.end(); + }); + + return; + } + /** * POST, PUT or PATCH */ if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') { let body = ''; - req.on('data', function (chunk) { + req.on('data', function (chunk: string) { body += chunk; }); diff --git a/server-pg/tsconfig.json b/server-pg/tsconfig.json new file mode 100644 index 00000000..6b46b833 --- /dev/null +++ b/server-pg/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../.config/tsconfig.json", + "include": ["./*.ts"] +} diff --git a/src/components/FormPanel/FormPanel.tsx b/src/components/FormPanel/FormPanel.tsx index 9238b1a0..4942aada 100644 --- a/src/components/FormPanel/FormPanel.tsx +++ b/src/components/FormPanel/FormPanel.tsx @@ -693,7 +693,13 @@ export const FormPanel: React.FC = ({ * Set Content Type */ const headers: HeadersInit = new Headers(); - headers.set('Content-Type', options.update.contentType); + + /** + * Browser should add header itself for form data content + */ + if (options.update.contentType !== ContentType.FORMDATA) { + headers.set('Content-Type', options.update.contentType); + } /** * Set Header