From 8a967cd8f09856faa7d740d874fa0fe7d5304f2f Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 19 Jan 2023 16:05:49 +1100 Subject: [PATCH 01/10] feat: export all rows, export as JSON --- src/dashboard/Data/Browser/Browser.react.js | 67 ++++++++++++++++--- .../Browser/ExportSelectedRowsDialog.react.js | 22 ++++-- 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index 8e35357351..af9925f0cf 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -70,6 +70,8 @@ class Browser extends DashboardView { filters: new List(), ordering: '-createdAt', selection: {}, + exporting: false, + exportingCount: 0, data: null, lastMax: -1, @@ -1255,15 +1257,12 @@ class Browser extends DashboardView { }); } - async confirmExportSelectedRows(rows) { - this.setState({ rowsToExport: null }); + async confirmExportSelectedRows(rows, type) { + this.setState({ rowsToExport: null, exporting: true, exportingCount: 0 }); const className = this.props.params.className; const query = new Parse.Query(className); - if (rows['*']) { - // Export all - query.limit(10000); - } else { + if (!rows['*']) { // Export selected const objectIds = []; for (const objectId in this.state.rowsToExport) { @@ -1273,6 +1272,8 @@ class Browser extends DashboardView { query.limit(objectIds.length); } + + const processObjects = (objects) => { const classColumns = this.getClassColumns(className, false); // create object with classColumns as property keys needed for ColumnPreferences.getOrder function const columnsObject = {}; @@ -1286,7 +1287,21 @@ class Browser extends DashboardView { className ).filter(column => column.visible); - const objects = await query.find({ useMasterKey: true }); + if (type === '.json') { + const element = document.createElement('a'); + const file = new Blob([JSON.stringify(objects.map(obj => { + const json = obj._toFullJSON(); + delete json.__type; + return json; + }), null, 2)], { type: 'application/json' }); + element.href = URL.createObjectURL(file); + element.download = `${className}.json`; + document.body.appendChild(element); // Required for this to work in FireFox + element.click(); + document.body.removeChild(element); + return; + } + let csvString = columns.map(column => column.name).join(',') + '\n'; for (const object of objects) { const row = columns.map(column => { @@ -1342,6 +1357,37 @@ class Browser extends DashboardView { document.body.appendChild(element); // Required for this to work in FireFox element.click(); document.body.removeChild(element); + } + + if (!rows["*"]) { + const objects = await query.find({ useMasterKey: true }); + processObjects(objects); + this.setState({ exporting: false, exportingCount: objects.length }); + } else { + let batch = []; + let completion = null; + query.each( + (obj) => { + batch.push(obj); + if (batch.length % 10 === 0) { + this.setState({exportingCount: batch.length}); + } + const one_gigabyte = Math.pow(2, 30); + const size = new TextEncoder().encode(JSON.stringify(batch)).length / one_gigabyte; + if (size.length > 1) { + processObjects(batch); + batch = []; + } + clearTimeout(completion); + completion = setTimeout(() => { + processObjects(batch); + batch = []; + this.setState({ exporting: false, exportingCount: 0 }); + }, 5000); + }, + { useMasterKey: true } + ); + } } getClassRelationColumns(className) { @@ -1755,7 +1801,7 @@ class Browser extends DashboardView { className={className} selection={this.state.rowsToExport} onCancel={this.cancelExportSelectedRows} - onConfirm={() => this.confirmExportSelectedRows(this.state.rowsToExport)} + onConfirm={(type) => this.confirmExportSelectedRows(this.state.rowsToExport, type)} /> ); } @@ -1772,6 +1818,11 @@ class Browser extends DashboardView { ); } + else if (this.state.exporting) { + notification = ( + + ); + } return (
diff --git a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js index 92f2ce50bf..21647c1c05 100644 --- a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js +++ b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js @@ -7,13 +7,18 @@ */ import Modal from 'components/Modal/Modal.react'; import React from 'react'; +import Dropdown from 'components/Dropdown/Dropdown.react'; +import Field from 'components/Field/Field.react'; +import Label from 'components/Label/Label.react'; +import Option from 'components/Dropdown/Option.react'; export default class ExportSelectedRowsDialog extends React.Component { constructor() { super(); this.state = { - confirmation: '' + confirmation: '', + exportType: '.csv' }; } @@ -28,13 +33,22 @@ export default class ExportSelectedRowsDialog extends React.Component { type={Modal.Types.INFO} icon='warn-outline' title={this.props.selection['*'] ? 'Export all rows?' : (selectionLength === 1 ? 'Export 1 selected row?' : `Export ${selectionLength} selected rows?`)} - subtitle={this.props.selection['*'] ? 'Note: Exporting is limited to the first 10,000 rows.' : ''} + subtitle={this.props.selection['*'] ? 'Note: This will export mutliple files with maximum 1gb each. This might take a while.' : ''} disabled={!this.valid()} confirmText='Export' cancelText='Cancel' onCancel={this.props.onCancel} - onConfirm={this.props.onConfirm}> - {} + onConfirm={() => this.props.onConfirm(this.state.exportType)}> + } + input={ + this.setState({ exportType })}> + + + + } /> ); } From 0226b5274ad396a14d4c76fa8312e2735b53dbf2 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 19 Jan 2023 16:07:48 +1100 Subject: [PATCH 02/10] Update Browser.react.js --- src/dashboard/Data/Browser/Browser.react.js | 186 +++++++++++--------- 1 file changed, 101 insertions(+), 85 deletions(-) diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index af9925f0cf..087d0a67fa 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -1262,102 +1262,116 @@ class Browser extends DashboardView { const className = this.props.params.className; const query = new Parse.Query(className); - if (!rows['*']) { + if (!rows["*"]) { // Export selected const objectIds = []; for (const objectId in this.state.rowsToExport) { objectIds.push(objectId); } - query.containedIn('objectId', objectIds); + query.containedIn("objectId", objectIds); query.limit(objectIds.length); } - const processObjects = (objects) => { - const classColumns = this.getClassColumns(className, false); - // create object with classColumns as property keys needed for ColumnPreferences.getOrder function - const columnsObject = {}; - classColumns.forEach((column) => { - columnsObject[column.name] = column; - }); - // get ordered list of class columns - const columns = ColumnPreferences.getOrder( - columnsObject, - this.context.applicationId, - className - ).filter(column => column.visible); - - if (type === '.json') { - const element = document.createElement('a'); - const file = new Blob([JSON.stringify(objects.map(obj => { - const json = obj._toFullJSON(); - delete json.__type; - return json; - }), null, 2)], { type: 'application/json' }); - element.href = URL.createObjectURL(file); - element.download = `${className}.json`; - document.body.appendChild(element); // Required for this to work in FireFox - element.click(); - document.body.removeChild(element); - return; - } + const classColumns = this.getClassColumns(className, false); + // create object with classColumns as property keys needed for ColumnPreferences.getOrder function + const columnsObject = {}; + classColumns.forEach((column) => { + columnsObject[column.name] = column; + }); + // get ordered list of class columns + const columns = ColumnPreferences.getOrder( + columnsObject, + this.context.applicationId, + className + ).filter((column) => column.visible); + + if (type === ".json") { + const element = document.createElement("a"); + const file = new Blob( + [ + JSON.stringify( + objects.map((obj) => { + const json = obj._toFullJSON(); + delete json.__type; + return json; + }), + null, + 2 + ), + ], + { type: "application/json" } + ); + element.href = URL.createObjectURL(file); + element.download = `${className}.json`; + document.body.appendChild(element); // Required for this to work in FireFox + element.click(); + document.body.removeChild(element); + return; + } - let csvString = columns.map(column => column.name).join(',') + '\n'; - for (const object of objects) { - const row = columns.map(column => { - const type = columnsObject[column.name].type; - if (column.name === 'objectId') { - return object.id; - } else if (type === 'Relation' || type === 'Pointer') { - if (object.get(column.name)) { - return object.get(column.name).id - } else { - return '' - } - } else { - let colValue; - if (column.name === 'ACL') { - colValue = object.getACL(); - } else { - colValue = object.get(column.name); - } - // Stringify objects and arrays - if (Object.prototype.toString.call(colValue) === '[object Object]' || Object.prototype.toString.call(colValue) === '[object Array]') { - colValue = JSON.stringify(colValue); - } - if(typeof colValue === 'string') { - if (colValue.includes('"')) { - // Has quote in data, escape and quote - // If the value contains both a quote and delimiter, adding quotes and escaping will take care of both scenarios - colValue = colValue.split('"').join('""'); - return `"${colValue}"`; - } else if (colValue.includes(',')) { - // Has delimiter in data, surround with quote (which the value doesn't already contain) - return `"${colValue}"`; + let csvString = columns.map((column) => column.name).join(",") + "\n"; + for (const object of objects) { + const row = columns + .map((column) => { + const type = columnsObject[column.name].type; + if (column.name === "objectId") { + return object.id; + } else if (type === "Relation" || type === "Pointer") { + if (object.get(column.name)) { + return object.get(column.name).id; + } else { + return ""; + } } else { - // No quote or delimiter, just include plainly - return `${colValue}`; + let colValue; + if (column.name === "ACL") { + colValue = object.getACL(); + } else { + colValue = object.get(column.name); + } + // Stringify objects and arrays + if ( + Object.prototype.toString.call(colValue) === + "[object Object]" || + Object.prototype.toString.call(colValue) === "[object Array]" + ) { + colValue = JSON.stringify(colValue); + } + if (typeof colValue === "string") { + if (colValue.includes('"')) { + // Has quote in data, escape and quote + // If the value contains both a quote and delimiter, adding quotes and escaping will take care of both scenarios + colValue = colValue.split('"').join('""'); + return `"${colValue}"`; + } else if (colValue.includes(",")) { + // Has delimiter in data, surround with quote (which the value doesn't already contain) + return `"${colValue}"`; + } else { + // No quote or delimiter, just include plainly + return `${colValue}`; + } + } else if (colValue === undefined) { + // Export as empty CSV field + return ""; + } else { + return `${colValue}`; + } } - } else if (colValue === undefined) { - // Export as empty CSV field - return ''; - } else { - return `${colValue}`; - } - } - }).join(','); - csvString += row + '\n'; - } + }) + .join(","); + csvString += row + "\n"; + } - // Deliver to browser to download file - const element = document.createElement('a'); - const file = new Blob([csvString], { type: 'text/csv' }); - element.href = URL.createObjectURL(file); - element.download = `${className}.csv`; - document.body.appendChild(element); // Required for this to work in FireFox - element.click(); - document.body.removeChild(element); - } + // Deliver to browser to download file + const element = document.createElement("a"); + const file = new Blob([csvString], { type: "text/csv" }); + element.href = URL.createObjectURL(file); + element.download = `${className}.csv`; + document.body.appendChild(element); // Required for this to work in FireFox + element.click(); + document.body.removeChild(element); + }; if (!rows["*"]) { const objects = await query.find({ useMasterKey: true }); @@ -1370,10 +1384,12 @@ class Browser extends DashboardView { (obj) => { batch.push(obj); if (batch.length % 10 === 0) { - this.setState({exportingCount: batch.length}); + this.setState({ exportingCount: batch.length }); } const one_gigabyte = Math.pow(2, 30); - const size = new TextEncoder().encode(JSON.stringify(batch)).length / one_gigabyte; + const size = + new TextEncoder().encode(JSON.stringify(batch)).length / + one_gigabyte; if (size.length > 1) { processObjects(batch); batch = []; From 070b352f4b224f75ff5a12859f7a7a25fc449256 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 19 Jan 2023 16:32:30 +1100 Subject: [PATCH 03/10] add indent --- src/dashboard/Data/Browser/Browser.react.js | 16 +++++++--------- .../Browser/ExportSelectedRowsDialog.react.js | 14 ++++++++++---- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index 087d0a67fa..a41cdbf9ff 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -1257,7 +1257,7 @@ class Browser extends DashboardView { }); } - async confirmExportSelectedRows(rows, type) { + async confirmExportSelectedRows(rows, type, indentation) { this.setState({ rowsToExport: null, exporting: true, exportingCount: 0 }); const className = this.props.params.className; const query = new Parse.Query(className); @@ -1297,7 +1297,7 @@ class Browser extends DashboardView { return json; }), null, - 2 + indentation ? 2 : null, ), ], { type: "application/json" } @@ -1379,10 +1379,9 @@ class Browser extends DashboardView { this.setState({ exporting: false, exportingCount: objects.length }); } else { let batch = []; - let completion = null; - query.each( + query.eachBatch( (obj) => { - batch.push(obj); + batch.push(...obj); if (batch.length % 10 === 0) { this.setState({ exportingCount: batch.length }); } @@ -1394,12 +1393,11 @@ class Browser extends DashboardView { processObjects(batch); batch = []; } - clearTimeout(completion); - completion = setTimeout(() => { + if (obj.length !== 100) { processObjects(batch); batch = []; this.setState({ exporting: false, exportingCount: 0 }); - }, 5000); + } }, { useMasterKey: true } ); @@ -1817,7 +1815,7 @@ class Browser extends DashboardView { className={className} selection={this.state.rowsToExport} onCancel={this.cancelExportSelectedRows} - onConfirm={(type) => this.confirmExportSelectedRows(this.state.rowsToExport, type)} + onConfirm={(type, indentation) => this.confirmExportSelectedRows(this.state.rowsToExport, type, indentation)} /> ); } diff --git a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js index 21647c1c05..3dd3835732 100644 --- a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js +++ b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js @@ -5,12 +5,13 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ -import Modal from 'components/Modal/Modal.react'; -import React from 'react'; +import Modal from 'components/Modal/Modal.react'; +import React from 'react'; import Dropdown from 'components/Dropdown/Dropdown.react'; import Field from 'components/Field/Field.react'; import Label from 'components/Label/Label.react'; import Option from 'components/Dropdown/Option.react'; +import Toggle from 'components/Toggle/Toggle.react'; export default class ExportSelectedRowsDialog extends React.Component { constructor() { @@ -18,7 +19,8 @@ export default class ExportSelectedRowsDialog extends React.Component { this.state = { confirmation: '', - exportType: '.csv' + exportType: '.csv', + indentation: true, }; } @@ -38,7 +40,7 @@ export default class ExportSelectedRowsDialog extends React.Component { confirmText='Export' cancelText='Cancel' onCancel={this.props.onCancel} - onConfirm={() => this.props.onConfirm(this.state.exportType)}> + onConfirm={() => this.props.onConfirm(this.state.exportType, this.state.indentation)}> } input={ @@ -49,6 +51,10 @@ export default class ExportSelectedRowsDialog extends React.Component { } /> + {this.state.exportType === '.json' && } + input={ {this.setState({indentation})}} />} /> + } ); } From a1c19b047d717de637026da1266f734b2f10b330 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 19 Jan 2023 16:37:22 +1100 Subject: [PATCH 04/10] Update Browser.react.js --- src/dashboard/Data/Browser/Browser.react.js | 40 ++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index a41cdbf9ff..1afbc13ff5 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -1262,13 +1262,13 @@ class Browser extends DashboardView { const className = this.props.params.className; const query = new Parse.Query(className); - if (!rows["*"]) { + if (!rows['*']) { // Export selected const objectIds = []; for (const objectId in this.state.rowsToExport) { objectIds.push(objectId); } - query.containedIn("objectId", objectIds); + query.containedIn('objectId', objectIds); query.limit(objectIds.length); } @@ -1286,8 +1286,8 @@ class Browser extends DashboardView { className ).filter((column) => column.visible); - if (type === ".json") { - const element = document.createElement("a"); + if (type === '.json') { + const element = document.createElement('a'); const file = new Blob( [ JSON.stringify( @@ -1300,7 +1300,7 @@ class Browser extends DashboardView { indentation ? 2 : null, ), ], - { type: "application/json" } + { type: 'application/json' } ); element.href = URL.createObjectURL(file); element.download = `${className}.json`; @@ -1310,22 +1310,22 @@ class Browser extends DashboardView { return; } - let csvString = columns.map((column) => column.name).join(",") + "\n"; + let csvString = columns.map((column) => column.name).join(',') + '\n'; for (const object of objects) { const row = columns .map((column) => { const type = columnsObject[column.name].type; - if (column.name === "objectId") { + if (column.name === 'objectId') { return object.id; - } else if (type === "Relation" || type === "Pointer") { + } else if (type === 'Relation' || type === 'Pointer') { if (object.get(column.name)) { return object.get(column.name).id; } else { - return ""; + return ''; } } else { let colValue; - if (column.name === "ACL") { + if (column.name === 'ACL') { colValue = object.getACL(); } else { colValue = object.get(column.name); @@ -1333,18 +1333,18 @@ class Browser extends DashboardView { // Stringify objects and arrays if ( Object.prototype.toString.call(colValue) === - "[object Object]" || - Object.prototype.toString.call(colValue) === "[object Array]" + '[object Object]' || + Object.prototype.toString.call(colValue) === '[object Array]' ) { colValue = JSON.stringify(colValue); } - if (typeof colValue === "string") { + if (typeof colValue === 'string') { if (colValue.includes('"')) { // Has quote in data, escape and quote // If the value contains both a quote and delimiter, adding quotes and escaping will take care of both scenarios colValue = colValue.split('"').join('""'); return `"${colValue}"`; - } else if (colValue.includes(",")) { + } else if (colValue.includes(',')) { // Has delimiter in data, surround with quote (which the value doesn't already contain) return `"${colValue}"`; } else { @@ -1353,19 +1353,19 @@ class Browser extends DashboardView { } } else if (colValue === undefined) { // Export as empty CSV field - return ""; + return ''; } else { return `${colValue}`; } } }) - .join(","); - csvString += row + "\n"; + .join(','); + csvString += row + '\n'; } // Deliver to browser to download file - const element = document.createElement("a"); - const file = new Blob([csvString], { type: "text/csv" }); + const element = document.createElement('a'); + const file = new Blob([csvString], { type: 'text/csv' }); element.href = URL.createObjectURL(file); element.download = `${className}.csv`; document.body.appendChild(element); // Required for this to work in FireFox @@ -1373,7 +1373,7 @@ class Browser extends DashboardView { document.body.removeChild(element); }; - if (!rows["*"]) { + if (!rows['*']) { const objects = await query.find({ useMasterKey: true }); processObjects(objects); this.setState({ exporting: false, exportingCount: objects.length }); From 46b1346f20e01d28e772e5ef8e1706c8ce63e917 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 21 Jan 2023 07:00:55 +1100 Subject: [PATCH 05/10] Update src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js Co-authored-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js index 3dd3835732..946169f2a5 100644 --- a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js +++ b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js @@ -35,7 +35,7 @@ export default class ExportSelectedRowsDialog extends React.Component { type={Modal.Types.INFO} icon='warn-outline' title={this.props.selection['*'] ? 'Export all rows?' : (selectionLength === 1 ? 'Export 1 selected row?' : `Export ${selectionLength} selected rows?`)} - subtitle={this.props.selection['*'] ? 'Note: This will export mutliple files with maximum 1gb each. This might take a while.' : ''} + subtitle={this.props.selection['*'] ? 'Large datasets are exported as multiple files of up to 1 GB each.' : ''} disabled={!this.valid()} confirmText='Export' cancelText='Cancel' From cd9fee0f941ed60aa16ea04f2873c40791599cc5 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 23 Jan 2023 10:20:08 +1100 Subject: [PATCH 06/10] add confirmation --- src/dashboard/Data/Browser/Browser.react.js | 2 ++ .../Browser/ExportSelectedRowsDialog.react.js | 34 +++++++++++++++++++ .../Browser/ExportSelectedRowsDialog.scss | 9 +++++ 3 files changed, 45 insertions(+) create mode 100644 src/dashboard/Data/Browser/ExportSelectedRowsDialog.scss diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index f065395f16..46e18e8947 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -1864,6 +1864,8 @@ class Browser extends DashboardView { this.confirmExportSelectedRows(this.state.rowsToExport, type, indentation)} /> diff --git a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js index 946169f2a5..90e252fe38 100644 --- a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js +++ b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js @@ -12,6 +12,8 @@ import Field from 'components/Field/Field.react'; import Label from 'components/Label/Label.react'; import Option from 'components/Dropdown/Option.react'; import Toggle from 'components/Toggle/Toggle.react'; +import TextInput from 'components/TextInput/TextInput.react'; +import styles from 'dashboard/Data/Browser/ExportSelectedRowsDialog.scss'; export default class ExportSelectedRowsDialog extends React.Component { constructor() { @@ -25,11 +27,28 @@ export default class ExportSelectedRowsDialog extends React.Component { } valid() { + if (this.state.confirmation !== 'export all') { + return false; + } return true; } + formatBytes(bytes) { + if (!+bytes) return '0 Bytes' + + const k = 1024 + const decimals = 2 + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}` +} + + render() { let selectionLength = Object.keys(this.props.selection).length; + const fileSize = new TextEncoder().encode(JSON.stringify(this.props.data, null, this.state.exportType === '.json' && this.state.indentation ? 2 : null)).length / this.props.data.length return ( this.props.onConfirm(this.state.exportType, this.state.indentation)}> +
+
} input={ @@ -55,6 +77,18 @@ export default class ExportSelectedRowsDialog extends React.Component { label={
); } diff --git a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.scss b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.scss new file mode 100644 index 0000000000..2ced8f2386 --- /dev/null +++ b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.scss @@ -0,0 +1,9 @@ +.row { + display: block; + position: relative; + height: 100px; + border-bottom: 1px solid #e0e0e1; +} +.label { + line-height: 16px; +} From 1d6be6108fa4da632bdc0f5119505ade846ef09c Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 23 Jan 2023 10:22:35 +1100 Subject: [PATCH 07/10] Update ExportSelectedRowsDialog.react.js --- src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js index 90e252fe38..d5345b209e 100644 --- a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js +++ b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js @@ -61,7 +61,7 @@ export default class ExportSelectedRowsDialog extends React.Component { onCancel={this.props.onCancel} onConfirm={() => this.props.onConfirm(this.state.exportType, this.state.indentation)}>
-
} From b0cf907ef0776dad570c380e3bf3a18b4a088f91 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 25 Jan 2023 10:52:13 +1100 Subject: [PATCH 08/10] Update ExportSelectedRowsDialog.react.js --- .../Data/Browser/ExportSelectedRowsDialog.react.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js index d5345b209e..4bdaf63d2f 100644 --- a/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js +++ b/src/dashboard/Data/Browser/ExportSelectedRowsDialog.react.js @@ -27,6 +27,9 @@ export default class ExportSelectedRowsDialog extends React.Component { } valid() { + if (!this.props.selection['*']) { + return true; + } if (this.state.confirmation !== 'export all') { return false; } @@ -60,9 +63,10 @@ export default class ExportSelectedRowsDialog extends React.Component { cancelText='Cancel' onCancel={this.props.onCancel} onConfirm={() => this.props.onConfirm(this.state.exportType, this.state.indentation)}> -
-