Commit 3537477f authored by Dirk Luijk's avatar Dirk Luijk

Separated frontend and backend and improved styling of frontend app

parent dcd08f07
......@@ -22,43 +22,63 @@ Stap voor stap gaan we de blockchain opbouwen. We gaan de volgende functionalite
## Initiele build
## Backend
Nadat je het project uit github hebt uitgecheckt, dien je het project eenmalig te bouwen:
Nadat je het project uit git hebt uitgecheckt, dien je eerst de backend te builden:
```
gradlew build
```
Dit duurt ongeveer 2 minuten. Je kan nu het project starten:
Dit duurt ongeveer 1 minuut. Je kan nu de backend starten met:
```
gradlew bootRun
```
Dit start een SpringBoot applicatie op poort 8080 op. Ga naar http://localhost:8080/index.html om de UI te zien. Hier zie je de volgende tabs:
Dit start een SpringBoot REST applicatie op poort 8080 op. De volgende endpoints zijn beschikbaar:
1. *Pending transactions*. Hier zie je alle transacties die nog niet in de blockchain opgenomen zijn.
2. *New transaction*. Hier kan je een nieuwe transactie het netwerk inschieten.
3. B*lockchain*. Hier zie je de huidige blockchain.
4. *Peers*. Hier staat een overzicht welke peers er allemaal aan deze node verbonden zijn.
5. *Mine*. Hier kan je een mining actie starten.
6. *Wallet*. Hier kan je een wallet bekijken.
* `GET /api/pendingtransactions`: alle transacties die nog niet in de blockchain opgenomen zijn
* `POST /api/blockchain`: nieuwe transactie het netwerk inschieten
* `GET /api/wallet/{walletId}`: de huidige blockchain
* `GET /api/peers`: overzicht welke peers er allemaal aan deze node verbonden zijn
* `POST /api/mine`: een mining actie starten
Uiteraard kan je middels Postman deze endpoints ook benaderen. De frontend is dus alleen ter convenience.
Dit correspondeert met de volgende endpoints:
## Frontend
Ga naar de frontend directory (`cd frontend`) en installeer je dependencies:
```
npm install
```
GET /api/pendingtransactions
POST /api/newtransaction
GET /api/blockchain
GET /api/wallet/{walletId}
GET /api/peers
POST /api/mine
of, als je `yarn` geïnstalleerd hebt staan:
```
yarn
```
Uiteraard kan je middels Postman deze api's ook benaderen. De Frontend is dus alleen ter convenience.
Dit kan enkele minuten duren. Start nu de frontend op:
```
npm start
```
of, als je `yarn` geïnstalleerd hebt staan:
```
yarn run start
```
Ga naar http://localhost:4200 om de UI te zien. Hier zie je de volgende tabs:
1. *Pending transactions*
2. *New transaction*
3. *Blockchain*
4. *Peers*
5. *Mine*
6. *Wallet*. Hier kan je een wallet bekijken.
Deze corresponderen met de API endpoints.
## Bouwen van het block
......
......@@ -13,8 +13,6 @@ buildscript {
plugins {
id 'org.springframework.boot' version '1.5.9.RELEASE'
id 'com.moowork.node' version '1.2.0'
}
apply plugin: 'java'
......@@ -25,39 +23,6 @@ group = 'nl.craftsmen.craftsmencoinnode'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
processResources {
from ('frontend/dist/') {
into 'public'
}
}
// configure gradle-node-plugin
node {
//version = '8.9.4'
//npmVersion = '5.6.0'
download = true
workDir = file("${project.projectDir}/frontend/node")
nodeModulesDir = file("${project.projectDir}/frontend")
}
// clean node/node_modules/dist
task npmClean(type: Delete) {
final def webDir = "${rootDir}/frontend"
delete "${webDir}/node"
delete "${webDir}/node_modules"
delete "${webDir}/dist"
delete "${webDir}/coverage"
delete "${webDir}/build"
}
clean.dependsOn(npmClean)
build.dependsOn(npm_install)
build.dependsOn(npm_run_build)
//processResources.dependsOn(npm_run_build)
repositories {
mavenCentral()
}
......@@ -69,7 +34,6 @@ bootRun {
args += ["--server.port=${project.port}"]
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compile 'commons-codec:commons-codec:1.10'
......@@ -77,6 +41,4 @@ dependencies {
compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.4'
compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.7.0'
compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.7.0'
}
......@@ -19,7 +19,7 @@
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles.css"
"styles.scss"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
......@@ -54,7 +54,8 @@
}
},
"defaults": {
"styleExt": "css",
"component": {}
"styleExt": "scss",
"component": {
}
}
}
......@@ -4,7 +4,7 @@
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve",
"start": "ng serve --aot -pc proxy.conf.json --open",
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
......@@ -33,6 +33,7 @@
"@types/jasmine": "~2.8.3",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~6.0.60",
"bootswatch": "^4.0.0-beta.3",
"codelyzer": "^4.0.1",
"jasmine-core": "~2.8.0",
"jasmine-spec-reporter": "~4.2.1",
......
{
"/api/*": {
"target": "http://localhost:8080",
"secure": false
}
}
import {Injectable} from '@angular/core';
import {Transaction} from './transaction';
import {TransactionResult} from './transactionresult';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import {TRANSACTIONS} from './mock-transactions';
import {Observable} from 'rxjs/Observable';
import {of} from 'rxjs/observable/of';
import {MessageService} from './message.service';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import { Transaction } from './transaction';
import { TransactionResult } from './transaction-result';
import { MessageService } from '../messages/message.service';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
// import {TRANSACTIONS} from './mock-transactions';
// import {of} from 'rxjs/observable/of';
@Injectable()
export class TransactionService {
private transactionsUrl = '/api/pendingtransactions'; // URL to web api
private transactionUrl = '/api/newtransaction';
constructor(private http: HttpClient, private messageService: MessageService) {
}
constructor(private http: HttpClient, private messageService: MessageService) {}
// getTransactions(): Observable<Transaction[]> {
// return of(TRANSACTIONS);
// }
public getTransactions(): Observable<Transaction[]> {
getTransactions(): Observable<Transaction[]> {
this.log('TransactionService: fetching transactions heroes');
return this.http.get<Transaction[]>(this.transactionsUrl)
return this.http.get<Transaction[]>('/api/pendingtransactions')
}
public save (transaction: Transaction): Observable<TransactionResult> {
save(transaction: Transaction): Observable<TransactionResult> {
this.log('Sending new transaction');
return this.http.post<TransactionResult>(this.transactionUrl, transaction, httpOptions);
return this.http.post<TransactionResult>('/api/newtransaction', transaction)
.do(result => this.log(result.message));
}
/** Log a HeroService message with the MessageService */
......
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TransactionsComponent } from './transactions/transactions.component';
import { TransactionComponent } from './transaction/transaction.component';
import { DashboardComponent } from './dashboard/dashboard.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'pendingtransactions', component: TransactionsComponent },
{ path: 'newtransaction', component: TransactionComponent },
{ path: 'dashboard', component: DashboardComponent }
];
@NgModule({
exports: [ RouterModule ],
imports: [ RouterModule.forRoot(routes) ]
})
export class AppRoutingModule {
}
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
<h1>
{{ title }}
</h1>
</div>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/pendingtransactions">Pending transactions</a>
<a routerLink="/newtransaction">New transaction</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
<div class="container">
<h1>CraftsCoin</h1>
<p class="lead">Node interface</p>
<nav>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" [routerLink]="['dashboard']" routerLinkActive="active">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" [routerLink]="['transactions', 'pending']" routerLinkActive="active">Pending transactions</a>
</li>
<li class="nav-item">
<a class="nav-link" [routerLink]="['transactions', 'new']" routerLinkActive="active">New transaction</a>
</li>
</ul>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
</div>
nav {
margin-bottom: 2rem;
}
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});
......@@ -3,8 +3,6 @@ import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'CraftsCoin node interface';
}
export class AppComponent {}
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { TransactionsComponent } from './transactions/transactions.component';
import { TransactionComponent } from './transaction/transaction.component';
import { FormsModule } from '@angular/forms';
import {TransactionService} from "./transaction.service";
import { TransactionService } from './api/transaction.service';
import { MessagesComponent } from './messages/messages.component';
import { MessageService } from './message.service';
import { AppRoutingModule } from './/app-routing.module';
import { MessageService } from './messages/message.service';
import { DashboardComponent } from './dashboard/dashboard.component';
import {HttpClientModule} from "@angular/common/http";
import {HttpClientInMemoryWebApiModule} from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
// import {HttpClientInMemoryWebApiModule} from 'angular-in-memory-web-api';
// import { InMemoryDataService } from './api/in-memory-data.service';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'transactions/pending', component: TransactionsComponent },
{ path: 'transactions/new', component: TransactionComponent },
];
@NgModule({
declarations: [
......@@ -25,7 +32,7 @@ import { InMemoryDataService } from './in-memory-data.service';
imports: [
BrowserModule,
FormsModule,
AppRoutingModule,
RouterModule.forRoot(routes),
HttpClientModule
// ,HttpClientInMemoryWebApiModule.forRoot(
// InMemoryDataService, { dataEncapsulation: false }
......@@ -34,4 +41,5 @@ import { InMemoryDataService } from './in-memory-data.service';
providers: [TransactionService, MessageService],
bootstrap: [AppComponent]
})
export class AppModule { }
export class AppModule {
}
/* DashboardComponent's private CSS styles */
[class*='col-'] {
float: left;
padding-right: 20px;
padding-bottom: 20px;
}
[class*='col-']:last-of-type {
padding-right: 0;
}
a {
text-decoration: none;
}
*, *:after, *:before {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
h3 {
text-align: center; margin-bottom: 0;
}
h4 {
position: relative;
}
.grid {
margin: 0;
}
.col-1-4 {
width: 25%;
}
.module {
padding: 20px;
text-align: center;
color: #eee;
max-height: 120px;
min-width: 120px;
background-color: #607D8B;
border-radius: 2px;
}
.module:hover {
background-color: #EEE;
cursor: pointer;
color: #607d8b;
}
.grid-pad {
padding: 10px 0;
}
.grid-pad > [class*='col-']:last-of-type {
padding-right: 20px;
}
@media (max-width: 600px) {
.module {
font-size: 10px;
max-height: 75px; }
}
@media (max-width: 1024px) {
.grid {
margin: 0;
}
.module {
min-width: 60px;
}
}
<h3>Top Transactions</h3>
<div class="grid grid-pad">
<a *ngFor="let transaction of transactions" class="col-1-4">
<div class="module hero">
<h4>{{transaction.id}}</h4>
</div>
</a>
<h2>Top transactions</h2>
<div *ngIf="transactions.length === 0" class="alert alert-info">
No transactions available.
</div>
<ul *ngIf="transactions.length > 0">
<li *ngFor="let transaction of transactions">
{{transaction.id}}
</li>
</ul>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
import { Transaction } from '../transaction';
import { TransactionService } from '../transaction.service';
import { Transaction } from '../api/transaction';
import { TransactionService } from '../api/transaction.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: [ './dashboard.component.css' ]
templateUrl: './dashboard.component.html'
})
export class DashboardComponent implements OnInit {
transactions: Transaction[] = [];
......
import { TestBed, inject } from '@angular/core/testing';
import { MessageService } from './message.service';
describe('MessageService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MessageService]
});
});
it('should be created', inject([MessageService], (service: MessageService) => {
expect(service).toBeTruthy();
}));
});
import { Injectable } from '@angular/core';
@Injectable()
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}
import { Injectable } from '@angular/core';
import { Observable, ReplaySubject } from 'rxjs';
@Injectable()
export class MessageService {
messages$: Observable<string[]>;
private messages: string[] = [];
private messagesSubject = new ReplaySubject<string[]>();
constructor() {
this.messages$ = this.messagesSubject.asObservable();
}
add(message: string): void {
this.messages.push(message);
this.messagesSubject.next(this.messages);
}
clear(): void {
this.messages = [];
this.messagesSubject.next(this.messages);
}
}
<div *ngIf="messageService.messages.length">
<h2>Messages</h2>
<h2>Messages</h2>
<button class="clear"
(click)="messageService.clear()">clear</button>
<div *ngFor='let message of messageService.messages'> {{message}} </div>
<div *ngIf="messages.length === 0" class="alert alert-info">
No messages.
</div>
<div *ngIf="messages.length > 0">
<button class="btn btn-secondary" (click)="clear()">Clear messages</button>
<pre>
<div *ngFor="let message of messages">{{ message }}</div>
</pre>
</div>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MessagesComponent } from './messages.component';
describe('MessagesComponent', () => {
let component: MessagesComponent;
let fixture: ComponentFixture<MessagesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MessagesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MessagesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
import { MessageService } from '../message.service';
import { MessageService } from './message.service';
@Component({
selector: 'app-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.css']
templateUrl: './messages.component.html'
})
export class MessagesComponent implements OnInit {
messages: string[] = [];
constructor(public messageService: MessageService) {}
constructor(private messageService: MessageService) {}
ngOnInit() {
ngOnInit(): void {
this.messageService.messages$.subscribe(messages => this.messages = messages);
}
clear(): void {
this.messageService.clear();
}
}
import { TestBed, inject } from '@angular/core/testing';
import { TransactionService } from './transaction.service';
describe('TransactionService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [TransactionService]
});
});
it('should be created', inject([TransactionService], (service: TransactionService) => {
expect(service).toBeTruthy();
}));
});
<h2>New transaction</h2>
<table>
<tr>
<td>from:</td>
<td><input [(ngModel)]="newTransaction.from" placeholder="from"></td>
<td><input [(ngModel)]="newTransaction.from" placeholder="from" class="form-control"></td>
</tr>
<tr>
<td>to:</td>
<td><input [(ngModel)]="newTransaction.to" placeholder="to"></td>
<td><input [(ngModel)]="newTransaction.to" placeholder="to" class="form-control"></td>
</tr>
<tr>
<td>amount:</td>
<td><input [(ngModel)]="newTransaction.amount" placeholder="amount"></td>
</tr>
<tr>
<td span="2"><button (click)="save()" value="Submit"></button></td>
<td><input [(ngModel)]="newTransaction.amount" placeholder="amount" class="form-control"></td>
</tr>
</table>
<button class="btn btn-primary" (click)="save()">Save</button>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TransactionComponent } from './transaction.component';
describe('TransactionComponent', () => {