1
0
Fork 0

Apply new table style to wizard logs view

Dieser Commit ist enthalten in:
angusmcleod 2021-09-09 14:07:12 +08:00
Ursprung a8e81150f1
Commit 7b57e7fcab
26 geänderte Dateien mit 599 neuen und 378 gelöschten Zeilen

Datei anzeigen

@ -1,10 +1,11 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { equal } from "@ember/object/computed"; import { equal, notEmpty } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n"; import I18n from "I18n";
export default Component.extend({ export default Component.extend({
classNameBindings: ["value.type"],
isText: equal("value.type", "text"), isText: equal("value.type", "text"),
isComposer: equal("value.type", "composer"), isComposer: equal("value.type", "composer"),
isDate: equal("value.type", "date"), isDate: equal("value.type", "date"),
@ -18,13 +19,29 @@ export default Component.extend({
isTag: equal("value.type", "tag"), isTag: equal("value.type", "tag"),
isCategory: equal("value.type", "category"), isCategory: equal("value.type", "category"),
isGroup: equal("value.type", "group"), isGroup: equal("value.type", "group"),
isUser: equal("fieldName", "username"),
isUserSelector: equal("value.type", "user_selector"), isUserSelector: equal("value.type", "user_selector"),
isSubmittedAt: equal("fieldName", "submitted_at"), isSubmittedAt: equal("field", "submitted_at"),
isTextArea: equal("value.type", "textarea"),
isComposerPreview: equal("value.type", "composer_preview"), isComposerPreview: equal("value.type", "composer_preview"),
textState: "text-collapsed", textState: "text-collapsed",
toggleText: I18n.t("admin.wizard.submissions.expand_text"), toggleText: I18n.t("admin.wizard.expand_text"),
@discourseComputed("value", "isUser")
hasValue(value, isUser) {
if (isUser) {
return value;
}
return value && value.value;
},
@discourseComputed("field", "value.type")
isUser(field, type) {
return field === "username" || field === "user" || type === "user";
},
@discourseComputed("value.type")
isLongtext(type) {
return type === "textarea" || type === "long_text";
},
@discourseComputed("value") @discourseComputed("value")
checkboxValue(value) { checkboxValue(value) {
@ -44,10 +61,10 @@ export default Component.extend({
if (state === "text-collapsed") { if (state === "text-collapsed") {
this.set("textState", "text-expanded"); this.set("textState", "text-expanded");
this.set("toggleText", I18n.t("admin.wizard.submissions.collapse_text")); this.set("toggleText", I18n.t("admin.wizard.collapse_text"));
} else if (state === "text-expanded") { } else if (state === "text-expanded") {
this.set("textState", "text-collapsed"); this.set("textState", "text-collapsed");
this.set("toggleText", I18n.t("admin.wizard.submissions.expand_text")); this.set("toggleText", I18n.t("admin.wizard.expand_text"));
} }
}, },
@ -83,19 +100,24 @@ export default Component.extend({
return users; return users;
}, },
@discourseComputed("value") @discourseComputed("isUser", "field", "value")
userProfileUrl(value) { username(isUser, field, value) {
const isUser = this.get("isUser"); if (isUser) {return value.username;}
if (field === "username") {return value.value;}
return null;
},
if (isUser) { showUsername: notEmpty("username"),
return `/u/${value.username}`;
} @discourseComputed("username")
userProfileUrl(username) {
if (username) {return `/u/${username}`;}
return "/";
}, },
@discourseComputed("value") @discourseComputed("value")
categoryUrl(value) { categoryUrl(value) {
const isCategory = this.get("isCategory"); const isCategory = this.get("isCategory");
if (isCategory) { if (isCategory) {
return `/c/${value.value}`; return `/c/${value.value}`;
} }
@ -104,7 +126,6 @@ export default Component.extend({
@discourseComputed("value") @discourseComputed("value")
groupUrl(value) { groupUrl(value) {
const isGroup = this.get("isGroup"); const isGroup = this.get("isGroup");
if (isGroup) { if (isGroup) {
return `/g/${value.value}`; return `/g/${value.value}`;
} }

Datei anzeigen

@ -6,10 +6,9 @@ export default Controller.extend(ModalFunctionality, {
save() { save() {
this.send("closeModal"); this.send("closeModal");
}, },
resetToDefault() { resetToDefault() {
this.get("model.fields").forEach((field) => { this.get("model.reset")();
field.set("enabled", true);
});
}, },
}, },
}); });

Datei anzeigen

@ -0,0 +1,52 @@
import discourseComputed from "discourse-common/utils/decorators";
import { notEmpty } from "@ember/object/computed";
import CustomWizardLogs from "../models/custom-wizard-logs";
import Controller from "@ember/controller";
export default Controller.extend({
refreshing: false,
hasLogs: notEmpty("logs"),
page: 0,
canLoadMore: true,
logs: [],
messageKey: "viewing",
loadLogs() {
if (!this.canLoadMore) {
return;
}
const page = this.get("page");
const wizardId = this.get("wizard.id");
this.set("refreshing", true);
CustomWizardLogs.list(wizardId, page)
.then((result) => {
this.set("logs", this.logs.concat(result.logs));
})
.finally(() => this.set("refreshing", false));
},
@discourseComputed("hasLogs", "refreshing")
noResults(hasLogs, refreshing) {
return !hasLogs && !refreshing;
},
actions: {
loadMore() {
if (!this.loadingMore && this.logs.length < this.total) {
this.set("page", (this.page += 1));
this.loadLogs();
}
},
refresh() {
this.setProperties({
canLoadMore: true,
page: 0,
logs: [],
});
this.loadLogs();
},
},
});

Datei anzeigen

@ -1,52 +1,34 @@
import discourseComputed from "discourse-common/utils/decorators";
import { notEmpty } from "@ember/object/computed";
import CustomWizardLogs from "../models/custom-wizard-logs";
import Controller from "@ember/controller"; import Controller from "@ember/controller";
import { default as discourseComputed } from "discourse-common/utils/decorators";
export default Controller.extend({ export default Controller.extend({
refreshing: false,
hasLogs: notEmpty("logs"),
page: 0,
canLoadMore: true,
logs: [],
documentationUrl: "https://thepavilion.io/t/2818", documentationUrl: "https://thepavilion.io/t/2818",
messageKey: "viewing",
loadLogs() { @discourseComputed("wizardId")
if (!this.canLoadMore) { wizardName(wizardId) {
return; let currentWizard = this.wizardList.find(
(wizard) => wizard.id === wizardId
);
if (currentWizard) {
return currentWizard.name;
}
},
@discourseComputed("wizardName")
messageOpts(wizardName) {
return {
wizardName,
};
},
@discourseComputed("wizardId")
messageKey(wizardId) {
let key = "select";
if (wizardId) {
key = "viewing";
} }
this.set("refreshing", true); return key;
CustomWizardLogs.list()
.then((result) => {
if (!result || result.length === 0) {
this.set("canLoadMore", false);
}
this.set("logs", this.logs.concat(result));
})
.finally(() => this.set("refreshing", false));
},
@discourseComputed("hasLogs", "refreshing")
noResults(hasLogs, refreshing) {
return !hasLogs && !refreshing;
},
actions: {
loadMore() {
this.set("page", (this.page += 1));
this.loadLogs();
},
refresh() {
this.setProperties({
canLoadMore: true,
page: 0,
logs: [],
});
this.loadLogs();
},
}, },
}); });

Datei anzeigen

@ -54,10 +54,14 @@ export default Controller.extend({
}, },
showEditColumnsModal() { showEditColumnsModal() {
return showModal("admin-wizards-submissions-columns", { return showModal("admin-wizards-columns", {
model: { model: {
fields: this.get("fields"), columns: this.get("fields"),
submissions: this.get("submissions"), reset: () => {
this.get("fields").forEach((field) => {
field.set("enabled", true);
});
},
}, },
}); });
}, },

Datei anzeigen

@ -43,10 +43,16 @@ export default {
} }
); );
this.route("adminWizardsLogs", { this.route(
path: "/logs", "adminWizardsLogs",
resetNamespace: true, { path: "/logs", resetNamespace: true },
}); function () {
this.route("adminWizardsLogsShow", {
path: "/:wizardId/",
resetNamespace: true,
});
}
);
this.route("adminWizardsManager", { this.route("adminWizardsManager", {
path: "/manager", path: "/manager",

Datei anzeigen

@ -3,14 +3,48 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
const CustomWizardLogs = EmberObject.extend(); const CustomWizardLogs = EmberObject.extend();
const logItemTypes = {
date: "date_time",
action: "text",
message: "long_text",
user: "user",
username: "text",
};
function logItem(item, attr) {
return {
value: item[attr],
type: logItemTypes[attr],
};
}
CustomWizardLogs.reopenClass({ CustomWizardLogs.reopenClass({
list(page = 0) { list(wizardId, page = 0) {
return ajax("/admin/wizards/logs", { let data = {
data: { page,
page, };
},
}).catch(popupAjaxError); return ajax(`/admin/wizards/logs/${wizardId}`, { data })
.catch(popupAjaxError)
.then((result) => {
if (result.logs) {
result.logs = result.logs.map((item) => {
let map = {};
if (item.date) {map.date = logItem(item, "date");}
if (item.action) {map.action = logItem(item, "action");}
if (item.user) {
map.user = item.user;
} else {
map.user = logItem(item, "username");
}
if (item.message) {map.message = logItem(item, "message");}
return map;
});
}
return result;
});
}, },
}); });

Datei anzeigen

@ -0,0 +1,17 @@
import CustomWizardLogs from "../models/custom-wizard-logs";
import DiscourseRoute from "discourse/routes/discourse";
import { A } from "@ember/array";
export default DiscourseRoute.extend({
model(params) {
return CustomWizardLogs.list(params.wizardId);
},
setupController(controller, model) {
controller.setProperties({
wizard: model.wizard,
logs: A(model.logs),
total: model.total,
});
},
});

Datei anzeigen

@ -1,12 +1,24 @@
import CustomWizardLogs from "../models/custom-wizard-logs";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import { ajax } from "discourse/lib/ajax";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
model() { model() {
return CustomWizardLogs.list(); return ajax(`/admin/wizards/wizard`);
}, },
setupController(controller, model) { setupController(controller, model) {
controller.set("logs", model); const showParams = this.paramsFor("adminWizardsLogsShow");
controller.setProperties({
wizardId: showParams.wizardId,
wizardList: model.wizard_list,
});
},
actions: {
changeWizard(wizardId) {
this.controllerFor("adminWizardsLogs").set("wizardId", wizardId);
this.transitionTo("adminWizardsLogsShow", wizardId);
},
}, },
}); });

Datei anzeigen

@ -0,0 +1,45 @@
{{#if logs}}
<div class="wizard-header large">
<label>
{{i18n "admin.wizard.log.title" name=wizard.name}}
</label>
<div class="controls">
{{d-button
label="refresh"
icon="sync"
action="refresh"
class="refresh"}}
</div>
</div>
<div class="wizard-table">
{{#load-more selector=".wizard-table tr" action=(action "loadMore")}}
{{#if noResults}}
<p>{{i18n "search.no_results"}}</p>
{{else}}
<table>
<thead>
<tr>
<th class="date">{{i18n "admin.wizard.log.date"}}</th>
<th>{{i18n "admin.wizard.log.action"}}</th>
<th>{{i18n "admin.wizard.log.user"}}</th>
<th>{{i18n "admin.wizard.log.message"}}</th>
</tr>
</thead>
<tbody>
{{#each logs as |log|}}
<tr>
{{#each-in log as |field value|}}
<td class="small">{{wizard-table-field field=field value=value}}</td>
{{/each-in}}
</tr>
{{/each}}
</tbody>
</table>
{{/if}}
{{conditional-loading-spinner condition=refreshing}}
{{/load-more}}
</div>
{{/if}}

Datei anzeigen

@ -1,11 +1,11 @@
<div class="admin-wizard-controls"> <div class="admin-wizard-select admin-wizard-controls">
<h3>{{i18n "admin.wizard.log.nav_label"}}</h3> {{combo-box
value=wizardId
{{d-button content=wizardList
label="refresh" onChange=(route-action "changeWizard")
icon="sync" options=(hash
action="refresh" none="admin.wizard.select"
class="refresh"}} )}}
</div> </div>
{{wizard-message {{wizard-message
@ -14,27 +14,6 @@
url=documentationUrl url=documentationUrl
component="logs"}} component="logs"}}
{{#load-more selector=".log-list tr" action=(action "loadMore") class="wizard-logs"}} <div class="admin-wizard-container">
{{#if noResults}} {{outlet}}
<p>{{i18n "search.no_results"}}</p> </div>
{{else}}
<table class="table grid">
<thead>
<tr>
<th>Message</th>
<th class="date">Date</th>
</tr>
</thead>
<tbody>
{{#each logs as |log|}}
<tr>
<td>{{log.message}}</td>
<td class="date">{{bound-date log.date}}</td>
</tr>
{{/each}}
</tbody>
</table>
{{/if}}
{{conditional-loading-spinner condition=refreshing}}
{{/load-more}}

Datei anzeigen

@ -7,7 +7,7 @@
<div class="controls"> <div class="controls">
{{d-button {{d-button
icon="sliders-h" icon="sliders-h"
label="admin.wizard.submissions.edit_columns" label="admin.wizard.edit_columns"
action=(action "showEditColumnsModal") action=(action "showEditColumnsModal")
class="btn-default open-edit-columns-btn download-link" class="btn-default open-edit-columns-btn download-link"
}} }}
@ -26,12 +26,10 @@
</a> </a>
</div> </div>
<div class="wizard-submissions"> <div class="wizard-table">
{{#load-more selector=".wizard-submissions tr" action=(action "loadMore")}} {{#load-more selector=".wizard-table tr" action=(action "loadMore")}}
{{#if noResults}} {{#if noResults}}
<p> <p>{{i18n "search.no_results"}}</p>
{{i18n "search.no_results"}}
</p>
{{else}} {{else}}
<table> <table>
<thead> <thead>
@ -49,9 +47,7 @@
{{#each displaySubmissions as |submission|}} {{#each displaySubmissions as |submission|}}
<tr> <tr>
{{#each-in submission as |field value|}} {{#each-in submission as |field value|}}
<td> <td>{{wizard-table-field field=field value=value}}</td>
{{submission-field fieldName=field value=value}}
</td>
{{/each-in}} {{/each-in}}
</tr> </tr>
{{/each}} {{/each}}

Datei anzeigen

@ -1,163 +0,0 @@
{{#if isText}}
{{value.value}}
{{/if}}
{{#if isTextArea}}
<div class="submission-long-text">
<p class="submission-long-text-content {{textState}}">
{{value.value}}
</p>
<a href {{action "expandText"}}>
{{toggleText}}
</a>
</div>
{{/if}}
{{#if isComposer}}
<div class="submission-long-text">
<p
class="submission-composer-text submission-long-text-content {{
textState
}}"
>
{{value.value}}
</p>
<a href {{action "expandText"}}>
{{toggleText}}
</a>
</div>
{{/if}}
{{#if isComposerPreview}}
{{d-icon "comment-alt"}}
<span class="submission-composer-text">
{{i18n "admin.wizard.submissions.composer_preview"}}: {{value.value}}
</span>
{{/if}}
{{#if isTextOnly}}
{{value.value}}
{{/if}}
{{#if isDate}}
<span class="submission-icon-item">
{{d-icon "calendar"}}{{value.value}}
</span>
{{/if}}
{{#if isTime}}
<span class="submission-icon-item">
{{d-icon "clock"}}{{value.value}}
</span>
{{/if}}
{{#if isDateTime}}
<span class="submission-icon-item" title={{value.value}}>
{{d-icon "calendar"}}{{format-date value.value format="medium"}}
</span>
{{/if}}
{{#if isNumber}}
{{value.value}}
{{/if}}
{{#if isCheckbox}}
{{#if checkboxValue}}
<span class="submission-icon-item submission-checkbox-true">
{{d-icon "check"}}{{value.value}}
</span>
{{else}}
<span class="submission-icon-item submission-checkbox-false">
{{d-icon "times"}}{{value.value}}
</span>
{{/if}}
{{/if}}
{{#if isUrl}}
<span class="submission-icon-item submission-url">
{{d-icon "link"}}
<a target="_blank" rel="noopener noreferrer" href={{value.value}}>
{{value.value}}
</a>
</span>
{{/if}}
{{#if isUpload}}
<a
target="_blank"
rel="noopener noreferrer"
class="attachment"
href={{file.url}}
download
>
{{file.original_filename}}
</a>
{{/if}}
{{#if isDropdown}}
<span class="submission-icon-item">
{{d-icon "check-square"}}
{{value.value}}
</span>
{{/if}}
{{#if isTag}}
{{#each value.value as |tag|}}
{{discourse-tag tag}}
{{/each}}
{{/if}}
{{#if isCategory}}
<strong>
{{i18n "admin.wizard.submissions.category_id"}}:
</strong>
<a
target="_blank"
rel="noopener noreferrer"
href={{categoryUrl}}
title={{value.value}}
>
{{value.value}}
</a>
{{/if}}
{{#if isGroup}}
<strong>
{{i18n "admin.wizard.submissions.group_id"}}:
</strong>
{{value.value}}
{{/if}}
{{#if isUserSelector}}
{{#each submittedUsers as |user|}}
{{d-icon "user"}}
<a
target="_blank"
rel="noopener noreferrer"
href={{user.url}}
title={{user.username}}
>
{{user.username}}
</a>
{{/each}}
{{/if}}
{{#if isUser}}
{{#link-to "user" value}}
{{avatar value imageSize="tiny"}}
{{/link-to}}
<a
target="_blank"
rel="noopener noreferrer"
href={{userProfileUrl}}
title={{value.name}}
>
{{value.username}}
</a>
{{/if}}
{{#if isSubmittedAt}}
<span class="submission-date" title={{value.value}}>
{{d-icon "clock"}}{{format-date value format="tiny"}}
</span>
{{/if}}

Datei anzeigen

@ -0,0 +1,161 @@
{{#if hasValue}}
{{#if isText}}
{{value.value}}
{{/if}}
{{#if isLongtext}}
<div class="wizard-table-long-text">
<p class="wizard-table-long-text-content {{textState}}">
{{value.value}}
</p>
<a href {{action "expandText"}}>
{{toggleText}}
</a>
</div>
{{/if}}
{{#if isComposer}}
<div class="wizard-table-long-text">
<p class="wizard-table-composer-text wizard-table-long-text-content {{textState}}">
{{value.value}}
</p>
<a href {{action "expandText"}}>
{{toggleText}}
</a>
</div>
{{/if}}
{{#if isComposerPreview}}
{{d-icon "comment-alt"}}
<span class="wizard-table-composer-text">
{{i18n "admin.wizard.submissions.composer_preview"}}: {{value.value}}
</span>
{{/if}}
{{#if isTextOnly}}
{{value.value}}
{{/if}}
{{#if isDate}}
<span class="wizard-table-icon-item">
{{d-icon "calendar"}}{{value.value}}
</span>
{{/if}}
{{#if isTime}}
<span class="wizard-table-icon-item">
{{d-icon "clock"}}{{value.value}}
</span>
{{/if}}
{{#if isDateTime}}
<span class="wizard-table-icon-item" title={{value.value}}>
{{d-icon "calendar"}}{{format-date value.value format="medium"}}
</span>
{{/if}}
{{#if isNumber}}
{{value.value}}
{{/if}}
{{#if isCheckbox}}
{{#if checkboxValue}}
<span class="wizard-table-icon-item checkbox-true">
{{d-icon "check"}}{{value.value}}
</span>
{{else}}
<span class="wizard-table-icon-item checkbox-false">
{{d-icon "times"}}{{value.value}}
</span>
{{/if}}
{{/if}}
{{#if isUrl}}
<span class="wizard-table-icon-item url">
{{d-icon "link"}}
<a target="_blank" rel="noopener noreferrer" href={{value.value}}>
{{value.value}}
</a>
</span>
{{/if}}
{{#if isUpload}}
<a
target="_blank"
rel="noopener noreferrer"
class="attachment"
href={{file.url}}
download
>
{{file.original_filename}}
</a>
{{/if}}
{{#if isDropdown}}
<span class="wizard-table-icon-item">
{{d-icon "check-square"}}
{{value.value}}
</span>
{{/if}}
{{#if isTag}}
{{#each value.value as |tag|}}
{{discourse-tag tag}}
{{/each}}
{{/if}}
{{#if isCategory}}
<strong>
{{i18n "admin.wizard.submissions.category_id"}}:
</strong>
<a
target="_blank"
rel="noopener noreferrer"
href={{categoryUrl}}
title={{value.value}}
>
{{value.value}}
</a>
{{/if}}
{{#if isGroup}}
<strong>
{{i18n "admin.wizard.submissions.group_id"}}:
</strong>
{{value.value}}
{{/if}}
{{#if isUserSelector}}
{{#each submittedUsers as |user|}}
{{d-icon "user"}}
<a
target="_blank"
rel="noopener noreferrer"
href={{user.url}}
title={{user.username}}
>
{{user.username}}
</a>
{{/each}}
{{/if}}
{{#if isUser}}
{{#link-to "user" value}}
{{avatar value imageSize="tiny"}}
{{/link-to}}
{{/if}}
{{#if showUsername}}
<a target="_blank" rel="noopener noreferrer" href={{userProfileUrl}} title={{username}}>
{{username}}
</a>
{{/if}}
{{#if isSubmittedAt}}
<span class="date" title={{value.value}}>
{{d-icon "clock"}}{{format-date value format="tiny"}}
</span>
{{/if}}
{{else}}
&mdash;
{{/if}}

Datei anzeigen

@ -1,14 +1,14 @@
{{#d-modal-body title="directory.edit_columns.title"}} {{#d-modal-body title="admin.wizard.edit_columns"}}
{{#if loading}} {{#if loading}}
{{loading-spinner size="large"}} {{loading-spinner size="large"}}
{{else}} {{else}}
<div class="edit-directory-columns-container"> <div class="edit-directory-columns-container">
{{#each model.fields as |field|}} {{#each model.columns as |column|}}
<div class="edit-directory-column"> <div class="edit-directory-column">
<div class="left-content"> <div class="left-content">
<label class="column-name"> <label class="column-name">
{{input type="checkbox" checked=field.enabled}} {{input type="checkbox" checked=column.enabled}}
{{directory-table-header-title field=field.label translated=true}} {{directory-table-header-title field=column.label translated=true}}
</label> </label>
</div> </div>
</div> </div>

Datei anzeigen

@ -66,10 +66,10 @@
} }
} }
.wizard-submissions { .wizard-table {
overflow: scroll; overflow: scroll;
table td { table td:not(.small) {
min-width: 150px; min-width: 150px;
} }
@ -77,25 +77,26 @@
text-transform: capitalize; text-transform: capitalize;
} }
.submission-icon-item { .wizard-table-icon-item {
display: flex; display: flex;
align-items: center; align-items: center;
svg { svg {
margin-right: 5px; margin-right: 5px;
} }
} }
.submission-checkbox-true { .wizard-table-checkbox-true {
text-transform: capitalize; text-transform: capitalize;
color: var(--success); color: var(--success);
} }
.submission-checkbox-false { .wizard-table-checkbox-false {
text-transform: capitalize; text-transform: capitalize;
color: var(--danger); color: var(--danger);
} }
.submission-long-text { .wizard-table-long-text {
&-content { &-content {
white-space: nowrap; white-space: nowrap;
word-wrap: break-word; word-wrap: break-word;
@ -114,25 +115,11 @@
} }
} }
.submission-composer-text { .wizard-table-composer-text {
font-family: monospace; font-family: monospace;
} }
} }
.admin-wizards-logs {
.admin-wizard-controls {
h3 {
margin: 0 7px;
}
}
.wizard-logs {
.date {
width: 100px;
}
}
}
.wizard-settings-parent { .wizard-settings-parent {
padding: 20px; padding: 20px;
border: 1px solid var(--primary-low); border: 1px solid var(--primary-low);
@ -215,6 +202,10 @@
margin-bottom: 0; margin-bottom: 0;
} }
button {
font-size: 1rem;
}
.download-link { .download-link {
font-size: 1rem; font-size: 1rem;
line-height: 20px; line-height: 20px;
@ -230,10 +221,6 @@
font-size: 1rem; font-size: 1rem;
background-color: var(--primary-low); background-color: var(--primary-low);
} }
button {
font-size: 1rem;
}
} }
} }

Datei anzeigen

@ -58,6 +58,9 @@ en:
select_type: "Select a type" select_type: "Select a type"
condition: "Condition" condition: "Condition"
index: "Index" index: "Index"
edit_columns: "Edit Columns"
expand_text: "Read More"
collapse_text: "Show Less"
pro_support_button: pro_support_button:
title: "Request Pro Support" title: "Request Pro Support"
@ -108,6 +111,7 @@ en:
viewing: "You're viewing the submissions of the %{wizardName}" viewing: "You're viewing the submissions of the %{wizardName}"
documentation: "Check out the submissions documentation" documentation: "Check out the submissions documentation"
logs: logs:
select: "Select a wizard to see its logs"
viewing: "View recent logs for wizards on the forum" viewing: "View recent logs for wizards on the forum"
documentation: "Check out the logs documentation" documentation: "Check out the logs documentation"
@ -378,9 +382,6 @@ en:
nav_label: "Submissions" nav_label: "Submissions"
title: "{{name}} Submissions" title: "{{name}} Submissions"
download: "Download" download: "Download"
edit_columns: "Edit Columns"
expand_text: "Read More"
collapse_text: "Show Less"
group_id: "Group ID" group_id: "Group ID"
category_id: "Category ID" category_id: "Category ID"
composer_preview: "Composer Preview" composer_preview: "Composer Preview"
@ -437,9 +438,14 @@ en:
log: log:
label: "Logs" label: "Logs"
log: log:
nav_label: "Logs" nav_label: "Logs"
title: "{{name}} Logs"
date: Date
action: Action
user: User
message: Message
manager: manager:
nav_label: Manager nav_label: Manager

Datei anzeigen

@ -38,6 +38,7 @@ Discourse::Application.routes.append do
get 'admin/wizards/api/:name/authorize' => 'admin_api#authorize' get 'admin/wizards/api/:name/authorize' => 'admin_api#authorize'
get 'admin/wizards/logs' => 'admin_logs#index' get 'admin/wizards/logs' => 'admin_logs#index'
get 'admin/wizards/logs/:wizard_id' => 'admin_logs#show'
get 'admin/wizards/manager' => 'admin_manager#index' get 'admin/wizards/manager' => 'admin_manager#index'
get 'admin/wizards/manager/export' => 'admin_manager#export' get 'admin/wizards/manager/export' => 'admin_manager#export'

Datei anzeigen

@ -1,9 +1,44 @@
# frozen_string_literal: true # frozen_string_literal: true
class CustomWizard::AdminLogsController < CustomWizard::AdminController class CustomWizard::AdminLogsController < CustomWizard::AdminController
before_action :find_wizard, except: [:index]
def index def index
render_serialized( render json: ActiveModel::ArraySerializer.new(
CustomWizard::Log.list(params[:page].to_i, params[:limit].to_i), CustomWizard::Wizard.list(current_user),
CustomWizard::LogSerializer each_serializer: CustomWizard::BasicWizardSerializer
) )
end end
def show
render_json_dump(
wizard: CustomWizard::BasicWizardSerializer.new(@wizard, root: false),
logs: ActiveModel::ArraySerializer.new(
log_list.logs,
each_serializer: CustomWizard::LogSerializer
),
total: log_list.total
)
end
protected
def log_list
@log_list ||= begin
list = CustomWizard::Log.list(params[:page].to_i, params[:limit].to_i, params[:wizard_id])
if list.logs.any? && (usernames = list.logs.map(&:username)).present?
user_map = User.where(username: usernames)
.reduce({}) do |result, user|
result[user.username] = user
result
end
list.logs.each do |log_item|
log_item.user = user_map[log_item.username]
end
end
list
end
end
end end

Datei anzeigen

@ -1,5 +1,5 @@
{ {
"result": { "result": {
"line": 91.96 "line": 92.14
} }
} }

Datei anzeigen

@ -1,51 +1,56 @@
# frozen_string_literal: true # frozen_string_literal: true
class SplitCustomWizardLogFields < ActiveRecord::Migration[6.1] class SplitCustomWizardLogFields < ActiveRecord::Migration[6.1]
KEY_MAP = {
wizard: "wizard_id",
action: "action",
user: "username",
date: "date",
message: "message"
}
def change def change
reversible do |dir| reversible do |dir|
dir.up do dir.up do
# separate wizard/action/user into their own keys # separate wizard/action/user into their own keys
wizard_logs = PluginStoreRow.where(" wizard_logs = PluginStoreRow.where("plugin_name = 'custom_wizard_log'")
plugin_name = 'custom_wizard_log'
")
if wizard_logs.exists? if wizard_logs.exists?
wizard_logs.each do |row| wizard_logs.each do |row|
begin begin
log_json = JSON.parse(row.value) log_json = JSON.parse(row.value)
rescue TypeError, JSON::ParserError rescue TypeError, JSON::ParserError
next next
end end
if log_json.key?('message') && log_json['message'].is_a?(String) if log_json.key?('message') && log_json['message'].is_a?(String)
attr_strs = [] attr_strs = []
# assumes no whitespace in the values # assumes no whitespace in the values
attr_strs << log_json['message'].slice!(/(wizard: \S*; )/, 1) attr_strs << log_json['message'].slice!(/(wizard: \S*; )/, 1)
attr_strs << log_json['message'].slice!(/(action: \S*; )/, 1) attr_strs << log_json['message'].slice!(/(action: \S*; )/, 1)
attr_strs << log_json['message'].slice!(/(user: \S*; )/, 1) attr_strs << log_json['message'].slice!(/(user: \S*; )/, 1)
attr_strs.each do |attr_str| attr_strs.each do |attr_str|
if attr_str.is_a? String if attr_str.is_a? String
attr_str.gsub!(/[;]/ , "") attr_str.gsub!(/[;]/ , "")
key, value = attr_str.split(': ') key, value = attr_str.split(': ')
value.strip! if value value.strip! if value
log_json[key] = value ? value : '' key = KEY_MAP[key.to_sym] ? KEY_MAP[key.to_sym] : key
end log_json[key] = value ? value : ''
end end
row.value = log_json.to_json
row.save
end end
row.value = log_json.to_json
row.save
end end
end end
end
end end
dir.down do dir.down do
wizard_logs = PluginStoreRow.where(" wizard_logs = PluginStoreRow.where("plugin_name = 'custom_wizard_log'")
plugin_name = 'custom_wizard_log'
")
if wizard_logs.exists? if wizard_logs.exists?
wizard_logs.each do |row| wizard_logs.each do |row|
@ -56,19 +61,26 @@ class SplitCustomWizardLogFields < ActiveRecord::Migration[6.1]
end end
# concatenate wizard/action/user to start of message # concatenate wizard/action/user to start of message
prefixes = log_json.extract!('wizard', 'action', 'user') prefixes = log_json.extract!('wizard_id', 'action', 'username')
message_prefix = ""
message_prefix = prefixes.map { |k, v| "#{k}: #{v}" }.join('; ') if prefixes.present?
message_prefix = prefixes.map do |k, v|
key = KEY_MAP.key(k) ? KEY_MAP.key(k) : k
"#{key.to_s}: #{v};"
end.join(' ')
end
if log_json.key?('message') if log_json.key?('message')
log_json['message'] = "#{message_prefix}; #{log_json['message']}" message = log_json['message']
message = "#{message_prefix} #{message}" if message_prefix.present?
log_json['message'] = message
else else
log_json['message'] = message_prefix log_json['message'] = message_prefix
end end
row.value = log_json.to_json row.value = log_json.to_json
row.save row.save
end end
end end
end end

Datei anzeigen

@ -2,46 +2,51 @@
class CustomWizard::Log class CustomWizard::Log
include ActiveModel::Serialization include ActiveModel::Serialization
attr_accessor :date, :wizard, :action, :user, :message attr_reader :date, :wizard_id, :action, :username, :message
attr_accessor :user
PAGE_LIMIT = 100 PAGE_LIMIT = 100
def initialize(attrs) def initialize(attrs)
@date = attrs['date'] @date = attrs['date']
@wizard = attrs['wizard']
@action = attrs['action'] @action = attrs['action']
@user = attrs['user']
@message = attrs['message'] @message = attrs['message']
@wizard_id = attrs['wizard_id']
@username = attrs['username']
end end
def self.create(wizard, action, user, message) def self.create(wizard_id, action, username, message)
log_id = SecureRandom.hex(12) log_id = SecureRandom.hex(12)
PluginStore.set('custom_wizard_log', PluginStore.set('custom_wizard_log',
log_id.to_s, log_id.to_s,
{ {
date: Time.now, date: Time.now,
wizard: wizard, wizard_id: wizard_id,
action: action, action: action,
user: user, username: username,
message: message message: message
} }
) )
end end
def self.list_query def self.list_query(wizard_id = nil)
PluginStoreRow.where(" query = PluginStoreRow.where("plugin_name = 'custom_wizard_log' AND (value::json->'date') IS NOT NULL")
plugin_name = 'custom_wizard_log' AND query = query.where("(value::json->>'wizard_id') = ?", wizard_id) if wizard_id
(value::json->'date') IS NOT NULL query.order("value::json->>'date' DESC")
").order("value::json->>'date' DESC")
end end
def self.list(page = 0, limit = nil) def self.list(page = 0, limit = nil, wizard_id = nil)
limit = limit.to_i > 0 ? limit.to_i : PAGE_LIMIT limit = limit.to_i > 0 ? limit.to_i : PAGE_LIMIT
page = page.to_i page = page.to_i
logs = self.list_query(wizard_id)
self.list_query.limit(limit) result = OpenStruct.new(logs: [], total: nil)
result.total = logs.size
result.logs = logs.limit(limit)
.offset(page * limit) .offset(page * limit)
.map { |r| self.new(JSON.parse(r.value)) } .map { |r| self.new(JSON.parse(r.value)) }
result
end end
end end

Datei anzeigen

@ -1,4 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class CustomWizard::LogSerializer < ApplicationSerializer class CustomWizard::LogSerializer < ApplicationSerializer
attributes :date, :wizard, :action, :user, :message attributes :date,
:action,
:username,
:message
has_one :user, serializer: ::BasicUserSerializer, embed: :objects
end end

Datei anzeigen

@ -10,19 +10,25 @@ describe CustomWizard::Log do
it "creates logs" do it "creates logs" do
expect( expect(
CustomWizard::Log.list.length CustomWizard::Log.list.logs.length
).to eq(3) ).to eq(3)
end end
it "lists logs by time created" do it "lists logs by time created" do
expect( expect(
CustomWizard::Log.list.first.message CustomWizard::Log.list.logs.first.message
).to eq("Third log message") ).to eq("Third log message")
end end
it "paginates logs" do it "paginates logs" do
expect( expect(
CustomWizard::Log.list(0, 2).length CustomWizard::Log.list(0, 2).logs.length
).to eq(2) ).to eq(2)
end end
it "lists logs by wizard" do
expect(
CustomWizard::Log.list(0, 2, 'third-test-wizard').logs.length
).to eq(1)
end
end end

Datei anzeigen

@ -3,21 +3,40 @@ require_relative '../../../plugin_helper'
describe CustomWizard::AdminLogsController do describe CustomWizard::AdminLogsController do
fab!(:admin_user) { Fabricate(:user, admin: true) } fab!(:admin_user) { Fabricate(:user, admin: true) }
let(:template) { get_wizard_fixture("wizard") }
before do before do
CustomWizard::Log.create('first-test-wizard', 'perform_first_action', 'first_test_user', 'First log message') ["first", "second", "third"].each_with_index do |key, index|
CustomWizard::Log.create('second-test-wizard', 'perform_second_action', 'second_test_user', 'Second log message') temp = template.dup
CustomWizard::Log.create('third-test-wizard', 'perform_third_action', 'third_test_user', 'Third log message') temp["id"] = "#{key}_test_wizard"
CustomWizard::Template.save(temp, skip_jobs: true)
CustomWizard::Log.create("#{key}_test_wizard", "perform_#{key}_action", "#{key}_test_user", "#{key} log message")
end
sign_in(admin_user) sign_in(admin_user)
end end
it "returns a list of logs" do it "returns a list of wizards" do
get "/admin/wizards/logs.json" get "/admin/wizards/logs.json"
expect(response.parsed_body.length).to eq(3) expect(response.parsed_body.length).to eq(3)
end end
it "returns a list of logs for a wizard" do
get "/admin/wizards/logs/first_test_wizard.json"
expect(response.parsed_body['logs'].length).to eq(1)
end
it "paginates" do it "paginates" do
get "/admin/wizards/logs.json", params: { page: 1, limit: 2 } get "/admin/wizards/logs/first_test_wizard.json", params: { page: 1 }
expect(response.parsed_body.length).to eq(1) expect(response.parsed_body['logs'].length).to eq(0)
end
it "returns total logs for a wizard" do
get "/admin/wizards/logs/first_test_wizard.json"
expect(response.parsed_body['total']).to eq(1)
end
it "returns basic wizard" do
get "/admin/wizards/logs/first_test_wizard.json"
expect(response.parsed_body['wizard']['id']).to eq("first_test_wizard")
end end
end end

Datei anzeigen

@ -10,13 +10,12 @@ describe CustomWizard::LogSerializer do
CustomWizard::Log.create('second-test-wizard', 'perform_second_action', 'second_test_user', 'Second log message') CustomWizard::Log.create('second-test-wizard', 'perform_second_action', 'second_test_user', 'Second log message')
json_array = ActiveModel::ArraySerializer.new( json_array = ActiveModel::ArraySerializer.new(
CustomWizard::Log.list(0), CustomWizard::Log.list(0).logs,
each_serializer: CustomWizard::LogSerializer each_serializer: CustomWizard::LogSerializer
).as_json ).as_json
expect(json_array.length).to eq(2) expect(json_array.length).to eq(2)
expect(json_array[0][:wizard]).to eq("second-test-wizard")
expect(json_array[0][:action]).to eq("perform_second_action") expect(json_array[0][:action]).to eq("perform_second_action")
expect(json_array[0][:user]).to eq("second_test_user") expect(json_array[0][:username]).to eq('second_test_user')
expect(json_array[0][:message]).to eq("Second log message") expect(json_array[0][:message]).to eq("Second log message")
end end
end end