mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 00:32:03 +02:00
FEATURE (s3): Add support of virtual-styled-domains and S3 prefix
This commit is contained in:
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 34 KiB |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -4,4 +4,6 @@ export interface S3Storage {
|
||||
s3AccessKey: string;
|
||||
s3SecretKey: string;
|
||||
s3Endpoint?: string;
|
||||
s3Prefix?: string;
|
||||
s3UseVirtualHostedStyle?: boolean;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Database[]>([]);
|
||||
@@ -22,7 +24,16 @@ export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }:
|
||||
const [isShowAddDatabase, setIsShowAddDatabase] = useState(false);
|
||||
const [selectedDatabaseId, setSelectedDatabaseId] = useState<string | undefined>(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 }:
|
||||
|
||||
<CreateDatabaseComponent
|
||||
workspaceId={workspace.id}
|
||||
onCreated={() => {
|
||||
loadDatabases();
|
||||
onCreated={(databaseId) => {
|
||||
loadDatabases(false, databaseId);
|
||||
setIsShowAddDatabase(false);
|
||||
}}
|
||||
onClose={() => setIsShowAddDatabase(false)}
|
||||
|
||||
@@ -255,7 +255,10 @@ export function EditNotifierComponent({
|
||||
<EditTelegramNotifierComponent
|
||||
notifier={notifier}
|
||||
setNotifier={setNotifier}
|
||||
setIsUnsaved={setIsUnsaved}
|
||||
setUnsaved={() => {
|
||||
setIsUnsaved(true);
|
||||
setIsTestNotificationSuccess(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -263,7 +266,10 @@ export function EditNotifierComponent({
|
||||
<EditEmailNotifierComponent
|
||||
notifier={notifier}
|
||||
setNotifier={setNotifier}
|
||||
setIsUnsaved={setIsUnsaved}
|
||||
setUnsaved={() => {
|
||||
setIsUnsaved(true);
|
||||
setIsTestNotificationSuccess(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -271,7 +277,10 @@ export function EditNotifierComponent({
|
||||
<EditWebhookNotifierComponent
|
||||
notifier={notifier}
|
||||
setNotifier={setNotifier}
|
||||
setIsUnsaved={setIsUnsaved}
|
||||
setUnsaved={() => {
|
||||
setIsUnsaved(true);
|
||||
setIsTestNotificationSuccess(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -279,7 +288,10 @@ export function EditNotifierComponent({
|
||||
<EditSlackNotifierComponent
|
||||
notifier={notifier}
|
||||
setNotifier={setNotifier}
|
||||
setIsUnsaved={setIsUnsaved}
|
||||
setUnsaved={() => {
|
||||
setIsUnsaved(true);
|
||||
setIsTestNotificationSuccess(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -287,14 +299,20 @@ export function EditNotifierComponent({
|
||||
<EditDiscordNotifierComponent
|
||||
notifier={notifier}
|
||||
setNotifier={setNotifier}
|
||||
setIsUnsaved={setIsUnsaved}
|
||||
setUnsaved={() => {
|
||||
setIsUnsaved(true);
|
||||
setIsTestNotificationSuccess(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{notifier?.notifierType === NotifierType.TEAMS && (
|
||||
<EditTeamsNotifierComponent
|
||||
notifier={notifier}
|
||||
setNotifier={setNotifier}
|
||||
setIsUnsaved={setIsUnsaved}
|
||||
setUnsaved={() => {
|
||||
setIsUnsaved(true);
|
||||
setIsTestNotificationSuccess(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="flex">
|
||||
@@ -26,7 +26,7 @@ export function EditDiscordNotifierComponent({ notifier, setNotifier, setIsUnsav
|
||||
channelWebhookUrl: e.target.value.trim(),
|
||||
},
|
||||
});
|
||||
setIsUnsaved(true);
|
||||
setUnsaved();
|
||||
}}
|
||||
size="small"
|
||||
className="w-full"
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="mb-1 flex items-center">
|
||||
@@ -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]"
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="mb-1 ml-[130px] max-w-[200px]" style={{ lineHeight: 1 }}>
|
||||
@@ -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"
|
||||
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
@@ -22,7 +22,7 @@ export function EditTeamsNotifierComponent({ notifier, setNotifier, setIsUnsaved
|
||||
powerAutomateUrl,
|
||||
},
|
||||
});
|
||||
setIsUnsaved(true);
|
||||
setUnsaved();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
@@ -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"
|
||||
|
||||
@@ -250,7 +250,10 @@ export function EditStorageComponent({
|
||||
<EditS3StorageComponent
|
||||
storage={storage}
|
||||
setStorage={setStorage}
|
||||
setIsUnsaved={setIsUnsaved}
|
||||
setUnsaved={() => {
|
||||
setIsUnsaved(true);
|
||||
setIsTestConnectionSuccess(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -258,7 +261,10 @@ export function EditStorageComponent({
|
||||
<EditGoogleDriveStorageComponent
|
||||
storage={storage}
|
||||
setStorage={setStorage}
|
||||
setIsUnsaved={setIsUnsaved}
|
||||
setUnsaved={() => {
|
||||
setIsUnsaved(true);
|
||||
setIsTestConnectionSuccess(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -266,7 +272,10 @@ export function EditStorageComponent({
|
||||
<EditNASStorageComponent
|
||||
storage={storage}
|
||||
setStorage={setStorage}
|
||||
setIsUnsaved={setIsUnsaved}
|
||||
setUnsaved={() => {
|
||||
setIsUnsaved(true);
|
||||
setIsTestConnectionSuccess(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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]"
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="mb-1 flex items-center">
|
||||
@@ -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]"
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="mb-2 flex items-center">
|
||||
@@ -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
|
||||
</div>
|
||||
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Access Key</div>
|
||||
<div className="min-w-[110px]">Access key</div>
|
||||
<Input.Password
|
||||
value={storage?.s3Storage?.s3AccessKey || ''}
|
||||
onChange={(e) => {
|
||||
@@ -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
|
||||
</div>
|
||||
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Secret Key</div>
|
||||
<div className="min-w-[110px]">Secret key</div>
|
||||
<Input.Password
|
||||
value={storage?.s3Storage?.s3SecretKey || ''}
|
||||
onChange={(e) => {
|
||||
@@ -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
|
||||
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 mb-3 flex items-center">
|
||||
<div
|
||||
className="flex cursor-pointer items-center text-sm text-blue-600 hover:text-blue-800"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
>
|
||||
<span className="mr-2">Advanced settings</span>
|
||||
|
||||
{showAdvanced ? (
|
||||
<UpOutlined style={{ fontSize: '12px' }} />
|
||||
) : (
|
||||
<DownOutlined style={{ fontSize: '12px' }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showAdvanced && (
|
||||
<>
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Folder prefix</div>
|
||||
<Input
|
||||
value={storage?.s3Storage?.s3Prefix || ''}
|
||||
onChange={(e) => {
|
||||
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)"
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
className="cursor-pointer"
|
||||
title="Optional prefix for all object keys (e.g., 'backups/' or 'my_team/'). May not work with some S3-compatible storages."
|
||||
>
|
||||
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Virtual host</div>
|
||||
|
||||
<Checkbox
|
||||
checked={storage?.s3Storage?.s3UseVirtualHostedStyle || false}
|
||||
onChange={(e) => {
|
||||
if (!storage?.s3Storage) return;
|
||||
|
||||
setStorage({
|
||||
...storage,
|
||||
s3Storage: {
|
||||
...storage.s3Storage,
|
||||
s3UseVirtualHostedStyle: e.target.checked,
|
||||
},
|
||||
});
|
||||
setUnsaved();
|
||||
}}
|
||||
>
|
||||
Use virtual-styled domains
|
||||
</Checkbox>
|
||||
|
||||
<Tooltip
|
||||
className="cursor-pointer"
|
||||
title="Use virtual-hosted-style URLs (bucket.s3.region.amazonaws.com) instead of path-style (s3.region.amazonaws.com/bucket). May be required if you see COS errors."
|
||||
>
|
||||
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mb-5" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,20 @@ export function ShowS3StorageComponent({ storage }: Props) {
|
||||
<div className="min-w-[110px]">Endpoint</div>
|
||||
{storage?.s3Storage?.s3Endpoint || '-'}
|
||||
</div>
|
||||
|
||||
{storage?.s3Storage?.s3Prefix && (
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Prefix</div>
|
||||
{storage.s3Storage.s3Prefix}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{storage?.s3Storage?.s3UseVirtualHostedStyle && (
|
||||
<div className="mb-1 flex items-center">
|
||||
<div className="min-w-[110px]">Virtual Host</div>
|
||||
Enabled
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user