diff --git a/assets/javascripts/discourse/components/param-field.js.es6 b/assets/javascripts/discourse/components/param-field.js.es6
new file mode 100644
index 0000000..89d1192
--- /dev/null
+++ b/assets/javascripts/discourse/components/param-field.js.es6
@@ -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()
+});
diff --git a/assets/javascripts/discourse/components/query-result.js.es6 b/assets/javascripts/discourse/components/query-result.js.es6
new file mode 100644
index 0000000..e795133
--- /dev/null
+++ b/assets/javascripts/discourse/components/query-result.js.es6
@@ -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("User #");
+ buffer.push(content);
+ buffer.push("");
+}});
+ColumnHandlers.push({ regex: /post_id/, render: function(buffer, content, defaultRender) {
+ if (!/^\d+$/.test(content)) {
+ return defaultRender(buffer, content);
+ }
+ buffer.push("Post #");
+ buffer.push(content);
+ buffer.push("");
+}});
+ColumnHandlers.push({ regex: /badge_id/, render: function(buffer, content, defaultRender) {
+ if (!/^\d+$/.test(content)) {
+ return defaultRender(buffer, content);
+ }
+ buffer.push("Badge #");
+ buffer.push(content);
+ buffer.push("");
+}});
+ColumnHandlers.push({ regex: /topic_id/, render: function(buffer, content, defaultRender) {
+ if (!/^\d+$/.test(content)) {
+ return defaultRender(buffer, content);
+ }
+ buffer.push("Topic #");
+ buffer.push(content);
+ buffer.push("");
+}});
+
+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("");
+ buffer.push(Discourse.Utilities.avatarImg({
+ size: "small",
+ avatarTemplate: avatarTemplate(obj.username, obj.uploaded_avatar_id)
+ }));
+ buffer.push(" ");
+ buffer.push(obj.username);
+ buffer.push("");
+});
+
+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
+
+ /*
+
+
+
+
+ Autobiographer
+
+ */
+
+ if (true) {
+ buffer.push('');
+ // icon-or-image
+ if (obj.icon.indexOf('fa-') === 0) {
+ buffer.push(" ");
+ } else {
+ buffer.push(" ");
+ }
+ buffer.push(Escape(obj.name));
+ buffer.push("");
+ }
+});
+
+registerRelationAssistedHandler('post', function(buffer, obj) {
+ /*
+
+ */
+ buffer.push("');
+});
+
+export default QueryResultComponent;
diff --git a/assets/javascripts/discourse/components/query-row-content.js.es6 b/assets/javascripts/discourse/components/query-row-content.js.es6
new file mode 100644
index 0000000..93e2d0a
--- /dev/null
+++ b/assets/javascripts/discourse/components/query-row-content.js.es6
@@ -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("