From e0e7022538e4e657494c86499c9a680f9e57a5a9 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 25 Aug 2015 20:48:19 -0700 Subject: [PATCH] WIP commit --- .../javascripts/admin/adapters/query.js.es6 | 5 + .../discourse/adapters/query.js.es6 | 5 - .../discourse/components/query-result.js.es6 | 263 +++--------------- .../components/query-row-content.js.es6 | 31 ++- .../polyfill-string-endswith.js.es6 | 16 ++ .../discourse/lib/binary-search.js.es6 | 29 ++ .../templates/explorer-query-result.hbs | 10 +- .../discourse/templates/explorer/html.raw.hbs | 1 + .../discourse/templates/explorer/text.raw.hbs | 2 + .../discourse/templates/explorer/user.raw.hbs | 5 + plugin.rb | 63 +++-- 11 files changed, 163 insertions(+), 267 deletions(-) create mode 100644 assets/javascripts/admin/adapters/query.js.es6 delete mode 100644 assets/javascripts/discourse/adapters/query.js.es6 create mode 100644 assets/javascripts/discourse/initializers/polyfill-string-endswith.js.es6 create mode 100644 assets/javascripts/discourse/lib/binary-search.js.es6 create mode 100644 assets/javascripts/discourse/templates/explorer/html.raw.hbs create mode 100644 assets/javascripts/discourse/templates/explorer/text.raw.hbs create mode 100644 assets/javascripts/discourse/templates/explorer/user.raw.hbs diff --git a/assets/javascripts/admin/adapters/query.js.es6 b/assets/javascripts/admin/adapters/query.js.es6 new file mode 100644 index 0000000..cfd6d75 --- /dev/null +++ b/assets/javascripts/admin/adapters/query.js.es6 @@ -0,0 +1,5 @@ +import buildPluginAdapter from 'admin/adapters/build-plugin'; + +export default buildPluginAdapter('explorer').extend({ + +}); diff --git a/assets/javascripts/discourse/adapters/query.js.es6 b/assets/javascripts/discourse/adapters/query.js.es6 deleted file mode 100644 index 2a09e09..0000000 --- a/assets/javascripts/discourse/adapters/query.js.es6 +++ /dev/null @@ -1,5 +0,0 @@ -import buildPluginAdapter from 'discourse/adapters/build-plugin'; - -export default buildPluginAdapter('explorer').extend({ - -}); diff --git a/assets/javascripts/discourse/components/query-result.js.es6 b/assets/javascripts/discourse/components/query-result.js.es6 index e4e02c9..537f300 100644 --- a/assets/javascripts/discourse/components/query-result.js.es6 +++ b/assets/javascripts/discourse/components/query-result.js.es6 @@ -16,6 +16,14 @@ function randomIdShort() { }); } +function transformedRelTable(table) { + const result = {}; + table.forEach(function(item) { + result[item.id] = item; + }); + return result; +} + const QueryResultComponent = Ember.Component.extend({ layoutName: 'explorer-query-result', @@ -25,7 +33,6 @@ const QueryResultComponent = Ember.Component.extend({ 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'), @@ -45,61 +52,46 @@ const QueryResultComponent = Ember.Component.extend({ return arr; }.property('params.@each'), - columnHandlers: function() { + columnDispNames: function() { + const templates = this.get('columnTemplates'); const self = this; if (!this.get('columns')) { 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 - } + if (colName.endsWith("_id")) { + return colName.slice(0, -3); } - - ColumnHandlers.forEach(function(handlerInfo) { - if (handlerInfo.regex.test(colName)) { - handler = handlerInfo.render; - } - }); - - return { - name: colName, - displayName: colName, - render: handler - }; + const dIdx = colName.indexOf('$'); + if (dIdx >= 0) { + return colName.substring(dIdx + 1); + } + return colName; }); }.property('content', 'columns.@each'), + columnTemplates: function() { + const self = this; + if (!this.get('columns')) { + return []; + } + return this.get('columns').map(function(colName, idx) { + let viewName = "text"; + if (self.get('content.colrender')[idx]) { + viewName = self.get('content.colrender')[idx]; + } + return {name: viewName, template: self.container.lookup('template:explorer/' + viewName + '.raw')}; + }); + }.property('content', 'columns.@each'), + + transformedUserTable: function() { + return transformedRelTable(this.get('content.relations.user')); + }.property('content.relations.user'), + + lookupUser: function(id) { + return this.get('transformedUserTable')[id]; + }, + downloadResult(format) { // Create a frame to submit the form in (?) // to avoid leaving an about:blank behind @@ -151,181 +143,4 @@ const QueryResultComponent = Ember.Component.extend({ }); -/** - * 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 index 93e2d0a..7a96585 100644 --- a/assets/javascripts/discourse/components/query-row-content.js.es6 +++ b/assets/javascripts/discourse/components/query-row-content.js.es6 @@ -1,19 +1,32 @@ +import binarySearch from 'discourse/plugins/discourse-data-explorer/discourse/lib/binary-search'; -const defaultRender = function(buffer, content) { - buffer.push(Handlebars.Utils.escapeExpression(content)); -}; const QueryRowContentComponent = Ember.Component.extend({ tagName: "tr", - render(buffer) { + transformedUserTable: function() { + return transformedRelTable(this.get('extra.relations.user')); + }.property('extra.relations.user'), + + render: function(buffer) { + const self = this; const row = this.get('row'); - const response = this.get('extra'); - this.get('colRenders').forEach(function(colRender, idx) { - buffer.push(""); - colRender.render(buffer, row[idx], defaultRender, response); - buffer.push(""); + const relations = this.get('extra.relations'); + + const parts = this.get('columnTemplates').map(function(t, idx) { + const params = {}; + if (t.name === "text") { + return row[idx]; + } else if (t.name === "user") { + params.user = self.get('parent').lookupUser(parseInt(row[idx])); + } else { + params.value = row[idx]; + } + + return new Handlebars.SafeString(t.template(params)); }); + + buffer.push("" + parts.join("") + ""); } }); diff --git a/assets/javascripts/discourse/initializers/polyfill-string-endswith.js.es6 b/assets/javascripts/discourse/initializers/polyfill-string-endswith.js.es6 new file mode 100644 index 0000000..3fca9cb --- /dev/null +++ b/assets/javascripts/discourse/initializers/polyfill-string-endswith.js.es6 @@ -0,0 +1,16 @@ +export default { + name: 'polyfill-string-endswith', + initialize(container) { + if (!String.prototype.endsWith) { + String.prototype.endsWith = function(searchString, position) { + var subjectString = this.toString(); + if (position === undefined || position > subjectString.length) { + position = subjectString.length; + } + position -= searchString.length; + var lastIndex = subjectString.indexOf(searchString, position); + return lastIndex !== -1 && lastIndex === position; + }; + } + } +}; diff --git a/assets/javascripts/discourse/lib/binary-search.js.es6 b/assets/javascripts/discourse/lib/binary-search.js.es6 new file mode 100644 index 0000000..01927cb --- /dev/null +++ b/assets/javascripts/discourse/lib/binary-search.js.es6 @@ -0,0 +1,29 @@ +// The binarySearch() function is licensed under the UNLICENSE +// https://github.com/Olical/binary-search + +// Modified for use in Discourse + +export default function binarySearch(list, target, keyProp) { + var min = 0; + var max = list.length - 1; + var guess; + var keyProperty = keyProp || "id"; + + while (min <= max) { + guess = Math.floor((min + max) / 2); + + if (Em.get(list[guess], keyProperty) === target) { + return guess; + } + else { + if (Em.get(list[guess], keyProperty) < target) { + min = guess + 1; + } + else { + max = guess - 1; + } + } + } + + return -1; +} diff --git a/assets/javascripts/discourse/templates/explorer-query-result.hbs b/assets/javascripts/discourse/templates/explorer-query-result.hbs index 40a40f2..b3701d4 100644 --- a/assets/javascripts/discourse/templates/explorer-query-result.hbs +++ b/assets/javascripts/discourse/templates/explorer-query-result.hbs @@ -16,14 +16,14 @@ - {{#each handler in columnHandlers}} - + {{#each columnDispNames as |col|}} + {{/each}} - {{#each row in rows}} - {{query-row-content row=row colRenders=columnHandlers extra=content}} - {{/each}} + {{~#each row in rows}} + {{~query-row-content row=row columnTemplates=columnTemplates parent=controller}} + {{~/each}}
{{handler.displayName}}{{col}}
diff --git a/assets/javascripts/discourse/templates/explorer/html.raw.hbs b/assets/javascripts/discourse/templates/explorer/html.raw.hbs new file mode 100644 index 0000000..741894a --- /dev/null +++ b/assets/javascripts/discourse/templates/explorer/html.raw.hbs @@ -0,0 +1 @@ +{{{value}}} diff --git a/assets/javascripts/discourse/templates/explorer/text.raw.hbs b/assets/javascripts/discourse/templates/explorer/text.raw.hbs new file mode 100644 index 0000000..f07ceec --- /dev/null +++ b/assets/javascripts/discourse/templates/explorer/text.raw.hbs @@ -0,0 +1,2 @@ +{{value}} +{{! note - this template isn't actually used, it gets short-circuited in query-row-content.js.es6}} diff --git a/assets/javascripts/discourse/templates/explorer/user.raw.hbs b/assets/javascripts/discourse/templates/explorer/user.raw.hbs new file mode 100644 index 0000000..f9a69bf --- /dev/null +++ b/assets/javascripts/discourse/templates/explorer/user.raw.hbs @@ -0,0 +1,5 @@ +{{#if user}} + {{avatar user imageSize="tiny"}} {{user.username}} +{{else}} + User #{{value}} (deleted) +{{/if}} diff --git a/plugin.rb b/plugin.rb index f4b7e46..934f543 100644 --- a/plugin.rb +++ b/plugin.rb @@ -141,32 +141,53 @@ SQL user: {class: User, fields: [:id, :username, :uploaded_avatar_id], serializer: BasicUserSerializer}, badge: {class: Badge, fields: [:id, :name, :badge_type_id, :description, :icon], include: [:badge_type], serializer: SmallBadgeSerializer}, post: {class: Post, fields: [:id, :topic_id, :post_number, :cooked, :user_id], include: [:user], serializer: SmallPostWithExcerptSerializer}, - topic: {class: Topic, fields: [:id, :title, :slug, :posts_count], serializer: BasicTopicSerializer} + topic: {class: Topic, fields: [:id, :title, :slug, :posts_count], serializer: BasicTopicSerializer}, + category: {class: Category, ignore: true}, + reltime: {ignore: true}, + html: {ignore: true}, } end + def self.column_regexes + @column_regexes ||= + extra_data_pluck_fields.map do |key, val| + if val[:class] + /(#{val[:class].to_s.downcase})_id$/ + end + end.compact + end + def self.add_extra_data(pg_result) needed_classes = {} pg_result.fields.each_with_index do |col, idx| - if col =~ /user_id$/ - needed_classes[:user] ||= [] - needed_classes[:user] << idx - elsif col =~ /topic_id$/ - needed_classes[:topic] ||= [] - needed_classes[:topic] << idx - elsif col =~ /badge_id/ - needed_classes[:badge] ||= [] - needed_classes[:badge] << idx - elsif col =~ /post_id/ - needed_classes[:post] ||= [] - needed_classes[:post] << idx + rgx = column_regexes.find { |rgx| rgx.match col } + if rgx + cls = (rgx.match col)[1].to_sym + needed_classes[cls] ||= [] + needed_classes[cls] << idx + elsif col =~ /^(\w+)\$/ + cls = $1.to_sym + needed_classes[cls] ||= [] + needed_classes[cls] << idx end end ret = {} + col_map = {} needed_classes.each do |cls, column_nums| next unless column_nums.present? + support_info = extra_data_pluck_fields[cls] + next unless support_info + + column_nums.each do |col_n| + col_map[col_n] = cls + end + + if support_info[:ignore] + ret[cls] = [] + next + end ids = Set.new column_nums.each do |col_n| @@ -175,14 +196,13 @@ SQL ids.delete nil ids.map! &:to_i - support_info = extra_data_pluck_fields[cls] object_class = support_info[:class] all_objs = object_class.select(support_info[:fields]). - where(id: ids.to_a.sort).includes(support_info[:include]) + where(id: ids.to_a.sort).includes(support_info[:include]).order(:id) ret[cls] = ActiveModel::ArraySerializer.new(all_objs, each_serializer: support_info[:serializer]) end - ret + [ret, col_map] end def self.sensitive_column_names @@ -1029,9 +1049,9 @@ SQL columns: cols, } json[:explain] = result[:explain] if opts[:explain] - if cols.any? { |col_name| special_serialization? col_name } - json[:relations] = DataExplorer.add_extra_data(pg_result) - end + ext = DataExplorer.add_extra_data(pg_result) + json[:colrender] = ext[1] + json[:relations] = ext[0] json[:rows] = pg_result.values @@ -1054,11 +1074,6 @@ SQL end end end - - private - def special_serialization?(col_name) - col_name =~ /(user|topic|post|badge)(_id)?$/ - end end class DataExplorer::QuerySerializer < ActiveModel::Serializer