diff --git a/app/controllers/discourse_data_explorer/query_controller.rb b/app/controllers/discourse_data_explorer/query_controller.rb index 2438a7a..84c6263 100644 --- a/app/controllers/discourse_data_explorer/query_controller.rb +++ b/app/controllers/discourse_data_explorer/query_controller.rb @@ -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 diff --git a/app/serializers/discourse_data_explorer/query_details_serializer.rb b/app/serializers/discourse_data_explorer/query_details_serializer.rb new file mode 100644 index 0000000..d211777 --- /dev/null +++ b/app/serializers/discourse_data_explorer/query_details_serializer.rb @@ -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 diff --git a/app/serializers/discourse_data_explorer/query_serializer.rb b/app/serializers/discourse_data_explorer/query_serializer.rb index 2a87ad4..724e255 100644 --- a/app/serializers/discourse_data_explorer/query_serializer.rb +++ b/app/serializers/discourse_data_explorer/query_serializer.rb @@ -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 diff --git a/assets/javascripts/discourse/controllers/admin-plugins-explorer-index.js b/assets/javascripts/discourse/controllers/admin-plugins-explorer-index.js new file mode 100644 index 0000000..9b28f88 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-plugins-explorer-index.js @@ -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; + } +} diff --git a/assets/javascripts/discourse/controllers/admin-plugins-explorer.js b/assets/javascripts/discourse/controllers/admin-plugins-explorer-queries-details.js similarity index 56% rename from assets/javascripts/discourse/controllers/admin-plugins-explorer.js rename to assets/javascripts/discourse/controllers/admin-plugins-explorer-queries-details.js index 578953e..694cca3 100644 --- a/assets/javascripts/discourse/controllers/admin-plugins-explorer.js +++ b/assets/javascripts/discourse/controllers/admin-plugins-explorer-queries-details.js @@ -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), diff --git a/assets/javascripts/discourse/explorer-route-map.js b/assets/javascripts/discourse/explorer-route-map.js index 985d520..8b793f7 100644 --- a/assets/javascripts/discourse/explorer-route-map.js +++ b/assets/javascripts/discourse/explorer-route-map.js @@ -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" }); + }); + }); }, }; diff --git a/assets/javascripts/discourse/models/query.js b/assets/javascripts/discourse/models/query.js index 1561806..b57fd41 100644 --- a/assets/javascripts/discourse/models/query.js +++ b/assets/javascripts/discourse/models/query.js @@ -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() { diff --git a/assets/javascripts/discourse/routes/admin-plugins-explorer-index.js b/assets/javascripts/discourse/routes/admin-plugins-explorer-index.js new file mode 100644 index 0000000..d3936f5 --- /dev/null +++ b/assets/javascripts/discourse/routes/admin-plugins-explorer-index.js @@ -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); + } +} diff --git a/assets/javascripts/discourse/routes/admin-plugins-explorer.js b/assets/javascripts/discourse/routes/admin-plugins-explorer-queries-details.js similarity index 72% rename from assets/javascripts/discourse/routes/admin-plugins-explorer.js rename to assets/javascripts/discourse/routes/admin-plugins-explorer-queries-details.js index 85a9be1..8953618 100644 --- a/assets/javascripts/discourse/routes/admin-plugins-explorer.js +++ b/assets/javascripts/discourse/routes/admin-plugins-explorer-queries-details.js @@ -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 }; }); }); diff --git a/assets/javascripts/discourse/templates/admin/plugins-explorer-index.hbs b/assets/javascripts/discourse/templates/admin/plugins-explorer-index.hbs new file mode 100644 index 0000000..80166b0 --- /dev/null +++ b/assets/javascripts/discourse/templates/admin/plugins-explorer-index.hbs @@ -0,0 +1,166 @@ +{{#if this.disallow}} +
+
+
+ |
+
+
+
+ |
+
+
+ {{i18n "explorer.query_groups"}}
+
+ |
+
+
+
+ |
+
+
+ {{#each this.filteredContent as |query|}}
+
---|---|---|---|
+
+ {{query.name}}
+ |
+
+
+ {{i18n "explorer.query_user"}}
+
+ {{#if query.username}}
+
+ {{/if}}
+ |
+
+
+ {{i18n "explorer.query_groups"}}
+
+
+ {{#each query.group_names as |group|}}
+
+ |
+
+
+ {{i18n "explorer.query_time"}}
+
+ {{#if query.last_run_at}}
+ |
+
-
-
- |
-
-
-
- |
-
-
- {{i18n "explorer.query_groups"}}
-
- |
-
-
-
- |
-
-
- {{#each this.filteredContent as |query|}}
-
---|---|---|---|
-
- {{query.name}}
- |
-
-
- {{i18n "explorer.query_user"}}
-
- {{#if query.username}}
-
- {{/if}}
- |
-
-
- {{i18n "explorer.query_groups"}}
-
-
- {{#each query.group_names as |group|}}
-
- |
-
-
- {{i18n "explorer.query_time"}}
-
- {{#if query.last_run_at}}
- |
-