@futo-org/backups-orchestrator-ui 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,10 @@
5
5
  LocalRepositoryDto,
6
6
  RepositoryBackendDto,
7
7
  } from "../../fetch-client";
8
- import { getBackendActions } from "../../services/backend.service";
8
+ import {
9
+ getBackendActions,
10
+ useYuccaLogin,
11
+ } from "../../services/backend.service";
9
12
  import { Badge, Icon, Text } from "@immich/ui";
10
13
  import { mdiCloudOutline, mdiHarddisk, mdiShieldCheckOutline } from "@mdi/js";
11
14
  import StackListItem from "../ui/StackListItem.svelte";
@@ -30,8 +33,12 @@
30
33
  s3: "S3 Server",
31
34
  };
32
35
 
36
+ const yuccaLogin = useYuccaLogin();
37
+
33
38
  const { LoginAgain, Reconfigure } = $derived(
34
- getBackendActions(repository, backend, repositoryBackend),
39
+ getBackendActions(repository, backend, repositoryBackend, () =>
40
+ yuccaLogin.mutate(undefined),
41
+ ),
35
42
  );
36
43
  </script>
37
44
 
@@ -3,9 +3,9 @@
3
3
  import { options } from "../../options";
4
4
  import {
5
5
  handleSetupLocalStorage,
6
- handleYuccaLogin,
7
6
  useBackendEventHandler,
8
7
  useBackends,
8
+ useYuccaLogin,
9
9
  } from "../../services/backend.service";
10
10
  import { Button, HStack } from "@immich/ui";
11
11
  import StackList from "../ui/StackList.svelte";
@@ -21,6 +21,7 @@
21
21
 
22
22
  const { advanced } = options;
23
23
  const query = useBackends();
24
+ const yuccaLogin = useYuccaLogin();
24
25
  const { onBackendCreate } = useBackendEventHandler();
25
26
 
26
27
  const repositoryBackends = $derived(
@@ -61,8 +62,11 @@
61
62
 
62
63
  {#if $advanced}
63
64
  <HStack>
64
- <Button size="small" variant="outline" onclick={() => handleYuccaLogin()}
65
- >Login with FUTO Backups</Button
65
+ <Button
66
+ size="small"
67
+ variant="outline"
68
+ loading={yuccaLogin.isPending}
69
+ onclick={() => yuccaLogin.mutate(undefined)}>Login with FUTO Backups</Button
66
70
  >
67
71
  <Button
68
72
  size="small"
@@ -13,6 +13,7 @@
13
13
  ModalFooter,
14
14
  Stack,
15
15
  Text,
16
+ toastManager,
16
17
  VStack,
17
18
  } from "@immich/ui";
18
19
  import { mdiContentCopy } from "@mdi/js";
@@ -35,6 +36,11 @@
35
36
  onClose();
36
37
  }
37
38
 
39
+ function onDeviceFlowFailure() {
40
+ toastManager.danger("Failed to log into FUTO Backups");
41
+ onClose();
42
+ }
43
+
38
44
  function onOpen() {
39
45
  window.open(verificationUri, "_blank");
40
46
  }
@@ -44,7 +50,7 @@
44
50
  }
45
51
  </script>
46
52
 
47
- <OnEvents {onBackendCreate} />
53
+ <OnEvents {onBackendCreate} {onDeviceFlowFailure} />
48
54
 
49
55
  <Modal title="Logging into FUTO Backups" icon={false} {onClose}>
50
56
  <ModalBody>
@@ -4,13 +4,14 @@
4
4
  import Suspense from "../../util/Suspense.svelte";
5
5
  import {
6
6
  handleSetupLocalStorage,
7
- handleYuccaLogin,
8
7
  useBackends,
8
+ useYuccaLogin,
9
9
  } from "../../../services/backend.service";
10
10
  import {
11
11
  Button,
12
12
  HStack,
13
13
  Icon,
14
+ LoadingSpinner,
14
15
  Modal,
15
16
  ModalBody,
16
17
  ModalFooter,
@@ -37,6 +38,7 @@
37
38
  }: Props = $props();
38
39
 
39
40
  const backends = useBackends();
41
+ const yuccaLogin = useYuccaLogin();
40
42
 
41
43
  const onFutoBackups = () => {
42
44
  const futoBackend = backends.data!.find(
@@ -46,7 +48,7 @@
46
48
  if (futoBackend) {
47
49
  onSelect(futoBackend.id);
48
50
  } else {
49
- handleYuccaLogin(onSelect);
51
+ yuccaLogin.mutate(onSelect);
50
52
  }
51
53
  };
52
54
 
@@ -63,7 +65,7 @@
63
65
 
64
66
  <Suspense query={backends}>
65
67
  <StackList>
66
- <StackListItem {disabled} onclick={onFutoBackups}>
68
+ <StackListItem disabled={disabled || yuccaLogin.isPending} onclick={onFutoBackups}>
67
69
  {#snippet icon()}
68
70
  <Icon icon={mdiShieldCheck} size="36px" />
69
71
  {/snippet}
@@ -74,10 +76,14 @@
74
76
  </Stack>
75
77
 
76
78
  {#snippet trailing()}
77
- <Icon icon={mdiChevronRight} />
79
+ {#if yuccaLogin.isPending}
80
+ <LoadingSpinner />
81
+ {:else}
82
+ <Icon icon={mdiChevronRight} />
83
+ {/if}
78
84
  {/snippet}
79
85
  </StackListItem>
80
- <StackListItem {disabled} onclick={onLocalBackups}>
86
+ <StackListItem disabled={disabled || yuccaLogin.isPending} onclick={onLocalBackups}>
81
87
  {#snippet icon()}
82
88
  <Icon icon={mdiHarddisk} size="36px" />
83
89
  {/snippet}
@@ -95,7 +101,7 @@
95
101
  {#each backends.data as backend}
96
102
  {#if backend.type === "local"}
97
103
  <StackListItem
98
- {disabled}
104
+ disabled={disabled || yuccaLogin.isPending}
99
105
  onclick={() => {
100
106
  onSelect(backend.id);
101
107
  }}
@@ -53,9 +53,9 @@
53
53
  <Heading size="tiny">Your library</Heading>
54
54
  <Text>
55
55
  {#if repository.meter}
56
- Estimated <FormatBytes bytes={repository.meter.sizeBytes} />
56
+ <FormatBytes bytes={repository.meter.sizeBytes} />
57
57
  {:else}
58
- <FormatBytes bytes={repository.metrics.sizeBytes} />
58
+ Estimated <FormatBytes bytes={repository.metrics.sizeBytes} />
59
59
  {/if} &middot;
60
60
  <span class="lowercase"
61
61
  >{cronstrue.toString(schedule.cron, {
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
+ import OnboardingBootstrapError from "../../onboarding/OnboardingBootstrapError.svelte";
2
3
  import OnboardingStageBackupServices from "../../onboarding/stages/OnboardingStageBackupServices.svelte";
3
4
  import OnboardingStageKeyImport from "../../onboarding/stages/OnboardingStageKeyImport.svelte";
5
+ import OnboardingStageTelemetry from "../../onboarding/stages/OnboardingStageTelemetry.svelte";
4
6
  import RestorePointFlow from "../../onboarding/restore-point-flow/RestorePointFlow.svelte";
5
7
  import type { OnboardingStatusResponseDto } from "../../../fetch-client";
6
8
  import {
@@ -19,6 +21,7 @@
19
21
 
20
22
  let status: OnboardingStatusResponseDto | undefined = $state();
21
23
  let stage:
24
+ | "telemetry"
22
25
  | "key-import"
23
26
  | "backup-service"
24
27
  | "restore-point"
@@ -28,48 +31,55 @@
28
31
  handleOnboardingStatus().then((data) => {
29
32
  status = data;
30
33
 
31
- if (data.hasOnboardedKey) {
32
- if (data.hasBackend) {
33
- stage = "restore-point";
34
- } else {
35
- stage = "backup-service";
36
- }
34
+ if (data.status !== "ready") {
35
+ return;
36
+ }
37
+
38
+ if (data.hasTelemetry === "none") {
39
+ stage = "telemetry";
40
+ } else if (data.hasOnboardedKey) {
41
+ stage = data.hasBackend ? "restore-point" : "backup-service";
37
42
  }
38
43
  });
39
44
  });
40
45
 
41
46
  const onKeyImported = async () => {
42
47
  await handleConfirmRecoveryKey();
43
-
44
- if (status?.hasBackend) {
45
- stage = "restore-point";
46
- } else {
47
- stage = "backup-service";
48
- }
48
+ stage = status?.hasBackend ? "restore-point" : "backup-service";
49
49
  };
50
50
  </script>
51
51
 
52
- {#if typeof status === "object"}
53
- {#if stage === "key-import"}
54
- <OnboardingStageKeyImport onImported={onKeyImported} onCancel={onExit} />
55
- {:else if stage === "backup-service"}
56
- <OnboardingStageBackupServices
57
- onNext={() => (stage = "restore-point")}
58
- onCancel={onExit}
59
- restore
60
- />
61
- {:else if stage === "key-reimport"}
62
- <OnboardingStageKeyImport
63
- onImported={() => (stage = "restore-point")}
64
- onCancel={() => (stage = "restore-point")}
65
- />
66
- {:else if stage === "restore-point"}
67
- <RestorePointFlow
68
- onImportKey={() => (stage = "key-reimport")}
69
- onCancel={onExit}
70
- {onFinish}
71
- />
72
- {/if}
73
- {:else}
52
+ {#if status === undefined || status.status === "not-ready"}
74
53
  <LoadingSpinner />
54
+ {:else if status.status === "error"}
55
+ <OnboardingBootstrapError error={status.error} onQuit={onExit} />
56
+ {:else if stage === "telemetry"}
57
+ <OnboardingStageTelemetry
58
+ onContinue={() =>
59
+ (stage = status?.hasOnboardedKey
60
+ ? status.hasBackend
61
+ ? "restore-point"
62
+ : "backup-service"
63
+ : "key-import")}
64
+ onCancel={onExit}
65
+ />
66
+ {:else if stage === "key-import"}
67
+ <OnboardingStageKeyImport onImported={onKeyImported} onCancel={onExit} />
68
+ {:else if stage === "backup-service"}
69
+ <OnboardingStageBackupServices
70
+ onNext={() => (stage = "restore-point")}
71
+ onCancel={onExit}
72
+ restore
73
+ />
74
+ {:else if stage === "key-reimport"}
75
+ <OnboardingStageKeyImport
76
+ onImported={() => (stage = "restore-point")}
77
+ onCancel={() => (stage = "restore-point")}
78
+ />
79
+ {:else if stage === "restore-point"}
80
+ <RestorePointFlow
81
+ onImportKey={() => (stage = "key-reimport")}
82
+ onCancel={onExit}
83
+ {onFinish}
84
+ />
75
85
  {/if}
@@ -1,9 +1,11 @@
1
1
  <script lang="ts">
2
+ import OnboardingBootstrapError from "../../onboarding/OnboardingBootstrapError.svelte";
2
3
  import OnboardingStageBackupServices from "../../onboarding/stages/OnboardingStageBackupServices.svelte";
3
4
  import OnboardingStageKeyConfirm from "../../onboarding/stages/OnboardingStageKeyConfirm.svelte";
4
5
  import OnboardingStageKeyImport from "../../onboarding/stages/OnboardingStageKeyImport.svelte";
5
6
  import OnboardingStageKeyIntro from "../../onboarding/stages/OnboardingStageKeyIntro.svelte";
6
7
  import OnboardingStageSaveKey from "../../onboarding/stages/OnboardingStageKeySave.svelte";
8
+ import OnboardingStageTelemetry from "../../onboarding/stages/OnboardingStageTelemetry.svelte";
7
9
  import OnboardingStageWelcome from "../../onboarding/stages/OnboardingStageWelcome.svelte";
8
10
  import type { OnboardingStatusResponseDto } from "../../../fetch-client";
9
11
  import {
@@ -28,6 +30,7 @@
28
30
  let status: OnboardingStatusResponseDto | undefined = $state();
29
31
  let stage:
30
32
  | `welcome`
33
+ | `telemetry`
31
34
  | `key-${"intro" | "save" | "confirm" | "import"}`
32
35
  | `backup-${"service" | "confirm" | "create"}`
33
36
  | `finished` = $state("welcome");
@@ -36,12 +39,17 @@
36
39
  handleOnboardingStatus().then(async (data) => {
37
40
  status = data;
38
41
 
42
+ if (data.status !== "ready") {
43
+ return;
44
+ }
45
+
39
46
  if (data.hasOnboardedKey) {
40
- if (data.hasBackup) {
41
- stage = "finished";
42
- } else {
43
- stage = "backup-service";
44
- }
47
+ stage =
48
+ data.hasTelemetry === "none"
49
+ ? "telemetry"
50
+ : data.hasBackup
51
+ ? "finished"
52
+ : "backup-service";
45
53
  } else {
46
54
  const { recoveryKey } = await handleCurrentRecoveryKey();
47
55
  code = recoveryKey;
@@ -60,54 +68,64 @@
60
68
  };
61
69
  </script>
62
70
 
63
- {#if typeof status === "object"}
64
- {#if stage === "finished"}
65
- {@render children()}
66
- {:else if stage === "welcome"}
67
- <OnboardingStageWelcome
68
- onNext={() => (stage = "key-intro")}
69
- onImportKey={() => (stage = "key-import")}
70
- onCancel={onExit}
71
- />
72
- {:else if stage === "key-import"}
73
- <OnboardingStageKeyImport
74
- onStart={() => (stage = "welcome")}
75
- onImported={() => (stage = "key-confirm")}
76
- onCancel={onExit}
77
- />
78
- {:else if stage === "key-intro"}
79
- <OnboardingStageKeyIntro
80
- onNext={() => (stage = "key-save")}
81
- onCancel={onExit}
82
- />
83
- {:else if stage === "key-save"}
84
- <OnboardingStageSaveKey
85
- {code}
86
- onNext={() => (stage = "key-confirm")}
87
- onCancel={onExit}
88
- />
89
- {:else if stage === "key-confirm"}
90
- <OnboardingStageKeyConfirm
91
- {code}
92
- onBack={() => (stage = "key-save")}
93
- onCancel={onExit}
94
- {onConfirmKey}
95
- />
96
- {:else if stage === "backup-service"}
97
- <OnboardingStageBackupServices onNext={onSelectBackend} onCancel={onExit} />
98
- {:else if stage === "backup-confirm"}
99
- <ImmichConfirmDefaultBackup
100
- onCustomize={() => (stage = "backup-create")}
101
- onConfirm={() => (stage = "finished")}
102
- onCancel={onExit}
103
- />
104
- {:else if stage === "backup-create"}
105
- <ImmichConfigureBackup
106
- onFinish={() => (stage = "finished")}
107
- onCancel={onExit}
108
- {backendId}
109
- />
110
- {/if}
111
- {:else}
71
+ {#if status === undefined || status.status === "not-ready"}
112
72
  <LoadingSpinner />
73
+ {:else if status.status === "error"}
74
+ <OnboardingBootstrapError error={status.error} onQuit={onExit} />
75
+ {:else if stage === "finished"}
76
+ {@render children()}
77
+ {:else if stage === "welcome"}
78
+ <OnboardingStageWelcome
79
+ onNext={() => (stage = status?.hasTelemetry === "none" ? "telemetry" : "key-intro")}
80
+ onImportKey={() => (stage = "key-import")}
81
+ onCancel={onExit}
82
+ />
83
+ {:else if stage === "telemetry"}
84
+ <OnboardingStageTelemetry
85
+ onContinue={() =>
86
+ (stage = status?.hasOnboardedKey
87
+ ? status.hasBackup
88
+ ? "finished"
89
+ : "backup-service"
90
+ : "key-intro")}
91
+ onCancel={onExit}
92
+ />
93
+ {:else if stage === "key-import"}
94
+ <OnboardingStageKeyImport
95
+ onStart={() => (stage = "welcome")}
96
+ onImported={() => (stage = "key-confirm")}
97
+ onCancel={onExit}
98
+ />
99
+ {:else if stage === "key-intro"}
100
+ <OnboardingStageKeyIntro
101
+ onNext={() => (stage = "key-save")}
102
+ onCancel={onExit}
103
+ />
104
+ {:else if stage === "key-save"}
105
+ <OnboardingStageSaveKey
106
+ {code}
107
+ onNext={() => (stage = "key-confirm")}
108
+ onCancel={onExit}
109
+ />
110
+ {:else if stage === "key-confirm"}
111
+ <OnboardingStageKeyConfirm
112
+ {code}
113
+ onBack={() => (stage = "key-save")}
114
+ onCancel={onExit}
115
+ {onConfirmKey}
116
+ />
117
+ {:else if stage === "backup-service"}
118
+ <OnboardingStageBackupServices onNext={onSelectBackend} onCancel={onExit} />
119
+ {:else if stage === "backup-confirm"}
120
+ <ImmichConfirmDefaultBackup
121
+ onCustomize={() => (stage = "backup-create")}
122
+ onConfirm={() => (stage = "finished")}
123
+ onCancel={onExit}
124
+ />
125
+ {:else if stage === "backup-create"}
126
+ <ImmichConfigureBackup
127
+ onFinish={() => (stage = "finished")}
128
+ onCancel={onExit}
129
+ {backendId}
130
+ />
113
131
  {/if}
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import {
3
+ Button,
4
+ HStack,
5
+ Modal,
6
+ ModalBody,
7
+ ModalFooter,
8
+ Stack,
9
+ Text,
10
+ } from "@immich/ui";
11
+ import { useReportError } from "../../services/onboarding.service";
12
+
13
+ type Props = {
14
+ error?: string;
15
+ onQuit: () => void;
16
+ };
17
+
18
+ const { error, onQuit }: Props = $props();
19
+
20
+ const mutation = useReportError();
21
+
22
+ const onReportAndQuit = () =>
23
+ mutation.mutate(undefined, { onSuccess: onQuit });
24
+ </script>
25
+
26
+ <Modal size="small" title="Something went wrong" onClose={onQuit} icon={false}>
27
+ <ModalBody>
28
+ <Stack>
29
+ <Text>We ran into an error setting up backups...</Text>
30
+ {#if error}
31
+ <Text size="small" color="danger" class="font-mono whitespace-pre-wrap"
32
+ >{error}</Text
33
+ >
34
+ {/if}
35
+ </Stack>
36
+ </ModalBody>
37
+ <ModalFooter>
38
+ <HStack>
39
+ <Button
40
+ color="danger"
41
+ onclick={onReportAndQuit}
42
+ loading={mutation.isPending}>Report error and quit</Button
43
+ >
44
+ <Button variant="ghost" onclick={onQuit} disabled={mutation.isPending}
45
+ >Go back</Button
46
+ >
47
+ </HStack>
48
+ </ModalFooter>
49
+ </Modal>
@@ -0,0 +1,7 @@
1
+ type Props = {
2
+ error?: string;
3
+ onQuit: () => void;
4
+ };
5
+ declare const OnboardingBootstrapError: import("svelte").Component<Props, {}, "">;
6
+ type OnboardingBootstrapError = ReturnType<typeof OnboardingBootstrapError>;
7
+ export default OnboardingBootstrapError;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { LoadingSpinner } from "@immich/ui";
3
3
  import { onMount, type Snippet } from "svelte";
4
+ import OnboardingBootstrapError from "./OnboardingBootstrapError.svelte";
4
5
  import SampleOnboarding from "./SampleOnboarding.svelte";
5
6
  import type { OnboardingStatusResponseDto } from "../../fetch-client";
6
7
  import { handleOnboardingStatus } from "../../services/onboarding.service";
@@ -13,14 +14,16 @@
13
14
 
14
15
  const { onExit, onFinish, children }: Props = $props();
15
16
 
16
- let status: OnboardingStatusResponseDto | undefined = $state();
17
+ let onboarding: OnboardingStatusResponseDto | undefined = $state();
17
18
 
18
19
  onMount(() => {
19
- handleOnboardingStatus().then((data) => (status = data));
20
+ handleOnboardingStatus().then((data) => (onboarding = data));
20
21
  });
21
22
 
22
23
  function onSkip() {
23
- status = {
24
+ onboarding = {
25
+ status: "ready",
26
+ hasTelemetry: "full",
24
27
  hasBackend: true,
25
28
  hasOnboardedKey: true,
26
29
  hasBackup: true,
@@ -30,19 +33,19 @@
30
33
  }
31
34
  </script>
32
35
 
33
- {#if typeof status === "object"}
34
- {#if !(status.hasBackend && status.hasOnboardedKey && (status.hasSkippedExtraConfig || (status.hasBackup && status.hasSchedule)))}
35
- <SampleOnboarding
36
- {status}
37
- onFinish={() => (onFinish ? onFinish() : onSkip())}
38
- onCancel={() => {
39
- onSkip();
40
- onExit();
41
- }}
42
- />
43
- {:else}
44
- {@render children()}
45
- {/if}
46
- {:else}
36
+ {#if onboarding === undefined || onboarding.status === "not-ready"}
47
37
  <LoadingSpinner />
38
+ {:else if onboarding.status === "error"}
39
+ <OnboardingBootstrapError error={onboarding.error} onQuit={onExit} />
40
+ {:else if onboarding.hasTelemetry === "none" || !(onboarding.hasBackend && onboarding.hasOnboardedKey && (onboarding.hasSkippedExtraConfig || (onboarding.hasBackup && onboarding.hasSchedule)))}
41
+ <SampleOnboarding
42
+ status={onboarding}
43
+ onFinish={() => (onFinish ? onFinish() : onSkip())}
44
+ onCancel={() => {
45
+ onSkip();
46
+ onExit();
47
+ }}
48
+ />
49
+ {:else}
50
+ {@render children()}
48
51
  {/if}
@@ -13,6 +13,7 @@
13
13
  import ImportKey from "./stages/OnboardingStageKeyImport.svelte";
14
14
  import KeyIntro from "./stages/OnboardingStageKeyIntro.svelte";
15
15
  import SaveKey from "./stages/OnboardingStageKeySave.svelte";
16
+ import Telemetry from "./stages/OnboardingStageTelemetry.svelte";
16
17
  import Welcome from "./stages/OnboardingStageWelcome.svelte";
17
18
 
18
19
  type Props = {
@@ -28,17 +29,20 @@
28
29
  // svelte-ignore state_referenced_locally
29
30
  let stage:
30
31
  | "welcome"
32
+ | "telemetry"
31
33
  | `key-${"intro" | "save" | "confirm" | "import"}`
32
34
  | "backup-service"
33
35
  | "backup-create"
34
36
  | "schedule-create" = $state(
35
37
  !status.hasOnboardedKey
36
38
  ? "welcome"
37
- : !status.hasBackend
38
- ? "backup-service"
39
- : !status.hasBackup
40
- ? "backup-create"
41
- : "schedule-create",
39
+ : status.hasTelemetry === "none"
40
+ ? "telemetry"
41
+ : !status.hasBackend
42
+ ? "backup-service"
43
+ : !status.hasBackup
44
+ ? "backup-create"
45
+ : "schedule-create",
42
46
  );
43
47
 
44
48
  onMount(() => {
@@ -65,10 +69,22 @@
65
69
 
66
70
  {#if stage === "welcome"}
67
71
  <Welcome
68
- onNext={() => (stage = "key-intro")}
72
+ onNext={() => (stage = status.hasTelemetry === "none" ? "telemetry" : "key-intro")}
69
73
  onImportKey={() => (stage = "key-import")}
70
74
  {onCancel}
71
75
  />
76
+ {:else if stage === "telemetry"}
77
+ <Telemetry
78
+ onContinue={() =>
79
+ (stage = !status.hasOnboardedKey
80
+ ? "key-intro"
81
+ : !status.hasBackend
82
+ ? "backup-service"
83
+ : !status.hasBackup
84
+ ? "backup-create"
85
+ : "schedule-create")}
86
+ {onCancel}
87
+ />
72
88
  {:else if stage === "key-intro"}
73
89
  <KeyIntro onNext={() => (stage = "key-save")} {onCancel} />
74
90
  {:else if stage === "key-save"}
@@ -3,12 +3,13 @@
3
3
  import StackListItem from "../../ui/StackListItem.svelte";
4
4
  import {
5
5
  handleSetupLocalStorage,
6
- handleYuccaLogin,
6
+ useYuccaLogin,
7
7
  } from "../../../services/backend.service";
8
8
  import {
9
9
  Button,
10
10
  HStack,
11
11
  Icon,
12
+ LoadingSpinner,
12
13
  Modal,
13
14
  ModalBody,
14
15
  ModalFooter,
@@ -25,8 +26,10 @@
25
26
 
26
27
  const { restore = false, onNext, onCancel }: Props = $props();
27
28
 
29
+ const yuccaLogin = useYuccaLogin();
30
+
28
31
  function onFutoBackups() {
29
- handleYuccaLogin(onNext);
32
+ yuccaLogin.mutate(onNext);
30
33
  }
31
34
 
32
35
  function onLocalBackups() {
@@ -44,7 +47,7 @@
44
47
  >
45
48
  <ModalBody>
46
49
  <StackList>
47
- <StackListItem onclick={onFutoBackups}>
50
+ <StackListItem disabled={yuccaLogin.isPending} onclick={onFutoBackups}>
48
51
  {#snippet icon()}
49
52
  <Icon icon={mdiShieldCheck} size="36px" />
50
53
  {/snippet}
@@ -55,10 +58,14 @@
55
58
  </Stack>
56
59
 
57
60
  {#snippet trailing()}
58
- <Icon icon={mdiChevronRight} />
61
+ {#if yuccaLogin.isPending}
62
+ <LoadingSpinner />
63
+ {:else}
64
+ <Icon icon={mdiChevronRight} />
65
+ {/if}
59
66
  {/snippet}
60
67
  </StackListItem>
61
- <StackListItem onclick={onLocalBackups}>
68
+ <StackListItem disabled={yuccaLogin.isPending} onclick={onLocalBackups}>
62
69
  {#snippet icon()}
63
70
  <Icon icon={mdiHarddisk} size="36px" />
64
71
  {/snippet}
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import {
3
+ Button,
4
+ HStack,
5
+ Icon,
6
+ Modal,
7
+ ModalBody,
8
+ ModalFooter,
9
+ Stack,
10
+ Text,
11
+ } from "@immich/ui";
12
+ import { useEnableTelemetry } from "../../../services/onboarding.service";
13
+ import { mdiChartBox, mdiEyeOff } from "@mdi/js";
14
+
15
+ type Props = {
16
+ onContinue: () => void;
17
+ onCancel: () => void;
18
+ };
19
+
20
+ const { onContinue, onCancel }: Props = $props();
21
+
22
+ const mutation = useEnableTelemetry();
23
+
24
+ const onConfirm = () => mutation.mutate(undefined, { onSuccess: onContinue });
25
+ </script>
26
+
27
+ <Modal
28
+ size="small"
29
+ title="Telemetry required for closed beta"
30
+ onClose={onCancel}
31
+ >
32
+ <ModalBody>
33
+ <Stack>
34
+ <HStack>
35
+ <Icon icon={mdiChartBox} class="shrink-0 place-self-start mt-1" />
36
+ <Text>
37
+ We collect usage and diagnostic data to understand how FUTO Backups is
38
+ used and to find problems.</Text
39
+ >
40
+ </HStack>
41
+ <HStack>
42
+ <Icon icon={mdiEyeOff} class="shrink-0 place-self-start mt-1" />
43
+ <Text>
44
+ Your photos, files, and recovery key are never collected and never
45
+ leave your device unencrypted.</Text
46
+ >
47
+ </HStack>
48
+ </Stack>
49
+ </ModalBody>
50
+ <ModalFooter>
51
+ <HStack>
52
+ <Button onclick={onConfirm} loading={mutation.isPending}>Continue</Button>
53
+ <Button
54
+ variant="ghost"
55
+ onclick={onCancel}
56
+ disabled={mutation.isPending}>Cancel</Button
57
+ >
58
+ </HStack>
59
+ </ModalFooter>
60
+ </Modal>
@@ -0,0 +1,7 @@
1
+ type Props = {
2
+ onContinue: () => void;
3
+ onCancel: () => void;
4
+ };
5
+ declare const OnboardingStageTelemetry: import("svelte").Component<Props, {}, "">;
6
+ type OnboardingStageTelemetry = ReturnType<typeof OnboardingStageTelemetry>;
7
+ export default OnboardingStageTelemetry;
@@ -82,7 +82,12 @@ export type ConfigureImmichIntegrationRequestDto = {
82
82
  libraries: "all" | string[];
83
83
  retentionPolicy?: (RetentionPolicyDto) | null;
84
84
  };
85
+ export type BootstrapStatus = "not-ready" | "ready" | "error";
86
+ export type TelemetryLevel = "full" | "none";
85
87
  export type OnboardingStatusResponseDto = {
88
+ status: BootstrapStatus;
89
+ error?: string;
90
+ hasTelemetry: TelemetryLevel;
86
91
  hasOnboardedKey: boolean;
87
92
  hasBackend: boolean;
88
93
  hasBackup: boolean;
@@ -269,6 +274,8 @@ export declare function currentRecoveryKey(opts?: Oazapfts.RequestOpts): Promise
269
274
  export declare function importRecoveryKey(importRecoveryKeyRequest: ImportRecoveryKeyRequest, opts?: Oazapfts.RequestOpts): Promise<never>;
270
275
  export declare function confirmRecoveryKey(opts?: Oazapfts.RequestOpts): Promise<never>;
271
276
  export declare function skipOnboardingExtraConfig(opts?: Oazapfts.RequestOpts): Promise<never>;
277
+ export declare function enableTelemetry(opts?: Oazapfts.RequestOpts): Promise<never>;
278
+ export declare function reportError(opts?: Oazapfts.RequestOpts): Promise<never>;
272
279
  export declare function createRepository(repositoryCreateRequestDto: RepositoryCreateRequestDto, { backend }?: {
273
280
  backend?: string;
274
281
  }, opts?: Oazapfts.RequestOpts): Promise<RepositoryCreateResponseDto>;
@@ -85,6 +85,18 @@ export function skipOnboardingExtraConfig(opts) {
85
85
  method: "POST"
86
86
  }));
87
87
  }
88
+ export function enableTelemetry(opts) {
89
+ return oazapfts.ok(oazapfts.fetchText("/api/yucca/onboarding/telemetry", {
90
+ ...opts,
91
+ method: "POST"
92
+ }));
93
+ }
94
+ export function reportError(opts) {
95
+ return oazapfts.ok(oazapfts.fetchText("/api/yucca/onboarding/report-error", {
96
+ ...opts,
97
+ method: "POST"
98
+ }));
99
+ }
88
100
  export function createRepository(repositoryCreateRequestDto, { backend } = {}, opts) {
89
101
  return oazapfts.ok(oazapfts.fetchJson(`/api/yucca/repository${QS.query(QS.explode({
90
102
  backend
@@ -10,14 +10,14 @@ export declare const useBackendEventHandler: () => {
10
10
  backend: BackendDto;
11
11
  }>): void;
12
12
  };
13
- export declare const handleYuccaLogin: (onCreate?: (backendId: string) => void) => Promise<void>;
13
+ export declare const useYuccaLogin: () => import("@tanstack/svelte-query").CreateMutationResult<void, Error, ((backendId: string) => void) | undefined, unknown>;
14
14
  export declare const handleSetupLocalStorage: (onCreate?: (backendId: string) => void) => void;
15
15
  export declare const useCreateLocalBackend: () => import("@tanstack/svelte-query").CreateMutationResult<import("../fetch-client").BackendResponseDto, Error, CreateLocalBackendRequestDto, unknown>;
16
16
  export declare const handleReconfigureRepositoryBackend: (repository: LocalRepositoryDto) => Promise<void>;
17
17
  export declare const handleRemoveRepositoryBackend: (_backend: BackendDto, _repositoryBackend: RepositoryBackendDto) => void;
18
18
  export declare const getBackendActions: (repository: LocalRepositoryDto | undefined, backend: BackendDto, repositoryBackend?: RepositoryBackendDto & {
19
19
  primary?: boolean;
20
- }) => {
20
+ }, onLogin?: () => void) => {
21
21
  LoginAgain: ActionItem;
22
22
  Reconfigure: ActionItem;
23
23
  Remove: ActionItem;
@@ -29,20 +29,18 @@ export const useBackendEventHandler = () => {
29
29
  },
30
30
  };
31
31
  };
32
- export const handleYuccaLogin = async (onCreate) => {
33
- try {
34
- const response = await oidcDeviceFlow();
35
- void modalManager.show(OAuthDeviceFlow, {
36
- ...response,
37
- onCreate,
38
- });
39
- window.open(response.verificationUri, '_blank');
40
- }
41
- catch (error) {
42
- handleError(error, 'Failed to start login');
43
- throw error;
44
- }
32
+ const startYuccaLogin = async (onCreate) => {
33
+ const response = await oidcDeviceFlow();
34
+ void modalManager.show(OAuthDeviceFlow, {
35
+ ...response,
36
+ onCreate,
37
+ });
38
+ window.open(response.verificationUri, '_blank');
45
39
  };
40
+ export const useYuccaLogin = () => createMutation(() => ({
41
+ mutationFn: (onCreate) => startYuccaLogin(onCreate),
42
+ onError: (error) => handleError(error, 'Failed to start login'),
43
+ }), () => queryClient);
46
44
  export const handleSetupLocalStorage = (onCreate) => {
47
45
  void modalManager.show(CreateLocalBackend, { onCreate });
48
46
  };
@@ -58,11 +56,11 @@ export const handleReconfigureRepositoryBackend = async (repository) => {
58
56
  export const handleRemoveRepositoryBackend = (_backend, _repositoryBackend) => {
59
57
  alert('TODO: implement me when multi-repository-backend support is added');
60
58
  };
61
- export const getBackendActions = (repository, backend, repositoryBackend) => {
59
+ export const getBackendActions = (repository, backend, repositoryBackend, onLogin) => {
62
60
  const LoginAgain = {
63
61
  icon: mdiLogin,
64
62
  title: 'Login again',
65
- onAction: () => void handleYuccaLogin(),
63
+ onAction: () => onLogin?.(),
66
64
  $if: () => backend.type === 'yucca' && !backend.isOnline,
67
65
  };
68
66
  const Reconfigure = {
@@ -9,3 +9,5 @@ export declare const handleCurrentRecoveryKey: () => Promise<sdk.CurrentRecovery
9
9
  export declare const handleConfirmRecoveryKey: () => Promise<void>;
10
10
  export declare const handleImportRecoveryKey: (dto: ImportRecoveryKeyRequest) => Promise<void>;
11
11
  export declare const handleSkipOnboardingExtraConfig: () => Promise<void>;
12
+ export declare const useEnableTelemetry: () => import("@tanstack/svelte-query").CreateMutationResult<never, Error, void, unknown>;
13
+ export declare const useReportError: () => import("@tanstack/svelte-query").CreateMutationResult<never, Error, void, unknown>;
@@ -1,7 +1,7 @@
1
1
  import { sdk } from '..';
2
2
  import { queryClient } from '../query-client';
3
3
  import { handleError } from '../utils/handle-error';
4
- import { createQuery } from '@tanstack/svelte-query';
4
+ import { createMutation, createQuery } from '@tanstack/svelte-query';
5
5
  export const recoveryKeyKeys = {
6
6
  all: ['recovery-key'],
7
7
  };
@@ -54,3 +54,11 @@ export const handleSkipOnboardingExtraConfig = async () => {
54
54
  throw error;
55
55
  }
56
56
  };
57
+ export const useEnableTelemetry = () => createMutation(() => ({
58
+ mutationFn: () => sdk.enableTelemetry(),
59
+ onError: (error) => handleError(error, 'Failed to save preferences'),
60
+ }), () => queryClient);
61
+ export const useReportError = () => createMutation(() => ({
62
+ mutationFn: () => sdk.reportError(),
63
+ onError: (error) => handleError(error, 'Failed to report error'),
64
+ }), () => queryClient);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@futo-org/backups-orchestrator-ui",
3
3
  "repository": "https://github.com/immich-app/yucca",
4
4
  "description": "Backups orchestrator (UI library)",
5
- "version": "0.3.1",
5
+ "version": "0.5.0",
6
6
  "license": "Source First License 1.1",
7
7
  "files": [
8
8
  "dist",
@@ -64,7 +64,7 @@
64
64
  "lodash.debounce": "^4.0.8",
65
65
  "luxon": "^3.7.2",
66
66
  "socket.io-client": "^4.8.3",
67
- "@futo-org/backups-api-client": "^0.3.1"
67
+ "@futo-org/backups-api-client": "^0.5.0"
68
68
  },
69
69
  "scripts": {
70
70
  "dev": "vite dev --port 5174",