Massive frontend overhaul

This commit is contained in:
Tyler 2018-04-03 23:30:31 -04:00
parent 1fa74c9ccf
commit d1028908bd
16 changed files with 357 additions and 93 deletions

View File

@ -1,5 +1,5 @@
before_script: before_script:
- export VERSION=1.0.1 - export VERSION=1.1.0
- chmod +x packaging/build-package.sh packaging/package-upload.sh - chmod +x packaging/build-package.sh packaging/package-upload.sh
stages: stages:

View File

@ -10,10 +10,15 @@
"build": "node build/build.js" "build": "node build/build.js"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^4.0.0-beta.2", "@fortawesome/fontawesome": "^1.1.5",
"bootstrap-vue": "^1.3.0", "@fortawesome/fontawesome-free-regular": "^5.0.9",
"@fortawesome/fontawesome-free-solid": "^5.0.9",
"@fortawesome/vue-fontawesome": "0.0.22",
"bootstrap": "^4.0.0",
"bootstrap-vue": "^1.5.1",
"event-emitter": "^0.3.5", "event-emitter": "^0.3.5",
"jquery": "^3.2.1", "jquery": "^3.2.1",
"moment": "^2.22.0",
"tiny-emitter": "^2.0.2", "tiny-emitter": "^2.0.2",
"util.inherits": "^1.0.3", "util.inherits": "^1.0.3",
"vue": "^2.4.2", "vue": "^2.4.2",
@ -39,18 +44,20 @@
"friendly-errors-webpack-plugin": "^1.1.3", "friendly-errors-webpack-plugin": "^1.1.3",
"html-webpack-plugin": "^2.28.0", "html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3", "http-proxy-middleware": "^0.17.3",
"webpack-bundle-analyzer": "^2.2.1", "node-sass": "^4.8.3",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"opn": "^5.1.0", "opn": "^5.1.0",
"optimize-css-assets-webpack-plugin": "^2.0.0", "optimize-css-assets-webpack-plugin": "^2.0.0",
"ora": "^1.2.0", "ora": "^1.2.0",
"rimraf": "^2.6.0", "rimraf": "^2.6.0",
"sass-loader": "^6.0.7",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"url-loader": "^0.5.8", "url-loader": "^0.5.8",
"vue-loader": "^13.0.4", "vue-loader": "^13.0.4",
"vue-style-loader": "^3.0.1", "vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.4.2", "vue-template-compiler": "^2.4.2",
"webpack": "^2.6.1", "webpack": "^2.7.0",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0", "webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.18.0", "webpack-hot-middleware": "^2.18.0",
"webpack-merge": "^4.1.0" "webpack-merge": "^4.1.0"

View File

@ -1,21 +1,23 @@
<template> <template>
<div id="app" class="container"> <div id="app">
<div class="row" style="padding-bottom: 10px;"> <n :sensors="sensors" />
<div class="col-sm"> <div class="container">
<h1>Ripper</h1> <div class="row">
<main role="main" class="col-md-12 ml-sm-auto col-lg-12 pt-3 px-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
<h1 class="h2">Jobs</h1>
</div> </div>
</div>
<status :sensors="sensors"></status>
<temperatures :temperatures="sensors.temperatures"></temperatures>
<job v-for="(job, id) in jobs" :job="job" :key="job.id"></job> <job v-for="(job, id) in jobs" :job="job" :key="job.id"></job>
</main>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import Vue from 'vue' import Vue from 'vue'
import job from './components/Job'; import job from './components/Job';
import status from './components/Status'; import navbar from './components/Nav';
import temperatures from './components/TempStatus';
import EventWebSocket from './websocket'; import EventWebSocket from './websocket';
let d = { let d = {
@ -70,8 +72,7 @@ export default {
name: 'app', name: 'app',
components: { components: {
job: job, job: job,
status: status, n: navbar,
temperatures: temperatures,
}, },
data() { data() {
return d; return d;

View File

@ -0,0 +1,93 @@
body {
font-size: .875rem;
}
.feather {
width: 16px;
height: 16px;
vertical-align: text-bottom;
}
/*
* Sidebar
*/
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100; /* Behind the navbar */
padding: 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
.sidebar-sticky {
position: -webkit-sticky;
position: sticky;
top: 48px; /* Height of navbar */
height: calc(100vh - 48px);
padding-top: .5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link .feather {
margin-right: 4px;
color: #999;
}
.sidebar .nav-link.active {
color: #007bff;
}
.sidebar .nav-link:hover .feather,
.sidebar .nav-link.active .feather {
color: inherit;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
/*
* Navbar
*/
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
font-size: 1rem;
background-color: rgba(0, 0, 0, .25);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}
.navbar .form-control {
padding: .75rem 1rem;
border-width: 0;
border-radius: 0;
}
.form-control-dark {
color: #fff;
background-color: rgba(255, 255, 255, .1);
border-color: rgba(255, 255, 255, .1);
}
.form-control-dark:focus {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
}
/*
* Utilities
*/
.border-top { border-top: 1px solid #e5e5e5; }
.border-bottom { border-bottom: 1px solid #e5e5e5; }

View File

@ -0,0 +1,23 @@
$cold: #6CA6CD;
$cool: #79CDCD;
$info: #4F94CD;
$acceptable: #66CDAA;
$success: #A2CD5A;
$high: #EEE685;
$warning: #E3A869;
$danger: #EE8262;
$badge-color: #545454;
$theme-colors: (
"cold": $cold,
"cool": $cool,
"acceptable": $acceptable,
"high": $high
);
@each $color, $value in $theme-colors {
.badge-#{$color} {
color: $badge-color !important;
}
}

24
src/assets/scss/app.scss Normal file
View File

@ -0,0 +1,24 @@
@import '_variables.scss';
@import '~bootstrap/scss/bootstrap';
@import '~bootstrap-vue/dist/bootstrap-vue.css';
@import '_sidebar.scss';
.margin-top-10 {
margin-top: 10px;
}
.margin-bottom-10 {
margin-bottom: 10px;
}
.margin-bottom-20 {
margin-bottom: 20px;
}
.badge-success {
color: #454545;
}
.nav-item .progress {
width: 70px;
}

View File

@ -1,20 +0,0 @@
<template>
<div class="row">
<job v-for="(job, index) in jobs" :job="job" :key="job_id"></job>
</div>
</template>
<script>
import job from '@/components/Job';
export default {
name: 'hello',
components: {
job: job
},
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
</script>

View File

@ -1,11 +1,19 @@
<template> <template>
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
<h3>{{ job.title }} - {{ stageValue }}</h3> <div class="card">
<div class="card-header">
<div class="row">
<div class="col-md">{{ job.title }}</div>
<div class="col-md text-center badge">{{ stageValue }}</div>
<div class="col-md text-right">Remaining: {{ eta }}</div>
</div>
</div>
<div class="card-body">
<template v-if="job.progress"> <template v-if="job.progress">
<span class="text-center">{{ job.progress.name }} ({{ job.progress.percentage }}%)</span> <span class="text-center">{{ job.progress.name }} ({{ job.progress.percentage }}%)</span>
<div class="progress"> <div class="progress">
<div class="progress-bar bg-success" role="progressbar" :style="'width: ' + job.progress.percentage + '%'" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100">{{ job.progress.percentage }}%</div> <div class="progress-bar bg-success" role="progressbar" :style="'width: ' + job.progress.percentage + '%'" :aria-valuenow=job.progress.percentage aria-valuemin="0" aria-valuemax="100">{{ job.progress.percentage }}%</div>
</div> </div>
<template v-if="job.progress.totalPercentage"> <template v-if="job.progress.totalPercentage">
<span class="text-center">Total Progress ({{ job.progress.totalPercentage }}%)</span> <span class="text-center">Total Progress ({{ job.progress.totalPercentage }}%)</span>
@ -16,6 +24,8 @@
</template> </template>
</div> </div>
</div> </div>
</div>
</div>
</template> </template>
<script> <script>
@ -28,8 +38,20 @@
return 'Ripping'; return 'Ripping';
} else if (this.job.stage === 'transcode') { } else if (this.job.stage === 'transcode') {
return 'Transcoding'; return 'Transcoding';
} else if (this.job.stage === 'copy') {
return 'Copying';
} }
return 'Unknown: ' + this.job.stage; return 'Unknown: ' + this.job.stage;
},
eta: function() {
if (this.job.eta == 0) {
return 'Unknown';
}
// Job is in nanoseconds because of Go's time.Duration
let eta = moment.duration(this.job.eta / 1000);
return eta.humanize();
} }
} }
} }

21
src/components/Nav.vue Normal file
View File

@ -0,0 +1,21 @@
<template>
<div class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top p-0">
<div class="container d-flex flex-column flex-md-row">
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="#">ARM</a>
<div class="collapse navbar-collapse">
<status :sensors="sensors" v-if="sensors"></status>
</div>
</div>
</div>
</template>
<script>
import status from './Status';
export default {
name: 'navbar',
props: [ 'sensors' ],
components: {
status: status,
}
}
</script>

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="row" v-if="sensors.cpu || sensors.memory || sensors.storage"> <ul class="navbar-nav ml-auto" v-if="sensors.cpu || sensors.memory || sensors.disks">
<cpu_status :status="sensors.cpu"></cpu_status> <cpu_status :status="sensors.cpu" :cpuName="sensors.cpuName" :temperatures="sensors.temperatures"></cpu_status>
<memory_status :status="sensors.memory"></memory_status> <memory_status :status="sensors.memory"></memory_status>
<disk_status :status="sensors.disk"></disk_status> <disk_status :status="sensors.disks"></disk_status>
</div> </ul>
</template> </template>
<script> <script>

View File

@ -1,8 +1,5 @@
<template> <template>
<div style="padding-top: 10px;"> <div class="margin-top-10 margin-bottom-20 text-center">
<template v-for="(temp, id) in temperatures">
<span class="badge badge-info">{{ temp.name }}: {{ temp.temperature }}C</span>&nbsp;
</template>
</div> </div>
</template> </template>
@ -10,6 +7,7 @@
export default { export default {
name: 'temp_status', name: 'temp_status',
props: [ 'temperatures' ], props: [ 'temperatures' ],
components: {}, methods: {
}
} }
</script> </script>

View File

@ -1,21 +1,38 @@
<template> <template>
<div class="col"> <b-nav-item-dropdown no-caret right>
<h4>CPU <span :class="'badge badge-' + percentageClass">{{ formattedPercentage }}%</span></h4> <template slot="button-content">
<div class="progress"> <font-awesome-icon icon="tachometer-alt" /> CPU
<div :class="'progress-bar bg-' + percentageClass" role="progressbar" :style="{ width : status.UserPct + '%' }" :aria-valuenow="status.UserPct" aria-valuemin="0" aria-valuemax="100"></div> <b-progress :value="status.UserPct" height=".5rem" :variant="percentageClass"></b-progress>
</template>
<b-dropdown-header class="text-center">CPU</b-dropdown-header>
<div class="text-center">
<small>{{ cpuName }}</small>
<br />
<span :class="'badge badge-' + percentageClass">{{ formattedPercentage }}%</span>
</div> </div>
<div class="text-center">
<template v-for="(temp, id) in temperatures">
<span :class="'badge badge-' + badgeClass(temp.temperature)" :title="temp.name">{{ temp.temperature }}C</span>&nbsp;
</template>
</div> </div>
</b-nav-item-dropdown>
</template> </template>
<script> <script>
export default { export default {
name: 'cpu_status', name: 'cpu_status',
props: [ 'status' ], props: [ 'status', 'cpuName', 'temperatures' ],
computed: { computed: {
formattedPercentage: function() { formattedPercentage: function() {
if (!this.status) {
return '0.00';
}
return parseFloat(this.status.UserPct).toFixed(2); return parseFloat(this.status.UserPct).toFixed(2);
}, },
percentageClass: function() { percentageClass: function() {
if (!this.status) {
return 'info';
}
if (this.status.UserPct >= 90) { if (this.status.UserPct >= 90) {
return 'danger'; return 'danger';
} else if (this.status.UserPct >= 80) { } else if (this.status.UserPct >= 80) {
@ -23,6 +40,24 @@
} }
return 'success' return 'success'
}, },
},
methods: {
badgeClass: function(temp) {
if (temp >= 90) {
return 'danger';
} else if (temp >= 70) {
return 'warning';
} else if (temp >= 60) {
return 'high';
} else if (temp >= 50) {
return 'success';
} else if (temp >= 40) {
return 'acceptable';
} else if (temp >= 30) {
return 'cool';
}
return 'cold';
},
} }
} }
</script> </script>

View File

@ -1,10 +1,22 @@
<template> <template>
<div class="col"> <b-nav-item-dropdown no-caret right>
<h4>Disk Space <span :class="'badge badge-' + percentageClass"><formatBytes :bytes="status.used"></formatBytes> of <formatBytes :bytes="status.total"></formatBytes></span></h4> <template slot="button-content">
<div class="progress"> <font-awesome-icon icon="hdd" /> Disks
<div :class="'progress-bar bg-' + percentageClass" role="progressbar" :style="{ width : usedPercentage + '%' }" :aria-valuenow="usedPercentage" aria-valuemin="0" aria-valuemax="100"></div> <b-progress :value="totalUsedPercentage" height=".5rem" :variant="totalPercentageClass"></b-progress>
</template>
<b-dropdown-header class="text-center">Disks</b-dropdown-header>
<div class="text-center">
Total
<br />
<span :class="'badge badge-' + totalPercentageClass"><formatBytes :bytes="totalUsed" /> / <formatBytes :bytes="total" /></span>
</div> </div>
<div v-for="(disk, index) in status" class="text-center">
{{ disk.path }}
<br />
<span :class="'badge badge-' + individualPercentages[index]"><formatBytes :bytes="disk.used" /> / <formatBytes :bytes="disk.total" /></span>
</div> </div>
</b-nav-item-dropdown>
</template> </template>
<script> <script>
@ -12,14 +24,40 @@
name: 'disk_status', name: 'disk_status',
props: [ 'status' ], props: [ 'status' ],
computed: { computed: {
usedPercentage: function() { totalUsed: function() {
if (!this.status) { let used = 0;
return 0;
let i;
for (i = 0; i < this.status.length; i++) {
used += this.status[i].used;
} }
return parseFloat((this.status.used / this.status.total) * 100).toFixed(1).toString();
return used;
}, },
percentageClass: function() { total: function() {
let usedPercent = (this.status.used / this.status.total) * 100; let total = 0;
let i;
for (i = 0; i < this.status.length; i++) {
total += this.status[i].total;
}
return total;
},
totalUsedPercentage: function() {
let used = 0;
let total = 0;
let i;
for (i = 0; i < this.status.length; i++) {
used += this.status[i].used;
total += this.status[i].total;
}
return (used / total) * 100;
},
totalPercentageClass: function() {
let usedPercent = this.totalUsedPercentage;
if (usedPercent >= 90) { if (usedPercent >= 90) {
return 'danger'; return 'danger';
} else if (usedPercent >= 75) { } else if (usedPercent >= 75) {
@ -27,6 +65,24 @@
} }
return 'success' return 'success'
}, },
individualPercentages: function() {
let percentages = [];
let i, usedPercent;
for (i = 0; i < this.status.length; i++) {
usedPercent = (this.status[i].used / this.status[i].total) * 100;
if (usedPercent >= 90) {
percentages[i] = 'danger';
} else if (usedPercent >= 75) {
percentages[i] = 'warning';
} else {
percentages[i] = 'success';
}
}
return percentages;
}
} }
} }
</script> </script>

View File

@ -1,11 +1,15 @@
<template> <template>
<div class="col"> <b-nav-item-dropdown no-caret right>
<h4>Memory <span :class="'badge badge-' + percentageClass"><formatBytes :bytes="(status.MemUsed + status.Buffers + status.Cached) * 1024" /> / <formatBytes :bytes="status.MemTotal * 1024" /></span></h4> <template slot="button-content">
<div class="progress"> <font-awesome-icon icon="microchip" /> Memory
<div class="progress-bar bg-success" role="progressbar" :style="{ width: memoryPercentage + '%' }" :aria-valuenow="memoryPercentage" aria-valuemin="0" aria-valuemax="100"></div> <b-progress :value="memoryPercentage" height=".5rem" :variant="percentageClass"></b-progress>
<div class="progress-bar bg-warning" role="progressbar" :style="{ width: memoryBufCachePercentage + '%' }" :aria-valuenow="memoryBufCachePercentage" aria-valuemin="0" aria-valuemax="100"></div> </template>
</div> <b-dropdown-header class="text-center">Memory</b-dropdown-header>
<div class="text-center">
<span :class="'badge badge-' + percentageClass"><formatBytes :bytes="status.MemUsed * 1024" /> / <formatBytes :bytes="status.MemTotal * 1024" /></span>
</div> </div>
</b-nav-item-dropdown>
</template> </template>
<script> <script>
@ -14,16 +18,10 @@
props: [ 'status' ], props: [ 'status' ],
computed: { computed: {
memoryPercentage: function() { memoryPercentage: function() {
if (!this.status) { return (this.status.MemUsed / this.status.MemTotal) * 100;
return 0;
}
return parseFloat((this.status.MemUsed / this.status.MemTotal) * 100).toFixed(1).toString();
}, },
memoryBufCachePercentage: function() { memoryBufCachePercentage: function() {
if (!this.status) { return ((this.status.Buffers + this.status.Cached) / this.status.MemTotal) * 100;
return 0;
}
return parseFloat(((this.status.Buffers + this.status.Cached) / this.status.MemTotal) * 100).toFixed(1).toString();
}, },
percentageClass: function() { percentageClass: function() {
let usedPercent = (this.status.MemUsed / this.status.MemTotal) * 100; let usedPercent = (this.status.MemUsed / this.status.MemTotal) * 100;

View File

@ -1,14 +1,20 @@
// The Vue build version to load with the `import` command // The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias. // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue' import Vue from 'vue'
import 'bootstrap/dist/css/bootstrap.css' import './assets/scss/app.scss'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import App from './App' import App from './App'
import fontawesome from '@fortawesome/fontawesome'
import FontAwesomeIcon from '@fortawesome/vue-fontawesome'
import solid from '@fortawesome/fontawesome-free-solid'
import BootstrapVue from 'bootstrap-vue' import BootstrapVue from 'bootstrap-vue'
Vue.use(BootstrapVue); Vue.use(BootstrapVue);
fontawesome.library.add(solid);
Vue.component('font-awesome-icon', FontAwesomeIcon);
Vue.component('formatBytes', { Vue.component('formatBytes', {
render: function (createElement) { render: function (createElement) {
return createElement( return createElement(

View File

@ -1,3 +1,3 @@
window.arm_config = { window.arm_config = {
url: 'ws://127.0.0.1:8080/ws' url: 'ws://192.168.2.85:8080/ws'
}; };