Skip to content

Commit 868028d

Browse files
authored
Merge branch 'ui-improvements' of 'https://github.com/evamillan/grimoirelab-core'
Merges #34 Closes #34
2 parents 2f8e4cc + 252d865 commit 868028d

File tree

17 files changed

+507
-80
lines changed

17 files changed

+507
-80
lines changed

src/grimoirelab/core/scheduler/api.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818

1919
import django_rq
2020

21+
from django.db.models import F
22+
2123
from rest_framework import (
24+
filters,
2225
generics,
2326
pagination,
2427
response,
@@ -137,9 +140,18 @@ def get_logs(self, obj):
137140

138141

139142
class EventizerTaskList(generics.ListAPIView):
140-
queryset = EventizerTask.objects.all().order_by('-scheduled_at')
141143
serializer_class = EventizerTaskListSerializer
142144
pagination_class = EventizerPaginator
145+
filter_backends = [filters.OrderingFilter]
146+
ordering_fields = ['scheduled_at', 'last_run']
147+
ordering = [F('last_run').desc(nulls_first=True)]
148+
149+
def get_queryset(self):
150+
queryset = EventizerTask.objects.all()
151+
status = self.request.query_params.get('status')
152+
if status is not None:
153+
queryset = queryset.filter(status=status)
154+
return queryset
143155

144156

145157
class EventizerTaskDetail(generics.RetrieveAPIView):
@@ -155,7 +167,11 @@ class EventizerJobList(generics.ListAPIView):
155167

156168
def get_queryset(self):
157169
task_id = self.kwargs['task_id']
158-
return get_registered_task_model('eventizer')[1].objects.filter(task__uuid=task_id)
170+
queryset = get_registered_task_model('eventizer')[1].objects.filter(task__uuid=task_id).order_by('-scheduled_at')
171+
status = self.request.query_params.get('status')
172+
if status is not None:
173+
queryset = queryset.filter(status=status)
174+
return queryset
159175

160176

161177
class EventizerJobDetail(generics.RetrieveAPIView):

ui/src/components/JobList.vue

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,35 @@
66
{{ count }}
77
</v-chip>
88
</h2>
9+
10+
<v-tabs
11+
v-model="tab"
12+
:items="tabs"
13+
align-tabs="left"
14+
class="mb-4"
15+
color="primary"
16+
height="36"
17+
slider-color="primary"
18+
@update:model-value="$emit('update:filters', { status: $event, page: 1 })"
19+
>
20+
<template #tab="{ item }">
21+
<v-tab :text="item.text" :value="item.value" class="text-none text-subtitle-2"></v-tab>
22+
</template>
23+
</v-tabs>
24+
25+
<div v-if="loading" class="d-flex justify-center pa-4">
26+
<v-progress-circular class="mx-auto" color="primary" indeterminate />
27+
</div>
28+
29+
<v-empty-state
30+
v-else-if="!loading && count === 0"
31+
icon="mdi-magnify"
32+
title="No results found"
33+
size="52"
34+
></v-empty-state>
35+
936
<status-card
37+
v-else
1038
v-for="job in jobs"
1139
:key="job.uuid"
1240
:status="job.status"
@@ -47,7 +75,7 @@
4775
:length="pages"
4876
color="primary"
4977
density="comfortable"
50-
@update:model-value="$emit('update:page', $event)"
78+
@update:model-value="$emit('update:filters', { page: $event, status: tab })"
5179
/>
5280
</div>
5381
</template>
@@ -58,7 +86,7 @@ import StatusCard from '@/components/StatusCard.vue'
5886
export default {
5987
name: 'JobList',
6088
components: { StatusCard },
61-
emits: ['update:page'],
89+
emits: ['update:filters'],
6290
props: {
6391
jobs: {
6492
type: Array,
@@ -71,21 +99,40 @@ export default {
7199
pages: {
72100
type: Number,
73101
required: true
102+
},
103+
loading: {
104+
type: Boolean,
105+
required: false,
106+
default: false
74107
}
75108
},
76109
data() {
77110
return {
78-
page: 1
111+
page: 1,
112+
tab: 'all',
113+
tabs: [
114+
{ text: 'All', value: 'all' },
115+
{ text: 'Failed', value: 5 },
116+
{ text: 'Completed', value: 4 }
117+
]
79118
}
80119
},
81120
methods: {
82121
formatDate,
83122
getDuration
123+
},
124+
watch: {
125+
tab() {
126+
this.page = 1
127+
}
84128
}
85129
}
86130
</script>
87131
<style lang="scss" scoped>
88132
.v-card .v-card-title {
89133
line-height: 1.7rem;
90134
}
135+
.v-tab.v-tab.v-btn {
136+
min-width: 0;
137+
}
91138
</style>

ui/src/components/OrderSelector.vue

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<template>
2+
<v-select
3+
v-model="selectedValue"
4+
:items="options"
5+
:list-props="{ nav: true }"
6+
:menu-props="{ offsetY: true, bottom: true, nudgeTop: 8 }"
7+
density="compact"
8+
label="Order by"
9+
class="select--segmented"
10+
variant="outlined"
11+
attach
12+
single-line
13+
@update:model-value="emitValue"
14+
>
15+
<template #prepend>
16+
<v-tooltip location="bottom">
17+
<template #activator="{ props }">
18+
<v-btn
19+
v-bind="props"
20+
variant="text"
21+
height="32"
22+
@click="changeOrder"
23+
@keyup.enter="changeOrder"
24+
>
25+
<v-icon small>
26+
{{ icon }}
27+
</v-icon>
28+
</v-btn>
29+
</template>
30+
<span> {{ descending ? 'Descending ' : 'Ascending ' }} order </span>
31+
</v-tooltip>
32+
</template>
33+
</v-select>
34+
</template>
35+
<script>
36+
export default {
37+
name: 'OrderSelector',
38+
emits: ['update:value'],
39+
props: {
40+
/** Objects should have title and value properties */
41+
options: {
42+
type: Array,
43+
required: true
44+
},
45+
default: {
46+
type: String,
47+
required: false,
48+
default: undefined
49+
}
50+
},
51+
data() {
52+
return {
53+
descending: true,
54+
selectedValue: this.default
55+
}
56+
},
57+
computed: {
58+
value() {
59+
return `${this.descending ? '-' : ''}${this.selectedValue}`
60+
},
61+
icon() {
62+
return this.descending ? 'mdi-sort-descending' : 'mdi-sort-ascending'
63+
}
64+
},
65+
methods: {
66+
emitValue() {
67+
this.$emit('update:value', this.value)
68+
},
69+
changeOrder() {
70+
this.descending = !this.descending
71+
if (this.selectedValue) {
72+
this.emitValue()
73+
}
74+
}
75+
}
76+
}
77+
</script>
78+
<style lang="scss" scoped>
79+
.select--segmented {
80+
max-width: fit-content;
81+
82+
.v-select__selection {
83+
margin-top: 0;
84+
height: 37px;
85+
}
86+
87+
:deep(.v-field) {
88+
font-size: 0.875rem;
89+
font-weight: 500;
90+
}
91+
92+
:deep(.v-input__prepend) {
93+
border: solid rgba(0, 0, 0, 0.15);
94+
border-width: 1px 0 1px 1px;
95+
border-radius: 4px 0 0 4px;
96+
margin: 0;
97+
}
98+
99+
:deep(.v-input__prepend) + .v-input__control > .v-field--center-affix {
100+
border-radius: 0 4px 4px 0;
101+
}
102+
103+
:deep(.v-field__outline) {
104+
--v-field-border-opacity: 0.15;
105+
}
106+
}
107+
</style>

ui/src/components/StatusIcon.vue

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<template>
2+
<v-icon :color="status" :size="size">
3+
{{ icon }}
4+
</v-icon>
5+
</template>
6+
<script>
7+
export default {
8+
name: 'StatusIcon',
9+
props: {
10+
status: {
11+
type: String,
12+
required: true
13+
},
14+
size: {
15+
type: String,
16+
required: false,
17+
default: 'small'
18+
}
19+
},
20+
computed: {
21+
icon() {
22+
switch (this.status) {
23+
case 'completed':
24+
return 'mdi-check'
25+
case 'failed':
26+
return 'mdi-alert-circle-outline'
27+
case 'running':
28+
return 'mdi-sync'
29+
default:
30+
return 'mdi-clock-outline'
31+
}
32+
}
33+
}
34+
}
35+
</script>

ui/src/components/TaskCard.vue

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
size="small"
2222
start
2323
>
24-
{{ statusIcon }}
24+
<status-icon :status="status" size="x-small" />
2525
</v-icon>
2626
Last run {{ lastRunDate }}
2727
</v-card-subtitle>
@@ -42,17 +42,25 @@
4242
{{ formattedInterval }}
4343
</span>
4444
</p>
45+
<p v-if="failures" class="pb-2 text-body-2">
46+
<v-icon color="failed" size="small" start> mdi-alert-circle-outline </v-icon>
47+
<span class="font-weight-medium">
48+
{{ failures }}
49+
</span>
50+
failure{{ failures > 1 ? 's' : '' }}
51+
</p>
4552
</v-col>
4653
</v-row>
4754
</status-card>
4855
</template>
4956
<script>
5057
import { formatDate } from '@/utils/dates'
5158
import StatusCard from '@/components/StatusCard.vue'
59+
import StatusIcon from './StatusIcon.vue'
5260
5361
export default {
5462
name: 'TaskCard',
55-
components: { StatusCard },
63+
components: { StatusCard, StatusIcon },
5664
props: {
5765
age: {
5866
type: [Number, String],
@@ -98,6 +106,11 @@ export default {
98106
type: String,
99107
required: false,
100108
default: null
109+
},
110+
failures: {
111+
type: Number,
112+
required: false,
113+
default: null
101114
}
102115
},
103116
computed: {
@@ -111,18 +124,6 @@ export default {
111124
return `${this.interval} seconds`
112125
}
113126
},
114-
statusIcon() {
115-
switch (this.status) {
116-
case 'completed':
117-
return 'mdi-check'
118-
case 'failed':
119-
return 'mdi-close'
120-
case 'running':
121-
return 'mdi-sync'
122-
default:
123-
return 'mdi-calendar'
124-
}
125-
},
126127
lastRunDate() {
127128
if (this.lastExecution) {
128129
return formatDate(this.lastExecution)

0 commit comments

Comments
 (0)