aboutsummaryrefslogtreecommitdiffstats
path: root/yocto_console_view/src/module/main.module.coffee
blob: 41ea23445e5289ede0dfaa39e26a774faf2b6ae1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# Register new module
class App extends App
    constructor: ->
        return [
            'ui.router'
            'ui.bootstrap'
            'ngAnimate'
            'guanlecoja.ui'
            'bbData'
        ]

class State extends Config
    constructor: ($stateProvider, glMenuServiceProvider, bbSettingsServiceProvider) ->

        # Name of the state
        name = 'console'

        # Menu configuration
        glMenuServiceProvider.addGroup
            name: name
            caption: 'Yocto Console View'
            icon: 'exclamation-circle'
            order: 5

        # Configuration
        cfg =
            group: name
            caption: 'Yocto Console View'

        # Register new state
        state =
            controller: "#{name}Controller"
            controllerAs: "c"
            templateUrl: "yocto_console_view/views/#{name}.html"
            name: name
            url: "/#{name}"
            data: cfg

        $stateProvider.state(state)

        bbSettingsServiceProvider.addSettingsGroup
            name: 'Console'
            caption: 'Console related settings'
            items: [
                type: 'integer'
                name: 'buildLimit'
                caption: 'Number of builds to fetch'
                default_value: 200
            ,
                type: 'integer'
                name: 'changeLimit'
                caption: 'Number of changes to fetch'
                default_value: 30
            ]

class Console extends Controller
    constructor: (@$scope, $q, @$window, dataService, bbSettingsService, resultsService,
        @$uibModal, @$timeout) ->
        angular.extend this, resultsService
        settings = bbSettingsService.getSettingsGroup('Console')
        @buildLimit = settings.buildLimit.value
        @changeLimit = settings.changeLimit.value
        @dataAccessor = dataService.open().closeOnDestroy(@$scope)
        @_infoIsExpanded = {}
        @$scope.all_builders = @all_builders = @dataAccessor.getBuilders()
        @$scope.builders = @builders = []
        if Intl?
            collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'})
            @strcompare = collator.compare
        else
            @strcompare = (a, b) ->
                if a < b
                    return -1
                if a == b
                    return 0
                return 1

        @$scope.revmapping = @revmapping = {}
        @$scope.branchmapping = @branchmapping = {}

        @$scope.builds = @builds = @dataAccessor.getBuilds
            property: ["yp_build_revision", "yp_build_branch", "reason"]
            limit: @buildLimit
            order: '-started_at'
        @changes = @dataAccessor.getChanges({limit: @changeLimit, order: '-changeid'})
        @buildrequests = @dataAccessor.getBuildrequests({limit: @buildLimit, order: '-submitted_at'})
        @buildsets = @dataAccessor.getBuildsets({limit: @buildLimit, order: '-submitted_at'})

        @builds.onChange = @changes.onChange = @buildrequests.onChange = @buildsets.onChange = @onChange

        @builds.onNew = (build) =>
            build.getProperties().onChange = (properties) =>
                change = false
                buildid = properties.endpoint.split('/')[1]
                if ! @revmapping[buildid]
                    rev = @getBuildProperty(properties[0], 'yp_build_revision')
                    if rev?
                        @revmapping[buildid] = rev
                        change = true
                if ! @branchmapping[buildid]
                    branch = @getBuildProperty(properties[0], 'yp_build_branch')
                    if branch?
                        @branchmapping[buildid] = branch
                        change = true
                if change and not @onchange_debounce?
                    @onchange_debounce = @$timeout(@_onChange, 100)


    getBuildProperty: (properties, property) ->
        hasProperty = properties && properties.hasOwnProperty(property)
        return  if hasProperty then properties[property][0] else null

    onChange: (s) =>
        # if there is no data, no need to try and build something.
        if @builds.length == 0 or @all_builders.length == 0 or not @changes.$resolved or
                @buildsets.length == 0 or @buildrequests == 0
            return
        if not @onchange_debounce?
            @onchange_debounce = @$timeout(@_onChange, 100)

    _onChange: =>
        @onchange_debounce = undefined
        # we only display builders who actually have builds
        for build in @builds
            @all_builders.get(build.builderid).hasBuild = true

        @sortBuildersByTags(@all_builders)

        @changesBySSID = {}
        @changesByRevision = {}
        for change in @changes
            @changesBySSID[change.sourcestamp.ssid] = change
            @changesByRevision[change.revision] = change
            @populateChange(change)


        for build in @builds
            @matchBuildWithChange(build)

        @filtered_changes = []
        for ssid, change of @changesBySSID
            if change.comments
                change.subject = change.comments.split("\n")[0]
            for builder in change.builders
                if builder.builds.length > 0
                    @filtered_changes.push(change)
                    break
    ###
    # Sort builders by tags
    # Buildbot eight has the category option, but it was only limited to one category per builder,
    # which make it easy to sort by category
    # Here, we have multiple tags per builder, we need to try to group builders with same tags together
    # The algorithm is rather twisted. It is a first try at the concept of grouping builders by tags..
    ###

    sortBuildersByTags: (all_builders) ->
        # first we only want builders with builds
        builders_with_builds = []
        builderids_with_builds = ""
        for builder in all_builders
            if builder.hasBuild
                builders_with_builds.push(builder)
                builderids_with_builds += "." + builder.builderid

        if builderids_with_builds == @last_builderids_with_builds
            # don't recalculate if it hasn't changed!
            return
        # we call recursive function, which finds non-overlapping groups
        tag_line = @_sortBuildersByTags(builders_with_builds)
        # we get a tree of builders grouped by tags
        # we now need to flatten the tree, in order to build several lines of tags
        # (each line is representing a depth in the tag tree)
        # we walk the tree left to right and build the list of builders in the tree order, and the tag_lines
        # in the tree, there are groups of remaining builders, which could not be grouped together,
        # those have the empty tag ''
        tag_lines = []

        sorted_builders = []
        set_tag_line = (depth, tag, colspan) ->
            # we build the tag lines by using a sparse array
            _tag_line = tag_lines[depth]
            if not _tag_line?
                # initialize the sparse array
                _tag_line = tag_lines[depth] = []
            else
                # if we were already initialized, look at the last tag if this is the same
                # we merge the two entries
                last_tag = _tag_line[_tag_line.length - 1]
                if last_tag.tag == tag
                    last_tag.colspan += colspan
                    return
            _tag_line.push(tag: tag, colspan: colspan)
        self = @
        # recursive tree walking
        walk_tree = (tag, depth) ->
            set_tag_line(depth, tag.tag, tag.builders.length)
            if not tag.tag_line? or tag.tag_line.length == 0
                # this is the leaf of the tree, sort by buildername, and add them to the
                # list of sorted builders
                tag.builders.sort (a, b) -> self.strcompare(a.name, b.name)
                sorted_builders = sorted_builders.concat(tag.builders)
                for i in [1..100]  # set the remaining depth of the tree to the same colspan
                                   # (we hardcode the maximum depth for now :/ )
                    set_tag_line(depth + i, '', tag.builders.length)
                return
            for _tag in tag.tag_line
                walk_tree(_tag, depth + 1)

        for tag in tag_line
            walk_tree(tag, 0)

        @builders = sorted_builders
        @tag_lines = []
        # make a new array to avoid it to be sparse, and to remove lines filled with null tags
        for tag_line in tag_lines
            if not (tag_line.length == 1 and tag_line[0].tag == "")
                @tag_lines.push(tag_line)
        @last_builderids_with_builds = builderids_with_builds
    ###
    # recursive function which sorts the builders by tags
    # call recursively with groups of builders smaller and smaller
    ###
    _sortBuildersByTags: (all_builders) ->

        # first find out how many builders there is by tags in that group
        builders_by_tags = {}
        for builder in all_builders
            if builder.tags?
                for tag in builder.tags
                    if not builders_by_tags[tag]?
                        builders_by_tags[tag] = []
                    builders_by_tags[tag].push(builder)
        tags = []
        for tag, builders of builders_by_tags
            # we don't want the tags that are on all the builders
            if builders.length < all_builders.length
                tags.push(tag: tag, builders: builders)

        # sort the tags to first look at tags with the larger number of builders
        # @FIXME maybe this is not the best method to find the best groups
        tags.sort (a, b) -> b.builders.length - a.builders.length

        tag_line = []
        chosen_builderids = {}
        # pick the tags one by one, by making sure we make non-overalaping groups
        for tag in tags
            excluded = false
            for builder in tag.builders
                if chosen_builderids.hasOwnProperty(builder.builderid)
                    excluded = true
                    break
            if not excluded
                for builder in tag.builders
                    chosen_builderids[builder.builderid] = tag.tag
                tag_line.push(tag)

        # some builders do not have tags, we put them in another group
        remaining_builders = []
        for builder in all_builders
            if not chosen_builderids.hasOwnProperty(builder.builderid)
                remaining_builders.push(builder)

        if remaining_builders.length
            tag_line.push(tag: "", builders: remaining_builders)

        # if there is more than one tag in this line, we need to recurse
        if tag_line.length > 1
            for tag in tag_line
                tag.tag_line = @_sortBuildersByTags(tag.builders)
        return tag_line

    ###
    # fill a change with a list of builders
    ###
    populateChange: (change) ->
        change.builders = []
        change.buildersById = {}
        for builder in @builders
            builder = builderid: builder.builderid, name: builder.name, builds: []
            change.builders.push(builder)
            change.buildersById[builder.builderid] = builder
    ###
    # Match builds with a change
    ###
    matchBuildWithChange: (build) =>
        buildrequest = @buildrequests.get(build.buildrequestid)
        if not buildrequest?
            return
        buildset = @buildsets.get(buildrequest.buildsetid)
        if not buildset?
            return
        if  buildset? and buildset.sourcestamps?
            for sourcestamp in buildset.sourcestamps
                change = @changesBySSID[sourcestamp.ssid]

        if build.properties?.yp_build_revision? or @revmapping[build.buildid]
            if build.properties?.yp_build_revision?
                rev = build.properties.yp_build_revision[0]
            else
                rev = @revmapping[build.buildid]
            change = @changesByRevision[rev]
            if not change?
                change = @changesBySSID[rev]
            if not change?
                change = @makeFakeChange(rev, build.started_at, rev)
            if buildset? and buildset.parent_buildid?
                oldrev = "Unresolved #{buildset.parent_buildid}"
                delete @changesBySSID[oldrev]
            oldrev = "Unresolved #{build.builderid}-#{build.buildid}"
            delete @changesBySSID[oldrev]

            change.caption = "Commit"
            if build.properties?.yp_build_branch?
                change.caption = build.properties.yp_build_branch[0]
            if @branchmapping[build.buildid]
                change.caption = @branchmapping[build.buildid]
            change.revlink = "http://git.yoctoproject.org/cgit.cgi/poky/commit/?id=" + rev
            change.errorlink = "http://errors.yoctoproject.org/Errors/Latest/?filter=" + rev + "&type=commit&limit=150"
            if build.properties?.reason?
                change.reason = build.properties.reason[0]
        else
            if buildset? and buildset.parent_buildid?
                rev = "Unresolved #{buildset.parent_buildid}"
                if not change?
                    change = @changesBySSID[rev]
                if not change?
                    oldrev = "Unresolved #{build.builderid}-#{build.buildid}"
                    delete @changesBySSID[oldrev]
                    change = @makeFakeChange(rev, build.started_at, rev)
            if not change?
                rev = "Unresolved #{build.builderid}-#{build.buildid}"
                if not change?
                    change = @changesBySSID[rev]
                if not change?
                    change = @makeFakeChange(rev, build.started_at, rev)
            change.caption = rev

        change.buildersById[build.builderid].builds.push(build)

    makeFakeChange: (revision, when_timestamp, comments) =>
        change =
            revision: revision
            changeid: revision
            when_timestamp: when_timestamp
            comments: comments
        @changesBySSID[revision] = change
        @populateChange(change)
        return change
    ###
    # Open all change row information
    ###
    openAll: ->
        for change in @filtered_changes
            change.show_details = true

    ###
    # Close all change row information
    ###
    closeAll: ->
        for change in @filtered_changes
            change.show_details = false

    ###
    # Calculate row header (aka first column) width
    # depending if we display commit comment, we reserve more space
    ###
    getRowHeaderWidth: ->
        if @hasExpanded()
            return 400  # magic value enough to hold 78 characters lines
        else
            return 200
    ###
    # Calculate col header (aka first row) height
    # It depends on the length of the longest builder
    ###
    getColHeaderHeight: ->
        max_buildername = 0
        for builder in @builders
            max_buildername = Math.max(builder.name.length, max_buildername)
        return Math.max(100, max_buildername * 3)

    ###
    #
    # Determine if we use a 100% width table or if we allow horizontal scrollbar
    # depending on number of builders, and size of window, we need a fixed column size or a 100% width table
    #
    ###
    isBigTable: ->
        padding = @getRowHeaderWidth()
        if ((@$window.innerWidth - padding) / @builders.length) < 40
            return true
        return false
    ###
    #
    # do we have at least one change expanded?
    #
    ###
    hasExpanded: ->
        for change in @changes
            if @infoIsExpanded(change)
                return true
        return false

    ###
    #
    # display build details
    #
    ###
    selectBuild: (build) ->
        modal = @$uibModal.open
            templateUrl: 'yocto_console_view/views/modal.html'
            controller: 'consoleModalController as modal'
            windowClass: 'modal-big'
            resolve:
                selectedBuild: -> build

    ###
    #
    # toggle display of additional info for that change
    #
    ###
    toggleInfo: (change) ->
        change.show_details = !change.show_details
    infoIsExpanded: (change) ->
        return change.show_details