@@ -2,9 +2,28 @@ import { inject as service } from '@ember/service';
2
2
import { alias } from '@ember/object/computed' ;
3
3
import Controller , { inject as controller } from '@ember/controller' ;
4
4
import { computed } from '@ember/object' ;
5
+ import { scheduleOnce } from '@ember/runloop' ;
6
+ import intersection from 'lodash.intersection' ;
5
7
import Sortable from 'nomad-ui/mixins/sortable' ;
6
8
import Searchable from 'nomad-ui/mixins/searchable' ;
7
9
10
+ // An unattractive but robust way to encode query params
11
+ const qpSerialize = arr => ( arr . length ? JSON . stringify ( arr ) : '' ) ;
12
+ const qpDeserialize = str => {
13
+ try {
14
+ return JSON . parse ( str )
15
+ . compact ( )
16
+ . without ( '' ) ;
17
+ } catch ( e ) {
18
+ return [ ] ;
19
+ }
20
+ } ;
21
+
22
+ const qpSelection = qpKey =>
23
+ computed ( qpKey , function ( ) {
24
+ return qpDeserialize ( this . get ( qpKey ) ) ;
25
+ } ) ;
26
+
8
27
export default Controller . extend ( Sortable , Searchable , {
9
28
system : service ( ) ,
10
29
jobsController : controller ( 'jobs' ) ,
@@ -16,6 +35,10 @@ export default Controller.extend(Sortable, Searchable, {
16
35
searchTerm : 'search' ,
17
36
sortProperty : 'sort' ,
18
37
sortDescending : 'desc' ,
38
+ qpType : 'type' ,
39
+ qpStatus : 'status' ,
40
+ qpDatacenter : 'dc' ,
41
+ qpPrefix : 'prefix' ,
19
42
} ,
20
43
21
44
currentPage : 1 ,
@@ -28,11 +51,95 @@ export default Controller.extend(Sortable, Searchable, {
28
51
fuzzySearchProps : computed ( ( ) => [ 'name' ] ) ,
29
52
fuzzySearchEnabled : true ,
30
53
54
+ qpType : '' ,
55
+ qpStatus : '' ,
56
+ qpDatacenter : '' ,
57
+ qpPrefix : '' ,
58
+
59
+ selectionType : qpSelection ( 'qpType' ) ,
60
+ selectionStatus : qpSelection ( 'qpStatus' ) ,
61
+ selectionDatacenter : qpSelection ( 'qpDatacenter' ) ,
62
+ selectionPrefix : qpSelection ( 'qpPrefix' ) ,
63
+
64
+ optionsType : computed ( ( ) => [
65
+ { key : 'batch' , label : 'Batch' } ,
66
+ { key : 'parameterized' , label : 'Parameterized' } ,
67
+ { key : 'periodic' , label : 'Periodic' } ,
68
+ { key : 'service' , label : 'Service' } ,
69
+ { key : 'system' , label : 'System' } ,
70
+ ] ) ,
71
+
72
+ optionsStatus : computed ( ( ) => [
73
+ { key : 'pending' , label : 'Pending' } ,
74
+ { key : 'running' , label : 'Running' } ,
75
+ { key : 'dead' , label : 'Dead' } ,
76
+ ] ) ,
77
+
78
+ optionsDatacenter : computed ( 'visibleJobs.[]' , function ( ) {
79
+ const flatten = ( acc , val ) => acc . concat ( val ) ;
80
+ const allDatacenters = new Set (
81
+ this . get ( 'visibleJobs' )
82
+ . mapBy ( 'datacenters' )
83
+ . reduce ( flatten , [ ] )
84
+ ) ;
85
+
86
+ // Remove any invalid datacenters from the query param/selection
87
+ const availableDatacenters = Array . from ( allDatacenters ) . compact ( ) ;
88
+ scheduleOnce ( 'actions' , ( ) => {
89
+ this . set (
90
+ 'qpDatacenter' ,
91
+ qpSerialize ( intersection ( availableDatacenters , this . get ( 'selectionDatacenter' ) ) )
92
+ ) ;
93
+ } ) ;
94
+
95
+ return availableDatacenters . sort ( ) . map ( dc => ( { key : dc , label : dc } ) ) ;
96
+ } ) ,
97
+
98
+ optionsPrefix : computed ( 'visibleJobs.[]' , function ( ) {
99
+ // A prefix is defined as the start of a job name up to the first - or .
100
+ // ex: mktg-analytics -> mktg, ds.supermodel.classifier -> ds
101
+ const hasPrefix = / .[ - . _ ] / ;
102
+
103
+ // Collect and count all the prefixes
104
+ const allNames = this . get ( 'visibleJobs' ) . mapBy ( 'name' ) ;
105
+ const nameHistogram = allNames . reduce ( ( hist , name ) => {
106
+ if ( hasPrefix . test ( name ) ) {
107
+ const prefix = name . match ( / ( .+ ?) [ - . ] / ) [ 1 ] ;
108
+ hist [ prefix ] = hist [ prefix ] ? hist [ prefix ] + 1 : 1 ;
109
+ }
110
+ return hist ;
111
+ } , { } ) ;
112
+
113
+ // Convert to an array
114
+ const nameTable = Object . keys ( nameHistogram ) . map ( key => ( {
115
+ prefix : key ,
116
+ count : nameHistogram [ key ] ,
117
+ } ) ) ;
118
+
119
+ // Only consider prefixes that match more than one name
120
+ const prefixes = nameTable . filter ( name => name . count > 1 ) ;
121
+
122
+ // Remove any invalid prefixes from the query param/selection
123
+ const availablePrefixes = prefixes . mapBy ( 'prefix' ) ;
124
+ scheduleOnce ( 'actions' , ( ) => {
125
+ this . set (
126
+ 'qpPrefix' ,
127
+ qpSerialize ( intersection ( availablePrefixes , this . get ( 'selectionPrefix' ) ) )
128
+ ) ;
129
+ } ) ;
130
+
131
+ // Sort, format, and include the count in the label
132
+ return prefixes . sortBy ( 'prefix' ) . map ( name => ( {
133
+ key : name . prefix ,
134
+ label : `${ name . prefix } (${ name . count } )` ,
135
+ } ) ) ;
136
+ } ) ,
137
+
31
138
/**
32
- Filtered jobs are those that match the selected namespace and aren't children
139
+ Visible jobs are those that match the selected namespace and aren't children
33
140
of periodic or parameterized jobs.
34
141
*/
35
- filteredJobs :
computed ( 'model.[]' , '[email protected] ' , function ( ) {
142
+ visibleJobs :
computed ( 'model.[]' , '[email protected] ' , function ( ) {
36
143
// Namespace related properties are ommitted from the dependent keys
37
144
// due to a prop invalidation bug caused by region switching.
38
145
const hasNamespaces = this . get ( 'system.namespaces.length' ) ;
@@ -44,12 +151,60 @@ export default Controller.extend(Sortable, Searchable, {
44
151
. filter ( job => ! job . get ( 'parent.content' ) ) ;
45
152
} ) ,
46
153
154
+ filteredJobs : computed (
155
+ 'visibleJobs.[]' ,
156
+ 'selectionType' ,
157
+ 'selectionStatus' ,
158
+ 'selectionDatacenter' ,
159
+ 'selectionPrefix' ,
160
+ function ( ) {
161
+ const {
162
+ selectionType : types ,
163
+ selectionStatus : statuses ,
164
+ selectionDatacenter : datacenters ,
165
+ selectionPrefix : prefixes ,
166
+ } = this . getProperties (
167
+ 'selectionType' ,
168
+ 'selectionStatus' ,
169
+ 'selectionDatacenter' ,
170
+ 'selectionPrefix'
171
+ ) ;
172
+
173
+ // A job must match ALL filter facets, but it can match ANY selection within a facet
174
+ // Always return early to prevent unnecessary facet predicates.
175
+ return this . get ( 'visibleJobs' ) . filter ( job => {
176
+ if ( types . length && ! types . includes ( job . get ( 'displayType' ) ) ) {
177
+ return false ;
178
+ }
179
+
180
+ if ( statuses . length && ! statuses . includes ( job . get ( 'status' ) ) ) {
181
+ return false ;
182
+ }
183
+
184
+ if ( datacenters . length && ! job . get ( 'datacenters' ) . find ( dc => datacenters . includes ( dc ) ) ) {
185
+ return false ;
186
+ }
187
+
188
+ const name = job . get ( 'name' ) ;
189
+ if ( prefixes . length && ! prefixes . find ( prefix => name . startsWith ( prefix ) ) ) {
190
+ return false ;
191
+ }
192
+
193
+ return true ;
194
+ } ) ;
195
+ }
196
+ ) ,
197
+
47
198
listToSort : alias ( 'filteredJobs' ) ,
48
199
listToSearch : alias ( 'listSorted' ) ,
49
200
sortedJobs : alias ( 'listSearched' ) ,
50
201
51
202
isShowingDeploymentDetails : false ,
52
203
204
+ setFacetQueryParam ( queryParam , selection ) {
205
+ this . set ( queryParam , qpSerialize ( selection ) ) ;
206
+ } ,
207
+
53
208
actions : {
54
209
gotoJob ( job ) {
55
210
this . transitionToRoute ( 'jobs.job' , job . get ( 'plainId' ) ) ;
0 commit comments