mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-29 10:12:41 +01:00
Start of a config web interface.
Summary: This is somewhat clowny, particularly in how it handles JSON encode/decode, but I've commented why I did things the way I did. The goal is to store minified JSON but show pretty-printed JSON where possible, to the user editing it. Test Plan: * Went to /config/ and saw a list of keys from the `default` config. * Clicked on one of them, submitted the default value successfully. * Changed the value to invalid JSON and got a decent error. * Changed the value to valid JSON and checked the DB to confirm it saved. * Confirmed the DB values were minified. * Confirmed the user-facing values were pretty-printed where they could be. * Confirmed that PHIDs were getting assigned properly and that isDeleted properly defaulted to false/0. Reviewers: epriestley Reviewed By: epriestley CC: aran, Korvin Maniphest Tasks: T2246 Differential Revision: https://secure.phabricator.com/D4290
This commit is contained in:
parent
1e2dfb5b6b
commit
a774620042
11 changed files with 352 additions and 0 deletions
12
resources/sql/patches/20121226.config.sql
Normal file
12
resources/sql/patches/20121226.config.sql
Normal file
|
@ -0,0 +1,12 @@
|
|||
CREATE TABLE {$NAMESPACE}_config.config_entry (
|
||||
`id` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||
`phid` VARCHAR(64) NOT NULL COLLATE utf8_bin,
|
||||
`namespace` VARCHAR(64) BINARY NOT NULL COLLATE utf8_bin,
|
||||
`configKey` VARCHAR(64) BINARY NOT NULL COLLATE utf8_bin,
|
||||
`value` LONGTEXT NOT NULL,
|
||||
`isDeleted` BOOL NOT NULL,
|
||||
`dateCreated` INT UNSIGNED NOT NULL,
|
||||
`dateModified` INT UNSIGNED NOT NULL,
|
||||
UNIQUE KEY `key_phid` (`phid`),
|
||||
UNIQUE KEY `key_name` (`namespace`, `configKey`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
|
@ -580,6 +580,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorApplicationAuth' => 'applications/auth/application/PhabricatorApplicationAuth.php',
|
||||
'PhabricatorApplicationCalendar' => 'applications/calendar/application/PhabricatorApplicationCalendar.php',
|
||||
'PhabricatorApplicationConduit' => 'applications/conduit/application/PhabricatorApplicationConduit.php',
|
||||
'PhabricatorApplicationConfig' => 'applications/config/application/PhabricatorApplicationConfig.php',
|
||||
'PhabricatorApplicationCountdown' => 'applications/countdown/application/PhabricatorApplicationCountdown.php',
|
||||
'PhabricatorApplicationDaemons' => 'applications/daemon/application/PhabricatorApplicationDaemons.php',
|
||||
'PhabricatorApplicationDifferential' => 'applications/differential/application/PhabricatorApplicationDifferential.php',
|
||||
|
@ -679,8 +680,13 @@ phutil_register_library_map(array(
|
|||
'PhabricatorConduitLogController' => 'applications/conduit/controller/PhabricatorConduitLogController.php',
|
||||
'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php',
|
||||
'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php',
|
||||
'PhabricatorConfigController' => 'applications/config/controller/PhabricatorConfigController.php',
|
||||
'PhabricatorConfigDictionarySource' => 'infrastructure/env/PhabricatorConfigDictionarySource.php',
|
||||
'PhabricatorConfigEditController' => 'applications/config/controller/PhabricatorConfigEditController.php',
|
||||
'PhabricatorConfigEntry' => 'applications/config/storage/PhabricatorConfigEntry.php',
|
||||
'PhabricatorConfigEntryDAO' => 'applications/config/storage/PhabricatorConfigEntryDAO.php',
|
||||
'PhabricatorConfigFileSource' => 'infrastructure/env/PhabricatorConfigFileSource.php',
|
||||
'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php',
|
||||
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
|
||||
'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
|
||||
'PhabricatorConfigStackSource' => 'infrastructure/env/PhabricatorConfigStackSource.php',
|
||||
|
@ -1873,6 +1879,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorApplicationAuth' => 'PhabricatorApplication',
|
||||
'PhabricatorApplicationCalendar' => 'PhabricatorApplication',
|
||||
'PhabricatorApplicationConduit' => 'PhabricatorApplication',
|
||||
'PhabricatorApplicationConfig' => 'PhabricatorApplication',
|
||||
'PhabricatorApplicationCountdown' => 'PhabricatorApplication',
|
||||
'PhabricatorApplicationDaemons' => 'PhabricatorApplication',
|
||||
'PhabricatorApplicationDifferential' => 'PhabricatorApplication',
|
||||
|
@ -1986,8 +1993,13 @@ phutil_register_library_map(array(
|
|||
'PhabricatorConduitLogController' => 'PhabricatorConduitController',
|
||||
'PhabricatorConduitMethodCallLog' => 'PhabricatorConduitDAO',
|
||||
'PhabricatorConduitTokenController' => 'PhabricatorConduitController',
|
||||
'PhabricatorConfigController' => 'PhabricatorController',
|
||||
'PhabricatorConfigDictionarySource' => 'PhabricatorConfigSource',
|
||||
'PhabricatorConfigEditController' => 'PhabricatorConfigController',
|
||||
'PhabricatorConfigEntry' => 'PhabricatorConfigEntryDAO',
|
||||
'PhabricatorConfigEntryDAO' => 'PhabricatorLiskDAO',
|
||||
'PhabricatorConfigFileSource' => 'PhabricatorConfigProxySource',
|
||||
'PhabricatorConfigListController' => 'PhabricatorConfigController',
|
||||
'PhabricatorConfigProxySource' => 'PhabricatorConfigSource',
|
||||
'PhabricatorConfigStackSource' => 'PhabricatorConfigSource',
|
||||
'PhabricatorContentSourceView' => 'AphrontView',
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorApplicationConfig extends PhabricatorApplication {
|
||||
|
||||
public function getBaseURI() {
|
||||
return '/config/';
|
||||
}
|
||||
|
||||
public function getIconName() {
|
||||
return 'config';
|
||||
}
|
||||
|
||||
public function getTitleGlyph() {
|
||||
return "\xE2\x98\xBA";
|
||||
}
|
||||
|
||||
public function getApplicationGroup() {
|
||||
return self::GROUP_ADMIN;
|
||||
}
|
||||
|
||||
public function shouldAppearInLaunchView() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getRoutes() {
|
||||
return array(
|
||||
'/config/' => array(
|
||||
'' => 'PhabricatorConfigListController',
|
||||
'edit/(?P<key>[\w\.\-]+)/' => 'PhabricatorConfigEditController',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorConfigController extends PhabricatorController {
|
||||
|
||||
public function shouldRequireAdmin() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function buildSideNavView($filter = null, $for_app = false) {
|
||||
$user = $this->getRequest()->getUser();
|
||||
|
||||
$nav = new AphrontSideNavFilterView();
|
||||
$nav->setBaseURI(new PhutilURI($this->getApplicationURI('filter/')));
|
||||
|
||||
return $nav;
|
||||
}
|
||||
|
||||
public function buildApplicationMenu() {
|
||||
return $this->buildSideNavView(null, true)->getMenu();
|
||||
}
|
||||
|
||||
public function buildApplicationCrumbs() {
|
||||
$crumbs = parent::buildApplicationCrumbs();
|
||||
return $crumbs;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorConfigEditController
|
||||
extends PhabricatorConfigController {
|
||||
|
||||
private $key;
|
||||
|
||||
public function willProcessRequest(array $data) {
|
||||
$this->key = idx($data, 'key');
|
||||
}
|
||||
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
$config = id(new PhabricatorConfigFileSource('default'))
|
||||
->getAllKeys();
|
||||
if (!$this->key || !array_key_exists($this->key, $config)) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
// Check if the config key is already stored in the database.
|
||||
// Grab the value if it is.
|
||||
$value = null;
|
||||
$config_entry = id(new PhabricatorConfigEntry())
|
||||
->loadOneWhere(
|
||||
'configKey = %s AND namespace=%s',
|
||||
$this->key,
|
||||
'default');
|
||||
if ($config_entry) {
|
||||
$value = $config_entry->getValue();
|
||||
} else {
|
||||
$config_entry = id(new PhabricatorConfigEntry())
|
||||
->setConfigKey($this->key);
|
||||
}
|
||||
|
||||
$e_value = null;
|
||||
$errors = array();
|
||||
if ($request->isFormPost()) {
|
||||
$new_value = $request->getStr('value');
|
||||
if (strlen($new_value)) {
|
||||
$json = json_decode($new_value, true);
|
||||
if ($json === null && strtolower($value) != 'null') {
|
||||
$e_value = 'Invalid';
|
||||
$errors[] = 'The given value must be valid JSON. This means, among '.
|
||||
'other things, that you must wrap strings in double-quotes.';
|
||||
$value = $new_value;
|
||||
} else {
|
||||
$value = $json;
|
||||
}
|
||||
} else {
|
||||
// TODO: When we do Transactions, make this just set isDeleted = 1
|
||||
$config_entry->delete();
|
||||
}
|
||||
|
||||
$config_entry->setValue($value);
|
||||
$config_entry->setNamespace('default');
|
||||
|
||||
if (!$errors) {
|
||||
$config_entry->save();
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI($config_entry->getURI());
|
||||
}
|
||||
}
|
||||
|
||||
$form = new AphrontFormView();
|
||||
$form->setFlexible(true);
|
||||
|
||||
$error_view = null;
|
||||
if ($errors) {
|
||||
$error_view = id(new AphrontErrorView())
|
||||
->setTitle('You broke everything!')
|
||||
->setErrors($errors);
|
||||
} else {
|
||||
// Check not only that it's an array, but that it's an "unnatural" array
|
||||
// meaning that the keys aren't 0 -> size_of_array.
|
||||
if (is_array($value) &&
|
||||
array_keys($value) != range(0, count($value) - 1)) {
|
||||
$value = id(new PhutilJSON())->encodeFormatted($value);
|
||||
} else {
|
||||
$value = json_encode($value);
|
||||
}
|
||||
}
|
||||
|
||||
$form
|
||||
->setUser($user)
|
||||
->appendChild(
|
||||
id(new AphrontFormTextAreaControl())
|
||||
->setLabel('JSON Value')
|
||||
->setError($e_value)
|
||||
->setValue($value)
|
||||
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
|
||||
->setCustomClass('PhabricatorMonospaced')
|
||||
->setName('value'))
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->addCancelButton($config_entry->getURI())
|
||||
->setValue(pht('Save Config Entry')));
|
||||
|
||||
$title = pht('Edit %s', $this->key);
|
||||
$short = pht('Edit');
|
||||
|
||||
$crumbs = $this->buildApplicationCrumbs($this->buildSideNavView());
|
||||
$crumbs->addCrumb(
|
||||
id(new PhabricatorCrumbView())
|
||||
->setName($this->key)
|
||||
->setHref('/config/edit/'.$this->key));
|
||||
$crumbs->addCrumb(
|
||||
id(new PhabricatorCrumbView())->setName($short));
|
||||
|
||||
return $this->buildApplicationPage(
|
||||
array(
|
||||
$crumbs,
|
||||
id(new PhabricatorHeaderView())->setHeader($title),
|
||||
$error_view,
|
||||
$form,
|
||||
),
|
||||
array(
|
||||
'title' => $title,
|
||||
'device' => true,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorConfigListController
|
||||
extends PhabricatorConfigController {
|
||||
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
$nav = $this->buildSideNavView();
|
||||
|
||||
$pager = new AphrontCursorPagerView();
|
||||
$pager->readFromRequest($request);
|
||||
|
||||
$config = new PhabricatorConfigFileSource('default');
|
||||
$list = $this->buildConfigList(array_keys($config->getAllKeys()));
|
||||
$list->setPager($pager);
|
||||
$list->setNoDataString(
|
||||
'No data. Something probably went wrong in reading the default config.');
|
||||
|
||||
$header = id(new PhabricatorHeaderView())
|
||||
->setHeader(pht('Configuration'));
|
||||
|
||||
$nav->appendChild(
|
||||
array(
|
||||
$header,
|
||||
$list,
|
||||
));
|
||||
|
||||
$crumbs = $this
|
||||
->buildApplicationCrumbs($nav)
|
||||
->addCrumb(
|
||||
id(new PhabricatorCrumbView())
|
||||
->setName(pht('Configuration'))
|
||||
->setHref($this->getApplicationURI('filter/')));
|
||||
|
||||
$nav->setCrumbs($crumbs);
|
||||
|
||||
return $this->buildApplicationPage(
|
||||
$nav,
|
||||
array(
|
||||
'title' => pht('Configuration'),
|
||||
'device' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function buildConfigList(array $keys) {
|
||||
$list = new PhabricatorObjectItemListView();
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$item = id(new PhabricatorObjectItemView())
|
||||
->setHeader($key)
|
||||
->setHref('/config/edit/'.$key)
|
||||
->setObject($key);
|
||||
$list->addItem($item);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
}
|
32
src/applications/config/storage/PhabricatorConfigEntry.php
Normal file
32
src/applications/config/storage/PhabricatorConfigEntry.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorConfigEntry extends PhabricatorConfigEntryDAO {
|
||||
|
||||
protected $id;
|
||||
protected $phid;
|
||||
protected $namespace;
|
||||
protected $configKey;
|
||||
protected $value;
|
||||
|
||||
// TODO: Remove this default when implementing Transactions.
|
||||
protected $isDeleted = 0;
|
||||
|
||||
public function getURI() {
|
||||
return '/config/edit/'.$this->configKey;
|
||||
}
|
||||
|
||||
public function getConfiguration() {
|
||||
return array(
|
||||
self::CONFIG_AUX_PHID => true,
|
||||
self::CONFIG_SERIALIZATION => array(
|
||||
'value' => self::SERIALIZATION_JSON,
|
||||
),
|
||||
) + parent::getConfiguration();
|
||||
}
|
||||
|
||||
public function generatePHID() {
|
||||
return PhabricatorPHID::generateNewPHID(
|
||||
PhabricatorPHIDConstants::PHID_TYPE_CONF);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorConfigEntryDAO extends PhabricatorLiskDAO {
|
||||
|
||||
public function getApplicationName() {
|
||||
return 'config';
|
||||
}
|
||||
|
||||
}
|
|
@ -30,6 +30,7 @@ final class PhabricatorPHIDConstants {
|
|||
const PHID_TYPE_ANSW = 'ANSW';
|
||||
const PHID_TYPE_MOCK = 'MOCK';
|
||||
const PHID_TYPE_MCRO = 'MCRO';
|
||||
const PHID_TYPE_CONF = 'CONF';
|
||||
|
||||
const PHID_TYPE_XACT = 'XACT';
|
||||
const PHID_TYPE_XCMT = 'XCMT';
|
||||
|
|
|
@ -77,6 +77,15 @@ final class PhabricatorObjectHandleData {
|
|||
$objects[$task->getPHID()] = $task;
|
||||
}
|
||||
break;
|
||||
case PhabricatorPHIDConstants::PHID_TYPE_CONF:
|
||||
$config_dao = new PhabricatorConfigEntry();
|
||||
$entries = $config_dao->loadAllWhere(
|
||||
'phid IN (%Ls)',
|
||||
$phids);
|
||||
foreach ($entries as $entry) {
|
||||
$objects[$entry->getPHID()] = $entry;
|
||||
}
|
||||
break;
|
||||
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
|
||||
$revision_dao = new DifferentialRevision();
|
||||
$revisions = $revision_dao->loadAllWhere(
|
||||
|
@ -366,6 +375,28 @@ final class PhabricatorObjectHandleData {
|
|||
$handles[$phid] = $handle;
|
||||
}
|
||||
break;
|
||||
case PhabricatorPHIDConstants::PHID_TYPE_CONF:
|
||||
$object = new PhabricatorConfigEntry();
|
||||
|
||||
$entries = $object->loadAllWhere('phid in (%Ls)', $phids);
|
||||
$entries = mpull($entries, null, 'getPHID');
|
||||
|
||||
foreach ($phids as $phid) {
|
||||
$handle = new PhabricatorObjectHandle();
|
||||
$handle->setPHID($phid);
|
||||
$handle->setType($type);
|
||||
if (empty($entries[$phid])) {
|
||||
$handle->setName('Unknown Config Entry');
|
||||
} else {
|
||||
$entry = $entry[$phid];
|
||||
$handle->setName($entry->getKey());
|
||||
$handle->setURI('/config/edit/'.$entry->getKey());
|
||||
$handle->setFullName($entry->getKey());
|
||||
$handle->setComplete(true);
|
||||
}
|
||||
$handles[$phid] = $handle;
|
||||
}
|
||||
break;
|
||||
case PhabricatorPHIDConstants::PHID_TYPE_FILE:
|
||||
$object = new PhabricatorFile();
|
||||
|
||||
|
|
|
@ -1064,6 +1064,14 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
|
|||
'type' => 'sql',
|
||||
'name' => $this->getPatchPath('20121220.generalcache.sql'),
|
||||
),
|
||||
'db.config' => array(
|
||||
'type' => 'db',
|
||||
'name' => 'config',
|
||||
),
|
||||
'20121226.config.sql' => array(
|
||||
'type' => 'sql',
|
||||
'name' => $this->getPatchPath('20121226.config.sql'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue