UX: Apply admin interface guidelines to Backups page (#28051)
This commit converts the Backups page in the admin interface to follow our new admin interface guidelines. As part of this work, I've also made `AdminPageHeader` and `AdminPageSubheader` components that can be reused on any admin page for consistency, that handle the title and action buttons and also breadcrumbs. Also renamed `AdminPluginFilteredSiteSettings` to `AdminFilteredSiteSettings` since it can be used generally to show a subset of filtered site settings, not only settings for a plugin. Not sure if it's ideal to have to define a new route for this for every config area, but not sure how else to do it right now.
This commit is contained in:
parent
5b17e85fe1
commit
1446596089
|
@ -0,0 +1,100 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import routeAction from "discourse/helpers/route-action";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class AdminBackupsActions extends Component {
|
||||
@service currentUser;
|
||||
@service site;
|
||||
@service dialog;
|
||||
|
||||
@action
|
||||
toggleReadOnlyMode() {
|
||||
if (!this.site.isReadOnly) {
|
||||
this.dialog.yesNoConfirm({
|
||||
message: I18n.t("admin.backups.read_only.enable.confirm"),
|
||||
didConfirm: () => {
|
||||
this.currentUser.set("hideReadOnlyAlert", true);
|
||||
this.#toggleReadOnlyMode(true);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.#toggleReadOnlyMode(false);
|
||||
}
|
||||
}
|
||||
|
||||
get rollbackDisabled() {
|
||||
return !this.rollbackEnabled;
|
||||
}
|
||||
|
||||
get rollbackEnabled() {
|
||||
return (
|
||||
this.args.backups.canRollback &&
|
||||
this.args.backups.restoreEnabled &&
|
||||
!this.args.backups.isOperationRunning
|
||||
);
|
||||
}
|
||||
|
||||
async #toggleReadOnlyMode(enable) {
|
||||
try {
|
||||
await ajax("/admin/backups/readonly", {
|
||||
type: "PUT",
|
||||
data: { enable },
|
||||
});
|
||||
this.site.set("isReadOnly", enable);
|
||||
} catch (err) {
|
||||
popupAjaxError(err);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if @backups.isOperationRunning}}
|
||||
<@actions.Danger
|
||||
@action={{routeAction "cancelOperation"}}
|
||||
@title="admin.backups.operations.cancel.title"
|
||||
@label="admin.backups.operations.cancel.label"
|
||||
@icon="times"
|
||||
class="admin-backups__cancel"
|
||||
/>
|
||||
{{else}}
|
||||
<@actions.Primary
|
||||
@action={{routeAction "showStartBackupModal"}}
|
||||
@title="admin.backups.operations.backup.title"
|
||||
@label="admin.backups.operations.backup.label"
|
||||
@icon="rocket"
|
||||
class="admin-backups__start"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if @backups.canRollback}}
|
||||
<@actions.Default
|
||||
@action={{routeAction "rollback"}}
|
||||
@label="admin.backups.operations.rollback.label"
|
||||
@title="admin.backups.operations.rollback.title"
|
||||
@icon="ambulance"
|
||||
@disabled={{this.rollbackDisabled}}
|
||||
class="admin-backups__rollback"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<@actions.Default
|
||||
@icon={{if this.site.isReadOnly "far-eye-slash" "far-eye"}}
|
||||
@action={{this.toggleReadOnlyMode}}
|
||||
@disabled={{@backups.isOperationRunning}}
|
||||
@title={{if
|
||||
this.site.isReadOnly
|
||||
"admin.backups.read_only.disable.title"
|
||||
"admin.backups.read_only.enable.title"
|
||||
}}
|
||||
@label={{if
|
||||
this.site.isReadOnly
|
||||
"admin.backups.read_only.disable.label"
|
||||
"admin.backups.read_only.enable.label"
|
||||
}}
|
||||
class="admin-backups__toggle-read-only"
|
||||
/>
|
||||
</template>
|
||||
}
|
|
@ -11,7 +11,7 @@ import discourseDebounce from "discourse-common/lib/debounce";
|
|||
import AdminSiteSettingsFilterControls from "admin/components/admin-site-settings-filter-controls";
|
||||
import SiteSetting from "admin/components/site-setting";
|
||||
|
||||
export default class AdminPluginFilteredSiteSettings extends Component {
|
||||
export default class AdminFilteredSiteSettings extends Component {
|
||||
@service currentUser;
|
||||
@tracked visibleSettings;
|
||||
@tracked loading = true;
|
|
@ -0,0 +1,46 @@
|
|||
import DButton from "discourse/components/d-button";
|
||||
|
||||
export const AdminPageActionButton = <template>
|
||||
<DButton
|
||||
class="admin-page-action-button btn-small"
|
||||
...attributes
|
||||
@action={{@action}}
|
||||
@label={{@label}}
|
||||
@title={{@title}}
|
||||
@icon={{@icon}}
|
||||
@isLoading={{@isLoading}}
|
||||
/>
|
||||
</template>;
|
||||
export const PrimaryButton = <template>
|
||||
<AdminPageActionButton
|
||||
class="btn-primary"
|
||||
...attributes
|
||||
@action={{@action}}
|
||||
@label={{@label}}
|
||||
@title={{@title}}
|
||||
@icon={{@icon}}
|
||||
@isLoading={{@isLoading}}
|
||||
/>
|
||||
</template>;
|
||||
export const DangerButton = <template>
|
||||
<AdminPageActionButton
|
||||
class="btn-danger"
|
||||
...attributes
|
||||
@action={{@action}}
|
||||
@label={{@label}}
|
||||
@title={{@title}}
|
||||
@icon={{@icon}}
|
||||
@isLoading={{@isLoading}}
|
||||
/>
|
||||
</template>;
|
||||
export const DefaultButton = <template>
|
||||
<AdminPageActionButton
|
||||
class="btn-default"
|
||||
...attributes
|
||||
@action={{@action}}
|
||||
@label={{@label}}
|
||||
@title={{@title}}
|
||||
@icon={{@icon}}
|
||||
@isLoading={{@isLoading}}
|
||||
/>
|
||||
</template>;
|
|
@ -0,0 +1,52 @@
|
|||
import { hash } from "@ember/helper";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import DBreadcrumbsContainer from "discourse/components/d-breadcrumbs-container";
|
||||
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
|
||||
import HorizontalOverflowNav from "discourse/components/horizontal-overflow-nav";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import {
|
||||
DangerButton,
|
||||
DefaultButton,
|
||||
PrimaryButton,
|
||||
} from "admin/components/admin-page-action-button";
|
||||
|
||||
const AdminPageHeader = <template>
|
||||
<div class="admin-page-header">
|
||||
<div class="admin-page-header__breadcrumbs">
|
||||
<DBreadcrumbsContainer />
|
||||
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
|
||||
{{yield to="breadcrumbs"}}
|
||||
</div>
|
||||
|
||||
<div class="admin-page-header__title-row">
|
||||
{{#if @titleLabel}}
|
||||
<h1 class="admin-page-header__title">{{i18n @titleLabel}}</h1>
|
||||
{{/if}}
|
||||
<div class="admin-page-header__actions">
|
||||
{{yield
|
||||
(hash Primary=PrimaryButton Default=DefaultButton Danger=DangerButton)
|
||||
to="actions"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if @descriptionLabel}}
|
||||
<p class="admin-page-header__description">
|
||||
{{i18n @descriptionLabel}}
|
||||
{{#if @learnMoreUrl}}
|
||||
{{htmlSafe (i18n "learn_more_with_link" url=@learnMoreUrl)}}
|
||||
{{/if}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#unless @hideTabs}}
|
||||
<div class="admin-nav-submenu">
|
||||
<HorizontalOverflowNav class="admin-nav-submenu__tabs">
|
||||
{{yield to="tabs"}}
|
||||
</HorizontalOverflowNav>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</template>;
|
||||
|
||||
export default AdminPageHeader;
|
|
@ -0,0 +1,23 @@
|
|||
import { hash } from "@ember/helper";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import {
|
||||
DangerButton,
|
||||
DefaultButton,
|
||||
PrimaryButton,
|
||||
} from "admin/components/admin-page-action-button";
|
||||
|
||||
const AdminPageSubheader = <template>
|
||||
<div class="admin-page-subheader">
|
||||
<div class="admin-page-subheader__title-row">
|
||||
<h3 class="admin-page-subheader__title">{{i18n @titleLabel}}</h3>
|
||||
<div class="admin-page-subheader__actions">
|
||||
{{yield
|
||||
(hash Primary=PrimaryButton Default=DefaultButton Danger=DangerButton)
|
||||
to="actions"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>;
|
||||
|
||||
export default AdminPageSubheader;
|
|
@ -3,9 +3,9 @@ import i18n from "discourse-common/helpers/i18n";
|
|||
const AdminPluginConfigMetadata = <template>
|
||||
<div class="admin-plugin-config-page__metadata">
|
||||
<div class="admin-plugin-config-area__metadata-title">
|
||||
<h2>
|
||||
<h1>
|
||||
{{@plugin.nameTitleized}}
|
||||
</h2>
|
||||
</h1>
|
||||
<p>
|
||||
{{@plugin.about}}
|
||||
{{#if @plugin.linkUrl}}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { action } from "@ember/object";
|
|||
import { alias, equal } from "@ember/object/computed";
|
||||
import { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { i18n, setting } from "discourse/lib/computed";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
|
@ -28,32 +29,21 @@ export default class AdminBackupsIndexController extends Controller {
|
|||
}
|
||||
|
||||
@action
|
||||
toggleReadOnlyMode() {
|
||||
if (!this.site.get("isReadOnly")) {
|
||||
this.dialog.yesNoConfirm({
|
||||
message: I18n.t("admin.backups.read_only.enable.confirm"),
|
||||
didConfirm: () => {
|
||||
this.set("currentUser.hideReadOnlyAlert", true);
|
||||
this._toggleReadOnlyMode(true);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this._toggleReadOnlyMode(false);
|
||||
async download(backup) {
|
||||
try {
|
||||
await ajax(`/admin/backups/${backup.filename}`, { type: "PUT" });
|
||||
this.dialog.alert(I18n.t("admin.backups.operations.download.alert"));
|
||||
} catch (err) {
|
||||
popupAjaxError(err);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
download(backup) {
|
||||
const link = backup.get("filename");
|
||||
ajax(`/admin/backups/${link}`, { type: "PUT" }).then(() =>
|
||||
this.dialog.alert(I18n.t("admin.backups.operations.download.alert"))
|
||||
);
|
||||
}
|
||||
@discourseComputed("status.isOperationRunning")
|
||||
deleteTitle() {
|
||||
if (this.status.isOperationRunning) {
|
||||
return "admin.backups.operations.is_running";
|
||||
}
|
||||
|
||||
_toggleReadOnlyMode(enable) {
|
||||
ajax("/admin/backups/readonly", {
|
||||
type: "PUT",
|
||||
data: { enable },
|
||||
}).then(() => this.site.set("isReadOnly", enable));
|
||||
return "admin.backups.operations.destroy.title";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
export default class AdminBackupsSettingsController extends Controller {
|
||||
filter = "";
|
||||
|
||||
@action
|
||||
filterChanged(filterData) {
|
||||
this.set("filter", filterData.filter);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,3 @@
|
|||
import Controller from "@ember/controller";
|
||||
import { and, not } from "@ember/object/computed";
|
||||
export default class AdminBackupsController extends Controller {
|
||||
@not("model.isOperationRunning") noOperationIsRunning;
|
||||
@not("rollbackEnabled") rollbackDisabled;
|
||||
@and("model.canRollback", "model.restoreEnabled", "noOperationIsRunning")
|
||||
rollbackEnabled;
|
||||
}
|
||||
|
||||
export default class AdminBackupsController extends Controller {}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import Route from "@ember/routing/route";
|
||||
import SiteSetting from "admin/models/site-setting";
|
||||
|
||||
export default class AdminBackupsSettingsRoute extends Route {
|
||||
queryParams = {
|
||||
filter: { replace: true },
|
||||
};
|
||||
|
||||
async model(params) {
|
||||
return {
|
||||
settings: await SiteSetting.findAll({ categories: ["backups"] }),
|
||||
initialFilter: params.filter,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -20,6 +20,10 @@ export default class AdminBackupsRoute extends DiscourseRoute {
|
|||
@service messageBus;
|
||||
@service modal;
|
||||
|
||||
titleToken() {
|
||||
return I18n.t("admin.backups.title");
|
||||
}
|
||||
|
||||
activate() {
|
||||
this.messageBus.subscribe(LOG_CHANNEL, this.onMessage);
|
||||
}
|
||||
|
|
|
@ -11,8 +11,10 @@ export default class AdminPluginsShowSettingsRoute extends Route {
|
|||
|
||||
async model(params) {
|
||||
const plugin = this.modelFor("adminPlugins.show");
|
||||
const settings = await SiteSetting.findAll({ plugin: plugin.name });
|
||||
|
||||
return { plugin, settings, initialFilter: params.filter };
|
||||
return {
|
||||
plugin,
|
||||
settings: await SiteSetting.findAll({ plugin: plugin.name }),
|
||||
initialFilter: params.filter,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,6 +142,7 @@ export default function () {
|
|||
{ path: "/backups", resetNamespace: true },
|
||||
function () {
|
||||
this.route("logs");
|
||||
this.route("settings");
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -1,46 +1,26 @@
|
|||
<div class="backup-options">
|
||||
{{#if this.localBackupStorage}}
|
||||
<UppyBackupUploader
|
||||
@done={{route-action "uploadSuccess"}}
|
||||
@localBackupStorage={{this.localBackupStorage}}
|
||||
/>
|
||||
{{else}}
|
||||
<UppyBackupUploader @done={{route-action "remoteUploadSuccess"}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.site.isReadOnly}}
|
||||
<DButton
|
||||
@icon="far-eye"
|
||||
@action={{this.toggleReadOnlyMode}}
|
||||
@disabled={{this.status.isOperationRunning}}
|
||||
@title="admin.backups.read_only.disable.title"
|
||||
@label="admin.backups.read_only.disable.label"
|
||||
class="btn-default"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@icon="far-eye"
|
||||
@action={{this.toggleReadOnlyMode}}
|
||||
@disabled={{this.status.isOperationRunning}}
|
||||
@title="admin.backups.read_only.enable.title"
|
||||
@label="admin.backups.read_only.enable.label"
|
||||
class="btn-default"
|
||||
/>
|
||||
{{/if}}
|
||||
<LinkTo @route="adminSiteSettingsCategory" @model="backups">
|
||||
{{i18n "admin.backups.settings"}}
|
||||
</LinkTo>
|
||||
|
||||
<div class="backup-message">
|
||||
{{#if this.status.restoreDisabled}}
|
||||
<a href="site_settings/category/all_results?filter=allow_restore">{{d-icon
|
||||
"info-circle"
|
||||
}}
|
||||
{{i18n "admin.backups.operations.restore.is_disabled"}}</a>
|
||||
<AdminPageSubheader @titleLabel="admin.backups.files_title">
|
||||
<:actions>
|
||||
{{#if this.localBackupStorage}}
|
||||
<UppyBackupUploader
|
||||
@done={{route-action "uploadSuccess"}}
|
||||
@localBackupStorage={{this.localBackupStorage}}
|
||||
/>
|
||||
{{else}}
|
||||
<UppyBackupUploader @done={{route-action "remoteUploadSuccess"}} />
|
||||
{{/if}}
|
||||
</:actions>
|
||||
</AdminPageSubheader>
|
||||
|
||||
{{#if this.status.restoreDisabled}}
|
||||
<div class="backup-message alert alert-info">
|
||||
<a href="site_settings/category/all_results?filter=allow_restore">{{d-icon
|
||||
"info-circle"
|
||||
}}
|
||||
{{i18n "admin.backups.operations.restore.is_disabled"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<table class="grid">
|
||||
{{/if}}
|
||||
|
||||
<table class="grid admin-backups-list">
|
||||
<thead>
|
||||
<th width="55%">{{i18n "admin.backups.columns.filename"}}</th>
|
||||
<th width="10%">{{i18n "admin.backups.columns.size"}}</th>
|
||||
|
@ -48,51 +28,49 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
{{#each this.model as |backup|}}
|
||||
<tr>
|
||||
<tr class="backup-item-row" data-backup-filename={{backup.filename}}>
|
||||
<td class="backup-filename">{{backup.filename}}</td>
|
||||
<td class="backup-size">{{human-size backup.size}}</td>
|
||||
<td class="backup-controls">
|
||||
<div>
|
||||
<DButton
|
||||
@action={{fn this.download backup}}
|
||||
@icon="download"
|
||||
@title="admin.backups.operations.download.title"
|
||||
@label="admin.backups.operations.download.label"
|
||||
class="btn-default download"
|
||||
/>
|
||||
{{#if this.status.isOperationRunning}}
|
||||
<DButton
|
||||
@icon="far-trash-alt"
|
||||
@action={{fn (route-action "destroyBackup") backup}}
|
||||
@disabled="true"
|
||||
@title="admin.backups.operations.is_running"
|
||||
class="btn-danger"
|
||||
/>
|
||||
<DButton
|
||||
@icon="play"
|
||||
@action={{fn (route-action "startRestore") backup}}
|
||||
@disabled={{this.status.restoreDisabled}}
|
||||
@title={{this.restoreTitle}}
|
||||
@label="admin.backups.operations.restore.label"
|
||||
class="btn-default"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@icon="far-trash-alt"
|
||||
@action={{fn (route-action "destroyBackup") backup}}
|
||||
@title="admin.backups.operations.destroy.title"
|
||||
class="btn-danger"
|
||||
/>
|
||||
<DButton
|
||||
@icon="play"
|
||||
@action={{fn (route-action "startRestore") backup}}
|
||||
@disabled={{this.status.restoreDisabled}}
|
||||
@title={{this.restoreTitle}}
|
||||
@label="admin.backups.operations.restore.label"
|
||||
class="btn-default btn-restore"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
<DButton
|
||||
@action={{fn this.download backup}}
|
||||
@icon="download"
|
||||
@title="admin.backups.operations.download.title"
|
||||
@label="admin.backups.operations.download.label"
|
||||
class="btn-default btn-small backup-item-row__download"
|
||||
/>
|
||||
|
||||
<DMenu
|
||||
@identifier="backup-item-menu"
|
||||
@title={{i18n "more_options"}}
|
||||
@icon="ellipsis-v"
|
||||
class="btn-small"
|
||||
>
|
||||
<:content>
|
||||
<DropdownMenu as |dropdown|>
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@icon="far-trash-alt"
|
||||
@action={{fn (route-action "destroyBackup") backup}}
|
||||
@disabled={{this.status.isOperationRunning}}
|
||||
@title={{this.deleteTitle}}
|
||||
@label="admin.backups.operations.destroy.title"
|
||||
class="btn-transparent btn-danger backup-item-row__delete"
|
||||
/>
|
||||
</dropdown.item>
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@icon="play"
|
||||
@action={{fn (route-action "startRestore") backup}}
|
||||
@disabled={{this.status.restoreDisabled}}
|
||||
@title={{this.restoreTitle}}
|
||||
@label="admin.backups.operations.restore.label"
|
||||
class="btn-transparent backup-item-row__restore"
|
||||
/>
|
||||
</dropdown.item>
|
||||
</DropdownMenu>
|
||||
</:content>
|
||||
</DMenu>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
|
|
|
@ -1 +1,6 @@
|
|||
<DBreadcrumbsItem
|
||||
@path="/admin/backups/logs"
|
||||
@label={{i18n "admin.backups.menu.logs"}}
|
||||
/>
|
||||
|
||||
<AdminBackupsLogs @logs={{this.logs}} @status={{this.status}} />
|
|
@ -0,0 +1,9 @@
|
|||
<DBreadcrumbsItem @path="/admin/backups/settings" @label={{i18n "settings"}} />
|
||||
|
||||
<div class="content-body admin-config-area__settings admin-detail pull-left">
|
||||
<AdminFilteredSiteSettings
|
||||
@initialFilter={{@model.initialFilter}}
|
||||
@settings={{@model.settings}}
|
||||
@onFilterChanged={{this.filterChanged}}
|
||||
/>
|
||||
</div>
|
|
@ -1,49 +1,42 @@
|
|||
<div class="admin-backups">
|
||||
<div class="admin-controls">
|
||||
<nav>
|
||||
<ul class="nav nav-pills">
|
||||
<NavItem
|
||||
@route="admin.backups.index"
|
||||
@label="admin.backups.menu.backups"
|
||||
/>
|
||||
<NavItem @route="admin.backups.logs" @label="admin.backups.menu.logs" />
|
||||
<PluginOutlet @name="downloader" @connectorTagName="div" />
|
||||
<div class="admin-actions">
|
||||
{{#if this.model.canRollback}}
|
||||
<DButton
|
||||
@action={{route-action "rollback"}}
|
||||
@label="admin.backups.operations.rollback.label"
|
||||
@title="admin.backups.operations.rollback.title"
|
||||
@icon="ambulance"
|
||||
@disabled={{this.rollbackDisabled}}
|
||||
class="btn-default btn-rollback"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.model.isOperationRunning}}
|
||||
<DButton
|
||||
@action={{route-action "cancelOperation"}}
|
||||
@title="admin.backups.operations.cancel.title"
|
||||
@label="admin.backups.operations.cancel.label"
|
||||
@icon="times"
|
||||
class="btn-danger"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{route-action "showStartBackupModal"}}
|
||||
@title="admin.backups.operations.backup.title"
|
||||
@label="admin.backups.operations.backup.label"
|
||||
@icon="rocket"
|
||||
class="btn-primary"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="admin-backups admin-config-page">
|
||||
|
||||
<AdminPageHeader
|
||||
@titleLabel="admin.backups.title"
|
||||
@descriptionLabel="admin.backups.description"
|
||||
@learnMoreUrl="https://meta.discourse.org/t/create-download-and-restore-a-backup-of-your-discourse-database/122710"
|
||||
>
|
||||
<:breadcrumbs>
|
||||
<DBreadcrumbsItem
|
||||
@path="/admin/backups"
|
||||
@label={{i18n "admin.backups.title"}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
<:actions as |actions|>
|
||||
<AdminBackupsActions @actions={{actions}} @backups={{@model}} />
|
||||
</:actions>
|
||||
<:tabs>
|
||||
<NavItem
|
||||
@route="admin.backups.settings"
|
||||
@label="settings"
|
||||
class="admin-backups-tabs__settings"
|
||||
/>
|
||||
<NavItem
|
||||
@route="admin.backups.index"
|
||||
@label="admin.backups.menu.backup_files"
|
||||
class="admin-backups-tabs__files"
|
||||
/>
|
||||
<NavItem
|
||||
@route="admin.backups.logs"
|
||||
@label="admin.backups.menu.logs"
|
||||
class="admin-backups-tabs__logs"
|
||||
/>
|
||||
<PluginOutlet @name="downloader" @connectorTagName="div" />
|
||||
</:tabs>
|
||||
</AdminPageHeader>
|
||||
|
||||
<PluginOutlet @name="before-backup-list" @connectorTagName="div" />
|
||||
|
||||
<div class="admin-container">
|
||||
<div class="admin-container admin-config-page__main-area">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
|
@ -2,6 +2,12 @@
|
|||
<PluginOutlet @name="admin-dashboard-top" @connectorTagName="div" />
|
||||
</span>
|
||||
|
||||
<AdminPageHeader @hideTabs={{true}}>
|
||||
<:breadcrumbs>
|
||||
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin.dashboard.title"}} />
|
||||
</:breadcrumbs>
|
||||
</AdminPageHeader>
|
||||
|
||||
{{#if this.showVersionChecks}}
|
||||
<div class="section-top">
|
||||
<div class="version-checks">
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<div
|
||||
class="content-body admin-plugin-config-area__settings admin-detail pull-left"
|
||||
>
|
||||
<AdminPluginFilteredSiteSettings
|
||||
<AdminFilteredSiteSettings
|
||||
@initialFilter={{@model.initialFilter}}
|
||||
@plugin={{@model.plugin}}
|
||||
@settings={{@model.settings}}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<label
|
||||
class="btn"
|
||||
class="btn btn-small btn-primary admin-backups-upload"
|
||||
disabled={{this.uploading}}
|
||||
title={{i18n "admin.backups.upload.title"}}
|
||||
>
|
||||
|
|
|
@ -1077,6 +1077,7 @@ a.inline-editable-field {
|
|||
@import "common/admin/admin_report_stacked_line_chart";
|
||||
@import "common/admin/admin_report_table";
|
||||
@import "common/admin/admin_report_inline_table";
|
||||
@import "common/admin/admin_page_header";
|
||||
@import "common/admin/admin_intro";
|
||||
@import "common/admin/admin_emojis";
|
||||
@import "common/admin/mini_profiler";
|
||||
|
|
|
@ -35,3 +35,31 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-config-page {
|
||||
&__main-area {
|
||||
.admin-detail {
|
||||
padding-top: 15px;
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-config-area {
|
||||
&__settings {
|
||||
.admin-site-settings-filter-controls {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
margin-left: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&__empty-list {
|
||||
padding: 1em;
|
||||
border: 1px solid var(--primary-low);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
.admin-page-header,
|
||||
.admin-page-subheader {
|
||||
&__title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
|
||||
h1,
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-page-header__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
button {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-nav-submenu {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
|
||||
.horizontal-overflow-nav {
|
||||
background: transparent;
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-pills {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +1,10 @@
|
|||
// Styles for /admin/backups
|
||||
|
||||
$rollback: #3d9970;
|
||||
$rollback-dark: darken($rollback, 10%) !default;
|
||||
$rollback-darker: darken($rollback, 20%) !default;
|
||||
.btn-rollback {
|
||||
color: var(--secondary);
|
||||
background: $rollback;
|
||||
.d-icon {
|
||||
color: var(--secondary);
|
||||
}
|
||||
&:hover {
|
||||
background: $rollback-dark;
|
||||
.d-icon {
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
@include linear-gradient($rollback-darker, $rollback-dark);
|
||||
}
|
||||
&[disabled] {
|
||||
background: $rollback;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-backups {
|
||||
.before-backup-list-outlet {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
@media screen and (min-width: 550px) {
|
||||
td.backup-filename {
|
||||
|
@ -71,43 +52,20 @@ $rollback-darker: darken($rollback, 20%) !default;
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
button.ru {
|
||||
position: relative;
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.ru-progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: rgba(0, 175, 0, 0.3);
|
||||
}
|
||||
|
||||
.is-uploading:hover .ru-progress {
|
||||
background: rgba(200, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.start-backup-modal {
|
||||
.alert {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.backup-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
.btn {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
.backup-message {
|
||||
margin-left: auto;
|
||||
@include breakpoint(mobile-extra-large) {
|
||||
margin: 1.25em 0 0;
|
||||
}
|
||||
}
|
||||
label {
|
||||
font-weight: normal;
|
||||
.backup-message {
|
||||
margin-left: auto;
|
||||
margin-top: 1em;
|
||||
@include breakpoint(mobile-extra-large) {
|
||||
margin: 1.25em 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
label.admin-backups-upload {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
.admin-reports,
|
||||
.dashboard-next {
|
||||
&.admin-contents {
|
||||
margin: 10px 0 0 0;
|
||||
|
||||
nav {
|
||||
position: relative;
|
||||
width: calc(100% + 10px);
|
||||
|
|
|
@ -295,6 +295,7 @@ en:
|
|||
now: "just now"
|
||||
read_more: "read more"
|
||||
more: "More"
|
||||
more_options: "More options"
|
||||
x_more:
|
||||
one: "%{count} More"
|
||||
other: "%{count} More"
|
||||
|
@ -2219,6 +2220,7 @@ en:
|
|||
}.
|
||||
|
||||
learn_more: "Learn more…"
|
||||
learn_more_with_link: "<a href='%{url}' target='_blank'>Learn more…</a>"
|
||||
|
||||
mute: Mute
|
||||
unmute: Unmute
|
||||
|
@ -5012,6 +5014,7 @@ en:
|
|||
admin_js:
|
||||
# This is a text input placeholder, keep the translation short
|
||||
type_to_filter: "type to filter…"
|
||||
settings: "Settings"
|
||||
|
||||
admin:
|
||||
title: "Discourse Admin"
|
||||
|
@ -5658,8 +5661,12 @@ en:
|
|||
|
||||
backups:
|
||||
title: "Backups"
|
||||
files_title: "Backup files"
|
||||
description: "Discourse backups include the full site database, which contains everything on the site: topics, posts, users, groups, settings, themes, etc. Depending on how the backup file is created, it may or may not include uploads."
|
||||
learn_more_url: ""
|
||||
menu:
|
||||
backups: "Backups"
|
||||
backup_files: "Backup files"
|
||||
logs: "Logs"
|
||||
none: "No backup available."
|
||||
read_only:
|
||||
|
|
|
@ -365,6 +365,7 @@ Discourse::Application.routes.draw do
|
|||
:format => :json
|
||||
|
||||
get "logs" => "backups#logs"
|
||||
get "settings" => "backups#index"
|
||||
get "status" => "backups#status"
|
||||
delete "cancel" => "backups#cancel"
|
||||
post "rollback" => "backups#rollback"
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
#frozen_string_literal: true
|
||||
|
||||
describe "Admin Backups Page", type: :system do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
let(:backups_page) { PageObjects::Pages::AdminBackups.new }
|
||||
let(:dialog) { PageObjects::Components::Dialog.new }
|
||||
let(:settings_page) { PageObjects::Pages::AdminSiteSettings.new }
|
||||
|
||||
let(:root_directory) { setup_local_backups }
|
||||
|
||||
def create_backups
|
||||
create_local_backup_file(
|
||||
root_directory: root_directory,
|
||||
db_name: "default",
|
||||
filename: "b.tar.gz",
|
||||
last_modified: "2024-07-13T15:10:00Z",
|
||||
size_in_bytes: 10,
|
||||
)
|
||||
create_local_backup_file(
|
||||
root_directory: root_directory,
|
||||
db_name: "default",
|
||||
filename: "old.tar.gz",
|
||||
last_modified: "2024-06-01T13:10:00Z",
|
||||
size_in_bytes: 5,
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(current_user)
|
||||
create_backups
|
||||
BackupRestore::LocalBackupStore.stubs(:base_directory).returns(
|
||||
root_directory + "/" + RailsMultisite::ConnectionManagement.current_db,
|
||||
)
|
||||
end
|
||||
after { teardown_local_backups(root_directory: root_directory) }
|
||||
|
||||
it "shows a list of backups" do
|
||||
backups_page.visit_page
|
||||
expect(backups_page).to have_backup_listed("b.tar.gz")
|
||||
expect(backups_page).to have_backup_listed("old.tar.gz")
|
||||
end
|
||||
|
||||
it "can download a backup, which sends an email" do
|
||||
backups_page.visit_page
|
||||
backups_page.download_backup("b.tar.gz")
|
||||
expect(page).to have_content(I18n.t("admin_js.admin.backups.operations.download.alert"))
|
||||
expect_job_enqueued(
|
||||
job: :download_backup_email,
|
||||
args: {
|
||||
user_id: current_user.id,
|
||||
backup_file_path: Discourse.base_url + "/admin/backups/b.tar.gz",
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "can delete a backup" do
|
||||
backups_page.visit_page
|
||||
backups_page.delete_backup("b.tar.gz")
|
||||
dialog.click_yes
|
||||
expect(backups_page).to have_no_backup_listed("b.tar.gz")
|
||||
end
|
||||
|
||||
it "can restore a backup" do
|
||||
backups_page.visit_page
|
||||
backups_page.expand_backup_row_menu("b.tar.gz")
|
||||
expect(backups_page).to have_css(backups_page.row_button_selector("restore"))
|
||||
end
|
||||
|
||||
it "can toggle read-only mode" do
|
||||
backups_page.visit_page
|
||||
backups_page.toggle_read_only
|
||||
dialog.click_yes
|
||||
expect(page).to have_content(I18n.t("js.read_only_mode.enabled"))
|
||||
backups_page.toggle_read_only
|
||||
expect(page).to have_no_content(I18n.t("js.read_only_mode.enabled"))
|
||||
end
|
||||
|
||||
it "can see backup site settings" do
|
||||
backups_page.visit_page
|
||||
backups_page.click_tab("settings")
|
||||
expect(settings_page).to have_setting("enable_backups")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class AdminBackups < PageObjects::Pages::Base
|
||||
def visit_page
|
||||
page.visit "/admin/backups"
|
||||
self
|
||||
end
|
||||
|
||||
def click_tab(tab_name)
|
||||
case tab_name
|
||||
when "settings"
|
||||
find(".admin-backups-tabs__settings").click
|
||||
when "files"
|
||||
find(".admin-backups-tabs__files").click
|
||||
when "logs"
|
||||
find(".admin-backups-tabs__logs").click
|
||||
end
|
||||
end
|
||||
|
||||
def has_backup_listed?(filename)
|
||||
page.has_css?(backup_row_selector(filename))
|
||||
end
|
||||
|
||||
def has_no_backup_listed?(filename)
|
||||
page.has_no_css?(backup_row_selector(filename))
|
||||
end
|
||||
|
||||
def open_upload_backup_modal
|
||||
find(".admin-backups__start").click
|
||||
end
|
||||
|
||||
def download_backup(filename)
|
||||
find_backup_row(filename).find(row_button_selector("download")).click
|
||||
end
|
||||
|
||||
def expand_backup_row_menu(filename)
|
||||
find_backup_row(filename).find(".backup-item-menu-trigger").click
|
||||
end
|
||||
|
||||
def delete_backup(filename)
|
||||
expand_backup_row_menu(filename)
|
||||
find(".backup-item-menu-content").find(row_button_selector("delete")).click
|
||||
end
|
||||
|
||||
def restore_backup(filename)
|
||||
expand_backup_row_menu(filename)
|
||||
find(".backup-item-menu-content").find(row_button_selector("restore")).click
|
||||
end
|
||||
|
||||
def find_backup_row(filename)
|
||||
find(backup_row_selector(filename))
|
||||
end
|
||||
|
||||
def backup_row_selector(filename)
|
||||
".admin-backups-list .backup-item-row[data-backup-filename='#{filename}']"
|
||||
end
|
||||
|
||||
def row_button_selector(button_name)
|
||||
".backup-item-row__#{button_name}"
|
||||
end
|
||||
|
||||
def toggle_read_only
|
||||
find(".admin-backups__toggle-read-only").click
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,9 +22,17 @@ module PageObjects
|
|||
self
|
||||
end
|
||||
|
||||
def setting_row_selector(setting_name)
|
||||
".row.setting[data-setting='#{setting_name}']"
|
||||
end
|
||||
|
||||
def has_setting?(setting_name)
|
||||
has_css?(".row.setting[data-setting=\"#{setting_name}\"]")
|
||||
end
|
||||
|
||||
def find_setting(setting_name, overridden: false)
|
||||
find(
|
||||
".admin-detail .row.setting[data-setting='#{setting_name}']#{overridden ? ".overridden" : ""}",
|
||||
".admin-detail #{setting_row_selector(setting_name)}#{overridden ? ".overridden" : ""}",
|
||||
)
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue