Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
index 731c4fa784..a9af90f2a5 100644
--- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php
@@ -1,598 +1,599 @@
<?php
final class HarbormasterPlanViewController extends HarbormasterPlanController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$plan = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$plan) {
return new Aphront404Response();
}
$title = $plan->getName();
$header = id(new PHUIHeaderView())
->setHeader($plan->getName())
->setUser($viewer)
->setPolicyObject($plan)
->setHeaderIcon('fa-ship');
$curtain = $this->buildCurtainView($plan);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($plan->getObjectName())
->setBorder(true);
list($step_list, $has_any_conflicts, $would_deadlock, $steps) =
$this->buildStepList($plan);
$error = null;
if (!$steps) {
$error = pht(
'This build plan does not have any build steps yet, so it will '.
'not do anything when run.');
} else if ($would_deadlock) {
$error = pht(
'This build plan will deadlock when executed, due to circular '.
'dependencies present in the build plan. Examine the step list '.
'and resolve the deadlock.');
} else if ($has_any_conflicts) {
// A deadlocking build will also cause all the artifacts to be
// invalid, so we just skip showing this message if that's the
// case.
$error = pht(
'This build plan has conflicts in one or more build steps. '.
'Examine the step list and resolve the listed errors.');
}
if ($error) {
$error = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild($error);
}
$builds_view = $this->newBuildsView($plan);
$options_view = $this->newOptionsView($plan);
$rules_view = $this->newRulesView($plan);
$timeline = $this->buildTransactionTimeline(
$plan,
new HarbormasterBuildPlanTransactionQuery());
$timeline->setShouldTerminate(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(
array(
$error,
$step_list,
$options_view,
$rules_view,
$builds_view,
$timeline,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($plan->getPHID()))
->appendChild($view);
}
private function buildStepList(HarbormasterBuildPlan $plan) {
$viewer = $this->getViewer();
$run_order = HarbormasterBuildGraph::determineDependencyExecution($plan);
$steps = id(new HarbormasterBuildStepQuery())
->setViewer($viewer)
->withBuildPlanPHIDs(array($plan->getPHID()))
->execute();
$steps = mpull($steps, null, 'getPHID');
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$plan,
PhabricatorPolicyCapability::CAN_EDIT);
$step_list = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(
pht('This build plan does not have any build steps yet.'));
$i = 1;
$last_depth = 0;
$has_any_conflicts = false;
$is_deadlocking = false;
foreach ($run_order as $run_ref) {
$step = $steps[$run_ref['node']->getPHID()];
$depth = $run_ref['depth'] + 1;
if ($last_depth !== $depth) {
$last_depth = $depth;
$i = 1;
} else {
$i++;
}
$step_id = $step->getID();
$view_uri = $this->getApplicationURI("step/view/{$step_id}/");
$item = id(new PHUIObjectItemView())
->setObjectName(pht('Step %d.%d', $depth, $i))
->setHeader($step->getName())
->setHref($view_uri);
$step_list->addItem($item);
$implementation = null;
try {
$implementation = $step->getStepImplementation();
} catch (Exception $ex) {
// We can't initialize the implementation. This might be because
// it's been renamed or no longer exists.
$item
->setStatusIcon('fa-warning red')
->addAttribute(pht(
'This step has an invalid implementation (%s).',
$step->getClassName()));
continue;
}
$item->addAttribute($implementation->getDescription());
$item->setHref($view_uri);
$depends = $step->getStepImplementation()->getDependencies($step);
$inputs = $step->getStepImplementation()->getArtifactInputs();
$outputs = $step->getStepImplementation()->getArtifactOutputs();
$has_conflicts = false;
if ($depends || $inputs || $outputs) {
$available_artifacts =
HarbormasterBuildStepImplementation::getAvailableArtifacts(
$plan,
$step,
null);
$available_artifacts = ipull($available_artifacts, 'type');
list($depends_ui, $has_conflicts) = $this->buildDependsOnList(
$depends,
pht('Depends On'),
$steps);
list($inputs_ui, $has_conflicts) = $this->buildArtifactList(
$inputs,
'in',
pht('Input Artifacts'),
$available_artifacts);
list($outputs_ui) = $this->buildArtifactList(
$outputs,
'out',
pht('Output Artifacts'),
array());
$item->appendChild(
phutil_tag(
'div',
array(
'class' => 'harbormaster-artifact-io',
),
array(
$depends_ui,
$inputs_ui,
$outputs_ui,
)));
}
if ($has_conflicts) {
$has_any_conflicts = true;
$item->setStatusIcon('fa-warning red');
}
if ($run_ref['cycle']) {
$is_deadlocking = true;
}
if ($is_deadlocking) {
$item->setStatusIcon('fa-warning red');
}
}
$step_list->setFlush(true);
$plan_id = $plan->getID();
$header = id(new PHUIHeaderView())
->setHeader(pht('Build Steps'))
->addActionLink(
id(new PHUIButtonView())
->setText(pht('Add Build Step'))
->setHref($this->getApplicationURI("step/add/{$plan_id}/"))
->setTag('a')
->setIcon('fa-plus')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$step_box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($step_list);
return array($step_box, $has_any_conflicts, $is_deadlocking, $steps);
}
private function buildCurtainView(HarbormasterBuildPlan $plan) {
$viewer = $this->getViewer();
$id = $plan->getID();
$curtain = $this->newCurtainView($plan);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$plan,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Plan'))
->setHref($this->getApplicationURI("plan/edit/{$id}/"))
->setWorkflow(!$can_edit)
->setDisabled(!$can_edit)
->setIcon('fa-pencil'));
if ($plan->isDisabled()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Enable Plan'))
->setHref($this->getApplicationURI("plan/disable/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setIcon('fa-check'));
} else {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Disable Plan'))
->setHref($this->getApplicationURI("plan/disable/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit)
->setIcon('fa-ban'));
}
$can_run = ($plan->hasRunCapability($viewer) && $plan->canRunManually());
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Run Plan Manually'))
->setHref($this->getApplicationURI("plan/run/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_run)
->setIcon('fa-play-circle'));
return $curtain;
}
private function buildArtifactList(
array $artifacts,
$kind,
$name,
array $available_artifacts) {
$has_conflicts = false;
if (!$artifacts) {
return array(null, $has_conflicts);
}
$this->requireResource('harbormaster-css');
$header = phutil_tag(
'div',
array(
'class' => 'harbormaster-artifact-summary-header',
),
$name);
$is_input = ($kind == 'in');
$list = new PHUIStatusListView();
foreach ($artifacts as $artifact) {
$error = null;
$key = idx($artifact, 'key');
if (!strlen($key)) {
$bound = phutil_tag('em', array(), pht('(null)'));
if ($is_input) {
// This is an unbound input. For now, all inputs are always required.
$icon = PHUIStatusItemView::ICON_WARNING;
$color = 'red';
$icon_label = pht('Required Input');
$has_conflicts = true;
$error = pht('This input is required, but not configured.');
} else {
// This is an unnamed output. Outputs do not necessarily need to be
// named.
$icon = PHUIStatusItemView::ICON_OPEN;
$color = 'bluegrey';
$icon_label = pht('Unused Output');
}
} else {
$bound = phutil_tag('strong', array(), $key);
if ($is_input) {
if (isset($available_artifacts[$key])) {
if ($available_artifacts[$key] == idx($artifact, 'type')) {
$icon = PHUIStatusItemView::ICON_ACCEPT;
$color = 'green';
$icon_label = pht('Valid Input');
} else {
$icon = PHUIStatusItemView::ICON_WARNING;
$color = 'red';
$icon_label = pht('Bad Input Type');
$has_conflicts = true;
$error = pht(
'This input is bound to the wrong artifact type. It is bound '.
'to a "%s" artifact, but should be bound to a "%s" artifact.',
$available_artifacts[$key],
idx($artifact, 'type'));
}
} else {
$icon = PHUIStatusItemView::ICON_QUESTION;
$color = 'red';
$icon_label = pht('Unknown Input');
$has_conflicts = true;
$error = pht(
'This input is bound to an artifact ("%s") which does not exist '.
'at this stage in the build process.',
$key);
}
} else {
$icon = PHUIStatusItemView::ICON_DOWN;
$color = 'green';
$icon_label = pht('Valid Output');
}
}
if ($error) {
$note = array(
phutil_tag('strong', array(), pht('ERROR:')),
' ',
$error,
);
} else {
$note = $bound;
}
$list->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, $color, $icon_label)
->setTarget($artifact['name'])
->setNote($note));
}
$ui = array(
$header,
$list,
);
return array($ui, $has_conflicts);
}
private function buildDependsOnList(
array $step_phids,
$name,
array $steps) {
$has_conflicts = false;
if (!$step_phids) {
return null;
}
$this->requireResource('harbormaster-css');
$steps = mpull($steps, null, 'getPHID');
$header = phutil_tag(
'div',
array(
'class' => 'harbormaster-artifact-summary-header',
),
$name);
$list = new PHUIStatusListView();
foreach ($step_phids as $step_phid) {
$error = null;
if (idx($steps, $step_phid) === null) {
$icon = PHUIStatusItemView::ICON_WARNING;
$color = 'red';
$icon_label = pht('Missing Dependency');
$has_conflicts = true;
$error = pht(
"This dependency specifies a build step which doesn't exist.");
} else {
$bound = phutil_tag(
'strong',
array(),
idx($steps, $step_phid)->getName());
$icon = PHUIStatusItemView::ICON_ACCEPT;
$color = 'green';
$icon_label = pht('Valid Input');
}
if ($error) {
$note = array(
phutil_tag('strong', array(), pht('ERROR:')),
' ',
$error,
);
} else {
$note = $bound;
}
$list->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, $color, $icon_label)
->setTarget(pht('Build Step'))
->setNote($note));
}
$ui = array(
$header,
$list,
);
return array($ui, $has_conflicts);
}
private function newBuildsView(HarbormasterBuildPlan $plan) {
$viewer = $this->getViewer();
$builds = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withBuildPlanPHIDs(array($plan->getPHID()))
->setLimit(10)
->execute();
$list = id(new HarbormasterBuildView())
->setViewer($viewer)
->setBuilds($builds)
->newObjectList();
$list->setNoDataString(pht('No recent builds.'));
$more_href = new PhutilURI(
$this->getApplicationURI('/build/'),
array('plan' => $plan->getPHID()));
$more_link = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-list-ul')
->setText(pht('View All Builds'))
->setHref($more_href);
$header = id(new PHUIHeaderView())
->setHeader(pht('Recent Builds'))
->addActionLink($more_link);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($list);
}
private function newRulesView(HarbormasterBuildPlan $plan) {
$viewer = $this->getViewer();
$rules = id(new HeraldRuleQuery())
->setViewer($viewer)
->withDisabled(false)
->withAffectedObjectPHIDs(array($plan->getPHID()))
->needValidateAuthors(true)
+ ->setLimit(10)
->execute();
$list = id(new HeraldRuleListView())
->setViewer($viewer)
->setRules($rules)
->newObjectList();
$list->setNoDataString(pht('No active Herald rules trigger this build.'));
$more_href = new PhutilURI(
'/herald/',
array('affectedPHID' => $plan->getPHID()));
$more_link = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-list-ul')
->setText(pht('View All Rules'))
->setHref($more_href);
$header = id(new PHUIHeaderView())
->setHeader(pht('Run By Herald Rules'))
->addActionLink($more_link);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($list);
}
private function newOptionsView(HarbormasterBuildPlan $plan) {
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$plan,
PhabricatorPolicyCapability::CAN_EDIT);
$behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors();
$rows = array();
foreach ($behaviors as $behavior) {
$option = $behavior->getPlanOption($plan);
$icon = $option->getIcon();
$icon = id(new PHUIIconView())->setIcon($icon);
$edit_uri = new PhutilURI(
$this->getApplicationURI(
urisprintf(
'plan/behavior/%d/%s/',
$plan->getID(),
$behavior->getKey())));
$edit_button = id(new PHUIButtonView())
->setTag('a')
->setColor(PHUIButtonView::GREY)
->setSize(PHUIButtonView::SMALL)
->setDisabled(!$can_edit)
->setWorkflow(true)
->setText(pht('Edit'))
->setHref($edit_uri);
$rows[] = array(
$icon,
$behavior->getName(),
$option->getName(),
$option->getDescription(),
$edit_button,
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
null,
pht('Name'),
pht('Behavior'),
pht('Details'),
null,
))
->setColumnClasses(
array(
null,
'pri',
null,
'wide',
null,
));
$header = id(new PHUIHeaderView())
->setHeader(pht('Plan Behaviors'));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
}
}
diff --git a/src/applications/herald/action/HeraldCallWebhookAction.php b/src/applications/herald/action/HeraldCallWebhookAction.php
index 953958e5c6..186a7a741f 100644
--- a/src/applications/herald/action/HeraldCallWebhookAction.php
+++ b/src/applications/herald/action/HeraldCallWebhookAction.php
@@ -1,66 +1,70 @@
<?php
final class HeraldCallWebhookAction extends HeraldAction {
const ACTIONCONST = 'webhook';
const DO_WEBHOOK = 'do.call-webhook';
public function getHeraldActionName() {
return pht('Call webhooks');
}
public function getActionGroupKey() {
return HeraldUtilityActionGroup::ACTIONGROUPKEY;
}
public function supportsObject($object) {
if (!$this->getAdapter()->supportsWebhooks()) {
return false;
}
return true;
}
public function supportsRuleType($rule_type) {
return ($rule_type !== HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
}
public function applyEffect($object, HeraldEffect $effect) {
$adapter = $this->getAdapter();
$rule = $effect->getRule();
$target = $effect->getTarget();
foreach ($target as $webhook_phid) {
$adapter->queueWebhook($webhook_phid, $rule->getPHID());
}
$this->logEffect(self::DO_WEBHOOK, $target);
}
public function getHeraldActionStandardType() {
return self::STANDARD_PHID_LIST;
}
protected function getActionEffectMap() {
return array(
self::DO_WEBHOOK => array(
'icon' => 'fa-cloud-upload',
'color' => 'green',
'name' => pht('Called Webhooks'),
),
);
}
public function renderActionDescription($value) {
return pht('Call webhooks: %s.', $this->renderHandleList($value));
}
protected function renderActionEffectDescription($type, $data) {
return pht('Called webhooks: %s.', $this->renderHandleList($data));
}
protected function getDatasource() {
return new HeraldWebhookDatasource();
}
+ public function getPHIDsAffectedByAction(HeraldActionRecord $record) {
+ return $record->getTarget();
+ }
+
}
diff --git a/src/applications/herald/controller/HeraldWebhookViewController.php b/src/applications/herald/controller/HeraldWebhookViewController.php
index d8e5eb3c54..5f6be9816c 100644
--- a/src/applications/herald/controller/HeraldWebhookViewController.php
+++ b/src/applications/herald/controller/HeraldWebhookViewController.php
@@ -1,197 +1,238 @@
<?php
final class HeraldWebhookViewController
extends HeraldWebhookController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$hook = id(new HeraldWebhookQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$hook) {
return new Aphront404Response();
}
$header = $this->buildHeaderView($hook);
$warnings = null;
if ($hook->isInErrorBackoff($viewer)) {
$message = pht(
'Many requests to this webhook have failed recently (at least %s '.
'errors in the last %s seconds). New requests are temporarily paused.',
$hook->getErrorBackoffThreshold(),
$hook->getErrorBackoffWindow());
$warnings = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
$message,
));
}
$curtain = $this->buildCurtain($hook);
$properties_view = $this->buildPropertiesView($hook);
$timeline = $this->buildTransactionTimeline(
$hook,
new HeraldWebhookTransactionQuery());
$timeline->setShouldTerminate(true);
$requests = id(new HeraldWebhookRequestQuery())
->setViewer($viewer)
->withWebhookPHIDs(array($hook->getPHID()))
->setLimit(20)
->execute();
$warnings = array();
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
$message = pht(
'Phabricator is currently configured in silent mode, so it will not '.
'publish webhooks. To adjust this setting, see '.
'@{config:phabricator.silent} in Config.');
$warnings[] = id(new PHUIInfoView())
->setTitle(pht('Silent Mode'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild(new PHUIRemarkupView($viewer, $message));
}
$requests_table = id(new HeraldWebhookRequestListView())
->setViewer($viewer)
->setRequests($requests)
->setHighlightID($request->getURIData('requestID'));
$requests_view = id(new PHUIObjectBoxView())
->setHeaderText(pht('Recent Requests'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($requests_table);
+ $rules_view = $this->newRulesView($hook);
+
$hook_view = id(new PHUITwoColumnView())
->setHeader($header)
->setMainColumn(
array(
$warnings,
$properties_view,
+ $rules_view,
$requests_view,
$timeline,
))
->setCurtain($curtain);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Webhook %d', $hook->getID()))
->setBorder(true);
return $this->newPage()
->setTitle(
array(
pht('Webhook %d', $hook->getID()),
$hook->getName(),
))
->setCrumbs($crumbs)
->setPageObjectPHIDs(
array(
$hook->getPHID(),
))
->appendChild($hook_view);
}
private function buildHeaderView(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$title = $hook->getName();
$status_icon = $hook->getStatusIcon();
$status_color = $hook->getStatusColor();
$status_name = $hook->getStatusDisplayName();
$header = id(new PHUIHeaderView())
->setHeader($title)
->setViewer($viewer)
->setPolicyObject($hook)
->setStatus($status_icon, $status_color, $status_name)
->setHeaderIcon('fa-cloud-upload');
return $header;
}
private function buildCurtain(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($hook);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$hook,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $hook->getID();
$edit_uri = $this->getApplicationURI("webhook/edit/{$id}/");
$test_uri = $this->getApplicationURI("webhook/test/{$id}/");
$key_view_uri = $this->getApplicationURI("webhook/key/view/{$id}/");
$key_cycle_uri = $this->getApplicationURI("webhook/key/cycle/{$id}/");
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Webhook'))
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($edit_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('New Test Request'))
->setIcon('fa-cloud-upload')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($test_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View HMAC Key'))
->setIcon('fa-key')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($key_view_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Regenerate HMAC Key'))
->setIcon('fa-refresh')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($key_cycle_uri));
return $curtain;
}
private function buildPropertiesView(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setViewer($viewer);
$properties->addProperty(
pht('URI'),
$hook->getWebhookURI());
$properties->addProperty(
pht('Status'),
$hook->getStatusDisplayName());
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Details'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
+ private function newRulesView(HeraldWebhook $hook) {
+ $viewer = $this->getViewer();
+
+ $rules = id(new HeraldRuleQuery())
+ ->setViewer($viewer)
+ ->withDisabled(false)
+ ->withAffectedObjectPHIDs(array($hook->getPHID()))
+ ->needValidateAuthors(true)
+ ->setLimit(10)
+ ->execute();
+
+ $list = id(new HeraldRuleListView())
+ ->setViewer($viewer)
+ ->setRules($rules)
+ ->newObjectList();
+
+ $list->setNoDataString(pht('No active Herald rules call this webhook.'));
+
+ $more_href = new PhutilURI(
+ '/herald/',
+ array('affectedPHID' => $hook->getPHID()));
+
+ $more_link = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon('fa-list-ul')
+ ->setText(pht('View All Rules'))
+ ->setHref($more_href);
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Called By Herald Rules'))
+ ->addActionLink($more_link);
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->appendChild($list);
+ }
+
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 21, 11:23 AM (41 m, 42 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
352540
Default Alt Text
(26 KB)

Event Timeline