@futo-org/backups-orchestrator-ui 0.1.71 → 0.3.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.
- package/dist/components/backends/BackendItem.svelte +7 -3
- package/dist/components/backends/BackendItem.svelte.d.ts +2 -1
- package/dist/components/backends/BackendsList.svelte +7 -6
- package/dist/components/backends/{CreateLocalBackend.svelte → dialogs/CreateLocalBackendModal.svelte} +2 -2
- package/dist/components/backends/dialogs/CreateLocalBackendModal.svelte.d.ts +7 -0
- package/dist/components/backends/{OAuthDeviceFlow.svelte → dialogs/OAuthDeviceFlowModal.svelte} +4 -4
- package/dist/components/backends/dialogs/OAuthDeviceFlowModal.svelte.d.ts +9 -0
- package/dist/components/backends/dialogs/SelectBackendModal.svelte +127 -0
- package/dist/components/backends/dialogs/SelectBackendModal.svelte.d.ts +11 -0
- package/dist/components/backups/BackupItem.svelte +13 -5
- package/dist/components/backups/dialogs/ImportRepositoryModal.svelte +16 -20
- package/dist/components/backups/dialogs/ReconfigureRepositoryBackendModal.svelte +40 -0
- package/dist/components/backups/dialogs/ReconfigureRepositoryBackendModal.svelte.d.ts +8 -0
- package/dist/components/backups/dialogs/RestoreSnapshotModal.svelte +1 -2
- package/dist/components/backups/dialogs/SelectRepositoryModal.svelte +101 -0
- package/dist/components/backups/dialogs/SelectRepositoryModal.svelte.d.ts +12 -0
- package/dist/components/backups/snapshots-list/RepositorySnapshotsListItem.svelte +2 -1
- package/dist/components/dashboard/DashboardTotalStored.svelte +12 -6
- package/dist/components/integrations/immich/ImmichConfigureBackup.svelte +2 -1
- package/dist/components/integrations/immich/ImmichManageBackupOverview.svelte +5 -1
- package/dist/components/onboarding/restore-point-flow/RestorePointFlow.svelte +13 -68
- package/dist/components/schedules/dialogs/ConfigureScheduleModal.svelte +5 -2
- package/dist/components/schedules/dialogs/CreateScheduleModal.svelte +5 -2
- package/dist/components/ui/StackListItem.svelte +4 -1
- package/dist/components/ui/StackListItem.svelte.d.ts +1 -0
- package/dist/fetch-client.d.ts +15 -1
- package/dist/fetch-client.js +11 -2
- package/dist/services/backend.service.d.ts +8 -2
- package/dist/services/backend.service.js +33 -5
- package/dist/services/integrations.service.js +0 -4
- package/dist/services/repository.service.d.ts +11 -4
- package/dist/services/repository.service.js +16 -24
- package/dist/services/schedule.service.js +0 -2
- package/package.json +6 -4
- package/dist/components/backends/CreateLocalBackend.svelte.d.ts +0 -7
- package/dist/components/backends/OAuthDeviceFlow.svelte.d.ts +0 -9
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type {
|
|
3
3
|
BackendDto,
|
|
4
4
|
BackendType,
|
|
5
|
+
LocalRepositoryDto,
|
|
5
6
|
RepositoryBackendDto,
|
|
6
7
|
} from "../../fetch-client";
|
|
7
8
|
import { getBackendActions } from "../../services/backend.service";
|
|
@@ -10,11 +11,12 @@
|
|
|
10
11
|
import StackListItem from "../ui/StackListItem.svelte";
|
|
11
12
|
|
|
12
13
|
type Props = {
|
|
14
|
+
repository?: LocalRepositoryDto;
|
|
13
15
|
backend: BackendDto;
|
|
14
16
|
repositoryBackend?: RepositoryBackendDto & { primary?: boolean };
|
|
15
17
|
};
|
|
16
18
|
|
|
17
|
-
const { backend, repositoryBackend }: Props = $props();
|
|
19
|
+
const { repository, backend, repositoryBackend }: Props = $props();
|
|
18
20
|
|
|
19
21
|
const BackendIcons: Record<BackendType, string> = {
|
|
20
22
|
yucca: mdiShieldCheckOutline,
|
|
@@ -28,10 +30,12 @@
|
|
|
28
30
|
s3: "S3 Server",
|
|
29
31
|
};
|
|
30
32
|
|
|
31
|
-
const { LoginAgain } = $derived(
|
|
33
|
+
const { LoginAgain, Reconfigure } = $derived(
|
|
34
|
+
getBackendActions(repository, backend, repositoryBackend),
|
|
35
|
+
);
|
|
32
36
|
</script>
|
|
33
37
|
|
|
34
|
-
<StackListItem actions={[LoginAgain]}>
|
|
38
|
+
<StackListItem actions={[LoginAgain, Reconfigure]}>
|
|
35
39
|
{#snippet icon()}
|
|
36
40
|
<Icon icon={BackendIcons[backend.type]} />
|
|
37
41
|
{/snippet}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { BackendDto, RepositoryBackendDto } from "../../fetch-client";
|
|
1
|
+
import type { BackendDto, LocalRepositoryDto, RepositoryBackendDto } from "../../fetch-client";
|
|
2
2
|
type Props = {
|
|
3
|
+
repository?: LocalRepositoryDto;
|
|
3
4
|
backend: BackendDto;
|
|
4
5
|
repositoryBackend?: RepositoryBackendDto & {
|
|
5
6
|
primary?: boolean;
|
|
@@ -49,12 +49,13 @@
|
|
|
49
49
|
{/snippet}
|
|
50
50
|
|
|
51
51
|
{#each query.data as backend (backend.id)}
|
|
52
|
-
|
|
53
|
-
{backend
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
{@const repositoryBackend = repositoryBackends.find(
|
|
53
|
+
({ id }) => backend.id === id,
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{#if typeof repository === typeof repositoryBackend}
|
|
57
|
+
<BackendItem {repository} {backend} {repositoryBackend} />
|
|
58
|
+
{/if}
|
|
58
59
|
{/each}
|
|
59
60
|
</StackList>
|
|
60
61
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import PathPickerField from "
|
|
3
|
-
import { useCreateLocalBackend } from "
|
|
2
|
+
import PathPickerField from "../../ui/PathPickerField.svelte";
|
|
3
|
+
import { useCreateLocalBackend } from "../../../services/backend.service";
|
|
4
4
|
import { FormModal, Stack } from "@immich/ui";
|
|
5
5
|
|
|
6
6
|
type Props = {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
onClose: () => void;
|
|
3
|
+
onCreate?: (backendId: string) => void;
|
|
4
|
+
};
|
|
5
|
+
declare const CreateLocalBackendModal: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type CreateLocalBackendModal = ReturnType<typeof CreateLocalBackendModal>;
|
|
7
|
+
export default CreateLocalBackendModal;
|
package/dist/components/backends/{OAuthDeviceFlow.svelte → dialogs/OAuthDeviceFlowModal.svelte}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { SocketEvent } from "
|
|
3
|
-
import type { BackendDto } from "
|
|
4
|
-
import { useBackendEventHandler } from "
|
|
2
|
+
import type { SocketEvent } from "../../../events";
|
|
3
|
+
import type { BackendDto } from "../../../fetch-client";
|
|
4
|
+
import { useBackendEventHandler } from "../../../services/backend.service";
|
|
5
5
|
import {
|
|
6
6
|
Button,
|
|
7
7
|
Code,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
VStack,
|
|
17
17
|
} from "@immich/ui";
|
|
18
18
|
import { mdiContentCopy } from "@mdi/js";
|
|
19
|
-
import OnEvents from "
|
|
19
|
+
import OnEvents from "../../util/OnEvents.svelte";
|
|
20
20
|
|
|
21
21
|
type Props = {
|
|
22
22
|
userCode: string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
userCode: string;
|
|
3
|
+
verificationUri: string;
|
|
4
|
+
onCreate?: (backendId: string) => void;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
};
|
|
7
|
+
declare const OAuthDeviceFlowModal: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type OAuthDeviceFlowModal = ReturnType<typeof OAuthDeviceFlowModal>;
|
|
9
|
+
export default OAuthDeviceFlowModal;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import StackList from "../../ui/StackList.svelte";
|
|
3
|
+
import StackListItem from "../../ui/StackListItem.svelte";
|
|
4
|
+
import Suspense from "../../util/Suspense.svelte";
|
|
5
|
+
import {
|
|
6
|
+
handleSetupLocalStorage,
|
|
7
|
+
handleYuccaLogin,
|
|
8
|
+
useBackends,
|
|
9
|
+
} from "../../../services/backend.service";
|
|
10
|
+
import {
|
|
11
|
+
Button,
|
|
12
|
+
HStack,
|
|
13
|
+
Icon,
|
|
14
|
+
Modal,
|
|
15
|
+
ModalBody,
|
|
16
|
+
ModalFooter,
|
|
17
|
+
Stack,
|
|
18
|
+
Text,
|
|
19
|
+
} from "@immich/ui";
|
|
20
|
+
import { mdiChevronRight, mdiHarddisk, mdiShieldCheck } from "@mdi/js";
|
|
21
|
+
import type { Snippet } from "svelte";
|
|
22
|
+
|
|
23
|
+
type Props = {
|
|
24
|
+
title?: string;
|
|
25
|
+
leadingContent?: Snippet;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
onSelect: (backendId: string) => void;
|
|
28
|
+
onCancel: () => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
title,
|
|
33
|
+
leadingContent,
|
|
34
|
+
disabled = false,
|
|
35
|
+
onSelect,
|
|
36
|
+
onCancel,
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
const backends = useBackends();
|
|
40
|
+
|
|
41
|
+
const onFutoBackups = () => {
|
|
42
|
+
const futoBackend = backends.data!.find(
|
|
43
|
+
(backend) => backend.type === "yucca" && backend.isOnline,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (futoBackend) {
|
|
47
|
+
onSelect(futoBackend.id);
|
|
48
|
+
} else {
|
|
49
|
+
handleYuccaLogin(onSelect);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const onLocalBackups = () => {
|
|
54
|
+
handleSetupLocalStorage(onSelect);
|
|
55
|
+
onCancel();
|
|
56
|
+
};
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<Modal size="small" {title} onClose={onCancel} icon={false}>
|
|
60
|
+
<ModalBody>
|
|
61
|
+
<Stack>
|
|
62
|
+
{@render leadingContent?.()}
|
|
63
|
+
|
|
64
|
+
<Suspense query={backends}>
|
|
65
|
+
<StackList>
|
|
66
|
+
<StackListItem {disabled} onclick={onFutoBackups}>
|
|
67
|
+
{#snippet icon()}
|
|
68
|
+
<Icon icon={mdiShieldCheck} size="36px" />
|
|
69
|
+
{/snippet}
|
|
70
|
+
|
|
71
|
+
<Stack gap={0}>
|
|
72
|
+
<Text class="font-bold">FUTO Backups</Text>
|
|
73
|
+
<Text>Simple, hosted backups.</Text>
|
|
74
|
+
</Stack>
|
|
75
|
+
|
|
76
|
+
{#snippet trailing()}
|
|
77
|
+
<Icon icon={mdiChevronRight} />
|
|
78
|
+
{/snippet}
|
|
79
|
+
</StackListItem>
|
|
80
|
+
<StackListItem {disabled} onclick={onLocalBackups}>
|
|
81
|
+
{#snippet icon()}
|
|
82
|
+
<Icon icon={mdiHarddisk} size="36px" />
|
|
83
|
+
{/snippet}
|
|
84
|
+
|
|
85
|
+
<Stack gap={0}>
|
|
86
|
+
<Text class="font-bold">Local Storage</Text>
|
|
87
|
+
<Text>A folder on this computer.</Text>
|
|
88
|
+
</Stack>
|
|
89
|
+
|
|
90
|
+
{#snippet trailing()}
|
|
91
|
+
<Icon icon={mdiChevronRight} />
|
|
92
|
+
{/snippet}
|
|
93
|
+
</StackListItem>
|
|
94
|
+
|
|
95
|
+
{#each backends.data as backend}
|
|
96
|
+
{#if backend.type === "local"}
|
|
97
|
+
<StackListItem
|
|
98
|
+
{disabled}
|
|
99
|
+
onclick={() => {
|
|
100
|
+
onSelect(backend.id);
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{#snippet icon()}
|
|
104
|
+
<Icon icon={mdiHarddisk} size="36px" />
|
|
105
|
+
{/snippet}
|
|
106
|
+
|
|
107
|
+
<Stack gap={0}>
|
|
108
|
+
<Text class="font-bold">Existing Local Storage</Text>
|
|
109
|
+
<Text>{backend.description}</Text>
|
|
110
|
+
</Stack>
|
|
111
|
+
|
|
112
|
+
{#snippet trailing()}
|
|
113
|
+
<Icon icon={mdiChevronRight} />
|
|
114
|
+
{/snippet}
|
|
115
|
+
</StackListItem>
|
|
116
|
+
{/if}
|
|
117
|
+
{/each}
|
|
118
|
+
</StackList>
|
|
119
|
+
</Suspense>
|
|
120
|
+
</Stack>
|
|
121
|
+
</ModalBody>
|
|
122
|
+
<ModalFooter>
|
|
123
|
+
<HStack>
|
|
124
|
+
<Button variant="ghost" {disabled} onclick={onCancel}>Cancel</Button>
|
|
125
|
+
</HStack>
|
|
126
|
+
</ModalFooter>
|
|
127
|
+
</Modal>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
type Props = {
|
|
3
|
+
title?: string;
|
|
4
|
+
leadingContent?: Snippet;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
onSelect: (backendId: string) => void;
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
};
|
|
9
|
+
declare const SelectBackendModal: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type SelectBackendModal = ReturnType<typeof SelectBackendModal>;
|
|
11
|
+
export default SelectBackendModal;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import type { LocalRepositoryDto } from "../../fetch-client";
|
|
3
3
|
import { getRepositoryActions } from "../../services/repository.service";
|
|
4
4
|
import { Badge, FormatBytes, Text } from "@immich/ui";
|
|
5
|
-
import RelativeTime from "../util/RelativeTime.svelte";
|
|
6
5
|
import StackListItem from "../ui/StackListItem.svelte";
|
|
6
|
+
import RelativeTime from "../util/RelativeTime.svelte";
|
|
7
7
|
|
|
8
8
|
type Props = {
|
|
9
9
|
repository: LocalRepositoryDto;
|
|
@@ -39,9 +39,15 @@
|
|
|
39
39
|
<Badge size="tiny" color="info">WORM</Badge>
|
|
40
40
|
{/if}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
|
|
42
|
+
{#if repository.meter}
|
|
43
|
+
<Badge size="tiny" color="secondary">
|
|
44
|
+
<FormatBytes bytes={repository.meter?.sizeBytes} />
|
|
45
|
+
</Badge>
|
|
46
|
+
{:else}
|
|
47
|
+
<Badge size="tiny" color="secondary">
|
|
48
|
+
Estimated <FormatBytes bytes={repository.metrics.sizeBytes} />
|
|
49
|
+
</Badge>
|
|
50
|
+
{/if}
|
|
45
51
|
|
|
46
52
|
{#snippet trailing()}
|
|
47
53
|
{#if repository.metrics.lastBackup && (!repository.metrics.lastSuccessfulBackup || +new Date(repository.metrics.lastBackup) > +new Date(repository.metrics.lastSuccessfulBackup))}
|
|
@@ -50,7 +56,9 @@
|
|
|
50
56
|
</Badge>
|
|
51
57
|
{:else if repository.metrics.lastSuccessfulBackup}
|
|
52
58
|
<Badge size="tiny" color="success">
|
|
53
|
-
Successful <RelativeTime
|
|
59
|
+
Successful <RelativeTime
|
|
60
|
+
time={repository.metrics.lastSuccessfulBackup}
|
|
61
|
+
/>
|
|
54
62
|
</Badge>
|
|
55
63
|
{:else}
|
|
56
64
|
<Badge size="tiny" color="warning">Never backed up</Badge>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import Suspense from "../../util/Suspense.svelte";
|
|
2
3
|
import { type LocalRepositoryDto } from "../../../fetch-client";
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
+
useCheckImportRepository,
|
|
5
6
|
useImportRepository,
|
|
6
7
|
} from "../../../services/repository.service";
|
|
7
|
-
import { FormModal,
|
|
8
|
-
import { onMount } from "svelte";
|
|
8
|
+
import { FormModal, modalManager, Text } from "@immich/ui";
|
|
9
9
|
import ConfigureRepositoryModal from "./ConfigureRepositoryModal.svelte";
|
|
10
10
|
|
|
11
11
|
type Props = {
|
|
@@ -14,16 +14,12 @@
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
let { onClose, repository }: Props = $props();
|
|
17
|
-
let readable: boolean | undefined = $state();
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
readable = check.readable;
|
|
26
|
-
});
|
|
18
|
+
// svelte-ignore state_referenced_locally
|
|
19
|
+
const check = useCheckImportRepository(
|
|
20
|
+
repository.id,
|
|
21
|
+
repository.backends!.primary.id,
|
|
22
|
+
);
|
|
27
23
|
|
|
28
24
|
const mutation = useImportRepository();
|
|
29
25
|
|
|
@@ -48,15 +44,15 @@
|
|
|
48
44
|
<FormModal
|
|
49
45
|
title={`Import ${repository.name}`}
|
|
50
46
|
submitText="Import"
|
|
51
|
-
disabled={readable !== true || mutation.isPending}
|
|
47
|
+
disabled={check.data?.readable !== true || mutation.isPending}
|
|
52
48
|
{onSubmit}
|
|
53
49
|
{onClose}
|
|
54
50
|
>
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
<Suspense query={check}>
|
|
52
|
+
{#if check.data?.readable}
|
|
53
|
+
<Text>Repository is readable and accessible!</Text>
|
|
54
|
+
{:else}
|
|
55
|
+
<Text>Can't read repository.</Text>
|
|
56
|
+
{/if}
|
|
57
|
+
</Suspense>
|
|
62
58
|
</FormModal>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import SelectBackendModal from "../../backends/dialogs/SelectBackendModal.svelte";
|
|
3
|
+
import type { LocalRepositoryDto } from "../../../fetch-client";
|
|
4
|
+
import { useReconfigureRepositoryPrimaryBackend } from "../../../services/repository.service";
|
|
5
|
+
import { Text } from "@immich/ui";
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
repository: LocalRepositoryDto;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const { repository, onClose }: Props = $props();
|
|
13
|
+
|
|
14
|
+
const reconfigure = useReconfigureRepositoryPrimaryBackend();
|
|
15
|
+
|
|
16
|
+
function onSelectBackend(backendId: string) {
|
|
17
|
+
reconfigure.mutate(
|
|
18
|
+
{ id: repository.id, backendId },
|
|
19
|
+
{ onSuccess: onClose },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<SelectBackendModal
|
|
25
|
+
title="Select new service"
|
|
26
|
+
disabled={reconfigure.isPending}
|
|
27
|
+
onSelect={onSelectBackend}
|
|
28
|
+
onCancel={onClose}
|
|
29
|
+
>
|
|
30
|
+
{#snippet leadingContent()}
|
|
31
|
+
{#if repository.backends?.secondary.length}
|
|
32
|
+
TODO: UI for promoting a secondary
|
|
33
|
+
{/if}
|
|
34
|
+
|
|
35
|
+
<Text
|
|
36
|
+
>You may reset and reconfigure your backups by selecting one of the
|
|
37
|
+
following.</Text
|
|
38
|
+
>
|
|
39
|
+
{/snippet}
|
|
40
|
+
</SelectBackendModal>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LocalRepositoryDto } from "../../../fetch-client";
|
|
2
|
+
type Props = {
|
|
3
|
+
repository: LocalRepositoryDto;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
};
|
|
6
|
+
declare const ReconfigureRepositoryBackendModal: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type ReconfigureRepositoryBackendModal = ReturnType<typeof ReconfigureRepositoryBackendModal>;
|
|
8
|
+
export default ReconfigureRepositoryBackendModal;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import StackList from "../../ui/StackList.svelte";
|
|
3
|
+
import StackListItem from "../../ui/StackListItem.svelte";
|
|
4
|
+
import { useInspectRepositories } from "../../../services/repository.service";
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
HStack,
|
|
8
|
+
Icon,
|
|
9
|
+
Modal,
|
|
10
|
+
ModalBody,
|
|
11
|
+
ModalFooter,
|
|
12
|
+
Stack,
|
|
13
|
+
Text,
|
|
14
|
+
} from "@immich/ui";
|
|
15
|
+
import { mdiChevronRight } from "@mdi/js";
|
|
16
|
+
import type { Snippet } from "svelte";
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
title?: string;
|
|
20
|
+
backendId?: string;
|
|
21
|
+
|
|
22
|
+
leadingContent?: Snippet;
|
|
23
|
+
footerContent?: Snippet;
|
|
24
|
+
|
|
25
|
+
onSelect: (repositoryId: string) => void;
|
|
26
|
+
onCancel: () => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
title = "Select Backup",
|
|
31
|
+
backendId,
|
|
32
|
+
leadingContent,
|
|
33
|
+
footerContent,
|
|
34
|
+
onSelect,
|
|
35
|
+
onCancel,
|
|
36
|
+
}: Props = $props();
|
|
37
|
+
|
|
38
|
+
// svelte-ignore state_referenced_locally
|
|
39
|
+
const query = useInspectRepositories(backendId);
|
|
40
|
+
|
|
41
|
+
const sortedRepositories = $derived(
|
|
42
|
+
query.data?.toSorted((a, b) => {
|
|
43
|
+
const validA = a.snapshots !== undefined;
|
|
44
|
+
const validB = b.snapshots !== undefined;
|
|
45
|
+
return validA !== validB
|
|
46
|
+
? Number(validB) - Number(validA)
|
|
47
|
+
: Number(b.name.includes("Immich")) - Number(a.name.includes("Immich"));
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<Modal {title} size="small" onClose={onCancel}>
|
|
53
|
+
<ModalBody>
|
|
54
|
+
<Stack>
|
|
55
|
+
{@render leadingContent?.()}
|
|
56
|
+
|
|
57
|
+
<StackList {query}>
|
|
58
|
+
{#each sortedRepositories ?? [] as repository (repository.id)}
|
|
59
|
+
{@const accessible = repository.snapshots !== undefined}
|
|
60
|
+
|
|
61
|
+
<StackListItem
|
|
62
|
+
onclick={accessible ? () => onSelect(repository.id) : undefined}
|
|
63
|
+
>
|
|
64
|
+
<Stack gap={0} class="grow min-w-0">
|
|
65
|
+
<Text>{repository.name}</Text>
|
|
66
|
+
<Text size="small" color={accessible ? "secondary" : "danger"}>
|
|
67
|
+
{#if !accessible}
|
|
68
|
+
Can't access, is your recovery key correct?
|
|
69
|
+
{:else if repository.snapshots.length}
|
|
70
|
+
Last backup: {new Date(
|
|
71
|
+
repository.snapshots[0].time,
|
|
72
|
+
).toLocaleDateString()}
|
|
73
|
+
{:else}
|
|
74
|
+
No backups yet
|
|
75
|
+
{/if}
|
|
76
|
+
</Text>
|
|
77
|
+
</Stack>
|
|
78
|
+
|
|
79
|
+
{#snippet trailing()}
|
|
80
|
+
{#if accessible}
|
|
81
|
+
<Icon icon={mdiChevronRight} />
|
|
82
|
+
{/if}
|
|
83
|
+
{/snippet}
|
|
84
|
+
</StackListItem>
|
|
85
|
+
{/each}
|
|
86
|
+
|
|
87
|
+
{#if (sortedRepositories ?? []).length === 0}
|
|
88
|
+
<StackListItem>
|
|
89
|
+
<Text color="muted">No backups found.</Text>
|
|
90
|
+
</StackListItem>
|
|
91
|
+
{/if}
|
|
92
|
+
</StackList>
|
|
93
|
+
</Stack>
|
|
94
|
+
</ModalBody>
|
|
95
|
+
<ModalFooter>
|
|
96
|
+
<HStack>
|
|
97
|
+
<Button variant="ghost" onclick={onCancel}>Cancel</Button>
|
|
98
|
+
{@render footerContent?.()}
|
|
99
|
+
</HStack>
|
|
100
|
+
</ModalFooter>
|
|
101
|
+
</Modal>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
type Props = {
|
|
3
|
+
title?: string;
|
|
4
|
+
backendId?: string;
|
|
5
|
+
leadingContent?: Snippet;
|
|
6
|
+
footerContent?: Snippet;
|
|
7
|
+
onSelect: (repositoryId: string) => void;
|
|
8
|
+
onCancel: () => void;
|
|
9
|
+
};
|
|
10
|
+
declare const SelectRepositoryModal: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type SelectRepositoryModal = ReturnType<typeof SelectRepositoryModal>;
|
|
12
|
+
export default SelectRepositoryModal;
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
{#if snapshot.summary}
|
|
24
24
|
<Text color="secondary">
|
|
25
|
-
· {snapshot.summary.totalFiles.toLocaleString()}
|
|
25
|
+
· {snapshot.summary.totalFiles.toLocaleString()}
|
|
26
|
+
{snapshot.summary.totalFiles > 1 ? "files" : "file"} ·
|
|
26
27
|
<FormatBytes bytes={snapshot.summary.totalBytes} />
|
|
27
28
|
</Text>
|
|
28
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { LocalRepositoryDto } from "../../fetch-client";
|
|
3
|
-
import { Card, CardBody, getByteUnitString } from "@immich/ui";
|
|
3
|
+
import { Badge, Card, CardBody, getByteUnitString } from "@immich/ui";
|
|
4
4
|
import VisualisationGauge from "../ui/VisualisationGauge.svelte";
|
|
5
5
|
|
|
6
6
|
type Props = {
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
const { repositories }: Props = $props();
|
|
11
11
|
|
|
12
12
|
const totalStored = $derived(
|
|
13
|
-
repositories.reduce(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
repositories.reduce((sum, repo) => sum + (repo.meter?.sizeBytes ?? 0), 0),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const estimatedStored = $derived(
|
|
17
|
+
repositories.reduce((sum, repo) => sum + (repo.metrics?.sizeBytes ?? 0), 0),
|
|
17
18
|
);
|
|
18
19
|
</script>
|
|
19
20
|
|
|
@@ -22,6 +23,11 @@
|
|
|
22
23
|
<VisualisationGauge
|
|
23
24
|
title="Total Stored"
|
|
24
25
|
content={getByteUnitString(totalStored)}
|
|
25
|
-
|
|
26
|
+
>
|
|
27
|
+
{#snippet subtitle()}
|
|
28
|
+
<Badge size="tiny">Estimated {getByteUnitString(estimatedStored)}</Badge
|
|
29
|
+
>
|
|
30
|
+
{/snippet}
|
|
31
|
+
</VisualisationGauge>
|
|
26
32
|
</CardBody>
|
|
27
33
|
</Card>
|
|
@@ -203,7 +203,8 @@
|
|
|
203
203
|
<FormModal
|
|
204
204
|
title="Backup settings"
|
|
205
205
|
size="large"
|
|
206
|
-
disabled={
|
|
206
|
+
disabled={(scheduleMode === "custom" &&
|
|
207
|
+
!validateCron(effectiveCron).isError()) ||
|
|
207
208
|
name.length === 0 ||
|
|
208
209
|
mutation.isPending}
|
|
209
210
|
onClose={() => {
|
|
@@ -52,7 +52,11 @@
|
|
|
52
52
|
<Stack gap={0}>
|
|
53
53
|
<Heading size="tiny">Your library</Heading>
|
|
54
54
|
<Text>
|
|
55
|
-
|
|
55
|
+
{#if repository.meter}
|
|
56
|
+
Estimated <FormatBytes bytes={repository.meter.sizeBytes} />
|
|
57
|
+
{:else}
|
|
58
|
+
<FormatBytes bytes={repository.metrics.sizeBytes} />
|
|
59
|
+
{/if} ·
|
|
56
60
|
<span class="lowercase"
|
|
57
61
|
>{cronstrue.toString(schedule.cron, {
|
|
58
62
|
verbose: true,
|
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import SelectRepositoryModal from "../../backups/dialogs/SelectRepositoryModal.svelte";
|
|
3
3
|
import { useInspectRepositories } from "../../../services/repository.service";
|
|
4
|
-
import {
|
|
5
|
-
Button,
|
|
6
|
-
HStack,
|
|
7
|
-
Modal,
|
|
8
|
-
ModalBody,
|
|
9
|
-
ModalFooter,
|
|
10
|
-
Stack,
|
|
11
|
-
Text,
|
|
12
|
-
} from "@immich/ui";
|
|
4
|
+
import { Button } from "@immich/ui";
|
|
13
5
|
import RestorePointFlow2SelectSnapshot from "./RestorePointFlow2SelectSnapshot.svelte";
|
|
14
6
|
|
|
15
7
|
type Props = {
|
|
@@ -22,22 +14,10 @@
|
|
|
22
14
|
|
|
23
15
|
const query = useInspectRepositories();
|
|
24
16
|
|
|
25
|
-
const sortedRepositories = $derived(
|
|
26
|
-
query.data?.toSorted((a, b) => {
|
|
27
|
-
const validA = a.snapshots !== undefined;
|
|
28
|
-
const validB = b.snapshots !== undefined;
|
|
29
|
-
return validA !== validB
|
|
30
|
-
? Number(validB) - Number(validA)
|
|
31
|
-
: Number(b.name.includes("Immich")) - Number(a.name.includes("Immich"));
|
|
32
|
-
}),
|
|
33
|
-
);
|
|
34
|
-
|
|
35
17
|
let selectedRepository: string | undefined = $state();
|
|
36
18
|
|
|
37
19
|
const repository = $derived(
|
|
38
|
-
query.data?.find((repository) =>
|
|
39
|
-
return repository.id === selectedRepository;
|
|
40
|
-
}),
|
|
20
|
+
query.data?.find((repository) => repository.id === selectedRepository),
|
|
41
21
|
);
|
|
42
22
|
</script>
|
|
43
23
|
|
|
@@ -48,49 +28,14 @@
|
|
|
48
28
|
{onFinish}
|
|
49
29
|
/>
|
|
50
30
|
{:else}
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{#if !accessible}
|
|
62
|
-
Unable to access repository
|
|
63
|
-
{:else if repo.snapshots.length}
|
|
64
|
-
Last backup: {new Date(
|
|
65
|
-
repo.snapshots[0].time,
|
|
66
|
-
).toLocaleDateString()}
|
|
67
|
-
{:else}
|
|
68
|
-
No backups yet
|
|
69
|
-
{/if}
|
|
70
|
-
</Text>
|
|
71
|
-
</Stack>
|
|
72
|
-
{#if accessible}
|
|
73
|
-
<Button onclick={() => (selectedRepository = repo.id)}>
|
|
74
|
-
Select
|
|
75
|
-
</Button>
|
|
76
|
-
{/if}
|
|
77
|
-
</HStack>
|
|
78
|
-
{/each}
|
|
79
|
-
{#if (sortedRepositories ?? []).length === 0}
|
|
80
|
-
<Text class="text-center py-6" color="muted">
|
|
81
|
-
No repositories found.
|
|
82
|
-
</Text>
|
|
83
|
-
{/if}
|
|
84
|
-
{/snippet}
|
|
85
|
-
</StackList>
|
|
86
|
-
</ModalBody>
|
|
87
|
-
<ModalFooter>
|
|
88
|
-
<HStack>
|
|
89
|
-
<Button variant="ghost" onclick={onCancel}>Cancel</Button>
|
|
90
|
-
<Button variant="ghost" onclick={onImportKey}
|
|
91
|
-
>Import a different key</Button
|
|
92
|
-
>
|
|
93
|
-
</HStack>
|
|
94
|
-
</ModalFooter>
|
|
95
|
-
</Modal>
|
|
31
|
+
<SelectRepositoryModal
|
|
32
|
+
onSelect={(repositoryId) => (selectedRepository = repositoryId)}
|
|
33
|
+
{onCancel}
|
|
34
|
+
>
|
|
35
|
+
{#snippet footerContent()}
|
|
36
|
+
<Button variant="ghost" onclick={onImportKey}>
|
|
37
|
+
Import a different key
|
|
38
|
+
</Button>
|
|
39
|
+
{/snippet}
|
|
40
|
+
</SelectRepositoryModal>
|
|
96
41
|
{/if}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { ScheduleDto } from "../../../fetch-client";
|
|
3
|
+
import { useUpdateSchedule } from "../../../services/schedule.service";
|
|
3
4
|
import { Field, FormModal, Input, Stack } from "@immich/ui";
|
|
4
5
|
import validate from "cron-validate";
|
|
5
|
-
import { useUpdateSchedule } from "../../../services/schedule.service";
|
|
6
6
|
import RepositoryPicker from "../RepositoryPicker.svelte";
|
|
7
7
|
|
|
8
8
|
type Props = {
|
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
<FormModal
|
|
32
32
|
title={`Edit ${schedule.name}`}
|
|
33
33
|
size="large"
|
|
34
|
-
disabled={name.length === 0 ||
|
|
34
|
+
disabled={name.length === 0 ||
|
|
35
|
+
validate(cron).isError() ||
|
|
36
|
+
repositories.length === 0 ||
|
|
37
|
+
mutation.isPending}
|
|
35
38
|
{onSubmit}
|
|
36
39
|
{onClose}
|
|
37
40
|
>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { useCreateSchedule } from "../../../services/schedule.service";
|
|
2
3
|
import { Field, FormModal, Input, Stack } from "@immich/ui";
|
|
3
4
|
import validate from "cron-validate";
|
|
4
|
-
import { useCreateSchedule } from "../../../services/schedule.service";
|
|
5
5
|
import RepositoryPicker from "../RepositoryPicker.svelte";
|
|
6
6
|
|
|
7
7
|
type Props = {
|
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
<FormModal
|
|
27
27
|
title="Create A New Schedule"
|
|
28
28
|
size="large"
|
|
29
|
-
disabled={name.length === 0 ||
|
|
29
|
+
disabled={name.length === 0 ||
|
|
30
|
+
validate(cron).isError() ||
|
|
31
|
+
repositories.length === 0 ||
|
|
32
|
+
mutation.isPending}
|
|
30
33
|
{onSubmit}
|
|
31
34
|
{onClose}
|
|
32
35
|
>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
children: Snippet;
|
|
11
11
|
trailing?: Snippet;
|
|
12
12
|
actions?: ActionItem[];
|
|
13
|
+
disabled?: boolean;
|
|
13
14
|
onclick?: () => void;
|
|
14
15
|
};
|
|
15
16
|
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
children,
|
|
20
21
|
trailing,
|
|
21
22
|
actions = [],
|
|
23
|
+
disabled = false,
|
|
22
24
|
onclick,
|
|
23
25
|
}: Props = $props();
|
|
24
26
|
</script>
|
|
@@ -54,8 +56,9 @@
|
|
|
54
56
|
{#if onclick}
|
|
55
57
|
<button
|
|
56
58
|
{onclick}
|
|
59
|
+
{disabled}
|
|
57
60
|
type="button"
|
|
58
|
-
class="block w-full text-left rounded-lg hover:bg-subtle focus-visible:outline-2 focus-visible:outline-primary-500 cursor-pointer"
|
|
61
|
+
class="block w-full text-left rounded-lg hover:bg-subtle focus-visible:outline-2 focus-visible:outline-primary-500 cursor-pointer disabled:cursor-not-allowed disabled:opacity-60"
|
|
59
62
|
>
|
|
60
63
|
{@render body()}
|
|
61
64
|
</button>
|
package/dist/fetch-client.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type BackendType = "yucca" | "local" | "s3";
|
|
|
17
17
|
export type BackendDto = {
|
|
18
18
|
id: string;
|
|
19
19
|
"type": BackendType;
|
|
20
|
+
description: string;
|
|
20
21
|
isOnline: boolean;
|
|
21
22
|
error?: string;
|
|
22
23
|
};
|
|
@@ -105,6 +106,11 @@ export type RepositoryMetricsDto = {
|
|
|
105
106
|
lastBackupDuration?: number;
|
|
106
107
|
sizeBytes: number;
|
|
107
108
|
};
|
|
109
|
+
export type RepositoryMeterDto = {
|
|
110
|
+
sizeBytes: number;
|
|
111
|
+
objectCount: number;
|
|
112
|
+
lastUpdated?: string;
|
|
113
|
+
};
|
|
108
114
|
export type RepositoryBackendDto = {
|
|
109
115
|
id: string;
|
|
110
116
|
"type": BackendType;
|
|
@@ -123,6 +129,7 @@ export type LocalRepositoryDto = {
|
|
|
123
129
|
worm: boolean;
|
|
124
130
|
name: string;
|
|
125
131
|
metrics: RepositoryMetricsDto;
|
|
132
|
+
meter?: RepositoryMeterDto;
|
|
126
133
|
backends?: RepositoryBackendsDto;
|
|
127
134
|
configuration?: RepositoryConfigurationDto;
|
|
128
135
|
};
|
|
@@ -151,6 +158,7 @@ export type InspectedLocalRepositoryDto = {
|
|
|
151
158
|
worm: boolean;
|
|
152
159
|
name: string;
|
|
153
160
|
metrics: RepositoryMetricsDto;
|
|
161
|
+
meter?: RepositoryMeterDto;
|
|
154
162
|
backends?: RepositoryBackendsDto;
|
|
155
163
|
configuration?: RepositoryConfigurationDto;
|
|
156
164
|
snapshots: SnapshotDto[];
|
|
@@ -172,6 +180,9 @@ export type LogResponseDto = {
|
|
|
172
180
|
export type RepositoryCheckImportResponseDto = {
|
|
173
181
|
readable: boolean;
|
|
174
182
|
};
|
|
183
|
+
export type RepositoryPrimaryBackendReconfigureRequestDto = {
|
|
184
|
+
backendId: string;
|
|
185
|
+
};
|
|
175
186
|
export type RunStatus = "incomplete" | "complete" | "failed";
|
|
176
187
|
export type RunType = "schedule" | "restore" | "backup" | "forget";
|
|
177
188
|
export type RunDto = {
|
|
@@ -262,7 +273,9 @@ export declare function createRepository(repositoryCreateRequestDto: RepositoryC
|
|
|
262
273
|
backend?: string;
|
|
263
274
|
}, opts?: Oazapfts.RequestOpts): Promise<RepositoryCreateResponseDto>;
|
|
264
275
|
export declare function getRepositories(opts?: Oazapfts.RequestOpts): Promise<RepositoryListResponseDto>;
|
|
265
|
-
export declare function inspectRepositories(
|
|
276
|
+
export declare function inspectRepositories({ backend }?: {
|
|
277
|
+
backend?: string;
|
|
278
|
+
}, opts?: Oazapfts.RequestOpts): Promise<RepositoryInspectResponseDto>;
|
|
266
279
|
export declare function updateRepository(id: string, repositoryUpdateRequestDto: RepositoryUpdateRequestDto, { backend }?: {
|
|
267
280
|
backend?: string;
|
|
268
281
|
}, opts?: Oazapfts.RequestOpts): Promise<RepositoryUpdateResponseDto>;
|
|
@@ -270,6 +283,7 @@ export declare function deleteRepository(id: string, opts?: Oazapfts.RequestOpts
|
|
|
270
283
|
export declare function createBackup(id: string, opts?: Oazapfts.RequestOpts): Promise<LogResponseDto>;
|
|
271
284
|
export declare function checkImportRepository(id: string, backend: string, opts?: Oazapfts.RequestOpts): Promise<RepositoryCheckImportResponseDto>;
|
|
272
285
|
export declare function importRepository(id: string, backend: string, opts?: Oazapfts.RequestOpts): Promise<RepositoryCreateResponseDto>;
|
|
286
|
+
export declare function reconfigureRepositoryPrimaryBackend(id: string, repositoryPrimaryBackendReconfigureRequestDto: RepositoryPrimaryBackendReconfigureRequestDto, opts?: Oazapfts.RequestOpts): Promise<RepositoryCreateResponseDto>;
|
|
273
287
|
export declare function getRunHistory(id: string, opts?: Oazapfts.RequestOpts): Promise<RunHistoryResponseDto>;
|
|
274
288
|
export declare function getSnapshots(id: string, opts?: Oazapfts.RequestOpts): Promise<ListSnapshotsResponseDto>;
|
|
275
289
|
export declare function pruneRepository(id: string, opts?: Oazapfts.RequestOpts): Promise<LogResponseDto>;
|
package/dist/fetch-client.js
CHANGED
|
@@ -99,8 +99,10 @@ export function getRepositories(opts) {
|
|
|
99
99
|
...opts
|
|
100
100
|
}));
|
|
101
101
|
}
|
|
102
|
-
export function inspectRepositories(opts) {
|
|
103
|
-
return oazapfts.ok(oazapfts.fetchJson(
|
|
102
|
+
export function inspectRepositories({ backend } = {}, opts) {
|
|
103
|
+
return oazapfts.ok(oazapfts.fetchJson(`/api/yucca/repository/inspect${QS.query(QS.explode({
|
|
104
|
+
backend
|
|
105
|
+
}))}`, {
|
|
104
106
|
...opts
|
|
105
107
|
}));
|
|
106
108
|
}
|
|
@@ -140,6 +142,13 @@ export function importRepository(id, backend, opts) {
|
|
|
140
142
|
method: "POST"
|
|
141
143
|
}));
|
|
142
144
|
}
|
|
145
|
+
export function reconfigureRepositoryPrimaryBackend(id, repositoryPrimaryBackendReconfigureRequestDto, opts) {
|
|
146
|
+
return oazapfts.ok(oazapfts.fetchJson(`/api/yucca/repository/${encodeURIComponent(id)}/backend`, oazapfts.json({
|
|
147
|
+
...opts,
|
|
148
|
+
method: "PUT",
|
|
149
|
+
body: repositoryPrimaryBackendReconfigureRequestDto
|
|
150
|
+
})));
|
|
151
|
+
}
|
|
143
152
|
export function getRunHistory(id, opts) {
|
|
144
153
|
return oazapfts.ok(oazapfts.fetchJson(`/api/yucca/repository/${encodeURIComponent(id)}/runs`, {
|
|
145
154
|
...opts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SocketEvent } from '../events';
|
|
2
|
-
import { type BackendDto, type CreateLocalBackendRequestDto } from '../fetch-client';
|
|
2
|
+
import { type BackendDto, type CreateLocalBackendRequestDto, type LocalRepositoryDto, type RepositoryBackendDto } from '../fetch-client';
|
|
3
3
|
import { type ActionItem } from '@immich/ui';
|
|
4
4
|
export declare const backendKeys: {
|
|
5
5
|
all: readonly ["backends"];
|
|
@@ -13,6 +13,12 @@ export declare const useBackendEventHandler: () => {
|
|
|
13
13
|
export declare const handleYuccaLogin: (onCreate?: (backendId: string) => void) => Promise<void>;
|
|
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
|
-
export declare const
|
|
16
|
+
export declare const handleReconfigureRepositoryBackend: (repository: LocalRepositoryDto) => Promise<void>;
|
|
17
|
+
export declare const handleRemoveRepositoryBackend: (_backend: BackendDto, _repositoryBackend: RepositoryBackendDto) => void;
|
|
18
|
+
export declare const getBackendActions: (repository: LocalRepositoryDto | undefined, backend: BackendDto, repositoryBackend?: RepositoryBackendDto & {
|
|
19
|
+
primary?: boolean;
|
|
20
|
+
}) => {
|
|
17
21
|
LoginAgain: ActionItem;
|
|
22
|
+
Reconfigure: ActionItem;
|
|
23
|
+
Remove: ActionItem;
|
|
18
24
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import CreateLocalBackend from '../components/backends/
|
|
2
|
-
import OAuthDeviceFlow from '../components/backends/
|
|
1
|
+
import CreateLocalBackend from '../components/backends/dialogs/CreateLocalBackendModal.svelte';
|
|
2
|
+
import OAuthDeviceFlow from '../components/backends/dialogs/OAuthDeviceFlowModal.svelte';
|
|
3
|
+
import ReconfigureRepositoryBackendModal from '../components/backups/dialogs/ReconfigureRepositoryBackendModal.svelte';
|
|
3
4
|
import { SocketEvent } from '../events';
|
|
4
5
|
import { createLocalBackend, getBackends, oidcDeviceFlow, } from '../fetch-client';
|
|
5
6
|
import { queryClient } from '../query-client';
|
|
@@ -47,15 +48,42 @@ export const handleSetupLocalStorage = (onCreate) => {
|
|
|
47
48
|
};
|
|
48
49
|
export const useCreateLocalBackend = () => createMutation(() => ({
|
|
49
50
|
mutationFn: (dto) => createLocalBackend(dto),
|
|
50
|
-
onSuccess: () => void queryClient.invalidateQueries({ queryKey: backendKeys.all }),
|
|
51
51
|
onError: (error) => handleError(error, 'Failed to create local backend'),
|
|
52
52
|
}), () => queryClient);
|
|
53
|
-
export const
|
|
53
|
+
export const handleReconfigureRepositoryBackend = async (repository) => {
|
|
54
|
+
await modalManager.show(ReconfigureRepositoryBackendModal, {
|
|
55
|
+
repository,
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
export const handleRemoveRepositoryBackend = (_backend, _repositoryBackend) => {
|
|
59
|
+
alert('TODO: implement me when multi-repository-backend support is added');
|
|
60
|
+
};
|
|
61
|
+
export const getBackendActions = (repository, backend, repositoryBackend) => {
|
|
54
62
|
const LoginAgain = {
|
|
55
63
|
icon: mdiLogin,
|
|
56
64
|
title: 'Login again',
|
|
57
65
|
onAction: () => void handleYuccaLogin(),
|
|
58
66
|
$if: () => backend.type === 'yucca' && !backend.isOnline,
|
|
59
67
|
};
|
|
60
|
-
|
|
68
|
+
const Reconfigure = {
|
|
69
|
+
icon: mdiLogin,
|
|
70
|
+
title: 'Reconfigure',
|
|
71
|
+
onAction: () => void handleReconfigureRepositoryBackend(repository),
|
|
72
|
+
$if: () => repositoryBackend
|
|
73
|
+
? backend.isOnline &&
|
|
74
|
+
!repositoryBackend.online &&
|
|
75
|
+
repositoryBackend.primary === true
|
|
76
|
+
: false,
|
|
77
|
+
};
|
|
78
|
+
const Remove = {
|
|
79
|
+
icon: mdiLogin,
|
|
80
|
+
title: 'Remove',
|
|
81
|
+
onAction: () => void handleRemoveRepositoryBackend(backend, repositoryBackend),
|
|
82
|
+
$if: () => repositoryBackend
|
|
83
|
+
? backend.isOnline &&
|
|
84
|
+
!repositoryBackend.online &&
|
|
85
|
+
repositoryBackend.primary === false
|
|
86
|
+
: false,
|
|
87
|
+
};
|
|
88
|
+
return { LoginAgain, Reconfigure, Remove };
|
|
61
89
|
};
|
|
@@ -17,7 +17,6 @@ export const useIntegrationEventHandler = () => ({
|
|
|
17
17
|
});
|
|
18
18
|
export const useConfigureImmichIntegration = () => createMutation(() => ({
|
|
19
19
|
mutationFn: (dto) => configureImmichIntegration(dto),
|
|
20
|
-
onSuccess: () => void queryClient.invalidateQueries({ queryKey: integrationsKeys.all }),
|
|
21
20
|
onError: (error) => handleError(error, 'Failed to save backup settings'),
|
|
22
21
|
}), () => queryClient);
|
|
23
22
|
export const useConfigureImmichDefaults = () => createMutation(() => ({
|
|
@@ -35,8 +34,5 @@ export const useConfigureImmichDefaults = () => createMutation(() => ({
|
|
|
35
34
|
libraries: 'all',
|
|
36
35
|
});
|
|
37
36
|
},
|
|
38
|
-
onSuccess: () => {
|
|
39
|
-
void queryClient.invalidateQueries({ queryKey: integrationsKeys.all });
|
|
40
|
-
},
|
|
41
37
|
onError: (error) => handleError(error, 'Failed to create backup'),
|
|
42
38
|
}), () => queryClient);
|
|
@@ -4,10 +4,11 @@ import { type InspectedLocalRepositoryDto, type LocalRepositoryDto, type Reposit
|
|
|
4
4
|
import { type ActionItem } from '@immich/ui';
|
|
5
5
|
export declare const repositoryKeys: {
|
|
6
6
|
all: readonly ["repositories"];
|
|
7
|
-
|
|
7
|
+
inspect: (backendId?: string) => readonly ["repositories", "inspect", string | undefined];
|
|
8
|
+
checkImport: (id: string, backendId: string) => readonly ["repositories", string, "check-import", string];
|
|
8
9
|
};
|
|
9
10
|
export declare const useRepositories: (initialData?: LocalRepositoryDto[]) => import("@tanstack/svelte-query").CreateQueryResult<sdk.LocalRepositoryDto[], Error>;
|
|
10
|
-
export declare const useInspectRepositories: (initialData?: InspectedLocalRepositoryDto[]) => import("@tanstack/svelte-query").CreateQueryResult<sdk.InspectedLocalRepositoryDto[], Error>;
|
|
11
|
+
export declare const useInspectRepositories: (backendId?: string, initialData?: InspectedLocalRepositoryDto[]) => import("@tanstack/svelte-query").CreateQueryResult<sdk.InspectedLocalRepositoryDto[], Error>;
|
|
11
12
|
export declare const useRepositoryEventHandler: () => {
|
|
12
13
|
onRepositoryCreate(event: SocketEvent<{
|
|
13
14
|
repository: LocalRepositoryDto;
|
|
@@ -16,9 +17,11 @@ export declare const useRepositoryEventHandler: () => {
|
|
|
16
17
|
repositoryId: string;
|
|
17
18
|
repository: Partial<LocalRepositoryDto>;
|
|
18
19
|
}>): void;
|
|
19
|
-
onRepositoryDelete(
|
|
20
|
+
onRepositoryDelete(event: SocketEvent<{
|
|
21
|
+
repositoryId: string;
|
|
22
|
+
}>): void;
|
|
20
23
|
};
|
|
21
|
-
export declare const
|
|
24
|
+
export declare const useCheckImportRepository: (id: string, backendId: string) => import("@tanstack/svelte-query").CreateQueryResult<sdk.RepositoryCheckImportResponseDto, Error>;
|
|
22
25
|
export declare const useCreateRepository: () => import("@tanstack/svelte-query").CreateMutationResult<sdk.RepositoryCreateResponseDto, Error, sdk.RepositoryCreateRequestDto, unknown>;
|
|
23
26
|
export declare const useImportRepository: () => import("@tanstack/svelte-query").CreateMutationResult<sdk.RepositoryCreateResponseDto, Error, {
|
|
24
27
|
id: string;
|
|
@@ -29,6 +32,10 @@ export declare const useUpdateRepository: () => import("@tanstack/svelte-query")
|
|
|
29
32
|
dto: RepositoryUpdateRequestDto;
|
|
30
33
|
local?: boolean;
|
|
31
34
|
}, unknown>;
|
|
35
|
+
export declare const useReconfigureRepositoryPrimaryBackend: () => import("@tanstack/svelte-query").CreateMutationResult<sdk.RepositoryCreateResponseDto, Error, {
|
|
36
|
+
id: string;
|
|
37
|
+
backendId: string;
|
|
38
|
+
}, unknown>;
|
|
32
39
|
export declare const useRemoveRepository: () => import("@tanstack/svelte-query").CreateMutationResult<never, Error, {
|
|
33
40
|
id: string;
|
|
34
41
|
local?: boolean;
|
|
@@ -6,7 +6,7 @@ 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 { deleteRepository, inspectRepositories, updateRepository, } from '../fetch-client';
|
|
9
|
+
import { deleteRepository, 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';
|
|
@@ -15,7 +15,8 @@ import { mdiCog, mdiFormatListBulletedType, mdiHistory, mdiImport, mdiListStatus
|
|
|
15
15
|
import { createMutation, createQuery } from '@tanstack/svelte-query';
|
|
16
16
|
export const repositoryKeys = {
|
|
17
17
|
all: ['repositories'],
|
|
18
|
-
|
|
18
|
+
inspect: (backendId) => ['repositories', 'inspect', backendId],
|
|
19
|
+
checkImport: (id, backendId) => ['repositories', id, 'check-import', backendId],
|
|
19
20
|
};
|
|
20
21
|
export const useRepositories = (initialData) => createQuery(() => ({
|
|
21
22
|
queryKey: repositoryKeys.all,
|
|
@@ -24,9 +25,9 @@ export const useRepositories = (initialData) => createQuery(() => ({
|
|
|
24
25
|
.then(({ repositories }) => repositories),
|
|
25
26
|
initialData,
|
|
26
27
|
}), () => queryClient);
|
|
27
|
-
export const useInspectRepositories = (initialData) => createQuery(() => ({
|
|
28
|
-
queryKey: repositoryKeys.
|
|
29
|
-
queryFn: () => inspectRepositories().then(({ repositories }) => repositories),
|
|
28
|
+
export const useInspectRepositories = (backendId, initialData) => createQuery(() => ({
|
|
29
|
+
queryKey: repositoryKeys.inspect(backendId),
|
|
30
|
+
queryFn: () => inspectRepositories({ backend: backendId }).then(({ repositories }) => repositories),
|
|
30
31
|
initialData,
|
|
31
32
|
}), () => queryClient);
|
|
32
33
|
export const useRepositoryEventHandler = () => {
|
|
@@ -50,42 +51,33 @@ export const useRepositoryEventHandler = () => {
|
|
|
50
51
|
: void 0;
|
|
51
52
|
});
|
|
52
53
|
},
|
|
53
|
-
onRepositoryDelete() {
|
|
54
|
-
queryClient
|
|
55
|
-
.invalidateQueries({
|
|
56
|
-
queryKey: repositoryKeys.all,
|
|
57
|
-
})
|
|
58
|
-
.catch(() => void 0);
|
|
54
|
+
onRepositoryDelete(event) {
|
|
55
|
+
queryClient.setQueryData(repositoryKeys.all, (data) => data?.filter((entry) => entry.id !== event.data.repositoryId));
|
|
59
56
|
},
|
|
60
57
|
};
|
|
61
58
|
};
|
|
62
|
-
export const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
catch (error) {
|
|
67
|
-
handleError(error, 'Failed to check repository');
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
59
|
+
export const useCheckImportRepository = (id, backendId) => createQuery(() => ({
|
|
60
|
+
queryKey: repositoryKeys.checkImport(id, backendId),
|
|
61
|
+
queryFn: () => sdk.checkImportRepository(id, backendId),
|
|
62
|
+
}), () => queryClient);
|
|
71
63
|
export const useCreateRepository = () => createMutation(() => ({
|
|
72
64
|
mutationFn: (dto) => sdk.createRepository(dto),
|
|
73
|
-
onSuccess: () => void queryClient.invalidateQueries({ queryKey: repositoryKeys.all }),
|
|
74
65
|
onError: (error) => handleError(error, 'Failed to create repository'),
|
|
75
66
|
}), () => queryClient);
|
|
76
67
|
export const useImportRepository = () => createMutation(() => ({
|
|
77
68
|
mutationFn: ({ id, backendId }) => sdk.importRepository(id, backendId),
|
|
78
|
-
onSuccess: () => void queryClient.invalidateQueries({ queryKey: repositoryKeys.all }),
|
|
79
69
|
onError: (error) => handleError(error, 'Failed to import repository'),
|
|
80
70
|
}), () => queryClient);
|
|
81
71
|
export const useUpdateRepository = () => createMutation(() => ({
|
|
82
72
|
mutationFn: ({ id, dto, local = false, }) => (local ? sdk.updateRepository(id, dto) : updateRepository(id, dto)),
|
|
83
|
-
onSuccess: () => void queryClient.invalidateQueries({ queryKey: repositoryKeys.all }),
|
|
84
73
|
onError: (error) => handleError(error, 'Failed to update repository'),
|
|
85
74
|
}), () => queryClient);
|
|
75
|
+
export const useReconfigureRepositoryPrimaryBackend = () => createMutation(() => ({
|
|
76
|
+
mutationFn: ({ id, backendId }) => reconfigureRepositoryPrimaryBackend(id, { backendId }),
|
|
77
|
+
onError: (error) => handleError(error, 'Failed to reconfigure backend'),
|
|
78
|
+
}), () => queryClient);
|
|
86
79
|
export const useRemoveRepository = () => createMutation(() => ({
|
|
87
80
|
mutationFn: ({ id, local = false }) => local ? sdk.deleteRepository(id) : deleteRepository(id),
|
|
88
|
-
onSuccess: () => void queryClient.invalidateQueries({ queryKey: repositoryKeys.all }),
|
|
89
81
|
onError: (error) => handleError(error, 'Failed to delete repository'),
|
|
90
82
|
}), () => queryClient);
|
|
91
83
|
export const handleCreateBackup = async (id) => {
|
|
@@ -50,12 +50,10 @@ export const handleGetSchedules = async () => {
|
|
|
50
50
|
};
|
|
51
51
|
export const useCreateSchedule = () => createMutation(() => ({
|
|
52
52
|
mutationFn: (dto) => sdk.createSchedule(dto),
|
|
53
|
-
onSuccess: () => void queryClient.invalidateQueries({ queryKey: scheduleKeys.all }),
|
|
54
53
|
onError: (error) => handleError(error, 'Failed to create schedule'),
|
|
55
54
|
}), () => queryClient);
|
|
56
55
|
export const useUpdateSchedule = () => createMutation(() => ({
|
|
57
56
|
mutationFn: ({ id, dto, }) => sdk.updateSchedule(id, dto),
|
|
58
|
-
onSuccess: () => void queryClient.invalidateQueries({ queryKey: scheduleKeys.all }),
|
|
59
57
|
onError: (error) => handleError(error, 'Failed to update schedule'),
|
|
60
58
|
}), () => queryClient);
|
|
61
59
|
export const handlePauseSchedule = async (id, name) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@futo-org/backups-orchestrator-ui",
|
|
3
|
-
"
|
|
3
|
+
"repository": "https://github.com/immich-app/yucca",
|
|
4
|
+
"description": "Backups orchestrator (UI library)",
|
|
5
|
+
"version": "0.3.1",
|
|
4
6
|
"license": "Source First License 1.1",
|
|
5
7
|
"files": [
|
|
6
8
|
"dist",
|
|
@@ -27,7 +29,7 @@
|
|
|
27
29
|
"svelte": "^5.0.0"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
|
-
"@playwright/test": "
|
|
32
|
+
"@playwright/test": "1.59.1",
|
|
31
33
|
"@sveltejs/adapter-auto": "^7.0.0",
|
|
32
34
|
"@sveltejs/kit": "^2.50.1",
|
|
33
35
|
"@sveltejs/package": "^2.5.7",
|
|
@@ -38,7 +40,7 @@
|
|
|
38
40
|
"@types/luxon": "^3.7.1",
|
|
39
41
|
"@vitest/browser-playwright": "^4.0.18",
|
|
40
42
|
"oazapfts": "^7.0.1",
|
|
41
|
-
"playwright": "
|
|
43
|
+
"playwright": "1.59.1",
|
|
42
44
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
43
45
|
"publint": "^0.3.17",
|
|
44
46
|
"svelte": "^5.48.2",
|
|
@@ -62,7 +64,7 @@
|
|
|
62
64
|
"lodash.debounce": "^4.0.8",
|
|
63
65
|
"luxon": "^3.7.2",
|
|
64
66
|
"socket.io-client": "^4.8.3",
|
|
65
|
-
"@futo-org/backups-api-client": "^0.1
|
|
67
|
+
"@futo-org/backups-api-client": "^0.3.1"
|
|
66
68
|
},
|
|
67
69
|
"scripts": {
|
|
68
70
|
"dev": "vite dev --port 5174",
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
type Props = {
|
|
2
|
-
userCode: string;
|
|
3
|
-
verificationUri: string;
|
|
4
|
-
onCreate?: (backendId: string) => void;
|
|
5
|
-
onClose: () => void;
|
|
6
|
-
};
|
|
7
|
-
declare const OAuthDeviceFlow: import("svelte").Component<Props, {}, "">;
|
|
8
|
-
type OAuthDeviceFlow = ReturnType<typeof OAuthDeviceFlow>;
|
|
9
|
-
export default OAuthDeviceFlow;
|