Skip to content
This repository was archived by the owner on Sep 27, 2021. It is now read-only.

Commit ca7ca35

Browse files
committed
modular ui + routing
1 parent cb5c963 commit ca7ca35

File tree

11 files changed

+27044
-331
lines changed

11 files changed

+27044
-331
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,6 @@ user_dumps/
6565
repos/
6666

6767
uplink_*
68+
69+
# bundled js
70+
wwww/assets/script.js

package-lock.json

Lines changed: 2142 additions & 84 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"main": "index.js",
66
"scripts": {
77
"test": "node scripts/worker-token generate 'test token' && mkdir -p user_dumps; rm -rf repos/*; jest --forceExit",
8-
"coverage": "jest --forceExit --coverage"
8+
"coverage": "jest --forceExit --coverage",
9+
"prepare": "browserify -t vueify -e ui/index.js | babel-minify --mangle false > www/assets/script.js",
10+
"dev": "watchify -t vueify -e ui/index.js -o www/assets/script.js"
911
},
1012
"repository": {
1113
"type": "git",
@@ -21,6 +23,7 @@
2123
"@sindresorhus/df": "^3.1.1",
2224
"async-zip": "^1.0.2",
2325
"axios": "^0.19.0",
26+
"babel-minify": "^0.5.1",
2427
"bunyan": "^1.8.12",
2528
"dateformat": "^3.0.3",
2629
"execa": "^4.0.0",
@@ -36,14 +39,19 @@
3639
"pretty-bytes": "^5.3.0",
3740
"request": "^2.88.0",
3841
"unzipper": "^0.10.5",
42+
"vue": "^2.6.11",
43+
"vue-router": "^3.1.5",
3944
"yauzl-promise": "^2.1.3",
4045
"zlib": "^1.0.5"
4146
},
4247
"devDependencies": {
48+
"browserify": "^16.5.0",
4349
"coveralls": "^3.0.9",
4450
"istanbul": "^1.1.0-alpha.1",
4551
"jest": "^24.9.0",
4652
"supertest": "^4.0.2",
53+
"vueify": "^9.4.1",
54+
"watchify": "^3.11.1",
4755
"xo": "^0.25.3"
4856
},
4957
"jest": {

ui/components/App.vue

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<template>
2+
<div>
3+
<div class="text-center mb-4">
4+
<a href="/">
5+
<img class="mb-4" src="./assets/logo.svg" alt="" width="72" height="72">
6+
<h1 class="h3 mb-3 font-weight-normal">GitBackup</h1>
7+
</a>
8+
9+
<p>We backup and archive <a href="https://github.com">GitHub</a>. Currently tracking {{totalUsers}} users/orgs. </p>
10+
</div>
11+
12+
<router-view></router-view>
13+
14+
<p class="mt-5 mb-3 text-muted text-center">
15+
<span v-if="stats">Storing {{stats.storage}}, {{stats.repos}} repositories, and {{stats.users}} users<br></span>
16+
<span v-if="stats">{{stats.bytesPerMinute}}/min, {{stats.reposPerMinute}} repos/min, {{stats.usersPerMinute}} users/min<br></span>
17+
<span>Built by <a href="https://github.com/super3">@super3</a>, <a href="https://github.com/montyanderson">@montyanderson</a>, and <a href="https://github.com/calebcase">@calebcase</a><br></span>
18+
</p>
19+
</div>
20+
</template>
21+
22+
<script>
23+
const axios = require('axios');
24+
25+
module.exports = {
26+
data: () => ({
27+
totalUsers: 0,
28+
stats: null
29+
}),
30+
methods: {
31+
async loadStats() {
32+
const {data} = await axios.get('/stats');
33+
34+
this.stats = data;
35+
}
36+
},
37+
async created() {
38+
this.loadStats();
39+
40+
setInterval(() => {
41+
this.loadStats();
42+
}, 10 * 1000);
43+
}
44+
};
45+
</script>

ui/components/UserList.vue

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<template>
2+
<div>
3+
<div class="form-label-group">
4+
<input v-model="search" v-on:keydown="loadPage(0); checkUser();" class="form-control search" required="" autofocus="" autocomplete="off" type="text" onsubmit="return false">
5+
6+
<label for="inputEmail">Type in a GitHub username or organization...</label>
7+
</div>
8+
9+
<ul class="list-group">
10+
<li v-if="search && exists !== true" class="list-group-item d-flex justify-content-between align-items-center">
11+
<span><i class="fas fa-user"></i> {{search}}</span>
12+
<button type="button" class="btn btn-sm btn-success" v-bind:disabled="!isValidUser" v-on:click="addUser">
13+
<i class="fas fa-plus"></i> Add
14+
</button>
15+
</li>
16+
17+
<li v-for="user in users" class="list-group-item ">
18+
<div class="d-flex justify-content-between align-items-center">
19+
<span><i class="fas fa-user"></i> {{user.username | truncateUsername}}</span>
20+
21+
<div class="btn-group" role="group" aria-label="Basic example">
22+
23+
<router-link v-bind:to="'/user/' + user.username">
24+
<button type="button" class="btn btn-sm btn-outline-dark">
25+
<i class="fas fa-code-branch"></i> View {{user.totalRepos}}/{{user.reportedRepos}} Repos
26+
</button>
27+
</router-link>
28+
29+
30+
<button type="button" class="btn btn-sm" v-bind:class="{
31+
'btn-outline-success': user.status === 'synced',
32+
'btn-outline-warning': user.status === 'syncing',
33+
'btn-outline-danger': user.status === 'unsynced',
34+
'btn-outline-dark': user.status === 'error'
35+
}">
36+
<i class="fas fa-sync-alt"></i> {{user.status | capitalize}}
37+
</button>
38+
</div>
39+
</div>
40+
</li>
41+
</ul>
42+
43+
<nav aria-label="Page navigation example">
44+
<ul class="pagination justify-content-center">
45+
<li class="page-item" v-bind:class="{ disabled: page === 0}">
46+
<a v-on:click="loadPage(page - 1)" class="page-link" href="#" tabindex="-1">Previous</a>
47+
</li>
48+
49+
<li v-if="page === 0" class="page-item disabled"><a class="page-link" href="#">0</a></li>
50+
<li v-if="page === 0 && totalPages > 1" class="page-item"><a class="page-link" v-on:click="loadPage(1)" href="#">1</a></li>
51+
<li v-if="page === 0 && totalPages > 2" class="page-item"><a class="page-link" v-on:click="loadPage(2)" href="#">2</a></li>
52+
53+
<li v-if="page !== 0 && page !== totalPages - 1" class="page-item"><a class="page-link" v-on:click="loadPage(page - 1)" href="#">{{page - 1}}</a></li>
54+
<li v-if="page !== 0 && page !== totalPages - 1 && totalPages > 1" class="page-item disabled"><a class="page-link" v-on:click="loadPage(page)" href="#">{{page}}</a></li>
55+
<li v-if="page !== 0 && page !== totalPages - 1 && totalPages > 2" class="page-item"><a class="page-link" v-on:click="loadPage(page + 1)" href="#">{{page + 1}}</a></li>
56+
57+
<li v-if="page !== 0 && page === totalPages - 1" class="page-item"><a class="page-link" v-on:click="loadPage(page - 2)" href="#">{{page - 2}}</a></li>
58+
<li v-if="page !== 0 && page === totalPages - 1 && totalPages > 1" class="page-item"><a class="page-link" v-on:click="loadPage(page - 1)" href="#">{{page - 1}}</a></li>
59+
<li v-if="page !== 0 && page === totalPages - 1 && totalPages > 2" class="page-item disabled"><a class="page-link" v-on:click="loadPage(page)" href="#">{{page}}</a></li>
60+
61+
<li class="page-item" v-bind:class="{ disabled: page + 1 == totalPages}">
62+
<a v-on:click="loadPage(page + 1)" class="page-link" href="#">Next</a>
63+
</li>
64+
</ul>
65+
</nav>
66+
</div>
67+
</template>
68+
69+
<script>
70+
const axios = require('axios');
71+
72+
const capitalize = require('../lib/capitalize');
73+
74+
const CancelToken = axios.CancelToken;
75+
const urlParams = new URLSearchParams(location.search);
76+
77+
let repoListTimeout;
78+
79+
module.exports = {
80+
data: () => ({
81+
search: '',
82+
isValidUser: false,
83+
users: [],
84+
page: 0,
85+
totalPages: 0,
86+
exists: false,
87+
cloneRepo: false,
88+
cancelPreviousCheckUser: null
89+
}),
90+
methods: {
91+
async checkUser() {
92+
if(this.search.trim().length < 1) {
93+
return;
94+
}
95+
96+
if(this.cancelPreviousCheckUser !== null) {
97+
this.cancelPreviousCheckUser();
98+
}
99+
100+
const {data} = await axios.get(`/isvaliduser/${this.search}`, {
101+
cancelToken: new CancelToken(c => this.cancelPreviousCheckUser = c)
102+
});
103+
104+
this.isValidUser = data;
105+
},
106+
async addUser() {
107+
await axios.get(`/adduser/${this.search}`);
108+
await this.loadPage(this.page);
109+
},
110+
async loadPage(i) {
111+
i = typeof i !== 'undefined' ? i : this.page;
112+
113+
const {data: {users, exists, total, totalPages}} = await axios.get(`/userlist/${i.toString()}`, {
114+
params: {
115+
filter: this.search
116+
}
117+
});
118+
119+
this.users = users;
120+
this.exists = exists;
121+
this.page = i;
122+
this.totalPages = totalPages;
123+
this.totalUsers = total;
124+
}
125+
},
126+
filters: {
127+
capitalize,
128+
truncateUsername: function (value) {
129+
const length = 30;
130+
131+
if(!value) return '';
132+
133+
if(value.length < length) {
134+
return value;
135+
}
136+
137+
return `${value.slice(0, length)}...`;
138+
}
139+
},
140+
async created() {
141+
this.loadPage(0);
142+
143+
setInterval(() => {
144+
this.loadPage();
145+
}, 10 * 1000);
146+
}
147+
};
148+
</script>

ui/components/UserPage.vue

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<template>
2+
<div>
3+
<div v-if="repoList === false" style="text-align: center; padding: 2rem;">
4+
<p>Fetching {{username}}'s repos...</p>
5+
<div class="spinner-border" ></div>
6+
</div>
7+
8+
<div v-else class="card">
9+
<div class="card-header justify-content-between align-items-center">
10+
<div class="row">
11+
<div class="col">
12+
<router-link to="/">
13+
<button class="btn btn-sm btn-outline-dark">
14+
<i class="fas fa-long-arrow-alt-left"></i>
15+
Back
16+
</button>
17+
</router-link>
18+
</div>
19+
20+
<div class="col" style="text-align: center; padding-top: 3px;">
21+
<i class="fas fa-user"></i> {{username}}
22+
</div>
23+
24+
<div class="col">
25+
<div class="btn-group float-right" role="group" aria-label="Basic example">
26+
<button type="button" class="btn btn-sm" v-bind:class="{
27+
'btn-outline-success': user.status === 'synced',
28+
'btn-outline-warning': user.status === 'syncing',
29+
'btn-outline-danger': user.status === 'unsynced',
30+
'btn-outline-dark': user.status === 'error'
31+
}">
32+
<i class="fas fa-sync-alt"></i> {{user.status | capitalize}}
33+
</button>
34+
</div>
35+
</div>
36+
</div>
37+
</div>
38+
39+
<ul class="list-group list-group-flush" style="padding-bottom: 0;">
40+
<li class="list-group-item" v-for="repo in repoList">
41+
{{repo}}
42+
43+
<div class="float-right">
44+
<div>
45+
<a v-bind:href="repoZip(repo)">
46+
<button class="btn btn-sm btn-outline-dark"><i class="fas fa-download"></i> Download ZIP</button>
47+
</a>
48+
49+
<a>
50+
<button class="btn btn-sm btn-outline-dark" v-on:click="cloneRepo = cloneRepo === repo ? false : repo"><i class="fas fa-download"></i> Clone Repo</button>
51+
</a>
52+
</div>
53+
</div>
54+
55+
<div v-if="cloneRepo === repo" style="margin-bottom: 1rem">
56+
<hr>
57+
58+
<label for="linuxCommand"><i class="fab fa-linux"></i> Linux / <i class="fab fa-apple"></i> Mac</label>
59+
60+
<input id="linuxCommand" class="form-control" v-bind:value="linuxCommand(repo)">
61+
62+
<label for="windowsCommand" style="margin-top: 0.5rem;"><i class="fab fa-windows"></i> Windows</label>
63+
<input id="windowsCommand" class="form-control" v-if="cloneRepo === repo" v-bind:value="windowsCommand(repo)">
64+
</div>
65+
</li>
66+
</ul>
67+
</div>
68+
</div>
69+
</template>
70+
71+
<script>
72+
const axios = require('axios');
73+
74+
const capitalize = require('../lib/capitalize');
75+
76+
module.exports = {
77+
methods: {
78+
repoZip(repo) {
79+
return `/repos/${this.user.username}/${repo}.zip`;
80+
},
81+
linuxCommand(repo) {
82+
return `wget https://gitbackup.org/repos/${this.user.username}/${repo}.bundle && git clone ${repo}.bundle`;
83+
},
84+
windowsCommand(repo) {
85+
return `wget https://gitbackup.org/repos/${this.user.username}/${repo}.bundle -o ${repo}.bundle; git clone ${repo}.bundle`;
86+
}
87+
},
88+
computed: {
89+
username() {
90+
return this.$route.params.username;
91+
}
92+
},
93+
data: (() => ({
94+
cloneRepo: false,
95+
repoList: false,
96+
user: false
97+
})),
98+
filters: {
99+
capitalize
100+
},
101+
async created() {
102+
const {data: {users, exists, total, totalPages}} = await axios.get(`/userlist/0`, {
103+
params: {
104+
filter: this.username
105+
}
106+
});
107+
108+
const reposResponse = await axios.get(`/user/${this.username}/repos`);
109+
110+
this.user = users[0];
111+
this.repoList = reposResponse.data;
112+
}
113+
};
114+
</script>

ui/index.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const Vue = require('vue');
2+
const Router = require('vue-router');
3+
4+
const App = require('./components/App.vue');
5+
const UserList = require('./components/UserList.vue');
6+
const UserPage = require('./components/UserPage.vue');
7+
8+
Vue.use(Router);
9+
10+
const router = new Router({
11+
routes: [
12+
{
13+
path: '/',
14+
component: UserList
15+
},
16+
{
17+
path: '/user/:username',
18+
component: UserPage
19+
}
20+
]
21+
});
22+
23+
new Vue({
24+
el: '#app',
25+
components: {
26+
App
27+
},
28+
render: createElement => createElement('app'),
29+
router
30+
});

ui/lib/capitalize.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = function capitalize(value) {
2+
if(!value) {
3+
return '';
4+
}
5+
6+
value = value.toString();
7+
8+
return value.charAt(0).toUpperCase() + value.slice(1)
9+
};

0 commit comments

Comments
 (0)