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 <ilyasmazouz@gmail.com>
This commit is contained in:
Sami Mazouz 2022-07-24 14:02:13 +01:00 committed by GitHub
parent 75aaef7d76
commit 795a500adb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1631 additions and 375 deletions

View File

@ -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),

View File

@ -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"
}
}

View File

@ -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 (
<div className="ExtensionPage-permissions PackageManager-controlSection">
<div className="ExtensionPage-permissions-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-package-manager.admin.sections.control.title')}</h2>
</div>
</div>
<div className="container">
{app.data['flarum-package-manager.writable_dirs'] ? (
<>
<Installer />
<Updater />
</>
) : (
<div className="Form-group">
<Alert type="warning" dismissible={false}>
{app.translator.trans('flarum-package-manager.admin.file_permissions')}
</Alert>
</div>
)}
</div>
</div>
);
}
}

View File

@ -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<Attrs extends ExtensionItemAttrs = ExtensionItemAttrs> extends Component<Attrs> {
view(vnode: Mithril.Vnode<Attrs, this>): 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 (
<div
@ -45,15 +41,10 @@ export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionI
<div className="PackageManager-extension-name">{extension.extra['flarum-extension'].title}</div>
<div className="PackageManager-extension-version">
<span className="PackageManager-extension-version-current">{this.version(extension.version)}</span>
{updates['latest-minor'] ? (
<span className="PackageManager-extension-version-latest PackageManager-extension-version-latest--minor">
{this.version(updates['latest-minor']!)}
</span>
) : null}
{updates['latest-major'] && !isCore ? (
<span className="PackageManager-extension-version-latest PackageManager-extension-version-latest--major">
{this.version(updates['latest-major']!)}
</span>
{latestVersion ? (
<Label className="PackageManager-extension-version-latest" type={updates['latest-minor'] ? 'success' : 'warning'}>
{this.version(latestVersion)}
</Label>
) : null}
</div>
</div>
@ -83,7 +74,7 @@ export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionI
);
}
private version(v: string): string {
version(v: string): string {
return 'v' + v.replace('v', '');
}
}

View File

@ -1,16 +1,21 @@
import type Mithril from 'mithril';
import app from 'flarum/admin/app';
import Component from 'flarum/common/Component';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import Button from 'flarum/common/components/Button';
import Stream from 'flarum/common/utils/Stream';
import LoadingModal from 'flarum/admin/components/LoadingModal';
import errorHandler from '../utils/errorHandler';
export default class Installer<Attrs> extends Component<Attrs> {
import errorHandler from '../utils/errorHandler';
import jumpToQueue from '../utils/jumpToQueue';
import { AsyncBackendResponse } from '../shims';
interface InstallerAttrs extends ComponentAttrs {}
export default class Installer extends Component<InstallerAttrs> {
packageName!: Stream<string>;
isLoading: boolean = false;
oninit(vnode: Mithril.Vnode<Attrs, this>): void {
oninit(vnode: Mithril.Vnode<InstallerAttrs, this>): void {
super.oninit(vnode);
this.packageName = Stream('');
@ -18,7 +23,7 @@ export default class Installer<Attrs> extends Component<Attrs> {
view(): Mithril.Children {
return (
<div className="Form-group">
<div className="Form-group PackageManager-installer">
<label htmlFor="install-extension">{app.translator.trans('flarum-package-manager.admin.extensions.install')}</label>
<p className="helpText">
{app.translator.trans('flarum-package-manager.admin.extensions.install_help', {
@ -46,7 +51,7 @@ export default class Installer<Attrs> extends Component<Attrs> {
app.modal.show(LoadingModal);
app
.request<{ id: string }>({
.request<AsyncBackendResponse & { id: number }>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/extensions`,
body: {
@ -55,13 +60,17 @@ export default class Installer<Attrs> extends Component<Attrs> {
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;

View File

@ -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<LabelAttrs> {
view(vnode: Mithril.Vnode<LabelAttrs, this>) {
const { className, type, ...attrs } = this.attrs;
return (
<span className={classList(['Label', `Label--${this.attrs.type}`, className])} {...attrs}>
{vnode.children}
</span>
);
}
}

View File

@ -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<T extends MajorUpdaterAttrs = MajorUpdaterAttr
app.modal.show(LoadingModal);
app
.request({
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/major-update`,
body: {
@ -92,9 +95,13 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
},
errorHandler,
})
.then(() => {
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();

View File

@ -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<PaginationAttrs> {
view() {
return (
<nav class="Pagination UserListPage-gridPagination">
<Button
disabled={!this.attrs.list.hasPrev()}
title={app.translator.trans('core.admin.users.pagination.back_button')}
onclick={() => this.attrs.list.prev()}
icon="fas fa-chevron-left"
className="Button Button--icon UserListPage-backBtn"
/>
<span class="UserListPage-pageNumber">
{app.translator.trans('core.admin.users.pagination.page_counter', {
current: this.attrs.list.pageNumber() + 1,
total: this.attrs.list.getTotalPages(),
})}
</span>
<Button
disabled={!this.attrs.list.hasNext()}
title={app.translator.trans('core.admin.users.pagination.next_button')}
onclick={() => this.attrs.list.next()}
icon="fas fa-chevron-right"
className="Button Button--icon UserListPage-nextBtn"
/>
</nav>
);
}
}

View File

@ -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 (
<section id="PackageManager-queueSection" className="ExtensionPage-permissions PackageManager-queueSection">
<div className="ExtensionPage-permissions-header PackageManager-queueSection-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-package-manager.admin.sections.queue.title')}</h2>
<Button
className="Button Button--icon"
icon="fas fa-sync-alt"
onclick={() => app.packageManagerQueue.load()}
aria-label={app.translator.trans('flarum-package-manager.admin.sections.queue.refresh')}
/>
</div>
</div>
<div className="container">{this.queueTable()}</div>
</section>
);
}
columns() {
const items = new ItemList<QueueTableColumn>();
items.add(
'operation',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.operation')),
content: (task) => (
<div className="PackageManager-queueTable-operation">
<span className="PackageManager-queueTable-operation-icon">{this.operationIcon(task.operation())}</span>
<span className="PackageManager-queueTable-operation-name">
{app.translator.trans(`flarum-package-manager.admin.sections.queue.operations.${task.operation()}`)}
</span>
</div>
),
},
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 ? (
<div className="PackageManager-queueTable-package">
<div className="PackageManager-queueTable-package-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''}
</div>
<div className="PackageManager-queueTable-package-details">
<span className="PackageManager-queueTable-package-title">{extension.extra['flarum-extension'].title}</span>
<span className="PackageManager-queueTable-package-name">{task.package()}</span>
</div>
</div>
) : (
task.package()
);
},
},
75
);
items.add(
'status',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.status')),
content: (task) => (
<Label
className="PackageManager-queueTable-status"
type={{ running: 'neutral', failure: 'error', pending: 'warning', success: 'success' }[task.status()]}
>
{app.translator.trans(`flarum-package-manager.admin.sections.queue.statuses.${task.status()}`)}
</Label>
),
},
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')
) : (
<Tooltip text={`${dayjs(task.startedAt()).format('LL LTS')} ${dayjs(task.finishedAt()).format('LL LTS')}`}>
<span>{humanDuration(task.startedAt(), task.finishedAt())}</span>
</Tooltip>
),
},
65
);
items.add(
'memoryUsed',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.peak_memory_used')),
content: (task) => <span>{task.peakMemoryUsed()}</span>,
},
60
);
items.add(
'details',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.details')),
content: (task) => (
<Button
className="Button Button--icon Table-controls-item"
icon="fas fa-file-alt"
aria-label={app.translator.trans('flarum-package-manager.admin.sections.queue.columns.details')}
// @todo fix in core
// @ts-ignore
onclick={() => app.modal.show(TaskOutputModal, { task })}
/>
),
className: 'Table-controls',
},
55
);
return items;
}
queueTable() {
const tasks = app.packageManagerQueue.getItems();
if (!tasks) {
return <LoadingIndicator />;
}
if (tasks && !tasks.length) {
return <h3 className="ExtensionPage-subHeader">{app.translator.trans('flarum-package-manager.admin.sections.queue.none')}</h3>;
}
const columns = this.columns();
return (
<>
<table className="Table PackageManager-queueTable">
<thead>
<tr>
{columns.toArray().map((item, index) => (
<th key={index}>{item.label}</th>
))}
</tr>
</thead>
<tbody>
{tasks.map((task, index) => (
<tr key={index}>
{columns.toArray().map((item, index) => {
const { label, content, ...attrs } = item;
return (
<td key={index} {...attrs}>
{content(task)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
<Pagination list={app.packageManagerQueue} />
</>
);
}
operationIcon(operation: TaskOperations): Mithril.Children {
return icon(
{
update_check: 'fas fa-sync-alt',
update_major: 'fas fa-play',
update_minor: 'fas fa-play',
update_global: 'fas fa-play',
extension_install: 'fas fa-download',
extension_remove: 'fas fa-times',
extension_update: 'fas fa-arrow-alt-circle-up',
why_not: 'fas fa-exclamation-circle',
}[operation]
);
}
}

View File

@ -0,0 +1,25 @@
import type Mithril from 'mithril';
import app from 'flarum/admin/app';
import ExtensionPage, { ExtensionPageAttrs } from 'flarum/admin/components/ExtensionPage';
import ItemList from 'flarum/common/utils/ItemList';
import QueueSection from './QueueSection';
import ControlSection from './ControlSection';
export default class SettingsPage extends ExtensionPage {
sections(vnode: Mithril.VnodeDOM<ExtensionPageAttrs, this>): ItemList<unknown> {
// @todo add core feature to register sections
const items = super.sections(vnode);
if (app.data.settings['flarum-package-manager.queue_jobs']) {
items.add('queue', <QueueSection />, 5);
}
items.add('control', <ControlSection />, 8);
items.setPriority('content', 10);
items.setPriority('permissions', 0);
return items;
}
}

View File

@ -0,0 +1,40 @@
import app from 'flarum/admin/app';
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import Task from '../models/Task';
interface TaskOutputModalAttrs extends IInternalModalAttrs {
task: Task;
}
export default class TaskOutputModal<CustomAttrs extends TaskOutputModalAttrs = TaskOutputModalAttrs> extends Modal<CustomAttrs> {
className() {
return 'Modal--large QuickModal';
}
title() {
return app.translator.trans(`flarum-package-manager.admin.sections.queue.operations.${this.attrs.task.operation()}`);
}
content() {
return (
<div className="Modal-body">
<div className="TaskOutputModal-data">
<div className="Form-group">
<label>{app.translator.trans('flarum-package-manager.admin.sections.queue.output_modal.command')}</label>
<div className="FormControl TaskOutputModal-data-command">
<code>$ composer {this.attrs.task.command()}</code>
</div>
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-package-manager.admin.sections.queue.output_modal.output')}</label>
<div className="FormControl TaskOutputModal-data-output">
<code>
<pre>{this.attrs.task.output()}</pre>
</code>
</div>
</div>
</div>
</div>
);
}
}

View File

@ -1,14 +1,17 @@
import Mithril from 'mithril';
import app from 'flarum/admin/app';
import Component from 'flarum/common/Component';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import Button from 'flarum/common/components/Button';
import humanTime from 'flarum/common/helpers/humanTime';
import extractText from 'flarum/common/utils/extractText';
import LoadingModal from 'flarum/admin/components/LoadingModal';
import errorHandler from '../utils/errorHandler';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import MajorUpdater from './MajorUpdater';
import ExtensionItem, { Extension } from './ExtensionItem';
import ExtensionItem from './ExtensionItem';
import extractText from 'flarum/common/utils/extractText';
import jumpToQueue from '../utils/jumpToQueue';
import { AsyncBackendResponse } from '../shims';
import { Extension } from 'flarum/admin/AdminApplication';
export type UpdatedPackage = {
name: string;
@ -44,7 +47,9 @@ export type LastUpdateRun = {
limitedPackages: () => string[];
};
export default class Updater<Attrs> extends Component<Attrs> {
interface UpdaterAttrs extends ComponentAttrs {}
export default class Updater extends Component<UpdaterAttrs> {
isLoading: string | null = null;
packageUpdates: Record<string, UpdatedPackage> = {};
lastUpdateCheck: LastUpdateCheck = JSON.parse(app.data.settings['flarum-package-manager.last_update_check']) as LastUpdateCheck;
@ -60,7 +65,7 @@ export default class Updater<Attrs> extends Component<Attrs> {
return lastUpdateRun;
}
oninit(vnode: Mithril.Vnode<Attrs, this>) {
oninit(vnode: Mithril.Vnode<UpdaterAttrs, this>) {
super.oninit(vnode);
}
@ -174,13 +179,17 @@ export default class Updater<Attrs> extends Component<Attrs> {
this.isLoading = 'check';
app
.request({
.request<AsyncBackendResponse | LastUpdateCheck>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/check-for-updates`,
errorHandler,
})
.then((response) => {
this.lastUpdateCheck = response as LastUpdateCheck;
if ((response as AsyncBackendResponse).processing) {
jumpToQueue();
} else {
this.lastUpdateCheck = response as LastUpdateCheck;
}
})
.finally(() => {
this.isLoading = null;
@ -194,14 +203,18 @@ export default class Updater<Attrs> extends Component<Attrs> {
this.isLoading = 'minor-update';
app
.request({
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/minor-update`,
errorHandler,
})
.then(() => {
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();
}
})
.finally(() => {
this.isLoading = null;
@ -215,17 +228,23 @@ export default class Updater<Attrs> extends Component<Attrs> {
this.isLoading = 'extension-update';
app
.request({
.request<AsyncBackendResponse | null>({
method: 'PATCH',
url: `${app.forum.attribute('apiUrl')}/package-manager/extensions/${extension.id}`,
errorHandler,
})
.then(() => {
app.alerts.show(
{ type: 'success' },
app.translator.trans('flarum-package-manager.admin.extensions.successful_update', { extension: extension.extra['flarum-extension'].title })
);
window.location.reload();
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show(
{ type: 'success' },
app.translator.trans('flarum-package-manager.admin.extensions.successful_update', {
extension: extension.extra['flarum-extension'].title,
})
);
window.location.reload();
}
})
.finally(() => {
this.isLoading = null;
@ -238,14 +257,18 @@ export default class Updater<Attrs> extends Component<Attrs> {
this.isLoading = 'global-update';
app
.request({
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/global-update`,
errorHandler,
})
.then(() => {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.updater.global_update_successful'));
window.location.reload();
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.updater.global_update_successful'));
window.location.reload();
}
})
.finally(() => {
this.isLoading = null;

View File

@ -1,14 +1,21 @@
import type Mithril from 'mithril';
import app from 'flarum/admin/app';
import Mithril from 'mithril';
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import errorHandler from '../utils/errorHandler';
type WhyNotResponse = {
data: {
reason: string;
};
};
export interface WhyNotModalAttrs extends IInternalModalAttrs {
package: string;
}
export default class WhyNotModal<Attrs extends WhyNotModalAttrs = WhyNotModalAttrs> extends Modal<Attrs> {
export default class WhyNotModal<CustomAttrs extends WhyNotModalAttrs = WhyNotModalAttrs> extends Modal<CustomAttrs> {
loading: boolean = true;
whyNot: string | null = null;
@ -20,7 +27,7 @@ export default class WhyNotModal<Attrs extends WhyNotModalAttrs = WhyNotModalAtt
return app.translator.trans('flarum-package-manager.admin.why_not_modal.title');
}
oncreate(vnode: Mithril.VnodeDOM<Attrs, this>) {
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
super.oncreate(vnode);
this.requestWhyNot();
@ -32,7 +39,7 @@ export default class WhyNotModal<Attrs extends WhyNotModalAttrs = WhyNotModalAtt
requestWhyNot(): void {
app
.request({
.request<WhyNotResponse>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/why-not`,
body: {
@ -42,9 +49,9 @@ export default class WhyNotModal<Attrs extends WhyNotModalAttrs = WhyNotModalAtt
},
errorHandler,
})
.then((response: any) => {
.then((response) => {
this.loading = false;
this.whyNot = response.data.whyNot;
this.whyNot = response.data.reason;
m.redraw();
});
}

View File

@ -1,43 +1,42 @@
import { extend } from 'flarum/common/extend';
import app from 'flarum/admin/app';
import Alert from 'flarum/common/components/Alert';
import ExtensionPage from 'flarum/admin/components/ExtensionPage';
import Button from 'flarum/common/components/Button';
import LoadingModal from 'flarum/admin/components/LoadingModal';
import Installer from './components/Installer';
import Updater from './components/Updater';
import isExtensionEnabled from 'flarum/admin/utils/isExtensionEnabled';
import SettingsPage from './components/SettingsPage';
import Task from './models/Task';
import jumpToQueue from './utils/jumpToQueue';
import QueueState from './states/QueueState';
import extractText from 'flarum/common/utils/extractText';
import { AsyncBackendResponse } from './shims';
app.initializers.add('flarum-package-manager', (app) => {
app.store.models['package-manager-tasks'] = Task;
app.packageManagerQueue = new QueueState();
app.extensionData
.for('flarum-package-manager')
.registerSetting(() => {
if (!app.data.isRequiredDirectoriesWritable) {
return (
<div className="Form-group">
<Alert type="warning" dismissible={false}>
{app.translator.trans('flarum-package-manager.admin.file_permissions')}
</Alert>
</div>
);
}
return null;
.registerSetting({
setting: 'flarum-package-manager.queue_jobs',
label: app.translator.trans('flarum-package-manager.admin.settings.queue_jobs'),
help: m.trust(
extractText(
app.translator.trans('flarum-package-manager.admin.settings.queue_jobs_help', {
basic_impl_link: 'https://discuss.flarum.org/d/28151-database-queue-the-simplest-queue-even-for-shared-hosting',
adv_impl_link: 'https://discuss.flarum.org/d/21873-redis-sessions-cache-queues',
php_version: `<strong>${app.data.phpVersion}</strong>`,
folder_perms_link: 'https://docs.flarum.org/install#folder-ownership',
})
)
),
default: false,
type: 'boolean',
disabled: app.data['flarum-package-manager.using_sync_queue'],
})
.registerSetting(() => {
if (app.data.isRequiredDirectoriesWritable) {
return <Installer />;
}
return null;
})
.registerSetting(() => {
if (app.data.isRequiredDirectoriesWritable) {
return <Updater />;
}
return null;
});
.registerPage(SettingsPage);
extend(ExtensionPage.prototype, 'topItems', function (items) {
if (this.extension.id === 'flarum-package-manager' || isExtensionEnabled(this.extension.id)) {
@ -53,13 +52,17 @@ app.initializers.add('flarum-package-manager', (app) => {
app.modal.show(LoadingModal);
app
.request({
.request<AsyncBackendResponse | null>({
url: `${app.forum.attribute('apiUrl')}/package-manager/extensions/${this.extension.id}`,
method: 'DELETE',
})
.then(() => {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.extensions.successful_remove'));
window.location = app.forum.attribute('adminUrl');
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.extensions.successful_remove'));
window.location = app.forum.attribute('adminUrl');
}
})
.finally(() => {
app.modal.close();

View File

@ -0,0 +1,50 @@
import Model from 'flarum/common/Model';
import prettyBytes from 'pretty-bytes';
export type TaskOperations =
| 'extension_install'
| 'extension_remove'
| 'extension_update'
| 'update_global'
| 'update_minor'
| 'update_major'
| 'update_check'
| 'why_not';
export default class Task extends Model {
status() {
return Model.attribute<'pending' | 'running' | 'failure' | 'success'>('status').call(this);
}
operation() {
return Model.attribute<TaskOperations>('operation').call(this);
}
command() {
return Model.attribute<string>('command').call(this);
}
package() {
return Model.attribute<string>('package').call(this);
}
output() {
return Model.attribute<string>('output').call(this);
}
createdAt() {
return Model.attribute('createdAt', Model.transformDate).call(this);
}
startedAt() {
return Model.attribute<Date, string>('startedAt', Model.transformDate).call(this);
}
finishedAt() {
return Model.attribute<Date, string>('finishedAt', Model.transformDate).call(this);
}
peakMemoryUsed() {
return prettyBytes(Model.attribute<number>('peakMemoryUsed').call(this) * 1024);
}
}

View File

@ -0,0 +1,11 @@
import QueueState from './states/QueueState';
export interface AsyncBackendResponse {
processing: boolean;
}
declare module 'flarum/admin/AdminApplication' {
export default interface AdminApplication {
packageManagerQueue: QueueState;
}
}

View File

@ -0,0 +1,65 @@
import app from 'flarum/admin/app';
import Task from '../models/Task';
import { ApiQueryParamsPlural } from 'flarum/common/Store';
export default class QueueState {
private tasks: Task[] | null = null;
private limit = 5;
private offset = 0;
private total = 0;
load(params?: ApiQueryParamsPlural) {
this.tasks = null;
params = {
page: {
limit: this.limit,
offset: this.offset,
...params?.page,
},
...params,
};
return app.store.find<Task[]>('package-manager-tasks', params || {}).then((data) => {
this.tasks = data;
this.total = data.payload.meta?.total;
m.redraw();
return data;
});
}
getItems() {
return this.tasks;
}
getTotalPages(): number {
return Math.ceil(this.total / this.limit);
}
pageNumber(): number {
return Math.ceil(this.offset / this.limit);
}
hasPrev(): boolean {
return this.pageNumber() !== 0;
}
hasNext(): boolean {
return this.offset + this.limit < this.total;
}
prev(): void {
if (this.hasPrev()) {
this.offset -= this.limit;
this.load();
}
}
next(): void {
if (this.hasNext()) {
this.offset += this.limit;
this.load();
}
}
}

View File

@ -0,0 +1,9 @@
import duration from 'dayjs/plugin/duration';
export default function humanDuration(start: Date, end: Date) {
dayjs.extend(duration);
const durationTime = dayjs(end).diff(start);
return dayjs.duration(durationTime).humanize();
}

View File

@ -0,0 +1,13 @@
import app from 'flarum/admin/app';
// @ts-ignore
window.jumpToQueue = jumpToQueue;
export default function jumpToQueue(): void {
app.modal.close();
m.route.set(app.route('extension', { id: 'flarum-package-manager' }));
app.packageManagerQueue.load();
setTimeout(() => {
document.getElementById('PackageManager-queueSection')?.scrollIntoView({ block: 'nearest' });
}, 200);
}

View File

@ -5,6 +5,7 @@
// and also tells your Typescript server to read core's global typings for
// access to `dayjs` and `$` in the global namespace.
"include": ["src/**/*", "../vendor/flarum/core/js/dist-typings/@types/**/*", "@types/**/*"],
"files": ["src/admin/shims.d.ts"],
"compilerOptions": {
// This will output typings to `dist-typings`
"declarationDir": "./dist-typings",

View File

@ -1,3 +1,14 @@
@import "admin/Label";
@import "admin/TaskOutputModal";
@import "admin/QueueSection";
@import "admin/ControlSection";
.PackageManager-controlSection, .PackageManager-queueSection {
> .container {
padding-bottom: 0;
}
}
.FormControl-container {
display: flex;
align-items: center;
@ -5,153 +16,14 @@
gap: 4px;
}
.ComposerFailureModal-output {
white-space: break-spaces;
}
.flarum-package-manager-Page .ExtensionPage-settings .Form-group:last-child {
display: none;
}
.PackageManager-lastUpdatedAt {
color: var(--control-color);
&-label {
font-weight: bold;
}
}
.PackageManager-updaterControls {
display: flex;
flex-wrap: wrap;
gap: 8px;
grid-area: controls;
}
.PackageManager-extensions {
&-grid {
--gap: 12px;
display: grid;
grid-template-columns: repeat(auto-fit, calc(~"100% / 3 - var(--gap)"));
gap: var(--gap);
margin-top: 16px;
}
}
.PackageManager-extension {
display: flex;
align-items: center;
gap: 8px;
background-color: var(--control-bg);
padding: 8px;
border-radius: var(--border-radius);
&-controls {
margin-left: auto;
}
&-icon {
--size: 40px;
}
&-name {
font-weight: bold;
}
&-version {
display: flex;
align-items: center;
gap: 8px;
&-latest {
border-radius: 30px;
padding: 0 6px;
font-weight: bold;
&--minor {
background-color: var(--alert-success-bg);
color: var(--alert-success-color);
}
&--major {
background-color: var(--alert-bg);
color: var(--alert-color);
}
}
}
&--core {
--bg-hover: darken(#e7672e, 5);
background-color: #e7672e;
color: #fff;
--button-color: #fff;
--button-bg-hover: var(--bg-hover);
.Button--danger {
color: #fff;
--button-bg-hover: var(--bg-hover);
}
}
&--core &-icon {
background-size: 100%;
background-color: transparent;
filter: grayscale(1) brightness(3.5);
}
&--danger {
background-color: var(--control-danger-bg);
}
}
.PackageManager-majorUpdate {
--space: 16px;
padding: var(--space);
display: grid;
grid-template-areas:
"title logo"
"helpText logo"
"controls logo"
"extensions extensions"
"failure failure";
grid-gap: 0 var(--space);
align-items: center;
> img {
grid-area: logo;
}
> label {
grid-area: title;
}
> .helpText {
grid-area: helpText;
}
&-failure {
--border-radius: 0;
grid-area: failure;
margin: calc(~"0px - var(--space)");
margin-top: var(--space);
}
&-incompatibleExtensions {
grid-area: extensions;
margin-top: var(--space);
padding-top: var(--space);
border-top: 1px solid var(--control-bg);
}
}
.Form-group--danger {
border: 2px solid var(--alert-error-bg);
border-radius: var(--border-radius);
background-color: transparent;
}
.WhyNotModal {
&-contents {
overflow-x: auto;
}
// @TODO add to core
.Checkbox--switch.disabled {
opacity: 0.6;
cursor: not-allowed;
}

View File

@ -0,0 +1,133 @@
.PackageManager-lastUpdatedAt {
color: var(--control-color);
&-label {
font-weight: bold;
}
}
.PackageManager-updaterControls {
display: flex;
flex-wrap: wrap;
gap: 8px;
grid-area: controls;
}
.PackageManager-extensions {
&-grid {
--gap: 12px;
display: grid;
grid-template-columns: repeat(auto-fit, calc(~"100% / 3 - var(--gap)"));
gap: var(--gap);
margin-top: 16px;
}
}
.PackageManager-extension {
display: flex;
align-items: center;
gap: 8px;
background-color: var(--control-bg);
padding: 8px;
border-radius: var(--border-radius);
&-controls {
margin-left: auto;
}
&-icon {
--size: 40px;
}
&-name {
font-weight: bold;
}
&-version {
display: flex;
align-items: center;
gap: 8px;
&-latest {
text-transform: lowercase;
padding: 2px 6px;
font-size: 0.7rem;
}
}
&--core {
--bg-hover: darken(#e7672e, 5);
background-color: #e7672e;
color: #fff;
--button-color: #fff;
--button-bg-hover: var(--bg-hover);
.Button--danger {
color: #fff;
--button-bg-hover: var(--bg-hover);
}
}
&--core &-icon {
background-size: 100%;
background-color: transparent;
filter: grayscale(1) brightness(3.5);
}
&--danger {
background-color: var(--control-danger-bg);
}
}
.PackageManager-majorUpdate {
--space: 16px;
padding: var(--space);
display: grid;
grid-template-areas:
"title logo"
"helpText logo"
"controls logo"
"extensions extensions"
"failure failure";
grid-gap: 0 var(--space);
align-items: center;
> img {
grid-area: logo;
}
> label {
grid-area: title;
}
> .helpText {
grid-area: helpText;
}
&-failure {
--border-radius: 0;
grid-area: failure;
margin: var(--space) calc(~"0px - var(--space)") calc(~"0px - var(--space)");
}
&-incompatibleExtensions {
grid-area: extensions;
margin-top: var(--space);
padding-top: var(--space);
border-top: 1px solid var(--control-bg);
}
}
.WhyNotModal {
&-contents {
overflow-x: auto;
}
}
.PackageManager-installer .FormControl-container {
max-width: 400px;
.FormControl {
width: 300px;
}
}

View File

@ -0,0 +1,41 @@
:root {
--label-bg: var(--control-bg);
--label-color: var(--control-color);
--label-success-bg: var(--alert-success-bg);
--label-success-color: var(--alert-success-color);
--label-error-bg: var(--alert-error-bg);
--label-error-color: var(--alert-error-color);
--label-warning-bg: var(--alert-bg);
--label-warning-color: var(--alert-color);
--label-neutral-bg: #2781dd;
--label-neutral-color: #f0f6ff;
}
.Label {
background-color: var(--label-bg);
color: var(--label-color);
font-weight: 600;
font-size: 0.65rem;
padding: 4px 6px;
border-radius: var(--border-radius);
&--success {
--label-bg: var(--label-success-bg);
--label-color: var(--label-success-color);
}
&--error {
--label-bg: var(--label-error-bg);
--label-color: var(--label-error-color);
}
&--warning {
--label-bg: var(--label-warning-bg);
--label-color: var(--label-warning-color);
}
&--neutral {
--label-bg: var(--label-neutral-bg);
--label-color: var(--label-neutral-color);
}
}

View File

@ -0,0 +1,57 @@
.PackageManager-queueSection {
&-header > .container {
display: flex;
justify-content: space-between;
align-items: center;
&::before, &::after {
content: none;
}
}
.Label {
text-transform: uppercase;
}
.Table {
width: 100%;
// @TODO move to core
height: 100%;
&-controls-item {
height: 100%;
}
}
}
.PackageManager-queueTable {
&-package {
display: flex;
align-items: center;
gap: 8px;
&-icon {
--size: 30px;
}
&-details {
display: flex;
flex-direction: column;
}
&-name {
font-size: 0.7rem;
}
}
&-operation {
display: flex;
gap: 16px;
&-icon {
width: 20px;
text-align: center;
}
}
}

View File

@ -0,0 +1,5 @@
.TaskOutputModal-data-output {
height: auto;
overflow: auto;
max-height: 40vh;
}

View File

@ -38,6 +38,45 @@ flarum-package-manager:
minor_update_confirmation:
content: This will also update any other extensions/packages with availabe updates.
sections:
control:
title: Manager
queue:
columns:
details: Details
elapsed_time: Completed in
peak_memory_used: Maximum Memory Used
operation: Operation
package: Package
status: Status
none: There are no tasks yet.
operations:
extension_install: Install extension
extension_remove: Remove extension
extension_update: Update extension
update_check: Check for updates
update_global: Update all software packages
update_major: Major update
update_minor: Minor update
why_not: Analyze why a package cannot be updated
output_modal:
command: Composer Command
output: Output
refresh: Refresh tasks list
statuses:
success: Success
failure: Failed
pending: Pending
running: Running
task_just_started: Task just started
title: Queue
settings:
queue_jobs: Run operations in the background queue
queue_jobs_help: >
You can read about a <a href='{basic_impl_link}'>basic queue</a> implementation or a <a href='{adv_impl_link}'>more advanced</a> one.
Make sure the PHP version used for the queue is {php_version}. Make sure <a href='{folder_perms_link}'>folder permissions</a> are correctly configured.
updater:
check_for_updates: Check for updates
flarum: Flarum Core

View File

@ -11,16 +11,18 @@ use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'generic_tasks',
'package_manager_tasks',
function (Blueprint $table) {
$table->increments('id');
$table->string('status', 50)->nullable();
$table->string('command', 50);
$table->string('command_class')->nullable();
$table->string('operation', 50);
$table->string('command', 50)->nullable();
$table->string('package', 100)->nullable();
$table->mediumText('output');
$table->dateTime('created_at');
$table->dateTime('started_at')->nullable();
$table->dateTime('finished_at')->nullable();
$table->timestamp('created_at');
$table->timestamp('started_at')->nullable();
$table->timestamp('finished_at')->nullable();
// Saved in KB
$table->unsignedMediumInteger('peak_memory_used')->nullable();
}
);

View File

@ -11,7 +11,7 @@ namespace Flarum\PackageManager\Api\Controller;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\CheckForUpdates;
use Illuminate\Contracts\Bus\Dispatcher;
use Flarum\PackageManager\Job\Dispatcher;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@ -29,14 +29,25 @@ class CheckForUpdatesController implements RequestHandlerInterface
$this->bus = $bus;
}
/**
* @throws \Flarum\User\Exception\PermissionDeniedException
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$actor = RequestUtil::getActor($request);
$lastUpdateCheck = $this->bus->dispatch(
$actor->assertAdmin();
/**
* @TODO somewhere, if we're queuing, check that a similar composer command isn't already running,
* to avoid duplicate jobs.
*/
$response = $this->bus->dispatch(
new CheckForUpdates($actor)
);
return new JsonResponse($lastUpdateCheck);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new JsonResponse($response->data);
}
}

View File

@ -9,10 +9,11 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\GlobalUpdate;
use Flarum\PackageManager\Job\Dispatcher;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@ -36,10 +37,12 @@ class GlobalUpdateController implements RequestHandlerInterface
{
$actor = RequestUtil::getActor($request);
$this->bus->dispatch(
$response = $this->bus->dispatch(
new GlobalUpdate($actor)
);
return new EmptyResponse(200);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new EmptyResponse(201);
}
}

View File

@ -1,32 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Api\Controller\AbstractListController;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Api\Serializer\TaskSerializer;
use Flarum\PackageManager\Task;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class ListTaskController extends AbstractListController
{
public $serializer = TaskSerializer::class;
/**
* @throws \Flarum\User\Exception\PermissionDeniedException
*/
protected function data(ServerRequestInterface $request, Document $document)
{
RequestUtil::getActor($request)->assertAdmin();
return Task::query()->orderBy('created_at', 'desc')->get();
}
}

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Api\Controller\AbstractListController;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Flarum\PackageManager\Api\Serializer\TaskSerializer;
use Flarum\PackageManager\Task\TaskRepository;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class ListTasksController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = TaskSerializer::class;
/**
* @var UrlGenerator
*/
protected $url;
/**
* @var TaskRepository
*/
protected $repository;
public function __construct(UrlGenerator $url, TaskRepository $repository)
{
$this->url = $url;
$this->repository = $repository;
}
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = RequestUtil::getActor($request);
$actor->assertAdmin();
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$results = $this->repository
->query()
->latest()
->offset($offset)
->limit($limit)
->get();
$total = $this->repository->query()->count();
$document->addMeta('total', $total);
$document->addPaginationLinks(
$this->url->to('api')->route('package-manager.tasks.index'),
$request->getQueryParams(),
$offset,
$limit,
$total
);
return $results;
}
}

View File

@ -9,11 +9,12 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\MajorUpdate;
use Flarum\PackageManager\Job\Dispatcher;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@ -35,10 +36,12 @@ class MajorUpdateController implements RequestHandlerInterface
$actor = RequestUtil::getActor($request);
$dryRun = (bool) (int) Arr::get($request->getParsedBody(), 'data.dryRun', 0);
$this->bus->dispatch(
$response = $this->bus->dispatch(
new MajorUpdate($actor, $dryRun)
);
return new EmptyResponse(200);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new EmptyResponse(201);
}
}

View File

@ -9,10 +9,11 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\MinorUpdate;
use Flarum\PackageManager\Job\Dispatcher;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@ -36,10 +37,12 @@ class MinorUpdateController implements RequestHandlerInterface
{
$actor = RequestUtil::getActor($request);
$this->bus->dispatch(
$response = $this->bus->dispatch(
new MinorUpdate($actor)
);
return new EmptyResponse(200);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new EmptyResponse(201);
}
}

View File

@ -9,11 +9,12 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\RemoveExtension;
use Flarum\PackageManager\Job\Dispatcher;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@ -35,10 +36,12 @@ class RemoveExtensionController implements RequestHandlerInterface
$actor = RequestUtil::getActor($request);
$extensionId = Arr::get($request->getQueryParams(), 'id');
$this->bus->dispatch(
$response = $this->bus->dispatch(
new RemoveExtension($actor, $extensionId)
);
return new EmptyResponse(200);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new EmptyResponse(201);
}
}

View File

@ -9,9 +9,9 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\RequireExtension;
use Flarum\PackageManager\Job\Dispatcher;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
@ -35,10 +35,12 @@ class RequireExtensionController implements RequestHandlerInterface
$actor = RequestUtil::getActor($request);
$package = Arr::get($request->getParsedBody(), 'data.package');
$data = $this->bus->dispatch(
$response = $this->bus->dispatch(
new RequireExtension($actor, $package)
);
return new JsonResponse($data);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new JsonResponse($response->data);
}
}

View File

@ -9,11 +9,12 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\UpdateExtension;
use Flarum\PackageManager\Job\Dispatcher;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@ -35,10 +36,12 @@ class UpdateExtensionController implements RequestHandlerInterface
$actor = RequestUtil::getActor($request);
$extensionId = Arr::get($request->getQueryParams(), 'id');
$this->bus->dispatch(
$response = $this->bus->dispatch(
new UpdateExtension($actor, $extensionId)
);
return new EmptyResponse(200);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new EmptyResponse(201);
}
}

View File

@ -9,9 +9,9 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\WhyNot;
use Flarum\PackageManager\Job\Dispatcher;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
@ -36,12 +36,10 @@ class WhyNotController implements RequestHandlerInterface
$package = Arr::get($request->getParsedBody(), 'data.package', '');
$version = Arr::get($request->getParsedBody(), 'data.version', '*');
$whyNot = $this->bus->dispatch(
$whyNot = $this->bus->sync()->dispatch(
new WhyNot($actor, $package, $version)
);
return new JsonResponse([
'data' => compact('whyNot')
]);
return new JsonResponse(['data' => ['reason' => $whyNot->data['reason']]]);
}
}

View File

@ -0,0 +1,49 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Api\Serializer;
use Flarum\Api\Serializer\AbstractSerializer;
use Flarum\PackageManager\Task\Task;
use InvalidArgumentException;
class TaskSerializer extends AbstractSerializer
{
/**
* {@inheritdoc}
*/
protected $type = 'package-manager-tasks';
/**
* {@inheritdoc}
*
* @param Task $model
* @throws InvalidArgumentException
*/
protected function getDefaultAttributes($model)
{
if (! ($model instanceof Task)) {
throw new InvalidArgumentException(
get_class($this).' can only serialize instances of '.Task::class
);
}
return [
'status' => $model->status,
'operation' => $model->operation,
'command' => $model->command,
'package' => $model->package,
'output' => $model->output,
'createdAt' => $model->created_at,
'startedAt' => $model->started_at,
'finishedAt' => $model->finished_at,
'peakMemoryUsed' => $model->peak_memory_used,
];
}
}

View File

@ -0,0 +1,15 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Command;
interface BusinessCommandInterface
{
public function getOperationName(): string;
}

View File

@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class CheckForUpdates
class CheckForUpdates implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var \Flarum\User\User
*/
@ -22,4 +28,9 @@ class CheckForUpdates
{
$this->actor = $actor;
}
public function getOperationName(): string
{
return Task::UPDATE_CHECK;
}
}

View File

@ -48,29 +48,24 @@ class CheckForUpdatesHandler
*
* The results from both commands are properly processed and merged to have new key values `latest-minor` and `latest-major`.
*
* @throws \Flarum\User\Exception\PermissionDeniedException|ComposerCommandFailedException
* @todo integration test
* @throws ComposerCommandFailedException
*/
public function handle(CheckForUpdates $command)
{
$actor = $command->actor;
$actor->assertAdmin();
$firstOutput = $this->runComposerCommand(false);
$firstOutput = $this->runComposerCommand(false, $command);
$firstOutput = json_decode($this->cleanJson($firstOutput), true);
$majorUpdates = false;
foreach ($firstOutput['installed'] as $package) {
if ($package['latest-status'] === 'update-possible') {
if (isset($package['latest-status']) && $package['latest-status'] === 'update-possible') {
$majorUpdates = true;
break;
}
}
if ($majorUpdates) {
$secondOutput = $this->runComposerCommand(true);
$secondOutput = $this->runComposerCommand(true, $command);
$secondOutput = json_decode($this->cleanJson($secondOutput), true);
}
@ -81,7 +76,7 @@ class CheckForUpdatesHandler
foreach ($firstOutput['installed'] as &$mainPackageUpdate) {
$mainPackageUpdate['latest-minor'] = $mainPackageUpdate['latest-major'] = null;
if ($mainPackageUpdate['latest-status'] === 'update-possible') {
if (isset($mainPackageUpdate['latest-status']) && $mainPackageUpdate['latest-status'] === 'update-possible') {
$mainPackageUpdate['latest-major'] = $mainPackageUpdate['latest'];
$minorPackageUpdate = array_filter($secondOutput['installed'], function ($package) use ($mainPackageUpdate) {
@ -92,7 +87,7 @@ class CheckForUpdatesHandler
$mainPackageUpdate['latest-minor'] = $minorPackageUpdate['latest'];
}
} else {
$mainPackageUpdate['latest-minor'] = $mainPackageUpdate['latest'];
$mainPackageUpdate['latest-minor'] = $mainPackageUpdate['latest'] ?? null;
}
}
@ -113,7 +108,7 @@ class CheckForUpdatesHandler
/**
* @throws ComposerCommandFailedException
*/
protected function runComposerCommand(bool $minorOnly): string
protected function runComposerCommand(bool $minorOnly, CheckForUpdates $command): string
{
$input = [
'command' => 'outdated',
@ -125,7 +120,7 @@ class CheckForUpdatesHandler
$input['--minor-only'] = true;
}
$output = $this->composer->run(new ArrayInput($input));
$output = $this->composer->run(new ArrayInput($input), $command->task ?? null);
if ($output->getExitCode() !== 0) {
throw new ComposerCommandFailedException('', $output->getContents());

View File

@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class GlobalUpdate
class GlobalUpdate implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var \Flarum\User\User
*/
@ -22,4 +28,9 @@ class GlobalUpdate
{
$this->actor = $actor;
}
public function getOperationName(): string
{
return Task::UPDATE_GLOBAL;
}
}

View File

@ -48,7 +48,8 @@ class GlobalUpdateHandler
$command->actor->assertAdmin();
$output = $this->composer->run(
new StringInput('update --prefer-dist --no-dev -a --with-all-dependencies')
new StringInput('update --prefer-dist --no-dev -a --with-all-dependencies'),
$command->task ?? null
);
if ($output->getExitCode() !== 0) {
@ -58,7 +59,5 @@ class GlobalUpdateHandler
$this->events->dispatch(
new FlarumUpdated($command->actor, FlarumUpdated::GLOBAL)
);
return true;
}
}

View File

@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class MajorUpdate
class MajorUpdate implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var \Flarum\User\User
*/
@ -28,4 +34,9 @@ class MajorUpdate
$this->actor = $actor;
$this->dryRun = $dryRun;
}
public function getOperationName(): string
{
return Task::UPDATE_MAJOR;
}
}

View File

@ -76,19 +76,17 @@ class MajorUpdateHandler
$this->updateComposerJson($majorVersion);
$this->runCommand($command->dryRun, $majorVersion);
$this->runCommand($command, $majorVersion);
if ($command->dryRun) {
$this->composerJson->revert();
return true;
return;
}
$this->events->dispatch(
new FlarumUpdated($command->actor, FlarumUpdated::MAJOR)
);
return true;
}
/**
@ -105,7 +103,7 @@ class MajorUpdateHandler
/**
* @throws MajorUpdateFailedException
*/
protected function runCommand(bool $dryRun, string $majorVersion): void
protected function runCommand(MajorUpdate $command, string $majorVersion): void
{
$input = [
'command' => 'update',
@ -116,11 +114,11 @@ class MajorUpdateHandler
'--with-all-dependencies' => true,
];
if ($dryRun) {
if ($command->dryRun) {
$input['--dry-run'] = true;
}
$output = $this->composer->run(new ArrayInput($input));
$output = $this->composer->run(new ArrayInput($input), $command->task ?? null);
if ($output->getExitCode() !== 0) {
throw new MajorUpdateFailedException('*', $output->getContents(), $majorVersion);

View File

@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class MinorUpdate
class MinorUpdate implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var \Flarum\User\User
*/
@ -22,4 +28,9 @@ class MinorUpdate
{
$this->actor = $actor;
}
public function getOperationName(): string
{
return Task::UPDATE_MINOR;
}
}

View File

@ -61,7 +61,8 @@ class MinorUpdateHandler
$this->composerJson->require('flarum/core', $coreRequirement);
$output = $this->composer->run(
new StringInput('update --prefer-dist --no-dev -a --with-all-dependencies')
new StringInput('update --prefer-dist --no-dev -a --with-all-dependencies'),
$command->task ?? null
);
if ($output->getExitCode() !== 0) {
@ -71,7 +72,5 @@ class MinorUpdateHandler
$this->events->dispatch(
new FlarumUpdated($command->actor, FlarumUpdated::MINOR)
);
return true;
}
}

View File

@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class RemoveExtension
class RemoveExtension implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var User
*/
@ -28,4 +34,9 @@ class RemoveExtension
$this->actor = $actor;
$this->extensionId = $extensionId;
}
public function getOperationName(): string
{
return Task::EXTENSION_REMOVE;
}
}

View File

@ -55,8 +55,13 @@ class RemoveExtensionHandler
throw new ExtensionNotInstalledException($command->extensionId);
}
if (isset($command->task)) {
$command->task->package = $extension->name;
}
$output = $this->composer->run(
new StringInput("remove $extension->name")
new StringInput("remove $extension->name"),
$command->task ?? null
);
if ($output->getExitCode() !== 0) {

View File

@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class RequireExtension
class RequireExtension implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var User
*/
@ -28,4 +34,9 @@ class RequireExtension
$this->actor = $actor;
$this->package = $package;
}
public function getOperationName(): string
{
return Task::EXTENSION_INSTALL;
}
}

View File

@ -74,7 +74,8 @@ class RequireExtensionHandler
}
$output = $this->composer->run(
new StringInput("require $packageName")
new StringInput("require $packageName"),
$command->task ?? null
);
if ($output->getExitCode() !== 0) {

View File

@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class UpdateExtension
class UpdateExtension implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var User
*/
@ -28,4 +34,9 @@ class UpdateExtension
$this->actor = $actor;
$this->extensionId = $extensionId;
}
public function getOperationName(): string
{
return Task::EXTENSION_UPDATE;
}
}

View File

@ -77,7 +77,8 @@ class UpdateExtensionHandler
}
$output = $this->composer->run(
new StringInput("require $extension->name:*")
new StringInput("require $extension->name:*"),
$command->task ?? null
);
if ($output->getExitCode() !== 0) {
@ -87,7 +88,5 @@ class UpdateExtensionHandler
$this->events->dispatch(
new Updated($command->actor, $extension)
);
return true;
}
}

View File

@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class WhyNot
class WhyNot implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var User
*/
@ -34,4 +40,9 @@ class WhyNot
$this->package = $package;
$this->version = $version;
}
public function getOperationName(): string
{
return Task::WHY_NOT;
}
}

View File

@ -53,13 +53,14 @@ class WhyNotHandler
]);
$output = $this->composer->run(
new StringInput("why-not $command->package $command->version")
new StringInput("why-not $command->package $command->version"),
$command->task ?? null
);
if ($output->getExitCode() !== 0) {
throw new ComposerRequireFailedException($command->package, $output->getContents());
}
return $output->getContents();
return ['reason' => $output->getContents()];
}
}

View File

@ -12,6 +12,7 @@ namespace Flarum\PackageManager\Composer;
use Composer\Console\Application;
use Flarum\Foundation\Paths;
use Flarum\PackageManager\OutputLogger;
use Flarum\PackageManager\Task\Task;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\BufferedOutput;
@ -48,7 +49,7 @@ class ComposerAdapter
$this->output = new BufferedOutput();
}
public function run(InputInterface $input): ComposerOutput
public function run(InputInterface $input, ?Task $task = null): ComposerOutput
{
$this->application->resetComposer();
@ -58,10 +59,15 @@ class ComposerAdapter
$exitCode = $this->application->run($input, $this->output);
chdir($currDir);
$outputContents = $this->output->fetch();
$command = $input->__toString();
$output = $this->output->fetch();
$this->logger->log($input->__toString(), $outputContents, $exitCode);
if ($task) {
$task->update(compact('command', 'output'));
} else {
$this->logger->log($command, $output, $exitCode);
}
return new ComposerOutput($exitCode, $outputContents);
return new ComposerOutput($exitCode, $output);
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Job;
use Flarum\Bus\Dispatcher;
use Flarum\PackageManager\Command\BusinessCommandInterface;
use Flarum\Queue\AbstractJob;
use Throwable;
class ComposerCommandJob extends AbstractJob
{
/**
* @var BusinessCommandInterface
*/
protected $command;
/**
* @var int[]
*/
protected $phpVersion;
public function __construct(BusinessCommandInterface $command, array $phpVersion)
{
$this->command = $command;
$this->phpVersion = $phpVersion;
}
public function handle(Dispatcher $bus)
{
try {
if ([PHP_MAJOR_VERSION, PHP_MINOR_VERSION] !== [$this->phpVersion[0], $this->phpVersion[1]]) {
$webPhpVersion = implode('.', $this->phpVersion);
$sshPhpVersion = implode('.', [PHP_MAJOR_VERSION, PHP_MINOR_VERSION]);
throw new \Exception("PHP version mismatch. SSH PHP version must match web server PHP version. Found SSH (PHP $sshPhpVersion) and Web Server (PHP $webPhpVersion).");
}
$this->command->task->start();
$bus->dispatch($this->command);
$this->command->task->end(true);
} catch (Throwable $exception) {
$this->abort($exception);
}
}
public function abort(Throwable $exception)
{
if (! $this->command->task->output) {
$this->command->task->output = $exception->getMessage();
}
$this->command->task->end(false);
$this->fail($exception);
}
}

View File

@ -0,0 +1,84 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Job;
use Flarum\Bus\Dispatcher as Bus;
use Flarum\PackageManager\Command\BusinessCommandInterface;
use Flarum\PackageManager\Task\Task;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\SyncQueue;
class Dispatcher
{
/**
* @var Bus
*/
protected $bus;
/**
* @var Queue
*/
protected $queue;
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
/**
* Overrides the user setting for execution mode if set.
* Runs synchronously regardless of user setting if set true.
* Asynchronously if set false.
*
* @var bool|null
*/
protected $runSyncOverride;
public function __construct(Bus $bus, Queue $queue, SettingsRepositoryInterface $settings)
{
$this->bus = $bus;
$this->queue = $queue;
$this->settings = $settings;
}
public function sync(): self
{
$this->runSyncOverride = true;
return $this;
}
public function async(): self
{
$this->runSyncOverride = false;
return $this;
}
public function dispatch(BusinessCommandInterface $command): DispatcherResponse
{
$queueJobs = ($this->runSyncOverride === false) || ($this->runSyncOverride !== true && $this->settings->get('flarum-package-manager.queue_jobs'));
if ($queueJobs && (! $this->queue instanceof SyncQueue)) {
$task = Task::build($command->getOperationName(), $command->package ?? null);
$command->task = $task;
$this->queue->push(
new ComposerCommandJob($command, [PHP_MAJOR_VERSION, PHP_MINOR_VERSION])
);
} else {
$data = $this->bus->dispatch($command);
}
return new DispatcherResponse($queueJobs, $data ?? null);
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Job;
class DispatcherResponse
{
public $queueJobs;
public $data;
public function __construct(bool $queueJobs, ?array $data)
{
$this->queueJobs = $queueJobs;
$this->data = $data;
}
}

View File

@ -0,0 +1,93 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Task;
use Carbon\Carbon;
use Flarum\Database\AbstractModel;
/**
* @property int id
* @property int status
* @property string operation
* @property string command
* @property string package
* @property string output
* @property Carbon created_at
* @property Carbon started_at
* @property Carbon finished_at
* @property int peak_memory_used
*/
class Task extends AbstractModel
{
/**
* Statuses (@todo use an enum with php8.1).
*/
public const PENDING = 'pending';
public const RUNNING = 'running';
public const FAILURE = 'failure';
public const SUCCESS = 'success';
/**
* Operations (@todo use an enum with php8.1).
*/
public const EXTENSION_INSTALL = 'extension_install';
public const EXTENSION_REMOVE = 'extension_remove';
public const EXTENSION_UPDATE = 'extension_update';
public const UPDATE_GLOBAL = 'update_global';
public const UPDATE_MINOR = 'update_minor';
public const UPDATE_MAJOR = 'update_major';
public const UPDATE_CHECK = 'update_check';
public const WHY_NOT = 'why_not';
public const UPDATED_AT = null;
protected $table = 'package_manager_tasks';
protected $fillable = ['command', 'output'];
public $timestamps = true;
protected $casts = [
self::CREATED_AT => 'datetime',
'started_at' => 'datetime',
'finished_at' => 'datetime',
];
public static function build(string $operation, ?string $package): self
{
$task = new static;
$task->operation = $operation;
$task->package = $package;
$task->status = static::PENDING;
$task->created_at = Carbon::now();
$task->save();
return $task;
}
public function start(): bool
{
$this->status = static::RUNNING;
$this->started_at = Carbon::now();
return $this->save();
}
public function end(bool $success): bool
{
$this->status = $success ? static::SUCCESS : static::FAILURE;
$this->finished_at = Carbon::now();
$this->peak_memory_used = round(memory_get_peak_usage() / 1024);
return $this->save();
}
}

View File

@ -0,0 +1,34 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Task;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Builder;
class TaskRepository
{
/**
* @return Builder
*/
public function query()
{
return Task::query();
}
/**
* @param int $id
* @param User $actor
* @return Task
*/
public function findOrFail($id, User $actor = null): Task
{
return Task::findOrFail($id);
}
}

View File

@ -37,8 +37,6 @@ class SetupComposer
if (file_exists($composerLock)) {
unlink($composerLock);
}
echo 'composer.json created with testing packages directory.';
}
private function getConfig(): array

View File

@ -27,6 +27,6 @@ class GlobalUpdateTest extends TestCase
])
);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(201, $response->getStatusCode());
}
}

View File

@ -81,7 +81,7 @@ class MajorUpdateTest extends TestCase
}
)[0]['latest-major'];
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(201, $response->getStatusCode());
$this->assertPackageVersion('flarum/core', str_replace('v', '^', $newMinorCoreVersion));
$this->assertPackageVersion('flarum/tags', '*');
$this->assertPackageVersion('flarum/dummy-compatible-extension', '*');

View File

@ -23,7 +23,7 @@ class MinorUpdateTest extends TestCase
use DummyExtensions;
/**
* @test--
* @test
*/
public function can_update_to_next_minor_version()
{
@ -48,7 +48,7 @@ class MinorUpdateTest extends TestCase
])
);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(201, $response->getStatusCode());
$this->assertPackageVersion('flarum/tags', '*');
$this->assertPackageVersion('flarum/dummy-compatible-extension', '*');
}
@ -85,7 +85,7 @@ class MinorUpdateTest extends TestCase
/** @var LastUpdateRun $lastUpdateRun */
$lastUpdateRun = $this->app()->getContainer()->make(LastUpdateRun::class);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(201, $response->getStatusCode());
$this->assertPackageVersion('flarum/tags', '*');
$this->assertPackageVersion('flarum/dummy-extension', '*');
$this->assertEquals([

View File

@ -35,7 +35,7 @@ class RemoveExtensionTest extends TestCase
])
);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(201, $response->getStatusCode());
$this->assertExtensionNotExists('flarum-tags');
}

View File

@ -35,7 +35,7 @@ class UpdateExtensionTest extends TestCase
])
);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(201, $response->getStatusCode());
$this->assertExtensionExists('flarum-tags');
}

View File

@ -27,6 +27,12 @@ class SendNotificationWhenReplyIsPosted
public function handle(Posted $event)
{
$discussion = $event->post->discussion;
/** @var \Psr\Log\LoggerInterface $log */
$log = resolve('log');
$log->info("running subscriptions send notification when reply is posted listener. last_post_number: $discussion->last_post_number");
$this->queue->push(
new SendReplyNotification($event->post, $event->post->discussion->last_post_number)
);

View File

@ -4,6 +4,7 @@ import ExtensionData from './utils/ExtensionData';
import IHistory from '../common/IHistory';
export declare type Extension = {
id: string;
name: string;
version: string;
description?: string;
icon?: {

View File

@ -9,6 +9,7 @@ import IHistory from '../common/IHistory';
export type Extension = {
id: string;
name: string;
version: string;
description?: string;
icon?: {

View File

@ -2556,11 +2556,21 @@ prettier@^2.4.1, prettier@^2.5.1:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
pretty-bytes@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.0.0.tgz#928be2ad1f51a2e336add8ba764739f9776a8140"
integrity sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==
prettier@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
pretty-bytes@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.0.0.tgz#928be2ad1f51a2e336add8ba764739f9776a8140"
integrity sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==
prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"