@futo-org/backups-orchestrator-ui 0.27.0 → 0.28.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.
Files changed (156) hide show
  1. package/dist/components/backends/BackendItem.svelte +28 -24
  2. package/dist/components/backends/BackendsList.svelte +33 -34
  3. package/dist/components/backends/BackendsList.svelte.d.ts +1 -0
  4. package/dist/components/backends/dialogs/SelectBackendModal.svelte +44 -55
  5. package/dist/components/backups/BackupItem.svelte +33 -26
  6. package/dist/components/backups/BackupStatus.svelte +200 -0
  7. package/dist/components/backups/BackupStatus.svelte.d.ts +19 -0
  8. package/dist/components/backups/BackupsList.svelte +17 -11
  9. package/dist/components/backups/dialogs/RestoreSnapshotModal.svelte +2 -2
  10. package/dist/components/backups/dialogs/SelectRepositoryModal.svelte +26 -36
  11. package/dist/components/backups/dialogs/ViewStatusModal.svelte +175 -0
  12. package/dist/components/backups/dialogs/ViewStatusModal.svelte.d.ts +8 -0
  13. package/dist/components/backups/run-history/RepositoryRunHistory.svelte +22 -10
  14. package/dist/components/backups/run-history/RepositoryRunHistory.svelte.d.ts +2 -0
  15. package/dist/components/backups/run-history/RepositoryRunHistoryItem.svelte +46 -20
  16. package/dist/components/backups/run-history/RepositoryRunHistoryPage.svelte +135 -0
  17. package/dist/components/backups/run-history/RepositoryRunHistoryPage.svelte.d.ts +8 -0
  18. package/dist/components/backups/run-history/RunHistoryModal.svelte +2 -2
  19. package/dist/components/backups/snapshots-list/RepositorySnapshotsList.svelte +22 -10
  20. package/dist/components/backups/snapshots-list/RepositorySnapshotsList.svelte.d.ts +2 -0
  21. package/dist/components/backups/snapshots-list/RepositorySnapshotsListItem.svelte +23 -21
  22. package/dist/components/backups/snapshots-list/RepositorySnapshotsPage.svelte +115 -0
  23. package/dist/components/backups/snapshots-list/RepositorySnapshotsPage.svelte.d.ts +9 -0
  24. package/dist/components/backups/snapshots-list/SnapshotsListModal.svelte +2 -2
  25. package/dist/components/dashboard/Dashboard.svelte +14 -7
  26. package/dist/components/dashboard/Dashboard.svelte.d.ts +1 -1
  27. package/dist/components/dashboard/DashboardAvgBackupTime.svelte +1 -1
  28. package/dist/components/dashboard/DashboardBackupHealth.svelte +5 -9
  29. package/dist/components/dashboard/DashboardBackupHealth.svelte.d.ts +1 -1
  30. package/dist/components/dashboard/DashboardCurrentUsage.svelte +1 -1
  31. package/dist/components/dashboard/DashboardDailyBackupTime.svelte +1 -1
  32. package/dist/components/dashboard/DashboardInstall.svelte +1 -1
  33. package/dist/components/dashboard/DashboardRecentBackups.svelte +47 -78
  34. package/dist/components/dashboard/DashboardTotalStored.svelte +1 -1
  35. package/dist/components/integrations/immich/ImmichBackupSettings.svelte +12 -0
  36. package/dist/components/integrations/immich/ImmichBackupSettings.svelte.d.ts +18 -0
  37. package/dist/components/integrations/immich/ImmichBackupsCard.svelte +73 -0
  38. package/dist/components/integrations/immich/ImmichBackupsCard.svelte.d.ts +12 -0
  39. package/dist/components/integrations/immich/ImmichBackupsNavButton.svelte +55 -0
  40. package/dist/components/integrations/immich/ImmichBackupsNavButton.svelte.d.ts +10 -0
  41. package/dist/components/integrations/immich/ImmichBackupsPage.svelte +15 -3
  42. package/dist/components/integrations/immich/ImmichBackupsPage.svelte.d.ts +7 -1
  43. package/dist/components/integrations/immich/ImmichBackupsSidebarItem.svelte +66 -0
  44. package/dist/components/integrations/immich/ImmichBackupsSidebarItem.svelte.d.ts +8 -0
  45. package/dist/components/integrations/immich/ImmichManageBackup.svelte +53 -14
  46. package/dist/components/integrations/immich/ImmichManageBackupOverview.svelte +45 -71
  47. package/dist/components/integrations/immich/ImmichOnboardingSetupFlow.svelte +123 -82
  48. package/dist/components/integrations/immich/ImmichOnboardingSetupFlow.svelte.d.ts +1 -1
  49. package/dist/components/integrations/immich/settings/ImmichSettingsBackupContents.svelte +313 -0
  50. package/dist/components/integrations/immich/settings/ImmichSettingsBackupContents.svelte.d.ts +3 -0
  51. package/dist/components/integrations/immich/settings/ImmichSettingsSchedule.svelte +218 -0
  52. package/dist/components/integrations/immich/settings/ImmichSettingsSchedule.svelte.d.ts +3 -0
  53. package/dist/components/integrations/immich/settings/ImmichSettingsStorage.svelte +207 -0
  54. package/dist/components/integrations/immich/settings/ImmichSettingsStorage.svelte.d.ts +3 -0
  55. package/dist/components/onboarding/RecoveryKeyDisplay.svelte +34 -20
  56. package/dist/components/onboarding/SampleOnboarding.svelte +57 -38
  57. package/dist/components/onboarding/stages/OnboardingStageBackupServices.svelte +10 -24
  58. package/dist/components/onboarding/steps/OnboardingStep1FinishSetup.svelte +44 -0
  59. package/dist/components/onboarding/steps/OnboardingStep1FinishSetup.svelte.d.ts +7 -0
  60. package/dist/components/onboarding/steps/OnboardingStep2ConnectAccount.svelte +50 -0
  61. package/dist/components/onboarding/steps/OnboardingStep2ConnectAccount.svelte.d.ts +7 -0
  62. package/dist/components/onboarding/steps/OnboardingStep3SaveRecoveryKey.svelte +45 -0
  63. package/dist/components/onboarding/steps/OnboardingStep3SaveRecoveryKey.svelte.d.ts +8 -0
  64. package/dist/components/onboarding/steps/OnboardingStep4FirstBackup.svelte +67 -0
  65. package/dist/components/onboarding/steps/OnboardingStep4FirstBackup.svelte.d.ts +9 -0
  66. package/dist/components/onboarding/steps/OnboardingStepLayout.svelte +58 -0
  67. package/dist/components/onboarding/steps/OnboardingStepLayout.svelte.d.ts +19 -0
  68. package/dist/components/onboarding/upsell/UpsellBackupFeaturesGrid.svelte +52 -0
  69. package/dist/components/onboarding/upsell/UpsellBackupFeaturesGrid.svelte.d.ts +18 -0
  70. package/dist/components/onboarding/upsell/UpsellExplainFutoBackups.svelte +33 -0
  71. package/dist/components/onboarding/upsell/UpsellExplainFutoBackups.svelte.d.ts +8 -0
  72. package/dist/components/onboarding/upsell/UpsellFrequentlyAskedQuestions.svelte +27 -0
  73. package/dist/components/onboarding/upsell/UpsellFrequentlyAskedQuestions.svelte.d.ts +10 -0
  74. package/dist/components/onboarding/upsell/UpsellFutoBackupsBadge.svelte +7 -0
  75. package/dist/components/onboarding/upsell/UpsellFutoBackupsBadge.svelte.d.ts +18 -0
  76. package/dist/components/onboarding/upsell/UpsellHowItWorks.svelte +46 -0
  77. package/dist/components/onboarding/upsell/UpsellHowItWorks.svelte.d.ts +6 -0
  78. package/dist/components/onboarding/upsell/UpsellModal.svelte +43 -0
  79. package/dist/components/onboarding/upsell/UpsellModal.svelte.d.ts +9 -0
  80. package/dist/components/onboarding/upsell/UpsellPage.svelte +39 -0
  81. package/dist/components/onboarding/upsell/UpsellPage.svelte.d.ts +13 -0
  82. package/dist/components/onboarding/upsell/UpsellProtectYourImmichLibrary.svelte +58 -0
  83. package/dist/components/onboarding/upsell/UpsellProtectYourImmichLibrary.svelte.d.ts +8 -0
  84. package/dist/components/schedules/RepositoryPicker.svelte +45 -38
  85. package/dist/components/schedules/ScheduleItem.svelte +32 -26
  86. package/dist/components/schedules/ScheduleList.svelte +16 -10
  87. package/dist/components/settings/GlobalSettings.svelte +8 -0
  88. package/dist/components/settings/GlobalSettings.svelte.d.ts +18 -0
  89. package/dist/components/settings/SettingsBackends.svelte +16 -0
  90. package/dist/components/settings/SettingsBackends.svelte.d.ts +18 -0
  91. package/dist/components/settings/SettingsBilling.svelte +69 -0
  92. package/dist/components/settings/SettingsBilling.svelte.d.ts +6 -0
  93. package/dist/components/settings/SettingsNotifications.svelte +47 -0
  94. package/dist/components/settings/SettingsNotifications.svelte.d.ts +6 -0
  95. package/dist/components/test/ImmichTestUi.svelte +129 -27
  96. package/dist/components/test/TestUi.svelte +4 -4
  97. package/dist/components/test/dashboard/ActiveJobs.svelte +2 -2
  98. package/dist/components/test/immich/MockImmichPhotos.svelte +36 -0
  99. package/dist/components/test/immich/MockImmichPhotos.svelte.d.ts +18 -0
  100. package/dist/components/test/immich/MockImmichPurchaseInfo.svelte +29 -0
  101. package/dist/components/test/immich/MockImmichPurchaseInfo.svelte.d.ts +18 -0
  102. package/dist/components/test/immich/MockImmichStorageSpace.svelte +37 -0
  103. package/dist/components/test/immich/MockImmichStorageSpace.svelte.d.ts +6 -0
  104. package/dist/components/ui/Accordion.svelte +67 -0
  105. package/dist/components/ui/Accordion.svelte.d.ts +11 -0
  106. package/dist/components/ui/ListToolbar.svelte +60 -0
  107. package/dist/components/ui/ListToolbar.svelte.d.ts +16 -0
  108. package/dist/components/ui/PageLayout.svelte +21 -9
  109. package/dist/components/ui/PageLayout.svelte.d.ts +1 -0
  110. package/dist/components/ui/Pagination.svelte +80 -0
  111. package/dist/components/ui/Pagination.svelte.d.ts +8 -0
  112. package/dist/components/ui/PathListField.svelte +17 -14
  113. package/dist/components/ui/StackList.svelte +18 -18
  114. package/dist/components/ui/StackList.svelte.d.ts +6 -27
  115. package/dist/components/ui/StackListItem.svelte +47 -25
  116. package/dist/components/ui/StackListItem.svelte.d.ts +5 -2
  117. package/dist/components/ui/StackListOption.svelte +33 -0
  118. package/dist/components/ui/StackListOption.svelte.d.ts +11 -0
  119. package/dist/components/ui/StackListPlaceholder.svelte +12 -0
  120. package/dist/components/ui/StackListPlaceholder.svelte.d.ts +7 -0
  121. package/dist/components/ui/StepNumber.svelte +20 -0
  122. package/dist/components/ui/StepNumber.svelte.d.ts +7 -0
  123. package/dist/components/util/Suspense.svelte +7 -3
  124. package/dist/components/util/YuccaContext.svelte +1 -1
  125. package/dist/events.js +1 -1
  126. package/dist/fetch-client.d.ts +2 -0
  127. package/dist/index.d.ts +6 -1
  128. package/dist/index.js +6 -1
  129. package/dist/services/backend.service.js +3 -0
  130. package/dist/services/immich.integration.service.d.ts +1 -1
  131. package/dist/services/immich.integration.service.js +2 -3
  132. package/dist/services/integrations.service.d.ts +1 -0
  133. package/dist/services/integrations.service.js +2 -1
  134. package/dist/services/log.service.svelte.d.ts +12 -0
  135. package/dist/services/log.service.svelte.js +15 -1
  136. package/dist/services/repository.service.js +9 -3
  137. package/dist/services/runHistory.service.js +2 -2
  138. package/dist/utils/format.d.ts +1 -1
  139. package/dist/utils/format.js +2 -2
  140. package/package.json +3 -3
  141. package/dist/components/backups/dialogs/ViewLogModal.svelte +0 -208
  142. package/dist/components/backups/dialogs/ViewLogModal.svelte.d.ts +0 -7
  143. package/dist/components/integrations/immich/ImmichConfigureBackup.svelte +0 -403
  144. package/dist/components/integrations/immich/ImmichConfigureBackup.svelte.d.ts +0 -9
  145. package/dist/components/integrations/immich/ImmichConfirmDefaultBackup.svelte +0 -80
  146. package/dist/components/integrations/immich/ImmichConfirmDefaultBackup.svelte.d.ts +0 -8
  147. package/dist/components/onboarding/stages/OnboardingStageKeyConfirm.svelte +0 -56
  148. package/dist/components/onboarding/stages/OnboardingStageKeyConfirm.svelte.d.ts +0 -9
  149. package/dist/components/onboarding/stages/OnboardingStageKeyIntro.svelte +0 -50
  150. package/dist/components/onboarding/stages/OnboardingStageKeyIntro.svelte.d.ts +0 -7
  151. package/dist/components/onboarding/stages/OnboardingStageKeySave.svelte +0 -44
  152. package/dist/components/onboarding/stages/OnboardingStageKeySave.svelte.d.ts +0 -8
  153. package/dist/components/onboarding/stages/OnboardingStageWelcome.svelte +0 -56
  154. package/dist/components/onboarding/stages/OnboardingStageWelcome.svelte.d.ts +0 -9
  155. package/dist/components/util/TimedButton.svelte +0 -37
  156. package/dist/components/util/TimedButton.svelte.d.ts +0 -7
@@ -16,7 +16,7 @@
16
16
  Text,
17
17
  } from "@immich/ui";
18
18
  import { SvelteSet } from "svelte/reactivity";
19
- import ViewLogModal from "./ViewLogModal.svelte";
19
+ import ViewStatusModal from "./ViewStatusModal.svelte";
20
20
 
21
21
  type Props = {
22
22
  repository: string;
@@ -47,7 +47,7 @@
47
47
  onSuccess: ({ logId }) => {
48
48
  onClose();
49
49
 
50
- modalManager.open(ViewLogModal, {
50
+ modalManager.open(ViewStatusModal, {
51
51
  logId,
52
52
  });
53
53
  },
@@ -1,18 +1,17 @@
1
1
  <script lang="ts">
2
2
  import StackList from "../../ui/StackList.svelte";
3
- import StackListItem from "../../ui/StackListItem.svelte";
3
+ import StackListOption from "../../ui/StackListOption.svelte";
4
+ import StackListPlaceholder from "../../ui/StackListPlaceholder.svelte";
5
+ import Suspense from "../../util/Suspense.svelte";
4
6
  import { useInspectRepositories } from "../../../services/repository.service";
5
7
  import {
6
8
  Button,
7
9
  HStack,
8
- Icon,
9
10
  Modal,
10
11
  ModalBody,
11
12
  ModalFooter,
12
13
  Stack,
13
- Text,
14
14
  } from "@immich/ui";
15
- import { mdiChevronRight } from "@mdi/js";
16
15
  import type { Snippet } from "svelte";
17
16
 
18
17
  type Props = {
@@ -54,41 +53,32 @@
54
53
  <Stack>
55
54
  {@render leadingContent?.()}
56
55
 
57
- <StackList {query}>
58
- {#each sortedRepositories ?? [] as repository (repository.id)}
59
- {@const accessible = repository.snapshots !== undefined}
56
+ <StackList>
57
+ <Suspense {query}>
58
+ {#each sortedRepositories ?? [] as repository (repository.id)}
59
+ {@const accessible = repository.snapshots !== undefined}
60
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} />
61
+ <StackListOption
62
+ title={repository.name}
63
+ disabled={!accessible}
64
+ onclick={() => onSelect(repository.id)}
65
+ >
66
+ {#if !accessible}
67
+ Can't access, is your recovery key correct?
68
+ {:else if repository.snapshots.length}
69
+ Last backup: {new Date(
70
+ repository.snapshots[0].time,
71
+ ).toLocaleDateString()}
72
+ {:else}
73
+ No backups yet
82
74
  {/if}
83
- {/snippet}
84
- </StackListItem>
85
- {/each}
75
+ </StackListOption>
76
+ {/each}
86
77
 
87
- {#if (sortedRepositories ?? []).length === 0}
88
- <StackListItem>
89
- <Text color="muted">No backups found.</Text>
90
- </StackListItem>
91
- {/if}
78
+ {#if (sortedRepositories ?? []).length === 0}
79
+ <StackListPlaceholder>No backups found.</StackListPlaceholder>
80
+ {/if}
81
+ </Suspense>
92
82
  </StackList>
93
83
  </Stack>
94
84
  </ModalBody>
@@ -0,0 +1,175 @@
1
+ <script lang="ts">
2
+ import BackupStatus, {
3
+ type BackupStatusState,
4
+ type BackupStatusType,
5
+ } from "../BackupStatus.svelte";
6
+ import OnEvents from "../../util/OnEvents.svelte";
7
+ import { options } from "../../../options";
8
+ import { createLogObserver } from "../../../services/log.service.svelte";
9
+ import { useRun, useRunEventHandler } from "../../../services/runHistory.service";
10
+ import { formatDuration } from "../../../utils/format";
11
+ import {
12
+ CloseButton,
13
+ FormatBytes,
14
+ Heading,
15
+ HStack,
16
+ Modal,
17
+ ModalBody,
18
+ ModalHeader,
19
+ Scrollable,
20
+ Stack,
21
+ Text,
22
+ } from "@immich/ui";
23
+ import { DateTime } from "luxon";
24
+ import { onDestroy } from "svelte";
25
+
26
+ type Props = {
27
+ logId: string;
28
+ onClose: () => void;
29
+ onRetry?: () => void;
30
+ };
31
+
32
+ const { logId, onClose, onRetry }: Props = $props();
33
+
34
+ const advanced = options.advanced;
35
+ const showAdvanced = $derived($advanced);
36
+ // svelte-ignore state_referenced_locally
37
+ const log = createLogObserver(logId);
38
+ // svelte-ignore state_referenced_locally
39
+ const runQuery = useRun(logId);
40
+ const run = $derived(runQuery.data);
41
+ const { onRunUpdate } = useRunEventHandler();
42
+
43
+ let now = $state(DateTime.now());
44
+ const tick = setInterval(() => (now = DateTime.now()), 1000);
45
+
46
+ onDestroy(() => {
47
+ clearInterval(tick);
48
+ log.destroy();
49
+ });
50
+
51
+ const type: BackupStatusType = $derived(
52
+ run?.type === "restore" || run?.type === "forget" ? run.type : "backup",
53
+ );
54
+
55
+ const backupState: BackupStatusState = $derived.by(() => {
56
+ if (!run) {
57
+ return "connecting";
58
+ }
59
+
60
+ if (run.status === "incomplete") {
61
+ return "running";
62
+ }
63
+
64
+ return run.status === "failed" ? "failed" : "complete";
65
+ });
66
+
67
+ const duration = $derived(
68
+ run
69
+ ? formatDuration(
70
+ (run.end ? DateTime.fromISO(run.end) : now).toMillis() -
71
+ DateTime.fromISO(run.start).toMillis(),
72
+ "long",
73
+ )
74
+ : "",
75
+ );
76
+
77
+ const titles: Record<BackupStatusType, Record<"running" | "done", string>> = {
78
+ backup: { running: "Backing up your library", done: "Backup" },
79
+ restore: { running: "Restoring your library", done: "Restore" },
80
+ forget: { running: "Pruning old backups", done: "Prune" },
81
+ };
82
+
83
+ const retry = $derived(
84
+ onRetry
85
+ ? () => {
86
+ onClose();
87
+ onRetry();
88
+ }
89
+ : undefined,
90
+ );
91
+
92
+ const title = $derived.by(() => {
93
+ switch (backupState) {
94
+ case "complete": {
95
+ return `${titles[type].done} complete`;
96
+ }
97
+ case "failed": {
98
+ return `${titles[type].done} failed`;
99
+ }
100
+ default: {
101
+ return titles[type].running;
102
+ }
103
+ }
104
+ });
105
+ </script>
106
+
107
+ <OnEvents {onRunUpdate} />
108
+
109
+ <Modal
110
+ size="medium"
111
+ {onClose}
112
+ // this is a bit of a hack to remove the frames from base modal...
113
+ // this one might be removed ; twin code is in UpsellModal
114
+ class="[&>div>div:first-child]:border-b-0 [&>div>div:first-child]:px-8 [&>div>div:first-child]:pb-0"
115
+ >
116
+ <ModalHeader>
117
+ <HStack fullWidth class="justify-end">
118
+ <CloseButton onclick={onClose} />
119
+ </HStack>
120
+ </ModalHeader>
121
+
122
+ <ModalBody class="px-8 pt-2 pb-8">
123
+ <BackupStatus
124
+ {title}
125
+ {type}
126
+ state={backupState}
127
+ progress={log.status.progress}
128
+ start={run?.start}
129
+ {duration}
130
+ errors={log.errors}
131
+ currentFiles={log.status.currentFiles}
132
+ onRetry={retry}
133
+ >
134
+ {#snippet details()}
135
+ {#if log.summary && type === "backup"}
136
+ {(log.summary.total_files_processed ?? 0).toLocaleString()} items backed
137
+ up &middot;
138
+ {(log.summary.files_new ?? 0).toLocaleString()} new items
139
+ {#if log.summary.total_bytes_processed !== undefined}
140
+ &middot;
141
+ <FormatBytes bytes={log.summary.total_bytes_processed} /> processed
142
+ {/if}
143
+ {:else if log.summary && type === "restore"}
144
+ {(log.summary.files_restored ?? 0).toLocaleString()} items restored{#if log.summary.files_skipped}
145
+ &middot; {log.summary.files_skipped.toLocaleString()} skipped
146
+ {/if}
147
+ {#if log.summary.bytes_restored !== undefined}
148
+ &middot; <FormatBytes bytes={log.summary.bytes_restored} /> restored
149
+ {/if}
150
+ {:else if type === "forget"}
151
+ {log.pruned.removed.toLocaleString()}
152
+ {log.pruned.removed === 1 ? "backup" : "backups"} removed &middot;
153
+ {log.pruned.kept.toLocaleString()} kept
154
+ {/if}
155
+ {/snippet}
156
+
157
+ {#snippet advanced()}
158
+ {#if showAdvanced}
159
+ <Stack gap={1}>
160
+ <Heading size="small">Event Log</Heading>
161
+ <Scrollable class="h-80 overflow-x-hidden">
162
+ <Stack gap={1}>
163
+ {#each log.events as event, index (index)}
164
+ <Text size="tiny" class="font-mono select-all">
165
+ {JSON.stringify(event)}
166
+ </Text>
167
+ {/each}
168
+ </Stack>
169
+ </Scrollable>
170
+ </Stack>
171
+ {/if}
172
+ {/snippet}
173
+ </BackupStatus>
174
+ </ModalBody>
175
+ </Modal>
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ logId: string;
3
+ onClose: () => void;
4
+ onRetry?: () => void;
5
+ };
6
+ declare const ViewStatusModal: import("svelte").Component<Props, {}, "">;
7
+ type ViewStatusModal = ReturnType<typeof ViewStatusModal>;
8
+ export default ViewStatusModal;
@@ -1,19 +1,23 @@
1
1
  <script lang="ts">
2
2
  import StackList from "../../ui/StackList.svelte";
3
+ import StackListPlaceholder from "../../ui/StackListPlaceholder.svelte";
4
+ import Suspense from "../../util/Suspense.svelte";
3
5
  import OnEvents from "../../util/OnEvents.svelte";
4
6
  import type { LocalRepositoryDto } from "../../../fetch-client";
5
7
  import {
6
8
  useRunEventHandler,
7
9
  useRunHistory,
8
10
  } from "../../../services/runHistory.service";
9
- import { Text } from "@immich/ui";
11
+ import { Button } from "@immich/ui";
10
12
  import RepositoryRunHistoryItem from "./RepositoryRunHistoryItem.svelte";
11
13
 
12
14
  type Props = {
13
15
  repository: LocalRepositoryDto;
16
+ limit?: number;
17
+ onViewAll?: () => void;
14
18
  };
15
19
 
16
- let { repository }: Props = $props();
20
+ let { repository, limit = 5, onViewAll }: Props = $props();
17
21
 
18
22
  // svelte-ignore state_referenced_locally
19
23
  const query = useRunHistory(repository.id);
@@ -22,18 +26,26 @@
22
26
 
23
27
  <OnEvents {onRunCreate} {onRunUpdate} />
24
28
 
25
- <StackList {query}>
29
+ <StackList>
26
30
  {#snippet title()}
27
31
  Recent backup attempts
28
32
  {/snippet}
29
33
 
30
- {#snippet children(runs)}
31
- {#if runs.length === 0}
32
- <Text class="text-center py-6" color="muted">No recent backups</Text>
33
- {:else}
34
- {#each runs.slice(0, 5) as run (run.id)}
35
- <RepositoryRunHistoryItem {run} />
36
- {/each}
34
+ {#snippet action()}
35
+ {#if onViewAll && query.data?.length}
36
+ <Button variant="ghost" size="small" onclick={onViewAll}>View all</Button>
37
37
  {/if}
38
38
  {/snippet}
39
+
40
+ <Suspense {query}>
41
+ {#snippet children(runs)}
42
+ {#each runs.slice(0, limit) as run (run.id)}
43
+ <RepositoryRunHistoryItem {run} />
44
+ {/each}
45
+
46
+ {#if runs.length === 0}
47
+ <StackListPlaceholder>No recent backups</StackListPlaceholder>
48
+ {/if}
49
+ {/snippet}
50
+ </Suspense>
39
51
  </StackList>
@@ -1,6 +1,8 @@
1
1
  import type { LocalRepositoryDto } from "../../../fetch-client";
2
2
  type Props = {
3
3
  repository: LocalRepositoryDto;
4
+ limit?: number;
5
+ onViewAll?: () => void;
4
6
  };
5
7
  declare const RepositoryRunHistory: import("svelte").Component<Props, {}, "">;
6
8
  type RepositoryRunHistory = ReturnType<typeof RepositoryRunHistory>;
@@ -5,9 +5,9 @@
5
5
  import { getRunActions } from "../../../services/runHistory.service";
6
6
  import { Icon } from "@immich/ui";
7
7
  import {
8
- mdiAlertCircleOutline,
9
- mdiCheckCircleOutline,
10
- mdiLoading,
8
+ mdiCloudCheckOutline,
9
+ mdiCloudOffOutline,
10
+ mdiCloudSyncOutline,
11
11
  } from "@mdi/js";
12
12
 
13
13
  type Props = {
@@ -16,30 +16,56 @@
16
16
 
17
17
  const { run }: Props = $props();
18
18
  const { ViewLog } = $derived(getRunActions(run));
19
+
20
+ const nouns = {
21
+ restore: { name: "restore", running: "Restore", done: "Restored" },
22
+ forget: { name: "prune", running: "Prune", done: "Pruned" },
23
+ backup: { name: "backup", running: "Backup", done: "Backed up" },
24
+ };
25
+
26
+ const noun = $derived(
27
+ run.type === "restore" || run.type === "forget"
28
+ ? nouns[run.type]
29
+ : nouns.backup,
30
+ );
31
+
32
+ const status = $derived.by(() => {
33
+ switch (run.status) {
34
+ case "failed": {
35
+ return {
36
+ title: `Failed ${noun.name}`,
37
+ color: "danger",
38
+ icon: mdiCloudOffOutline,
39
+ } as const;
40
+ }
41
+ case "incomplete": {
42
+ return {
43
+ title: `${noun.running} in progress`,
44
+ color: "primary",
45
+ icon: mdiCloudSyncOutline,
46
+ } as const;
47
+ }
48
+ default: {
49
+ return {
50
+ title: `Successful ${noun.name}`,
51
+ color: "success",
52
+ icon: mdiCloudCheckOutline,
53
+ } as const;
54
+ }
55
+ }
56
+ });
19
57
  </script>
20
58
 
21
- <StackListItem actions={[ViewLog]}>
59
+ <StackListItem title={status.title} color={status.color} actions={[ViewLog]}>
22
60
  {#snippet icon()}
23
- {#if run.status === "complete"}
24
- <Icon icon={mdiCheckCircleOutline} class="text-success-500" />
25
- {:else if run.status === "failed"}
26
- <Icon icon={mdiAlertCircleOutline} class="text-danger-500" />
27
- {:else}
28
- <Icon icon={mdiLoading} class="animate-spin opacity-60" />
29
- {/if}
61
+ <Icon icon={status.icon} />
30
62
  {/snippet}
31
63
 
32
64
  {#if run.status === "incomplete"}
33
- {#if run.type === "restore"}Restoring{:else if run.type === "forget"}Pruning{:else}Backing
34
- up{/if} &middot; started
35
- <RelativeTime time={run.start} />
65
+ Started <RelativeTime time={run.start} />
36
66
  {:else if run.status === "failed"}
37
- {#if run.type === "restore"}Restore{:else if run.type === "forget"}Prune{:else}Backup{/if}
38
- failed
39
- {#if run.end}<RelativeTime time={run.end} />{/if}
67
+ Attempted {#if run.end}<RelativeTime time={run.end} />{/if}
40
68
  {:else}
41
- {#if run.type === "restore"}Restored{:else if run.type === "forget"}Pruned{:else}Backed
42
- up{/if}
43
- {#if run.end}<RelativeTime time={run.end} />{/if}
69
+ {noun.done} {#if run.end}<RelativeTime time={run.end} />{/if}
44
70
  {/if}
45
71
  </StackListItem>
@@ -0,0 +1,135 @@
1
+ <script lang="ts">
2
+ import ListToolbar from "../../ui/ListToolbar.svelte";
3
+ import Pagination from "../../ui/Pagination.svelte";
4
+ import StackList from "../../ui/StackList.svelte";
5
+ import StackListPlaceholder from "../../ui/StackListPlaceholder.svelte";
6
+ import OnEvents from "../../util/OnEvents.svelte";
7
+ import Suspense from "../../util/Suspense.svelte";
8
+ import type { LocalRepositoryDto, RunDto } from "../../../fetch-client";
9
+ import {
10
+ useRunEventHandler,
11
+ useRunHistory,
12
+ } from "../../../services/runHistory.service";
13
+ import { Stack, type ActionItem } from "@immich/ui";
14
+ import { mdiRadioboxBlank, mdiRadioboxMarked } from "@mdi/js";
15
+ import RepositoryRunHistoryItem from "./RepositoryRunHistoryItem.svelte";
16
+
17
+ type Props = {
18
+ repository: LocalRepositoryDto;
19
+ pageSize?: number;
20
+ };
21
+
22
+ let { repository, pageSize = 10 }: Props = $props();
23
+
24
+ // svelte-ignore state_referenced_locally
25
+ const query = useRunHistory(repository.id);
26
+ const { onRunCreate, onRunUpdate } = useRunEventHandler();
27
+
28
+ let search = $state("");
29
+ let newestFirst = $state(true);
30
+ let status = $state<"all" | "complete" | "failed" | "incomplete">("all");
31
+ let page = $state(1);
32
+
33
+ const onSearch = (value: string) => {
34
+ search = value;
35
+ page = 1;
36
+ };
37
+
38
+ const onFilter = (value: typeof status) => {
39
+ status = value;
40
+ page = 1;
41
+ };
42
+
43
+ const sortOptions = [
44
+ { newest: true, title: "Newest first" },
45
+ { newest: false, title: "Oldest first" },
46
+ ];
47
+
48
+ const statusOptions: { value: typeof status; title: string }[] = [
49
+ { value: "all", title: "All attempts" },
50
+ { value: "complete", title: "Successful only" },
51
+ { value: "failed", title: "Failed only" },
52
+ { value: "incomplete", title: "In progress only" },
53
+ ];
54
+
55
+ const tick = (selected: boolean) =>
56
+ selected ? mdiRadioboxMarked : mdiRadioboxBlank;
57
+
58
+ const sort = $derived({
59
+ label: newestFirst ? "Newest first" : "Oldest first",
60
+ items: sortOptions.map(
61
+ (option): ActionItem => ({
62
+ title: option.title,
63
+ icon: tick(newestFirst === option.newest),
64
+ onAction: () => (newestFirst = option.newest),
65
+ }),
66
+ ),
67
+ });
68
+
69
+ const filters = $derived({
70
+ label:
71
+ statusOptions.find((option) => option.value === status)?.title ??
72
+ "Filters",
73
+ active: status !== "all",
74
+ items: statusOptions.map(
75
+ (option): ActionItem => ({
76
+ title: option.title,
77
+ icon: tick(status === option.value),
78
+ onAction: () => onFilter(option.value),
79
+ }),
80
+ ),
81
+ });
82
+
83
+ const matches = (run: RunDto) => {
84
+ const term = search.trim().toLowerCase();
85
+
86
+ if (!term) {
87
+ return true;
88
+ }
89
+
90
+ return `${run.type} ${run.status}`.toLowerCase().includes(term);
91
+ };
92
+
93
+ const filtered = $derived(
94
+ (query.data ?? [])
95
+ .filter((run) => status === "all" || run.status === status)
96
+ .filter(matches)
97
+ .toSorted((a, b) =>
98
+ newestFirst
99
+ ? +new Date(b.start) - +new Date(a.start)
100
+ : +new Date(a.start) - +new Date(b.start),
101
+ ),
102
+ );
103
+
104
+ const pageCount = $derived(Math.max(Math.ceil(filtered.length / pageSize), 1));
105
+ const current = $derived(Math.min(page, pageCount));
106
+ const visible = $derived(
107
+ filtered.slice((current - 1) * pageSize, current * pageSize),
108
+ );
109
+ </script>
110
+
111
+ <OnEvents {onRunCreate} {onRunUpdate} />
112
+
113
+ <Stack gap={4}>
114
+ <ListToolbar
115
+ placeholder="Search backups"
116
+ {search}
117
+ {onSearch}
118
+ {sort}
119
+ {filters}
120
+ />
121
+
122
+ <StackList>
123
+ <Suspense {query}>
124
+ {#each visible as run (run.id)}
125
+ <RepositoryRunHistoryItem {run} />
126
+ {/each}
127
+
128
+ {#if filtered.length === 0}
129
+ <StackListPlaceholder>No backups found</StackListPlaceholder>
130
+ {/if}
131
+ </Suspense>
132
+ </StackList>
133
+
134
+ <Pagination page={current} {pageCount} onChange={(next) => (page = next)} />
135
+ </Stack>
@@ -0,0 +1,8 @@
1
+ import type { LocalRepositoryDto } from "../../../fetch-client";
2
+ type Props = {
3
+ repository: LocalRepositoryDto;
4
+ pageSize?: number;
5
+ };
6
+ declare const RepositoryRunHistoryPage: import("svelte").Component<Props, {}, "">;
7
+ type RepositoryRunHistoryPage = ReturnType<typeof RepositoryRunHistoryPage>;
8
+ export default RepositoryRunHistoryPage;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { LocalRepositoryDto } from "../../../fetch-client";
3
3
  import { Modal, ModalBody } from "@immich/ui";
4
- import RepositoryRunHistory from "./RepositoryRunHistory.svelte";
4
+ import RepositoryRunHistoryPage from "./RepositoryRunHistoryPage.svelte";
5
5
 
6
6
  type Props = {
7
7
  repository: LocalRepositoryDto;
@@ -13,6 +13,6 @@
13
13
 
14
14
  <Modal title={`Run History for ${repository.name}`} size="giant" {onClose}>
15
15
  <ModalBody>
16
- <RepositoryRunHistory {repository} />
16
+ <RepositoryRunHistoryPage {repository} />
17
17
  </ModalBody>
18
18
  </Modal>
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import StackList from "../../ui/StackList.svelte";
3
+ import StackListPlaceholder from "../../ui/StackListPlaceholder.svelte";
3
4
  import OnEvents from "../../util/OnEvents.svelte";
5
+ import Suspense from "../../util/Suspense.svelte";
4
6
  import type { LocalRepositoryDto } from "../../../fetch-client";
5
7
  import { options } from "../../../options";
6
8
  import { handlePruneRepository } from "../../../services/repository.service";
@@ -8,15 +10,17 @@
8
10
  useSnapshotEventHandler,
9
11
  useSnapshots,
10
12
  } from "../../../services/snapshot.service";
11
- import { Button, HStack, Text } from "@immich/ui";
13
+ import { Button, HStack } from "@immich/ui";
12
14
  import RepositorySnapshotsListItem from "./RepositorySnapshotsListItem.svelte";
13
15
 
14
16
  type Props = {
15
17
  repository: LocalRepositoryDto;
16
18
  immich?: boolean;
19
+ limit?: number;
20
+ onViewAll?: () => void;
17
21
  };
18
22
 
19
- let { repository, immich = false }: Props = $props();
23
+ let { repository, immich = false, limit, onViewAll }: Props = $props();
20
24
 
21
25
  const { advanced } = options;
22
26
  // svelte-ignore state_referenced_locally
@@ -26,24 +30,32 @@
26
30
 
27
31
  <OnEvents {onRunUpdate} />
28
32
 
29
- <StackList {query}>
33
+ <StackList>
30
34
  {#snippet title()}
31
35
  Snapshots
32
36
  {/snippet}
33
37
 
34
- {#snippet children(snapshots)}
35
- {#if snapshots.length === 0}
36
- <Text class="text-center py-6" color="muted">No backups yet</Text>
37
- {:else}
38
- {#each snapshots as snapshot (snapshot.id)}
38
+ {#snippet action()}
39
+ {#if onViewAll && query.data?.length}
40
+ <Button variant="ghost" size="small" onclick={onViewAll}>View all</Button>
41
+ {/if}
42
+ {/snippet}
43
+
44
+ <Suspense {query}>
45
+ {#snippet children(snapshots)}
46
+ {#each limit ? snapshots.slice(0, limit) : snapshots as snapshot (snapshot.id)}
39
47
  <RepositorySnapshotsListItem
40
48
  repositoryId={repository.id}
41
49
  {snapshot}
42
50
  {immich}
43
51
  />
44
52
  {/each}
45
- {/if}
46
- {/snippet}
53
+
54
+ {#if snapshots.length === 0}
55
+ <StackListPlaceholder>No backups yet</StackListPlaceholder>
56
+ {/if}
57
+ {/snippet}
58
+ </Suspense>
47
59
  </StackList>
48
60
 
49
61
  {#if $advanced}