Skip to content

Commit

Permalink
feat: extension bisect (#3980)
Browse files Browse the repository at this point in the history
* feat: extension bisect
* Apply fixes from StyleCI
* chore: review
* Apply suggestions from code review
* feat: add stop bisect button
* feat: redirect to result extension page

Co-authored-by: Alexander Skvortsov <[email protected]>
  • Loading branch information
SychO9 and askvortsov1 authored May 3, 2024
1 parent e0025df commit b02f819
Show file tree
Hide file tree
Showing 20 changed files with 685 additions and 4 deletions.
22 changes: 22 additions & 0 deletions framework/core/js/src/admin/components/AdvancedPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import FormSectionGroup, { FormSection } from './FormSectionGroup';
import ItemList from '../../common/utils/ItemList';
import InfoTile from '../../common/components/InfoTile';
import { MaintenanceMode } from '../../common/Application';
import Button from '../../common/components/Button';
import classList from '../../common/utils/classList';
import ExtensionBisect from './ExtensionBisect';

export default class AdvancedPage<CustomAttrs extends IPageAttrs = IPageAttrs> extends AdminPage<CustomAttrs> {
searchDriverOptions: Record<string, Record<string, string>> = {};
urlRequestedModalHasBeenShown = false;

oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);
Expand All @@ -36,6 +40,11 @@ export default class AdvancedPage<CustomAttrs extends IPageAttrs = IPageAttrs> e
}

content() {
if (m.route.param('modal') === 'extension-bisect' && !this.urlRequestedModalHasBeenShown) {
this.urlRequestedModalHasBeenShown = true;
setTimeout(() => app.modal.show(ExtensionBisect), 150);
}

return [
<Form className="AdvancedPage-container">
<FormSectionGroup>{this.sectionItems().toArray()}</FormSectionGroup>
Expand Down Expand Up @@ -104,6 +113,7 @@ export default class AdvancedPage<CustomAttrs extends IPageAttrs = IPageAttrs> e
help: app.translator.trans('core.admin.advanced.maintenance.help'),
setting: 'maintenance_mode',
refreshAfterSaving: true,
disabled: app.data.bisecting,
options: {
[MaintenanceMode.NO_MAINTENANCE]: app.translator.trans('core.admin.advanced.maintenance.options.' + MaintenanceMode.NO_MAINTENANCE),
[MaintenanceMode.HIGH_MAINTENANCE]: {
Expand Down Expand Up @@ -161,6 +171,18 @@ export default class AdvancedPage<CustomAttrs extends IPageAttrs = IPageAttrs> e
<strong className="helpText">{app.translator.trans('core.admin.advanced.maintenance.options.' + app.data.maintenanceMode)}</strong>
</div>
) : null}
<div className="Form-group">
<label>{app.translator.trans('core.admin.advanced.maintenance.bisect.label')}</label>
<p className="helpText">{app.translator.trans('core.admin.advanced.maintenance.bisect.help')}</p>
<Button
className={classList('Button', { 'Button--warning': app.data.bisecting })}
onclick={() => app.modal.show(ExtensionBisect)}
disabled={app.data.maintenanceMode && app.data.maintenanceMode !== MaintenanceMode.LOW_MAINTENANCE}
icon="fas fa-bug"
>
{app.translator.trans('core.admin.advanced.maintenance.bisect.' + (app.data.bisecting ? 'continue_button_text' : 'begin_button_text'))}
</Button>
</div>
</Form>
</FormSection>
);
Expand Down
20 changes: 20 additions & 0 deletions framework/core/js/src/admin/components/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ export default class DashboardPage extends AdminPage {
availableWidgets(): ItemList<Children> {
const items = new ItemList<Children>();

if (app.data.bisecting) {
items.add(
'bisecting',
<AlertWidget
alert={{
type: 'error',
dismissible: false,
controls: [
<Link className="Button Button--link" href={app.route('advanced', { modal: 'extension-bisect' })}>
{app.translator.trans('core.lib.notices.bisecting_continue')}
</Link>,
],
}}
>
{app.translator.trans('core.lib.notices.bisecting')}
</AlertWidget>,
120
);
}

if (app.data.maintenanceMode) {
items.add(
'maintenanceMode',
Expand Down
166 changes: 166 additions & 0 deletions framework/core/js/src/admin/components/ExtensionBisect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import Modal, { IDismissibleOptions, type IInternalModalAttrs } from '../../common/components/Modal';
import Mithril from 'mithril';
import app from '../app';
import Button from '../../common/components/Button';
import Form from '../../common/components/Form';
import Stream from '../../common/utils/Stream';
import Icon from '../../common/components/Icon';

type BisectResult = {
stepsLeft: number;
relevantEnabled: string[];
relevantDisabled: string[];
extension: string | null;
};

export default class ExtensionBisect<CustomAttrs extends IInternalModalAttrs = IInternalModalAttrs> extends Modal<CustomAttrs> {
private result = Stream<BisectResult | null>(null);
private bisecting = Stream<boolean>(app.data.bisecting || false);

protected static readonly isDismissibleViaCloseButton: boolean = true;
protected static readonly isDismissibleViaEscKey: boolean = false;
protected static readonly isDismissibleViaBackdropClick: boolean = false;

protected get dismissibleOptions(): IDismissibleOptions {
return {
viaCloseButton: !this.bisecting(),
viaEscKey: !this.bisecting(),
viaBackdropClick: !this.bisecting(),
};
}

oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);

if (m.route.param('modal') !== 'extension-bisect') {
window.history.replaceState({}, '', window.location.pathname + '#' + app.route('advanced', { modal: 'extension-bisect' }));
}
}

className(): string {
return 'ExtensionBisectModal Modal--small';
}

title(): Mithril.Children {
return app.translator.trans('core.admin.advanced.maintenance.bisect_modal.title');
}

content(): Mithril.Children {
const result = this.result();

if (result && result.extension) {
const extension = app.data.extensions[result.extension];

return (
<div className="Modal-body">
<Form className="Form--centered">
<p className="helpText">{app.translator.trans('core.admin.advanced.maintenance.bisect_modal.result_description')}</p>
<div className="ExtensionBisectModal-extension">
<div className="ExtensionBisectModal-extension-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? <Icon name={extension.icon.name} /> : ''}
</div>
<div className="ExtensionBisectModal-extension-info">
<div className="ExtensionBisectModal-extension-name">{extension.extra['flarum-extension'].title}</div>
<div className="ExtensionBisectModal-extension-version">
<span className="ExtensionBisectModal-extension-version">{extension.version}</span>
</div>
</div>
</div>
<Button className="Button Button--primary" onclick={() => this.hide(extension.id)}>
{app.translator.trans('core.admin.advanced.maintenance.bisect_modal.end_button')}
</Button>
</Form>
</div>
);
}

return (
<div className="Modal-body">
<Form className="Form--centered">
<p className="helpText">{app.translator.trans('core.admin.advanced.maintenance.bisect_modal.description')}</p>
<p className="helpText">
{app.translator.trans('core.admin.advanced.maintenance.bisect_modal.' + (this.bisecting() ? 'steps_left' : 'total_steps'), {
steps: this.stepsLeft(),
})}
</p>
{this.bisecting() ? (
<div className="Form-group">
<label>{app.translator.trans('core.admin.advanced.maintenance.bisect_modal.issue_question')}</label>
<p className="helpText">{app.translator.trans('core.admin.advanced.maintenance.bisect_modal.issue_question_help')}</p>
</div>
) : null}
{this.bisecting() ? (
<>
<div className="ButtonGroup ButtonGroup--block">
<Button className="Button Button--danger" onclick={() => this.submit(true)} loading={this.loading}>
{app.translator.trans('core.admin.advanced.maintenance.bisect_modal.yes_button')}
</Button>
<Button className="Button Button--success" onclick={() => this.submit(false)} loading={this.loading}>
{app.translator.trans('core.admin.advanced.maintenance.bisect_modal.no_button')}
</Button>
</div>
<Button className="Button Button--primary Button--block" onclick={() => this.submit(null, true)} loading={this.loading}>
{app.translator.trans('core.admin.advanced.maintenance.bisect_modal.stop_button')}
</Button>
</>
) : (
<Button className="Button Button--primary Button--block" onclick={() => this.submit(true)} loading={this.loading}>
{app.translator.trans('core.admin.advanced.maintenance.bisect_modal.start_button')}
</Button>
)}
</Form>
</div>
);
}

stepsLeft(): number {
if (this.result()) {
return this.result()!.stepsLeft;
}

let steps;

if (this.bisecting()) {
const { low, high } = JSON.parse(app.data.settings.extension_bisect_state);
steps = Math.ceil(Math.log2(high - low)) + 1;
} else {
steps = Math.ceil(Math.log2(JSON.parse(app.data.settings.extensions_enabled).length)) + 1;
}

return steps;
}

submit(issue: boolean | null, end: boolean = false) {
this.loading = true;
m.redraw();

app
.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/extension-bisect',
body: { issue, end },
})
.then((response) => {
this.loading = false;
this.bisecting(!end);
this.result(response as BisectResult);
m.redraw();

if (end) {
this.hide();
}
});
}

hide(extension?: string) {
this.attrs.animateHide(() => {
if (extension) {
m.route.set(app.route('extension', { id: extension }));
} else {
m.route.set(app.route('advanced'));
}

window.location.reload();
});
}
}
1 change: 1 addition & 0 deletions framework/core/js/src/common/Application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export interface ApplicationData {
resources: SavedModelData[];
session: { userId: number; csrfToken: string };
maintenanceMode?: MaintenanceMode;
bisecting?: boolean;
[key: string]: unknown;
}

Expand Down
2 changes: 1 addition & 1 deletion framework/core/js/src/common/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export default abstract class Modal<ModalAttrs extends IInternalModalAttrs = IIn
m.redraw();
}

private get dismissibleOptions(): IDismissibleOptions {
protected get dismissibleOptions(): IDismissibleOptions {
return (this.constructor as typeof Modal).dismissibleOptions;
}
}
23 changes: 22 additions & 1 deletion framework/core/js/src/forum/components/Notices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,33 @@ export default class Notices extends Component {
);
}

if (app.data.bisecting) {
items.add(
'bisecting',
<Alert
type="error"
dismissible={false}
className="Alert--bisecting"
containerClassName="container"
controls={[
<a className="Button Button--link" target="_blank" href={app.forum.attribute('adminUrl') + '#/advanced?modal=extension-bisect'}>
{app.translator.trans('core.lib.notices.bisecting_continue')}
</a>,
]}
>
{app.translator.trans('core.lib.notices.bisecting')}
</Alert>,
90
);
}

if (app.data.maintenanceMode) {
items.add(
'maintenanceMode',
<Alert type="error" dismissible={false} className="Alert--maintenanceMode" containerClassName="container">
{app.translator.trans('core.lib.notices.maintenance_mode_' + app.data.maintenanceMode)}
</Alert>
</Alert>,
80
);
}

Expand Down
1 change: 1 addition & 0 deletions framework/core/less/admin.less
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

@import "admin/AdminHeader";
@import "admin/AdminNav";
@import "admin/AdvancedPage";
@import "admin/CreateUserModal";
@import "admin/DashboardPage";
@import "admin/AlertWidget";
Expand Down
14 changes: 14 additions & 0 deletions framework/core/less/admin/AdvancedPage.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.ExtensionBisectModal-extension {
padding: 2rem 0;
border-radius: var(--border-radius);
background-color: var(--body-bg);
}

.ExtensionBisectModal-extension-icon {
--size: 60px;
}

.ExtensionBisectModal-extension-name {
font-size: 15px;
margin: 1rem 0 0;
}
19 changes: 19 additions & 0 deletions framework/core/less/common/Button.less
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,26 @@
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

.Form--centered & {
margin-left: 0;
margin-right: 0;
}
}

&, & > .Button {
max-width: 100%;
}
}

.ButtonGroup--block {
width: 100%;

.Button {
flex-grow: 1;
}
}

//
// Buttons
// --------------------------------------------------
Expand Down Expand Up @@ -174,6 +187,12 @@
.Button--danger {
.Button--color-auto('control-danger');
}
.Button--success {
.Button--color-auto('control-success');
}
.Button--warning {
.Button--color-auto('control-warning');
}
.Button--more {
padding: 2px 4px;
line-height: 1;
Expand Down
6 changes: 6 additions & 0 deletions framework/core/less/common/root.less
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
--control-color: @control-color;
--control-danger-bg: @control-danger-bg;
--control-danger-color: @control-danger-color;
--control-success-bg: @control-success-bg;
--control-success-color: @control-success-color;
--control-warning-bg: @control-warning-bg;
--control-warning-color: @control-warning-color;
--control-body-bg-mix: mix(@control-bg, @body-bg, 50%);
--control-muted-color: lighten(@control-color, 40%);

Expand Down Expand Up @@ -93,6 +97,8 @@
.Button--color-vars(@control-color, @control-bg, 'button');
.Button--color-vars(@body-bg, @primary-color, 'button-primary');
.Button--color-vars(@control-danger-color, @control-danger-bg, 'control-danger');
.Button--color-vars(@control-success-color, @control-success-bg, 'control-success');
.Button--color-vars(@control-warning-color, @control-warning-bg, 'control-warning');
.Button--color-vars(@muted-more-color, fade(@muted-more-color, 30%), 'muted-more');
.Button--color-vars(@control-color, @body-bg, 'button-inverted');

Expand Down
Loading

0 comments on commit b02f819

Please sign in to comment.