diff --git a/assets/javascripts/discourse/components/query-result.js.es6 b/assets/javascripts/discourse/components/query-result.js.es6 index 116dd83..4c9a522 100644 --- a/assets/javascripts/discourse/components/query-result.js.es6 +++ b/assets/javascripts/discourse/components/query-result.js.es6 @@ -10,6 +10,11 @@ var defaultFallback = function(buffer, content, defaultRender) { defaultRender(b function isoYMD(date) { return date.getUTCFullYear() + "-" + date.getUTCMonth() + "-" + date.getUTCDate(); } +function randomIdShort() { + return 'xxxxxxxx'.replace(/[xy]/g, function() { + return (Math.random() * 16 | 0).toString(16); + }); +} const QueryResultComponent = Ember.Component.extend({ layoutName: 'explorer-query-result', @@ -25,10 +30,6 @@ const QueryResultComponent = Ember.Component.extend({ return this.get('content.columns').length; }.property('content.columns.length'), - downloadName: function() { - return this.get('query.name') + "@" + window.location.host + "-" + isoYMD(new Date()) + ".dcqresult.json"; - }.property(), - duration: function() { return I18n.t('explorer.run_time', {value: I18n.toNumber(this.get('content.duration'), {precision: 1})}); }.property('content.duration'), @@ -99,19 +100,45 @@ const QueryResultComponent = Ember.Component.extend({ }); }.property('content', 'columns.@each'), - _clickDownloadButton: function() { - const self = this; - const $button = this.$().find("#result-download"); - // use $.one to do once - $button.one('mouseover', function(e) { - const a = e.target; - let resultString = "data:text/plain;base64,"; - var jsonString = JSON.stringify(self.get('content')); - resultString += btoa(jsonString); + actions: { + downloadResult() { + const blankUrl = "about:blank"; + const windowName = randomIdShort(); - a.href = resultString; - }); - }.on('didInsertElement'), + let form = document.createElement("form"); + form.setAttribute('id', 'query-download-result'); + form.setAttribute('method', 'post'); + form.setAttribute('action', Discourse.getURL('/admin/plugins/explorer/queries/' + this.get('query.id') + '/run.json?download=1')); + form.setAttribute('target', windowName); + form.setAttribute('style', 'display:none;'); + + function addInput(form, name, value) { + let field; + field = document.createElement('input'); + field.setAttribute('name', name); + field.setAttribute('value', value); + form.appendChild(field); + } + + addInput(form, 'params', JSON.stringify(this.get('params'))); + addInput(form, 'explain', this.get('hasExplain')); + addInput(form, 'limit', '1000000'); + + const newWindow = window.open(blankUrl, windowName); + + Discourse.ajax('/session/csrf.json').then(function(csrf) { + addInput(form, 'authenticity_token', csrf.csrf); + + document.body.appendChild(form); + form.submit(); + + Em.run.next('afterRender', function() { + document.body.removeChild(form); + newWindow.close(); + }); + }); + } + }, parent: function() { return this; }.property() diff --git a/assets/javascripts/discourse/controllers/admin-plugins-explorer.js.es6 b/assets/javascripts/discourse/controllers/admin-plugins-explorer.js.es6 index f620c74..aa6762a 100644 --- a/assets/javascripts/discourse/controllers/admin-plugins-explorer.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-plugins-explorer.js.es6 @@ -48,7 +48,7 @@ export default Ember.ArrayController.extend({ return this.get('selectedItem').save().then(function() { const query = self.get('selectedItem'); query.markNotDirty(); - self.set('editName', false); + self.set('editing', false); }).catch(function(x) { popupAjaxError(x); throw x; @@ -70,7 +70,7 @@ export default Ember.ArrayController.extend({ }, editName() { - this.set('editName', true); + this.set('editing', true); }, download() { @@ -112,7 +112,7 @@ export default Ember.ArrayController.extend({ const query = self.get('selectedItem'); query.setProperties(result.getProperties(Query.updatePropertyNames)); query.markNotDirty(); - self.set('editName', false); + self.set('editing', false); }).catch(popupAjaxError).finally(function() { self.set('loading', false); }); @@ -145,6 +145,9 @@ export default Ember.ArrayController.extend({ if (this.get('selectedItem.dirty')) { return; } + if (this.get('runDisabled')) { + return; + } this.set('loading', true); this.set('showResults', false); diff --git a/assets/javascripts/discourse/templates/admin/plugins-explorer.hbs b/assets/javascripts/discourse/templates/admin/plugins-explorer.hbs index 554dbcf..3a55a79 100644 --- a/assets/javascripts/discourse/templates/admin/plugins-explorer.hbs +++ b/assets/javascripts/discourse/templates/admin/plugins-explorer.hbs @@ -28,7 +28,7 @@
{{#if selectedItem}}
- {{#if editName}} + {{#if editing}} {{text-field value=selectedItem.name}} {{else}}

{{selectedItem.name}}

@@ -36,7 +36,7 @@ {{/if}}
- {{#if editName}} + {{#if editing}} {{textarea value=selectedItem.description}} {{else}} {{selectedItem.description}} @@ -46,9 +46,7 @@
-
- {{textarea value=selectedItem.sql class="grippie-target"}} -
+ {{ace-editor content=selectedItem.sql mode="sql"}}
@@ -76,12 +74,12 @@ {{/if}}
-
+
{{#if selectedItem.param_names}}
- {{d-button action="saveDefaults" label="explorer.save_params"}} - {{d-button action="resetParams" label="explorer.reset_params"}} + {{d-button action="saveDefaults" label="explorer.save_params" type="button"}} + {{d-button action="resetParams" label="explorer.reset_params" type="button"}}
{{#each selectedItem.param_names as |pname|}}
@@ -101,9 +99,9 @@ {{d-button action="saverun" label="explorer.saverun"}} {{/if}} {{else}} - {{d-button action="run" label="explorer.run" disabled=runDisabled class="btn-primary"}} + {{d-button action="run" label="explorer.run" disabled=runDisabled class="btn-primary" type="submit"}} {{/if}} -
+
{{/if}} diff --git a/assets/javascripts/discourse/templates/explorer-query-result.hbs b/assets/javascripts/discourse/templates/explorer-query-result.hbs index a09dbf0..8f1978b 100644 --- a/assets/javascripts/discourse/templates/explorer-query-result.hbs +++ b/assets/javascripts/discourse/templates/explorer-query-result.hbs @@ -1,8 +1,5 @@
- - {{fa-icon "download"}} - {{i18n "explorer.download_json"}} - + {{d-button action="downloadResult" icon="download" label="explorer.download_json"}}
{{duration}}
diff --git a/assets/stylesheets/explorer.scss b/assets/stylesheets/explorer.scss index 147c418..f2669be 100644 --- a/assets/stylesheets/explorer.scss +++ b/assets/stylesheets/explorer.scss @@ -8,22 +8,22 @@ .editor-panel { float: left; width: 65%; - - .sql textarea { - padding: 4px; - height: calc(400px - 8px - 2px); + .ace-wrapper { + position: relative; + height: 400px; width: 100%; - font-family: monospace; - box-shadow: none; - border: 1px solid dark-light-diff($primary, $secondary, 80%, -20%); - margin: 0; - border-radius: 0; - transition: initial; + } + .ace_editor { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; } } .right-panel { float: right; - width: calc(35% - 10px); + width: calc(35% - 0px); .schema { border-left: 1px solid dark-light-diff($primary, $secondary, 80%, -20%); height: 400px; diff --git a/plugin.rb b/plugin.rb index 1a8fee7..09050fc 100644 --- a/plugin.rb +++ b/plugin.rb @@ -107,11 +107,12 @@ after_initialize do WITH query AS ( #{query.sql} ) SELECT * FROM query -LIMIT #{opts[:limit] || 1000} +LIMIT #{opts[:limit] || 250} SQL time_start = Time.now result = ActiveRecord::Base.exec_sql(sql, query_args) + result.check # make sure it's done time_end = Time.now if opts[:explain] @@ -130,8 +131,9 @@ SQL { error: err, pg_result: result, - duration_nanos: time_end.nsec - time_start.nsec, + duration_secs: time_end - time_start, explain: explain, + params_full: query_args.tap {|h| h.delete :xxdummy} } end @@ -347,7 +349,6 @@ SQL end skip_before_filter :check_xhr, only: [:show] - def show check_xhr unless params[:export] @@ -407,6 +408,7 @@ SQL end end + skip_before_filter :check_xhr, only: [:run] # Return value: # success - true/false. if false, inspect the errors value. # errors - array of strings. @@ -416,11 +418,20 @@ SQL # explain - string. (Optional - pass explain=true in the request) Postgres query plan, UNIX newlines. # rows - array of array of strings. Results of the query. In the same order as 'columns'. def run + check_xhr unless params[:download] query = DataExplorer::Query.find(params[:id].to_i) + if params[:download] + response.headers['Content-Disposition'] = + "attachment; filename=#{query.slug}@#{Slug.for(Discourse.current_hostname, 'discourse')}-#{Date.today}.dcqresult.json" + response.sending_file = true + end + query_params = MultiJson.load(params[:params]) + opts = {current_user: current_user.username} opts[:explain] = true if params[:explain] == "true" opts[:limit] = params[:limit].to_i if params[:limit] + result = DataExplorer.run_query(query, query_params, opts) if result[:error] @@ -431,7 +442,7 @@ SQL err_msg = err.message if err.is_a? ActiveRecord::StatementInvalid err_class = err.original_exception.class - err_msg.gsub!("#{err_class.to_s}:", '') + err_msg.gsub!("#{err_class}:", '') else err_msg = "#{err_class}: #{err_msg}" end @@ -446,7 +457,8 @@ SQL json = { success: true, errors: [], - duration: (result[:duration_nanos].to_f / 1_000_000).round(1), + duration: (result[:duration_secs].to_f * 1000).round(1), + params: result[:params_full], columns: cols, } json[:explain] = result[:explain] if opts[:explain]