mirror of
https://github.com/flarum/core.git
synced 2025-08-20 15:21:49 +02:00
feat: misc additions
- Detect extensions that didn't update between updates - Add composer why not command where approriate (when extension didn't update, when major update failed) - Detect incompatible extensions in major update failure and show the extensions in the frontend - Create last update run setting value which holds the state of the latest update runs - Other fixes
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import 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 { UpdatedPackage } from './Updater';
|
||||
import WhyNotModal from './WhyNotModal';
|
||||
|
||||
/*
|
||||
* @todo fix in core
|
||||
*/
|
||||
export type Extension = BaseExtension & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export interface ExtensionItemAttrs extends ComponentAttrs {
|
||||
extension: Extension;
|
||||
updates: UpdatedPackage;
|
||||
onClickUpdate: CallableFunction;
|
||||
whyNotWarning?: boolean;
|
||||
isCore?: boolean;
|
||||
updatable?: boolean;
|
||||
isDanger?: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classList({
|
||||
'PackageManager-extension': true,
|
||||
'PackageManager-extension--core': isCore,
|
||||
'PackageManager-extension--danger': isDanger,
|
||||
})}
|
||||
>
|
||||
<div className="PackageManager-extension-icon ExtensionIcon" style={extension.icon}>
|
||||
{extension.icon ? icon(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(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>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="PackageManager-extension-controls">
|
||||
{onClickUpdate ? (
|
||||
<Tooltip text={app.translator.trans('flarum-package-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')}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{whyNotWarning ? (
|
||||
<Tooltip text={app.translator.trans('flarum-package-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')}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private version(v: string): string {
|
||||
return 'v' + v.replace('v', '');
|
||||
}
|
||||
}
|
@@ -3,20 +3,33 @@ 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 } from './Updater';
|
||||
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';
|
||||
|
||||
interface MajorUpdaterAttrs extends ComponentAttrs {
|
||||
coreUpdate: UpdatedPackage;
|
||||
updateState: UpdateState;
|
||||
}
|
||||
|
||||
export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttrs> extends Component<T> {
|
||||
isLoading: string | null = null;
|
||||
updateState!: UpdateState;
|
||||
|
||||
view(vnode: Mithril.Vnode<ComponentAttrs, this>): Mithril.Children {
|
||||
oninit(vnode: Mithril.Vnode<T, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.updateState = this.attrs.updateState;
|
||||
}
|
||||
|
||||
view(vnode: Mithril.Vnode<T, this>): Mithril.Children {
|
||||
// @todo move Form-group--danger class to core for reuse
|
||||
return (
|
||||
<div className="Form-group PackageManager-majorUpdate">
|
||||
<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>
|
||||
@@ -26,10 +39,42 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
|
||||
{app.translator.trans('flarum-package-manager.admin.major_updater.dry_run')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button className="Button" icon="fas fa-play" onclick={this.update.bind(this, false)}>
|
||||
<Button className="Button Button--danger" icon="fas fa-play" onclick={this.update.bind(this, false)}>
|
||||
{app.translator.trans('flarum-package-manager.admin.major_updater.update')}
|
||||
</Button>
|
||||
</div>
|
||||
{this.updateState.incompatibleExtensions.length ? (
|
||||
<div className="PackageManager-majorUpdate-incompatibleExtensions PackageManager-extensions-grid">
|
||||
{this.updateState.incompatibleExtensions.map((extension: string) => (
|
||||
<ExtensionItem
|
||||
extension={app.data.extensions[extension.replace('flarum-', '').replace('flarum-ext-', '').replace('/', '-')]}
|
||||
updates={{}}
|
||||
onClickUpdate={null}
|
||||
isDanger={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{this.updateState.status === 'failure' ? (
|
||||
<Alert
|
||||
type="error"
|
||||
className="PackageManager-majorUpdate-failure"
|
||||
dismissible={false}
|
||||
controls={[
|
||||
<Button
|
||||
className="Button Button--text PackageManager-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')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<p className="PackageManager-majorUpdate-failure-desc">
|
||||
{app.translator.trans('flarum-package-manager.admin.major_updater.failure.desc')}
|
||||
</p>
|
||||
</Alert>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -51,6 +96,11 @@ export default class MajorUpdater<T extends MajorUpdaterAttrs = MajorUpdaterAttr
|
||||
app.alerts.show({ type: 'success' }, app.translator.trans('flarum-package-manager.admin.update_successful'));
|
||||
window.location.reload();
|
||||
})
|
||||
.catch((e: RequestError) => {
|
||||
app.modal.close();
|
||||
this.updateState.status = 'failure';
|
||||
this.updateState.incompatibleExtensions = e.response?.errors?.pop()?.incompatible_extensions as string[];
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = null;
|
||||
m.redraw();
|
||||
|
@@ -1,16 +1,13 @@
|
||||
import Mithril from "mithril";
|
||||
import Mithril from 'mithril';
|
||||
import app from 'flarum/admin/app';
|
||||
import Component from 'flarum/common/Component';
|
||||
import icon from 'flarum/common/helpers/icon';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import humanTime from 'flarum/common/helpers/humanTime';
|
||||
import LoadingModal from 'flarum/admin/components/LoadingModal';
|
||||
import Tooltip from 'flarum/common/components/Tooltip';
|
||||
import errorHandler from '../utils/errorHandler';
|
||||
import classList from 'flarum/common/utils/classList';
|
||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||
import MajorUpdater from './MajorUpdater';
|
||||
import {Extension} from "flarum/admin/AdminApplication";
|
||||
import ExtensionItem, { Extension } from './ExtensionItem';
|
||||
|
||||
export type UpdatedPackage = {
|
||||
name: string;
|
||||
@@ -27,27 +24,54 @@ export type ComposerUpdates = {
|
||||
};
|
||||
|
||||
export type LastUpdateCheck = {
|
||||
checkedAt: Date;
|
||||
checkedAt: Date | null;
|
||||
updates: ComposerUpdates;
|
||||
};
|
||||
|
||||
type UpdateType = 'major' | 'minor' | 'global';
|
||||
type UpdateStatus = 'success' | 'failure' | null;
|
||||
export type UpdateState = {
|
||||
ranAt: Date | null;
|
||||
status: UpdateStatus;
|
||||
limitedPackages: string[];
|
||||
incompatibleExtensions: string[];
|
||||
};
|
||||
|
||||
export type LastUpdateRun = {
|
||||
[key in UpdateType]: UpdateState;
|
||||
} & {
|
||||
limitedPackages: () => string[];
|
||||
};
|
||||
|
||||
export default class Updater<Attrs> extends Component<Attrs> {
|
||||
isLoading: string | null = null;
|
||||
lastUpdateCheck: LastUpdateCheck = (app.data.lastUpdateCheck as LastUpdateCheck) || {};
|
||||
packageUpdates: Record<string, UpdatedPackage> = {};
|
||||
lastUpdateCheck: LastUpdateCheck = JSON.parse(app.data.settings['flarum-package-manager.last_update_check']) as LastUpdateCheck;
|
||||
get lastUpdateRun(): LastUpdateRun {
|
||||
const lastUpdateRun = JSON.parse(app.data.settings['flarum-package-manager.last_update_run']) as LastUpdateRun;
|
||||
|
||||
lastUpdateRun.limitedPackages = () => [
|
||||
...lastUpdateRun.major.limitedPackages,
|
||||
...lastUpdateRun.minor.limitedPackages,
|
||||
...lastUpdateRun.global.limitedPackages,
|
||||
];
|
||||
|
||||
return lastUpdateRun;
|
||||
}
|
||||
|
||||
oninit(vnode: Mithril.Vnode<Attrs, this>) {
|
||||
super.oninit(vnode);
|
||||
}
|
||||
|
||||
view() {
|
||||
const extensions: any = this.getExtensionUpdates();
|
||||
const coreUpdate: UpdatedPackage | undefined = this.getCoreUpdate();
|
||||
let core: any = null;
|
||||
const extensions = this.getExtensionUpdates();
|
||||
let coreUpdate: UpdatedPackage | undefined = this.getCoreUpdate();
|
||||
let core: any;
|
||||
|
||||
if (coreUpdate) {
|
||||
core = {
|
||||
id: "flarum-core",
|
||||
id: 'flarum-core',
|
||||
name: 'flarum/core',
|
||||
version: app.data.settings.version,
|
||||
icon: {
|
||||
backgroundImage: `url(${app.forum.attribute('baseUrl')}/assets/extensions/flarum-package-manager/flarum.svg`,
|
||||
@@ -55,25 +79,23 @@ export default class Updater<Attrs> extends Component<Attrs> {
|
||||
extra: {
|
||||
'flarum-extension': {
|
||||
title: app.translator.trans('flarum-package-manager.admin.updater.flarum'),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
this.packageUpdates['flarum-core'] = 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>
|
||||
{Object.keys(this.lastUpdateCheck).length ? (
|
||||
{this.lastUpdateCheck?.checkedAt && (
|
||||
<p className="PackageManager-lastUpdatedAt">
|
||||
<span className="PackageManager-lastUpdatedAt-label">
|
||||
{app.translator.trans('flarum-package-manager.admin.updater.last_update_checked_at')}
|
||||
</span>
|
||||
<span className="PackageManager-lastUpdatedAt-value">{humanTime(this.lastUpdateCheck?.checkedAt)}</span>
|
||||
<span className="PackageManager-lastUpdatedAt-value">{humanTime(this.lastUpdateCheck.checkedAt)}</span>
|
||||
</p>
|
||||
) : null}
|
||||
)}
|
||||
<div className="PackageManager-updaterControls">
|
||||
<Button
|
||||
className="Button"
|
||||
@@ -101,64 +123,36 @@ export default class Updater<Attrs> extends Component<Attrs> {
|
||||
) : extensions.length || core ? (
|
||||
<div className="PackageManager-extensions">
|
||||
<div className="PackageManager-extensions-grid">
|
||||
{core ? this.extensionItem(core, true) : null}
|
||||
{extensions.map((extension: any) => this.extensionItem(extension))}
|
||||
{core ? (
|
||||
<ExtensionItem
|
||||
extension={core}
|
||||
updates={coreUpdate}
|
||||
isCore={true}
|
||||
onClickUpdate={this.updateCoreMinor.bind(this)}
|
||||
whyNotWarning={this.lastUpdateRun.limitedPackages().includes('flarum/core')}
|
||||
/>
|
||||
) : null}
|
||||
{extensions.map((extension: Extension) => (
|
||||
<ExtensionItem
|
||||
extension={extension}
|
||||
updates={this.packageUpdates[extension.id]}
|
||||
onClickUpdate={this.updateExtension.bind(this, extension)}
|
||||
whyNotWarning={this.lastUpdateRun.limitedPackages().includes(extension.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>,
|
||||
coreUpdate && coreUpdate['latest-major'] ? <MajorUpdater coreUpdate={coreUpdate} /> : null,
|
||||
coreUpdate && coreUpdate['latest-major'] ? <MajorUpdater coreUpdate={coreUpdate} updateState={this.lastUpdateRun.major} /> : null,
|
||||
];
|
||||
}
|
||||
|
||||
extensionItem(extension: Extension, isCore: boolean = false) {
|
||||
return (
|
||||
<div
|
||||
className={classList({
|
||||
'PackageManager-extension': true,
|
||||
'PackageManager-extension--core': isCore,
|
||||
})}
|
||||
>
|
||||
<div className="PackageManager-extension-icon ExtensionIcon" style={extension.icon}>
|
||||
{extension.icon ? icon(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(extension.version)}</span>
|
||||
{this.packageUpdates[extension.id]['latest-minor'] ? (
|
||||
<span className="PackageManager-extension-version-latest PackageManager-extension-version-latest--minor">
|
||||
{this.version(this.packageUpdates[extension.id]['latest-minor']!)}
|
||||
</span>
|
||||
) : null}
|
||||
{this.packageUpdates[extension.id]['latest-major'] && !isCore ? (
|
||||
<span className="PackageManager-extension-version-latest PackageManager-extension-version-latest--major">
|
||||
{this.version(this.packageUpdates[extension.id]['latest-major']!)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="PackageManager-extension-controls">
|
||||
<Tooltip text={app.translator.trans('flarum-package-manager.admin.extensions.update')}>
|
||||
<Button
|
||||
icon="fas fa-arrow-alt-circle-up"
|
||||
className="Button Button--icon Button--flat"
|
||||
onclick={isCore ? this.updateCoreMinor.bind(this) : this.updateExtension.bind(this, extension)}
|
||||
aria-label={app.translator.trans('flarum-package-manager.admin.extensions.update')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
version(v: string) {
|
||||
return 'v' + v.replace('v', '');
|
||||
}
|
||||
|
||||
getExtensionUpdates() {
|
||||
getExtensionUpdates(): Extension[] {
|
||||
this.lastUpdateCheck?.updates?.installed?.filter((composerPackage: UpdatedPackage) => {
|
||||
const extension = app.data.extensions[composerPackage.name.replace('/', '-').replace(/(flarum-ext-)|(flarum-)/, '')];
|
||||
const id = composerPackage.name.replace('/', '-').replace(/(flarum-ext-)|(flarum-)/, '');
|
||||
|
||||
const extension = app.data.extensions[id];
|
||||
const safeToUpdate = ['semver-safe-update', 'update-possible'].includes(composerPackage['latest-status']);
|
||||
|
||||
if (extension && safeToUpdate) {
|
||||
@@ -168,7 +162,7 @@ export default class Updater<Attrs> extends Component<Attrs> {
|
||||
return extension && safeToUpdate;
|
||||
});
|
||||
|
||||
return Object.values(app.data.extensions).filter((extension: any) => this.packageUpdates[extension.id]);
|
||||
return (Object.values(app.data.extensions) as Extension[]).filter((extension: Extension) => this.packageUpdates[extension.id]);
|
||||
}
|
||||
|
||||
getCoreUpdate(): UpdatedPackage | undefined {
|
||||
|
@@ -0,0 +1,51 @@
|
||||
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';
|
||||
|
||||
export interface WhyNotModalAttrs extends IInternalModalAttrs {
|
||||
package: string;
|
||||
}
|
||||
|
||||
export default class WhyNotModal<Attrs extends WhyNotModalAttrs = WhyNotModalAttrs> extends Modal<Attrs> {
|
||||
loading: boolean = true;
|
||||
whyNot: string | null = null;
|
||||
|
||||
className() {
|
||||
return 'Modal--large WhyNotModal';
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.translator.trans('flarum-package-manager.admin.why_not_modal.title');
|
||||
}
|
||||
|
||||
oncreate(vnode: Mithril.VnodeDOM<Attrs, this>) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
this.requestWhyNot();
|
||||
}
|
||||
|
||||
content() {
|
||||
return <div className="Modal-body">{this.loading ? <LoadingIndicator /> : <pre className="WhyNotModal-contents">{this.whyNot}</pre>}</div>;
|
||||
}
|
||||
|
||||
requestWhyNot(): void {
|
||||
app
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: `${app.forum.attribute('apiUrl')}/package-manager/why-not`,
|
||||
body: {
|
||||
data: {
|
||||
package: this.attrs.package,
|
||||
},
|
||||
},
|
||||
errorHandler,
|
||||
})
|
||||
.then((response: any) => {
|
||||
this.loading = false;
|
||||
this.whyNot = response.data.whyNot;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}
|
@@ -10,8 +10,10 @@ export default function (e: any) {
|
||||
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-package-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'));
|
||||
}
|
||||
break;
|
||||
|
||||
|
Reference in New Issue
Block a user