mirror of
git://git.yoctoproject.org/yocto-autobuilder2.git
synced 2025-07-19 20:59:02 +02:00
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:
parent
421dd04863
commit
c2793d8bfd
|
@ -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.
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)'
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
18
yocto_console_view/src/index.ts
Normal file
18
yocto_console_view/src/index.ts
Normal 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
|
@ -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 }}
|
|
|
@ -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');
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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])
|
|
|
@ -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")
|
|
|
@ -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]);
|
|
|
@ -1,9 +0,0 @@
|
||||||
.modal-big {
|
|
||||||
.modal-dialog {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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')
|
|
|
@ -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])
|
|
|
@ -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 }}
|
|
||||||
a(ng-if="change.errorlink", ng-href="{{change.errorlink}}")
|
|
||||||
| {{ "Errors" }}
|
|
||||||
a(ng-if="change.publishurl", ng-href="{{change.publishurl}}")
|
|
||||||
| {{ "Output" }}
|
|
||||||
span(ng-if="!change.revlink", uib-tooltip="{{change.comments}}")
|
|
||||||
| {{ change.caption }}
|
|
||||||
span(ng-if="!compact" uib-tooltip="{{change.when_timestamp | dateformat:'LLL'}}")
|
|
||||||
| ({{ change.when_timestamp | timeago }})
|
|
||||||
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
|
|
|
@ -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;
|
|
||||||
}
|
|
38
yocto_console_view/src/views/ConsoleView/ConsoleView.scss
Normal file
38
yocto_console_view/src/views/ConsoleView/ConsoleView.scss
Normal 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;
|
||||||
|
}
|
97
yocto_console_view/src/views/ConsoleView/ConsoleView.test.ts
Normal file
97
yocto_console_view/src/views/ConsoleView/ConsoleView.test.ts
Normal 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}],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
541
yocto_console_view/src/views/ConsoleView/ConsoleView.tsx
Normal file
541
yocto_console_view/src/views/ConsoleView/ConsoleView.tsx
Normal 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;
|
|
@ -1,2 +0,0 @@
|
||||||
# app module is necessary for plugins, but only in the test environment
|
|
||||||
angular.module("app", []).constant("config", {"url": "foourl"})
|
|
27
yocto_console_view/tsconfig.json
Normal file
27
yocto_console_view/tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
64
yocto_console_view/vite.config.ts
Normal file
64
yocto_console_view/vite.config.ts
Normal 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"
|
||||||
|
},
|
||||||
|
});
|
|
@ -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
1755
yocto_console_view/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -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
|
@ -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*/
|
|
|
@ -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":""}
|
|
Loading…
Reference in New Issue
Block a user