Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert stages to use card #140

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/frontend/multi-pipeline-graph-view/app.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "../pipeline-graph-view/cards";
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,7 @@ export const MultiPipelineGraph = () => {
}
}, [runs, poll]);
return (
<table className="jenkins-table sortable">
<thead>
<tr>
<th className="jenkins-table__cell--tight">id</th>
<th data-sort-disable="true">pipeline</th>
</tr>
</thead>
<table>
<tbody>
{runs.map((run) => (
<SingleRun key={run.id} run={run} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,23 @@ interface Props {

export const SingleRun: (data: Props) => JSX.Element = ({ run }) => {
const [stages, setStages] = useState<Array<StageInfo>>([]);
const path = `graph?runId=${run.id}`;
const path = `tree?runId=${run.id}`;
const singleRunPage = `../${run.id}/pipeline-graph/`;

const handleNodeClick = (nodeName: string, id: number) => {
console.log(nodeName, id);
window.location.href = `../${run.id}/pipeline-console?selected-node=${id}`;
};

const buildNameStyle: React.CSSProperties = {
verticalAlign: "middle",
};

return (
<tr>
<td>
<td style={buildNameStyle}>
<a href={singleRunPage} className="jenkins-table__link">
{run.id}
#{run.id}
</a>
</td>
<td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { RunInfo } from "../MultiPipelineGraphModel";
*/
export default function startPollingRunsStatus(
onFetchSuccess: (data: Array<RunInfo>) => void,
onFetchError: (err: Error) => void,
interval = 10000
onFetchError: (err: Error) => void
) {
const path = "runs";
async function fetchPipelineData() {
Expand All @@ -19,7 +18,8 @@ export default function startPollingRunsStatus(
// TODO: implement exponential backoff of the timeout interval
onFetchError(err);
} finally {
setTimeout(() => fetchPipelineData(), interval);
// TODO reimplement live loading but only while run is actually running, we don't want to be continuously hitting the controller for this
// setTimeout(() => fetchPipelineData(), interval);
}
}
fetchPipelineData();
Expand Down
16 changes: 16 additions & 0 deletions src/main/frontend/pipeline-graph-view/_cards.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
:root {
--card-background: hsl(212, 30%, 96%);
--card-gap: 1rem;
}

[data-theme="dark"] {
--card-background: hsl(230deg 14% 23%);
}

@media (prefers-color-scheme: dark) {
[data-theme="dark-system"],
[data-theme="dark-system"] {
--card-background: hsl(230deg 14% 23%);
}
}

.pgv-cards {
@media (max-width: 699px) {
&__item {
Expand Down
16 changes: 0 additions & 16 deletions src/main/frontend/pipeline-graph-view/app.scss
Original file line number Diff line number Diff line change
@@ -1,19 +1,3 @@
:root {
--card-background: hsl(212, 30%, 96%);
--card-gap: 1rem;
}

[data-theme="dark"] {
--card-background: hsl(230deg 14% 23%);
}

@media (prefers-color-scheme: dark) {
[data-theme="dark-system"],
[data-theme="dark-system"] {
--card-background: hsl(230deg 14% 23%);
}
}

@import "cards";

.app-details__item svg {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ public Permission getPermission() {
}

@GET
@WebMethod(name = "graph")
public HttpResponse getGraph(StaplerRequest req) throws JsonProcessingException {
@WebMethod(name = "tree")
public HttpResponse getTree(StaplerRequest req) throws JsonProcessingException {
String runId = req.getParameter("runId");
WorkflowRun run = target.getBuildByNumber(Integer.parseInt(runId));
PipelineGraphApi api = new PipelineGraphApi(run);
JSONObject graph = createGraphJson(api.createGraph());
return HttpResponses.okJSON(graph);
JSONObject tree = createGraphJson(api.createTree());
return HttpResponses.okJSON(tree);
}

protected JSONObject createGraphJson(PipelineGraph pipelineGraph) throws JsonProcessingException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.verb.GET;

public abstract class AbstractPipelineViewAction implements Action, IconSpec {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
Expand Down Expand Up @@ -55,19 +54,9 @@
return JSONObject.fromObject(graph);
}

@GET
@WebMethod(name = "graph")
public HttpResponse getGraph() throws JsonProcessingException {
// TODO for automatic json serialisation look at:
// https://github.com/jenkinsci/blueocean-plugin/blob/4f2aa260fca22604a087629dc0da5c80735e0548/blueocean-commons/src/main/java/io/jenkins/blueocean/commons/stapler/Export.java#L101
// https://github.com/jenkinsci/blueocean-plugin/blob/4f2aa260fca22604a087629dc0da5c80735e0548/blueocean-commons/src/main/java/io/jenkins/blueocean/commons/stapler/TreeResponse.java#L48
JSONObject graph = createJson(api.createGraph());
return HttpResponses.okJSON(graph);
}

@WebMethod(name = "tree")
public HttpResponse getTree() throws JsonProcessingException {
// TODO: This need to be updated to return a tree representation of the graph, not the graph.

Check warning on line 59 in src/main/java/io/jenkins/plugins/pipelinegraphview/utils/AbstractPipelineViewAction.java

View check run for this annotation

ci.jenkins.io / Open Tasks Scanner

TODO

NORMAL: This need to be updated to return a tree representation of the graph, not the graph.
// Here is how FlowGraphTree does it:
// https://github.com/jenkinsci/workflow-support-plugin/blob/master/src/main/java/org/jenkinsci/plugins/workflow/support/visualization/table/FlowGraphTable.java#L126
JSONObject graph = createJson(api.createTree());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,89 +69,12 @@
50, // TODO how ???
flowNodeWrapper.getType().name(),
flowNodeWrapper
.getDisplayName(), // TODO blue ocean uses timing information: "Passed in 0s"

Check warning on line 72 in src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineGraphApi.java

View check run for this annotation

ci.jenkins.io / Open Tasks Scanner

TODO

NORMAL: blue ocean uses timing information: "Passed in 0s"
flowNodeWrapper.isSynthetic());
})
.collect(Collectors.toList());
}

public PipelineGraph createGraph() {
List<PipelineStageInternal> stages = getPipelineNodes();

// id => stage
Map<String, PipelineStageInternal> stageMap =
stages.stream()
.collect(
Collectors.toMap(
PipelineStageInternal::getId, stage -> stage, (u, v) -> u, LinkedHashMap::new));

Map<String, List<String>> stageToChildrenMap = new HashMap<>();

List<String> stagesThatAreNested = new ArrayList<>();

Map<String, String> nextSiblingToOlderSibling = new HashMap<>();

List<String> stagesThatAreChildrenOrNestedStages = new ArrayList<>();
stages.forEach(
stage -> {
if (stage.getParents().isEmpty()) {
stageToChildrenMap.put(stage.getId(), new ArrayList<>());
} else if (stage.getType().equals("PARALLEL")) {
String parentId = stage.getParents().get(0); // assume one parent for now
List<String> childrenOfParent =
stageToChildrenMap.getOrDefault(parentId, new ArrayList<>());
childrenOfParent.add(stage.getId());
stageToChildrenMap.put(parentId, childrenOfParent);
stagesThatAreChildrenOrNestedStages.add(stage.getId());
} else if (stageMap.get(stage.getParents().get(0)).getType().equals("PARALLEL")) {
String parentId = stage.getParents().get(0);
PipelineStageInternal parent = stageMap.get(parentId);
parent.setSeqContainerName(parent.getName());
parent.setName(stage.getName());
parent.setSequential(true);
parent.setType(stage.getType());
parent.setTitle(stage.getTitle());
parent.setCompletePercent(stage.getCompletePercent());
stage.setSequential(true);

nextSiblingToOlderSibling.put(stage.getId(), parentId);
stagesThatAreNested.add(stage.getId());
stagesThatAreChildrenOrNestedStages.add(stage.getId());
// nested stage of nested stage
} else if (stagesThatAreNested.contains(
stageMap.get(stage.getParents().get(0)).getId())) {
PipelineStageInternal parent =
stageMap.get(nextSiblingToOlderSibling.get(stage.getParents().get(0)));
// shouldn't happen but found it after restarting a matrix build
// this breaks the layout badly but prevents a null pointer
if (parent != null) {
stage.setSequential(true);
parent.setNextSibling(stage);
stagesThatAreNested.add(stage.getId());
stagesThatAreChildrenOrNestedStages.add(stage.getId());
}
}
});

List<PipelineStage> stageResults =
stageMap.values().stream()
.map(
pipelineStageInternal -> {
List<PipelineStage> children =
stageToChildrenMap.getOrDefault(pipelineStageInternal.getId(), emptyList())
.stream()
.map(mapper(stageMap, stageToChildrenMap))
.collect(Collectors.toList());

return pipelineStageInternal.toPipelineStage(children);
})
.filter(stage -> !stagesThatAreChildrenOrNestedStages.contains(stage.getId()))
.collect(Collectors.toList());

FlowExecution execution = run.getExecution();
return new PipelineGraph(stageResults, execution != null && execution.isComplete());
}

private Function<String, PipelineStage> mapper(
Map<String, PipelineStageInternal> stageMap, Map<String, List<String>> stageToChildrenMap) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:p="/lib/pipeline-graph-view">
<l:layout title="${%Graph} [${it.buildDisplayName}]" type="one-column">
<l:layout title="${%Details} [${it.buildDisplayName}]" type="one-column">
<l:main-panel>
<div class="jenkins-app-bar">
<div class="jenkins-app-bar__content">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:l="/lib/layout">
<l:layout title="Stages [${it.jobDisplayName}]" type="one-column">
<j:jelly xmlns:j="jelly:core" xmlns:l="/lib/layout" xmlns:p="/lib/pipeline-graph-view">
<l:layout title="${%Stages} [${it.jobDisplayName}]" type="one-column">
<l:main-panel>
<div class="jenkins-app-bar">
<div class="jenkins-app-bar__content">
Expand All @@ -23,8 +23,12 @@
</a>
</div>
</div>
<div id="multiple-pipeline-root"/>
<script src="${rootURL}/plugin/pipeline-graph-view/js/bundles/multi-pipeline-graph-view-bundle.js"/>
<div class="pgv-cards">
<p:card title="${%Stages}" type="wide" expandable="true">
<div id="multiple-pipeline-root"/>
<script src="${rootURL}/plugin/pipeline-graph-view/js/bundles/multi-pipeline-graph-view-bundle.js"/>
</p:card>
</div>
<script src="${rootURL}/plugin/pipeline-graph-view/js/build.js"/>
</l:main-panel>
</l:layout>
Expand Down
21 changes: 17 additions & 4 deletions src/main/resources/lib/pipeline-graph-view/card.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@
The title for the card
</st:attribute>
<st:attribute name="expandable">
Set true if you want to allow users to click a button to pop out a larger version of the card
Set true if you want to allow users to expand the card (by default it opens a modal, see expandableLink to link to
a different page)
</st:attribute>
<st:attribute name="expandableLink">
Set path for the expandable link to go to.
</st:attribute>
</st:documentation>
<div class="pgv-cards__item${attrs.type == 'wide' ? ' pgv-cards__item--wide' : ''}${attrs.expandable == 'true' ? ' pgv-modal' : ''}">
<div
class="pgv-cards__item${attrs.type == 'wide' ? ' pgv-cards__item--wide' : ''}${attrs.expandable == 'true' ? ' pgv-modal' : ''}">
<j:choose>
<j:when test="${attrs.expandable == 'true'}">
<j:when test="${attrs.expandable == 'true' and attrs.expandableLink == null}">
<div class="pgv-modal__content">
<p class="pgv-cards__item__title">${attrs.title}
<a href="#" class="pgv-modal__expander">
Expand All @@ -34,7 +39,15 @@
</div>
</j:when>
<j:otherwise>
<p class="pgv-cards__item__title">${attrs.title}</p>
<p class="pgv-cards__item__title">${attrs.title}
<j:if test="${attrs.expandable == 'true' and attrs.expandableLink != null}">
<a href="${attrs.expandableLink}" class="pgv-modal__expander">
<span class="icon-sm">
<l:icon src="symbol-resize-outline plugin-ionicons-api" tooltip="${%Expand} ${attrs.title}"/>
</span>
</a>
</j:if>
</p>
<d:invokeBody/>
</j:otherwise>
</j:choose>
Expand Down
23 changes: 12 additions & 11 deletions src/main/webapp/js/card.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.pgv-modal__expander')
.forEach(expander => {
expander.addEventListener('click', (event) => {
event.preventDefault();

const modal = expander.closest('.pgv-modal')
modal.classList.add('pgv-modal__open');
if (expander.getAttribute('href') === '#') {
expander.addEventListener('click', (event) => {
event.preventDefault();

window.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
modal.classList.remove('pgv-modal__open');
}
}, { once: true });
const modal = expander.closest('.pgv-modal')
modal.classList.add('pgv-modal__open');

})
window.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
modal.classList.remove('pgv-modal__open');
}
}, { once: true });
})
}
})

document.querySelectorAll('.pgv-modal__closer')
Expand Down
Loading
Loading