PageRenderTime 53ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/src/applications/harbormaster/controller/HarbormasterPlanViewController.php

https://gitlab.com/jforge/phabricator
PHP | 481 lines | 401 code | 72 blank | 8 comment | 35 complexity | 25de39d1d525abe93e7e4c9c69ef9fd7 MD5 | raw file
Possible License(s): LGPL-3.0, MIT, MPL-2.0-no-copyleft-exception, BSD-3-Clause, Apache-2.0, LGPL-2.0, LGPL-2.1
  1. <?php
  2. final class HarbormasterPlanViewController extends HarbormasterPlanController {
  3. private $id;
  4. public function willProcessRequest(array $data) {
  5. $this->id = $data['id'];
  6. }
  7. public function processRequest() {
  8. $request = $this->getRequest();
  9. $viewer = $request->getUser();
  10. $id = $this->id;
  11. $plan = id(new HarbormasterBuildPlanQuery())
  12. ->setViewer($viewer)
  13. ->withIDs(array($id))
  14. ->executeOne();
  15. if (!$plan) {
  16. return new Aphront404Response();
  17. }
  18. $xactions = id(new HarbormasterBuildPlanTransactionQuery())
  19. ->setViewer($viewer)
  20. ->withObjectPHIDs(array($plan->getPHID()))
  21. ->execute();
  22. $xaction_view = id(new PhabricatorApplicationTransactionView())
  23. ->setUser($viewer)
  24. ->setObjectPHID($plan->getPHID())
  25. ->setTransactions($xactions)
  26. ->setShouldTerminate(true);
  27. $title = pht('Plan %d', $id);
  28. $header = id(new PHUIHeaderView())
  29. ->setHeader($title)
  30. ->setUser($viewer)
  31. ->setPolicyObject($plan);
  32. $box = id(new PHUIObjectBoxView())
  33. ->setHeader($header);
  34. $actions = $this->buildActionList($plan);
  35. $this->buildPropertyLists($box, $plan, $actions);
  36. $crumbs = $this->buildApplicationCrumbs();
  37. $crumbs->addTextCrumb(pht('Plan %d', $id));
  38. list($step_list, $has_any_conflicts, $would_deadlock) =
  39. $this->buildStepList($plan);
  40. if ($would_deadlock) {
  41. $box->setFormErrors(
  42. array(
  43. pht(
  44. 'This build plan will deadlock when executed, due to '.
  45. 'circular dependencies present in the build plan. '.
  46. 'Examine the step list and resolve the deadlock.'),
  47. ));
  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. $box->setFormErrors(
  53. array(
  54. pht(
  55. 'This build plan has conflicts in one or more build steps. '.
  56. 'Examine the step list and resolve the listed errors.'),
  57. ));
  58. }
  59. return $this->buildApplicationPage(
  60. array(
  61. $crumbs,
  62. $box,
  63. $step_list,
  64. $xaction_view,
  65. ),
  66. array(
  67. 'title' => $title,
  68. ));
  69. }
  70. private function buildStepList(HarbormasterBuildPlan $plan) {
  71. $request = $this->getRequest();
  72. $viewer = $request->getUser();
  73. $run_order =
  74. HarbormasterBuildGraph::determineDependencyExecution($plan);
  75. $steps = id(new HarbormasterBuildStepQuery())
  76. ->setViewer($viewer)
  77. ->withBuildPlanPHIDs(array($plan->getPHID()))
  78. ->execute();
  79. $steps = mpull($steps, null, 'getPHID');
  80. $can_edit = $this->hasApplicationCapability(
  81. HarbormasterManagePlansCapability::CAPABILITY);
  82. $step_list = id(new PHUIObjectItemListView())
  83. ->setUser($viewer)
  84. ->setNoDataString(
  85. pht('This build plan does not have any build steps yet.'));
  86. $i = 1;
  87. $last_depth = 0;
  88. $has_any_conflicts = false;
  89. $is_deadlocking = false;
  90. foreach ($run_order as $run_ref) {
  91. $step = $steps[$run_ref['node']->getPHID()];
  92. $depth = $run_ref['depth'] + 1;
  93. if ($last_depth !== $depth) {
  94. $last_depth = $depth;
  95. $i = 1;
  96. } else {
  97. $i++;
  98. }
  99. $implementation = null;
  100. try {
  101. $implementation = $step->getStepImplementation();
  102. } catch (Exception $ex) {
  103. // We can't initialize the implementation. This might be because
  104. // it's been renamed or no longer exists.
  105. $item = id(new PHUIObjectItemView())
  106. ->setObjectName(pht('Step %d.%d', $depth, $i))
  107. ->setHeader(pht('Unknown Implementation'))
  108. ->setBarColor('red')
  109. ->addAttribute(pht(
  110. 'This step has an invalid implementation (%s).',
  111. $step->getClassName()))
  112. ->addAction(
  113. id(new PHUIListItemView())
  114. ->setIcon('fa-times')
  115. ->addSigil('harbormaster-build-step-delete')
  116. ->setWorkflow(true)
  117. ->setRenderNameAsTooltip(true)
  118. ->setName(pht('Delete'))
  119. ->setHref(
  120. $this->getApplicationURI('step/delete/'.$step->getID().'/')));
  121. $step_list->addItem($item);
  122. continue;
  123. }
  124. $item = id(new PHUIObjectItemView())
  125. ->setObjectName(pht('Step %d.%d', $depth, $i))
  126. ->setHeader($step->getName());
  127. $item->addAttribute($implementation->getDescription());
  128. $step_id = $step->getID();
  129. $edit_uri = $this->getApplicationURI("step/edit/{$step_id}/");
  130. $delete_uri = $this->getApplicationURI("step/delete/{$step_id}/");
  131. if ($can_edit) {
  132. $item->setHref($edit_uri);
  133. }
  134. $item
  135. ->setHref($edit_uri)
  136. ->addAction(
  137. id(new PHUIListItemView())
  138. ->setIcon('fa-times')
  139. ->addSigil('harbormaster-build-step-delete')
  140. ->setWorkflow(true)
  141. ->setDisabled(!$can_edit)
  142. ->setHref(
  143. $this->getApplicationURI('step/delete/'.$step->getID().'/')));
  144. $depends = $step->getStepImplementation()->getDependencies($step);
  145. $inputs = $step->getStepImplementation()->getArtifactInputs();
  146. $outputs = $step->getStepImplementation()->getArtifactOutputs();
  147. $has_conflicts = false;
  148. if ($depends || $inputs || $outputs) {
  149. $available_artifacts =
  150. HarbormasterBuildStepImplementation::getAvailableArtifacts(
  151. $plan,
  152. $step,
  153. null);
  154. $available_artifacts = ipull($available_artifacts, 'type');
  155. list($depends_ui, $has_conflicts) = $this->buildDependsOnList(
  156. $depends,
  157. pht('Depends On'),
  158. $steps);
  159. list($inputs_ui, $has_conflicts) = $this->buildArtifactList(
  160. $inputs,
  161. 'in',
  162. pht('Input Artifacts'),
  163. $available_artifacts);
  164. list($outputs_ui) = $this->buildArtifactList(
  165. $outputs,
  166. 'out',
  167. pht('Output Artifacts'),
  168. array());
  169. $item->appendChild(
  170. phutil_tag(
  171. 'div',
  172. array(
  173. 'class' => 'harbormaster-artifact-io',
  174. ),
  175. array(
  176. $depends_ui,
  177. $inputs_ui,
  178. $outputs_ui,
  179. )));
  180. }
  181. if ($has_conflicts) {
  182. $has_any_conflicts = true;
  183. $item->setBarColor('red');
  184. }
  185. if ($run_ref['cycle']) {
  186. $is_deadlocking = true;
  187. }
  188. if ($is_deadlocking) {
  189. $item->setBarColor('red');
  190. }
  191. $step_list->addItem($item);
  192. }
  193. return array($step_list, $has_any_conflicts, $is_deadlocking);
  194. }
  195. private function buildActionList(HarbormasterBuildPlan $plan) {
  196. $request = $this->getRequest();
  197. $viewer = $request->getUser();
  198. $id = $plan->getID();
  199. $list = id(new PhabricatorActionListView())
  200. ->setUser($viewer)
  201. ->setObject($plan)
  202. ->setObjectURI($this->getApplicationURI("plan/{$id}/"));
  203. $can_edit = $this->hasApplicationCapability(
  204. HarbormasterManagePlansCapability::CAPABILITY);
  205. $list->addAction(
  206. id(new PhabricatorActionView())
  207. ->setName(pht('Edit Plan'))
  208. ->setHref($this->getApplicationURI("plan/edit/{$id}/"))
  209. ->setWorkflow(!$can_edit)
  210. ->setDisabled(!$can_edit)
  211. ->setIcon('fa-pencil'));
  212. if ($plan->isDisabled()) {
  213. $list->addAction(
  214. id(new PhabricatorActionView())
  215. ->setName(pht('Enable Plan'))
  216. ->setHref($this->getApplicationURI("plan/disable/{$id}/"))
  217. ->setWorkflow(true)
  218. ->setDisabled(!$can_edit)
  219. ->setIcon('fa-check'));
  220. } else {
  221. $list->addAction(
  222. id(new PhabricatorActionView())
  223. ->setName(pht('Disable Plan'))
  224. ->setHref($this->getApplicationURI("plan/disable/{$id}/"))
  225. ->setWorkflow(true)
  226. ->setDisabled(!$can_edit)
  227. ->setIcon('fa-ban'));
  228. }
  229. $list->addAction(
  230. id(new PhabricatorActionView())
  231. ->setName(pht('Add Build Step'))
  232. ->setHref($this->getApplicationURI("step/add/{$id}/"))
  233. ->setWorkflow(true)
  234. ->setDisabled(!$can_edit)
  235. ->setIcon('fa-plus'));
  236. $list->addAction(
  237. id(new PhabricatorActionView())
  238. ->setName(pht('Run Plan Manually'))
  239. ->setHref($this->getApplicationURI("plan/run/{$id}/"))
  240. ->setWorkflow(true)
  241. ->setDisabled(!$can_edit)
  242. ->setIcon('fa-play-circle'));
  243. return $list;
  244. }
  245. private function buildPropertyLists(
  246. PHUIObjectBoxView $box,
  247. HarbormasterBuildPlan $plan,
  248. PhabricatorActionListView $actions) {
  249. $request = $this->getRequest();
  250. $viewer = $request->getUser();
  251. $properties = id(new PHUIPropertyListView())
  252. ->setUser($viewer)
  253. ->setObject($plan)
  254. ->setActionList($actions);
  255. $box->addPropertyList($properties);
  256. $properties->addProperty(
  257. pht('Created'),
  258. phabricator_datetime($plan->getDateCreated(), $viewer));
  259. }
  260. private function buildArtifactList(
  261. array $artifacts,
  262. $kind,
  263. $name,
  264. array $available_artifacts) {
  265. $has_conflicts = false;
  266. if (!$artifacts) {
  267. return array(null, $has_conflicts);
  268. }
  269. $this->requireResource('harbormaster-css');
  270. $header = phutil_tag(
  271. 'div',
  272. array(
  273. 'class' => 'harbormaster-artifact-summary-header',
  274. ),
  275. $name);
  276. $is_input = ($kind == 'in');
  277. $list = new PHUIStatusListView();
  278. foreach ($artifacts as $artifact) {
  279. $error = null;
  280. $key = idx($artifact, 'key');
  281. if (!strlen($key)) {
  282. $bound = phutil_tag('em', array(), pht('(null)'));
  283. if ($is_input) {
  284. // This is an unbound input. For now, all inputs are always required.
  285. $icon = PHUIStatusItemView::ICON_WARNING;
  286. $color = 'red';
  287. $icon_label = pht('Required Input');
  288. $has_conflicts = true;
  289. $error = pht('This input is required, but not configured.');
  290. } else {
  291. // This is an unnamed output. Outputs do not necessarily need to be
  292. // named.
  293. $icon = PHUIStatusItemView::ICON_OPEN;
  294. $color = 'bluegrey';
  295. $icon_label = pht('Unused Output');
  296. }
  297. } else {
  298. $bound = phutil_tag('strong', array(), $key);
  299. if ($is_input) {
  300. if (isset($available_artifacts[$key])) {
  301. if ($available_artifacts[$key] == idx($artifact, 'type')) {
  302. $icon = PHUIStatusItemView::ICON_ACCEPT;
  303. $color = 'green';
  304. $icon_label = pht('Valid Input');
  305. } else {
  306. $icon = PHUIStatusItemView::ICON_WARNING;
  307. $color = 'red';
  308. $icon_label = pht('Bad Input Type');
  309. $has_conflicts = true;
  310. $error = pht(
  311. 'This input is bound to the wrong artifact type. It is bound '.
  312. 'to a "%s" artifact, but should be bound to a "%s" artifact.',
  313. $available_artifacts[$key],
  314. idx($artifact, 'type'));
  315. }
  316. } else {
  317. $icon = PHUIStatusItemView::ICON_QUESTION;
  318. $color = 'red';
  319. $icon_label = pht('Unknown Input');
  320. $has_conflicts = true;
  321. $error = pht(
  322. 'This input is bound to an artifact ("%s") which does not exist '.
  323. 'at this stage in the build process.',
  324. $key);
  325. }
  326. } else {
  327. $icon = PHUIStatusItemView::ICON_DOWN;
  328. $color = 'green';
  329. $icon_label = pht('Valid Output');
  330. }
  331. }
  332. if ($error) {
  333. $note = array(
  334. phutil_tag('strong', array(), pht('ERROR:')),
  335. ' ',
  336. $error,
  337. );
  338. } else {
  339. $note = $bound;
  340. }
  341. $list->addItem(
  342. id(new PHUIStatusItemView())
  343. ->setIcon($icon, $color, $icon_label)
  344. ->setTarget($artifact['name'])
  345. ->setNote($note));
  346. }
  347. $ui = array(
  348. $header,
  349. $list,
  350. );
  351. return array($ui, $has_conflicts);
  352. }
  353. private function buildDependsOnList(
  354. array $step_phids,
  355. $name,
  356. array $steps) {
  357. $has_conflicts = false;
  358. if (count($step_phids) === 0) {
  359. return null;
  360. }
  361. $this->requireResource('harbormaster-css');
  362. $steps = mpull($steps, null, 'getPHID');
  363. $header = phutil_tag(
  364. 'div',
  365. array(
  366. 'class' => 'harbormaster-artifact-summary-header',
  367. ),
  368. $name);
  369. $list = new PHUIStatusListView();
  370. foreach ($step_phids as $step_phid) {
  371. $error = null;
  372. if (idx($steps, $step_phid) === null) {
  373. $icon = PHUIStatusItemView::ICON_WARNING;
  374. $color = 'red';
  375. $icon_label = pht('Missing Dependency');
  376. $has_conflicts = true;
  377. $error = pht(
  378. 'This dependency specifies a build step which doesn\'t exist.');
  379. } else {
  380. $bound = phutil_tag(
  381. 'strong',
  382. array(),
  383. idx($steps, $step_phid)->getName());
  384. $icon = PHUIStatusItemView::ICON_ACCEPT;
  385. $color = 'green';
  386. $icon_label = pht('Valid Input');
  387. }
  388. if ($error) {
  389. $note = array(
  390. phutil_tag('strong', array(), pht('ERROR:')),
  391. ' ',
  392. $error,
  393. );
  394. } else {
  395. $note = $bound;
  396. }
  397. $list->addItem(
  398. id(new PHUIStatusItemView())
  399. ->setIcon($icon, $color, $icon_label)
  400. ->setTarget(pht('Build Step'))
  401. ->setNote($note));
  402. }
  403. $ui = array(
  404. $header,
  405. $list,
  406. );
  407. return array($ui, $has_conflicts);
  408. }
  409. }