@futo-org/backups-orchestrator-ui 0.38.0 → 0.40.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.
- package/dist/components/backends/dialogs/OAuthDeviceFlowModal.svelte +30 -55
- package/dist/components/backups/BackupItem.svelte +20 -4
- package/dist/components/backups/BackupItem.svelte.d.ts +1 -0
- package/dist/components/backups/BackupsList.svelte +1 -1
- package/dist/components/backups/dialogs/ConfigureRepositoryModal.svelte +1 -27
- package/dist/components/backups/dialogs/SelectRepositoryModal.svelte +3 -4
- package/dist/components/integrations/immich/settings/ImmichSettingsStorage.svelte +5 -9
- package/dist/components/onboarding/OnboardingGate.svelte +7 -0
- package/dist/components/onboarding/OnboardingLogin.svelte +64 -0
- package/dist/components/onboarding/OnboardingLogin.svelte.d.ts +6 -0
- package/dist/components/onboarding/restore-point-flow/RestorePointFlow2SelectSnapshot.svelte +4 -2
- package/dist/components/util/DeviceFlowAction.svelte +21 -0
- package/dist/components/util/DeviceFlowAction.svelte.d.ts +7 -0
- package/dist/components/util/DeviceFlowCode.svelte +36 -0
- package/dist/components/util/DeviceFlowCode.svelte.d.ts +7 -0
- package/dist/fetch-client.d.ts +32 -8
- package/dist/fetch-client.js +20 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/backend.service.d.ts +0 -2
- package/dist/services/backend.service.js +1 -15
- package/dist/services/deviceFlow.service.svelte.d.ts +19 -0
- package/dist/services/deviceFlow.service.svelte.js +70 -0
- package/dist/services/repository.service.d.ts +4 -5
- package/dist/services/repository.service.js +47 -8
- package/dist/services/session.service.d.ts +4 -0
- package/dist/services/session.service.js +19 -0
- package/package.json +2 -2
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import DeviceFlowAction from "../../util/DeviceFlowAction.svelte";
|
|
3
|
+
import DeviceFlowCode from "../../util/DeviceFlowCode.svelte";
|
|
3
4
|
import type { SocketEvent } from "../../../events";
|
|
4
5
|
import { type BackendDto } from "../../../fetch-client";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "../../../services/backend.service";
|
|
6
|
+
import { useBackendEventHandler } from "../../../services/backend.service";
|
|
7
|
+
import { createDeviceFlow } from "../../../services/deviceFlow.service.svelte";
|
|
8
|
+
import { useCreateSession } from "../../../services/session.service";
|
|
9
9
|
import {
|
|
10
10
|
Button,
|
|
11
|
-
Code,
|
|
12
11
|
HStack,
|
|
13
|
-
IconButton,
|
|
14
12
|
LoadingSpinner,
|
|
15
13
|
Modal,
|
|
16
14
|
ModalBody,
|
|
17
15
|
ModalFooter,
|
|
18
|
-
Stack,
|
|
19
16
|
Text,
|
|
20
|
-
toastManager,
|
|
21
|
-
VStack,
|
|
22
17
|
} from "@immich/ui";
|
|
23
|
-
import {
|
|
18
|
+
import { onDestroy } from "svelte";
|
|
24
19
|
import OnEvents from "../../util/OnEvents.svelte";
|
|
25
20
|
|
|
26
21
|
type Props = {
|
|
@@ -30,67 +25,47 @@
|
|
|
30
25
|
|
|
31
26
|
let { onCreate, onClose }: Props = $props();
|
|
32
27
|
|
|
33
|
-
const uid = $props.id();
|
|
34
|
-
const query = useDeviceFlow(uid);
|
|
35
|
-
|
|
36
28
|
const { onBackendCreate: onBackendCreateHandler } = useBackendEventHandler();
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
onBackendCreateHandler(event);
|
|
40
|
-
onCreate?.(event.data.backend.id);
|
|
41
|
-
onClose();
|
|
42
|
-
}
|
|
30
|
+
const session = useCreateSession();
|
|
43
31
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
const flow = createDeviceFlow("oidc", {
|
|
33
|
+
createSession: (token) => session.mutateAsync(token),
|
|
34
|
+
onComplete: (event) => {
|
|
35
|
+
if (event.backendId) {
|
|
36
|
+
onCreate?.(event.backendId);
|
|
37
|
+
}
|
|
48
38
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
39
|
+
onClose();
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
flow.start();
|
|
44
|
+
onDestroy(flow.stop);
|
|
52
45
|
|
|
53
|
-
function
|
|
54
|
-
|
|
46
|
+
function onBackendCreate(event: SocketEvent<{ backend: BackendDto }>) {
|
|
47
|
+
onBackendCreateHandler(event);
|
|
55
48
|
}
|
|
56
49
|
</script>
|
|
57
50
|
|
|
58
|
-
<OnEvents {onBackendCreate}
|
|
51
|
+
<OnEvents {onBackendCreate} />
|
|
59
52
|
|
|
60
53
|
<Modal title="Logging into FUTO Backups" icon={false} {onClose}>
|
|
61
54
|
<ModalBody>
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
variant="outline"
|
|
70
|
-
icon={mdiContentCopy}
|
|
71
|
-
onclick={onCopy}
|
|
72
|
-
aria-label="Copy code"
|
|
73
|
-
/>
|
|
74
|
-
</Stack>
|
|
75
|
-
|
|
76
|
-
<HStack class="mt-4">
|
|
77
|
-
<LoadingSpinner />
|
|
78
|
-
<Text>Waiting for you to confirm login...</Text>
|
|
79
|
-
</HStack>
|
|
80
|
-
</VStack>
|
|
81
|
-
</Suspense>
|
|
55
|
+
{#if flow.state.userCode}
|
|
56
|
+
<DeviceFlowCode {flow} />
|
|
57
|
+
{:else if flow.state.error}
|
|
58
|
+
<Text color="danger">{flow.state.error}</Text>
|
|
59
|
+
{:else}
|
|
60
|
+
<LoadingSpinner />
|
|
61
|
+
{/if}
|
|
82
62
|
</ModalBody>
|
|
83
63
|
<ModalFooter>
|
|
84
64
|
<HStack fullWidth>
|
|
85
65
|
<Button shape="round" color="secondary" fullWidth onclick={onClose}>
|
|
86
66
|
Cancel
|
|
87
67
|
</Button>
|
|
88
|
-
<
|
|
89
|
-
shape="round"
|
|
90
|
-
fullWidth
|
|
91
|
-
onclick={onRetry}
|
|
92
|
-
disabled={query.isFetching}>Try again</Button
|
|
93
|
-
>
|
|
68
|
+
<DeviceFlowAction {flow} />
|
|
94
69
|
</HStack>
|
|
95
70
|
</ModalFooter>
|
|
96
71
|
</Modal>
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
type Props = {
|
|
11
11
|
repository: LocalRepositoryDto;
|
|
12
|
+
local?: boolean
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
const { repository }: Props = $props();
|
|
15
|
+
const { repository, local }: Props = $props();
|
|
15
16
|
|
|
16
17
|
const BackendNames = {
|
|
17
18
|
yucca: "FUTO Backups",
|
|
@@ -21,14 +22,29 @@
|
|
|
21
22
|
|
|
22
23
|
const outcome = $derived(getBackupOutcome(repository.metrics));
|
|
23
24
|
|
|
24
|
-
const {
|
|
25
|
-
|
|
25
|
+
const {
|
|
26
|
+
BackupNow,
|
|
27
|
+
Snapshots,
|
|
28
|
+
History,
|
|
29
|
+
Configure,
|
|
30
|
+
Import,
|
|
31
|
+
MetricsHistory,
|
|
32
|
+
Delete,
|
|
33
|
+
} = $derived(getRepositoryActions(repository, local));
|
|
26
34
|
</script>
|
|
27
35
|
|
|
28
36
|
<StackListItem
|
|
29
37
|
title={repository.name}
|
|
30
38
|
color={outcome === "failed" ? "danger" : "primary"}
|
|
31
|
-
actions={[
|
|
39
|
+
actions={[
|
|
40
|
+
BackupNow,
|
|
41
|
+
Snapshots,
|
|
42
|
+
History,
|
|
43
|
+
Configure,
|
|
44
|
+
Import,
|
|
45
|
+
MetricsHistory,
|
|
46
|
+
Delete,
|
|
47
|
+
]}
|
|
32
48
|
>
|
|
33
49
|
{#snippet icon()}
|
|
34
50
|
<Icon icon={mdiArchiveOutline} />
|
|
@@ -4,15 +4,12 @@
|
|
|
4
4
|
import { type LocalRepositoryDto } from "../../../fetch-client";
|
|
5
5
|
import { options } from "../../../options";
|
|
6
6
|
import {
|
|
7
|
-
useRemoveRepository,
|
|
8
7
|
useUpdateRepository,
|
|
9
8
|
} from "../../../services/repository.service";
|
|
10
9
|
import {
|
|
11
|
-
Button,
|
|
12
10
|
Field,
|
|
13
11
|
FormModal,
|
|
14
12
|
Input,
|
|
15
|
-
modalManager,
|
|
16
13
|
Stack,
|
|
17
14
|
} from "@immich/ui";
|
|
18
15
|
import { SvelteSet } from "svelte/reactivity";
|
|
@@ -32,7 +29,6 @@
|
|
|
32
29
|
const local = $derived(typeof repository.configuration === "object");
|
|
33
30
|
|
|
34
31
|
const updateMutation = useUpdateRepository();
|
|
35
|
-
const removeMutation = useRemoveRepository();
|
|
36
32
|
|
|
37
33
|
const onSubmit = () =>
|
|
38
34
|
updateMutation.mutate(
|
|
@@ -40,30 +36,12 @@
|
|
|
40
36
|
{ onSuccess: () => onClose() },
|
|
41
37
|
);
|
|
42
38
|
|
|
43
|
-
const onRemove = async () => {
|
|
44
|
-
const confirm = await modalManager.showDialog({
|
|
45
|
-
confirmText: local ? "Remove" : "Delete",
|
|
46
|
-
title: local ? "Remove Repository" : "Delete Repository",
|
|
47
|
-
prompt: local
|
|
48
|
-
? "Repository will be removed locally."
|
|
49
|
-
: "This repository will be removed.",
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
if (!confirm) return;
|
|
53
|
-
|
|
54
|
-
removeMutation.mutate(
|
|
55
|
-
{ id: repository.id, local },
|
|
56
|
-
{ onSuccess: () => onClose() },
|
|
57
|
-
);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
39
|
const { advanced } = options;
|
|
61
40
|
</script>
|
|
62
41
|
|
|
63
42
|
<FormModal
|
|
64
43
|
disabled={name.length === 0 ||
|
|
65
|
-
updateMutation.isPending
|
|
66
|
-
removeMutation.isPending}
|
|
44
|
+
updateMutation.isPending}
|
|
67
45
|
title={`Configure ${name}`}
|
|
68
46
|
size="large"
|
|
69
47
|
{onSubmit}
|
|
@@ -89,10 +67,6 @@
|
|
|
89
67
|
</PathListField>
|
|
90
68
|
{/if}
|
|
91
69
|
|
|
92
|
-
<Button color="danger" loading={removeMutation.isPending} onclick={onRemove}
|
|
93
|
-
>Remove Repository from This Device</Button
|
|
94
|
-
>
|
|
95
|
-
|
|
96
70
|
{#if advanced}
|
|
97
71
|
<BackendsList {repository} />
|
|
98
72
|
{/if}
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
<Suspense {query}>
|
|
58
58
|
{#each sortedRepositories ?? [] as repository (repository.id)}
|
|
59
59
|
{@const accessible = repository.snapshots !== undefined}
|
|
60
|
+
{@const snapshots = repository.snapshots ?? []}
|
|
60
61
|
|
|
61
62
|
<StackListOption
|
|
62
63
|
title={repository.name}
|
|
@@ -65,10 +66,8 @@
|
|
|
65
66
|
>
|
|
66
67
|
{#if !accessible}
|
|
67
68
|
Can't access, is your recovery key correct?
|
|
68
|
-
{:else if
|
|
69
|
-
Last backup: {new Date(
|
|
70
|
-
repository.snapshots[0].time,
|
|
71
|
-
).toLocaleDateString()}
|
|
69
|
+
{:else if snapshots.length}
|
|
70
|
+
Last backup: {new Date(snapshots[0].time).toLocaleDateString()}
|
|
72
71
|
{:else}
|
|
73
72
|
No backups yet
|
|
74
73
|
{/if}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
useIntegrations,
|
|
10
10
|
} from "../../../../services/integrations.service";
|
|
11
11
|
import {
|
|
12
|
+
handleDisableWormRepository,
|
|
12
13
|
useRepositories,
|
|
13
14
|
useRepositoryEventHandler,
|
|
14
15
|
} from "../../../../services/repository.service";
|
|
@@ -85,7 +86,6 @@
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
name = repository.name;
|
|
88
|
-
worm = repository.worm;
|
|
89
89
|
|
|
90
90
|
const policy = repository.configuration?.retentionPolicy ?? null;
|
|
91
91
|
retentionKey =
|
|
@@ -153,17 +153,13 @@
|
|
|
153
153
|
<BackendsList {repository} />
|
|
154
154
|
|
|
155
155
|
{#if repository?.worm}
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
readOnly // @immich/ui forces enabled={false} when disabled={true}
|
|
160
|
-
>
|
|
161
|
-
<Switch checked class="cursor-not-allowed opacity-38" />
|
|
162
|
-
</Field>
|
|
156
|
+
<Button onclick={() => handleDisableWormRepository(repository.id, true)}>
|
|
157
|
+
Disable write-only
|
|
158
|
+
</Button>
|
|
163
159
|
{:else}
|
|
164
160
|
<Field
|
|
165
161
|
label="Write-only"
|
|
166
|
-
description="Once written, backups can't be removed.
|
|
162
|
+
description="Once written, backups can't be removed."
|
|
167
163
|
>
|
|
168
164
|
<Switch bind:checked={worm} />
|
|
169
165
|
</Field>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { LoadingSpinner } from "@immich/ui";
|
|
5
5
|
import { onMount, type Snippet } from "svelte";
|
|
6
6
|
import OnboardingBootstrapError from "./OnboardingBootstrapError.svelte";
|
|
7
|
+
import OnboardingLogin from "./OnboardingLogin.svelte";
|
|
7
8
|
import SampleOnboarding from "./SampleOnboarding.svelte";
|
|
8
9
|
|
|
9
10
|
type Props = {
|
|
@@ -23,6 +24,8 @@
|
|
|
23
24
|
function onSkip() {
|
|
24
25
|
onboarding = {
|
|
25
26
|
status: "ready",
|
|
27
|
+
requiresAuthentication: false,
|
|
28
|
+
isAuthenticated: true,
|
|
26
29
|
hasTelemetry: "full",
|
|
27
30
|
hasBackend: true,
|
|
28
31
|
hasOnboardedKey: true,
|
|
@@ -37,6 +40,10 @@
|
|
|
37
40
|
<LoadingSpinner />
|
|
38
41
|
{:else if onboarding.status === "error"}
|
|
39
42
|
<OnboardingBootstrapError error={onboarding.error} onQuit={onExit} />
|
|
43
|
+
{:else if onboarding.requiresAuthentication && !onboarding.isAuthenticated}
|
|
44
|
+
<OnboardingLogin
|
|
45
|
+
onAuthenticated={() => (onboarding!.isAuthenticated = true)}
|
|
46
|
+
/>
|
|
40
47
|
{:else if onboarding.hasTelemetry === "none" || !(onboarding.hasBackend && onboarding.hasOnboardedKey)}
|
|
41
48
|
<SampleOnboarding
|
|
42
49
|
status={onboarding}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import DeviceFlowAction from "../util/DeviceFlowAction.svelte";
|
|
3
|
+
import DeviceFlowCode from "../util/DeviceFlowCode.svelte";
|
|
4
|
+
import { createDeviceFlow } from "../../services/deviceFlow.service.svelte";
|
|
5
|
+
import { useCreateSession } from "../../services/session.service";
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
HStack,
|
|
9
|
+
Modal,
|
|
10
|
+
ModalBody,
|
|
11
|
+
ModalFooter,
|
|
12
|
+
Stack,
|
|
13
|
+
Text,
|
|
14
|
+
} from "@immich/ui";
|
|
15
|
+
import { onDestroy } from "svelte";
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
onAuthenticated: () => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const { onAuthenticated }: Props = $props();
|
|
22
|
+
|
|
23
|
+
const session = useCreateSession();
|
|
24
|
+
|
|
25
|
+
const flow = createDeviceFlow("session", {
|
|
26
|
+
createSession: (token) => session.mutateAsync(token),
|
|
27
|
+
onComplete: () => onAuthenticated(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
onDestroy(flow.stop);
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<Modal title="Log in to FUTO Backups" icon={false} onClose={() => {}}>
|
|
34
|
+
<ModalBody>
|
|
35
|
+
{#if flow.state.userCode}
|
|
36
|
+
<DeviceFlowCode {flow} />
|
|
37
|
+
{:else}
|
|
38
|
+
<Stack gap={4}>
|
|
39
|
+
<Text>
|
|
40
|
+
This instance is connected to a FUTO Backups account. Log in with that
|
|
41
|
+
account to manage it.
|
|
42
|
+
</Text>
|
|
43
|
+
|
|
44
|
+
{#if flow.state.error}
|
|
45
|
+
<Text color="danger">{flow.state.error}</Text>
|
|
46
|
+
{/if}
|
|
47
|
+
</Stack>
|
|
48
|
+
{/if}
|
|
49
|
+
</ModalBody>
|
|
50
|
+
<ModalFooter>
|
|
51
|
+
<HStack fullWidth>
|
|
52
|
+
{#if flow.state.userCode}
|
|
53
|
+
<DeviceFlowAction {flow} />
|
|
54
|
+
{:else}
|
|
55
|
+
<Button
|
|
56
|
+
shape="round"
|
|
57
|
+
fullWidth
|
|
58
|
+
loading={flow.state.pending}
|
|
59
|
+
onclick={flow.start}>Log in with FUTO</Button
|
|
60
|
+
>
|
|
61
|
+
{/if}
|
|
62
|
+
</HStack>
|
|
63
|
+
</ModalFooter>
|
|
64
|
+
</Modal>
|
package/dist/components/onboarding/restore-point-flow/RestorePointFlow2SelectSnapshot.svelte
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
const { onBack, onFinish, repository }: Props = $props();
|
|
29
29
|
|
|
30
30
|
let selectedSnapshot: SnapshotDto | undefined = $state();
|
|
31
|
+
|
|
32
|
+
const snapshots = $derived(repository.snapshots ?? []);
|
|
31
33
|
</script>
|
|
32
34
|
|
|
33
35
|
{#if selectedSnapshot}
|
|
@@ -45,7 +47,7 @@
|
|
|
45
47
|
>
|
|
46
48
|
<ModalBody>
|
|
47
49
|
<StackList>
|
|
48
|
-
{#each
|
|
50
|
+
{#each snapshots.toSorted((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime()) as snapshot (snapshot.id)}
|
|
49
51
|
<HStack gap={2} class="px-4 py-3">
|
|
50
52
|
<Stack gap={0} class="grow min-w-0">
|
|
51
53
|
<HStack
|
|
@@ -67,7 +69,7 @@
|
|
|
67
69
|
</Button>
|
|
68
70
|
</HStack>
|
|
69
71
|
{/each}
|
|
70
|
-
{#if
|
|
72
|
+
{#if snapshots.length === 0}
|
|
71
73
|
<Text class="text-center py-6" color="muted">
|
|
72
74
|
No snapshots in this repository.
|
|
73
75
|
</Text>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { DeviceFlow } from "../../services/deviceFlow.service.svelte";
|
|
3
|
+
import { Button } from "@immich/ui";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
flow: DeviceFlow;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const { flow }: Props = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
{#if flow.state.opened}
|
|
13
|
+
<Button shape="round" fullWidth onclick={flow.start}>Try again</Button>
|
|
14
|
+
{:else}
|
|
15
|
+
<Button
|
|
16
|
+
shape="round"
|
|
17
|
+
fullWidth
|
|
18
|
+
disabled={!flow.state.verificationUri}
|
|
19
|
+
onclick={flow.open}>Continue to login</Button
|
|
20
|
+
>
|
|
21
|
+
{/if}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DeviceFlow } from "../../services/deviceFlow.service.svelte";
|
|
2
|
+
type Props = {
|
|
3
|
+
flow: DeviceFlow;
|
|
4
|
+
};
|
|
5
|
+
declare const DeviceFlowAction: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type DeviceFlowAction = ReturnType<typeof DeviceFlowAction>;
|
|
7
|
+
export default DeviceFlowAction;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { DeviceFlow } from "../../services/deviceFlow.service.svelte";
|
|
3
|
+
import { Code, HStack, IconButton, LoadingSpinner, Stack, Text } from "@immich/ui";
|
|
4
|
+
import { mdiContentCopy } from "@mdi/js";
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
flow: DeviceFlow;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const { flow }: Props = $props();
|
|
11
|
+
|
|
12
|
+
function onCopy() {
|
|
13
|
+
navigator.clipboard.writeText(flow.state.userCode!);
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<Stack gap={4}>
|
|
18
|
+
<Text>You may be asked or shown the following code:</Text>
|
|
19
|
+
<Stack direction="row" align="center">
|
|
20
|
+
<Code class="text-3xl select-all">{flow.state.userCode}</Code>
|
|
21
|
+
<IconButton
|
|
22
|
+
color="secondary"
|
|
23
|
+
variant="outline"
|
|
24
|
+
icon={mdiContentCopy}
|
|
25
|
+
onclick={onCopy}
|
|
26
|
+
aria-label="Copy code"
|
|
27
|
+
/>
|
|
28
|
+
</Stack>
|
|
29
|
+
|
|
30
|
+
{#if flow.state.opened}
|
|
31
|
+
<HStack>
|
|
32
|
+
<LoadingSpinner />
|
|
33
|
+
<Text>Waiting for you to confirm login...</Text>
|
|
34
|
+
</HStack>
|
|
35
|
+
{/if}
|
|
36
|
+
</Stack>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DeviceFlow } from "../../services/deviceFlow.service.svelte";
|
|
2
|
+
type Props = {
|
|
3
|
+
flow: DeviceFlow;
|
|
4
|
+
};
|
|
5
|
+
declare const DeviceFlowCode: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type DeviceFlowCode = ReturnType<typeof DeviceFlowCode>;
|
|
7
|
+
export default DeviceFlowCode;
|
package/dist/fetch-client.d.ts
CHANGED
|
@@ -9,9 +9,28 @@ export declare const defaults: Oazapfts.Defaults<Oazapfts.CustomHeaders>;
|
|
|
9
9
|
export declare const servers: {
|
|
10
10
|
server1: string;
|
|
11
11
|
};
|
|
12
|
-
export type
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
export type DeviceFlowEventType = "START" | "SUCCESS" | "FAILURE";
|
|
13
|
+
export type DeviceFlowFailureReason = "NOT_CONNECTED" | "DEVICE_FLOW_FAILED" | "WRONG_ACCOUNT" | "UNKNOWN";
|
|
14
|
+
export type DeviceFlowEventDto = {
|
|
15
|
+
"type": DeviceFlowEventType;
|
|
16
|
+
userCode?: string;
|
|
17
|
+
verificationUri?: string;
|
|
18
|
+
token?: string;
|
|
19
|
+
backendId?: string;
|
|
20
|
+
reason?: DeviceFlowFailureReason;
|
|
21
|
+
};
|
|
22
|
+
export type CreateSessionRequestDto = {
|
|
23
|
+
token: string;
|
|
24
|
+
};
|
|
25
|
+
export type TicketAction = "repository.delete" | "repository.disable-worm";
|
|
26
|
+
export type TicketCreateRequestDto = {
|
|
27
|
+
action: TicketAction;
|
|
28
|
+
/** Repository the ticket is bound to */
|
|
29
|
+
repositoryId: string;
|
|
30
|
+
};
|
|
31
|
+
export type TicketCreateResponseDto = {
|
|
32
|
+
/** Identity provider URL the browser must be sent to */
|
|
33
|
+
redirectTo: string;
|
|
15
34
|
};
|
|
16
35
|
export type BackendType = "yucca" | "local" | "s3";
|
|
17
36
|
export type BackendDto = {
|
|
@@ -94,6 +113,8 @@ export type OnboardingStatusResponseDto = {
|
|
|
94
113
|
status: BootstrapStatus;
|
|
95
114
|
error?: string;
|
|
96
115
|
hasTelemetry: TelemetryLevel;
|
|
116
|
+
requiresAuthentication: boolean;
|
|
117
|
+
isAuthenticated: boolean;
|
|
97
118
|
hasOnboardedKey: boolean;
|
|
98
119
|
hasBackend: boolean;
|
|
99
120
|
hasBackup: boolean;
|
|
@@ -115,7 +136,7 @@ export type RepositoryCreateRequestDto = {
|
|
|
115
136
|
};
|
|
116
137
|
export type TaskStatus = "incomplete" | "complete" | "warn" | "failed";
|
|
117
138
|
export type RepositoryMetricsDto = {
|
|
118
|
-
lastBackup?: string;
|
|
139
|
+
lastBackup?: string | null;
|
|
119
140
|
lastBackupStatus?: TaskStatus;
|
|
120
141
|
lastBackupDuration?: number;
|
|
121
142
|
sizeBytes: number;
|
|
@@ -123,7 +144,7 @@ export type RepositoryMetricsDto = {
|
|
|
123
144
|
export type RepositoryMeterDto = {
|
|
124
145
|
sizeBytes: number;
|
|
125
146
|
objectCount: number;
|
|
126
|
-
lastUpdated?: string;
|
|
147
|
+
lastUpdated?: string | null;
|
|
127
148
|
};
|
|
128
149
|
export type RepositoryBackendDto = {
|
|
129
150
|
id: string;
|
|
@@ -180,7 +201,7 @@ export type InspectedLocalRepositoryDto = {
|
|
|
180
201
|
meter?: RepositoryMeterDto;
|
|
181
202
|
backends?: RepositoryBackendsDto;
|
|
182
203
|
configuration?: RepositoryConfigurationDto;
|
|
183
|
-
snapshots
|
|
204
|
+
snapshots?: SnapshotDto[];
|
|
184
205
|
};
|
|
185
206
|
export type RepositoryInspectResponseDto = {
|
|
186
207
|
repositories: InspectedLocalRepositoryDto[];
|
|
@@ -209,7 +230,7 @@ export type RunDto = {
|
|
|
209
230
|
id: string;
|
|
210
231
|
repositoryId: string;
|
|
211
232
|
start: string;
|
|
212
|
-
end
|
|
233
|
+
end?: string;
|
|
213
234
|
logFilePath: string;
|
|
214
235
|
status: RunStatus;
|
|
215
236
|
"type": RunType;
|
|
@@ -274,7 +295,10 @@ export type ScheduleUpdateRequestDto = {
|
|
|
274
295
|
export type ScheduleUpdateResponseDto = {
|
|
275
296
|
schedule: ScheduleDto;
|
|
276
297
|
};
|
|
277
|
-
export declare function
|
|
298
|
+
export declare function connectDeviceFlow(opts?: Oazapfts.RequestOpts): Promise<DeviceFlowEventDto>;
|
|
299
|
+
export declare function sessionDeviceFlow(opts?: Oazapfts.RequestOpts): Promise<DeviceFlowEventDto>;
|
|
300
|
+
export declare function createSession(createSessionRequestDto: CreateSessionRequestDto, opts?: Oazapfts.RequestOpts): Promise<never>;
|
|
301
|
+
export declare function createTicket(ticketCreateRequestDto: TicketCreateRequestDto, opts?: Oazapfts.RequestOpts): Promise<TicketCreateResponseDto>;
|
|
278
302
|
export declare function getBackends(opts?: Oazapfts.RequestOpts): Promise<BackendsResponseDto>;
|
|
279
303
|
export declare function createLocalBackend(createLocalBackendRequestDto: CreateLocalBackendRequestDto, opts?: Oazapfts.RequestOpts): Promise<BackendResponseDto>;
|
|
280
304
|
export declare function resetOrchestrator(opts?: Oazapfts.RequestOpts): Promise<never>;
|
package/dist/fetch-client.js
CHANGED
|
@@ -14,11 +14,30 @@ const oazapfts = Oazapfts.runtime(defaults);
|
|
|
14
14
|
export const servers = {
|
|
15
15
|
server1: "http://localhost:22676"
|
|
16
16
|
};
|
|
17
|
-
export function
|
|
17
|
+
export function connectDeviceFlow(opts) {
|
|
18
18
|
return oazapfts.ok(oazapfts.fetchJson("/api/yucca/auth/oidc/device", {
|
|
19
19
|
...opts
|
|
20
20
|
}));
|
|
21
21
|
}
|
|
22
|
+
export function sessionDeviceFlow(opts) {
|
|
23
|
+
return oazapfts.ok(oazapfts.fetchJson("/api/yucca/auth/session/device", {
|
|
24
|
+
...opts
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
export function createSession(createSessionRequestDto, opts) {
|
|
28
|
+
return oazapfts.ok(oazapfts.fetchText("/api/yucca/auth/session", oazapfts.json({
|
|
29
|
+
...opts,
|
|
30
|
+
method: "POST",
|
|
31
|
+
body: createSessionRequestDto
|
|
32
|
+
})));
|
|
33
|
+
}
|
|
34
|
+
export function createTicket(ticketCreateRequestDto, opts) {
|
|
35
|
+
return oazapfts.ok(oazapfts.fetchJson("/api/yucca/auth/ticket", oazapfts.json({
|
|
36
|
+
...opts,
|
|
37
|
+
method: "POST",
|
|
38
|
+
body: ticketCreateRequestDto
|
|
39
|
+
})));
|
|
40
|
+
}
|
|
22
41
|
export function getBackends(opts) {
|
|
23
42
|
return oazapfts.ok(oazapfts.fetchJson("/api/yucca/backend", {
|
|
24
43
|
...opts
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { default as Suspense } from './components/util/Suspense.svelte';
|
|
1
2
|
export { default as BackendsList } from './components/backends/BackendsList.svelte';
|
|
2
3
|
export { default as BackupsList } from './components/backups/BackupsList.svelte';
|
|
3
4
|
export { default as ViewStatusModal } from './components/backups/dialogs/ViewStatusModal.svelte';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { default as Suspense } from './components/util/Suspense.svelte';
|
|
1
2
|
export { default as BackendsList } from './components/backends/BackendsList.svelte';
|
|
2
3
|
export { default as BackupsList } from './components/backups/BackupsList.svelte';
|
|
3
4
|
export { default as ViewStatusModal } from './components/backups/dialogs/ViewStatusModal.svelte';
|
|
@@ -3,10 +3,8 @@ 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];
|
|
7
6
|
};
|
|
8
7
|
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>;
|
|
10
8
|
export declare const useBackendEventHandler: () => {
|
|
11
9
|
onBackendCreate(event: SocketEvent<{
|
|
12
10
|
backend: BackendDto;
|
|
@@ -2,7 +2,7 @@ import CreateLocalBackend from '../components/backends/dialogs/CreateLocalBacken
|
|
|
2
2
|
import OAuthDeviceFlow from '../components/backends/dialogs/OAuthDeviceFlowModal.svelte';
|
|
3
3
|
import ReconfigureRepositoryBackendModal from '../components/backups/dialogs/ReconfigureRepositoryBackendModal.svelte';
|
|
4
4
|
import { SocketEvent } from '../events';
|
|
5
|
-
import { createLocalBackend, getBackends,
|
|
5
|
+
import { createLocalBackend, getBackends, } from '../fetch-client';
|
|
6
6
|
import { queryClient } from '../query-client';
|
|
7
7
|
import { handleError } from '../utils/handle-error';
|
|
8
8
|
import { modalManager } from '@immich/ui';
|
|
@@ -10,25 +10,11 @@ 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],
|
|
14
13
|
};
|
|
15
14
|
export const useBackends = () => createQuery(() => ({
|
|
16
15
|
queryKey: backendKeys.all,
|
|
17
16
|
queryFn: () => getBackends().then(({ backends }) => backends),
|
|
18
17
|
}), () => 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
|
-
gcTime: Infinity,
|
|
27
|
-
retry: 0,
|
|
28
|
-
refetchOnReconnect: false,
|
|
29
|
-
refetchOnWindowFocus: false,
|
|
30
|
-
refetchOnMount: true,
|
|
31
|
-
}), () => queryClient);
|
|
32
18
|
export const useBackendEventHandler = () => {
|
|
33
19
|
return {
|
|
34
20
|
onBackendCreate(event) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DeviceFlowEventDto } from '../fetch-client';
|
|
2
|
+
import { type DeviceFlowKind } from './session.service';
|
|
3
|
+
export declare function createDeviceFlow(kind: DeviceFlowKind, handlers: {
|
|
4
|
+
createSession: (token: string) => Promise<unknown>;
|
|
5
|
+
onComplete: (event: DeviceFlowEventDto) => void;
|
|
6
|
+
onFailure?: (message: string) => void;
|
|
7
|
+
}): {
|
|
8
|
+
state: {
|
|
9
|
+
userCode: string | undefined;
|
|
10
|
+
verificationUri: string | undefined;
|
|
11
|
+
error: string | undefined;
|
|
12
|
+
pending: boolean;
|
|
13
|
+
opened: boolean;
|
|
14
|
+
};
|
|
15
|
+
start: () => void;
|
|
16
|
+
stop: () => void;
|
|
17
|
+
open: () => void;
|
|
18
|
+
};
|
|
19
|
+
export type DeviceFlow = ReturnType<typeof createDeviceFlow>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { startDeviceFlow, } from './session.service';
|
|
2
|
+
const failures = {
|
|
3
|
+
NOT_CONNECTED: 'This instance is not connected to a FUTO Backups account yet.',
|
|
4
|
+
DEVICE_FLOW_FAILED: 'Login was cancelled or timed out.',
|
|
5
|
+
WRONG_ACCOUNT: 'That account does not own this instance. Log in with the account it was connected with.',
|
|
6
|
+
UNKNOWN: 'Could not reach FUTO Backups. Check your connection.',
|
|
7
|
+
};
|
|
8
|
+
export function createDeviceFlow(kind, handlers) {
|
|
9
|
+
const state = $state({
|
|
10
|
+
userCode: undefined,
|
|
11
|
+
verificationUri: undefined,
|
|
12
|
+
error: undefined,
|
|
13
|
+
pending: false,
|
|
14
|
+
opened: false,
|
|
15
|
+
});
|
|
16
|
+
let close;
|
|
17
|
+
const stop = () => {
|
|
18
|
+
close?.();
|
|
19
|
+
close = undefined;
|
|
20
|
+
};
|
|
21
|
+
const complete = async (event) => {
|
|
22
|
+
if (event.token) {
|
|
23
|
+
await handlers.createSession(event.token);
|
|
24
|
+
}
|
|
25
|
+
state.pending = false;
|
|
26
|
+
handlers.onComplete(event);
|
|
27
|
+
};
|
|
28
|
+
const start = () => {
|
|
29
|
+
stop();
|
|
30
|
+
state.userCode = undefined;
|
|
31
|
+
state.error = undefined;
|
|
32
|
+
state.pending = true;
|
|
33
|
+
state.opened = false;
|
|
34
|
+
close = startDeviceFlow(kind, (event) => {
|
|
35
|
+
switch (event.type) {
|
|
36
|
+
case 'START': {
|
|
37
|
+
state.userCode = event.userCode;
|
|
38
|
+
state.verificationUri = event.verificationUri;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case 'SUCCESS': {
|
|
42
|
+
stop();
|
|
43
|
+
void complete(event);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case 'FAILURE': {
|
|
47
|
+
stop();
|
|
48
|
+
state.pending = false;
|
|
49
|
+
state.userCode = undefined;
|
|
50
|
+
state.error = failures[event.reason ?? 'UNKNOWN'];
|
|
51
|
+
handlers.onFailure?.(state.error);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const open = () => {
|
|
58
|
+
if (!state.verificationUri) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
state.opened = true;
|
|
62
|
+
window.open(state.verificationUri, 'futo-backups-device');
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
state,
|
|
66
|
+
start,
|
|
67
|
+
stop,
|
|
68
|
+
open,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -36,17 +36,16 @@ export declare const useReconfigureRepositoryPrimaryBackend: () => import("@tans
|
|
|
36
36
|
id: string;
|
|
37
37
|
backendId: string;
|
|
38
38
|
}, unknown>;
|
|
39
|
-
export declare const useRemoveRepository: () => import("@tanstack/svelte-query").CreateMutationResult<never, Error, {
|
|
40
|
-
id: string;
|
|
41
|
-
local?: boolean;
|
|
42
|
-
}, unknown>;
|
|
43
39
|
export declare const handleCreateBackup: (id: string) => Promise<sdk.LogResponseDto>;
|
|
44
40
|
export declare const handlePruneRepository: (id: string) => Promise<sdk.LogResponseDto>;
|
|
45
|
-
export declare const
|
|
41
|
+
export declare const handleDeleteRepository: (repositoryId: string, local?: boolean) => Promise<void>;
|
|
42
|
+
export declare const handleDisableWormRepository: (repositoryId: string, local?: boolean) => Promise<void>;
|
|
43
|
+
export declare const getRepositoryActions: (repository: LocalRepositoryDto, local?: boolean) => {
|
|
46
44
|
BackupNow: ActionItem;
|
|
47
45
|
Snapshots: ActionItem;
|
|
48
46
|
History: ActionItem;
|
|
49
47
|
Configure: ActionItem;
|
|
50
48
|
Import: ActionItem;
|
|
51
49
|
MetricsHistory: ActionItem;
|
|
50
|
+
Delete: ActionItem;
|
|
52
51
|
};
|
|
@@ -6,12 +6,13 @@ import MetricsHistoryModal from '../components/backups/metrics-history/MetricsHi
|
|
|
6
6
|
import RunHistoryModal from '../components/backups/run-history/RunHistoryModal.svelte';
|
|
7
7
|
import SnapshotsListModal from '../components/backups/snapshots-list/SnapshotsListModal.svelte';
|
|
8
8
|
import { SocketEvent } from '../events';
|
|
9
|
-
import {
|
|
9
|
+
import { inspectRepositories, reconfigureRepositoryPrimaryBackend, updateRepository, } from '../fetch-client';
|
|
10
10
|
import { getProvider } from '../providers';
|
|
11
11
|
import { queryClient } from '../query-client';
|
|
12
12
|
import { handleError } from '../utils/handle-error';
|
|
13
|
+
import { createTicket } from '@futo-org/backups-api-client';
|
|
13
14
|
import { modalManager, toastManager } from '@immich/ui';
|
|
14
|
-
import { mdiCog, mdiFormatListBulletedType, mdiHistory, mdiImport, mdiListStatus, mdiPlay, } from '@mdi/js';
|
|
15
|
+
import { mdiCog, mdiFormatListBulletedType, mdiHistory, mdiImport, mdiListStatus, mdiPlay, mdiTrashCan, } from '@mdi/js';
|
|
15
16
|
import { createMutation, createQuery } from '@tanstack/svelte-query';
|
|
16
17
|
export const repositoryKeys = {
|
|
17
18
|
all: ['repositories'],
|
|
@@ -76,10 +77,6 @@ export const useReconfigureRepositoryPrimaryBackend = () => createMutation(() =>
|
|
|
76
77
|
mutationFn: ({ id, backendId }) => reconfigureRepositoryPrimaryBackend(id, { backendId }),
|
|
77
78
|
onError: (error) => handleError(error, 'Failed to reconfigure backend'),
|
|
78
79
|
}), () => queryClient);
|
|
79
|
-
export const useRemoveRepository = () => createMutation(() => ({
|
|
80
|
-
mutationFn: ({ id, local = false }) => local ? sdk.deleteRepository(id) : deleteRepository(id),
|
|
81
|
-
onError: (error) => handleError(error, 'Failed to delete repository'),
|
|
82
|
-
}), () => queryClient);
|
|
83
80
|
export const handleCreateBackup = async (id) => {
|
|
84
81
|
try {
|
|
85
82
|
toastManager.info('Started backup');
|
|
@@ -110,7 +107,35 @@ export const handlePruneRepository = async (id) => {
|
|
|
110
107
|
throw error;
|
|
111
108
|
}
|
|
112
109
|
};
|
|
113
|
-
export const
|
|
110
|
+
export const handleDeleteRepository = async (repositoryId, local) => {
|
|
111
|
+
try {
|
|
112
|
+
const action = local ? sdk.createTicket : createTicket;
|
|
113
|
+
const { redirectTo } = await action({
|
|
114
|
+
action: 'repository.delete',
|
|
115
|
+
repositoryId,
|
|
116
|
+
});
|
|
117
|
+
window.open(redirectTo, '_blank');
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
handleError(error, 'Failed to start deletion');
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
export const handleDisableWormRepository = async (repositoryId, local) => {
|
|
125
|
+
try {
|
|
126
|
+
const action = local ? sdk.createTicket : createTicket;
|
|
127
|
+
const { redirectTo } = await action({
|
|
128
|
+
action: 'repository.disable-worm',
|
|
129
|
+
repositoryId,
|
|
130
|
+
});
|
|
131
|
+
window.open(redirectTo, '_blank');
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
handleError(error, 'Failed to start disable write-only process');
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
export const getRepositoryActions = (repository, local) => {
|
|
114
139
|
const online = Boolean(repository.configuration && repository.backends?.primary.online);
|
|
115
140
|
const configured = Boolean(repository.configuration);
|
|
116
141
|
const BackupNow = {
|
|
@@ -151,5 +176,19 @@ export const getRepositoryActions = (repository) => {
|
|
|
151
176
|
onAction: () => void modalManager.open(MetricsHistoryModal, { repository }),
|
|
152
177
|
$if: () => !repository.backends,
|
|
153
178
|
};
|
|
154
|
-
|
|
179
|
+
const Delete = {
|
|
180
|
+
title: 'Delete repository',
|
|
181
|
+
icon: mdiTrashCan,
|
|
182
|
+
onAction: () => void handleDeleteRepository(repository.id, local),
|
|
183
|
+
$if: () => !repository.backends || repository.backends.primary.type === 'yucca',
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
BackupNow,
|
|
187
|
+
Snapshots,
|
|
188
|
+
History,
|
|
189
|
+
Configure,
|
|
190
|
+
Import,
|
|
191
|
+
MetricsHistory,
|
|
192
|
+
Delete,
|
|
193
|
+
};
|
|
155
194
|
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type DeviceFlowEventDto } from '../fetch-client';
|
|
2
|
+
export type DeviceFlowKind = 'oidc' | 'session';
|
|
3
|
+
export declare const startDeviceFlow: (kind: DeviceFlowKind, onEvent: (event: DeviceFlowEventDto) => void) => () => void;
|
|
4
|
+
export declare const useCreateSession: () => import("@tanstack/svelte-query").CreateMutationResult<never, Error, string, unknown>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createSession, defaults, } from '../fetch-client';
|
|
2
|
+
import { queryClient } from '../query-client';
|
|
3
|
+
import { handleError } from '../utils/handle-error';
|
|
4
|
+
import { createMutation } from '@tanstack/svelte-query';
|
|
5
|
+
export const startDeviceFlow = (kind, onEvent) => {
|
|
6
|
+
const source = new EventSource(`${defaults.baseUrl.replace(/\/$/, '')}/api/yucca/auth/${kind}/device`, { withCredentials: true });
|
|
7
|
+
source.addEventListener('message', (event) => {
|
|
8
|
+
onEvent(JSON.parse(event.data));
|
|
9
|
+
});
|
|
10
|
+
source.addEventListener('error', () => {
|
|
11
|
+
onEvent({ type: 'FAILURE', reason: 'UNKNOWN' });
|
|
12
|
+
source.close();
|
|
13
|
+
});
|
|
14
|
+
return () => source.close();
|
|
15
|
+
};
|
|
16
|
+
export const useCreateSession = () => createMutation(() => ({
|
|
17
|
+
mutationFn: (token) => createSession({ token }),
|
|
18
|
+
onError: (error) => handleError(error, 'Failed to log in'),
|
|
19
|
+
}), () => 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.
|
|
5
|
+
"version": "0.40.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.
|
|
67
|
+
"@futo-org/backups-api-client": "^0.40.0"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"dev": "vite dev --port 5174",
|