Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php
12242 views
1
<?php
2
3
final class PhabricatorDashboardPanelRenderingEngine extends Phobject {
4
5
const HEADER_MODE_NORMAL = 'normal';
6
const HEADER_MODE_NONE = 'none';
7
const HEADER_MODE_EDIT = 'edit';
8
9
private $panel;
10
private $panelPHID;
11
private $viewer;
12
private $enableAsyncRendering;
13
private $parentPanelPHIDs;
14
private $headerMode = self::HEADER_MODE_NORMAL;
15
private $movable;
16
private $panelHandle;
17
private $editMode;
18
private $contextObject;
19
private $panelKey;
20
21
public function setContextObject($object) {
22
$this->contextObject = $object;
23
return $this;
24
}
25
26
public function getContextObject() {
27
return $this->contextObject;
28
}
29
30
public function setPanelKey($panel_key) {
31
$this->panelKey = $panel_key;
32
return $this;
33
}
34
35
public function getPanelKey() {
36
return $this->panelKey;
37
}
38
39
public function setHeaderMode($header_mode) {
40
$this->headerMode = $header_mode;
41
return $this;
42
}
43
44
public function getHeaderMode() {
45
return $this->headerMode;
46
}
47
48
public function setPanelHandle(PhabricatorObjectHandle $panel_handle) {
49
$this->panelHandle = $panel_handle;
50
return $this;
51
}
52
53
public function getPanelHandle() {
54
return $this->panelHandle;
55
}
56
57
public function isEditMode() {
58
return $this->editMode;
59
}
60
61
public function setEditMode($mode) {
62
$this->editMode = $mode;
63
return $this;
64
}
65
66
/**
67
* Allow the engine to render the panel via Ajax.
68
*/
69
public function setEnableAsyncRendering($enable) {
70
$this->enableAsyncRendering = $enable;
71
return $this;
72
}
73
74
public function setParentPanelPHIDs(array $parents) {
75
$this->parentPanelPHIDs = $parents;
76
return $this;
77
}
78
79
public function getParentPanelPHIDs() {
80
return $this->parentPanelPHIDs;
81
}
82
83
public function setViewer(PhabricatorUser $viewer) {
84
$this->viewer = $viewer;
85
return $this;
86
}
87
88
public function getViewer() {
89
return $this->viewer;
90
}
91
92
public function setPanel(PhabricatorDashboardPanel $panel) {
93
$this->panel = $panel;
94
return $this;
95
}
96
97
public function setMovable($movable) {
98
$this->movable = $movable;
99
return $this;
100
}
101
102
public function getMovable() {
103
return $this->movable;
104
}
105
106
public function getPanel() {
107
return $this->panel;
108
}
109
110
public function setPanelPHID($panel_phid) {
111
$this->panelPHID = $panel_phid;
112
return $this;
113
}
114
115
public function getPanelPHID() {
116
return $this->panelPHID;
117
}
118
119
public function renderPanel() {
120
$panel = $this->getPanel();
121
122
if (!$panel) {
123
$handle = $this->getPanelHandle();
124
if ($handle->getPolicyFiltered()) {
125
return $this->renderErrorPanel(
126
pht('Restricted Panel'),
127
pht(
128
'You do not have permission to see this panel.'));
129
} else {
130
return $this->renderErrorPanel(
131
pht('Invalid Panel'),
132
pht(
133
'This panel is invalid or does not exist. It may have been '.
134
'deleted.'));
135
}
136
}
137
138
$panel_type = $panel->getImplementation();
139
if (!$panel_type) {
140
return $this->renderErrorPanel(
141
$panel->getName(),
142
pht(
143
'This panel has type "%s", but that panel type is unknown.',
144
$panel->getPanelType()));
145
}
146
147
try {
148
$this->detectRenderingCycle($panel);
149
150
if ($this->enableAsyncRendering) {
151
if ($panel_type->shouldRenderAsync()) {
152
return $this->renderAsyncPanel();
153
}
154
}
155
156
return $this->renderNormalPanel();
157
} catch (Exception $ex) {
158
return $this->renderErrorPanel(
159
$panel->getName(),
160
pht(
161
'%s: %s',
162
phutil_tag('strong', array(), get_class($ex)),
163
$ex->getMessage()));
164
}
165
}
166
167
private function renderNormalPanel() {
168
$panel = $this->getPanel();
169
$panel_type = $panel->getImplementation();
170
171
$content = $panel_type->renderPanelContent(
172
$this->getViewer(),
173
$panel,
174
$this);
175
$header = $this->renderPanelHeader();
176
177
return $this->renderPanelDiv(
178
$content,
179
$header);
180
}
181
182
183
private function renderAsyncPanel() {
184
$context_phid = $this->getContextPHID();
185
$panel = $this->getPanel();
186
187
$panel_id = celerity_generate_unique_node_id();
188
189
Javelin::initBehavior(
190
'dashboard-async-panel',
191
array(
192
'panelID' => $panel_id,
193
'parentPanelPHIDs' => $this->getParentPanelPHIDs(),
194
'headerMode' => $this->getHeaderMode(),
195
'contextPHID' => $context_phid,
196
'panelKey' => $this->getPanelKey(),
197
'movable' => $this->getMovable(),
198
'uri' => '/dashboard/panel/render/'.$panel->getID().'/',
199
));
200
201
$header = $this->renderPanelHeader();
202
$content = id(new PHUIPropertyListView())
203
->addTextContent(pht('Loading...'));
204
205
return $this->renderPanelDiv(
206
$content,
207
$header,
208
$panel_id);
209
}
210
211
private function renderErrorPanel($title, $body) {
212
switch ($this->getHeaderMode()) {
213
case self::HEADER_MODE_NONE:
214
$header = null;
215
break;
216
case self::HEADER_MODE_EDIT:
217
$header = id(new PHUIHeaderView())
218
->setHeader($title);
219
$header = $this->addPanelHeaderActions($header);
220
break;
221
case self::HEADER_MODE_NORMAL:
222
default:
223
$header = id(new PHUIHeaderView())
224
->setHeader($title);
225
break;
226
}
227
228
$icon = id(new PHUIIconView())
229
->setIcon('fa-warning red msr');
230
231
$content = id(new PHUIBoxView())
232
->addClass('dashboard-box')
233
->addMargin(PHUI::MARGIN_LARGE)
234
->appendChild($icon)
235
->appendChild($body);
236
237
return $this->renderPanelDiv(
238
$content,
239
$header);
240
}
241
242
private function renderPanelDiv(
243
$content,
244
$header = null,
245
$id = null) {
246
require_celerity_resource('phabricator-dashboard-css');
247
248
$panel = $this->getPanel();
249
if (!$id) {
250
$id = celerity_generate_unique_node_id();
251
}
252
253
$box = new PHUIObjectBoxView();
254
255
$interface = 'PhabricatorApplicationSearchResultView';
256
if ($content instanceof $interface) {
257
if ($content->getObjectList()) {
258
$box->setObjectList($content->getObjectList());
259
}
260
if ($content->getTable()) {
261
$box->setTable($content->getTable());
262
}
263
if ($content->getContent()) {
264
$box->appendChild($content->getContent());
265
}
266
} else {
267
$box->appendChild($content);
268
}
269
270
$box
271
->setHeader($header)
272
->setID($id)
273
->addClass('dashboard-box')
274
->addSigil('dashboard-panel');
275
276
if ($this->getMovable()) {
277
$box->addSigil('panel-movable');
278
}
279
280
if ($panel) {
281
$box->setMetadata(
282
array(
283
'panelKey' => $this->getPanelKey(),
284
));
285
}
286
287
return $box;
288
}
289
290
291
private function renderPanelHeader() {
292
293
$panel = $this->getPanel();
294
switch ($this->getHeaderMode()) {
295
case self::HEADER_MODE_NONE:
296
$header = null;
297
break;
298
case self::HEADER_MODE_EDIT:
299
// In edit mode, include the panel monogram to make managing boards
300
// a little easier.
301
$header_text = pht('%s %s', $panel->getMonogram(), $panel->getName());
302
$header = id(new PHUIHeaderView())
303
->setHeader($header_text);
304
$header = $this->addPanelHeaderActions($header);
305
break;
306
case self::HEADER_MODE_NORMAL:
307
default:
308
$header = id(new PHUIHeaderView())
309
->setHeader($panel->getName());
310
$panel_type = $panel->getImplementation();
311
$header = $panel_type->adjustPanelHeader(
312
$this->getViewer(),
313
$panel,
314
$this,
315
$header);
316
break;
317
}
318
return $header;
319
}
320
321
private function addPanelHeaderActions(
322
PHUIHeaderView $header) {
323
324
$viewer = $this->getViewer();
325
$panel = $this->getPanel();
326
$context_phid = $this->getContextPHID();
327
328
$actions = array();
329
330
if ($panel) {
331
try {
332
$panel_actions = $panel->newHeaderEditActions(
333
$viewer,
334
$context_phid);
335
} catch (Exception $ex) {
336
$error_action = id(new PhabricatorActionView())
337
->setIcon('fa-exclamation-triangle red')
338
->setName(pht('<Rendering Exception>'));
339
$panel_actions[] = $error_action;
340
}
341
342
if ($panel_actions) {
343
foreach ($panel_actions as $panel_action) {
344
$actions[] = $panel_action;
345
}
346
$actions[] = id(new PhabricatorActionView())
347
->setType(PhabricatorActionView::TYPE_DIVIDER);
348
}
349
350
$panel_id = $panel->getID();
351
352
$edit_uri = "/dashboard/panel/edit/{$panel_id}/";
353
$params = array(
354
'contextPHID' => $context_phid,
355
);
356
$edit_uri = new PhutilURI($edit_uri, $params);
357
358
$actions[] = id(new PhabricatorActionView())
359
->setIcon('fa-pencil')
360
->setName(pht('Edit Panel'))
361
->setHref($edit_uri);
362
363
$actions[] = id(new PhabricatorActionView())
364
->setIcon('fa-window-maximize')
365
->setName(pht('View Panel Details'))
366
->setHref($panel->getURI());
367
}
368
369
if ($context_phid) {
370
$panel_phid = $this->getPanelPHID();
371
372
$remove_uri = urisprintf('/dashboard/adjust/remove/');
373
$params = array(
374
'contextPHID' => $context_phid,
375
'panelKey' => $this->getPanelKey(),
376
);
377
$remove_uri = new PhutilURI($remove_uri, $params);
378
379
$actions[] = id(new PhabricatorActionView())
380
->setIcon('fa-times')
381
->setHref($remove_uri)
382
->setName(pht('Remove Panel'))
383
->setWorkflow(true);
384
}
385
386
$dropdown_menu = id(new PhabricatorActionListView())
387
->setViewer($viewer);
388
389
foreach ($actions as $action) {
390
$dropdown_menu->addAction($action);
391
}
392
393
$action_menu = id(new PHUIButtonView())
394
->setTag('a')
395
->setIcon('fa-cog')
396
->setText(pht('Manage Panel'))
397
->setDropdownMenu($dropdown_menu);
398
399
$header->addActionLink($action_menu);
400
401
return $header;
402
}
403
404
405
/**
406
* Detect graph cycles in panels, and deeply nested panels.
407
*
408
* This method throws if the current rendering stack is too deep or contains
409
* a cycle. This can happen if you embed layout panels inside each other,
410
* build a big stack of panels, or embed a panel in remarkup inside another
411
* panel. Generally, all of this stuff is ridiculous and we just want to
412
* shut it down.
413
*
414
* @param PhabricatorDashboardPanel Panel being rendered.
415
* @return void
416
*/
417
private function detectRenderingCycle(PhabricatorDashboardPanel $panel) {
418
if ($this->parentPanelPHIDs === null) {
419
throw new PhutilInvalidStateException('setParentPanelPHIDs');
420
}
421
422
$max_depth = 4;
423
if (count($this->parentPanelPHIDs) >= $max_depth) {
424
throw new Exception(
425
pht(
426
'To render more than %s levels of panels nested inside other '.
427
'panels, purchase a subscription to %s Gold.',
428
new PhutilNumber($max_depth),
429
PlatformSymbols::getPlatformServerName()));
430
}
431
432
if (in_array($panel->getPHID(), $this->parentPanelPHIDs)) {
433
throw new Exception(
434
pht(
435
'You awake in a twisting maze of mirrors, all alike. '.
436
'You are likely to be eaten by a graph cycle. '.
437
'Should you escape alive, you resolve to be more careful about '.
438
'putting dashboard panels inside themselves.'));
439
}
440
}
441
442
private function getContextPHID() {
443
$context = $this->getContextObject();
444
445
if ($context) {
446
return $context->getPHID();
447
}
448
449
return null;
450
}
451
452
}
453
454