@futo-org/backups-orchestrator-ui 0.12.0 → 0.13.1

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.
@@ -7,7 +7,7 @@
7
7
  } from "../../fetch-client";
8
8
  import {
9
9
  getBackendActions,
10
- useYuccaLogin,
10
+ handleStartYuccaLogin,
11
11
  } from "../../services/backend.service";
12
12
  import { Badge, Icon, Text } from "@immich/ui";
13
13
  import { mdiCloudOutline, mdiHarddisk, mdiShieldCheckOutline } from "@mdi/js";
@@ -33,11 +33,9 @@
33
33
  s3: "S3 Server",
34
34
  };
35
35
 
36
- const yuccaLogin = useYuccaLogin();
37
-
38
36
  const { LoginAgain, Reconfigure } = $derived(
39
37
  getBackendActions(repository, backend, repositoryBackend, () =>
40
- yuccaLogin.mutate(undefined),
38
+ handleStartYuccaLogin(),
41
39
  ),
42
40
  );
43
41
  </script>
@@ -3,9 +3,9 @@
3
3
  import { options } from "../../options";
4
4
  import {
5
5
  handleSetupLocalStorage,
6
+ handleStartYuccaLogin,
6
7
  useBackendEventHandler,
7
8
  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,7 +21,6 @@
21
21
 
22
22
  const { advanced } = options;
23
23
  const query = useBackends();
24
- const yuccaLogin = useYuccaLogin();
25
24
  const { onBackendCreate } = useBackendEventHandler();
26
25
 
27
26
  const repositoryBackends = $derived(
@@ -65,8 +64,7 @@
65
64
  <Button
66
65
  size="small"
67
66
  variant="outline"
68
- loading={yuccaLogin.isPending}
69
- onclick={() => yuccaLogin.mutate(undefined)}>Login with FUTO Backups</Button
67
+ onclick={() => handleStartYuccaLogin()}>Login with FUTO Backups</Button
70
68
  >
71
69
  <Button
72
70
  size="small"
@@ -1,7 +1,11 @@
1
1
  <script lang="ts">
2
+ import Suspense from "../../util/Suspense.svelte";
2
3
  import type { SocketEvent } from "../../../events";
3
- import type { BackendDto } from "../../../fetch-client";
4
- import { useBackendEventHandler } from "../../../services/backend.service";
4
+ import { type BackendDto } from "../../../fetch-client";
5
+ import {
6
+ useBackendEventHandler,
7
+ useDeviceFlow,
8
+ } from "../../../services/backend.service";
5
9
  import {
6
10
  Button,
7
11
  Code,
@@ -20,13 +24,14 @@
20
24
  import OnEvents from "../../util/OnEvents.svelte";
21
25
 
22
26
  type Props = {
23
- userCode: string;
24
- verificationUri: string;
25
27
  onCreate?: (backendId: string) => void;
26
28
  onClose: () => void;
27
29
  };
28
30
 
29
- let { userCode, verificationUri, onCreate, onClose }: Props = $props();
31
+ let { onCreate, onClose }: Props = $props();
32
+
33
+ const uid = $props.id();
34
+ const query = useDeviceFlow(uid);
30
35
 
31
36
  const { onBackendCreate: onBackendCreateHandler } = useBackendEventHandler();
32
37
 
@@ -41,12 +46,12 @@
41
46
  onClose();
42
47
  }
43
48
 
44
- function onOpen() {
45
- window.open(verificationUri, "_blank");
49
+ function onRetry() {
50
+ query.refetch();
46
51
  }
47
52
 
48
53
  function onCopy() {
49
- navigator.clipboard.writeText(userCode);
54
+ navigator.clipboard.writeText(query.data!.userCode);
50
55
  }
51
56
  </script>
52
57
 
@@ -54,31 +59,38 @@
54
59
 
55
60
  <Modal title="Logging into FUTO Backups" icon={false} {onClose}>
56
61
  <ModalBody>
57
- <VStack>
58
- <Text>You may be asked or shown the following code:</Text>
59
- <Stack direction="row" align="center">
60
- <Code class="text-3xl select-all">{userCode}</Code>
61
- <IconButton
62
- color="secondary"
63
- variant="outline"
64
- icon={mdiContentCopy}
65
- onclick={onCopy}
66
- aria-label="Copy code"
67
- />
68
- </Stack>
62
+ <Suspense {query}>
63
+ <VStack>
64
+ <Text>You may be asked or shown the following code:</Text>
65
+ <Stack direction="row" align="center">
66
+ <Code class="text-3xl select-all">{query.data!.userCode}</Code>
67
+ <IconButton
68
+ color="secondary"
69
+ variant="outline"
70
+ icon={mdiContentCopy}
71
+ onclick={onCopy}
72
+ aria-label="Copy code"
73
+ />
74
+ </Stack>
69
75
 
70
- <HStack class="mt-4">
71
- <LoadingSpinner />
72
- <Text>Waiting for you to confirm login...</Text>
73
- </HStack>
74
- </VStack>
76
+ <HStack class="mt-4">
77
+ <LoadingSpinner />
78
+ <Text>Waiting for you to confirm login...</Text>
79
+ </HStack>
80
+ </VStack>
81
+ </Suspense>
75
82
  </ModalBody>
76
83
  <ModalFooter>
77
84
  <HStack fullWidth>
78
85
  <Button shape="round" color="secondary" fullWidth onclick={onClose}>
79
86
  Cancel
80
87
  </Button>
81
- <Button shape="round" fullWidth onclick={onOpen}>Open login again</Button>
88
+ <Button
89
+ shape="round"
90
+ fullWidth
91
+ onclick={onRetry}
92
+ disabled={query.isFetching}>Try again</Button
93
+ >
82
94
  </HStack>
83
95
  </ModalFooter>
84
96
  </Modal>
@@ -1,6 +1,4 @@
1
1
  type Props = {
2
- userCode: string;
3
- verificationUri: string;
4
2
  onCreate?: (backendId: string) => void;
5
3
  onClose: () => void;
6
4
  };
@@ -4,14 +4,13 @@
4
4
  import Suspense from "../../util/Suspense.svelte";
5
5
  import {
6
6
  handleSetupLocalStorage,
7
+ handleStartYuccaLogin,
7
8
  useBackends,
8
- useYuccaLogin,
9
9
  } from "../../../services/backend.service";
10
10
  import {
11
11
  Button,
12
12
  HStack,
13
13
  Icon,
14
- LoadingSpinner,
15
14
  Modal,
16
15
  ModalBody,
17
16
  ModalFooter,
@@ -38,7 +37,6 @@
38
37
  }: Props = $props();
39
38
 
40
39
  const backends = useBackends();
41
- const yuccaLogin = useYuccaLogin();
42
40
 
43
41
  const onFutoBackups = () => {
44
42
  const futoBackend = backends.data!.find(
@@ -48,7 +46,7 @@
48
46
  if (futoBackend) {
49
47
  onSelect(futoBackend.id);
50
48
  } else {
51
- yuccaLogin.mutate(onSelect);
49
+ handleStartYuccaLogin(onSelect);
52
50
  }
53
51
  };
54
52
 
@@ -65,7 +63,7 @@
65
63
 
66
64
  <Suspense query={backends}>
67
65
  <StackList>
68
- <StackListItem disabled={disabled || yuccaLogin.isPending} onclick={onFutoBackups}>
66
+ <StackListItem {disabled} onclick={onFutoBackups}>
69
67
  {#snippet icon()}
70
68
  <Icon icon={mdiShieldCheck} size="36px" />
71
69
  {/snippet}
@@ -76,14 +74,10 @@
76
74
  </Stack>
77
75
 
78
76
  {#snippet trailing()}
79
- {#if yuccaLogin.isPending}
80
- <LoadingSpinner />
81
- {:else}
82
- <Icon icon={mdiChevronRight} />
83
- {/if}
77
+ <Icon icon={mdiChevronRight} />
84
78
  {/snippet}
85
79
  </StackListItem>
86
- <StackListItem disabled={disabled || yuccaLogin.isPending} onclick={onLocalBackups}>
80
+ <StackListItem {disabled} onclick={onLocalBackups}>
87
81
  {#snippet icon()}
88
82
  <Icon icon={mdiHarddisk} size="36px" />
89
83
  {/snippet}
@@ -101,7 +95,7 @@
101
95
  {#each backends.data as backend}
102
96
  {#if backend.type === "local"}
103
97
  <StackListItem
104
- disabled={disabled || yuccaLogin.isPending}
98
+ {disabled}
105
99
  onclick={() => {
106
100
  onSelect(backend.id);
107
101
  }}
@@ -3,13 +3,12 @@
3
3
  import StackListItem from "../../ui/StackListItem.svelte";
4
4
  import {
5
5
  handleSetupLocalStorage,
6
- useYuccaLogin,
6
+ handleStartYuccaLogin,
7
7
  } from "../../../services/backend.service";
8
8
  import {
9
9
  Button,
10
10
  HStack,
11
11
  Icon,
12
- LoadingSpinner,
13
12
  Modal,
14
13
  ModalBody,
15
14
  ModalFooter,
@@ -26,10 +25,8 @@
26
25
 
27
26
  const { restore = false, onNext, onCancel }: Props = $props();
28
27
 
29
- const yuccaLogin = useYuccaLogin();
30
-
31
28
  function onFutoBackups() {
32
- yuccaLogin.mutate(onNext);
29
+ handleStartYuccaLogin(onNext);
33
30
  }
34
31
 
35
32
  function onLocalBackups() {
@@ -47,7 +44,7 @@
47
44
  >
48
45
  <ModalBody>
49
46
  <StackList>
50
- <StackListItem disabled={yuccaLogin.isPending} onclick={onFutoBackups}>
47
+ <StackListItem onclick={onFutoBackups}>
51
48
  {#snippet icon()}
52
49
  <Icon icon={mdiShieldCheck} size="36px" />
53
50
  {/snippet}
@@ -58,14 +55,10 @@
58
55
  </Stack>
59
56
 
60
57
  {#snippet trailing()}
61
- {#if yuccaLogin.isPending}
62
- <LoadingSpinner />
63
- {:else}
64
- <Icon icon={mdiChevronRight} />
65
- {/if}
58
+ <Icon icon={mdiChevronRight} />
66
59
  {/snippet}
67
60
  </StackListItem>
68
- <StackListItem disabled={yuccaLogin.isPending} onclick={onLocalBackups}>
61
+ <StackListItem onclick={onLocalBackups}>
69
62
  {#snippet icon()}
70
63
  <Icon icon={mdiHarddisk} size="36px" />
71
64
  {/snippet}
@@ -3,14 +3,16 @@ import { type BackendDto, type CreateLocalBackendRequestDto, type LocalRepositor
3
3
  import { type ActionItem } from '@immich/ui';
4
4
  export declare const backendKeys: {
5
5
  all: readonly ["backends"];
6
+ deviceFlow: (uid: string) => readonly ["deviceFlow", string];
6
7
  };
7
8
  export declare const useBackends: () => import("@tanstack/svelte-query").CreateQueryResult<BackendDto[], Error>;
9
+ export declare const useDeviceFlow: (uid: string) => import("@tanstack/svelte-query").CreateQueryResult<import("../fetch-client").DeviceFlowResponseDto, Error>;
8
10
  export declare const useBackendEventHandler: () => {
9
11
  onBackendCreate(event: SocketEvent<{
10
12
  backend: BackendDto;
11
13
  }>): void;
12
14
  };
13
- export declare const useYuccaLogin: () => import("@tanstack/svelte-query").CreateMutationResult<void, Error, ((backendId: string) => void) | undefined, unknown>;
15
+ export declare const handleStartYuccaLogin: (onCreate?: (backendId: string) => void) => void;
14
16
  export declare const handleSetupLocalStorage: (onCreate?: (backendId: string) => void) => void;
15
17
  export declare const useCreateLocalBackend: () => import("@tanstack/svelte-query").CreateMutationResult<import("../fetch-client").BackendResponseDto, Error, CreateLocalBackendRequestDto, unknown>;
16
18
  export declare const handleReconfigureRepositoryBackend: (repository: LocalRepositoryDto) => Promise<void>;
@@ -10,11 +10,22 @@ import { mdiLogin } from '@mdi/js';
10
10
  import { createMutation, createQuery } from '@tanstack/svelte-query';
11
11
  export const backendKeys = {
12
12
  all: ['backends'],
13
+ deviceFlow: (uid) => ['deviceFlow', uid],
13
14
  };
14
15
  export const useBackends = () => createQuery(() => ({
15
16
  queryKey: backendKeys.all,
16
17
  queryFn: () => getBackends().then(({ backends }) => backends),
17
18
  }), () => queryClient);
19
+ export const useDeviceFlow = (uid) => createQuery(() => ({
20
+ queryKey: backendKeys.deviceFlow(uid),
21
+ queryFn: async () => {
22
+ const response = await oidcDeviceFlow();
23
+ window.open(response.verificationUri, '_blank');
24
+ return response;
25
+ },
26
+ refetchOnReconnect: false,
27
+ refetchOnWindowFocus: false,
28
+ }), () => queryClient);
18
29
  export const useBackendEventHandler = () => {
19
30
  return {
20
31
  onBackendCreate(event) {
@@ -29,18 +40,11 @@ export const useBackendEventHandler = () => {
29
40
  },
30
41
  };
31
42
  };
32
- const startYuccaLogin = async (onCreate) => {
33
- const response = await oidcDeviceFlow();
43
+ export const handleStartYuccaLogin = (onCreate) => {
34
44
  void modalManager.show(OAuthDeviceFlow, {
35
- ...response,
36
45
  onCreate,
37
46
  });
38
- window.open(response.verificationUri, '_blank');
39
47
  };
40
- export const useYuccaLogin = () => createMutation(() => ({
41
- mutationFn: (onCreate) => startYuccaLogin(onCreate),
42
- onError: (error) => handleError(error, 'Failed to start login'),
43
- }), () => queryClient);
44
48
  export const handleSetupLocalStorage = (onCreate) => {
45
49
  void modalManager.show(CreateLocalBackend, { onCreate });
46
50
  };
@@ -37,6 +37,8 @@ type LogEvent = SummaryEvent | {
37
37
  percent_done: number;
38
38
  seconds_remaining?: number;
39
39
  current_files?: string[];
40
+ bytes_done?: number;
41
+ total_bytes?: number;
40
42
  };
41
43
  export type LogStatus = {
42
44
  progress: number;
@@ -34,7 +34,10 @@ export function createLogObserver(logId) {
34
34
  case 'status': {
35
35
  state.status = {
36
36
  progress: event.percent_done,
37
- text: event.seconds_remaining && event.percent_done < 100
37
+ text: event.seconds_remaining &&
38
+ (event.bytes_done && event.total_bytes
39
+ ? event.bytes_done < event.total_bytes
40
+ : event.percent_done < 100)
38
41
  ? `${formatDuration(event.seconds_remaining * 1000)} remaining`
39
42
  : 'Nearly done.',
40
43
  currentFiles: event.current_files ?? [],
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.12.0",
5
+ "version": "0.13.1",
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.12.0"
67
+ "@futo-org/backups-api-client": "^0.13.1"
68
68
  },
69
69
  "scripts": {
70
70
  "dev": "vite dev --port 5174",