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

Compare commits

...

12 Commits

Author SHA1 Message Date
David Wheatley
55050914f3 Use FA icons in info box 2021-08-11 21:49:35 +02:00
David Wheatley
961390da46 Add frontend code 2021-08-11 21:42:25 +02:00
Daniel Klabbers
60300939bc wip 2021-08-11 16:52:25 +02:00
Daniel Klabbers
c1754af74a try to patch up the advanced page 2021-08-11 16:52:25 +02:00
Daniel Klabbers
731fae666f wip 2021-08-11 16:51:48 +02:00
Daniel Klabbers
6006ad00a2 wip 2021-08-11 16:51:48 +02:00
Daniel Klabbers
7cd67720d3 wip 2021-08-11 16:51:48 +02:00
luceos
65a5ed4e86 Apply fixes from StyleCI
[ci skip] [skip ci]
2021-08-11 16:51:48 +02:00
Daniel Klabbers
72780f514f further additions for queue handling with db and a advanced pane 2021-08-11 16:51:48 +02:00
Daniel Klabbers
93dbd4ec86 rename vars in use 2021-08-11 16:51:48 +02:00
luceos
df2c4323ff Apply fixes from StyleCI
[ci skip] [skip ci]
2021-08-11 16:48:06 +02:00
Daniel Klabbers
06e5922be5 wip 2021-08-11 16:48:06 +02:00
16 changed files with 444 additions and 16 deletions

View File

@@ -34,6 +34,7 @@ import EditCustomCssModal from './components/EditCustomCssModal';
import EditGroupModal from './components/EditGroupModal';
import routes from './routes';
import AdminApplication from './AdminApplication';
import AdvancedPage from './components/AdvancedPage';
export default Object.assign(compat, {
'utils/saveSettings': saveSettings,
@@ -68,6 +69,7 @@ export default Object.assign(compat, {
'components/AdminHeader': AdminHeader,
'components/EditCustomCssModal': EditCustomCssModal,
'components/EditGroupModal': EditGroupModal,
'components/AdvancedPage': AdvancedPage,
routes: routes,
AdminApplication: AdminApplication,
});

View File

@@ -75,6 +75,16 @@ export default class AdminNav extends Component {
</LinkButton>
);
// We only display the advanced pane when a certain threshold is reached or it is manually activated.
if (app.data.settings['advanced_settings_pane_enabled']) {
items.add(
'advanced',
<LinkButton href={app.route('advanced')} icon="fas fa-rocket" title={app.translator.trans('core.admin.nav.advanced_title')}>
{app.translator.trans('core.admin.nav.advanced_button')}
</LinkButton>
);
}
items.add(
'mail',
<LinkButton href={app.route('mail')} icon="fas fa-envelope" title={app.translator.trans('core.admin.nav.email_title')}>

View File

@@ -0,0 +1,40 @@
import FieldSet from '../../common/components/FieldSet';
import ItemList from '../../common/utils/ItemList';
import AdminPage from './AdminPage';
import Alert from '../../common/components/Alert';
export default class AdvancedPage extends AdminPage {
oninit(vnode) {
super.oninit(vnode);
this.queueDrivers = {};
app.data.queueDrivers.forEach((driver) => {
this.queueDrivers[driver] = app.translator.trans('core.admin.queue.' + driver);
});
}
headerInfo() {
return {
className: 'AdvancedPage',
icon: 'fas fa-rocket',
title: app.translator.trans('core.admin.advanced.title'),
description: app.translator.trans('core.admin.advanced.description'),
};
}
content() {
return [
<div className="Form">
{this.buildSettingComponent({
type: 'select',
setting: 'queue_driver',
options: Object.keys(this.queueDrivers).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
label: app.translator.trans('core.admin.queue.driver_heading'),
className: 'AdvancedPage-QueueSettings',
})}
{this.submitButton()}
</div>,
];
}
}

View File

@@ -0,0 +1,158 @@
import Mithril from 'mithril';
import Link from '../../common/components/Link';
import classList from '../../common/utils/classList';
import ItemList from '../../common/utils/ItemList';
import app from '../app';
import AdminPage from './AdminPage';
export interface IAdvancedPageAttrs extends Mithril.Attributes {}
export interface ICreateDriverComponentOptions<Options extends string[]> {
/**
* The default driver value.
*
* This will appear selected if the driver is not specified.
*/
defaultValue: Options[number];
/**
* Custom class to apply to the `<select>` component.
*
* This is applied in addition to the default `AdvancedPage-driverSelect` class.
*/
className: string;
}
export default class AdvancedPage extends AdminPage {
oninit(vnode: Mithril.Vnode<IAdvancedPageAttrs, this>) {
super.oninit(vnode);
}
headerInfo() {
return {
className: 'AdvancedPage',
icon: 'fas fa-rocket',
title: app.translator.trans('core.admin.advanced.title'),
description: app.translator.trans('core.admin.advanced.description'),
};
}
content() {
return (
<>
<form class="Form">{this.items().toArray()}</form>
</>
);
}
items(): ItemList {
const items = new ItemList();
if (!app.data.settings.advanced_settings_pane_enabled) {
items.add(
'page_not_enabled',
// TODO: Add link to docs page
<p class="AdvancedPage-notEnabledWarning">
{app.translator.trans('core.admin.advanced.not_enabled_warning', {
a: <Link external href="https://docs.flarum.org/" />,
icon: <span aria-label={app.translator.trans('core.admin.advanced.warning_icon_accessible_label')} class="fas fa-exclamation-triangle" />,
})}
</p>,
110
);
} else {
items.add(
'large_community_text',
// TODO: Add link to docs page
<p class="AdvancedPage-congratsText">
{app.translator.trans('core.admin.advanced.large_community_note', {
a: <Link external href="https://docs.flarum.org/" />,
icon: <span aria-label={app.translator.trans('core.admin.advanced.info_icon_accessible_label')} class="fas fa-info-circle" />,
})}
</p>,
110
);
}
items.add(
'drivers',
<fieldset class="Form-group AdvancedPage-category">
<legend>{app.translator.trans('core.admin.advanced.drivers.legend')}</legend>
{this.drivers().toArray()}
</fieldset>,
90
);
items.add('save', this.submitButton(), -10);
return items;
}
drivers(): ItemList {
const items = new ItemList();
items.add(
'queueDriver',
this.createDriverComponent('queue_driver', 'core.admin.advanced.drivers.queue', app.data.queueDrivers, {
className: 'AdvancedPage-queueDriver',
defaultValue: 'database',
}),
100
);
return items;
}
/**
* Build a form component for a given driver.
*
* Requires the follow translations under the given prefix:
* - `driver_heading` (shown as legend for the form group)
* - `driver_label` (shown as the label for the select box)
* - `names.{driver_id}` (shown as the options for the select box)
*
* @param settingKey The setting key for the driver.
* @param driverTranslatorPrefix The prefix used for translations.
* @param driverOptions An array of possible driver values.
* @param options Optional settings for the component.
*
* @example <caption>Queue driver</caption>
* this.createDriverComponent(
* 'queue_driver',
* 'core.admin.advanced.drivers.queue',
* [ 'database', 'sync' ],
* },
* {
* defaultValue: 'database',
* },
* );
*/
createDriverComponent<Options extends string[]>(
settingKey: string,
driverTranslatorPrefix: string,
driverOptions: Options,
options: Partial<ICreateDriverComponentOptions<Options>> = {}
): JSX.Element {
return (
<fieldset class="Form-group">
<legend>{app.translator.trans(`${driverTranslatorPrefix}.driver_heading`)}</legend>
{this.buildSettingComponent({
type: 'select',
setting: settingKey,
options: driverOptions.reduce(
(acc, value) => ({
...acc,
[value]: app.translator.trans(`${driverTranslatorPrefix}.names.${value}`),
}),
{} as Record<Options[number], ReturnType<typeof app.translator.trans>>
),
default: options.defaultValue,
label: app.translator.trans(`${driverTranslatorPrefix}.driver_label`),
className: classList('AdvancedPage-driverSelect', options.className),
})}
</fieldset>
);
}
}

View File

@@ -1,4 +1,5 @@
import DashboardPage from './components/DashboardPage';
import AdvancedPage from './components/AdvancedPage';
import BasicsPage from './components/BasicsPage';
import PermissionsPage from './components/PermissionsPage';
import AppearancePage from './components/AppearancePage';
@@ -8,16 +9,18 @@ import ExtensionPage from './components/ExtensionPage';
import ExtensionPageResolver from './resolvers/ExtensionPageResolver';
/**
* The `routes` initializer defines the forum app's routes.
* The `routes` initializer defines the admin app's routes.
*
* @param {App} app
* @param {import('./app').default} app
*/
export default function (app) {
app.routes = {
dashboard: { path: '/', component: DashboardPage },
basics: { path: '/basics', component: BasicsPage },
advanced: { path: '/advanced', component: AdvancedPage },
permissions: { path: '/permissions', component: PermissionsPage },
appearance: { path: '/appearance', component: AppearancePage },
advanced: { path: '/advanced', component: AdvancedPage },
mail: { path: '/mail', component: MailPage },
users: { path: '/users', component: UserListPage },
extension: { path: '/extension/:id', component: ExtensionPage, resolverClass: ExtensionPageResolver },

View File

@@ -11,3 +11,4 @@
@import "admin/AppearancePage";
@import "admin/MailPage";
@import "admin/UsersListPage.less";
@import "admin/AdvancedPage.less";

View File

@@ -0,0 +1,33 @@
.AdvancedPage {
&-notEnabledWarning,
&-congratsText {
padding: 8px 8px 8px 12px;
max-width: 600px;
}
&-notEnabledWarning {
border-left: 8px solid @error-color;
background: fade(@error-color, 5%);
}
&-congratsText {
border-left: 8px solid @control-color;
}
&-category {
&:first-of-type {
margin-top: 16px;
}
> legend {
font-size: 1.25rem;
}
fieldset {
> legend {
font-size: 1.1rem;
margin-bottom: 4px;
}
}
}
}

View File

@@ -6,6 +6,35 @@ core:
# Translations in this namespace are used by the admin interface.
admin:
advanced:
description: Settings relating to Flarum's scalability.
drivers:
legend: Drivers
queue:
driver_heading: Queue Driver
driver_label: Choose a Queue Driver
names:
database: Database
sync: Sync
info_icon_accessible_label: information symbol
# Shown on the page when it's meant to be enabled
large_community_note: |
<icon></icon> This page is intended for very active communities, like yours! Great job! Modifying
some of these settings may require advanced setup or create extra load for your
installation. <a>Learn more about these settings.</a>
# Shown on the page when it's hidden and was directly navigated to
not_enabled_warning: |
<icon></icon> This page is intended for very active communities. This page is hidden for your
forum because you don't meet the recommended criteria for modifying these settings.
Doing so may require advanced setup or create extra load for your installation.
<a>Learn more about these settings.</a>
title: Advanced settings
warning_icon_accessible_label: warning symbol
# These translations are used in the Appearance page.
appearance:
@@ -143,6 +172,8 @@ core:
# These translations are used in the navigation bar.
nav:
advanced_button: => core.admin.advanced.title
advanced_title: => core.admin.advanced.description
appearance_button: => core.admin.appearance.title
appearance_title: => core.admin.appearance.description
basics_button: => core.admin.basics.title

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'queue_failed_jobs',
function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection')->nullable();
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
}
);

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
return Migration::createTable(
'queue_jobs',
function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
}
);

View File

@@ -79,6 +79,7 @@ class AdminPayload
$document->payload['slugDrivers'] = array_map(function ($resourceDrivers) {
return array_keys($resourceDrivers);
}, $this->container->make('flarum.http.slugDrivers'));
$document->payload['queueDrivers'] = array_keys($this->container->make('flarum.queue.supported_drivers'));
$document->payload['phpVersion'] = PHP_VERSION;
$document->payload['mysqlVersion'] = $this->db->selectOne('select version() as version')->version;

View File

@@ -112,7 +112,7 @@ class ApiServiceProvider extends AbstractServiceProvider
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\AuthenticateWithHeader::class,
HttpMiddleware\CheckCsrfToken::class,
HttpMiddleware\RememberFromCookie::class,
// HttpMiddleware\RememberFromCookie::class,
];
});

View File

@@ -14,6 +14,7 @@ use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Capsule\Manager;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\DatabaseTransactionsManager;
class DatabaseServiceProvider extends AbstractServiceProvider
{
@@ -63,6 +64,10 @@ class DatabaseServiceProvider extends AbstractServiceProvider
$this->container->singleton('flarum.database.model_private_checkers', function () {
return [];
});
$this->container->singleton('db.transactions', function () {
return new DatabaseTransactionsManager;
});
}
public function boot(Container $container)

View File

@@ -131,6 +131,7 @@ class InstalledSite implements SiteInterface
$laravel->register(NotificationServiceProvider::class);
$laravel->register(PostServiceProvider::class);
$laravel->register(QueueServiceProvider::class);
$laravel->register(ScalabilityServiceProvider::class);
$laravel->register(SearchServiceProvider::class);
$laravel->register(SessionServiceProvider::class);
$laravel->register(SettingsServiceProvider::class);

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Foundation;
use Flarum\Settings\Event\Deserializing;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Queue\SyncQueue;
class ScalabilityServiceProvider extends AbstractServiceProvider
{
public function boot(Dispatcher $events, Queue $queue)
{
if ($queue instanceof SyncQueue) {
$events->listen(JobProcessing::class, [$this, 'trackQueueLoad']);
}
$events->listen(Deserializing::class, [$this, 'recommendations']);
}
public function trackQueueLoad(JobProcessing $event)
{
/** @var Repository $cache */
$cache = resolve('cache.store');
// Retrieve existing queue load.
$count = (int) $cache->get('flarum.scalability.queue-load', 0);
$count++;
// Store the queue load, but only for one minute.
$cache->set('flarum.scalability.queue-load', $count, 60);
// If within that minute 10 queue tasks were fired, we need to suggest an alternative driver.
if ($count > 10) {
/** @var SettingsRepositoryInterface $settings */
$settings = resolve(SettingsRepositoryInterface::class);
$settings->set('flarum.scalability.queue-recommended', true);
}
}
public function recommendations(Deserializing $event)
{
/** @var Config $config */
$config = resolve(Config::class);
// Toggles the advanced pane for admins.
$event->settings['advanced_settings_pane_enabled'] = $event->settings['flarum.scalability.queue-recommended']
?? $config->offsetGet('advancedSettings')
?? false;
}
}

View File

@@ -14,6 +14,7 @@ use Flarum\Foundation\Config;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
use Flarum\Foundation\Paths;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Cache\Factory as CacheFactory;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandling;
@@ -22,11 +23,13 @@ use Illuminate\Contracts\Queue\Factory;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\Connectors\ConnectorInterface;
use Illuminate\Queue\Console as Commands;
use Illuminate\Queue\DatabaseQueue;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\Failed\NullFailedJobProvider;
use Illuminate\Queue\Failed\DatabaseFailedJobProvider;
use Illuminate\Queue\Listener as QueueListener;
use Illuminate\Queue\SyncQueue;
use Illuminate\Queue\Worker;
use Illuminate\Support\Arr;
class QueueServiceProvider extends AbstractServiceProvider
{
@@ -42,6 +45,34 @@ class QueueServiceProvider extends AbstractServiceProvider
public function register()
{
$this->container->singleton('flarum.queue.supported_drivers', function () {
return [
'sync' => SyncQueue::class,
'database' => DatabaseQueue::class,
];
});
$this->container->singleton('flarum.queue.connection', function (Container $container) {
/** @var array $drivers */
$drivers = $container->make('flarum.queue.supported_drivers');
/** @var SettingsRepositoryInterface $settings */
$settings = $container->make(SettingsRepositoryInterface::class);
$driverName = $settings->get('queue_driver', 'sync');
$driverClass = Arr::get($drivers, $driverName);
/** @var Queue $driver */
$driver = $container->make($driverClass);
// This method only exists on the Laravel abstract Queue implementation, not the contract,
// for simplicity we will try to inject the container if the method is available on the driver.
if (method_exists($driver, 'setContainer')) {
$driver->setContainer($container);
}
return $driver;
});
// Register a simple connection factory that always returns the same
// connection, as that is enough for our purposes.
$this->container->singleton(Factory::class, function (Container $container) {
@@ -50,15 +81,6 @@ class QueueServiceProvider extends AbstractServiceProvider
});
});
// Extensions can override this binding if they want to make Flarum use
// a different queuing backend.
$this->container->singleton('flarum.queue.connection', function (Container $container) {
$queue = new SyncQueue;
$queue->setContainer($container);
return $queue;
});
$this->container->singleton(ExceptionHandling::class, function (Container $container) {
return new ExceptionHandler($container['log']);
});
@@ -78,7 +100,7 @@ class QueueServiceProvider extends AbstractServiceProvider
});
// Override the Laravel native Listener, so that we can ignore the environment
// option and force the binary to flarum.
// option and force the binary to Flarum.
$this->container->singleton(QueueListener::class, function (Container $container) {
return new Listener($container->make(Paths::class)->base);
});
@@ -110,10 +132,21 @@ class QueueServiceProvider extends AbstractServiceProvider
};
});
$this->container->singleton('queue.failer', function () {
return new NullFailedJobProvider();
$this->container->singleton('queue.failer', function (Container $container) {
/** @var Config $config */
$config = $container->make(Config::class);
return new DatabaseFailedJobProvider(
$container->make('db'),
$config->offsetGet('database.database'),
'queue_failed_jobs'
);
});
$this->container->when(DatabaseQueue::class)
->needs('$table')
->give('queue_jobs');
$this->container->alias('flarum.queue.connection', Queue::class);
$this->container->alias(ConnectorInterface::class, 'queue.connection');