3 final class HarbormasterPlanViewController
extends HarbormasterPlanController
{
5 public function shouldAllowPublic() {
9 public function handleRequest(AphrontRequest
$request) {
10 $viewer = $this->getViewer();
11 $id = $request->getURIData('id');
13 $plan = id(new HarbormasterBuildPlanQuery())
18 return new Aphront404Response();
21 $title = $plan->getName();
23 $header = id(new PHUIHeaderView())
24 ->setHeader($plan->getName())
26 ->setPolicyObject($plan)
27 ->setHeaderIcon('fa-ship');
29 $curtain = $this->buildCurtainView($plan);
31 $crumbs = $this->buildApplicationCrumbs()
32 ->addTextCrumb($plan->getObjectName())
35 list($step_list, $has_any_conflicts, $would_deadlock, $steps) =
36 $this->buildStepList($plan);
41 'This build plan does not have any build steps yet, so it will '.
42 'not do anything when run.');
43 } else if ($would_deadlock) {
45 'This build plan will deadlock when executed, due to circular '.
46 'dependencies present in the build plan. Examine the step list '.
47 'and resolve the deadlock.');
48 } else if ($has_any_conflicts) {
49 // A deadlocking build will also cause all the artifacts to be
50 // invalid, so we just skip showing this message if that's the
53 'This build plan has conflicts in one or more build steps. '.
54 'Examine the step list and resolve the listed errors.');
58 $error = id(new PHUIInfoView())
59 ->setSeverity(PHUIInfoView
::SEVERITY_WARNING
)
60 ->appendChild($error);
63 $builds_view = $this->newBuildsView($plan);
64 $options_view = $this->newOptionsView($plan);
65 $rules_view = $this->newRulesView($plan);
67 $timeline = $this->buildTransactionTimeline(
69 new HarbormasterBuildPlanTransactionQuery());
70 $timeline->setShouldTerminate(true);
72 $view = id(new PHUITwoColumnView())
74 ->setCurtain($curtain)
85 return $this->newPage()
88 ->setPageObjectPHIDs(array($plan->getPHID()))
92 private function buildStepList(HarbormasterBuildPlan
$plan) {
93 $viewer = $this->getViewer();
95 $run_order = HarbormasterBuildGraph
::determineDependencyExecution($plan);
97 $steps = id(new HarbormasterBuildStepQuery())
99 ->withBuildPlanPHIDs(array($plan->getPHID()))
101 $steps = mpull($steps, null, 'getPHID');
103 $can_edit = PhabricatorPolicyFilter
::hasCapability(
106 PhabricatorPolicyCapability
::CAN_EDIT
);
108 $step_list = id(new PHUIObjectItemListView())
111 pht('This build plan does not have any build steps yet.'));
115 $has_any_conflicts = false;
116 $is_deadlocking = false;
117 foreach ($run_order as $run_ref) {
118 $step = $steps[$run_ref['node']->getPHID()];
119 $depth = $run_ref['depth'] +
1;
120 if ($last_depth !== $depth) {
121 $last_depth = $depth;
127 $step_id = $step->getID();
128 $view_uri = $this->getApplicationURI("step/view/{$step_id}/");
130 $item = id(new PHUIObjectItemView())
131 ->setObjectName(pht('Step %d.%d', $depth, $i))
132 ->setHeader($step->getName())
133 ->setHref($view_uri);
135 $step_list->addItem($item);
137 $implementation = null;
139 $implementation = $step->getStepImplementation();
140 } catch (Exception
$ex) {
141 // We can't initialize the implementation. This might be because
142 // it's been renamed or no longer exists.
144 ->setStatusIcon('fa-warning red')
146 'This step has an invalid implementation (%s).',
147 $step->getClassName()));
151 $item->addAttribute($implementation->getDescription());
152 $item->setHref($view_uri);
154 $depends = $step->getStepImplementation()->getDependencies($step);
155 $inputs = $step->getStepImplementation()->getArtifactInputs();
156 $outputs = $step->getStepImplementation()->getArtifactOutputs();
158 $has_conflicts = false;
159 if ($depends ||
$inputs ||
$outputs) {
160 $available_artifacts =
161 HarbormasterBuildStepImplementation
::getAvailableArtifacts(
165 $available_artifacts = ipull($available_artifacts, 'type');
167 list($depends_ui, $has_conflicts) = $this->buildDependsOnList(
172 list($inputs_ui, $has_conflicts) = $this->buildArtifactList(
175 pht('Input Artifacts'),
176 $available_artifacts);
178 list($outputs_ui) = $this->buildArtifactList(
181 pht('Output Artifacts'),
188 'class' => 'harbormaster-artifact-io',
197 if ($has_conflicts) {
198 $has_any_conflicts = true;
199 $item->setStatusIcon('fa-warning red');
202 if ($run_ref['cycle']) {
203 $is_deadlocking = true;
206 if ($is_deadlocking) {
207 $item->setStatusIcon('fa-warning red');
211 $step_list->setFlush(true);
213 $plan_id = $plan->getID();
215 $header = id(new PHUIHeaderView())
216 ->setHeader(pht('Build Steps'))
218 id(new PHUIButtonView())
219 ->setText(pht('Add Build Step'))
220 ->setHref($this->getApplicationURI("step/add/{$plan_id}/"))
223 ->setDisabled(!$can_edit)
224 ->setWorkflow(!$can_edit));
226 $step_box = id(new PHUIObjectBoxView())
228 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
229 ->appendChild($step_list);
231 return array($step_box, $has_any_conflicts, $is_deadlocking, $steps);
234 private function buildCurtainView(HarbormasterBuildPlan
$plan) {
235 $viewer = $this->getViewer();
236 $id = $plan->getID();
238 $curtain = $this->newCurtainView($plan);
240 $can_edit = PhabricatorPolicyFilter
::hasCapability(
243 PhabricatorPolicyCapability
::CAN_EDIT
);
246 id(new PhabricatorActionView())
247 ->setName(pht('Edit Plan'))
248 ->setHref($this->getApplicationURI("plan/edit/{$id}/"))
249 ->setWorkflow(!$can_edit)
250 ->setDisabled(!$can_edit)
251 ->setIcon('fa-pencil'));
253 if ($plan->isDisabled()) {
255 id(new PhabricatorActionView())
256 ->setName(pht('Enable Plan'))
257 ->setHref($this->getApplicationURI("plan/disable/{$id}/"))
259 ->setDisabled(!$can_edit)
260 ->setIcon('fa-check'));
263 id(new PhabricatorActionView())
264 ->setName(pht('Disable Plan'))
265 ->setHref($this->getApplicationURI("plan/disable/{$id}/"))
267 ->setDisabled(!$can_edit)
268 ->setIcon('fa-ban'));
271 $can_run = ($plan->hasRunCapability($viewer) && $plan->canRunManually());
274 id(new PhabricatorActionView())
275 ->setName(pht('Run Plan Manually'))
276 ->setHref($this->getApplicationURI("plan/run/{$id}/"))
278 ->setDisabled(!$can_run)
279 ->setIcon('fa-play-circle'));
284 private function buildArtifactList(
288 array $available_artifacts) {
289 $has_conflicts = false;
292 return array(null, $has_conflicts);
295 $this->requireResource('harbormaster-css');
297 $header = phutil_tag(
300 'class' => 'harbormaster-artifact-summary-header',
304 $is_input = ($kind == 'in');
306 $list = new PHUIStatusListView();
307 foreach ($artifacts as $artifact) {
310 $key = idx($artifact, 'key');
312 $bound = phutil_tag('em', array(), pht('(null)'));
314 // This is an unbound input. For now, all inputs are always required.
315 $icon = PHUIStatusItemView
::ICON_WARNING
;
317 $icon_label = pht('Required Input');
318 $has_conflicts = true;
319 $error = pht('This input is required, but not configured.');
321 // This is an unnamed output. Outputs do not necessarily need to be
323 $icon = PHUIStatusItemView
::ICON_OPEN
;
325 $icon_label = pht('Unused Output');
328 $bound = phutil_tag('strong', array(), $key);
330 if (isset($available_artifacts[$key])) {
331 if ($available_artifacts[$key] == idx($artifact, 'type')) {
332 $icon = PHUIStatusItemView
::ICON_ACCEPT
;
334 $icon_label = pht('Valid Input');
336 $icon = PHUIStatusItemView
::ICON_WARNING
;
338 $icon_label = pht('Bad Input Type');
339 $has_conflicts = true;
341 'This input is bound to the wrong artifact type. It is bound '.
342 'to a "%s" artifact, but should be bound to a "%s" artifact.',
343 $available_artifacts[$key],
344 idx($artifact, 'type'));
347 $icon = PHUIStatusItemView
::ICON_QUESTION
;
349 $icon_label = pht('Unknown Input');
350 $has_conflicts = true;
352 'This input is bound to an artifact ("%s") which does not exist '.
353 'at this stage in the build process.',
357 $icon = PHUIStatusItemView
::ICON_DOWN
;
359 $icon_label = pht('Valid Output');
365 phutil_tag('strong', array(), pht('ERROR:')),
374 id(new PHUIStatusItemView())
375 ->setIcon($icon, $color, $icon_label)
376 ->setTarget($artifact['name'])
385 return array($ui, $has_conflicts);
388 private function buildDependsOnList(
392 $has_conflicts = false;
398 $this->requireResource('harbormaster-css');
400 $steps = mpull($steps, null, 'getPHID');
402 $header = phutil_tag(
405 'class' => 'harbormaster-artifact-summary-header',
409 $list = new PHUIStatusListView();
410 foreach ($step_phids as $step_phid) {
413 if (idx($steps, $step_phid) === null) {
414 $icon = PHUIStatusItemView
::ICON_WARNING
;
416 $icon_label = pht('Missing Dependency');
417 $has_conflicts = true;
419 "This dependency specifies a build step which doesn't exist.");
424 idx($steps, $step_phid)->getName());
425 $icon = PHUIStatusItemView
::ICON_ACCEPT
;
427 $icon_label = pht('Valid Input');
432 phutil_tag('strong', array(), pht('ERROR:')),
441 id(new PHUIStatusItemView())
442 ->setIcon($icon, $color, $icon_label)
443 ->setTarget(pht('Build Step'))
452 return array($ui, $has_conflicts);
455 private function newBuildsView(HarbormasterBuildPlan
$plan) {
456 $viewer = $this->getViewer();
459 $builds = id(new HarbormasterBuildQuery())
461 ->withBuildPlanPHIDs(array($plan->getPHID()))
462 ->setLimit($limit +
1)
465 $more_results = (count($builds) > $limit);
466 $builds = array_slice($builds, 0, $limit);
468 $list = id(new HarbormasterBuildView())
473 $list->setNoDataString(pht('No recent builds.'));
475 $more_href = new PhutilURI(
476 $this->getApplicationURI('/build/'),
477 array('plan' => $plan->getPHID()));
480 $list->newTailButton()
481 ->setHref($more_href);
484 $more_link = id(new PHUIButtonView())
486 ->setIcon('fa-list-ul')
487 ->setText(pht('View All Builds'))
488 ->setHref($more_href);
490 $header = id(new PHUIHeaderView())
491 ->setHeader(pht('Recent Builds'))
492 ->addActionLink($more_link);
494 return id(new PHUIObjectBoxView())
496 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
497 ->appendChild($list);
500 private function newRulesView(HarbormasterBuildPlan
$plan) {
501 $viewer = $this->getViewer();
504 $rules = id(new HeraldRuleQuery())
506 ->withDisabled(false)
507 ->withAffectedObjectPHIDs(array($plan->getPHID()))
508 ->needValidateAuthors(true)
509 ->setLimit($limit +
1)
512 $more_results = (count($rules) > $limit);
513 $rules = array_slice($rules, 0, $limit);
515 $list = id(new HeraldRuleListView())
520 $list->setNoDataString(pht('No active Herald rules trigger this build.'));
522 $more_href = new PhutilURI(
524 array('affectedPHID' => $plan->getPHID()));
527 $list->newTailButton()
528 ->setHref($more_href);
531 $more_link = id(new PHUIButtonView())
533 ->setIcon('fa-list-ul')
534 ->setText(pht('View All Rules'))
535 ->setHref($more_href);
537 $header = id(new PHUIHeaderView())
538 ->setHeader(pht('Run By Herald Rules'))
539 ->addActionLink($more_link);
541 return id(new PHUIObjectBoxView())
543 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
544 ->appendChild($list);
547 private function newOptionsView(HarbormasterBuildPlan
$plan) {
548 $viewer = $this->getViewer();
550 $can_edit = PhabricatorPolicyFilter
::hasCapability(
553 PhabricatorPolicyCapability
::CAN_EDIT
);
555 $behaviors = HarbormasterBuildPlanBehavior
::newPlanBehaviors();
558 foreach ($behaviors as $behavior) {
559 $option = $behavior->getPlanOption($plan);
561 $icon = $option->getIcon();
562 $icon = id(new PHUIIconView())->setIcon($icon);
564 $edit_uri = new PhutilURI(
565 $this->getApplicationURI(
567 'plan/behavior/%d/%s/',
569 $behavior->getKey())));
571 $edit_button = id(new PHUIButtonView())
573 ->setColor(PHUIButtonView
::GREY
)
574 ->setSize(PHUIButtonView
::SMALL
)
575 ->setDisabled(!$can_edit)
577 ->setText(pht('Edit'))
578 ->setHref($edit_uri);
582 $behavior->getName(),
584 $option->getDescription(),
589 $table = id(new AphrontTableView($rows))
608 $header = id(new PHUIHeaderView())
609 ->setHeader(pht('Plan Behaviors'));
611 return id(new PHUIObjectBoxView())
613 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)