mirror of
https://github.com/semaphoreui/semaphore.git
synced 2025-01-20 23:39:56 +01:00
feat(fe): correct tables layout, display task status
Added animation for running task status.
This commit is contained in:
parent
389b566482
commit
ecf8b612ff
@ -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 {
|
||||
|
105
web2/src/components/IndeterminateProgressCircular.vue
Normal file
105
web2/src/components/IndeterminateProgressCircular.vue
Normal 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>
|
6
web2/src/lib/TaskStatus.js
Normal file
6
web2/src/lib/TaskStatus.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default Object.freeze({
|
||||
WAITING: 'waiting',
|
||||
RUNNING: 'running',
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error',
|
||||
});
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -38,11 +38,13 @@ export default {
|
||||
text: 'Time',
|
||||
value: 'created',
|
||||
sortable: false,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
text: 'Description',
|
||||
value: 'description',
|
||||
sortable: false,
|
||||
width: '80%',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
@ -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',
|
||||
|
@ -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) {
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -163,6 +163,7 @@ export default {
|
||||
text: 'Actions',
|
||||
value: 'actions',
|
||||
sortable: false,
|
||||
width: '0%',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user