From c7f5c23374d7c40d128e96c65e362bd175ba776b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 11 Sep 2022 18:45:31 +0200 Subject: [PATCH] Implement a filter for Log Table (part 1) --- src/Controller/LogController.php | 15 +- src/DataTables/Filters/AttachmentFilter.php | 3 +- .../Filters/Constraints/ChoiceConstraint.php | 6 +- .../Filters/Constraints/EntityConstraint.php | 14 +- src/DataTables/Filters/LogFilter.php | 110 +++++++++++++ src/DataTables/LogDataTable.php | 25 ++- src/Entity/LogSystem/AbstractLogEntry.php | 2 +- src/Form/Filters/AttachmentFilterType.php | 4 +- src/Form/Filters/LogFilterType.php | 153 ++++++++++++++++++ src/Form/Filters/PartFilterType.php | 16 +- templates/LogSystem/log_list.html.twig | 47 ++++++ translations/messages.en.xlf | 72 +++++++++ 12 files changed, 444 insertions(+), 23 deletions(-) create mode 100644 src/DataTables/Filters/LogFilter.php create mode 100644 src/Form/Filters/LogFilterType.php diff --git a/src/Controller/LogController.php b/src/Controller/LogController.php index 991cd0e4..c4c81df8 100644 --- a/src/Controller/LogController.php +++ b/src/Controller/LogController.php @@ -42,6 +42,7 @@ declare(strict_types=1); namespace App\Controller; +use App\DataTables\Filters\LogFilter; use App\DataTables\LogDataTable; use App\Entity\Base\AbstractDBElement; use App\Entity\LogSystem\AbstractLogEntry; @@ -49,6 +50,7 @@ use App\Entity\LogSystem\CollectionElementDeleted; use App\Entity\LogSystem\ElementCreatedLogEntry; use App\Entity\LogSystem\ElementDeletedLogEntry; use App\Entity\LogSystem\ElementEditedLogEntry; +use App\Form\Filters\LogFilterType; use App\Services\LogSystem\EventUndoHelper; use App\Services\LogSystem\TimeTravel; use Doctrine\ORM\EntityManagerInterface; @@ -86,7 +88,17 @@ class LogController extends AbstractController { $this->denyAccessUnlessGranted('@system.show_logs'); - $table = $dataTable->createFromType(LogDataTable::class) + $formRequest = clone $request; + $formRequest->setMethod('GET'); + $filter = new LogFilter(); + + $filterForm = $this->createForm(LogFilterType::class, $filter, ['method' => 'GET']); + + $filterForm->handleRequest($formRequest); + + $table = $dataTable->createFromType(LogDataTable::class, [ + 'filter' => $filter, + ]) ->handleRequest($request); if ($table->isCallback()) { @@ -95,6 +107,7 @@ class LogController extends AbstractController return $this->render('LogSystem/log_list.html.twig', [ 'datatable' => $table, + 'filterForm' => $filterForm->createView(), ]); } diff --git a/src/DataTables/Filters/AttachmentFilter.php b/src/DataTables/Filters/AttachmentFilter.php index 81c33deb..bbdadca2 100644 --- a/src/DataTables/Filters/AttachmentFilter.php +++ b/src/DataTables/Filters/AttachmentFilter.php @@ -6,6 +6,7 @@ use App\DataTables\Filters\Constraints\BooleanConstraint; use App\DataTables\Filters\Constraints\DateTimeConstraint; use App\DataTables\Filters\Constraints\EntityConstraint; use App\DataTables\Filters\Constraints\InstanceOfConstraint; +use App\DataTables\Filters\Constraints\IntConstraint; use App\DataTables\Filters\Constraints\NumberConstraint; use App\DataTables\Filters\Constraints\TextConstraint; use App\Entity\Attachments\AttachmentType; @@ -40,7 +41,7 @@ class AttachmentFilter implements FilterInterface public function __construct(NodesListBuilder $nodesListBuilder) { - $this->dbId = new NumberConstraint('attachment.id'); + $this->dbId = new IntConstraint('attachment.id'); $this->name = new TextConstraint('attachment.name'); $this->targetType = new InstanceOfConstraint('attachment'); $this->attachmentType = new EntityConstraint($nodesListBuilder, AttachmentType::class, 'attachment.attachment_type'); diff --git a/src/DataTables/Filters/Constraints/ChoiceConstraint.php b/src/DataTables/Filters/Constraints/ChoiceConstraint.php index c1355889..63737cc0 100644 --- a/src/DataTables/Filters/Constraints/ChoiceConstraint.php +++ b/src/DataTables/Filters/Constraints/ChoiceConstraint.php @@ -9,7 +9,7 @@ class ChoiceConstraint extends AbstractConstraint public const ALLOWED_OPERATOR_VALUES = ['ANY', 'NONE']; /** - * @var string[] The values to compare to + * @var string[]|int[] The values to compare to */ protected $value; @@ -19,7 +19,7 @@ class ChoiceConstraint extends AbstractConstraint protected $operator; /** - * @return string[] + * @return string[]|int[] */ public function getValue(): array { @@ -27,7 +27,7 @@ class ChoiceConstraint extends AbstractConstraint } /** - * @param string[] $value + * @param string[]|int[] $value * @return ChoiceConstraint */ public function setValue(array $value): ChoiceConstraint diff --git a/src/DataTables/Filters/Constraints/EntityConstraint.php b/src/DataTables/Filters/Constraints/EntityConstraint.php index 0e2ec7ce..359507c0 100644 --- a/src/DataTables/Filters/Constraints/EntityConstraint.php +++ b/src/DataTables/Filters/Constraints/EntityConstraint.php @@ -36,18 +36,22 @@ class EntityConstraint extends AbstractConstraint protected $value; /** - * @param NodesListBuilder $nodesListBuilder - * @param class-string $class + * @param NodesListBuilder|null $nodesListBuilder + * @param class-string $class * @param string $property * @param string|null $identifier - * @param $value - * @param string $operator + * @param null $value + * @param string $operator */ - public function __construct(NodesListBuilder $nodesListBuilder, string $class, string $property, string $identifier = null, $value = null, string $operator = '') + public function __construct(?NodesListBuilder $nodesListBuilder, string $class, string $property, string $identifier = null, $value = null, string $operator = '') { $this->nodesListBuilder = $nodesListBuilder; $this->class = $class; + if ($nodesListBuilder === null && $this->isStructural()) { + throw new \InvalidArgumentException('NodesListBuilder must be provided for structural entities'); + } + parent::__construct($property, $identifier); $this->value = $value; $this->operator = $operator; diff --git a/src/DataTables/Filters/LogFilter.php b/src/DataTables/Filters/LogFilter.php new file mode 100644 index 00000000..ad24b29e --- /dev/null +++ b/src/DataTables/Filters/LogFilter.php @@ -0,0 +1,110 @@ +timestamp = new DateTimeConstraint('log.timestamp'); + $this->dbId = new IntConstraint('log.id'); + $this->level = new ChoiceConstraint('log.level'); + $this->eventType = new InstanceOfConstraint('log'); + $this->user = new EntityConstraint(null, User::class, 'log.user'); + + $this->targetType = new ChoiceConstraint('log.target_type'); + $this->targetId = new IntConstraint('log.target_id'); + } + + public function apply(QueryBuilder $queryBuilder): void + { + $this->applyAllChildFilters($queryBuilder); + } + + /** + * @return DateTimeConstraint + */ + public function getTimestamp(): DateTimeConstraint + { + return $this->timestamp; + } + + /** + * @return IntConstraint|NumberConstraint + */ + public function getDbId() + { + return $this->dbId; + } + + /** + * @return ChoiceConstraint + */ + public function getLevel(): ChoiceConstraint + { + return $this->level; + } + + /** + * @return InstanceOfConstraint + */ + public function getEventType(): InstanceOfConstraint + { + return $this->eventType; + } + + /** + * @return ChoiceConstraint + */ + public function getTargetType(): ChoiceConstraint + { + return $this->targetType; + } + + /** + * @return IntConstraint + */ + public function getTargetId(): IntConstraint + { + return $this->targetId; + } + + public function getUser(): EntityConstraint + { + return $this->user; + } + + +} \ No newline at end of file diff --git a/src/DataTables/LogDataTable.php b/src/DataTables/LogDataTable.php index 5e68f5d4..20a6c641 100644 --- a/src/DataTables/LogDataTable.php +++ b/src/DataTables/LogDataTable.php @@ -47,6 +47,8 @@ use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\LogEntryExtraColumn; use App\DataTables\Column\LogEntryTargetColumn; use App\DataTables\Column\RevertLogColumn; +use App\DataTables\Filters\AttachmentFilter; +use App\DataTables\Filters\LogFilter; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\TimeTravelInterface; use App\Entity\LogSystem\AbstractLogEntry; @@ -61,6 +63,7 @@ use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; use Omines\DataTablesBundle\Column\TextColumn; use Omines\DataTablesBundle\DataTable; @@ -97,10 +100,12 @@ class LogDataTable implements DataTableTypeInterface $optionsResolver->setDefaults([ 'mode' => 'system_log', 'filter_elements' => [], + 'filter' => null, ]); $optionsResolver->setAllowedTypes('filter_elements', ['array', 'object']); $optionsResolver->setAllowedTypes('mode', 'string'); + $optionsResolver->setAllowedTypes('filter', LogFilter::class); $optionsResolver->setNormalizer('filter_elements', static function (Options $options, $value) { if (!is_array($value)) { @@ -192,8 +197,8 @@ class LogDataTable implements DataTableTypeInterface 'label' => $this->translator->trans('log.level'), 'visible' => 'system_log' === $options['mode'], 'propertyPath' => 'levelString', - 'render' => static function (string $value, AbstractLogEntry $context) { - return $value; + 'render' => function (string $value, AbstractLogEntry $context) { + return $this->translator->trans('log.level.'.$value); }, ]); @@ -271,9 +276,24 @@ class LogDataTable implements DataTableTypeInterface 'query' => function (QueryBuilder $builder) use ($options): void { $this->getQuery($builder, $options); }, + 'criteria' => [ + function (QueryBuilder $builder) use ($options): void { + $this->buildCriteria($builder, $options); + }, + new SearchCriteriaProvider(), + ], ]); } + private function buildCriteria(QueryBuilder $builder, array $options): void + { + if (!empty($options['filter'])) { + $filter = $options['filter']; + $filter->apply($builder); + } + + } + protected function getQuery(QueryBuilder $builder, array $options): void { $builder->distinct()->select('log') @@ -281,6 +301,7 @@ class LogDataTable implements DataTableTypeInterface ->from(AbstractLogEntry::class, 'log') ->leftJoin('log.user', 'user'); + /* Do this here as we don't want to show up the global count of all log entries in the footer line, with these modes */ if ('last_activity' === $options['mode']) { $builder->where('log INSTANCE OF '.ElementCreatedLogEntry::class) ->orWhere('log INSTANCE OF '.ElementDeletedLogEntry::class) diff --git a/src/Entity/LogSystem/AbstractLogEntry.php b/src/Entity/LogSystem/AbstractLogEntry.php index 4496a77a..83440719 100644 --- a/src/Entity/LogSystem/AbstractLogEntry.php +++ b/src/Entity/LogSystem/AbstractLogEntry.php @@ -447,6 +447,6 @@ abstract class AbstractLogEntry extends AbstractDBElement } } - throw new InvalidArgumentException('No target ID for this class is existing!'); + throw new InvalidArgumentException('No target ID for this class is existing! (Class: '.$class.')'); } } diff --git a/src/Form/Filters/AttachmentFilterType.php b/src/Form/Filters/AttachmentFilterType.php index dfdb9527..69325b8d 100644 --- a/src/Form/Filters/AttachmentFilterType.php +++ b/src/Form/Filters/AttachmentFilterType.php @@ -23,7 +23,7 @@ use App\Form\Filters\Constraints\BooleanConstraintType; use App\Form\Filters\Constraints\DateTimeConstraintType; use App\Form\Filters\Constraints\InstanceOfConstraintType; use App\Form\Filters\Constraints\NumberConstraintType; -use App\Form\Filters\Constraints\StructuralEntityConstraintType; +use App\Form\Filters\Constraints\UserEntityConstraintType; use App\Form\Filters\Constraints\TextConstraintType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ResetType; @@ -73,7 +73,7 @@ class AttachmentFilterType extends AbstractType ] ]); - $builder->add('attachmentType', StructuralEntityConstraintType::class, [ + $builder->add('attachmentType', UserEntityConstraintType::class, [ 'label' => 'attachment.attachment_type', 'entity_class' => AttachmentType::class ]); diff --git a/src/Form/Filters/LogFilterType.php b/src/Form/Filters/LogFilterType.php new file mode 100644 index 00000000..c1f2fed3 --- /dev/null +++ b/src/Form/Filters/LogFilterType.php @@ -0,0 +1,153 @@ + AbstractLogEntry::LEVEL_DEBUG, + 'log.level.info' => AbstractLogEntry::LEVEL_INFO, + 'log.level.notice' => AbstractLogEntry::LEVEL_NOTICE, + 'log.level.warning' => AbstractLogEntry::LEVEL_WARNING, + 'log.level.error' => AbstractLogEntry::LEVEL_ERROR, + 'log.level.critical' => AbstractLogEntry::LEVEL_CRITICAL, + 'log.level.alert' => AbstractLogEntry::LEVEL_ALERT, + 'log.level.emergency' => AbstractLogEntry::LEVEL_EMERGENCY, + ]; + + protected const TARGET_TYPE_CHOICES = [ + 'log.type.collection_element_deleted' => CollectionElementDeleted::class, + 'log.type.database_updated' => DatabaseUpdatedLogEntry::class, + 'log.type.element_created' => ElementCreatedLogEntry::class, + 'log.type.element_deleted' => ElementDeletedLogEntry::class, + 'log.type.element_edited' => ElementEditedLogEntry::class, + 'log.type.security' => SecurityEventLogEntry::class, + 'log.type.user_login' => UserLoginLogEntry::class, + 'log.type.user_logout' => UserLogoutLogEntry::class, + 'log.type.user_not_allowed' => UserNotAllowedLogEntry::class, + + //Legacy entries + 'log.type.instock_changed' => InstockChangedLogEntry::class, + ]; + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'compound' => true, + 'data_class' => LogFilter::class, + 'csrf_protection' => false, + ]); + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add('dbId', NumberConstraintType::class, [ + 'label' => 'part.filter.dbId', + 'min' => 1, + 'step' => 1, + ]); + + $builder->add('timestamp', DateTimeConstraintType::class, [ + 'label' => 'log.timestamp', + ]); + + + + $builder->add('level', ChoiceConstraintType::class, [ + 'label' => 'log.level', + 'choices' => self::LEVEL_CHOICES, + ]); + + $builder->add('eventType', InstanceOfConstraintType::class, [ + 'label' => 'log.type', + 'choices' => self::TARGET_TYPE_CHOICES + ]); + + $builder->add('user', StructuralEntityConstraintType::class, [ + 'label' => 'log.user', + 'entity_class' => User::class, + ]); + + $builder->add('targetType', ChoiceConstraintType::class, [ + 'label' => 'log.target_type', + 'choices' => [ + 'user.label' => AbstractLogEntry::targetTypeClassToID(User::class), + 'attachment.label' => AbstractLogEntry::targetTypeClassToID(Attachment::class), + 'attachment_type.label' => AbstractLogEntry::targetTypeClassToID(AttachmentType::class), + 'category.label' => AbstractLogEntry::targetTypeClassToID(Category::class), + 'device.label' => AbstractLogEntry::targetTypeClassToID(Device::class), + 'device_part.label' => AbstractLogEntry::targetTypeClassToID(DevicePart::class), + 'footprint.label' => AbstractLogEntry::targetTypeClassToID(Footprint::class), + 'group.label' => AbstractLogEntry::targetTypeClassToID(Group::class), + 'manufacturer.label' => AbstractLogEntry::targetTypeClassToID(Manufacturer::class), + 'part.label' => AbstractLogEntry::targetTypeClassToID(Part::class), + 'storelocation.label' => AbstractLogEntry::targetTypeClassToID(Storelocation::class), + 'supplier.label' => AbstractLogEntry::targetTypeClassToID(Supplier::class), + 'part_lot.label' => AbstractLogEntry::targetTypeClassToID(PartLot::class), + 'currency.label' => AbstractLogEntry::targetTypeClassToID(Currency::class), + 'orderdetail.label' => AbstractLogEntry::targetTypeClassToID(Orderdetail::class), + 'pricedetail.label' => AbstractLogEntry::targetTypeClassToID(Pricedetail::class), + 'measurement_unit.label' => AbstractLogEntry::targetTypeClassToID(MeasurementUnit::class), + 'parameter.label' => AbstractLogEntry::targetTypeClassToID(AbstractParameter::class), + 'label_profile.label' => AbstractLogEntry::targetTypeClassToID(LabelProfile::class), + ] + ]); + + $builder->add('targetId', NumberConstraintType::class, [ + 'label' => 'log.target_id', + 'min' => 1, + 'step' => 1, + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'filter.submit', + ]); + + $builder->add('discard', ResetType::class, [ + 'label' => 'filter.discard', + ]); + } +} \ No newline at end of file diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php index 99948355..a1b5766f 100644 --- a/src/Form/Filters/PartFilterType.php +++ b/src/Form/Filters/PartFilterType.php @@ -15,7 +15,7 @@ use App\Form\Filters\Constraints\ChoiceConstraintType; use App\Form\Filters\Constraints\DateTimeConstraintType; use App\Form\Filters\Constraints\NumberConstraintType; use App\Form\Filters\Constraints\ParameterConstraintType; -use App\Form\Filters\Constraints\StructuralEntityConstraintType; +use App\Form\Filters\Constraints\UserEntityConstraintType; use App\Form\Filters\Constraints\TagsConstraintType; use App\Form\Filters\Constraints\TextConstraintType; use Svg\Tag\Text; @@ -54,12 +54,12 @@ class PartFilterType extends AbstractType 'label' => 'part.edit.description', ]); - $builder->add('category', StructuralEntityConstraintType::class, [ + $builder->add('category', UserEntityConstraintType::class, [ 'label' => 'part.edit.category', 'entity_class' => Category::class ]); - $builder->add('footprint', StructuralEntityConstraintType::class, [ + $builder->add('footprint', UserEntityConstraintType::class, [ 'label' => 'part.edit.footprint', 'entity_class' => Footprint::class ]); @@ -96,7 +96,7 @@ class PartFilterType extends AbstractType 'min' => 0, ]); - $builder->add('measurementUnit', StructuralEntityConstraintType::class, [ + $builder->add('measurementUnit', UserEntityConstraintType::class, [ 'label' => 'part.edit.partUnit', 'entity_class' => MeasurementUnit::class ]); @@ -114,7 +114,7 @@ class PartFilterType extends AbstractType * Manufacturer tab */ - $builder->add('manufacturer', StructuralEntityConstraintType::class, [ + $builder->add('manufacturer', UserEntityConstraintType::class, [ 'label' => 'part.edit.manufacturer.label', 'entity_class' => Manufacturer::class ]); @@ -145,7 +145,7 @@ class PartFilterType extends AbstractType * Purchasee informations */ - $builder->add('supplier', StructuralEntityConstraintType::class, [ + $builder->add('supplier', UserEntityConstraintType::class, [ 'label' => 'supplier.label', 'entity_class' => Manufacturer::class ]); @@ -163,7 +163,7 @@ class PartFilterType extends AbstractType /* * Stocks tabs */ - $builder->add('storelocation', StructuralEntityConstraintType::class, [ + $builder->add('storelocation', UserEntityConstraintType::class, [ 'label' => 'storelocation.label', 'entity_class' => Storelocation::class ]); @@ -210,7 +210,7 @@ class PartFilterType extends AbstractType 'min' => 0, ]); - $builder->add('attachmentType', StructuralEntityConstraintType::class, [ + $builder->add('attachmentType', UserEntityConstraintType::class, [ 'label' => 'attachment.attachment_type', 'entity_class' => AttachmentType::class ]); diff --git a/templates/LogSystem/log_list.html.twig b/templates/LogSystem/log_list.html.twig index 5ad1d7a3..fbe61d63 100644 --- a/templates/LogSystem/log_list.html.twig +++ b/templates/LogSystem/log_list.html.twig @@ -3,5 +3,52 @@ {% block title %}{% trans %}log.list.title{% endtrans %}{% endblock %} {% block content %} +
+
+
+ +
+
+
+ +
+
+ + +
+ +
+
+ +
+
+
+ {{ form_start(filterForm, {"attr": {"data-controller": "helpers--form-cleanup", "data-action": "helpers--form-cleanup#submit"}}) }} + + {{ form_row(filterForm.dbId) }} + {{ form_row(filterForm.timestamp) }} + {{ form_row(filterForm.eventType) }} + {{ form_row(filterForm.level) }} + {{ form_row(filterForm.targetType) }} + {{ form_row(filterForm.targetId) }} + + {{ form_row(filterForm.submit) }} + {{ form_row(filterForm.discard) }} + +
+
+ +
+
+ + {{ form_end(filterForm) }} +
+
+
+
+ {% include "LogSystem/_log_table.html.twig" %} {% endblock %} \ No newline at end of file diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index de8ef7d6..503e7550 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -9699,5 +9699,77 @@ Element 3 Associated element type + + + log.level.debug + Debug + + + + + log.level.info + Info + + + + + log.level.notice + Notice + + + + + log.level.warning + Warning + + + + + log.level.error + Error + + + + + log.level.critical + Critical + + + + + log.level.alert + Alert + + + + + log.level.emergency + Emergency + + + + + log.type.security + Security related event + + + + + log.type.instock_changed + [LEGACY] Instock changed + + + + + device_part.label + Device part + + + + + log.target_id + Target element ID + +