feat(fe): correct tables layout, display task status

Added animation for running task status.
This commit is contained in:
Denis Gukov 2020-11-20 04:29:08 +05:00
parent 389b566482
commit ecf8b612ff
13 changed files with 248 additions and 106 deletions

View File

@ -354,10 +354,14 @@
.theme--light.v-data-table > .v-data-table__wrapper > table > thead > tr:last-child > th {
text-transform: uppercase;
white-space: nowrap;
}
.v-data-table > .v-data-table__wrapper > table > tbody > tr {
background: transparent !important;
& > td {
white-space: nowrap;
}
& > td:first-child {
//font-weight: bold !important;
a {

View File

@ -0,0 +1,105 @@
<template>
<v-progress-circular
color="white"
class="indeterminate-progress-circular mr-2"
size="20"
width="10"
:rotate="rotate"
:value="value"
>
</v-progress-circular>
</template>
<style lang="scss">
.indeterminate-progress-circular {
.v-progress-circular__overlay {
transition: 0s !important;
}
}
</style>
<script>
class IndeterminateTimer {
constructor() {
this.listeners = {};
this.direction = 1;
this.value = 0;
this.rotate = 0;
}
start() {
const STEP = 1;
const self = this;
self.valueTimer = setInterval(() => {
if (self.direction === 1 && self.value >= 100) {
self.direction = -1;
} else if (self.direction === -1 && self.value <= 0) {
self.direction = 1;
}
if (self.direction === 1) {
self.rotate += STEP;
self.value += STEP;
} else {
self.rotate += STEP * 5;
self.value += -STEP;
}
if (self.rotate > 360) {
self.rotate %= 360;
}
Object.keys(self.listeners).forEach((id) => {
const listener = self.listeners[id];
listener({
value: self.value,
rotate: self.rotate,
});
});
}, 50);
}
stop() {
clearInterval(this.valueTimer);
}
addListener(callback) {
if (Object.keys(this.listeners).length === 0) {
this.start();
}
const id = Math.floor(Math.random() * 100000000);
this.listeners[id] = callback;
return id;
}
removeListener(id) {
delete this.listeners[id];
if (Object.keys(this.listeners).length === 0) {
this.stop();
}
}
}
const indeterminateTimer = new IndeterminateTimer();
export default {
data() {
return {
value: null,
rotate: null,
listenerId: null,
};
},
mounted() {
this.value = indeterminateTimer.value;
this.rotate = indeterminateTimer.rotate;
this.listenerId = indeterminateTimer.addListener(({ value, rotate }) => {
this.value = value;
this.rotate = rotate;
});
},
beforeDestroy() {
indeterminateTimer.removeListener(this.listenerId);
},
};
</script>

View File

@ -0,0 +1,6 @@
export default Object.freeze({
WAITING: 'waiting',
RUNNING: 'running',
SUCCESS: 'success',
ERROR: 'error',
});

View File

@ -7,7 +7,7 @@ import vuetify from './plugins/vuetify';
Vue.config.productionTip = false;
Vue.filter('formatDate', (value) => (value ? moment(String(value)).fromNow() : undefined));
Vue.filter('formatMinutes', (value) => (value ? moment.duration(parseInt(value, 10), 'minutes').humanize() : undefined));
Vue.filter('formatMilliseconds', (value) => (value ? moment.duration(parseInt(value, 10), 'milliseconds').humanize() : undefined));
new Vue({
router,

View File

@ -49,54 +49,18 @@
:items-per-page="Number.MAX_VALUE"
>
<template v-slot:item.external="{ item }">
<v-btn
icon
v-if="item.external"
disabled
>
<v-icon>mdi-checkbox-marked</v-icon>
</v-btn>
<v-btn
icon
v-else
disabled
>
<v-icon>mdi-checkbox-blank-outline</v-icon>
</v-btn>
<v-icon v-if="item.external">mdi-checkbox-marked</v-icon>
<v-icon v-else>mdi-checkbox-blank-outline</v-icon>
</template>
<template v-slot:item.alert="{ item }">
<v-btn
icon
v-if="item.alert"
disabled
>
<v-icon>mdi-checkbox-marked</v-icon>
</v-btn>
<v-btn
icon
v-else
disabled
>
<v-icon>mdi-checkbox-blank-outline</v-icon>
</v-btn>
<v-icon v-if="item.alert">mdi-checkbox-marked</v-icon>
<v-icon v-else>mdi-checkbox-blank-outline</v-icon>
</template>
<template v-slot:item.admin="{ item }">
<v-btn
icon
v-if="item.admin"
disabled
>
<v-icon>mdi-checkbox-marked</v-icon>
</v-btn>
<v-btn
icon
v-else
disabled
>
<v-icon>mdi-checkbox-blank-outline</v-icon>
</v-btn>
<v-icon v-if="item.admin">mdi-checkbox-marked</v-icon>
<v-icon v-else>mdi-checkbox-blank-outline</v-icon>
</template>
<template v-slot:item.actions="{ item }">
@ -143,6 +107,7 @@ export default {
return [{
text: 'Name',
value: 'name',
width: '50%',
},
{
text: 'Username',
@ -163,6 +128,7 @@ export default {
{
text: 'External',
value: 'external',
width: '50%',
},
{
text: 'Actions',

View File

@ -38,11 +38,13 @@ export default {
text: 'Time',
value: 'created',
sortable: false,
width: '20%',
},
{
text: 'Description',
value: 'description',
sortable: false,
width: '80%',
},
];
},

View File

@ -45,35 +45,25 @@
>
<template v-slot:item.actions="{ item }">
<div style="white-space: nowrap">
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<v-btn
icon
class="mr-1"
v-bind="attrs"
v-on="on"
@click="askDeleteItem(item.id)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
<span>Delete environment</span>
</v-tooltip>
<v-btn
icon
class="mr-1"
v-bind="attrs"
v-on="on"
@click="askDeleteItem(item.id)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<v-btn
icon
class="mr-1"
v-bind="attrs"
v-on="on"
@click="editItem(item.id)"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
</template>
<span>Edit environment</span>
</v-tooltip>
<v-btn
icon
class="mr-1"
v-bind="attrs"
v-on="on"
@click="editItem(item.id)"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
</div>
</template>
</v-data-table>
@ -92,6 +82,7 @@ export default {
return [{
text: 'Name',
value: 'name',
width: '100%',
},
{
text: 'Actions',

View File

@ -25,7 +25,8 @@
<template v-slot:item.status="{ item }">
<v-chip style="font-weight: bold;" :color="getStatusColor(item.status)">
<v-icon left>{{ getStatusIcon(item.status) }}</v-icon>
<v-icon v-if="item.status !== 'running'" left>{{ getStatusIcon(item.status) }}</v-icon>
<IndeterminateProgressCircular v-else style="margin-left: -5px;" />
{{ humanizeStatus(item.status) }}
</v-chip>
</template>
@ -36,46 +37,112 @@
</template>
<template v-slot:item.end="{ item }">
<span v-if="item.end">{{ (item.end - item.start) | formatMinutes }}</span>
<span v-if="item.end">
{{ (new Date(item.end) - new Date(item.start)) | formatMilliseconds }}
</span>
<v-chip v-else>Not ended</v-chip>
</template>
</v-data-table>
</div>
</template>
<style lang="scss">
.running-task-progress-circular {
.v-progress-circular__overlay {
transition: 0s;
}
}
</style>
<script>
import ItemListPageBase from '@/components/ItemListPageBase';
import EventBus from '@/event-bus';
import TaskStatus from '@/lib/TaskStatus';
import IndeterminateProgressCircular from '@/components/IndeterminateProgressCircular.vue';
export default {
mixins: [ItemListPageBase],
components: { IndeterminateProgressCircular },
data() {
return {
runningTaskProgress: 0,
runningTaskRotate: 0,
runningTaskInterval: null,
};
},
watch: {
async projectId() {
await this.loadItems();
},
},
mounted() {
this.startRunningTaskProgress();
this.runningTaskInterval = setInterval(() => {
this.runningTaskRotate += 5;
}, 100);
},
beforeDestroy() {
clearInterval(this.runningTaskInterval);
},
methods: {
startRunningTaskProgress() {
if (this.runningTaskProgress > 100) {
this.runningTaskProgress = 0;
setTimeout(() => this.startRunningTaskProgress(), 1000);
} else {
this.runningTaskProgress += 5;
setTimeout(() => this.startRunningTaskProgress(), 300);
}
},
getStatusIcon(status) {
switch (status) {
case 'error':
case TaskStatus.WAITING:
return 'mdi-alarm';
case TaskStatus.RUNNING:
return '';
case TaskStatus.SUCCESS:
return 'mdi-check-circle';
case TaskStatus.ERROR:
return 'mdi-information';
default:
return status;
throw new Error(`Unknown task status ${status}`);
}
},
humanizeStatus(status) {
switch (status) {
case 'error':
case TaskStatus.WAITING:
return 'Waiting';
case TaskStatus.RUNNING:
return 'Running';
case TaskStatus.SUCCESS:
return 'Success';
case TaskStatus.ERROR:
return 'Failed';
default:
return status;
throw new Error(`Unknown task status ${status}`);
}
},
getStatusColor(status) {
return status;
switch (status) {
case TaskStatus.WAITING:
return '';
case TaskStatus.RUNNING:
return 'primary';
case TaskStatus.SUCCESS:
return 'success';
case TaskStatus.ERROR:
return 'error';
default:
throw new Error(`Unknown task status ${status}`);
}
},
showTaskLog(taskId) {

View File

@ -79,14 +79,17 @@ export default {
return [{
text: 'Name',
value: 'name',
width: '33.33%',
},
{
text: 'Type',
value: 'type',
width: '33.33%',
},
{
text: 'Path',
value: 'inventory',
width: '33.33%',
},
{
text: 'Actions',

View File

@ -46,35 +46,25 @@
>
<template v-slot:item.actions="{ item }">
<div style="white-space: nowrap">
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<v-btn
icon
class="mr-1"
v-bind="attrs"
v-on="on"
@click="askDeleteItem(item.id)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
<span>Delete key</span>
</v-tooltip>
<v-btn
icon
class="mr-1"
v-bind="attrs"
v-on="on"
@click="askDeleteItem(item.id)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<v-btn
icon
class="mr-1"
v-bind="attrs"
v-on="on"
@click="editItem(item.id)"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
</template>
<span>Edit key</span>
</v-tooltip>
<v-btn
icon
class="mr-1"
v-bind="attrs"
v-on="on"
@click="editItem(item.id)"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
</div>
</template>
</v-data-table>
@ -93,10 +83,12 @@ export default {
return [{
text: 'Name',
value: 'name',
width: '50%',
},
{
text: 'Type',
value: 'type',
width: '50%',
},
{
text: 'Actions',

View File

@ -96,14 +96,17 @@ export default {
return [{
text: 'Name',
value: 'name',
width: '25%',
},
{
text: 'Git URL',
value: 'git_url',
width: '50%',
},
{
text: 'SSH Key',
value: 'ssh_key_id',
width: '25%',
},
{
text: 'Actions',

View File

@ -94,6 +94,7 @@ export default {
{
text: 'Name',
value: 'name',
width: '50%',
},
{
text: 'Username',
@ -102,6 +103,7 @@ export default {
{
text: 'Email',
value: 'email',
width: '50%',
},
{
text: 'Admin',

View File

@ -163,6 +163,7 @@ export default {
text: 'Actions',
value: 'actions',
sortable: false,
width: '0%',
},
];
},