import { create } from "@bufbuild/protobuf"; import { isEqual } from "lodash-es"; import { CloudIcon, DatabaseIcon, FolderIcon, LucideIcon } from "lucide-react"; import React, { useEffect, useMemo, useState } from "react"; import { toast } from "react-hot-toast"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Switch } from "@/components/ui/switch"; import { useInstance } from "@/contexts/InstanceContext"; import { handleError } from "@/lib/error"; import { cn } from "@/lib/utils"; import { InstanceSetting_Key, InstanceSetting_StorageSetting, InstanceSetting_StorageSetting_S3Config, InstanceSetting_StorageSetting_S3ConfigSchema, InstanceSetting_StorageSetting_StorageType, InstanceSetting_StorageSettingSchema, InstanceSettingSchema, } from "@/types/proto/api/v1/instance_service_pb"; import { useTranslate } from "@/utils/i18n"; import SettingGroup from "./SettingGroup"; import SettingRow from "./SettingRow"; import SettingSection from "./SettingSection"; const DEFAULT_FILEPATH_TEMPLATE = "assets/{timestamp}_{uuid}_{filename}"; type StorageTypeOption = { storageType: InstanceSetting_StorageSetting_StorageType; id: string; titleKey: "setting.storage.type-database" | "setting.storage.type-local" | "setting.storage.type-s3"; descriptionKey: "setting.storage.database-description" | "setting.storage.local-description" | "setting.storage.s3-description"; noteKeys: readonly [ "setting.storage.database-note-backup" | "setting.storage.local-note-path" | "setting.storage.s3-note-scale", "setting.storage.database-note-size" | "setting.storage.local-note-backup" | "setting.storage.s3-note-config", ]; icon: LucideIcon; badges?: readonly ("setting.storage.badge-default" | "setting.storage.badge-recommended")[]; }; const storageTypeOptions: StorageTypeOption[] = [ { storageType: InstanceSetting_StorageSetting_StorageType.LOCAL, id: "storage-type-local", titleKey: "setting.storage.type-local", descriptionKey: "setting.storage.local-description", noteKeys: ["setting.storage.local-note-path", "setting.storage.local-note-backup"], icon: FolderIcon, badges: ["setting.storage.badge-default", "setting.storage.badge-recommended"], }, { storageType: InstanceSetting_StorageSetting_StorageType.DATABASE, id: "storage-type-database", titleKey: "setting.storage.type-database", descriptionKey: "setting.storage.database-description", noteKeys: ["setting.storage.database-note-backup", "setting.storage.database-note-size"], icon: DatabaseIcon, }, { storageType: InstanceSetting_StorageSetting_StorageType.S3, id: "storage-type-s3", titleKey: "setting.storage.type-s3", descriptionKey: "setting.storage.s3-description", noteKeys: ["setting.storage.s3-note-scale", "setting.storage.s3-note-config"], icon: CloudIcon, }, ]; const StorageSection = () => { const t = useTranslate(); const { storageSetting: originalSetting, updateSetting, fetchSetting } = useInstance(); const [instanceStorageSetting, setInstanceStorageSetting] = useState(originalSetting); const selectedStorageOption = useMemo( () => storageTypeOptions.find((option) => option.storageType === instanceStorageSetting.storageType) ?? storageTypeOptions[0], [instanceStorageSetting.storageType], ); const SelectedStorageIcon = selectedStorageOption.icon; useEffect(() => { setInstanceStorageSetting(originalSetting); }, [originalSetting]); const allowSaveStorageSetting = useMemo(() => { if (instanceStorageSetting.uploadSizeLimitMb <= 0) { return false; } if (instanceStorageSetting.storageType === InstanceSetting_StorageSetting_StorageType.LOCAL) { if (instanceStorageSetting.filepathTemplate.length === 0) { return false; } } else if (instanceStorageSetting.storageType === InstanceSetting_StorageSetting_StorageType.S3) { const hasExistingS3Config = originalSetting.s3Config !== undefined; if ( !instanceStorageSetting.filepathTemplate || !instanceStorageSetting.s3Config?.accessKeyId || (!hasExistingS3Config && !instanceStorageSetting.s3Config?.accessKeySecret) || !instanceStorageSetting.s3Config?.endpoint || !instanceStorageSetting.s3Config?.region || !instanceStorageSetting.s3Config?.bucket ) { return false; } } return !isEqual(originalSetting, instanceStorageSetting); }, [instanceStorageSetting, originalSetting]); const handleMaxUploadSizeChanged = (event: React.ChangeEvent) => { let num = parseInt(event.target.value); if (Number.isNaN(num)) { num = 0; } setInstanceStorageSetting( create(InstanceSetting_StorageSettingSchema, { ...instanceStorageSetting, uploadSizeLimitMb: BigInt(num), }), ); }; const handleFilepathTemplateChanged = (event: React.ChangeEvent) => { setInstanceStorageSetting( create(InstanceSetting_StorageSettingSchema, { ...instanceStorageSetting, filepathTemplate: event.target.value, }), ); }; const handleS3FieldChange = (field: keyof InstanceSetting_StorageSetting_S3Config, value: string | boolean) => { const existing = instanceStorageSetting.s3Config; setInstanceStorageSetting( create(InstanceSetting_StorageSettingSchema, { storageType: instanceStorageSetting.storageType, filepathTemplate: instanceStorageSetting.filepathTemplate, uploadSizeLimitMb: instanceStorageSetting.uploadSizeLimitMb, s3Config: create(InstanceSetting_StorageSetting_S3ConfigSchema, { accessKeyId: existing?.accessKeyId ?? "", accessKeySecret: existing?.accessKeySecret ?? "", endpoint: existing?.endpoint ?? "", region: existing?.region ?? "", bucket: existing?.bucket ?? "", usePathStyle: existing?.usePathStyle ?? false, [field]: value, }), }), ); }; const handleStorageTypeChanged = (storageType: InstanceSetting_StorageSetting_StorageType) => { setInstanceStorageSetting( create(InstanceSetting_StorageSettingSchema, { ...instanceStorageSetting, storageType, filepathTemplate: instanceStorageSetting.filepathTemplate || DEFAULT_FILEPATH_TEMPLATE, }), ); }; const saveInstanceStorageSetting = async () => { try { await updateSetting( create(InstanceSettingSchema, { name: `instance/settings/${InstanceSetting_Key[InstanceSetting_Key.STORAGE]}`, value: { case: "storageSetting", value: instanceStorageSetting, }, }), ); await fetchSetting(InstanceSetting_Key.STORAGE); toast.success(t("message.update-succeed")); } catch (error: unknown) { handleError(error, toast.error, { context: "Update storage settings", }); } }; return ( { handleStorageTypeChanged(Number(value) as InstanceSetting_StorageSetting_StorageType); }} className="overflow-hidden rounded-lg border border-border bg-background divide-y divide-border" > {storageTypeOptions.map((option) => { const Icon = option.icon; const selected = instanceStorageSetting.storageType === option.storageType; return (
{selected &&
}
); })}
{t("setting.storage.selected-backend")} {t(selectedStorageOption.titleKey)}
    {selectedStorageOption.noteKeys.map((note) => (
  • {t(note)}
  • ))}
{instanceStorageSetting.storageType !== InstanceSetting_StorageSetting_StorageType.DATABASE && ( )} {instanceStorageSetting.storageType === InstanceSetting_StorageSetting_StorageType.S3 && ( handleS3FieldChange("accessKeyId", e.target.value)} /> handleS3FieldChange("accessKeySecret", e.target.value)} /> handleS3FieldChange("endpoint", e.target.value)} /> handleS3FieldChange("region", e.target.value)} /> handleS3FieldChange("bucket", e.target.value)} /> handleS3FieldChange("usePathStyle", checked)} /> )}
); }; export default StorageSection;