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({
|
export default Ember.ArrayController.extend({
|
||||||
selectedQueryId: null,
|
selectedQueryId: null,
|
||||||
results: null,
|
results: null,
|
||||||
dirty: false,
|
showResults: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
explain: false,
|
explain: false,
|
||||||
|
@ -21,10 +21,16 @@ export default Ember.ArrayController.extend({
|
||||||
});
|
});
|
||||||
}.property('selectedQueryId'),
|
}.property('selectedQueryId'),
|
||||||
|
|
||||||
|
clearResults: function() {
|
||||||
|
this.set('showResults', false);
|
||||||
|
this.set('results', null);
|
||||||
|
}.observes('selectedQueryId'),
|
||||||
|
|
||||||
addCreatedRecord(record) {
|
addCreatedRecord(record) {
|
||||||
this.pushObject(record);
|
this.pushObject(record);
|
||||||
this.set('selectedQueryId', Ember.get(record, 'id'));
|
this.set('selectedQueryId', Ember.get(record, 'id'));
|
||||||
this.get('selectedItem').set('dirty', false);
|
this.get('selectedItem').set('dirty', false);
|
||||||
|
this.set('showResults', false);
|
||||||
this.set('results', null);
|
this.set('results', null);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -48,6 +54,14 @@ export default Ember.ArrayController.extend({
|
||||||
window.open(this.get('selectedItem.downloadUrl'), "_blank");
|
window.open(this.get('selectedItem.downloadUrl'), "_blank");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resetParams() {
|
||||||
|
this.get('selectedItem').resetParams();
|
||||||
|
},
|
||||||
|
|
||||||
|
saveDefaults() {
|
||||||
|
this.get('selectedItem').saveDefaults();
|
||||||
|
},
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
const self = this;
|
const self = this;
|
||||||
this.set('loading', true);
|
this.set('loading', true);
|
||||||
|
@ -109,20 +123,25 @@ export default Ember.ArrayController.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
run() {
|
run() {
|
||||||
|
const self = this;
|
||||||
if (this.get('selectedItem.dirty')) {
|
if (this.get('selectedItem.dirty')) {
|
||||||
self.set('results', {errors: [I18n.t('errors.explorer.dirty')]});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const self = this;
|
|
||||||
this.set('loading', true);
|
this.set('loading', true);
|
||||||
Discourse.ajax("/admin/plugins/explorer/queries/" + this.get('selectedItem.id') + "/run", {
|
Discourse.ajax("/admin/plugins/explorer/queries/" + this.get('selectedItem.id') + "/run", {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: {
|
data: {
|
||||||
params: JSON.stringify({foo: 34}),
|
params: JSON.stringify(this.get('selectedItem.params')),
|
||||||
explain: true
|
explain: true
|
||||||
}
|
}
|
||||||
}).then(function(result) {
|
}).then(function(result) {
|
||||||
|
if (!result.success) {
|
||||||
|
return popupAjaxError(result);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(result);
|
console.log(result);
|
||||||
|
self.set('showResults', true);
|
||||||
self.set('results', result);
|
self.set('results', result);
|
||||||
}).catch(popupAjaxError).finally(function() {
|
}).catch(popupAjaxError).finally(function() {
|
||||||
self.set('loading', false);
|
self.set('loading', false);
|
||||||
|
|
|
@ -3,15 +3,45 @@ import RestModel from 'discourse/models/rest';
|
||||||
let Query;
|
let Query;
|
||||||
Query = RestModel.extend({
|
Query = RestModel.extend({
|
||||||
dirty: false,
|
dirty: false,
|
||||||
|
params: {},
|
||||||
|
|
||||||
|
_initParams: function() {
|
||||||
|
this.resetParams();
|
||||||
|
}.on('init').observes('param_names'),
|
||||||
|
|
||||||
markDirty: function() {
|
markDirty: function() {
|
||||||
this.set('dirty', true);
|
this.set('dirty', true);
|
||||||
}.observes('name', 'description', 'sql', 'defaults'),
|
}.observes('name', 'description', 'sql', 'options', 'options.defaults'),
|
||||||
|
|
||||||
markNotDirty() {
|
markNotDirty() {
|
||||||
this.set('dirty', false);
|
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() {
|
downloadUrl: function() {
|
||||||
// TODO - can we change this to use the store/adapter?
|
// TODO - can we change this to use the store/adapter?
|
||||||
return Discourse.getURL("/admin/plugins/explorer/queries/" + this.get('id') + ".json?export=1");
|
return Discourse.getURL("/admin/plugins/explorer/queries/" + this.get('id') + ".json?export=1");
|
||||||
|
@ -50,7 +80,7 @@ Query = RestModel.extend({
|
||||||
});
|
});
|
||||||
|
|
||||||
Query.reopenClass({
|
Query.reopenClass({
|
||||||
updatePropertyNames: ["name", "description", "sql", "defaults"]
|
updatePropertyNames: ["name", "description", "sql", "options"]
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Query;
|
export default Query;
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="left-buttons">
|
<div class="left-buttons">
|
||||||
{{d-button action="save" label="explorer.save" disabled=saveDisabled}}
|
{{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"}}
|
{{d-button action="download" label="explorer.export" disabled=runDisabled icon="download"}}
|
||||||
</div>
|
</div>
|
||||||
<div class="right-buttons">
|
<div class="right-buttons">
|
||||||
|
|
|
@ -14,8 +14,20 @@
|
||||||
<div class="query-edit">
|
<div class="query-edit">
|
||||||
{{partial "admin/plugins-explorer-show" model=selectedItem}}
|
{{partial "admin/plugins-explorer-show" model=selectedItem}}
|
||||||
</div>
|
</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>
|
<hr>
|
||||||
{{conditional-loading-spinner condition=loading}}
|
{{conditional-loading-spinner condition=loading}}
|
||||||
<div class="query-results">
|
{{#if results}}
|
||||||
{{results}}
|
<div class="query-results">
|
||||||
</div>
|
{{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"
|
undo: "Revert"
|
||||||
delete: "Delete"
|
delete: "Delete"
|
||||||
recover: "Undelete Query"
|
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}
|
return {error: err, duration_nanos: 0}
|
||||||
end
|
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
|
time_start, time_end, explain, err, result = nil
|
||||||
begin
|
begin
|
||||||
|
@ -123,13 +123,14 @@ SQL
|
||||||
|
|
||||||
# Reimplement a couple ActiveRecord methods, but use PluginStore for storage instead
|
# Reimplement a couple ActiveRecord methods, but use PluginStore for storage instead
|
||||||
class DataExplorer::Query
|
class DataExplorer::Query
|
||||||
attr_accessor :id, :name, :description, :sql, :defaults
|
attr_accessor :id, :name, :description, :sql
|
||||||
|
attr_reader :qopts
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@name = 'Unnamed Query'
|
@name = 'Unnamed Query'
|
||||||
@description = 'Enter a description here'
|
@description = 'Enter a description here'
|
||||||
@sql = 'SELECT 1'
|
@sql = 'SELECT 1'
|
||||||
@defaults = {}
|
@qopts = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
def param_names
|
def param_names
|
||||||
|
@ -154,24 +155,27 @@ SQL
|
||||||
end
|
end
|
||||||
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)
|
def self.from_hash(h)
|
||||||
query = DataExplorer::Query.new
|
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]
|
query.send("#{sym}=", h[sym]) if h[sym]
|
||||||
end
|
end
|
||||||
if h[:id]
|
if h[:id]
|
||||||
query.id = h[:id].to_i
|
query.id = h[:id].to_i
|
||||||
end
|
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
|
query
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -181,7 +185,7 @@ SQL
|
||||||
name: @name,
|
name: @name,
|
||||||
description: @description,
|
description: @description,
|
||||||
sql: @sql,
|
sql: @sql,
|
||||||
defaults: @defaults,
|
qopts: @qopts.to_hash,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -244,16 +248,10 @@ SQL
|
||||||
render_serialized query, DataExplorer::QuerySerializer, root: 'query'
|
render_serialized query, DataExplorer::QuerySerializer, root: 'query'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper endpoint for logic
|
|
||||||
def parse_params
|
|
||||||
render json: (DataExplorer.extract_params params.require(:sql))[:names]
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
# guardian.ensure_can_create_explorer_query!
|
# guardian.ensure_can_create_explorer_query!
|
||||||
|
|
||||||
query = DataExplorer::Query.from_hash params.require(:query)
|
query = DataExplorer::Query.from_hash params.require(:query)
|
||||||
binding.pry
|
|
||||||
query.id = nil # json import will assign an id, which is wrong
|
query.id = nil # json import will assign an id, which is wrong
|
||||||
query.save
|
query.save
|
||||||
|
|
||||||
|
@ -273,7 +271,7 @@ SQL
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
[:name, :sql, :defaults, :description].each do |sym|
|
[:name, :sql, :description, :qopts].each do |sym|
|
||||||
query.send("#{sym}=", hash[sym]) if hash[sym]
|
query.send("#{sym}=", hash[sym]) if hash[sym]
|
||||||
end
|
end
|
||||||
query.save
|
query.save
|
||||||
|
@ -328,8 +326,7 @@ SQL
|
||||||
# json[:relations] = DataExplorer.add_extra_data(pg_result)
|
# json[:relations] = DataExplorer.add_extra_data(pg_result)
|
||||||
# end
|
# end
|
||||||
|
|
||||||
# TODO - can we tweak this to save network traffic
|
json[:rows] = pg_result.values
|
||||||
json[:rows] = pg_result.to_a
|
|
||||||
|
|
||||||
render json: json
|
render json: json
|
||||||
end
|
end
|
||||||
|
@ -337,7 +334,7 @@ SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
class DataExplorer::QuerySerializer < ActiveModel::Serializer
|
class DataExplorer::QuerySerializer < ActiveModel::Serializer
|
||||||
attributes :id, :sql, :name, :description, :defaults
|
attributes :id, :sql, :name, :description, :qopts, :param_names
|
||||||
end
|
end
|
||||||
|
|
||||||
DataExplorer::Engine.routes.draw do
|
DataExplorer::Engine.routes.draw do
|
||||||
|
|
Loading…
Reference in New Issue