Add a basic, general-purpose export workflow for all objects with SearchEngine support
Summary: Depends on D18918. Ref T13046. Ref T5954. Pull logs can currently be browsed in the web UI, but this isn't very powerful, especially if you have thousands of them. Allow SearchEngine implementations to define exportable fields so that users can "Use Results > Export Data" on any query. In particular, they can use this workflow to download a file with pull logs. In the future, this can replace the existing "Export to Excel" feature in Maniphest. For now, we hard-code JSON as the only supported datatype and don't actually make any effort to format the data properly, but this leaves room to add more exporters (CSV, Excel) and data type awareness (integer casting, date formatting, etc) in the future. For sufficiently large result sets, this will probably time out. At some point, I'll make this use the job queue (like bulk editing) when the export is "large" (affects more than 1K rows?). Test Plan: Downloaded pull logs in JSON format. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13046, T5954 Differential Revision: https://secure.phabricator.com/D18919
This commit is contained in:
@@ -66,6 +66,11 @@ final class PhabricatorApplicationSearchController
|
||||
public function processRequest() {
|
||||
$this->validateDelegatingController();
|
||||
|
||||
$query_action = $this->getRequest()->getURIData('queryAction');
|
||||
if ($query_action == 'export') {
|
||||
return $this->processExportRequest();
|
||||
}
|
||||
|
||||
$key = $this->getQueryKey();
|
||||
if ($key == 'edit') {
|
||||
return $this->processEditRequest();
|
||||
@@ -374,6 +379,96 @@ final class PhabricatorApplicationSearchController
|
||||
->appendChild($body);
|
||||
}
|
||||
|
||||
private function processExportRequest() {
|
||||
$viewer = $this->getViewer();
|
||||
$engine = $this->getSearchEngine();
|
||||
$request = $this->getRequest();
|
||||
|
||||
if (!$this->canExport()) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$query_key = $this->getQueryKey();
|
||||
if ($engine->isBuiltinQuery($query_key)) {
|
||||
$saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
|
||||
} else if ($query_key) {
|
||||
$saved_query = id(new PhabricatorSavedQueryQuery())
|
||||
->setViewer($viewer)
|
||||
->withQueryKeys(array($query_key))
|
||||
->executeOne();
|
||||
if (!$saved_query) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
}
|
||||
|
||||
$cancel_uri = $engine->getQueryResultsPageURI($query_key);
|
||||
|
||||
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
|
||||
|
||||
if ($named_query) {
|
||||
$filename = $named_query->getQueryName();
|
||||
} else {
|
||||
$filename = $engine->getResultTypeDescription();
|
||||
}
|
||||
$filename = phutil_utf8_strtolower($filename);
|
||||
$filename = PhabricatorFile::normalizeFileName($filename);
|
||||
|
||||
if ($request->isFormPost()) {
|
||||
$query = $engine->buildQueryFromSavedQuery($saved_query);
|
||||
|
||||
// NOTE: We aren't reading the pager from the request. Exports always
|
||||
// affect the entire result set.
|
||||
$pager = $engine->newPagerForSavedQuery($saved_query);
|
||||
$pager->setPageSize(0x7FFFFFFF);
|
||||
|
||||
$objects = $engine->executeQuery($query, $pager);
|
||||
|
||||
$extension = 'json';
|
||||
$mime_type = 'application/json';
|
||||
$filename = $filename.'.'.$extension;
|
||||
|
||||
$result = $engine->newExport($objects);
|
||||
$result = id(new PhutilJSON())
|
||||
->encodeAsList($result);
|
||||
|
||||
$file = PhabricatorFile::newFromFileData(
|
||||
$result,
|
||||
array(
|
||||
'name' => $filename,
|
||||
'authorPHID' => $viewer->getPHID(),
|
||||
'ttl.relative' => phutil_units('15 minutes in seconds'),
|
||||
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
|
||||
'mime-type' => $mime_type,
|
||||
));
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Download Results'))
|
||||
->appendParagraph(
|
||||
pht('Click the download button to download the exported data.'))
|
||||
->addCancelButton($cancel_uri, pht('Done'))
|
||||
->setSubmitURI($file->getDownloadURI())
|
||||
->setDisableWorkflowOnSubmit(true)
|
||||
->addSubmitButton(pht('Download Results'));
|
||||
}
|
||||
|
||||
$export_form = id(new AphrontFormView())
|
||||
->setViewer($viewer)
|
||||
->appendControl(
|
||||
id(new AphrontFormSelectControl())
|
||||
->setName('format')
|
||||
->setLabel(pht('Format'))
|
||||
->setOptions(
|
||||
array(
|
||||
'json' => 'JSON',
|
||||
)));
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Export Results'))
|
||||
->appendForm($export_form)
|
||||
->addCancelButton($cancel_uri)
|
||||
->addSubmitButton(pht('Continue'));
|
||||
}
|
||||
|
||||
private function processEditRequest() {
|
||||
$parent = $this->getDelegatingController();
|
||||
$request = $this->getRequest();
|
||||
@@ -720,7 +815,6 @@ final class PhabricatorApplicationSearchController
|
||||
$viewer);
|
||||
|
||||
if ($can_use && $is_installed) {
|
||||
$dashboard_uri = '/dashboard/install/';
|
||||
$actions[] = id(new PhabricatorActionView())
|
||||
->setIcon('fa-dashboard')
|
||||
->setName(pht('Add to Dashboard'))
|
||||
@@ -728,6 +822,15 @@ final class PhabricatorApplicationSearchController
|
||||
->setHref("/dashboard/panel/install/{$engine_class}/{$query_key}/");
|
||||
}
|
||||
|
||||
if ($this->canExport()) {
|
||||
$export_uri = $engine->getExportURI($query_key);
|
||||
$actions[] = id(new PhabricatorActionView())
|
||||
->setIcon('fa-download')
|
||||
->setName(pht('Export Results'))
|
||||
->setWorkflow(true)
|
||||
->setHref($export_uri);
|
||||
}
|
||||
|
||||
if ($is_dev) {
|
||||
$engine = $this->getSearchEngine();
|
||||
$nux_uri = $engine->getQueryBaseURI();
|
||||
@@ -753,4 +856,22 @@ final class PhabricatorApplicationSearchController
|
||||
return $actions;
|
||||
}
|
||||
|
||||
private function canExport() {
|
||||
$engine = $this->getSearchEngine();
|
||||
if (!$engine->canExport()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't allow logged-out users to perform exports. There's no technical
|
||||
// or policy reason they can't, but we don't normally give them access
|
||||
// to write files or jobs. For now, just err on the side of caution.
|
||||
|
||||
$viewer = $this->getViewer();
|
||||
if (!$viewer->getPHID()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user