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:
Martin Brennan 2024-08-20 09:59:43 +10:00 committed by GitHub
parent 5b17e85fe1
commit 1446596089
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 649 additions and 222 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -142,6 +142,7 @@ export default function () {
{ path: "/backups", resetNamespace: true },
function () {
this.route("logs");
this.route("settings");
}
);

View File

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

View File

@ -1 +1,6 @@
<DBreadcrumbsItem
@path="/admin/backups/logs"
@label={{i18n "admin.backups.menu.logs"}}
/>
<AdminBackupsLogs @logs={{this.logs}} @status={{this.status}} />

View File

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

View File

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

View File

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

View File

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

View File

@ -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"}}
>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
.admin-reports,
.dashboard-next {
&.admin-contents {
margin: 10px 0 0 0;
nav {
position: relative;
width: calc(100% + 10px);

View File

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

View File

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

View File

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

View File

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

View File

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