diff --git a/src/Controller/LogController.php b/src/Controller/LogController.php index 91b37269..94e50c5b 100644 --- a/src/Controller/LogController.php +++ b/src/Controller/LogController.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Controller; +use App\DataTables\Column\LogEntryTargetColumn; use App\DataTables\Filters\LogFilter; use App\DataTables\LogDataTable; use App\Entity\Base\AbstractDBElement; @@ -33,6 +34,9 @@ use App\Entity\LogSystem\ElementEditedLogEntry; use App\Form\Filters\LogFilterType; use App\Repository\DBElementRepository; use App\Services\LogSystem\EventUndoHelper; +use App\Services\LogSystem\LogEntryExtraFormatter; +use App\Services\LogSystem\LogLevelHelper; +use App\Services\LogSystem\LogTargetHelper; use App\Services\LogSystem\TimeTravel; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; @@ -93,6 +97,28 @@ class LogController extends AbstractController ]); } + /** + * @Route("/{id}/details", name="log_details") + * @param Request $request + * @param AbstractLogEntry $logEntry + * @return Response + */ + public function logDetails(Request $request, AbstractLogEntry $logEntry, LogEntryExtraFormatter $logEntryExtraFormatter, + LogLevelHelper $logLevelHelper, LogTargetHelper $logTargetHelper): Response + { + $this->denyAccessUnlessGranted('read', $logEntry); + + $extra_html = $logEntryExtraFormatter->format($logEntry); + $target_html = $logTargetHelper->formatTarget($logEntry); + + return $this->render('log_system/details/log_details.html.twig', [ + 'log_entry' => $logEntry, + 'extra_html' => $extra_html, + 'target_html' => $target_html, + 'log_level_helper' => $logLevelHelper, + ]); + } + /** * @Route("/undo", name="log_undo", methods={"POST"}) */ diff --git a/src/DataTables/Column/LogEntryTargetColumn.php b/src/DataTables/Column/LogEntryTargetColumn.php index 4aaeb069..0f61d567 100644 --- a/src/DataTables/Column/LogEntryTargetColumn.php +++ b/src/DataTables/Column/LogEntryTargetColumn.php @@ -36,6 +36,7 @@ use App\Exceptions\EntityNotSupportedException; use App\Repository\LogEntryRepository; use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; +use App\Services\LogSystem\LogTargetHelper; use Doctrine\ORM\EntityManagerInterface; use Omines\DataTablesBundle\Column\AbstractColumn; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -43,21 +44,11 @@ use Symfony\Contracts\Translation\TranslatorInterface; class LogEntryTargetColumn extends AbstractColumn { - protected EntityManagerInterface $em; - protected LogEntryRepository $entryRepository; - protected EntityURLGenerator $entityURLGenerator; - protected ElementTypeNameGenerator $elementTypeNameGenerator; - protected TranslatorInterface $translator; + private LogTargetHelper $logTargetHelper; - public function __construct(EntityManagerInterface $entityManager, EntityURLGenerator $entityURLGenerator, - ElementTypeNameGenerator $elementTypeNameGenerator, TranslatorInterface $translator) + public function __construct(LogTargetHelper $logTargetHelper) { - $this->em = $entityManager; - $this->entryRepository = $entityManager->getRepository(AbstractLogEntry::class); - - $this->entityURLGenerator = $entityURLGenerator; - $this->elementTypeNameGenerator = $elementTypeNameGenerator; - $this->translator = $translator; + $this->logTargetHelper = $logTargetHelper; } /** @@ -80,71 +71,9 @@ class LogEntryTargetColumn extends AbstractColumn public function render($value, $context): string { - if ($context instanceof UserNotAllowedLogEntry && $this->options['showAccessDeniedPath']) { - return htmlspecialchars($context->getPath()); - } - - /** @var AbstractLogEntry $context */ - $target = $this->entryRepository->getTargetElement($context); - - $tmp = ''; - - //The element is existing - if ($target instanceof NamedElementInterface && !empty($target->getName())) { - try { - $tmp = sprintf( - '%s', - $this->entityURLGenerator->infoURL($target), - $this->elementTypeNameGenerator->getTypeNameCombination($target, true) - ); - } catch (EntityNotSupportedException $exception) { - $tmp = $this->elementTypeNameGenerator->getTypeNameCombination($target, true); - } - } elseif ($target instanceof AbstractDBElement) { //Target does not have a name - $tmp = sprintf( - '%s: %s', - $this->elementTypeNameGenerator->getLocalizedTypeLabel($target), - $target->getID() - ); - } elseif (null === $target && $context->hasTarget()) { //Element was deleted - $tmp = sprintf( - '%s: %s [%s]', - $this->elementTypeNameGenerator->getLocalizedTypeLabel($context->getTargetClass()), - $context->getTargetID(), - $this->translator->trans('log.target_deleted') - ); - } - - //Add a hint to the associated element if possible - if (null !== $target && $this->options['show_associated']) { - if ($target instanceof Attachment && null !== $target->getElement()) { - $on = $target->getElement(); - } elseif ($target instanceof AbstractParameter && null !== $target->getElement()) { - $on = $target->getElement(); - } elseif ($target instanceof PartLot && null !== $target->getPart()) { - $on = $target->getPart(); - } elseif ($target instanceof Orderdetail && null !== $target->getPart()) { - $on = $target->getPart(); - } elseif ($target instanceof Pricedetail && null !== $target->getOrderdetail() && null !== $target->getOrderdetail()->getPart()) { - $on = $target->getOrderdetail()->getPart(); - } elseif ($target instanceof ProjectBOMEntry && null !== $target->getProject()) { - $on = $target->getProject(); - } - - if (isset($on) && is_object($on)) { - try { - $tmp .= sprintf( - ' (%s)', - $this->entityURLGenerator->infoURL($on), - $this->elementTypeNameGenerator->getTypeNameCombination($on, true) - ); - } catch (EntityNotSupportedException $exception) { - $tmp .= ' ('.$this->elementTypeNameGenerator->getTypeNameCombination($target, true).')'; - } - } - } - - //Log is not associated with an element - return $tmp; + return $this->logTargetHelper->formatTarget($context, [ + 'showAccessDeniedPath' => $this->options['showAccessDeniedPath'], + 'show_associated' => $this->options['show_associated'], + ]); } } diff --git a/src/DataTables/LogDataTable.php b/src/DataTables/LogDataTable.php index 85ab3113..554861b5 100644 --- a/src/DataTables/LogDataTable.php +++ b/src/DataTables/LogDataTable.php @@ -44,6 +44,7 @@ use App\Exceptions\EntityNotSupportedException; use App\Repository\LogEntryRepository; use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; +use App\Services\LogSystem\LogLevelHelper; use App\Services\UserSystem\UserAvatarHelper; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; @@ -70,10 +71,11 @@ class LogDataTable implements DataTableTypeInterface protected LogEntryRepository $logRepo; protected Security $security; protected UserAvatarHelper $userAvatarHelper; + protected LogLevelHelper $logLevelHelper; public function __construct(ElementTypeNameGenerator $elementTypeNameGenerator, TranslatorInterface $translator, UrlGeneratorInterface $urlGenerator, EntityURLGenerator $entityURLGenerator, EntityManagerInterface $entityManager, - Security $security, UserAvatarHelper $userAvatarHelper) + Security $security, UserAvatarHelper $userAvatarHelper, LogLevelHelper $logLevelHelper) { $this->elementTypeNameGenerator = $elementTypeNameGenerator; $this->translator = $translator; @@ -82,6 +84,7 @@ class LogDataTable implements DataTableTypeInterface $this->logRepo = $entityManager->getRepository(AbstractLogEntry::class); $this->security = $security; $this->userAvatarHelper = $userAvatarHelper; + $this->logLevelHelper = $logLevelHelper; } public function configureOptions(OptionsResolver $optionsResolver): void @@ -115,69 +118,18 @@ class LogDataTable implements DataTableTypeInterface //This special $$rowClass column is used to set the row class depending on the log level. The class gets set by the frontend controller $dataTable->add('dont_matter', RowClassColumn::class, [ - 'render' => static function ($value, AbstractLogEntry $context) { - switch ($context->getLevel()) { - case AbstractLogEntry::LEVEL_EMERGENCY: - case AbstractLogEntry::LEVEL_ALERT: - case AbstractLogEntry::LEVEL_CRITICAL: - case AbstractLogEntry::LEVEL_ERROR: - return 'table-danger'; - case AbstractLogEntry::LEVEL_WARNING: - return 'table-warning'; - case AbstractLogEntry::LEVEL_NOTICE: - return 'table-info'; - default: - return ''; - } + 'render' => function ($value, AbstractLogEntry $context) { + return $this->logLevelHelper->logLevelToTableColorClass($context->getLevelString()); }, ]); $dataTable->add('symbol', TextColumn::class, [ 'label' => '', 'className' => 'no-colvis', - 'render' => static function ($value, AbstractLogEntry $context) { - switch ($context->getLevelString()) { - case LogLevel::DEBUG: - $symbol = 'fa-bug'; - - break; - case LogLevel::INFO: - $symbol = 'fa-info'; - - break; - case LogLevel::NOTICE: - $symbol = 'fa-flag'; - - break; - case LogLevel::WARNING: - $symbol = 'fa-exclamation-circle'; - - break; - case LogLevel::ERROR: - $symbol = 'fa-exclamation-triangle'; - - break; - case LogLevel::CRITICAL: - $symbol = 'fa-bolt'; - - break; - case LogLevel::ALERT: - $symbol = 'fa-radiation'; - - break; - case LogLevel::EMERGENCY: - $symbol = 'fa-skull-crossbones'; - - break; - default: - $symbol = 'fa-question-circle'; - - break; - } - + 'render' => function ($value, AbstractLogEntry $context) { return sprintf( '', - $symbol, + $this->logLevelHelper->logLevelToIconClass($context->getLevelString()), $context->getLevelString() ); }, @@ -191,6 +143,12 @@ class LogDataTable implements DataTableTypeInterface $dataTable->add('timestamp', LocaleDateTimeColumn::class, [ 'label' => 'log.timestamp', 'timeFormat' => 'medium', + 'render' => function (string $value, AbstractLogEntry $context) { + return sprintf('%s', + $this->urlGenerator->generate('log_details', ['id' => $context->getId()]), + $value + ); + } ]); $dataTable->add('type', TextColumn::class, [ diff --git a/src/Security/Voter/LogEntryVoter.php b/src/Security/Voter/LogEntryVoter.php index b03cd99f..058dac0b 100644 --- a/src/Security/Voter/LogEntryVoter.php +++ b/src/Security/Voter/LogEntryVoter.php @@ -53,7 +53,7 @@ class LogEntryVoter extends ExtendedVoter protected function supports($attribute, $subject): bool { if ($subject instanceof AbstractLogEntry) { - return in_array($subject, static::ALLOWED_OPS, true); + return in_array($attribute, static::ALLOWED_OPS, true); } return false; diff --git a/src/Services/LogSystem/LogLevelHelper.php b/src/Services/LogSystem/LogLevelHelper.php new file mode 100644 index 00000000..926531da --- /dev/null +++ b/src/Services/LogSystem/LogLevelHelper.php @@ -0,0 +1,80 @@ +. + */ + +namespace App\Services\LogSystem; + +use App\Entity\LogSystem\AbstractLogEntry; +use Psr\Log\LogLevel; + +class LogLevelHelper +{ + /** + * Returns the FontAwesome icon class for the given log level. + * This returns just the specific icon class (so 'fa-info' for example). + * @param string $logLevel The string representation of the log level (one of the LogLevel::* constants) + * @return string + */ + public function logLevelToIconClass(string $logLevel): string + { + switch ($logLevel) { + case LogLevel::DEBUG: + return 'fa-bug'; + case LogLevel::INFO: + return 'fa-info'; + case LogLevel::NOTICE: + return 'fa-flag'; + case LogLevel::WARNING: + return 'fa-exclamation-circle'; + case LogLevel::ERROR: + return 'fa-exclamation-triangle'; + case LogLevel::CRITICAL: + return 'fa-bolt'; + case LogLevel::ALERT: + return 'fa-radiation'; + case LogLevel::EMERGENCY: + return 'fa-skull-crossbones'; + default: + return 'fa-question-circle'; + } + } + + /** + * Returns the Bootstrap table color class for the given log level. + * @param string $logLevel The string representation of the log level (one of the LogLevel::* constants) + * @return string The table color class (one of the 'table-*' classes) + */ + public function logLevelToTableColorClass(string $logLevel): string + { + + switch ($logLevel) { + case LogLevel::EMERGENCY: + case LogLevel::ALERT: + case LogLevel::CRITICAL: + case LogLevel::ERROR: + return 'table-danger'; + case LogLevel::WARNING: + return 'table-warning'; + case LogLevel::NOTICE: + return 'table-info'; + default: + return ''; + } + } +} \ No newline at end of file diff --git a/src/Services/LogSystem/LogTargetHelper.php b/src/Services/LogSystem/LogTargetHelper.php new file mode 100644 index 00000000..852afa16 --- /dev/null +++ b/src/Services/LogSystem/LogTargetHelper.php @@ -0,0 +1,141 @@ +. + */ + +namespace App\Services\LogSystem; + +use App\Entity\Attachments\Attachment; +use App\Entity\Base\AbstractDBElement; +use App\Entity\Contracts\NamedElementInterface; +use App\Entity\LogSystem\AbstractLogEntry; +use App\Entity\LogSystem\UserNotAllowedLogEntry; +use App\Entity\Parameters\AbstractParameter; +use App\Entity\Parts\PartLot; +use App\Entity\PriceInformations\Orderdetail; +use App\Entity\PriceInformations\Pricedetail; +use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Exceptions\EntityNotSupportedException; +use App\Repository\LogEntryRepository; +use App\Services\ElementTypeNameGenerator; +use App\Services\EntityURLGenerator; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +class LogTargetHelper +{ + protected EntityManagerInterface $em; + protected LogEntryRepository $entryRepository; + protected EntityURLGenerator $entityURLGenerator; + protected ElementTypeNameGenerator $elementTypeNameGenerator; + protected TranslatorInterface $translator; + + public function __construct(EntityManagerInterface $entityManager, EntityURLGenerator $entityURLGenerator, + ElementTypeNameGenerator $elementTypeNameGenerator, TranslatorInterface $translator) + { + $this->em = $entityManager; + $this->entryRepository = $entityManager->getRepository(AbstractLogEntry::class); + + $this->entityURLGenerator = $entityURLGenerator; + $this->elementTypeNameGenerator = $elementTypeNameGenerator; + $this->translator = $translator; + } + + private function configureOptions(OptionsResolver $resolver): self + { + $resolver->setDefault('show_associated', true); + $resolver->setDefault('showAccessDeniedPath', true); + + return $this; + } + + public function formatTarget(AbstractLogEntry $context, array $options = []): string + { + $optionsResolver = new OptionsResolver(); + $this->configureOptions($optionsResolver); + $options = $optionsResolver->resolve($options); + + if ($context instanceof UserNotAllowedLogEntry && $options['showAccessDeniedPath']) { + return htmlspecialchars($context->getPath()); + } + + /** @var AbstractLogEntry $context */ + $target = $this->entryRepository->getTargetElement($context); + + $tmp = ''; + + //The element is existing + if ($target instanceof NamedElementInterface && !empty($target->getName())) { + try { + $tmp = sprintf( + '%s', + $this->entityURLGenerator->infoURL($target), + $this->elementTypeNameGenerator->getTypeNameCombination($target, true) + ); + } catch (EntityNotSupportedException $exception) { + $tmp = $this->elementTypeNameGenerator->getTypeNameCombination($target, true); + } + } elseif ($target instanceof AbstractDBElement) { //Target does not have a name + $tmp = sprintf( + '%s: %s', + $this->elementTypeNameGenerator->getLocalizedTypeLabel($target), + $target->getID() + ); + } elseif (null === $target && $context->hasTarget()) { //Element was deleted + $tmp = sprintf( + '%s: %s [%s]', + $this->elementTypeNameGenerator->getLocalizedTypeLabel($context->getTargetClass()), + $context->getTargetID(), + $this->translator->trans('log.target_deleted') + ); + } + + //Add a hint to the associated element if possible + if (null !== $target && $options['show_associated']) { + if ($target instanceof Attachment && null !== $target->getElement()) { + $on = $target->getElement(); + } elseif ($target instanceof AbstractParameter && null !== $target->getElement()) { + $on = $target->getElement(); + } elseif ($target instanceof PartLot && null !== $target->getPart()) { + $on = $target->getPart(); + } elseif ($target instanceof Orderdetail && null !== $target->getPart()) { + $on = $target->getPart(); + } elseif ($target instanceof Pricedetail && null !== $target->getOrderdetail() && null !== $target->getOrderdetail()->getPart()) { + $on = $target->getOrderdetail()->getPart(); + } elseif ($target instanceof ProjectBOMEntry && null !== $target->getProject()) { + $on = $target->getProject(); + } + + if (isset($on) && is_object($on)) { + try { + $tmp .= sprintf( + ' (%s)', + $this->entityURLGenerator->infoURL($on), + $this->elementTypeNameGenerator->getTypeNameCombination($on, true) + ); + } catch (EntityNotSupportedException $exception) { + $tmp .= ' ('.$this->elementTypeNameGenerator->getTypeNameCombination($target, true).')'; + } + } + } + + //Log is not associated with an element + return $tmp; + } +} \ No newline at end of file diff --git a/templates/log_system/details/log_details.html.twig b/templates/log_system/details/log_details.html.twig new file mode 100644 index 00000000..84681296 --- /dev/null +++ b/templates/log_system/details/log_details.html.twig @@ -0,0 +1,63 @@ +{% extends "main_card.html.twig" %} + +{% import "helper.twig" as helper %} + +{% block title %} + {% trans %}log.details.title{% endtrans %}: + {{ ('log.type.' ~ log_entry.type) | trans }} ({{ log_entry.timestamp | format_datetime('short') }}) +{% endblock %} + +{% block card_title %} + + {% trans %}log.details.title{% endtrans %}: + {{ ('log.type.' ~ log_entry.type) | trans }} ({{ log_entry.timestamp | format_datetime('short') }}) + ID: {{ log_entry.iD }} +{% endblock %} + +{% block card_body %} + + + + + + + + + + + + + + + + + + + + +
{% trans %}log.timestamp{% endtrans %}{{ log_entry.timestamp | format_datetime('full') }}
{% trans %}log.type{% endtrans %} + {{ ('log.type.' ~ log_entry.type) | trans }} + {% if log_entry.type == 'part_stock_changed' %} + ({{ ('log.part_stock_changed.' ~ log_entry.instockChangeType)|trans }}) + {% endif %} +
{% trans %}log.level{% endtrans %} + + {{ ('log.level.'~ log_entry.levelString)|trans }} +
{% trans %}log.user{% endtrans %} + + {% if log_entry.cLIEntry %} + + {{ log_entry.cLIUsername }} ({% trans %}log.cli_user{% endtrans %}) + {% else %} + {% if log_entry.user %} + {{ helper.user_icon_link(log_entry.user) }} (@{{ log_entry.user.username }}) + {% else %} + @{{ log_entry.username }} ({% trans %}log.target_deleted{% endtrans %} + {% endif %} + {% endif %} +
{% trans %}log.target{% endtrans %}{{ target_html|raw }}
+ +
+ {{ extra_html | raw }} +
+{% endblock %} \ No newline at end of file diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 05b7046f..6f90af13 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -11289,5 +11289,11 @@ Element 3 Show email on public profile page + + + log.details.title + Log details + +