From 795a500adb8902c4d0e3ca206dcef164a9276ea9 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sun, 24 Jul 2022 14:02:13 +0100 Subject: [PATCH] feat: Queue package manager commands (#3418) * feat: Queue package manager commands * adjust tests * fix: force run whynot command synchronously * chore: maximize command output box's height * chore: more user instructions on background queue * feat: track command peak memory usage * feat: exit of CLI php version doesn't match web php version * chore: install deps * chore: format and typing workflow fix Signed-off-by: Sami Mazouz --- extensions/package-manager/extend.php | 12 +- extensions/package-manager/js/package.json | 20 +- .../src/admin/components/ControlSection.tsx | 34 +++ .../js/src/admin/components/ExtensionItem.tsx | 29 +-- .../js/src/admin/components/Installer.tsx | 35 +-- .../js/src/admin/components/Label.tsx | 19 ++ .../js/src/admin/components/MajorUpdater.tsx | 25 +- .../js/src/admin/components/Pagination.tsx | 40 ++++ .../js/src/admin/components/QueueSection.tsx | 215 ++++++++++++++++++ .../js/src/admin/components/SettingsPage.tsx | 25 ++ .../src/admin/components/TaskOutputModal.tsx | 40 ++++ .../js/src/admin/components/Updater.tsx | 67 ++++-- .../js/src/admin/components/WhyNotModal.tsx | 19 +- .../package-manager/js/src/admin/index.tsx | 69 +++--- .../js/src/admin/models/Task.ts | 50 ++++ .../package-manager/js/src/admin/shims.d.ts | 11 + .../js/src/admin/states/QueueState.ts | 65 ++++++ .../js/src/admin/utils/humanDuration.ts | 9 + .../js/src/admin/utils/jumpToQueue.ts | 13 ++ extensions/package-manager/js/tsconfig.json | 1 + extensions/package-manager/less/admin.less | 158 ++----------- .../less/admin/ControlSection.less | 133 +++++++++++ .../package-manager/less/admin/Label.less | 41 ++++ .../less/admin/QueueSection.less | 57 +++++ .../less/admin/TaskOutputModal.less | 5 + extensions/package-manager/locale/en.yml | 39 ++++ ...00_create_package_manager_tasks_table.php} | 14 +- .../Controller/CheckForUpdatesController.php | 17 +- .../Api/Controller/GlobalUpdateController.php | 9 +- .../src/Api/Controller/ListTaskController.php | 32 --- .../Api/Controller/ListTasksController.php | 73 ++++++ .../Api/Controller/MajorUpdateController.php | 9 +- .../Api/Controller/MinorUpdateController.php | 9 +- .../Controller/RemoveExtensionController.php | 9 +- .../Controller/RequireExtensionController.php | 8 +- .../Controller/UpdateExtensionController.php | 9 +- .../src/Api/Controller/WhyNotController.php | 8 +- .../src/Api/Serializer/TaskSerializer.php | 49 ++++ .../src/Command/BusinessCommandInterface.php | 15 ++ .../src/Command/CheckForUpdates.php | 13 +- .../src/Command/CheckForUpdatesHandler.php | 21 +- .../src/Command/GlobalUpdate.php | 13 +- .../src/Command/GlobalUpdateHandler.php | 5 +- .../src/Command/MajorUpdate.php | 13 +- .../src/Command/MajorUpdateHandler.php | 12 +- .../src/Command/MinorUpdate.php | 13 +- .../src/Command/MinorUpdateHandler.php | 5 +- .../src/Command/RemoveExtension.php | 13 +- .../src/Command/RemoveExtensionHandler.php | 7 +- .../src/Command/RequireExtension.php | 13 +- .../src/Command/RequireExtensionHandler.php | 3 +- .../src/Command/UpdateExtension.php | 13 +- .../src/Command/UpdateExtensionHandler.php | 5 +- .../package-manager/src/Command/WhyNot.php | 13 +- .../src/Command/WhyNotHandler.php | 5 +- .../src/Composer/ComposerAdapter.php | 14 +- .../src/Job/ComposerCommandJob.php | 65 ++++++ .../package-manager/src/Job/Dispatcher.php | 84 +++++++ .../src/Job/DispatcherResponse.php | 23 ++ extensions/package-manager/src/Task/Task.php | 93 ++++++++ .../src/Task/TaskRepository.php | 34 +++ .../tests/integration/SetupComposer.php | 2 - .../integration/api/GlobalUpdateTest.php | 2 +- .../tests/integration/api/MajorUpdateTest.php | 2 +- .../tests/integration/api/MinorUpdateTest.php | 6 +- .../api/extensions/RemoveExtensionTest.php | 2 +- .../api/extensions/UpdateExtensionTest.php | 2 +- .../SendNotificationWhenReplyIsPosted.php | 6 + .../dist-typings/admin/AdminApplication.d.ts | 1 + .../core/js/src/admin/AdminApplication.ts | 1 + yarn.lock | 10 + 71 files changed, 1631 insertions(+), 375 deletions(-) create mode 100644 extensions/package-manager/js/src/admin/components/ControlSection.tsx create mode 100644 extensions/package-manager/js/src/admin/components/Label.tsx create mode 100644 extensions/package-manager/js/src/admin/components/Pagination.tsx create mode 100644 extensions/package-manager/js/src/admin/components/QueueSection.tsx create mode 100644 extensions/package-manager/js/src/admin/components/SettingsPage.tsx create mode 100644 extensions/package-manager/js/src/admin/components/TaskOutputModal.tsx create mode 100644 extensions/package-manager/js/src/admin/models/Task.ts create mode 100644 extensions/package-manager/js/src/admin/shims.d.ts create mode 100644 extensions/package-manager/js/src/admin/states/QueueState.ts create mode 100644 extensions/package-manager/js/src/admin/utils/humanDuration.ts create mode 100644 extensions/package-manager/js/src/admin/utils/jumpToQueue.ts create mode 100644 extensions/package-manager/less/admin/ControlSection.less create mode 100644 extensions/package-manager/less/admin/Label.less create mode 100644 extensions/package-manager/less/admin/QueueSection.less create mode 100644 extensions/package-manager/less/admin/TaskOutputModal.less rename extensions/package-manager/migrations/{2017_04_09_000000_create_bazaar_tasks_table.php => 2022_02_22_000000_create_package_manager_tasks_table.php} (57%) delete mode 100755 extensions/package-manager/src/Api/Controller/ListTaskController.php create mode 100644 extensions/package-manager/src/Api/Controller/ListTasksController.php create mode 100644 extensions/package-manager/src/Api/Serializer/TaskSerializer.php create mode 100644 extensions/package-manager/src/Command/BusinessCommandInterface.php create mode 100644 extensions/package-manager/src/Job/ComposerCommandJob.php create mode 100644 extensions/package-manager/src/Job/Dispatcher.php create mode 100644 extensions/package-manager/src/Job/DispatcherResponse.php create mode 100644 extensions/package-manager/src/Task/Task.php create mode 100644 extensions/package-manager/src/Task/TaskRepository.php diff --git a/extensions/package-manager/extend.php b/extensions/package-manager/extend.php index a56849684..2c87c807f 100755 --- a/extensions/package-manager/extend.php +++ b/extensions/package-manager/extend.php @@ -19,6 +19,8 @@ use Flarum\PackageManager\Exception\ExceptionHandler; use Flarum\PackageManager\Exception\MajorUpdateFailedException; use Flarum\PackageManager\Settings\LastUpdateCheck; use Flarum\PackageManager\Settings\LastUpdateRun; +use Illuminate\Contracts\Queue\Queue; +use Illuminate\Queue\SyncQueue; return [ (new Extend\Routes('api')) @@ -29,7 +31,8 @@ return [ ->post('/package-manager/why-not', 'package-manager.why-not', Api\Controller\WhyNotController::class) ->post('/package-manager/minor-update', 'package-manager.minor-update', Api\Controller\MinorUpdateController::class) ->post('/package-manager/major-update', 'package-manager.major-update', Api\Controller\MajorUpdateController::class) - ->post('/package-manager/global-update', 'package-manager.global-update', Api\Controller\GlobalUpdateController::class), + ->post('/package-manager/global-update', 'package-manager.global-update', Api\Controller\GlobalUpdateController::class) + ->get('/package-manager-tasks', 'package-manager.tasks.index', Api\Controller\ListTasksController::class), (new Extend\Frontend('admin')) ->css(__DIR__.'/less/admin.less') @@ -37,17 +40,20 @@ return [ ->content(function (Document $document) { $paths = resolve(Paths::class); - $document->payload['isRequiredDirectoriesWritable'] = is_writable($paths->vendor) + $document->payload['flarum-package-manager.writable_dirs'] = is_writable($paths->vendor) && is_writable($paths->storage.'/.composer') && is_writable($paths->base.'/composer.json') && is_writable($paths->base.'/composer.lock'); + + $document->payload['flarum-package-manager.using_sync_queue'] = resolve(Queue::class) instanceof SyncQueue; }), new Extend\Locales(__DIR__.'/locale'), (new Extend\Settings()) ->default(LastUpdateCheck::key(), json_encode(LastUpdateCheck::default())) - ->default(LastUpdateRun::key(), json_encode(LastUpdateRun::default())), + ->default(LastUpdateRun::key(), json_encode(LastUpdateRun::default())) + ->default('flarum-package-manager.queue_jobs', false), (new Extend\ServiceProvider) ->register(PackageManagerServiceProvider::class), diff --git a/extensions/package-manager/js/package.json b/extensions/package-manager/js/package.json index 6978f7a99..a1a5ef75d 100755 --- a/extensions/package-manager/js/package.json +++ b/extensions/package-manager/js/package.json @@ -1,17 +1,17 @@ { - "private": true, "name": "@flarum/package-manager", "version": "0.0.0", + "private": true, "prettier": "@flarum/prettier-config", "devDependencies": { - "prettier": "^2.5.1", - "flarum-webpack-config": "^2.0.0", - "webpack": "^5.65.0", - "webpack-cli": "^4.9.1", "@flarum/prettier-config": "^1.0.0", "flarum-tsconfig": "^1.0.2", + "flarum-webpack-config": "^2.0.0", + "prettier": "^2.5.1", "typescript": "^4.5.4", - "typescript-coverage-report": "^0.6.1" + "typescript-coverage-report": "^0.6.1", + "webpack": "^5.65.0", + "webpack-cli": "^4.9.1" }, "scripts": { "dev": "webpack --mode development --watch", @@ -21,9 +21,11 @@ "ci": "yarn install --immutable --immutable-cache", "analyze": "cross-env ANALYZER=true yarn run build", "clean-typings": "npx rimraf dist-typings && mkdir dist-typings", - "build-typings": "yarn run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && yarn run post-build-typings", + "build-typings": "yarn run clean-typings && tsc && [ -e src/@types ] && cp -r src/@types dist-typings/@types", "check-typings": "tsc --noEmit --emitDeclarationOnly false", - "check-typings-coverage": "typescript-coverage-report", - "post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'" + "check-typings-coverage": "typescript-coverage-report" + }, + "dependencies": { + "pretty-bytes": "^6.0.0" } } diff --git a/extensions/package-manager/js/src/admin/components/ControlSection.tsx b/extensions/package-manager/js/src/admin/components/ControlSection.tsx new file mode 100644 index 000000000..8f296003f --- /dev/null +++ b/extensions/package-manager/js/src/admin/components/ControlSection.tsx @@ -0,0 +1,34 @@ +import app from 'flarum/admin/app'; +import Component from 'flarum/common/Component'; +import Alert from 'flarum/common/components/Alert'; + +import Installer from './Installer'; +import Updater from './Updater'; + +export default class ControlSection extends Component { + view() { + return ( +
+
+
+

{app.translator.trans('flarum-package-manager.admin.sections.control.title')}

+
+
+
+ {app.data['flarum-package-manager.writable_dirs'] ? ( + <> + + + + ) : ( +
+ + {app.translator.trans('flarum-package-manager.admin.file_permissions')} + +
+ )} +
+
+ ); + } +} diff --git a/extensions/package-manager/js/src/admin/components/ExtensionItem.tsx b/extensions/package-manager/js/src/admin/components/ExtensionItem.tsx index 17d141296..7b37c1f33 100644 --- a/extensions/package-manager/js/src/admin/components/ExtensionItem.tsx +++ b/extensions/package-manager/js/src/admin/components/ExtensionItem.tsx @@ -1,20 +1,15 @@ -import Mithril from 'mithril'; +import type Mithril from 'mithril'; import app from 'flarum/admin/app'; import Component, { ComponentAttrs } from 'flarum/common/Component'; import classList from 'flarum/common/utils/classList'; import icon from 'flarum/common/helpers/icon'; import Tooltip from 'flarum/common/components/Tooltip'; import Button from 'flarum/common/components/Button'; -import { Extension as BaseExtension } from 'flarum/admin/AdminApplication'; +import { Extension } from 'flarum/admin/AdminApplication'; + import { UpdatedPackage } from './Updater'; import WhyNotModal from './WhyNotModal'; - -/* - * @todo fix in core - */ -export type Extension = BaseExtension & { - name: string; -}; +import Label from './Label'; export interface ExtensionItemAttrs extends ComponentAttrs { extension: Extension; @@ -29,6 +24,7 @@ export interface ExtensionItemAttrs extends ComponentAttrs { export default class ExtensionItem extends Component { view(vnode: Mithril.Vnode): Mithril.Children { const { extension, updates, onClickUpdate, whyNotWarning, isCore, isDanger } = this.attrs; + const latestVersion = updates['latest-minor'] ?? (updates['latest-major'] && !isCore ? updates['latest-major'] : null); return (
{extension.extra['flarum-extension'].title}
{this.version(extension.version)} - {updates['latest-minor'] ? ( - - {this.version(updates['latest-minor']!)} - - ) : null} - {updates['latest-major'] && !isCore ? ( - - {this.version(updates['latest-major']!)} - + {latestVersion ? ( + ) : null}
@@ -83,7 +74,7 @@ export default class ExtensionItem extends Component { +import errorHandler from '../utils/errorHandler'; +import jumpToQueue from '../utils/jumpToQueue'; +import { AsyncBackendResponse } from '../shims'; + +interface InstallerAttrs extends ComponentAttrs {} + +export default class Installer extends Component { packageName!: Stream; isLoading: boolean = false; - oninit(vnode: Mithril.Vnode): void { + oninit(vnode: Mithril.Vnode): void { super.oninit(vnode); this.packageName = Stream(''); @@ -18,7 +23,7 @@ export default class Installer extends Component { view(): Mithril.Children { return ( -
+

{app.translator.trans('flarum-package-manager.admin.extensions.install_help', { @@ -46,7 +51,7 @@ export default class Installer extends Component { app.modal.show(LoadingModal); app - .request<{ id: string }>({ + .request({ method: 'POST', url: `${app.forum.attribute('apiUrl')}/package-manager/extensions`, body: { @@ -55,13 +60,17 @@ export default class Installer extends Component { errorHandler, }) .then((response) => { - const extensionId = response.id; - app.alerts.show( - { type: 'success' }, - app.translator.trans('flarum-package-manager.admin.extensions.successful_install', { extension: extensionId }) - ); - window.location.href = `${app.forum.attribute('adminUrl')}#/extension/${extensionId}`; - window.location.reload(); + if (response.processing) { + jumpToQueue(); + } else { + const extensionId = response.id; + app.alerts.show( + { type: 'success' }, + app.translator.trans('flarum-package-manager.admin.extensions.successful_install', { extension: extensionId }) + ); + window.location.href = `${app.forum.attribute('adminUrl')}#/extension/${extensionId}`; + window.location.reload(); + } }) .finally(() => { this.isLoading = false; diff --git a/extensions/package-manager/js/src/admin/components/Label.tsx b/extensions/package-manager/js/src/admin/components/Label.tsx new file mode 100644 index 000000000..7196b16b2 --- /dev/null +++ b/extensions/package-manager/js/src/admin/components/Label.tsx @@ -0,0 +1,19 @@ +import type Mithril from 'mithril'; +import Component, { ComponentAttrs } from 'flarum/common/Component'; +import classList from 'flarum/common/utils/classList'; + +interface LabelAttrs extends ComponentAttrs { + type: 'success' | 'error' | 'neutral' | 'warning'; +} + +export default class Label extends Component { + view(vnode: Mithril.Vnode) { + const { className, type, ...attrs } = this.attrs; + + return ( + + {vnode.children} + + ); + } +} diff --git a/extensions/package-manager/js/src/admin/components/MajorUpdater.tsx b/extensions/package-manager/js/src/admin/components/MajorUpdater.tsx index bb1099e68..92c096f57 100644 --- a/extensions/package-manager/js/src/admin/components/MajorUpdater.tsx +++ b/extensions/package-manager/js/src/admin/components/MajorUpdater.tsx @@ -1,15 +1,18 @@ +import type Mithril from 'mithril'; import app from 'flarum/admin/app'; import Component, { ComponentAttrs } from 'flarum/common/Component'; -import Mithril from 'mithril'; import Button from 'flarum/common/components/Button'; import Tooltip from 'flarum/common/components/Tooltip'; -import { UpdatedPackage, UpdateState } from './Updater'; import LoadingModal from 'flarum/admin/components/LoadingModal'; -import errorHandler from '../utils/errorHandler'; import Alert from 'flarum/common/components/Alert'; -import WhyNotModal from './WhyNotModal'; import RequestError from 'flarum/common/utils/RequestError'; -import ExtensionItem, { Extension } from './ExtensionItem'; + +import { UpdatedPackage, UpdateState } from './Updater'; +import errorHandler from '../utils/errorHandler'; +import WhyNotModal from './WhyNotModal'; +import ExtensionItem from './ExtensionItem'; +import { AsyncBackendResponse } from '../shims'; +import jumpToQueue from '../utils/jumpToQueue'; interface MajorUpdaterAttrs extends ComponentAttrs { coreUpdate: UpdatedPackage; @@ -84,7 +87,7 @@ export default class MajorUpdater({ method: 'POST', url: `${app.forum.attribute('apiUrl')}/package-manager/major-update`, body: { @@ -92,9 +95,13 @@ export default class MajorUpdater { - app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.update_successful')); - window.location.reload(); + .then((response) => { + if (response?.processing) { + jumpToQueue(); + } else { + app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.update_successful')); + window.location.reload(); + } }) .catch((e: RequestError) => { app.modal.close(); diff --git a/extensions/package-manager/js/src/admin/components/Pagination.tsx b/extensions/package-manager/js/src/admin/components/Pagination.tsx new file mode 100644 index 000000000..c640ccdcd --- /dev/null +++ b/extensions/package-manager/js/src/admin/components/Pagination.tsx @@ -0,0 +1,40 @@ +import app from 'flarum/admin/app'; +import Component, { ComponentAttrs } from 'flarum/common/Component'; +import Button from 'flarum/common/components/Button'; +import QueueState from '../states/QueueState'; + +interface PaginationAttrs extends ComponentAttrs { + list: QueueState; +} + +/** + * @todo make it abstract in core for reusability. + */ +export default class Pagination extends Component { + view() { + return ( +

+ ); + } +} diff --git a/extensions/package-manager/js/src/admin/components/QueueSection.tsx b/extensions/package-manager/js/src/admin/components/QueueSection.tsx new file mode 100644 index 000000000..717189a38 --- /dev/null +++ b/extensions/package-manager/js/src/admin/components/QueueSection.tsx @@ -0,0 +1,215 @@ +import type Mithril from 'mithril'; +import app from 'flarum/admin/app'; +import Component, { ComponentAttrs } from 'flarum/common/Component'; +import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; +import Button from 'flarum/common/components/Button'; +import Tooltip from 'flarum/common/components/Tooltip'; +import { Extension } from 'flarum/admin/AdminApplication'; +import icon from 'flarum/common/helpers/icon'; +import ItemList from 'flarum/common/utils/ItemList'; +import extractText from 'flarum/common/utils/extractText'; + +import Label from './Label'; +import TaskOutputModal from './TaskOutputModal'; +import humanDuration from '../utils/humanDuration'; +import Task, { TaskOperations } from '../models/Task'; +import Pagination from './Pagination'; + +interface QueueTableColumn extends ComponentAttrs { + label: string; + content: (task: Task) => Mithril.Children; +} + +export default class QueueSection extends Component<{}> { + oninit(vnode: Mithril.Vnode<{}, this>) { + super.oninit(vnode); + + app.packageManagerQueue.load(); + } + + view() { + return ( +
+
+
+

{app.translator.trans('flarum-package-manager.admin.sections.queue.title')}

+
+
+
{this.queueTable()}
+
+ ); + } + + columns() { + const items = new ItemList(); + + items.add( + 'operation', + { + label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.operation')), + content: (task) => ( +
+ {this.operationIcon(task.operation())} + + {app.translator.trans(`flarum-package-manager.admin.sections.queue.operations.${task.operation()}`)} + +
+ ), + }, + 80 + ); + + items.add( + 'package', + { + label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.package')), + content: (task) => { + const extension: Extension | null = app.data.extensions[task.package()?.replace(/(\/flarum-|\/flarum-ext-|\/)/g, '-')]; + + return extension ? ( +
+
+ {extension.icon ? icon(extension.icon.name) : ''} +
+
+ {extension.extra['flarum-extension'].title} + {task.package()} +
+
+ ) : ( + task.package() + ); + }, + }, + 75 + ); + + items.add( + 'status', + { + label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.status')), + content: (task) => ( + + ), + }, + 70 + ); + + items.add( + 'elapsedTime', + { + label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.elapsed_time')), + content: (task) => + !task.startedAt() ? ( + app.translator.trans('flarum-package-manager.admin.sections.queue.task_just_started') + ) : ( + + {humanDuration(task.startedAt(), task.finishedAt())} + + ), + }, + 65 + ); + + items.add( + 'memoryUsed', + { + label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.peak_memory_used')), + content: (task) => {task.peakMemoryUsed()}, + }, + 60 + ); + + items.add( + 'details', + { + label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.details')), + content: (task) => ( +