mirror of
https://github.com/discourse/discourse-data-explorer.git
synced 2025-03-06 01:19:32 +00:00
DEV: Split the Query Listing and Query Editing code (#356)
The code for listing all of the defined queries is mixed together with the code for editing a single query. Notably, this results in large amounts of unnecessary data being loaded for the list view, which causes substantial rendering slowdowns. To address this issue, we now only load the necessary data for the list view, and load the full data when it's actually needed (any endpoint that returns a single query). The primary changes that achieve this are: - Create a new `QueryDetailsSerializer` serialiser, which includes all of the query info, and change the existing `QuerySerializer` serialiser to only include the necessary attributes of each query for generating a list of them all. - Split the monolith `/plugins/explorer` route into `/plugins/explorer` for showing just the list of queries, and `/plugins/explorer/queries/:query_id`, for showing/editing/running a specific query.
This commit is contained in:
parent
bd6263e9b8
commit
d726c4889e
@ -40,7 +40,7 @@ module ::DiscourseDataExplorer
|
||||
end
|
||||
|
||||
return raise Discourse::NotFound if !guardian.user_can_access_query?(@query) || @query.hidden
|
||||
render_serialized @query, QuerySerializer, root: "query"
|
||||
render_serialized @query, QueryDetailsSerializer, root: "query"
|
||||
end
|
||||
|
||||
def groups
|
||||
@ -68,7 +68,7 @@ module ::DiscourseDataExplorer
|
||||
query_group = QueryGroup.find_by(query_id: @query.id, group_id: @group.id)
|
||||
|
||||
render json: {
|
||||
query: serialize_data(@query, QuerySerializer, root: nil),
|
||||
query: serialize_data(@query, QueryDetailsSerializer, root: nil),
|
||||
query_group: serialize_data(query_group, QueryGroupSerializer, root: nil),
|
||||
}
|
||||
end
|
||||
@ -93,7 +93,7 @@ module ::DiscourseDataExplorer
|
||||
)
|
||||
group_ids = params.require(:query)[:group_ids]
|
||||
group_ids&.each { |group_id| query.query_groups.find_or_create_by!(group_id: group_id) }
|
||||
render_serialized query, QuerySerializer, root: "query"
|
||||
render_serialized query, QueryDetailsSerializer, root: "query"
|
||||
end
|
||||
|
||||
def update
|
||||
@ -107,7 +107,7 @@ module ::DiscourseDataExplorer
|
||||
group_ids&.each { |group_id| @query.query_groups.find_or_create_by!(group_id: group_id) }
|
||||
end
|
||||
|
||||
render_serialized @query, QuerySerializer, root: "query"
|
||||
render_serialized @query, QueryDetailsSerializer, root: "query"
|
||||
rescue ValidationError => e
|
||||
render_json_error e.message
|
||||
end
|
||||
|
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ::DiscourseDataExplorer
|
||||
class QueryDetailsSerializer < QuerySerializer
|
||||
attributes :sql, :param_info, :created_at, :hidden
|
||||
|
||||
def param_info
|
||||
object&.params&.uniq { |p| p.identifier }&.map(&:to_hash)
|
||||
end
|
||||
end
|
||||
end
|
@ -2,21 +2,7 @@
|
||||
|
||||
module ::DiscourseDataExplorer
|
||||
class QuerySerializer < ActiveModel::Serializer
|
||||
attributes :id,
|
||||
:sql,
|
||||
:name,
|
||||
:description,
|
||||
:param_info,
|
||||
:created_at,
|
||||
:username,
|
||||
:group_ids,
|
||||
:last_run_at,
|
||||
:hidden,
|
||||
:user_id
|
||||
|
||||
def param_info
|
||||
object&.params&.uniq { |p| p.identifier }&.map(&:to_hash)
|
||||
end
|
||||
attributes :id, :name, :description, :username, :group_ids, :last_run_at, :user_id
|
||||
|
||||
def username
|
||||
object&.user&.username
|
||||
|
@ -0,0 +1,192 @@
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { Promise } from "rsvp";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class PluginsExplorerController extends Controller {
|
||||
@service dialog;
|
||||
@service appEvents;
|
||||
@service router;
|
||||
|
||||
@tracked sortByProperty = "last_run_at";
|
||||
@tracked sortDescending = true;
|
||||
@tracked params;
|
||||
@tracked search;
|
||||
@tracked newQueryName;
|
||||
@tracked showCreate;
|
||||
@tracked loading = false;
|
||||
|
||||
queryParams = ["id"];
|
||||
explain = false;
|
||||
acceptedImportFileTypes = ["application/json"];
|
||||
order = null;
|
||||
form = null;
|
||||
|
||||
get sortedQueries() {
|
||||
const sortedQueries = this.model.sortBy(this.sortByProperty);
|
||||
return this.sortDescending ? sortedQueries.reverse() : sortedQueries;
|
||||
}
|
||||
|
||||
get parsedParams() {
|
||||
return this.params ? JSON.parse(this.params) : null;
|
||||
}
|
||||
|
||||
get filteredContent() {
|
||||
const regexp = new RegExp(this.search, "i");
|
||||
return this.sortedQueries.filter(
|
||||
(result) => regexp.test(result.name) || regexp.test(result.description)
|
||||
);
|
||||
}
|
||||
|
||||
get createDisabled() {
|
||||
return (this.newQueryName || "").trim().length === 0;
|
||||
}
|
||||
|
||||
addCreatedRecord(record) {
|
||||
this.model.pushObject(record);
|
||||
this.router.transitionTo(
|
||||
"adminPlugins.explorer.queries.details",
|
||||
record.id
|
||||
);
|
||||
}
|
||||
|
||||
async _importQuery(file) {
|
||||
const json = await this._readFileAsTextAsync(file);
|
||||
const query = this._parseQuery(json);
|
||||
const record = this.store.createRecord("query", query);
|
||||
const response = await record.save();
|
||||
return response.target;
|
||||
}
|
||||
|
||||
_parseQuery(json) {
|
||||
const parsed = JSON.parse(json);
|
||||
const query = parsed.query;
|
||||
if (!query || !query.sql) {
|
||||
throw new TypeError();
|
||||
}
|
||||
query.id = 0; // 0 means no Id yet
|
||||
return query;
|
||||
}
|
||||
|
||||
_readFileAsTextAsync(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
dragMove(e) {
|
||||
if (!e.movementY && !e.movementX) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editPane = document.querySelector(".query-editor");
|
||||
const target = editPane.querySelector(".panels-flex");
|
||||
const grippie = editPane.querySelector(".grippie");
|
||||
|
||||
// we need to get the initial height / width of edit pane
|
||||
// before we manipulate the size
|
||||
if (!this.initialPaneWidth && !this.originalPaneHeight) {
|
||||
this.originalPaneWidth = target.clientWidth;
|
||||
this.originalPaneHeight = target.clientHeight;
|
||||
}
|
||||
|
||||
const newHeight = Math.max(
|
||||
this.originalPaneHeight,
|
||||
target.clientHeight + e.movementY
|
||||
);
|
||||
const newWidth = Math.max(
|
||||
this.originalPaneWidth,
|
||||
target.clientWidth + e.movementX
|
||||
);
|
||||
|
||||
target.style.height = newHeight + "px";
|
||||
target.style.width = newWidth + "px";
|
||||
grippie.style.width = newWidth + "px";
|
||||
this.appEvents.trigger("ace:resize");
|
||||
}
|
||||
|
||||
@bind
|
||||
scrollTop() {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
@action
|
||||
async import(files) {
|
||||
try {
|
||||
this.loading = true;
|
||||
const file = files[0];
|
||||
const record = await this._importQuery(file);
|
||||
this.addCreatedRecord(record);
|
||||
} catch (e) {
|
||||
if (e.jqXHR) {
|
||||
popupAjaxError(e);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
this.dialog.alert(i18n("explorer.import.unparseable_json"));
|
||||
} else if (e instanceof TypeError) {
|
||||
this.dialog.alert(i18n("explorer.import.wrong_json"));
|
||||
} else {
|
||||
this.dialog.alert(i18n("errors.desc.unknown"));
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
displayCreate() {
|
||||
this.showCreate = true;
|
||||
}
|
||||
|
||||
@action
|
||||
resetParams() {
|
||||
this.selectedItem.resetParams();
|
||||
}
|
||||
|
||||
@action
|
||||
updateSortProperty(property) {
|
||||
if (this.sortByProperty === property) {
|
||||
this.sortDescending = !this.sortDescending;
|
||||
} else {
|
||||
this.sortByProperty = property;
|
||||
this.sortDescending = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async create() {
|
||||
try {
|
||||
const name = this.newQueryName.trim();
|
||||
this.loading = true;
|
||||
this.showCreate = false;
|
||||
const result = await this.store.createRecord("query", { name }).save();
|
||||
this.addCreatedRecord(result.target);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
updateSearch(value) {
|
||||
this.search = value;
|
||||
}
|
||||
|
||||
@action
|
||||
updateNewQueryName(value) {
|
||||
this.newQueryName = value;
|
||||
}
|
||||
}
|
@ -6,44 +6,29 @@ import { Promise } from "rsvp";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import QueryHelp from "discourse/plugins/discourse-data-explorer/discourse/components/modal/query-help";
|
||||
import { ParamValidationError } from "discourse/plugins/discourse-data-explorer/discourse/components/param-input-form";
|
||||
import Query from "discourse/plugins/discourse-data-explorer/discourse/models/query";
|
||||
|
||||
const NoQuery = Query.create({ name: "No queries", fake: true, group_ids: [] });
|
||||
|
||||
export default class PluginsExplorerController extends Controller {
|
||||
@service modal;
|
||||
@service dialog;
|
||||
@service appEvents;
|
||||
@service router;
|
||||
|
||||
@tracked sortByProperty = "last_run_at";
|
||||
@tracked sortDescending = true;
|
||||
@tracked params;
|
||||
@tracked search;
|
||||
@tracked newQueryName;
|
||||
@tracked showCreate;
|
||||
@tracked editingName = false;
|
||||
@tracked editingQuery = false;
|
||||
@tracked selectedQueryId;
|
||||
@tracked loading = false;
|
||||
@tracked showResults = false;
|
||||
@tracked hideSchema = false;
|
||||
@tracked results = this.selectedItem.results;
|
||||
@tracked results = this.model.results;
|
||||
@tracked dirty = false;
|
||||
|
||||
queryParams = ["params", { selectedQueryId: "id" }];
|
||||
queryParams = ["params"];
|
||||
explain = false;
|
||||
acceptedImportFileTypes = ["application/json"];
|
||||
order = null;
|
||||
form = null;
|
||||
|
||||
get validQueryPresent() {
|
||||
return !!this.selectedItem.id;
|
||||
}
|
||||
|
||||
get saveDisabled() {
|
||||
return !this.dirty;
|
||||
}
|
||||
@ -52,33 +37,12 @@ export default class PluginsExplorerController extends Controller {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
get sortedQueries() {
|
||||
const sortedQueries = this.model.sortBy(this.sortByProperty);
|
||||
return this.sortDescending ? sortedQueries.reverse() : sortedQueries;
|
||||
}
|
||||
|
||||
get parsedParams() {
|
||||
return this.params ? JSON.parse(this.params) : null;
|
||||
}
|
||||
|
||||
get filteredContent() {
|
||||
const regexp = new RegExp(this.search, "i");
|
||||
return this.sortedQueries.filter(
|
||||
(result) => regexp.test(result.name) || regexp.test(result.description)
|
||||
);
|
||||
}
|
||||
|
||||
get createDisabled() {
|
||||
return (this.newQueryName || "").trim().length === 0;
|
||||
}
|
||||
|
||||
get selectedItem() {
|
||||
const query = this.model.findBy("id", parseInt(this.selectedQueryId, 10));
|
||||
return query || NoQuery;
|
||||
}
|
||||
|
||||
get editDisabled() {
|
||||
return parseInt(this.selectedQueryId, 10) < 0 ? true : false;
|
||||
return parseInt(this.model.id, 10) < 0 ? true : false;
|
||||
}
|
||||
|
||||
get groupOptions() {
|
||||
@ -89,27 +53,11 @@ export default class PluginsExplorerController extends Controller {
|
||||
});
|
||||
}
|
||||
|
||||
get othersDirty() {
|
||||
return !!this.model.find((q) => q !== this.selectedItem && this.dirty);
|
||||
}
|
||||
|
||||
addCreatedRecord(record) {
|
||||
this.model.pushObject(record);
|
||||
this.selectedQueryId = record.id;
|
||||
this.dirty = false;
|
||||
this.setProperties({
|
||||
showResults: false,
|
||||
results: null,
|
||||
editingName: true,
|
||||
editingQuery: true,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async save() {
|
||||
try {
|
||||
this.loading = true;
|
||||
await this.selectedItem.save();
|
||||
await this.model.save();
|
||||
|
||||
this.dirty = false;
|
||||
this.editingName = false;
|
||||
@ -194,17 +142,10 @@ export default class PluginsExplorerController extends Controller {
|
||||
@bind
|
||||
didEndDrag() {}
|
||||
|
||||
@bind
|
||||
scrollTop() {
|
||||
window.scrollTo(0, 0);
|
||||
this.editingName = false;
|
||||
this.editingQuery = false;
|
||||
}
|
||||
|
||||
@action
|
||||
updateGroupIds(value) {
|
||||
this.dirty = true;
|
||||
this.selectedItem.set("group_ids", value);
|
||||
this.model.set("group_ids", value);
|
||||
}
|
||||
|
||||
@action
|
||||
@ -212,36 +153,6 @@ export default class PluginsExplorerController extends Controller {
|
||||
this.hideSchema = value;
|
||||
}
|
||||
|
||||
@action
|
||||
async import(files) {
|
||||
try {
|
||||
this.loading = true;
|
||||
const file = files[0];
|
||||
const record = await this._importQuery(file);
|
||||
this.addCreatedRecord(record);
|
||||
} catch (e) {
|
||||
if (e.jqXHR) {
|
||||
popupAjaxError(e);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
this.dialog.alert(i18n("explorer.import.unparseable_json"));
|
||||
} else if (e instanceof TypeError) {
|
||||
this.dialog.alert(i18n("explorer.import.wrong_json"));
|
||||
} else {
|
||||
this.dialog.alert(i18n("errors.desc.unknown"));
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
displayCreate() {
|
||||
this.showCreate = true;
|
||||
}
|
||||
|
||||
@action
|
||||
editName() {
|
||||
this.editingName = true;
|
||||
@ -254,18 +165,12 @@ export default class PluginsExplorerController extends Controller {
|
||||
|
||||
@action
|
||||
download() {
|
||||
window.open(this.selectedItem.downloadUrl, "_blank");
|
||||
window.open(this.model.downloadUrl, "_blank");
|
||||
}
|
||||
|
||||
@action
|
||||
goHome() {
|
||||
this.order = null;
|
||||
this.showResults = false;
|
||||
this.selectedQueryId = null;
|
||||
this.params = null;
|
||||
this.sortByProperty = "last_run_at";
|
||||
this.sortDescending = true;
|
||||
this.router.transitionTo({ queryParams: { id: null, params: null } });
|
||||
this.router.transitionTo("adminPlugins.explorer");
|
||||
}
|
||||
|
||||
@action
|
||||
@ -275,48 +180,17 @@ export default class PluginsExplorerController extends Controller {
|
||||
|
||||
@action
|
||||
resetParams() {
|
||||
this.selectedItem.resetParams();
|
||||
}
|
||||
|
||||
@action
|
||||
updateSortProperty(property) {
|
||||
if (this.sortByProperty === property) {
|
||||
this.sortDescending = !this.sortDescending;
|
||||
} else {
|
||||
this.sortByProperty = property;
|
||||
this.sortDescending = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async create() {
|
||||
try {
|
||||
const name = this.newQueryName.trim();
|
||||
this.loading = true;
|
||||
this.showCreate = false;
|
||||
const result = await this.store.createRecord("query", { name }).save();
|
||||
this.addCreatedRecord(result.target);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.dirty = true;
|
||||
}
|
||||
this.model.resetParams();
|
||||
}
|
||||
|
||||
@action
|
||||
async discard() {
|
||||
try {
|
||||
this.loading = true;
|
||||
const result = await this.store.find("query", this.selectedItem.id);
|
||||
this.selectedItem.setProperties(
|
||||
result.getProperties(Query.updatePropertyNames)
|
||||
);
|
||||
if (
|
||||
!this.selectedItem.group_ids ||
|
||||
!Array.isArray(this.selectedItem.group_ids)
|
||||
) {
|
||||
this.selectedItem.set("group_ids", []);
|
||||
const result = await this.store.find("query", this.model.id);
|
||||
this.model.setProperties(result.getProperties(Query.updatePropertyNames));
|
||||
if (!this.model.group_ids || !Array.isArray(this.model.group_ids)) {
|
||||
this.model.set("group_ids", []);
|
||||
}
|
||||
this.dirty = false;
|
||||
} catch (error) {
|
||||
@ -331,8 +205,8 @@ export default class PluginsExplorerController extends Controller {
|
||||
try {
|
||||
this.loading = true;
|
||||
this.showResults = false;
|
||||
await this.store.destroyRecord("query", this.selectedItem);
|
||||
this.selectedItem.set("destroyed", true);
|
||||
await this.store.destroyRecord("query", this.model);
|
||||
this.model.set("destroyed", true);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
@ -345,8 +219,8 @@ export default class PluginsExplorerController extends Controller {
|
||||
try {
|
||||
this.loading = true;
|
||||
this.showResults = true;
|
||||
await this.selectedItem.save();
|
||||
this.selectedItem.set("destroyed", false);
|
||||
await this.model.save();
|
||||
this.model.set("destroyed", false);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
@ -359,16 +233,6 @@ export default class PluginsExplorerController extends Controller {
|
||||
this.form = form;
|
||||
}
|
||||
|
||||
@action
|
||||
updateSearch(value) {
|
||||
this.search = value;
|
||||
}
|
||||
|
||||
@action
|
||||
updateNewQueryName(value) {
|
||||
this.newQueryName = value;
|
||||
}
|
||||
|
||||
@action
|
||||
setDirty() {
|
||||
this.dirty = true;
|
||||
@ -382,7 +246,7 @@ export default class PluginsExplorerController extends Controller {
|
||||
@action
|
||||
async run() {
|
||||
let params = null;
|
||||
if (this.selectedItem.hasParams) {
|
||||
if (this.model.hasParams) {
|
||||
try {
|
||||
params = await this.form?.submit();
|
||||
} catch (err) {
|
||||
@ -400,7 +264,7 @@ export default class PluginsExplorerController extends Controller {
|
||||
params: JSON.stringify(params),
|
||||
});
|
||||
|
||||
ajax("/admin/plugins/explorer/queries/" + this.selectedItem.id + "/run", {
|
||||
ajax("/admin/plugins/explorer/queries/" + this.model.id + "/run", {
|
||||
type: "POST",
|
||||
data: {
|
||||
params: JSON.stringify(params),
|
@ -2,6 +2,10 @@ export default {
|
||||
resource: "admin.adminPlugins",
|
||||
path: "/plugins",
|
||||
map() {
|
||||
this.route("explorer");
|
||||
this.route("explorer", function () {
|
||||
this.route("queries", function () {
|
||||
this.route("details", { path: "/:query_id" });
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -24,18 +24,18 @@ export default class Query extends RestModel {
|
||||
return getURL(`/admin/plugins/explorer/queries/${this.id}.json?export=1`);
|
||||
}
|
||||
|
||||
@computed("param_info", "updateing")
|
||||
@computed("param_info", "updating")
|
||||
get hasParams() {
|
||||
// When saving, we need to refresh the param-input component to clean up the old key
|
||||
return this.param_info.length && !this.updateing;
|
||||
return this.param_info.length && !this.updating;
|
||||
}
|
||||
|
||||
beforeUpdate() {
|
||||
this.set("updateing", true);
|
||||
this.set("updating", true);
|
||||
}
|
||||
|
||||
afterUpdate() {
|
||||
this.set("updateing", false);
|
||||
this.set("updating", false);
|
||||
}
|
||||
|
||||
resetParams() {
|
||||
|
@ -0,0 +1,47 @@
|
||||
import { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminPluginsExplorerIndex extends DiscourseRoute {
|
||||
@service router;
|
||||
|
||||
beforeModel(transition) {
|
||||
// Redirect old /explorer?id=123 route to /explorer/queries/123
|
||||
if (transition.to.queryParams.id) {
|
||||
this.router.transitionTo(
|
||||
"adminPlugins.explorer.queries.details",
|
||||
transition.to.queryParams.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
model() {
|
||||
if (!this.currentUser.admin) {
|
||||
// display "Only available to admins" message
|
||||
return { model: null, schema: null, disallow: true, groups: null };
|
||||
}
|
||||
|
||||
const groupPromise = ajax("/admin/plugins/explorer/groups.json");
|
||||
const queryPromise = this.store.findAll("query");
|
||||
|
||||
return groupPromise.then((groups) => {
|
||||
let groupNames = {};
|
||||
groups.forEach((g) => {
|
||||
groupNames[g.id] = g.name;
|
||||
});
|
||||
return queryPromise.then((model) => {
|
||||
model.forEach((query) => {
|
||||
query.set(
|
||||
"group_names",
|
||||
(query.group_ids || []).map((id) => groupNames[id])
|
||||
);
|
||||
});
|
||||
return { model, groups };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.setProperties(model);
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminPluginsExplorer extends DiscourseRoute {
|
||||
model() {
|
||||
export default class AdminPluginsExplorerQueriesDetails extends DiscourseRoute {
|
||||
model(params) {
|
||||
if (!this.currentUser.admin) {
|
||||
// display "Only available to admins" message
|
||||
return { model: null, schema: null, disallow: true, groups: null };
|
||||
@ -12,7 +12,7 @@ export default class AdminPluginsExplorer extends DiscourseRoute {
|
||||
const schemaPromise = ajax("/admin/plugins/explorer/schema.json", {
|
||||
cache: true,
|
||||
});
|
||||
const queryPromise = this.store.findAll("query");
|
||||
const queryPromise = this.store.find("query", params.query_id);
|
||||
|
||||
return groupPromise.then((groups) => {
|
||||
let groupNames = {};
|
||||
@ -21,12 +21,10 @@ export default class AdminPluginsExplorer extends DiscourseRoute {
|
||||
});
|
||||
return schemaPromise.then((schema) => {
|
||||
return queryPromise.then((model) => {
|
||||
model.forEach((query) => {
|
||||
query.set(
|
||||
"group_names",
|
||||
(query.group_ids || []).map((id) => groupNames[id])
|
||||
);
|
||||
});
|
||||
model.set(
|
||||
"group_names",
|
||||
(model.group_ids || []).map((id) => groupNames[id])
|
||||
);
|
||||
return { model, schema, groups };
|
||||
});
|
||||
});
|
@ -0,0 +1,166 @@
|
||||
{{#if this.disallow}}
|
||||
<h1>{{i18n "explorer.admins_only"}}</h1>
|
||||
{{else}}
|
||||
<div class="query-list">
|
||||
<TextField
|
||||
@value={{this.search}}
|
||||
@placeholderKey="explorer.search_placeholder"
|
||||
@onChange={{this.updateSearch}}
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.displayCreate}}
|
||||
@icon="plus"
|
||||
class="no-text btn-right"
|
||||
/>
|
||||
<PickFilesButton
|
||||
@label="explorer.import.label"
|
||||
@icon="upload"
|
||||
@acceptedFormatsOverride={{this.acceptedImportFileTypes}}
|
||||
@showButton="true"
|
||||
@onFilesPicked={{this.import}}
|
||||
class="import-btn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.showCreate}}
|
||||
<div class="query-create">
|
||||
<TextField
|
||||
@value={{this.newQueryName}}
|
||||
@placeholderKey="explorer.create_placeholder"
|
||||
@onChange={{this.updateNewQueryName}}
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.create}}
|
||||
@disabled={{this.createDisabled}}
|
||||
@label="explorer.create"
|
||||
@icon="plus"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.othersDirty}}
|
||||
<div class="warning">
|
||||
{{d-icon "triangle-exclamation"}}
|
||||
{{i18n "explorer.others_dirty"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.model.length}}
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||
|
||||
<div class="container">
|
||||
<table class="d-admin-table recent-queries">
|
||||
<thead class="heading-container">
|
||||
<th class="col heading name">
|
||||
<div
|
||||
role="button"
|
||||
class="heading-toggle"
|
||||
{{on "click" (fn this.updateSortProperty "name")}}
|
||||
>
|
||||
<TableHeaderToggle
|
||||
@field="name"
|
||||
@labelKey="explorer.query_name"
|
||||
@order={{this.order}}
|
||||
@asc={{not this.sortDescending}}
|
||||
@automatic="true"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th class="col heading created-by">
|
||||
<div
|
||||
role="button"
|
||||
class="heading-toggle"
|
||||
{{on "click" (fn this.updateSortProperty "username")}}
|
||||
>
|
||||
<TableHeaderToggle
|
||||
@field="username"
|
||||
@labelKey="explorer.query_user"
|
||||
@order={{this.order}}
|
||||
@asc={{not this.sortDescending}}
|
||||
@automatic="true"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th class="col heading group-names">
|
||||
<div class="group-names-header">
|
||||
{{i18n "explorer.query_groups"}}
|
||||
</div>
|
||||
</th>
|
||||
<th class="col heading created-at">
|
||||
<div
|
||||
role="button"
|
||||
class="heading-toggle"
|
||||
{{on "click" (fn this.updateSortProperty "last_run_at")}}
|
||||
>
|
||||
<TableHeaderToggle
|
||||
@field="last_run_at"
|
||||
@labelKey="explorer.query_time"
|
||||
@order={{this.order}}
|
||||
@asc={{not this.sortDescending}}
|
||||
@automatic="true"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.filteredContent as |query|}}
|
||||
<tr class="d-admin-row__content query-row">
|
||||
<td class="d-admin-row__overview">
|
||||
<a
|
||||
{{on "click" this.scrollTop}}
|
||||
href="/admin/plugins/explorer/queries/{{query.id}}"
|
||||
>
|
||||
<b class="query-name">{{query.name}}</b>
|
||||
<medium class="query-desc">{{query.description}}</medium>
|
||||
</a>
|
||||
</td>
|
||||
<td class="d-admin-row__detail query-created-by">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "explorer.query_user"}}
|
||||
</div>
|
||||
{{#if query.username}}
|
||||
<div>
|
||||
<a href="/u/{{query.username}}/activity">
|
||||
<medium>{{query.username}}</medium>
|
||||
</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail query-group-names">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "explorer.query_groups"}}
|
||||
</div>
|
||||
<div class="group-names">
|
||||
{{#each query.group_names as |group|}}
|
||||
<ShareReport @group={{group}} @query={{query}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-admin-row__detail query-created-at">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "explorer.query_time"}}
|
||||
</div>
|
||||
{{#if query.last_run_at}}
|
||||
<medium>
|
||||
{{bound-date query.last_run_at}}
|
||||
</medium>
|
||||
{{else if query.created_at}}
|
||||
<medium>
|
||||
{{bound-date query.created_at}}
|
||||
</medium>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<br />
|
||||
<em class="no-search-results">
|
||||
{{i18n "explorer.no_search_results"}}
|
||||
</em>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="explorer-pad-bottom"></div>
|
||||
{{/if}}
|
||||
{{/if}}
|
@ -0,0 +1,223 @@
|
||||
{{#if this.disallow}}
|
||||
<h1>{{i18n "explorer.admins_only"}}</h1>
|
||||
{{else}}
|
||||
|
||||
<div class="query-edit {{if this.editName 'editing'}}">
|
||||
{{#if this.editingName}}
|
||||
<div class="name">
|
||||
<DButton
|
||||
@action={{this.goHome}}
|
||||
@icon="chevron-left"
|
||||
class="previous"
|
||||
/>
|
||||
<DButton @action={{this.exitEdit}} @icon="xmark" class="previous" />
|
||||
<div class="name-text-field">
|
||||
<TextField @value={{this.model.name}} @onChange={{this.setDirty}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="desc">
|
||||
<DTextarea
|
||||
@value={{this.model.description}}
|
||||
@placeholder={{i18n "explorer.description_placeholder"}}
|
||||
@input={{this.setDirty}}
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="name">
|
||||
<DButton
|
||||
@action={{this.goHome}}
|
||||
@icon="chevron-left"
|
||||
class="previous"
|
||||
/>
|
||||
|
||||
<h1>
|
||||
{{this.model.name}}
|
||||
{{#unless this.editDisabled}}
|
||||
<a href {{action "editName"}} class="edit-query-name">
|
||||
{{d-icon "pencil"}}
|
||||
</a>
|
||||
{{/unless}}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="desc">
|
||||
{{this.model.description}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#unless this.model.destroyed}}
|
||||
<div class="groups">
|
||||
<span class="label">{{i18n "explorer.allow_groups"}}</span>
|
||||
<span>
|
||||
<MultiSelect
|
||||
@value={{this.model.group_ids}}
|
||||
@content={{this.groupOptions}}
|
||||
@options={{hash allowAny=false}}
|
||||
@onChange={{this.updateGroupIds}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<div class="clear"></div>
|
||||
|
||||
{{#if this.editingQuery}}
|
||||
<div class="query-editor {{if this.hideSchema 'no-schema'}}">
|
||||
<div class="panels-flex">
|
||||
<div class="editor-panel">
|
||||
<AceEditor
|
||||
{{on "click" this.setDirty}}
|
||||
@content={{this.model.sql}}
|
||||
@onChange={{fn (mut this.model.sql)}}
|
||||
@mode="sql"
|
||||
@disabled={{this.model.destroyed}}
|
||||
@save={{this.save}}
|
||||
@submit={{this.saveAndRun}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<ExplorerSchema
|
||||
@schema={{this.schema}}
|
||||
@hideSchema={{this.hideSchema}}
|
||||
@updateHideSchema={{this.updateHideSchema}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grippie"
|
||||
{{draggable
|
||||
didStartDrag=this.didStartDrag
|
||||
didEndDrag=this.didEndDrag
|
||||
dragMove=this.dragMove
|
||||
}}
|
||||
>
|
||||
{{d-icon "discourse-expand"}}
|
||||
</div>
|
||||
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="sql">
|
||||
<CodeView
|
||||
@value={{this.model.sql}}
|
||||
@codeClass="sql"
|
||||
@setDirty={{this.setDirty}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="clear"></div>
|
||||
|
||||
<div class="pull-left left-buttons">
|
||||
{{#if this.editingQuery}}
|
||||
<DButton
|
||||
class="btn-save-query"
|
||||
@action={{this.save}}
|
||||
@label="explorer.save"
|
||||
@disabled={{this.saveDisabled}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#unless this.editDisabled}}
|
||||
<DButton
|
||||
class="btn-edit-query"
|
||||
@action={{this.editQuery}}
|
||||
@label="explorer.edit"
|
||||
@icon="pencil"
|
||||
/>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
<DButton
|
||||
@action={{this.download}}
|
||||
@label="explorer.export"
|
||||
@disabled={{this.runDisabled}}
|
||||
@icon="download"
|
||||
/>
|
||||
|
||||
{{#if this.editingQuery}}
|
||||
<DButton
|
||||
@action={{this.showHelpModal}}
|
||||
@label="explorer.help.label"
|
||||
@icon="circle-question"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="pull-right right-buttons">
|
||||
{{#if this.model.destroyed}}
|
||||
<DButton
|
||||
@action={{this.recover}}
|
||||
@icon="arrow-rotate-left"
|
||||
@label="explorer.recover"
|
||||
/>
|
||||
{{else}}
|
||||
{{#if this.editingQuery}}
|
||||
<DButton
|
||||
@action={{this.discard}}
|
||||
@icon="arrow-rotate-left"
|
||||
@label="explorer.undo"
|
||||
@disabled={{this.saveDisabled}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<DButton
|
||||
@action={{this.destroyQuery}}
|
||||
@icon="trash-can"
|
||||
@label="explorer.delete"
|
||||
class="btn-danger"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
|
||||
<form class="query-run" {{on "submit" this.run}}>
|
||||
{{#if this.model.hasParams}}
|
||||
<ParamInputForm
|
||||
@initialValues={{this.parsedParams}}
|
||||
@paramInfo={{this.model.param_info}}
|
||||
@onRegisterApi={{this.onRegisterApi}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.runDisabled}}
|
||||
{{#if this.saveDisabled}}
|
||||
<DButton @label="explorer.run" @disabled="true" class="btn-primary" />
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{this.saveAndRun}}
|
||||
@icon="play"
|
||||
@label="explorer.saverun"
|
||||
class="btn-primary"
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{this.run}}
|
||||
@icon="play"
|
||||
@label="explorer.run"
|
||||
@disabled={{this.runDisabled}}
|
||||
@type="submit"
|
||||
class="btn-primary"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<label class="query-plan">
|
||||
<Input @type="checkbox" @checked={{this.explain}} name="explain" />
|
||||
{{i18n "explorer.explain_label"}}
|
||||
</label>
|
||||
</form>
|
||||
<hr />
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||
|
||||
<QueryResultsWrapper
|
||||
@results={{this.results}}
|
||||
@showResults={{this.showResults}}
|
||||
@query={{this.selectedItem}}
|
||||
@content={{this.results}}
|
||||
/>
|
||||
{{/if}}
|
@ -1,405 +0,0 @@
|
||||
{{#if this.disallow}}
|
||||
<h1>{{i18n "explorer.admins_only"}}</h1>
|
||||
{{else}}
|
||||
{{#unless this.validQueryPresent}}
|
||||
<div class="query-list">
|
||||
<TextField
|
||||
@value={{this.search}}
|
||||
@placeholderKey="explorer.search_placeholder"
|
||||
@onChange={{this.updateSearch}}
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.displayCreate}}
|
||||
@icon="plus"
|
||||
class="no-text btn-right"
|
||||
/>
|
||||
<PickFilesButton
|
||||
@label="explorer.import.label"
|
||||
@icon="upload"
|
||||
@acceptedFormatsOverride={{this.acceptedImportFileTypes}}
|
||||
@showButton="true"
|
||||
@onFilesPicked={{this.import}}
|
||||
class="import-btn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.showCreate}}
|
||||
<div class="query-create">
|
||||
<TextField
|
||||
@value={{this.newQueryName}}
|
||||
@placeholderKey="explorer.create_placeholder"
|
||||
@onChange={{this.updateNewQueryName}}
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.create}}
|
||||
@disabled={{this.createDisabled}}
|
||||
@label="explorer.create"
|
||||
@icon="plus"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.othersDirty}}
|
||||
<div class="warning">
|
||||
{{d-icon "triangle-exclamation"}}
|
||||
{{i18n "explorer.others_dirty"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if this.model.length}}
|
||||
{{#unless this.selectedItem.fake}}
|
||||
<div class="query-edit {{if this.editName 'editing'}}">
|
||||
{{#if this.selectedItem}}
|
||||
{{#if this.editingName}}
|
||||
<div class="name">
|
||||
<DButton
|
||||
@action={{this.goHome}}
|
||||
@icon="chevron-left"
|
||||
class="previous"
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.exitEdit}}
|
||||
@icon="xmark"
|
||||
class="previous"
|
||||
/>
|
||||
<div class="name-text-field">
|
||||
<TextField
|
||||
@value={{this.selectedItem.name}}
|
||||
@onChange={{this.setDirty}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="desc">
|
||||
<DTextarea
|
||||
@value={{this.selectedItem.description}}
|
||||
@placeholder={{i18n "explorer.description_placeholder"}}
|
||||
@input={{this.setDirty}}
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="name">
|
||||
<DButton
|
||||
@action={{this.goHome}}
|
||||
@icon="chevron-left"
|
||||
class="previous"
|
||||
/>
|
||||
|
||||
<h1>
|
||||
{{this.selectedItem.name}}
|
||||
{{#unless this.editDisabled}}
|
||||
<a href {{action "editName"}} class="edit-query-name">
|
||||
{{d-icon "pencil"}}
|
||||
</a>
|
||||
{{/unless}}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="desc">
|
||||
{{this.selectedItem.description}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#unless this.selectedItem.destroyed}}
|
||||
<div class="groups">
|
||||
<span class="label">{{i18n "explorer.allow_groups"}}</span>
|
||||
<span>
|
||||
<MultiSelect
|
||||
@value={{this.selectedItem.group_ids}}
|
||||
@content={{this.groupOptions}}
|
||||
@options={{hash allowAny=false}}
|
||||
@onChange={{this.updateGroupIds}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<div class="clear"></div>
|
||||
|
||||
{{#if this.editingQuery}}
|
||||
<div class="query-editor {{if this.hideSchema 'no-schema'}}">
|
||||
<div class="panels-flex">
|
||||
<div class="editor-panel">
|
||||
<AceEditor
|
||||
{{on "click" this.setDirty}}
|
||||
@content={{this.selectedItem.sql}}
|
||||
@onChange={{fn (mut this.selectedItem.sql)}}
|
||||
@mode="sql"
|
||||
@disabled={{this.selectedItem.destroyed}}
|
||||
@save={{this.save}}
|
||||
@submit={{this.saveAndRun}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<ExplorerSchema
|
||||
@schema={{this.schema}}
|
||||
@hideSchema={{this.hideSchema}}
|
||||
@updateHideSchema={{this.updateHideSchema}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grippie"
|
||||
{{draggable
|
||||
didStartDrag=this.didStartDrag
|
||||
didEndDrag=this.didEndDrag
|
||||
dragMove=this.dragMove
|
||||
}}
|
||||
>
|
||||
{{d-icon "discourse-expand"}}
|
||||
</div>
|
||||
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="sql">
|
||||
<CodeView
|
||||
@value={{this.selectedItem.sql}}
|
||||
@codeClass="sql"
|
||||
@setDirty={{this.setDirty}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="clear"></div>
|
||||
|
||||
<div class="pull-left left-buttons">
|
||||
{{#if this.editingQuery}}
|
||||
<DButton
|
||||
class="btn-save-query"
|
||||
@action={{this.save}}
|
||||
@label="explorer.save"
|
||||
@disabled={{this.saveDisabled}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#unless this.editDisabled}}
|
||||
<DButton
|
||||
class="btn-edit-query"
|
||||
@action={{this.editQuery}}
|
||||
@label="explorer.edit"
|
||||
@icon="pencil"
|
||||
/>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
<DButton
|
||||
@action={{this.download}}
|
||||
@label="explorer.export"
|
||||
@disabled={{this.runDisabled}}
|
||||
@icon="download"
|
||||
/>
|
||||
|
||||
{{#if this.editingQuery}}
|
||||
<DButton
|
||||
@action={{this.showHelpModal}}
|
||||
@label="explorer.help.label"
|
||||
@icon="circle-question"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="pull-right right-buttons">
|
||||
{{#if this.selectedItem.destroyed}}
|
||||
<DButton
|
||||
@action={{this.recover}}
|
||||
@icon="arrow-rotate-left"
|
||||
@label="explorer.recover"
|
||||
/>
|
||||
{{else}}
|
||||
{{#if this.editingQuery}}
|
||||
<DButton
|
||||
@action={{this.discard}}
|
||||
@icon="arrow-rotate-left"
|
||||
@label="explorer.undo"
|
||||
@disabled={{this.saveDisabled}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<DButton
|
||||
@action={{this.destroyQuery}}
|
||||
@icon="trash-can"
|
||||
@label="explorer.delete"
|
||||
class="btn-danger"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="clear"></div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<form class="query-run" {{on "submit" this.run}}>
|
||||
{{#if this.selectedItem.hasParams}}
|
||||
<ParamInputForm
|
||||
@initialValues={{this.parsedParams}}
|
||||
@paramInfo={{this.selectedItem.param_info}}
|
||||
@onRegisterApi={{this.onRegisterApi}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.runDisabled}}
|
||||
{{#if this.saveDisabled}}
|
||||
<DButton
|
||||
@label="explorer.run"
|
||||
@disabled="true"
|
||||
class="btn-primary"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{this.saveAndRun}}
|
||||
@icon="play"
|
||||
@label="explorer.saverun"
|
||||
class="btn-primary"
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{this.run}}
|
||||
@icon="play"
|
||||
@label="explorer.run"
|
||||
@disabled={{this.runDisabled}}
|
||||
@type="submit"
|
||||
class="btn-primary"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<label class="query-plan">
|
||||
<Input @type="checkbox" @checked={{this.explain}} name="explain" />
|
||||
{{i18n "explorer.explain_label"}}
|
||||
</label>
|
||||
</form>
|
||||
<hr />
|
||||
{{/unless}}
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||
|
||||
{{#unless this.selectedItem.fake}}
|
||||
<QueryResultsWrapper
|
||||
@results={{this.results}}
|
||||
@showResults={{this.showResults}}
|
||||
@query={{this.selectedItem}}
|
||||
@content={{this.results}}
|
||||
/>
|
||||
{{/unless}}
|
||||
|
||||
{{#unless this.validQueryPresent}}
|
||||
<div class="container">
|
||||
<table class="d-admin-table recent-queries">
|
||||
<thead class="heading-container">
|
||||
<th class="col heading name">
|
||||
<div
|
||||
role="button"
|
||||
class="heading-toggle"
|
||||
{{on "click" (fn this.updateSortProperty "name")}}
|
||||
>
|
||||
<TableHeaderToggle
|
||||
@field="name"
|
||||
@labelKey="explorer.query_name"
|
||||
@order={{this.order}}
|
||||
@asc={{not this.sortDescending}}
|
||||
@automatic="true"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th class="col heading created-by">
|
||||
<div
|
||||
role="button"
|
||||
class="heading-toggle"
|
||||
{{on "click" (fn this.updateSortProperty "username")}}
|
||||
>
|
||||
<TableHeaderToggle
|
||||
@field="username"
|
||||
@labelKey="explorer.query_user"
|
||||
@order={{this.order}}
|
||||
@asc={{not this.sortDescending}}
|
||||
@automatic="true"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th class="col heading group-names">
|
||||
<div class="group-names-header">
|
||||
{{i18n "explorer.query_groups"}}
|
||||
</div>
|
||||
</th>
|
||||
<th class="col heading created-at">
|
||||
<div
|
||||
role="button"
|
||||
class="heading-toggle"
|
||||
{{on "click" (fn this.updateSortProperty "last_run_at")}}
|
||||
>
|
||||
<TableHeaderToggle
|
||||
@field="last_run_at"
|
||||
@labelKey="explorer.query_time"
|
||||
@order={{this.order}}
|
||||
@asc={{not this.sortDescending}}
|
||||
@automatic="true"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.filteredContent as |query|}}
|
||||
<tr class="d-admin-row__content query-row">
|
||||
<td class="d-admin-row__overview">
|
||||
<a
|
||||
{{on "click" this.scrollTop}}
|
||||
href="/admin/plugins/explorer/?id={{query.id}}"
|
||||
>
|
||||
<b class="query-name">{{query.name}}</b>
|
||||
<medium class="query-desc">{{query.description}}</medium>
|
||||
</a>
|
||||
</td>
|
||||
<td class="d-admin-row__detail query-created-by">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "explorer.query_user"}}
|
||||
</div>
|
||||
{{#if query.username}}
|
||||
<div>
|
||||
<a href="/u/{{query.username}}/activity">
|
||||
<medium>{{query.username}}</medium>
|
||||
</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail query-group-names">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "explorer.query_groups"}}
|
||||
</div>
|
||||
<div class="group-names">
|
||||
{{#each query.group_names as |group|}}
|
||||
<ShareReport @group={{group}} @query={{query}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-admin-row__detail query-created-at">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "explorer.query_time"}}
|
||||
</div>
|
||||
{{#if query.last_run_at}}
|
||||
<medium>
|
||||
{{bound-date query.last_run_at}}
|
||||
</medium>
|
||||
{{else if query.created_at}}
|
||||
<medium>
|
||||
{{bound-date query.created_at}}
|
||||
</medium>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<br />
|
||||
<em class="no-search-results">
|
||||
{{i18n "explorer.no_search_results"}}
|
||||
</em>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<div class="explorer-pad-bottom"></div>
|
||||
{{/if}}
|
||||
{{/if}}
|
@ -23,7 +23,7 @@ en:
|
||||
Here are the results:
|
||||
%{table}
|
||||
|
||||
<a href='%{base_url}/admin/plugins/explorer?id=%{query_id}'>View query in Data Explorer</a>
|
||||
<a href='%{base_url}/admin/plugins/explorer/queries/%{query_id}'>View query in Data Explorer</a>
|
||||
|
||||
Report created at %{created_at} (%{timezone})
|
||||
post:
|
||||
@ -33,7 +33,7 @@ en:
|
||||
Here are the results:
|
||||
%{table}
|
||||
|
||||
<a href='%{base_url}/admin/plugins/explorer?id=%{query_id}'>View query in Data Explorer</a>
|
||||
<a href='%{base_url}/admin/plugins/explorer/queries/%{query_id}'>View query in Data Explorer</a>
|
||||
|
||||
Report created at %{created_at} (%{timezone})
|
||||
upload_appendix: "Appendix: [%{filename}|attachment](%{short_url})"
|
||||
|
@ -3,12 +3,12 @@
|
||||
DiscourseDataExplorer::Engine.routes.draw do
|
||||
root to: "query#index"
|
||||
get "queries" => "query#index"
|
||||
get "queries/:id" => "query#show"
|
||||
|
||||
scope "/", defaults: { format: :json } do
|
||||
get "schema" => "query#schema"
|
||||
get "groups" => "query#groups"
|
||||
post "queries" => "query#create"
|
||||
get "queries/:id" => "query#show"
|
||||
put "queries/:id" => "query#update"
|
||||
delete "queries/:id" => "query#destroy"
|
||||
post "queries/:id/run" => "query#run", :constraints => { format: /(json|csv)/ }
|
||||
|
@ -46,7 +46,7 @@ module ::DiscourseDataExplorer
|
||||
sql = <<-SQL
|
||||
/*
|
||||
* DiscourseDataExplorer Query
|
||||
* Query: /admin/plugins/explorer?id=#{query.id}
|
||||
* Query: /admin/plugins/explorer/queries/#{query.id}
|
||||
* Started by: #{opts[:current_user]}
|
||||
*/
|
||||
WITH query AS (
|
||||
|
@ -29,6 +29,32 @@ RSpec.describe "Explorer", type: :system, js: true do
|
||||
end
|
||||
end
|
||||
|
||||
context "with the old url format" do
|
||||
fab!(:query_1) do
|
||||
Fabricate(
|
||||
:query,
|
||||
name: "My query",
|
||||
description: "Test query",
|
||||
sql: "SELECT * FROM users",
|
||||
user: admin,
|
||||
)
|
||||
end
|
||||
|
||||
it "redirects to the new url format" do
|
||||
visit("/admin/plugins/explorer/?id=#{query_1.id}")
|
||||
|
||||
expect(page).to have_current_path("/admin/plugins/explorer/queries/#{query_1.id}")
|
||||
end
|
||||
|
||||
it "redirects to the new url format with params" do
|
||||
visit("/admin/plugins/explorer/?id=#{query_1.id}¶ms=%7B%22limit%22%3A%2210%22%7D")
|
||||
|
||||
expect(page).to have_current_path(
|
||||
"/admin/plugins/explorer/queries/#{query_1.id}?params=%7B%22limit%22%3A%2210%22%7D",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a group_list param" do
|
||||
fab!(:q2) do
|
||||
Fabricate(
|
||||
@ -43,7 +69,7 @@ RSpec.describe "Explorer", type: :system, js: true do
|
||||
|
||||
it "supports setting a group_list param" do
|
||||
visit(
|
||||
"/admin/plugins/explorer?id=#{q2.id}¶ms=%7B\"groups\"%3A\"admins%2Ctrust_level_1\"%7D",
|
||||
"/admin/plugins/explorer/queries/#{q2.id}?params=%7B\"groups\"%3A\"admins%2Ctrust_level_1\"%7D",
|
||||
)
|
||||
find(".query-run .btn-primary").click
|
||||
|
||||
|
@ -62,7 +62,7 @@ RSpec.describe "Param input", type: :system, js: true do
|
||||
end
|
||||
|
||||
it "correctly displays parameter input boxes" do
|
||||
visit("/admin/plugins/explorer?id=#{all_params_query.id}")
|
||||
visit("/admin/plugins/explorer/queries/#{all_params_query.id}")
|
||||
|
||||
::DiscourseDataExplorer::Parameter
|
||||
.create_from_sql(ALL_PARAMS_SQL)
|
||||
|
@ -88,44 +88,22 @@ acceptance("Data Explorer Plugin | List Queries", function (needs) {
|
||||
queries: [
|
||||
{
|
||||
id: -5,
|
||||
sql: "-- [params]\n-- int :months_ago = 1\n\nWITH query_period AS\n(SELECT date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' AS period_start,\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' + INTERVAL '1 month' - INTERVAL '1 second' AS period_end)\nSELECT t.id AS topic_id,\n t.category_id,\n COUNT(p.id) AS reply_count\nFROM topics t\nJOIN posts p ON t.id = p.topic_id\nJOIN query_period qp ON p.created_at >= qp.period_start\nAND p.created_at <= qp.period_end\nWHERE t.archetype = 'regular'\nAND t.user_id > 0\nGROUP BY t.id\nORDER BY COUNT(p.id) DESC, t.score DESC\nLIMIT 100\n",
|
||||
name: "Top 100 Active Topics",
|
||||
description:
|
||||
"based on the number of replies, it accepts a ‘months_ago’ parameter, defaults to 1 to give results for the last calendar month.",
|
||||
param_info: [
|
||||
{
|
||||
identifier: "months_ago",
|
||||
type: "int",
|
||||
default: "1",
|
||||
nullable: false,
|
||||
},
|
||||
],
|
||||
created_at: "2021-02-05T16:42:45.572Z",
|
||||
username: "system",
|
||||
group_ids: [],
|
||||
last_run_at: "2021-02-08T15:37:49.188Z",
|
||||
hidden: false,
|
||||
user_id: -1,
|
||||
},
|
||||
{
|
||||
id: -6,
|
||||
sql: "-- [params]\n-- int :months_ago = 1\n\nWITH query_period AS (\n SELECT\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' as period_start,\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' + INTERVAL '1 month' - INTERVAL '1 second' as period_end\n )\n\n SELECT\n ua.user_id,\n count(1) AS like_count\n FROM user_actions ua\n INNER JOIN query_period qp\n ON ua.created_at >= qp.period_start\n AND ua.created_at <= qp.period_end\n WHERE ua.action_type = 1\n GROUP BY ua.user_id\n ORDER BY like_count DESC\n LIMIT 100\n",
|
||||
name: "Top 100 Likers",
|
||||
description:
|
||||
"returns the top 100 likers for a given monthly period ordered by like_count. It accepts a ‘months_ago’ parameter, defaults to 1 to give results for the last calendar month.",
|
||||
param_info: [
|
||||
{
|
||||
identifier: "months_ago",
|
||||
type: "int",
|
||||
default: "1",
|
||||
nullable: false,
|
||||
},
|
||||
],
|
||||
created_at: "2021-02-02T12:21:11.449Z",
|
||||
username: "system",
|
||||
group_ids: [],
|
||||
last_run_at: "2021-02-11T08:29:59.337Z",
|
||||
hidden: false,
|
||||
user_id: -1,
|
||||
},
|
||||
],
|
||||
|
@ -46,6 +46,32 @@ acceptance("Data Explorer Plugin | New Query", function (needs) {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
server.get("/admin/plugins/explorer/queries/-15", () => {
|
||||
return helper.response({
|
||||
query: {
|
||||
id: -15,
|
||||
sql: "-- [params]\n-- int :months_ago = 1\n\nWITH query_period AS\n(SELECT date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' AS period_start,\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' + INTERVAL '1 month' - INTERVAL '1 second' AS period_end)\nSELECT t.id AS topic_id,\n t.category_id,\n COUNT(p.id) AS reply_count\nFROM topics t\nJOIN posts p ON t.id = p.topic_id\nJOIN query_period qp ON p.created_at >= qp.period_start\nAND p.created_at <= qp.period_end\nWHERE t.archetype = 'regular'\nAND t.user_id > 0\nGROUP BY t.id\nORDER BY COUNT(p.id) DESC, t.score DESC\nLIMIT 100\n",
|
||||
name: "foo",
|
||||
description:
|
||||
"based on the number of replies, it accepts a ‘months_ago’ parameter, defaults to 1 to give results for the last calendar month.",
|
||||
param_info: [
|
||||
{
|
||||
identifier: "months_ago",
|
||||
type: "int",
|
||||
default: "1",
|
||||
nullable: false,
|
||||
},
|
||||
],
|
||||
created_at: "2021-02-05T16:42:45.572Z",
|
||||
username: "system",
|
||||
group_ids: [],
|
||||
last_run_at: "2021-02-08T15:37:49.188Z",
|
||||
hidden: false,
|
||||
user_id: -1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("creates a new query", async function (assert) {
|
||||
@ -57,6 +83,6 @@ acceptance("Data Explorer Plugin | New Query", function (needs) {
|
||||
// select create new query button
|
||||
await click(".query-create button");
|
||||
|
||||
assert.strictEqual(currentURL(), "/admin/plugins/explorer?id=-15");
|
||||
assert.strictEqual(currentURL(), "/admin/plugins/explorer/queries/-15");
|
||||
});
|
||||
});
|
||||
|
@ -91,62 +91,62 @@ acceptance("Data Explorer Plugin | Param Input", function (needs) {
|
||||
queries: [
|
||||
{
|
||||
id: -6,
|
||||
sql: "-- [params]\n-- int :months_ago = 1\n\nWITH query_period AS (\n SELECT\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' as period_start,\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' + INTERVAL '1 month' - INTERVAL '1 second' as period_end\n )\n\n SELECT\n ua.user_id,\n count(1) AS like_count\n FROM user_actions ua\n INNER JOIN query_period qp\n ON ua.created_at >= qp.period_start\n AND ua.created_at <= qp.period_end\n WHERE ua.action_type = 1\n GROUP BY ua.user_id\n ORDER BY like_count DESC\n LIMIT 100\n",
|
||||
name: "Top 100 Likers",
|
||||
description:
|
||||
"returns the top 100 likers for a given monthly period ordered by like_count. It accepts a ‘months_ago’ parameter, defaults to 1 to give results for the last calendar month.",
|
||||
param_info: [
|
||||
{
|
||||
identifier: "months_ago",
|
||||
type: "int",
|
||||
default: "1",
|
||||
nullable: false,
|
||||
},
|
||||
],
|
||||
created_at: "2021-02-02T12:21:11.449Z",
|
||||
username: "system",
|
||||
group_ids: [],
|
||||
last_run_at: "2021-02-11T08:29:59.337Z",
|
||||
hidden: false,
|
||||
user_id: -1,
|
||||
},
|
||||
{
|
||||
id: -7,
|
||||
sql: "-- [params]\n-- user_id :user\n\nSELECT :user_id\n\n",
|
||||
name: "Invalid Query",
|
||||
description: "",
|
||||
param_info: [
|
||||
{
|
||||
identifier: "user",
|
||||
type: "user_id",
|
||||
default: null,
|
||||
nullable: false,
|
||||
},
|
||||
],
|
||||
created_at: "2022-01-14T16:40:05.458Z",
|
||||
username: "bianca",
|
||||
group_ids: [],
|
||||
last_run_at: "2022-01-14T16:47:34.244Z",
|
||||
hidden: false,
|
||||
user_id: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
sql: "SELECT 1",
|
||||
name: "Params test",
|
||||
description: "test for params.",
|
||||
param_info: [],
|
||||
created_at: "2021-02-02T12:21:11.449Z",
|
||||
username: "system",
|
||||
group_ids: [41],
|
||||
last_run_at: "2021-02-11T08:29:59.337Z",
|
||||
hidden: false,
|
||||
user_id: -1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
server.get("/admin/plugins/explorer/queries/-6", () => {
|
||||
return helper.response({
|
||||
query: {
|
||||
id: -6,
|
||||
sql: "-- [params]\n-- int :months_ago = 1\n\nWITH query_period AS (\n SELECT\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' as period_start,\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' + INTERVAL '1 month' - INTERVAL '1 second' as period_end\n )\n\n SELECT\n ua.user_id,\n count(1) AS like_count\n FROM user_actions ua\n INNER JOIN query_period qp\n ON ua.created_at >= qp.period_start\n AND ua.created_at <= qp.period_end\n WHERE ua.action_type = 1\n GROUP BY ua.user_id\n ORDER BY like_count DESC\n LIMIT 100\n",
|
||||
name: "Top 100 Likers",
|
||||
description:
|
||||
"returns the top 100 likers for a given monthly period ordered by like_count. It accepts a ‘months_ago’ parameter, defaults to 1 to give results for the last calendar month.",
|
||||
param_info: [
|
||||
{
|
||||
identifier: "months_ago",
|
||||
type: "int",
|
||||
default: "1",
|
||||
nullable: false,
|
||||
},
|
||||
],
|
||||
created_at: "2021-02-02T12:21:11.449Z",
|
||||
username: "system",
|
||||
group_ids: [],
|
||||
last_run_at: "2021-02-11T08:29:59.337Z",
|
||||
hidden: false,
|
||||
user_id: -1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
server.put("/admin/plugins/explorer/queries/-6", () => {
|
||||
return helper.response({
|
||||
success: true,
|
||||
@ -244,6 +244,31 @@ acceptance("Data Explorer Plugin | Param Input", function (needs) {
|
||||
});
|
||||
});
|
||||
|
||||
server.get("/admin/plugins/explorer/queries/-7", () => {
|
||||
return helper.response({
|
||||
query: {
|
||||
id: -7,
|
||||
sql: "-- [params]\n-- user_id :user\n\nSELECT :user_id\n\n",
|
||||
name: "Invalid Query",
|
||||
description: "",
|
||||
param_info: [
|
||||
{
|
||||
identifier: "user",
|
||||
type: "user_id",
|
||||
default: null,
|
||||
nullable: false,
|
||||
},
|
||||
],
|
||||
created_at: "2022-01-14T16:40:05.458Z",
|
||||
username: "bianca",
|
||||
group_ids: [],
|
||||
last_run_at: "2022-01-14T16:47:34.244Z",
|
||||
hidden: false,
|
||||
user_id: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
server.post("/admin/plugins/explorer/queries/-7/run", () => {
|
||||
return helper.response({
|
||||
success: true,
|
||||
@ -351,7 +376,7 @@ acceptance("Data Explorer Plugin | Param Input", function (needs) {
|
||||
});
|
||||
|
||||
test("puts params for the query into the url", async function (assert) {
|
||||
await visit("/admin/plugins/explorer?id=-6");
|
||||
await visit("/admin/plugins/explorer/queries/-6");
|
||||
const monthsAgoValue = "2";
|
||||
await fillIn(".query-params input", monthsAgoValue);
|
||||
await click("form.query-run button");
|
||||
@ -373,7 +398,7 @@ acceptance("Data Explorer Plugin | Param Input", function (needs) {
|
||||
});
|
||||
|
||||
test("loads the page if one of the parameter is null", async function (assert) {
|
||||
await visit('/admin/plugins/explorer?id=-7¶ms={"user":null}');
|
||||
await visit('/admin/plugins/explorer/queries/-7?params={"user":null}');
|
||||
assert.dom(".query-params .user-chooser").exists();
|
||||
assert.dom(".query-run .btn.btn-primary").exists();
|
||||
});
|
||||
@ -393,7 +418,7 @@ acceptance("Data Explorer Plugin | Param Input", function (needs) {
|
||||
});
|
||||
|
||||
test("creates input boxes if has parameters when save", async function (assert) {
|
||||
await visit("/admin/plugins/explorer?id=3");
|
||||
await visit("/admin/plugins/explorer/queries/3");
|
||||
assert.dom(".query-params input").doesNotExist();
|
||||
await click(".query-edit .btn-edit-query");
|
||||
await click(".query-editor .ace_text-input");
|
||||
|
@ -88,42 +88,71 @@ acceptance("Data Explorer Plugin | Run Query", function (needs) {
|
||||
queries: [
|
||||
{
|
||||
id: -6,
|
||||
sql: "-- [params]\n-- int :months_ago = 1\n\nWITH query_period AS (\n SELECT\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' as period_start,\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' + INTERVAL '1 month' - INTERVAL '1 second' as period_end\n )\n\n SELECT\n ua.user_id,\n count(1) AS like_count\n FROM user_actions ua\n INNER JOIN query_period qp\n ON ua.created_at >= qp.period_start\n AND ua.created_at <= qp.period_end\n WHERE ua.action_type = 1\n GROUP BY ua.user_id\n ORDER BY like_count DESC\n LIMIT 100\n",
|
||||
name: "Top 100 Likers",
|
||||
description:
|
||||
"returns the top 100 likers for a given monthly period ordered by like_count. It accepts a ‘months_ago’ parameter, defaults to 1 to give results for the last calendar month.",
|
||||
param_info: [
|
||||
{
|
||||
identifier: "months_ago",
|
||||
type: "int",
|
||||
default: "1",
|
||||
nullable: false,
|
||||
},
|
||||
],
|
||||
created_at: "2021-02-02T12:21:11.449Z",
|
||||
username: "system",
|
||||
group_ids: [],
|
||||
last_run_at: "2021-02-11T08:29:59.337Z",
|
||||
hidden: false,
|
||||
user_id: -1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
sql: 'SELECT 0 zero, null "null", false "false"',
|
||||
name: "What about 0?",
|
||||
description: "",
|
||||
param_info: [],
|
||||
created_at: "2023-05-04T22:16:06.007Z",
|
||||
username: "system",
|
||||
group_ids: [],
|
||||
last_run_at: "2023-05-04T22:16:23.858Z",
|
||||
hidden: false,
|
||||
user_id: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
server.get("/admin/plugins/explorer/queries/-6", () => {
|
||||
return helper.response({
|
||||
query: {
|
||||
id: -6,
|
||||
sql: "-- [params]\n-- int :months_ago = 1\n\nWITH query_period AS (\n SELECT\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' as period_start,\n date_trunc('month', CURRENT_DATE) - INTERVAL ':months_ago months' + INTERVAL '1 month' - INTERVAL '1 second' as period_end\n )\n\n SELECT\n ua.user_id,\n count(1) AS like_count\n FROM user_actions ua\n INNER JOIN query_period qp\n ON ua.created_at >= qp.period_start\n AND ua.created_at <= qp.period_end\n WHERE ua.action_type = 1\n GROUP BY ua.user_id\n ORDER BY like_count DESC\n LIMIT 100\n",
|
||||
name: "Top 100 Likers",
|
||||
description:
|
||||
"returns the top 100 likers for a given monthly period ordered by like_count. It accepts a ‘months_ago’ parameter, defaults to 1 to give results for the last calendar month.",
|
||||
param_info: [
|
||||
{
|
||||
identifier: "months_ago",
|
||||
type: "int",
|
||||
default: "1",
|
||||
nullable: false,
|
||||
},
|
||||
],
|
||||
created_at: "2021-02-02T12:21:11.449Z",
|
||||
username: "system",
|
||||
group_ids: [],
|
||||
last_run_at: "2021-02-11T08:29:59.337Z",
|
||||
hidden: false,
|
||||
user_id: -1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
server.get("/admin/plugins/explorer/queries/2", () => {
|
||||
return helper.response({
|
||||
query: {
|
||||
id: 2,
|
||||
sql: 'SELECT 0 zero, null "null", false "false"',
|
||||
name: "What about 0?",
|
||||
description: "",
|
||||
param_info: [],
|
||||
created_at: "2023-05-04T22:16:06.007Z",
|
||||
username: "system",
|
||||
group_ids: [],
|
||||
last_run_at: "2023-05-04T22:16:23.858Z",
|
||||
hidden: false,
|
||||
user_id: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
server.post("/admin/plugins/explorer/queries/-6/run", () => {
|
||||
return helper.response({
|
||||
success: true,
|
||||
@ -177,7 +206,7 @@ acceptance("Data Explorer Plugin | Run Query", function (needs) {
|
||||
});
|
||||
|
||||
test("runs query and renders data and a chart", async function (assert) {
|
||||
await visit("/admin/plugins/explorer?id=-6");
|
||||
await visit("/admin/plugins/explorer/queries/-6");
|
||||
|
||||
assert
|
||||
.dom("div.name h1")
|
||||
@ -205,7 +234,7 @@ acceptance("Data Explorer Plugin | Run Query", function (needs) {
|
||||
});
|
||||
|
||||
test("runs query and renders 0, false, and NULL values correctly", async function (assert) {
|
||||
await visit("/admin/plugins/explorer?id=2");
|
||||
await visit("/admin/plugins/explorer/queries/2");
|
||||
|
||||
assert
|
||||
.dom("div.name h1")
|
||||
|
Loading…
x
Reference in New Issue
Block a user