1
0
mirror of https://github.com/flarum/core.git synced 2025-08-20 15:21:49 +02:00

feat(em): port extension manager from 1.0 (#3959)

* feat(em): port extension manager from 1.0

* Apply fixes from StyleCI

* chore: phpstan

---------

Co-authored-by: StyleCI Bot <bot@styleci.io>
This commit is contained in:
Sami Mazouz
2024-01-22 18:58:08 +01:00
committed by GitHub
parent 8f29b7af82
commit 3fbe05fd18
114 changed files with 2003 additions and 636 deletions

View File

@@ -0,0 +1,88 @@
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import Mithril from 'mithril';
import app from 'flarum/admin/app';
import Select from 'flarum/common/components/Select';
import Stream from 'flarum/common/utils/Stream';
import Button from 'flarum/common/components/Button';
import extractText from 'flarum/common/utils/extractText';
export interface IAuthMethodModalAttrs extends IInternalModalAttrs {
onsubmit: (type: string, host: string, token: string) => void;
type?: string;
host?: string;
token?: string;
}
export default class AuthMethodModal<CustomAttrs extends IAuthMethodModalAttrs = IAuthMethodModalAttrs> extends Modal<CustomAttrs> {
protected type!: Stream<string>;
protected host!: Stream<string>;
protected token!: Stream<string>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.type = Stream(this.attrs.type || 'bearer');
this.host = Stream(this.attrs.host || '');
this.token = Stream(this.attrs.token || '');
}
className(): string {
return 'AuthMethodModal Modal--small';
}
title(): Mithril.Children {
const context = this.attrs.host ? 'edit' : 'add';
return app.translator.trans(`flarum-extension-manager.admin.auth_config.${context}_label`);
}
content(): Mithril.Children {
const types = {
'github-oauth': app.translator.trans('flarum-extension-manager.admin.auth_config.types.github-oauth'),
'gitlab-oauth': app.translator.trans('flarum-extension-manager.admin.auth_config.types.gitlab-oauth'),
'gitlab-token': app.translator.trans('flarum-extension-manager.admin.auth_config.types.gitlab-token'),
bearer: app.translator.trans('flarum-extension-manager.admin.auth_config.types.bearer'),
};
return (
<div className="Modal-body">
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.type_label')}</label>
<Select options={types} value={this.type()} onchange={this.type} />
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.host_label')}</label>
<input
className="FormControl"
bidi={this.host}
placeholder={app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.host_placeholder')}
/>
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.token_label')}</label>
<textarea
className="FormControl"
oninput={(e: InputEvent) => this.token((e.target as HTMLTextAreaElement).value)}
rows="6"
placeholder={
this.token().startsWith('unchanged:')
? extractText(app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.unchanged_token_placeholder'))
: ''
}
>
{this.token().startsWith('unchanged:') ? '' : this.token()}
</textarea>
</div>
<div className="Form-group">
<Button className="Button Button--primary" onclick={this.submit.bind(this)}>
{app.translator.trans('flarum-extension-manager.admin.auth_config.add_modal.submit_button')}
</Button>
</div>
</div>
);
}
submit() {
this.attrs.onsubmit(this.type(), this.host(), this.token());
this.hide();
}
}

View File

@@ -0,0 +1,120 @@
import app from 'flarum/admin/app';
import type Mithril from 'mithril';
import ConfigureJson, { IConfigureJson } from './ConfigureJson';
import Button from 'flarum/common/components/Button';
import AuthMethodModal from './AuthMethodModal';
import extractText from 'flarum/common/utils/extractText';
export default class ConfigureAuth extends ConfigureJson<IConfigureJson> {
protected type = 'auth';
title(): Mithril.Children {
return app.translator.trans('flarum-extension-manager.admin.auth_config.title');
}
className(): string {
return 'ConfigureAuth';
}
content(): Mithril.Children {
const authSettings = Object.keys(this.settings);
const hasAuthSettings =
authSettings.length &&
authSettings.every((type) => {
const data = this.settings[type]();
return Array.isArray(data) ? data.length : Object.keys(data).length;
});
return (
<div className="ExtensionManager-SettingsGroups-content">
{hasAuthSettings ? (
authSettings.map((type) => {
const hosts = this.settings[type]();
return (
<div className="Form-group">
<label>{app.translator.trans(`flarum-extension-manager.admin.auth_config.types.${type}`)}</label>
<div className="ConfigureAuth-hosts">
{Object.keys(hosts).map((host) => {
const data = hosts[host] as string | Record<string, string>;
return (
<div className="ButtonGroup ButtonGroup--full">
<Button
className="Button"
icon="fas fa-key"
onclick={() =>
app.modal.show(AuthMethodModal, {
type,
host,
token: data,
onsubmit: this.onchange.bind(this, host),
})
}
>
{host}
</Button>
<Button
className="Button Button--icon"
icon="fas fa-trash"
aria-label={app.translator.trans('flarum-extension-manager.admin.auth_config.delete_label')}
onclick={() => {
if (confirm(extractText(app.translator.trans('flarum-extension-manager.admin.auth_config.delete_confirmation')))) {
const newType = { ...this.setting(type)() };
delete newType[host];
if (Object.keys(newType).length) {
this.setting(type)(newType);
} else {
delete this.settings[type];
}
}
}}
/>
</div>
);
})}
</div>
</div>
);
})
) : (
<span className="helpText">{app.translator.trans('flarum-extension-manager.admin.auth_config.no_auth_methods_configured')}</span>
)}
</div>
);
}
submitButton(): Mithril.Children[] {
const items = super.submitButton();
items.push(
<Button
className="Button"
loading={this.loading}
onclick={() =>
app.modal.show(AuthMethodModal, {
onsubmit: this.onchange.bind(this, null),
})
}
>
{app.translator.trans('flarum-extension-manager.admin.auth_config.add_label')}
</Button>
);
return items;
}
onchange(oldHost: string | null, type: string, host: string, token: string) {
const data = { ...this.setting(type)() };
if (oldHost) {
delete data[oldHost];
}
data[host] = token;
this.setting(type)(data);
}
}

View File

@@ -0,0 +1,115 @@
import app from 'flarum/admin/app';
import type Mithril from 'mithril';
import ConfigureJson, { type IConfigureJson } from './ConfigureJson';
import Button from 'flarum/common/components/Button';
import extractText from 'flarum/common/utils/extractText';
import RepositoryModal from './RepositoryModal';
export type Repository = {
type: 'composer' | 'vcs' | 'path';
url: string;
};
export default class ConfigureComposer extends ConfigureJson<IConfigureJson> {
protected type = 'composer';
title(): Mithril.Children {
return app.translator.trans('flarum-extension-manager.admin.composer.title');
}
className(): string {
return 'ConfigureComposer';
}
content(): Mithril.Children {
return (
<div className="Form ExtensionManager-SettingsGroups-content">
{this.attrs.buildSettingComponent.call(this, {
setting: 'minimum-stability',
label: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.label'),
help: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.help'),
type: 'select',
options: {
stable: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.stable'),
RC: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.rc'),
beta: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.beta'),
alpha: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.alpha'),
dev: app.translator.trans('flarum-extension-manager.admin.composer.minimum_stability.options.dev'),
},
})}
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.label')}</label>
<div className="helpText">{app.translator.trans('flarum-extension-manager.admin.composer.repositories.help')}</div>
<div className="ConfigureComposer-repositories">
{Object.keys(this.setting('repositories')() || {}).map((name) => {
const repository = this.setting('repositories')()[name] as Repository;
return (
<div className="ButtonGroup ButtonGroup--full">
<Button
className="Button"
icon={
{
composer: 'fas fa-cubes',
vcs: 'fas fa-code-branch',
path: 'fas fa-folder',
}[repository.type]
}
onclick={() =>
app.modal.show(RepositoryModal, {
name,
repository,
onsubmit: (repository: Repository, newName: string) => {
const repositories = this.setting('repositories')();
delete repositories[name];
this.setting('repositories')(repositories);
this.onchange(repository, newName);
},
})
}
>
{name} ({repository.type})
</Button>
<Button
className="Button Button--icon"
icon="fas fa-trash"
aria-label={app.translator.trans('flarum-extension-manager.admin.composer.delete_repository_label')}
onclick={() => {
if (confirm(extractText(app.translator.trans('flarum-extension-manager.admin.composer.delete_repository_confirmation')))) {
const repositories = { ...this.setting('repositories')() };
delete repositories[name];
this.setting('repositories')(repositories);
}
}}
/>
</div>
);
})}
</div>
</div>
</div>
);
}
submitButton(): Mithril.Children[] {
const items = super.submitButton();
items.push(
<Button className="Button" onclick={() => app.modal.show(RepositoryModal, { onsubmit: this.onchange.bind(this) })}>
{app.translator.trans('flarum-extension-manager.admin.composer.add_repository_label')}
</Button>
);
return items;
}
onchange(repository: Repository, name: string) {
this.setting('repositories')({
...this.setting('repositories')(),
[name]: repository,
});
}
}

View File

@@ -0,0 +1,94 @@
import app from 'flarum/admin/app';
import type Mithril from 'mithril';
import Component, { type ComponentAttrs } from 'flarum/common/Component';
import { CommonSettingsItemOptions, type SettingsComponentOptions } from '@flarum/core/src/admin/components/AdminPage';
import AdminPage from 'flarum/admin/components/AdminPage';
import type ItemList from 'flarum/common/utils/ItemList';
import Stream from 'flarum/common/utils/Stream';
import Button from 'flarum/common/components/Button';
import classList from 'flarum/common/utils/classList';
export interface IConfigureJson extends ComponentAttrs {
buildSettingComponent: (entry: ((this: this) => Mithril.Children) | SettingsComponentOptions) => Mithril.Children;
}
export default abstract class ConfigureJson<CustomAttrs extends IConfigureJson = IConfigureJson> extends Component<CustomAttrs> {
protected settings: Record<string, Stream<any>> = {};
protected initialSettings: Record<string, any> | null = null;
protected loading: boolean = false;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.submit(true);
}
protected abstract type: string;
abstract title(): Mithril.Children;
abstract content(): Mithril.Children;
className(): string {
return '';
}
view(): Mithril.Children {
return (
<div className={classList('FormSection', this.className())}>
<label>{this.title()}</label>
{this.content()}
<div className="Form-group Form-controls">{this.submitButton()}</div>
</div>
);
}
submitButton(): Mithril.Children[] {
return [
<Button className="Button Button--primary" loading={this.loading} onclick={() => this.submit(false)} disabled={!this.isDirty()}>
{app.translator.trans('core.admin.settings.submit_button')}
</Button>,
];
}
customSettingComponents(): ItemList<(attributes: CommonSettingsItemOptions) => Mithril.Children> {
return AdminPage.prototype.customSettingComponents();
}
setting(key: string) {
return this.settings[key] ?? (this.settings[key] = Stream());
}
submit(readOnly: boolean) {
this.loading = true;
const configuration: any = {};
Object.keys(this.settings).forEach((key) => {
configuration[key] = this.settings[key]();
});
app
.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/extension-manager/composer',
body: {
type: this.type,
data: readOnly ? null : configuration,
},
})
.then(({ data }: any) => {
Object.keys(data).forEach((key) => {
this.settings[key] = Stream(data[key]);
});
this.initialSettings = Array.isArray(data) ? {} : data;
})
.finally(() => {
this.loading = false;
m.redraw();
});
}
isDirty() {
return JSON.stringify(this.initialSettings) !== JSON.stringify(this.settings);
}
}

View File

@@ -6,6 +6,7 @@ import { ComponentAttrs } from 'flarum/common/Component';
import Installer from './Installer';
import Updater from './Updater';
import Mithril from 'mithril';
import Form from 'flarum/common/components/Form';
export default class ControlSection extends Component<ComponentAttrs> {
oninit(vnode: Mithril.Vnode<ComponentAttrs, this>) {
@@ -14,22 +15,22 @@ export default class ControlSection extends Component<ComponentAttrs> {
view() {
return (
<div className="ExtensionPage-permissions PackageManager-controlSection">
<div className="ExtensionPage-permissions ExtensionManager-controlSection">
<div className="ExtensionPage-permissions-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-package-manager.admin.sections.control.title')}</h2>
<h2 className="ExtensionTitle">{app.translator.trans('flarum-extension-manager.admin.sections.control.title')}</h2>
</div>
</div>
<div className="container">
{app.data['flarum-package-manager.writable_dirs'] ? (
<>
{app.data['flarum-extension-manager.writable_dirs'] ? (
<Form>
<Installer />
<Updater />
</>
</Form>
) : (
<div className="Form-group">
<Alert type="warning" dismissible={false}>
{app.translator.trans('flarum-package-manager.admin.file_permissions')}
{app.translator.trans('flarum-extension-manager.admin.file_permissions')}
</Alert>
</div>
)}

View File

@@ -10,11 +10,17 @@ import { Extension } from 'flarum/admin/AdminApplication';
import { UpdatedPackage } from '../states/ControlSectionState';
import WhyNotModal from './WhyNotModal';
import Label from './Label';
import Dropdown from 'flarum/common/components/Dropdown';
export interface ExtensionItemAttrs extends ComponentAttrs {
extension: Extension;
updates: UpdatedPackage;
onClickUpdate: CallableFunction;
onClickUpdate:
| CallableFunction
| {
soft: CallableFunction;
hard: CallableFunction;
};
whyNotWarning?: boolean;
isCore?: boolean;
updatable?: boolean;
@@ -29,43 +35,56 @@ export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionI
return (
<div
className={classList({
'PackageManager-extension': true,
'PackageManager-extension--core': isCore,
'PackageManager-extension--danger': isDanger,
'ExtensionManager-extension': true,
'ExtensionManager-extension--core': isCore,
'ExtensionManager-extension--danger': isDanger,
})}
>
<div className="PackageManager-extension-icon ExtensionIcon" style={extension.icon}>
<div className="ExtensionManager-extension-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? <Icon name={extension.icon.name} /> : ''}
</div>
<div className="PackageManager-extension-info">
<div className="PackageManager-extension-name">{extension.extra['flarum-extension'].title}</div>
<div className="PackageManager-extension-version">
<span className="PackageManager-extension-version-current">{this.version(updates['version'])}</span>
<div className="ExtensionManager-extension-info">
<div className="ExtensionManager-extension-name">{extension.extra['flarum-extension'].title}</div>
<div className="ExtensionManager-extension-version">
<span className="ExtensionManager-extension-version-current">{this.version(updates['version'])}</span>
{latestVersion ? (
<Label className="PackageManager-extension-version-latest" type={updates['latest-minor'] ? 'success' : 'warning'}>
<Label className="ExtensionManager-extension-version-latest" type={updates['latest-minor'] ? 'success' : 'warning'}>
{this.version(latestVersion)}
</Label>
) : null}
</div>
</div>
<div className="PackageManager-extension-controls">
{onClickUpdate ? (
<Tooltip text={app.translator.trans('flarum-package-manager.admin.extensions.update')}>
<div className="ExtensionManager-extension-controls">
{onClickUpdate && typeof onClickUpdate === 'function' ? (
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.extensions.update')}>
<Button
icon="fas fa-arrow-alt-circle-up"
className="Button Button--icon Button--flat"
onclick={onClickUpdate}
aria-label={app.translator.trans('flarum-package-manager.admin.extensions.update')}
aria-label={app.translator.trans('flarum-extension-manager.admin.extensions.update')}
/>
</Tooltip>
) : onClickUpdate ? (
<Dropdown
buttonClassName="Button Button--icon Button--flat"
icon="fas fa-arrow-alt-circle-up"
label={app.translator.trans('flarum-extension-manager.admin.extensions.update')}
>
<Button icon="fas fa-arrow-alt-circle-up" className="Button" onclick={onClickUpdate.soft}>
{app.translator.trans('flarum-extension-manager.admin.extensions.update_soft_label')}
</Button>
<Button icon="fas fa-arrow-alt-circle-up" className="Button" onclick={onClickUpdate.hard} disabled={!updates['direct-dependency']}>
{app.translator.trans('flarum-extension-manager.admin.extensions.update_hard_label')}
</Button>
</Dropdown>
) : null}
{whyNotWarning ? (
<Tooltip text={app.translator.trans('flarum-package-manager.admin.extensions.check_why_it_failed_updating')}>
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.extensions.check_why_it_failed_updating')}>
<Button
icon="fas fa-exclamation-circle"
className="Button Button--icon Button--flat Button--danger"
onclick={() => app.modal.show(WhyNotModal, { package: extension.name })}
aria-label={app.translator.trans('flarum-package-manager.admin.extensions.check_why_it_failed_updating')}
aria-label={app.translator.trans('flarum-extension-manager.admin.extensions.check_why_it_failed_updating')}
/>
</Tooltip>
) : null}
@@ -75,6 +94,6 @@ export default class ExtensionItem<Attrs extends ExtensionItemAttrs = ExtensionI
}
version(v: string): string {
return 'v' + v.replace('v', '');
return v.charAt(0) === 'v' ? v.substring(1) : v;
}
}

View File

@@ -3,11 +3,6 @@ import app from 'flarum/admin/app';
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';
import jumpToQueue from '../utils/jumpToQueue';
import { AsyncBackendResponse } from '../shims';
export interface InstallerAttrs extends ComponentAttrs {}
@@ -24,23 +19,25 @@ export default class Installer extends Component<InstallerAttrs> {
view(): Mithril.Children {
return (
<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', {
<div className="Form-group ExtensionManager-installer">
<label htmlFor="install-extension">{app.translator.trans('flarum-extension-manager.admin.extensions.install')}</label>
<div className="helpText">
{app.translator.trans('flarum-extension-manager.admin.extensions.install_help', {
extiverse: <a href="https://extiverse.com">extiverse.com</a>,
semantic_link: <a href="https://devhints.io/semver" />,
code: <code />,
})}
</p>
</div>
<div className="FormControl-container">
<input className="FormControl" id="install-extension" placeholder="vendor/package-name" bidi={this.packageName} />
<Button
className="Button"
icon="fas fa-download"
onclick={this.onsubmit.bind(this)}
loading={app.packageManager.control.isLoading('extension-install')}
disabled={app.packageManager.control.isLoadingOtherThan('extension-install')}
loading={app.extensionManager.control.isLoading('extension-install')}
disabled={app.extensionManager.control.hasOperationRunning()}
>
{app.translator.trans('flarum-package-manager.admin.extensions.proceed')}
{app.translator.trans('flarum-extension-manager.admin.extensions.proceed')}
</Button>
</div>
</div>
@@ -54,35 +51,6 @@ export default class Installer extends Component<InstallerAttrs> {
}
onsubmit(): void {
app.packageManager.control.setLoading('extension-install');
app.modal.show(LoadingModal);
app
.request<AsyncBackendResponse & { id: number }>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/extensions`,
body: {
data: this.data(),
},
})
.then((response) => {
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();
}
})
.catch(errorHandler)
.finally(() => {
app.packageManager.control.setLoading(null);
app.modal.close();
m.redraw();
});
app.extensionManager.control.requirePackage(this.data());
}
}

View File

@@ -3,16 +3,12 @@ import app from 'flarum/admin/app';
import Component, { ComponentAttrs } from 'flarum/common/Component';
import Button from 'flarum/common/components/Button';
import Tooltip from 'flarum/common/components/Tooltip';
import LoadingModal from 'flarum/admin/components/LoadingModal';
import Alert from 'flarum/common/components/Alert';
import RequestError from 'flarum/common/utils/RequestError';
import { UpdatedPackage, UpdateState } from '../states/ControlSectionState';
import errorHandler from '../utils/errorHandler';
import WhyNotModal from './WhyNotModal';
import ExtensionItem from './ExtensionItem';
import { AsyncBackendResponse } from '../shims';
import jumpToQueue from '../utils/jumpToQueue';
import classList from 'flarum/common/utils/classList';
export interface MajorUpdaterAttrs extends ComponentAttrs {
coreUpdate: UpdatedPackage;
@@ -33,32 +29,39 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
view(): Mithril.Children {
// @todo move Form-group--danger class to core for reuse
return (
<div className="Form-group Form-group--danger PackageManager-majorUpdate">
<img alt="flarum logo" src={app.forum.attribute('baseUrl') + '/assets/extensions/flarum-package-manager/flarum.svg'} />
<label>{app.translator.trans('flarum-package-manager.admin.major_updater.title', { version: this.attrs.coreUpdate['latest-major'] })}</label>
<p className="helpText">{app.translator.trans('flarum-package-manager.admin.major_updater.description')}</p>
<div className="PackageManager-updaterControls">
<Tooltip text={app.translator.trans('flarum-package-manager.admin.major_updater.dry_run_help')}>
<div
className={classList('Form-group Form-group--danger ExtensionManager-majorUpdate', {
'ExtensionManager-majorUpdate--failed': this.updateState.status === 'failure',
'ExtensionManager-majorUpdate--incompatibleExtensions': this.updateState.incompatibleExtensions.length,
})}
>
<img alt="flarum logo" src={app.forum.attribute('baseUrl') + '/assets/extensions/flarum-extension-manager/flarum.svg'} />
<label>
{app.translator.trans('flarum-extension-manager.admin.major_updater.title', { version: this.attrs.coreUpdate['latest-major'] })}
</label>
<p className="helpText">{app.translator.trans('flarum-extension-manager.admin.major_updater.description')}</p>
<div className="ExtensionManager-updaterControls">
<Tooltip text={app.translator.trans('flarum-extension-manager.admin.major_updater.dry_run_help')}>
<Button
className="Button"
icon="fas fa-vial"
onclick={this.update.bind(this, true)}
disabled={app.packageManager.control.isLoadingOtherThan('major-update-dry-run')}
disabled={app.extensionManager.control.hasOperationRunning()}
>
{app.translator.trans('flarum-package-manager.admin.major_updater.dry_run')}
{app.translator.trans('flarum-extension-manager.admin.major_updater.dry_run')}
</Button>
</Tooltip>
<Button
className="Button Button--danger"
icon="fas fa-play"
onclick={this.update.bind(this, false)}
disabled={app.packageManager.control.isLoadingOtherThan('major-update')}
disabled={app.extensionManager.control.hasOperationRunning()}
>
{app.translator.trans('flarum-package-manager.admin.major_updater.update')}
{app.translator.trans('flarum-extension-manager.admin.major_updater.update')}
</Button>
</div>
{this.updateState.incompatibleExtensions.length ? (
<div className="PackageManager-majorUpdate-incompatibleExtensions PackageManager-extensions-grid">
<div className="ExtensionManager-majorUpdate-incompatibleExtensions ExtensionManager-extensions-grid">
{this.updateState.incompatibleExtensions.map((extension: string) => (
<ExtensionItem
extension={app.data.extensions[extension.replace('flarum-', '').replace('flarum-ext-', '').replace('/', '-')]}
@@ -72,20 +75,20 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
{this.updateState.status === 'failure' ? (
<Alert
type="error"
className="PackageManager-majorUpdate-failure"
className="ExtensionManager-majorUpdate-failure"
dismissible={false}
controls={[
<Button
className="Button Button--text PackageManager-majorUpdate-failure-details"
className="Button Button--text ExtensionManager-majorUpdate-failure-details"
icon="fas fa-question-circle"
onclick={() => app.modal.show(WhyNotModal, { package: 'flarum/core' })}
>
{app.translator.trans('flarum-package-manager.admin.major_updater.failure.why')}
{app.translator.trans('flarum-extension-manager.admin.major_updater.failure.why')}
</Button>,
]}
>
<p className="PackageManager-majorUpdate-failure-desc">
{app.translator.trans('flarum-package-manager.admin.major_updater.failure.desc')}
<p className="ExtensionManager-majorUpdate-failure-desc">
{app.translator.trans('flarum-extension-manager.admin.major_updater.failure.desc')}
</p>
</Alert>
) : null}
@@ -94,34 +97,6 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
}
update(dryRun: boolean) {
app.packageManager.control.setLoading(dryRun ? 'major-update-dry-run' : 'major-update');
app.modal.show(LoadingModal);
app
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/major-update`,
body: {
data: { dryRun },
},
})
.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(errorHandler)
.catch((e: RequestError) => {
app.modal.close();
this.updateState.status = 'failure';
this.updateState.incompatibleExtensions = e.response?.errors?.pop()?.incompatible_extensions as string[];
})
.finally(() => {
app.packageManager.control.setLoading(null);
m.redraw();
});
app.extensionManager.control.majorUpdate({ dryRun });
}
}

View File

@@ -15,7 +15,7 @@ export default class Pagination extends Component<PaginationAttrs> {
return (
<nav className="Pagination UserListPage-gridPagination">
<Button
disabled={!this.attrs.list.hasPrev()}
disabled={!this.attrs.list.hasPrev() || app.extensionManager.control.isLoading()}
title={app.translator.trans('core.admin.users.pagination.back_button')}
onclick={() => this.attrs.list.prev()}
icon="fas fa-chevron-left"
@@ -28,7 +28,7 @@ export default class Pagination extends Component<PaginationAttrs> {
})}
</span>
<Button
disabled={!this.attrs.list.hasNext()}
disabled={!this.attrs.list.hasNext() || app.extensionManager.control.isLoading()}
title={app.translator.trans('core.admin.users.pagination.next_button')}
onclick={() => this.attrs.list.next()}
icon="fas fa-chevron-right"

View File

@@ -8,6 +8,7 @@ import { Extension } from 'flarum/admin/AdminApplication';
import Icon from 'flarum/common/components/Icon';
import ItemList from 'flarum/common/utils/ItemList';
import extractText from 'flarum/common/utils/extractText';
import Link from 'flarum/common/components/Link';
import Label from './Label';
import TaskOutputModal from './TaskOutputModal';
@@ -24,20 +25,21 @@ export default class QueueSection extends Component<{}> {
oninit(vnode: Mithril.Vnode<{}, this>) {
super.oninit(vnode);
app.packageManager.queue.load();
app.extensionManager.queue.load();
}
view() {
return (
<section id="PackageManager-queueSection" className="ExtensionPage-permissions PackageManager-queueSection">
<div className="ExtensionPage-permissions-header PackageManager-queueSection-header">
<section id="ExtensionManager-queueSection" className="ExtensionPage-permissions ExtensionManager-queueSection">
<div className="ExtensionPage-permissions-header ExtensionManager-queueSection-header">
<div className="container">
<h2 className="ExtensionTitle">{app.translator.trans('flarum-package-manager.admin.sections.queue.title')}</h2>
<h2 className="ExtensionTitle">{app.translator.trans('flarum-extension-manager.admin.sections.queue.title')}</h2>
<Button
className="Button Button--icon"
icon="fas fa-sync-alt"
onclick={() => app.packageManager.queue.load()}
aria-label={app.translator.trans('flarum-package-manager.admin.sections.queue.refresh')}
onclick={() => app.extensionManager.queue.load()}
aria-label={app.translator.trans('flarum-extension-manager.admin.sections.queue.refresh')}
disabled={app.extensionManager.control.isLoading()}
/>
</div>
</div>
@@ -52,12 +54,12 @@ export default class QueueSection extends Component<{}> {
items.add(
'operation',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.operation')),
label: extractText(app.translator.trans('flarum-extension-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()}`)}
<div className="ExtensionManager-queueTable-operation">
<span className="ExtensionManager-queueTable-operation-icon">{this.operationIcon(task.operation())}</span>
<span className="ExtensionManager-queueTable-operation-name">
{app.translator.trans(`flarum-extension-manager.admin.sections.queue.operations.${task.operation()}`)}
</span>
</div>
),
@@ -68,20 +70,20 @@ export default class QueueSection extends Component<{}> {
items.add(
'package',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.package')),
label: extractText(app.translator.trans('flarum-extension-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}>
<Link className="ExtensionManager-queueTable-package" href={app.route('extension', { id: extension.id })}>
<div className="ExtensionManager-queueTable-package-icon ExtensionIcon" style={extension.icon}>
{!!extension.icon && <Icon name={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 className="ExtensionManager-queueTable-package-details">
<span className="ExtensionManager-queueTable-package-title">{extension.extra['flarum-extension'].title}</span>
<span className="ExtensionManager-queueTable-package-name">{task.package()}</span>
</div>
</div>
</Link>
) : (
task.package()
);
@@ -93,14 +95,17 @@ export default class QueueSection extends Component<{}> {
items.add(
'status',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.status')),
label: extractText(app.translator.trans('flarum-extension-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>
<>
<Label
className="ExtensionManager-queueTable-status"
type={{ running: 'neutral', failure: 'error', pending: 'warning', success: 'success' }[task.status()]}
>
{app.translator.trans(`flarum-extension-manager.admin.sections.queue.statuses.${task.status()}`)}
</Label>
{['pending', 'running'].includes(task.status()) && <LoadingIndicator size="small" display="inline" />}
</>
),
},
70
@@ -109,10 +114,10 @@ export default class QueueSection extends Component<{}> {
items.add(
'elapsedTime',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.elapsed_time')),
label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.elapsed_time')),
content: (task) =>
!task.startedAt() ? (
app.translator.trans('flarum-package-manager.admin.sections.queue.task_just_started')
!task.startedAt() || !task.finishedAt() ? (
app.translator.trans('flarum-extension-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>
@@ -125,7 +130,7 @@ export default class QueueSection extends Component<{}> {
items.add(
'memoryUsed',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.peak_memory_used')),
label: extractText(app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.peak_memory_used')),
content: (task) => <span>{task.peakMemoryUsed()}</span>,
},
60
@@ -134,15 +139,16 @@ export default class QueueSection extends Component<{}> {
items.add(
'details',
{
label: extractText(app.translator.trans('flarum-package-manager.admin.sections.queue.columns.details')),
label: extractText(app.translator.trans('flarum-extension-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')}
aria-label={app.translator.trans('flarum-extension-manager.admin.sections.queue.columns.details')}
// @todo fix in core
// @ts-ignore
onclick={() => app.modal.show(TaskOutputModal, { task })}
disabled={['pending', 'running'].includes(task.status())}
/>
),
className: 'Table-controls',
@@ -154,21 +160,21 @@ export default class QueueSection extends Component<{}> {
}
queueTable() {
const tasks = app.packageManager.queue.getItems();
const tasks = app.extensionManager.queue.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>;
return <h3 className="ExtensionPage-subHeader">{app.translator.trans('flarum-extension-manager.admin.sections.queue.none')}</h3>;
}
const columns = this.columns();
return (
<>
<table className="Table PackageManager-queueTable">
<table className="Table ExtensionManager-queueTable">
<thead>
<tr>
{columns.toArray().map((item, index) => (
@@ -193,23 +199,27 @@ export default class QueueSection extends Component<{}> {
</tbody>
</table>
<Pagination list={app.packageManager.queue} />
<Pagination list={app.extensionManager.queue} />
</>
);
}
operationIcon(operation: TaskOperations): Mithril.Children {
const iconName = {
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];
return <Icon name={iconName} />;
return (
<Icon
name={
{
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,77 @@
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
import Mithril from 'mithril';
import app from 'flarum/admin/app';
import Select from 'flarum/common/components/Select';
import Stream from 'flarum/common/utils/Stream';
import Button from 'flarum/common/components/Button';
import { type Repository } from './ConfigureComposer';
export interface IRepositoryModalAttrs extends IInternalModalAttrs {
onsubmit: (repository: Repository, key: string) => void;
name?: string;
repository?: Repository;
}
export default class RepositoryModal<CustomAttrs extends IRepositoryModalAttrs = IRepositoryModalAttrs> extends Modal<CustomAttrs> {
protected name!: Stream<string>;
protected repository!: Stream<Repository>;
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
this.name = Stream(this.attrs.name || '');
this.repository = Stream(this.attrs.repository || { type: 'composer', url: '' });
}
className(): string {
return 'RepositoryModal Modal--small';
}
title(): Mithril.Children {
const context = this.attrs.repository ? 'edit' : 'add';
return app.translator.trans(`flarum-extension-manager.admin.composer.${context}_repository_label`);
}
content(): Mithril.Children {
const types = {
composer: app.translator.trans('flarum-extension-manager.admin.composer.repositories.types.composer'),
vcs: app.translator.trans('flarum-extension-manager.admin.composer.repositories.types.vcs'),
path: app.translator.trans('flarum-extension-manager.admin.composer.repositories.types.path'),
};
return (
<div className="Modal-body">
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.add_modal.name_label')}</label>
<input className="FormControl" bidi={this.name} />
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.add_modal.type_label')}</label>
<Select
options={types}
value={this.repository().type}
onchange={(value: 'composer' | 'vcs' | 'path') => this.repository({ ...this.repository(), type: value })}
/>
</div>
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.composer.repositories.add_modal.url')}</label>
<input
className="FormControl"
onchange={(e: Event) => this.repository({ ...this.repository(), url: (e.target as HTMLInputElement).value })}
value={this.repository().url}
/>
</div>
<div className="Form-group">
<Button className="Button Button--primary" onclick={this.submit.bind(this)}>
{app.translator.trans('flarum-extension-manager.admin.composer.repositories.add_modal.submit_button')}
</Button>
</div>
</div>
);
}
submit() {
this.attrs.onsubmit(this.repository(), this.name());
this.hide();
}
}

View File

@@ -5,8 +5,45 @@ import ItemList from 'flarum/common/utils/ItemList';
import QueueSection from './QueueSection';
import ControlSection from './ControlSection';
import ConfigureComposer from './ConfigureComposer';
import Alert from 'flarum/common/components/Alert';
import listItems from 'flarum/common/helpers/listItems';
import ConfigureAuth from './ConfigureAuth';
export default class SettingsPage extends ExtensionPage {
content() {
const settings = app.extensionData.getSettings(this.extension.id);
const warnings = [app.translator.trans('flarum-extension-manager.admin.settings.access_warning')];
if (app.data.debugEnabled) warnings.push(app.translator.trans('flarum-extension-manager.admin.settings.debug_mode_warning'));
return (
<div className="ExtensionPage-settings">
<div className="container">
<div className="ExtensionManager-warnings Form-group">
<Alert className="ExtensionManager-primaryWarning" type="warning" dismissible={false}>
<ul>{listItems(warnings)}</ul>
</Alert>
</div>
{settings ? (
<div className="FormSectionGroup ExtensionManager-SettingsGroups">
<div className="FormSection">
<label>{app.translator.trans('flarum-extension-manager.admin.settings.title')}</label>
<div className="Form">{settings.map(this.buildSettingComponent.bind(this))}</div>
<div className="Form-group Form--controls">{this.submitButton()}</div>
</div>
<ConfigureComposer buildSettingComponent={this.buildSettingComponent} />
<ConfigureAuth buildSettingComponent={this.buildSettingComponent} />
</div>
) : (
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.no_settings')}</h3>
)}
</div>
</div>
);
}
sections(vnode: Mithril.VnodeDOM<ExtensionPageAttrs, this>): ItemList<unknown> {
const items = super.sections(vnode);
@@ -14,12 +51,17 @@ export default class SettingsPage extends ExtensionPage {
items.add('control', <ControlSection />, 8);
if (parseInt(app.data.settings['flarum-package-manager.queue_jobs'])) {
if (app.data.settings['flarum-extension-manager.queue_jobs'] !== '0' && app.data.settings['flarum-extension-manager.queue_jobs']) {
items.add('queue', <QueueSection />, 5);
}
items.setPriority('permissions', 0);
items.remove('permissions');
return items;
}
onsaved() {
super.onsaved();
m.redraw();
}
}

View File

@@ -12,21 +12,33 @@ export default class TaskOutputModal<CustomAttrs extends TaskOutputModalAttrs =
}
title() {
return app.translator.trans(`flarum-package-manager.admin.sections.queue.operations.${this.attrs.task.operation()}`);
return app.translator.trans(`flarum-extension-manager.admin.sections.queue.operations.${this.attrs.task.operation()}`);
}
content() {
return (
<div className="Modal-body">
<div className="TaskOutputModal-data">
{this.attrs.task.status() === 'failure' && (
<div className="Form-group">
<label>{app.translator.trans('flarum-extension-manager.admin.sections.queue.output_modal.guessed_cause')}</label>
<div className="FormControl TaskOutputModal-data-guessed-cause">
{(this.attrs.task.guessedCause() &&
app.translator.trans('flarum-extension-manager.admin.exceptions.guessed_cause.' + this.attrs.task.guessedCause())) ||
app.translator.trans('flarum-extension-manager.admin.sections.queue.output_modal.cause_unknown')}
</div>
</div>
)}
<div className="Form-group">
<label>{app.translator.trans('flarum-package-manager.admin.sections.queue.output_modal.command')}</label>
<label>{app.translator.trans('flarum-extension-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>
<label>{app.translator.trans('flarum-extension-manager.admin.sections.queue.output_modal.output')}</label>
<div className="FormControl TaskOutputModal-data-output">
<code>
<pre>{this.attrs.task.output()}</pre>

View File

@@ -6,7 +6,6 @@ import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import MajorUpdater from './MajorUpdater';
import ExtensionItem from './ExtensionItem';
import { Extension } from 'flarum/admin/AdminApplication';
import Alert from 'flarum/common/components/Alert';
import ItemList from 'flarum/common/utils/ItemList';
export interface IUpdaterAttrs extends ComponentAttrs {}
@@ -15,30 +14,30 @@ export type UpdaterLoadingTypes = 'check' | 'minor-update' | 'global-update' | '
export default class Updater extends Component<IUpdaterAttrs> {
view() {
const core = app.packageManager.control.coreUpdate;
const core = app.extensionManager.control.coreUpdate;
return [
<div className="Form-group">
<label>{app.translator.trans('flarum-package-manager.admin.updater.updater_title')}</label>
<p className="helpText">{app.translator.trans('flarum-package-manager.admin.updater.updater_help')}</p>
<label>{app.translator.trans('flarum-extension-manager.admin.updater.updater_title')}</label>
<div className="helpText">{app.translator.trans('flarum-extension-manager.admin.updater.updater_help')}</div>
{this.lastUpdateCheckView()}
<div className="PackageManager-updaterControls">{this.controlItems().toArray()}</div>
<div className="ExtensionManager-updaterControls">{this.controlItems().toArray()}</div>
{this.availableUpdatesView()}
</div>,
core && core.package['latest-major'] ? (
<MajorUpdater coreUpdate={core.package} updateState={app.packageManager.control.lastUpdateRun.major} />
<MajorUpdater coreUpdate={core.package} updateState={app.extensionManager.control.lastUpdateRun.major} />
) : null,
];
}
lastUpdateCheckView() {
return (
(app.packageManager.control.lastUpdateCheck?.checkedAt && (
<p className="PackageManager-lastUpdatedAt">
<span className="PackageManager-lastUpdatedAt-label">
{app.translator.trans('flarum-package-manager.admin.updater.last_update_checked_at')}
(app.extensionManager.control.lastUpdateCheck?.checkedAt && (
<p className="ExtensionManager-lastUpdatedAt">
<span className="ExtensionManager-lastUpdatedAt-label">
{app.translator.trans('flarum-extension-manager.admin.updater.last_update_checked_at')}
</span>
<span className="PackageManager-lastUpdatedAt-value">{humanTime(app.packageManager.control.lastUpdateCheck.checkedAt)}</span>
<span className="ExtensionManager-lastUpdatedAt-value">{humanTime(app.extensionManager.control.lastUpdateCheck.checkedAt)}</span>
</p>
)) ||
null
@@ -46,33 +45,33 @@ export default class Updater extends Component<IUpdaterAttrs> {
}
availableUpdatesView() {
const state = app.packageManager.control;
const state = app.extensionManager.control;
if (app.packageManager.control.isLoading()) {
if (app.extensionManager.control.isLoading('check') || app.extensionManager.control.isLoading('global-update')) {
return (
<div className="PackageManager-extensions">
<div className="ExtensionManager-extensions">
<LoadingIndicator />
</div>
);
}
if (!(state.extensionUpdates.length || state.coreUpdate)) {
const hasMinorCoreUpdate = state.coreUpdate && state.coreUpdate.package['latest-minor'];
if (!(state.extensionUpdates.length || hasMinorCoreUpdate)) {
return (
<div className="PackageManager-extensions">
<Alert type="success" dismissible={false}>
{app.translator.trans('flarum-package-manager.admin.updater.up_to_date')}
</Alert>
<div className="ExtensionManager-extensions">
<span className="helpText">{app.translator.trans('flarum-extension-manager.admin.updater.up_to_date')}</span>
</div>
);
}
return (
<div className="PackageManager-extensions">
<div className="PackageManager-extensions-grid">
{state.coreUpdate ? (
<div className="ExtensionManager-extensions">
<div className="ExtensionManager-extensions-grid">
{hasMinorCoreUpdate ? (
<ExtensionItem
extension={state.coreUpdate.extension}
updates={state.coreUpdate.package}
extension={state.coreUpdate!.extension}
updates={state.coreUpdate!.package}
isCore={true}
onClickUpdate={() => state.updateCoreMinor()}
whyNotWarning={state.lastUpdateRun.limitedPackages().includes('flarum/core')}
@@ -82,7 +81,10 @@ export default class Updater extends Component<IUpdaterAttrs> {
<ExtensionItem
extension={extension}
updates={state.packageUpdates[extension.id]}
onClickUpdate={() => state.updateExtension(extension)}
onClickUpdate={{
soft: () => state.updateExtension(extension, 'soft'),
hard: () => state.updateExtension(extension, 'hard'),
}}
whyNotWarning={state.lastUpdateRun.limitedPackages().includes(extension.name)}
/>
))}
@@ -99,11 +101,11 @@ export default class Updater extends Component<IUpdaterAttrs> {
<Button
className="Button"
icon="fas fa-sync-alt"
onclick={() => app.packageManager.control.checkForUpdates()}
loading={app.packageManager.control.isLoading('check')}
disabled={app.packageManager.control.isLoadingOtherThan('check')}
onclick={() => app.extensionManager.control.checkForUpdates()}
loading={app.extensionManager.control.isLoading('check')}
disabled={app.extensionManager.control.hasOperationRunning()}
>
{app.translator.trans('flarum-package-manager.admin.updater.check_for_updates')}
{app.translator.trans('flarum-extension-manager.admin.updater.check_for_updates')}
</Button>,
100
);
@@ -113,11 +115,11 @@ export default class Updater extends Component<IUpdaterAttrs> {
<Button
className="Button"
icon="fas fa-play"
onclick={() => app.packageManager.control.updateGlobally()}
loading={app.packageManager.control.isLoading('global-update')}
disabled={app.packageManager.control.isLoadingOtherThan('global-update')}
onclick={() => app.extensionManager.control.updateGlobally()}
loading={app.extensionManager.control.isLoading('global-update')}
disabled={app.extensionManager.control.hasOperationRunning()}
>
{app.translator.trans('flarum-package-manager.admin.updater.run_global_update')}
{app.translator.trans('flarum-extension-manager.admin.updater.run_global_update')}
</Button>
);

View File

@@ -24,7 +24,7 @@ export default class WhyNotModal<CustomAttrs extends WhyNotModalAttrs = WhyNotMo
}
title() {
return app.translator.trans('flarum-package-manager.admin.why_not_modal.title');
return app.translator.trans('flarum-extension-manager.admin.why_not_modal.title');
}
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
@@ -41,7 +41,7 @@ export default class WhyNotModal<CustomAttrs extends WhyNotModalAttrs = WhyNotMo
app
.request<WhyNotResponse>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/why-not`,
url: `${app.forum.attribute('apiUrl')}/extension-manager/why-not`,
body: {
data: {
package: this.attrs.package,

View File

@@ -4,35 +4,30 @@ import ExtensionPage from 'flarum/admin/components/ExtensionPage';
import Button from 'flarum/common/components/Button';
import LoadingModal from 'flarum/admin/components/LoadingModal';
import isExtensionEnabled from 'flarum/admin/utils/isExtensionEnabled';
import Alert from 'flarum/common/components/Alert';
import SettingsPage from './components/SettingsPage';
import Task from './models/Task';
import jumpToQueue from './utils/jumpToQueue';
import extractText from 'flarum/common/utils/extractText';
import { AsyncBackendResponse } from './shims';
import PackageManagerState from './states/PackageManagerState';
import ExtensionManagerState from './states/ExtensionManagerState';
app.initializers.add('flarum-package-manager', (app) => {
app.store.models['package-manager-tasks'] = Task;
app.initializers.add('flarum-extension-manager', (app) => {
app.store.models['extension-manager-tasks'] = Task;
app.packageManager = new PackageManagerState();
app.extensionManager = new ExtensionManagerState();
if (app.data['flarum-extension-manager.using_sync_queue']) {
app.data.settings['flarum-extension-manager.queue_jobs'] = '0';
}
app.extensionData
.for('flarum-package-manager')
.registerSetting(() => (
<div className="Form-group">
<Alert type="warning" dismissible={false}>
{app.translator.trans('flarum-package-manager.admin.settings.access_warning')}
</Alert>
</div>
))
.for('flarum-extension-manager')
.registerSetting({
setting: 'flarum-package-manager.queue_jobs',
label: app.translator.trans('flarum-package-manager.admin.settings.queue_jobs'),
setting: 'flarum-extension-manager.queue_jobs',
label: app.translator.trans('flarum-extension-manager.admin.settings.queue_jobs'),
help: m.trust(
extractText(
app.translator.trans('flarum-package-manager.admin.settings.queue_jobs_help', {
app.translator.trans('flarum-extension-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>`,
@@ -40,14 +35,19 @@ app.initializers.add('flarum-package-manager', (app) => {
})
)
),
default: false,
type: 'boolean',
disabled: app.data['flarum-package-manager.using_sync_queue'],
disabled: app.data['flarum-extension-manager.using_sync_queue'],
})
.registerSetting({
setting: 'flarum-extension-manager.task_retention_days',
label: app.translator.trans('flarum-extension-manager.admin.settings.task_retention_days'),
help: app.translator.trans('flarum-extension-manager.admin.settings.task_retention_days_help'),
type: 'number',
})
.registerPage(SettingsPage);
extend(ExtensionPage.prototype, 'topItems', function (items) {
if (this.extension.id === 'flarum-package-manager' || isExtensionEnabled(this.extension.id)) {
if (this.extension.id === 'flarum-extension-manager' || isExtensionEnabled(this.extension.id)) {
return;
}
@@ -61,14 +61,14 @@ app.initializers.add('flarum-package-manager', (app) => {
app
.request<AsyncBackendResponse | null>({
url: `${app.forum.attribute('apiUrl')}/package-manager/extensions/${this.extension.id}`,
url: `${app.forum.attribute('apiUrl')}/extension-manager/extensions/${this.extension.id}`,
method: 'DELETE',
})
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.extensions.successful_remove'));
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-extension-manager.admin.extensions.successful_remove'));
window.location = app.forum.attribute('adminUrl');
}
})
@@ -77,7 +77,7 @@ app.initializers.add('flarum-package-manager', (app) => {
});
}}
>
Remove
{app.translator.trans('flarum-extension-manager.admin.extensions.remove')}
</Button>
);
});

View File

@@ -32,6 +32,10 @@ export default class Task extends Model {
return Model.attribute<string>('output').call(this);
}
guessedCause() {
return Model.attribute<string>('guessedCause').call(this);
}
createdAt() {
return Model.attribute('createdAt', Model.transformDate).call(this);
}

View File

@@ -1,4 +1,5 @@
import PackageManagerState from './states/PackageManagerState';
import 'dayjs/plugin/relativeTime';
import ExtensionManagerState from './states/ExtensionManagerState';
export interface AsyncBackendResponse {
processing: boolean;
@@ -6,6 +7,6 @@ export interface AsyncBackendResponse {
declare module 'flarum/admin/AdminApplication' {
export default interface AdminApplication {
packageManager: PackageManagerState;
extensionManager: ExtensionManagerState;
}
}

View File

@@ -8,6 +8,7 @@ import errorHandler from '../utils/errorHandler';
import jumpToQueue from '../utils/jumpToQueue';
import { Extension } from 'flarum/admin/AdminApplication';
import extractText from 'flarum/common/utils/extractText';
import RequestError from 'flarum/common/utils/RequestError';
export type UpdatedPackage = {
name: string;
@@ -16,6 +17,8 @@ export type UpdatedPackage = {
'latest-minor': string | null;
'latest-major': string | null;
'latest-status': string;
'required-as': string;
'direct-dependency': boolean;
description: string;
};
@@ -43,7 +46,7 @@ export type LastUpdateRun = {
limitedPackages: () => string[];
};
export type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes;
export type LoadingTypes = UpdaterLoadingTypes | InstallerLoadingTypes | MajorUpdaterLoadingTypes | 'queued-action';
export type CoreUpdate = {
package: UpdatedPackage;
@@ -58,7 +61,7 @@ export default class ControlSectionState {
public extensionUpdates!: Extension[];
public coreUpdate: CoreUpdate | null = null;
get lastUpdateRun(): LastUpdateRun {
const lastUpdateRun = JSON.parse(app.data.settings['flarum-package-manager.last_update_run']) as LastUpdateRun;
const lastUpdateRun = JSON.parse(app.data.settings['flarum-extension-manager.last_update_run']) as LastUpdateRun;
lastUpdateRun.limitedPackages = () => [
...lastUpdateRun.major.limitedPackages,
@@ -70,7 +73,7 @@ export default class ControlSectionState {
}
constructor() {
this.lastUpdateCheck = JSON.parse(app.data.settings['flarum-package-manager.last_update_check']) as LastUpdateCheck;
this.lastUpdateCheck = JSON.parse(app.data.settings['flarum-extension-manager.last_update_check']) as LastUpdateCheck;
this.extensionUpdates = this.formatExtensionUpdates(this.lastUpdateCheck);
this.coreUpdate = this.formatCoreUpdate(this.lastUpdateCheck);
}
@@ -79,21 +82,53 @@ export default class ControlSectionState {
return (name && this.loading === name) || (!name && this.loading !== null);
}
isLoadingOtherThan(name: LoadingTypes): boolean {
return this.loading !== null && this.loading !== name;
hasOperationRunning(): boolean {
return this.isLoading() || app.extensionManager.queue.hasPending();
}
setLoading(name: LoadingTypes): void {
this.loading = name;
}
requirePackage(data: any) {
app.extensionManager.control.setLoading('extension-install');
app.modal.show(LoadingModal);
app
.request<AsyncBackendResponse & { id: number }>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/extension-manager/extensions`,
body: {
data,
},
})
.then((response) => {
if (response.processing) {
jumpToQueue();
} else {
const extensionId = response.id;
app.alerts.show(
{ type: 'success' },
app.translator.trans('flarum-extension-manager.admin.extensions.successful_install', { extension: extensionId })
);
window.location.href = `${app.forum.attribute('adminUrl')}#/extension/${extensionId}`;
window.location.reload();
}
})
.catch(errorHandler)
.finally(() => {
app.modal.close();
m.redraw();
});
}
checkForUpdates() {
this.setLoading('check');
app
.request<AsyncBackendResponse | LastUpdateCheck>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/check-for-updates`,
url: `${app.forum.attribute('apiUrl')}/extension-manager/check-for-updates`,
})
.then((response) => {
if ((response as AsyncBackendResponse).processing) {
@@ -102,51 +137,55 @@ export default class ControlSectionState {
this.lastUpdateCheck = response as LastUpdateCheck;
this.extensionUpdates = this.formatExtensionUpdates(response as LastUpdateCheck);
this.coreUpdate = this.formatCoreUpdate(response as LastUpdateCheck);
this.setLoading(null);
m.redraw();
}
})
.catch(errorHandler)
.finally(() => {
this.setLoading(null);
m.redraw();
});
}
updateCoreMinor() {
if (confirm(extractText(app.translator.trans('flarum-package-manager.admin.minor_update_confirmation.content')))) {
if (confirm(extractText(app.translator.trans('flarum-extension-manager.admin.minor_update_confirmation.content')))) {
app.modal.show(LoadingModal);
this.setLoading('minor-update');
app
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/minor-update`,
url: `${app.forum.attribute('apiUrl')}/extension-manager/minor-update`,
})
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.update_successful'));
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-extension-manager.admin.update_successful'));
window.location.reload();
}
})
.catch(errorHandler)
.finally(() => {
this.setLoading(null);
app.modal.close();
m.redraw();
});
}
}
updateExtension(extension: Extension) {
updateExtension(extension: Extension, updateMode: 'soft' | 'hard') {
app.modal.show(LoadingModal);
this.setLoading('extension-update');
app
.request<AsyncBackendResponse | null>({
method: 'PATCH',
url: `${app.forum.attribute('apiUrl')}/package-manager/extensions/${extension.id}`,
url: `${app.forum.attribute('apiUrl')}/extension-manager/extensions/${extension.id}`,
body: {
data: {
updateMode,
},
},
})
.then((response) => {
if (response?.processing) {
@@ -154,7 +193,7 @@ export default class ControlSectionState {
} else {
app.alerts.show(
{ type: 'success' },
app.translator.trans('flarum-package-manager.admin.extensions.successful_update', {
app.translator.trans('flarum-extension-manager.admin.extensions.successful_update', {
extension: extension.extra['flarum-extension'].title,
})
);
@@ -163,7 +202,6 @@ export default class ControlSectionState {
})
.catch(errorHandler)
.finally(() => {
this.setLoading(null);
app.modal.close();
m.redraw();
});
@@ -176,19 +214,18 @@ export default class ControlSectionState {
app
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/package-manager/global-update`,
url: `${app.forum.attribute('apiUrl')}/extension-manager/global-update`,
})
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.updater.global_update_successful'));
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-extension-manager.admin.updater.global_update_successful'));
window.location.reload();
}
})
.catch(errorHandler)
.finally(() => {
this.setLoading(null);
app.modal.close();
m.redraw();
});
@@ -226,14 +263,46 @@ export default class ControlSectionState {
version: app.data.settings.version,
icon: {
// @ts-ignore
backgroundImage: `url(${app.data.resources[0]['attributes']['baseUrl']}/assets/extensions/flarum-package-manager/flarum.svg`,
backgroundImage: `url(${app.data.resources[0]['attributes']['baseUrl']}/assets/extensions/flarum-extension-manager/flarum.svg`,
},
extra: {
'flarum-extension': {
title: extractText(app.translator.trans('flarum-package-manager.admin.updater.flarum')),
title: extractText(app.translator.trans('flarum-extension-manager.admin.updater.flarum')),
},
},
},
};
}
majorUpdate({ dryRun }: { dryRun: boolean }) {
app.extensionManager.control.setLoading(dryRun ? 'major-update-dry-run' : 'major-update');
app.modal.show(LoadingModal);
const updateState = this.lastUpdateRun.major;
app
.request<AsyncBackendResponse | null>({
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/extension-manager/major-update`,
body: {
data: { dryRun },
},
})
.then((response) => {
if (response?.processing) {
jumpToQueue();
} else {
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-extension-manager.admin.update_successful'));
window.location.reload();
}
})
.catch(errorHandler)
.catch((e: RequestError) => {
app.modal.close();
updateState.status = 'failure';
updateState.incompatibleExtensions = e.response?.errors?.pop()?.incompatible_extensions as string[];
})
.finally(() => {
m.redraw();
});
}
}

View File

@@ -0,0 +1,7 @@
import QueueState from './QueueState';
import ControlSectionState from './ControlSectionState';
export default class ExtensionManagerState {
public queue: QueueState = new QueueState();
public control: ControlSectionState = new ControlSectionState();
}

View File

@@ -3,12 +3,13 @@ import Task from '../models/Task';
import { ApiQueryParamsPlural } from 'flarum/common/Store';
export default class QueueState {
private polling: any = null;
private tasks: Task[] | null = null;
private limit = 20;
private offset = 0;
private total = 0;
load(params?: ApiQueryParamsPlural) {
load(params?: ApiQueryParamsPlural, actionTaken = false): Promise<Task[]> {
this.tasks = null;
params = {
page: {
@@ -19,12 +20,26 @@ export default class QueueState {
...params,
};
return app.store.find<Task[]>('package-manager-tasks', params || {}).then((data) => {
return app.store.find<Task[]>('extension-manager-tasks', params || {}).then((data) => {
this.tasks = data;
this.total = data.payload.meta?.total!;
this.total = data.payload.meta?.total;
m.redraw();
// Check if there is a pending or running task
const pendingTask = data?.find((task) => task.status() === 'pending' || task.status() === 'running');
if (pendingTask) {
this.pollQueue(actionTaken);
} else if (actionTaken) {
app.extensionManager.control.setLoading(null);
// Refresh the page
window.location.reload();
} else if (app.extensionManager.control.isLoading()) {
app.extensionManager.control.setLoading(null);
}
return data;
});
}
@@ -62,4 +77,18 @@ export default class QueueState {
this.load();
}
}
pollQueue(actionTaken = false): void {
if (this.polling) {
clearTimeout(this.polling);
}
this.polling = setTimeout(() => {
this.load({}, actionTaken);
}, 6000);
}
hasPending() {
return !!this.tasks?.find((task) => task.status() === 'pending' || task.status() === 'running');
}
}

View File

@@ -1,29 +1,33 @@
import app from 'flarum/admin/app';
export default function (e: any) {
app.extensionManager.control.setLoading(null);
const error = e.response.errors[0];
if (!['composer_command_failure', 'extension_already_installed', 'extension_not_installed'].includes(error.code)) {
throw e;
}
app.alerts.clear();
switch (error.code) {
case 'composer_command_failure':
if (error.guessed_cause) {
app.alerts.show({ type: 'error' }, app.translator.trans(`flarum-package-manager.admin.exceptions.guessed_cause.${error.guessed_cause}`));
app.alerts.show({ type: 'error' }, app.translator.trans(`flarum-extension-manager.admin.exceptions.guessed_cause.${error.guessed_cause}`));
app.modal.close();
} else {
app.alerts.show({ type: 'error' }, app.translator.trans('flarum-package-manager.admin.exceptions.composer_command_failure'));
app.alerts.show({ type: 'error' }, app.translator.trans('flarum-extension-manager.admin.exceptions.composer_command_failure'));
}
break;
case 'extension_already_installed':
app.alerts.show({ type: 'error' }, app.translator.trans('flarum-package-manager.admin.exceptions.extension_already_installed'));
app.alerts.show({ type: 'error' }, app.translator.trans('flarum-extension-manager.admin.exceptions.extension_already_installed'));
app.modal.close();
break;
case 'extension_not_installed':
app.alerts.show({ type: 'error' }, app.translator.trans('flarum-package-manager.admin.exceptions.extension_not_installed'));
app.alerts.show({ type: 'error' }, app.translator.trans('flarum-extension-manager.admin.exceptions.extension_not_installed'));
app.modal.close();
}
}

View File

@@ -5,9 +5,12 @@ window.jumpToQueue = jumpToQueue;
export default function jumpToQueue(): void {
app.modal.close();
m.route.set(app.route('extension', { id: 'flarum-package-manager' }));
app.packageManager.queue.load();
m.route.set(app.route('extension', { id: 'flarum-extension-manager' }));
app.extensionManager.queue.load({}, true);
setTimeout(() => {
document.getElementById('PackageManager-queueSection')?.scrollIntoView({ block: 'nearest' });
document.getElementById('ExtensionManager-queueSection')?.scrollIntoView({ block: 'nearest' });
}, 200);
}