Skip to content
Snippets Groups Projects
Commit cfe97820 authored by Allen Gilliland's avatar Allen Gilliland
Browse files

Merge pull request #1916 from metabase/fix-csv-download

switch CSV download over to using an http POST request so we can support long queries
parents 29e74312 9a90f17b
Branches
Tags
No related merge requests found
......@@ -115,7 +115,7 @@ NSString *BaseURL() {
[self.webView.mainFrame loadRequest:request];
}
- (void)saveCSV:(NSString *)apiURL {
- (void)saveCSV:(NSString *)datasetQuery {
NSSavePanel *savePanel = [NSSavePanel savePanel];
savePanel.allowedFileTypes = @[@"csv"];
savePanel.allowsOtherFileTypes = NO;
......@@ -133,8 +133,14 @@ NSString *BaseURL() {
if ([savePanel runModal] == NSFileHandlingPanelOKButton) {
NSLog(@"Will save CSV at: %@", savePanel.URL);
NSURL *url = [NSURL URLWithString:apiURL relativeToURL:[NSURL URLWithString:BaseURL()]];
NSURLRequest *csvRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10.0f];
NSURL *url = [NSURL URLWithString:@"/api/dataset/csv" relativeToURL:[NSURL URLWithString:BaseURL()]];
NSString *postBody = [@"query=" stringByAppendingString:datasetQuery];
NSData *data = [postBody dataUsingEncoding:NSUTF8StringEncoding];
NSMutableURLRequest *csvRequest = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10.0f];
csvRequest.HTTPMethod = @"POST";
[csvRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
[csvRequest setValue:[NSString stringWithFormat:@"%lu", (NSUInteger)data.length] forHTTPHeaderField:@"Content-Length"];
csvRequest.HTTPBody = data;
[NSURLConnection sendAsynchronousRequest:csvRequest queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSError *writeError = nil;
[data writeToURL:savePanel.URL options:NSDataWritingAtomic error:&writeError];
......@@ -159,8 +165,8 @@ NSString *BaseURL() {
};
// custom functions for OS X integration are available to the frontend as properties of window.OSX
context[@"OSX"] = @{@"saveCSV": ^(JSValue *apiURL){
[self saveCSV:apiURL.description];
context[@"OSX"] = @{@"saveCSV": ^(JSValue *datasetQuery){
[self saveCSV:datasetQuery.description];
}};
}
......
......@@ -346,12 +346,6 @@ CardControllers.controller('CardDetail', [
visualizationModel.isRunning = isRunning;
visualizationModel.isObjectDetail = isObjectDetail;
if (queryResult && !queryResult.error) {
visualizationModel.downloadLink = '/api/dataset/csv?query=' + encodeURIComponent(JSON.stringify(card.dataset_query));
} else {
visualizationModel.downloadLink = null;
}
React.render(<QueryVisualization {...visualizationModel}/>, document.getElementById('react_qb_viz'));
}
......
......@@ -33,7 +33,6 @@ export default class QueryVisualization extends Component {
tableMetadata: PropTypes.object,
tableForeignKeys: PropTypes.array,
tableForeignKeyReferences: PropTypes.object,
downloadLink: PropTypes.string,
setDisplayFn: PropTypes.func.isRequired,
setChartColorFn: PropTypes.func.isRequired,
setSortFn: PropTypes.func.isRequired,
......@@ -132,23 +131,33 @@ export default class QueryVisualization extends Component {
}
}
renderDownloadButton() {
const { downloadLink } = this.props;
onDownloadCSV() {
const form = this._downloadCsvForm.getDOMNode();
form.query.value = JSON.stringify(this.props.card.dataset_query);
form.submit();
}
// NOTE: we expect our component provider set this to something falsey if download not available
if (downloadLink) {
const { result } = this.props;
renderDownloadButton() {
const { card, result } = this.props;
if (result && !result.error) {
if (result && result.data && result.data.rows_truncated) {
// this is a "large" dataset, so show a modal to inform users about this and make them click again to d/l
let downloadButton;
if (window.OSX) {
downloadButton = (<button className="Button Button--primary" onClick={() => {
window.OSX.saveCSV(this.props.downloadLink);
window.OSX.saveCSV(JSON.stringify(card.dataset_query));
this.refs.downloadModal.toggle()
}}>Download CSV</button>);
} else {
downloadButton = (<a className="Button Button--primary" href={downloadLink} target="_blank" onClick={() => this.refs.downloadModal.toggle()}>Download CSV</a>);
downloadButton = (
<form ref={(c) => this._downloadCsvForm = c} method="POST" action="/api/dataset/csv">
<input type="hidden" name="query" value="" />
<a className="Button Button--primary" onClick={() => {this.onDownloadCSV(); this.refs.downloadModal.toggle();}}>
Download CSV
</a>
</form>
);
}
return (
......@@ -176,16 +185,19 @@ export default class QueryVisualization extends Component {
if (window.OSX) {
return (
<a classname="mx1" title="Download this data" onClick={function() {
window.OSX.saveCSV(downloadLink);
window.OSX.saveCSV(JSON.stringify(card.dataset_query));
}}>
<Icon name='download' width="16px" height="16px" />
</a>
);
} else {
return (
<a className="mx1" href={downloadLink} title="Download this data" target="_blank">
<Icon name='download' width="16px" height="16px" />
</a>
<form ref={(c) => this._downloadCsvForm = c} method="POST" action="/api/dataset/csv">
<input type="hidden" name="query" value="" />
<a className="mx1" title="Download this data" onClick={() => this.onDownloadCSV()}>
<Icon name='download' width="16px" height="16px" />
</a>
</form>
);
}
}
......
......@@ -45,4 +45,25 @@
{:status 500
:body (:error response)})))
(defendpoint POST "/csv"
"Execute an MQL query and download the result data as a CSV file."
[query]
{query [Required String->Dict]}
(clojure.pprint/pprint query)
(read-check Database (:database query))
(let [{{:keys [columns rows]} :data :keys [status] :as response} (driver/dataset-query query {:executed_by *current-user-id*})
columns (map name columns)] ; turn keywords into strings, otherwise we get colons in our output
(if (= status :completed)
;; successful query, send CSV file
{:status 200
:body (with-out-str
(csv/write-csv *out* (into [columns] rows)))
:headers {"Content-Type" "text/csv"
"Content-Disposition" (str "attachment; filename=\"query_result_" (u/date->iso-8601) ".csv\"")}}
;; failed query, send error message
{:status 500
:body (:error response)})))
(define-routes)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment