Remove product literal strings in "pht()", part 6
[phabricator.git] / src / applications / harbormaster / controller / HarbormasterPlanViewController.php
blob4f2b70fcaf69b5b57f79cf6b5c7216833caf5c29
1 <?php
3 final class HarbormasterPlanViewController extends HarbormasterPlanController {
5 public function shouldAllowPublic() {
6 return true;
9 public function handleRequest(AphrontRequest $request) {
10 $viewer = $this->getViewer();
11 $id = $request->getURIData('id');
13 $plan = id(new HarbormasterBuildPlanQuery())
14 ->setViewer($viewer)
15 ->withIDs(array($id))
16 ->executeOne();
17 if (!$plan) {
18 return new Aphront404Response();
21 $title = $plan->getName();
23 $header = id(new PHUIHeaderView())
24 ->setHeader($plan->getName())
25 ->setUser($viewer)
26 ->setPolicyObject($plan)
27 ->setHeaderIcon('fa-ship');
29 $curtain = $this->buildCurtainView($plan);
31 $crumbs = $this->buildApplicationCrumbs()
32 ->addTextCrumb($plan->getObjectName())
33 ->setBorder(true);
35 list($step_list, $has_any_conflicts, $would_deadlock, $steps) =
36 $this->buildStepList($plan);
38 $error = null;
39 if (!$steps) {
40 $error = pht(
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) {
44 $error = pht(
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
51 // case.
52 $error = pht(
53 'This build plan has conflicts in one or more build steps. '.
54 'Examine the step list and resolve the listed errors.');
57 if ($error) {
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(
68 $plan,
69 new HarbormasterBuildPlanTransactionQuery());
70 $timeline->setShouldTerminate(true);
72 $view = id(new PHUITwoColumnView())
73 ->setHeader($header)
74 ->setCurtain($curtain)
75 ->setMainColumn(
76 array(
77 $error,
78 $step_list,
79 $options_view,
80 $rules_view,
81 $builds_view,
82 $timeline,
83 ));
85 return $this->newPage()
86 ->setTitle($title)
87 ->setCrumbs($crumbs)
88 ->setPageObjectPHIDs(array($plan->getPHID()))
89 ->appendChild($view);
92 private function buildStepList(HarbormasterBuildPlan $plan) {
93 $viewer = $this->getViewer();
95 $run_order = HarbormasterBuildGraph::determineDependencyExecution($plan);
97 $steps = id(new HarbormasterBuildStepQuery())
98 ->setViewer($viewer)
99 ->withBuildPlanPHIDs(array($plan->getPHID()))
100 ->execute();
101 $steps = mpull($steps, null, 'getPHID');
103 $can_edit = PhabricatorPolicyFilter::hasCapability(
104 $viewer,
105 $plan,
106 PhabricatorPolicyCapability::CAN_EDIT);
108 $step_list = id(new PHUIObjectItemListView())
109 ->setUser($viewer)
110 ->setNoDataString(
111 pht('This build plan does not have any build steps yet.'));
113 $i = 1;
114 $last_depth = 0;
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;
122 $i = 1;
123 } else {
124 $i++;
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;
138 try {
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.
143 $item
144 ->setStatusIcon('fa-warning red')
145 ->addAttribute(pht(
146 'This step has an invalid implementation (%s).',
147 $step->getClassName()));
148 continue;
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(
162 $plan,
163 $step,
164 null);
165 $available_artifacts = ipull($available_artifacts, 'type');
167 list($depends_ui, $has_conflicts) = $this->buildDependsOnList(
168 $depends,
169 pht('Depends On'),
170 $steps);
172 list($inputs_ui, $has_conflicts) = $this->buildArtifactList(
173 $inputs,
174 'in',
175 pht('Input Artifacts'),
176 $available_artifacts);
178 list($outputs_ui) = $this->buildArtifactList(
179 $outputs,
180 'out',
181 pht('Output Artifacts'),
182 array());
184 $item->appendChild(
185 phutil_tag(
186 'div',
187 array(
188 'class' => 'harbormaster-artifact-io',
190 array(
191 $depends_ui,
192 $inputs_ui,
193 $outputs_ui,
194 )));
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'))
217 ->addActionLink(
218 id(new PHUIButtonView())
219 ->setText(pht('Add Build Step'))
220 ->setHref($this->getApplicationURI("step/add/{$plan_id}/"))
221 ->setTag('a')
222 ->setIcon('fa-plus')
223 ->setDisabled(!$can_edit)
224 ->setWorkflow(!$can_edit));
226 $step_box = id(new PHUIObjectBoxView())
227 ->setHeader($header)
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(
241 $viewer,
242 $plan,
243 PhabricatorPolicyCapability::CAN_EDIT);
245 $curtain->addAction(
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()) {
254 $curtain->addAction(
255 id(new PhabricatorActionView())
256 ->setName(pht('Enable Plan'))
257 ->setHref($this->getApplicationURI("plan/disable/{$id}/"))
258 ->setWorkflow(true)
259 ->setDisabled(!$can_edit)
260 ->setIcon('fa-check'));
261 } else {
262 $curtain->addAction(
263 id(new PhabricatorActionView())
264 ->setName(pht('Disable Plan'))
265 ->setHref($this->getApplicationURI("plan/disable/{$id}/"))
266 ->setWorkflow(true)
267 ->setDisabled(!$can_edit)
268 ->setIcon('fa-ban'));
271 $can_run = ($plan->hasRunCapability($viewer) && $plan->canRunManually());
273 $curtain->addAction(
274 id(new PhabricatorActionView())
275 ->setName(pht('Run Plan Manually'))
276 ->setHref($this->getApplicationURI("plan/run/{$id}/"))
277 ->setWorkflow(true)
278 ->setDisabled(!$can_run)
279 ->setIcon('fa-play-circle'));
281 return $curtain;
284 private function buildArtifactList(
285 array $artifacts,
286 $kind,
287 $name,
288 array $available_artifacts) {
289 $has_conflicts = false;
291 if (!$artifacts) {
292 return array(null, $has_conflicts);
295 $this->requireResource('harbormaster-css');
297 $header = phutil_tag(
298 'div',
299 array(
300 'class' => 'harbormaster-artifact-summary-header',
302 $name);
304 $is_input = ($kind == 'in');
306 $list = new PHUIStatusListView();
307 foreach ($artifacts as $artifact) {
308 $error = null;
310 $key = idx($artifact, 'key');
311 if (!strlen($key)) {
312 $bound = phutil_tag('em', array(), pht('(null)'));
313 if ($is_input) {
314 // This is an unbound input. For now, all inputs are always required.
315 $icon = PHUIStatusItemView::ICON_WARNING;
316 $color = 'red';
317 $icon_label = pht('Required Input');
318 $has_conflicts = true;
319 $error = pht('This input is required, but not configured.');
320 } else {
321 // This is an unnamed output. Outputs do not necessarily need to be
322 // named.
323 $icon = PHUIStatusItemView::ICON_OPEN;
324 $color = 'bluegrey';
325 $icon_label = pht('Unused Output');
327 } else {
328 $bound = phutil_tag('strong', array(), $key);
329 if ($is_input) {
330 if (isset($available_artifacts[$key])) {
331 if ($available_artifacts[$key] == idx($artifact, 'type')) {
332 $icon = PHUIStatusItemView::ICON_ACCEPT;
333 $color = 'green';
334 $icon_label = pht('Valid Input');
335 } else {
336 $icon = PHUIStatusItemView::ICON_WARNING;
337 $color = 'red';
338 $icon_label = pht('Bad Input Type');
339 $has_conflicts = true;
340 $error = pht(
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'));
346 } else {
347 $icon = PHUIStatusItemView::ICON_QUESTION;
348 $color = 'red';
349 $icon_label = pht('Unknown Input');
350 $has_conflicts = true;
351 $error = pht(
352 'This input is bound to an artifact ("%s") which does not exist '.
353 'at this stage in the build process.',
354 $key);
356 } else {
357 $icon = PHUIStatusItemView::ICON_DOWN;
358 $color = 'green';
359 $icon_label = pht('Valid Output');
363 if ($error) {
364 $note = array(
365 phutil_tag('strong', array(), pht('ERROR:')),
366 ' ',
367 $error,
369 } else {
370 $note = $bound;
373 $list->addItem(
374 id(new PHUIStatusItemView())
375 ->setIcon($icon, $color, $icon_label)
376 ->setTarget($artifact['name'])
377 ->setNote($note));
380 $ui = array(
381 $header,
382 $list,
385 return array($ui, $has_conflicts);
388 private function buildDependsOnList(
389 array $step_phids,
390 $name,
391 array $steps) {
392 $has_conflicts = false;
394 if (!$step_phids) {
395 return null;
398 $this->requireResource('harbormaster-css');
400 $steps = mpull($steps, null, 'getPHID');
402 $header = phutil_tag(
403 'div',
404 array(
405 'class' => 'harbormaster-artifact-summary-header',
407 $name);
409 $list = new PHUIStatusListView();
410 foreach ($step_phids as $step_phid) {
411 $error = null;
413 if (idx($steps, $step_phid) === null) {
414 $icon = PHUIStatusItemView::ICON_WARNING;
415 $color = 'red';
416 $icon_label = pht('Missing Dependency');
417 $has_conflicts = true;
418 $error = pht(
419 "This dependency specifies a build step which doesn't exist.");
420 } else {
421 $bound = phutil_tag(
422 'strong',
423 array(),
424 idx($steps, $step_phid)->getName());
425 $icon = PHUIStatusItemView::ICON_ACCEPT;
426 $color = 'green';
427 $icon_label = pht('Valid Input');
430 if ($error) {
431 $note = array(
432 phutil_tag('strong', array(), pht('ERROR:')),
433 ' ',
434 $error,
436 } else {
437 $note = $bound;
440 $list->addItem(
441 id(new PHUIStatusItemView())
442 ->setIcon($icon, $color, $icon_label)
443 ->setTarget(pht('Build Step'))
444 ->setNote($note));
447 $ui = array(
448 $header,
449 $list,
452 return array($ui, $has_conflicts);
455 private function newBuildsView(HarbormasterBuildPlan $plan) {
456 $viewer = $this->getViewer();
458 $limit = 10;
459 $builds = id(new HarbormasterBuildQuery())
460 ->setViewer($viewer)
461 ->withBuildPlanPHIDs(array($plan->getPHID()))
462 ->setLimit($limit + 1)
463 ->execute();
465 $more_results = (count($builds) > $limit);
466 $builds = array_slice($builds, 0, $limit);
468 $list = id(new HarbormasterBuildView())
469 ->setViewer($viewer)
470 ->setBuilds($builds)
471 ->newObjectList();
473 $list->setNoDataString(pht('No recent builds.'));
475 $more_href = new PhutilURI(
476 $this->getApplicationURI('/build/'),
477 array('plan' => $plan->getPHID()));
479 if ($more_results) {
480 $list->newTailButton()
481 ->setHref($more_href);
484 $more_link = id(new PHUIButtonView())
485 ->setTag('a')
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())
495 ->setHeader($header)
496 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
497 ->appendChild($list);
500 private function newRulesView(HarbormasterBuildPlan $plan) {
501 $viewer = $this->getViewer();
503 $limit = 10;
504 $rules = id(new HeraldRuleQuery())
505 ->setViewer($viewer)
506 ->withDisabled(false)
507 ->withAffectedObjectPHIDs(array($plan->getPHID()))
508 ->needValidateAuthors(true)
509 ->setLimit($limit + 1)
510 ->execute();
512 $more_results = (count($rules) > $limit);
513 $rules = array_slice($rules, 0, $limit);
515 $list = id(new HeraldRuleListView())
516 ->setViewer($viewer)
517 ->setRules($rules)
518 ->newObjectList();
520 $list->setNoDataString(pht('No active Herald rules trigger this build.'));
522 $more_href = new PhutilURI(
523 '/herald/',
524 array('affectedPHID' => $plan->getPHID()));
526 if ($more_results) {
527 $list->newTailButton()
528 ->setHref($more_href);
531 $more_link = id(new PHUIButtonView())
532 ->setTag('a')
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())
542 ->setHeader($header)
543 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
544 ->appendChild($list);
547 private function newOptionsView(HarbormasterBuildPlan $plan) {
548 $viewer = $this->getViewer();
550 $can_edit = PhabricatorPolicyFilter::hasCapability(
551 $viewer,
552 $plan,
553 PhabricatorPolicyCapability::CAN_EDIT);
555 $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
557 $rows = array();
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(
566 urisprintf(
567 'plan/behavior/%d/%s/',
568 $plan->getID(),
569 $behavior->getKey())));
571 $edit_button = id(new PHUIButtonView())
572 ->setTag('a')
573 ->setColor(PHUIButtonView::GREY)
574 ->setSize(PHUIButtonView::SMALL)
575 ->setDisabled(!$can_edit)
576 ->setWorkflow(true)
577 ->setText(pht('Edit'))
578 ->setHref($edit_uri);
580 $rows[] = array(
581 $icon,
582 $behavior->getName(),
583 $option->getName(),
584 $option->getDescription(),
585 $edit_button,
589 $table = id(new AphrontTableView($rows))
590 ->setHeaders(
591 array(
592 null,
593 pht('Name'),
594 pht('Behavior'),
595 pht('Details'),
596 null,
598 ->setColumnClasses(
599 array(
600 null,
601 'pri',
602 null,
603 'wide',
604 null,
608 $header = id(new PHUIHeaderView())
609 ->setHeader(pht('Plan Behaviors'));
611 return id(new PHUIObjectBoxView())
612 ->setHeader($header)
613 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
614 ->setTable($table);