1
0
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:
SychO9
2021-11-23 23:02:56 +01:00
parent c8b8dacb67
commit f4bb8158ef
31 changed files with 1877 additions and 1007 deletions

View File

@@ -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', '');
}
}

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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();
});
}
}

View File

@@ -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;