From 408675023a9533fdbc852d196d9b7fcbafa6f277 Mon Sep 17 00:00:00 2001 From: Rostislav Dugin Date: Sun, 16 Nov 2025 11:22:03 +0300 Subject: [PATCH] FEATURE (s3): Add support of virtual-styled-domains and S3 prefix --- assets/logo.svg | 59 ++++++---- .../features/storages/models/s3/model.go | 52 +++++++-- ...5135_add_virtual_host_and_prefix_to_s3.sql | 17 +++ .../src/entity/storages/models/S3Storage.ts | 2 + .../databases/ui/CreateDatabaseComponent.tsx | 4 +- .../databases/ui/DatabasesComponent.tsx | 39 +++++-- .../ui/edit/EditNotifierComponent.tsx | 30 ++++- .../EditDiscordNotifierComponent.tsx | 6 +- .../notifiers/EditEmailNotifierComponent.tsx | 16 +-- .../notifiers/EditSlackNotifierComponent.tsx | 8 +- .../notifiers/EditTeamsNotifierComponent.tsx | 6 +- .../EditTelegramNotifierComponent.tsx | 12 +- .../EditWebhookNotifierComponent.tsx | 8 +- .../storages/ui/edit/EditStorageComponent.tsx | 15 ++- .../EditGoogleDriveStorageComponent.tsx | 8 +- .../edit/storages/EditNASStorageComponent.tsx | 20 ++-- .../edit/storages/EditS3StorageComponent.tsx | 106 ++++++++++++++++-- .../show/storages/ShowS3StorageComponent.tsx | 14 +++ 18 files changed, 321 insertions(+), 101 deletions(-) create mode 100644 backend/migrations/20251116075135_add_virtual_host_and_prefix_to_s3.sql diff --git a/assets/logo.svg b/assets/logo.svg index 5b9e3b4..9e47459 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,25 +1,44 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + diff --git a/backend/internal/features/storages/models/s3/model.go b/backend/internal/features/storages/models/s3/model.go index 65a6d84..54be1f3 100644 --- a/backend/internal/features/storages/models/s3/model.go +++ b/backend/internal/features/storages/models/s3/model.go @@ -22,6 +22,9 @@ type S3Storage struct { S3AccessKey string `json:"s3AccessKey" gorm:"not null;type:text;column:s3_access_key"` S3SecretKey string `json:"s3SecretKey" gorm:"not null;type:text;column:s3_secret_key"` S3Endpoint string `json:"s3Endpoint" gorm:"type:text;column:s3_endpoint"` + + S3Prefix string `json:"s3Prefix" gorm:"type:text;column:s3_prefix"` + S3UseVirtualHostedStyle bool `json:"s3UseVirtualHostedStyle" gorm:"default:false;column:s3_use_virtual_hosted_style"` } func (s *S3Storage) TableName() string { @@ -34,11 +37,13 @@ func (s *S3Storage) SaveFile(logger *slog.Logger, fileID uuid.UUID, file io.Read return err } + objectKey := s.buildObjectKey(fileID.String()) + // Upload the file using MinIO client with streaming (size = -1 for unknown size) _, err = client.PutObject( context.TODO(), s.S3Bucket, - fileID.String(), + objectKey, file, -1, minio.PutObjectOptions{}, @@ -56,10 +61,12 @@ func (s *S3Storage) GetFile(fileID uuid.UUID) (io.ReadCloser, error) { return nil, err } + objectKey := s.buildObjectKey(fileID.String()) + object, err := client.GetObject( context.TODO(), s.S3Bucket, - fileID.String(), + objectKey, minio.GetObjectOptions{}, ) if err != nil { @@ -90,11 +97,13 @@ func (s *S3Storage) DeleteFile(fileID uuid.UUID) error { return err } + objectKey := s.buildObjectKey(fileID.String()) + // Delete the object using MinIO client err = client.RemoveObject( context.TODO(), s.S3Bucket, - fileID.String(), + objectKey, minio.RemoveObjectOptions{}, ) if err != nil { @@ -150,6 +159,7 @@ func (s *S3Storage) TestConnection() error { // Test write and delete permissions by uploading and removing a small test file testFileID := uuid.New().String() + "-test" + testObjectKey := s.buildObjectKey(testFileID) testData := []byte("test connection") testReader := bytes.NewReader(testData) @@ -157,7 +167,7 @@ func (s *S3Storage) TestConnection() error { _, err = client.PutObject( ctx, s.S3Bucket, - testFileID, + testObjectKey, testReader, int64(len(testData)), minio.PutObjectOptions{}, @@ -170,7 +180,7 @@ func (s *S3Storage) TestConnection() error { err = client.RemoveObject( ctx, s.S3Bucket, - testFileID, + testObjectKey, minio.RemoveObjectOptions{}, ) if err != nil { @@ -189,6 +199,7 @@ func (s *S3Storage) Update(incoming *S3Storage) { s.S3Bucket = incoming.S3Bucket s.S3Region = incoming.S3Region s.S3Endpoint = incoming.S3Endpoint + s.S3UseVirtualHostedStyle = incoming.S3UseVirtualHostedStyle if incoming.S3AccessKey != "" { s.S3AccessKey = incoming.S3AccessKey @@ -197,6 +208,24 @@ func (s *S3Storage) Update(incoming *S3Storage) { if incoming.S3SecretKey != "" { s.S3SecretKey = incoming.S3SecretKey } + + // we do not allow to change the prefix after creation, + // otherwise we will have to migrate all the data to the new prefix +} + +func (s *S3Storage) buildObjectKey(fileName string) string { + if s.S3Prefix == "" { + return fileName + } + + prefix := s.S3Prefix + prefix = strings.TrimPrefix(prefix, "/") + + if !strings.HasSuffix(prefix, "/") { + prefix = prefix + "/" + } + + return prefix + fileName } func (s *S3Storage) getClient() (*minio.Client, error) { @@ -215,11 +244,18 @@ func (s *S3Storage) getClient() (*minio.Client, error) { endpoint = fmt.Sprintf("s3.%s.amazonaws.com", s.S3Region) } + // Configure bucket lookup strategy + bucketLookup := minio.BucketLookupAuto + if s.S3UseVirtualHostedStyle { + bucketLookup = minio.BucketLookupDNS + } + // Initialize the MinIO client minioClient, err := minio.New(endpoint, &minio.Options{ - Creds: credentials.NewStaticV4(s.S3AccessKey, s.S3SecretKey, ""), - Secure: useSSL, - Region: s.S3Region, + Creds: credentials.NewStaticV4(s.S3AccessKey, s.S3SecretKey, ""), + Secure: useSSL, + Region: s.S3Region, + BucketLookup: bucketLookup, }) if err != nil { return nil, fmt.Errorf("failed to initialize MinIO client: %w", err) diff --git a/backend/migrations/20251116075135_add_virtual_host_and_prefix_to_s3.sql b/backend/migrations/20251116075135_add_virtual_host_and_prefix_to_s3.sql new file mode 100644 index 0000000..40dc77d --- /dev/null +++ b/backend/migrations/20251116075135_add_virtual_host_and_prefix_to_s3.sql @@ -0,0 +1,17 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE s3_storages + ADD COLUMN s3_prefix TEXT; + +ALTER TABLE s3_storages + ADD COLUMN s3_use_virtual_hosted_style BOOLEAN NOT NULL DEFAULT FALSE; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE s3_storages + DROP COLUMN s3_use_virtual_hosted_style; + +ALTER TABLE s3_storages + DROP COLUMN s3_prefix; +-- +goose StatementEnd diff --git a/frontend/src/entity/storages/models/S3Storage.ts b/frontend/src/entity/storages/models/S3Storage.ts index 3216ee2..40c741b 100644 --- a/frontend/src/entity/storages/models/S3Storage.ts +++ b/frontend/src/entity/storages/models/S3Storage.ts @@ -4,4 +4,6 @@ export interface S3Storage { s3AccessKey: string; s3SecretKey: string; s3Endpoint?: string; + s3Prefix?: string; + s3UseVirtualHostedStyle?: boolean; } diff --git a/frontend/src/features/databases/ui/CreateDatabaseComponent.tsx b/frontend/src/features/databases/ui/CreateDatabaseComponent.tsx index aa61e1e..2c3c9d0 100644 --- a/frontend/src/features/databases/ui/CreateDatabaseComponent.tsx +++ b/frontend/src/features/databases/ui/CreateDatabaseComponent.tsx @@ -16,7 +16,7 @@ import { EditDatabaseSpecificDataComponent } from './edit/EditDatabaseSpecificDa interface Props { workspaceId: string; - onCreated: () => void; + onCreated: (databaseId: string) => void; onClose: () => void; } @@ -58,7 +58,7 @@ export const CreateDatabaseComponent = ({ workspaceId, onCreated, onClose }: Pro await backupsApi.makeBackup(createdDatabase.id); } - onCreated(); + onCreated(createdDatabase.id); onClose(); } catch (error) { alert(error); diff --git a/frontend/src/features/databases/ui/DatabasesComponent.tsx b/frontend/src/features/databases/ui/DatabasesComponent.tsx index 6aa3589..f7da05a 100644 --- a/frontend/src/features/databases/ui/DatabasesComponent.tsx +++ b/frontend/src/features/databases/ui/DatabasesComponent.tsx @@ -14,6 +14,8 @@ interface Props { isCanManageDBs: boolean; } +const SELECTED_DATABASE_STORAGE_KEY = 'selectedDatabaseId'; + export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }: Props) => { const [isLoading, setIsLoading] = useState(true); const [databases, setDatabases] = useState([]); @@ -22,7 +24,16 @@ export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }: const [isShowAddDatabase, setIsShowAddDatabase] = useState(false); const [selectedDatabaseId, setSelectedDatabaseId] = useState(undefined); - const loadDatabases = (isSilent = false) => { + const updateSelectedDatabaseId = (databaseId: string | undefined) => { + setSelectedDatabaseId(databaseId); + if (databaseId) { + localStorage.setItem(`${SELECTED_DATABASE_STORAGE_KEY}_${workspace.id}`, databaseId); + } else { + localStorage.removeItem(`${SELECTED_DATABASE_STORAGE_KEY}_${workspace.id}`); + } + }; + + const loadDatabases = (isSilent = false, selectDatabaseId?: string) => { if (!isSilent) { setIsLoading(true); } @@ -31,8 +42,17 @@ export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }: .getDatabases(workspace.id) .then((databases) => { setDatabases(databases); - if (!selectedDatabaseId && !isSilent) { - setSelectedDatabaseId(databases[0]?.id); + if (selectDatabaseId) { + updateSelectedDatabaseId(selectDatabaseId); + } else if (!selectedDatabaseId && !isSilent) { + const savedDatabaseId = localStorage.getItem( + `${SELECTED_DATABASE_STORAGE_KEY}_${workspace.id}`, + ); + const databaseToSelect = + savedDatabaseId && databases.some((db) => db.id === savedDatabaseId) + ? savedDatabaseId + : databases[0]?.id; + updateSelectedDatabaseId(databaseToSelect); } }) .catch((e) => alert(e.message)) @@ -95,7 +115,7 @@ export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }: key={database.id} database={database} selectedDatabaseId={selectedDatabaseId} - setSelectedDatabaseId={setSelectedDatabaseId} + setSelectedDatabaseId={updateSelectedDatabaseId} /> )) : searchQuery && ( @@ -119,10 +139,11 @@ export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }: loadDatabases(); }} onDatabaseDeleted={() => { - loadDatabases(); - setSelectedDatabaseId( - databases.filter((database) => database.id !== selectedDatabaseId)[0]?.id, + const remainingDatabases = databases.filter( + (database) => database.id !== selectedDatabaseId, ); + updateSelectedDatabaseId(remainingDatabases[0]?.id); + loadDatabases(); }} isCanManageDBs={isCanManageDBs} /> @@ -141,8 +162,8 @@ export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }: { - loadDatabases(); + onCreated={(databaseId) => { + loadDatabases(false, databaseId); setIsShowAddDatabase(false); }} onClose={() => setIsShowAddDatabase(false)} diff --git a/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx index ac502f4..eba90e6 100644 --- a/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx @@ -255,7 +255,10 @@ export function EditNotifierComponent({ { + setIsUnsaved(true); + setIsTestNotificationSuccess(false); + }} /> )} @@ -263,7 +266,10 @@ export function EditNotifierComponent({ { + setIsUnsaved(true); + setIsTestNotificationSuccess(false); + }} /> )} @@ -271,7 +277,10 @@ export function EditNotifierComponent({ { + setIsUnsaved(true); + setIsTestNotificationSuccess(false); + }} /> )} @@ -279,7 +288,10 @@ export function EditNotifierComponent({ { + setIsUnsaved(true); + setIsTestNotificationSuccess(false); + }} /> )} @@ -287,14 +299,20 @@ export function EditNotifierComponent({ { + setIsUnsaved(true); + setIsTestNotificationSuccess(false); + }} /> )} {notifier?.notifierType === NotifierType.TEAMS && ( { + setIsUnsaved(true); + setIsTestNotificationSuccess(false); + }} /> )} diff --git a/frontend/src/features/notifiers/ui/edit/notifiers/EditDiscordNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/notifiers/EditDiscordNotifierComponent.tsx index afde355..198e4f8 100644 --- a/frontend/src/features/notifiers/ui/edit/notifiers/EditDiscordNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/edit/notifiers/EditDiscordNotifierComponent.tsx @@ -5,10 +5,10 @@ import type { Notifier } from '../../../../../entity/notifiers'; interface Props { notifier: Notifier; setNotifier: (notifier: Notifier) => void; - setIsUnsaved: (isUnsaved: boolean) => void; + setUnsaved: () => void; } -export function EditDiscordNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) { +export function EditDiscordNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) { return ( <>
@@ -26,7 +26,7 @@ export function EditDiscordNotifierComponent({ notifier, setNotifier, setIsUnsav channelWebhookUrl: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full" diff --git a/frontend/src/features/notifiers/ui/edit/notifiers/EditEmailNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/notifiers/EditEmailNotifierComponent.tsx index 593f856..981f2f5 100644 --- a/frontend/src/features/notifiers/ui/edit/notifiers/EditEmailNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/edit/notifiers/EditEmailNotifierComponent.tsx @@ -6,10 +6,10 @@ import type { Notifier } from '../../../../../entity/notifiers'; interface Props { notifier: Notifier; setNotifier: (notifier: Notifier) => void; - setIsUnsaved: (isUnsaved: boolean) => void; + setUnsaved: () => void; } -export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) { +export function EditEmailNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) { return ( <>
@@ -26,7 +26,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved targetEmail: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -52,7 +52,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved smtpHost: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -75,7 +75,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved smtpPort: Number(e.target.value), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -97,7 +97,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved smtpUser: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -120,7 +120,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved smtpPassword: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -142,7 +142,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved from: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" diff --git a/frontend/src/features/notifiers/ui/edit/notifiers/EditSlackNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/notifiers/EditSlackNotifierComponent.tsx index 93d3e2b..27bb814 100644 --- a/frontend/src/features/notifiers/ui/edit/notifiers/EditSlackNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/edit/notifiers/EditSlackNotifierComponent.tsx @@ -5,10 +5,10 @@ import type { Notifier } from '../../../../../entity/notifiers'; interface Props { notifier: Notifier; setNotifier: (notifier: Notifier) => void; - setIsUnsaved: (isUnsaved: boolean) => void; + setUnsaved: () => void; } -export function EditSlackNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) { +export function EditSlackNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) { return ( <>
@@ -38,7 +38,7 @@ export function EditSlackNotifierComponent({ notifier, setNotifier, setIsUnsaved botToken: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full" @@ -63,7 +63,7 @@ export function EditSlackNotifierComponent({ notifier, setNotifier, setIsUnsaved targetChatId: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full" diff --git a/frontend/src/features/notifiers/ui/edit/notifiers/EditTeamsNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/notifiers/EditTeamsNotifierComponent.tsx index 0bea827..d2c500c 100644 --- a/frontend/src/features/notifiers/ui/edit/notifiers/EditTeamsNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/edit/notifiers/EditTeamsNotifierComponent.tsx @@ -7,10 +7,10 @@ import type { Notifier } from '../../../../../entity/notifiers'; interface Props { notifier: Notifier; setNotifier: (notifier: Notifier) => void; - setIsUnsaved: (isUnsaved: boolean) => void; + setUnsaved: () => void; } -export function EditTeamsNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) { +export function EditTeamsNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) { const value = notifier?.teamsNotifier?.powerAutomateUrl || ''; const onChange = (e: React.ChangeEvent) => { @@ -22,7 +22,7 @@ export function EditTeamsNotifierComponent({ notifier, setNotifier, setIsUnsaved powerAutomateUrl, }, }); - setIsUnsaved(true); + setUnsaved(); }; return ( diff --git a/frontend/src/features/notifiers/ui/edit/notifiers/EditTelegramNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/notifiers/EditTelegramNotifierComponent.tsx index ede4d2d..b2d49d1 100644 --- a/frontend/src/features/notifiers/ui/edit/notifiers/EditTelegramNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/edit/notifiers/EditTelegramNotifierComponent.tsx @@ -7,10 +7,10 @@ import type { Notifier } from '../../../../../entity/notifiers'; interface Props { notifier: Notifier; setNotifier: (notifier: Notifier) => void; - setIsUnsaved: (isUnsaved: boolean) => void; + setUnsaved: () => void; } -export function EditTelegramNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) { +export function EditTelegramNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) { const [isShowHowToGetChatId, setIsShowHowToGetChatId] = useState(false); useEffect(() => { @@ -42,7 +42,7 @@ export function EditTelegramNotifierComponent({ notifier, setNotifier, setIsUnsa botToken: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full" @@ -78,7 +78,7 @@ export function EditTelegramNotifierComponent({ notifier, setNotifier, setIsUnsa targetChatId: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full" @@ -137,7 +137,7 @@ export function EditTelegramNotifierComponent({ notifier, setNotifier, setIsUnsa threadId: checked ? notifier.telegramNotifier.threadId : undefined, }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" /> @@ -171,7 +171,7 @@ export function EditTelegramNotifierComponent({ notifier, setNotifier, setIsUnsa threadId: !isNaN(threadId!) ? threadId : undefined, }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full" diff --git a/frontend/src/features/notifiers/ui/edit/notifiers/EditWebhookNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/notifiers/EditWebhookNotifierComponent.tsx index e79aa99..2365db9 100644 --- a/frontend/src/features/notifiers/ui/edit/notifiers/EditWebhookNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/edit/notifiers/EditWebhookNotifierComponent.tsx @@ -7,10 +7,10 @@ import { WebhookMethod } from '../../../../../entity/notifiers/models/webhook/We interface Props { notifier: Notifier; setNotifier: (notifier: Notifier) => void; - setIsUnsaved: (isUnsaved: boolean) => void; + setUnsaved: () => void; } -export function EditWebhookNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) { +export function EditWebhookNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) { return ( <>
@@ -27,7 +27,7 @@ export function EditWebhookNotifierComponent({ notifier, setNotifier, setIsUnsav webhookUrl: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full" @@ -50,7 +50,7 @@ export function EditWebhookNotifierComponent({ notifier, setNotifier, setIsUnsav webhookMethod: value, }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full" diff --git a/frontend/src/features/storages/ui/edit/EditStorageComponent.tsx b/frontend/src/features/storages/ui/edit/EditStorageComponent.tsx index c90fb26..72d1c36 100644 --- a/frontend/src/features/storages/ui/edit/EditStorageComponent.tsx +++ b/frontend/src/features/storages/ui/edit/EditStorageComponent.tsx @@ -250,7 +250,10 @@ export function EditStorageComponent({ { + setIsUnsaved(true); + setIsTestConnectionSuccess(false); + }} /> )} @@ -258,7 +261,10 @@ export function EditStorageComponent({ { + setIsUnsaved(true); + setIsTestConnectionSuccess(false); + }} /> )} @@ -266,7 +272,10 @@ export function EditStorageComponent({ { + setIsUnsaved(true); + setIsTestConnectionSuccess(false); + }} /> )}
diff --git a/frontend/src/features/storages/ui/edit/storages/EditGoogleDriveStorageComponent.tsx b/frontend/src/features/storages/ui/edit/storages/EditGoogleDriveStorageComponent.tsx index 8d39a0f..7bd3c7a 100644 --- a/frontend/src/features/storages/ui/edit/storages/EditGoogleDriveStorageComponent.tsx +++ b/frontend/src/features/storages/ui/edit/storages/EditGoogleDriveStorageComponent.tsx @@ -7,10 +7,10 @@ import type { StorageOauthDto } from '../../../../../entity/storages/models/Stor interface Props { storage: Storage; setStorage: (storage: Storage) => void; - setIsUnsaved: (isUnsaved: boolean) => void; + setUnsaved: () => void; } -export function EditGoogleDriveStorageComponent({ storage, setStorage, setIsUnsaved }: Props) { +export function EditGoogleDriveStorageComponent({ storage, setStorage, setUnsaved }: Props) { const goToAuthUrl = () => { if (!storage?.googleDriveStorage?.clientId || !storage?.googleDriveStorage?.clientSecret) { return; @@ -60,7 +60,7 @@ export function EditGoogleDriveStorageComponent({ storage, setStorage, setIsUnsa clientId: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -83,7 +83,7 @@ export function EditGoogleDriveStorageComponent({ storage, setStorage, setIsUnsa clientSecret: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" diff --git a/frontend/src/features/storages/ui/edit/storages/EditNASStorageComponent.tsx b/frontend/src/features/storages/ui/edit/storages/EditNASStorageComponent.tsx index 3cd4cca..56d2691 100644 --- a/frontend/src/features/storages/ui/edit/storages/EditNASStorageComponent.tsx +++ b/frontend/src/features/storages/ui/edit/storages/EditNASStorageComponent.tsx @@ -6,10 +6,10 @@ import type { Storage } from '../../../../../entity/storages'; interface Props { storage: Storage; setStorage: (storage: Storage) => void; - setIsUnsaved: (isUnsaved: boolean) => void; + setUnsaved: () => void; } -export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: Props) { +export function EditNASStorageComponent({ storage, setStorage, setUnsaved }: Props) { return ( <>
@@ -26,7 +26,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P host: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -48,7 +48,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P port: value, }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -72,7 +72,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P share: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -94,7 +94,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P username: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -116,7 +116,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P password: e.target.value, }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -138,7 +138,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P useSsl: checked, }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" /> @@ -162,7 +162,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P domain: e.target.value.trim() || undefined, }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -197,7 +197,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P path: pathValue || undefined, }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" diff --git a/frontend/src/features/storages/ui/edit/storages/EditS3StorageComponent.tsx b/frontend/src/features/storages/ui/edit/storages/EditS3StorageComponent.tsx index bda1184..1ef7161 100644 --- a/frontend/src/features/storages/ui/edit/storages/EditS3StorageComponent.tsx +++ b/frontend/src/features/storages/ui/edit/storages/EditS3StorageComponent.tsx @@ -1,15 +1,20 @@ -import { InfoCircleOutlined } from '@ant-design/icons'; -import { Input, Tooltip } from 'antd'; +import { DownOutlined, InfoCircleOutlined, UpOutlined } from '@ant-design/icons'; +import { Checkbox, Input, Tooltip } from 'antd'; +import { useState } from 'react'; import type { Storage } from '../../../../../entity/storages'; interface Props { storage: Storage; setStorage: (storage: Storage) => void; - setIsUnsaved: (isUnsaved: boolean) => void; + setUnsaved: () => void; } -export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Props) { +export function EditS3StorageComponent({ storage, setStorage, setUnsaved }: Props) { + const hasAdvancedValues = + !!storage?.s3Storage?.s3Prefix || !!storage?.s3Storage?.s3UseVirtualHostedStyle; + const [showAdvanced, setShowAdvanced] = useState(hasAdvancedValues); + return ( <>
@@ -36,7 +41,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr s3Bucket: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -58,7 +63,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr s3Region: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -67,7 +72,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr
-
Access Key
+
Access key
{ @@ -80,7 +85,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr s3AccessKey: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -89,7 +94,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr
-
Secret Key
+
Secret key
{ @@ -102,7 +107,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr s3SecretKey: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -124,7 +129,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr s3Endpoint: e.target.value.trim(), }, }); - setIsUnsaved(true); + setUnsaved(); }} size="small" className="w-full max-w-[250px]" @@ -138,6 +143,85 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr
+ +
+
setShowAdvanced(!showAdvanced)} + > + Advanced settings + + {showAdvanced ? ( + + ) : ( + + )} +
+
+ + {showAdvanced && ( + <> +
+
Folder prefix
+ { + if (!storage?.s3Storage) return; + + setStorage({ + ...storage, + s3Storage: { + ...storage.s3Storage, + s3Prefix: e.target.value.trim(), + }, + }); + setUnsaved(); + }} + size="small" + className="w-full max-w-[250px]" + placeholder="my-prefix/ (optional)" + /> + + + + +
+ +
+
Virtual host
+ + { + if (!storage?.s3Storage) return; + + setStorage({ + ...storage, + s3Storage: { + ...storage.s3Storage, + s3UseVirtualHostedStyle: e.target.checked, + }, + }); + setUnsaved(); + }} + > + Use virtual-styled domains + + + + + +
+ + )} + +
); } diff --git a/frontend/src/features/storages/ui/show/storages/ShowS3StorageComponent.tsx b/frontend/src/features/storages/ui/show/storages/ShowS3StorageComponent.tsx index f35bba3..4eb8fdb 100644 --- a/frontend/src/features/storages/ui/show/storages/ShowS3StorageComponent.tsx +++ b/frontend/src/features/storages/ui/show/storages/ShowS3StorageComponent.tsx @@ -31,6 +31,20 @@ export function ShowS3StorageComponent({ storage }: Props) {
Endpoint
{storage?.s3Storage?.s3Endpoint || '-'}
+ + {storage?.s3Storage?.s3Prefix && ( +
+
Prefix
+ {storage.s3Storage.s3Prefix} +
+ )} + + {storage?.s3Storage?.s3UseVirtualHostedStyle && ( +
+
Virtual Host
+ Enabled +
+ )} ); }