Import the result table and we're live
This commit is contained in:
parent
dba181d92e
commit
c56a40cacd
|
@ -0,0 +1,8 @@
|
|||
export default Ember.TextField.extend({
|
||||
value: function(key, value, previousValue) {
|
||||
if (arguments.length > 1) {
|
||||
this.get('params')[this.get('pname')] = value;
|
||||
}
|
||||
return this.get('params')[this.get('pname')];
|
||||
}.property()
|
||||
});
|
|
@ -0,0 +1,297 @@
|
|||
var ColumnHandlers = [];
|
||||
var AssistedHandlers = {};
|
||||
const Escape = Handlebars.Utils.escapeExpression;
|
||||
|
||||
import avatarTemplate from 'discourse/lib/avatar-template';
|
||||
import { categoryLinkHTML } from 'discourse/helpers/category-link';
|
||||
|
||||
var defaultFallback = function(buffer, content, defaultRender) { defaultRender(buffer, content); };
|
||||
|
||||
function isoYMD(date) {
|
||||
return date.getUTCFullYear() + "-" + date.getUTCMonth() + "-" + date.getUTCDate();
|
||||
}
|
||||
|
||||
const QueryResultComponent = Ember.Component.extend({
|
||||
layoutName: 'explorer-query-result',
|
||||
|
||||
rows: Em.computed.alias('content.rows'),
|
||||
columns: Em.computed.alias('content.columns'),
|
||||
params: Em.computed.alias('content.params'),
|
||||
explainText: Em.computed.alias('content.explain'),
|
||||
|
||||
hasExplain: Em.computed.notEmpty('content.explain'),
|
||||
noParams: Em.computed.empty('params'),
|
||||
colCount: function() {
|
||||
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'),
|
||||
|
||||
parameterAry: function() {
|
||||
let arr = [];
|
||||
const params = this.get('params');
|
||||
for (var key in params) {
|
||||
if (params.hasOwnProperty(key)) {
|
||||
arr.push({key: key, value: params[key]});
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}.property('params.@each'),
|
||||
|
||||
columnHandlers: function() {
|
||||
const self = this;
|
||||
if (!this.get('content')) {
|
||||
return;
|
||||
}
|
||||
if (self.get('opts.notransform')) {
|
||||
return this.get('columns').map(function(colName) {
|
||||
return {
|
||||
name: colName,
|
||||
displayName: colName,
|
||||
render: defaultFallback
|
||||
};
|
||||
});
|
||||
}
|
||||
return this.get('columns').map(function(colName, idx) {
|
||||
let handler = defaultFallback;
|
||||
|
||||
if (/\$/.test(colName)) {
|
||||
var match = /(\w+)\$(\w*)/.exec(colName);
|
||||
if (match[1] && self.get('content.relations')[match[1]] && AssistedHandlers[match[1]]) {
|
||||
return {
|
||||
name: colName,
|
||||
displayName: match[2] || match[1],
|
||||
render: AssistedHandlers[match[1]]
|
||||
};
|
||||
} else if (match[1] == '') {
|
||||
// Return as "$column" for no special handling
|
||||
return {
|
||||
name: colName,
|
||||
displayName: match[2] || match[1],
|
||||
render: defaultFallback
|
||||
}
|
||||
}
|
||||
} else if (/\?column\?/.test(colName)) {
|
||||
return {
|
||||
name: "generic-column",
|
||||
displayName: I18n.t('explorer.column', {number: idx+1}),
|
||||
render: defaultFallback
|
||||
}
|
||||
}
|
||||
|
||||
ColumnHandlers.forEach(function(handlerInfo) {
|
||||
if (handlerInfo.regex.test(colName)) {
|
||||
handler = handlerInfo.render;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
name: colName,
|
||||
displayName: colName,
|
||||
render: handler
|
||||
};
|
||||
});
|
||||
}.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);
|
||||
|
||||
a.href = resultString;
|
||||
});
|
||||
}.on('didInsertElement'),
|
||||
|
||||
parent: function() { return this; }.property()
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* ColumnHandler callback arguments:
|
||||
* buffer: rendering buffer
|
||||
* content: content of the query result cell
|
||||
* defaultRender: call this wth (buffer, content) to fall back
|
||||
* extra: the entire response
|
||||
*/
|
||||
|
||||
ColumnHandlers.push({ regex: /user_id/, render: function(buffer, content, defaultRender) {
|
||||
if (!/^\d+$/.test(content)) {
|
||||
return defaultRender(buffer, content);
|
||||
}
|
||||
buffer.push("<a href='/users/by-id/");
|
||||
buffer.push(content);
|
||||
buffer.push("'>User #");
|
||||
buffer.push(content);
|
||||
buffer.push("</a>");
|
||||
}});
|
||||
ColumnHandlers.push({ regex: /post_id/, render: function(buffer, content, defaultRender) {
|
||||
if (!/^\d+$/.test(content)) {
|
||||
return defaultRender(buffer, content);
|
||||
}
|
||||
buffer.push("<a href='/p/");
|
||||
buffer.push(content);
|
||||
buffer.push("'>Post #");
|
||||
buffer.push(content);
|
||||
buffer.push("</a>");
|
||||
}});
|
||||
ColumnHandlers.push({ regex: /badge_id/, render: function(buffer, content, defaultRender) {
|
||||
if (!/^\d+$/.test(content)) {
|
||||
return defaultRender(buffer, content);
|
||||
}
|
||||
buffer.push("<a href='/badges/");
|
||||
buffer.push(content);
|
||||
buffer.push("/-'>Badge #");
|
||||
buffer.push(content);
|
||||
buffer.push("</a>");
|
||||
}});
|
||||
ColumnHandlers.push({ regex: /topic_id/, render: function(buffer, content, defaultRender) {
|
||||
if (!/^\d+$/.test(content)) {
|
||||
return defaultRender(buffer, content);
|
||||
}
|
||||
buffer.push("<a href='/t/");
|
||||
buffer.push(content);
|
||||
buffer.push("/from-link'>Topic #");
|
||||
buffer.push(content);
|
||||
buffer.push("</a>");
|
||||
}});
|
||||
|
||||
AssistedHandlers['reltime'] = function(buffer, content, defaultRender) {
|
||||
const parsedDate = new Date(content);
|
||||
if (!parsedDate.getTime()) {
|
||||
return defaultRender(buffer, content);
|
||||
}
|
||||
|
||||
buffer.push(Discourse.Formatter.relativeAge(parsedDate, {format: 'medium'}));
|
||||
};
|
||||
|
||||
AssistedHandlers['category'] = function(buffer, content, defaultRender) {
|
||||
const contentId = parseInt(content, 10);
|
||||
if (isNaN(contentId)) {
|
||||
return defaultRender(buffer, content);
|
||||
}
|
||||
const category = Discourse.Category.findById(contentId);
|
||||
if (!category) {
|
||||
return defaultRender(buffer, content);
|
||||
}
|
||||
|
||||
const opts = {
|
||||
link: true,
|
||||
allowUncategorized: true
|
||||
};
|
||||
buffer.push(categoryLinkHTML(category, opts));
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to wrap the handler in a function that fetches the object out of the response.
|
||||
*
|
||||
* @param name the part of the column name before the $
|
||||
* @param callback Function(buffer, object [, defaultRender])
|
||||
*/
|
||||
function registerRelationAssistedHandler(name, callback) {
|
||||
AssistedHandlers[name] = function(buffer, content, defaultRender, response) {
|
||||
const contentId = parseInt(content, 10);
|
||||
if (isNaN(contentId)) {
|
||||
return defaultRender(buffer, content);
|
||||
}
|
||||
const relationObject = response.relations[name].find(function(relObj) {
|
||||
return relObj.id === contentId;
|
||||
});
|
||||
if (!relationObject) {
|
||||
Em.Logger.warn("Couldn't find " + name + " with id " + contentId + " in query response");
|
||||
return defaultRender(buffer, content);
|
||||
}
|
||||
|
||||
callback(buffer, relationObject, defaultRender);
|
||||
}
|
||||
}
|
||||
|
||||
registerRelationAssistedHandler('user', function(buffer, obj) {
|
||||
buffer.push("<a href='/users/");
|
||||
buffer.push(obj.username);
|
||||
buffer.push("'>");
|
||||
buffer.push(Discourse.Utilities.avatarImg({
|
||||
size: "small",
|
||||
avatarTemplate: avatarTemplate(obj.username, obj.uploaded_avatar_id)
|
||||
}));
|
||||
buffer.push(" ");
|
||||
buffer.push(obj.username);
|
||||
buffer.push("</a>");
|
||||
});
|
||||
|
||||
registerRelationAssistedHandler('badge', function(buffer, obj) {
|
||||
// TODO It would be nice to be able to invoke the {{user-badge}} helper from here.
|
||||
// Looks like that would need a ContainerView
|
||||
|
||||
/*
|
||||
<span id="ember2197" class="ember-view">
|
||||
<a id="ember2201" class="ember-view" href="/badges/9/autobiographer">
|
||||
<span id="ember2221" class="ember-view user-badge badge-type-bronze" data-badge-name="Autobiographer" title="Filled user profile information">
|
||||
<i class="fa fa-certificate"></i>
|
||||
Autobiographer
|
||||
</span></a></span>
|
||||
*/
|
||||
|
||||
if (true) {
|
||||
buffer.push('<span><a href="/badges/');
|
||||
buffer.push(obj.id + '/' + Escape(obj.name));
|
||||
buffer.push('"><span data-badge-name="');
|
||||
buffer.push(Escape(obj.name));
|
||||
buffer.push('" class="user-badge badge-type-');
|
||||
buffer.push(Escape(obj.badge_type.toLowerCase()));
|
||||
buffer.push('" title="');
|
||||
buffer.push(Escape(obj.description));
|
||||
buffer.push('">');
|
||||
// icon-or-image
|
||||
if (obj.icon.indexOf('fa-') === 0) {
|
||||
buffer.push(" <i class='fa " + obj.icon + "'></i> ");
|
||||
} else {
|
||||
buffer.push(" <img src='" + obj.icon + "'> ");
|
||||
}
|
||||
buffer.push(Escape(obj.name));
|
||||
buffer.push("</span></a></span>");
|
||||
}
|
||||
});
|
||||
|
||||
registerRelationAssistedHandler('post', function(buffer, obj) {
|
||||
/*
|
||||
<aside class="quote" data-post="35" data-topic="117">
|
||||
<div class="title" style="cursor: pointer;">
|
||||
<div class="quote-controls">
|
||||
<i class="fa fa-chevron-down" title="expand/collapse"></i>
|
||||
<a href="/t/usability-on-the-cheap-and-easy/117/35" title="go to the quoted post" class="back"></a>
|
||||
</div>
|
||||
<img width="20" height="20" src="/user_avatar/localhost/riking/40/75.png" class="avatar">riking:</div>
|
||||
<blockquote>$EXCERPT</blockquote>
|
||||
</aside>
|
||||
*/
|
||||
buffer.push("<aside class='quote' data-post='" + obj.post_number + "' data-topic='" + obj.topic_id + "'>");
|
||||
buffer.push('<div class="title" style="cursor: pointer;">' +
|
||||
'<div class="quote-controls">' +
|
||||
'<i class="fa" title="expand/collapse"></i>');
|
||||
buffer.push('<a href="');
|
||||
buffer.push("/t/" + obj.slug + "/" + obj.topic_id + "/" + obj.post_number);
|
||||
buffer.push('" title="go to the post" class="quote-other-topic"></a>');
|
||||
buffer.push('</div>');
|
||||
buffer.push(Discourse.Utilities.avatarImg({
|
||||
size: "small",
|
||||
avatarTemplate: avatarTemplate(obj.username, obj.uploaded_avatar_id)
|
||||
}));
|
||||
buffer.push(obj.username + ":");
|
||||
buffer.push('</div>' +
|
||||
'<blockquote>');
|
||||
buffer.push(obj.excerpt);
|
||||
buffer.push('</blockquote></aside>');
|
||||
});
|
||||
|
||||
export default QueryResultComponent;
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
const defaultRender = function(buffer, content) {
|
||||
buffer.push(Handlebars.Utils.escapeExpression(content));
|
||||
};
|
||||
|
||||
const QueryRowContentComponent = Ember.Component.extend({
|
||||
tagName: "tr",
|
||||
|
||||
render(buffer) {
|
||||
const row = this.get('row');
|
||||
const response = this.get('extra');
|
||||
this.get('colRenders').forEach(function(colRender, idx) {
|
||||
buffer.push("<td data-column-name=" + Handlebars.Utils.escapeExpression(colRender.name) + ">");
|
||||
colRender.render(buffer, row[idx], defaultRender, response);
|
||||
buffer.push("</td>");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default QueryRowContentComponent;
|
|
@ -5,7 +5,7 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
|
|||
export default Ember.ArrayController.extend({
|
||||
selectedQueryId: null,
|
||||
results: null,
|
||||
dirty: false,
|
||||
showResults: false,
|
||||
loading: false,
|
||||
|
||||
explain: false,
|
||||
|
@ -21,10 +21,16 @@ export default Ember.ArrayController.extend({
|
|||
});
|
||||
}.property('selectedQueryId'),
|
||||
|
||||
clearResults: function() {
|
||||
this.set('showResults', false);
|
||||
this.set('results', null);
|
||||
}.observes('selectedQueryId'),
|
||||
|
||||
addCreatedRecord(record) {
|
||||
this.pushObject(record);
|
||||
this.set('selectedQueryId', Ember.get(record, 'id'));
|
||||
this.get('selectedItem').set('dirty', false);
|
||||
this.set('showResults', false);
|
||||
this.set('results', null);
|
||||
},
|
||||
|
||||
|
@ -48,6 +54,14 @@ export default Ember.ArrayController.extend({
|
|||
window.open(this.get('selectedItem.downloadUrl'), "_blank");
|
||||
},
|
||||
|
||||
resetParams() {
|
||||
this.get('selectedItem').resetParams();
|
||||
},
|
||||
|
||||
saveDefaults() {
|
||||
this.get('selectedItem').saveDefaults();
|
||||
},
|
||||
|
||||
create() {
|
||||
const self = this;
|
||||
this.set('loading', true);
|
||||
|
@ -109,20 +123,25 @@ export default Ember.ArrayController.extend({
|
|||
},
|
||||
|
||||
run() {
|
||||
const self = this;
|
||||
if (this.get('selectedItem.dirty')) {
|
||||
self.set('results', {errors: [I18n.t('errors.explorer.dirty')]});
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
|
||||
this.set('loading', true);
|
||||
Discourse.ajax("/admin/plugins/explorer/queries/" + this.get('selectedItem.id') + "/run", {
|
||||
type: "POST",
|
||||
data: {
|
||||
params: JSON.stringify({foo: 34}),
|
||||
params: JSON.stringify(this.get('selectedItem.params')),
|
||||
explain: true
|
||||
}
|
||||
}).then(function(result) {
|
||||
if (!result.success) {
|
||||
return popupAjaxError(result);
|
||||
}
|
||||
|
||||
console.log(result);
|
||||
self.set('showResults', true);
|
||||
self.set('results', result);
|
||||
}).catch(popupAjaxError).finally(function() {
|
||||
self.set('loading', false);
|
||||
|
|
|
@ -3,15 +3,45 @@ import RestModel from 'discourse/models/rest';
|
|||
let Query;
|
||||
Query = RestModel.extend({
|
||||
dirty: false,
|
||||
params: {},
|
||||
|
||||
_initParams: function() {
|
||||
this.resetParams();
|
||||
}.on('init').observes('param_names'),
|
||||
|
||||
markDirty: function() {
|
||||
this.set('dirty', true);
|
||||
}.observes('name', 'description', 'sql', 'defaults'),
|
||||
}.observes('name', 'description', 'sql', 'options', 'options.defaults'),
|
||||
|
||||
markNotDirty() {
|
||||
this.set('dirty', false);
|
||||
},
|
||||
|
||||
resetParams() {
|
||||
let newParams = {};
|
||||
let defaults = this.get('options.defaults');
|
||||
if (!defaults) {
|
||||
defaults = {};
|
||||
}
|
||||
(this.get('param_names') || []).forEach(function(name) {
|
||||
if (defaults[name]) {
|
||||
newParams[name] = defaults[name];
|
||||
} else {
|
||||
newParams[name] = '';
|
||||
}
|
||||
});
|
||||
this.set('params', newParams);
|
||||
},
|
||||
|
||||
saveDefaults() {
|
||||
const currentParams = this.get('params');
|
||||
let defaults = {};
|
||||
(this.get('param_names') || []).forEach(function(name) {
|
||||
defaults[name] = currentParams[name];
|
||||
});
|
||||
this.set('options.defaults', defaults);
|
||||
},
|
||||
|
||||
downloadUrl: function() {
|
||||
// TODO - can we change this to use the store/adapter?
|
||||
return Discourse.getURL("/admin/plugins/explorer/queries/" + this.get('id') + ".json?export=1");
|
||||
|
@ -50,7 +80,7 @@ Query = RestModel.extend({
|
|||
});
|
||||
|
||||
Query.reopenClass({
|
||||
updatePropertyNames: ["name", "description", "sql", "defaults"]
|
||||
updatePropertyNames: ["name", "description", "sql", "options"]
|
||||
});
|
||||
|
||||
export default Query;
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
</div>
|
||||
<div class="left-buttons">
|
||||
{{d-button action="save" label="explorer.save" disabled=saveDisabled}}
|
||||
{{d-button action="run" label="explorer.run" disabled=runDisabled}}
|
||||
{{d-button action="download" label="explorer.export" disabled=runDisabled icon="download"}}
|
||||
</div>
|
||||
<div class="right-buttons">
|
||||
|
|
|
@ -14,8 +14,20 @@
|
|||
<div class="query-edit">
|
||||
{{partial "admin/plugins-explorer-show" model=selectedItem}}
|
||||
</div>
|
||||
<div class="query-run">
|
||||
{{#if selectedItem.param_names}}
|
||||
<div class="query-params">
|
||||
{{#each selectedItem.param_names as |pname|}}
|
||||
{{param-field params=selectedItem.params pname=pname}} {{pname}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{d-button action="run" label="explorer.run" disabled=runDisabled}}
|
||||
</div>
|
||||
<hr>
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
<div class="query-results">
|
||||
{{results}}
|
||||
</div>
|
||||
{{#if results}}
|
||||
<div class="query-results">
|
||||
{{query-result query=selectedItem content=results}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<div class="result-info">
|
||||
<a class="btn" id="result-download" {{bind-attr download=downloadName}} data-auto-route="true">
|
||||
{{fa-icon "download"}}
|
||||
{{i18n "explorer.download_json"}}
|
||||
</a>
|
||||
<div class="result-about">
|
||||
{{duration}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if hasExplain}}
|
||||
<pre><code>{{content.explain}}</code></pre>
|
||||
{{/if}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="headers">
|
||||
{{#each handler in columnHandlers}}
|
||||
<th>{{handler.displayName}}</th>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each row in rows}}
|
||||
{{query-row-content row=row colRenders=columnHandlers extra=content}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
|
@ -34,3 +34,6 @@ en:
|
|||
undo: "Revert"
|
||||
delete: "Delete"
|
||||
recover: "Undelete Query"
|
||||
download_json: "Save Query Results"
|
||||
run_time: "Query completed in {{value}} ms."
|
||||
column: "Column {{number}}"
|
||||
|
|
47
plugin.rb
47
plugin.rb
|
@ -73,7 +73,7 @@ after_initialize do
|
|||
return {error: err, duration_nanos: 0}
|
||||
end
|
||||
|
||||
query_args = query.defaults.merge(params)
|
||||
query_args = (query.qopts[:defaults] || {}).with_indifferent_access.merge(params)
|
||||
|
||||
time_start, time_end, explain, err, result = nil
|
||||
begin
|
||||
|
@ -123,13 +123,14 @@ SQL
|
|||
|
||||
# Reimplement a couple ActiveRecord methods, but use PluginStore for storage instead
|
||||
class DataExplorer::Query
|
||||
attr_accessor :id, :name, :description, :sql, :defaults
|
||||
attr_accessor :id, :name, :description, :sql
|
||||
attr_reader :qopts
|
||||
|
||||
def initialize
|
||||
@name = 'Unnamed Query'
|
||||
@description = 'Enter a description here'
|
||||
@sql = 'SELECT 1'
|
||||
@defaults = {}
|
||||
@qopts = {}
|
||||
end
|
||||
|
||||
def param_names
|
||||
|
@ -154,24 +155,27 @@ SQL
|
|||
end
|
||||
end
|
||||
|
||||
def qopts=(val)
|
||||
case val
|
||||
when String
|
||||
@qopts = HashWithIndifferentAccess.new(MultiJson.load(val))
|
||||
when HashWithIndifferentAccess
|
||||
@qopts = val
|
||||
when Hash
|
||||
@qopts = val.with_indifferent_access
|
||||
else
|
||||
raise ArgumentError.new('invalid type for qopts')
|
||||
end
|
||||
end
|
||||
|
||||
def self.from_hash(h)
|
||||
query = DataExplorer::Query.new
|
||||
[:name, :description, :sql].each do |sym|
|
||||
[:name, :description, :sql, :qopts].each do |sym|
|
||||
query.send("#{sym}=", h[sym]) if h[sym]
|
||||
end
|
||||
if h[:id]
|
||||
query.id = h[:id].to_i
|
||||
end
|
||||
if h[:defaults]
|
||||
case h[:defaults]
|
||||
when String
|
||||
query.defaults = MultiJson.load(h[:defaults])
|
||||
when Hash
|
||||
query.defaults = h[:defaults]
|
||||
else
|
||||
raise ArgumentError.new('invalid type for :defaults')
|
||||
end
|
||||
end
|
||||
query
|
||||
end
|
||||
|
||||
|
@ -181,7 +185,7 @@ SQL
|
|||
name: @name,
|
||||
description: @description,
|
||||
sql: @sql,
|
||||
defaults: @defaults,
|
||||
qopts: @qopts.to_hash,
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -244,16 +248,10 @@ SQL
|
|||
render_serialized query, DataExplorer::QuerySerializer, root: 'query'
|
||||
end
|
||||
|
||||
# Helper endpoint for logic
|
||||
def parse_params
|
||||
render json: (DataExplorer.extract_params params.require(:sql))[:names]
|
||||
end
|
||||
|
||||
def create
|
||||
# guardian.ensure_can_create_explorer_query!
|
||||
|
||||
query = DataExplorer::Query.from_hash params.require(:query)
|
||||
binding.pry
|
||||
query.id = nil # json import will assign an id, which is wrong
|
||||
query.save
|
||||
|
||||
|
@ -273,7 +271,7 @@ SQL
|
|||
end
|
||||
end
|
||||
|
||||
[:name, :sql, :defaults, :description].each do |sym|
|
||||
[:name, :sql, :description, :qopts].each do |sym|
|
||||
query.send("#{sym}=", hash[sym]) if hash[sym]
|
||||
end
|
||||
query.save
|
||||
|
@ -328,8 +326,7 @@ SQL
|
|||
# json[:relations] = DataExplorer.add_extra_data(pg_result)
|
||||
# end
|
||||
|
||||
# TODO - can we tweak this to save network traffic
|
||||
json[:rows] = pg_result.to_a
|
||||
json[:rows] = pg_result.values
|
||||
|
||||
render json: json
|
||||
end
|
||||
|
@ -337,7 +334,7 @@ SQL
|
|||
end
|
||||
|
||||
class DataExplorer::QuerySerializer < ActiveModel::Serializer
|
||||
attributes :id, :sql, :name, :description, :defaults
|
||||
attributes :id, :sql, :name, :description, :qopts, :param_names
|
||||
end
|
||||
|
||||
DataExplorer::Engine.routes.draw do
|
||||
|
|
Loading…
Reference in New Issue