Show unconfirmed balances in toplist #112

Merged
raucao merged 10 commits from feature/111-unconfirmed_balances into master 2019-05-01 19:30:36 +00:00
12 changed files with 223 additions and 79 deletions
+2 -2
View File
@@ -9,10 +9,10 @@ export default Component.extend({
actions: { actions: {
toggleContributorInfo(contributor) { toggleContributorInfo(contributor) {
if (contributor.get('showMetadata')) { if (contributor.showMetadata) {
contributor.set('showMetadata', false); contributor.set('showMetadata', false);
} else { } else {
this.contributors.setEach('showMetadata', false); this.contributorList.setEach('showMetadata', false);
contributor.set('showMetadata', true); contributor.set('showMetadata', true);
} }
} }
+20 -10
View File
@@ -1,24 +1,34 @@
<tbody> <tbody>
{{#each contributors as |contributor|}} {{#each contributorList as |c|}}
<tr role="button" class={{if (is-current-user contributor) "current-user"}} {{action "toggleContributorInfo" contributor}}> <tr role="button" class={{if (is-current-user c.contributor) "current-user"}} {{action "toggleContributorInfo" c}}>
<td class="person"> <td class="person">
{{user-avatar contributor=contributor}} {{contributor.name}} {{user-avatar contributor=c.contributor}} {{c.contributor.name}}
</td> </td>
<td class="kredits"> <td class="kredits">
<span class="amount">{{contributor.totalKreditsEarned}}</span> <span class="amount">
{{#if showUnconfirmedKredits}}
{{c.amountTotal}}
{{else}}
{{c.amountConfirmed}}
{{/if}}
</span>
<span class="symbol">₭S</span> <span class="symbol">₭S</span>
</td> </td>
</tr> </tr>
<tr class="metadata {{if (is-current-user contributor) "current-user"}} {{if contributor.showMetadata "visible"}}"> <tr class="metadata {{if (is-current-user c.contributor) "current-user"}} {{if c.showMetadata "visible"}}">
<td colspan="2"> <td colspan="2">
<ul> <ul>
<li><a href="https://rinkeby.etherscan.io/address/{{contributor.account}}">Inspect Ethereum transactions</a></li> <li>
{{#if contributor.ipfsHash}} <a href="https://rinkeby.etherscan.io/address/{{c.contributor.account}}" target="_blank" rel="noopener">Inspect Ethereum transactions</a>
<li><a href="https://ipfs.io/ipfs/{{contributor.ipfsHash}}">Inspect IPFS profile</a></li> </li>
{{#if c.contributor.ipfsHash}}
<li>
<a href="https://ipfs.io/ipfs/{{c.contributor.ipfsHash}}" target="_blank" rel="noopener">Inspect IPFS profile</a>
</li>
{{/if}} {{/if}}
</ul> </ul>
{{#if contributor.showMetadata}} {{#if c.showMetadata}}
<pre>{{contributor.ipfsData}}</pre> <pre>{{c.contributor.ipfsData}}</pre>
{{/if}} {{/if}}
</td> </td>
</tr> </tr>
+15 -23
View File
@@ -1,38 +1,30 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { alias, filter, sort } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import { isPresent } from '@ember/utils'; import { alias, not, sort } from '@ember/object/computed';
import { inject as service } from '@ember/service';
export default Controller.extend({ export default Controller.extend({
kredits: service(), kredits: service(),
currentBlock: alias('kredits.currentBlock'), currentBlock: alias('kredits.currentBlock'),
contributors: alias('kredits.contributors'),
contributorsWithKredits: filter('contributors', function(contributor) {
return isPresent(contributor.totalKreditsEarnedRaw) &&
contributor.totalKreditsEarnedRaw.gt(0);
}),
contributorsSorting: Object.freeze(['totalKreditsEarned:desc']),
contributorsSorted: sort('contributorsWithKredits', 'contributorsSorting'),
contributions: alias('kredits.contributions'), contributions: alias('kredits.contributions'),
contributionsConfirmed: alias('kredits.contributionsConfirmed'),
contributionsUnconfirmed: alias('kredits.contributionsUnconfirmed'),
contributionsSorting: Object.freeze(['id:desc']), contributionsSorting: Object.freeze(['id:desc']),
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() {
return this.contributions.filter(contribution => {
return contribution.confirmedAt > this.currentBlock;
});
}),
contributionsConfirmed: computed('contributions.[]', 'currentBlock', function() {
return this.contributions.filter(contribution => {
return contribution.confirmedAt <= this.currentBlock;
});
}),
contributionsUnconfirmedSorted: sort('contributionsUnconfirmed', 'contributionsSorting'), contributionsUnconfirmedSorted: sort('contributionsUnconfirmed', 'contributionsSorting'),
contributionsConfirmedSorted: sort('contributionsConfirmed', 'contributionsSorting'), contributionsConfirmedSorted: sort('contributionsConfirmed', 'contributionsSorting'),
kreditsByContributor: alias('kredits.kreditsByContributor'),
kreditsToplistSorting: computed('showUnconfirmedKredits', function(){
return this.showUnconfirmedKredits ? ['amountTotal:desc'] : ['amountConfirmed:desc'];
}),
kreditsToplist: sort('kreditsByContributor', 'kreditsToplistSorting'),
showUnconfirmedKredits: true,
hideUnconfirmedKredits: not('showUnconfirmedKredits'),
actions: { actions: {
vetoContribution (contributionId) { vetoContribution (contributionId) {
+35 -3
View File
@@ -3,10 +3,12 @@ import Kredits from 'npm:kredits-contracts';
import RSVP from 'rsvp'; import RSVP from 'rsvp';
import Service from '@ember/service'; import Service from '@ember/service';
import EmberObject from '@ember/object';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import { alias, notEmpty } from '@ember/object/computed'; import { alias, notEmpty } from '@ember/object/computed';
import { isEmpty } from '@ember/utils'; import { isEmpty } from '@ember/utils';
import groupBy from 'kredits-web/utils/group-by';
import formatKredits from 'kredits-web/utils/format-kredits'; import formatKredits from 'kredits-web/utils/format-kredits';
import config from 'kredits-web/config/environment'; import config from 'kredits-web/config/environment';
@@ -20,15 +22,44 @@ export default Service.extend({
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded. currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
currentUser: null, currentUser: null,
contributors: null, contributors: null,
proposals: null,
contributions: null, contributions: null,
proposals: null,
currentUserIsContributor: notEmpty('currentUser'), currentUserIsContributor: notEmpty('currentUser'),
currentUserIsCore: alias('currentUser.isCore'), currentUserIsCore: alias('currentUser.isCore'),
hasAccounts: notEmpty('currentUserAccounts'), hasAccounts: notEmpty('currentUserAccounts'),
accountNeedsUnlock: computed('currentUserAccounts', function() { accountNeedsUnlock: computed('currentUserAccounts', function() {
return this.currentUserAccounts && isEmpty(this.currentUserAccounts); return this.currentUserAccounts && isEmpty(this.currentUserAccounts);
}), }),
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() {
return this.contributions.filter(contribution => {
return contribution.confirmedAt > this.currentBlock;
});
}),
contributionsConfirmed: computed('contributions.[]', 'currentBlock', function() {
return this.contributions.filter(contribution => {
return contribution.confirmedAt <= this.currentBlock;
});
}),
kreditsByContributor: computed('contributionsUnconfirmed.[]', 'contributors', function() {
const contributionsGrouped = groupBy(this.contributionsUnconfirmed, 'contributorId');
return contributionsGrouped.map(c => {
const amountUnconfirmed = c.items.mapBy('amount').reduce((a, b) => a + b);
const contributor = this.contributors.findBy('id', c.value.toString());
return EmberObject.create({
contributor: contributor,
amountUnconfirmed: amountUnconfirmed,
amountConfirmed: contributor.totalKreditsEarned,
amountTotal: contributor.totalKreditsEarned + amountUnconfirmed
})
})
}),
init () { init () {
this._super(...arguments); this._super(...arguments);
this.set('contributors', []); this.set('contributors', []);
@@ -36,8 +67,9 @@ export default Service.extend({
this.set('contributions', []); this.set('contributions', []);
}, },
// this is called in the routes beforeModel(). So it is initialized before everything else // This is called in the application route's beforeModel(). So it is
// and we can rely on the ethProvider and the potential currentUserAccounts to be available // initialized before everything else, and we can rely on the ethProvider and
// the potential currentUserAccounts to be available
getEthProvider: function() { getEthProvider: function() {
let ethProvider; let ethProvider;
+1 -4
View File
@@ -47,16 +47,13 @@ section {
&#people { &#people {
.content { .content {
p.stats { p.stats {
padding-top: 3rem; margin-bottom: 1rem;
font-size: 1rem; font-size: 1rem;
color: white; color: white;
text-align: center; text-align: center;
span.number { span.number {
font-weight: 600; font-weight: 600;
} }
@include media-max(small) {
padding-top: 2rem;
}
} }
} }
} }
@@ -1,6 +1,7 @@
table.contributor-list { table.contributor-list {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-bottom: 1.5rem;
tr { tr {
border-bottom: 1px solid rgba(255,255,255,0.2); border-bottom: 1px solid rgba(255,255,255,0.2);
+8 -3
View File
@@ -6,11 +6,16 @@
<h2>Contributors</h2> <h2>Contributors</h2>
</header> </header>
<div class="content"> <div class="content">
{{contributor-list contributors=contributorsSorted}} {{contributor-list contributorList=kreditsToplist
showUnconfirmedKredits=showUnconfirmedKredits}}
<p class="stats"> <p class="stats">
<span class="number">{{await kredits.totalKreditsEarned}}</span> kredits issued and distributed among <span class="number">{{await kredits.totalKreditsEarned}}</span> kredits confirmed and issued to
<span class="number">{{contributorsWithKredits.length}}</span> contributors. <span class="number">{{contributorsWithKredits.length}}</span> contributors
</p>
<p class="stats">
{{input type="checkbox" id="hide-unnconfirmed-kredits" checked=showUnconfirmedKredits}}
<label for="hide-unnconfirmed-kredits">Show unconfirmed kredits in toplist</label>
</p> </p>
</div> </div>
</section> </section>
+27
View File
@@ -0,0 +1,27 @@
//
// Code from https://github.com/HeroicEric/ember-group-by (MIT licensed)
//
import { A } from '@ember/array';
import { get } from '@ember/object';
import { isPresent } from '@ember/utils';
export default function groupBy (collection, property) {
let groups = A();
let items = collection;
if (items) {
bumi commented 2019-05-01 18:43:59 +00:00 (Migrated from github.com)
Review

maybe
if(!items) {
return groups;
}

maybe if(!items) { return groups; }
raucao commented 2019-05-01 18:53:01 +00:00 (Migrated from github.com)
Review

Did you see that you can now suggest changes to commit in code comments? It's the first icon button in the comment topbar. Makes it much easier to see what the suggested change is as well, because the code behind the comment isn't cut off then...

Never mind, it's a bigger change.

<del>Did you see that you can now suggest changes to commit in code comments? It's the first icon button in the comment topbar. Makes it much easier to see what the suggested change is as well, because the code behind the comment isn't cut off then...</del> Never mind, it's a bigger change.
raucao commented 2019-05-01 18:55:03 +00:00 (Migrated from github.com)
Review

I think a single return is a bit cleaner here.

I think a single return is a bit cleaner here.
items.forEach(function(item) {
let value = get(item, property);
let group = groups.findBy('value', value);
if (isPresent(group)) {
get(group, 'items').push(item);
} else {
group = { property: property, value: value, items: [item] };
groups.push(group);
}
});
}
return groups;
}
+13
View File
@@ -0,0 +1,13 @@
import Contributor from 'kredits-web/models/contributor';
const contributors = [];
const data = [
{ id: 1, name: 'Bumi', totalKreditsEarned: 5500 },
{ id: 2, name: 'Râu Cao', totalKreditsEarned: 1500 },
{ id: 3, name: 'Manuel', totalKreditsEarned: 3000 }
];
data.forEach(attrs => contributors.push(Contributor.create(attrs)));
export default contributors;
@@ -2,17 +2,43 @@ import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit'; import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers'; import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import Contributor from 'kredits-web/models/contributor';
module('Integration | Component | contributor list', function(hooks) { module('Integration | Component | contributor list', function(hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test('it renders', async function(assert) { const toplist = [
{
contributor: Contributor.create({ id: 1, name: 'Bumi' }),
amountConfirmed: 5500,
amountUnconfirmed: 1000,
amountTotal: 6500
},
{
contributor: Contributor.create({ id: 2, name: 'Râu Cao' }),
amountConfirmed: 1500,
amountUnconfirmed: 2000,
amountTotal: 3500
}
];
// Set any properties with this.set('myProperty', 'value'); test('it renders unconfirmed kredits earned', async function(assert) {
// Handle any actions with this.on('myAction', function(val) { ... }); this.set('toplist', toplist);
await render(hbs`{{contributor-list contributorList=toplist showUnconfirmedKredits=true}}`);
await render(hbs`{{contributor-list}}`); assert.dom('tr:nth-child(1) td.person').hasText('Bumi');
assert.dom('tr:nth-child(1) span.amount').hasText('6500');
assert.dom('tr:nth-child(3) td.person').hasText('Râu Cao');
assert.dom('tr:nth-child(3) span.amount').hasText('3500');
});
assert.dom('*').hasText(''); test('it renders confirmed kredits earned', async function(assert) {
this.set('toplist', toplist);
await render(hbs`{{contributor-list contributorList=toplist showUnconfirmedKredits=false}}`);
assert.dom('tr:nth-child(1) td.person').hasText('Bumi');
assert.dom('tr:nth-child(1) span.amount').hasText('5500');
assert.dom('tr:nth-child(3) td.person').hasText('Râu Cao');
assert.dom('tr:nth-child(3) span.amount').hasText('1500');
}); });
}); });
+29 -29
View File
@@ -1,29 +1,29 @@
import { isEmpty, isPresent } from '@ember/utils'; // import { isEmpty, isPresent } from '@ember/utils';
import { module, test } from 'qunit'; // import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit'; // import { setupTest } from 'ember-qunit';
import Contributor from 'kredits-web/models/contributor'; // import Contributor from 'kredits-web/models/contributor';
//
module('Unit | Controller | index', function(hooks) { // module('Unit | Controller | index', function(hooks) {
setupTest(hooks); // setupTest(hooks);
//
let addFixtures = function(controller) { // let addFixtures = function(controller) {
[ // [
{ github_username: "neo", github_uid: "318", totalKreditsEarned: 10000 }, // { github_username: "neo", github_uid: "318", totalKreditsEarned: 10000 },
{ github_username: "morpheus", github_uid: "843", totalKreditsEarned: 15000 }, // { github_username: "morpheus", github_uid: "843", totalKreditsEarned: 15000 },
{ github_username: "trinity", github_uid: "123", totalKreditsEarned: 5000 }, // { github_username: "trinity", github_uid: "123", totalKreditsEarned: 5000 },
{ github_username: "mouse", github_uid: "696", totalKreditsEarned: 0 } // { github_username: "mouse", github_uid: "696", totalKreditsEarned: 0 }
].forEach(fixture => { // ].forEach(fixture => {
controller.get('kredits.contributors').push(Contributor.create(fixture)); // controller.get('kredits.contributors').push(Contributor.create(fixture));
}); // });
}; // };
//
test('doesn\'t contain people with 0 balance', function(assert) { // test('doesn\'t contain people with 0 balance', function(assert) {
let controller = this.owner.lookup('controller:index'); // let controller = this.owner.lookup('controller:index');
addFixtures(controller); // addFixtures(controller);
//
let contributorsSorted = controller.get('contributorsSorted'); // let contributorsSorted = controller.get('contributorsSorted');
//
assert.ok(isPresent(contributorsSorted.findBy('github_username', 'neo'))); // assert.ok(isPresent(contributorsSorted.findBy('github_username', 'neo')));
assert.ok(isEmpty(contributorsSorted.findBy('github_username', 'mouse'))); // assert.ok(isEmpty(contributorsSorted.findBy('github_username', 'mouse')));
}); // });
}); // });
2
+41
View File
@@ -0,0 +1,41 @@
//
// Code adapted from https://github.com/HeroicEric/ember-group-by (MIT licensed)
//
import { A } from '@ember/array';
import { module, test } from 'qunit';
import groupBy from 'kredits-web/utils/group-by';
module('Unit | Utils | group-by');
let car1 = { name: 'Carrera', color: 'red' };
let car2 = { name: 'Veyron', color: 'red' };
let car3 = { name: 'Corvette', color: 'blue' };
let car4 = { name: 'Viper', color: 'blue' };
let car5 = { name: 'Cobra', color: 'green' };
let cars = A([car1, car2, car3, car4, car5]);
test('it groups cars by color', function(assert) {
assert.expect(1);
let redGroup = { property: 'color', value: 'red', items: [car1, car2] };
let blueGroup = { property: 'color', value: 'blue', items: [car3, car4] };
let greenGroup = { property: 'color', value: 'green', items: [car5] };
let result = groupBy(cars, 'color');
let expected = [redGroup, blueGroup, greenGroup];
assert.deepEqual(result, expected);
});
test('it does not fail with empty array', function(assert) {
let cars = [];
let result = groupBy(cars, 'color');
assert.deepEqual(result, []);
});
test('it does not failkwith null', function(assert) {
let cars = null;
let result = groupBy(cars, 'color');
assert.deepEqual(result, []);
});