summaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
authorDessalines <tyhou13@gmx.com>2019-03-20 18:22:31 -0700
committerDessalines <tyhou13@gmx.com>2019-03-20 18:22:31 -0700
commit816aa0b15f3766e340d8722f03e8b3a7633ab6fb (patch)
tree23dd0fc329e8f08c71dc6f10dd398b35d92c047c /ui
parent064d7f84b25236195eeb33a8671935bc9df37e57 (diff)
Adding initial UI and Websocket server.
Diffstat (limited to 'ui')
-rw-r--r--ui/.gitignore30
-rw-r--r--ui/assets/favicon.icobin0 -> 1150 bytes
-rw-r--r--ui/fuse.js55
-rw-r--r--ui/package.json31
-rw-r--r--ui/src/components/home.tsx14
-rw-r--r--ui/src/components/login.tsx145
-rw-r--r--ui/src/components/navbar.tsx38
-rw-r--r--ui/src/components/search.tsx205
-rw-r--r--ui/src/env.ts3
-rw-r--r--ui/src/index.html19
-rw-r--r--ui/src/index.tsx42
-rw-r--r--ui/src/interfaces.ts14
-rw-r--r--ui/src/main.css0
-rw-r--r--ui/src/services.ts57
-rw-r--r--ui/src/utils.ts2
-rw-r--r--ui/tsconfig.json12
-rw-r--r--ui/tslint.json28
-rw-r--r--ui/yarn.lock3084
18 files changed, 3779 insertions, 0 deletions
diff --git a/ui/.gitignore b/ui/.gitignore
new file mode 100644
index 00000000..cc0ab540
--- /dev/null
+++ b/ui/.gitignore
@@ -0,0 +1,30 @@
+dist
+.fusebox
+_site
+.alm
+.history
+.git
+build
+.build
+.git
+.history
+.idea
+.jshintrc
+.nyc_output
+.sass-cache
+.vscode
+build
+coverage
+jsconfig.json
+Gemfile.lock
+node_modules
+.DS_Store
+*.map
+*.log
+*.swp
+*~
+test/data/result.json
+
+package-lock.json
+*.orig
+
diff --git a/ui/assets/favicon.ico b/ui/assets/favicon.ico
new file mode 100644
index 00000000..13f310e9
--- /dev/null
+++ b/ui/assets/favicon.ico
Binary files differ
diff --git a/ui/fuse.js b/ui/fuse.js
new file mode 100644
index 00000000..ff1e6d15
--- /dev/null
+++ b/ui/fuse.js
@@ -0,0 +1,55 @@
+const {
+ FuseBox,
+ Sparky,
+ EnvPlugin,
+ CSSPlugin,
+ WebIndexPlugin,
+ QuantumPlugin
+} = require('fuse-box');
+// const transformInferno = require('../../dist').default
+const transformInferno = require('ts-transform-inferno').default;
+const transformClasscat = require('ts-transform-classcat').default;
+let fuse, app;
+let isProduction = false;
+
+Sparky.task('config', _ => {
+ fuse = new FuseBox({
+ homeDir: 'src',
+ hash: isProduction,
+ output: 'dist/$name.js',
+ experimentalFeatures: true,
+ cache: !isProduction,
+ sourceMaps: !isProduction,
+ transformers: {
+ before: [transformClasscat(), transformInferno()],
+ },
+ plugins: [
+ EnvPlugin({ NODE_ENV: isProduction ? 'production' : 'development' }),
+ CSSPlugin(),
+ WebIndexPlugin({
+ title: 'Inferno Typescript FuseBox Example',
+ template: 'src/index.html',
+ path: isProduction ? "/static" : "/"
+ }),
+ isProduction &&
+ QuantumPlugin({
+ bakeApiIntoBundle: 'app',
+ treeshake: true,
+ uglify: true,
+ }),
+ ],
+ });
+ app = fuse.bundle('app').instructions('>index.tsx');
+});
+Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/'));
+Sparky.task('env', _ => (isProduction = true));
+Sparky.task('copy-assets', () => Sparky.src('assets/*.ico').dest('dist/'));
+Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => {
+ fuse.dev();
+ app.hmr().watch();
+ return fuse.run();
+});
+Sparky.task('prod', ['clean', 'env', 'config', 'copy-assets'], _ => {
+ // fuse.dev({ reload: true }); // remove after demo
+ return fuse.run();
+});
diff --git a/ui/package.json b/ui/package.json
new file mode 100644
index 00000000..ca4fa368
--- /dev/null
+++ b/ui/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "rust_reddit_fediverse",
+ "version": "1.0.0",
+ "description": "A simple UI for rust_reddit_fediverse",
+ "main": "index.js",
+ "scripts": {
+ "start": "node fuse dev",
+ "build": "node fuse prod"
+ },
+ "keywords": [],
+ "author": "Dessalines",
+ "license": "GPL-2.0-or-later",
+ "engines": {
+ "node": ">=8.9.0"
+ },
+ "engineStrict": true,
+ "dependencies": {
+ "classcat": "^1.1.3",
+ "dotenv": "^6.1.0",
+ "inferno": "^7.0.1",
+ "inferno-router": "^7.0.1",
+ "moment": "^2.22.2"
+ },
+ "devDependencies": {
+ "fuse-box": "3.1.3",
+ "ts-transform-classcat": "^0.0.2",
+ "ts-transform-inferno": "^4.0.2",
+ "typescript": "^3.3.3333",
+ "uglify-es": "^3.3.9"
+ }
+}
diff --git a/ui/src/components/home.tsx b/ui/src/components/home.tsx
new file mode 100644
index 00000000..07cb94f5
--- /dev/null
+++ b/ui/src/components/home.tsx
@@ -0,0 +1,14 @@
+import { Component } from 'inferno';
+import { repoUrl } from '../utils';
+
+export class Home extends Component<any, any> {
+
+ render() {
+ return (
+ <div class="container">
+ hola this is me.
+ </div>
+ )
+ }
+
+}
diff --git a/ui/src/components/login.tsx b/ui/src/components/login.tsx
new file mode 100644
index 00000000..fd6f5045
--- /dev/null
+++ b/ui/src/components/login.tsx
@@ -0,0 +1,145 @@
+import { Component, linkEvent } from 'inferno';
+
+import { LoginForm, RegisterForm } from '../interfaces';
+import { WebSocketService } from '../services';
+
+interface State {
+ loginForm: LoginForm;
+ registerForm: RegisterForm;
+}
+
+let emptyState: State = {
+ loginForm: {
+ username: null,
+ password: null
+ },
+ registerForm: {
+ username: null,
+ password: null,
+ password_verify: null
+ }
+}
+
+export class Login extends Component<any, State> {
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = emptyState;
+
+ }
+ render() {
+ return (
+ <div class="container">
+ <div class="row">
+ <div class="col-12 col-lg-6 mb-4">
+ {this.loginForm()}
+ </div>
+ <div class="col-12 col-lg-6">
+ {this.registerForm()}
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ loginForm() {
+ return (
+ <div>
+ <form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
+ <h3>Login</h3>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">Email or Username</label>
+ <div class="col-sm-10">
+ <input type="text" class="form-control" value={this.state.loginForm.username} onInput={linkEvent(this, this.handleLoginUsernameChange)} required minLength={3} />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">Password</label>
+ <div class="col-sm-10">
+ <input type="password" value={this.state.loginForm.password} onInput={linkEvent(this, this.handleLoginPasswordChange)} class="form-control" required />
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <button type="submit" class="btn btn-secondary">Login</button>
+ </div>
+ </div>
+ </form>
+ Forgot your password or deleted your account? Reset your password. TODO
+ </div>
+ );
+ }
+ registerForm() {
+ return (
+ <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
+ <h3>Sign Up</h3>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">Username</label>
+ <div class="col-sm-10">
+ <input type="text" class="form-control" value={this.state.registerForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">Email</label>
+ <div class="col-sm-10">
+ <input type="email" class="form-control" value={this.state.registerForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">Password</label>
+ <div class="col-sm-10">
+ <input type="password" value={this.state.registerForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-sm-2 col-form-label">Verify Password</label>
+ <div class="col-sm-10">
+ <input type="password" value={this.state.registerForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required />
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-sm-10">
+ <button type="submit" class="btn btn-secondary">Sign Up</button>
+ </div>
+ </div>
+ </form>
+ );
+ }
+
+ handleLoginSubmit(i: Login, event) {
+ console.log(i.state);
+ event.preventDefault();
+ WebSocketService.Instance.login(i.state.loginForm);
+ }
+
+ handleLoginUsernameChange(i: Login, event) {
+ i.state.loginForm.username = event.target.value;
+ }
+
+ handleLoginPasswordChange(i: Login, event) {
+ i.state.loginForm.password = event.target.value;
+ }
+
+ handleRegisterSubmit(i: Login, event) {
+ console.log(i.state);
+ event.preventDefault();
+ WebSocketService.Instance.register(i.state.registerForm);
+ }
+
+ handleRegisterUsernameChange(i: Login, event) {
+ i.state.registerForm.username = event.target.value;
+ }
+
+ handleRegisterEmailChange(i: Login, event) {
+ i.state.registerForm.email = event.target.value;
+ }
+
+ handleRegisterPasswordChange(i: Login, event) {
+ i.state.registerForm.password = event.target.value;
+ }
+
+ handleRegisterPasswordVerifyChange(i: Login, event) {
+ i.state.registerForm.password_verify = event.target.value;
+ }
+}
diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx
new file mode 100644
index 00000000..86d5d1d2
--- /dev/null
+++ b/ui/src/components/navbar.tsx
@@ -0,0 +1,38 @@
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import { repoUrl } from '../utils';
+
+export class Navbar extends Component<any, any> {
+
+ constructor(props, context) {
+ super(props, context);
+ }
+
+ render() {
+ return (
+ <div class="sticky-top">{this.navbar()}</div>
+ )
+ }
+
+ // TODO class active corresponding to current page
+ navbar() {
+ return (
+ <nav class="navbar navbar-light bg-light p-0 px-3 shadow">
+ <a class="navbar-brand mx-1" href="#">
+ rrf
+ </a>
+ <ul class="navbar-nav mr-auto">
+ <li class="nav-item">
+ <a class="nav-item nav-link" href={repoUrl}>github</a>
+ </li>
+ </ul>
+ <ul class="navbar-nav ml-auto mr-2">
+ <li class="nav-item">
+ <Link class="nav-item nav-link" to="/login">Login</Link>
+ </li>
+ </ul>
+ </nav>
+ );
+ }
+
+}
diff --git a/ui/src/components/search.tsx b/ui/src/components/search.tsx
new file mode 100644
index 00000000..080761f9
--- /dev/null
+++ b/ui/src/components/search.tsx
@@ -0,0 +1,205 @@
+import { Component, linkEvent } from 'inferno';
+import * as moment from 'moment';
+
+import { endpoint } from '../env';
+import { SearchParams, Results, Torrent } from '../interfaces';
+import { humanFileSize, magnetLink, getFileName } from '../utils';
+
+interface State {
+ results: Results;
+ searchParams: SearchParams;
+ searching: Boolean;
+}
+
+export class Search extends Component<any, State> {
+
+ state: State = {
+ results: {
+ torrents: []
+ },
+ searchParams: {
+ q: "",
+ page: 1,
+ type_: 'torrent'
+ },
+ searching: false
+ };
+
+ constructor(props, context) {
+ super(props, context);
+ }
+
+ componentDidMount() {
+ this.state.searchParams = {
+ page: Number(this.props.match.params.page),
+ q: this.props.match.params.q,
+ type_: this.props.match.params.type_
+ }
+ this.search();
+ }
+
+ // Re-do search if the props have changed
+ componentDidUpdate(lastProps, lastState, snapshot) {
+ if (lastProps.match && lastProps.match.params !== this.props.match.params) {
+ this.state.searchParams = {
+ page: Number(this.props.match.params.page),
+ q: this.props.match.params.q,
+ type_: this.props.match.params.type_
+ }
+ this.search();
+ }
+
+ }
+
+ search() {
+ if (!!this.state.searchParams.q) {
+ this.setState({ searching: true, results: { torrents: [] } });
+ this.fetchData(this.state.searchParams)
+ .then(torrents => {
+ if (!!torrents) {
+ this.setState({
+ results: {
+ torrents: torrents
+ }
+ });
+ }
+ }).catch(error => {
+ console.error('request failed', error);
+ }).then(() => this.setState({ searching: false }));
+ } else {
+ this.setState({ results: { torrents: [] } });
+ }
+ }
+
+ fetchData(searchParams: SearchParams): Promise<Array<Torrent>> {
+ let q = encodeURI(searchParams.q);
+ return fetch(`${endpoint}/service/search?q=${q}&page=${searchParams.page}&type_=${searchParams.type_}`)
+ .then(data => data.json());
+ }
+
+ render() {
+ return (
+ <div>
+ {
+ this.state.searching ?
+ this.spinner() : this.state.results.torrents[0] ?
+ this.torrentsTable()
+ : this.noResults()
+ }
+ </div>
+ );
+ }
+
+ spinner() {
+ return (
+ <div class="text-center m-5 p-5">
+ <svg class="icon icon-spinner spinner"><use xlinkHref="#icon-spinner"></use></svg>
+ </div>
+ );
+ }
+
+ noResults() {
+ return (
+ <div class="text-center m-5 p-5">
+ <h1>No Results</h1>
+ </div>
+ )
+ }
+
+ torrentsTable() {
+ return (
+ <div>
+ <table class="table table-fixed table-hover table-sm table-striped table-hover-purple table-padding">
+ <thead>
+ <tr>
+ <th class="search-name-col">Name</th>
+ <th class="text-right">Size</th>
+ <th class="text-right">Seeds</th>
+ <th class="text-right d-none d-md-table-cell">Leeches</th>
+ <th class="text-right d-none d-md-table-cell">Created</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {this.state.results.torrents.map(torrent => (
+ <tr>
+ { !torrent.name ? (
+ <td className="path_column">
+ <a class="text-body"
+ href={magnetLink(torrent.infohash, torrent.path, torrent.index_)}>
+ {getFileName(torrent.path)}
+ </a>
+ </td>
+ ) : (
+ <td class="search-name-cell">
+ <a class="text-body"
+ href={magnetLink(torrent.infohash, torrent.name, torrent.index_)}>
+ {torrent.name}
+ </a>
+ </td>
+ )}
+ <td class="text-right text-muted">{humanFileSize(torrent.size_bytes, true)}</td>
+ <td class="text-right text-success">
+ <svg class="icon icon-arrow-up d-none d-sm-inline mr-1"><use xlinkHref="#icon-arrow-up"></use></svg>
+ {torrent.seeders}
+ </td>
+ <td class="text-right text-danger d-none d-md-table-cell">
+ <svg class="icon icon-arrow-down mr-1"><use xlinkHref="#icon-arrow-down"></use></svg>
+ {torrent.leechers}
+ </td>
+ <td class="text-right text-muted d-none d-md-table-cell"
+ data-balloon={`Scraped ${moment(torrent.scraped_date * 1000).fromNow()}`}
+ data-balloon-pos="down">
+ {moment(torrent.created_unix * 1000).fromNow()}
+ </td>
+ <td class="text-right">
+ <a class="btn btn-sm no-outline p-1"
+ href={magnetLink(torrent.infohash, (torrent.name) ? torrent.name : torrent.path, torrent.index_)}
+ data-balloon="Magnet link"
+ data-balloon-pos="left">
+ <svg class="icon icon-magnet"><use xlinkHref="#icon-magnet"></use></svg>
+ </a>
+ <a class="btn btn-sm no-outline p-1 d-none d-sm-inline"
+ href={`https://gitlab.com/dessalines/torrents.csv/issues/new?issue[title]=Report%20Torrent%20infohash%20${torrent.infohash}`}
+ target="_blank"
+ data-balloon="Report Torrent"
+ data-balloon-pos="left">
+ <svg class="icon icon-flag"><use xlinkHref="#icon-flag"></use></svg>
+ </a>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ {this.paginator()}
+ </div>
+ );
+ }
+
+ paginator() {
+ return (
+ <nav>
+ <ul class="pagination justify-content-center">
+ <li className={(this.state.searchParams.page == 1) ? "page-item disabled" : "page-item"}>
+ <button class="page-link"
+ onClick={linkEvent({ i: this, nextPage: false }, this.switchPage)}>
+ Previous
+ </button>
+ </li>
+ <li class="page-item">
+ <button class="page-link"
+ onClick={linkEvent({ i: this, nextPage: true }, this.switchPage)}>
+ Next
+ </button>
+ </li>
+ </ul>
+ </nav>
+ );
+ }
+
+ switchPage(a: { i: Search, nextPage: boolean }, event) {
+ let newSearch = a.i.state.searchParams;
+ newSearch.page += (a.nextPage) ? 1 : -1;
+ a.i.props.history.push(`/search/${newSearch.type_}/${newSearch.q}/${newSearch.page}`);
+ }
+}
diff --git a/ui/src/env.ts b/ui/src/env.ts
new file mode 100644
index 00000000..a8e72d90
--- /dev/null
+++ b/ui/src/env.ts
@@ -0,0 +1,3 @@
+// export const endpoint = window.location.origin;
+export const endpoint = "http://localhost:8080";
+export let wsUri = (window.location.protocol=='https:') ? 'wss://' : 'ws://' + endpoint.substr(7) + '/service/ws';
diff --git a/ui/src/index.html b/ui/src/index.html
new file mode 100644
index 00000000..2a3e4198
--- /dev/null
+++ b/ui/src/index.html
@@ -0,0 +1,19 @@
+<html lang="en">
+
+<head>
+ <!-- Required meta tags -->
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <link rel="shortcut icon" type="image/ico" href="/static/assets/favicon.ico" />
+
+ <title>rust-reddit-fediverse</title>
+ <link rel="stylesheet" href="https://bootswatch.com/4/darkly/bootstrap.min.css">
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/balloon-css/0.5.0/balloon.min.css">
+</head>
+
+<body>
+ <div id="app"></div>
+ $bundles
+</body>
+
+</html>
diff --git a/ui/src/index.tsx b/ui/src/index.tsx
new file mode 100644
index 00000000..36e5681d
--- /dev/null
+++ b/ui/src/index.tsx
@@ -0,0 +1,42 @@
+import { render, Component } from 'inferno';
+import { HashRouter, Route, Switch } from 'inferno-router';
+
+import { Navbar } from './components/navbar';
+import { Home } from './components/home';
+import { Login } from './components/login';
+
+import './main.css';
+
+import { WebSocketService } from './services';
+
+const container = document.getElementById('app');
+
+class Index extends Component<any, any> {
+
+ constructor(props, context) {
+ super(props, context);
+ WebSocketService.Instance;
+ }
+
+ render() {
+ return (
+ <HashRouter>
+ <Navbar />
+ <div class="mt-3 p-0">
+ <Switch>
+ <Route exact path="/" component={Home} />
+ <Route path={`/login`} component={Login} />
+ {/*
+ <Route path={`/search/:type_/:q/:page`} component={Search} />
+ <Route path={`/submit`} component={Submit} />
+ <Route path={`/user/:id`} component={Login} />
+ <Route path={`/community/:id`} component={Login} />
+ */}
+ </Switch>
+ </div>
+ </HashRouter>
+ );
+ }
+}
+
+render(<Index />, container);
diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts
new file mode 100644
index 00000000..c1550cc1
--- /dev/null
+++ b/ui/src/interfaces.ts
@@ -0,0 +1,14 @@
+export interface LoginForm {
+ username: string;
+ password: string;
+}
+export interface RegisterForm {
+ username: string;
+ email?: string;
+ password: string;
+ password_verify: string;
+}
+
+export enum UserOperation {
+ Login, Register
+}
diff --git a/ui/src/main.css b/ui/src/main.css
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ui/src/main.css
diff --git a/ui/src/services.ts b/ui/src/services.ts
new file mode 100644
index 00000000..b9536aed
--- /dev/null
+++ b/ui/src/services.ts
@@ -0,0 +1,57 @@
+import { wsUri } from './env';
+import { LoginForm, RegisterForm, UserOperation } from './interfaces';
+
+export class WebSocketService {
+ private static _instance: WebSocketService;
+ private _ws;
+ private conn: WebSocket;
+
+ private constructor() {
+ console.log("Creating WSS");
+ this.connect();
+ console.log(wsUri);
+ }
+
+ public static get Instance(){
+ return this._instance || (this._instance = new this());
+ }
+
+ private connect() {
+ this.disconnect();
+ this.conn = new WebSocket(wsUri);
+ console.log('Connecting...');
+ this.conn.onopen = (() => {
+ console.log('Connected.');
+ });
+ this.conn.onmessage = (e => {
+ console.log('Received: ' + e.data);
+ });
+ this.conn.onclose = (() => {
+ console.log('Disconnected.');
+ this.conn = null;
+ });
+ }
+ private disconnect() {
+ if (this.conn != null) {
+ console.log('Disconnecting...');
+ this.conn.close();
+ this.conn = null;
+ }
+ }
+
+ public login(loginForm: LoginForm) {
+ this.conn.send(this.wsSendWrapper(UserOperation.Login, loginForm));
+ }
+
+ public register(registerForm: RegisterForm) {
+ this.conn.send(this.wsSendWrapper(UserOperation.Register, registerForm));
+ }
+
+ private wsSendWrapper(op: UserOperation, data: any): string {
+ let send = { op: UserOperation[op], data: data };
+ console.log(send);
+ return JSON.stringify(send);
+ }
+
+
+}
diff --git a/ui/src/utils.ts b/ui/src/utils.ts
new file mode 100644
index 00000000..e141c681
--- /dev/null
+++ b/ui/src/utils.ts
@@ -0,0 +1,2 @@
+export let repoUrl = 'https://github.com/dessalines/rust-reddit-fediverse';
+export let wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/service/ws/';
diff --git a/ui/tsconfig.json b/ui/tsconfig.json
new file mode 100644
index 00000000..f58da758
--- /dev/null
+++ b/ui/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es2015",
+ "sourceMap": true,
+ "inlineSources": true,
+ "jsx": "preserve",
+ "importHelpers": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true
+ }
+}
diff --git a/ui/tslint.json b/ui/tslint.json
new file mode 100644
index 00000000..d3e7a8a9
--- /dev/null
+++ b/ui/tslint.json
@@ -0,0 +1,28 @@
+{
+ "extends": "tslint:recommended",
+ "rules": {
+ "forin": false,
+ "indent": [ true, "tabs" ],
+ "interface-name": false,
+ "ban-types": true,
+ "max-classes-per-file": true,
+ "max-line-length": false,
+ "member-access": true,
+ "member-ordering": false,
+ "no-bitwise": false,
+ "no-conditional-assignment": false,
+ "no-debugger": false,
+ "no-empty": true,
+ "no-namespace": false,
+ "no-unused-expression": true,
+ "object-literal-sort-keys": true,
+ "one-variable-per-declaration": [true, "ignore-for-loop"],
+ "only-arrow-functions": [false],
+ "ordered-imports": true,
+ "prefer-const": true,
+ "prefer-for-of": false,
+ "quotemark": [ true, "single", "jsx-double" ],
+ "trailing-comma": [true, {"multiline": "never", "singleline": "never"}],
+ "variable-name": false
+ }
+}
diff --git a/ui/yarn.lock b/ui/yarn.lock
new file mode 100644
index 00000000..92a1250a
--- /dev/null
+++ b/ui/yarn.lock
@@ -0,0 +1,3084 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/runtime@^7.1.2":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83"
+ integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==
+ dependencies:
+ regenerator-runtime "^0.12.0"
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+ integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
+accepts@~1.3.5:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
+ integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I=
+ dependencies:
+ mime-types "~2.1.18"
+ negotiator "0.6.1"
+
+acorn-jsx@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-4.1.1.tgz#e8e41e48ea2fe0c896740610ab6a4ffd8add225e"
+ integrity sha512-JY+iV6r+cO21KtntVvFkD+iqjtdpRUpGqKWgfkCdZq1R+kbreEl8EcdcJR4SmiIgsIQT33s6QzheQ9a275Q8xw==
+ dependencies:
+ acorn "^5.0.3"
+
+acorn@^5.0.3, acorn@^5.1.2:
+ version "5.7.3"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
+ integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
+
+ajax-request@^1.2.0:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/ajax-request/-/ajax-request-1.2.3.tgz#99fcbec1d6d2792f85fa949535332bd14f5f3790"
+ integrity sha1-mfy+wdbSeS+F+pSVNTMr0U9fN5A=
+ dependencies:
+ file-system "^2.1.1"
+ utils-extend "^1.0.7"
+
+ajv@^6.5.5:
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
+ integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
+ dependencies:
+ fast-deep-equal "^2.0.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-escapes@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
+ integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+ integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+ integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+ansi@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21"
+ integrity sha1-DELU+xcWDVqa8eSEus4cZpIsGyE=
+
+anymatch@^1.3.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
+ integrity sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==
+ dependencies:
+ micromatch "^2.1.5"
+ normalize-path "^2.0.0"
+
+app-root-path@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-1.4.0.tgz#6335d865c9640d0fad99004e5a79232238e92dfa"
+ integrity sha1-YzXYZclkDQ+tmQBOWnkjIjjpLfo=
+
+app-root-path@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.1.0.tgz#98bf6599327ecea199309866e8140368fd2e646a"
+ integrity sha1-mL9lmTJ+zqGZMJhm6BQDaP0uZGo=
+
+aproba@^1.0.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+ integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
+are-we-there-yet@~1.1.2:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
+ integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=
+ dependencies:
+ arr-flatten "^1.0.1"
+
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+ integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.0.1, arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+ integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+ integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+ integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+ integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+ integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+asn1@~0.2.3:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+ integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+ dependencies:
+ safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+ integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+assign