Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
12256 views
1
<?php
2
3
final class HarbormasterPlanViewController extends HarbormasterPlanController {
4
5
public function shouldAllowPublic() {
6
return true;
7
}
8
9
public function handleRequest(AphrontRequest $request) {
10
$viewer = $this->getViewer();
11
$id = $request->getURIData('id');
12
13
$plan = id(new HarbormasterBuildPlanQuery())
14
->setViewer($viewer)
15
->withIDs(array($id))
16
->executeOne();
17
if (!$plan) {
18
return new Aphront404Response();
19
}
20
21
$title = $plan->getName();
22
23
$header = id(new PHUIHeaderView())
24
->setHeader($plan->getName())
25
->setUser($viewer)
26
->setPolicyObject($plan)
27
->setHeaderIcon('fa-ship');
28
29
$curtain = $this->buildCurtainView($plan);
30
31
$crumbs = $this->buildApplicationCrumbs()
32
->addTextCrumb($plan->getObjectName())
33
->setBorder(true);
34
35
list($step_list, $has_any_conflicts, $would_deadlock, $steps) =
36
$this->buildStepList($plan);
37
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.');
55
}
56
57
if ($error) {
58
$error = id(new PHUIInfoView())
59
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
60
->appendChild($error);
61
}
62
63
$builds_view = $this->newBuildsView($plan);
64
$options_view = $this->newOptionsView($plan);
65
$rules_view = $this->newRulesView($plan);
66
67
$timeline = $this->buildTransactionTimeline(
68
$plan,
69
new HarbormasterBuildPlanTransactionQuery());
70
$timeline->setShouldTerminate(true);
71
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
));
84
85
return $this->newPage()
86
->setTitle($title)
87
->setCrumbs($crumbs)
88
->setPageObjectPHIDs(array($plan->getPHID()))
89
->appendChild($view);
90
}
91
92
private function buildStepList(HarbormasterBuildPlan $plan) {
93
$viewer = $this->getViewer();
94
95
$run_order = HarbormasterBuildGraph::determineDependencyExecution($plan);
96
97
$steps = id(new HarbormasterBuildStepQuery())
98
->setViewer($viewer)
99
->withBuildPlanPHIDs(array($plan->getPHID()))
100
->execute();
101
$steps = mpull($steps, null, 'getPHID');
102
103
$can_edit = PhabricatorPolicyFilter::hasCapability(
104
$viewer,
105
$plan,
106
PhabricatorPolicyCapability::CAN_EDIT);
107
108
$step_list = id(new PHUIObjectItemListView())
109
->setUser($viewer)
110
->setNoDataString(
111
pht('This build plan does not have any build steps yet.'));
112
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++;
125
}
126
127
$step_id = $step->getID();
128
$view_uri = $this->getApplicationURI("step/view/{$step_id}/");
129
130
$item = id(new PHUIObjectItemView())
131
->setObjectName(pht('Step %d.%d', $depth, $i))
132
->setHeader($step->getName())
133
->setHref($view_uri);
134
135
$step_list->addItem($item);
136
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;
149
}
150
151
$item->addAttribute($implementation->getDescription());
152
$item->setHref($view_uri);
153
154
$depends = $step->getStepImplementation()->getDependencies($step);
155
$inputs = $step->getStepImplementation()->getArtifactInputs();
156
$outputs = $step->getStepImplementation()->getArtifactOutputs();
157
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');
166
167
list($depends_ui, $has_conflicts) = $this->buildDependsOnList(
168
$depends,
169
pht('Depends On'),
170
$steps);
171
172
list($inputs_ui, $has_conflicts) = $this->buildArtifactList(
173
$inputs,
174
'in',
175
pht('Input Artifacts'),
176
$available_artifacts);
177
178
list($outputs_ui) = $this->buildArtifactList(
179
$outputs,
180
'out',
181
pht('Output Artifacts'),
182
array());
183
184
$item->appendChild(
185
phutil_tag(
186
'div',
187
array(
188
'class' => 'harbormaster-artifact-io',
189
),
190
array(
191
$depends_ui,
192
$inputs_ui,
193
$outputs_ui,
194
)));
195
}
196
197
if ($has_conflicts) {
198
$has_any_conflicts = true;
199
$item->setStatusIcon('fa-warning red');
200
}
201
202
if ($run_ref['cycle']) {
203
$is_deadlocking = true;
204
}
205
206
if ($is_deadlocking) {
207
$item->setStatusIcon('fa-warning red');
208
}
209
}
210
211
$step_list->setFlush(true);
212
213
$plan_id = $plan->getID();
214
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));
225
226
$step_box = id(new PHUIObjectBoxView())
227
->setHeader($header)
228
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
229
->appendChild($step_list);
230
231
return array($step_box, $has_any_conflicts, $is_deadlocking, $steps);
232
}
233
234
private function buildCurtainView(HarbormasterBuildPlan $plan) {
235
$viewer = $this->getViewer();
236
$id = $plan->getID();
237
238
$curtain = $this->newCurtainView($plan);
239
240
$can_edit = PhabricatorPolicyFilter::hasCapability(
241
$viewer,
242
$plan,
243
PhabricatorPolicyCapability::CAN_EDIT);
244
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'));
252
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'));
269
}
270
271
$can_run = ($plan->hasRunCapability($viewer) && $plan->canRunManually());
272
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'));
280
281
return $curtain;
282
}
283
284
private function buildArtifactList(
285
array $artifacts,
286
$kind,
287
$name,
288
array $available_artifacts) {
289
$has_conflicts = false;
290
291
if (!$artifacts) {
292
return array(null, $has_conflicts);
293
}
294
295
$this->requireResource('harbormaster-css');
296
297
$header = phutil_tag(
298
'div',
299
array(
300
'class' => 'harbormaster-artifact-summary-header',
301
),
302
$name);
303
304
$is_input = ($kind == 'in');
305
306
$list = new PHUIStatusListView();
307
foreach ($artifacts as $artifact) {
308
$error = null;
309
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');
326
}
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'));
345
}
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);
355
}
356
} else {
357
$icon = PHUIStatusItemView::ICON_DOWN;
358
$color = 'green';
359
$icon_label = pht('Valid Output');
360
}
361
}
362
363
if ($error) {
364
$note = array(
365
phutil_tag('strong', array(), pht('ERROR:')),
366
' ',
367
$error,
368
);
369
} else {
370
$note = $bound;
371
}
372
373
$list->addItem(
374
id(new PHUIStatusItemView())
375
->setIcon($icon, $color, $icon_label)
376
->setTarget($artifact['name'])
377
->setNote($note));
378
}
379
380
$ui = array(
381
$header,
382
$list,
383
);
384
385
return array($ui, $has_conflicts);
386
}
387
388
private function buildDependsOnList(
389
array $step_phids,
390
$name,
391
array $steps) {
392
$has_conflicts = false;
393
394
if (!$step_phids) {
395
return null;
396
}
397
398
$this->requireResource('harbormaster-css');
399
400
$steps = mpull($steps, null, 'getPHID');
401
402
$header = phutil_tag(
403
'div',
404
array(
405
'class' => 'harbormaster-artifact-summary-header',
406
),
407
$name);
408
409
$list = new PHUIStatusListView();
410
foreach ($step_phids as $step_phid) {
411
$error = null;
412
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');
428
}
429
430
if ($error) {
431
$note = array(
432
phutil_tag('strong', array(), pht('ERROR:')),
433
' ',
434
$error,
435
);
436
} else {
437
$note = $bound;
438
}
439
440
$list->addItem(
441
id(new PHUIStatusItemView())
442
->setIcon($icon, $color, $icon_label)
443
->setTarget(pht('Build Step'))
444
->setNote($note));
445
}
446
447
$ui = array(
448
$header,
449
$list,
450
);
451
452
return array($ui, $has_conflicts);
453
}
454
455
private function newBuildsView(HarbormasterBuildPlan $plan) {
456
$viewer = $this->getViewer();
457
458
$limit = 10;
459
$builds = id(new HarbormasterBuildQuery())
460
->setViewer($viewer)
461
->withBuildPlanPHIDs(array($plan->getPHID()))
462
->setLimit($limit + 1)
463
->execute();
464
465
$more_results = (count($builds) > $limit);
466
$builds = array_slice($builds, 0, $limit);
467
468
$list = id(new HarbormasterBuildView())
469
->setViewer($viewer)
470
->setBuilds($builds)
471
->newObjectList();
472
473
$list->setNoDataString(pht('No recent builds.'));
474
475
$more_href = new PhutilURI(
476
$this->getApplicationURI('/build/'),
477
array('plan' => $plan->getPHID()));
478
479
if ($more_results) {
480
$list->newTailButton()
481
->setHref($more_href);
482
}
483
484
$more_link = id(new PHUIButtonView())
485
->setTag('a')
486
->setIcon('fa-list-ul')
487
->setText(pht('View All Builds'))
488
->setHref($more_href);
489
490
$header = id(new PHUIHeaderView())
491
->setHeader(pht('Recent Builds'))
492
->addActionLink($more_link);
493
494
return id(new PHUIObjectBoxView())
495
->setHeader($header)
496
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
497
->appendChild($list);
498
}
499
500
private function newRulesView(HarbormasterBuildPlan $plan) {
501
$viewer = $this->getViewer();
502
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();
511
512
$more_results = (count($rules) > $limit);
513
$rules = array_slice($rules, 0, $limit);
514
515
$list = id(new HeraldRuleListView())
516
->setViewer($viewer)
517
->setRules($rules)
518
->newObjectList();
519
520
$list->setNoDataString(pht('No active Herald rules trigger this build.'));
521
522
$more_href = new PhutilURI(
523
'/herald/',
524
array('affectedPHID' => $plan->getPHID()));
525
526
if ($more_results) {
527
$list->newTailButton()
528
->setHref($more_href);
529
}
530
531
$more_link = id(new PHUIButtonView())
532
->setTag('a')
533
->setIcon('fa-list-ul')
534
->setText(pht('View All Rules'))
535
->setHref($more_href);
536
537
$header = id(new PHUIHeaderView())
538
->setHeader(pht('Run By Herald Rules'))
539
->addActionLink($more_link);
540
541
return id(new PHUIObjectBoxView())
542
->setHeader($header)
543
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
544
->appendChild($list);
545
}
546
547
private function newOptionsView(HarbormasterBuildPlan $plan) {
548
$viewer = $this->getViewer();
549
550
$can_edit = PhabricatorPolicyFilter::hasCapability(
551
$viewer,
552
$plan,
553
PhabricatorPolicyCapability::CAN_EDIT);
554
555
$behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
556
557
$rows = array();
558
foreach ($behaviors as $behavior) {
559
$option = $behavior->getPlanOption($plan);
560
561
$icon = $option->getIcon();
562
$icon = id(new PHUIIconView())->setIcon($icon);
563
564
$edit_uri = new PhutilURI(
565
$this->getApplicationURI(
566
urisprintf(
567
'plan/behavior/%d/%s/',
568
$plan->getID(),
569
$behavior->getKey())));
570
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);
579
580
$rows[] = array(
581
$icon,
582
$behavior->getName(),
583
$option->getName(),
584
$option->getDescription(),
585
$edit_button,
586
);
587
}
588
589
$table = id(new AphrontTableView($rows))
590
->setHeaders(
591
array(
592
null,
593
pht('Name'),
594
pht('Behavior'),
595
pht('Details'),
596
null,
597
))
598
->setColumnClasses(
599
array(
600
null,
601
'pri',
602
null,
603
'wide',
604
null,
605
));
606
607
608
$header = id(new PHUIHeaderView())
609
->setHeader(pht('Plan Behaviors'));
610
611
return id(new PHUIObjectBoxView())
612
->setHeader($header)
613
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
614
->setTable($table);
615
}
616
617
}
618
619