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 AdminSiteSettingsFilterControls from "admin/components/admin-site-settings-filter-controls";
|
||||||
import SiteSetting from "admin/components/site-setting";
|
import SiteSetting from "admin/components/site-setting";
|
||||||
|
|
||||||
export default class AdminPluginFilteredSiteSettings extends Component {
|
export default class AdminFilteredSiteSettings extends Component {
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@tracked visibleSettings;
|
@tracked visibleSettings;
|
||||||
@tracked loading = true;
|
@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>
|
const AdminPluginConfigMetadata = <template>
|
||||||
<div class="admin-plugin-config-page__metadata">
|
<div class="admin-plugin-config-page__metadata">
|
||||||
<div class="admin-plugin-config-area__metadata-title">
|
<div class="admin-plugin-config-area__metadata-title">
|
||||||
<h2>
|
<h1>
|
||||||
{{@plugin.nameTitleized}}
|
{{@plugin.nameTitleized}}
|
||||||
</h2>
|
</h1>
|
||||||
<p>
|
<p>
|
||||||
{{@plugin.about}}
|
{{@plugin.about}}
|
||||||
{{#if @plugin.linkUrl}}
|
{{#if @plugin.linkUrl}}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { action } from "@ember/object";
|
||||||
import { alias, equal } from "@ember/object/computed";
|
import { alias, equal } from "@ember/object/computed";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { i18n, setting } from "discourse/lib/computed";
|
import { i18n, setting } from "discourse/lib/computed";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import I18n from "discourse-i18n";
|
import I18n from "discourse-i18n";
|
||||||
|
@ -28,32 +29,21 @@ export default class AdminBackupsIndexController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleReadOnlyMode() {
|
async download(backup) {
|
||||||
if (!this.site.get("isReadOnly")) {
|
try {
|
||||||
this.dialog.yesNoConfirm({
|
await ajax(`/admin/backups/${backup.filename}`, { type: "PUT" });
|
||||||
message: I18n.t("admin.backups.read_only.enable.confirm"),
|
this.dialog.alert(I18n.t("admin.backups.operations.download.alert"));
|
||||||
didConfirm: () => {
|
} catch (err) {
|
||||||
this.set("currentUser.hideReadOnlyAlert", true);
|
popupAjaxError(err);
|
||||||
this._toggleReadOnlyMode(true);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._toggleReadOnlyMode(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@discourseComputed("status.isOperationRunning")
|
||||||
download(backup) {
|
deleteTitle() {
|
||||||
const link = backup.get("filename");
|
if (this.status.isOperationRunning) {
|
||||||
ajax(`/admin/backups/${link}`, { type: "PUT" }).then(() =>
|
return "admin.backups.operations.is_running";
|
||||||
this.dialog.alert(I18n.t("admin.backups.operations.download.alert"))
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_toggleReadOnlyMode(enable) {
|
return "admin.backups.operations.destroy.title";
|
||||||
ajax("/admin/backups/readonly", {
|
|
||||||
type: "PUT",
|
|
||||||
data: { enable },
|
|
||||||
}).then(() => this.site.set("isReadOnly", enable));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 Controller from "@ember/controller";
|
||||||
import { and, not } from "@ember/object/computed";
|
|
||||||
export default class AdminBackupsController extends Controller {
|
export default class AdminBackupsController extends Controller {}
|
||||||
@not("model.isOperationRunning") noOperationIsRunning;
|
|
||||||
@not("rollbackEnabled") rollbackDisabled;
|
|
||||||
@and("model.canRollback", "model.restoreEnabled", "noOperationIsRunning")
|
|
||||||
rollbackEnabled;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 messageBus;
|
||||||
@service modal;
|
@service modal;
|
||||||
|
|
||||||
|
titleToken() {
|
||||||
|
return I18n.t("admin.backups.title");
|
||||||
|
}
|
||||||
|
|
||||||
activate() {
|
activate() {
|
||||||
this.messageBus.subscribe(LOG_CHANNEL, this.onMessage);
|
this.messageBus.subscribe(LOG_CHANNEL, this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,10 @@ export default class AdminPluginsShowSettingsRoute extends Route {
|
||||||
|
|
||||||
async model(params) {
|
async model(params) {
|
||||||
const plugin = this.modelFor("adminPlugins.show");
|
const plugin = this.modelFor("adminPlugins.show");
|
||||||
const settings = await SiteSetting.findAll({ plugin: plugin.name });
|
return {
|
||||||
|
plugin,
|
||||||
return { plugin, settings, initialFilter: params.filter };
|
settings: await SiteSetting.findAll({ plugin: plugin.name }),
|
||||||
|
initialFilter: params.filter,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,6 +142,7 @@ export default function () {
|
||||||
{ path: "/backups", resetNamespace: true },
|
{ path: "/backups", resetNamespace: true },
|
||||||
function () {
|
function () {
|
||||||
this.route("logs");
|
this.route("logs");
|
||||||
|
this.route("settings");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,46 +1,26 @@
|
||||||
<div class="backup-options">
|
<AdminPageSubheader @titleLabel="admin.backups.files_title">
|
||||||
{{#if this.localBackupStorage}}
|
<:actions>
|
||||||
<UppyBackupUploader
|
{{#if this.localBackupStorage}}
|
||||||
@done={{route-action "uploadSuccess"}}
|
<UppyBackupUploader
|
||||||
@localBackupStorage={{this.localBackupStorage}}
|
@done={{route-action "uploadSuccess"}}
|
||||||
/>
|
@localBackupStorage={{this.localBackupStorage}}
|
||||||
{{else}}
|
/>
|
||||||
<UppyBackupUploader @done={{route-action "remoteUploadSuccess"}} />
|
{{else}}
|
||||||
{{/if}}
|
<UppyBackupUploader @done={{route-action "remoteUploadSuccess"}} />
|
||||||
|
|
||||||
{{#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>
|
|
||||||
{{/if}}
|
{{/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>
|
||||||
</div>
|
{{/if}}
|
||||||
<table class="grid">
|
|
||||||
|
<table class="grid admin-backups-list">
|
||||||
<thead>
|
<thead>
|
||||||
<th width="55%">{{i18n "admin.backups.columns.filename"}}</th>
|
<th width="55%">{{i18n "admin.backups.columns.filename"}}</th>
|
||||||
<th width="10%">{{i18n "admin.backups.columns.size"}}</th>
|
<th width="10%">{{i18n "admin.backups.columns.size"}}</th>
|
||||||
|
@ -48,51 +28,49 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{#each this.model as |backup|}}
|
{{#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-filename">{{backup.filename}}</td>
|
||||||
<td class="backup-size">{{human-size backup.size}}</td>
|
<td class="backup-size">{{human-size backup.size}}</td>
|
||||||
<td class="backup-controls">
|
<td class="backup-controls">
|
||||||
<div>
|
<DButton
|
||||||
<DButton
|
@action={{fn this.download backup}}
|
||||||
@action={{fn this.download backup}}
|
@icon="download"
|
||||||
@icon="download"
|
@title="admin.backups.operations.download.title"
|
||||||
@title="admin.backups.operations.download.title"
|
@label="admin.backups.operations.download.label"
|
||||||
@label="admin.backups.operations.download.label"
|
class="btn-default btn-small backup-item-row__download"
|
||||||
class="btn-default download"
|
/>
|
||||||
/>
|
|
||||||
{{#if this.status.isOperationRunning}}
|
<DMenu
|
||||||
<DButton
|
@identifier="backup-item-menu"
|
||||||
@icon="far-trash-alt"
|
@title={{i18n "more_options"}}
|
||||||
@action={{fn (route-action "destroyBackup") backup}}
|
@icon="ellipsis-v"
|
||||||
@disabled="true"
|
class="btn-small"
|
||||||
@title="admin.backups.operations.is_running"
|
>
|
||||||
class="btn-danger"
|
<:content>
|
||||||
/>
|
<DropdownMenu as |dropdown|>
|
||||||
<DButton
|
<dropdown.item>
|
||||||
@icon="play"
|
<DButton
|
||||||
@action={{fn (route-action "startRestore") backup}}
|
@icon="far-trash-alt"
|
||||||
@disabled={{this.status.restoreDisabled}}
|
@action={{fn (route-action "destroyBackup") backup}}
|
||||||
@title={{this.restoreTitle}}
|
@disabled={{this.status.isOperationRunning}}
|
||||||
@label="admin.backups.operations.restore.label"
|
@title={{this.deleteTitle}}
|
||||||
class="btn-default"
|
@label="admin.backups.operations.destroy.title"
|
||||||
/>
|
class="btn-transparent btn-danger backup-item-row__delete"
|
||||||
{{else}}
|
/>
|
||||||
<DButton
|
</dropdown.item>
|
||||||
@icon="far-trash-alt"
|
<dropdown.item>
|
||||||
@action={{fn (route-action "destroyBackup") backup}}
|
<DButton
|
||||||
@title="admin.backups.operations.destroy.title"
|
@icon="play"
|
||||||
class="btn-danger"
|
@action={{fn (route-action "startRestore") backup}}
|
||||||
/>
|
@disabled={{this.status.restoreDisabled}}
|
||||||
<DButton
|
@title={{this.restoreTitle}}
|
||||||
@icon="play"
|
@label="admin.backups.operations.restore.label"
|
||||||
@action={{fn (route-action "startRestore") backup}}
|
class="btn-transparent backup-item-row__restore"
|
||||||
@disabled={{this.status.restoreDisabled}}
|
/>
|
||||||
@title={{this.restoreTitle}}
|
</dropdown.item>
|
||||||
@label="admin.backups.operations.restore.label"
|
</DropdownMenu>
|
||||||
class="btn-default btn-restore"
|
</:content>
|
||||||
/>
|
</DMenu>
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
@ -1 +1,6 @@
|
||||||
|
<DBreadcrumbsItem
|
||||||
|
@path="/admin/backups/logs"
|
||||||
|
@label={{i18n "admin.backups.menu.logs"}}
|
||||||
|
/>
|
||||||
|
|
||||||
<AdminBackupsLogs @logs={{this.logs}} @status={{this.status}} />
|
<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-backups admin-config-page">
|
||||||
<div class="admin-controls">
|
|
||||||
<nav>
|
<AdminPageHeader
|
||||||
<ul class="nav nav-pills">
|
@titleLabel="admin.backups.title"
|
||||||
<NavItem
|
@descriptionLabel="admin.backups.description"
|
||||||
@route="admin.backups.index"
|
@learnMoreUrl="https://meta.discourse.org/t/create-download-and-restore-a-backup-of-your-discourse-database/122710"
|
||||||
@label="admin.backups.menu.backups"
|
>
|
||||||
/>
|
<:breadcrumbs>
|
||||||
<NavItem @route="admin.backups.logs" @label="admin.backups.menu.logs" />
|
<DBreadcrumbsItem
|
||||||
<PluginOutlet @name="downloader" @connectorTagName="div" />
|
@path="/admin/backups"
|
||||||
<div class="admin-actions">
|
@label={{i18n "admin.backups.title"}}
|
||||||
{{#if this.model.canRollback}}
|
/>
|
||||||
<DButton
|
</:breadcrumbs>
|
||||||
@action={{route-action "rollback"}}
|
<:actions as |actions|>
|
||||||
@label="admin.backups.operations.rollback.label"
|
<AdminBackupsActions @actions={{actions}} @backups={{@model}} />
|
||||||
@title="admin.backups.operations.rollback.title"
|
</:actions>
|
||||||
@icon="ambulance"
|
<:tabs>
|
||||||
@disabled={{this.rollbackDisabled}}
|
<NavItem
|
||||||
class="btn-default btn-rollback"
|
@route="admin.backups.settings"
|
||||||
/>
|
@label="settings"
|
||||||
{{/if}}
|
class="admin-backups-tabs__settings"
|
||||||
{{#if this.model.isOperationRunning}}
|
/>
|
||||||
<DButton
|
<NavItem
|
||||||
@action={{route-action "cancelOperation"}}
|
@route="admin.backups.index"
|
||||||
@title="admin.backups.operations.cancel.title"
|
@label="admin.backups.menu.backup_files"
|
||||||
@label="admin.backups.operations.cancel.label"
|
class="admin-backups-tabs__files"
|
||||||
@icon="times"
|
/>
|
||||||
class="btn-danger"
|
<NavItem
|
||||||
/>
|
@route="admin.backups.logs"
|
||||||
{{else}}
|
@label="admin.backups.menu.logs"
|
||||||
<DButton
|
class="admin-backups-tabs__logs"
|
||||||
@action={{route-action "showStartBackupModal"}}
|
/>
|
||||||
@title="admin.backups.operations.backup.title"
|
<PluginOutlet @name="downloader" @connectorTagName="div" />
|
||||||
@label="admin.backups.operations.backup.label"
|
</:tabs>
|
||||||
@icon="rocket"
|
</AdminPageHeader>
|
||||||
class="btn-primary"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<PluginOutlet @name="before-backup-list" @connectorTagName="div" />
|
<PluginOutlet @name="before-backup-list" @connectorTagName="div" />
|
||||||
|
|
||||||
<div class="admin-container">
|
<div class="admin-container admin-config-page__main-area">
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -2,6 +2,12 @@
|
||||||
<PluginOutlet @name="admin-dashboard-top" @connectorTagName="div" />
|
<PluginOutlet @name="admin-dashboard-top" @connectorTagName="div" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<AdminPageHeader @hideTabs={{true}}>
|
||||||
|
<:breadcrumbs>
|
||||||
|
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin.dashboard.title"}} />
|
||||||
|
</:breadcrumbs>
|
||||||
|
</AdminPageHeader>
|
||||||
|
|
||||||
{{#if this.showVersionChecks}}
|
{{#if this.showVersionChecks}}
|
||||||
<div class="section-top">
|
<div class="section-top">
|
||||||
<div class="version-checks">
|
<div class="version-checks">
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div
|
<div
|
||||||
class="content-body admin-plugin-config-area__settings admin-detail pull-left"
|
class="content-body admin-plugin-config-area__settings admin-detail pull-left"
|
||||||
>
|
>
|
||||||
<AdminPluginFilteredSiteSettings
|
<AdminFilteredSiteSettings
|
||||||
@initialFilter={{@model.initialFilter}}
|
@initialFilter={{@model.initialFilter}}
|
||||||
@plugin={{@model.plugin}}
|
@plugin={{@model.plugin}}
|
||||||
@settings={{@model.settings}}
|
@settings={{@model.settings}}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<label
|
<label
|
||||||
class="btn"
|
class="btn btn-small btn-primary admin-backups-upload"
|
||||||
disabled={{this.uploading}}
|
disabled={{this.uploading}}
|
||||||
title={{i18n "admin.backups.upload.title"}}
|
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_stacked_line_chart";
|
||||||
@import "common/admin/admin_report_table";
|
@import "common/admin/admin_report_table";
|
||||||
@import "common/admin/admin_report_inline_table";
|
@import "common/admin/admin_report_inline_table";
|
||||||
|
@import "common/admin/admin_page_header";
|
||||||
@import "common/admin/admin_intro";
|
@import "common/admin/admin_intro";
|
||||||
@import "common/admin/admin_emojis";
|
@import "common/admin/admin_emojis";
|
||||||
@import "common/admin/mini_profiler";
|
@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
|
// 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 {
|
.admin-backups {
|
||||||
|
.before-backup-list-outlet {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@media screen and (min-width: 550px) {
|
@media screen and (min-width: 550px) {
|
||||||
td.backup-filename {
|
td.backup-filename {
|
||||||
|
@ -71,43 +52,20 @@ $rollback-darker: darken($rollback, 20%) !default;
|
||||||
overflow: auto;
|
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 {
|
.start-backup-modal {
|
||||||
.alert {
|
.alert {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.backup-options {
|
.backup-message {
|
||||||
display: flex;
|
margin-left: auto;
|
||||||
align-items: center;
|
margin-top: 1em;
|
||||||
flex-wrap: wrap;
|
@include breakpoint(mobile-extra-large) {
|
||||||
.btn {
|
margin: 1.25em 0 0;
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
.backup-message {
|
|
||||||
margin-left: auto;
|
|
||||||
@include breakpoint(mobile-extra-large) {
|
|
||||||
margin: 1.25em 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label.admin-backups-upload {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
.admin-reports,
|
.admin-reports,
|
||||||
.dashboard-next {
|
.dashboard-next {
|
||||||
&.admin-contents {
|
&.admin-contents {
|
||||||
margin: 10px 0 0 0;
|
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: calc(100% + 10px);
|
width: calc(100% + 10px);
|
||||||
|
|
|
@ -295,6 +295,7 @@ en:
|
||||||
now: "just now"
|
now: "just now"
|
||||||
read_more: "read more"
|
read_more: "read more"
|
||||||
more: "More"
|
more: "More"
|
||||||
|
more_options: "More options"
|
||||||
x_more:
|
x_more:
|
||||||
one: "%{count} More"
|
one: "%{count} More"
|
||||||
other: "%{count} More"
|
other: "%{count} More"
|
||||||
|
@ -2219,6 +2220,7 @@ en:
|
||||||
}.
|
}.
|
||||||
|
|
||||||
learn_more: "Learn more…"
|
learn_more: "Learn more…"
|
||||||
|
learn_more_with_link: "<a href='%{url}' target='_blank'>Learn more…</a>"
|
||||||
|
|
||||||
mute: Mute
|
mute: Mute
|
||||||
unmute: Unmute
|
unmute: Unmute
|
||||||
|
@ -5012,6 +5014,7 @@ en:
|
||||||
admin_js:
|
admin_js:
|
||||||
# This is a text input placeholder, keep the translation short
|
# This is a text input placeholder, keep the translation short
|
||||||
type_to_filter: "type to filter…"
|
type_to_filter: "type to filter…"
|
||||||
|
settings: "Settings"
|
||||||
|
|
||||||
admin:
|
admin:
|
||||||
title: "Discourse Admin"
|
title: "Discourse Admin"
|
||||||
|
@ -5658,8 +5661,12 @@ en:
|
||||||
|
|
||||||
backups:
|
backups:
|
||||||
title: "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:
|
menu:
|
||||||
backups: "Backups"
|
backups: "Backups"
|
||||||
|
backup_files: "Backup files"
|
||||||
logs: "Logs"
|
logs: "Logs"
|
||||||
none: "No backup available."
|
none: "No backup available."
|
||||||
read_only:
|
read_only:
|
||||||
|
|
|
@ -365,6 +365,7 @@ Discourse::Application.routes.draw do
|
||||||
:format => :json
|
:format => :json
|
||||||
|
|
||||||
get "logs" => "backups#logs"
|
get "logs" => "backups#logs"
|
||||||
|
get "settings" => "backups#index"
|
||||||
get "status" => "backups#status"
|
get "status" => "backups#status"
|
||||||
delete "cancel" => "backups#cancel"
|
delete "cancel" => "backups#cancel"
|
||||||
post "rollback" => "backups#rollback"
|
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
|
self
|
||||||
end
|
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)
|
def find_setting(setting_name, overridden: false)
|
||||||
find(
|
find(
|
||||||
".admin-detail .row.setting[data-setting='#{setting_name}']#{overridden ? ".overridden" : ""}",
|
".admin-detail #{setting_row_selector(setting_name)}#{overridden ? ".overridden" : ""}",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue