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:
Mathieu Dubois-Briand 2024-12-06 14:17:57 +01:00
parent c2793d8bfd
commit c8f1590596
8 changed files with 355 additions and 239 deletions

View File

@ -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")

View File

@ -1,9 +1,9 @@
{ {
"name": "buildbot-console-view", "name": "yocto-console-view",
"private": true, "private": true,
"type": "module", "type": "module",
"module": "buildbot_console_view/static/scripts.js", "module": "yocto_console_view/static/scripts.js",
"style": "buildbot_console_view/static/styles.css", "style": "yocto_console_view/static/styles.css",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"build": "vite build", "build": "vite build",

View File

@ -28,12 +28,12 @@ except ImportError:
sys.exit(1) sys.exit(1)
setup_www_plugin( setup_www_plugin(
name='buildbot-console-view', name='yocto-console-view',
description='Buildbot Console View plugin', description='Yocto Project Console View plugin.',
author='Pierre Tardy', author=u'Richard Purdie',
author_email='tardyp@gmail.com', author_email=u'richard.purdie@linuxfoundation.org',
url='http://buildbot.net/', url='http://autobuilder.yoctoproject.org/',
packages=['buildbot_console_view'], packages=['yocto_console_view'],
package_data={ package_data={
'': [ '': [
'VERSION', 'VERSION',
@ -43,7 +43,7 @@ setup_www_plugin(
}, },
entry_points=""" entry_points="""
[buildbot.www] [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)'], classifiers=['License :: OSI Approved :: GNU General Public License v2 (GPLv2)'],
) )

View File

@ -1,6 +1,11 @@
$headcol-width: 20px;
tr.bb-console-table-first-row { tr.bb-console-table-first-row {
background-color: #fff !important; .column {
width: $headcol-width;
min-width: $headcol-width;
max-width: $headcol-width;
}
th { th {
border: none; border: none;
} }
@ -12,8 +17,8 @@ tr.bb-console-table-first-row {
font-size: 1em; font-size: 1em;
width: 1.5em; width: 1.5em;
text-align: center; text-align: center;
transform: rotate(-25deg) ; transform: rotate(-45deg) translate(.5em, 1em);
transform-origin: 0% 100%; transform-origin: bottom left;
text-decoration: none; text-decoration: none;
white-space: nowrap; white-space: nowrap;
} }
@ -36,3 +41,9 @@ tr.bb-console-table-first-row {
.bb-console-changes-expand-icon { .bb-console-changes-expand-icon {
float: left; float: left;
} }
.bb-console {
td.column {
padding: .2em;
}
}

View File

@ -41,6 +41,7 @@ import {
pushIntoMapOfArrays, pushIntoMapOfArrays,
useWindowSize useWindowSize
} from "buildbot-ui"; } from "buildbot-ui";
import {YoctoChangeDetails} from './YoctoChangeDetails.tsx';
type ChangeInfo = { type ChangeInfo = {
change: Change; change: Change;
@ -53,152 +54,71 @@ export type TagTreeItem = {
childItems: TagTreeItem[]; childItems: TagTreeItem[];
} }
export type TagItemConfig = { export type BuilderGroup = {
tag: string, name: string;
colSpan: number tag: string;
builders: Builder[];
colspan: int;
}; };
export type TagLineConfig = TagItemConfig[]; // Sorts and groups builders together by their tags.
export function getBuildersGroups(builders: Builder[]) : [Builder[], BuilderGroup[]]
export function buildTagTree(builders: Builder[])
{ {
const buildersByTags = new Map<string, Builder[]>(); const buildersByTags = new Map<string, Builder[]>();
for (const builder of builders) { for (const builder of builders) {
if (builder.tags === null) { if (builder.name === "indexing") {
continue; continue;
} }
for (const tag of builder.tags) { if ((builder.tags !== null) && (builder.tags.length != 0)) {
pushIntoMapOfArrays(buildersByTags, tag, builder); 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;
} }
} } else {
if (!excluded) { pushIntoMapOfArrays(buildersByTags, '', builder);
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 buildersGroups: BuilderGroup[] = [];
const remainingBuilders = []; for (const [tag, builders] of buildersByTags) {
for (const builder of builders) { builders.sort((a, b) => a.name.localeCompare(b.name));
if (!builderIdToTag.has(builder.builderid)) { if (tag !== '') {
remainingBuilders.push(builder); 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) { buildersGroups.sort((a, b) => a.name.localeCompare(b.name));
tagItems.push({tag: "", builders: remainingBuilders, childItems: []});
}
// if there is more than one tag in this line, we need to recurse const sortedBuilders: Builder[] = [];
if (tagItems.length > 1) { for (const buildersGroup of buildersGroups) {
for (const tagItem of tagItems) { for (const builder of buildersGroup.builders) {
tagItem.childItems = buildTagTree(tagItem.builders); sortedBuilders.push(builder);
} }
} }
return tagItems;
return [sortedBuilders, buildersGroups];
} }
// Sorts and groups builders together by their tags. function resolveFakeChange(revision: string, whenTimestamp: number, comment: string,
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 changesByFakeId: Map<string, ChangeInfo>): ChangeInfo
{ {
const fakeId = `${codebase}-${revision}`; const fakeId = `${revision}-${comment}`;
const existingChange = changesByFakeId.get(fakeId); const existingChange = changesByFakeId.get(fakeId);
if (existingChange !== undefined) { if (existingChange !== undefined) {
return existingChange; return existingChange;
@ -206,11 +126,10 @@ function resolveFakeChange(codebase: string, revision: string, whenTimestamp: nu
const newChange = { const newChange = {
change: new Change(undefined as unknown as IDataAccessor, "a/1", { change: new Change(undefined as unknown as IDataAccessor, "a/1", {
changeid: 0, changeid: revision,
author: "", author: "",
branch: "", branch: "",
codebase: codebase, comments: comment,
comments: `Unknown revision ${revision}`,
files: [], files: [],
parent_changeids: [], parent_changeids: [],
project: "", project: "",
@ -230,47 +149,63 @@ function resolveFakeChange(codebase: string, revision: string, whenTimestamp: nu
function selectChangeForBuild(build: Build, buildset: Buildset, function selectChangeForBuild(build: Build, buildset: Buildset,
changesBySsid: Map<number, ChangeInfo>, changesBySsid: Map<number, ChangeInfo>,
changesByRevision: Map<string, ChangeInfo>, changesByRevision: Map<string, ChangeInfo>,
changesByFakeId: Map<string, ChangeInfo>) { changesByFakeId: Map<string, ChangeInfo>,
if (buildset.sourcestamps !== null) { revMapping: Map<int, string>,
for (const sourcestamp of buildset.sourcestamps) { branchMapping: Map<int, string>)
const change = changesBySsid.get(sourcestamp.ssid); {
if (change !== undefined) { if ((build.properties !== null && ('yp_build_revision' in build.properties)) || (build.buildid in revMapping)) {
return change; 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 // got_revision can be per codebase or just the revision string
if (typeof(revision) === "string") { if (typeof(revision) === "string") {
const change = changesByRevision.get(revision); change = changesByRevision.get(revision);
if (change !== undefined) { if (change === undefined) {
return change; 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}; let bid = build.buildid;
for (const codebase in revisionMap) { if ((buildset !== null) && (buildset.parent_buildid != null)) {
const codebaseRevision = revisionMap[codebase]; bid = buildset.parent_buildid;
const change = changesByRevision.get(codebaseRevision); }
if (change !== undefined) { if (build.properties !== null && ('reason' in build.properties)) {
return change; 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); return change;
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}`; const revision = `Unresolved Revision`
return resolveFakeChange("unknown codebase", revision, build.started_at, changesByFakeId); 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(() => { export const ConsoleView = observer(() => {
@ -300,7 +235,7 @@ export const ConsoleView = observer(() => {
const buildsQuery = useDataApiQuery(() => Build.getAll(accessor, {query: { const buildsQuery = useDataApiQuery(() => Build.getAll(accessor, {query: {
limit: buildFetchLimit, limit: buildFetchLimit,
order: '-started_at', order: '-started_at',
property: ["got_revision"], property: ["yp_build_revision", "yp_build_branch", "reason", "publish_destination"],
}})); }}));
const windowSize = useWindowSize() const windowSize = useWindowSize()
@ -313,13 +248,65 @@ export const ConsoleView = observer(() => {
buildrequestsQuery.resolved && buildrequestsQuery.resolved &&
buildsQuery.resolved; buildsQuery.resolved;
// FIXME: fa-spin
if (!queriesResolved) {
return (
<div className="bb-console-container">
<LoadingIndicator/>
</div>
);
}
const builderIdsWithBuilds = new Set<number>(); const builderIdsWithBuilds = new Set<number>();
for (const build of buildsQuery.array) { for (const build of buildsQuery.array) {
builderIdsWithBuilds.add(build.builderid); 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 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 changesByRevision = new Map<string, ChangeInfo>();
const changesBySsid = new Map<number, ChangeInfo>(); const changesBySsid = new Map<number, ChangeInfo>();
@ -347,7 +334,7 @@ export const ConsoleView = observer(() => {
} }
const change = selectChangeForBuild(build, buildset, changesBySsid, changesByRevision, const change = selectChangeForBuild(build, buildset, changesBySsid, changesByRevision,
changesByFakeId); changesByFakeId, revMapping, branchMapping);
pushIntoMapOfArrays(change.buildsByBuilderId, build.builderid, build); pushIntoMapOfArrays(change.buildsByBuilderId, build.builderid, build);
} }
@ -393,21 +380,11 @@ export const ConsoleView = observer(() => {
} }
}; };
// FIXME: fa-spin if (buildsQuery.array.length === 0) {
if (!queriesResolved) {
return (
<div className="bb-console-container">
<LoadingIndicator/>
</div>
);
}
if (changesQuery.array.length === 0) {
return ( return (
<div className="bb-console-container"> <div className="bb-console-container">
<p> <p>
No changes. Console View needs a changesource to be setup, No builds. Console View needs run builds to be setup.
and <Link to="/changes">changes</Link> to be in the system.
</p> </p>
</div> </div>
); );
@ -423,23 +400,15 @@ export const ConsoleView = observer(() => {
) )
}); });
const tagLineRows = tagLineConfigs.map((tagLineConfig, i) => { const tagLineColumns = builderGroups.map((builderGroup, 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 ( return (
<tr className="bb-console-tag-row" key={`tag-${i}`}> <td key={i} colSpan={builderGroup.colspan} style={{textAlign: 'center'}}>
<td className="row-header"></td> {builderGroup.tag}
{columns} </td>
</tr> );
)
}); });
const changeRows = changesToShow.map(changeInfo => { const changeRows = changesToShow.map(changeInfo => {
const change = changeInfo.change; const change = changeInfo.change;
@ -460,7 +429,7 @@ export const ConsoleView = observer(() => {
return ( return (
<tr key={`change-${change.changeid}-${change.codebase}-${change.revision ?? ''}`}> <tr key={`change-${change.changeid}-${change.codebase}-${change.revision ?? ''}`}>
<td> <td>
<ChangeDetails change={change} compact={true} <YoctoChangeDetails change={change} compact={true}
showDetails={changeIsExpandedByChangeId.get(change.changeid) ?? false} showDetails={changeIsExpandedByChangeId.get(change.changeid) ?? false}
setShowDetails={(show: boolean) => changeIsExpandedByChangeId.set(change.changeid, show)}/> setShowDetails={(show: boolean) => changeIsExpandedByChangeId.set(change.changeid, show)}/>
</td> </td>
@ -474,28 +443,16 @@ export const ConsoleView = observer(() => {
<Table striped bordered className={(isBigTable() ? 'table-fixedwidth' : '')}> <Table striped bordered className={(isBigTable() ? 'table-fixedwidth' : '')}>
<thead> <thead>
<tr className="bb-console-table-first-row first-row"> <tr className="bb-console-table-first-row first-row">
<th className="row-header" style={{width: rowHeaderWidth}}> <th className="row-header">
<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> </th>
{builderColumns} {builderColumns}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{tagLineRows} <tr className="bb-console-tag-row" key="tag">
<td className="row-header"></td>
{tagLineColumns}
</tr>
{changeRows} {changeRows}
</tbody> </tbody>
</Table> </Table>
@ -506,7 +463,7 @@ export const ConsoleView = observer(() => {
buildbotSetupPlugin(reg => { buildbotSetupPlugin(reg => {
reg.registerMenuGroup({ reg.registerMenuGroup({
name: 'console', name: 'console',
caption: 'Console View', caption: 'Yocto Console View',
icon: <FaExclamationCircle/>, icon: <FaExclamationCircle/>,
order: 5, order: 5,
route: '/console', route: '/console',

View File

@ -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
}

View 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>
);
});

View File

@ -2,7 +2,7 @@ import {resolve} from "path";
import {defineConfig} from "vite"; import {defineConfig} from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
const outDir = 'buildbot_console_view/static'; const outDir = 'yocto_console_view/static';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [