BackupModal.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. import { useEffect, useState } from 'react';
  2. import { Download, X, Settings, Bell, FileText, Plug, Printer, Palette, Wrench, Archive, Loader2, Key, AlertTriangle, Link, FolderKanban, Upload } from 'lucide-react';
  3. import { useTranslation } from 'react-i18next';
  4. import { Card, CardContent } from './Card';
  5. import { Button } from './Button';
  6. import { Toggle } from './Toggle';
  7. interface BackupCategory {
  8. id: string;
  9. labelKey: string;
  10. defaultLabel: string;
  11. icon: React.ReactNode;
  12. default: boolean;
  13. description: string;
  14. }
  15. const BACKUP_CATEGORIES: BackupCategory[] = [
  16. {
  17. id: 'settings',
  18. labelKey: 'backup.categories.settings',
  19. defaultLabel: 'App Settings',
  20. icon: <Settings className="w-4 h-4" />,
  21. default: true,
  22. description: 'Language, theme, update preferences',
  23. },
  24. {
  25. id: 'notifications',
  26. labelKey: 'backup.categories.notifications',
  27. defaultLabel: 'Notification Providers',
  28. icon: <Bell className="w-4 h-4" />,
  29. default: true,
  30. description: 'ntfy, Pushover, Discord, etc.',
  31. },
  32. {
  33. id: 'templates',
  34. labelKey: 'backup.categories.templates',
  35. defaultLabel: 'Notification Templates',
  36. icon: <FileText className="w-4 h-4" />,
  37. default: true,
  38. description: 'Custom message templates',
  39. },
  40. {
  41. id: 'smart_plugs',
  42. labelKey: 'backup.categories.smartPlugs',
  43. defaultLabel: 'Smart Plugs',
  44. icon: <Plug className="w-4 h-4" />,
  45. default: true,
  46. description: 'Tasmota plug configurations',
  47. },
  48. {
  49. id: 'external_links',
  50. labelKey: 'backup.categories.externalLinks',
  51. defaultLabel: 'External Links',
  52. icon: <Link className="w-4 h-4" />,
  53. default: true,
  54. description: 'Sidebar links to external services',
  55. },
  56. {
  57. id: 'printers',
  58. labelKey: 'backup.categories.printers',
  59. defaultLabel: 'Printers',
  60. icon: <Printer className="w-4 h-4" />,
  61. default: false,
  62. description: 'Printer info (access codes excluded)',
  63. },
  64. {
  65. id: 'filaments',
  66. labelKey: 'backup.categories.filaments',
  67. defaultLabel: 'Filament Inventory',
  68. icon: <Palette className="w-4 h-4" />,
  69. default: false,
  70. description: 'Filament types and costs',
  71. },
  72. {
  73. id: 'maintenance',
  74. labelKey: 'backup.categories.maintenance',
  75. defaultLabel: 'Maintenance Types',
  76. icon: <Wrench className="w-4 h-4" />,
  77. default: false,
  78. description: 'Custom maintenance schedules',
  79. },
  80. {
  81. id: 'archives',
  82. labelKey: 'backup.categories.archives',
  83. defaultLabel: 'Print Archives',
  84. icon: <Archive className="w-4 h-4" />,
  85. default: false,
  86. description: 'All print data + files (3MF, thumbnails, photos)',
  87. },
  88. {
  89. id: 'projects',
  90. labelKey: 'backup.categories.projects',
  91. defaultLabel: 'Projects',
  92. icon: <FolderKanban className="w-4 h-4" />,
  93. default: false,
  94. description: 'Projects, BOM items, and attachments',
  95. },
  96. {
  97. id: 'pending_uploads',
  98. labelKey: 'backup.categories.pendingUploads',
  99. defaultLabel: 'Pending Uploads',
  100. icon: <Upload className="w-4 h-4" />,
  101. default: false,
  102. description: 'Virtual printer uploads awaiting review',
  103. },
  104. {
  105. id: 'api_keys',
  106. labelKey: 'backup.categories.apiKeys',
  107. defaultLabel: 'API Keys',
  108. icon: <Key className="w-4 h-4" />,
  109. default: false,
  110. description: 'Webhook API keys (new keys generated on import)',
  111. },
  112. ];
  113. interface BackupModalProps {
  114. onClose: () => void;
  115. onExport: (categories: Record<string, boolean>) => Promise<void>;
  116. }
  117. export function BackupModal({ onClose, onExport }: BackupModalProps) {
  118. const { t } = useTranslation();
  119. const [selected, setSelected] = useState<Record<string, boolean>>(() => {
  120. const initial: Record<string, boolean> = {};
  121. BACKUP_CATEGORIES.forEach((cat) => {
  122. initial[cat.id] = cat.default;
  123. });
  124. return initial;
  125. });
  126. const [includeAccessCodes, setIncludeAccessCodes] = useState(false);
  127. const [isExporting, setIsExporting] = useState(false);
  128. // Close on Escape key
  129. useEffect(() => {
  130. const handleKeyDown = (e: KeyboardEvent) => {
  131. if (e.key === 'Escape') onClose();
  132. };
  133. window.addEventListener('keydown', handleKeyDown);
  134. return () => window.removeEventListener('keydown', handleKeyDown);
  135. }, [onClose]);
  136. const toggleCategory = (id: string) => {
  137. setSelected((prev) => ({ ...prev, [id]: !prev[id] }));
  138. };
  139. const selectAll = () => {
  140. const all: Record<string, boolean> = {};
  141. BACKUP_CATEGORIES.forEach((cat) => {
  142. all[cat.id] = true;
  143. });
  144. setSelected(all);
  145. };
  146. const selectNone = () => {
  147. const none: Record<string, boolean> = {};
  148. BACKUP_CATEGORIES.forEach((cat) => {
  149. none[cat.id] = false;
  150. });
  151. setSelected(none);
  152. };
  153. const selectedCount = Object.values(selected).filter(Boolean).length;
  154. const handleExport = async () => {
  155. setIsExporting(true);
  156. try {
  157. await onExport({ ...selected, access_codes: includeAccessCodes && selected.printers });
  158. } finally {
  159. setIsExporting(false);
  160. }
  161. };
  162. return (
  163. <div
  164. className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
  165. onClick={isExporting ? undefined : onClose}
  166. >
  167. <Card className="w-full max-w-lg" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
  168. <CardContent className="p-0">
  169. {/* Header */}
  170. <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
  171. <div className="flex items-center gap-3">
  172. <div className="p-2 rounded-full bg-bambu-green/20 text-bambu-green">
  173. <Download className="w-5 h-5" />
  174. </div>
  175. <div>
  176. <h3 className="text-lg font-semibold text-white">
  177. {t('backup.exportTitle', { defaultValue: 'Export Backup' })}
  178. </h3>
  179. <p className="text-sm text-bambu-gray">
  180. {t('backup.selectCategories', { defaultValue: 'Select data to include' })}
  181. </p>
  182. </div>
  183. </div>
  184. <button
  185. onClick={onClose}
  186. className="p-2 hover:bg-bambu-dark-tertiary rounded-lg transition-colors"
  187. >
  188. <X className="w-5 h-5" />
  189. </button>
  190. </div>
  191. {/* Quick actions */}
  192. <div className="flex gap-2 px-4 pt-4">
  193. <button
  194. onClick={selectAll}
  195. disabled={isExporting}
  196. className="text-sm text-bambu-green hover:text-bambu-green/80 disabled:opacity-50 disabled:cursor-not-allowed"
  197. >
  198. {t('common.selectAll', { defaultValue: 'Select All' })}
  199. </button>
  200. <span className="text-bambu-gray">|</span>
  201. <button
  202. onClick={selectNone}
  203. disabled={isExporting}
  204. className="text-sm text-bambu-gray hover:text-white disabled:opacity-50 disabled:cursor-not-allowed"
  205. >
  206. {t('common.selectNone', { defaultValue: 'Select None' })}
  207. </button>
  208. </div>
  209. {/* Categories */}
  210. <div className={`p-4 space-y-2 max-h-[400px] overflow-y-auto ${isExporting ? 'opacity-50 pointer-events-none' : ''}`}>
  211. {BACKUP_CATEGORIES.map((category) => (
  212. <label
  213. key={category.id}
  214. className={`flex items-center gap-3 p-3 rounded-lg cursor-pointer transition-colors ${
  215. selected[category.id]
  216. ? 'bg-bambu-green/10 border border-bambu-green/30'
  217. : 'bg-bambu-dark hover:bg-bambu-dark-tertiary border border-transparent'
  218. }`}
  219. >
  220. <input
  221. type="checkbox"
  222. checked={selected[category.id]}
  223. onChange={() => toggleCategory(category.id)}
  224. disabled={isExporting}
  225. className="w-4 h-4 rounded border-bambu-gray bg-bambu-dark text-bambu-green focus:ring-bambu-green focus:ring-offset-0"
  226. />
  227. <div className={`${selected[category.id] ? 'text-bambu-green' : 'text-bambu-gray'}`}>
  228. {category.icon}
  229. </div>
  230. <div className="flex-1">
  231. <div className="text-white text-sm font-medium">
  232. {t(category.labelKey, { defaultValue: category.defaultLabel })}
  233. </div>
  234. <div className="text-xs text-bambu-gray">{category.description}</div>
  235. </div>
  236. </label>
  237. ))}
  238. </div>
  239. {/* Archive warning */}
  240. {selected.archives && (
  241. <div className="mx-4 mb-2 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
  242. <div className="flex items-start gap-2 text-sm">
  243. <Archive className="w-4 h-4 text-yellow-500 mt-0.5 flex-shrink-0" />
  244. <div className="text-yellow-200 dark:text-yellow-200 text-yellow-700">
  245. <span className="font-medium">ZIP file will be created.</span>
  246. <span className="text-yellow-600 dark:text-yellow-200/70"> Includes all 3MF files, thumbnails, timelapses, and photos. This may take a while and result in a large file.</span>
  247. </div>
  248. </div>
  249. </div>
  250. )}
  251. {/* Access codes option - only shown when printers are selected */}
  252. {selected.printers && (
  253. <div className="mx-4 mb-2 p-3 rounded-lg bg-bambu-dark border border-bambu-dark-tertiary">
  254. <div className="flex items-center justify-between">
  255. <div className="flex items-start gap-2">
  256. <Key className="w-4 h-4 text-orange-500 dark:text-orange-400 mt-0.5 flex-shrink-0" />
  257. <div>
  258. <p className="text-sm font-medium text-white">Include Access Codes</p>
  259. <p className="text-xs text-bambu-gray">For transferring to another machine</p>
  260. </div>
  261. </div>
  262. <Toggle checked={includeAccessCodes} onChange={setIncludeAccessCodes} />
  263. </div>
  264. {includeAccessCodes && (
  265. <div className="mt-2 p-2 rounded bg-orange-500/10 border border-orange-500/30">
  266. <div className="flex items-start gap-2 text-xs">
  267. <AlertTriangle className="w-3 h-3 text-orange-500 dark:text-orange-400 mt-0.5 flex-shrink-0" />
  268. <span className="text-orange-700 dark:text-orange-200">
  269. Access codes will be included in plain text. Keep this backup file secure!
  270. </span>
  271. </div>
  272. </div>
  273. )}
  274. </div>
  275. )}
  276. {/* Footer */}
  277. <div className="flex items-center justify-between p-4 border-t border-bambu-dark-tertiary">
  278. <span className="text-sm text-bambu-gray">
  279. {t('backup.selectedCount', {
  280. count: selectedCount,
  281. defaultValue: `${selectedCount} categories selected`,
  282. })}
  283. </span>
  284. <div className="flex gap-3">
  285. <Button variant="secondary" onClick={onClose} disabled={isExporting}>
  286. {t('common.cancel', { defaultValue: 'Cancel' })}
  287. </Button>
  288. <Button
  289. onClick={handleExport}
  290. disabled={selectedCount === 0 || isExporting}
  291. className="bg-bambu-green hover:bg-bambu-green-dark disabled:opacity-50 disabled:cursor-not-allowed min-w-[100px]"
  292. >
  293. {isExporting ? (
  294. <>
  295. <Loader2 className="w-4 h-4 mr-2 animate-spin" />
  296. {t('backup.exporting', { defaultValue: 'Exporting...' })}
  297. </>
  298. ) : (
  299. <>
  300. <Download className="w-4 h-4 mr-2" />
  301. {t('backup.export', { defaultValue: 'Export' })}
  302. </>
  303. )}
  304. </Button>
  305. </div>
  306. </div>
  307. </CardContent>
  308. </Card>
  309. </div>
  310. );
  311. }