yocto_console_view: import console_view plugin from buildbot

Plugin changed a lot with the buildbot transition from angularjs to
reactjs: start our own plugin customisation from scratch.

This commit only copies sources from buildboot git, without any
modification.

Signed-off-by: Mathieu Dubois-Briand <mathieu.dubois-briand@bootlin.com>
This commit is contained in:
Mathieu Dubois-Briand 2024-12-06 14:14:20 +01:00
parent 421dd04863
commit c2793d8bfd
30 changed files with 2611 additions and 1197 deletions

View File

@ -1,17 +0,0 @@
This custom buildbot plugin does three things:
* Replaces the "Console View" with our own "Yocto Console View"
* Adds a yoctochangedetails element to customise the information we display about a build
(link to the code repository, link to error reporting for the build)
* Add a custom field element, ReleaseSelector to the force build scheduler allowing
us to customise the form input fields to allow auto population of fields for
specific release branch combinations
The plugin ships in compiled form along with its source code. The generated files are:
yocto_console_view/static/*
yocto_console_view/VERSION
In order to build this plugin you need a buildbot development environment setup along
with its dependencies. FIXME, add more info on building.

View File

@ -14,13 +14,6 @@
# Copyright Buildbot Team Members # Copyright Buildbot Team Members
from buildbot.www.plugin import Application from buildbot.www.plugin import Application
from buildbot.schedulers.forcesched import ChoiceStringParameter
# create the interface for the setuptools entry point # create the interface for the setuptools entry point
ep = Application(__name__, "Buildbot Console View UI") ep = Application(__package__, "Buildbot Console View plugin")
class ReleaseSelector(ChoiceStringParameter):
spec_attributes = ["selectors"]
type = "releaseselector"
selectors = None

View File

@ -1,25 +1,57 @@
{ {
"name": "yocto-console-view", "name": "buildbot-console-view",
"plugin_name": "console_view",
"private": true, "private": true,
"main": "yocto_console_view/static/scripts.js", "type": "module",
"style": "yocto_console_view/static/styles.js", "module": "buildbot_console_view/static/scripts.js",
"style": "buildbot_console_view/static/styles.css",
"scripts": { "scripts": {
"build": "rimraf yocto_console_view/static && webpack --bail --progress --profile --env prod", "start": "vite",
"build-dev": "rimraf yocto_console_view/static && webpack --bail --progress --profile --env dev", "build": "vite build",
"dev": "webpack --bail --progress --profile --watch --env dev" "build-dev": "vite build -m development",
"test": "vitest run",
"test-watch": "vitest --watch"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"babel": {
"presets": [
"react-app"
]
},
"peerDependencies": {
"axios": "~1.7.7",
"mobx": "^6.6.1",
"mobx-react": "^9.1.1",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0"
}, },
"devDependencies": { "devDependencies": {
"angular-mocks": "^1.7.9", "@vitejs/plugin-react": "~4.3.1",
"buildbot-build-common": ">0.1", "axios": "~1.7.7",
"lodash": "^4.17.11", "axios-mock-adapter": "~2.0.0",
"rimraf": "^2.6.3" "buildbot-data-js": "link:../data-module",
"buildbot-plugin-support": "link:../plugin_support",
"buildbot-ui": "link:../ui",
"jsdom": "^25.0.0",
"mobx": "^6.6.1",
"mobx-react": "^9.1.1",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-app-polyfill": "~3.0.0",
"react-bootstrap": "^1.6.5",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"sass": "^1.56.0",
"vite": "~5.4.3",
"vitest": "~2.0.5"
}, },
"license": "GPL-2.0",
"dependencies": { "dependencies": {
"@uirouter/angularjs": "^1.0.15", "react-icons": "^5.3.0"
"angular": "^1.7.9",
"angular-animate": "^1.7.9",
"buildbot-data-js": ">0.1",
"guanlecoja-ui": ">0.1"
} }
} }

View File

@ -19,27 +19,31 @@ try:
from buildbot_pkg import setup_www_plugin from buildbot_pkg import setup_www_plugin
except ImportError: except ImportError:
import sys import sys
print("Please install buildbot_pkg module in order to install that package, or use the pre-build .whl modules available on pypi", file=sys.stderr)
print(
'Please install buildbot_pkg module in order to install that '
'package, or use the pre-build .whl modules available on pypi',
file=sys.stderr,
)
sys.exit(1) sys.exit(1)
setup_www_plugin( setup_www_plugin(
name='yocto-console-view', name='buildbot-console-view',
description='Yocto Project Console View plugin.', description='Buildbot Console View plugin',
author=u'Richard Purdie', author='Pierre Tardy',
author_email=u'richard.purdie@linuxfoundation.org', author_email='tardyp@gmail.com',
url='http://autobuilder.yoctoproject.org/', url='http://buildbot.net/',
packages=['yocto_console_view'], packages=['buildbot_console_view'],
package_data={ package_data={
'': [ '': [
'VERSION', 'VERSION',
'static/*' 'static/*',
'static/assets/*',
] ]
}, },
entry_points=""" entry_points="""
[buildbot.www] [buildbot.www]
console_view = yocto_console_view:ep console_view = yocto_console_view:ep
""", """,
classifiers=[ classifiers=['License :: OSI Approved :: GNU General Public License v2 (GPLv2)'],
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)'
],
) )

View File

@ -0,0 +1,18 @@
/*
This file is part of Buildbot. Buildbot is free software: you can
redistribute it and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation, version 2.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
details.
You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 51
Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Copyright Buildbot Team Members
*/
import './views/ConsoleView/ConsoleView';

File diff suppressed because one or more lines are too long

View File

@ -1,39 +0,0 @@
.console.no-select
.load-indicator(ng-hide='c.builds.$resolved && c.changes.$resolved && c.buildrequests.$resolved && c.buildsets.$resolved')
.spinner
i.fa.fa-circle-o-notch.fa-spin.fa-2x
p loading
div(ng-show="c.changes.$resolved && c.filtered_changes.length==0")
p No changes. Console view needs changesource to be setup, and
a(href="#changes") changes
| to be in the system.
table.table.table-striped.table-bordered(ng-hide="c.filtered_changes.length==0" ng-class="{'table-fixedwidth': c.isBigTable()}")
tr.first-row
th.row-header(ng-style="{'width': c.getRowHeaderWidth()}")
i.fa.fa-plus-circle.pull-left(ng-click='c.openAll()' uib-tooltip='Open information for all changes' uib-tooltip-placement='right')
i.fa.fa-minus-circle.pull-left(ng-click='c.closeAll()' uib-tooltip='Close information for all changes' uib-tooltip-placement='right')
th.column(ng-repeat="builder in c.builders")
span.builder(ng-style="{'margin-top': c.getColHeaderHeight()}")
a(ng-href='#/builders/{{ builder.builderid }}'
ng-bind='builder.name')
tr.tag_row
td.row-header
td(ng-repeat="buildergroup in c.buildergroups" colspan="{{buildergroup.colspan}}" ng-style="{'text-align': 'center'}")
| {{buildergroup.tag}}
tr(ng-repeat="change in c.filtered_changes | orderBy: ['-when_timestamp'] track by change.changeid")
td
yoctochangedetails(change="change")
td.column(ng-repeat="builder in change.builders" colspan="{{builder.colspan}}")
span(ng-repeat="build in builder.builds | orderBy: ['number']")
script(type="text/ng-template" id="buildsummarytooltip")
buildsummary(buildid="build.buildid" type="tooltip")
span.badge-status(ng-if='build.buildid'
uib-tooltip-template="'buildsummarytooltip'"
tooltip-class="buildsummarytooltipstyle"
tooltip-placement="auto left-bottom"
tooltip-popup-delay="400"
tooltip-popup-close-delay="400"
ng-class="c.results2class(build, 'pulse')"
ng-click='c.selectBuild(build)')
| {{ build.number }}

View File

@ -1,496 +0,0 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import 'angular-animate';
import '@uirouter/angularjs';
import 'guanlecoja-ui';
import 'buildbot-data-js';
class ConsoleState {
constructor($stateProvider, glMenuServiceProvider, bbSettingsServiceProvider) {
// Name of the state
const name = 'console';
// Menu configuration
glMenuServiceProvider.addGroup({
name,
caption: 'Yocto Console View',
icon: 'exclamation-circle',
order: 5
});
// Configuration
const cfg = {
group: name,
caption: 'Yocto Console View'
};
// Register new state
const state = {
controller: `${name}Controller`,
controllerAs: "c",
template: require('./console.tpl.jade'),
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 {
constructor($scope, $q, $window, dataService, bbSettingsService, resultsService,
$uibModal, $timeout) {
this.onChange = this.onChange.bind(this);
this._onChange = this._onChange.bind(this);
this.matchBuildWithChange = this.matchBuildWithChange.bind(this);
this.makeFakeChange = this.makeFakeChange.bind(this);
this.$scope = $scope;
this.$window = $window;
this.$uibModal = $uibModal;
this.$timeout = $timeout;
angular.extend(this, resultsService);
const settings = bbSettingsService.getSettingsGroup('Console');
this.buildLimit = settings.buildLimit.value;
this.changeLimit = settings.changeLimit.value;
this.dataAccessor = dataService.open().closeOnDestroy(this.$scope);
this._infoIsExpanded = {};
this.$scope.all_builders = (this.all_builders = this.dataAccessor.getBuilders());
this.$scope.builders = (this.builders = []);
this.$scope.buildergroups = (this.buildergroups = []);
if (typeof Intl !== 'undefined' && Intl !== null) {
const collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
this.strcompare = collator.compare;
} else {
this.strcompare = function(a, b) {
if (a < b) {
return -1;
}
if (a === b) {
return 0;
}
return 1;
};
}
this.$scope.revmapping = (this.revmapping = {});
this.$scope.branchmapping = (this.branchmapping = {});
this.$scope.builds = (this.builds = this.dataAccessor.getBuilds({
property: ["yp_build_revision", "yp_build_branch", "reason", "publish_destination"],
limit: this.buildLimit,
order: '-started_at'
}));
this.changes = this.dataAccessor.getChanges({limit: this.changeLimit, order: '-changeid'});
this.$scope.fakechanges = (this.fakechanges = []);
this.buildrequests = this.dataAccessor.getBuildrequests({limit: this.buildLimit, order: '-submitted_at'});
this.buildsets = this.dataAccessor.getBuildsets({limit: this.buildLimit, order: '-submitted_at'});
this.builds.onChange = this.onChange;
this.changes.onChange = this.onChange;
this.buildrequests.onChange = this.onChange;
this.buildsets.onChange = this.onChange;
this.builds.onNew = build => {
let change = false;
let {
buildid
} = build;
if ((build.properties != null ? build.properties.yp_build_revision : undefined) != null) {
this.revmapping[build.buildid] = build.properties.yp_build_revision[0];
change = true;
}
if ((build.properties != null ? build.properties.yp_build_branch : undefined) != null) {
this.branchmapping[build.buildid] = build.properties.yp_build_branch[0];
change = true;
}
if ((!this.revmapping[buildid] || !this.branchmapping[buildid]) && !build.complete_at) {
build.getProperties().onChange = properties => {
change = false;
buildid = properties.endpoint.split('/')[1];
if (!this.revmapping[buildid]) {
const rev = this.getBuildProperty(properties[0], 'yp_build_revision');
if (rev != null) {
this.revmapping[buildid] = rev;
change = true;
}
}
if (!this.branchmapping[buildid]) {
const branch = this.getBuildProperty(properties[0], 'yp_build_branch');
if (branch != null) {
this.branchmapping[buildid] = branch;
change = true;
}
}
if (change && (this.onchange_debounce == null)) {
this.onchange_debounce = this.$timeout(this._onChange, 100);
}
};
}
if (change && (this.onchange_debounce == null)) {
this.onchange_debounce = this.$timeout(this._onChange, 100);
}
};
}
getBuildProperty(properties, property) {
const hasProperty = properties && properties.hasOwnProperty(property);
if (hasProperty) { return properties[property][0]; } else { return null; }
}
onChange(s) {
// if there is no data, no need to try and build something.
if ((this.builds.length === 0) || (this.all_builders.length === 0) || !this.changes.$resolved ||
(this.buildsets.length === 0) || (this.buildrequests === 0)) {
return;
}
if ((this.onchange_debounce == null)) {
this.onchange_debounce = this.$timeout(this._onChange, 100);
}
}
_onChange() {
let build, change;
this.onchange_debounce = undefined;
// we only display builders who actually have builds
for (build of Array.from(this.builds)) {
this.all_builders.get(build.builderid).hasBuild = true;
}
this.sortBuildersByTags(this.all_builders);
if (this.changesBySSID == null) { this.changesBySSID = {}; }
if (this.changesByRevision == null) { this.changesByRevision = {}; }
for (change of Array.from(this.changes)) {
this.changesBySSID[change.sourcestamp.ssid] = change;
this.changesByRevision[change.revision] = change;
this.populateChange(change);
}
for (change of Array.from(this.fakechanges)) {
this.populateChange(change);
}
for (build of Array.from(this.builds)) {
this.matchBuildWithChange(build);
}
this.filtered_changes = [];
for (let ssid in this.changesBySSID) {
change = this.changesBySSID[ssid];
if (change.comments) {
change.subject = change.comments.split("\n")[0];
}
for (let builder of Array.from(change.builders)) {
if (builder.builds.length > 0) {
this.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
let builder, builders, tag;
const builders_with_builds = [];
let builderids_with_builds = "";
for (let builder of Array.from(all_builders)) {
if (builder.hasBuild && builder.name != 'indexing') {
builders_with_builds.push(builder);
builderids_with_builds += `.${builder.builderid}`;
}
}
if (builderids_with_builds === this.last_builderids_with_builds) {
// don't recalculate if it hasn't changed!
return;
}
const builders_by_tags = {};
for (builder of Array.from(builders_with_builds)) {
if (builder.tags != null && builder.tags.length) {
for (tag of Array.from(builder.tags)) {
if ((builders_by_tags[tag] == null)) {
builders_by_tags[tag] = [];
}
builders_by_tags[tag].push(builder);
}
} else {
if ((builders_by_tags[''] == null)) {
builders_by_tags[''] = [];
}
builders_by_tags[''].push(builder);
}
}
const self = this;
for (tag in builders_by_tags) {
builders_by_tags[tag].sort((a, b) => self.strcompare(a.name, b.name));
}
let buildergroups = [];
for (tag in builders_by_tags) {
if (tag != '') {
buildergroups.push({
name: builders_by_tags[tag][0].name,
tag: tag,
builders: builders_by_tags[tag],
colspan: builders_by_tags[tag].length
});
}
}
for (builder in builders_by_tags['']) {
buildergroups.push({
name: builders_by_tags[''][builder].name,
tag: '',
builders: [builders_by_tags[''][builder]],
colspan: 1
});
}
buildergroups.sort((a, b) => self.strcompare(a.name, b.name));
let sorted_builders = [];
for (let group in buildergroups) {
for (builder in buildergroups[group].builders) {
sorted_builders.push(buildergroups[group].builders[builder])
}
}
this.builders = sorted_builders;
this.buildergroups = buildergroups;
this.tag_lines = [];
return this.last_builderids_with_builds = builderids_with_builds;
}
/*
* fill a change with a list of builders
*/
populateChange(change) {
change.builders = [];
change.buildersById = {};
for (let buildergroup of Array.from(this.buildergroups)) {
let builderg = {name: buildergroup.name, builds: [], builders: [], colspan: buildergroup.builders.length};
for (let builder of Array.from(buildergroup.builders)) {
builderg.builders.push(builder);
change.buildersById[builder.builderid] = builderg;
}
change.builders.push(builderg);
}
}
/*
* Match builds with a change
*/
matchBuildWithChange(build) {
let change, oldrev, rev;
const buildrequest = this.buildrequests.get(build.buildrequestid);
if ((buildrequest == null)) {
return;
}
const buildset = this.buildsets.get(buildrequest.buildsetid);
if ((buildset == null)) {
return;
}
if (((build.properties != null ? build.properties.yp_build_revision : undefined) != null) || this.revmapping[build.buildid]) {
if ((build.properties != null ? build.properties.yp_build_revision : undefined) != null) {
rev = build.properties.yp_build_revision[0];
} else {
rev = this.revmapping[build.buildid];
}
change = this.changesByRevision[rev];
if ((change == null)) {
change = this.changesBySSID[rev];
}
if ((change == null)) {
change = this.makeFakeChange(rev, build.started_at, rev);
this.fakechanges.push(change)
}
change.caption = "Commit";
if ((build.properties != null ? build.properties.yp_build_branch : undefined) != null) {
change.caption = build.properties.yp_build_branch[0];
}
if (this.branchmapping[build.buildid]) {
change.caption = this.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";
let bid = build.buildid;
if ((buildset != null) && (buildset.parent_buildid != null)) {
bid = buildset.parent_buildid;
}
if ((build.properties != null ? build.properties.reason : undefined) != null) {
change.reason = build.properties.reason[0];
}
if ((build.properties != null ? build.properties.publish_destination : undefined) != null) {
change.publishurl = build.properties.publish_destination[0].replace("/srv/autobuilder/autobuilder.yoctoproject.org/", "https://autobuilder.yocto.io/");
change.publishurl = change.publishurl.replace("/srv/autobuilder/autobuilder.yocto.io/", "https://autobuilder.yocto.io/");
}
} else {
rev = `Unresolved Revision`;
if ((change == null)) {
change = this.changesBySSID[rev];
}
if ((change == null)) {
change = this.makeFakeChange(rev, build.started_at, rev);
change.caption = rev;
this.fakechanges.push(change)
}
}
if (build.builderid in change.buildersById) {
change.buildersById[build.builderid].builds.push(build);
}
}
makeFakeChange(revision, when_timestamp, comments) {
const change = {
revision,
changeid: revision,
when_timestamp,
comments
};
this.changesBySSID[revision] = change;
this.populateChange(change);
return change;
}
/*
* Open all change row information
*/
openAll() {
return Array.from(this.filtered_changes).map((change) =>
(change.show_details = true));
}
/*
* Close all change row information
*/
closeAll() {
return Array.from(this.filtered_changes).map((change) =>
(change.show_details = false));
}
/*
* Calculate row header (aka first column) width
* depending if we display commit comment, we reserve more space
*/
getRowHeaderWidth() {
if (this.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() {
let max_buildername = 0;
for (let builder of Array.from(this.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() {
const padding = this.getRowHeaderWidth();
if (((this.$window.innerWidth - padding) / this.builders.length) < 40) {
return true;
}
return false;
}
/*
*
* do we have at least one change expanded?
*
*/
hasExpanded() {
for (let change of Array.from(this.changes)) {
if (this.infoIsExpanded(change)) {
return true;
}
}
return false;
}
/*
*
* display build details
*
*/
selectBuild(build) {
let modal;
return modal = this.$uibModal.open({
template: require('./view/modal/modal.tpl.jade'),
controller: 'consoleModalController as modal',
windowClass: 'modal-big',
resolve: {
selectedBuild() { return build; }
}
});
}
/*
*
* toggle display of additional info for that change
*
*/
toggleInfo(change) {
return change.show_details = !change.show_details;
}
infoIsExpanded(change) {
return change.show_details;
}
}
angular.module('yocto_console_view', [
'ui.router', 'ui.bootstrap', 'ngAnimate', 'guanlecoja.ui', 'bbData'])
.config(['$stateProvider', 'glMenuServiceProvider', 'bbSettingsServiceProvider', ConsoleState])
.controller('consoleController', ['$scope', '$q', '$window', 'dataService', 'bbSettingsService', 'resultsService', '$uibModal', '$timeout', Console]);
require('./view/modal/modal.controller.js');
require('./releaseselectorfield.directive.js');
require('./yoctochangedetails.directive.js');

View File

@ -1,228 +0,0 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
beforeEach(function() {
angular.mock.module(function($provide) {
$provide.service('$uibModal', function() { return {open() {}}; });
});
angular.mock.module(function($provide) {
$provide.service('resultsService', function() { return {results2class() {}}; });
});
// Mock bbSettingsProvider
angular.mock.module(function($provide) {
$provide.provider('bbSettingsService', (function() {
let group = undefined;
const Cls = class {
static initClass() {
group = {};
}
addSettingsGroup(g) { return g.items.map(function(i) {
if (i.name === 'lazy_limit_waterfall') {
i.default_value = 2;
}
return group[i.name] = {value: i.default_value};
}); }
$get() {
return {
getSettingsGroup() {
return group;
},
save() {}
};
}
};
Cls.initClass();
return Cls;
})()
);
});
angular.mock.module('yocto_console_view');
});
describe('Console view', function() {
let $state = null;
beforeEach(inject($injector => $state = $injector.get('$state'))
);
it('should register a new state with the correct configuration', function() {
const name = 'console';
const state = $state.get().pop();
const { data } = state;
expect(state.controller).toBe(`${name}Controller`);
expect(state.controllerAs).toBe('c');
expect(state.url).toBe(`/${name}`);
});
});
describe('Console view controller', function() {
// Test data
let $rootScope, $timeout, $window, dataService, scope;
let builders = [{
builderid: 1,
masterids: [1]
}
, {
builderid: 2,
masterids: [1]
}
, {
builderid: 3,
masterids: [1]
}
, {
builderid: 4,
masterids: [1]
}
];
const builds1 = [{
buildid: 1,
builderid: 1,
buildrequestid: 1
}
, {
buildid: 2,
builderid: 2,
buildrequestid: 1
}
, {
buildid: 3,
builderid: 4,
buildrequestid: 2
}
, {
buildid: 4,
builderid: 3,
buildrequestid: 2
}
];
const builds2 = [{
buildid: 5,
builderid: 2,
buildrequestid: 3
}
];
const builds = builds1.concat(builds2);
const buildrequests = [{
builderid: 1,
buildrequestid: 1,
buildsetid: 1
}
, {
builderid: 1,
buildrequestid: 2,
buildsetid: 1
}
, {
builderid: 1,
buildrequestid: 3,
buildsetid: 2
}
];
const buildsets = [{
bsid: 1,
sourcestamps: [
{ssid: 1}
]
}
, {
bsid: 2,
sourcestamps: [
{ssid: 2}
]
}
];
const changes = [{
changeid: 1,
sourcestamp: {
ssid: 1
}
}
];
let createController = (scope = ($rootScope = (dataService = ($window = ($timeout = null)))));
const injected = function($injector) {
const $q = $injector.get('$q');
$rootScope = $injector.get('$rootScope');
$window = $injector.get('$window');
$timeout = $injector.get('$timeout');
dataService = $injector.get('dataService');
scope = $rootScope.$new();
dataService.when('builds', builds);
dataService.when('builders', builders);
dataService.when('changes', changes);
dataService.when('buildrequests', buildrequests);
dataService.when('buildsets', buildsets);
// Create new controller using controller as syntax
const $controller = $injector.get('$controller');
createController = () =>
$controller('consoleController as c', {
// Inject controller dependencies
$q,
$window,
$scope: scope
}
)
;
};
beforeEach(inject(injected));
it('should be defined', function() {
createController();
expect(scope.c).toBeDefined();
});
it('should bind the builds, builders, changes, buildrequests and buildsets to scope', function() {
createController();
$rootScope.$digest();
$timeout.flush();
expect(scope.c.builds).toBeDefined();
expect(scope.c.builds.length).toBe(builds.length);
expect(scope.c.all_builders).toBeDefined();
expect(scope.c.all_builders.length).toBe(builders.length);
expect(scope.c.changes).toBeDefined();
expect(scope.c.changes.length).toBe(changes.length);
expect(scope.c.buildrequests).toBeDefined();
expect(scope.c.buildrequests.length).toBe(buildrequests.length);
expect(scope.c.buildsets).toBeDefined();
expect(scope.c.buildsets.length).toBe(buildsets.length);
});
it('should match the builds with the change', function() {
createController();
$timeout.flush();
$rootScope.$digest();
$timeout.flush();
expect(scope.c.changes[0]).toBeDefined();
expect(scope.c.changes[0].builders).toBeDefined();
({ builders } = scope.c.changes[0]);
expect(builders[0].builds[0].buildid).toBe(1);
expect(builders[1].builds[0].buildid).toBe(2);
expect(builders[2].builds[0].buildid).toBe(4);
expect(builders[3].builds[0].buildid).toBe(3);
});
xit('should match sort the builders by tag groups', function() {
createController();
const _builders = FIXTURES['builders.fixture.json'].builders;
for (let builder of Array.from(_builders)) {
builder.hasBuild = true;
}
scope.c.sortBuildersByTags(_builders);
expect(_builders.length).toBe(scope.c.builders.length);
expect(scope.c.tag_lines.length).toEqual(5);
});
});

View File

@ -1,68 +0,0 @@
/*
* decaffeinate suggestions:
* DS002: Fix invalid constructor
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
class Releaseselectorfield {
constructor() {
return {
replace: false,
restrict: 'E',
scope: false,
template: require('./releaseselectorfield.tpl.jade'),
controller: '_ReleaseselectorfieldController'
};
}
}
class _releaseselectorfield {
constructor($scope, $http) {
// HACK: we find the rootfield by doing $scope.$parent.$parent
let rootfield = $scope;
while ((rootfield != null) && (rootfield.rootfield == null)) {
rootfield = rootfield.$parent;
}
if ((rootfield == null)) {
console.log("rootfield not found!?!?");
return;
}
// copy paste of code in forcedialog, which flatten the fields to be able to find easily
const fields_ref = {};
var gatherFields = fields => Array.from(fields).map((field) =>
(field.fields != null) ?
gatherFields(field.fields)
:
(fields_ref[field.fullName] = field));
gatherFields(rootfield.rootfield.fields);
// when our field change, we update the fields that we are suppose to
$scope.$watch("field.value", function(n, o) {
const selector = $scope.field.selectors[n];
if (selector != null) {
return (() => {
const result = [];
for (let k in selector) {
const v = selector[k];
if (k in fields_ref) {
result.push(fields_ref[k].value = v);
}
}
return result;
})();
}
});
}
}
angular.module('yocto_console_view')
.directive('releaseselectorfield', [Releaseselectorfield])
.controller('_ReleaseselectorfieldController', ['$scope', '$http', _releaseselectorfield])

View File

@ -1,5 +0,0 @@
basefield
label.control-label.col-sm-2(for="{{field.name}}")
| {{field.label}}
.col-sm-10
select.form-control(ng-model="field.value", ng-options="v for v in field.choices")

View File

@ -1,22 +0,0 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
class ConsoleModal {
constructor($scope, $uibModalInstance, selectedBuild) {
this.$uibModalInstance = $uibModalInstance;
this.selectedBuild = selectedBuild;
$scope.$on('$stateChangeStart', () => {
return this.close();
});
}
close() {
return this.$uibModalInstance.close();
}
}
angular.module('yocto_console_view')
.controller('consoleModalController', ['$scope', '$uibModalInstance', 'selectedBuild', ConsoleModal]);

View File

@ -1,9 +0,0 @@
.modal-big {
.modal-dialog {
width: 80%;
}
.fa {
cursor: pointer;
}
}

View File

@ -1,6 +0,0 @@
// Show build summary for the selected build in a modal window
.modal-header
i.fa.fa-times.pull-right(ng-click='modal.close()')
h4.modal-title Build summary
.modal-body
buildsummary(ng-if='modal.selectedBuild' buildid='modal.selectedBuild.buildid')

View File

@ -1,21 +0,0 @@
/*
* decaffeinate suggestions:
* DS002: Fix invalid constructor
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
class Yoctochangedetails {
constructor() {
return {
replace: true,
restrict: 'E',
scope: {
change: '=',
compact: '=?'
},
template: require('./yoctochangedetails.tpl.jade')
};
}
}
angular.module('yocto_console_view')
.directive('yoctochangedetails', [Yoctochangedetails])

View File

@ -1,42 +0,0 @@
div.yoctochangedetails(style="width:100%;")
div(style="width:100%;", ng-click="change.show_details = !change.show_details")
a(ng-if="change.revlink", ng-href="{{change.revlink}}", uib-tooltip="{{change.comments}}")
| {{ change.caption }} &nbsp;
a(ng-if="change.errorlink", ng-href="{{change.errorlink}}")
| {{ "Errors" }} &nbsp;
a(ng-if="change.publishurl", ng-href="{{change.publishurl}}")
| {{ "Output" }} &nbsp;
span(ng-if="!change.revlink", uib-tooltip="{{change.comments}}")
| {{ change.caption }} &nbsp;
span(ng-if="!compact" uib-tooltip="{{change.when_timestamp | dateformat:'LLL'}}")
| ({{ change.when_timestamp | timeago }}) &nbsp;
i.fa.fa-chevron-circle-right.rotate.clickable(ng-class="{'fa-rotate-90':change.show_details}")
div.anim-changedetails(ng-show="change.show_details")
table.table.table-striped.table-condensed(ng-show="change.show_details")
tr(ng-show="change.reason")
td Reason
td {{ change.reason }}
tr(ng-show="change.author")
td Author
td {{ change.author }}
tr
td Date
td {{ change.when_timestamp | dateformat:'LLL'}} ({{ change.when_timestamp | timeago }})
tr(ng-show="change.repository")
td Repository
td {{ change.repository }}
tr(ng-show="change.branch")
td Branch
td {{ change.branch }}
tr
td Revision
td
a(ng-if="change.revlink", ng-href="{{change.revlink}}")
| {{ change.revision }}
h5 Comment
pre {{ change.comments }}
h5 Changed files
ul
li(ng-repeat='file in change.files') {{file}}
p(ng-hide="change.files.length") No files

View File

@ -1,89 +0,0 @@
@column-width: 40px;
@headcol-width: 20px;
.console {
.table-fixedwidth {
width: initial;
}
.load-indicator {
width: 100%;
height: 100%;
z-index: 900;
background-color: #ffffff;
display: table;
.spinner {
display: table-cell;
vertical-align: middle;
text-align: center;
p {
font-weight: 300;
margin-top: 10px;
}
}
}
.column {
min-width: @column-width;
max-width: @column-width;
width: @column-width;
}
table {
border: none;
}
.tag_row{
td {
margin:0px;
padding:0px;
}
span {
position: relative;
float: left;
font-size: 10px;
overflow: hidden;
text-decoration: none;
white-space: nowrap;
}
}
tr.first-row {
background-color: #fff!important;
th {
border: none;
background-color: #fff !important;
}
.builder {
position: relative;
float: left;
font-size: 12px;
text-align: center;
transform: rotate(-45deg) ;
transform-origin: 0% 100%;
text-decoration: none;
white-space: nowrap;
}
.column {
width: @headcol-width;
min-width: @headcol-width;
max-width: @headcol-width;
}
}
}
.yoctochangedetails > .no-select > * {
margin-left: 0.3em;
margin-right: 0.3em;
}
.select-editable {
position:absolute;
top:0;
border:none;
margin:2px;
width:90%;
height:29px;
}
.select-editable input:focus {
outline:none;
}

View File

@ -0,0 +1,38 @@
tr.bb-console-table-first-row {
background-color: #fff !important;
th {
border: none;
}
}
.bb-console-table-builder {
position: relative;
float: left;
font-size: 1em;
width: 1.5em;
text-align: center;
transform: rotate(-25deg) ;
transform-origin: 0% 100%;
text-decoration: none;
white-space: nowrap;
}
.bb-console-tag-row {
td {
margin: 0;
padding: 0;
}
span {
position: relative;
float: left;
font-size: 10px;
overflow: hidden;
text-decoration: none;
white-space: nowrap;
}
}
.bb-console-changes-expand-icon {
float: left;
}

View File

@ -0,0 +1,97 @@
/*
This file is part of Buildbot. Buildbot is free software: you can
redistribute it and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation, version 2.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
details.
You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 51
Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Copyright Buildbot Team Members
*/
import {describe, expect, it} from "vitest";
import {Builder, IDataAccessor} from "buildbot-data-js";
import {sortBuildersByTags, TagLineConfig} from "./ConsoleView";
type TestBuilder = {
builderid: number;
tags: string[];
}
function testBuilderToReal(b: TestBuilder) {
return new Builder(undefined as unknown as IDataAccessor, 'a/1', {
builderid: b.builderid,
description: "desc",
masterids: [1],
name: `name${b.builderid}`,
tags: b.tags,
});
}
describe('ConsoleView', function() {
describe('sortBuildersByTags', function() {
function testSortBuildersByTags(builders: TestBuilder[],
expectedBuilders: TestBuilder[],
expectedTagLines: TagLineConfig[]) {
const [resultBuilders, resultLineConfigs] =
sortBuildersByTags(builders.map(b => testBuilderToReal(b)));
expect(resultBuilders).toStrictEqual(expectedBuilders.map(b => testBuilderToReal(b)));
expect(resultLineConfigs).toStrictEqual(expectedTagLines);
}
it('empty', function() {
testSortBuildersByTags([], [], []);
});
it('identical tag', function() {
testSortBuildersByTags([
{builderid: 1, tags: ['tag']},
{builderid: 2, tags: ['tag']},
{builderid: 3, tags: ['tag']}
], [
{builderid: 1, tags: ['tag']},
{builderid: 2, tags: ['tag']},
{builderid: 3, tags: ['tag']}
], []);
});
it('two tags', function() {
testSortBuildersByTags([
{builderid: 1, tags: ['tag']},
{builderid: 2, tags: ['tag']},
{builderid: 3, tags: ['tag']},
{builderid: 4, tags: ['tag2']}
], [
{builderid: 1, tags: ['tag']},
{builderid: 2, tags: ['tag']},
{builderid: 3, tags: ['tag']},
{builderid: 4, tags: ['tag2']}
], [[{tag: 'tag', colSpan: 3}, {tag: 'tag2', colSpan: 1}]]);
});
it('hierarchical', function() {
testSortBuildersByTags([
{builderid: 1, tags: ['tag10', 'tag21']},
{builderid: 2, tags: ['tag10', 'tag21']},
{builderid: 3, tags: ['tag10', 'tag22']},
{builderid: 4, tags: ['tag11', 'tag22']}
], [
{builderid: 1, tags: ['tag10', 'tag21']},
{builderid: 2, tags: ['tag10', 'tag21']},
{builderid: 3, tags: ['tag10', 'tag22']},
{builderid: 4, tags: ['tag11', 'tag22']},
], [
[{tag: 'tag10', colSpan: 3}, {tag: 'tag11', colSpan: 1}],
[{tag: 'tag21', colSpan: 2}, {tag: 'tag22', colSpan: 1}, {tag: "", colSpan: 1}],
]);
});
});
});

View File

@ -0,0 +1,541 @@
/*
This file is part of Buildbot. Buildbot is free software: you can
redistribute it and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation, version 2.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
details.
You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 51
Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Copyright Buildbot Team Members
*/
import './ConsoleView.scss'
import {ObservableMap} from "mobx";
import {observer, useLocalObservable} from "mobx-react";
import {Link} from "react-router-dom";
import {
FaExclamationCircle,
FaMinusCircle,
FaPlusCircle
} from "react-icons/fa";
import {OverlayTrigger, Table, Tooltip} from "react-bootstrap";
import {buildbotGetSettings, buildbotSetupPlugin} from "buildbot-plugin-support";
import {
Build,
Buildset,
Builder,
Buildrequest,
Change,
useDataAccessor, useDataApiQuery, IDataAccessor
} from "buildbot-data-js";
import {
BuildLinkWithSummaryTooltip,
ChangeDetails,
LoadingIndicator,
pushIntoMapOfArrays,
useWindowSize
} from "buildbot-ui";
type ChangeInfo = {
change: Change;
buildsByBuilderId: Map<number, Build[]>;
}
export type TagTreeItem = {
builders: Builder[];
tag: string;
childItems: TagTreeItem[];
}
export type TagItemConfig = {
tag: string,
colSpan: number
};
export type TagLineConfig = TagItemConfig[];
export function buildTagTree(builders: Builder[])
{
const buildersByTags = new Map<string, Builder[]>();
for (const builder of builders) {
if (builder.tags === null) {
continue;
}
for (const tag of builder.tags) {
pushIntoMapOfArrays(buildersByTags, tag, builder);
}
}
type TagInfo = {
tag: string;
builders: Builder[];
};
const undecidedTags: TagInfo[] = [];
for (const [tag, tagBuilders] of buildersByTags) {
if (tagBuilders.length < builders.length) {
undecidedTags.push({tag: tag, builders: tagBuilders});
}
}
// 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
undecidedTags.sort((a, b) => b.builders.length - a.builders.length);
const tagItems: TagTreeItem[] = [];
const builderIdToTag = new Map<number, string>();
// pick the tags one by one, by making sure we make non-overalaping groups
for (const tagInfo of undecidedTags) {
let excluded = false;
for (const builder of tagInfo.builders) {
if (builderIdToTag.has(builder.builderid)) {
excluded = true;
break;
}
}
if (!excluded) {
for (const builder of tagInfo.builders) {
builderIdToTag.set(builder.builderid, tagInfo.tag);
}
tagItems.push({tag: tagInfo.tag, builders: tagInfo.builders, childItems: []});
}
}
// some builders do not have tags, we put them in another group
const remainingBuilders = [];
for (const builder of builders) {
if (!builderIdToTag.has(builder.builderid)) {
remainingBuilders.push(builder);
}
}
if (remainingBuilders.length) {
tagItems.push({tag: "", builders: remainingBuilders, childItems: []});
}
// if there is more than one tag in this line, we need to recurse
if (tagItems.length > 1) {
for (const tagItem of tagItems) {
tagItem.childItems = buildTagTree(tagItem.builders);
}
}
return tagItems;
}
// Sorts and groups builders together by their tags.
export function sortBuildersByTags(builders: Builder[]) : [Builder[], TagLineConfig[]]
{
// we call recursive function, which finds non-overlapping groups
const tagLineItems = buildTagTree(builders);
// 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 ''
const tagLineConfigAtDepth = new Map<number, TagLineConfig>();
const resultBuilders: Builder[] = [];
const setTagLine = (depth: number, tag: string, colSpan: number) => {
const lineConfig = tagLineConfigAtDepth.get(depth);
if (lineConfig === undefined) {
tagLineConfigAtDepth.set(depth, [{tag: tag, colSpan: colSpan}]);
return;
}
// Attempt to merge identical tags
const lastItem = lineConfig[lineConfig.length - 1];
if (lastItem.tag === tag) {
lastItem.colSpan += colSpan;
return;
}
lineConfig.push({tag: tag, colSpan: colSpan});
};
const walkItemTree = (tagItem: TagTreeItem, depth: number) => {
setTagLine(depth, tagItem.tag, tagItem.builders.length);
if (tagItem.childItems.length === 0) {
// this is the leaf of the tree, sort by buildername, and add them to the
// list of sorted builders
tagItem.builders.sort((a, b) => a.name.localeCompare(b.name));
resultBuilders.push(...tagItem.builders);
for (let i = 1; i <= 100; i++) {
// set the remaining depth of the tree to the same colspan
// (we hardcode the maximum depth for now :/ )
setTagLine(depth + i, '', tagItem.builders.length);
}
return;
}
tagItem.childItems.map(item => walkItemTree(item, depth + 1));
};
for (const tagItem of tagLineItems) {
walkItemTree(tagItem, 0);
}
const resultTagLineConfigs: TagLineConfig[] = [];
for (const tagLineItems of tagLineConfigAtDepth.values()) {
if (tagLineItems.length === 1 && tagLineItems[0].tag === "") {
continue;
}
resultTagLineConfigs.push(tagLineItems);
}
return [resultBuilders, resultTagLineConfigs];
}
function resolveFakeChange(codebase: string, revision: string, whenTimestamp: number,
changesByFakeId: Map<string, ChangeInfo>): ChangeInfo
{
const fakeId = `${codebase}-${revision}`;
const existingChange = changesByFakeId.get(fakeId);
if (existingChange !== undefined) {
return existingChange;
}
const newChange = {
change: new Change(undefined as unknown as IDataAccessor, "a/1", {
changeid: 0,
author: "",
branch: "",
codebase: codebase,
comments: `Unknown revision ${revision}`,
files: [],
parent_changeids: [],
project: "",
properties: {},
repository: "",
revision: revision,
revlink: null,
when_timestamp: whenTimestamp,
}),
buildsByBuilderId: new Map<number, Build[]>
};
changesByFakeId.set(fakeId, newChange);
return newChange;
}
// Adjusts changesByFakeId for any new fake changes that are created
function selectChangeForBuild(build: Build, buildset: Buildset,
changesBySsid: Map<number, ChangeInfo>,
changesByRevision: Map<string, ChangeInfo>,
changesByFakeId: Map<string, ChangeInfo>) {
if (buildset.sourcestamps !== null) {
for (const sourcestamp of buildset.sourcestamps) {
const change = changesBySsid.get(sourcestamp.ssid);
if (change !== undefined) {
return change;
}
}
}
if (build.properties !== null && ('got_revision' in build.properties)) {
const revision = build.properties['got_revision'][0];
// got_revision can be per codebase or just the revision string
if (typeof(revision) === "string") {
const change = changesByRevision.get(revision);
if (change !== undefined) {
return change;
}
return resolveFakeChange("", revision, build.started_at, changesByFakeId);
}
const revisionMap = revision as {[codebase: string]: string};
for (const codebase in revisionMap) {
const codebaseRevision = revisionMap[codebase];
const change = changesByRevision.get(codebaseRevision);
if (change !== undefined) {
return change;
}
}
const codebases = Object.keys(revisionMap);
if (codebases.length === 0) {
return resolveFakeChange("unknown codebase", "", build.started_at, changesByFakeId);
}
return resolveFakeChange(codebases[0], revisionMap[codebases[0]], build.started_at,
changesByFakeId);
}
const revision = `unknown revision ${build.builderid}-${build.buildid}`;
return resolveFakeChange("unknown codebase", revision, build.started_at, changesByFakeId);
}
export const ConsoleView = observer(() => {
const accessor = useDataAccessor([]);
const settings = buildbotGetSettings();
const changeFetchLimit = settings.getIntegerSetting("Console.changeLimit");
const buildFetchLimit = settings.getIntegerSetting("Console.buildLimit");
const buildsetsQuery = useDataApiQuery(() => Buildset.getAll(accessor, {query: {
limit: buildFetchLimit,
order: '-submitted_at',
}}));
const changesQuery = useDataApiQuery(() => Change.getAll(accessor, {query: {
limit: changeFetchLimit,
order: '-changeid',
}}));
const buildersQuery = useDataApiQuery(() => Builder.getAll(accessor));
const buildrequestsQuery = useDataApiQuery(() => Buildrequest.getAll(accessor, {query: {
limit: buildFetchLimit,
order: '-submitted_at',
}}));
const buildsQuery = useDataApiQuery(() => Build.getAll(accessor, {query: {
limit: buildFetchLimit,
order: '-started_at',
property: ["got_revision"],
}}));
const windowSize = useWindowSize()
const changeIsExpandedByChangeId = useLocalObservable(() => new ObservableMap<number, boolean>());
const queriesResolved =
buildsetsQuery.resolved &&
changesQuery.resolved &&
buildersQuery.resolved &&
buildrequestsQuery.resolved &&
buildsQuery.resolved;
const builderIdsWithBuilds = new Set<number>();
for (const build of buildsQuery.array) {
builderIdsWithBuilds.add(build.builderid);
}
const buildersWithBuilds = buildersQuery.array.filter(b => builderIdsWithBuilds.has(b.builderid));
const [buildersToShow, tagLineConfigs] = sortBuildersByTags(buildersWithBuilds);
const changesByRevision = new Map<string, ChangeInfo>();
const changesBySsid = new Map<number, ChangeInfo>();
const changesByFakeId = new Map<string, ChangeInfo>();
for (const change of changesQuery.array) {
const changeInfo: ChangeInfo = {change: change, buildsByBuilderId: new Map<number, Build[]>()};
if (change.revision !== null) {
changesByRevision.set(change.revision, changeInfo);
}
changesBySsid.set(change.sourcestamp.ssid, changeInfo);
}
for (const build of buildsQuery.array) {
if (build.buildrequestid === null) {
continue;
}
const buildrequest = buildrequestsQuery.getByIdOrNull(build.buildrequestid.toString());
if (buildrequest === null) {
continue;
}
const buildset = buildsetsQuery.getByIdOrNull(buildrequest.buildsetid.toString());
if (buildset === null) {
continue;
}
const change = selectChangeForBuild(build, buildset, changesBySsid, changesByRevision,
changesByFakeId);
pushIntoMapOfArrays(change.buildsByBuilderId, build.builderid, build);
}
const changesToShow = [...changesBySsid.values(), ...changesByFakeId.values()]
.filter(ch => ch.buildsByBuilderId.size > 0)
.sort((a, b) => b.change.when_timestamp - a.change.when_timestamp);
const hasExpandedChanges = [...changeIsExpandedByChangeId.values()].includes(true);
// The magic value is selected so that the column holds 78 character lines without wrapping
const rowHeaderWidth = hasExpandedChanges ? 400 : 200;
// 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
const isBigTable = () => {
const padding = rowHeaderWidth;
if (((windowSize.width - padding) / buildersToShow.length) < 40) {
return true;
}
return false;
}
const getColHeaderHeight = () => {
let maxBuilderName = 0;
for (const builder of buildersToShow) {
maxBuilderName = Math.max(builder.name.length, maxBuilderName);
}
return Math.max(100, maxBuilderName * 3);
}
const openAllChanges = () => {
for (const change of changesToShow) {
changeIsExpandedByChangeId.set(change.change.changeid, true);
}
};
const closeAllChanges = () => {
for (const changeid of changeIsExpandedByChangeId.keys()) {
changeIsExpandedByChangeId.set(changeid, false);
}
};
// FIXME: fa-spin
if (!queriesResolved) {
return (
<div className="bb-console-container">
<LoadingIndicator/>
</div>
);
}
if (changesQuery.array.length === 0) {
return (
<div className="bb-console-container">
<p>
No changes. Console View needs a changesource to be setup,
and <Link to="/changes">changes</Link> to be in the system.
</p>
</div>
);
}
const builderColumns = buildersToShow.map(builder => {
return (
<th key={builder.name} className="column">
<span style={{marginTop: getColHeaderHeight()}} className="bb-console-table-builder">
<Link to={`/builders/${builder.builderid}`}>{builder.name}</Link>
</span>
</th>
)
});
const tagLineRows = tagLineConfigs.map((tagLineConfig, i) => {
const columns = tagLineConfig.map((item, i) => {
return (
<td key={i} colSpan={item.colSpan}>
<span style={{width: item.colSpan * 50}}>{item.tag}</span>
</td>
);
});
return (
<tr className="bb-console-tag-row" key={`tag-${i}`}>
<td className="row-header"></td>
{columns}
</tr>
)
});
const changeRows = changesToShow.map(changeInfo => {
const change = changeInfo.change;
const builderColumns = buildersToShow.map(builder => {
const builds = changeInfo.buildsByBuilderId.get(builder.builderid) ?? [];
const buildLinks = builds.map(build => (
<BuildLinkWithSummaryTooltip key={build.buildid} build={build}/>
));
return (
<td key={builder.name} title={builder.name} className="column">
{buildLinks}
</td>
);
});
// Note that changeid may not be unique because fake changes always have changeid of 0
return (
<tr key={`change-${change.changeid}-${change.codebase}-${change.revision ?? ''}`}>
<td>
<ChangeDetails change={change} compact={true}
showDetails={changeIsExpandedByChangeId.get(change.changeid) ?? false}
setShowDetails={(show: boolean) => changeIsExpandedByChangeId.set(change.changeid, show)}/>
</td>
{builderColumns}
</tr>
);
});
return (
<div className="container bb-console">
<Table striped bordered className={(isBigTable() ? 'table-fixedwidth' : '')}>
<thead>
<tr className="bb-console-table-first-row first-row">
<th className="row-header" style={{width: rowHeaderWidth}}>
<OverlayTrigger trigger="click" placement="top" overlay={
<Tooltip id="bb-console-view-open-all-changes">
Open information for all changes
</Tooltip>
} rootClose={true}>
<FaPlusCircle onClick={e => openAllChanges()} className="bb-console-changes-expand-icon clickable"/>
</OverlayTrigger>
<OverlayTrigger trigger="click" placement="top" overlay={
<Tooltip id="bb-console-view-close-all-changes">
Close information for all changes
</Tooltip>
} rootClose={true}>
<FaMinusCircle onClick={e => closeAllChanges()} className="bb-console-changes-expand-icon clickable"/>
</OverlayTrigger>
</th>
{builderColumns}
</tr>
</thead>
<tbody>
{tagLineRows}
{changeRows}
</tbody>
</Table>
</div>
);
});
buildbotSetupPlugin(reg => {
reg.registerMenuGroup({
name: 'console',
caption: 'Console View',
icon: <FaExclamationCircle/>,
order: 5,
route: '/console',
parentName: null,
});
reg.registerRoute({
route: "/console",
group: "console",
element: () => <ConsoleView/>,
});
reg.registerSettingGroup({
name: "Console",
caption: "Console related settings",
items: [
{
type: 'integer',
name: 'changeLimit',
caption: 'Maximum number of changes to fetch',
defaultValue: 30
}, {
type: 'integer',
name: 'buildLimit',
caption: 'Maximum number of builds to fetch',
defaultValue: 200
}
]
});
});
export default ConsoleView;

View File

@ -1,2 +0,0 @@
# app module is necessary for plugins, but only in the test environment
angular.module("app", []).constant("config", {"url": "foourl"})

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "es2020",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

View File

@ -0,0 +1,64 @@
import {resolve} from "path";
import {defineConfig} from "vite";
import react from "@vitejs/plugin-react";
const outDir = 'buildbot_console_view/static';
export default defineConfig({
plugins: [
react({
babel: {
parserOpts: {
plugins: ['decorators-legacy', 'classProperties']
}
}
}),
],
define: {
'process.env.NODE_ENV': '"production"',
},
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: "buildbotConsoleViewPlugin",
formats: ["umd"],
fileName: "scripts",
},
rollupOptions: {
external: [
'axios',
'buildbot-data-js',
'buildbot-plugin-support',
'buildbot-ui',
'mobx',
'mobx-react',
'moment',
'react',
'react-dom',
'react-router-dom'
],
output: {
assetFileNames: 'styles.css',
entryFileNames: 'scripts.js',
globals: {
axios: "axios",
"buildbot-data-js": "BuildbotDataJs",
"buildbot-plugin-support": "BuildbotPluginSupport",
"buildbot-ui": "BuildbotUi",
mobx: "mobx",
"mobx-react": "mobxReact",
react: "React",
moment: "moment",
"react-dom": "ReactDOM",
"react-router-dom": "ReactRouterDOM",
},
},
},
target: ['es2020'],
outDir: outDir,
emptyOutDir: true,
},
test: {
environment: "jsdom"
},
});

View File

@ -1,26 +0,0 @@
'use strict';
const common = require('buildbot-build-common');
const env = require('yargs').argv.env;
const pkg = require('./package.json');
var event = process.env.npm_lifecycle_event;
var isTest = event === 'test' || event === 'test-watch';
var isProd = env === 'prod';
module.exports = function() {
return common.createTemplateWebpackConfig({
entry: {
scripts: './src/module/main.module.js',
styles: './src/styles/styles.less',
},
libraryName: pkg.name,
pluginName: pkg.plugin_name,
dirname: __dirname,
isTest: isTest,
isProd: isProd,
outputPath: __dirname + '/yocto_console_view/static',
extractStyles: true,
});
}();

1755
yocto_console_view/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
1.1.3.dev66

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,79 +0,0 @@
.console .table-fixedwidth {
width: initial;
}
.console .load-indicator {
width: 100%;
height: 100%;
z-index: 900;
background-color: #ffffff;
display: table;
}
.console .load-indicator .spinner {
display: table-cell;
vertical-align: middle;
text-align: center;
}
.console .load-indicator .spinner p {
font-weight: 300;
margin-top: 10px;
}
.console .column {
min-width: 40px;
max-width: 40px;
width: 40px;
}
.console table {
border: none;
}
.console .tag_row td {
margin: 0px;
padding: 0px;
}
.console .tag_row span {
position: relative;
float: left;
font-size: 10px;
overflow: hidden;
text-decoration: none;
white-space: nowrap;
}
.console tr.first-row {
background-color: #fff !important;
}
.console tr.first-row th {
border: none;
background-color: #fff !important;
}
.console tr.first-row .builder {
position: relative;
float: left;
font-size: 12px;
text-align: center;
transform: rotate(-45deg);
transform-origin: 0% 100%;
text-decoration: none;
white-space: nowrap;
}
.console tr.first-row .column {
width: 20px;
min-width: 20px;
max-width: 20px;
}
.yoctochangedetails > .no-select > * {
margin-left: 0.3em;
margin-right: 0.3em;
}
.select-editable {
position: absolute;
top: 0;
border: none;
margin: 2px;
width: 90%;
height: 29px;
}
.select-editable input:focus {
outline: none;
}
/*# sourceMappingURL=styles.css.map*/

View File

@ -1 +0,0 @@
{"version":3,"sources":["webpack://yocto-console-view/./src/styles/styles.less"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"styles.css","sourcesContent":[".console .table-fixedwidth {\n width: initial;\n}\n.console .load-indicator {\n width: 100%;\n height: 100%;\n z-index: 900;\n background-color: #ffffff;\n display: table;\n}\n.console .load-indicator .spinner {\n display: table-cell;\n vertical-align: middle;\n text-align: center;\n}\n.console .load-indicator .spinner p {\n font-weight: 300;\n margin-top: 10px;\n}\n.console .column {\n min-width: 40px;\n max-width: 40px;\n width: 40px;\n}\n.console table {\n border: none;\n}\n.console .tag_row td {\n margin: 0px;\n padding: 0px;\n}\n.console .tag_row span {\n position: relative;\n float: left;\n font-size: 10px;\n overflow: hidden;\n text-decoration: none;\n white-space: nowrap;\n}\n.console tr.first-row {\n background-color: #fff !important;\n}\n.console tr.first-row th {\n border: none;\n background-color: #fff !important;\n}\n.console tr.first-row .builder {\n position: relative;\n float: left;\n font-size: 12px;\n text-align: center;\n transform: rotate(-45deg);\n transform-origin: 0% 100%;\n text-decoration: none;\n white-space: nowrap;\n}\n.console tr.first-row .column {\n width: 20px;\n min-width: 20px;\n max-width: 20px;\n}\n.yoctochangedetails > .no-select > * {\n margin-left: 0.3em;\n margin-right: 0.3em;\n}\n.select-editable {\n position: absolute;\n top: 0;\n border: none;\n margin: 2px;\n width: 90%;\n height: 29px;\n}\n.select-editable input:focus {\n outline: none;\n}\n"],"sourceRoot":""}