Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions browser/data-browser/src/helpers/syncProblem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,68 @@ describe('describeSyncProblem', () => {
}),
).toBeNull();
});

describe('a refused workspace', () => {
const OTHER = 'atomic:otherdrive';
const refused = (drive: string) =>
`Drive ${drive} is not enrolled for sync on this node`;
const blockedEntry = (message: string) => [
{
subject: 'atomic:somedoc',
blocked: true,
failures: 5,
lastAttemptError: message,
},
];

it('keeps the plain wording for the workspace in view', () => {
const problem = describeSyncProblem({
...base,
entries: blockedEntry(refused(DRIVE)),
driveOf: () => DRIVE,
});

expect(problem?.title).toBe('This server does not have this workspace');
expect(problem?.otherDrive).toBeUndefined();
expect(problem?.actions).toEqual(['connect-device', 'review-sync']);
});

it('names another workspace by its title, and is not offered the viewed switch', () => {
const problem = describeSyncProblem({
...base,
entries: blockedEntry(refused('did:ad:otherdrive')),
driveTitle: () => 'Team notes',
});

expect(problem?.title).toBe('This server does not have “Team notes”');
expect(problem?.body).toContain('not the workspace you are viewing');
expect(problem?.otherDrive).toBe(OTHER);
expect(problem?.actions).toEqual(['connect-device']);
expect(problem?.detail).toContain('is not enrolled');
});

it('falls back to a neutral phrase when the title cannot be loaded', () => {
const problem = describeSyncProblem({
...base,
entries: blockedEntry(refused(OTHER)),
driveTitle: () => undefined,
});

expect(problem?.title).toBe(
'This server does not have another workspace on this device',
);
expect(problem?.title).not.toContain(OTHER);
});

it('finds the workspace from the entry when the message names none', () => {
const problem = describeSyncProblem({
...base,
entries: blockedEntry('Parent of atomic:x not found'),
driveOf: () => OTHER,
driveTitle: () => 'Team notes',
});

expect(problem?.otherDrive).toBe(OTHER);
});
});
});
83 changes: 73 additions & 10 deletions browser/data-browser/src/helpers/syncProblem.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { canonicalizeScheme } from '@tomic/lib';
import type { OutboxEntry, StoreSyncStatus } from '@tomic/react';

/** What the person can do about a sync problem, from the banner. */
Expand All @@ -24,6 +25,12 @@ export type SyncProblem = {
actions: SyncProblemAction[];
/** The raw message, for a person who wants to quote it. */
detail?: string;
/**
* The workspace the problem is about, when it is not the one being viewed.
* The banner's "turn off server sync" would act on the viewed workspace, so
* a problem with another one does not offer it.
*/
otherDrive?: string;
/**
* Sentry already has this one: the outbox reports a commit that keeps
* failing by itself, so reporting it again would count it twice.
Expand All @@ -42,8 +49,14 @@ export type SyncProblemInput = {
>[];
/** The server in use, as a person reads it: `atomicserver.eu`. */
serverName: string;
/** `store.driveOfSubject`: the workspace an outbox entry belongs to. */
driveOf?: (subject: string) => string | undefined;
/** The workspace's name, or undefined while it cannot be loaded. */
driveTitle?: (drive: string) => string | undefined;
};

type Entry = SyncProblemInput['entries'][number];

/** The outbox reports a commit to Sentry after this many failed drains. */
const REPORTED_AFTER_FAILURES = 4;

Expand All @@ -53,20 +66,68 @@ const NO_ACCESS =
const MISSING_PARENT = /Parent of .+ not found/;
const NOT_ENROLLED = /is not enrolled for sync on this node/;

function firstError(
function firstEntry(
entries: SyncProblemInput['entries'],
keep: (entry: SyncProblemInput['entries'][number]) => boolean,
keep: (entry: Entry) => boolean,
): Entry | undefined {
return entries.find(e => keep(e) && e.lastAttemptError);
}

/** The workspace a "not enrolled" refusal names, in any spelling. */
function driveInMessage(message: string): string | undefined {
return /Drive (\S+) is not enrolled/.exec(message)?.[1];
}

/**
* Which workspace a problem is about, when it is not the viewed one.
* The server's message names it; failing that, the entry that was refused.
*/
function otherDriveOf(
input: SyncProblemInput,
message: string,
entry: Entry | undefined,
): string | undefined {
return entries.find(e => keep(e) && e.lastAttemptError)?.lastAttemptError;
const viewed = input.status.drive && canonicalizeScheme(input.status.drive);
const named = driveInMessage(message);
const drive =
named ?? (entry ? input.driveOf?.(entry.subject) : undefined) ?? undefined;

if (!drive || !viewed) return undefined;

const canonical = canonicalizeScheme(drive);

return canonical === viewed ? undefined : canonical;
}

/** Name the remedy for an error message that retrying cannot fix, or null. */
function fromMessage(
input: SyncProblemInput,
message: string,
serverName: string,
reported: boolean,
entry?: Entry,
): SyncProblem | null {
const { serverName } = input;

if (NOT_ENROLLED.test(message) || MISSING_PARENT.test(message)) {
const other = otherDriveOf(input, message, entry);

if (other) {
const name = input.driveTitle?.(other)?.trim();

return {
key: 'not-hosted',
cause: 'you',
title: name
? `This server does not have “${name}”`
: 'This server does not have another workspace on this device',
body: `That is not the workspace you are viewing. ${serverName} refuses changes to it, so they stay on this device for now. Connect a device that has it, or open that workspace and turn off server sync there.`,
actions: ['connect-device'],
detail: message,
otherDrive: other,
reported,
};
}

return {
key: 'not-hosted',
cause: 'you',
Expand Down Expand Up @@ -138,8 +199,8 @@ export function describeSyncProblem(

if (refusedByServer) {
const refused = fromMessage(
input,
'is not enrolled for sync on this node',
serverName,
true,
);

Expand Down Expand Up @@ -167,17 +228,18 @@ export function describeSyncProblem(

if (driveError) {
return (
fromMessage(driveError, serverName, false) ??
fromMessage(input, driveError, false) ??
ours('drive-sync', 'This workspace could not sync', driveError, false)
);
}

const blocked = entries.filter(e => e.blocked);

if (status.blockedCount > 0 || blocked.length > 0) {
const message = firstError(entries, e => !!e.blocked);
const entry = firstEntry(entries, e => !!e.blocked);
const message = entry?.lastAttemptError;
// Parked after the outbox reported them, so Sentry has these already.
const known = message ? fromMessage(message, serverName, true) : null;
const known = message ? fromMessage(input, message, true, entry) : null;

return (
known ?? ours('blocked', 'Some changes could not sync', message, true)
Expand All @@ -189,10 +251,11 @@ export function describeSyncProblem(
);

if (failing.length > 0 && status.serverConnected) {
const message = firstError(entries, e => failing.includes(e));
const entry = firstEntry(entries, e => failing.includes(e));
const message = entry?.lastAttemptError;

return (
(message ? fromMessage(message, serverName, true) : null) ??
(message ? fromMessage(input, message, true, entry) : null) ??
ours('failing', 'Some changes keep failing to sync', message, true)
);
}
Expand Down
16 changes: 16 additions & 0 deletions browser/data-browser/src/locales/de.po
Original file line number Diff line number Diff line change
Expand Up @@ -3268,6 +3268,7 @@ msgstr ""
#: src/helpers/managed/cloudSync.ts
#: src/helpers/managed/prepareDriveSharing.ts
#: src/helpers/managed/reconcile.ts
#: src/helpers/managed/refusedDrive.ts
msgid "Disabled"
msgstr ""

Expand Down Expand Up @@ -12880,6 +12881,7 @@ msgstr ""

#: src/components/Vault/VaultStorage.tsx
msgid "Older backup copies are removed, and so is the edit history of the backup: it keeps only the current state of every item. Restoring from it brings back your workspace as it is now, without its version history. Items you deleted recently can no longer be recovered from the backup. Your devices keep their own edit history and your workspace is not changed."
msgstr ""

#: src/components/ConnectDevice.tsx
msgid "Could not reach that address."
Expand All @@ -12888,3 +12890,17 @@ msgstr ""
#: src/components/ConnectDevice.tsx
msgid "That address answers, but it is not an AtomicServer."
msgstr ""

#. 0: name
#: src/helpers/syncProblem.ts
msgid "This server does not have “{0}”"
msgstr ""

#: src/helpers/syncProblem.ts
msgid "This server does not have another workspace on this device"
msgstr ""

#. 0: serverName
#: src/helpers/syncProblem.ts
msgid "That is not the workspace you are viewing. {0} refuses changes to it, so they stay on this device for now. Connect a device that has it, or open that workspace and turn off server sync there."
msgstr ""
15 changes: 15 additions & 0 deletions browser/data-browser/src/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -3268,6 +3268,7 @@ msgstr "Search"
#: src/helpers/managed/cloudSync.ts
#: src/helpers/managed/prepareDriveSharing.ts
#: src/helpers/managed/reconcile.ts
#: src/helpers/managed/refusedDrive.ts
msgid "Disabled"
msgstr "Disabled"

Expand Down Expand Up @@ -12902,3 +12903,17 @@ msgstr "Could not reach that address."
#: src/components/ConnectDevice.tsx
msgid "That address answers, but it is not an AtomicServer."
msgstr "That address answers, but it is not an AtomicServer."

#. 0: name
#: src/helpers/syncProblem.ts
msgid "This server does not have “{0}”"
msgstr "This server does not have “{0}”"

#: src/helpers/syncProblem.ts
msgid "This server does not have another workspace on this device"
msgstr "This server does not have another workspace on this device"

#. 0: serverName
#: src/helpers/syncProblem.ts
msgid "That is not the workspace you are viewing. {0} refuses changes to it, so they stay on this device for now. Connect a device that has it, or open that workspace and turn off server sync there."
msgstr "That is not the workspace you are viewing. {0} refuses changes to it, so they stay on this device for now. Connect a device that has it, or open that workspace and turn off server sync there."
16 changes: 16 additions & 0 deletions browser/data-browser/src/locales/es.po
Original file line number Diff line number Diff line change
Expand Up @@ -3268,6 +3268,7 @@ msgstr "Buscar"
#: src/helpers/managed/cloudSync.ts
#: src/helpers/managed/prepareDriveSharing.ts
#: src/helpers/managed/reconcile.ts
#: src/helpers/managed/refusedDrive.ts
msgid "Disabled"
msgstr ""

Expand Down Expand Up @@ -12880,6 +12881,7 @@ msgstr ""

#: src/components/Vault/VaultStorage.tsx
msgid "Older backup copies are removed, and so is the edit history of the backup: it keeps only the current state of every item. Restoring from it brings back your workspace as it is now, without its version history. Items you deleted recently can no longer be recovered from the backup. Your devices keep their own edit history and your workspace is not changed."
msgstr ""

#: src/components/ConnectDevice.tsx
msgid "Could not reach that address."
Expand All @@ -12888,3 +12890,17 @@ msgstr ""
#: src/components/ConnectDevice.tsx
msgid "That address answers, but it is not an AtomicServer."
msgstr ""

#. 0: name
#: src/helpers/syncProblem.ts
msgid "This server does not have “{0}”"
msgstr ""

#: src/helpers/syncProblem.ts
msgid "This server does not have another workspace on this device"
msgstr ""

#. 0: serverName
#: src/helpers/syncProblem.ts
msgid "That is not the workspace you are viewing. {0} refuses changes to it, so they stay on this device for now. Connect a device that has it, or open that workspace and turn off server sync there."
msgstr ""
16 changes: 16 additions & 0 deletions browser/data-browser/src/locales/fr.po
Original file line number Diff line number Diff line change
Expand Up @@ -3268,6 +3268,7 @@ msgstr "Rechercher"
#: src/helpers/managed/cloudSync.ts
#: src/helpers/managed/prepareDriveSharing.ts
#: src/helpers/managed/reconcile.ts
#: src/helpers/managed/refusedDrive.ts
msgid "Disabled"
msgstr ""

Expand Down Expand Up @@ -12880,6 +12881,7 @@ msgstr ""

#: src/components/Vault/VaultStorage.tsx
msgid "Older backup copies are removed, and so is the edit history of the backup: it keeps only the current state of every item. Restoring from it brings back your workspace as it is now, without its version history. Items you deleted recently can no longer be recovered from the backup. Your devices keep their own edit history and your workspace is not changed."
msgstr ""

#: src/components/ConnectDevice.tsx
msgid "Could not reach that address."
Expand All @@ -12888,3 +12890,17 @@ msgstr ""
#: src/components/ConnectDevice.tsx
msgid "That address answers, but it is not an AtomicServer."
msgstr ""

#. 0: name
#: src/helpers/syncProblem.ts
msgid "This server does not have “{0}”"
msgstr ""

#: src/helpers/syncProblem.ts
msgid "This server does not have another workspace on this device"
msgstr ""

#. 0: serverName
#: src/helpers/syncProblem.ts
msgid "That is not the workspace you are viewing. {0} refuses changes to it, so they stay on this device for now. Connect a device that has it, or open that workspace and turn off server sync there."
msgstr ""
26 changes: 22 additions & 4 deletions browser/data-browser/src/routes/SyncRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import {
type StoreSyncStatus,
type CommitLogEntry,
useStore,
useResource,
useString,
core,
useProperty,
truncateUrl,
Datatype,
Expand Down Expand Up @@ -1873,13 +1876,28 @@ function SyncPage() {

// Why this workspace is not syncing, if it is not. A local-only workspace is
// not supposed to be, so it has nothing to explain.
const syncProblemInput = {
status,
refusedByServer: store.isDriveRefusedByServer(status.drive),
entries: store.outbox.pending(),
serverName: serverLabel(status.serverUrl ?? ''),
driveOf: (subject: string) => store.driveOfSubject(subject),
};
// A problem can be about another workspace than the one in view; name it by
// its title, which needs a first pass to know which one it is.
const problemDrive = localOnlyDrive
? undefined
: describeSyncProblem(syncProblemInput)?.otherDrive;
const problemDriveResource = useResource(problemDrive);
const [problemDriveName] = useString(
problemDriveResource,
core.properties.name,
);
const syncProblem = localOnlyDrive
? null
: describeSyncProblem({
status,
refusedByServer: store.isDriveRefusedByServer(status.drive),
entries: store.outbox.pending(),
serverName: serverLabel(status.serverUrl ?? ''),
...syncProblemInput,
driveTitle: () => (problemDrive ? problemDriveName : undefined),
});
const syncProblemKey = syncProblem?.key;
const syncProblemOurs = syncProblem?.cause === 'ours';
Expand Down
Loading
Loading