DEV: Upgrade `admin-plugins-explorer` to Octane (#209)

- Drop `explorer-container` and move its logic to `admin-plugin-explorer` container
- Convert resizing of the query edit pane from jquery -> draggable modifier
This commit is contained in:
Isaac Janzen 2023-01-05 09:27:10 -06:00 committed by GitHub
parent b1df914549
commit 4d26cf78f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 731 additions and 769 deletions

View File

@ -0,0 +1 @@
<pre><code class={{@codeClass}}>{{@value}}</code></pre>

View File

@ -1,96 +0,0 @@
import Component from "@ember/component";
import { observes } from "discourse-common/utils/decorators";
import { schedule, throttle } from "@ember/runloop";
export default Component.extend({
@observes("hideSchema")
_onHideSchema() {
this.appEvents.trigger("ace:resize");
},
@observes("everEditing")
_onInsertEditor() {
schedule("afterRender", this, () => this._bindControls());
},
_bindControls() {
if (this._state !== "inDOM") {
return;
}
const $editPane = $(".query-editor");
if (!$editPane.length) {
return;
}
const oldGrippie = this.grippie;
if (oldGrippie) {
oldGrippie.off("mousedown mousemove mouseup");
}
const $grippie = $editPane.find(".grippie");
const $target = $editPane.find(".panels-flex");
const $document = $(document);
const minWidth = $target.width();
const minHeight = $target.height();
this.set("grippie", $grippie);
const mousemove = (e) => {
const diffY = this.startY - e.screenY;
const diffX = this.startX - e.screenX;
const newHeight = Math.max(minHeight, this.startHeight - diffY);
const newWidth = Math.max(minWidth, this.startWidth - diffX);
$target.height(newHeight);
$target.width(newWidth);
$grippie.width(newWidth);
this.appEvents.trigger("ace:resize");
};
const throttledMousemove = ((event) => {
event.preventDefault();
throttle(this, mousemove, event, 20);
}).bind(this);
const mouseup = (() => {
$document.off("mousemove", throttledMousemove);
$document.off("mouseup", mouseup);
this.setProperties({
startY: null,
startX: null,
startHeight: null,
startWidth: null,
});
}).bind(this);
$grippie.on("mousedown", (e) => {
this.setProperties({
startY: e.screenY,
startX: e.screenX,
startHeight: $target.height(),
startWidth: $target.width(),
});
$document.on("mousemove", throttledMousemove);
$document.on("mouseup", mouseup);
e.preventDefault();
});
},
didInsertElement() {
this._super(...arguments);
this._bindControls();
},
willDestroyElement() {
this._super(...arguments);
if (this.everEditing) {
this.grippie && this.grippie.off("mousedown");
this.set("grippie", null);
}
},
});

View File

@ -3,140 +3,126 @@ import showModal from "discourse/lib/show-modal";
import Query from "discourse/plugins/discourse-data-explorer/discourse/models/query"; import Query from "discourse/plugins/discourse-data-explorer/discourse/models/query";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import discourseComputed, { import { bind } from "discourse-common/utils/decorators";
bind,
observes,
} from "discourse-common/utils/decorators";
import I18n from "I18n"; import I18n from "I18n";
import { Promise } from "rsvp"; import { Promise } from "rsvp";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { get } from "@ember/object"; import { action } from "@ember/object";
import { not, reads, sort } from "@ember/object/computed"; import { tracked } from "@glimmer/tracking";
const NoQuery = Query.create({ name: "No queries", fake: true, group_ids: [] }); const NoQuery = Query.create({ name: "No queries", fake: true, group_ids: [] });
export default Controller.extend({ export default class PluginsExplorerController extends Controller {
dialog: service(), @service dialog;
queryParams: { selectedQueryId: "id", params: "params" }, @service appEvents;
selectedQueryId: null,
editDisabled: false,
showResults: false,
hideSchema: false,
loading: false,
explain: false,
saveDisabled: not("selectedItem.dirty"), @tracked sortByProperty = "last_run_at";
runDisabled: reads("selectedItem.dirty"), @tracked sortDescending = true;
results: reads("selectedItem.results"), @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 dirty = false;
asc: null, queryParams = ["params", { selectedQueryId: "id" }];
order: null, explain = false;
editing: false, acceptedImportFileTypes = ["application/json"];
everEditing: false, order = null;
showRecentQueries: true,
sortBy: ["last_run_at:desc"],
sortedQueries: sort("model", "sortBy"),
@discourseComputed("params") get validQueryPresent() {
parsedParams(params) { return !!this.selectedItem.id;
return params ? JSON.parse(params) : null; }
},
@discourseComputed get saveDisabled() {
acceptedImportFileTypes() { return !this.dirty;
return ["application/json"]; }
},
@discourseComputed("search", "sortBy") get runDisabled() {
filteredContent(search) { return this.dirty;
const regexp = new RegExp(search, "i"); }
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( return this.sortedQueries.filter(
(result) => regexp.test(result.name) || regexp.test(result.description) (result) => regexp.test(result.name) || regexp.test(result.description)
); );
}, }
@discourseComputed("newQueryName") get createDisabled() {
createDisabled(newQueryName) { return (this.newQueryName || "").trim().length === 0;
return (newQueryName || "").trim().length === 0; }
},
@discourseComputed("selectedQueryId") get selectedItem() {
selectedItem(selectedQueryId) { const query = this.model.findBy("id", parseInt(this.selectedQueryId, 10));
const id = parseInt(selectedQueryId, 10); return query || NoQuery;
const item = this.model.findBy("id", id); }
!isNaN(id) get editDisabled() {
? this.set("showRecentQueries", false) return parseInt(this.selectedQueryId, 10) < 0 ? true : false;
: this.set("showRecentQueries", true); }
if (id < 0) { get groupOptions() {
this.set("editDisabled", true); return this.groups
}
return item || NoQuery;
},
@discourseComputed("selectedItem", "editing")
selectedGroupNames() {
const groupIds = this.selectedItem.group_ids || [];
const groupNames = groupIds.map((id) => {
return this.groupOptions.find((groupOption) => groupOption.id === id)
.name;
});
return groupNames.join(", ");
},
@discourseComputed("groups")
groupOptions(groups) {
return groups
.filter((g) => g.id !== 0) .filter((g) => g.id !== 0)
.map((g) => { .map((g) => {
return { id: g.id, name: g.name }; return { id: g.id, name: g.name };
}); });
}, }
@discourseComputed("selectedItem", "selectedItem.dirty") get othersDirty() {
othersDirty(selectedItem) { return !!this.model.find((q) => q !== this.selectedItem && this.dirty);
return !!this.model.find((q) => q !== selectedItem && q.dirty); }
},
@observes("editing")
setEverEditing() {
if (this.editing && !this.everEditing) {
this.set("everEditing", true);
}
},
addCreatedRecord(record) { addCreatedRecord(record) {
this.model.pushObject(record); this.model.pushObject(record);
this.set("selectedQueryId", get(record, "id")); this.selectedQueryId = record.id;
this.selectedItem.set("dirty", false); this.dirty = false;
this.setProperties({ this.setProperties({
showResults: false, showResults: false,
results: null, results: null,
editing: true, editingName: true,
editingQuery: true,
}); });
}, }
@action
save() { save() {
this.set("loading", true); this.loading = true;
if (this.get("selectedItem.description") === "") {
this.set("selectedItem.description", "");
}
return this.selectedItem return this.selectedItem
.save() .save()
.then(() => { .then(() => {
const query = this.selectedItem; this.dirty = false;
query.markNotDirty(); this.editingName = false;
this.set("editing", false); this.editingQuery = false;
}) })
.catch((x) => { .catch((x) => {
popupAjaxError(x); popupAjaxError(x);
throw x; throw x;
}) })
.finally(() => this.set("loading", false)); .finally(() => (this.loading = false));
}, }
@action
saveAndRun() {
this.save().then(() => this.run());
}
async _importQuery(file) { async _importQuery(file) {
const json = await this._readFileAsTextAsync(file); const json = await this._readFileAsTextAsync(file);
@ -144,7 +130,7 @@ export default Controller.extend({
const record = this.store.createRecord("query", query); const record = this.store.createRecord("query", query);
const response = await record.save(); const response = await record.save();
return response.target; return response.target;
}, }
_parseQuery(json) { _parseQuery(json) {
const parsed = JSON.parse(json); const parsed = JSON.parse(json);
@ -154,7 +140,7 @@ export default Controller.extend({
} }
query.id = 0; // 0 means no Id yet query.id = 0; // 0 means no Id yet
return query; return query;
}, }
_readFileAsTextAsync(file) { _readFileAsTextAsync(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -166,200 +152,270 @@ export default Controller.extend({
reader.readAsText(file); 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
didStartDrag() {}
@bind
didEndDrag() {}
@bind @bind
scrollTop() { scrollTop() {
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setProperties({ editing: false, everEditing: false }); this.editingName = false;
}, this.editingQuery = false;
}
actions: { @action
updateHideSchema(value) { updateGroupIds(value) {
this.set("hideSchema", value); this.dirty = true;
}, this.selectedItem.set("group_ids", value);
}
import(files) { @action
this.set("loading", true); updateHideSchema(value) {
const file = files[0]; this.hideSchema = value;
this._importQuery(file) }
.then((record) => this.addCreatedRecord(record))
.catch((e) => {
if (e.jqXHR) {
popupAjaxError(e);
} else if (e instanceof SyntaxError) {
this.dialog.alert(I18n.t("explorer.import.unparseable_json"));
} else if (e instanceof TypeError) {
this.dialog.alert(I18n.t("explorer.import.wrong_json"));
} else {
this.dialog.alert(I18n.t("errors.desc.unknown"));
// eslint-disable-next-line no-console
console.error(e);
}
})
.finally(() => {
this.set("loading", false);
});
},
showCreate() { @action
this.set("showCreate", true); import(files) {
}, this.loading = true;
const file = files[0];
editName() { this._importQuery(file)
this.set("editing", true); .then((record) => this.addCreatedRecord(record))
}, .catch((e) => {
if (e.jqXHR) {
download() { popupAjaxError(e);
window.open(this.get("selectedItem.downloadUrl"), "_blank"); } else if (e instanceof SyntaxError) {
}, this.dialog.alert(I18n.t("explorer.import.unparseable_json"));
} else if (e instanceof TypeError) {
goHome() { this.dialog.alert(I18n.t("explorer.import.wrong_json"));
this.setProperties({ } else {
asc: null, this.dialog.alert(I18n.t("errors.desc.unknown"));
order: null, // eslint-disable-next-line no-console
showResults: false, console.error(e);
editDisabled: false,
showRecentQueries: true,
selectedQueryId: null,
params: null,
sortBy: ["last_run_at:desc"],
});
this.transitionToRoute({ queryParams: { id: null, params: null } });
},
showHelpModal() {
showModal("query-help");
},
resetParams() {
this.selectedItem.resetParams();
},
saveDefaults() {
this.selectedItem.saveDefaults();
},
save() {
this.save();
},
saverun() {
this.save().then(() => this.send("run"));
},
sortByProperty(property) {
if (this.sortBy[0] === `${property}:desc`) {
this.set("sortBy", [`${property}:asc`]);
} else {
this.set("sortBy", [`${property}:desc`]);
}
},
create() {
const name = this.newQueryName.trim();
this.setProperties({
loading: true,
showCreate: false,
showRecentQueries: false,
});
this.store
.createRecord("query", { name })
.save()
.then((result) => this.addCreatedRecord(result.target))
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
},
discard() {
this.set("loading", true);
this.store
.find("query", this.get("selectedItem.id"))
.then((result) => {
const query = this.get("selectedItem");
query.setProperties(result.getProperties(Query.updatePropertyNames));
if (!query.group_ids || !Array.isArray(query.group_ids)) {
query.set("group_ids", []);
}
query.markNotDirty();
this.set("editing", false);
})
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
},
destroy() {
const query = this.selectedItem;
this.setProperties({ loading: true, showResults: false });
this.store
.destroyRecord("query", query)
.then(() => query.set("destroyed", true))
.catch(popupAjaxError)
.finally(() => this.set("loading", false));
},
recover() {
const query = this.selectedItem;
this.setProperties({ loading: true, showResults: true });
query
.save()
.then(() => query.set("destroyed", false))
.catch(popupAjaxError)
.finally(() => {
this.set("loading", false);
});
},
// This is necessary with glimmer's one way data stream to get the child's
// changes of 'params' to bubble up.
updateParams(identifier, value) {
this.selectedItem.set(`params.${identifier}`, value);
},
run() {
if (this.get("selectedItem.dirty")) {
return;
}
if (this.runDisabled) {
return;
}
this.setProperties({
loading: true,
showResults: false,
params: JSON.stringify(this.selectedItem.params),
});
ajax(
"/admin/plugins/explorer/queries/" +
this.get("selectedItem.id") +
"/run",
{
type: "POST",
data: {
params: JSON.stringify(this.get("selectedItem.params")),
explain: this.explain,
},
} }
) })
.then((result) => { .finally(() => {
this.set("results", result); this.loading = false;
if (!result.success) { this.dirty = true;
this.set("showResults", false); });
return; }
}
this.set("showResults", true); @action
}) displayCreate() {
.catch((err) => { this.showCreate = true;
this.set("showResults", false); }
if (err.jqXHR && err.jqXHR.status === 422 && err.jqXHR.responseJSON) {
this.set("results", err.jqXHR.responseJSON); @action
} else { editName() {
popupAjaxError(err); this.editingName = true;
} }
})
.finally(() => this.set("loading", false)); @action
}, editQuery() {
}, this.editingQuery = true;
}); }
@action
download() {
window.open(this.selectedItem.downloadUrl, "_blank");
}
@action
goHome() {
this.setProperties({
order: null,
showResults: false,
selectedQueryId: null,
params: null,
sortByProperty: "last_run_at",
sortDescending: true,
});
this.transitionToRoute({ queryParams: { id: null, params: null } });
}
@action
showHelpModal() {
showModal("query-help");
}
@action
resetParams() {
this.selectedItem.resetParams();
}
@action
saveDefaults() {
this.selectedItem.saveDefaults();
}
@action
updateSortProperty(property) {
if (this.sortByProperty === property) {
this.sortDescending = !this.sortDescending;
} else {
this.sortByProperty = property;
this.sortDescending = true;
}
}
@action
create() {
const name = this.newQueryName.trim();
this.setProperties({
loading: true,
showCreate: false,
});
this.store
.createRecord("query", { name })
.save()
.then((result) => this.addCreatedRecord(result.target))
.catch(popupAjaxError)
.finally(() => {
this.loading = false;
this.dirty = true;
});
}
@action
discard() {
this.loading = true;
this.store
.find("query", this.selectedItem.id)
.then((result) => {
this.selectedItem.setProperties(
result.getProperties(Query.updatePropertyNames)
);
if (
!this.selectedItem.group_ids ||
!Array.isArray(this.selectedItem.group_ids)
) {
this.selectedItem.set("group_ids", []);
}
this.dirty = false;
this.editingName = false;
this.editingQuery = false;
})
.catch(popupAjaxError)
.finally(() => (this.loading = false));
}
@action
destroyQuery() {
this.loading = true;
this.showResults = false;
this.store
.destroyRecord("query", this.selectedItem)
.then(() => this.selectedItem.set("destroyed", true))
.catch(popupAjaxError)
.finally(() => (this.loading = false));
}
@action
recover() {
this.loading = true;
this.showResults = true;
this.selectedItem
.save()
.then(() => this.selectedItem.set("destroyed", false))
.catch(popupAjaxError)
.finally(() => (this.loading = false));
}
@action
updateParams(identifier, value) {
this.selectedItem.set(`params.${identifier}`, value);
}
@action
updateSearch(value) {
this.search = value;
}
@action
updateNewQueryName(value) {
this.newQueryName = value;
}
@action
setDirty() {
this.dirty = true;
}
@action
exitEdit() {
this.editingName = false;
}
@action
run() {
if (this.dirty || this.runDisabled) {
return;
}
this.setProperties({
loading: true,
showResults: false,
params: JSON.stringify(this.selectedItem.params),
});
ajax("/admin/plugins/explorer/queries/" + this.selectedItem.id + "/run", {
type: "POST",
data: {
params: JSON.stringify(this.selectedItem.params),
explain: this.explain,
},
})
.then((result) => {
this.results = result;
if (!result.success) {
this.showResults = false;
return;
}
this.showResults = true;
})
.catch((err) => {
this.showResults = false;
if (err.jqXHR && err.jqXHR.status === 422 && err.jqXHR.responseJSON) {
this.results = err.jqXHR.responseJSON;
} else {
popupAjaxError(err);
}
})
.finally(() => (this.loading = false));
}
}

View File

@ -7,16 +7,9 @@ import RestModel from "discourse/models/rest";
import { reads } from "@ember/object/computed"; import { reads } from "@ember/object/computed";
const Query = RestModel.extend({ const Query = RestModel.extend({
dirty: false,
params: {}, params: {},
results: null, results: null,
hasParams: reads("param_info.length"),
@on("init")
_init() {
this._super(...arguments);
this.set("dirty", false);
},
@on("init") @on("init")
@observes("param_info") @observes("param_info")
@ -24,17 +17,6 @@ const Query = RestModel.extend({
this.resetParams(); this.resetParams();
}, },
@observes("name", "description", "sql", "group_ids")
markDirty() {
this.set("dirty", true);
},
markNotDirty() {
this.set("dirty", false);
},
hasParams: reads("param_info.length"),
resetParams() { resetParams() {
const newParams = {}; const newParams = {};
const oldParams = this.params; const oldParams = this.params;

View File

@ -25,7 +25,6 @@ export default DiscourseRoute.extend({
return schemaPromise.then((schema) => { return schemaPromise.then((schema) => {
return queryPromise.then((model) => { return queryPromise.then((model) => {
model.forEach((query) => { model.forEach((query) => {
query.markNotDirty();
query.set( query.set(
"group_names", "group_names",
(query.group_ids || []).map((id) => groupNames[id]) (query.group_ids || []).map((id) => groupNames[id])

View File

@ -1,368 +1,389 @@
{{#explorer-container hideSchema=hideSchema everEditing=everEditing}} {{#if this.disallow}}
{{#if disallow}} <h1>{{i18n "explorer.admins_only"}}</h1>
<h1>{{i18n "explorer.admins_only"}}</h1> {{else}}
{{else}} {{#unless this.validQueryPresent}}
{{#unless selectedQueryId}} <div class="query-list">
<div class="query-list"> <TextField
{{text-field value=search placeholderKey="explorer.search_placeholder"}} @value={{this.search}}
{{d-button @placeholderKey="explorer.search_placeholder"
action=(action "showCreate") @onChange={{this.updateSearch}}
icon="plus" />
class="no-text btn-right" <DButton
}} @action={{this.displayCreate}}
{{pick-files-button @icon="plus"
class="import-btn" @class="no-text btn-right"
label="explorer.import.label" />
icon="upload" <PickFilesButton
acceptedFormatsOverride=acceptedImportFileTypes @class="import-btn"
showButton=true @label="explorer.import.label"
onFilesPicked=(action "import") @icon="upload"
}} @acceptedFormatsOverride={{this.acceptedImportFileTypes}}
@showButton="true"
@onFilesPicked={{this.import}}
/>
</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 "exclamation-triangle"}}
{{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="times"
@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-alt"}}
</a>
{{/unless}}
</h1>
</div>
<div class="desc">
{{this.selectedItem.description}}
</div>
{{/if}}
{{#unless this.selectedItem.destroyed}}
<div class="pull-left">
<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>
</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
@content={{this.selectedItem.sql}}
@mode="sql"
@disabled={{this.selectedItem.destroyed}}
{{on "click" this.setDirty}}
/>
</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={{selectedItem.sql}}
@codeClass="sql"
@setDirty={{this.setDirty}}
/>
</div>
{{/if}}
<div class="clear"></div>
<div class="pull-left left-buttons">
{{#if this.editingQuery}}
<DButton
@action={{this.save}}
@label="explorer.save"
@disabled={{this.saveDisabled}}
/>
{{else}}
{{#unless this.editDisabled}}
<DButton
@action={{this.editQuery}}
@label="explorer.edit"
@icon="pencil-alt"
/>
{{/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="question-circle"
/>
{{/if}}
</div>
<div class="pull-right right-buttons">
{{#if this.selectedItem.destroyed}}
<DButton
@action={{this.recover}}
@icon="undo"
@label="explorer.recover"
/>
{{else}}
{{#if this.editingQuery}}
<DButton
@action={{this.discard}}
@icon="undo"
@label="explorer.undo"
@disabled={{this.saveDisabled}}
/>
{{/if}}
<DButton
@action={{this.destroyQuery}}
@class="btn-danger"
@icon="trash-alt"
@label="explorer.delete"
/>
{{/if}}
</div>
<div class="clear"></div>
{{/if}}
</div> </div>
{{#if showCreate}} <form class="query-run" {{on "submit" this.run}}>
<div class="query-create"> <ParamInputsWrapper
{{text-field @hasParams={{this.selectedItem.hasParams}}
value=newQueryName @params={{this.selectedItem.params}}
placeholderKey="explorer.create_placeholder" @initialValues={{this.parsedParams}}
}} @paramInfo={{this.selectedItem.param_info}}
{{d-button @updateParams={{this.updateParams}}
action=(action "create") />
disabled=createDisabled
label="explorer.create"
icon="plus"
}}
</div>
{{/if}}
{{#if othersDirty}} {{#if this.runDisabled}}
<div class="warning"> {{#if this.saveDisabled}}
{{d-icon "exclamation-triangle"}} <DButton
{{i18n "explorer.others_dirty"}} @label="explorer.run"
</div> @disabled="true"
{{/if}} @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}}
@class="btn-primary"
@type="submit"
/>
{{/if}}
<label class="query-plan">
<Input @type="checkbox" @checked={{this.explain}} name="explain" />
{{i18n "explorer.explain_label"}}
</label>
</form>
<hr />
{{/unless}} {{/unless}}
{{#if model.length}} <ConditionalLoadingSpinner @condition={{this.loading}} />
{{#unless selectedItem.fake}}
<div class="query-edit {{if editName 'editing'}}">
{{#if selectedItem}}
{{#if editing}}
<div class="name">
{{d-button
action=(action "goHome")
icon="chevron-left"
class="previous"
}}
<div class="name-text-field">
{{text-field value=selectedItem.name}}
</div>
</div>
<div class="desc"> {{#unless this.selectedItem.fake}}
{{textarea <QueryResultsWrapper
value=selectedItem.description @results={{results}}
placeholder=(i18n "explorer.description_placeholder") @showResults={{showResults}}
}} @query={{selectedItem}}
</div> @content={{results}}
{{else}} />
<div class="name"> {{/unless}}
{{d-button
action=(action "goHome")
icon="chevron-left"
class="previous"
}}
<h1> {{#unless this.validQueryPresent}}
{{selectedItem.name}} <div class="container">
{{#unless editDisabled}} <table class="recent-queries">
<a href {{action "editName" class="edit-query-name"}}> <thead class="heading-container">
{{d-icon "pencil-alt"}} <th class="col heading name">
<div
role="button"
class="heading-toggle"
{{on "click" (fn this.updateSortProperty "name")}}
>
<TableHeaderToggle
@field="name"
@labelKey="explorer.query_user"
@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="query-row">
<td>
<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="query-created-by">
{{#if query.username}}
<a href="/u/{{query.username}}/activity">
<medium>{{query.username}}</medium>
</a> </a>
{{/unless}} {{/if}}
</h1> </td>
</div> <td class="query-group-names">
{{#each query.group_names as |group|}}
<div class="desc"> <ShareReport @group={{group}} @query={{query}} />
{{selectedItem.description}} {{/each}}
</div> </td>
{{/if}} <td class="query-created-at">
{{#if query.last_run_at}}
{{#unless selectedItem.destroyed}} <medium>
<div class="pull-left"> {{bound-date query.last_run_at}}
<div class="groups"> </medium>
<span class="label">{{i18n "explorer.allow_groups"}}</span> {{else if query.created_at}}
<span> <medium>
{{multi-select {{bound-date query.created_at}}
value=selectedItem.group_ids </medium>
content=groupOptions {{/if}}
allowAny=false </td>
onSelect=(action (mut selectedItem.group_ids)) </tr>
}}
</span>
</div>
</div>
{{/unless}}
<div class="clear"></div>
{{! the SQL editor will show the first time you }}
{{#if everEditing}}
<div class="query-editor {{if hideSchema 'no-schema'}}">
<div class="panels-flex">
<div class="editor-panel">
{{ace-editor
content=selectedItem.sql
mode="sql"
disabled=selectedItem.destroyed
}}
</div>
<div class="right-panel">
<ExplorerSchema
@schema={{schema}}
@hideSchema={{hideSchema}}
@updateHideSchema={{action "updateHideSchema"}}
/>
</div>
</div>
<div class="grippie">
{{d-icon "discourse-expand"}}
</div>
<div class="clear"></div>
</div>
{{else}} {{else}}
<div class="sql"> <br />
{{hljs-code-view value=selectedItem.sql codeClass="sql"}} <em class="no-search-results">
</div> {{i18n "explorer.no_search_results"}}
{{/if}} </em>
{{/each}}
</tbody>
</table>
</div>
{{/unless}}
<div class="clear"></div> <div class="explorer-pad-bottom"></div>
<div class="pull-left left-buttons">
{{#if everEditing}}
{{d-button
action=(action "save")
label="explorer.save"
disabled=saveDisable
}}
{{else}}
{{#unless editDisabled}}
{{d-button
action=(action "editName")
label="explorer.edit"
icon="pencil-alt"
}}
{{/unless}}
{{/if}}
{{d-button
action=(action "download")
label="explorer.export"
disabled=runDisabled
icon="download"
}}
{{#if everEditing}}
{{d-button
action=(action "showHelpModal")
label="explorer.help.label"
icon="question-circle"
}}
{{/if}}
</div>
<div class="pull-right right-buttons">
{{#if selectedItem.destroyed}}
{{d-button
action=(action "recover")
class=""
icon="undo"
label="explorer.recover"
}}
{{else}}
{{#if everEditing}}
{{d-button
action=(action "discard")
icon="undo"
label="explorer.undo"
disabled=saveDisabled
}}
{{/if}}
{{d-button
action=(action "destroy")
class="btn-danger"
icon="trash-alt"
label="explorer.delete"
}}
{{/if}}
</div>
<div class="clear"></div>
{{/if}}
</div>
<form class="query-run" {{action "run" on="submit"}}>
<ParamInputsWrapper
@hasParams={{selectedItem.hasParams}}
@params={{selectedItem.params}}
@initialValues={{parsedParams}}
@paramInfo={{selectedItem.param_info}}
@updateParams={{action "updateParams"}}
/>
{{#if runDisabled}}
{{#if saveDisabled}}
{{d-button
label="explorer.run"
disabled="true"
class="btn-primary"
}}
{{else}}
{{d-button
action=(action "saverun")
icon="play"
label="explorer.saverun"
class="btn-primary"
}}
{{/if}}
{{else}}
{{d-button
action=(action "run")
icon="play"
label="explorer.run"
disabled=runDisabled
class="btn-primary"
type="submit"
}}
{{/if}}
<label class="query-plan">
{{input type="checkbox" checked=explain name="explain"}}
{{i18n "explorer.explain_label"}}
</label>
</form>
<hr />
{{/unless}}
{{conditional-loading-spinner condition=loading}}
{{#unless selectedItem.fake}}
<QueryResultsWrapper
@results={{results}}
@showResults={{showResults}}
@query={{selectedItem}}
@content={{results}}
/>
{{/unless}}
{{#if showRecentQueries}}
<div class="container">
<table class="recent-queries">
<thead class="heading-container">
<th class="col heading name">
<div
role="button"
class="heading-toggle"
{{action "sortByProperty" "name"}}
>
{{table-header-toggle
field="name"
labelKey="explorer.query_name"
order=order
asc=asc
automatic=true
}}
</div>
</th>
<th class="col heading created-by">
<div
role="button"
class="heading-toggle"
{{action "sortByProperty" "username"}}
>
{{table-header-toggle
field="username"
labelKey="explorer.query_user"
order=order
asc=asc
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"
{{action "sortByProperty" "last_run_at"}}
>
{{table-header-toggle
field="last_run_at"
labelKey="explorer.query_time"
order=order
asc=asc
automatic=true
}}
</div>
</th>
</thead>
<tbody>
{{#each filteredContent as |query|}}
<tr class="query-row">
<td>
<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="query-created-by">
{{#if query.username}}
<a href="/u/{{query.username}}/activity">
<medium>{{query.username}}</medium>
</a>
{{/if}}
</td>
<td class="query-group-names">
{{#each query.group_names as |group|}}
<ShareReport @group={{group}} @query={{query}} />
{{/each}}
</td>
<td class="query-created-at">
{{#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>
{{/if}}
<div class="explorer-pad-bottom"></div>
{{/if}}
{{/if}} {{/if}}
{{/explorer-container}} {{/if}}

View File

@ -1 +0,0 @@
<pre><code class={{codeClass}}>{{value}}</code></pre>

View File

@ -1,6 +1,6 @@
{{#d-modal-body title="explorer.help.modal_title"}} <DModalBody @title="explorer.help.modal_title">
{{html-safe (i18n "explorer.help.auto_resolution")}} {{html-safe (i18n "explorer.help.auto_resolution")}}
{{html-safe (i18n "explorer.help.custom_params")}} {{html-safe (i18n "explorer.help.custom_params")}}
{{html-safe (i18n "explorer.help.default_values")}} {{html-safe (i18n "explorer.help.default_values")}}
{{html-safe (i18n "explorer.help.data_types")}} {{html-safe (i18n "explorer.help.data_types")}}
{{/d-modal-body}} </DModalBody>

View File

@ -22,7 +22,7 @@ ar:
label: "استيراد" label: "استيراد"
modal: "استيراد استعلام" modal: "استيراد استعلام"
unparseable_json: "ملف JSON غير قابل للتحليل" unparseable_json: "ملف JSON غير قابل للتحليل"
wrong_json: "ملف JSON خاطئ. يجب أن يحتوي ملف JSON على كائن \"استعلام\"، والذي يجب أن يحتوي على الأقل على خاصية \"sql\"." wrong_json: 'ملف JSON خاطئ. يجب أن يحتوي ملف JSON على كائن "استعلام"، والذي يجب أن يحتوي على الأقل على خاصية "sql".'
help: help:
label: "المساعدة" label: "المساعدة"
modal_title: "مساعدة مستكشف البيانات" modal_title: "مساعدة مستكشف البيانات"

View File

@ -22,7 +22,7 @@ it:
label: "Importa" label: "Importa"
modal: "Importa una query" modal: "Importa una query"
unparseable_json: "File JSON non analizzabile." unparseable_json: "File JSON non analizzabile."
wrong_json: "File JSON errato. Un file JSON dovrebbe contenere un oggetto \"query\" contenente almeno una proprietà \"sql\"." wrong_json: 'File JSON errato. Un file JSON dovrebbe contenere un oggetto "query" contenente almeno una proprietà "sql".'
help: help:
label: "Guida" label: "Guida"
modal_title: "Guida di Data Explorer" modal_title: "Guida di Data Explorer"

View File

@ -22,7 +22,7 @@ pt_BR:
label: "Importar" label: "Importar"
modal: "Importar Uma Consulta" modal: "Importar Uma Consulta"
unparseable_json: "Arquivo JSON não analisável" unparseable_json: "Arquivo JSON não analisável"
wrong_json: "Arquivo JSON incorreto. Um arquivo JSON deve conter um objeto \"consulta\", que deve ter pelo menos a propriedade \"sql\"" wrong_json: 'Arquivo JSON incorreto. Um arquivo JSON deve conter um objeto "consulta", que deve ter pelo menos a propriedade "sql"'
help: help:
label: "Ajuda" label: "Ajuda"
modal_title: "Ajuda do Explorador de Dados" modal_title: "Ajuda do Explorador de Dados"

View File

@ -6,4 +6,4 @@
hr: hr:
site_settings: site_settings:
data_explorer_enabled: "Uključi \"Data Explorer\" na /admin/plugins/explorer" data_explorer_enabled: 'Uključi "Data Explorer" na /admin/plugins/explorer'

View File

@ -6,4 +6,4 @@
pt: pt:
site_settings: site_settings:
data_explorer_enabled: "Ativar o «Explorador de Dados» em \"/admin/plugins/explorer\"" data_explorer_enabled: 'Ativar o «Explorador de Dados» em "/admin/plugins/explorer"'

View File

@ -6,4 +6,4 @@
sk: sk:
site_settings: site_settings:
data_explorer_enabled: "Povoľ \"Data explorer\" v /admin/plugins/explorer" data_explorer_enabled: 'Povoľ "Data explorer" v /admin/plugins/explorer'

View File

@ -1,7 +1,7 @@
# Configuration file for discourse-translator-bot # Configuration file for discourse-translator-bot
files: files:
- source_path: config/locales/client.en.yml - source_path: config/locales/client.en.yml
destination_path: client.yml destination_path: client.yml
- source_path: config/locales/server.en.yml - source_path: config/locales/server.en.yml
destination_path: server.yml destination_path: server.yml