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:
@@ -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",
|
||||
|
Reference in New Issue
Block a user