diff --git a/.docker/compose.yaml b/.docker/compose.yaml new file mode 100644 index 0000000..fbb442b --- /dev/null +++ b/.docker/compose.yaml @@ -0,0 +1,14 @@ +x-build-args: &build-args + UID: "${UID:-1000}" + GID: "${GID:-1000}" + +name: cleverage-ui-process-bundle + +services: + php: + build: + context: php + args: + <<: *build-args + volumes: + - ../:/var/www diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile new file mode 100644 index 0000000..f98c3ba --- /dev/null +++ b/.docker/php/Dockerfile @@ -0,0 +1,29 @@ +FROM php:8.2-fpm-alpine + +ARG UID +ARG GID + +RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" +COPY /conf.d/ "$PHP_INI_DIR/conf.d/" + +RUN apk update && apk add \ + tzdata \ + shadow \ + nano \ + bash \ + icu-dev \ + && docker-php-ext-configure intl \ + && docker-php-ext-install intl opcache \ + && docker-php-ext-enable opcache + +RUN ln -s /usr/share/zoneinfo/Europe/Paris /etc/localtime \ + && sed -i "s/^;date.timezone =.*/date.timezone = Europe\/Paris/" $PHP_INI_DIR/php.ini + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +RUN usermod -u $UID www-data \ + && groupmod -g $GID www-data + +USER www-data:www-data + +WORKDIR /var/www diff --git a/.docker/php/conf.d/dev.ini b/.docker/php/conf.d/dev.ini new file mode 100644 index 0000000..2a141be --- /dev/null +++ b/.docker/php/conf.d/dev.ini @@ -0,0 +1,5 @@ +display_errors = 1 +error_reporting = E_ALL + +opcache.validate_timestamps = 1 +opcache.revalidate_freq = 0 diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..7711713 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,14 @@ +## Description + + + +## Requirements + +* Documentation updates + - [ ] Reference + - [ ] Changelog +* [ ] Unit tests + +## Breaking changes + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..58db37d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +## Description + + + +## Requirements + +* Documentation updates + - [ ] Reference + - [ ] Changelog +* [ ] Unit tests + +## Breaking changes + + diff --git a/.github/dependabot.yml.old b/.github/dependabot.yml.old deleted file mode 100644 index 3bcc9ea..0000000 --- a/.github/dependabot.yml.old +++ /dev/null @@ -1,26 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "composer" - directory: "/" - target-branch: "v2-dev" - schedule: - interval: "weekly" - allow: - - dependency-type: "all" - groups: - symfony: - patterns: - - "symfony*" - update-types: - - "minor" - - "patch" - - package-ecosystem: "npm" - directory: "/" - target-branch: "v2-dev" - schedule: - interval: "weekly" - - package-ecosystem: "github-actions" - directory: "/" - target-branch: "v2-dev" - schedule: - interval: "weekly" diff --git a/.github/workflows/notifications.yml b/.github/workflows/notifications.yml new file mode 100644 index 0000000..7ebf430 --- /dev/null +++ b/.github/workflows/notifications.yml @@ -0,0 +1,23 @@ +name: Rocket chat notifications + +# Controls when the action will run. +on: + push: + tags: + - '*' + +jobs: + notification: + runs-on: ubuntu-latest + + steps: + - name: Get the tag short reference + id: get_tag + run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + + - name: Rocket.Chat Notification + uses: madalozzo/Rocket.Chat.GitHub.Action.Notification@master + with: + type: success + job_name: "[cleverage/ui-process-bundle](https://github.com/cleverage/ui-process-bundle) : ${{ steps.get_tag.outputs.TAG }} has been released" + url: ${{ secrets.CLEVER_AGE_ROCKET_CHAT_WEBOOK_URL }} diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..c926661 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,79 @@ +name: Quality + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v3 + - name: PHPStan + run: vendor/bin/phpstan --no-progress --memory-limit=1G analyse --error-format=github + + php-cs-fixer: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v3 + - name: PHP-CS-Fixer + run: vendor/bin/php-cs-fixer fix --diff --dry-run --show-progress=none + + rector: + name: Rector + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v3 + - name: Rector + run: vendor/bin/rector --no-progress-bar --dry-run + + twig-cs-fixer: + name: Twig-CS-Fixer + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v3 + - name: Twig-CS-Fixer + run: vendor/bin/twig-cs-fixer lint --report=github diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml deleted file mode 100644 index e40efa0..0000000 --- a/.github/workflows/super-linter.yml +++ /dev/null @@ -1,17 +0,0 @@ -# It checks the syntax of the PHP code (using PHP-CS-Fixer) -name: "Linter: Code Syntax" -on: - push: - branches: [ v2-dev ] - pull_request: - branches: [ v2-dev ] -jobs: - php-cs-fixer: - name: PHP-CS-Fixer - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: PHP-CS-Fixer - uses: docker://oskarstark/php-cs-fixer-ga - with: - args: --config=.project.php_cs --dry-run diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2d7e7a4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,74 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + test: + name: PHP ${{ matrix.php-version }} + ${{ matrix.dependencies }} + ${{ matrix.variant }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.allowed-to-fail }} + env: + SYMFONY_REQUIRE: ${{matrix.symfony-require}} + + strategy: + matrix: + php-version: + - '8.2' + - '8.3' + dependencies: [highest] + allowed-to-fail: [false] + symfony-require: [''] + variant: [normal] + include: + - php-version: '8.2' + dependencies: highest + allowed-to-fail: false + symfony-require: 6.4.* + variant: symfony/symfony:"6.4.*" + - php-version: '8.2' + dependencies: highest + allowed-to-fail: false + symfony-require: 7.1.* + variant: symfony/symfony:"7.1.*" + - php-version: '8.3' + dependencies: highest + allowed-to-fail: false + symfony-require: 6.4.* + variant: symfony/symfony:"6.4.*" + - php-version: '8.3' + dependencies: highest + allowed-to-fail: false + symfony-require: 7.1.* + variant: symfony/symfony:"7.1.*" + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: pcov + tools: composer:v2, flex + - name: Add PHPUnit matcher + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Install variant + if: matrix.variant != 'normal' && !startsWith(matrix.variant, 'symfony/symfony') + run: composer require ${{ matrix.variant }} --no-update + - name: Install Composer dependencies (${{ matrix.dependencies }}) + uses: ramsey/composer-install@v3 + with: + dependency-versions: ${{ matrix.dependencies }} + - name: Run Tests with coverage + run: vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover build/logs/clover.xml + #- name: Send coverage to Codecov + # uses: codecov/codecov-action@v4 + # with: + # files: build/logs/clover.xml diff --git a/.gitignore b/.gitignore index 60f5bb4..40db549 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,37 @@ +/composer.lock +/vendor +.env +.idea +/phpunit.xml +.phpunit.result.cache +.phpunit.cache +.php-cs-fixer.cache +coverage-report + +###> phpunit/phpunit ### +/phpunit.xml +.phpunit.result.cache +###< phpunit/phpunit ### + +###> symfony/phpunit-bridge ### +.phpunit.result.cache +/phpunit.xml +###< symfony/phpunit-bridge ### + ###> symfony/framework-bundle ### -/.env /.env.local /.env.local.php -/.env.test /.env.*.local /config/secrets/prod/prod.decrypt.private.php /public/bundles/ /var/ /vendor/ ###< symfony/framework-bundle ### -###> custom ### -/docker-compose.override.yml -/.idea -/.pdepend -/.composer/cache -###< custom ### + +###> friendsofphp/php-cs-fixer ### +/.php-cs-fixer.php +/.php-cs-fixer.cache +###< friendsofphp/php-cs-fixer ### ###> symfony/webpack-encore-bundle ### /node_modules/ @@ -23,13 +40,6 @@ npm-debug.log yarn-error.log ###< symfony/webpack-encore-bundle ### -###> squizlabs/php_codesniffer ### -/.phpcs-cache -/phpcs.xml -/.php-cs-fixer.cache -###< squizlabs/php_codesniffer ### - -###> friendsofphp/php-cs-fixer ### -/.php-cs-fixer.php -/.php-cs-fixer.cache -###< friendsofphp/php-cs-fixer ### +###> vincentlanglet/twig-cs-fixer ### +/.twig-cs-fixer.cache +###< vincentlanglet/twig-cs-fixer ### diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 0fd58f8..ed5c8c5 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,31 +1,47 @@ in(__DIR__.'/src') - ->ignoreDotFiles(true) - ->ignoreVCS(true) - ->exclude(['build', 'vendor']) - ->files() - ->name('*.php') -; +/* + * This file is part of the CleverAge/UiProcessBundle package. + * + * Copyright (c) Clever-Age + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ -$config = new PhpCsFixer\Config(); +if (!file_exists(__DIR__.'/src')) { + exit(0); +} -return $config - ->setUsingCache(true) - ->setRiskyAllowed(true) - ->setFinder($finder) +$fileHeaderComment = <<<'EOF' + This file is part of the CleverAge/UiProcessBundle package. + + Copyright (c) Clever-Age + + For the full copyright and license information, please view the LICENSE + file that was distributed with this source code. + EOF; + +return (new PhpCsFixer\Config()) ->setRules([ - '@PHP80Migration' => true, - '@PHP80Migration:risky' => true, + '@PHP71Migration' => true, + '@PHP82Migration' => true, + '@PHPUnit75Migration:risky' => true, '@Symfony' => true, '@Symfony:risky' => true, - 'array_syntax' => ['syntax' => 'short'], - 'fopen_flags' => false, - 'ordered_imports' => true, + '@DoctrineAnnotation' => true, 'protected_to_private' => false, - 'single_line_throw' => false, - // this must be disabled because the output of some tests include NBSP characters - 'non_printable_character' => false, + 'native_constant_invocation' => ['strict' => false], + 'header_comment' => ['header' => $fileHeaderComment], + 'modernize_strpos' => true, + 'get_class_to_class_keyword' => true, ]) - ; + ->setRiskyAllowed(true) + ->setFinder( + (new PhpCsFixer\Finder()) + ->in(__DIR__.'/src') + ->in(__DIR__.'/tests') + ->append([__FILE__]) + ) + ->setCacheFile('.php-cs-fixer.cache') +; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bb04c2f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,67 @@ +v2.0 +------ + +## BC breaks + +* [#4](https://github.com/cleverage/ui-process-bundle/issues/4) Update composer : "doctrine/*" using same versions of doctrine-process-bundle. + Remove "sensio/framework-extra-bundle" & "symfony/flex". Update require-dev using "process-bundle" standard. Reinstall "symfony/debug-pack". + "symfony/*" from ^5.4 to ^6.4|^7.1 => Update changes on code. +* [#2](https://github.com/cleverage/ui-process-bundle/issues/2) Routes must be prefixed with the bundle alias => `cleverage_ui_process` +* [#2](https://github.com/cleverage/ui-process-bundle/issues/2) Update services according to Symfony best practices. Services should not use autowiring or autoconfiguration. Instead, all services should be defined explicitly. + Services must be prefixed with the bundle alias instead of using fully qualified class names => `cleverage_ui_process` +* [#3](https://github.com/cleverage/ui-process-bundle/issues/3) Rename process-ui-bundle to ui-process-bundle, + cleverage:process-ui:xxx to cleverage:ui-process:xxx, clever_age_process_ui to cleverage_ui_process and ProcessUi*** to UiProcess*** + +### Changes + +* [#1](https://github.com/cleverage/ui-process-bundle/issues/1) Add Makefile & .docker for local standalone usage +* [#1](https://github.com/cleverage/ui-process-bundle/issues/1) Add rector, phpstan & php-cs-fixer configurations & apply it. Remove phpcs configuration. +* [#11](https://github.com/cleverage/ui-process-bundle/issues/11) Restrict "Download log file" and "Show logs stored in database" buttons visibility + + +v1.0.6 +------ + +### Fixes + +* Update ProcessExecutionCrudController.php. Avoid fatal error if no permission to display row + +v1.0.5 +------ + +### Fixes + +* [#1](https://github.com/cleverage/processuibundle/issues/1) fix fatal error + +v1.0.4 +------ + +### Changes + +* Only logs errors to level >= INFO + +v1.0.3 +------ + +### Changes + +* Add search fields to the ProcessExecution Crud + +v1.0.2 +------ + +### Fixes + +* Fix setSearchFields on ProcessCrudController + +v1.0.1 +------ + +### Changes + +* Php-cs-fixer & phpstan rules applying. Update README + +v1.0.0 +------ + +* Initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d06b733 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +Contributing +============ + +First of all, **thank you** for contributing, **you are awesome**! + +Here are a few rules to follow in order to ease code reviews, and discussions before +maintainers accept and merge your work. + +You MUST run the quality & test suites. + +You SHOULD write (or update) unit tests. + +You SHOULD write documentation. + +Please, write [commit messages that make sense](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), +and [rebase your branch](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) before submitting your Pull Request. + +One may ask you to [squash your commits](https://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) +too. This is used to "clean" your Pull Request before merging it (we don't want +commits such as `fix tests`, `fix 2`, `fix 3`, etc.). + +Thank you! + +## Running the quality & test suites + +Tests suite uses Docker environments in order to be idempotent to OS's. More than this +PHP version is written inside the Dockerfile; this assures to test the bundle with +the same resources. No need to have PHP installed. + +You only need Docker set it up. + +To allow testing environments more smooth we implemented **Makefile**. +You have two commands available: + +```bash +make quality +``` + +```bash +make tests +``` + +## Deprecations notices + +When a feature should be deprecated, or when you have a breaking change for a future version, please : +* [Fill an issue](https://github.com/cleverage/ui-process-bundle/issues/new) +* Add TODO comments with the following format: `@TODO deprecated v2.0` +* Trigger a deprecation error: `@trigger_error('This feature will be deprecated in v2.0', E_USER_DEPRECATED);` + +You can check which deprecation notice is triggered in tests +* `make bash` +* `SYMFONY_DEPRECATIONS_HELPER=0 ./vendor/bin/phpunit` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..045d824 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Clever-Age + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4970ded --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +.ONESHELL: +SHELL := /bin/bash + +DOCKER_RUN_PHP = docker compose -f .docker/compose.yaml run --rm php "bash" "-c" +DOCKER_COMPOSE = docker compose -f .docker/compose.yaml + +start: upd #[Global] Start application + +src/vendor: #[Composer] install dependencies + $(DOCKER_RUN_PHP) "composer install --no-interaction" + +upd: #[Docker] Start containers detached + touch .docker/.env + make src/vendor + $(DOCKER_COMPOSE) up --remove-orphans --detach + +up: #[Docker] Start containers + touch .docker/.env + make src/vendor + $(DOCKER_COMPOSE) up --remove-orphans + +stop: #[Docker] Down containers + $(DOCKER_COMPOSE) stop + +down: #[Docker] Down containers + $(DOCKER_COMPOSE) down + +build: #[Docker] Build containers + $(DOCKER_COMPOSE) build + +ps: # [Docker] Show running containers + $(DOCKER_COMPOSE) ps + +bash: #[Docker] Connect to php container with current host user + $(DOCKER_COMPOSE) exec php bash + +logs: #[Docker] Show logs + $(DOCKER_COMPOSE) logs -f + +quality: phpstan php-cs-fixer rector twig-cs-fixer #[Quality] Run all quality checks + +phpstan: #[Quality] Run PHPStan + $(DOCKER_RUN_PHP) "vendor/bin/phpstan --no-progress --memory-limit=1G analyse" + +php-cs-fixer: #[Quality] Run PHP-CS-Fixer + $(DOCKER_RUN_PHP) "vendor/bin/php-cs-fixer fix --diff --verbose" + +rector: #[Quality] Run Rector + $(DOCKER_RUN_PHP) "vendor/bin/rector" + +twig-cs-fixer: #[Quality] Run Rector + $(DOCKER_RUN_PHP) "vendor/bin/twig-cs-fixer fix --verbose" + +tests: phpunit #[Tests] Run all tests + +phpunit: #[Tests] Run PHPUnit + $(DOCKER_RUN_PHP) "vendor/bin/phpunit" diff --git a/README.md b/README.md index 475ceab..d65d342 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,22 @@ -![Code Style](https://github.com/cleverage/processuibundle/actions/workflows/super-linter.yml/badge.svg) ![Composer](https://github.com/cleverage/processuibundle/actions/workflows/php.yml/badge.svg) - -## CleverAge/ProcessUIBundle -A simple UX for cleverage/processbundle using EasyAdmin - -**Installation** -* Import routes -```yaml -#config/routes.yaml -process-ui: - resource: '@CleverAgeProcessUiBundle/Resources/config/routes.yaml' -``` -* Run doctrine migration -* Create an user using cleverage:process-ui:user-create console. - -Now you can access Process UI via http://your-domain.com/process - -**Indexing logs** - -You can index logs line into database to perform search on ****Process > History**** page. -See configuration section. - -When indexation is enabled you can perform it async. - -```yaml -#config/messenger.yaml -framework: - messenger: - transports: - log_index: 'doctrine://default' - - routing: - CleverAge\ProcessUiBundle\Message\LogIndexerMessage: log_index -``` - -Then you have to consume messages by running (use a supervisor to keep consumer alive) -``` -bin/console messenger:consume log_index --memory-limit=64M -``` - -See official symfony/messenger component documentations for more informations https://symfony.com/doc/current/messenger.html - -**Integrate CrudController** - -Of course you can integrate ProcessUI CRUD into your own easy admin Dashboard -```php - public function configureMenuItems(): iterable - { - /* ... your configuration */ - yield MenuItem::linkToCrud('History', null, ProcessExecution::class); - } -``` - -**Configuration** -```yaml -clever_age_process_ui: - index_logs: - enabled: false - level: ERROR #Minimum log level to index. Allowed values are DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY -``` +CleverAge/UiProcessBundle +======================= + +This bundle is a part of the [CleverAge/ProcessBundle](https://github.com/cleverage/process-bundle) project. +It provides a simple UX using [EasyAdmin](https://symfony.com/bundles/EasyAdminBundle/4.x/index.html) on Process bundle. + +Compatible with [Symfony stable version and latest Long-Term Support (LTS) release](https://symfony.com/releases). + +## Documentation + +For usage documentation, see: +[docs/index.md](docs/index.md) + +## Support & Contribution + +For general support and questions, please use [Github](https://github.com/cleverage/ui-process-bundle/issues). +If you think you found a bug or you have a feature idea to propose, feel free to open an issue after looking at the [contributing](CONTRIBUTING.md) guide. + +## License + +This bundle is under the MIT license. +For the whole copyright, see the [LICENSE](LICENSE) file distributed with this source code. diff --git a/composer.json b/composer.json index 9091777..4e75364 100644 --- a/composer.json +++ b/composer.json @@ -1,16 +1,19 @@ { "name": "cleverage/ui-process-bundle", + "description": "UI for cleverage/process-bundle", + "keywords": [ + "process", + "task", + "etl", + "transformation", + "import", + "export", + "ui" + ], + "homepage": "https://github.com/cleverage/ui-process-bundle", "type": "symfony-bundle", "license": "MIT", - "description": "UI for cleverage/process-bundle", - "minimum-stability": "dev", - "prefer-stable": true, "authors": [ - { - "name": "Baudouin Douliery", - "email": "bdouliery@clever-age.com", - "role": "Developer" - }, { "name": "Grégory Tonon", "email": "gtonon@clever-age.com", @@ -22,94 +25,62 @@ "role": "Developer" } ], + "autoload": { + "psr-4": { + "CleverAge\\UiProcessBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "CleverAge\\UiProcessBundle\\Tests\\": "tests/" + } + }, "require": { - "php": ">=8.0", + "php": ">=8.2", "ext-ctype": "*", "ext-iconv": "*", - "cleverage/process-bundle": "^3.2", - "easycorp/easyadmin-bundle": "^4.0", - "ddtraceweb/monolog-parser": "^1.3", - "league/flysystem": "^2.2", - "composer/package-versions-deprecated": "^1.11", - "doctrine/doctrine-bundle": "^2.4", - "doctrine/doctrine-migrations-bundle": "^3.1", - "doctrine/orm": "^2.9", - "sensio/framework-extra-bundle": "^6.1", - "symfony/console": "^5.4", - "symfony/messenger": "^5.4", - "symfony/doctrine-messenger": "^5.4", - "symfony/filesystem": "^5.4", - "symfony/flex": "^1.3.1", - "symfony/form": "^5.4", - "symfony/framework-bundle": "^5.4", - "symfony/mime": "^5.4", - "symfony/proxy-manager-bridge": "^5.4", - "symfony/runtime": "^5.4", - "symfony/security-bundle": "^5.4", - "symfony/stopwatch": "^5.4", - "symfony/twig-bundle": "^5.4", - "symfony/validator": "^5.4", - "symfony/webpack-encore-bundle": "^1.11", - "symfony/yaml": "^5.4", - "twig/extra-bundle": "^2.12|^3.0", - "twig/intl-extra": "^3.3" + "cleverage/process-bundle": "^4.0", + "doctrine/common": "^3.0", + "doctrine/dbal": "^2.9 || ^3.0", + "doctrine/doctrine-bundle": "^2.5", + "doctrine/doctrine-migrations-bundle": "^3.2", + "doctrine/orm": "^2.9 || ^3.0", + "easycorp/easyadmin-bundle": "^4.8", + "symfony/doctrine-messenger": "^6.4|^7.1", + "symfony/dotenv": "^6.4|^7.1", + "symfony/messenger": "^6.4|^7.1", + "symfony/runtime": "^6.4|^7.1", + "symfony/scheduler": "^6.4|^7.1", + "symfony/string": "^6.4|^7.1", + "symfony/uid": "^6.4|^7.1" }, "require-dev": { "doctrine/doctrine-fixtures-bundle": "^3.4", - "friendsofphp/php-cs-fixer": "^3.6", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1", - "phpstan/phpstan-doctrine": "^1.2", - "phpstan/phpstan-symfony": "^1.1", - "rector/rector": "^0.12.13", + "friendsofphp/php-cs-fixer": "*", + "phpstan/extension-installer": "*", + "phpstan/phpstan": "*", + "phpstan/phpstan-doctrine": "*", + "phpstan/phpstan-symfony": "*", + "phpunit/phpunit": "<10.0", + "rector/rector": "*", "roave/security-advisories": "dev-latest", - "squizlabs/php_codesniffer": "^3.6", - "symfony/dotenv": "^5.4", + "symfony/browser-kit": "^6.4|^7.1", + "symfony/css-selector": "^6.4|^7.1", + "symfony/debug-bundle": "^6.4|^7.1", "symfony/maker-bundle": "^1.31", - "symfony/web-profiler-bundle": "^5.4" + "symfony/web-profiler-bundle": "^6.4|^7.1", + "vincentlanglet/twig-cs-fixer": "^3.3" + }, + "conflict": { + "symfony/twig-bridge": "7.2.0", + "twig/twig": "v3.15.0|v3.16.0" }, "config": { - "optimize-autoloader": true, - "preferred-install": { - "*": "dist" - }, - "sort-packages": true, "allow-plugins": { - "composer/package-versions-deprecated": true, + "phpstan/extension-installer": true, "symfony/flex": true, - "symfony/runtime": true, - "phpstan/extension-installer": true - } - }, - "autoload": { - "psr-4": { - "CleverAge\\ProcessUiBundle\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "CleverAge\\ProcessUiBundle\\Tests\\": "tests/" - } - }, - "scripts": { - "auto-scripts": { - "cache:clear": "symfony-cmd", - "assets:install %PUBLIC_DIR%": "symfony-cmd" + "symfony/runtime": true }, - "post-install-cmd": [ - "@auto-scripts" - ], - "post-update-cmd": [ - "@auto-scripts" - ] - }, - "conflict": { - "symfony/symfony": "*" - }, - "extra": { - "symfony": { - "allow-contrib": false, - "require": "5.4.*" - } + "sort-packages": true } } diff --git a/config/routes.yaml b/config/routes.yaml deleted file mode 100755 index 8cfca5b..0000000 --- a/config/routes.yaml +++ /dev/null @@ -1,4 +0,0 @@ -_process-ui: - prefix: '/process' - resource: ../src/Controller/ - type: annotation \ No newline at end of file diff --git a/config/services.yaml b/config/services.yaml deleted file mode 100644 index 3b371fb..0000000 --- a/config/services.yaml +++ /dev/null @@ -1,20 +0,0 @@ -parameters: - uploads_directory: '%kernel.project_dir%/public/uploads' - cleverage_processes: '%kernel.project_dir%/config/packages' - process_logs_dir: '%kernel.logs_dir%/process' - -services: - _defaults: - autowire: true - autoconfigure: true - bind: - $processLogDir: '%process_logs_dir%' - $indexLogs: '%clever_age_process_ui.index_logs.enabled%' - - CleverAge\ProcessUiBundle\: - resource: '../src' - exclude: - - '../src/DependencyInjection/' - - '../src/Entity/' - - '../src/Tests/' - - '../src/Migrations' \ No newline at end of file diff --git a/config/services/command.yaml b/config/services/command.yaml new file mode 100644 index 0000000..e8f5c9f --- /dev/null +++ b/config/services/command.yaml @@ -0,0 +1,10 @@ +services: + cleverage_ui_process.command.user_create: + class: CleverAge\UiProcessBundle\Command\UserCreateCommand + public: false + tags: + - { name: console.command } + arguments: + - '@validator' + - '@security.user_password_hasher' + - '@doctrine.orm.entity_manager' diff --git a/config/services/controller.yaml b/config/services/controller.yaml new file mode 100644 index 0000000..4cec79e --- /dev/null +++ b/config/services/controller.yaml @@ -0,0 +1,16 @@ +services: + CleverAge\UiProcessBundle\Controller\: + resource: '../../src/Controller/' + autowire: true + autoconfigure: true + bind: + $processConfigurationsManager: '@cleverage_ui_process.manager.process_configuration' + $localeSwitcher: '@translation.locale_switcher' + $requestStack: '@request_stack' + $messageBus: '@messenger.default_bus' + $uploadDirectory: '%upload_directory%' + $context: '@EasyCorp\Bundle\EasyAdminBundle\Factory\AdminContextFactory' + $logDirectory: '%kernel.logs_dir%' + $processExecutionRepository: '@cleverage_ui_process.repository.process_execution' + tags: + - { name: 'controller.service_arguments' } diff --git a/config/services/event_subscriber.yaml b/config/services/event_subscriber.yaml new file mode 100644 index 0000000..1a35949 --- /dev/null +++ b/config/services/event_subscriber.yaml @@ -0,0 +1,10 @@ +services: + cleverage_ui_process.event_subscriber.process: + class: CleverAge\UiProcessBundle\EventSubscriber\ProcessEventSubscriber + public: false + tags: + - { name: 'kernel.event_subscriber' } + arguments: + - '@cleverage_ui_process.monolog_handler.process' + - '@cleverage_ui_process.monolog_handler.doctrine_process' + - '@cleverage_ui_process.manager.process_execution' diff --git a/config/services/form_type.yaml b/config/services/form_type.yaml new file mode 100644 index 0000000..f47ec48 --- /dev/null +++ b/config/services/form_type.yaml @@ -0,0 +1,9 @@ +services: + cleverage_ui_process.form_type.launch: + class: CleverAge\UiProcessBundle\Form\Type\LaunchType + public: false + arguments: + - '@cleverage_process.registry.process_configuration' + - '@cleverage_ui_process.manager.process_configuration' + tags: + - { name: 'form.type' } diff --git a/config/services/http_value_resolver.yaml b/config/services/http_value_resolver.yaml new file mode 100644 index 0000000..21b40f2 --- /dev/null +++ b/config/services/http_value_resolver.yaml @@ -0,0 +1,14 @@ +services: + cleverage_ui_process.http_value_resolver.http_process_execute: + class: CleverAge\UiProcessBundle\Http\ValueResolver\HttpProcessExecuteValueResolver + autoconfigure: true + public: false + arguments: + - '%upload_directory%' + + cleverage_ui_process.http_value_resolver.process_configuration: + class: CleverAge\UiProcessBundle\Http\ValueResolver\ProcessConfigurationValueResolver + autoconfigure: true + public: false + arguments: + - '@cleverage_process.registry.process_configuration' diff --git a/config/services/manager.yaml b/config/services/manager.yaml new file mode 100644 index 0000000..4521e68 --- /dev/null +++ b/config/services/manager.yaml @@ -0,0 +1,12 @@ +services: + cleverage_ui_process.manager.process_execution: + class: CleverAge\UiProcessBundle\Manager\ProcessExecutionManager + public: false + arguments: + - '@cleverage_ui_process.repository.process_execution' + + cleverage_ui_process.manager.process_configuration: + class: CleverAge\UiProcessBundle\Manager\ProcessConfigurationsManager + public: false + arguments: + - '@cleverage_process.registry.process_configuration' diff --git a/config/services/message.yaml b/config/services/message.yaml new file mode 100644 index 0000000..8cb3330 --- /dev/null +++ b/config/services/message.yaml @@ -0,0 +1,16 @@ +services: + cleverage_ui_process.message.cron_process_message_handler: + class: CleverAge\UiProcessBundle\Message\CronProcessMessageHandler + public: false + arguments: + - '@messenger.default_bus' + tags: [messenger.message_handler] + + cleverage_ui_process.message.process_execute_handler: + class: CleverAge\UiProcessBundle\Message\ProcessExecuteHandler + public: false + arguments: + - '@cleverage_process.manager.process' + - '@cleverage_ui_process.monolog_handler.process' + tags: [messenger.message_handler] + diff --git a/config/services/monolog_handler.yaml b/config/services/monolog_handler.yaml new file mode 100644 index 0000000..1bd99b8 --- /dev/null +++ b/config/services/monolog_handler.yaml @@ -0,0 +1,18 @@ +services: + cleverage_ui_process.monolog_handler.doctrine_process: + class: CleverAge\UiProcessBundle\Monolog\Handler\DoctrineProcessHandler + public: false + calls: + - [ setEntityManager, [ '@doctrine.orm.entity_manager' ] ] + - [ setProcessExecutionManager, [ '@cleverage_ui_process.manager.process_execution' ] ] + CleverAge\UiProcessBundle\Monolog\Handler\DoctrineProcessHandler: + alias: cleverage_ui_process.monolog_handler.doctrine_process + + cleverage_ui_process.monolog_handler.process: + class: CleverAge\UiProcessBundle\Monolog\Handler\ProcessHandler + public: false + arguments: + - '%kernel.logs_dir%' + - '@cleverage_ui_process.manager.process_execution' + CleverAge\UiProcessBundle\Monolog\Handler\ProcessHandler: + alias: cleverage_ui_process.monolog_handler.process diff --git a/config/services/parameters.yaml b/config/services/parameters.yaml new file mode 100644 index 0000000..3cee611 --- /dev/null +++ b/config/services/parameters.yaml @@ -0,0 +1,2 @@ +parameters: + upload_directory: '%kernel.project_dir%/var/storage/uploads' diff --git a/config/services/repository.yaml b/config/services/repository.yaml new file mode 100644 index 0000000..6e8a5bd --- /dev/null +++ b/config/services/repository.yaml @@ -0,0 +1,13 @@ +services: + cleverage_ui_process.repository.process_execution: + class: CleverAge\UiProcessBundle\Repository\ProcessExecutionRepository + public: false + arguments: + - '@doctrine.orm.entity_manager' + + cleverage_ui_process.repository.process_schedule: + class: CleverAge\UiProcessBundle\Repository\ProcessScheduleRepository + public: false + arguments: + - '@doctrine' + diff --git a/config/services/scheduler.yaml b/config/services/scheduler.yaml new file mode 100644 index 0000000..ec15dd9 --- /dev/null +++ b/config/services/scheduler.yaml @@ -0,0 +1,11 @@ +services: + cleverage_ui_process.scheduler.cron: + class: CleverAge\UiProcessBundle\Scheduler\CronScheduler + public: false + arguments: + - '@cleverage_ui_process.repository.process_schedule' + - '@validator' + - '@logger' + tags: + - scheduler.schedule_provider: { name: 'cron' } + - { name: 'monolog.logger', channel: 'scheduler' } diff --git a/config/services/security.yaml b/config/services/security.yaml new file mode 100644 index 0000000..2eda664 --- /dev/null +++ b/config/services/security.yaml @@ -0,0 +1,7 @@ +services: + cleverage_ui_process.security.http_process_execution_authenticator: + class: CleverAge\UiProcessBundle\Security\HttpProcessExecutionAuthenticator + public: false + arguments: + - '@doctrine.orm.entity_manager' + diff --git a/config/services/twig.yaml b/config/services/twig.yaml new file mode 100644 index 0000000..4f54954 --- /dev/null +++ b/config/services/twig.yaml @@ -0,0 +1,59 @@ +services: + cleverage_ui_process.twig.log_level_extension: + class: CleverAge\UiProcessBundle\Twig\Extension\LogLevelExtension + public: false + tags: + - { name: 'twig.extension' } + + cleverage_ui_process.twig.md5_extension: + class: CleverAge\UiProcessBundle\Twig\Extension\MD5Extension + public: false + tags: + - { name: 'twig.extension' } + + cleverage_ui_process.twig.process_execution_extension: + class: CleverAge\UiProcessBundle\Twig\Extension\ProcessExecutionExtension + public: false + tags: + - { name: 'twig.extension' } + + cleverage_ui_process.twig.process_extension: + class: CleverAge\UiProcessBundle\Twig\Extension\ProcessExtension + public: false + tags: + - { name: 'twig.extension' } + + cleverage_ui_process.twig.log_level_extension_runtime: + class: CleverAge\UiProcessBundle\Twig\Runtime\LogLevelExtensionRuntime + public: false + tags: + - { name: 'twig.runtime' } + + cleverage_ui_process.twig.md5_extension_runtime: + class: CleverAge\UiProcessBundle\Twig\Runtime\MD5ExtensionRuntime + public: false + tags: + - { name: 'twig.runtime' } + + cleverage_ui_process.twig.process_execution_extension_runtime: + class: CleverAge\UiProcessBundle\Twig\Runtime\ProcessExecutionExtensionRuntime + public: false + tags: + - { name: 'twig.runtime' } + arguments: + - '@cleverage_ui_process.repository.process_execution' + - '@cleverage_ui_process.manager.process_configuration' + + cleverage_ui_process.twig.process_extension_runtime: + class: CleverAge\UiProcessBundle\Twig\Runtime\ProcessExtensionRuntime + public: false + tags: + - { name: 'twig.runtime' } + arguments: + - '@cleverage_ui_process.manager.process_configuration' + + cleverage_ui_process.twig.component.bootstrap_modal: + class: CleverAge\UiProcessBundle\Twig\Components\BootstrapModal + shared: false + tags: + - { name: 'twig.component', key: 'ui:BootstrapModal', template: '@CleverAgeUiProcess/components/BootstrapModal.html.twig' } \ No newline at end of file diff --git a/config/services/validator.yaml b/config/services/validator.yaml new file mode 100644 index 0000000..c4da724 --- /dev/null +++ b/config/services/validator.yaml @@ -0,0 +1,21 @@ +services: + cleverage_ui_process.validator.cron_expression_validator: + class: CleverAge\UiProcessBundle\Validator\CronExpressionValidator + public: false + tags: + - { name: 'validator.constraint_validator' } + + cleverage_ui_process.validator.every_expression_validator: + class: CleverAge\UiProcessBundle\Validator\EveryExpressionValidator + public: false + tags: + - { name: 'validator.constraint_validator' } + + cleverage_ui_process.validator.is_valid_process_code: + class: CleverAge\UiProcessBundle\Validator\IsValidProcessCodeValidator + public: false + tags: + - { name: 'validator.constraint_validator' } + arguments: + - '@cleverage_process.registry.process_configuration' + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..db90d03 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,136 @@ +## Prerequisite + +CleverAge/ProcessBundle must be [installed](https://github.com/cleverage/process-bundle/blob/main/docs/01-quick_start.md#installation. + +## Installation + +Make sure Composer is installed globally, as explained in the [installation chapter](https://getcomposer.org/doc/00-intro.md) +of the Composer documentation. + +Open a command console, enter your project directory and install it using composer: + +```bash +composer require cleverage/ui-process-bundle +``` + +Remember to add the following line to `config/bundles.php` (not required if Symfony Flex is used) + +```php +CleverAge\UiProcessBundle\CleverAgeUiProcessBundle::class => ['all' => true], +``` + +## Import routes + +```yaml +ui-process-bundle: + resource: '@CleverAgeUiProcessBundle/src/Controller' + type: attribute +``` +* Run doctrine migration +* Create a user using `cleverage:ui-process:user-create` console. + +Now you can access UI Process via http://your-domain.com/process + +## Features + +### Launch process via UI +From UI "Process List" menu entry you can run a process by clicking on "Rocket" action. +You can manage this behaviour by setting some ui option `ui_launch_mode` + +| Value | UI behaviour | +|:---------------:|:--------------------------------------------------------------:| +| modal (default) | On click, open confirmation modal to confirm process execution | +| form | On click, open a form to set input and context execution | +| null | Run process without any confirmation | + +### Launch process via http request +You can launch a process via http post request +First you need to generate a token via UI User edit form. The UiProcess generate for you a auth token (keep it in secured area, it will display once). + +That's all, now you can launch a process via http post request + +***Curl sample*** +```bash +make bash +curl --location 'http://apache2/http/process/execute?code=demo.die' \ +--header 'Authorization: Bearer 3da8409b5f5b640fb0c43d68e8ac8d23' \ +--form 'input=@"/file.csv"' \ +--form 'context[context_1]="FOO"' \ +--form 'context[context_2]="BAR"' +``` +* Query string code parameter must be a valid process code +* Header Authorization: Bearer is the previously generated token +* input could be string or file representation +* context you can pass multiple context values + + +### Scheduler +You can schedule process execution via UI using cron expression (*/5 * * * *) or periodical triggers (5 seconds) +For more details about cron expression and periodical triggers visit +https://symfony.com/doc/6.4/scheduler.html#cron-expression-triggers and https://symfony.com/doc/6.4/scheduler.html#periodical-triggers + +In order to make scheduler process working be sure the following command is running +```bash +bin/console messenger:consume scheduler_cron +``` +See more details about ***messenger:consume*** command in consume message section + +## Consume Messages +Symfony messenger is used in order to run process via UI or schedule process + +*To consume process launched via UI make sure the following command is running* +```bash +bin/console messenger:consume execute_process +``` + +*To consume scheduled process make sure the following command is running* +```bash +bin/console messenger:consume scheduler_cron +``` +You can pass some options to messenger:consume command +``` +Options: + -l, --limit=LIMIT Limit the number of received messages + -f, --failure-limit=FAILURE-LIMIT The number of failed messages the worker can consume + -m, --memory-limit=MEMORY-LIMIT The memory limit the worker can consume + -t, --time-limit=TIME-LIMIT The time limit in seconds the worker can handle new messages + --sleep=SLEEP Seconds to sleep before asking for new messages after no messages were found [default: 1] + -b, --bus=BUS Name of the bus to which received messages should be dispatched (if not passed, bus is determined automatically) + --queues=QUEUES Limit receivers to only consume from the specified queues (multiple values allowed) + --no-reset Do not reset container services after each message +``` + +It's recommended to use supervisor app or equivalent to keep command alive + +***Sample supervisor configuration*** +``` +[program:scheduler] +command=php /var/www/html/bin/console messenger:consume scheduler_cron +autostart=false +autorestart=true +startretries=1 +startsecs=1 +redirect_stderr=true +stderr_logfile=/var/log/supervisor.scheduler-err.log +stdout_logfile=/var/log/supervisor.scheduler-out.log +user=www-data +killasgroup=true +stopasgroup=true + +[program:process] +command=php /var/www/html/bin/console messenger:consume execute_process +autostart=false +autorestart=true +startretries=1 +startsecs=1 +redirect_stderr=true +stderr_logfile=/var/log/supervisor.process-err.log +stdout_logfile=/var/log/supervisor.process-out.log +user=www-data +killasgroup=true +stopasgroup=true +``` + +## Reference + +_TODO_ diff --git a/docs/reference/tasks/_template.md b/docs/reference/tasks/_template.md new file mode 100644 index 0000000..ed1d4a5 --- /dev/null +++ b/docs/reference/tasks/_template.md @@ -0,0 +1,44 @@ +TaskName +======== + +_Describe main goal an use cases of the task_ + +Task reference +-------------- + +* **Service**: `ClassName` + +Accepted inputs +--------------- + +_Description of allowed types_ + +Possible outputs +---------------- + +_Description of possible types_ + +Options +------- + +| Code | Type | Required | Default | Description | +| ---- | ---- | :------: | ------- | ----------- | +| `code` | `type` | **X** _or nothing_ | `default value` _if available_ | _description_ | + +Examples +-------- + +_YAML samples and explanations_ + +* Example 1 + - details + - details + +```yaml +# Task configuration level +code: + service: '@service_ref' + options: + a: 1 + b: 2 +``` diff --git a/phpcs.xml.dist b/phpcs.xml.dist deleted file mode 100644 index 89195e2..0000000 --- a/phpcs.xml.dist +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - bin/ - config/ - public/ - src/ - tests/ - - diff --git a/phpstan.neon b/phpstan.neon index 648f580..1cd333b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,10 +1,5 @@ parameters: - level: 8 - paths: - - src - excludePaths: - - src/DependencyInjection/Configuration.php - ignoreErrors: - - '#is never written, only read#' - - '#has no value type specified in iterable type#' - - '#has parameter .* with no value type specified in iterable type#' \ No newline at end of file + level: 8 + paths: + - src + - tests diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..766495c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + tests + + + + + + src + + + diff --git a/public/logo.jpg b/public/logo.jpg new file mode 100644 index 0000000..d110ec9 Binary files /dev/null and b/public/logo.jpg differ diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..997c7e5 --- /dev/null +++ b/rector.php @@ -0,0 +1,33 @@ +withPhpVersion(PhpVersion::PHP_82) + ->withPaths([ + __DIR__.'/src', + __DIR__.'/tests', + ]) + ->withPhpSets(php82: true) + // here we can define, what prepared sets of rules will be applied + ->withPreparedSets( + deadCode: true, + codeQuality: true + ) + ->withSets([ + LevelSetList::UP_TO_PHP_82, + SymfonySetList::SYMFONY_64, + SymfonySetList::SYMFONY_71, + SymfonySetList::SYMFONY_CODE_QUALITY, + SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, + SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES, + DoctrineSetList::DOCTRINE_CODE_QUALITY, + DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES, + ]) +; diff --git a/src/Admin/Field/ContextField.php b/src/Admin/Field/ContextField.php new file mode 100644 index 0000000..c67a930 --- /dev/null +++ b/src/Admin/Field/ContextField.php @@ -0,0 +1,30 @@ +setProperty($propertyName) + ->setLabel($label) + ->setTemplatePath('@CleverAgeUiProcess/admin/field/array.html.twig'); + } +} diff --git a/src/Admin/Field/EnumField.php b/src/Admin/Field/EnumField.php new file mode 100644 index 0000000..fd936f2 --- /dev/null +++ b/src/Admin/Field/EnumField.php @@ -0,0 +1,30 @@ +setProperty($propertyName) + ->setLabel($label) + ->setTemplatePath('@CleverAgeUiProcess/admin/field/enum.html.twig'); + } +} diff --git a/src/Admin/Field/LogLevelField.php b/src/Admin/Field/LogLevelField.php new file mode 100644 index 0000000..6f6fcfb --- /dev/null +++ b/src/Admin/Field/LogLevelField.php @@ -0,0 +1,30 @@ +setProperty($propertyName) + ->setLabel($label) + ->setTemplatePath('@CleverAgeUiProcess/admin/field/log_level.html.twig'); + } +} diff --git a/src/Admin/Filter/LogProcessFilter.php b/src/Admin/Filter/LogProcessFilter.php new file mode 100644 index 0000000..15aac0e --- /dev/null +++ b/src/Admin/Filter/LogProcessFilter.php @@ -0,0 +1,63 @@ + $executionId]; + } + + return (new self()) + ->setFilterFqcn(self::class) + ->setProperty('process') + ->setLabel($label) + ->setFormType(ChoiceFilterType::class) + ->setFormTypeOption('value_type_options', ['choices' => $choices]) + ->setFormTypeOption('data', ['comparison' => ComparisonType::EQ, 'value' => $executionId]); + } + + public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void + { + $value = $filterDataDto->getValue(); + $queryBuilder->join('entity.processExecution', 'pe'); + if (is_numeric($value)) { + $queryBuilder->andWhere($queryBuilder->expr()->eq('pe.id', ':id')); + $queryBuilder->setParameter('id', $value); + + return; + } + $queryBuilder->where('pe.code IN (:codes)'); + $queryBuilder->setParameter('codes', $filterDataDto->getValue()); + } +} diff --git a/src/CleverAgeProcessUiBundle.php b/src/CleverAgeProcessUiBundle.php deleted file mode 100644 index b89f243..0000000 --- a/src/CleverAgeProcessUiBundle.php +++ /dev/null @@ -1,33 +0,0 @@ -addCompilerPass(new RegisterLogHandlerCompilerPass()); - } - - public function getPath(): string - { - return \dirname(__DIR__); - } -} diff --git a/src/CleverAgeUiProcessBundle.php b/src/CleverAgeUiProcessBundle.php new file mode 100644 index 0000000..8acf1d7 --- /dev/null +++ b/src/CleverAgeUiProcessBundle.php @@ -0,0 +1,24 @@ +managerRegistry = $managerRegistry; - } - - /** - * @required - */ - public function setProcessLogDir(string $processLogDir): void - { - $this->processLogDir = $processLogDir; - } - - protected function configure(): void - { - $this->setName('cleverage:process-ui:purge'); - $this->setDescription('Purge process_execution table.'); - $this->setDefinition( - new InputDefinition([ - new InputOption( - 'days', - 'd', - InputOption::VALUE_OPTIONAL, - 'Days to keep. Default 180', - 180 - ), - new InputOption( - 'remove-files', - 'rf', - InputOption::VALUE_NEGATABLE, - 'Remove log files ? (default false)', - false - ), - ]) - ); - } - - public function execute(InputInterface $input, OutputInterface $output): int - { - $days = $input->getOption('days'); - $removeFiles = $input->getOption('remove-files'); - $date = new \DateTime(); - $date->modify("-$days day"); - if ($removeFiles) { - $finder = new Finder(); - $fs = new Filesystem(); - $finder->in($this->processLogDir)->date('before '.$date->format(DateTimeInterface::ATOM)); - $count = $finder->count(); - $fs->remove($finder); - $output->writeln("$count log files are deleted on filesystem."); - } - /** @var ProcessExecutionRepository $repository */ - $repository = $this->managerRegistry->getRepository(ProcessExecution::class); - $repository->deleteBefore($date); - - $output->writeln(<<Process Execution before {$date->format(DateTimeInterface::ATOM)} are deleted into database. - EOT); - - return Command::SUCCESS; - } -} diff --git a/src/Command/UserCreateCommand.php b/src/Command/UserCreateCommand.php index adeb90f..806cc0c 100644 --- a/src/Command/UserCreateCommand.php +++ b/src/Command/UserCreateCommand.php @@ -2,51 +2,57 @@ declare(strict_types=1); -namespace CleverAge\ProcessUiBundle\Command; +/* + * This file is part of the CleverAge/UiProcessBundle package. + * + * Copyright (c) Clever-Age + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CleverAge\UiProcessBundle\Command; -use CleverAge\ProcessUiBundle\Entity\User; +use CleverAge\UiProcessBundle\Entity\User; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Validator\ValidatorInterface; -/** - * Class UserCreateCommand. - */ -final class UserCreateCommand extends Command +#[AsCommand( + name: 'cleverage:ui-process:user-create', + description: 'Command to create a new admin into database for ui process.' +)] +class UserCreateCommand extends Command { - private ValidatorInterface $validator; - private UserPasswordHasherInterface $passwordEncoder; - private EntityManagerInterface $em; - public function __construct( - ValidatorInterface $validator, - UserPasswordHasherInterface $passwordEncoder, - EntityManagerInterface $em + private readonly ValidatorInterface $validator, + private readonly UserPasswordHasherInterface $passwordEncoder, + private readonly EntityManagerInterface $em, ) { - $this->validator = $validator; - $this->passwordEncoder = $passwordEncoder; - $this->em = $em; parent::__construct(); } - protected function configure(): void - { - $this->setName('cleverage:process-ui:user-create'); - $this->setDescription('Command to create a new admin into database for process ui.'); - } - protected function execute(InputInterface $input, OutputInterface $output): int { $style = new SymfonyStyle($input, $output); $username = $this->ask('Please enter the email.', $style, [new Email()]); - $password = $this->ask('Please enter the user password.', $style, [new NotBlank(), new Length(min: 8)]); + + $password = $this->askPassword( + (new Question('Please enter the user password.'))->setHidden(true)->setHiddenFallback(false), + $input, + $output + ); $user = new User(); $user->setEmail($username); @@ -62,7 +68,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } /** - * @param array $constraints + * @param Constraint[] $constraints */ private function ask(string $question, SymfonyStyle $style, array $constraints = []): mixed { @@ -77,4 +83,21 @@ private function ask(string $question, SymfonyStyle $style, array $constraints = return $value; } + + private function askPassword(Question $question, InputInterface $input, OutputInterface $output): mixed + { + $constraints = [new NotBlank(), new Length(min: 8)]; + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $password = $helper->ask($input, $output, $question); + $violations = $this->validator->validate($password, $constraints); + while ($violations->count() > 0) { + $violationsMessage = $violations->get(0)->getMessage(); + $output->writeln("$violationsMessage"); + $password = $helper->ask($input, $output, $question); + $violations = $this->validator->validate($password, $constraints); + } + + return $password; + } } diff --git a/src/Controller/Admin/LogRecordCrudController.php b/src/Controller/Admin/LogRecordCrudController.php new file mode 100644 index 0000000..ce515a5 --- /dev/null +++ b/src/Controller/Admin/LogRecordCrudController.php @@ -0,0 +1,95 @@ +setMaxLength(512), + DateTimeField::new('createdAt')->setFormat('Y/M/dd H:mm:ss'), + ContextField::new('context') + ->onlyOnDetail(), + BooleanField::new('contextIsEmpty', 'Has context info ?') + ->onlyOnIndex() + ->renderAsSwitch(false), + ]; + } + + public function configureCrud(Crud $crud): Crud + { + return $crud->showEntityActionsInlined()->setPaginatorPageSize(250); + } + + public function configureActions(Actions $actions): Actions + { + return Actions::new() + ->add(Crud::PAGE_INDEX, Action::new('detail', false, 'fas fa-eye') + ->setHtmlAttributes( + [ + 'data-bs-toggle' => 'tooltip', + 'data-bs-placement' => 'top', + 'title' => 'Show details', + ] + ) + ->linkToCrudAction('detail')) + ->add(Crud::PAGE_DETAIL, 'index'); + } + + public function configureFilters(Filters $filters): Filters + { + $id = $this->requestStack->getMainRequest()?->query->all('filters')['process']['value'] ?? null; + $processList = $this->processConfigurationsManager->getPublicProcesses(); + $processList = array_map(fn (ProcessConfiguration $cfg) => $cfg->getCode(), $processList); + + return $filters->add( + LogProcessFilter::new('process', $processList, $id) + )->add( + ChoiceFilter::new('level')->setChoices(array_combine(Level::NAMES, Level::VALUES)) + )->add('message')->add('context')->add('createdAt'); + } +} diff --git a/src/Controller/Admin/Process/LaunchAction.php b/src/Controller/Admin/Process/LaunchAction.php new file mode 100644 index 0000000..8f6bed5 --- /dev/null +++ b/src/Controller/Admin/Process/LaunchAction.php @@ -0,0 +1,140 @@ + '\w+'], + methods: ['POST', 'GET'] +)] +#[IsGranted('ROLE_USER')] +class LaunchAction extends AbstractController +{ + public function __construct(private readonly MessageBusInterface $messageBus) + { + } + + public function __invoke( + RequestStack $requestStack, + string $uploadDirectory, + ProcessConfigurationsManager $processConfigurationsManager, + AdminContext $context, + ): Response { + $processCode = $requestStack->getMainRequest()?->get('process'); + if (null === $processCode) { + throw new MissingProcessException(); + } + $uiOptions = $processConfigurationsManager->getUiOptions($processCode); + if (null === $uiOptions) { + throw new \InvalidArgumentException('Missing UI Options'); + } + if (null === $uiOptions['ui_launch_mode'] || 'modal' === $uiOptions['ui_launch_mode']) { + $this->dispatch($processCode); + $this->addFlash( + 'success', + 'Process has been added to queue. It will start as soon as possible' + ); + + return $this->redirectToRoute('process', ['routeName' => 'process_list']); + } + $form = $this->createForm( + LaunchType::class, + null, + [ + 'constraints' => $uiOptions['constraints'], + 'process_code' => $processCode, + ] + ); + if (false === $form->isSubmitted()) { + $default = $uiOptions['default']; + if (false === $form->get('input')->getConfig()->getType()->getInnerType() instanceof TextType + && isset($default['input']) + ) { + unset($default['input']); + } + $form->setData($default); + } + $form->handleRequest($requestStack->getMainRequest()); + if ($form->isSubmitted() && $form->isValid()) { + $input = $form->get('input')->getData(); + if ($input instanceof UploadedFile) { + $filename = \sprintf('%s/%s.%s', $uploadDirectory, Uuid::v4(), $input->getClientOriginalExtension()); + (new Filesystem())->dumpFile($filename, $input->getContent()); + $input = $filename; + } + $this->dispatch( + $form->getConfig()->getOption('process_code'), + $input, + $form->get('context')->getData() + ); + $this->addFlash( + 'success', + 'Process has been added to queue. It will start as soon as possible' + ); + + return $this->redirectToRoute('process', ['routeName' => 'process_list']); + } + $context->getAssets()->addJsAsset(Asset::fromEasyAdminAssetPackage('field-collection.js')->getAsDto()); + + return $this->render( + '@CleverAgeUiProcess/admin/process/launch.html.twig', + [ + 'form' => $form->createView(), + ] + ); + } + + /** + * @param mixed[] $context + */ + protected function dispatch(string $processCode, mixed $input = null, array $context = []): void + { + $message = new ProcessExecuteMessage( + $processCode, + $input, + array_merge( + ['execution_user' => $this->getUser()?->getEmail()], + $context + ) + ); + $this->messageBus->dispatch($message); + } + + protected function getUser(): ?User + { + /** @var User $user */ + $user = parent::getUser(); + + return $user; + } +} diff --git a/src/Controller/Admin/Process/ListAction.php b/src/Controller/Admin/Process/ListAction.php new file mode 100644 index 0000000..4bd60e9 --- /dev/null +++ b/src/Controller/Admin/Process/ListAction.php @@ -0,0 +1,35 @@ +render( + '@CleverAgeUiProcess/admin/process/list.html.twig', + [ + 'processes' => $processConfigurationsManager->getPublicProcesses(), + ] + ); + } +} diff --git a/src/Controller/Admin/Process/UploadAndExecuteAction.php b/src/Controller/Admin/Process/UploadAndExecuteAction.php new file mode 100644 index 0000000..8392263 --- /dev/null +++ b/src/Controller/Admin/Process/UploadAndExecuteAction.php @@ -0,0 +1,81 @@ + '\w+'], + methods: ['POST', 'GET'] +)] +#[IsGranted('ROLE_USER')] +class UploadAndExecuteAction extends AbstractController +{ + public function __invoke( + RequestStack $requestStack, + MessageBusInterface $messageBus, + string $uploadDirectory, + #[ValueResolver('process')] ProcessConfiguration $processConfiguration, + ): Response { + if (!$processConfiguration->getEntryPoint() instanceof TaskConfiguration) { + throw new \RuntimeException('You must set an entry_point.'); + } + $form = $this->createForm( + ProcessUploadFileType::class, + null, + ['process_code' => $requestStack->getMainRequest()?->get('process')] + ); + $form->handleRequest($requestStack->getMainRequest()); + if ($form->isSubmitted() && $form->isValid()) { + /** @var UploadedFile $file */ + $file = $form->getData(); + $savedFilepath = \sprintf('%s/%s.%s', $uploadDirectory, Uuid::v4(), $file->getClientOriginalExtension()); + (new Filesystem())->dumpFile($savedFilepath, $file->getContent()); + $messageBus->dispatch( + new ProcessExecuteMessage( + $form->getConfig()->getOption('process_code'), + $savedFilepath + ) + ); + $this->addFlash( + 'success', + 'Process has been added to queue. It will start as soon as possible' + ); + + return $this->redirectToRoute('process', ['routeName' => 'process_list']); + } + + return $this->render( + '@CleverAgeUiProcess/admin/process/upload_and_execute.html.twig', + [ + 'form' => $form->createView(), + ] + ); + } +} diff --git a/src/Controller/Admin/ProcessDashboardController.php b/src/Controller/Admin/ProcessDashboardController.php new file mode 100644 index 0000000..6a60e97 --- /dev/null +++ b/src/Controller/Admin/ProcessDashboardController.php @@ -0,0 +1,83 @@ +container->get(AdminUrlGenerator::class); + + return $this->redirect($adminUrlGenerator->setController(ProcessExecutionCrudController::class)->generateUrl()); + } + + public function configureDashboard(): Dashboard + { + return Dashboard::new() + ->setTitle(''); + } + + public function configureMenuItems(): iterable + { + yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home'); + yield MenuItem::subMenu('Process', 'fas fa-gear')->setSubItems( + [ + MenuItem::linkToRoute('Process list', 'fas fa-list', 'process_list'), + MenuItem::linkToCrud('Executions', 'fas fa-rocket', ProcessExecution::class), + MenuItem::linkToCrud('Logs', 'fas fa-pen', LogRecord::class), + MenuItem::linkToCrud('Scheduler', 'fas fa-solid fa-clock', ProcessSchedule::class), + ] + ); + if ($this->isGranted('ROLE_ADMIN')) { + yield MenuItem::subMenu('Users', 'fas fa-user')->setSubItems( + [ + MenuItem::linkToCrud('User List', 'fas fa-user', User::class), + ] + ); + } + } + + public function configureCrud(): Crud + { + /** @var ?User $user */ + $user = $this->getUser(); + if (null !== $user?->getLocale()) { + $this->localeSwitcher->setLocale($user->getLocale()); + } + + return parent::configureCrud()->setTimezone($user?->getTimezone() ?? date_default_timezone_get()); + } +} diff --git a/src/Controller/Admin/ProcessExecutionCrudController.php b/src/Controller/Admin/ProcessExecutionCrudController.php new file mode 100644 index 0000000..5e60a60 --- /dev/null +++ b/src/Controller/Admin/ProcessExecutionCrudController.php @@ -0,0 +1,156 @@ +setFormat('Y/M/dd H:mm:ss'), + DateTimeField::new('endDate')->setFormat('Y/M/dd H:mm:ss'), + TextField::new('source')->setTemplatePath('@CleverAgeUiProcess/admin/field/process_source.html.twig'), + TextField::new('target')->setTemplatePath('@CleverAgeUiProcess/admin/field/process_target.html.twig'), + TextField::new('duration')->formatValue(function ($value, ProcessExecution $entity) { + return $entity->duration(); // returned format can be changed here + }), + ArrayField::new('report')->setTemplatePath('@CleverAgeUiProcess/admin/field/report.html.twig'), + ContextField::new('context'), + ]; + } + + public function configureCrud(Crud $crud): Crud + { + $crud->showEntityActionsInlined(); + $crud->setDefaultSort(['startDate' => 'DESC']); + + return $crud; + } + + public function configureActions(Actions $actions): Actions + { + return Actions::new() + ->add( + Crud::PAGE_INDEX, + Action::new('showLogs', false, 'fas fa-eye') + ->setHtmlAttributes( + [ + 'data-bs-toggle' => 'tooltip', + 'data-bs-placement' => 'top', + 'title' => 'Show logs stored in database', + ] + ) + ->linkToCrudAction('showLogs') + ->displayIf(fn (ProcessExecution $entity) => $this->processExecutionRepository->hasLogs($entity)) + )->add( + Crud::PAGE_INDEX, + Action::new('downloadLogfile', false, 'fas fa-download') + ->setHtmlAttributes( + [ + 'data-bs-toggle' => 'tooltip', + 'data-bs-placement' => 'top', + 'title' => 'Download log file', + ] + ) + ->linkToCrudAction('downloadLogFile') + ->displayIf(fn (ProcessExecution $entity) => file_exists($this->getLogFilePath($entity))) + ); + } + + public function showLogs(AdminContext $adminContext): RedirectResponse + { + /** @var AdminUrlGenerator $adminUrlGenerator */ + $adminUrlGenerator = $this->container->get(AdminUrlGenerator::class); + $url = $adminUrlGenerator + ->setController(LogRecordCrudController::class) + ->setAction('index') + ->setEntityId(null) + ->set( + 'filters', + [ + 'process' => [ + 'comparison' => '=', + 'value' => $this->getContext()?->getEntity()->getInstance()->getId(), + ], + ] + ) + ->generateUrl(); + + return $this->redirect($url); + } + + public function downloadLogFile( + AdminContext $context, + ): Response { + /** @var ProcessExecution $processExecution */ + $processExecution = $context->getEntity()->getInstance(); + $filepath = $this->getLogFilePath($processExecution); + $basename = basename($filepath); + $content = file_get_contents($filepath); + if (false === $content) { + throw new NotFoundHttpException('Log file not found.'); + } + $response = new Response($content); + $response->headers->set('Content-Type', 'text/plain; charset=utf-8'); + $response->headers->set('Content-Disposition', "attachment; filename=\"$basename\""); + + return $response; + } + + public function configureFilters(Filters $filters): Filters + { + return $filters->add('code')->add('startDate'); + } + + private function getLogFilePath(ProcessExecution $processExecution): string + { + return $this->logDirectory. + \DIRECTORY_SEPARATOR.$processExecution->code. + \DIRECTORY_SEPARATOR.$processExecution->logFilename + ; + } +} diff --git a/src/Controller/Admin/ProcessScheduleCrudController.php b/src/Controller/Admin/ProcessScheduleCrudController.php new file mode 100644 index 0000000..df5b2f2 --- /dev/null +++ b/src/Controller/Admin/ProcessScheduleCrudController.php @@ -0,0 +1,122 @@ +setPageTitle('index', 'Scheduler') + ->showEntityActionsInlined(); + } + + public function configureActions(Actions $actions): Actions + { + return $actions + ->update(Crud::PAGE_INDEX, Action::NEW, fn (Action $action) => $action->setIcon('fa fa-plus') + ->setLabel(false) + ->addCssClass(''))->update(Crud::PAGE_INDEX, Action::EDIT, fn (Action $action) => $action->setIcon('fa fa-edit') + ->setLabel(false) + ->addCssClass('text-warning'))->update(Crud::PAGE_INDEX, Action::DELETE, fn (Action $action) => $action->setIcon('fa fa-trash-o') + ->setLabel(false) + ->addCssClass(''))->update(Crud::PAGE_INDEX, Action::BATCH_DELETE, fn (Action $action) => $action->setLabel('Delete') + ->addCssClass('')); + } + + public static function getEntityFqcn(): string + { + return ProcessSchedule::class; + } + + public function configureFields(string $pageName): iterable + { + $choices = array_map(fn (ProcessConfiguration $configuration) => [$configuration->getCode()], $this->processConfigurationsManager->getPublicProcesses()); + + return [ + FormField::addTab('General'), + TextField::new('process') + ->setFormType(ChoiceType::class) + ->setFormTypeOption('choices', array_combine(array_keys($choices), array_keys($choices))), + EnumField::new('type') + ->setFormType(EnumType::class) + ->setFormTypeOption('class', ProcessScheduleType::class), + TextField::new('expression'), + DateTimeField::new('nextExecution') + ->setFormTypeOption('mapped', false) + ->setVirtual(true) + ->hideOnForm() + ->hideOnDetail() + ->formatValue(fn ($value, ProcessSchedule $entity) => ProcessScheduleType::CRON === $entity->getType() + ? CronExpressionTrigger::fromSpec($entity->getExpression() ?? '') + ->getNextRunDate(new \DateTimeImmutable()) + ?->format('c') + : null), + FormField::addTab('Input'), + TextField::new('input'), + FormField::addTab('Context'), + ArrayField::new('context') + ->setFormTypeOption('entry_type', ProcessContextType::class) + ->hideOnIndex() + ->setFormTypeOption('entry_options.label', 'Context (key/value)') + ->setFormTypeOption('label', '') + ->setFormTypeOption('required', false), + ]; + } + + public function index(AdminContext $context): KeyValueStore|RedirectResponse|Response + { + if (false === $this->schedulerIsRunning()) { + $this->addFlash('warning', 'To run scheduler, ensure "bin/console messenger:consume scheduler_cron" console is alive. See https://symfony.com/doc/current/messenger.html#supervisor-configuration.'); + } + + return parent::index($context); + } + + private function schedulerIsRunning(): bool + { + $process = Process::fromShellCommandline('ps -faux'); + $process->run(); + $out = $process->getOutput(); + + return str_contains($out, 'scheduler_cron'); + } +} diff --git a/src/Controller/Admin/Security/LoginController.php b/src/Controller/Admin/Security/LoginController.php new file mode 100644 index 0000000..c55b1e0 --- /dev/null +++ b/src/Controller/Admin/Security/LoginController.php @@ -0,0 +1,33 @@ +render( + '@CleverAgeUiProcess/admin/login.html.twig', + [ + 'page_title' => 'Login', + 'target_path' => '/process', + ] + ); + } +} diff --git a/src/Controller/Admin/Security/LogoutController.php b/src/Controller/Admin/Security/LogoutController.php new file mode 100644 index 0000000..9ddd963 --- /dev/null +++ b/src/Controller/Admin/Security/LogoutController.php @@ -0,0 +1,30 @@ +logout(); + + return $this->redirectToRoute('process_login'); + } +} diff --git a/src/Controller/Admin/UserCrudController.php b/src/Controller/Admin/UserCrudController.php new file mode 100644 index 0000000..d084fdd --- /dev/null +++ b/src/Controller/Admin/UserCrudController.php @@ -0,0 +1,121 @@ + $roles */ + public function __construct(private readonly array $roles) + { + } + + public function configureCrud(Crud $crud): Crud + { + $crud->showEntityActionsInlined(); + $crud->setEntityPermission('ROLE_ADMIN'); + + return $crud; + } + + public static function getEntityFqcn(): string + { + return User::class; + } + + public function configureFields(string $pageName): iterable + { + yield FormField::addTab('Credentials')->setIcon('fa fa-key'); + yield EmailField::new('email'); + yield TextField::new('password', 'New password') + ->onlyOnForms() + ->setFormType(RepeatedType::class) + ->setFormTypeOptions( + [ + 'type' => PasswordType::class, + 'first_options' => [ + 'label' => 'New password', + 'hash_property_path' => 'password', + 'always_empty' => false, + ], + 'second_options' => ['label' => 'Repeat password'], + 'mapped' => false, + ] + ); + yield FormField::addTab('Informations')->setIcon('fa fa-user'); + yield TextField::new('firstname'); + yield TextField::new('lastname'); + + yield FormField::addTab('Roles')->setIcon('fa fa-theater-masks'); + yield ChoiceField::new('roles', false) + ->setChoices($this->roles) + ->setFormTypeOptions(['multiple' => true, 'expanded' => true]); + yield FormField::addTab('Intl.')->setIcon('fa fa-flag'); + yield TimezoneField::new('timezone'); + yield LocaleField::new('locale'); + } + + public function configureActions(Actions $actions): Actions + { + return $actions + ->update(Crud::PAGE_INDEX, Action::NEW, fn (Action $action) => $action->setIcon('fa fa-plus') + ->setLabel(false) + ->addCssClass(''))->update(Crud::PAGE_INDEX, Action::EDIT, fn (Action $action) => $action->setIcon('fa fa-edit') + ->setLabel(false) + ->addCssClass('text-warning'))->update(Crud::PAGE_INDEX, Action::DELETE, fn (Action $action) => $action->setIcon('fa fa-trash-o') + ->setLabel(false) + ->addCssClass(''))->update(Crud::PAGE_INDEX, Action::BATCH_DELETE, fn (Action $action) => $action->setLabel('Delete') + ->addCssClass(''))->add(Crud::PAGE_EDIT, Action::new('generateToken')->linkToCrudAction('generateToken')); + } + + public function generateToken(AdminContext $adminContext, AdminUrlGenerator $adminUrlGenerator): Response + { + /** @var User $user */ + $user = $adminContext->getEntity()->getInstance(); + $token = md5(uniqid(date('YmdHis'))); + $user->setToken((new Pbkdf2PasswordHasher())->hash($token)); + $this->persistEntity( + $this->container->get('doctrine')->getManagerForClass($adminContext->getEntity()->getFqcn()), + $user + ); + $this->addFlash('success', 'New token generated '.$token.' (keep it in secured area. This token will never be displayed anymore)'); + + return $this->redirect( + $adminUrlGenerator + ->setController(self::class) + ->setAction(Action::EDIT) + ->setEntityId($user->getId()) + ->generateUrl() + ); + } +} diff --git a/src/Controller/Crud/ProcessCrudController.php b/src/Controller/Crud/ProcessCrudController.php deleted file mode 100644 index ee17c4b..0000000 --- a/src/Controller/Crud/ProcessCrudController.php +++ /dev/null @@ -1,141 +0,0 @@ -processUiConfigurationManager = $processUiConfigurationManager; - } - - public static function getEntityFqcn(): string - { - return Process::class; - } - - public function configureCrud(Crud $crud): Crud - { - $crud->showEntityActionsInlined(); - $crud->setDefaultSort(['lastExecutionDate' => SortOrder::DESC]); - $crud->setEntityPermission('ROLE_ADMIN'); - $crud->setSearchFields(['processCode', 'source', 'target']); - - return $crud; - } - - /** - * @return array - */ - public function configureFields(string $pageName): array - { - return [ - Field::new('processCode', 'Process'), - 'source', - 'target', - 'lastExecutionDate', - IntegerField::new('lastExecutionStatus')->formatValue(static function (?int $value) { - return match ($value) { - ProcessExecution::STATUS_FAIL => '', - ProcessExecution::STATUS_START => '', - ProcessExecution::STATUS_SUCCESS => '', - default => '', - }; - }), - ]; - } - - public function configureActions(Actions $actions): Actions - { - $actions->remove(Crud::PAGE_INDEX, Action::EDIT); - $actions->remove(Crud::PAGE_INDEX, Action::DELETE); - $actions->remove(Crud::PAGE_INDEX, Action::NEW); - $runProcess = Action::new('run', '', 'fa fa-rocket') - ->linkToCrudAction('runProcessAction'); - $runProcess->setHtmlAttributes(['data-toggle' => 'tooltip', 'title' => 'Run process in background']); - $runProcess->displayIf(fn (Process $process) => $this->processUiConfigurationManager->canRun($process)); - $viewHistoryAction = Action::new('viewHistory', '', 'fa fa-history') - ->linkToCrudAction('viewHistoryAction'); - $viewHistoryAction->setHtmlAttributes(['data-toggle' => 'tooltip', 'title' => 'View executions history']); - $actions->add(Crud::PAGE_INDEX, $viewHistoryAction); - $actions->add(Crud::PAGE_INDEX, $runProcess); - - return $actions; - } - - public function runProcessAction(AdminContext $context): Response - { - try { - /** @var Process $process */ - $process = $context->getEntity()->getInstance(); - if (false === $this->processUiConfigurationManager->canRun($process)) { - $this->addFlash( - 'warning', - 'Process is not run-able via Ui.' - ); - } else { - $message = new ProcessRunMessage($process->getProcessCode()); - $this->dispatchMessage($message); - $this->addFlash( - 'success', - 'Process has been added to queue. It will start as soon as possible' - ); - } - } catch (Exception $e) { - $this->addFlash('warning', 'Cannot run process.'); - } - - /** @var AdminUrlGenerator $routeBuilder */ - $routeBuilder = $this->get(AdminUrlGenerator::class); - - return $this->redirect( - $routeBuilder->setController(__CLASS__)->setAction(Action::INDEX)->generateUrl() - ); - } - - public function viewHistoryAction(AdminContext $adminContext): RedirectResponse - { - /** @var AdminUrlGenerator $routeBuilder */ - $routeBuilder = $this->get(AdminUrlGenerator::class); - /** @var Process $process */ - $process = $adminContext->getEntity()->getInstance(); - - return $this->redirect( - $routeBuilder - ->setController(ProcessExecutionCrudController::class) - ->setEntityId(null) - ->setAction(Action::INDEX) - ->setAll([ - 'filters' => [ - 'processCode' => ['comparison' => ComparisonType::EQ, 'value' => $process->getProcessCode()], - ], - ]) - ->generateUrl() - ); - } -} diff --git a/src/Controller/Crud/ProcessExecutionCrudController.php b/src/Controller/Crud/ProcessExecutionCrudController.php deleted file mode 100644 index c5e8a4c..0000000 --- a/src/Controller/Crud/ProcessExecutionCrudController.php +++ /dev/null @@ -1,146 +0,0 @@ -indexLogs = $indexLogs; - } - - /** - * @required - */ - public function setProcessLogDir(string $processLogDir): void - { - $this->processLogDir = $processLogDir; - } - - /** - * @required true - */ - public function setProcessUiConfigurationManager(ProcessUiConfigurationManager $processUiConfigurationManager): void - { - $this->processUiConfigurationManager = $processUiConfigurationManager; - } - - public static function getEntityFqcn(): string - { - return ProcessExecution::class; - } - - public function configureCrud(Crud $crud): Crud - { - $crud->showEntityActionsInlined(); - $crud->setDefaultSort(['startDate' => SortOrder::DESC]); - $crud->setEntityPermission('ROLE_ADMIN'); - $crud->setSearchFields(true === $this->indexLogs ? ['processCode', 'source', 'target', 'logRecords.message'] : ['processCode', 'source', 'target']); - - return $crud; - } - - /** - * @return array - */ - public function configureFields(string $pageName): array - { - return [ - Field::new('processCode', 'Process'), - 'source', - 'target', - 'startDate', - 'endDate', - IntegerField::new('status')->formatValue(static function (?int $value) { - return match ($value) { - ProcessExecution::STATUS_FAIL => '', - ProcessExecution::STATUS_START => '', - ProcessExecution::STATUS_SUCCESS => '', - default => '', - }; - }), - ]; - } - - public function configureFilters(Filters $filters): Filters - { - $processCodeChoices = $this->processUiConfigurationManager->getProcessChoices(); - if (\count($processCodeChoices) > 0) { - $filters->add(ChoiceFilter::new('processCode', 'Process')->setChoices($processCodeChoices)); - } - - $sourceChoices = $this->processUiConfigurationManager->getSourceChoices(); - if (\count($sourceChoices) > 0) { - $filters->add(ChoiceFilter::new('source')->setChoices($sourceChoices)); - } - - $targetChoices = $this->processUiConfigurationManager->getTargetChoices(); - if (\count($targetChoices) > 0) { - $filters->add(ChoiceFilter::new('target')->setChoices($targetChoices)); - } - $filters->add(ChoiceFilter::new('status')->setChoices([ - 'failed' => ProcessExecution::STATUS_FAIL, - 'success' => ProcessExecution::STATUS_SUCCESS, - 'started' => ProcessExecution::STATUS_START, - ])); - $filters->add('startDate'); - $filters->add('endDate'); - - return $filters; - } - - public function configureActions(Actions $actions): Actions - { - $actions->remove(Crud::PAGE_INDEX, Action::EDIT); - $actions->remove(Crud::PAGE_INDEX, Action::DELETE); - $actions->remove(Crud::PAGE_INDEX, Action::NEW); - - $downloadLogAction = Action::new('downloadLog', '', 'fa fa-file-download') - ->linkToCrudAction('downloadLog'); - $downloadLogAction->setHtmlAttributes(['data-toggle' => 'tooltip', 'title' => 'Download log file']); - $actions->add(Crud::PAGE_INDEX, $downloadLogAction); - - return $actions; - } - - public function downloadLog(AdminContext $context): Response - { - /** @var ProcessExecution $processExecution */ - $processExecution = $context->getEntity()->getInstance(); - $filepath = $this->processLogDir.\DIRECTORY_SEPARATOR.$processExecution->getLog(); - $basename = basename($filepath); - $content = file_get_contents($filepath); - if (false === $content) { - throw new NotFoundHttpException('Log file not found.'); - } - $response = new Response($content); - $response->headers->set('Content-Type', 'text/plain; charset=utf-8'); - $response->headers->set('Content-Disposition', "attachment; filename=\"$basename\""); - - return $response; - } -} diff --git a/src/Controller/Crud/UserCrudController.php b/src/Controller/Crud/UserCrudController.php deleted file mode 100644 index d432029..0000000 --- a/src/Controller/Crud/UserCrudController.php +++ /dev/null @@ -1,128 +0,0 @@ -passwordHasher = $passwordHasher; - } - - public function configureCrud(Crud $crud): Crud - { - $crud->showEntityActionsInlined(); - $crud->setEntityPermission('ROLE_ADMIN'); - - return $crud; - } - - public static function getEntityFqcn(): string - { - return User::class; - } - - public function configureFields(string $pageName): iterable - { - yield FormField::addPanel('Credentials')->setIcon('fa fa-key'); - yield EmailField::new('email'); - yield TextField::new('password', 'New password') - ->onlyOnForms() - ->setFormType(RepeatedType::class) - ->setFormTypeOptions([ - 'type' => PasswordType::class, - 'first_options' => ['label' => 'New password'], - 'second_options' => ['label' => 'Repeat password'], - ]); - - yield FormField::addPanel('Informations')->setIcon('fa fa-user'); - yield TextField::new('firstname'); - yield TextField::new('lastname'); - - yield FormField::addPanel('Roles')->setIcon('fa fa-theater-masks'); - yield ChoiceField::new('roles', false) - ->setChoices(['ROLE_ADMIN' => 'ROLE_ADMIN', 'ROLE_USER' => 'ROLE_USER']) - ->setFormTypeOptions(['multiple' => true, 'expanded' => true]); - } - - public function configureActions(Actions $actions): Actions - { - return $actions - ->update(Crud::PAGE_INDEX, Action::NEW, function (Action $action) { - return $action->setIcon(CleverAgeProcessUiBundle::ICON_NEW) - ->setLabel(CleverAgeProcessUiBundle::LABEL_NEW) - ->addCssClass(CleverAgeProcessUiBundle::CLASS_NEW); - })->update(Crud::PAGE_INDEX, Action::EDIT, function (Action $action) { - return $action->setIcon(CleverAgeProcessUiBundle::ICON_EDIT) - ->setLabel(CleverAgeProcessUiBundle::LABEL_EDIT) - ->addCssClass(CleverAgeProcessUiBundle::CLASS_EDIT); - })->update(Crud::PAGE_INDEX, Action::DELETE, function (Action $action) { - return $action->setIcon(CleverAgeProcessUiBundle::ICON_DELETE) - ->setLabel(CleverAgeProcessUiBundle::LABEL_DELETE) - ->addCssClass(CleverAgeProcessUiBundle::CLASS_DELETE); - })->update(Crud::PAGE_INDEX, Action::BATCH_DELETE, function (Action $action) { - return $action->setLabel('Delete') - ->addCssClass(CleverAgeProcessUiBundle::CLASS_DELETE); - }); - } - - public function createEditFormBuilder( - EntityDto $entityDto, - KeyValueStore $formOptions, - AdminContext $context - ): FormBuilderInterface { - $formBuilder = parent::createEditFormBuilder($entityDto, $formOptions, $context); - - $this->addEncodePasswordEventListener($formBuilder); - - return $formBuilder; - } - - public function createNewFormBuilder( - EntityDto $entityDto, - KeyValueStore $formOptions, - AdminContext $context - ): FormBuilderInterface { - $formBuilder = parent::createNewFormBuilder($entityDto, $formOptions, $context); - - $this->addEncodePasswordEventListener($formBuilder); - - return $formBuilder; - } - - protected function addEncodePasswordEventListener(FormBuilderInterface $formBuilder): void - { - $formBuilder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event): void { - /** @var User $user */ - $user = $event->getData(); - $password = $user->getPassword(); - if ($password) { - $user->setPassword($this->passwordHasher->hashPassword($user, $password)); - } - }); - } -} diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php deleted file mode 100644 index dc90303..0000000 --- a/src/Controller/DashboardController.php +++ /dev/null @@ -1,44 +0,0 @@ -container->get(AdminUrlGenerator::class); - - return $this->redirect($routeBuilder->setController(ProcessCrudController::class)->generateUrl()); - } - - public function configureDashboard(): Dashboard - { - return Dashboard::new()->setTitle('CleverAge Process UI'); - } - - public function configureMenuItems(): iterable - { - yield MenuItem::section('Process', 'fas fa-tasks')->setPermission('ROLE_ADMIN'); - yield MenuItem::linkToCrud('List', 'fa fa-list', Process::class)->setPermission('ROLE_ADMIN'); - yield MenuItem::linkToCrud('History', 'fa fa-history', ProcessExecution::class)->setPermission('ROLE_ADMIN'); - - yield MenuItem::section(); - yield MenuItem::section('Settings', 'fas fa-tools')->setPermission('ROLE_ADMIN'); - yield MenuItem::linkToCrud('Users', 'fa fa-users', User::class)->setPermission('ROLE_ADMIN'); - } -} diff --git a/src/Controller/ProcessExecuteController.php b/src/Controller/ProcessExecuteController.php new file mode 100644 index 0000000..e14260c --- /dev/null +++ b/src/Controller/ProcessExecuteController.php @@ -0,0 +1,52 @@ +validate($httpProcessExecution); + if ($violations->count() > 0) { + $violationsMessages = []; + foreach ($violations as $violation) { + $violationsMessages[] = $violation->getMessage(); + } + throw new UnprocessableEntityHttpException(implode('. ', $violationsMessages)); + } + $bus->dispatch( + new ProcessExecuteMessage( + $httpProcessExecution->code ?? '', + $httpProcessExecution->input, + $httpProcessExecution->context + ) + ); + + return new JsonResponse('Process has been added to queue. It will start as soon as possible.'); + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php deleted file mode 100644 index 2c7c54f..0000000 --- a/src/Controller/SecurityController.php +++ /dev/null @@ -1,46 +0,0 @@ -getUser()) { - return $this->redirectToRoute('/'); - } - - // get the login error if there is one - $error = $authenticationUtils->getLastAuthenticationError(); - // last username entered by the user - $lastUsername = $authenticationUtils->getLastUsername(); - - return $this->render('@EasyAdmin/page/login.html.twig', [ - 'last_username' => $lastUsername, - 'error' => $error, - 'page_title' => 'Login', - 'username_parameter' => 'email', - 'username_label' => 'Email', - 'password_parameter' => 'password', - 'csrf_token_intention' => 'authenticate', - ]); - } - - /** - * @Route("/logout", name="app_logout") - */ - public function logout(): void - { - throw new \LogicException( - 'This method can be blank - it will be intercepted by the logout key on your firewall.' - ); - } -} diff --git a/src/DependencyInjection/CleverAgeProcessUiExtension.php b/src/DependencyInjection/CleverAgeProcessUiExtension.php deleted file mode 100644 index 504ae37..0000000 --- a/src/DependencyInjection/CleverAgeProcessUiExtension.php +++ /dev/null @@ -1,63 +0,0 @@ -load('services.yaml'); - - $configuration = new Configuration(); - $config = $this->processConfiguration($configuration, $configs); - $container->setParameter('clever_age_process_ui.index_logs.enabled', $config['index_logs']['enabled']); - } - - /** - * Update default configuration for migrations, twig form theme, assets ... - */ - public function prepend(ContainerBuilder $container): void - { - $container->loadFromExtension( - 'doctrine_migrations', - [ - 'migrations_paths' => ['CleverAgeProcessUi' => \dirname(__DIR__).'/Migrations'], - ] - ); - $container->loadFromExtension( - 'framework', - [ - 'assets' => ['json_manifest_path' => null], - 'messenger' => [ - 'transport' => [ - [ - 'name' => 'run_process', - 'dsn' => 'doctrine://default', - 'retry_strategy' => ['max_retries' => 0], - ], - [ - 'name' => 'index_logs', - 'dsn' => 'doctrine://default', - 'retry_strategy' => ['max_retries' => 0], - ], - ], - 'routing' => [ - ProcessRunMessage::class => 'run_process', - LogIndexerMessage::class => 'index_logs', - ], - ], - ] - ); - } -} diff --git a/src/DependencyInjection/CleverAgeUiProcessExtension.php b/src/DependencyInjection/CleverAgeUiProcessExtension.php new file mode 100644 index 0000000..9e7654e --- /dev/null +++ b/src/DependencyInjection/CleverAgeUiProcessExtension.php @@ -0,0 +1,171 @@ +findServices($container, __DIR__.'/../../config/services'); + + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + $container->getDefinition(UserCrudController::class) + ->setArgument('$roles', array_combine($config['security']['roles'], $config['security']['roles'])); + $container->getDefinition('cleverage_ui_process.monolog_handler.process') + ->addMethodCall('setReportIncrementLevel', [$config['logs']['report_increment_level']]); + $container->getDefinition(ProcessDashboardController::class) + ->setArgument('$logoPath', $config['design']['logo_path']); + } + + /** + * Update default configuration for migrations, twig form theme, assets ... + */ + public function prepend(ContainerBuilder $container): void + { + $env = $container->getParameter('kernel.environment'); + $container->loadFromExtension( + 'monolog', + [ + 'handlers' => [ + 'pb_ui_file' => [ + 'type' => 'service', + 'id' => 'cleverage_ui_process.monolog_handler.process', + ], + 'pb_ui_orm' => [ + 'type' => 'service', + 'id' => 'cleverage_ui_process.monolog_handler.doctrine_process', + ], + ], + ] + ); + if ('dev' === $env) { + $container->loadFromExtension( + 'monolog', + [ + 'handlers' => [ + 'pb_ui_file_filter' => [ + 'type' => 'filter', + 'min_level' => Level::Debug->name, + 'handler' => 'pb_ui_file', + 'channels' => ['cleverage_process', 'cleverage_process_task'], + ], + 'pb_ui_orm_filter' => [ + 'type' => 'filter', + 'min_level' => Level::Debug->name, + 'handler' => 'pb_ui_orm', + 'channels' => ['cleverage_process', 'cleverage_process_task'], + ], + ], + ] + ); + } else { + $container->loadFromExtension( + 'monolog', + [ + 'handlers' => [ + 'pb_ui_file_filter' => [ + 'type' => 'filter', + 'min_level' => Level::Info->name, + 'handler' => 'pb_ui_file', + 'channels' => ['cleverage_process', 'cleverage_process_task'], + ], + 'pb_ui_orm_filter' => [ + 'type' => 'filter', + 'min_level' => Level::Info->name, + 'handler' => 'pb_ui_orm', + 'channels' => ['cleverage_process', 'cleverage_process_task'], + ], + ], + ] + ); + } + $container->loadFromExtension( + 'doctrine_migrations', + [ + 'migrations_paths' => ['CleverAge\UiProcessBundle\Migrations' => \dirname(__DIR__).'/Migrations'], + ] + ); + $container->loadFromExtension( + 'framework', + [ + 'messenger' => [ + 'transport' => [ + [ + 'name' => 'execute_process', + 'dsn' => 'doctrine://default', + 'retry_strategy' => ['max_retries' => 0], + ], + ], + 'routing' => [ + ProcessExecuteMessage::class => 'execute_process', + ], + ], + ] + ); + $container->loadFromExtension( + 'security', + [ + 'providers' => [ + 'process_user_provider' => [ + 'entity' => [ + 'class' => User::class, + 'property' => 'email', + ], + ], + ], + 'firewalls' => [ + 'main' => [ + 'provider' => 'process_user_provider', + 'form_login' => [ + 'login_path' => 'process_login', + 'check_path' => 'process_login', + ], + 'logout' => [ + 'path' => 'process_logout', + 'target' => 'process_login', + 'clear_site_data' => '*', + ], + ], + ], + ] + ); + } + + /** + * Recursively import config files into container. + */ + protected function findServices(ContainerBuilder $container, string $path, string $extension = 'yaml'): void + { + $finder = new Finder(); + $finder->in($path) + ->name('*.'.$extension)->files(); + $loader = new YamlFileLoader($container, new FileLocator($path)); + foreach ($finder as $file) { + $loader->load($file->getFilename()); + } + } +} diff --git a/src/DependencyInjection/Compiler/RegisterLogHandlerCompilerPass.php b/src/DependencyInjection/Compiler/RegisterLogHandlerCompilerPass.php deleted file mode 100644 index 3b28378..0000000 --- a/src/DependencyInjection/Compiler/RegisterLogHandlerCompilerPass.php +++ /dev/null @@ -1,29 +0,0 @@ -has($logger)) { - $container - ->getDefinition($logger) - ->addMethodCall('pushHandler', [new Reference(ProcessLogHandler::class)]); - } - } - } -} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 0e5c270..4dd50a6 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -2,8 +2,19 @@ declare(strict_types=1); -namespace CleverAge\ProcessUiBundle\DependencyInjection; +/* + * This file is part of the CleverAge/UiProcessBundle package. + * + * Copyright (c) Clever-Age + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace CleverAge\UiProcessBundle\DependencyInjection; + +use Monolog\Level; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -11,16 +22,32 @@ class Configuration implements ConfigurationInterface { public function getConfigTreeBuilder(): TreeBuilder { - $treeBuilder = new TreeBuilder('clever_age_process_ui'); - $treeBuilder->getRootNode() + $tb = new TreeBuilder('clever_age_ui_process'); + /** @var ArrayNodeDefinition $rootNode */ + $rootNode = $tb->getRootNode(); + $rootNode ->children() - ->arrayNode('index_logs') - ->ignoreExtraKeys() - ->addDefaultsIfNotSet() + ->arrayNode('security') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('roles')->defaultValue(['ROLE_ADMIN'])->scalarPrototype()->end(); // Roles displayed inside user edit form + $rootNode + ->children() + ->arrayNode('logs') + ->addDefaultsIfNotSet() ->children() - ->booleanNode('enabled')->defaultFalse()->end() - ?->end(); + ->booleanNode('store_in_database')->defaultValue(true)->end() // enable/disable store log in database (log_record table) + ->scalarNode('database_level')->defaultValue(Level::Debug->name)->end() // min log level to store log record in database + ->scalarNode('report_increment_level')->defaultValue(Level::Warning->name)->end() // min log level to increment process execution report + ->end(); + $rootNode + ->children() + ->arrayNode('design') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('logo_path')->defaultValue('bundles/cleverageuiprocess/logo.jpg')->end() + ->end(); - return $treeBuilder; + return $tb; } } diff --git a/src/Entity/.gitignore b/src/Entity/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/Entity/Enum/ProcessExecutionStatus.php b/src/Entity/Enum/ProcessExecutionStatus.php new file mode 100644 index 0000000..5589885 --- /dev/null +++ b/src/Entity/Enum/ProcessExecutionStatus.php @@ -0,0 +1,21 @@ + $context */ + #[ORM\Column(type: Types::JSON)] + public readonly array $context; + + #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] + public readonly \DateTimeImmutable $createdAt; + + public function getId(): ?int + { + return $this->id; + } + + public function __construct( + \Monolog\LogRecord $record, + #[ORM\ManyToOne(targetEntity: ProcessExecution::class, cascade: ['all'])] + #[ORM\JoinColumn(name: 'process_execution_id', referencedColumnName: 'id', onDelete: 'CASCADE', nullable: false)] + private readonly ProcessExecution $processExecution, + ) { + $this->channel = (string) (new UnicodeString($record->channel))->truncate(64); + $this->level = $record->level->value; + $this->message = (string) (new UnicodeString($record->message))->truncate(512); + $this->context = $record->context; + $this->createdAt = \DateTimeImmutable::createFromMutable(new \DateTime()); + } + + public function contextIsEmpty(): bool + { + return [] !== $this->context; + } +} diff --git a/src/Entity/Process.php b/src/Entity/Process.php deleted file mode 100644 index 7d0ca7b..0000000 --- a/src/Entity/Process.php +++ /dev/null @@ -1,113 +0,0 @@ - - * @ORM\OneToMany(targetEntity="CleverAge\ProcessUiBundle\Entity\ProcessExecution", mappedBy="process") - */ - private $executions; - - /** - * @ORM\Column(name="last_execution_status", type="integer", nullable=true) - */ - private ?int $lastExecutionStatus; - - public function __construct( - string $processCode, - ?string $source = null, - ?string $target = null, - ?DateTime $lastExecutionDate = null, - ?int $lastExecutionStatus = null - ) { - $this->processCode = $processCode; - $this->source = $source; - $this->target = $target; - $this->lastExecutionDate = $lastExecutionDate; - $this->lastExecutionStatus = $lastExecutionStatus; - } - - public function getId(): ?int - { - return $this->id; - } - - public function getProcessCode(): string - { - return $this->processCode; - } - - public function getSource(): ?string - { - return $this->source; - } - - public function getTarget(): ?string - { - return $this->target; - } - - public function getLastExecutionDate(): ?DateTimeInterface - { - return $this->lastExecutionDate; - } - - public function getLastExecutionStatus(): ?int - { - return $this->lastExecutionStatus; - } - - public function setLastExecutionDate(DateTimeInterface $lastExecutionDate): self - { - $this->lastExecutionDate = $lastExecutionDate; - - return $this; - } - - public function setLastExecutionStatus(int $lastExecutionStatus): self - { - $this->lastExecutionStatus = $lastExecutionStatus; - - return $this; - } -} diff --git a/src/Entity/ProcessExecution.php b/src/Entity/ProcessExecution.php index 47ff1ac..a924eda 100644 --- a/src/Entity/ProcessExecution.php +++ b/src/Entity/ProcessExecution.php @@ -2,255 +2,131 @@ declare(strict_types=1); -namespace CleverAge\ProcessUiBundle\Entity; - -use CleverAge\ProcessUiBundle\Repository\ProcessExecutionRepository; -use DateTime; -use DateTimeInterface; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; - -/** - * @ORM\Entity(repositoryClass=ProcessExecutionRepository::class) +/* + * This file is part of the CleverAge/UiProcessBundle package. + * + * Copyright (c) Clever-Age + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ -class ProcessExecution -{ - public const STATUS_START = 0; - public const STATUS_SUCCESS = 1; - public const STATUS_FAIL = -1; - - /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue() - */ - private ?int $id; - - /** - * @ORM\Column(name="process_code", type="string", length=255, nullable=true) - */ - private ?string $processCode; - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ - private ?string $source; +namespace CleverAge\UiProcessBundle\Entity; - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ - private ?string $target; - - /** - * @ORM\Column(type="datetime") - */ - private DateTimeInterface $startDate; - - /** - * @ORM\Column(type="datetime", nullable=true) - */ - private ?DateTimeInterface $endDate; +use CleverAge\UiProcessBundle\Entity\Enum\ProcessExecutionStatus; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\String\UnicodeString; - /** - * @ORM\Column(type="integer") - */ - private int $status; +#[ORM\Entity] +#[ORM\Index(name: 'idx_process_execution_code', columns: ['code'])] +#[ORM\Index(name: 'idx_process_execution_start_date', columns: ['start_date'])] +class ProcessExecution implements \Stringable +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ - private ?string $response; + #[ORM\Column(type: Types::STRING, length: 255)] + public readonly string $code; - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ - private ?string $data; + #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] + public readonly \DateTimeImmutable $startDate; - /** - * @ORM\Column(type="json", nullable=true) - */ - private ?array $report; + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] + public ?\DateTimeImmutable $endDate = null; - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ - private ?string $log; + #[ORM\Column(type: Types::STRING, enumType: ProcessExecutionStatus::class)] + public ProcessExecutionStatus $status = ProcessExecutionStatus::Started; /** - * @ORM\ManyToOne(targetEntity="CleverAge\ProcessUiBundle\Entity\Process", inversedBy="executions") - * @ORM\JoinColumn(name="process_id", referencedColumnName="id", onDelete="SET NULL") + * @var array */ - private Process $process; + #[ORM\Column(type: Types::JSON)] + private array $report = []; /** - * @ORM\OneToMany(targetEntity="ProcessExecutionLogRecord", mappedBy="processExecution", cascade={"persist"}) - * - * @var Collection + * @var array */ - private Collection $logRecords; - - public function __construct(Process $process) - { - $this->process = $process; - $this->status = self::STATUS_START; - $this->startDate = new DateTime(); - $this->logRecords = new ArrayCollection(); - } + #[ORM\Column(type: Types::JSON, nullable: true)] + private ?array $context = []; public function getId(): ?int { return $this->id; } - public function getProcessCode(): ?string - { - return $this->processCode; - } - - public function setProcessCode(?string $processCode): self - { - $this->processCode = $processCode; - - return $this; - } - - public function getSource(): ?string - { - return $this->source; - } - - public function setSource(?string $source): self - { - $this->source = $source; - - return $this; - } - - public function getTarget(): ?string - { - return $this->target; - } - - public function setTarget(?string $target): self - { - $this->target = $target; - - return $this; - } - - public function getStartDate(): DateTimeInterface - { - return $this->startDate; - } - - public function setStartDate(DateTimeInterface $startDate): self - { - $this->startDate = $startDate; - - return $this; - } - - public function getEndDate(): ?DateTimeInterface - { - return $this->endDate; - } - - public function setEndDate(DateTimeInterface $endDate): self - { - $this->endDate = $endDate; - - return $this; + /** + * @param array $context + */ + public function __construct( + string $code, + #[ORM\Column(type: Types::STRING, length: 255)] public readonly string $logFilename, + ?array $context = [], + ) { + $this->code = (string) (new UnicodeString($code))->truncate(255); + $this->startDate = \DateTimeImmutable::createFromMutable(new \DateTime()); + $this->context = $context ?? []; } - public function getStatus(): ?int + public function __toString(): string { - return $this->status; + return \sprintf('%s (%s)', $this->id, $this->code); } - public function setStatus(int $status): self + public function setStatus(ProcessExecutionStatus $status): void { $this->status = $status; - - return $this; - } - - public function getResponse(): ?string - { - return $this->response; } - public function setResponse(?string $response): self + public function end(): void { - $this->response = $response; - - return $this; + $this->endDate = \DateTimeImmutable::createFromMutable(new \DateTime()); } - public function getData(): ?string + public function addReport(string $key, mixed $value): void { - return $this->data; + $this->report[$key] = $value; } - public function setData(?string $data): self + public function getReport(?string $key = null, mixed $default = null): mixed { - $this->data = $data; - - return $this; - } + if (null === $key) { + return $this->report; + } - public function getLog(): ?string - { - return $this->log; + return $this->report[$key] ?? $default; } - public function setLog(string $log): self + public function duration(string $format = '%H hour(s) %I min(s) %S s'): ?string { - $this->log = $log; + if (!$this->endDate instanceof \DateTimeImmutable) { + return null; + } + $diff = $this->endDate->diff($this->startDate); - return $this; + return $diff->format($format); } - public function addLogRecord(ProcessExecutionLogRecord $processExecutionLogRecord): void + public function getCode(): string { - $processExecutionLogRecord->setProcessExecution($this); - $this->logRecords->add($processExecutionLogRecord); + return $this->code; } /** - * @return Collection + * @return array */ - public function getLogRecords(): Collection + public function getContext(): ?array { - return $this->logRecords; + return $this->context; } /** - * @param Collection $logRecords + * @param array $context */ - public function setLogRecords(Collection $logRecords): self + public function setContext(array $context): void { - foreach ($logRecords as $logRecord) { - $this->addLogRecord($logRecord); - } - - return $this; - } - - public function getProcess(): Process - { - return $this->process; - } - - public function getReport(): array - { - return $this->report ?? []; - } - - public function setReport(?array $report): self - { - $this->report = $report; - - return $this; + $this->context = $context; } } diff --git a/src/Entity/ProcessExecutionLogRecord.php b/src/Entity/ProcessExecutionLogRecord.php deleted file mode 100644 index c70807b..0000000 --- a/src/Entity/ProcessExecutionLogRecord.php +++ /dev/null @@ -1,88 +0,0 @@ -logLevel = $logLevel; - $this->message = $message; - } - - public function getId(): ?int - { - return $this->id; - } - - public function getLogLevel(): int - { - return $this->logLevel; - } - - public function setLogLevel(int $logLevel): self - { - $this->logLevel = $logLevel; - - return $this; - } - - public function getMessage(): string - { - return $this->message; - } - - public function setMessage(string $message): self - { - $this->message = $message; - - return $this; - } - - public function setProcessExecution(ProcessExecution $processExecution): self - { - $this->processExecution = $processExecution; - - return $this; - } - - public function getProcessExecution(): ?ProcessExecution - { - return $this->processExecution; - } -} diff --git a/src/Entity/ProcessSchedule.php b/src/Entity/ProcessSchedule.php new file mode 100644 index 0000000..8eea928 --- /dev/null +++ b/src/Entity/ProcessSchedule.php @@ -0,0 +1,130 @@ + + */ + #[ORM\Column(type: Types::JSON)] + private string|array $context = []; + + public function getId(): ?int + { + return $this->id; + } + + public function getProcess(): ?string + { + return $this->process; + } + + public function setProcess(string $process): static + { + $this->process = $process; + + return $this; + } + + /** + * @return array + */ + public function getContext(): array + { + return \is_array($this->context) ? $this->context : json_decode($this->context); + } + + /** + * @param array $context + */ + public function setContext(array $context): void + { + $this->context = $context; + } + + public function getNextExecution(): null + { + return null; + } + + public function getType(): ProcessScheduleType + { + return $this->type; + } + + public function setType(ProcessScheduleType $type): self + { + $this->type = $type; + + return $this; + } + + public function getExpression(): ?string + { + return $this->expression; + } + + public function setExpression(string $expression): self + { + $this->expression = $expression; + + return $this; + } + + public function getInput(): ?string + { + return $this->input; + } + + public function setInput(?string $input): self + { + $this->input = $input; + + return $this; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index 20dbed5..11532f7 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -2,52 +2,58 @@ declare(strict_types=1); -namespace CleverAge\ProcessUiBundle\Entity; +/* + * This file is part of the CleverAge/UiProcessBundle package. + * + * Copyright (c) Clever-Age + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CleverAge\UiProcessBundle\Entity; -use CleverAge\ProcessUiBundle\Repository\UserRepository; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; -/** - * @ORM\Entity(repositoryClass=UserRepository::class) - * @ORM\Table(name="user") - */ +#[ORM\Entity] +#[ORM\Table(name: 'process_user')] +#[ORM\Index(name: 'idx_process_user_email', columns: ['email'])] class User implements UserInterface, PasswordAuthenticatedUserInterface { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ - private ?int $id; + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; - /** - * @ORM\Column(type="string", length=255, unique=true) - */ - private ?string $email; + #[ORM\Column(type: Types::STRING, length: 255, unique: true)] + private string $email; - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ - private ?string $firstname; + #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] + private ?string $firstname = null; - /** - * @ORM\Column(type="string", length=255, nullable=true) - */ - private ?string $lastname; + #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] + private ?string $lastname = null; /** - * @ORM\Column(type="json") - * - * @var array + * @var string[] */ + #[ORM\Column(type: Types::JSON)] private array $roles = []; - /** - * @ORM\Column(type="string") - */ - private ?string $password; + #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] + private ?string $password = null; + + #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] + private ?string $timezone = null; + + #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] + private ?string $locale = null; + + #[ORM\Column(type: Types::STRING, length: 255, nullable: true)] + private ?string $token = null; public function getId(): ?int { @@ -90,14 +96,13 @@ public function setLastname(?string $lastname): self return $this; } - /** - * A visual identifier that represents this user. - * - * @see UserInterface - */ public function getUserIdentifier(): string { - return (string) $this->email; + if ('' === $this->email) { + throw new \LogicException('The User class must have an email.'); + } + + return $this->email; } public function getUsername(): string @@ -105,20 +110,37 @@ public function getUsername(): string return $this->getUserIdentifier(); } - /** - * @see UserInterface - */ - public function getRoles(): array + public function getTimezone(): ?string + { + return $this->timezone; + } + + public function setTimezone(?string $timezone): self { - $roles = $this->roles; - // guarantee every user at least has ROLE_USER - $roles[] = 'ROLE_USER'; + $this->timezone = $timezone; - return array_unique($roles); + return $this; + } + + public function getLocale(): ?string + { + return $this->locale; + } + + public function setLocale(?string $locale): self + { + $this->locale = $locale; + + return $this; + } + + public function getRoles(): array + { + return array_merge(['ROLE_USER'], $this->roles); } /** - * @param array $roles + * @param array $roles */ public function setRoles(array $roles): self { @@ -127,9 +149,6 @@ public function setRoles(array $roles): self return $this; } - /** - * @see PasswordAuthenticatedUserInterface - */ public function getPassword(): ?string { return $this->password; @@ -142,20 +161,18 @@ public function setPassword(string $password): self return $this; } - /** - * Returning a salt is only needed, if you are not using a modern - * hashing algorithm (e.g. bcrypt or sodium) in your security.yaml. - * - * @see UserInterface - */ - public function getSalt(): ?string + public function getToken(): ?string { - return null; + return $this->token; + } + + public function setToken(?string $token): self + { + $this->token = $token; + + return $this; } - /** - * @see UserInterface - */ public function eraseCredentials(): void { // If you store any temporary, sensitive data on the user, clear it here diff --git a/src/Event/IncrementReportInfoEvent.php b/src/Event/IncrementReportInfoEvent.php deleted file mode 100644 index 4d10e7e..0000000 --- a/src/Event/IncrementReportInfoEvent.php +++ /dev/null @@ -1,28 +0,0 @@ -key = $key; - $this->processCode = $processCode; - } - - public function getKey(): string - { - return $this->key; - } - - public function getProcessCode(): string - { - return $this->processCode; - } -} diff --git a/src/Event/SetReportInfoEvent.php b/src/Event/SetReportInfoEvent.php deleted file mode 100644 index 20d14af..0000000 --- a/src/Event/SetReportInfoEvent.php +++ /dev/null @@ -1,35 +0,0 @@ -key = $key; - $this->value = $value; - $this->processCode = $processCode; - } - - public function getKey(): string - { - return $this->key; - } - - public function getValue(): mixed - { - return $this->value; - } - - public function getProcessCode(): string - { - return $this->processCode; - } -} diff --git a/src/EventSubscriber/Crud/ProcessCrudListener.php b/src/EventSubscriber/Crud/ProcessCrudListener.php deleted file mode 100644 index f8a7543..0000000 --- a/src/EventSubscriber/Crud/ProcessCrudListener.php +++ /dev/null @@ -1,38 +0,0 @@ -entityManager = $entityManager; - } - - /** - * @return array - */ - public static function getSubscribedEvents(): array - { - return [BeforeCrudActionEvent::class => 'syncProcessIntoDatabase']; - } - - public function syncProcessIntoDatabase(BeforeCrudActionEvent $event): void - { - if (Process::class === $event->getAdminContext()?->getEntity()->getFqcn()) { - /** @var ProcessRepository $repository */ - $repository = $this->entityManager->getRepository(Process::class); - $repository->sync(); - } - } -} diff --git a/src/EventSubscriber/ProcessEventSubscriber.php b/src/EventSubscriber/ProcessEventSubscriber.php index e9ed92f..ed90faa 100644 --- a/src/EventSubscriber/ProcessEventSubscriber.php +++ b/src/EventSubscriber/ProcessEventSubscriber.php @@ -2,164 +2,87 @@ declare(strict_types=1); -namespace CleverAge\ProcessUiBundle\EventSubscriber; +/* + * This file is part of the CleverAge/UiProcessBundle package. + * + * Copyright (c) Clever-Age + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CleverAge\UiProcessBundle\EventSubscriber; use CleverAge\ProcessBundle\Event\ProcessEvent; -use CleverAge\ProcessUiBundle\Entity\Process; -use CleverAge\ProcessUiBundle\Entity\ProcessExecution; -use CleverAge\ProcessUiBundle\Event\IncrementReportInfoEvent; -use CleverAge\ProcessUiBundle\Event\SetReportInfoEvent; -use CleverAge\ProcessUiBundle\Manager\ProcessUiConfigurationManager; -use CleverAge\ProcessUiBundle\Message\LogIndexerMessage; -use CleverAge\ProcessUiBundle\Monolog\Handler\ProcessLogHandler; -use CleverAge\ProcessUiBundle\Repository\ProcessRepository; -use DateTime; -use Doctrine\ORM\EntityManagerInterface; -use RuntimeException; -use SplFileObject; +use CleverAge\UiProcessBundle\Entity\Enum\ProcessExecutionStatus; +use CleverAge\UiProcessBundle\Entity\ProcessExecution; +use CleverAge\UiProcessBundle\Manager\ProcessExecutionManager; +use CleverAge\UiProcessBundle\Monolog\Handler\DoctrineProcessHandler; +use CleverAge\UiProcessBundle\Monolog\Handler\ProcessHandler; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Uid\Uuid; -class ProcessEventSubscriber implements EventSubscriberInterface +final readonly class ProcessEventSubscriber implements EventSubscriberInterface { - private array $processExecution = []; - private EntityManagerInterface $entityManager; - private ProcessLogHandler $processLogHandler; - private MessageBusInterface $messageBus; - private ProcessUiConfigurationManager $processUiConfigurationManager; - private string $processLogDir; - private bool $indexLogs; - public function __construct( - EntityManagerInterface $entityManager, - ProcessLogHandler $processLogHandler, - MessageBusInterface $messageBus, - ProcessUiConfigurationManager $processUiConfigurationManager, - string $processLogDir, - bool $indexLogs + private ProcessHandler $processHandler, + private DoctrineProcessHandler $doctrineProcessHandler, + private ProcessExecutionManager $processExecutionManager, ) { - $this->entityManager = $entityManager; - $this->processLogHandler = $processLogHandler; - $this->messageBus = $messageBus; - $this->processUiConfigurationManager = $processUiConfigurationManager; - $this->processLogDir = $processLogDir; - $this->indexLogs = $indexLogs; - } - - public static function getSubscribedEvents(): array - { - return [ - ProcessEvent::EVENT_PROCESS_STARTED => [ - ['syncProcessIntoDatabase', 1000], - ['onProcessStarted', 0], - ], - ProcessEvent::EVENT_PROCESS_ENDED => [ - ['onProcessEnded'], - ], - ProcessEvent::EVENT_PROCESS_FAILED => [ - ['onProcessFailed'], - ], - IncrementReportInfoEvent::NAME => [ - ['updateProcessExecutionReport'], - ], - SetReportInfoEvent::NAME => [ - ['updateProcessExecutionReport'], - ], - ]; } - public function onProcessStarted(ProcessEvent $event): void + public function onProcessStart(ProcessEvent $event): void { - $process = $this->entityManager->getRepository(Process::class) - ->findOneBy(['processCode' => $event->getProcessCode()]); - if (null === $process) { - throw new RuntimeException('Unable to found process into database.'); + if (false === $this->processHandler->hasFilename()) { + $this->processHandler->setFilename(\sprintf('%s/%s.log', $event->getProcessCode(), Uuid::v4())); } - $processExecution = new ProcessExecution($process); - $processExecution->setProcessCode($event->getProcessCode()); - $processExecution->setSource($this->processUiConfigurationManager->getSource($event->getProcessCode())); - $processExecution->setTarget($this->processUiConfigurationManager->getTarget($event->getProcessCode())); - $logFilename = sprintf( - 'process_%s_%s.log', - $event->getProcessCode(), - sha1(uniqid((string) mt_rand(), true)) - ); - $this->processLogHandler->setLogFilename($logFilename, $event->getProcessCode()); - $this->processLogHandler->setCurrentProcessCode($event->getProcessCode()); - $processExecution->setLog($logFilename); - $this->entityManager->persist($processExecution); - $this->entityManager->flush(); - $this->processExecution[$event->getProcessCode()] = $processExecution; - } - - public function onProcessEnded(ProcessEvent $processEvent): void - { - if ($processExecution = ($this->processExecution[$processEvent->getProcessCode()] ?? null)) { - $this->processExecution = array_filter($this->processExecution); - array_pop($this->processExecution); - $this->processLogHandler->setCurrentProcessCode((string) array_key_last($this->processExecution)); - $processExecution->setEndDate(new DateTime()); - $processExecution->setStatus(ProcessExecution::STATUS_SUCCESS); - $processExecution->getProcess()->setLastExecutionDate($processExecution->getStartDate()); - $processExecution->getProcess()->setLastExecutionStatus( - ProcessExecution::STATUS_SUCCESS + if (!$this->processExecutionManager->getCurrentProcessExecution() instanceof ProcessExecution) { + $processExecution = new ProcessExecution( + $event->getProcessCode(), + basename((string) $this->processHandler->getFilename()), + $event->getProcessContext() ); - $this->entityManager->persist($processExecution); - $this->entityManager->flush(); - $this->dispatchLogIndexerMessage($processExecution); - $this->processExecution[$processEvent->getProcessCode()] = null; + $this->processExecutionManager->setCurrentProcessExecution($processExecution)->save(); } } - public function onProcessFailed(ProcessEvent $processEvent): void + public function success(ProcessEvent $event): void { - if ($processExecution = ($this->processExecution[$processEvent->getProcessCode()] ?? null)) { - $processExecution->setEndDate(new DateTime()); - $processExecution->setStatus(ProcessExecution::STATUS_FAIL); - $processExecution->getProcess()->setLastExecutionDate($processExecution->getStartDate()); - $processExecution->getProcess()->setLastExecutionStatus(ProcessExecution::STATUS_FAIL); - $this->entityManager->persist($processExecution); - $this->entityManager->flush(); - $this->dispatchLogIndexerMessage($processExecution); - $this->processExecution[$processEvent->getProcessCode()] = null; + if ($event->getProcessCode() === $this->processExecutionManager->getCurrentProcessExecution()?->getCode()) { + $this->processExecutionManager->getCurrentProcessExecution()->setStatus(ProcessExecutionStatus::Finish); + $this->processExecutionManager->getCurrentProcessExecution()->end(); + $this->processExecutionManager->save()->unsetProcessExecution($event->getProcessCode()); + $this->processHandler->close(); } } - public function syncProcessIntoDatabase(): void + public function fail(ProcessEvent $event): void { - /** @var ProcessRepository $repository */ - $repository = $this->entityManager->getRepository(Process::class); - $repository->sync(); + if ($event->getProcessCode() === $this->processExecutionManager->getCurrentProcessExecution()?->getCode()) { + $this->processExecutionManager->getCurrentProcessExecution()->setStatus(ProcessExecutionStatus::Failed); + $this->processExecutionManager->getCurrentProcessExecution()->end(); + $this->processExecutionManager->save()->unsetProcessExecution($event->getProcessCode()); + $this->processHandler->close(); + } } - protected function dispatchLogIndexerMessage(ProcessExecution $processExecution): void + public function flushDoctrineLogs(ProcessEvent $event): void { - if ($this->indexLogs && null !== $processExecutionId = $processExecution->getId()) { - $filePath = $this->processLogDir.\DIRECTORY_SEPARATOR.$processExecution->getLog(); - $file = new SplFileObject($filePath); - $file->seek(\PHP_INT_MAX); - $chunkSize = LogIndexerMessage::DEFAULT_OFFSET; - $chunk = (int) ($file->key() / $chunkSize) + 1; - for ($i = 0; $i < $chunk; ++$i) { - $this->messageBus->dispatch( - new LogIndexerMessage( - $processExecutionId, - $this->processLogDir.\DIRECTORY_SEPARATOR.$processExecution->getLog(), - $i * $chunkSize - ) - ); - } - } + $this->doctrineProcessHandler->flush(); } - public function updateProcessExecutionReport(IncrementReportInfoEvent|SetReportInfoEvent $event): void + public static function getSubscribedEvents(): array { - if ($processExecution = ($this->processExecution[$event->getProcessCode()] ?? false)) { - $report = $processExecution->getReport(); - $event instanceof IncrementReportInfoEvent - ? $report[$event->getKey()] = ($report[$event->getKey()] ?? 0) + 1 - : $report[$event->getKey()] = $event->getValue(); - $processExecution->setReport($report); - } + return [ + ProcessEvent::EVENT_PROCESS_STARTED => 'onProcessStart', + ProcessEvent::EVENT_PROCESS_ENDED => [ + ['flushDoctrineLogs', 100], + ['success', 100], + ], + ProcessEvent::EVENT_PROCESS_FAILED => [ + ['flushDoctrineLogs', 100], + ['fail', 100], + ], + ]; } } diff --git a/src/Form/Type/LaunchType.php b/src/Form/Type/LaunchType.php new file mode 100644 index 0000000..3c876bc --- /dev/null +++ b/src/Form/Type/LaunchType.php @@ -0,0 +1,76 @@ +> */ +class LaunchType extends AbstractType +{ + public function __construct( + private readonly ProcessConfigurationRegistry $registry, + private readonly ProcessConfigurationsManager $configurationsManager, + ) { + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $code = $options['process_code']; + $configuration = $this->registry->getProcessConfiguration($code); + $uiOptions = $this->configurationsManager->getUiOptions($code); + if (isset($uiOptions['entrypoint_type'])) { + $builder->add( + 'input', + 'file' === $uiOptions['entrypoint_type'] ? FileType::class : TextType::class, + [ + 'required' => $configuration->getEntryPoint() instanceof TaskConfiguration, + ] + ); + } + $builder->add( + 'context', + CollectionType::class, + [ + 'entry_type' => ProcessContextType::class, + 'allow_add' => true, + 'allow_delete' => true, + 'required' => false, + ] + ); + $builder->get('context')->addModelTransformer(new CallbackTransformer( + fn ($data) => $data ?? [], + fn ($data) => array_column($data ?? [], 'value', 'key'), + )); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('process_code'); + } + + public function getParent(): string + { + return FormType::class; + } +} diff --git a/src/Form/Type/ProcessContextType.php b/src/Form/Type/ProcessContextType.php new file mode 100644 index 0000000..d95fd19 --- /dev/null +++ b/src/Form/Type/ProcessContextType.php @@ -0,0 +1,48 @@ + */ +class ProcessContextType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add( + 'key', + null, + [ + 'label' => 'Context Key', + 'attr' => ['placeholder' => 'key'], + 'constraints' => [new NotBlank()], + ] + )->add( + 'value', + null, + [ + 'label' => 'Context Value', + 'attr' => ['placeholder' => 'value'], + 'constraints' => [new NotBlank()], + ] + ); + } + + public function configureOptions(OptionsResolver $resolver): void + { + } +} diff --git a/src/Form/Type/ProcessUploadFileType.php b/src/Form/Type/ProcessUploadFileType.php new file mode 100644 index 0000000..57619ff --- /dev/null +++ b/src/Form/Type/ProcessUploadFileType.php @@ -0,0 +1,32 @@ + */ +class ProcessUploadFileType extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('process_code'); + } + + public function getParent(): string + { + return FileType::class; + } +} diff --git a/src/Http/Model/HttpProcessExecution.php b/src/Http/Model/HttpProcessExecution.php new file mode 100644 index 0000000..fc4272f --- /dev/null +++ b/src/Http/Model/HttpProcessExecution.php @@ -0,0 +1,32 @@ + $context + */ + public function __construct( + #[Sequentially(constraints: [new NotNull(message: 'Process code is required.'), new IsValidProcessCode()])] + public ?string $code = null, + public ?string $input = null, + public array $context = [], + ) { + } +} diff --git a/src/Http/ValueResolver/HttpProcessExecuteValueResolver.php b/src/Http/ValueResolver/HttpProcessExecuteValueResolver.php new file mode 100644 index 0000000..5e2aafe --- /dev/null +++ b/src/Http/ValueResolver/HttpProcessExecuteValueResolver.php @@ -0,0 +1,45 @@ + + */ + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + $input = $request->get('input', $request->files->get('input')); + if ($input instanceof UploadedFile) { + $uploadFileName = $this->storageDir.\DIRECTORY_SEPARATOR.date('YmdHis').'_'.uniqid().'_'.$input->getClientOriginalName(); + (new Filesystem())->dumpFile($uploadFileName, $input->getContent()); + $input = $uploadFileName; + } + + return [new HttpProcessExecution($request->get('code'), $input, $request->get('context', []))]; + } +} diff --git a/src/Http/ValueResolver/ProcessConfigurationValueResolver.php b/src/Http/ValueResolver/ProcessConfigurationValueResolver.php new file mode 100644 index 0000000..bf88b84 --- /dev/null +++ b/src/Http/ValueResolver/ProcessConfigurationValueResolver.php @@ -0,0 +1,37 @@ + + */ + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + return [$this->registry->getProcessConfiguration($request->get('process'))]; + } +} diff --git a/src/Manager/ProcessConfigurationsManager.php b/src/Manager/ProcessConfigurationsManager.php new file mode 100644 index 0000000..ff8b8de --- /dev/null +++ b/src/Manager/ProcessConfigurationsManager.php @@ -0,0 +1,116 @@ +getConfigurations(), fn (ProcessConfiguration $cfg) => $cfg->isPublic()); + } + + /** @return ProcessConfiguration[] */ + public function getPrivateProcesses(): array + { + return array_filter($this->getConfigurations(), fn (ProcessConfiguration $cfg) => !$cfg->isPublic()); + } + + /** + * @return UiOptions|null + */ + public function getUiOptions(string $processCode): ?array + { + if (false === $this->registry->hasProcessConfiguration($processCode)) { + return null; + } + + $configuration = $this->registry->getProcessConfiguration($processCode); + + return $this->resolveUiOptions($configuration->getOptions())['ui']; + } + + /** + * @param array $options + * + * @return array{'ui': UiOptions} + */ + private function resolveUiOptions(array $options): array + { + $resolver = new OptionsResolver(); + $resolver->setDefault('ui', function (OptionsResolver $uiResolver): void { + $uiResolver->setDefaults( + [ + 'source' => null, + 'target' => null, + 'entrypoint_type' => 'text', + 'ui_launch_mode' => 'modal', + 'constraints' => [], + 'run' => null, + 'default' => function (OptionsResolver $defaultResolver) { + $defaultResolver->setDefault('input', null); + $defaultResolver->setDefault('context', function (OptionsResolver $contextResolver) { + $contextResolver->setPrototype(true); + $contextResolver->setRequired(['key', 'value']); + }); + }, + ] + ); + $uiResolver->setDeprecated( + 'run', + 'cleverage/ui-process-bundle', + '2', + 'run ui option is deprecated. Use public option instead to hide a process from UI' + ); + $uiResolver->setAllowedValues('entrypoint_type', ['text', 'file']); + $uiResolver->setNormalizer('constraints', fn (Options $options, array $values): array => (new ConstraintLoader())->buildConstraints($values)); + $uiResolver->setAllowedValues('ui_launch_mode', ['modal', null, 'form']); + }); + /** + * @var array{'ui': UiOptions} $options + */ + $options = $resolver->resolve($options); + + return $options; + } + + /** @return ProcessConfiguration[] */ + private function getConfigurations(): array + { + return $this->registry->getProcessConfigurations(); + } +} diff --git a/src/Manager/ProcessExecutionManager.php b/src/Manager/ProcessExecutionManager.php new file mode 100644 index 0000000..84a6041 --- /dev/null +++ b/src/Manager/ProcessExecutionManager.php @@ -0,0 +1,71 @@ +currentProcessExecution instanceof ProcessExecution) { + $this->currentProcessExecution = $processExecution; + } + + return $this; + } + + public function getCurrentProcessExecution(): ?ProcessExecution + { + return $this->currentProcessExecution; + } + + public function unsetProcessExecution(string $processCode): self + { + if ($this->currentProcessExecution?->code === $processCode) { + $this->currentProcessExecution = null; + } + + return $this; + } + + public function save(): self + { + if ($this->currentProcessExecution instanceof ProcessExecution) { + $this->processExecutionRepository->save($this->currentProcessExecution); + } + + return $this; + } + + public function increment(string $incrementKey, int $step = 1): void + { + $this->currentProcessExecution?->addReport( + $incrementKey, + $this->currentProcessExecution->getReport($incrementKey, 0) + $step + ); + } + + public function setReport(string $incrementKey, string $value): void + { + $this->currentProcessExecution?->addReport($incrementKey, $value); + } +} diff --git a/src/Manager/ProcessUiConfigurationManager.php b/src/Manager/ProcessUiConfigurationManager.php deleted file mode 100644 index d852b1e..0000000 --- a/src/Manager/ProcessUiConfigurationManager.php +++ /dev/null @@ -1,97 +0,0 @@ -processConfigurationRegistry = $processConfigurationRegistry; - } - - /** - * @return array - */ - public function getProcessChoices(): array - { - return array_map(static fn (ProcessConfiguration $configuration) => $configuration->getCode(), $this->processConfigurationRegistry->getProcessConfigurations()); - } - - /** - * @return array - */ - public function getSourceChoices(): array - { - $sources = []; - foreach ($this->processConfigurationRegistry->getProcessConfigurations() as $configuration) { - $source = $this->getSource($configuration->getCode()); - $sources[(string) $source] = (string) $source; - } - - return $sources; - } - - /** - * @return array - */ - public function getTargetChoices(): array - { - $targets = []; - foreach ($this->processConfigurationRegistry->getProcessConfigurations() as $configuration) { - $target = $this->getTarget($configuration->getCode()); - $targets[(string) $target] = (string) $target; - } - - return $targets; - } - - public function getSource(Process|string $process): ?string - { - return $this->resolveUiOptions($process)[self::UI_OPTION_SOURCE]; - } - - public function getTarget(Process|string $process): ?string - { - return $this->resolveUiOptions($process)[self::UI_OPTION_TARGET]; - } - - public function canRun(Process|string $process): bool - { - return (bool) $this->resolveUiOptions($process)[self::UI_OPTION_RUN]; - } - - /** - * @return array - */ - private function resolveUiOptions(Process|string $process): array - { - $code = $process instanceof Process ? $process->getProcessCode() : $process; - $resolver = new OptionsResolver(); - - $resolver->setDefaults([ - self::UI_OPTION_SOURCE => null, - self::UI_OPTION_TARGET => null, - self::UI_OPTION_RUN => true, - ]); - $resolver->setAllowedTypes(self::UI_OPTION_RUN, 'bool'); - $resolver->setAllowedTypes(self::UI_OPTION_SOURCE, ['string', 'null']); - $resolver->setAllowedTypes(self::UI_OPTION_TARGET, ['string', 'null']); - - return $resolver->resolve( - $this->processConfigurationRegistry->getProcessConfiguration($code)->getOptions()['ui_options'] ?? [] - ); - } -} diff --git a/src/Message/CronProcessMessage.php b/src/Message/CronProcessMessage.php new file mode 100644 index 0000000..8e94c09 --- /dev/null +++ b/src/Message/CronProcessMessage.php @@ -0,0 +1,23 @@ +processSchedule; + $context = array_merge(...array_map(fn ($ctx) => [$ctx['key'] => $ctx['value']], $schedule->getContext())); + $this->bus->dispatch( + new ProcessExecuteMessage($schedule->getProcess() ?? '', $schedule->getInput(), $context) + ); + } +} diff --git a/src/Message/LogIndexerHandler.php b/src/Message/LogIndexerHandler.php deleted file mode 100644 index deced04..0000000 --- a/src/Message/LogIndexerHandler.php +++ /dev/null @@ -1,62 +0,0 @@ -managerRegistry = $managerRegistry; - } - - public function __invoke(LogIndexerMessage $logIndexerMessage): void - { - /** @var EntityManagerInterface $manager */ - $manager = $this->managerRegistry->getManagerForClass(ProcessExecutionLogRecord::class); - $table = $manager->getClassMetadata(ProcessExecutionLogRecord::class)->getTableName(); - $file = new \SplFileObject($logIndexerMessage->getLogPath()); - $file->seek($logIndexerMessage->getStart()); - $offset = $logIndexerMessage->getOffset(); - $parser = new LineLogParser(); - $parameters = []; - while ($offset > 0 && !$file->eof()) { - /** @var string $currentLine */ - $currentLine = $file->current(); - $parsedLine = $parser->parse($currentLine); - if (!empty($parsedLine) && true === ($parsedLine['context'][self::INDEX_LOG_RECORD] ?? false)) { - $parameters[] = $logIndexerMessage->getProcessExecutionId(); - $parameters[] = Logger::toMonologLevel($parsedLine['level']); - $parameters[] = substr($parsedLine['message'], 0, 255); - } - $file->next(); - --$offset; - } - if (\count($parameters) > 0) { - $statement = $this->getStatement($table, (int) (\count($parameters) / 3)); - $manager->getConnection()->executeStatement($statement, $parameters); - } - } - - private function getStatement(string $table, int $size): string - { - $sql = 'INSERT INTO '.$table.' (process_execution_id, log_level, message) VALUES '; - while ($size > 0) { - $sql .= $size > 1 ? '(?, ?, ?),' : '(?, ?, ?)'; - --$size; - } - - return $sql; - } -} diff --git a/src/Message/LogIndexerMessage.php b/src/Message/LogIndexerMessage.php deleted file mode 100644 index 760b63d..0000000 --- a/src/Message/LogIndexerMessage.php +++ /dev/null @@ -1,46 +0,0 @@ -processExecutionId = $processExecutionId; - $this->logPath = $logPath; - $this->start = $start; - $this->offset = $offset; - } - - public function getProcessExecutionId(): int - { - return $this->processExecutionId; - } - - public function getLogPath(): string - { - return $this->logPath; - } - - public function getStart(): int - { - return $this->start; - } - - public function getOffset(): int - { - return $this->offset; - } -} diff --git a/src/Message/ProcessExecuteHandler.php b/src/Message/ProcessExecuteHandler.php new file mode 100644 index 0000000..dec4a05 --- /dev/null +++ b/src/Message/ProcessExecuteHandler.php @@ -0,0 +1,32 @@ +processHandler->close(); + $this->manager->execute($message->code, $message->input, $message->context); + } +} diff --git a/src/Message/ProcessExecuteMessage.php b/src/Message/ProcessExecuteMessage.php new file mode 100644 index 0000000..84fddb0 --- /dev/null +++ b/src/Message/ProcessExecuteMessage.php @@ -0,0 +1,24 @@ +command = $command; - } - - /** - * @throws Exception - */ - public function __invoke(ProcessRunMessage $processRunMessage): void - { - $this->command->run( - new ArrayInput( - [ - 'processCodes' => [$processRunMessage->getProcessCode()], - ] - ), - new NullOutput() - ); - } -} diff --git a/src/Message/ProcessRunMessage.php b/src/Message/ProcessRunMessage.php deleted file mode 100644 index 5b9682a..0000000 --- a/src/Message/ProcessRunMessage.php +++ /dev/null @@ -1,35 +0,0 @@ - */ - private array $processInput; - - /** - * @param array $processInput - */ - public function __construct(string $processCode, array $processInput = []) - { - $this->processCode = $processCode; - $this->processInput = $processInput; - } - - public function getProcessCode(): string - { - return $this->processCode; - } - - /** - * @return array - */ - public function getProcessInput(): array - { - return $this->processInput; - } -} diff --git a/src/Migrations/Version20210903142035.php b/src/Migrations/Version20210903142035.php deleted file mode 100644 index 88e869c..0000000 --- a/src/Migrations/Version20210903142035.php +++ /dev/null @@ -1,107 +0,0 @@ -addSql(<<addSql(<<addSql(<<addSql(<<addSql(<<addSql('ALTER TABLE process_execution ADD process_id INT DEFAULT NULL'); - $this->addSql(<<addSql('CREATE INDEX IDX_98E995D27EC2F574 ON process_execution (process_id)'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('DROP TABLE process'); - $this->addSql('DROP TABLE process_execution'); - $this->addSql('DROP TABLE process_execution_log_record'); - $this->addSql('DROP TABLE user'); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/src/Migrations/Version20211028081845.php b/src/Migrations/Version20211028081845.php deleted file mode 100644 index 0b2ff9e..0000000 --- a/src/Migrations/Version20211028081845.php +++ /dev/null @@ -1,36 +0,0 @@ -addSql('ALTER TABLE process_execution ADD report JSON DEFAULT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE process_execution DROP report'); - } - - public function isTransactional(): bool - { - return false; - } -} diff --git a/src/Migrations/Version20231006111525.php b/src/Migrations/Version20231006111525.php new file mode 100644 index 0000000..5d8619f --- /dev/null +++ b/src/Migrations/Version20231006111525.php @@ -0,0 +1,80 @@ +connection->getDatabasePlatform(); + + if ($platform instanceof MariaDBPlatform || $platform instanceof MySQLPlatform) { + if (!$schema->hasTable('log_record')) { + $this->addSql('CREATE TABLE log_record (id INT AUTO_INCREMENT NOT NULL, process_execution_id INT DEFAULT NULL, channel VARCHAR(64) NOT NULL, level INT NOT NULL, message VARCHAR(512) NOT NULL, context JSON NOT NULL COMMENT \'(DC2Type:json)\', created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_8ECECC333DAC0075 (process_execution_id), INDEX idx_log_record_level (level), INDEX idx_log_record_created_at (created_at), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + if (!$schema->hasTable('process_execution')) { + $this->addSql('CREATE TABLE process_execution (id INT AUTO_INCREMENT NOT NULL, code VARCHAR(255) NOT NULL, log_filename VARCHAR(255) NOT NULL, start_date DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', end_date DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', status VARCHAR(255) NOT NULL, report JSON NOT NULL COMMENT \'(DC2Type:json)\', INDEX idx_process_execution_code (code), INDEX idx_process_execution_start_date (start_date), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE log_record ADD CONSTRAINT FK_8ECECC333DAC0075 FOREIGN KEY (process_execution_id) REFERENCES process_execution (id) ON DELETE CASCADE'); + } + if (!$schema->hasTable('process_user')) { + $this->addSql('CREATE TABLE process_user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(255) NOT NULL, firstname VARCHAR(255) DEFAULT NULL, lastname VARCHAR(255) DEFAULT NULL, roles JSON NOT NULL COMMENT \'(DC2Type:json)\', password VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_627A047CE7927C74 (email), INDEX idx_process_user_email (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + } + + if ($platform instanceof PostgreSQLPlatform) { + if (!$schema->hasTable('log_record')) { + $this->addSql('CREATE SEQUENCE log_record_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE log_record (id INT NOT NULL, process_execution_id INT DEFAULT NULL, channel VARCHAR(64) NOT NULL, level INT NOT NULL, message VARCHAR(512) NOT NULL, context JSON NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_8ECECC333DAC0075 ON log_record (process_execution_id)'); + $this->addSql('CREATE INDEX idx_log_record_level ON log_record (level)'); + $this->addSql('CREATE INDEX idx_log_record_created_at ON log_record (created_at)'); + $this->addSql('COMMENT ON COLUMN log_record.created_at IS \'(DC2Type:datetime_immutable)\''); + } + if (!$schema->hasTable('process_execution')) { + $this->addSql('CREATE SEQUENCE process_execution_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE process_execution (id INT NOT NULL, code VARCHAR(255) NOT NULL, log_filename VARCHAR(255) NOT NULL, start_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, end_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, status VARCHAR(255) NOT NULL, report JSON NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX idx_process_execution_code ON process_execution (code)'); + $this->addSql('CREATE INDEX idx_process_execution_start_date ON process_execution (start_date)'); + $this->addSql('COMMENT ON COLUMN process_execution.start_date IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN process_execution.end_date IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE log_record ADD CONSTRAINT FK_8ECECC333DAC0075 FOREIGN KEY (process_execution_id) REFERENCES process_execution (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + if (!$schema->hasTable('process_user')) { + $this->addSql('CREATE SEQUENCE process_user_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE process_user (id INT NOT NULL, email VARCHAR(255) NOT NULL, firstname VARCHAR(255) DEFAULT NULL, lastname VARCHAR(255) DEFAULT NULL, roles JSON NOT NULL, password VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_627A047CE7927C74 ON process_user (email)'); + $this->addSql('CREATE INDEX idx_process_user_email ON process_user (email)'); + } + } + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE log_record DROP CONSTRAINT FK_8ECECC333DAC0075'); + $this->addSql('DROP TABLE log_record'); + $this->addSql('DROP TABLE process_execution'); + $this->addSql('DROP TABLE process_user'); + } +} diff --git a/src/Migrations/Version20240729151928.php b/src/Migrations/Version20240729151928.php new file mode 100644 index 0000000..d1635b8 --- /dev/null +++ b/src/Migrations/Version20240729151928.php @@ -0,0 +1,48 @@ +connection->getDatabasePlatform(); + if ($platform instanceof PostgreSQLPlatform) { + $this->addSql('CREATE TABLE process_schedule (id INT AUTO_INCREMENT NOT NULL, process VARCHAR(255) NOT NULL, type VARCHAR(6) NOT NULL, expression VARCHAR(255) NOT NULL, input VARCHAR(255), context JSON NOT NULL, PRIMARY KEY(id))'); + } + + if ($platform instanceof MariaDBPlatform || $platform instanceof MySQLPlatform) { + $this->addSql('CREATE TABLE process_schedule (id INT AUTO_INCREMENT NOT NULL, process VARCHAR(255) NOT NULL, type VARCHAR(6) NOT NULL, expression VARCHAR(255) NOT NULL, input VARCHAR(255), context JSON NOT NULL COMMENT \'(DC2Type:json)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB;'); + } + } + + public function down(Schema $schema): void + { + $this->addSql('DROP TABLE process_schedule'); + } +} diff --git a/src/Migrations/Version20240730090403.php b/src/Migrations/Version20240730090403.php new file mode 100644 index 0000000..101395a --- /dev/null +++ b/src/Migrations/Version20240730090403.php @@ -0,0 +1,38 @@ +addSql('ALTER TABLE process_user ADD token VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE process_user DROP token'); + } +} diff --git a/src/Migrations/Version20241007134542.php b/src/Migrations/Version20241007134542.php new file mode 100644 index 0000000..1599e1b --- /dev/null +++ b/src/Migrations/Version20241007134542.php @@ -0,0 +1,42 @@ +hasTable('process_user') && !$schema->getTable('process_user')->hasColumn('timezone')) { + $this->addSql('ALTER TABLE process_user ADD timezone VARCHAR(255) DEFAULT NULL'); + } + } + + public function down(Schema $schema): void + { + if ($schema->hasTable('process_user') && $schema->getTable('process_user')->hasColumn('timezone')) { + $this->addSql('ALTER TABLE process_user DROP timezone'); + } + } +} diff --git a/src/Migrations/Version20241007152613.php b/src/Migrations/Version20241007152613.php new file mode 100644 index 0000000..5b8cf45 --- /dev/null +++ b/src/Migrations/Version20241007152613.php @@ -0,0 +1,42 @@ +hasTable('process_execution') && !$schema->getTable('process_execution')->hasColumn('context')) { + $this->addSql('ALTER TABLE process_execution ADD context JSON NOT NULL'); + } + } + + public function down(Schema $schema): void + { + if ($schema->hasTable('process_execution') && $schema->getTable('process_execution')->hasColumn('context')) { + $this->addSql('ALTER TABLE process_execution DROP context'); + } + } +} diff --git a/src/Migrations/Version20241009075733.php b/src/Migrations/Version20241009075733.php new file mode 100644 index 0000000..b66faee --- /dev/null +++ b/src/Migrations/Version20241009075733.php @@ -0,0 +1,39 @@ +hasTable('process_user') && !$schema->getTable('process_user')->hasColumn('locale')) { + $this->addSql('ALTER TABLE process_user ADD locale VARCHAR(255) DEFAULT NULL'); + } + } + + public function down(Schema $schema): void + { + if ($schema->hasTable('process_user') && $schema->getTable('process_user')->hasColumn('locale')) { + $this->addSql('ALTER TABLE process_user DROP locale'); + } + } +} diff --git a/src/Monolog/Handler/DoctrineProcessHandler.php b/src/Monolog/Handler/DoctrineProcessHandler.php new file mode 100644 index 0000000..07c4a0a --- /dev/null +++ b/src/Monolog/Handler/DoctrineProcessHandler.php @@ -0,0 +1,75 @@ + */ + private ArrayCollection $records; + private ?ProcessExecutionManager $processExecutionManager = null; + private ?EntityManagerInterface $em = null; + + public function __construct(int|string|Level $level = Level::Debug, bool $bubble = true) + { + parent::__construct($level, $bubble); + $this->records = new ArrayCollection(); + } + + public function setEntityManager(EntityManagerInterface $em): void + { + $this->em = $em; + } + + public function setProcessExecutionManager(ProcessExecutionManager $processExecutionManager): void + { + $this->processExecutionManager = $processExecutionManager; + } + + public function __destruct() + { + $this->flush(); + parent::__destruct(); + } + + public function flush(): void + { + foreach ($this->records as $record) { + if (($currentProcessExecution = $this->processExecutionManager?->getCurrentProcessExecution()) instanceof ProcessExecution) { + $entity = new \CleverAge\UiProcessBundle\Entity\LogRecord($record, $currentProcessExecution); + $this->em?->persist($entity); + } + } + $this->em?->flush(); + foreach ($this->records as $record) { + $this->em?->detach($record); + } + $this->records = new ArrayCollection(); + } + + protected function write(LogRecord $record): void + { + $this->records->add($record); + if (500 === $this->records->count()) { + $this->flush(); + } + } +} diff --git a/src/Monolog/Handler/ProcessHandler.php b/src/Monolog/Handler/ProcessHandler.php new file mode 100644 index 0000000..6b58789 --- /dev/null +++ b/src/Monolog/Handler/ProcessHandler.php @@ -0,0 +1,68 @@ +directory); + } + + /** + * @param 'ALERT'|'Alert'|'alert'|'CRITICAL'|'Critical'|'critical'|'DEBUG'|'Debug'|'debug'|'EMERGENCY'|'Emergency'|'emergency'|'ERROR'|'Error'|'error'|'INFO'|'Info'|'info'|'NOTICE'|'Notice'|'notice'|'WARNING'|'Warning'|'warning' $level + */ + public function setReportIncrementLevel(string $level): void + { + $this->reportIncrementLevel = Level::fromName($level); + } + + public function hasFilename(): bool + { + return $this->directory !== $this->url; + } + + public function setFilename(string $filename): void + { + $this->url = \sprintf('%s/%s', $this->directory, $filename); + } + + public function close(): void + { + $this->url = $this->directory; + parent::close(); + } + + public function getFilename(): ?string + { + return $this->url; + } + + public function write(LogRecord $record): void + { + parent::write($record); + if ($record->level->value >= $this->reportIncrementLevel->value) { + $this->processExecutionManager->increment($record->level->name); + } + } +} diff --git a/src/Monolog/Handler/ProcessLogHandler.php b/src/Monolog/Handler/ProcessLogHandler.php deleted file mode 100644 index 1278e5f..0000000 --- a/src/Monolog/Handler/ProcessLogHandler.php +++ /dev/null @@ -1,66 +0,0 @@ -logDir = $processLogDir; - } - - /** - * @param array $record - * - * @throws FilesystemException - */ - protected function write(array $record): void - { - if (null === $logFilename = ($this->logFilenames[$this->currentProcessCode] ?? null)) { - return; - } - - if ($record['level'] < Logger::INFO) { - return; - } - - if (null === $this->filesystem) { - $this->filesystem = new Filesystem( - new LocalFilesystemAdapter($this->logDir, null, \FILE_APPEND) - ); - } - $this->filesystem->write($logFilename, $record['formatted']); - } - - public function setLogFilename(string $logFilename, string $processCode): void - { - $this->logFilenames[$processCode] = $logFilename; - } - - public function setCurrentProcessCode(?string $code): void - { - $this->currentProcessCode = $code; - } - - public function getLogFilename(): ?string - { - return $this->logFilenames[$this->currentProcessCode] ?? null; - } -} diff --git a/src/Repository/.gitignore b/src/Repository/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/Repository/ProcessExecutionRepository.php b/src/Repository/ProcessExecutionRepository.php index 9294fa7..e5d09e0 100644 --- a/src/Repository/ProcessExecutionRepository.php +++ b/src/Repository/ProcessExecutionRepository.php @@ -2,77 +2,64 @@ declare(strict_types=1); -namespace CleverAge\ProcessUiBundle\Repository; +/* + * This file is part of the CleverAge/UiProcessBundle package. + * + * Copyright (c) Clever-Age + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CleverAge\UiProcessBundle\Repository; -use CleverAge\ProcessUiBundle\Entity\ProcessExecution; -use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\Persistence\ManagerRegistry; +use CleverAge\UiProcessBundle\Entity\LogRecord; +use CleverAge\UiProcessBundle\Entity\ProcessExecution; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; /** - * @extends ServiceEntityRepository + * @extends EntityRepository + * + * @method ProcessExecution|null find($id, $lockMode = null, $lockVersion = null) + * @method ProcessExecution|null findOneBy(mixed[] $criteria, string[] $orderBy = null) + * @method ProcessExecution[] findAll() + * @method ProcessExecution[] findBy(mixed[] $criteria, string[] $orderBy = null, $limit = null, $offset = null) */ -class ProcessExecutionRepository extends ServiceEntityRepository +class ProcessExecutionRepository extends EntityRepository { - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, ProcessExecution::class); - } - - /** - * @return array - */ - public function getProcessCodeChoices(): array + public function __construct(EntityManagerInterface $em) { - $choices = []; - $qb = $this->createQueryBuilder('pe'); - $qb->distinct(true); - $qb->select('pe.processCode'); - foreach ($qb->getQuery()->getArrayResult() as $result) { - $choices[(string) $result['processCode']] = (string) $result['processCode']; - } - - return $choices; + parent::__construct($em, $em->getClassMetadata(ProcessExecution::class)); } - /** - * @return array - */ - public function getSourceChoices(): array + public function save(ProcessExecution $processExecution): void { - $choices = []; - $qb = $this->createQueryBuilder('pe'); - $qb->distinct(true); - $qb->select('pe.source'); - foreach ($qb->getQuery()->getArrayResult() as $result) { - $choices[(string) $result['source']] = (string) $result['source']; - } - - return $choices; + $this->getEntityManager()->persist($processExecution); + $this->getEntityManager()->flush(); } - /** - * @return array - */ - public function getTargetChoices(): array + public function getLastProcessExecution(string $code): ?ProcessExecution { - $choices = []; $qb = $this->createQueryBuilder('pe'); - $qb->distinct(true); - $qb->select('pe.target'); - foreach ($qb->getQuery()->getArrayResult() as $result) { - $choices[(string) $result['target']] = (string) $result['target']; - } - return $choices; + return $qb->select('pe') + ->where($qb->expr()->eq('pe.code', $qb->expr()->literal($code))) + ->orderBy('pe.startDate', 'DESC') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); } - public function deleteBefore(\DateTime $dateTime): void + public function hasLogs(ProcessExecution $processExecution): bool { - $qb = $this->createQueryBuilder('pe'); - $qb->delete(); - $qb->where('pe.startDate < :date'); - $qb->setParameter('date', $dateTime); + $qb = $this->createQueryBuilder('pe') + ->select('count(lr.id)') + ->join(LogRecord::class, 'lr', 'WITH', 'lr.processExecution = pe') + ->where('pe.id = :id') + ->setParameter('id', $processExecution->getId() + ); - $qb->getQuery()->execute(); + return (int) $qb->getQuery()->getSingleScalarResult() > 0; } } diff --git a/src/Repository/ProcessRepository.php b/src/Repository/ProcessRepository.php deleted file mode 100644 index 71140df..0000000 --- a/src/Repository/ProcessRepository.php +++ /dev/null @@ -1,66 +0,0 @@ - - */ -class ProcessRepository extends ServiceEntityRepository -{ - private ProcessUiConfigurationManager $processUiConfigurationManager; - private ProcessConfigurationRegistry $processConfigurationRegistry; - - /** - * @required - */ - public function setProcessUiConfigurationManager(ProcessUiConfigurationManager $processUiConfigurationManager): void - { - $this->processUiConfigurationManager = $processUiConfigurationManager; - } - - /** - * @required - */ - public function setProcessConfigurationRegistry(ProcessConfigurationRegistry $processConfigurationRegistry): void - { - $this->processConfigurationRegistry = $processConfigurationRegistry; - } - - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, Process::class); - } - - public function sync(): void - { - // Create missing process into database - $codes = []; - foreach ($this->processConfigurationRegistry->getProcessConfigurations() as $configuration) { - $process = $this->findOneBy(['processCode' => $configuration->getCode()]); - $codes[] = $configuration->getCode(); - if (null === $process) { - $process = new Process( - $configuration->getCode(), - $this->processUiConfigurationManager->getSource($configuration->getCode()), - $this->processUiConfigurationManager->getTarget($configuration->getCode()), - ); - $this->getEntityManager()->persist($process); - } - } - $this->getEntityManager()->flush(); - - // Delete process in database if not into configuration registry - $qb = $this->createQueryBuilder('p'); - $qb->delete(); - $qb->where($qb->expr()->notIn('p.processCode', $codes)); - $qb->getQuery()->execute(); - } -} diff --git a/src/Repository/ProcessScheduleRepository.php b/src/Repository/ProcessScheduleRepository.php new file mode 100644 index 0000000..6c8e3ea --- /dev/null +++ b/src/Repository/ProcessScheduleRepository.php @@ -0,0 +1,34 @@ + + * + * @method ProcessSchedule|null find($id, $lockMode = null, $lockVersion = null) + * @method ProcessSchedule|null findOneBy(mixed[] $criteria, string[] $orderBy = null) + * @method ProcessSchedule[] findAll() + * @method ProcessSchedule[] findBy(mixed[] $criteria, string[] $orderBy = null, $limit = null, $offset = null) + */ +class ProcessScheduleRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ProcessSchedule::class); + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php deleted file mode 100644 index e03ffa9..0000000 --- a/src/Repository/UserRepository.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ -class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface -{ - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, User::class); - } - - /** - * Used to upgrade (rehash) the user's password automatically over time. - */ - public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newEncodedPassword): void - { - if (!$user instanceof User) { - throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); - } - - $user->setPassword($newEncodedPassword); - $this->_em->persist($user); - $this->_em->flush(); - } -} diff --git a/src/Scheduler/CronScheduler.php b/src/Scheduler/CronScheduler.php new file mode 100644 index 0000000..b12fd20 --- /dev/null +++ b/src/Scheduler/CronScheduler.php @@ -0,0 +1,71 @@ +repository->findAll() as $processSchedule) { + $violations = $this->validator->validate($processSchedule); + if (0 !== $violations->count()) { + foreach ($violations as $violation) { + $this->logger->info( + 'Scheduler configuration is not valid.', + ['reason' => $violation->getMessage()] + ); + } + continue; + } + if (ProcessScheduleType::CRON === $processSchedule->getType()) { + $schedule->add( + RecurringMessage::cron( + $processSchedule->getExpression() ?? '', + new CronProcessMessage($processSchedule) + ) + ); + } elseif (ProcessScheduleType::EVERY === $processSchedule->getType()) { + $schedule->add( + RecurringMessage::every( + $processSchedule->getExpression() ?? '', + new CronProcessMessage($processSchedule) + ) + ); + } + } + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + } + + return $schedule; + } +} diff --git a/src/Security/HttpProcessExecutionAuthenticator.php b/src/Security/HttpProcessExecutionAuthenticator.php new file mode 100644 index 0000000..85734f9 --- /dev/null +++ b/src/Security/HttpProcessExecutionAuthenticator.php @@ -0,0 +1,70 @@ +get('_route') && $request->isMethod(Request::METHOD_POST); + } + + public function authenticate(Request $request): Passport + { + if (false === $request->headers->has('Authorization')) { + throw new AuthenticationException('Missing auth token.'); + } + $token = $request->headers->get('Authorization'); + $token = str_replace('Bearer ', '', $token ?? ''); + $user = $this->entityManager->getRepository(User::class)->findOneBy( + ['token' => (new Pbkdf2PasswordHasher())->hash($token)] + ); + if (null === $user) { + throw new AuthenticationException('Invalid token.'); + } + + return new SelfValidatingPassport(new UserBadge($user->getEmail() ?? '')); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $data = [ + 'message' => $exception->getMessage(), + ]; + + return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); + } +} diff --git a/src/Security/LoginFormAuthAuthenticator.php b/src/Security/LoginFormAuthAuthenticator.php deleted file mode 100644 index fc08421..0000000 --- a/src/Security/LoginFormAuthAuthenticator.php +++ /dev/null @@ -1,62 +0,0 @@ -urlGenerator = $urlGenerator; - } - - public function authenticate(Request $request): PassportInterface - { - $username = (string) $request->request->get('email', ''); - - $request->getSession()->set(Security::LAST_USERNAME, $username); - - return new Passport( - new UserBadge($username), - new PasswordCredentials((string) $request->request->get('password', '')), - [ - new CsrfTokenBadge('authenticate', $request->get('_csrf_token')), - ] - ); - } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response - { - if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { - return new RedirectResponse($targetPath); - } - - return new RedirectResponse($this->urlGenerator->generate('process_admin')); - } - - protected function getLoginUrl(Request $request): string - { - return $this->urlGenerator->generate(self::LOGIN_ROUTE); - } -} diff --git a/src/Twig/Components/BootstrapModal.php b/src/Twig/Components/BootstrapModal.php new file mode 100644 index 0000000..1e8c362 --- /dev/null +++ b/src/Twig/Components/BootstrapModal.php @@ -0,0 +1,20 @@ +getName(); + } + + public function getCssClass(string|int $value): string + { + return \is_int($value) ? + match ($value) { + Level::Warning->value => 'warning', + Level::Error->value, Level::Emergency->value, Level::Critical->value, Level::Alert->value => 'danger', + Level::Debug->value, Level::Info->value => 'success', + default => '', + } + : match ($value) { + Level::Warning->name => 'warning', + Level::Error->name, Level::Emergency->name, Level::Critical->name, Level::Alert->name => 'danger', + Level::Debug->name, Level::Info->name => 'success', + default => '', + }; + } +} diff --git a/src/Twig/Runtime/MD5ExtensionRuntime.php b/src/Twig/Runtime/MD5ExtensionRuntime.php new file mode 100644 index 0000000..8ec0da9 --- /dev/null +++ b/src/Twig/Runtime/MD5ExtensionRuntime.php @@ -0,0 +1,24 @@ +processExecutionRepository->getLastProcessExecution($code); + } + + public function getProcessSource(string $code): ?string + { + return $this->processConfigurationsManager->getUiOptions($code)['source'] ?? null; + } + + public function getProcessTarget(string $code): ?string + { + return $this->processConfigurationsManager->getUiOptions($code)['target'] ?? null; + } +} diff --git a/src/Twig/Runtime/ProcessExtensionRuntime.php b/src/Twig/Runtime/ProcessExtensionRuntime.php new file mode 100644 index 0000000..e4f31ed --- /dev/null +++ b/src/Twig/Runtime/ProcessExtensionRuntime.php @@ -0,0 +1,33 @@ +processConfigurationsManager->getUiOptions($code) ?? []; + } +} diff --git a/src/Validator/CronExpression.php b/src/Validator/CronExpression.php new file mode 100644 index 0000000..d7e2866 --- /dev/null +++ b/src/Validator/CronExpression.php @@ -0,0 +1,22 @@ +context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $value) + ->addViolation(); + } + } +} diff --git a/src/Validator/EveryExpression.php b/src/Validator/EveryExpression.php new file mode 100644 index 0000000..302fdb5 --- /dev/null +++ b/src/Validator/EveryExpression.php @@ -0,0 +1,22 @@ +context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $value) + ->addViolation(); + } +} diff --git a/src/Validator/IsValidProcessCode.php b/src/Validator/IsValidProcessCode.php new file mode 100644 index 0000000..cba6cda --- /dev/null +++ b/src/Validator/IsValidProcessCode.php @@ -0,0 +1,23 @@ +registry->hasProcessConfiguration($value)) { + $this->context->buildViolation($constraint->messageNotExists) + ->setParameter('{{ value }}', $value) + ->addViolation(); + + return; + } + + if (!$this->registry->getProcessConfiguration($value)->isPublic()) { + $this->context->buildViolation($constraint->messageIsNotPublic) + ->setParameter('{{ value }}', $value) + ->addViolation(); + } + } +} diff --git a/symfony.lock b/symfony.lock deleted file mode 100644 index ea6af83..0000000 --- a/symfony.lock +++ /dev/null @@ -1,512 +0,0 @@ -{ - "cleverage/process-bundle": { - "version": "v3.2.3" - }, - "composer/package-versions-deprecated": { - "version": "1.11.99.2" - }, - "composer/semver": { - "version": "3.2.9" - }, - "composer/xdebug-handler": { - "version": "2.0.1" - }, - "doctrine/annotations": { - "version": "1.0", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "1.0", - "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" - }, - "files": [ - "config/routes/annotations.yaml" - ] - }, - "doctrine/cache": { - "version": "2.0.3" - }, - "doctrine/collections": { - "version": "1.6.7" - }, - "doctrine/common": { - "version": "3.1.2" - }, - "doctrine/data-fixtures": { - "version": "1.5.0" - }, - "doctrine/dbal": { - "version": "2.13.x-dev" - }, - "doctrine/deprecations": { - "version": "v0.5.3" - }, - "doctrine/doctrine-bundle": { - "version": "2.4", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "2.4", - "ref": "dda18c8830b143bc31c0e0457fb13b9029614d76" - }, - "files": [ - "config/packages/doctrine.yaml", - "config/packages/prod/doctrine.yaml", - "config/packages/test/doctrine.yaml", - "src/Entity/.gitignore", - "src/Repository/.gitignore" - ] - }, - "doctrine/doctrine-fixtures-bundle": { - "version": "3.0", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "3.0", - "ref": "e5b542d4ef47d8a003c91beb35650c76907f7e53" - }, - "files": [ - "src/DataFixtures/AppFixtures.php" - ] - }, - "doctrine/doctrine-migrations-bundle": { - "version": "3.1", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "3.1", - "ref": "ee609429c9ee23e22d6fa5728211768f51ed2818" - }, - "files": [ - "config/packages/doctrine_migrations.yaml", - "migrations/.gitignore" - ] - }, - "doctrine/event-manager": { - "version": "1.1.1" - }, - "doctrine/inflector": { - "version": "2.0.3" - }, - "doctrine/instantiator": { - "version": "1.4.0" - }, - "doctrine/lexer": { - "version": "1.2.1" - }, - "doctrine/migrations": { - "version": "3.1.3" - }, - "doctrine/orm": { - "version": "2.9.2" - }, - "doctrine/persistence": { - "version": "2.2.1" - }, - "doctrine/sql-formatter": { - "version": "1.1.1" - }, - "friendsofphp/php-cs-fixer": { - "version": "3.6", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "3.0", - "ref": "be2103eb4a20942e28a6dd87736669b757132435" - }, - "files": [ - ".php-cs-fixer.dist.php" - ] - }, - "friendsofphp/proxy-manager-lts": { - "version": "v1.0.5" - }, - "laminas/laminas-code": { - "version": "4.3.0" - }, - "laminas/laminas-eventmanager": { - "version": "3.3.1" - }, - "laminas/laminas-zendframework-bridge": { - "version": "1.2.0" - }, - "monolog/monolog": { - "version": "2.2.0" - }, - "nikic/php-parser": { - "version": "v4.10.5" - }, - "php-cs-fixer/diff": { - "version": "v2.0.2" - }, - "phpstan/extension-installer": { - "version": "1.1.0" - }, - "phpstan/phpstan": { - "version": "0.12.89" - }, - "phpstan/phpstan-doctrine": { - "version": "1.2.10" - }, - "phpstan/phpstan-symfony": { - "version": "1.1.5" - }, - "psr/cache": { - "version": "1.0.1" - }, - "psr/container": { - "version": "1.1.1" - }, - "psr/event-dispatcher": { - "version": "1.0.0" - }, - "psr/log": { - "version": "1.1.4" - }, - "rector/rector": { - "version": "0.12.13" - }, - "roave/security-advisories": { - "version": "dev-latest" - }, - "sensio/framework-extra-bundle": { - "version": "5.2", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "5.2", - "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b" - }, - "files": [ - "config/packages/sensio_framework_extra.yaml" - ] - }, - "squizlabs/php_codesniffer": { - "version": "3.6", - "recipe": { - "repo": "github.com/symfony/recipes-contrib", - "branch": "master", - "version": "3.6", - "ref": "1019e5c08d4821cb9b77f4891f8e9c31ff20ac6f" - }, - "files": [ - "phpcs.xml.dist" - ] - }, - "symfony/asset": { - "version": "v5.3.0" - }, - "symfony/cache": { - "version": "v5.3.0" - }, - "symfony/cache-contracts": { - "version": "v2.4.0" - }, - "symfony/config": { - "version": "v5.3.0" - }, - "symfony/console": { - "version": "5.3", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "5.3", - "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047" - }, - "files": [ - "bin/console" - ] - }, - "symfony/dependency-injection": { - "version": "v5.3.0" - }, - "symfony/deprecation-contracts": { - "version": "v2.4.0" - }, - "symfony/doctrine-bridge": { - "version": "v5.3.1" - }, - "symfony/dotenv": { - "version": "v5.3.0" - }, - "symfony/error-handler": { - "version": "v5.3.0" - }, - "symfony/event-dispatcher": { - "version": "v5.3.0" - }, - "symfony/event-dispatcher-contracts": { - "version": "v2.4.0" - }, - "symfony/expression-language": { - "version": "v5.3.0" - }, - "symfony/filesystem": { - "version": "v5.3.0" - }, - "symfony/finder": { - "version": "v5.3.0" - }, - "symfony/flex": { - "version": "1.0", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "1.0", - "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e" - }, - "files": [ - ".env" - ] - }, - "symfony/form": { - "version": "v5.3.3" - }, - "symfony/framework-bundle": { - "version": "5.3", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "5.3", - "ref": "772b77cfb5017644547ef7f9364c54e4eb9a6c61" - }, - "files": [ - "config/packages/cache.yaml", - "config/packages/framework.yaml", - "config/preload.php", - "config/routes/framework.yaml", - "config/services.yaml", - "public/index.php", - "src/Controller/.gitignore", - "src/Kernel.php" - ] - }, - "symfony/http-client-contracts": { - "version": "v2.4.0" - }, - "symfony/http-foundation": { - "version": "v5.3.1" - }, - "symfony/http-kernel": { - "version": "v5.3.1" - }, - "symfony/intl": { - "version": "v5.3.4" - }, - "symfony/maker-bundle": { - "version": "1.0", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "1.0", - "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" - } - }, - "symfony/mime": { - "version": "v5.3.2" - }, - "symfony/monolog-bridge": { - "version": "v5.3.0" - }, - "symfony/monolog-bundle": { - "version": "3.7", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "3.7", - "ref": "329f6a5ef2e7aa033f802be833ef8d1268dd0848" - }, - "files": [ - "config/packages/dev/monolog.yaml", - "config/packages/prod/deprecations.yaml", - "config/packages/prod/monolog.yaml", - "config/packages/test/monolog.yaml" - ] - }, - "symfony/options-resolver": { - "version": "v5.3.0" - }, - "symfony/orm-pack": { - "version": "v2.1.0" - }, - "symfony/password-hasher": { - "version": "v5.3.0" - }, - "symfony/polyfill-intl-grapheme": { - "version": "v1.23.0" - }, - "symfony/polyfill-intl-icu": { - "version": "v1.23.0" - }, - "symfony/polyfill-intl-idn": { - "version": "v1.23.0" - }, - "symfony/polyfill-intl-normalizer": { - "version": "v1.23.0" - }, - "symfony/polyfill-mbstring": { - "version": "v1.23.0" - }, - "symfony/polyfill-php73": { - "version": "v1.23.0" - }, - "symfony/polyfill-php80": { - "version": "v1.23.0" - }, - "symfony/polyfill-php81": { - "version": "v1.23.0" - }, - "symfony/process": { - "version": "v5.3.0" - }, - "symfony/profiler-pack": { - "version": "v1.0.5" - }, - "symfony/property-access": { - "version": "v5.3.0" - }, - "symfony/property-info": { - "version": "v5.3.1" - }, - "symfony/proxy-manager-bridge": { - "version": "v5.3.0" - }, - "symfony/routing": { - "version": "5.3", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "5.3", - "ref": "2d706bd8c6a9e6730343bb22092dabba1f42f4f3" - }, - "files": [ - "config/packages/routing.yaml", - "config/routes.yaml" - ] - }, - "symfony/runtime": { - "version": "v5.3.0" - }, - "symfony/security-bundle": { - "version": "5.3", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "5.3", - "ref": "8b35bfc00a7716db4ca5a764a4b338476ca3a704" - }, - "files": [ - "config/packages/security.yaml" - ] - }, - "symfony/security-core": { - "version": "v5.3.1" - }, - "symfony/security-csrf": { - "version": "v5.3.0" - }, - "symfony/security-guard": { - "version": "v5.3.0" - }, - "symfony/security-http": { - "version": "v5.3.1" - }, - "symfony/serializer": { - "version": "v5.3.1" - }, - "symfony/service-contracts": { - "version": "v2.4.0" - }, - "symfony/stopwatch": { - "version": "v5.3.0" - }, - "symfony/string": { - "version": "v5.3.0" - }, - "symfony/translation-contracts": { - "version": "v2.4.0" - }, - "symfony/twig-bridge": { - "version": "v5.3.0" - }, - "symfony/twig-bundle": { - "version": "5.3", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "5.3", - "ref": "b416645602504d22d15912e0918001e6d71bb9fa" - }, - "files": [ - "config/packages/twig.yaml", - "templates/base.html.twig" - ] - }, - "symfony/validator": { - "version": "4.3", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "4.3", - "ref": "d902da3e4952f18d3bf05aab29512eb61cabd869" - }, - "files": [ - "config/packages/test/validator.yaml", - "config/packages/validator.yaml" - ] - }, - "symfony/var-dumper": { - "version": "v5.3.0" - }, - "symfony/var-exporter": { - "version": "v5.3.0" - }, - "symfony/web-profiler-bundle": { - "version": "3.3", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "3.3", - "ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6" - }, - "files": [ - "config/packages/dev/web_profiler.yaml", - "config/packages/test/web_profiler.yaml", - "config/routes/dev/web_profiler.yaml" - ] - }, - "symfony/webpack-encore-bundle": { - "version": "1.9", - "recipe": { - "repo": "github.com/symfony/recipes", - "branch": "master", - "version": "1.9", - "ref": "9399a0bfc6ee7a0c019f952bca46d6a6045dd451" - }, - "files": [ - "assets/app.js", - "assets/bootstrap.js", - "assets/controllers.json", - "assets/controllers/hello_controller.js", - "assets/styles/app.css", - "config/packages/assets.yaml", - "config/packages/prod/webpack_encore.yaml", - "config/packages/test/webpack_encore.yaml", - "config/packages/webpack_encore.yaml", - "package.json", - "webpack.config.js" - ] - }, - "symfony/yaml": { - "version": "v5.3.0" - }, - "twig/extra-bundle": { - "version": "v3.3.1" - }, - "twig/intl-extra": { - "version": "v3.3.0" - }, - "twig/twig": { - "version": "v3.3.2" - } -} diff --git a/templates/admin/field/array.html.twig b/templates/admin/field/array.html.twig new file mode 100644 index 0000000..8cdf10d --- /dev/null +++ b/templates/admin/field/array.html.twig @@ -0,0 +1,16 @@ +{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #} +{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #} +{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #} +
    + {{ _self.render(field.value) }} +
+ +{% macro render(value) %} + {% for key, item in value %} + {% if item is iterable %} +
  • {{ key }}
    • {{ _self.render(item) }}
    + {% else %} +
  • {{ key }} : {{ item }}
  • + {% endif %} + {% endfor %} +{% endmacro %} diff --git a/templates/admin/field/enum.html.twig b/templates/admin/field/enum.html.twig new file mode 100644 index 0000000..e615276 --- /dev/null +++ b/templates/admin/field/enum.html.twig @@ -0,0 +1,2 @@ +{% set class = field.value.value == 'failed' ? 'danger' : 'success' %} +{{ field.value.value }} diff --git a/templates/admin/field/log_level.html.twig b/templates/admin/field/log_level.html.twig new file mode 100644 index 0000000..2a666ad --- /dev/null +++ b/templates/admin/field/log_level.html.twig @@ -0,0 +1 @@ +{{ log_label(field.value) }} diff --git a/templates/admin/field/process_source.html.twig b/templates/admin/field/process_source.html.twig new file mode 100644 index 0000000..fea3203 --- /dev/null +++ b/templates/admin/field/process_source.html.twig @@ -0,0 +1 @@ +{{ get_process_source(entity.instance.code) }} diff --git a/templates/admin/field/process_target.html.twig b/templates/admin/field/process_target.html.twig new file mode 100644 index 0000000..15470e6 --- /dev/null +++ b/templates/admin/field/process_target.html.twig @@ -0,0 +1 @@ +{{ get_process_target(entity.instance.code) }} diff --git a/templates/admin/field/report.html.twig b/templates/admin/field/report.html.twig new file mode 100644 index 0000000..745e19c --- /dev/null +++ b/templates/admin/field/report.html.twig @@ -0,0 +1,6 @@ +{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #} +{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #} +{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #} +
      + {{ field.value }} +
    diff --git a/templates/admin/login.html.twig b/templates/admin/login.html.twig new file mode 100644 index 0000000..1d1d077 --- /dev/null +++ b/templates/admin/login.html.twig @@ -0,0 +1 @@ +{% extends '@EasyAdmin/page/login.html.twig' %} diff --git a/templates/admin/process/launch.html.twig b/templates/admin/process/launch.html.twig new file mode 100644 index 0000000..88d0f7b --- /dev/null +++ b/templates/admin/process/launch.html.twig @@ -0,0 +1,18 @@ +{% extends ea.templatePath('layout') %} +{% trans_default_domain ea.i18n.translationDomain %} + +{% block main %} + {% form_theme form '@EasyAdmin/crud/form_theme.html.twig' %} +
    +
    +
    + {{ form_start(form) }} + {{ form_widget(form) }} +
    + +
    + {{ form_end(form) }} +
    +
    +
    +{% endblock %} diff --git a/templates/admin/process/list.html.twig b/templates/admin/process/list.html.twig new file mode 100644 index 0000000..083f821 --- /dev/null +++ b/templates/admin/process/list.html.twig @@ -0,0 +1,78 @@ +{# @var urlGenerator \EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator #} +{% extends ea.templatePath('layout') %} +{% trans_default_domain ea.i18n.translationDomain %} + +{% block main %} + + + {% block table_head %} + + + + + + + + + {% endblock %} + + + {% block table_body %} + {# @var process \CleverAge\ProcessBundle\Configuration\ProcessConfiguration #} + {% for process in processes %} + {% set lastExecution = get_last_execution_date(process.code) %} + {% set uiOptions = resolve_ui_options(process.code) %} + {% set statusClass = '' %} + {% if lastExecution is not null %} + {% set statusClass = lastExecution.status.value == 'failed' ? 'danger' : 'success' %} + {% endif %} + + + + + + + + + + {% endfor %} + {% endblock %} + +
    {{ 'Process code'|trans }}{{ 'Last execution'|trans }}{{ 'Status'|trans }}{{ 'Source'|trans }}{{ 'Target'|trans }}{{ 'Actions'|trans }}
    {{ process.code }}{% if lastExecution is not null %}{{ lastExecution.startDate|date('Y/m/d H:i:s') }}{% endif %}{% if lastExecution is not null %}{{ lastExecution.status.value }}{% endif %}{% if process.options.ui.source is defined %}{{ process.options.ui.source }}{% endif %}{% if process.options.ui.target is defined %}{{ process.options.ui.target }}{% endif %} + {% if ('modal' == uiOptions.ui_launch_mode) %} + + + + {% else %} + + + + {% endif %} + + + +
    +{% endblock %} diff --git a/templates/components/BootstrapModal.html.twig b/templates/components/BootstrapModal.html.twig new file mode 100644 index 0000000..9c226de --- /dev/null +++ b/templates/components/BootstrapModal.html.twig @@ -0,0 +1,37 @@ +
    + +
    diff --git a/src/Controller/.gitignore b/tests/.gitkeep similarity index 100% rename from src/Controller/.gitignore rename to tests/.gitkeep diff --git a/translations/messages.fr.yaml b/translations/messages.fr.yaml new file mode 100644 index 0000000..4d276e3 --- /dev/null +++ b/translations/messages.fr.yaml @@ -0,0 +1,54 @@ +Generate Token: Générer un jeton +Credentials: Informations d'identification +User: Utilisateur +Dashboard: Tableau de bord +Process: Processus +Process list: Liste des processus +Executions: Exécutions +Logs: Logs +Scheduler: Planificateur +Users: Utilisateurs +User List: Liste des utilisateurs +Informations: Informations +Roles: Rôles +Intl.: Internationalisation +Email: Email +New password: Nouveau mot de passe +Repeat password: Répéter le mot de passe +Firstname: Prénom +Lastname: Nom +Timezone: Fuseau horaire +Locale: Locale +ProcessExecution: Exécution des processus +Code: Code +Status: Status +Start Date: Date de début +End Date: Date de fin +Source: Source +Target: Destination +Duration: Durée +Report: Rapport +Context: Context +Launch: Exécuter +View executions: Voir les éxécutions +Process code: Code du processus +Last execution: Dernière exécution +Actions: Actions +LogRecord: Logs +Level: Niveau +Message: Message +Created At: Date de création +Has context info ?: Context ? +ProcessSchedule: Planificateur +To run scheduler, ensure "bin/console messenger:consume scheduler_cron" console is alive. See https://symfony.com/doc/current/messenger.html#supervisor-configuration.: "Pour fonctionner, assurez vous que la commande \"bin/console messenger:consume scheduler_cron\" soit exécutée. Plus d''informations sur https://symfony.com/doc/current/messenger.html#supervisor-configuration." +Delete: Supprimer +Type: Type +Expression: Expression +Next Execution: Prochaîne exécution +Input: Input +General: Général +Context (key/value): Context (clé/valeur) +key: clé +value: valeur +Context Key: Context clé +Context Value: Context valeur