diff --git a/core/modules/config/config.routing.yml b/core/modules/config/config.routing.yml index 0fdb30b..61ff8e3 100644 --- a/core/modules/config/config.routing.yml +++ b/core/modules/config/config.routing.yml @@ -7,7 +7,7 @@ config.sync: _permission: 'synchronize configuration' config.diff: - path: '/admin/config/development/configuration/sync/diff/{source_name}/{target_name}' + path: '/admin/config/development/configuration/sync/diff/{source_storage}/{source_name}/{target_name}' defaults: _content: '\Drupal\config\Controller\ConfigController::diff' target_name: NULL @@ -15,7 +15,7 @@ config.diff: _permission: 'synchronize configuration' config.diff_collection: - path: '/admin/config/development/configuration/sync/diff_collection/{collection}/{source_name}/{target_name}' + path: '/admin/config/development/configuration/sync/diff_collection/{source_storage}/{collection}/{source_name}/{target_name}' defaults: _content: '\Drupal\config\Controller\ConfigController::diff' target_name: NULL diff --git a/core/modules/config/src/Controller/ConfigController.php b/core/modules/config/src/Controller/ConfigController.php index eeec671..a5039b3 100644 --- a/core/modules/config/src/Controller/ConfigController.php +++ b/core/modules/config/src/Controller/ConfigController.php @@ -22,18 +22,18 @@ class ConfigController implements ContainerInjectionInterface { /** - * The target storage. + * The active storage. * * @var \Drupal\Core\Config\StorageInterface */ - protected $targetStorage; + protected $activeStorage; /** - * The source storage. + * The staging storage. * * @var \Drupal\Core\Config\StorageInterface */ - protected $sourceStorage; + protected $stagingStorage; /** * The configuration manager. @@ -56,6 +56,7 @@ public static function create(ContainerInterface $container) { return new static( $container->get('config.storage'), $container->get('config.storage.staging'), + $container->get('config.storage.snapshot'), $container->get('config.manager'), new FileDownloadController() ); @@ -64,16 +65,19 @@ public static function create(ContainerInterface $container) { /** * Constructs a ConfigController object. * - * @param \Drupal\Core\Config\StorageInterface $target_storage - * The target storage. - * @param \Drupal\Core\Config\StorageInterface $source_storage - * The source storage + * @param \Drupal\Core\Config\StorageInterface $active_storage + * The active storage. + * @param \Drupal\Core\Config\StorageInterface $staging_storage + * The staging storage. + * @param \Drupal\Core\Config\StorageInterface $snapshot_storage + * The snapshot storage. * @param \Drupal\system\FileDownloadController $file_download_controller * The file download controller. */ - public function __construct(StorageInterface $target_storage, StorageInterface $source_storage, ConfigManagerInterface $config_manager, FileDownloadController $file_download_controller) { - $this->targetStorage = $target_storage; - $this->sourceStorage = $source_storage; + public function __construct(StorageInterface $active_storage, StorageInterface $staging_storage, StorageInterface $snapshot_storage, ConfigManagerInterface $config_manager, FileDownloadController $file_download_controller) { + $this->activeStorage = $active_storage; + $this->stagingStorage = $staging_storage; + $this->snapshotStorage = $snapshot_storage; $this->configManager = $config_manager; $this->fileDownloadController = $file_download_controller; } @@ -90,8 +94,8 @@ public function downloadExport() { $archiver->addString("$name.yml", Yaml::encode($this->configManager->getConfigFactory()->get($name)->getRawData())); } // Get all override data from the remaining collections. - foreach ($this->targetStorage->getAllCollectionNames() as $collection) { - $collection_storage = $this->targetStorage->createCollection($collection); + foreach ($this->activeStorage->getAllCollectionNames() as $collection) { + $collection_storage = $this->activeStorage->createCollection($collection); foreach ($collection_storage->listAll() as $name) { $archiver->addString(str_replace('.', '/', $collection) . "/$name.yml", Yaml::encode($collection_storage->read($name))); } @@ -104,6 +108,8 @@ public function downloadExport() { /** * Shows diff of specificed configuration file. * + * @param string $source_storage + * The name of the source storage to diff against active. * @param string $source_name * The name of the configuration file. * @param string $target_name @@ -116,25 +122,26 @@ public function downloadExport() { * @return string * Table showing a two-way diff between the active and staged configuration. */ - public function diff($source_name, $target_name = NULL, $collection = NULL) { + public function diff($source_storage, $source_name, $target_name = NULL, $collection = NULL) { if (!isset($collection)) { $collection = StorageInterface::DEFAULT_COLLECTION; } - $diff = $this->configManager->diff($this->targetStorage, $this->sourceStorage, $source_name, $target_name, $collection); + $source = $source_storage . 'Storage'; + $diff = $this->configManager->diff($this->activeStorage, $this->$source, $source_name, $target_name, $collection); $formatter = new \DrupalDiffFormatter(); $formatter->show_header = FALSE; $build = array(); - $build['#title'] = t('View changes of @config_file', array('@config_file' => $source_name)); + $build['#title'] = \Drupal::translation()->translate('View changes of @config_file', array('@config_file' => $source_name)); // Add the CSS for the inline diff. $build['#attached']['css'][] = drupal_get_path('module', 'system') . '/css/system.diff.css'; $build['diff'] = array( '#type' => 'table', '#header' => array( - array('data' => t('Old'), 'colspan' => '2'), - array('data' => t('New'), 'colspan' => '2'), + array('data' => $source_storage, 'colspan' => '2'), + array('data' => \Drupal::translation()->translate('Active'), 'colspan' => '2'), ), '#rows' => $formatter->format($diff), ); diff --git a/core/modules/config/src/Form/ConfigSync.php b/core/modules/config/src/Form/ConfigSync.php index 7c48dd5..c5f1bb0 100644 --- a/core/modules/config/src/Form/ConfigSync.php +++ b/core/modules/config/src/Form/ConfigSync.php @@ -36,18 +36,25 @@ class ConfigSync extends FormBase { protected $lock; /** - * The source configuration object. + * The staging storage configuration object. * * @var \Drupal\Core\Config\StorageInterface */ - protected $sourceStorage; + protected $stagingStorage; /** - * The target configuration object. + * The active storage configuration object. * * @var \Drupal\Core\Config\StorageInterface */ - protected $targetStorage; + protected $activeStorage; + + /** + * The snapshot storage configuration object. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $snapshotStorage; /** * Event dispatcher. @@ -94,10 +101,12 @@ class ConfigSync extends FormBase { /** * Constructs the object. * - * @param \Drupal\Core\Config\StorageInterface $sourceStorage - * The source storage object. - * @param \Drupal\Core\Config\StorageInterface $targetStorage - * The target storage manager. + * @param \Drupal\Core\Config\StorageInterface $stagingStorage + * The staging storage object. + * @param \Drupal\Core\Config\StorageInterface $activeStorage + * The active storage manager. + * @param \Drupal\Core\Config\StorageInterface $snapshotStorage + * The snapshot storage manager. * @param \Drupal\Core\Lock\LockBackendInterface $lock * The lock object. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher @@ -113,9 +122,10 @@ class ConfigSync extends FormBase { * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler * The theme handler */ - public function __construct(StorageInterface $sourceStorage, StorageInterface $targetStorage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, UrlGeneratorInterface $url_generator, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) { - $this->sourceStorage = $sourceStorage; - $this->targetStorage = $targetStorage; + public function __construct(StorageInterface $stagingStorage, StorageInterface $activeStorage, StorageInterface $snapshotStorage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, UrlGeneratorInterface $url_generator, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) { + $this->stagingStorage = $stagingStorage; + $this->activeStorage = $activeStorage; + $this->snapshotStorage = $snapshotStorage; $this->lock = $lock; $this->eventDispatcher = $event_dispatcher; $this->configManager = $config_manager; @@ -132,6 +142,7 @@ public static function create(ContainerInterface $container) { return new static( $container->get('config.storage.staging'), $container->get('config.storage'), + $container->get('config.storage.snapshot'), $container->get('lock'), $container->get('event_dispatcher'), $container->get('config.manager'), @@ -159,9 +170,108 @@ public function buildForm(array $form, array &$form_state) { '#value' => $this->t('Import all'), ); - $source_list = $this->sourceStorage->listAll(); - $storage_comparer = new StorageComparer($this->sourceStorage, $this->targetStorage, $this->configManager); - if (empty($source_list) || !$storage_comparer->createChangelist()->hasChanges()) { + $snapshot_comparer = new StorageComparer($this->activeStorage, $this->snapshotStorage, $this->configManager); + + if (empty($form_state['input']) && $snapshot_comparer->createChangelist()->hasChanges()) { + drupal_set_message($this->t('Changes have been made to your active configuration, which might be lost on the next import attempt.'), 'warning'); + + $form['snapshot']['heading'] = array( + '#type' => 'html_tag', + '#tag' => 'h1', + '#value' => $this->t('Changes to the Active Configuration'), + ); + foreach ($snapshot_comparer->getAllCollectionNames() as $collection) { + if ($collection != StorageInterface::DEFAULT_COLLECTION) { + $form['snapshot'][$collection]['collection_heading'] = array( + '#type' => 'html_tag', + '#tag' => 'h2', + '#value' => $this->t('!collection configuration collection', array('!collection' => $collection)), + ); + } + foreach ($snapshot_comparer->getChangelist(NULL, $collection) as $config_change_type => $config_names) { + if (empty($config_names)) { + continue; + } + + // @todo A table caption would be more appropriate, but does not have the + // visual importance of a heading. + $form['snapshot'][$collection][$config_change_type]['heading'] = array( + '#type' => 'html_tag', + '#tag' => 'h3', + ); + switch ($config_change_type) { + case 'create': + $form['snapshot'][$collection][$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count new', '@count new'); + break; + + case 'update': + $form['snapshot'][$collection][$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count changed', '@count changed'); + break; + + case 'delete': + $form['snapshot'][$collection][$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count removed', '@count removed'); + break; + + case 'rename': + $form['snapshot'][$collection][$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count renamed', '@count renamed'); + break; + } + $form['snapshot'][$collection][$config_change_type]['list'] = array( + '#type' => 'table', + '#header' => array('Name', 'Operations'), + ); + + foreach ($config_names as $config_name) { + if ($config_change_type == 'rename') { + $names = $snapshot_comparer->extractRenameNames($config_name); + $route_options = array('source_storage' => 'snapshot', 'source_name' => $names['old_name'], 'target_name' => $names['new_name']); + $config_name = $this->t('!source_name to !target_name', array('!source_name' => $names['old_name'], '!target_name' => $names['new_name'])); + } + else { + $route_options = array('source_storage' => 'snapshot', 'source_name' => $config_name); + } + if ($collection != StorageInterface::DEFAULT_COLLECTION) { + $route_options['collection'] = $collection; + $href = $this->urlGenerator->getPathFromRoute('config.diff_collection', $route_options); + } + else { + $href = $this->urlGenerator->getPathFromRoute('config.diff', $route_options); + } + $links['view_diff'] = array( + 'title' => $this->t('View differences'), + 'href' => $href, + 'attributes' => array( + 'class' => array('use-ajax'), + 'data-accepts' => 'application/vnd.drupal-modal', + 'data-dialog-options' => json_encode(array( + 'width' => 700, + )), + ), + ); + $form['snapshot'][$collection][$config_change_type]['list']['#rows'][] = array( + 'name' => $config_name, + 'operations' => array( + 'data' => array( + '#type' => 'operations', + '#links' => $links, + ), + ), + ); + } + } + } + } + + $source_list = $this->stagingStorage->listAll(); + $staging_comparer = new StorageComparer($this->stagingStorage, $this->activeStorage, $this->configManager); + + $form['heading'] = array( + '#type' => 'html_tag', + '#tag' => 'h1', + '#value' => $this->t('Changes to be Imported'), + ); + + if (empty($source_list) || !$staging_comparer->createChangelist()->hasChanges()) { $form['no_changes'] = array( '#type' => 'table', '#header' => array('Name', 'Operations'), @@ -171,20 +281,20 @@ public function buildForm(array $form, array &$form_state) { $form['actions']['#access'] = FALSE; return $form; } - elseif (!$storage_comparer->validateSiteUuid()) { + elseif (!$staging_comparer->validateSiteUuid()) { drupal_set_message($this->t('The staged configuration cannot be imported, because it originates from a different site than this site. You can only synchronize configuration between cloned instances of this site.'), 'error'); $form['actions']['#access'] = FALSE; return $form; } else { // Store the comparer for use in the submit. - $form_state['storage_comparer'] = $storage_comparer; + $form_state['staging_comparer'] = $staging_comparer; } // Add the AJAX library to the form for dialog support. $form['#attached']['library'][] = 'core/drupal.ajax'; - foreach ($storage_comparer->getAllCollectionNames() as $collection) { + foreach ($staging_comparer->getAllCollectionNames() as $collection) { if ($collection != StorageInterface::DEFAULT_COLLECTION) { $form[$collection]['collection_heading'] = array( '#type' => 'html_tag', @@ -192,7 +302,7 @@ public function buildForm(array $form, array &$form_state) { '#value' => $this->t('!collection configuration collection', array('!collection' => $collection)), ); } - foreach ($storage_comparer->getChangelist(NULL, $collection) as $config_change_type => $config_names) { + foreach ($staging_comparer->getChangelist(NULL, $collection) as $config_change_type => $config_names) { if (empty($config_names)) { continue; } @@ -227,12 +337,12 @@ public function buildForm(array $form, array &$form_state) { foreach ($config_names as $config_name) { if ($config_change_type == 'rename') { - $names = $storage_comparer->extractRenameNames($config_name); - $route_options = array('source_name' => $names['old_name'], 'target_name' => $names['new_name']); + $names = $staging_comparer->extractRenameNames($config_name); + $route_options = array('source_storage' => 'staging', 'source_name' => $names['old_name'], 'target_name' => $names['new_name']); $config_name = $this->t('!source_name to !target_name', array('!source_name' => $names['old_name'], '!target_name' => $names['new_name'])); } else { - $route_options = array('source_name' => $config_name); + $route_options = array('source_storage' => 'staging', 'source_name' => $config_name); } if ($collection != StorageInterface::DEFAULT_COLLECTION) { $route_options['collection'] = $collection; @@ -248,7 +358,7 @@ public function buildForm(array $form, array &$form_state) { 'class' => array('use-ajax'), 'data-accepts' => 'application/vnd.drupal-modal', 'data-dialog-options' => json_encode(array( - 'width' => 700 + 'width' => 700, )), ), ); @@ -272,7 +382,7 @@ public function buildForm(array $form, array &$form_state) { */ public function submitForm(array &$form, array &$form_state) { $config_importer = new ConfigImporter( - $form_state['storage_comparer'], + $form_state['staging_comparer'], $this->eventDispatcher, $this->configManager, $this->lock, @@ -284,16 +394,16 @@ public function submitForm(array &$form, array &$form_state) { if ($config_importer->alreadyImporting()) { drupal_set_message($this->t('Another request may be synchronizing configuration already.')); } - else{ + else { try { $sync_steps = $config_importer->initialize(); $batch = array( 'operations' => array(), 'finished' => array(get_class($this), 'finishBatch'), - 'title' => t('Synchronizing configuration'), - 'init_message' => t('Starting configuration synchronization.'), - 'progress_message' => t('Completed @current step of @total.'), - 'error_message' => t('Configuration synchronization has encountered an error.'), + 'title' => $this->t('Synchronizing configuration'), + 'init_message' => $this->t('Starting configuration synchronization.'), + 'progress_message' => $this->t('Completed @current step of @total.'), + 'error_message' => $this->t('Configuration synchronization has encountered an error.'), 'file' => drupal_get_path('module', 'config') . '/config.admin.inc', ); foreach ($sync_steps as $sync_step) {