1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

log views: show event diffs by default, toggle to show the full event.

This commit is contained in:
Jari Bakken 2015-01-26 16:54:50 +01:00 committed by Ivar Conradi Østhus
parent 72b48223e5
commit 12710a6d04
7 changed files with 279 additions and 21 deletions

View File

@ -1,9 +1,11 @@
var eventDb = require('./eventDb');
var eventDb = require('./eventDb');
var eventDiffer = require('./eventDiffer');
module.exports = function (app) {
app.get('/events', function (req, res) {
eventDb.getEvents().then(function (events) {
eventDiffer.addDiffs(events);
res.json({events: events});
});
});
@ -11,6 +13,7 @@ module.exports = function (app) {
app.get('/events/:name', function (req, res) {
eventDb.getEventsFilterByName(req.params.name).then(function (events) {
if (events) {
eventDiffer.addDiffs(events);
res.json(events);
} else {
res.status(404).json({error: 'Could not find events'});
@ -18,5 +21,4 @@ module.exports = function (app) {
});
});
};

74
lib/eventDiffer.js Normal file
View File

@ -0,0 +1,74 @@
var eventType = require('./eventType');
var diff = require('deep-diff').diff;
var strategyTypes = [
eventType.strategyCreated,
eventType.strategyDeleted
];
var featureTypes = [
eventType.featureCreated,
eventType.featureUpdated,
eventType.featureArchive,
eventType.featureRevive
];
function baseTypeFor(event) {
if (featureTypes.indexOf(event.type) !== -1) {
return 'features';
} else if (strategyTypes.indexOf(event.type) !== -1) {
return 'strategies';
} else {
throw new Error('unknown event type: ' + JSON.stringify(event));
}
}
function groupByBaseTypeAndName(events) {
var groups = {};
events.forEach(function (event) {
var baseType = baseTypeFor(event);
groups[baseType] = groups[baseType] || {};
groups[baseType][event.data.name] = groups[baseType][event.data.name] || [];
groups[baseType][event.data.name].push(event);
});
return groups;
}
function eachConsecutiveEvent(events, callback) {
var groups = groupByBaseTypeAndName(events);
Object.keys(groups).forEach(function (baseType) {
var group = groups[baseType];
Object.keys(group).forEach(function (name) {
var events = group[name];
var left, right, i, l;
for (i = 0, l = events.length; i < l; i++) {
left = events[i];
right = events[i + 1];
callback(left, right);
}
});
});
}
function addDiffs(events) {
eachConsecutiveEvent(events, function (left, right) {
if (right) {
left.diffs = diff(right.data, left.data);
left.diffs = left.diffs || [];
} else {
left.diffs = null;
}
});
}
module.exports = {
addDiffs: addDiffs
};

View File

@ -36,6 +36,7 @@
"bluebird": "2.6.2",
"body-parser": "1.10.1",
"db-migrate": "0.7.1",
"deep-diff": "^0.3.0",
"errorhandler": "1.3.2",
"express": "4.9.8",
"express-validator": "2.8.0",

View File

@ -23,3 +23,15 @@ code {
word-wrap: break-word;
white-space: pre;
}
code > .diff-N {
color: green;
}
code > .diff-D {
color: red;
}
code > .diff-A, .diff-E {
color: black;
}

View File

@ -1,5 +1,19 @@
var React = require('react');
var DIFF_PREFIXES = {
A: ' ',
E: ' ',
D: '-',
N: '+'
}
var SPADEN_CLASS = {
A: 'blue', // array edited
E: 'blue', // edited
D: 'negative', // deleted
N: 'positive', // added
}
var LogEntry = React.createClass({
propTypes: {
event: React.PropTypes.object.isRequired
@ -7,25 +21,64 @@ var LogEntry = React.createClass({
render: function() {
var d = new Date(this.props.event.createdAt);
return (
<tr>
<td>
{d.getDate() + "." + (d.getMonth() + 1) + "." + d.getFullYear()}<br />
kl. {d.getHours() + "." + d.getMinutes()}
</td>
<td>
<strong>{this.props.event.data.name}</strong><em>[{this.props.event.type}]</em>
</td>
<td style={{maxWidth: 300}}>
{this.renderEventDiff()}
</td>
<td>{this.props.event.createdBy}</td>
</tr>
);
},
renderFullEventData: function() {
var localEventData = JSON.parse(JSON.stringify(this.props.event.data));
delete localEventData.description;
delete localEventData.name;
return (
<tr>
<td>
{d.getDate() + "." + d.getMonth() + "." + d.getFullYear()}<br />
kl. {d.getHours() + "." + d.getMinutes()}
</td>
<td>
<strong>{this.props.event.data.name}</strong><em>[{this.props.event.type}]</em>
</td>
<td style={{maxWidth: 300}}>
<code className='JSON smalltext man'>{JSON.stringify(localEventData, null, 2)}</code>
</td>
<td>{this.props.event.createdBy}</td>
</tr>
);
var prettyPrinted = JSON.stringify(localEventData, null, 2);
return (<code className='JSON smalltext man'>{prettyPrinted}</code>)
},
renderEventDiff: function() {
if (!this.props.showFullEvents && this.props.event.diffs) {
var changes = this.props.event.diffs.map(this.buildDiff);
return (<code className='smalltext man'>{changes.length === 0 ? '(no changes)' : changes}</code>)
} else {
return this.renderFullEventData();
}
},
buildDiff: function(diff, idx) {
var change;
var key = diff.path.join('.');
if (diff.lhs !== undefined && diff.rhs !== undefined) {
change = (
<div>
<div className={SPADEN_CLASS.D}>- {key}: {JSON.stringify(diff.lhs)}</div>
<div className={SPADEN_CLASS.N}>+ {key}: {JSON.stringify(diff.rhs)}</div>
</div>
);
} else {
var spadenClass = SPADEN_CLASS[diff.kind]
var prefix = DIFF_PREFIXES[diff.kind];
change = (<div className={spadenClass}>{prefix} {key}: {JSON.stringify(diff.rhs)}</div>)
}
return (<div key={idx}>{change}</div>)
}
});
module.exports = LogEntry;

View File

@ -6,18 +6,32 @@ var LogEntryList = React.createClass({
events: React.PropTypes.array.isRequired
},
getInitialState: function() {
return {
showFullEvents: false
}
},
render: function() {
var logEntryNodes = this.props.events.map(function(event) {
return <LogEntry event={event} key={event.name} />;
});
return <LogEntry event={event} key={event.id} showFullEvents={this.state.showFullEvents} />;
}.bind(this));
return (
<div className=''>
<div>
<label className="prs fright-ht768 smalltext">
Show full events
<input type="checkbox" className="mlm" value={this.state.fullEvents} onChange={this.toggleFullEvents}></input>
</label>
<table className='outerborder zebra-striped'>
<thead>
<tr>
<th>When</th>
<th>Action</th>
<th>Data</th>
<th>
Data
</th>
<th>Author</th>
</tr>
</thead>
@ -27,7 +41,12 @@ var LogEntryList = React.createClass({
</table>
</div>
);
},
toggleFullEvents: function(e) {
this.setState({showFullEvents: !this.state.showFullEvents});
}
});
module.exports = LogEntryList;

97
test/eventDifferTest.js Normal file
View File

@ -0,0 +1,97 @@
var eventDiffer = require('../lib/eventDiffer');
var eventType = require('../lib/eventType');
var assert = require('assert');
describe('eventDiffer', function () {
it('fails if events include an unknown event type', function () {
var events = [
{type: eventType.featureCreated, data: {}},
{type: 'unknown-type', data: {}}
];
assert.throws(function () {
eventDiffer.addDiffs(events);
});
});
it('diffs a feature-update event', function () {
var name = 'foo';
var desc = 'bar';
var events = [
{
type: eventType.featureUpdated,
data: {name: name, description: desc, strategy: 'default', enabled: true, parameters: {value: 2 }}
},
{
type: eventType.featureCreated,
data: {name: name, description: desc, strategy: 'default', enabled: false, parameters: {value: 1}}
}
];
eventDiffer.addDiffs(events);
assert.deepEqual(events[0].diffs, [
{kind: 'E', path: ["enabled"], lhs: false, rhs: true},
{kind: 'E', path: ["parameters", "value"], lhs: 1, rhs: 2}
]);
assert.strictEqual(events[1].diffs, null);
});
it('diffs only against features with the same name', function () {
var events = [
{
type: eventType.featureUpdated,
data: {name: 'bar', description: 'desc', strategy: 'default', enabled: true, parameters: {}}
},
{
type: eventType.featureUpdated,
data: {name: 'foo', description: 'desc', strategy: 'default', enabled: false, parameters: {}}
},
{
type: eventType.featureCreated,
data: {name: 'bar', description: 'desc', strategy: 'default', enabled: false, parameters: {}}
},
{
type: eventType.featureCreated,
data: {name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {}}
}
];
eventDiffer.addDiffs(events);
assert.strictEqual(events[0].diffs[0].rhs, true);
assert.strictEqual(events[1].diffs[0].rhs, false);
assert.strictEqual(events[2].diffs, null);
assert.strictEqual(events[3].diffs, null);
});
it('sets an empty array of diffs if nothing was changed', function () {
var events = [
{
type: eventType.featureUpdated,
data: {name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {}}
},
{
type: eventType.featureCreated,
data: {name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {}}
}
];
eventDiffer.addDiffs(events);
assert.deepEqual(events[0].diffs, []);
});
it('sets diffs to null if there was nothing to diff against', function () {
var events = [
{
type: eventType.featureUpdated,
data: {name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {}}
}
];
eventDiffer.addDiffs(events);
assert.strictEqual(events[0].diffs, null);
});
});