refs #5517 vnLog: View improved
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Juan Ferrer 2023-04-03 03:54:35 +02:00
parent 0ea30552dd
commit 5b80951e3e
9 changed files with 313 additions and 137 deletions

View File

@ -32,6 +32,7 @@ import './float-button';
import './icon-menu'; import './icon-menu';
import './icon-button'; import './icon-button';
import './input-number'; import './input-number';
import './json-value';
import './label-value'; import './label-value';
import './range'; import './range';
import './input-time'; import './input-time';

View File

@ -0,0 +1 @@
<span></span>

View File

@ -0,0 +1,56 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
import './style.scss';
const maxStrLen = 25;
/**
* Displays pretty JSON value.
*
* @property {*} value The value
*/
export default class Controller extends Component {
get value() {
return this._value;
}
set value(value) {
this._value = value;
const span = this.element;
const formattedValue = this.formatValue(value);
span.textContent = formattedValue;
span.title = typeof value == 'string' && value.length > maxStrLen ? value : '';
span.className = `js-${value == null ? 'null' : typeof value}`;
}
formatValue(value) {
if (value == null) return '∅';
switch (typeof value) {
case 'boolean':
return value ? '✓' : '✗';
case 'string':
return value.length <= maxStrLen
? value
: value.substring(0, maxStrLen) + '...';
case 'object':
if (value instanceof Date) {
const hasZeroTime =
value.getHours() === 0 &&
value.getMinutes() === 0 &&
value.getSeconds() === 0;
const format = hasZeroTime ? 'dd/MM/yyyy' : 'dd/MM/yyyy HH:mm:ss';
return this.$filter('date')(value, format);
} else
return value;
default:
return value;
}
}
}
ngModule.vnComponent('vnJsonValue', {
controller: Controller,
bindings: {
value: '<?'
}
});

View File

@ -0,0 +1,21 @@
vn-json-value {
display: inline;
&.js-string {
color: #d172cc;
}
&.js-object {
/*color: #d1a572;*/
color: #d172cc;
}
&.js-number {
color: #85d0ff;
}
&.js-boolean {
color: #7dc489;
}
&.js-null {
color: #cd7c7c;
font-style: italic;
}
}

View File

@ -41,10 +41,15 @@ vn-table {
display: table-row; display: table-row;
height: 48px; height: 48px;
} }
vn-thead, .vn-thead, & > thead,
vn-tbody, .vn-tbody, & > tbody,
vn-tfoot, .vn-tfoot, & > tfoot,
thead, tbody, tfoot { & > vn-thead,
& > vn-tbody,
& > vn-tfoot,
& > .vn-thead,
& > .vn-tbody,
& > .vn-tfoot {
& > * { & > * {
display: table-row; display: table-row;
@ -111,14 +116,14 @@ vn-table {
color: inherit; color: inherit;
} }
} }
a.vn-tbody { & > a.vn-tbody {
&.clickable { &.clickable {
@extend %clickable; @extend %clickable;
} }
} }
vn-tbody > *, & > vn-tbody > *,
.vn-tbody > *, & > .vn-tbody > *,
tbody > * { & > tbody > * {
border-bottom: $border-thin; border-bottom: $border-thin;
&:last-child { &:last-child {

View File

@ -9,63 +9,99 @@
limit="20" limit="20"
auto-load="true"> auto-load="true">
</vn-crud-model> </vn-crud-model>
<vn-data-viewer model="model" class="vn-w-xl"> <vn-data-viewer model="model" class="vn-w-md">
<vn-card> <vn-card>
<vn-table model="model"> <table class="vn-table" model="model">
<vn-thead>
<vn-tr>
<vn-th field="creationDate">Date</vn-th>
<vn-th field="userFk" shrink>User</vn-th>
<vn-th field="changedModel" ng-if="$ctrl.showModelName" shrink>Model</vn-th>
<vn-th field="action" shrink>Action</vn-th>
<vn-th field="changedModelValue" ng-if="$ctrl.showModelName">Name</vn-th>
<vn-th expand>Changes</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="log in $ctrl.logs">
<vn-td shrink-datetime>
{{::log.creationDate | date:'dd/MM/yyyy HH:mm'}}
</vn-td>
<vn-td>
<span ng-class="{'link': log.user.worker.id, 'value': !log.user.worker.id}"
ng-click="$ctrl.showWorkerDescriptor($event, log.user.worker.id)"
translate>{{::log.user.name || 'System' | translate}}
</span>
</vn-td>
<vn-td ng-if="$ctrl.showModelName">
{{::log.changedModel}}
</vn-td>
<vn-td shrink translate>
{{::$ctrl.actionsText[log.action]}}
</vn-td>
<vn-td ng-if="$ctrl.showModelName">
{{::log.changedModelValue}}
</vn-td>
<vn-td expand>
<table class="attributes">
<thead> <thead>
<tr> <tr>
<th translate class="field">Field</th> <th translate field="action" class="action">
<th translate>Before</th> Action
<th translate>After</th> </th>
<th field="changedModel">
<span ng-if="$ctrl.showModelName" translate>Model</span>
</th>
<th translate field="creationDate" class="date">
Date
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody ng-repeat="log in $ctrl.logs">
<tr ng-repeat="prop in ::log.props"> <tr class="change-header">
<td class="field">{{prop.name}}</td> <td class="user">
<td class="before">{{prop.old}}</td> <span ng-if="::log.user.worker.id"
<td class="after">{{prop.new}}</td> class="link"
ng-click="$ctrl.showWorkerDescriptor($event, log.user.worker.id)">
{{::log.user.name}}
</span>
<span ng-if="::!log.user.worker.id" translate>
System
</span>
</td>
<td title="{{::log.changedModelValue}}">
<span class="model-name" ng-if="::$ctrl.showModelName">
{{::log.changedModel}}
</span>
<span class="model-value">
{{::log.changedModelValue}}
</span>
<span class="model-id">
#{{::log.changedModelId}}
</span>
</td>
<td class="date">
{{::log.creationDate | date:'dd/MM/yyyy HH:mm'}}
</td>
</tr>
<tr class="change-detail">
<td class="action">
<span class="chip {{::$ctrl.actionsClass[log.action]}}" translate>
{{::$ctrl.actionsText[log.action]}}
</span>
</td>
<td colspan="2">
<div vn-id="changes" class="changes {{::log.props.length ? 'props' : 'no-props'}}">
<vn-icon icon="visibility"
class="expand-button"
ng-click="$ctrl.toggleAttributes(log, changes, true)">
</vn-icon>
<vn-icon icon="visibility_off"
class="shrink-button"
ng-click="$ctrl.toggleAttributes(log, changes, false)">
</vn-icon>
<div class="changes-wrapper">
<span ng-if="::log.props.length"
class="attributes">
<span ng-if="!log.expand" ng-repeat="prop in ::log.props"
class="basic-json">
<span class="json-field">{{::prop.name}}:</span>
<vn-json-value value="$ctrl.mainVal(prop, log.action)"></vn-json-value><span ng-if="::!$last">,</span>
</span>
<div ng-if="log.expand"
class="expanded-json">
<div ng-repeat="prop in ::log.props">
<span class="json-field">{{::prop.name}}:</span>
<vn-json-value value="$ctrl.mainVal(prop, log.action)"></vn-json-value>
<span ng-if="::log.action == 'update'">
<vn-json-value value="prop.old"></vn-json-value>
</span>
</div>
</div>
</span>
<span ng-if="::!log.props.length"
class="description">
{{::log.description}}
</span>
<span ng-if="!log.description && !log.props.length"
class="no-changes"
translate>
No changes
</span>
</div>
</div>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div ng-if="log.description != null">
{{::log.description}}
</div>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
<vn-pagination model="model"></vn-pagination> <vn-pagination model="model"></vn-pagination>
</vn-card> </vn-card>
</vn-data-viewer> </vn-data-viewer>

View File

@ -13,6 +13,12 @@ export default class Controller extends Section {
delete: 'Deletes', delete: 'Deletes',
select: 'Views' select: 'Views'
}; };
this.actionsClass = {
insert: 'success',
update: 'warning',
delete: 'alert',
select: 'notice'
};
this.filter = { this.filter = {
include: [{ include: [{
relation: 'user', relation: 'user',
@ -50,8 +56,8 @@ export default class Controller extends Section {
for (const prop of props) { for (const prop of props) {
log.props.push({ log.props.push({
name: locale[prop] || prop, name: locale[prop] || prop,
old: this.formatValue(oldValues[prop]), old: this.castValue(oldValues[prop]),
new: this.formatValue(newValues[prop]) new: this.castValue(newValues[prop])
}); });
} }
} }
@ -61,31 +67,19 @@ export default class Controller extends Section {
return !(this.changedModel && this.changedModelId); return !(this.changedModel && this.changedModelId);
} }
formatValue(value) { castValue(value) {
let type = typeof value; return typeof value === 'string' && validDate.test(value)
? new Date(value)
if (type === 'string' && validDate.test(value)) { : value;
value = new Date(value);
type = typeof value;
} }
switch (type) { mainVal(prop, action) {
case 'boolean': return action == 'delete' ? prop.old : prop.new;
return value ? '✓' : '✗';
case 'object':
if (value instanceof Date) {
const hasZeroTime =
value.getHours() === 0 &&
value.getMinutes() === 0 &&
value.getSeconds() === 0;
const format = hasZeroTime ? 'dd/MM/yyyy' : 'dd/MM/yyyy HH:mm:ss';
return this.$filter('date')(value, format);
}
else
return value;
default:
return value;
} }
toggleAttributes(log, changesEl, force) {
log.expand = force;
changesEl.classList.toggle('expanded', force);
} }
showWorkerDescriptor(event, workerId) { showWorkerDescriptor(event, workerId) {

View File

@ -13,3 +13,4 @@ Views: Visualiza
System: Sistema System: Sistema
note: nota note: nota
Changes: Cambios Changes: Cambios
No changes: No hay cambios

View File

@ -1,66 +1,127 @@
@import "variables"; @import "variables";
vn-log { vn-log {
vn-td { .vn-table {
vertical-align: initial !important; table-layout: fixed;
}
.changes {
display: none;
}
.label {
color: $color-font-secondary;
}
.value {
color: $color-font;
}
@media screen and (max-width: 1570px) { & > thead,
vn-table .expendable { & > tbody {
display: none; & > tr {
td, th {
&:first-child {
padding-left: 16px;
} }
.changes { &:last-child {
padding-top: 10px; padding-right: 16px;
display: block;
} }
} }
.attributes { }
width: 100%; }
& > thead > tr > th {
max-width: initial;
}
& > tbody {
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
tr { &:last-child {
height: 10px; border-bottom: none;
}
& > tr {
border-bottom: none;
height: initial;
& > td { & > td {
padding: 2px; padding-top: 16px;
padding-bottom: 16px;
&.action > .chip {
display: inline-block;
} }
& > td.field,
& > th.field {
width: 20%;
color: gray;
} }
& > td.before, &.change-header > td {
& > th.before, padding-bottom: 0;
& > td.after, }
& > th.after { &.change-detail > td {
width: 40%; padding-top: 6px;
white-space: pre-line; vertical-align: top;
} }
} }
} }
th, td {
&.action,
&.user {
width: 90px;
} }
.ellipsis { &.date {
white-space: nowrap; width: 120px;
text-align: right;
}
}
}
.model-value {
font-style: italic;
color: #c7bd2b;
}
.model-id {
color: $color-font-secondary;
font-size: .9em;
}
.changes {
overflow: hidden;
background-color: rgba(255, 255, 255, .05);
border-radius: 4px;
color: $color-font-secondary;
transition: max-height 150ms ease-in-out;
max-height: 28px;
position: relative;
& > .expand-button,
& > .shrink-button {
display: none;
}
&.props {
padding-right: 24px;
& > .expand-button,
& > .shrink-button {
position: absolute;
top: 6px;
right: 8px;
font-size: inherit;
float: right;
cursor: pointer;
}
& > .expand-button {
display: block;
}
&.expanded {
max-height: 500px;
padding-right: 0;
& > .changes-wrapper {
text-overflow: initial;
white-space: initial;
}
& > .shrink-button {
display: block;
}
& > .expand-button {
display: none;
}
}
}
& > .changes-wrapper {
padding: 4px 6px;
overflow: hidden; overflow: hidden;
max-width: 400px;
text-overflow: ellipsis; text-overflow: ellipsis;
display: inline-block; white-space: nowrap;
& > .no-changes {
font-style: italic;
}
.json-field {
text-transform: capitalize;
}
} }
.no-ellipsize,
[no-ellipsize] {
text-overflow: '';
white-space: normal;
overflow: auto;
} }
.alignSpan {
overflow: hidden;
display: inline-block;
} }