From afbd913eac89337c61ece90fb45957a74bceefd5 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 19 Jul 2025 02:27:44 +0200 Subject: [PATCH 1/5] feat: support link cells in Views --- src/dashboard/Data/Views/Views.react.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/dashboard/Data/Views/Views.react.js b/src/dashboard/Data/Views/Views.react.js index 1ce02dd9ae..7084c55738 100644 --- a/src/dashboard/Data/Views/Views.react.js +++ b/src/dashboard/Data/Views/Views.react.js @@ -132,7 +132,13 @@ class Views extends TableView { if (text === undefined) { text = ''; } else if (text && typeof text === 'object') { - text = text.__type === 'Date' && text.iso ? text.iso : JSON.stringify(text); + if (text.__type === 'Date' && text.iso) { + text = text.iso; + } else if (text.__type === 'Link' && text.text) { + text = text.text; + } else { + text = JSON.stringify(text); + } } text = String(text); if (typeof document !== 'undefined') { @@ -166,6 +172,8 @@ class Views extends TableView { type = 'File'; } else if (val.__type === 'GeoPoint') { type = 'GeoPoint'; + } else if (val.__type === 'Link') { + type = 'Link'; } else { type = 'Object'; } @@ -285,6 +293,8 @@ class Views extends TableView { type = 'File'; } else if (value.__type === 'GeoPoint') { type = 'GeoPoint'; + } else if (value.__type === 'Link') { + type = 'Link'; } else { type = 'Object'; } @@ -306,6 +316,15 @@ class Views extends TableView { content = JSON.stringify(value); } else if (type === 'Date') { content = value && value.iso ? value.iso : String(value); + } else if (type === 'Link') { + const url = value.isRelativeUrl + ? `apps/${this.context.slug}/${value.url}` + : value.url; + content = ( + + {value.text} + + ); } else if (value === undefined) { content = ''; } else { From 85ec3221f61f4722eab4787d3498aea055179cb3 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 19 Jul 2025 03:31:11 +0200 Subject: [PATCH 2/5] Update README.md --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/README.md b/README.md index 8b84e7a23c..2050837a99 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,9 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https - [Limitations](#limitations) - [CSV Export](#csv-export) - [Views](#views) + - [View Table](#view-table) + - [Pointer](#pointer) + - [Link](#link) - [Contributing](#contributing) # Getting Started @@ -1253,12 +1256,53 @@ This feature allows you to change how a pointer is represented in the browser. B This feature will take either selected rows or all rows of an individual class and saves them to a CSV file, which is then downloaded. CSV headers are added to the top of the file matching the column names. > ⚠️ There is currently a 10,000 row limit when exporting all data. If more than 10,000 rows are present in the class, the CSV file will only contain 10,000 rows. + ## Views ▶️ *Core > Views* Views are saved queries that display aggregated data from your classes. Create a view by providing a name, selecting a class and defining an aggregation pipeline. Optionally enable the object counter to show how many items match the view. Saved views appear in the sidebar, where you can select, edit, or delete them. +### View Table + +When designing the aggregation pipeline, consider that some values are rendered specially in the output table. + +#### Pointer + +Parse Object pointers are automatically displayed as links to the target object. + +Example: + +```json +{ "__type": "Pointer", "className": "_User", "objectId": "xWMyZ4YEGZ" } +``` + +#### Link + +Links are rendered as hyperlinks that open in a new browser tab. + +Example: + +```json +{ + "__type": "Link", + "url": "https://example.com", + "text": "Link" +} +``` + +Set `isRelativeUrl: true` when linking to another dashboard page, in which case the base URL for the relative URL will be `:////apps//`. The key `isRelativeUrl` is optional and `false` by default. + +Example: + +```json +{ + "__type": "Link", + "url": "browser/_Installation?filters=%5B%7B%22field%22%3A%22objectId%22%2C%22constraint%22%3A%22eq%22%2C%22compareTo%22%3A%22xWMyZ4YEGZ%22%2C%22class%22%3A%22_Installation%22%7D%5D", + "isRelativeUrl": true, + "text": "Link" +} +``` # Contributing From 42bbdb1e9fb2d2d22d245ee5e6b820b1e65c18a8 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 19 Jul 2025 04:23:43 +0200 Subject: [PATCH 3/5] add urlQuery --- README.md | 25 ++++++++++++++++++++++- src/dashboard/Data/Views/Views.react.js | 27 +++++++++++++++++++++---- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2050837a99..44672392aa 100644 --- a/README.md +++ b/README.md @@ -1263,6 +1263,9 @@ This feature will take either selected rows or all rows of an individual class a Views are saved queries that display aggregated data from your classes. Create a view by providing a name, selecting a class and defining an aggregation pipeline. Optionally enable the object counter to show how many items match the view. Saved views appear in the sidebar, where you can select, edit, or delete them. +> [!Caution] +> Values are generally rendered without sanitization in the resulting data table. If rendered values come from user input or untrusted data, make sure to remove potentially dangerous HTML or JavaScript, to prevent an attacker from injecting malicious code, to exploit vulnerabilities like Cross-Site-Scripting (XSS). + ### View Table When designing the aggregation pipeline, consider that some values are rendered specially in the output table. @@ -1298,12 +1301,32 @@ Example: ```json { "__type": "Link", - "url": "browser/_Installation?filters=%5B%7B%22field%22%3A%22objectId%22%2C%22constraint%22%3A%22eq%22%2C%22compareTo%22%3A%22xWMyZ4YEGZ%22%2C%22class%22%3A%22_Installation%22%7D%5D", + "url": "browser/_Installation", + "isRelativeUrl": true, + "text": "Link" +} +``` + +A query part of the URL can be easily added using the `urlQuery` key which will automatically escape the quey string. + +Example: + +```json +{ + "__type": "Link", + "url": "browser/_Installation", + "urlQuery": "filters=[{\"field\":\"objectId\",\"constraint\":\"eq\",\"compareTo\":\"xWMyZ4YEGZ\",\"class\":\"_Installation\"}]", "isRelativeUrl": true, "text": "Link" } ``` +In the example above, the query string will be escaped and added to the url, resulting in the complete URL: + +```js +"browser/_Installation?filters=%5B%7B%22field%22%3A%22objectId%22%2C%22constraint%22%3A%22eq%22%2C%22compareTo%22%3A%22xWMyZ4YEGZ%22%2C%22class%22%3A%22_Installation%22%7D%5D" +``` + # Contributing We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Dashboard guide](CONTRIBUTING.md). diff --git a/src/dashboard/Data/Views/Views.react.js b/src/dashboard/Data/Views/Views.react.js index 7084c55738..759ce4e65e 100644 --- a/src/dashboard/Data/Views/Views.react.js +++ b/src/dashboard/Data/Views/Views.react.js @@ -317,12 +317,31 @@ class Views extends TableView { } else if (type === 'Date') { content = value && value.iso ? value.iso : String(value); } else if (type === 'Link') { - const url = value.isRelativeUrl - ? `apps/${this.context.slug}/${value.url}` - : value.url; + // Sanitize URL + let url = value.url; + if ( + url.match(/javascript/i) || + url.match(/