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) => ( +