mirror of
https://github.com/flarum/framework.git
synced 2025-03-24 23:55:16 +08:00
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:
parent
75aaef7d76
commit
795a500adb
@ -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),
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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', '');
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
19
extensions/package-manager/js/src/admin/components/Label.tsx
Normal file
19
extensions/package-manager/js/src/admin/components/Label.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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]
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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();
|
||||
|
50
extensions/package-manager/js/src/admin/models/Task.ts
Normal file
50
extensions/package-manager/js/src/admin/models/Task.ts
Normal 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);
|
||||
}
|
||||
}
|
11
extensions/package-manager/js/src/admin/shims.d.ts
vendored
Normal file
11
extensions/package-manager/js/src/admin/shims.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
65
extensions/package-manager/js/src/admin/states/QueueState.ts
Normal file
65
extensions/package-manager/js/src/admin/states/QueueState.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
13
extensions/package-manager/js/src/admin/utils/jumpToQueue.ts
Normal file
13
extensions/package-manager/js/src/admin/utils/jumpToQueue.ts
Normal 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);
|
||||
}
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
133
extensions/package-manager/less/admin/ControlSection.less
Normal file
133
extensions/package-manager/less/admin/ControlSection.less
Normal 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;
|
||||
}
|
||||
}
|
41
extensions/package-manager/less/admin/Label.less
Normal file
41
extensions/package-manager/less/admin/Label.less
Normal 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);
|
||||
}
|
||||
}
|
57
extensions/package-manager/less/admin/QueueSection.less
Normal file
57
extensions/package-manager/less/admin/QueueSection.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
.TaskOutputModal-data-output {
|
||||
height: auto;
|
||||
overflow: auto;
|
||||
max-height: 40vh;
|
||||
}
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
);
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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']]]);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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()];
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
65
extensions/package-manager/src/Job/ComposerCommandJob.php
Normal file
65
extensions/package-manager/src/Job/ComposerCommandJob.php
Normal 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);
|
||||
}
|
||||
}
|
84
extensions/package-manager/src/Job/Dispatcher.php
Normal file
84
extensions/package-manager/src/Job/Dispatcher.php
Normal 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);
|
||||
}
|
||||
}
|
23
extensions/package-manager/src/Job/DispatcherResponse.php
Normal file
23
extensions/package-manager/src/Job/DispatcherResponse.php
Normal 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;
|
||||
}
|
||||
}
|
93
extensions/package-manager/src/Task/Task.php
Normal file
93
extensions/package-manager/src/Task/Task.php
Normal 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();
|
||||
}
|
||||
}
|
34
extensions/package-manager/src/Task/TaskRepository.php
Normal file
34
extensions/package-manager/src/Task/TaskRepository.php
Normal 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);
|
||||
}
|
||||
}
|
@ -37,8 +37,6 @@ class SetupComposer
|
||||
if (file_exists($composerLock)) {
|
||||
unlink($composerLock);
|
||||
}
|
||||
|
||||
echo 'composer.json created with testing packages directory.';
|
||||
}
|
||||
|
||||
private function getConfig(): array
|
||||
|
@ -27,6 +27,6 @@ class GlobalUpdateTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
}
|
||||
}
|
||||
|
@ -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', '*');
|
||||
|
@ -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([
|
||||
|
@ -35,7 +35,7 @@ class RemoveExtensionTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
$this->assertExtensionNotExists('flarum-tags');
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ class UpdateExtensionTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
$this->assertExtensionExists('flarum-tags');
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
);
|
||||
|
1
framework/core/js/dist-typings/admin/AdminApplication.d.ts
generated
vendored
1
framework/core/js/dist-typings/admin/AdminApplication.d.ts
generated
vendored
@ -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?: {
|
||||
|
@ -9,6 +9,7 @@ import IHistory from '../common/IHistory';
|
||||
|
||||
export type Extension = {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
icon?: {
|
||||
|
10
yarn.lock
10
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user