1
0
mirror of https://github.com/flarum/core.git synced 2025-07-31 13:40:20 +02: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:
Sami Mazouz
2022-07-24 14:02:13 +01:00
committed by GitHub
parent 75aaef7d76
commit 795a500adb
71 changed files with 1631 additions and 375 deletions

View File

@@ -1,17 +1,17 @@
{
"private": true,
"name": "@flarum/package-manager",
"version": "0.0.0",
"private": true,
"prettier": "@flarum/prettier-config",
"devDependencies": {
"prettier": "^2.5.1",
"flarum-webpack-config": "^2.0.0",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"@flarum/prettier-config": "^1.0.0",
"flarum-tsconfig": "^1.0.2",
"flarum-webpack-config": "^2.0.0",
"prettier": "^2.5.1",
"typescript": "^4.5.4",
"typescript-coverage-report": "^0.6.1"
"typescript-coverage-report": "^0.6.1",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1"
},
"scripts": {
"dev": "webpack --mode development --watch",
@@ -21,9 +21,11 @@
"ci": "yarn install --immutable --immutable-cache",
"analyze": "cross-env ANALYZER=true yarn run build",
"clean-typings": "npx rimraf dist-typings && mkdir dist-typings",
"build-typings": "yarn run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && yarn run post-build-typings",
"build-typings": "yarn run clean-typings && tsc && [ -e src/@types ] && cp -r src/@types dist-typings/@types",
"check-typings": "tsc --noEmit --emitDeclarationOnly false",
"check-typings-coverage": "typescript-coverage-report",
"post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'"
"check-typings-coverage": "typescript-coverage-report"
},
"dependencies": {
"pretty-bytes": "^6.0.0"
}
}

View File

@@ -0,0 +1,34 @@
import app from 'flarum/admin/app';
import Component from 'flarum/common/Component';
import Alert from 'flarum/common/components/Alert';
import Installer from './Installer';
import Updater from './Updater';
export default class ControlSection extends Component {
view() {
return (
<div className="ExtensionPage-permissions PackageManager-controlSection">
<div className="ExtensionPage-permissions-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-package-manager.admin.sections.control.title')}</h2>
</div>
</div>
<div className="container">
{app.data['flarum-package-manager.writable_dirs'] ? (
<>
<Installer />
<Updater />
</>
) : (
<div className="Form-group">
<Alert type="warning" dismissible={false}>
{app.translator.trans('flarum-package-manager.admin.file_permissions')}
</Alert>
</div>
)}
</div>
</div>
);
}
}

View File

@@ -1,20 +1,15 @@
import Mithril from 'mithril';
import type Mithril from 'mithril';
import app from 'flarum/admin/app';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import classList from 'flarum/common/utils/classList';
import icon from 'flarum/common/helpers/icon';
import Tooltip from 'flarum/common/components/Tooltip';
import Button from 'flarum/common/components/Button';
import { Extension as BaseExtension } from 'flarum/admin/AdminApplication';
import { Extension } from 'flarum/admin/AdminApplication';
import { UpdatedPackage } from './Updater';
import WhyNotModal from './WhyNotModal';
/*
* @todo fix in core
*/
export type Extension = BaseExtension & {
name: string;
};
import Label from './Label';
export interface ExtensionItemAttrs extends ComponentAttrs {
extension: Extension;
@@ -29,6 +24,7 @@ export interface ExtensionItemAttrs extends ComponentAttrs {
export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionItemAttrs> extends Component<Attrs> {
view(vnode: Mithril.Vnode<Attrs, this>): Mithril.Children {
const { extension, updates, onClickUpdate, whyNotWarning, isCore, isDanger } = this.attrs;
const latestVersion = updates['latest-minor'] ?? (updates['latest-major'] && !isCore ? updates['latest-major'] : null);
return (
<div
@@ -45,15 +41,10 @@ export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionI
<div className="PackageManager-extension-name">{extension.extra['flarum-extension'].title}</div>
<div className="PackageManager-extension-version">
<span className="PackageManager-extension-version-current">{this.version(extension.version)}</span>
{updates['latest-minor'] ? (
<span className="PackageManager-extension-version-latest PackageManager-extension-version-latest--minor">
{this.version(updates['latest-minor']!)}
</span>
) : null}
{updates['latest-major'] && !isCore ? (
<span className="PackageManager-extension-version-latest PackageManager-extension-version-latest--major">
{this.version(updates['latest-major']!)}
</span>
{latestVersion ? (
<Label className="PackageManager-extension-version-latest" type={updates['latest-minor'] ? 'success' : 'warning'}>
{this.version(latestVersion)}
</Label>
) : null}
</div>
</div>
@@ -83,7 +74,7 @@ export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionI
);
}
private version(v: string): string {
version(v: string): string {
return 'v' + v.replace('v', '');
}
}

View File

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

View File

@@ -0,0 +1,19 @@
import type Mithril from 'mithril';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import classList from 'flarum/common/utils/classList';
interface LabelAttrs extends ComponentAttrs {
type: 'success' | 'error' | 'neutral' | 'warning';
}
export default class Label extends Component<LabelAttrs> {
view(vnode: Mithril.Vnode<LabelAttrs, this>) {
const { className, type, ...attrs } = this.attrs;
return (
<span className={classList(['Label', `Label--${this.attrs.type}`, className])} {...attrs}>
{vnode.children}
</span>
);
}
}

View File

@@ -1,15 +1,18 @@
import type Mithril from 'mithril';
import app from 'flarum/admin/app';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import Mithril from 'mithril';
import Button from 'flarum/common/components/Button';
import Tooltip from 'flarum/common/components/Tooltip';
import { UpdatedPackage, UpdateState } from './Updater';
import LoadingModal from 'flarum/admin/components/LoadingModal';
import errorHandler from '../utils/errorHandler';
import Alert from 'flarum/common/components/Alert';
import WhyNotModal from './WhyNotModal';
import RequestError from 'flarum/common/utils/RequestError';
import ExtensionItem, { Extension } from './ExtensionItem';
import { UpdatedPackage, UpdateState } from './Updater';
import errorHandler from '../utils/errorHandler';
import WhyNotModal from './WhyNotModal';
import ExtensionItem from './ExtensionItem';
import { AsyncBackendResponse } from '../shims';
import jumpToQueue from '../utils/jumpToQueue';
interface MajorUpdaterAttrs extends ComponentAttrs {
coreUpdate: UpdatedPackage;
@@ -84,7 +87,7 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
app.modal.show(LoadingModal);
app
.request({
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/major-update`,
body: {
@@ -92,9 +95,13 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
},
errorHandler,
})
.then(() => {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.update_successful'));
window.location.reload();
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.update_successful'));
window.location.reload();
}
})
.catch((e: RequestError) => {
app.modal.close();

View File

@@ -0,0 +1,40 @@
import app from 'flarum/admin/app';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import Button from 'flarum/common/components/Button';
import QueueState from '../states/QueueState';
interface PaginationAttrs extends ComponentAttrs {
list: QueueState;
}
/**
* @todo make it abstract in core for reusability.
*/
export default class Pagination extends Component<PaginationAttrs> {
view() {
return (
<nav class="Pagination UserListPage-gridPagination">
<Button
disabled={!this.attrs.list.hasPrev()}
title={app.translator.trans('core.admin.users.pagination.back_button')}
onclick={() => this.attrs.list.prev()}
icon="fas fa-chevron-left"
className="Button Button--icon UserListPage-backBtn"
/>
<span class="UserListPage-pageNumber">
{app.translator.trans('core.admin.users.pagination.page_counter', {
current: this.attrs.list.pageNumber() + 1,
total: this.attrs.list.getTotalPages(),
})}
</span>
<Button
disabled={!this.attrs.list.hasNext()}
title={app.translator.trans('core.admin.users.pagination.next_button')}
onclick={() => this.attrs.list.next()}
icon="fas fa-chevron-right"
className="Button Button--icon UserListPage-nextBtn"
/>
</nav>
);
}
}

View File

@@ -0,0 +1,215 @@
import type Mithril from 'mithril';
import app from 'flarum/admin/app';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import Button from 'flarum/common/components/Button';
import Tooltip from 'flarum/common/components/Tooltip';
import { Extension } from 'flarum/admin/AdminApplication';
import icon from 'flarum/common/helpers/icon';
import ItemList from 'flarum/common/utils/ItemList';
import extractText from 'flarum/common/utils/extractText';
import Label from './Label';
import TaskOutputModal from './TaskOutputModal';
import humanDuration from '../utils/humanDuration';
import Task, { TaskOperations } from '../models/Task';
import Pagination from './Pagination';
interface QueueTableColumn extends ComponentAttrs {
label: string;
content: (task: Task) => Mithril.Children;
}
export default class QueueSection extends Component<{}> {
oninit(vnode: Mithril.Vnode<{}, this>) {
super.oninit(vnode);
app.packageManagerQueue.load();
}
view() {
return (
<section id="PackageManager-queueSection" className="ExtensionPage-permissions PackageManager-queueSection">
<div className="ExtensionPage-permissions-header PackageManager-queueSection-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-package-manager.admin.sections.queue.title')}</h2>
<Button
className="Button Button--icon"
icon="fas fa-sync-alt"
onclick={() => app.packageManagerQueue.load()}
aria-label={app.translator.trans('flarum-package-manager.admin.sections.queue.refresh')}
/>
</div>
</div>
<div className="container">{this.queueTable()}</div>
</section>
);
}
columns() {
const items = new ItemList<QueueTableColumn>();
items.add(
'operation',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.operation')),
content: (task) => (
<div className="PackageManager-queueTable-operation">
<span className="PackageManager-queueTable-operation-icon">{this.operationIcon(task.operation())}</span>
<span className="PackageManager-queueTable-operation-name">
{app.translator.trans(`flarum-package-manager.admin.sections.queue.operations.${task.operation()}`)}
</span>
</div>
),
},
80
);
items.add(
'package',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.package')),
content: (task) => {
const extension: Extension | null = app.data.extensions[task.package()?.replace(/(\/flarum-|\/flarum-ext-|\/)/g, '-')];
return extension ? (
<div className="PackageManager-queueTable-package">
<div className="PackageManager-queueTable-package-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''}
</div>
<div className="PackageManager-queueTable-package-details">
<span className="PackageManager-queueTable-package-title">{extension.extra['flarum-extension'].title}</span>
<span className="PackageManager-queueTable-package-name">{task.package()}</span>
</div>
</div>
) : (
task.package()
);
},
},
75
);
items.add(
'status',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.status')),
content: (task) => (
<Label
className="PackageManager-queueTable-status"
type={{ running: 'neutral', failure: 'error', pending: 'warning', success: 'success' }[task.status()]}
>
{app.translator.trans(`flarum-package-manager.admin.sections.queue.statuses.${task.status()}`)}
</Label>
),
},
70
);
items.add(
'elapsedTime',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.elapsed_time')),
content: (task) =>
!task.startedAt() ? (
app.translator.trans('flarum-package-manager.admin.sections.queue.task_just_started')
) : (
<Tooltip text={`${dayjs(task.startedAt()).format('LL LTS')} ${dayjs(task.finishedAt()).format('LL LTS')}`}>
<span>{humanDuration(task.startedAt(), task.finishedAt())}</span>
</Tooltip>
),
},
65
);
items.add(
'memoryUsed',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.peak_memory_used')),
content: (task) => <span>{task.peakMemoryUsed()}</span>,
},
60
);
items.add(
'details',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.details')),
content: (task) => (
<Button
className="Button Button--icon Table-controls-item"
icon="fas fa-file-alt"
aria-label={app.translator.trans('flarum-package-manager.admin.sections.queue.columns.details')}
// @todo fix in core
// @ts-ignore
onclick={() => app.modal.show(TaskOutputModal, { task })}
/>
),
className: 'Table-controls',
},
55
);
return items;
}
queueTable() {
const tasks = app.packageManagerQueue.getItems();
if (!tasks) {
return <LoadingIndicator />;
}
if (tasks && !tasks.length) {
return <h3 className="ExtensionPage-subHeader">{app.translator.trans('flarum-package-manager.admin.sections.queue.none')}</h3>;
}
const columns = this.columns();
return (
<>
<table className="Table PackageManager-queueTable">
<thead>
<tr>
{columns.toArray().map((item, index) => (
<th key={index}>{item.label}</th>
))}
</tr>
</thead>
<tbody>
{tasks.map((task, index) => (
<tr key={index}>
{columns.toArray().map((item, index) => {
const { label, content, ...attrs } = item;
return (
<td key={index} {...attrs}>
{content(task)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
<Pagination list={app.packageManagerQueue} />
</>
);
}
operationIcon(operation: TaskOperations): Mithril.Children {
return icon(
{
update_check: 'fas fa-sync-alt',
update_major: 'fas fa-play',
update_minor: 'fas fa-play',
update_global: 'fas fa-play',
extension_install: 'fas fa-download',
extension_remove: 'fas fa-times',
extension_update: 'fas fa-arrow-alt-circle-up',
why_not: 'fas fa-exclamation-circle',
}[operation]
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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