countdown-8.x-1.8/src/Controller/CountdownAdminController.php
src/Controller/CountdownAdminController.php
<?php
declare(strict_types=1);
namespace Drupal\countdown\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Link;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\countdown\CountdownConstants;
use Drupal\countdown\CountdownLibraryPluginManager;
use Drupal\countdown\Form\CountdownSettings;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Controller for the Countdown admin pages.
*
* Provides the settings page with library status display
* and configuration form.
*
* @package Drupal\countdown\Controller
*/
class CountdownAdminController extends ControllerBase {
/**
* The plugin manager.
*
* @var \Drupal\countdown\CountdownLibraryPluginManager
*/
protected CountdownLibraryPluginManager $pluginManager;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected RequestStack $requestStack;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected RendererInterface $renderer;
/**
* Constructs a CountdownAdminController object.
*
* @param \Drupal\countdown\CountdownLibraryPluginManager $plugin_manager
* The plugin manager.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(CountdownLibraryPluginManager $plugin_manager, RequestStack $request_stack, RendererInterface $renderer) {
$this->pluginManager = $plugin_manager;
$this->requestStack = $request_stack;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.countdown_library'),
$container->get('request_stack'),
$container->get('renderer')
);
}
/**
* Settings page: renders library status block and settings form.
*
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse
* Render array for the settings page or redirect response.
*/
public function settingsPage() {
$request = $this->requestStack->getCurrentRequest();
// Optional refresh functionality.
if ($request && $request->query->get('refresh') === '1') {
$this->pluginManager->resetAllCaches();
$this->messenger()->addStatus($this->t('Library status refreshed.'));
// Redirect to remove the query parameter.
$url = Url::fromRoute('countdown.settings');
return new RedirectResponse($url->toString());
}
// Build the page content.
$build = [];
// Add page title.
$build['#title'] = $this->t('Countdown settings');
// Build status block.
$build['status'] = $this->buildLibraryStatusBlock();
// Build the settings form.
$build['form'] = $this->formBuilder()->getForm(CountdownSettings::class);
// Attach needed libraries.
$build['#attached']['library'][] = 'core/drupal.collapse';
$build['#attached']['library'][] = 'countdown/admin';
// Add cache contexts and tags.
$build['#cache'] = [
'contexts' => ['user.permissions'],
'tags' => [CountdownConstants::CACHE_TAG_SETTINGS],
];
return $build;
}
/**
* Builds the library status block.
*
* @return array
* Render array for status block.
*/
protected function buildLibraryStatusBlock(): array {
// Get all plugins and their statuses.
$plugin_statuses = $this->pluginManager->getAllPluginStatuses();
$config = $this->config('countdown.settings');
$active_library = $config->get('library');
$rows = [];
foreach ($plugin_statuses as $plugin_id => $status_info) {
/** @var \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin */
$plugin = $status_info['plugin'];
// Determine status classes and text.
$status_class = 'project-update__status--not-supported';
$gin_status = 'gin-status--danger';
$status_text = $this->t('Not Installed');
if ($plugin->isInstalled()) {
if ($status_info['version_status'] === 'ok') {
$status_class = 'project-update__status--current';
$gin_status = 'gin-status--success';
$status_text = $this->t('Installed');
}
elseif ($status_info['version_status'] === 'outdated') {
$status_class = 'project-update__status--not-current';
$gin_status = 'gin-status--warning';
$status_text = $this->t('Outdated');
}
else {
$status_class = 'project-update__status--not-supported';
$gin_status = 'gin-status--warning';
$status_text = $this->t('Unknown Version');
}
}
// Add active indicator if this is the active library.
if ($plugin_id === $active_library) {
$status_text .= ' (' . $this->t('Active') . ')';
}
// Build status pill.
$status_element = [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => [
'class' => [
'project-update__status',
$status_class,
'gin-status',
$gin_status,
],
],
'#value' => '<span class="gin-status-icon"></span><span>' . $status_text . '</span>',
'#allowed_tags' => ['span', 'div'],
];
// Version display.
$version_display = $this->buildVersionDisplay($plugin, $status_info);
// Path / Download element.
$path_element = $this->buildPathElement($plugin);
// Name cell with metadata.
$library_cell = $this->buildLibraryCell($plugin, $plugin_id);
// Version cell with HTML support.
$version_cell = [
'#type' => 'html_tag',
'#tag' => 'div',
'#value' => $version_display,
'#allowed_tags' => ['span'],
];
// Build table row.
$row = [
'data' => [
['data' => $library_cell],
['data' => $status_element],
['data' => $version_cell, 'class' => ['project-update__title']],
['data' => $path_element],
],
'class' => [$this->getRowClass($plugin, $status_info)],
];
// Add row attributes for active library.
if ($plugin_id === $active_library) {
$row['class'][] = 'countdown-active-library';
}
$rows[] = $row;
}
// Build table.
$table = [
'#type' => 'table',
'#header' => [
$this->t('Library'),
$this->t('Status'),
$this->t('Version'),
$this->t('Path / Installation'),
],
'#rows' => $rows,
'#empty' => $this->t('No libraries found.'),
'#attributes' => ['class' => ['countdown-library-status']],
];
// Build refresh link.
$refresh_url = Url::fromRoute('countdown.settings', [], [
'query' => ['refresh' => '1'],
'attributes' => ['class' => ['button', 'button--small']],
]);
$refresh_link = Link::fromTextAndUrl($this->t('Refresh library status'), $refresh_url)->toRenderable();
// Build statistics summary.
$summary = $this->buildStatisticsSummary();
// Assemble the complete status block.
return [
'#type' => 'container',
'#attributes' => ['class' => ['countdown-library-status', 'gin-layer-wrapper']],
'heading' => [
'#type' => 'html_tag',
'#tag' => 'h2',
'#value' => $this->t('Library Status'),
'#attributes' => ['class' => ['system-status-general-info__header']],
],
'details' => [
'#type' => 'details',
// Use the persisted hide config to determine initial open state.
'#open' => !$config->get('hide'),
'#title' => $this->t('Countdown Libraries'),
'#attributes' => [
'class' => ['claro-details', 'claro-details--system-status-report', 'countdown-library-status'],
],
'wrapper' => [
'#type' => 'container',
'#attributes' => [
'class' => ['claro-details__wrapper', 'claro-details__wrapper--system-status-report'],
],
'summary' => $summary,
'table' => $table,
'refresh' => $refresh_link,
],
],
'#attached' => [
'library' => ['countdown/admin'],
],
];
}
/**
* Build version display for a library.
*
* @param \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin
* The plugin instance.
* @param array $status_info
* Status information array.
*
* @return string
* HTML string for version display.
*/
protected function buildVersionDisplay($plugin, array $status_info): string {
if ($plugin->isInstalled()) {
$installed_version = $plugin->getInstalledVersion();
$required_version = $plugin->getRequiredVersion();
if ($installed_version) {
$version_display = $installed_version;
// Add comparison with required version if applicable.
if ($required_version && $installed_version !== $required_version) {
if (!$plugin->versionMeetsRequirements()) {
$version_display .= ' <span class="color-error">(' . $this->t('min: @version', ['@version' => $required_version]) . ')</span>';
}
}
}
else {
$version_display = '<span class="color-warning">' . $this->t('Unknown') . '</span>';
}
}
else {
// Not installed - show required version.
if ($required_version = $plugin->getRequiredVersion()) {
$version_display = '<span class="color-muted">' . $this->t('Required: @version', ['@version' => $required_version]) . '</span>';
}
else {
$version_display = '<span class="color-muted">' . $this->t('Any version') . '</span>';
}
}
return $version_display;
}
/**
* Build path/download element for a library.
*
* @param \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin
* The plugin instance.
*
* @return array
* Render array for path element.
*/
protected function buildPathElement($plugin): array {
if ($plugin->isInstalled()) {
// Show the library path.
$path = $plugin->getLibraryPath();
return [
'#type' => 'html_tag',
'#tag' => 'code',
'#value' => $path,
'#attributes' => ['class' => ['path']],
];
}
else {
// Show download/install options.
$links = [];
// NPM package info.
$npm_package = $plugin->getNpmPackage();
if ($npm_package) {
$links[] = [
'#type' => 'html_tag',
'#tag' => 'code',
'#value' => 'npm install ' . $npm_package,
];
}
// Homepage link.
$homepage = $plugin->getHomepage();
if ($homepage) {
$links[] = [
'#type' => 'link',
'#title' => $this->t('Download'),
'#url' => Url::fromUri($homepage),
'#attributes' => [
'target' => '_blank',
'rel' => 'noopener noreferrer',
'class' => ['button', 'button--small'],
],
];
}
if (empty($links)) {
return [
'#markup' => '<em>' . $this->t('Manual installation required') . '</em>',
];
}
return [
'#type' => 'container',
'#attributes' => ['class' => ['install-options']],
'items' => $links,
];
}
}
/**
* Build library name cell with metadata.
*
* @param \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin
* The plugin instance.
* @param string $plugin_id
* The plugin ID.
*
* @return array
* Render array for library cell.
*/
protected function buildLibraryCell($plugin, string $plugin_id): array {
// Build type flag.
$type_flag = $plugin->getType();
if ($plugin->isExperimental()) {
$type_flag .= ' | ' . $this->t('experimental');
}
// Check if plugin was added by another module.
$definition = $this->pluginManager->getDefinition($plugin_id, FALSE);
$provider = $definition['provider'] ?? 'countdown';
if ($provider !== 'countdown') {
$type_flag .= ' | ' . $provider;
}
$class = $plugin->isInstalled() ? 'countdown-library-status__status-title' : 'countdown-library-status__status-icon--error';
return [
'#type' => 'html_tag',
'#tag' => 'div',
'#value' =>
'<strong>' . $plugin->getLabel() . '</strong>' .
'<span class="gin-experimental-flag">' . $type_flag . '</span>' .
'<br/><small>' . $plugin->getDescription() . '</small>',
'#attributes' => ['class' => [$class]],
'#allowed_tags' => ['div', 'span', 'strong', 'br', 'small'],
];
}
/**
* Get row class based on plugin status.
*
* @param \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin
* The plugin instance.
* @param array $status_info
* Status information array.
*
* @return string
* CSS class for the row.
*/
protected function getRowClass($plugin, array $status_info): string {
if (!$plugin->isInstalled()) {
return 'color-error';
}
if ($status_info['version_status'] === 'ok') {
return 'color-success';
}
return 'color-warning';
}
/**
* Build statistics summary.
*
* @return array
* Render array for summary.
*/
protected function buildStatisticsSummary(): array {
$plugin_statuses = $this->pluginManager->getAllPluginStatuses();
$total_libraries = count($plugin_statuses);
$installed_plugins = $this->pluginManager->getInstalledPlugins();
$installed_count = count($installed_plugins);
$cdn_capable = count($this->pluginManager->getCdnCapablePlugins());
return [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => ['class' => ['countdown-summary']],
'#value' => $this->t('@installed of @total libraries installed (@cdn CDN-capable) <span class="countdown-api-info form-item__description">Developers can add custom countdown libraries using the Plugin API. See countdown.api.php for documentation.</span>', [
'@installed' => $installed_count,
'@total' => $total_libraries,
'@cdn' => $cdn_capable,
]),
];
}
}
