mirror of
git://git.yoctoproject.org/yocto-autobuilder2.git
synced 2025-07-19 12:49:03 +02:00
yocto_console_view: create our own console
Modify existing console view plugin to recreate our own console view. Signed-off-by: Mathieu Dubois-Briand <mathieu.dubois-briand@bootlin.com>
This commit is contained in:
parent
c2793d8bfd
commit
c8f1590596
|
@ -1,19 +0,0 @@
|
|||
# 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
|
||||
|
||||
from buildbot.www.plugin import Application
|
||||
|
||||
# create the interface for the setuptools entry point
|
||||
ep = Application(__package__, "Buildbot Console View plugin")
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"name": "buildbot-console-view",
|
||||
"name": "yocto-console-view",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"module": "buildbot_console_view/static/scripts.js",
|
||||
"style": "buildbot_console_view/static/styles.css",
|
||||
"module": "yocto_console_view/static/scripts.js",
|
||||
"style": "yocto_console_view/static/styles.css",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
|
|
|
@ -28,12 +28,12 @@ except ImportError:
|
|||
sys.exit(1)
|
||||
|
||||
setup_www_plugin(
|
||||
name='buildbot-console-view',
|
||||
description='Buildbot Console View plugin',
|
||||
author='Pierre Tardy',
|
||||
author_email='tardyp@gmail.com',
|
||||
url='http://buildbot.net/',
|
||||
packages=['buildbot_console_view'],
|
||||
name='yocto-console-view',
|
||||
description='Yocto Project Console View plugin.',
|
||||
author=u'Richard Purdie',
|
||||
author_email=u'richard.purdie@linuxfoundation.org',
|
||||
url='http://autobuilder.yoctoproject.org/',
|
||||
packages=['yocto_console_view'],
|
||||
package_data={
|
||||
'': [
|
||||
'VERSION',
|
||||
|
@ -43,7 +43,7 @@ setup_www_plugin(
|
|||
},
|
||||
entry_points="""
|
||||
[buildbot.www]
|
||||
console_view = yocto_console_view:ep
|
||||
yocto_console_view = yocto_console_view:ep
|
||||
""",
|
||||
classifiers=['License :: OSI Approved :: GNU General Public License v2 (GPLv2)'],
|
||||
)
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
$headcol-width: 20px;
|
||||
|
||||
tr.bb-console-table-first-row {
|
||||
background-color: #fff !important;
|
||||
.column {
|
||||
width: $headcol-width;
|
||||
min-width: $headcol-width;
|
||||
max-width: $headcol-width;
|
||||
}
|
||||
th {
|
||||
border: none;
|
||||
}
|
||||
|
@ -12,8 +17,8 @@ tr.bb-console-table-first-row {
|
|||
font-size: 1em;
|
||||
width: 1.5em;
|
||||
text-align: center;
|
||||
transform: rotate(-25deg) ;
|
||||
transform-origin: 0% 100%;
|
||||
transform: rotate(-45deg) translate(.5em, 1em);
|
||||
transform-origin: bottom left;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -36,3 +41,9 @@ tr.bb-console-table-first-row {
|
|||
.bb-console-changes-expand-icon {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.bb-console {
|
||||
td.column {
|
||||
padding: .2em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
pushIntoMapOfArrays,
|
||||
useWindowSize
|
||||
} from "buildbot-ui";
|
||||
import {YoctoChangeDetails} from './YoctoChangeDetails.tsx';
|
||||
|
||||
type ChangeInfo = {
|
||||
change: Change;
|
||||
|
@ -53,152 +54,71 @@ export type TagTreeItem = {
|
|||
childItems: TagTreeItem[];
|
||||
}
|
||||
|
||||
export type TagItemConfig = {
|
||||
tag: string,
|
||||
colSpan: number
|
||||
export type BuilderGroup = {
|
||||
name: string;
|
||||
tag: string;
|
||||
builders: Builder[];
|
||||
colspan: int;
|
||||
};
|
||||
|
||||
export type TagLineConfig = TagItemConfig[];
|
||||
|
||||
export function buildTagTree(builders: Builder[])
|
||||
// Sorts and groups builders together by their tags.
|
||||
export function getBuildersGroups(builders: Builder[]) : [Builder[], BuilderGroup[]]
|
||||
{
|
||||
const buildersByTags = new Map<string, Builder[]>();
|
||||
for (const builder of builders) {
|
||||
if (builder.tags === null) {
|
||||
if (builder.name === "indexing") {
|
||||
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 ((builder.tags !== null) && (builder.tags.length != 0)) {
|
||||
for (const tag of builder.tags) {
|
||||
pushIntoMapOfArrays(buildersByTags, tag, builder);
|
||||
}
|
||||
}
|
||||
if (!excluded) {
|
||||
for (const builder of tagInfo.builders) {
|
||||
builderIdToTag.set(builder.builderid, tagInfo.tag);
|
||||
}
|
||||
tagItems.push({tag: tagInfo.tag, builders: tagInfo.builders, childItems: []});
|
||||
} else {
|
||||
pushIntoMapOfArrays(buildersByTags, '', builder);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
const buildersGroups: BuilderGroup[] = [];
|
||||
for (const [tag, builders] of buildersByTags) {
|
||||
builders.sort((a, b) => a.name.localeCompare(b.name));
|
||||
if (tag !== '') {
|
||||
buildersGroups.push({
|
||||
name: builders[0].name,
|
||||
tag: tag,
|
||||
builders: builders,
|
||||
colspan: builders.length
|
||||
});
|
||||
}
|
||||
}
|
||||
if (buildersByTags.has('')) {
|
||||
const builders = buildersByTags.get('');
|
||||
builders.sort((a, b) => a.name.localeCompare(b.name));
|
||||
for (const builder of builders) {
|
||||
buildersGroups.push({
|
||||
name: builder.name,
|
||||
tag: '',
|
||||
builders: [builder],
|
||||
colspan: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (remainingBuilders.length) {
|
||||
tagItems.push({tag: "", builders: remainingBuilders, childItems: []});
|
||||
}
|
||||
buildersGroups.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// 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);
|
||||
const sortedBuilders: Builder[] = [];
|
||||
for (const buildersGroup of buildersGroups) {
|
||||
for (const builder of buildersGroup.builders) {
|
||||
sortedBuilders.push(builder);
|
||||
}
|
||||
}
|
||||
return tagItems;
|
||||
|
||||
return [sortedBuilders, buildersGroups];
|
||||
}
|
||||
|
||||
// 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,
|
||||
function resolveFakeChange(revision: string, whenTimestamp: number, comment: string,
|
||||
changesByFakeId: Map<string, ChangeInfo>): ChangeInfo
|
||||
{
|
||||
const fakeId = `${codebase}-${revision}`;
|
||||
const fakeId = `${revision}-${comment}`;
|
||||
const existingChange = changesByFakeId.get(fakeId);
|
||||
if (existingChange !== undefined) {
|
||||
return existingChange;
|
||||
|
@ -206,11 +126,10 @@ function resolveFakeChange(codebase: string, revision: string, whenTimestamp: nu
|
|||
|
||||
const newChange = {
|
||||
change: new Change(undefined as unknown as IDataAccessor, "a/1", {
|
||||
changeid: 0,
|
||||
changeid: revision,
|
||||
author: "",
|
||||
branch: "",
|
||||
codebase: codebase,
|
||||
comments: `Unknown revision ${revision}`,
|
||||
comments: comment,
|
||||
files: [],
|
||||
parent_changeids: [],
|
||||
project: "",
|
||||
|
@ -230,47 +149,63 @@ function resolveFakeChange(codebase: string, revision: string, whenTimestamp: nu
|
|||
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;
|
||||
}
|
||||
changesByFakeId: Map<string, ChangeInfo>,
|
||||
revMapping: Map<int, string>,
|
||||
branchMapping: Map<int, string>)
|
||||
{
|
||||
if ((build.properties !== null && ('yp_build_revision' in build.properties)) || (build.buildid in revMapping)) {
|
||||
let revision;
|
||||
let change = undefined;
|
||||
if (build.properties !== null && ('yp_build_revision' in build.properties)) {
|
||||
revision = build.properties['yp_build_revision'][0];
|
||||
} else {
|
||||
revision = revMapping[build.buildid];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
change = changesByRevision.get(revision);
|
||||
if (change === undefined) {
|
||||
change = changesBySsid.get(revision);
|
||||
}
|
||||
if (change === undefined) {
|
||||
change = resolveFakeChange(revision, build.started_at, revision, changesByFakeId);
|
||||
}
|
||||
|
||||
return resolveFakeChange("", revision, build.started_at, changesByFakeId);
|
||||
}
|
||||
change.change.caption = "Commit";
|
||||
if (build.properties !== null && ('yp_build_revision' in build.properties)) {
|
||||
change.change.caption = build.properties['yp_build_branch'][0];
|
||||
}
|
||||
if (build.buildid in branchMapping) {
|
||||
change.change.caption = branchMapping[build.buildid];
|
||||
}
|
||||
change.change.revlink = "http://git.yoctoproject.org/cgit.cgi/poky/commit/?id=" + revision;
|
||||
change.change.errorlink = "http://errors.yoctoproject.org/Errors/Latest/?filter=" + revision + "&type=commit&limit=150";
|
||||
|
||||
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;
|
||||
let bid = build.buildid;
|
||||
if ((buildset !== null) && (buildset.parent_buildid != null)) {
|
||||
bid = buildset.parent_buildid;
|
||||
}
|
||||
if (build.properties !== null && ('reason' in build.properties)) {
|
||||
change.change.reason = build.properties['reason'][0];
|
||||
}
|
||||
if (build.properties !== null && ('publish_destination' in build.properties)) {
|
||||
change.change.publishurl = build.properties['publish_destination'][0].replace("/srv/autobuilder/autobuilder.yoctoproject.org/", "https://autobuilder.yocto.io/");
|
||||
change.change.publishurl = change.change.publishurl.replace("/srv/autobuilder/autobuilder.yocto.io/", "https://autobuilder.yocto.io/");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return change;
|
||||
}
|
||||
|
||||
const revision = `unknown revision ${build.builderid}-${build.buildid}`;
|
||||
return resolveFakeChange("unknown codebase", revision, build.started_at, changesByFakeId);
|
||||
const revision = `Unresolved Revision`
|
||||
const change = changesBySsid.get(revision);
|
||||
if (change !== undefined) {
|
||||
return change
|
||||
}
|
||||
|
||||
const fakeChange = resolveFakeChange(revision, build.started_at, revision, changesByFakeId);
|
||||
fakeChange.change.caption = revision;
|
||||
return fakeChange
|
||||
}
|
||||
|
||||
export const ConsoleView = observer(() => {
|
||||
|
@ -300,7 +235,7 @@ export const ConsoleView = observer(() => {
|
|||
const buildsQuery = useDataApiQuery(() => Build.getAll(accessor, {query: {
|
||||
limit: buildFetchLimit,
|
||||
order: '-started_at',
|
||||
property: ["got_revision"],
|
||||
property: ["yp_build_revision", "yp_build_branch", "reason", "publish_destination"],
|
||||
}}));
|
||||
|
||||
const windowSize = useWindowSize()
|
||||
|
@ -313,13 +248,65 @@ export const ConsoleView = observer(() => {
|
|||
buildrequestsQuery.resolved &&
|
||||
buildsQuery.resolved;
|
||||
|
||||
// FIXME: fa-spin
|
||||
if (!queriesResolved) {
|
||||
return (
|
||||
<div className="bb-console-container">
|
||||
<LoadingIndicator/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const builderIdsWithBuilds = new Set<number>();
|
||||
for (const build of buildsQuery.array) {
|
||||
builderIdsWithBuilds.add(build.builderid);
|
||||
}
|
||||
|
||||
const revMapping = new Map<int, string>();
|
||||
const branchMapping = new Map<int, string>();
|
||||
for (const build of buildsQuery.array) {
|
||||
let change = false;
|
||||
let {
|
||||
buildid
|
||||
} = build;
|
||||
if (build.properties !== null && ('yp_build_revision' in build.properties)) {
|
||||
revMapping[build.buildid] = build.properties['yp_build_revision'][0];
|
||||
change = true;
|
||||
}
|
||||
if (build.properties !== null && ('yp_build_branch' in build.properties)) {
|
||||
branchMapping[build.buildid] = build.properties.yp_build_branch[0];
|
||||
change = true;
|
||||
}
|
||||
if ((!revMapping[buildid] || !branchMapping[buildid]) && !build.complete_at) {
|
||||
build.getProperties().onChange = properties => {
|
||||
change = false;
|
||||
buildid = properties.endpoint.split('/')[1];
|
||||
if (!revMapping[buildid]) {
|
||||
const rev = getBuildProperty(properties[0], 'yp_build_revision');
|
||||
if (rev != null) {
|
||||
revMapping[buildid] = rev;
|
||||
change = true;
|
||||
}
|
||||
}
|
||||
if (!branchMapping[buildid]) {
|
||||
const branch = getBuildProperty(properties[0], 'yp_build_branch');
|
||||
if (branch != null) {
|
||||
branchMapping[buildid] = branch;
|
||||
change = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getBuildProperty(properties, property) {
|
||||
const hasProperty = properties && properties.hasOwnProperty(property);
|
||||
if (hasProperty) { return properties[property][0]; } else { return null; }
|
||||
}
|
||||
|
||||
const buildersWithBuilds = buildersQuery.array.filter(b => builderIdsWithBuilds.has(b.builderid));
|
||||
const [buildersToShow, tagLineConfigs] = sortBuildersByTags(buildersWithBuilds);
|
||||
const [buildersToShow, builderGroups] = getBuildersGroups(buildersWithBuilds);
|
||||
|
||||
const changesByRevision = new Map<string, ChangeInfo>();
|
||||
const changesBySsid = new Map<number, ChangeInfo>();
|
||||
|
@ -347,7 +334,7 @@ export const ConsoleView = observer(() => {
|
|||
}
|
||||
|
||||
const change = selectChangeForBuild(build, buildset, changesBySsid, changesByRevision,
|
||||
changesByFakeId);
|
||||
changesByFakeId, revMapping, branchMapping);
|
||||
|
||||
pushIntoMapOfArrays(change.buildsByBuilderId, build.builderid, build);
|
||||
}
|
||||
|
@ -393,21 +380,11 @@ export const ConsoleView = observer(() => {
|
|||
}
|
||||
};
|
||||
|
||||
// FIXME: fa-spin
|
||||
if (!queriesResolved) {
|
||||
return (
|
||||
<div className="bb-console-container">
|
||||
<LoadingIndicator/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (changesQuery.array.length === 0) {
|
||||
if (buildsQuery.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.
|
||||
No builds. Console View needs run builds to be setup.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
@ -423,23 +400,15 @@ export const ConsoleView = observer(() => {
|
|||
)
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
});
|
||||
|
||||
const tagLineColumns = builderGroups.map((builderGroup, i) => {
|
||||
return (
|
||||
<tr className="bb-console-tag-row" key={`tag-${i}`}>
|
||||
<td className="row-header"></td>
|
||||
{columns}
|
||||
</tr>
|
||||
)
|
||||
<td key={i} colSpan={builderGroup.colspan} style={{textAlign: 'center'}}>
|
||||
{builderGroup.tag}
|
||||
</td>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const changeRows = changesToShow.map(changeInfo => {
|
||||
const change = changeInfo.change;
|
||||
|
||||
|
@ -460,7 +429,7 @@ export const ConsoleView = observer(() => {
|
|||
return (
|
||||
<tr key={`change-${change.changeid}-${change.codebase}-${change.revision ?? ''}`}>
|
||||
<td>
|
||||
<ChangeDetails change={change} compact={true}
|
||||
<YoctoChangeDetails change={change} compact={true}
|
||||
showDetails={changeIsExpandedByChangeId.get(change.changeid) ?? false}
|
||||
setShowDetails={(show: boolean) => changeIsExpandedByChangeId.set(change.changeid, show)}/>
|
||||
</td>
|
||||
|
@ -474,28 +443,16 @@ export const ConsoleView = observer(() => {
|
|||
<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 className="row-header">
|
||||
</th>
|
||||
{builderColumns}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tagLineRows}
|
||||
<tr className="bb-console-tag-row" key="tag">
|
||||
<td className="row-header"></td>
|
||||
{tagLineColumns}
|
||||
</tr>
|
||||
{changeRows}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
@ -506,7 +463,7 @@ export const ConsoleView = observer(() => {
|
|||
buildbotSetupPlugin(reg => {
|
||||
reg.registerMenuGroup({
|
||||
name: 'console',
|
||||
caption: 'Console View',
|
||||
caption: 'Yocto Console View',
|
||||
icon: <FaExclamationCircle/>,
|
||||
order: 5,
|
||||
route: '/console',
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
.yoctochangedetails {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.yoctochangedetails-heading {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.yoctochangedetails-heading > * {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.changedetails-properties {
|
||||
padding: unset;
|
||||
margin: unset;
|
||||
border: unset;
|
||||
background-color: unset
|
||||
}
|
149
yocto_console_view/src/views/ConsoleView/YoctoChangeDetails.tsx
Normal file
149
yocto_console_view/src/views/ConsoleView/YoctoChangeDetails.tsx
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
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 './YoctoChangeDetails.scss';
|
||||
import {useState} from "react";
|
||||
import {observer} from "mobx-react";
|
||||
import {OverlayTrigger, Popover, Table} from "react-bootstrap";
|
||||
import {Change, parseChangeAuthorNameAndEmail} from "buildbot-data-js";
|
||||
import {dateFormat, durationFromNowFormat, useCurrentTime} from "buildbot-ui";
|
||||
import {ArrowExpander} from "buildbot-ui";
|
||||
|
||||
type ChangeDetailsProps = {
|
||||
change: Change;
|
||||
compact: boolean;
|
||||
showDetails: boolean;
|
||||
setShowDetails: ((show: boolean) => void) | null;
|
||||
}
|
||||
|
||||
export const YoctoChangeDetails = observer(({change, compact, showDetails, setShowDetails}: ChangeDetailsProps) => {
|
||||
const now = useCurrentTime();
|
||||
const [showProps, setShowProps] = useState(false);
|
||||
|
||||
const renderChangeDetails = () => (
|
||||
<div className="anim-yoctochangedetails">
|
||||
<Table striped size="sm">
|
||||
<tbody>
|
||||
{ change.reason !== null
|
||||
? <tr>
|
||||
<td>Reason</td>
|
||||
<td>{change.reason}</td>
|
||||
</tr>
|
||||
: <></>
|
||||
}
|
||||
{ change.author != null
|
||||
? <tr>
|
||||
<td>Author</td>
|
||||
<td>{change.author}</td>
|
||||
</tr>
|
||||
: <></>
|
||||
}
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>{dateFormat(change.when_timestamp)} ({durationFromNowFormat(change.when_timestamp, now)})</td>
|
||||
</tr>
|
||||
{ change.repository !== null
|
||||
? <tr>
|
||||
<td>Repository</td>
|
||||
<td>{change.repository}</td>
|
||||
</tr>
|
||||
: <></>
|
||||
}
|
||||
{ change.branch !== null
|
||||
? <tr>
|
||||
<td>Branch</td>
|
||||
<td>{change.branch}</td>
|
||||
</tr>
|
||||
: <></>
|
||||
}
|
||||
<tr>
|
||||
<td>Revision</td>
|
||||
<td>
|
||||
{
|
||||
change.revlink
|
||||
? <a href={change.revlink}>{change.revision}</a>
|
||||
: <></>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
<h5>Comment</h5>
|
||||
<pre>{change.comments}</pre>
|
||||
<h5>Changed files</h5>
|
||||
{change.files.length === 0
|
||||
? <p>No files</p>
|
||||
: <ul>{change.files.map(file => (<li key={file}>{file}</li>))}</ul>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
const [changeAuthorName, changeEmail] = parseChangeAuthorNameAndEmail(change.author);
|
||||
|
||||
const popoverWithText = (id: string, text: string) => {
|
||||
return (
|
||||
<Popover id={"bb-popover-change-details-" + id}>
|
||||
<Popover.Content>
|
||||
{text}
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const onHeadingClicked = () => {
|
||||
if (setShowDetails === null)
|
||||
return;
|
||||
setShowDetails(!showDetails);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="yoctochangedetails">
|
||||
<div className="yoctochangedetails-heading" onClick={onHeadingClicked}>
|
||||
<OverlayTrigger placement="top"
|
||||
overlay={popoverWithText("comments-" + change.id, change.caption)}>
|
||||
<React.Fragment>
|
||||
{
|
||||
change.revlink
|
||||
? <a href={change.revlink}>{change.caption}</a>
|
||||
: <span>{change.caption}</span>
|
||||
}
|
||||
{
|
||||
change.errorlink
|
||||
? <a href={change.errorlink}>Error</a>
|
||||
: <></>
|
||||
}
|
||||
{
|
||||
change.publishurl
|
||||
? <a href={change.publishurl}>Output</a>
|
||||
: <></>
|
||||
}
|
||||
</React.Fragment>
|
||||
</OverlayTrigger>
|
||||
{ !compact
|
||||
? <OverlayTrigger placement="top"
|
||||
overlay={popoverWithText("date-" + change.id,
|
||||
dateFormat(change.when_timestamp))}>
|
||||
<span>({durationFromNowFormat(change.when_timestamp, now)})</span>
|
||||
</OverlayTrigger>
|
||||
: <></>
|
||||
}
|
||||
{setShowDetails !== null ? <ArrowExpander isExpanded={showDetails}/> : <></>}
|
||||
</div>
|
||||
{showDetails ? renderChangeDetails() : <></>}
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -2,7 +2,7 @@ import {resolve} from "path";
|
|||
import {defineConfig} from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
const outDir = 'buildbot_console_view/static';
|
||||
const outDir = 'yocto_console_view/static';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
|
|
Loading…
Reference in New Issue
Block a user