WIP commit
This commit is contained in:
parent
c4789df9e7
commit
e0e7022538
|
@ -0,0 +1,5 @@
|
|||
import buildPluginAdapter from 'admin/adapters/build-plugin';
|
||||
|
||||
export default buildPluginAdapter('explorer').extend({
|
||||
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
import buildPluginAdapter from 'discourse/adapters/build-plugin';
|
||||
|
||||
export default buildPluginAdapter('explorer').extend({
|
||||
|
||||
});
|
|
@ -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("<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;
|
||||
|
|
|
@ -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("<td data-column-name=" + Handlebars.Utils.escapeExpression(colRender.name) + ">");
|
||||
colRender.render(buffer, row[idx], defaultRender, response);
|
||||
buffer.push("</td>");
|
||||
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("<td>" + parts.join("</td><td>") + "</td>");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -16,14 +16,14 @@
|
|||
<table>
|
||||
<thead>
|
||||
<tr class="headers">
|
||||
{{#each handler in columnHandlers}}
|
||||
<th>{{handler.displayName}}</th>
|
||||
{{#each columnDispNames as |col|}}
|
||||
<th>{{col}}</th>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#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}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{{{value}}}
|
|
@ -0,0 +1,2 @@
|
|||
{{value}}
|
||||
{{! note - this template isn't actually used, it gets short-circuited in query-row-content.js.es6}}
|
|
@ -0,0 +1,5 @@
|
|||
{{#if user}}
|
||||
<a href="/users/{{user.username}}/activity">{{avatar user imageSize="tiny"}} {{user.username}}</a>
|
||||
{{else}}
|
||||
User #{{value}} (deleted)
|
||||
{{/if}}
|
63
plugin.rb
63
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
|
||||
|
|
Loading…
Reference in New Issue