@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Render charts from storage instead of just one ad-hoc hard-coded chart

Summary:
Ref T13279. This changes the chart controller:

- if we have no arguments, build a demo chart and redirect to it;
- otherwise, load the specified chart from storage and render it.

This mostly prepares for "Chart" panels on dashboards.

Test Plan: Visited `/fact/chart/`, got redirected to a chart from storage.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: yelirekim

Maniphest Tasks: T13279

Differential Revision: https://secure.phabricator.com/D20483

+370 -159
+4
src/__phutil_library_map__.php
··· 2663 2663 'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', 2664 2664 'PhabricatorChartAxis' => 'applications/fact/chart/PhabricatorChartAxis.php', 2665 2665 'PhabricatorChartDataQuery' => 'applications/fact/chart/PhabricatorChartDataQuery.php', 2666 + 'PhabricatorChartDataset' => 'applications/fact/chart/PhabricatorChartDataset.php', 2667 + 'PhabricatorChartEngine' => 'applications/fact/engine/PhabricatorChartEngine.php', 2666 2668 'PhabricatorChartFunction' => 'applications/fact/chart/PhabricatorChartFunction.php', 2667 2669 'PhabricatorChartFunctionArgument' => 'applications/fact/chart/PhabricatorChartFunctionArgument.php', 2668 2670 'PhabricatorChartFunctionArgumentParser' => 'applications/fact/chart/PhabricatorChartFunctionArgumentParser.php', ··· 8669 8671 'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 8670 8672 'PhabricatorChartAxis' => 'Phobject', 8671 8673 'PhabricatorChartDataQuery' => 'Phobject', 8674 + 'PhabricatorChartDataset' => 'Phobject', 8675 + 'PhabricatorChartEngine' => 'Phobject', 8672 8676 'PhabricatorChartFunction' => 'Phobject', 8673 8677 'PhabricatorChartFunctionArgument' => 'Phobject', 8674 8678 'PhabricatorChartFunctionArgumentParser' => 'Phobject',
+3 -1
src/applications/fact/application/PhabricatorFactApplication.php
··· 30 30 return array( 31 31 '/fact/' => array( 32 32 '' => 'PhabricatorFactHomeController', 33 - '(?<mode>chart|draw)/' => 'PhabricatorFactChartController', 33 + 'chart/' => 'PhabricatorFactChartController', 34 + 'chart/(?P<chartKey>[^/]+)/(?:(?P<mode>draw)/)?' => 35 + 'PhabricatorFactChartController', 34 36 'object/(?<phid>[^/]+)/' => 'PhabricatorFactObjectController', 35 37 ), 36 38 );
+37
src/applications/fact/chart/PhabricatorChartDataset.php
··· 1 + <?php 2 + 3 + final class PhabricatorChartDataset 4 + extends Phobject { 5 + 6 + private $function; 7 + 8 + public function getFunction() { 9 + return $this->function; 10 + } 11 + 12 + public static function newFromDictionary(array $map) { 13 + PhutilTypeSpec::checkMap( 14 + $map, 15 + array( 16 + 'function' => 'list<wild>', 17 + )); 18 + 19 + $dataset = new self(); 20 + 21 + $dataset->function = id(new PhabricatorComposeChartFunction()) 22 + ->setArguments(array($map['function'])); 23 + 24 + return $dataset; 25 + } 26 + 27 + public function toDictionary() { 28 + // Since we wrap the raw value in a "compose(...)", when deserializing, 29 + // we need to unwrap it when serializing. 30 + $function_raw = head($this->getFunction()->toDictionary()); 31 + 32 + return array( 33 + 'function' => $function_raw, 34 + ); 35 + } 36 + 37 + }
+4
src/applications/fact/chart/PhabricatorChartFunction.php
··· 43 43 return $this; 44 44 } 45 45 46 + public function toDictionary() { 47 + return $this->getArgumentParser()->getRawArguments(); 48 + } 49 + 46 50 public function getSubfunctions() { 47 51 $result = array(); 48 52 $result[] = $this;
+4
src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php
··· 103 103 return array_values($this->argumentMap); 104 104 } 105 105 106 + public function getRawArguments() { 107 + return $this->rawArguments; 108 + } 109 + 106 110 public function parseArguments() { 107 111 $have_count = count($this->rawArguments); 108 112 $want_count = count($this->argumentMap);
+60 -154
src/applications/fact/controller/PhabricatorFactChartController.php
··· 5 5 public function handleRequest(AphrontRequest $request) { 6 6 $viewer = $request->getViewer(); 7 7 8 + $chart_key = $request->getURIData('chartKey'); 9 + if ($chart_key === null) { 10 + return $this->newDemoChart(); 11 + } 12 + 13 + $chart = id(new PhabricatorFactChart())->loadOneWhere( 14 + 'chartKey = %s', 15 + $chart_key); 16 + if (!$chart) { 17 + return new Aphront404Response(); 18 + } 19 + 20 + $engine = id(new PhabricatorChartEngine()) 21 + ->setViewer($viewer) 22 + ->setChart($chart); 23 + 8 24 // When drawing a chart, we send down a placeholder piece of HTML first, 9 25 // then fetch the data via async request. Determine if we're drawing 10 26 // the structure or actually pulling the data. 11 27 $mode = $request->getURIData('mode'); 12 - $is_chart_mode = ($mode === 'chart'); 13 28 $is_draw_mode = ($mode === 'draw'); 14 29 30 + // TODO: For now, always pull the data. We'll throw it away if we're just 31 + // drawing the frame, but this makes errors easier to debug. 32 + $chart_data = $engine->newChartData(); 33 + 34 + if ($is_draw_mode) { 35 + return id(new AphrontAjaxResponse())->setContent($chart_data); 36 + } 37 + 38 + $chart_view = $engine->newChartView(); 39 + return $this->newChartResponse($chart_view); 40 + } 41 + 42 + private function newChartResponse($chart_view) { 43 + $box = id(new PHUIObjectBoxView()) 44 + ->setHeaderText(pht('Chart')) 45 + ->appendChild($chart_view); 46 + 47 + $crumbs = $this->buildApplicationCrumbs() 48 + ->addTextCrumb(pht('Chart')) 49 + ->setBorder(true); 50 + 51 + $title = pht('Chart'); 52 + 53 + return $this->newPage() 54 + ->setTitle($title) 55 + ->setCrumbs($crumbs) 56 + ->appendChild($box); 57 + } 58 + 59 + private function newDemoChart() { 60 + $viewer = $this->getViewer(); 61 + 15 62 $argvs = array(); 16 63 17 64 $argvs[] = array('fact', 'tasks.count.create'); ··· 40 87 array('shift', 800), 41 88 ); 42 89 43 - $functions = array(); 44 - foreach ($argvs as $argv) { 45 - $functions[] = id(new PhabricatorComposeChartFunction()) 46 - ->setArguments(array($argv)); 47 - } 48 - 49 - $subfunctions = array(); 50 - foreach ($functions as $function) { 51 - foreach ($function->getSubfunctions() as $subfunction) { 52 - $subfunctions[] = $subfunction; 53 - } 54 - } 55 - 56 - foreach ($subfunctions as $subfunction) { 57 - $subfunction->loadData(); 58 - } 59 - 60 - list($domain_min, $domain_max) = $this->getDomain($functions); 61 - 62 - $axis = id(new PhabricatorChartAxis()) 63 - ->setMinimumValue($domain_min) 64 - ->setMaximumValue($domain_max); 65 - 66 - $data_query = id(new PhabricatorChartDataQuery()) 67 - ->setMinimumValue($domain_min) 68 - ->setMaximumValue($domain_max) 69 - ->setLimit(2000); 70 - 71 90 $datasets = array(); 72 - foreach ($functions as $function) { 73 - $points = $function->newDatapoints($data_query); 74 - 75 - $x = array(); 76 - $y = array(); 77 - 78 - foreach ($points as $point) { 79 - $x[] = $point['x']; 80 - $y[] = $point['y']; 81 - } 82 - 83 - $datasets[] = array( 84 - 'x' => $x, 85 - 'y' => $y, 86 - 'color' => '#ff00ff', 87 - ); 88 - } 89 - 90 - 91 - $y_min = 0; 92 - $y_max = 0; 93 - foreach ($datasets as $dataset) { 94 - if (!$dataset['y']) { 95 - continue; 96 - } 97 - 98 - $y_min = min($y_min, min($dataset['y'])); 99 - $y_max = max($y_max, max($dataset['y'])); 100 - } 101 - 102 - $chart_data = array( 103 - 'datasets' => $datasets, 104 - 'xMin' => $domain_min, 105 - 'xMax' => $domain_max, 106 - 'yMin' => $y_min, 107 - 'yMax' => $y_max, 108 - ); 109 - 110 - // TODO: Move this back up, it's just down here for now to make 111 - // debugging easier so the main page throws a more visible exception when 112 - // something goes wrong. 113 - if ($is_chart_mode) { 114 - return $this->newChartResponse(); 91 + foreach ($argvs as $argv) { 92 + $datasets[] = PhabricatorChartDataset::newFromDictionary( 93 + array( 94 + 'function' => $argv, 95 + )); 115 96 } 116 97 117 - return id(new AphrontAjaxResponse())->setContent($chart_data); 118 - } 119 - 120 - private function newChartResponse() { 121 - $request = $this->getRequest(); 122 - $chart_node_id = celerity_generate_unique_node_id(); 123 - 124 - $chart_view = phutil_tag( 125 - 'div', 126 - array( 127 - 'id' => $chart_node_id, 128 - 'style' => 'background: #ffffff; '. 129 - 'height: 480px; ', 130 - ), 131 - ''); 98 + $chart = id(new PhabricatorFactChart()) 99 + ->setDatasets($datasets); 132 100 133 - $data_uri = $request->getRequestURI(); 134 - $data_uri->setPath('/fact/draw/'); 101 + $engine = id(new PhabricatorChartEngine()) 102 + ->setViewer($viewer) 103 + ->setChart($chart); 135 104 136 - Javelin::initBehavior( 137 - 'line-chart', 138 - array( 139 - 'chartNodeID' => $chart_node_id, 140 - 'dataURI' => (string)$data_uri, 141 - )); 105 + $chart = $engine->getStoredChart(); 142 106 143 - $box = id(new PHUIObjectBoxView()) 144 - ->setHeaderText(pht('Chart')) 145 - ->appendChild($chart_view); 146 - 147 - $crumbs = $this->buildApplicationCrumbs() 148 - ->addTextCrumb(pht('Chart')) 149 - ->setBorder(true); 150 - 151 - $title = pht('Chart'); 152 - 153 - return $this->newPage() 154 - ->setTitle($title) 155 - ->setCrumbs($crumbs) 156 - ->appendChild($box); 157 - 107 + return id(new AphrontRedirectResponse())->setURI($chart->getURI()); 158 108 } 159 - 160 - private function getDomain(array $functions) { 161 - $domain_min_list = null; 162 - $domain_max_list = null; 163 - 164 - foreach ($functions as $function) { 165 - $domain = $function->getDomain(); 166 - 167 - list($function_min, $function_max) = $domain; 168 - 169 - if ($function_min !== null) { 170 - $domain_min_list[] = $function_min; 171 - } 172 - 173 - if ($function_max !== null) { 174 - $domain_max_list[] = $function_max; 175 - } 176 - } 177 - 178 - $domain_min = null; 179 - $domain_max = null; 180 - 181 - if ($domain_min_list) { 182 - $domain_min = min($domain_min_list); 183 - } 184 - 185 - if ($domain_max_list) { 186 - $domain_max = max($domain_max_list); 187 - } 188 - 189 - // If we don't have any domain data from the actual functions, pick a 190 - // plausible domain automatically. 191 - 192 - if ($domain_max === null) { 193 - $domain_max = PhabricatorTime::getNow(); 194 - } 195 - 196 - if ($domain_min === null) { 197 - $domain_min = $domain_max - phutil_units('365 days in seconds'); 198 - } 199 - 200 - return array($domain_min, $domain_max); 201 - } 202 - 203 109 204 110 }
+214
src/applications/fact/engine/PhabricatorChartEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorChartEngine 4 + extends Phobject { 5 + 6 + private $viewer; 7 + private $chart; 8 + private $storedChart; 9 + 10 + public function setViewer(PhabricatorUser $viewer) { 11 + $this->viewer = $viewer; 12 + return $this; 13 + } 14 + 15 + public function getViewer() { 16 + return $this->viewer; 17 + } 18 + 19 + public function setChart(PhabricatorFactChart $chart) { 20 + $this->chart = $chart; 21 + return $this; 22 + } 23 + 24 + public function getChart() { 25 + return $this->chart; 26 + } 27 + 28 + public function getStoredChart() { 29 + if (!$this->storedChart) { 30 + $chart = $this->getChart(); 31 + $chart_key = $chart->getChartKey(); 32 + if (!$chart_key) { 33 + $chart_key = $chart->newChartKey(); 34 + 35 + $stored_chart = id(new PhabricatorFactChart())->loadOneWhere( 36 + 'chartKey = %s', 37 + $chart_key); 38 + if ($stored_chart) { 39 + $chart = $stored_chart; 40 + } else { 41 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 42 + 43 + try { 44 + $chart->save(); 45 + } catch (AphrontDuplicateKeyQueryException $ex) { 46 + $chart = id(new PhabricatorFactChart())->loadOneWhere( 47 + 'chartKey = %s', 48 + $chart_key); 49 + if (!$chart) { 50 + throw new Exception( 51 + pht( 52 + 'Failed to load chart with key "%s" after key collision. '. 53 + 'This should not be possible.', 54 + $chart_key)); 55 + } 56 + } 57 + 58 + unset($unguarded); 59 + } 60 + $this->setChart($chart); 61 + } 62 + 63 + $this->storedChart = $chart; 64 + } 65 + 66 + return $this->storedChart; 67 + } 68 + 69 + public function newChartView() { 70 + $chart = $this->getStoredChart(); 71 + $chart_key = $chart->getChartKey(); 72 + 73 + $chart_node_id = celerity_generate_unique_node_id(); 74 + 75 + $chart_view = phutil_tag( 76 + 'div', 77 + array( 78 + 'id' => $chart_node_id, 79 + 'style' => 'background: #ffffff; '. 80 + 'height: 480px; ', 81 + ), 82 + ''); 83 + 84 + $data_uri = urisprintf('/fact/chart/%s/draw/', $chart_key); 85 + 86 + Javelin::initBehavior( 87 + 'line-chart', 88 + array( 89 + 'chartNodeID' => $chart_node_id, 90 + 'dataURI' => (string)$data_uri, 91 + )); 92 + 93 + return $chart_view; 94 + } 95 + 96 + public function newChartData() { 97 + $chart = $this->getStoredChart(); 98 + $chart_key = $chart->getChartKey(); 99 + 100 + $datasets = $chart->getDatasets(); 101 + 102 + $functions = array(); 103 + foreach ($datasets as $dataset) { 104 + $functions[] = $dataset->getFunction(); 105 + } 106 + 107 + $subfunctions = array(); 108 + foreach ($functions as $function) { 109 + foreach ($function->getSubfunctions() as $subfunction) { 110 + $subfunctions[] = $subfunction; 111 + } 112 + } 113 + 114 + foreach ($subfunctions as $subfunction) { 115 + $subfunction->loadData(); 116 + } 117 + 118 + list($domain_min, $domain_max) = $this->getDomain($functions); 119 + 120 + $axis = id(new PhabricatorChartAxis()) 121 + ->setMinimumValue($domain_min) 122 + ->setMaximumValue($domain_max); 123 + 124 + $data_query = id(new PhabricatorChartDataQuery()) 125 + ->setMinimumValue($domain_min) 126 + ->setMaximumValue($domain_max) 127 + ->setLimit(2000); 128 + 129 + $datasets = array(); 130 + foreach ($functions as $function) { 131 + $points = $function->newDatapoints($data_query); 132 + 133 + $x = array(); 134 + $y = array(); 135 + 136 + foreach ($points as $point) { 137 + $x[] = $point['x']; 138 + $y[] = $point['y']; 139 + } 140 + 141 + $datasets[] = array( 142 + 'x' => $x, 143 + 'y' => $y, 144 + 'color' => '#ff00ff', 145 + ); 146 + } 147 + 148 + 149 + $y_min = 0; 150 + $y_max = 0; 151 + foreach ($datasets as $dataset) { 152 + if (!$dataset['y']) { 153 + continue; 154 + } 155 + 156 + $y_min = min($y_min, min($dataset['y'])); 157 + $y_max = max($y_max, max($dataset['y'])); 158 + } 159 + 160 + $chart_data = array( 161 + 'datasets' => $datasets, 162 + 'xMin' => $domain_min, 163 + 'xMax' => $domain_max, 164 + 'yMin' => $y_min, 165 + 'yMax' => $y_max, 166 + ); 167 + 168 + return $chart_data; 169 + } 170 + 171 + private function getDomain(array $functions) { 172 + $domain_min_list = null; 173 + $domain_max_list = null; 174 + 175 + foreach ($functions as $function) { 176 + $domain = $function->getDomain(); 177 + 178 + list($function_min, $function_max) = $domain; 179 + 180 + if ($function_min !== null) { 181 + $domain_min_list[] = $function_min; 182 + } 183 + 184 + if ($function_max !== null) { 185 + $domain_max_list[] = $function_max; 186 + } 187 + } 188 + 189 + $domain_min = null; 190 + $domain_max = null; 191 + 192 + if ($domain_min_list) { 193 + $domain_min = min($domain_min_list); 194 + } 195 + 196 + if ($domain_max_list) { 197 + $domain_max = max($domain_max_list); 198 + } 199 + 200 + // If we don't have any domain data from the actual functions, pick a 201 + // plausible domain automatically. 202 + 203 + if ($domain_max === null) { 204 + $domain_max = PhabricatorTime::getNow(); 205 + } 206 + 207 + if ($domain_min === null) { 208 + $domain_min = $domain_max - phutil_units('365 days in seconds'); 209 + } 210 + 211 + return array($domain_min, $domain_max); 212 + } 213 + 214 + }
+44 -4
src/applications/fact/storage/PhabricatorFactChart.php
··· 7 7 protected $chartKey; 8 8 protected $chartParameters = array(); 9 9 10 + private $datasets; 11 + 10 12 protected function getConfiguration() { 11 13 return array( 12 14 self::CONFIG_SERIALIZATION => array( ··· 33 35 return idx($this->chartParameters, $key, $default); 34 36 } 35 37 38 + public function newChartKey() { 39 + $digest = serialize($this->chartParameters); 40 + $digest = PhabricatorHash::digestForIndex($digest); 41 + return $digest; 42 + } 43 + 36 44 public function save() { 37 45 if ($this->getID()) { 38 46 throw new Exception( ··· 41 49 'overwrite an existing chart configuration.')); 42 50 } 43 51 44 - $digest = serialize($this->chartParameters); 45 - $digest = PhabricatorHash::digestForIndex($digest); 46 - 47 - $this->chartKey = $digest; 52 + $this->chartKey = $this->newChartKey(); 48 53 49 54 return parent::save(); 55 + } 56 + 57 + public function setDatasets(array $datasets) { 58 + assert_instances_of($datasets, 'PhabricatorChartDataset'); 59 + 60 + $dataset_list = array(); 61 + foreach ($datasets as $dataset) { 62 + $dataset_list[] = $dataset->toDictionary(); 63 + } 64 + 65 + $this->setChartParameter('datasets', $dataset_list); 66 + $this->datasets = null; 67 + 68 + return $this; 69 + } 70 + 71 + public function getDatasets() { 72 + if ($this->datasets === null) { 73 + $this->datasets = $this->newDatasets(); 74 + } 75 + return $this->datasets; 76 + } 77 + 78 + private function newDatasets() { 79 + $datasets = $this->getChartParameter('datasets', array()); 80 + 81 + foreach ($datasets as $key => $dataset) { 82 + $datasets[$key] = PhabricatorChartDataset::newFromDictionary($dataset); 83 + } 84 + 85 + return $datasets; 86 + } 87 + 88 + public function getURI() { 89 + return urisprintf('/fact/chart/%s/', $this->getChartKey()); 50 90 } 51 91 52 92 /* -( PhabricatorPolicyInterface )----------------------------------------- */