Path: blob/master/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
12256 views
<?php12final class HarbormasterPlanViewController extends HarbormasterPlanController {34public function shouldAllowPublic() {5return true;6}78public function handleRequest(AphrontRequest $request) {9$viewer = $this->getViewer();10$id = $request->getURIData('id');1112$plan = id(new HarbormasterBuildPlanQuery())13->setViewer($viewer)14->withIDs(array($id))15->executeOne();16if (!$plan) {17return new Aphront404Response();18}1920$title = $plan->getName();2122$header = id(new PHUIHeaderView())23->setHeader($plan->getName())24->setUser($viewer)25->setPolicyObject($plan)26->setHeaderIcon('fa-ship');2728$curtain = $this->buildCurtainView($plan);2930$crumbs = $this->buildApplicationCrumbs()31->addTextCrumb($plan->getObjectName())32->setBorder(true);3334list($step_list, $has_any_conflicts, $would_deadlock, $steps) =35$this->buildStepList($plan);3637$error = null;38if (!$steps) {39$error = pht(40'This build plan does not have any build steps yet, so it will '.41'not do anything when run.');42} else if ($would_deadlock) {43$error = pht(44'This build plan will deadlock when executed, due to circular '.45'dependencies present in the build plan. Examine the step list '.46'and resolve the deadlock.');47} else if ($has_any_conflicts) {48// A deadlocking build will also cause all the artifacts to be49// invalid, so we just skip showing this message if that's the50// case.51$error = pht(52'This build plan has conflicts in one or more build steps. '.53'Examine the step list and resolve the listed errors.');54}5556if ($error) {57$error = id(new PHUIInfoView())58->setSeverity(PHUIInfoView::SEVERITY_WARNING)59->appendChild($error);60}6162$builds_view = $this->newBuildsView($plan);63$options_view = $this->newOptionsView($plan);64$rules_view = $this->newRulesView($plan);6566$timeline = $this->buildTransactionTimeline(67$plan,68new HarbormasterBuildPlanTransactionQuery());69$timeline->setShouldTerminate(true);7071$view = id(new PHUITwoColumnView())72->setHeader($header)73->setCurtain($curtain)74->setMainColumn(75array(76$error,77$step_list,78$options_view,79$rules_view,80$builds_view,81$timeline,82));8384return $this->newPage()85->setTitle($title)86->setCrumbs($crumbs)87->setPageObjectPHIDs(array($plan->getPHID()))88->appendChild($view);89}9091private function buildStepList(HarbormasterBuildPlan $plan) {92$viewer = $this->getViewer();9394$run_order = HarbormasterBuildGraph::determineDependencyExecution($plan);9596$steps = id(new HarbormasterBuildStepQuery())97->setViewer($viewer)98->withBuildPlanPHIDs(array($plan->getPHID()))99->execute();100$steps = mpull($steps, null, 'getPHID');101102$can_edit = PhabricatorPolicyFilter::hasCapability(103$viewer,104$plan,105PhabricatorPolicyCapability::CAN_EDIT);106107$step_list = id(new PHUIObjectItemListView())108->setUser($viewer)109->setNoDataString(110pht('This build plan does not have any build steps yet.'));111112$i = 1;113$last_depth = 0;114$has_any_conflicts = false;115$is_deadlocking = false;116foreach ($run_order as $run_ref) {117$step = $steps[$run_ref['node']->getPHID()];118$depth = $run_ref['depth'] + 1;119if ($last_depth !== $depth) {120$last_depth = $depth;121$i = 1;122} else {123$i++;124}125126$step_id = $step->getID();127$view_uri = $this->getApplicationURI("step/view/{$step_id}/");128129$item = id(new PHUIObjectItemView())130->setObjectName(pht('Step %d.%d', $depth, $i))131->setHeader($step->getName())132->setHref($view_uri);133134$step_list->addItem($item);135136$implementation = null;137try {138$implementation = $step->getStepImplementation();139} catch (Exception $ex) {140// We can't initialize the implementation. This might be because141// it's been renamed or no longer exists.142$item143->setStatusIcon('fa-warning red')144->addAttribute(pht(145'This step has an invalid implementation (%s).',146$step->getClassName()));147continue;148}149150$item->addAttribute($implementation->getDescription());151$item->setHref($view_uri);152153$depends = $step->getStepImplementation()->getDependencies($step);154$inputs = $step->getStepImplementation()->getArtifactInputs();155$outputs = $step->getStepImplementation()->getArtifactOutputs();156157$has_conflicts = false;158if ($depends || $inputs || $outputs) {159$available_artifacts =160HarbormasterBuildStepImplementation::getAvailableArtifacts(161$plan,162$step,163null);164$available_artifacts = ipull($available_artifacts, 'type');165166list($depends_ui, $has_conflicts) = $this->buildDependsOnList(167$depends,168pht('Depends On'),169$steps);170171list($inputs_ui, $has_conflicts) = $this->buildArtifactList(172$inputs,173'in',174pht('Input Artifacts'),175$available_artifacts);176177list($outputs_ui) = $this->buildArtifactList(178$outputs,179'out',180pht('Output Artifacts'),181array());182183$item->appendChild(184phutil_tag(185'div',186array(187'class' => 'harbormaster-artifact-io',188),189array(190$depends_ui,191$inputs_ui,192$outputs_ui,193)));194}195196if ($has_conflicts) {197$has_any_conflicts = true;198$item->setStatusIcon('fa-warning red');199}200201if ($run_ref['cycle']) {202$is_deadlocking = true;203}204205if ($is_deadlocking) {206$item->setStatusIcon('fa-warning red');207}208}209210$step_list->setFlush(true);211212$plan_id = $plan->getID();213214$header = id(new PHUIHeaderView())215->setHeader(pht('Build Steps'))216->addActionLink(217id(new PHUIButtonView())218->setText(pht('Add Build Step'))219->setHref($this->getApplicationURI("step/add/{$plan_id}/"))220->setTag('a')221->setIcon('fa-plus')222->setDisabled(!$can_edit)223->setWorkflow(!$can_edit));224225$step_box = id(new PHUIObjectBoxView())226->setHeader($header)227->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)228->appendChild($step_list);229230return array($step_box, $has_any_conflicts, $is_deadlocking, $steps);231}232233private function buildCurtainView(HarbormasterBuildPlan $plan) {234$viewer = $this->getViewer();235$id = $plan->getID();236237$curtain = $this->newCurtainView($plan);238239$can_edit = PhabricatorPolicyFilter::hasCapability(240$viewer,241$plan,242PhabricatorPolicyCapability::CAN_EDIT);243244$curtain->addAction(245id(new PhabricatorActionView())246->setName(pht('Edit Plan'))247->setHref($this->getApplicationURI("plan/edit/{$id}/"))248->setWorkflow(!$can_edit)249->setDisabled(!$can_edit)250->setIcon('fa-pencil'));251252if ($plan->isDisabled()) {253$curtain->addAction(254id(new PhabricatorActionView())255->setName(pht('Enable Plan'))256->setHref($this->getApplicationURI("plan/disable/{$id}/"))257->setWorkflow(true)258->setDisabled(!$can_edit)259->setIcon('fa-check'));260} else {261$curtain->addAction(262id(new PhabricatorActionView())263->setName(pht('Disable Plan'))264->setHref($this->getApplicationURI("plan/disable/{$id}/"))265->setWorkflow(true)266->setDisabled(!$can_edit)267->setIcon('fa-ban'));268}269270$can_run = ($plan->hasRunCapability($viewer) && $plan->canRunManually());271272$curtain->addAction(273id(new PhabricatorActionView())274->setName(pht('Run Plan Manually'))275->setHref($this->getApplicationURI("plan/run/{$id}/"))276->setWorkflow(true)277->setDisabled(!$can_run)278->setIcon('fa-play-circle'));279280return $curtain;281}282283private function buildArtifactList(284array $artifacts,285$kind,286$name,287array $available_artifacts) {288$has_conflicts = false;289290if (!$artifacts) {291return array(null, $has_conflicts);292}293294$this->requireResource('harbormaster-css');295296$header = phutil_tag(297'div',298array(299'class' => 'harbormaster-artifact-summary-header',300),301$name);302303$is_input = ($kind == 'in');304305$list = new PHUIStatusListView();306foreach ($artifacts as $artifact) {307$error = null;308309$key = idx($artifact, 'key');310if (!strlen($key)) {311$bound = phutil_tag('em', array(), pht('(null)'));312if ($is_input) {313// This is an unbound input. For now, all inputs are always required.314$icon = PHUIStatusItemView::ICON_WARNING;315$color = 'red';316$icon_label = pht('Required Input');317$has_conflicts = true;318$error = pht('This input is required, but not configured.');319} else {320// This is an unnamed output. Outputs do not necessarily need to be321// named.322$icon = PHUIStatusItemView::ICON_OPEN;323$color = 'bluegrey';324$icon_label = pht('Unused Output');325}326} else {327$bound = phutil_tag('strong', array(), $key);328if ($is_input) {329if (isset($available_artifacts[$key])) {330if ($available_artifacts[$key] == idx($artifact, 'type')) {331$icon = PHUIStatusItemView::ICON_ACCEPT;332$color = 'green';333$icon_label = pht('Valid Input');334} else {335$icon = PHUIStatusItemView::ICON_WARNING;336$color = 'red';337$icon_label = pht('Bad Input Type');338$has_conflicts = true;339$error = pht(340'This input is bound to the wrong artifact type. It is bound '.341'to a "%s" artifact, but should be bound to a "%s" artifact.',342$available_artifacts[$key],343idx($artifact, 'type'));344}345} else {346$icon = PHUIStatusItemView::ICON_QUESTION;347$color = 'red';348$icon_label = pht('Unknown Input');349$has_conflicts = true;350$error = pht(351'This input is bound to an artifact ("%s") which does not exist '.352'at this stage in the build process.',353$key);354}355} else {356$icon = PHUIStatusItemView::ICON_DOWN;357$color = 'green';358$icon_label = pht('Valid Output');359}360}361362if ($error) {363$note = array(364phutil_tag('strong', array(), pht('ERROR:')),365' ',366$error,367);368} else {369$note = $bound;370}371372$list->addItem(373id(new PHUIStatusItemView())374->setIcon($icon, $color, $icon_label)375->setTarget($artifact['name'])376->setNote($note));377}378379$ui = array(380$header,381$list,382);383384return array($ui, $has_conflicts);385}386387private function buildDependsOnList(388array $step_phids,389$name,390array $steps) {391$has_conflicts = false;392393if (!$step_phids) {394return null;395}396397$this->requireResource('harbormaster-css');398399$steps = mpull($steps, null, 'getPHID');400401$header = phutil_tag(402'div',403array(404'class' => 'harbormaster-artifact-summary-header',405),406$name);407408$list = new PHUIStatusListView();409foreach ($step_phids as $step_phid) {410$error = null;411412if (idx($steps, $step_phid) === null) {413$icon = PHUIStatusItemView::ICON_WARNING;414$color = 'red';415$icon_label = pht('Missing Dependency');416$has_conflicts = true;417$error = pht(418"This dependency specifies a build step which doesn't exist.");419} else {420$bound = phutil_tag(421'strong',422array(),423idx($steps, $step_phid)->getName());424$icon = PHUIStatusItemView::ICON_ACCEPT;425$color = 'green';426$icon_label = pht('Valid Input');427}428429if ($error) {430$note = array(431phutil_tag('strong', array(), pht('ERROR:')),432' ',433$error,434);435} else {436$note = $bound;437}438439$list->addItem(440id(new PHUIStatusItemView())441->setIcon($icon, $color, $icon_label)442->setTarget(pht('Build Step'))443->setNote($note));444}445446$ui = array(447$header,448$list,449);450451return array($ui, $has_conflicts);452}453454private function newBuildsView(HarbormasterBuildPlan $plan) {455$viewer = $this->getViewer();456457$limit = 10;458$builds = id(new HarbormasterBuildQuery())459->setViewer($viewer)460->withBuildPlanPHIDs(array($plan->getPHID()))461->setLimit($limit + 1)462->execute();463464$more_results = (count($builds) > $limit);465$builds = array_slice($builds, 0, $limit);466467$list = id(new HarbormasterBuildView())468->setViewer($viewer)469->setBuilds($builds)470->newObjectList();471472$list->setNoDataString(pht('No recent builds.'));473474$more_href = new PhutilURI(475$this->getApplicationURI('/build/'),476array('plan' => $plan->getPHID()));477478if ($more_results) {479$list->newTailButton()480->setHref($more_href);481}482483$more_link = id(new PHUIButtonView())484->setTag('a')485->setIcon('fa-list-ul')486->setText(pht('View All Builds'))487->setHref($more_href);488489$header = id(new PHUIHeaderView())490->setHeader(pht('Recent Builds'))491->addActionLink($more_link);492493return id(new PHUIObjectBoxView())494->setHeader($header)495->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)496->appendChild($list);497}498499private function newRulesView(HarbormasterBuildPlan $plan) {500$viewer = $this->getViewer();501502$limit = 10;503$rules = id(new HeraldRuleQuery())504->setViewer($viewer)505->withDisabled(false)506->withAffectedObjectPHIDs(array($plan->getPHID()))507->needValidateAuthors(true)508->setLimit($limit + 1)509->execute();510511$more_results = (count($rules) > $limit);512$rules = array_slice($rules, 0, $limit);513514$list = id(new HeraldRuleListView())515->setViewer($viewer)516->setRules($rules)517->newObjectList();518519$list->setNoDataString(pht('No active Herald rules trigger this build.'));520521$more_href = new PhutilURI(522'/herald/',523array('affectedPHID' => $plan->getPHID()));524525if ($more_results) {526$list->newTailButton()527->setHref($more_href);528}529530$more_link = id(new PHUIButtonView())531->setTag('a')532->setIcon('fa-list-ul')533->setText(pht('View All Rules'))534->setHref($more_href);535536$header = id(new PHUIHeaderView())537->setHeader(pht('Run By Herald Rules'))538->addActionLink($more_link);539540return id(new PHUIObjectBoxView())541->setHeader($header)542->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)543->appendChild($list);544}545546private function newOptionsView(HarbormasterBuildPlan $plan) {547$viewer = $this->getViewer();548549$can_edit = PhabricatorPolicyFilter::hasCapability(550$viewer,551$plan,552PhabricatorPolicyCapability::CAN_EDIT);553554$behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();555556$rows = array();557foreach ($behaviors as $behavior) {558$option = $behavior->getPlanOption($plan);559560$icon = $option->getIcon();561$icon = id(new PHUIIconView())->setIcon($icon);562563$edit_uri = new PhutilURI(564$this->getApplicationURI(565urisprintf(566'plan/behavior/%d/%s/',567$plan->getID(),568$behavior->getKey())));569570$edit_button = id(new PHUIButtonView())571->setTag('a')572->setColor(PHUIButtonView::GREY)573->setSize(PHUIButtonView::SMALL)574->setDisabled(!$can_edit)575->setWorkflow(true)576->setText(pht('Edit'))577->setHref($edit_uri);578579$rows[] = array(580$icon,581$behavior->getName(),582$option->getName(),583$option->getDescription(),584$edit_button,585);586}587588$table = id(new AphrontTableView($rows))589->setHeaders(590array(591null,592pht('Name'),593pht('Behavior'),594pht('Details'),595null,596))597->setColumnClasses(598array(599null,600'pri',601null,602'wide',603null,604));605606607$header = id(new PHUIHeaderView())608->setHeader(pht('Plan Behaviors'));609610return id(new PHUIObjectBoxView())611->setHeader($header)612->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)613->setTable($table);614}615616}617618619