@futo-org/backups-orchestrator-ui 0.35.1 → 0.37.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/backups/BackupItem.svelte +10 -12
- package/dist/components/backups/BackupStatus.svelte +38 -28
- package/dist/components/backups/BackupStatus.svelte.d.ts +1 -1
- package/dist/components/backups/dialogs/ConfigureRepositoryModal.svelte +3 -5
- package/dist/components/backups/dialogs/ViewStatusModal.svelte +16 -4
- package/dist/components/backups/run-history/RepositoryRunHistoryItem.svelte +7 -0
- package/dist/components/backups/run-history/RepositoryRunHistoryPage.svelte +4 -1
- package/dist/components/dashboard/DashboardBackupHealth.svelte +29 -9
- package/dist/components/dashboard/DashboardRecentBackups.svelte +6 -5
- package/dist/components/integrations/immich/ImmichBackupsSidebarItem.svelte +10 -7
- package/dist/components/integrations/immich/ImmichManageBackupOverview.svelte +24 -11
- package/dist/components/integrations/immich/ImmichOnboardingSetupFlow.svelte +16 -9
- package/dist/components/onboarding/OnboardingGate.svelte +3 -3
- package/dist/components/onboarding/SampleOnboarding.svelte +19 -40
- package/dist/components/test/dashboard/ActiveJobs.svelte +14 -1
- package/dist/components/test/dashboard/BackupHealth.svelte +5 -6
- package/dist/components/test/dashboard/RecentBackups.svelte +2 -1
- package/dist/components/ui/PathPickerModal.svelte +22 -5
- package/dist/fetch-client.d.ts +3 -3
- package/dist/services/log.service.svelte.js +6 -0
- package/dist/utils/backup-status.d.ts +3 -0
- package/dist/utils/backup-status.js +16 -0
- package/package.json +2 -2
- package/dist/components/onboarding/stages/SampleCreateFirstBackup.svelte +0 -43
- package/dist/components/onboarding/stages/SampleCreateFirstBackup.svelte.d.ts +0 -7
- package/dist/components/onboarding/stages/SampleCreateFirstSchedule.svelte +0 -49
- package/dist/components/onboarding/stages/SampleCreateFirstSchedule.svelte.d.ts +0 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { LocalRepositoryDto } from "../../fetch-client";
|
|
3
3
|
import { getRepositoryActions } from "../../services/repository.service";
|
|
4
|
+
import { getBackupOutcome } from "../../utils/backup-status";
|
|
4
5
|
import { Badge, FormatBytes, Icon } from "@immich/ui";
|
|
5
6
|
import { mdiArchiveOutline } from "@mdi/js";
|
|
6
7
|
import StackListItem from "../ui/StackListItem.svelte";
|
|
@@ -18,12 +19,7 @@
|
|
|
18
19
|
s3: "S3 Server",
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
const
|
|
22
|
-
repository.metrics.lastBackup &&
|
|
23
|
-
(!repository.metrics.lastSuccessfulBackup ||
|
|
24
|
-
+new Date(repository.metrics.lastBackup) >
|
|
25
|
-
+new Date(repository.metrics.lastSuccessfulBackup)),
|
|
26
|
-
);
|
|
22
|
+
const outcome = $derived(getBackupOutcome(repository.metrics));
|
|
27
23
|
|
|
28
24
|
const { BackupNow, Snapshots, History, Configure, Import, MetricsHistory } =
|
|
29
25
|
$derived(getRepositoryActions(repository));
|
|
@@ -31,7 +27,7 @@
|
|
|
31
27
|
|
|
32
28
|
<StackListItem
|
|
33
29
|
title={repository.name}
|
|
34
|
-
color={failed ? "danger" : "primary"}
|
|
30
|
+
color={outcome === "failed" ? "danger" : "primary"}
|
|
35
31
|
actions={[BackupNow, Snapshots, History, Configure, Import, MetricsHistory]}
|
|
36
32
|
>
|
|
37
33
|
{#snippet icon()}
|
|
@@ -57,15 +53,17 @@
|
|
|
57
53
|
<Badge size="tiny" color="danger">Offline</Badge>
|
|
58
54
|
{/if}
|
|
59
55
|
|
|
60
|
-
{#if failed}
|
|
56
|
+
{#if outcome === "failed"}
|
|
61
57
|
<Badge size="tiny" color="danger">
|
|
62
58
|
Failed <RelativeTime time={repository.metrics.lastBackup!} />
|
|
63
59
|
</Badge>
|
|
64
|
-
{:else if
|
|
60
|
+
{:else if outcome === "warn"}
|
|
61
|
+
<Badge size="tiny" color="warning">
|
|
62
|
+
Warnings <RelativeTime time={repository.metrics.lastBackup!} />
|
|
63
|
+
</Badge>
|
|
64
|
+
{:else if outcome === "complete"}
|
|
65
65
|
<Badge size="tiny" color="success">
|
|
66
|
-
Successful <RelativeTime
|
|
67
|
-
time={repository.metrics.lastSuccessfulBackup}
|
|
68
|
-
/>
|
|
66
|
+
Successful <RelativeTime time={repository.metrics.lastBackup!} />
|
|
69
67
|
</Badge>
|
|
70
68
|
{:else}
|
|
71
69
|
<Badge size="tiny" color="warning">Never backed up</Badge>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
| "connecting"
|
|
6
6
|
| "running"
|
|
7
7
|
| "complete"
|
|
8
|
+
| "warned"
|
|
8
9
|
| "failed";
|
|
9
10
|
</script>
|
|
10
11
|
|
|
@@ -50,8 +51,6 @@
|
|
|
50
51
|
onRetry,
|
|
51
52
|
}: Props = $props();
|
|
52
53
|
|
|
53
|
-
let showErrors = $state(false);
|
|
54
|
-
|
|
55
54
|
const phases: Record<BackupStatusType, [string, string, string]> = {
|
|
56
55
|
backup: ["Preparing backup", "Backing up", "Finalizing backup"],
|
|
57
56
|
restore: ["Preparing restore", "Restoring", "Finalizing restore"],
|
|
@@ -64,6 +63,12 @@
|
|
|
64
63
|
forget: "Old backups were pruned successfully",
|
|
65
64
|
};
|
|
66
65
|
|
|
66
|
+
const warned: Record<BackupStatusType, string> = {
|
|
67
|
+
backup: "Your library was backed up, with warnings",
|
|
68
|
+
restore: "Your library was restored, with warnings",
|
|
69
|
+
forget: "Old backups were pruned, with warnings",
|
|
70
|
+
};
|
|
71
|
+
|
|
67
72
|
const failed: Record<BackupStatusType, string> = {
|
|
68
73
|
backup: "Your library could not be backed up",
|
|
69
74
|
restore: "Your library could not be restored",
|
|
@@ -104,6 +109,9 @@
|
|
|
104
109
|
case "complete": {
|
|
105
110
|
return succeeded[type];
|
|
106
111
|
}
|
|
112
|
+
case "warned": {
|
|
113
|
+
return warned[type];
|
|
114
|
+
}
|
|
107
115
|
default: {
|
|
108
116
|
return `${phase} · ${Math.round(progress * 100)}%`;
|
|
109
117
|
}
|
|
@@ -111,10 +119,20 @@
|
|
|
111
119
|
});
|
|
112
120
|
|
|
113
121
|
const titleColor = $derived(
|
|
114
|
-
backupState === "complete"
|
|
122
|
+
backupState === "complete"
|
|
123
|
+
? "success"
|
|
124
|
+
: backupState === "warned"
|
|
125
|
+
? "warning"
|
|
126
|
+
: backupState === "failed"
|
|
127
|
+
? "danger"
|
|
128
|
+
: "primary",
|
|
115
129
|
);
|
|
116
130
|
|
|
117
|
-
const terminal = $derived(
|
|
131
|
+
const terminal = $derived(
|
|
132
|
+
backupState === "complete" ||
|
|
133
|
+
backupState === "warned" ||
|
|
134
|
+
backupState === "failed",
|
|
135
|
+
);
|
|
118
136
|
</script>
|
|
119
137
|
|
|
120
138
|
<Stack gap={4}>
|
|
@@ -132,7 +150,7 @@
|
|
|
132
150
|
<Text color="muted">Completed in {duration}</Text>
|
|
133
151
|
{/if}
|
|
134
152
|
|
|
135
|
-
{#if backupState === "
|
|
153
|
+
{#if backupState === "warned" && errors.length > 0}
|
|
136
154
|
<Text color="warning">
|
|
137
155
|
Completed with {errors.length}
|
|
138
156
|
{errors.length === 1 ? "warning" : "warnings"}.
|
|
@@ -151,38 +169,30 @@
|
|
|
151
169
|
{/if}
|
|
152
170
|
|
|
153
171
|
{#if terminal && (errors.length > 0 || (backupState === "failed" && onRetry))}
|
|
154
|
-
<
|
|
172
|
+
<Stack gap={4}>
|
|
155
173
|
{#if backupState === "failed" && onRetry}
|
|
156
|
-
<
|
|
174
|
+
<HStack gap={4} class="items-center">
|
|
175
|
+
<Button shape="round" onclick={onRetry}>Try again</Button>
|
|
176
|
+
</HStack>
|
|
157
177
|
{/if}
|
|
158
178
|
|
|
159
179
|
{#if errors.length > 0}
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
180
|
+
<Scrollable class="max-h-64">
|
|
181
|
+
<Stack gap={2}>
|
|
182
|
+
{#each errors as error, index (index)}
|
|
183
|
+
<Alert color={backupState === "failed" ? "danger" : "warning"}>
|
|
184
|
+
{error}
|
|
185
|
+
</Alert>
|
|
186
|
+
{/each}
|
|
187
|
+
</Stack>
|
|
188
|
+
</Scrollable>
|
|
167
189
|
{/if}
|
|
168
|
-
</
|
|
169
|
-
|
|
170
|
-
{#if showErrors}
|
|
171
|
-
<Scrollable class="max-h-32">
|
|
172
|
-
<Stack gap={1}>
|
|
173
|
-
{#each errors as error, index (index)}
|
|
174
|
-
<Alert color={backupState === "failed" ? "danger" : "warning"}>
|
|
175
|
-
{error}
|
|
176
|
-
</Alert>
|
|
177
|
-
{/each}
|
|
178
|
-
</Stack>
|
|
179
|
-
</Scrollable>
|
|
180
|
-
{/if}
|
|
190
|
+
</Stack>
|
|
181
191
|
{/if}
|
|
182
192
|
|
|
183
193
|
{#if backupState === "running"}
|
|
184
194
|
<Text color="muted">{running[type]}</Text>
|
|
185
|
-
{:else if backupState === "complete" && details}
|
|
195
|
+
{:else if (backupState === "complete" || backupState === "warned") && details}
|
|
186
196
|
<Text color="muted">{@render details()}</Text>
|
|
187
197
|
{/if}
|
|
188
198
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type BackupStatusType = "backup" | "restore" | "forget";
|
|
2
|
-
export type BackupStatusState = "connecting" | "running" | "complete" | "failed";
|
|
2
|
+
export type BackupStatusState = "connecting" | "running" | "complete" | "warned" | "failed";
|
|
3
3
|
import type { Snippet } from "svelte";
|
|
4
4
|
type Props = {
|
|
5
5
|
title: string;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import BackendsList from "../../backends/BackendsList.svelte";
|
|
3
|
+
import PathListField from "../../ui/PathListField.svelte";
|
|
3
4
|
import { type LocalRepositoryDto } from "../../../fetch-client";
|
|
4
5
|
import { options } from "../../../options";
|
|
5
6
|
import {
|
|
6
7
|
useRemoveRepository,
|
|
7
8
|
useUpdateRepository,
|
|
8
9
|
} from "../../../services/repository.service";
|
|
9
|
-
import PathListField from "../../ui/PathListField.svelte";
|
|
10
10
|
import {
|
|
11
11
|
Button,
|
|
12
12
|
Field,
|
|
@@ -89,10 +89,8 @@
|
|
|
89
89
|
</PathListField>
|
|
90
90
|
{/if}
|
|
91
91
|
|
|
92
|
-
<Button
|
|
93
|
-
|
|
94
|
-
loading={removeMutation.isPending}
|
|
95
|
-
onclick={onRemove}>Remove Repository</Button
|
|
92
|
+
<Button color="danger" loading={removeMutation.isPending} onclick={onRemove}
|
|
93
|
+
>Remove Repository from This Device</Button
|
|
96
94
|
>
|
|
97
95
|
|
|
98
96
|
{#if advanced}
|
|
@@ -57,11 +57,20 @@
|
|
|
57
57
|
return "connecting";
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
switch (run.status) {
|
|
61
|
+
case "incomplete": {
|
|
62
|
+
return "running";
|
|
63
|
+
}
|
|
64
|
+
case "failed": {
|
|
65
|
+
return "failed";
|
|
66
|
+
}
|
|
67
|
+
case "warn": {
|
|
68
|
+
return "warned";
|
|
69
|
+
}
|
|
70
|
+
default: {
|
|
71
|
+
return "complete";
|
|
72
|
+
}
|
|
62
73
|
}
|
|
63
|
-
|
|
64
|
-
return run.status === "failed" ? "failed" : "complete";
|
|
65
74
|
});
|
|
66
75
|
|
|
67
76
|
const duration = $derived(
|
|
@@ -94,6 +103,9 @@
|
|
|
94
103
|
case "complete": {
|
|
95
104
|
return `${titles[type].done} complete`;
|
|
96
105
|
}
|
|
106
|
+
case "warned": {
|
|
107
|
+
return `${titles[type].done} completed with warnings`;
|
|
108
|
+
}
|
|
97
109
|
case "failed": {
|
|
98
110
|
return `${titles[type].done} failed`;
|
|
99
111
|
}
|
|
@@ -38,6 +38,13 @@
|
|
|
38
38
|
icon: mdiCloudOffOutline,
|
|
39
39
|
} as const;
|
|
40
40
|
}
|
|
41
|
+
case "warn": {
|
|
42
|
+
return {
|
|
43
|
+
title: `${noun.done} with warnings`,
|
|
44
|
+
color: "warning",
|
|
45
|
+
icon: mdiCloudCheckOutline,
|
|
46
|
+
} as const;
|
|
47
|
+
}
|
|
41
48
|
case "incomplete": {
|
|
42
49
|
return {
|
|
43
50
|
title: `${noun.running} in progress`,
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
|
|
27
27
|
let search = $state("");
|
|
28
28
|
let newestFirst = $state(true);
|
|
29
|
-
let status = $state<"all" | "complete" | "failed" | "incomplete">(
|
|
29
|
+
let status = $state<"all" | "complete" | "warn" | "failed" | "incomplete">(
|
|
30
|
+
"all",
|
|
31
|
+
);
|
|
30
32
|
let page = $state(1);
|
|
31
33
|
|
|
32
34
|
const onSearch = (value: string) => {
|
|
@@ -47,6 +49,7 @@
|
|
|
47
49
|
const statusOptions: { value: typeof status; title: string }[] = [
|
|
48
50
|
{ value: "all", title: "All attempts" },
|
|
49
51
|
{ value: "complete", title: "Successful only" },
|
|
52
|
+
{ value: "warn", title: "With warnings only" },
|
|
50
53
|
{ value: "failed", title: "Failed only" },
|
|
51
54
|
{ value: "incomplete", title: "In progress only" },
|
|
52
55
|
];
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
CardTitle,
|
|
9
9
|
HStack,
|
|
10
10
|
} from "@immich/ui";
|
|
11
|
+
import { getBackupOutcome } from "../../utils/backup-status";
|
|
11
12
|
import VisualisationSegmentedBar from "../ui/VisualisationSegmentedBar.svelte";
|
|
12
13
|
|
|
13
14
|
type Props = {
|
|
@@ -25,18 +26,31 @@
|
|
|
25
26
|
(tally, repo) => {
|
|
26
27
|
if (local && !repo.backends?.primary.online) {
|
|
27
28
|
tally.offline++;
|
|
28
|
-
|
|
29
|
-
tally.neverRun++;
|
|
30
|
-
} else if (
|
|
31
|
-
repo.metrics.lastBackup === repo.metrics.lastSuccessfulBackup
|
|
32
|
-
) {
|
|
33
|
-
tally.success++;
|
|
34
|
-
} else {
|
|
35
|
-
tally.failed++;
|
|
29
|
+
return tally;
|
|
36
30
|
}
|
|
31
|
+
|
|
32
|
+
switch (getBackupOutcome(repo.metrics)) {
|
|
33
|
+
case "never": {
|
|
34
|
+
tally.neverRun++;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
case "failed": {
|
|
38
|
+
tally.failed++;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case "warn": {
|
|
42
|
+
tally.warned++;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
default: {
|
|
46
|
+
tally.success++;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
return tally;
|
|
38
52
|
},
|
|
39
|
-
{ success: 0, offline: 0, failed: 0, neverRun: 0 },
|
|
53
|
+
{ success: 0, offline: 0, warned: 0, failed: 0, neverRun: 0 },
|
|
40
54
|
),
|
|
41
55
|
);
|
|
42
56
|
</script>
|
|
@@ -69,6 +83,12 @@
|
|
|
69
83
|
color: "var(--immich-ui-warning-500)",
|
|
70
84
|
badge: "warning",
|
|
71
85
|
},
|
|
86
|
+
{
|
|
87
|
+
value: status.warned,
|
|
88
|
+
label: "With warnings",
|
|
89
|
+
color: "var(--immich-ui-warning-500)",
|
|
90
|
+
badge: "warning",
|
|
91
|
+
},
|
|
72
92
|
{
|
|
73
93
|
value: status.failed,
|
|
74
94
|
label: "Failed",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
mdiCloudOffOutline,
|
|
7
7
|
mdiHistory,
|
|
8
8
|
} from "@mdi/js";
|
|
9
|
+
import { getBackupOutcome } from "../../utils/backup-status";
|
|
9
10
|
import MetricsHistoryModal from "../backups/metrics-history/MetricsHistoryModal.svelte";
|
|
10
11
|
import StackList from "../ui/StackList.svelte";
|
|
11
12
|
import StackListPlaceholder from "../ui/StackListPlaceholder.svelte";
|
|
@@ -54,19 +55,19 @@
|
|
|
54
55
|
{/if}
|
|
55
56
|
|
|
56
57
|
{#each recentAttempts as repository (repository.id)}
|
|
57
|
-
{@const
|
|
58
|
-
|
|
58
|
+
{@const outcome = getBackupOutcome(repository.metrics)}
|
|
59
|
+
{@const failed = outcome === "failed"}
|
|
59
60
|
|
|
60
61
|
<StackListItem
|
|
61
62
|
title={repository.name}
|
|
62
|
-
color={
|
|
63
|
+
color={failed ? "danger" : outcome === "warn" ? "warning" : "success"}
|
|
63
64
|
actions={getActions(repository)}
|
|
64
65
|
>
|
|
65
66
|
{#snippet icon()}
|
|
66
|
-
<Icon icon={
|
|
67
|
+
<Icon icon={failed ? mdiCloudOffOutline : mdiCloudCheckOutline} />
|
|
67
68
|
{/snippet}
|
|
68
69
|
|
|
69
|
-
{
|
|
70
|
+
{failed ? "Attempted" : outcome === "warn" ? "Backed up with warnings" : "Backed up"}
|
|
70
71
|
<RelativeTime time={repository.metrics.lastBackup!} />
|
|
71
72
|
</StackListItem>
|
|
72
73
|
{/each}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
useScheduleEventHandler,
|
|
18
18
|
useSchedules,
|
|
19
19
|
} from "../../../services/schedule.service";
|
|
20
|
+
import { getBackupOutcome } from "../../../utils/backup-status";
|
|
20
21
|
import { HStack, Icon, Text } from "@immich/ui";
|
|
21
22
|
import {
|
|
22
23
|
mdiChevronRight,
|
|
@@ -73,11 +74,7 @@
|
|
|
73
74
|
|
|
74
75
|
const lastBackup = $derived(repository?.metrics.lastBackup ?? undefined);
|
|
75
76
|
|
|
76
|
-
const
|
|
77
|
-
Boolean(
|
|
78
|
-
lastBackup && lastBackup !== repository?.metrics.lastSuccessfulBackup,
|
|
79
|
-
),
|
|
80
|
-
);
|
|
77
|
+
const outcome = $derived(getBackupOutcome(repository?.metrics));
|
|
81
78
|
|
|
82
79
|
const loading = $derived(
|
|
83
80
|
integrations.isLoading || repositories.isLoading || schedules.isLoading,
|
|
@@ -91,10 +88,14 @@
|
|
|
91
88
|
return { color: "primary", icon: mdiCloudUploadOutline } as const;
|
|
92
89
|
}
|
|
93
90
|
|
|
94
|
-
if (failed) {
|
|
91
|
+
if (outcome === "failed") {
|
|
95
92
|
return { color: "danger", icon: mdiCloudOffOutline } as const;
|
|
96
93
|
}
|
|
97
94
|
|
|
95
|
+
if (outcome === "warn") {
|
|
96
|
+
return { color: "warning", icon: mdiCloudCheckVariantOutline } as const;
|
|
97
|
+
}
|
|
98
|
+
|
|
98
99
|
if (paused) {
|
|
99
100
|
return { color: "warning", icon: mdiCloudAlertOutline } as const;
|
|
100
101
|
}
|
|
@@ -138,8 +139,10 @@
|
|
|
138
139
|
<Text size="tiny" color={status.color} class="flex-1 truncate">
|
|
139
140
|
{#if running}
|
|
140
141
|
Backing up now
|
|
141
|
-
{:else if failed}
|
|
142
|
+
{:else if outcome === "failed"}
|
|
142
143
|
Backup failed
|
|
144
|
+
{:else if outcome === "warn" && lastBackup}
|
|
145
|
+
Backed up with warnings <RelativeTime time={lastBackup} />
|
|
143
146
|
{:else if paused}
|
|
144
147
|
Backups paused
|
|
145
148
|
{:else if !configured}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import RelativeTime from "../../util/RelativeTime.svelte";
|
|
5
5
|
import type { LocalRepositoryDto, ScheduleDto } from "../../../fetch-client";
|
|
6
6
|
import { handleCreateBackup } from "../../../services/repository.service";
|
|
7
|
+
import { getBackupOutcome } from "../../../utils/backup-status";
|
|
7
8
|
import { Button, FormatBytes, Icon } from "@immich/ui";
|
|
8
9
|
import {
|
|
9
10
|
mdiAlert,
|
|
@@ -21,15 +22,23 @@
|
|
|
21
22
|
|
|
22
23
|
const { repository, schedule }: Props = $props();
|
|
23
24
|
|
|
25
|
+
const outcome = $derived(getBackupOutcome(repository.metrics));
|
|
26
|
+
|
|
24
27
|
const status = $derived.by(() => {
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
switch (outcome) {
|
|
29
|
+
case "never": {
|
|
30
|
+
return { color: "warning", icon: mdiInformation } as const;
|
|
31
|
+
}
|
|
32
|
+
case "failed": {
|
|
33
|
+
return { color: "danger", icon: mdiAlert } as const;
|
|
34
|
+
}
|
|
35
|
+
case "warn": {
|
|
36
|
+
return { color: "warning", icon: mdiAlert } as const;
|
|
37
|
+
}
|
|
38
|
+
default: {
|
|
39
|
+
return { color: "success", icon: mdiCheck } as const;
|
|
40
|
+
}
|
|
27
41
|
}
|
|
28
|
-
|
|
29
|
-
return repository.metrics.lastBackup !==
|
|
30
|
-
repository.metrics.lastSuccessfulBackup
|
|
31
|
-
? ({ color: "danger", icon: mdiAlert } as const)
|
|
32
|
-
: ({ color: "success", icon: mdiCheck } as const);
|
|
33
42
|
});
|
|
34
43
|
|
|
35
44
|
</script>
|
|
@@ -64,13 +73,17 @@
|
|
|
64
73
|
{#snippet footer()}
|
|
65
74
|
<Icon icon={status.icon} />
|
|
66
75
|
|
|
67
|
-
{#if
|
|
76
|
+
{#if outcome === "never"}
|
|
68
77
|
Backup is yet to run.
|
|
69
|
-
{:else if
|
|
70
|
-
Last backup failed <RelativeTime time={repository.metrics.lastBackup} />
|
|
78
|
+
{:else if outcome === "failed"}
|
|
79
|
+
Last backup failed <RelativeTime time={repository.metrics.lastBackup!} />
|
|
80
|
+
{:else if outcome === "warn"}
|
|
81
|
+
Last backup finished with warnings <RelativeTime
|
|
82
|
+
time={repository.metrics.lastBackup!}
|
|
83
|
+
/>
|
|
71
84
|
{:else}
|
|
72
85
|
Last backup successful <RelativeTime
|
|
73
|
-
time={repository.metrics.lastBackup}
|
|
86
|
+
time={repository.metrics.lastBackup!}
|
|
74
87
|
/>
|
|
75
88
|
{/if}
|
|
76
89
|
{/snippet}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import OnboardingStepConnectAccount from "../../onboarding/steps/OnboardingStep2ConnectAccount.svelte";
|
|
7
7
|
import OnboardingStepSaveRecoveryKey from "../../onboarding/steps/OnboardingStep3SaveRecoveryKey.svelte";
|
|
8
8
|
import OnboardingStepFirstBackup from "../../onboarding/steps/OnboardingStep4FirstBackup.svelte";
|
|
9
|
-
import type
|
|
9
|
+
import { type OnboardingStatusResponseDto } from "../../../fetch-client";
|
|
10
10
|
import {
|
|
11
11
|
handleSetupLocalStorage,
|
|
12
12
|
handleStartYuccaLogin,
|
|
@@ -78,11 +78,11 @@
|
|
|
78
78
|
const onCancel = () => (stage = "idle");
|
|
79
79
|
|
|
80
80
|
const afterTelemetry = () =>
|
|
81
|
-
(stage =
|
|
82
|
-
? "finished"
|
|
83
|
-
: "connect");
|
|
81
|
+
(stage =
|
|
82
|
+
status?.hasOnboardedKey && status.hasBackup ? "finished" : "connect");
|
|
84
83
|
|
|
85
|
-
const onBackendReady = () =>
|
|
84
|
+
const onBackendReady = () =>
|
|
85
|
+
(stage = status?.hasOnboardedKey ? "backup" : "key");
|
|
86
86
|
|
|
87
87
|
const onConnect = () => {
|
|
88
88
|
storageLocation = "FUTO Backups";
|
|
@@ -105,6 +105,16 @@
|
|
|
105
105
|
}
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
+
const onImportedKey = async (key: string) => {
|
|
109
|
+
try {
|
|
110
|
+
await handleConfirmRecoveryKey();
|
|
111
|
+
code = key;
|
|
112
|
+
stage = "connect";
|
|
113
|
+
} catch {
|
|
114
|
+
// no-op
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
108
118
|
const onStartBackup = () =>
|
|
109
119
|
defaults.mutate(undefined, { onSuccess: () => (stage = "finished") });
|
|
110
120
|
</script>
|
|
@@ -142,10 +152,7 @@
|
|
|
142
152
|
{:else if stage === "key-import"}
|
|
143
153
|
<OnboardingStageKeyImport
|
|
144
154
|
onStart={() => (stage = "intro")}
|
|
145
|
-
onImported={
|
|
146
|
-
code = key;
|
|
147
|
-
stage = "connect";
|
|
148
|
-
}}
|
|
155
|
+
onImported={onImportedKey}
|
|
149
156
|
{onCancel}
|
|
150
157
|
/>
|
|
151
158
|
{:else if stage === "key"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { OnboardingStatusResponseDto } from "../../fetch-client";
|
|
3
|
+
import { handleOnboardingStatus } from "../../services/onboarding.service";
|
|
2
4
|
import { LoadingSpinner } from "@immich/ui";
|
|
3
5
|
import { onMount, type Snippet } from "svelte";
|
|
4
6
|
import OnboardingBootstrapError from "./OnboardingBootstrapError.svelte";
|
|
5
7
|
import SampleOnboarding from "./SampleOnboarding.svelte";
|
|
6
|
-
import type { OnboardingStatusResponseDto } from "../../fetch-client";
|
|
7
|
-
import { handleOnboardingStatus } from "../../services/onboarding.service";
|
|
8
8
|
|
|
9
9
|
type Props = {
|
|
10
10
|
onExit: () => void;
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<LoadingSpinner />
|
|
38
38
|
{:else if onboarding.status === "error"}
|
|
39
39
|
<OnboardingBootstrapError error={onboarding.error} onQuit={onExit} />
|
|
40
|
-
{:else if onboarding.hasTelemetry === "none" || !(onboarding.hasBackend && onboarding.hasOnboardedKey
|
|
40
|
+
{:else if onboarding.hasTelemetry === "none" || !(onboarding.hasBackend && onboarding.hasOnboardedKey)}
|
|
41
41
|
<SampleOnboarding
|
|
42
42
|
status={onboarding}
|
|
43
43
|
onFinish={() => (onFinish ? onFinish() : onSkip())}
|
|
@@ -7,12 +7,9 @@
|
|
|
7
7
|
import {
|
|
8
8
|
handleConfirmRecoveryKey,
|
|
9
9
|
handleCurrentRecoveryKey,
|
|
10
|
-
handleSkipOnboardingExtraConfig,
|
|
11
10
|
} from "../../services/onboarding.service";
|
|
12
11
|
import { Modal, ModalBody } from "@immich/ui";
|
|
13
12
|
import { onMount } from "svelte";
|
|
14
|
-
import CreateFirstBackup from "./stages/SampleCreateFirstBackup.svelte";
|
|
15
|
-
import CreateFirstSchedule from "./stages/SampleCreateFirstSchedule.svelte";
|
|
16
13
|
import ImportKey from "./stages/OnboardingStageKeyImport.svelte";
|
|
17
14
|
import Telemetry from "./stages/OnboardingStageTelemetry.svelte";
|
|
18
15
|
import StepFinishSetup from "./steps/OnboardingStep1FinishSetup.svelte";
|
|
@@ -31,23 +28,12 @@
|
|
|
31
28
|
let confirming = $state(false);
|
|
32
29
|
|
|
33
30
|
// svelte-ignore state_referenced_locally
|
|
34
|
-
let stage:
|
|
35
|
-
| "intro"
|
|
36
|
-
| "telemetry"
|
|
37
|
-
| "key"
|
|
38
|
-
| "key-import"
|
|
39
|
-
| "connect"
|
|
40
|
-
| "backup-create"
|
|
41
|
-
| "schedule-create" = $state(
|
|
31
|
+
let stage: "intro" | "telemetry" | "key" | "key-import" | "connect" = $state(
|
|
42
32
|
!status.hasOnboardedKey
|
|
43
33
|
? "intro"
|
|
44
34
|
: status.hasTelemetry === "none"
|
|
45
35
|
? "telemetry"
|
|
46
|
-
:
|
|
47
|
-
? "connect"
|
|
48
|
-
: !status.hasBackup
|
|
49
|
-
? "backup-create"
|
|
50
|
-
: "schedule-create",
|
|
36
|
+
: "connect",
|
|
51
37
|
);
|
|
52
38
|
|
|
53
39
|
onMount(() => {
|
|
@@ -56,11 +42,6 @@
|
|
|
56
42
|
}
|
|
57
43
|
});
|
|
58
44
|
|
|
59
|
-
const onSkip = () => {
|
|
60
|
-
void handleSkipOnboardingExtraConfig();
|
|
61
|
-
onFinish();
|
|
62
|
-
};
|
|
63
|
-
|
|
64
45
|
const onConfirmKey = async () => {
|
|
65
46
|
confirming = true;
|
|
66
47
|
|
|
@@ -77,7 +58,15 @@
|
|
|
77
58
|
}
|
|
78
59
|
};
|
|
79
60
|
|
|
80
|
-
const
|
|
61
|
+
const onTelemetryConfirmed = () => {
|
|
62
|
+
if (!status.hasOnboardedKey) {
|
|
63
|
+
stage = "key";
|
|
64
|
+
} else if (status.hasBackend) {
|
|
65
|
+
onFinish();
|
|
66
|
+
} else {
|
|
67
|
+
stage = "connect";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
81
70
|
</script>
|
|
82
71
|
|
|
83
72
|
{#if stage === "intro"}
|
|
@@ -91,21 +80,15 @@
|
|
|
91
80
|
</ModalBody>
|
|
92
81
|
</Modal>
|
|
93
82
|
{:else if stage === "telemetry"}
|
|
94
|
-
<Telemetry
|
|
95
|
-
onContinue={() =>
|
|
96
|
-
(stage = !status.hasOnboardedKey
|
|
97
|
-
? "key"
|
|
98
|
-
: !status.hasBackend
|
|
99
|
-
? "connect"
|
|
100
|
-
: !status.hasBackup
|
|
101
|
-
? "backup-create"
|
|
102
|
-
: "schedule-create")}
|
|
103
|
-
{onCancel}
|
|
104
|
-
/>
|
|
83
|
+
<Telemetry onContinue={onTelemetryConfirmed} {onCancel} />
|
|
105
84
|
{:else if stage === "key"}
|
|
106
85
|
<Modal size="small" title="FUTO Backups" onClose={onCancel}>
|
|
107
86
|
<ModalBody>
|
|
108
|
-
<StepSaveRecoveryKey
|
|
87
|
+
<StepSaveRecoveryKey
|
|
88
|
+
{code}
|
|
89
|
+
onContinue={onConfirmKey}
|
|
90
|
+
loading={confirming}
|
|
91
|
+
/>
|
|
109
92
|
</ModalBody>
|
|
110
93
|
</Modal>
|
|
111
94
|
{:else if stage === "key-import"}
|
|
@@ -121,13 +104,9 @@
|
|
|
121
104
|
<Modal size="small" title="FUTO Backups" onClose={onCancel}>
|
|
122
105
|
<ModalBody>
|
|
123
106
|
<StepConnectAccount
|
|
124
|
-
onConnect={() => handleStartYuccaLogin(
|
|
125
|
-
onLocalStorage={() => handleSetupLocalStorage(
|
|
107
|
+
onConnect={() => handleStartYuccaLogin(onFinish)}
|
|
108
|
+
onLocalStorage={() => handleSetupLocalStorage(onFinish)}
|
|
126
109
|
/>
|
|
127
110
|
</ModalBody>
|
|
128
111
|
</Modal>
|
|
129
|
-
{:else if stage === "backup-create"}
|
|
130
|
-
<CreateFirstBackup onNext={() => (stage = "schedule-create")} {onSkip} />
|
|
131
|
-
{:else if stage === "schedule-create"}
|
|
132
|
-
<CreateFirstSchedule {onFinish} {onSkip} />
|
|
133
112
|
{/if}
|
|
@@ -88,12 +88,17 @@
|
|
|
88
88
|
const hasFailedItems = (task: LiveTask) =>
|
|
89
89
|
task.scheduleStatus?.some((item) => item.status === "failed") ?? false;
|
|
90
90
|
|
|
91
|
+
const hasWarnItems = (task: LiveTask) =>
|
|
92
|
+
task.scheduleStatus?.some((item) => item.status === "warn") ?? false;
|
|
93
|
+
|
|
91
94
|
const statusColor = (task: LiveTask) =>
|
|
92
95
|
!task.completedAt
|
|
93
96
|
? "var(--immich-ui-warning-500)"
|
|
94
97
|
: hasFailedItems(task)
|
|
95
98
|
? "var(--immich-ui-danger-500)"
|
|
96
|
-
:
|
|
99
|
+
: hasWarnItems(task)
|
|
100
|
+
? "var(--immich-ui-warning-500)"
|
|
101
|
+
: "var(--immich-ui-success-500)";
|
|
97
102
|
|
|
98
103
|
onMount(async () => {
|
|
99
104
|
const taskData = await handleGetRunningTasks();
|
|
@@ -210,6 +215,12 @@
|
|
|
210
215
|
size="16"
|
|
211
216
|
class="text-danger-500"
|
|
212
217
|
/>
|
|
218
|
+
{:else if item.status === "warn"}
|
|
219
|
+
<Icon
|
|
220
|
+
icon={mdiCheckCircleOutline}
|
|
221
|
+
size="16"
|
|
222
|
+
class="text-warning-500"
|
|
223
|
+
/>
|
|
213
224
|
{:else if subLog && subLog.progress > 0}
|
|
214
225
|
<Icon
|
|
215
226
|
icon={mdiLoading}
|
|
@@ -232,6 +243,8 @@
|
|
|
232
243
|
<Badge size="tiny" color="success">Done</Badge>
|
|
233
244
|
{:else if item.status === "failed"}
|
|
234
245
|
<Badge size="tiny" color="danger">Failed</Badge>
|
|
246
|
+
{:else if item.status === "warn"}
|
|
247
|
+
<Badge size="tiny" color="warning">Warnings</Badge>
|
|
235
248
|
{:else if subLog && subLog.progress > 0}
|
|
236
249
|
<Badge size="tiny" color="warning"
|
|
237
250
|
>{Math.round(subLog.progress * 100)}%</Badge
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { LocalRepositoryDto } from "../../../fetch-client";
|
|
3
|
+
import { getBackupOutcome } from "../../../utils/backup-status";
|
|
3
4
|
import { Alert, Text } from "@immich/ui";
|
|
4
5
|
import SegmentedBar from "../../ui/VisualisationSegmentedBar.svelte";
|
|
5
6
|
|
|
@@ -16,14 +17,12 @@
|
|
|
16
17
|
(tally, repo) => {
|
|
17
18
|
if (!repo.backends?.primary.online) {
|
|
18
19
|
tally.offline++;
|
|
19
|
-
} else if (
|
|
20
|
+
} else if (getBackupOutcome(repo.metrics) === "never") {
|
|
20
21
|
tally.neverRun++;
|
|
21
|
-
} else if (
|
|
22
|
-
repo.metrics.lastBackup === repo.metrics.lastSuccessfulBackup
|
|
23
|
-
) {
|
|
24
|
-
tally.success++;
|
|
25
|
-
} else {
|
|
22
|
+
} else if (getBackupOutcome(repo.metrics) === "failed") {
|
|
26
23
|
tally.failed++;
|
|
24
|
+
} else {
|
|
25
|
+
tally.success++;
|
|
27
26
|
}
|
|
28
27
|
return tally;
|
|
29
28
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import RelativeTime from "../../util/RelativeTime.svelte";
|
|
3
3
|
import type { LocalRepositoryDto } from "../../../fetch-client";
|
|
4
|
+
import { getBackupOutcome } from "../../../utils/backup-status";
|
|
4
5
|
import { formatDuration } from "../../../utils/format";
|
|
5
6
|
import {
|
|
6
7
|
Button,
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
{/if}
|
|
63
64
|
<HStack class="items-center justify-between py-2">
|
|
64
65
|
<HStack class="items-center gap-2">
|
|
65
|
-
{#if repo.metrics
|
|
66
|
+
{#if getBackupOutcome(repo.metrics) !== "failed"}
|
|
66
67
|
<Icon
|
|
67
68
|
icon={mdiCheckCircleOutline}
|
|
68
69
|
size="16"
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
} from "@immich/ui";
|
|
17
17
|
import {
|
|
18
18
|
mdiArrowUp,
|
|
19
|
+
mdiCheck,
|
|
19
20
|
mdiClose,
|
|
20
21
|
mdiFileOutline,
|
|
21
22
|
mdiFolderOutline,
|
|
@@ -149,7 +150,12 @@
|
|
|
149
150
|
Browse
|
|
150
151
|
{/snippet}
|
|
151
152
|
|
|
152
|
-
<HStack
|
|
153
|
+
<HStack
|
|
154
|
+
gap={2}
|
|
155
|
+
class="items-center px-4 py-3 {selected.has(listing.path)
|
|
156
|
+
? 'bg-primary/10'
|
|
157
|
+
: 'bg-subtle'}"
|
|
158
|
+
>
|
|
153
159
|
<Icon icon={mdiFolderOutline} color="primary" />
|
|
154
160
|
<Text class="grow truncate" title={listing.path}>
|
|
155
161
|
{listing.path}
|
|
@@ -167,7 +173,13 @@
|
|
|
167
173
|
|
|
168
174
|
{#each sortedItems as item (item.path)}
|
|
169
175
|
{#if item.isDirectory || !foldersOnly}
|
|
170
|
-
|
|
176
|
+
{@const isSelected = selected.has(item.path)}
|
|
177
|
+
<HStack
|
|
178
|
+
gap={2}
|
|
179
|
+
class="items-center px-2 py-1 {isSelected
|
|
180
|
+
? 'bg-primary/10'
|
|
181
|
+
: ''}"
|
|
182
|
+
>
|
|
171
183
|
{#if item.isDirectory}
|
|
172
184
|
<ListButton
|
|
173
185
|
leadingIcon={mdiFolderOutline}
|
|
@@ -189,11 +201,16 @@
|
|
|
189
201
|
</HStack>
|
|
190
202
|
{/if}
|
|
191
203
|
<IconButton
|
|
192
|
-
icon={mdiPlus}
|
|
193
|
-
aria-label={
|
|
204
|
+
icon={isSelected ? mdiCheck : mdiPlus}
|
|
205
|
+
aria-label={isSelected
|
|
206
|
+
? "Selected"
|
|
207
|
+
: single
|
|
208
|
+
? "Select"
|
|
209
|
+
: "Add"}
|
|
194
210
|
size="tiny"
|
|
195
211
|
variant="ghost"
|
|
196
|
-
|
|
212
|
+
color={isSelected ? "primary" : undefined}
|
|
213
|
+
disabled={!single && isSelected}
|
|
197
214
|
onclick={() => add(item.path)}
|
|
198
215
|
/>
|
|
199
216
|
</HStack>
|
package/dist/fetch-client.d.ts
CHANGED
|
@@ -113,9 +113,10 @@ export type RepositoryCreateRequestDto = {
|
|
|
113
113
|
site?: string;
|
|
114
114
|
paths?: string[];
|
|
115
115
|
};
|
|
116
|
+
export type TaskStatus = "incomplete" | "complete" | "warn" | "failed";
|
|
116
117
|
export type RepositoryMetricsDto = {
|
|
117
118
|
lastBackup?: string;
|
|
118
|
-
|
|
119
|
+
lastBackupStatus?: TaskStatus;
|
|
119
120
|
lastBackupDuration?: number;
|
|
120
121
|
sizeBytes: number;
|
|
121
122
|
};
|
|
@@ -202,7 +203,7 @@ export type RepositoryCheckImportResponseDto = {
|
|
|
202
203
|
export type RepositoryPrimaryBackendReconfigureRequestDto = {
|
|
203
204
|
backendId: string;
|
|
204
205
|
};
|
|
205
|
-
export type RunStatus = "incomplete" | "complete" | "failed";
|
|
206
|
+
export type RunStatus = "incomplete" | "complete" | "warn" | "failed";
|
|
206
207
|
export type RunType = "schedule" | "restore" | "backup" | "forget";
|
|
207
208
|
export type RunDto = {
|
|
208
209
|
id: string;
|
|
@@ -231,7 +232,6 @@ export type RunResponseDto = {
|
|
|
231
232
|
run: RunDto;
|
|
232
233
|
};
|
|
233
234
|
export type TaskType = "schedule" | "restore" | "backup" | "forget";
|
|
234
|
-
export type TaskStatus = "incomplete" | "complete" | "failed";
|
|
235
235
|
export type ActiveScheduleItemDto = {
|
|
236
236
|
repositoryId: string;
|
|
237
237
|
status: TaskStatus;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const getBackupOutcome = (metrics) => {
|
|
2
|
+
if (!metrics?.lastBackup) {
|
|
3
|
+
return 'never';
|
|
4
|
+
}
|
|
5
|
+
switch (metrics.lastBackupStatus) {
|
|
6
|
+
case 'failed': {
|
|
7
|
+
return 'failed';
|
|
8
|
+
}
|
|
9
|
+
case 'warn': {
|
|
10
|
+
return 'warn';
|
|
11
|
+
}
|
|
12
|
+
default: {
|
|
13
|
+
return 'complete';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
};
|
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.37.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.37.0"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"dev": "vite dev --port 5174",
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Checkbox, Field, FormModal, Input, Stack } from "@immich/ui";
|
|
3
|
-
import { useCreateRepository } from "../../../services/repository.service";
|
|
4
|
-
import { SvelteSet } from "svelte/reactivity";
|
|
5
|
-
|
|
6
|
-
type Props = {
|
|
7
|
-
onNext: () => void;
|
|
8
|
-
onSkip: () => void;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
let { onNext, onSkip }: Props = $props();
|
|
12
|
-
|
|
13
|
-
let name = $state("");
|
|
14
|
-
let worm = $state(false);
|
|
15
|
-
let paths = new SvelteSet([]);
|
|
16
|
-
|
|
17
|
-
const mutation = useCreateRepository();
|
|
18
|
-
|
|
19
|
-
const onSubmit = () =>
|
|
20
|
-
mutation.mutate(
|
|
21
|
-
{ name, worm, paths: [...paths] },
|
|
22
|
-
{ onSuccess: () => onSkip() },
|
|
23
|
-
);
|
|
24
|
-
</script>
|
|
25
|
-
|
|
26
|
-
<FormModal
|
|
27
|
-
title="Create Your First Backup"
|
|
28
|
-
disabled={name.length === 0 || mutation.isPending}
|
|
29
|
-
onClose={onSkip}
|
|
30
|
-
{onSubmit}
|
|
31
|
-
>
|
|
32
|
-
<Stack gap={4}>
|
|
33
|
-
<Field label="Name" description="A memorable name for this backup">
|
|
34
|
-
<Input bind:value={name} />
|
|
35
|
-
</Field>
|
|
36
|
-
<Field
|
|
37
|
-
label="Write once (WORM)"
|
|
38
|
-
description="Prevent anything being deleted"
|
|
39
|
-
>
|
|
40
|
-
<Checkbox bind:checked={worm} />
|
|
41
|
-
</Field>
|
|
42
|
-
</Stack>
|
|
43
|
-
</FormModal>
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Field, FormModal, Input, Stack } from "@immich/ui";
|
|
3
|
-
import validate from "cron-validate";
|
|
4
|
-
import { useCreateSchedule } from "../../../services/schedule.service";
|
|
5
|
-
import { useRepositories } from "../../../services/repository.service";
|
|
6
|
-
|
|
7
|
-
type Props = {
|
|
8
|
-
onFinish: () => void;
|
|
9
|
-
onSkip: () => void;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const { onFinish, onSkip }: Props = $props();
|
|
13
|
-
|
|
14
|
-
const query = useRepositories();
|
|
15
|
-
const repositories = $derived(
|
|
16
|
-
(query.data ?? []).filter((repository) => repository.configuration),
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
let name = $state("");
|
|
20
|
-
let cron = $state("*/15 * * * *");
|
|
21
|
-
|
|
22
|
-
const mutation = useCreateSchedule();
|
|
23
|
-
|
|
24
|
-
const onSubmit = () =>
|
|
25
|
-
mutation.mutate(
|
|
26
|
-
{
|
|
27
|
-
name,
|
|
28
|
-
cron,
|
|
29
|
-
repositories: repositories.map((repository) => repository.id),
|
|
30
|
-
},
|
|
31
|
-
{ onSuccess: () => onFinish() },
|
|
32
|
-
);
|
|
33
|
-
</script>
|
|
34
|
-
|
|
35
|
-
<FormModal
|
|
36
|
-
title="Create First Schedule"
|
|
37
|
-
disabled={name.length === 0 || validate(cron).isError() || mutation.isPending}
|
|
38
|
-
onClose={onSkip}
|
|
39
|
-
{onSubmit}
|
|
40
|
-
>
|
|
41
|
-
<Stack gap={4}>
|
|
42
|
-
<Field label="Name" description="Give this schedule a name">
|
|
43
|
-
<Input bind:value={name} />
|
|
44
|
-
</Field>
|
|
45
|
-
<Field label="Schedule" description="Uses cron syntax">
|
|
46
|
-
<Input bind:value={cron} />
|
|
47
|
-
</Field>
|
|
48
|
-
</Stack>
|
|
49
|
-
</FormModal>
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
type Props = {
|
|
2
|
-
onFinish: () => void;
|
|
3
|
-
onSkip: () => void;
|
|
4
|
-
};
|
|
5
|
-
declare const SampleCreateFirstSchedule: import("svelte").Component<Props, {}, "">;
|
|
6
|
-
type SampleCreateFirstSchedule = ReturnType<typeof SampleCreateFirstSchedule>;
|
|
7
|
-
export default SampleCreateFirstSchedule;
|