Bug 1893269 - Build out basic infrastructure for backup management UI. r=backup-reviewers,settings-reviewers,firefox-desktop-core-reviewers ,Gijs,fluent-reviewers,kpatenio

Differential Revision: https://phabricator.services.mozilla.com/D209161
This commit is contained in:
Mike Conley 2024-05-09 21:34:35 +00:00
Родитель ab7f68990a
Коммит 26bf957cd0
20 изменённых файлов: 366 добавлений и 6 удалений

Просмотреть файл

@ -425,6 +425,20 @@ let JSWINDOWACTORS = {
enablePreference: "browser.aboutwelcome.enabled",
},
BackupUI: {
parent: {
esModuleURI: "resource:///actors/BackupUIParent.sys.mjs",
},
child: {
esModuleURI: "resource:///actors/BackupUIChild.sys.mjs",
events: {
"BackupUI:InitWidget": { wantUntrusted: true },
},
},
matches: ["about:preferences*", "about:settings*"],
enablePreference: "browser.backup.preferences.ui.enabled",
},
BlockedSite: {
parent: {
esModuleURI: "resource:///actors/BlockedSiteParent.sys.mjs",

Просмотреть файл

@ -34,7 +34,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
* backups. It also does most of the heavy lifting for the restoration of a
* profile backup.
*/
export class BackupService {
export class BackupService extends EventTarget {
/**
* The BackupService singleton instance.
*
@ -50,12 +50,47 @@ export class BackupService {
*/
#resources = new Map();
/**
* Set to true if a backup is currently in progress. Causes stateUpdate()
* to be called.
*
* @see BackupService.stateUpdate()
* @param {boolean} val
* True if a backup is in progress.
*/
set #backupInProgress(val) {
if (this.#_state.backupInProgress != val) {
this.#_state.backupInProgress = val;
this.stateUpdate();
}
}
/**
* True if a backup is currently in progress.
*
* @type {boolean}
*/
#backupInProgress = false;
get #backupInProgress() {
return this.#_state.backupInProgress;
}
/**
* Dispatches an event to let listeners know that the BackupService state
* object has been updated.
*/
stateUpdate() {
this.dispatchEvent(new CustomEvent("BackupService:StateUpdate"));
}
/**
* An object holding the current state of the BackupService instance, for
* the purposes of representing it in the user interface. Ideally, this would
* be named #state instead of #_state, but sphinx-js seems to be fairly
* unhappy with that coupled with the ``state`` getter.
*
* @type {object}
*/
#_state = { backupInProgress: false };
/**
* A Promise that will resolve once the postRecovery steps are done. It will
@ -190,6 +225,7 @@ export class BackupService {
* @param {object} [backupResources=DefaultBackupResources] - Object containing BackupResource classes to associate with this service.
*/
constructor(backupResources = DefaultBackupResources) {
super();
lazy.logConsole.debug("Instantiated");
for (const resourceName in backupResources) {
@ -214,6 +250,17 @@ export class BackupService {
return this.#postRecoveryPromise;
}
/**
* Returns a state object describing the state of the BackupService for the
* purposes of representing it in the user interface. The returned state
* object is immutable.
*
* @type {object}
*/
get state() {
return Object.freeze(structuredClone(this.#_state));
}
/**
* @typedef {object} CreateBackupResult
* @property {string} stagingPath

Просмотреть файл

@ -0,0 +1,54 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* A JSWindowActor that is responsible for marshalling information between
* the BackupService singleton and any registered UI widgets that need to
* represent data from that service. Any UI widgets that want to receive
* state updates from BackupService should emit a BackupUI:InitWidget
* event in a document that this actor pair is registered for.
*/
export class BackupUIChild extends JSWindowActorChild {
#inittedWidgets = new WeakSet();
/**
* Handles BackupUI:InitWidget custom events fired by widgets that want to
* register with BackupUIChild. Firing this event sends a message to the
* parent to request the BackupService state which will result in a
* `backupServiceState` property of the widget to be set when that state is
* received. Subsequent state updates will also cause that state property to
* be set.
*
* @param {Event} event
* The BackupUI:InitWidget custom event that the widget fired.
*/
handleEvent(event) {
if (event.type == "BackupUI:InitWidget") {
this.#inittedWidgets.add(event.target);
this.sendAsyncMessage("RequestState");
}
}
/**
* Handles messages sent by BackupUIParent.
*
* @param {ReceiveMessageArgument} message
* The message received from the BackupUIParent.
*/
receiveMessage(message) {
if (message.name == "StateUpdate") {
let widgets = ChromeUtils.nondeterministicGetWeakSetKeys(
this.#inittedWidgets
);
for (let widget of widgets) {
if (widget.isConnected) {
// Note: we might need to switch to using Cu.cloneInto here in the
// event that these widgets are embedded in a non-parent-process
// context, like in an onboarding card.
widget.backupServiceState = message.data.state;
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,82 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BackupService: "resource:///modules/backup/BackupService.sys.mjs",
});
/**
* A JSWindowActor that is responsible for marshalling information between
* the BackupService singleton and any registered UI widgets that need to
* represent data from that service.
*/
export class BackupUIParent extends JSWindowActorParent {
/**
* A reference to the BackupService singleton instance.
*
* @type {BackupService}
*/
#bs;
/**
* Create a BackupUIParent instance. If a BackupUIParent is instantiated
* before BrowserGlue has a chance to initialize the BackupService, this
* constructor will cause it to initialize first.
*/
constructor() {
super();
// We use init() rather than get(), since it's possible to load
// about:preferences before the service has had a chance to init itself
// via BrowserGlue.
this.#bs = lazy.BackupService.init();
}
/**
* Called once the BackupUIParent/BackupUIChild pair have been connected.
*/
actorCreated() {
this.#bs.addEventListener("BackupService:StateUpdate", this);
}
/**
* Called once the BackupUIParent/BackupUIChild pair have been disconnected.
*/
didDestroy() {
this.#bs.removeEventListener("BackupService:StateUpdate", this);
}
/**
* Handles events fired by the BackupService.
*
* @param {Event} event
* The event that the BackupService emitted.
*/
handleEvent(event) {
if (event.type == "BackupService:StateUpdate") {
this.sendState();
}
}
/**
* Handles messages sent by BackupUIChild.
*
* @param {ReceiveMessageArgument} message
* The message received from the BackupUIChild.
*/
receiveMessage(message) {
if (message.name == "RequestState") {
this.sendState();
}
}
/**
* Sends the StateUpdate message to the BackupUIChild, along with the most
* recent state object from BackupService.
*/
sendState() {
this.sendAsyncMessage("StateUpdate", { state: this.#bs.state });
}
}

Просмотреть файл

@ -0,0 +1,47 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { html } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
/**
* The widget for managing the BackupService that is embedded within the main
* document of about:settings / about:preferences.
*/
export default class BackupSettings extends MozLitElement {
static properties = {
backupServiceState: { type: Object },
};
/**
* Creates a BackupSettings instance and sets the initial default
* state.
*/
constructor() {
super();
this.backupServiceState = {
backupInProgress: false,
};
}
/**
* Dispatches the BackupUI:InitWidget custom event upon being attached to the
* DOM, which registers with BackupUIChild for BackupService state updates.
*/
connectedCallback() {
super.connectedCallback();
this.dispatchEvent(
new CustomEvent("BackupUI:InitWidget", { bubbles: true })
);
}
render() {
return html`<div>
Backup in progress:
${this.backupServiceState.backupInProgress ? "Yes" : "No"}
</div>`;
}
}
customElements.define("backup-settings", BackupSettings);

Просмотреть файл

@ -0,0 +1,32 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// eslint-disable-next-line import/no-unresolved
import { html } from "lit.all.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "./backup-settings.mjs";
export default {
title: "Domain-specific UI Widgets/Backup/Backup Settings",
component: "backup-settings",
argTypes: {},
};
const Template = ({ backupServiceState }) => html`
<backup-settings .backupServiceState=${backupServiceState}></backup-settings>
`;
export const BackingUpNotInProgress = Template.bind({});
BackingUpNotInProgress.args = {
backupServiceState: {
backupInProgress: false,
},
};
export const BackingUpInProgress = Template.bind({});
BackingUpInProgress.args = {
backupServiceState: {
backupInProgress: true,
},
};

Просмотреть файл

@ -0,0 +1,22 @@
==========================
Backup UI Actors Reference
==========================
The ``BackupUIParent`` and ``BackupUIChild`` actors allow UI widgets to access
the current state of the ``BackupService`` and to subscribe to state updates.
UI widgets that want to subscribe to state updates must ensure that they are
running in a process and on a page that the ``BackupUIParent/BackupUIChild``
actor pair are registered for, and then fire a ``BackupUI::InitWidget`` event.
It is expected that these UI widgets will respond to having their
``backupServiceState`` property set.
.. js:autoclass:: BackupUIParent
:members:
:private-members:
.. js:autoclass:: BackupUIChild
.. js::autoattribute:: BackupUIChild#inittedWidgets
:members:
:private-members:

Просмотреть файл

@ -12,3 +12,4 @@ into a single file that can be easily restored from.
backup-service
backup-resources
backup-ui-actors

Просмотреть файл

@ -7,4 +7,5 @@ browser.jar:
content/browser/backup/debug.html (content/debug.html)
content/browser/backup/debug.js (content/debug.js)
#endif
content/browser/backup/BackupManifest.1.schema.json (content/BackupManifest.1.schema.json)
content/browser/backup/BackupManifest.1.schema.json (content/BackupManifest.1.schema.json)
content/browser/backup/backup-settings.mjs (content/backup-settings.mjs)

Просмотреть файл

@ -14,6 +14,12 @@ SPHINX_TREES["docs"] = "docs"
XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
MARIONETTE_MANIFESTS += ["tests/marionette/manifest.toml"]
BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.toml"]
FINAL_TARGET_FILES.actors += [
"actors/BackupUIChild.sys.mjs",
"actors/BackupUIParent.sys.mjs",
]
EXTRA_JS_MODULES.backup += [
"BackupResources.sys.mjs",

Просмотреть файл

@ -4,4 +4,4 @@ prefs = [
"browser.backup.preferences.ui.enabled=true",
]
["browser_preferences.js"]
["browser_settings.js"]

Просмотреть файл

@ -0,0 +1,4 @@
[DEFAULT]
skip-if = ["os == 'android'"]
["test_backup_settings.html"]

Просмотреть файл

@ -0,0 +1,43 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Tests for the BackupSettings component</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script
src="chrome://browser/content/backup/backup-settings.mjs"
type="module"
></script>
<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
<script>
const { BrowserTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/BrowserTestUtils.sys.mjs"
);
/**
* Tests that adding a backup-settings element to the DOM causes it to
* fire a BackupUI:InitWidget event.
*/
add_task(async function test_initWidget() {
let settings = document.createElement("backup-settings");
let content = document.getElementById("content");
let sawInitWidget = BrowserTestUtils.waitForEvent(content, "BackupUI:InitWidget");
content.appendChild(settings);
await sawInitWidget;
ok(true, "Saw BackupUI:InitWidget");
settings.remove();
});
</script>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none">
<backup-settings id="test-backup-settings"></backup-settings>
</div>
<pre id="test"></pre>
</body>
</html>

Просмотреть файл

@ -84,6 +84,9 @@
<groupbox id="dataBackupGroup" data-category="paneGeneral" hidden="true"
data-hidden-from-search="true">
<label><html:h2 data-l10n-id="settings-data-backup-header"/></label>
<hbox flex="1">
<html:backup-settings />
</hbox>
</groupbox>
<!-- Tab preferences -->

Просмотреть файл

@ -68,7 +68,7 @@
<link rel="localization" href="browser/translations.ftl"/>
<link rel="localization" href="preview/translations.ftl"/>
<link rel="localization" href="preview/enUS-searchFeatures.ftl"/>
<link rel="localization" href="preview/backupPreferences.ftl"/>
<link rel="localization" href="preview/backupSettings.ftl"/>
<link rel="localization" href="security/certificates/certManager.ftl"/>
<link rel="localization" href="security/certificates/deviceManager.ftl"/>
<link rel="localization" href="toolkit/updates/history.ftl"/>
@ -87,6 +87,7 @@
<script type="module" src="chrome://global/content/elements/moz-label.mjs"/>
<script type="module" src="chrome://global/content/elements/moz-card.mjs"></script>
<script type="module" src="chrome://global/content/elements/moz-button.mjs"></script>
<script type="module" src="chrome://browser/content/backup/backup-settings.mjs"></script>
</head>
<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"

Просмотреть файл

@ -23,6 +23,8 @@ module.exports = {
`${projectRoot}/toolkit/content/widgets/**/*.stories.@(js|jsx|mjs|ts|tsx|md)`,
// about:logins components stories
`${projectRoot}/browser/components/aboutlogins/content/components/**/*.stories.mjs`,
// Backup components stories
`${projectRoot}/browser/components/backup/content/**/*.stories.mjs`,
// Reader View components stories
`${projectRoot}/toolkit/components/reader/**/*.stories.mjs`,
// Everything else

Просмотреть файл

@ -18,7 +18,7 @@
preview/translations.ftl (../locales-preview/translations.ftl)
browser (%browser/**/*.ftl)
preview/profiles.ftl (../components/profiles/content/profiles.ftl)
preview/backupPreferences.ftl (../locales-preview/backupPreferences.ftl)
preview/backupSettings.ftl (../locales-preview/backupSettings.ftl)
@AB_CD@.jar:
% locale browser @AB_CD@ %locale/browser/

Просмотреть файл

@ -56,6 +56,7 @@ extensions = [
# excluded from valid-jsdoc in the top-level .eslintrc.js.
js_source_path = [
"../browser/components/backup",
"../browser/components/backup/actors",
"../browser/components/backup/resources",
"../browser/components/extensions",
"../browser/components/migration",