Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5eb9009
feat(pci-object-storage): add Object Lock functionality to order funn…
AkdM Nov 14, 2025
732b169
feat(pci-object-storage): implement Object Lock feature with UI and a…
AkdM Nov 17, 2025
4079f8c
feat(pci-object-storage): add alert Object Lock when enabled on conta…
AkdM Nov 18, 2025
6137622
feat(pci-object-storage): add ObjectLockOptions component for managin…
AkdM Nov 20, 2025
a001cca
feat(pci-object-storage): added Object Lock options with sheet routing
AkdM Nov 20, 2025
1d28830
feat(pci-object-storage): Object Lock options with retention mode and…
AkdM Nov 21, 2025
6f0d2e3
fix(object-lock-options): update import path for RouteSheet component…
AkdM Nov 21, 2025
c35799e
fix(object-lock-options): remove useless space
AkdM Nov 24, 2025
94c4f31
fix(object-lock-options): remove useless fragment
AkdM Nov 24, 2025
6bfd2e7
fix(object-lock-options): added missing translation
AkdM Nov 24, 2025
6ee23d8
feat(object-lock-options): integrate ISO8601 duration function into a…
AkdM Nov 24, 2025
7c8140a
fix(object-lock-options): update Object Lock messages and enforce ver…
AkdM Nov 26, 2025
9251fe9
fix(object-lock-options): correct French translations for compliance …
AkdM Nov 26, 2025
6e8fe62
feat(object-lock-options): add acknowledgement checkbox for Object Lo…
AkdM Nov 26, 2025
b7e8bf9
feat(object-lock-options): add acknowledgement checkbox and error han…
AkdM Nov 26, 2025
63bf469
feat(object-lock-options): enhance Object Lock summary
AkdM Nov 27, 2025
9742d36
fix(object-lock-options): disable object lock when versioning is disa…
AkdM Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,9 @@
"pci_instances_common_instance_region-3-az_deployment_mode_description": "Déploiement haute résilience/haute disponibilité pour vos applications critiques sur 3 zones de disponibilité.",
"pci_instances_common_instance_localzone_deployment_mode_description": "Déploiement de vos applications au plus près de vos utilisateurs pour une faible latence et la résidence des données.",
"selectGeographicalZone": "Selectionnez une zone géographique",
"showAllRegions": "Affichez toutes les régions"
"showAllRegions": "Affichez toutes les régions",
"labelObjectLock": "Object Lock",
"descriptionObjectLock": "Empêche la suppression ou la modification d'objets afin de protéger vos données contre les suppressions accidentelles.",
"objectLockTypeDisabledPopover": "L'Object Lock ne peut pas être activé car le versioning est désactivé. Modifiez l'étape Versioning si vous souhaitez l'activer.",
"objectLockCannotDisableAlert": " Une fois l'Object Lock activé, vous ne pourrez plus le désactiver"
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"activateButton": "Activer",
"cancelButton": "Annuler",
"confirmButton": "Confirmer",
"objectLockTitle": "Object lock",
"objectLockTitle": "Object Lock",
"addButtonLabel": "Ajouter",
"comingSoonLabel": "Disponible via API",
"disableEncryptionDesc1": "Les données stockées dans votre bucket ne sont actuellement pas chiffrées.",
Expand All @@ -39,5 +39,25 @@
"enableVersionning": "Vos objets sont versionnés pour garantir une meilleure traçabilité et récupération en cas de perte ou de modification.",
"activateVersionningTitle": "Activer le versioning",
"activateVersionningDescription": "Automatise la création de versions d’un objet pour pallier la suppression accidentelle de ce dernier, ou simplement quand un nouvel objet est uploadé.",
"activateVersionningToastSuccessDescription": "Le versionning a été activé"
"activateVersionningToastSuccessDescription": "Le versionning a été activé",
"activateObjectLockTitle": "Activer l'Object Lock",
"activateObjectLockDescription": "Empêche la suppression ou la modification d'objets afin de protéger vos données contre les suppressions accidentelles.",
"activateObjectLockToastSuccessDescription": "L'Object Lock a été activé",
"objectLockOptionsButton": "Options",
"editObjectLockTitle": "Object Lock",
"editObjectLockDescription": "L'Object Lock est une fonctionnalité qui vous permet de stocker des objets en utilisant le modèle Write Once, Read Many (WORM) et peut être utilisée dans des scénarios où il est impératif que les données ne soient ni modifiées ni supprimées après avoir été écrites.",
"objectLockRetentionGroupLabel": "Rétention des objets",
"objectLockRetentionLabelenabled": "Activée",
"objectLockRetentionLabeldisabled": "Désactivée",
"objectLockOptionsButtonCancel": "Annuler",
"objectLockOptionsButtonConfirm": "Sauvegarder",
"objectLockRetentionModeGroupLabel": "Mode de rétention",
"objectLockRetentionModeLabel_governance": "Gouvernance",
"objectLockRetentionModeLabel_compliance": "Compliance",
"objectLockDurationLabel": "Durée de rétention",
"objectLockDurationUnitDays": "Jours",
"objectLockDurationUnitYears": "Années",
"objectLockComplianceModeWarning": "En mode confirmité, un objet est immuable jusqu'à l'expiration de sa date de conservation. Pour supprimer les objets configurés de cette manière, vous devez fermer le compte auquel ils sont associés.",
"objectLockRetentionModeDescription_governance": "Les utilisateurs disposant d'autorisations IAM spécifiques peuvent écraser ou supprimer un objet protégé pendant la période de conservation.",
"objectLockRetentionModeDescription_compliance": "Aucun utilisateur ne peut écraser ou supprimer les versions d'objets protégés pendant le mode de rétention."
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import OrderSummary from './OrderSummary.component';
import RegionsStep from './steps/RegionStep.component';
import OffsiteReplicationStep from './steps/OffsiteReplicationStep.component';
import VersionningStep from './steps/VersionningStep.component';
import ObjectLockStep from './steps/ObjectLockStep.component';
import ContainerTypeStep from './steps/ContainerTypeStep.component';
import UserStep from './steps/UserStep.component';
import EncryptStep from './steps/EncryptStep.component';
Expand Down Expand Up @@ -62,7 +63,7 @@ const OrderFunnel = ({
const storagePricesLink = useLink(STORAGE_PRICES_LINK);
const replicationLink = useLink(STORAGE_ASYNC_REPLICATION_LINK);

const { form, availableRegions, model, result } = useOrderFunnel({
const { form, availableRegions, model, versioning, result } = useOrderFunnel({
regions,
users,
availabilities,
Expand Down Expand Up @@ -203,6 +204,7 @@ const OrderFunnel = ({

{!isLZ && (
<>
{/* Versioning */}
<OrderSection
id="versions"
title={t('labelVersioning')}
Expand All @@ -220,6 +222,20 @@ const OrderFunnel = ({
</FormField>
</OrderSection>

{/* Object Lock */}
<OrderSection
id="object-lock"
title={t('labelObjectLock')}
description={t('descriptionObjectLock')}
>
<FormField name="objectLock" form={form}>
{(field) => (
<ObjectLockStep versioning={versioning} {...field} />
)}
</FormField>
</OrderSection>

{/* User */}
<OrderSection
id="user"
title={t('labelUser')}
Expand All @@ -230,6 +246,7 @@ const OrderFunnel = ({
</FormField>
</OrderSection>

{/* Encryption */}
<OrderSection
id="encryption"
title={t('labelEncryption')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const createOrderFunnelFormSchema = (t: typeof i18next.t) => {
encryption: z.nativeEnum(storages.EncryptionAlgorithmEnum).optional(),
replication: replicationSchema,
versioning: z.nativeEnum(storages.VersioningStatusEnum).optional(),
objectLock: z.nativeEnum(storages.ObjectLockStatusEnum).optional(),
});

const swiftSchema = baseSchema.extend({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
Alert,
AlertDescription,
RadioGroup,
RadioGroupItem,
Label,
Popover,
PopoverTrigger,
PopoverContent,
} from '@datatr-ux/uxlib';
import { HelpCircleIcon, Info } from 'lucide-react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import storages from '@/types/Storages';
import { cn } from '@/lib/utils';

interface ObjectLockStepProps {
value?: storages.ObjectLockStatusEnum;
onChange?: (newValue: storages.ObjectLockStatusEnum) => void;
versioning?: storages.VersioningStatusEnum;
}

const ObjectLockStep = React.forwardRef<HTMLInputElement, ObjectLockStepProps>(
({ value, onChange, versioning }, ref) => {
const { t } = useTranslation('pci-object-storage/order-funnel');
const isVersioningDisabled =
versioning === storages.VersioningStatusEnum.disabled;
const isObjectLockEnabled = value === storages.ObjectLockStatusEnum.enabled;

return (
<RadioGroup
value={value}
onValueChange={(newValue) => {
onChange(newValue as storages.ObjectLockStatusEnum);
}}
data-testid="object-lock-select-container"
ref={ref}
>
{/* Disabled option */}
<div className="flex items-center gap-3">
<RadioGroupItem
value={storages.ObjectLockStatusEnum.disabled}
id="object-lock-disabled-option"
/>
<Label
htmlFor="object-lock-disabled-option"
className="flex gap-2 justify-center"
>
{t(
`versionningTypeLabel-${storages.ObjectLockStatusEnum.disabled}`,
)}{' '}
</Label>
</div>

{/* Enabled option with versioning constraint */}
<div className="flex items-center gap-3">
<RadioGroupItem
value={storages.ObjectLockStatusEnum.enabled}
id="object-lock-enabled-option"
disabled={isVersioningDisabled}
/>
<Label htmlFor="object-lock-enabled-option">
<span
className={cn(
isVersioningDisabled && 'opacity-70 cursor-not-allowed',
)}
>
{t(
`versionningTypeLabel-${storages.ObjectLockStatusEnum.enabled}`,
)}
</span>
</Label>
{/* Help popover shown when versioning is disabled */}
{isVersioningDisabled && (
<Popover>
<PopoverTrigger asChild>
<HelpCircleIcon className="size-4" />
</PopoverTrigger>
<PopoverContent className="text-sm">
{t('objectLockTypeDisabledPopover')}
</PopoverContent>
</Popover>
)}
</div>
{/* Alert shown when Object Lock is enabled */}
{isObjectLockEnabled && (
<Alert variant="information">
<AlertDescription className="flex gap-2 items-center">
<Info className="size-4" />
{t('objectLockCannotDisableAlert')}
</AlertDescription>
</Alert>
)}
</RadioGroup>
);
},
);

ObjectLockStep.displayName = 'ObjectLockStep';
export default ObjectLockStep;
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function useOrderFunnel({
region: undefined,
},
versioning: storages.VersioningStatusEnum.disabled,
objectLock: storages.ObjectLockStatusEnum.disabled,
user: users[0]?.id ?? null,
encryption: storages.EncryptionAlgorithmEnum.AES256,
},
Expand All @@ -75,6 +76,7 @@ export function useOrderFunnel({
const { name, offer, region, user } = form.watch();
const replication = form.watch('replication');
const versioning = form.watch('versioning');
const objectLock = form.watch('objectLock');
const encryption = form.watch('encryption');
const containerType = form.watch('containerType');
const currentRegion = regions.find((r) => r.name === region);
Expand Down Expand Up @@ -117,6 +119,7 @@ export function useOrderFunnel({
}
}, [currentRegion, form]);

// Force enable versioning if replication is enabled
useEffect(() => {
if (replication?.enabled) {
form.setValue('versioning', storages.VersioningStatusEnum.enabled, {
Expand All @@ -125,6 +128,15 @@ export function useOrderFunnel({
}
}, [replication, form]);

// Force disable object lock if versioning is disabled
useEffect(() => {
if (versioning === storages.VersioningStatusEnum.disabled) {
form.setValue('objectLock', storages.ObjectLockStatusEnum.disabled, {
shouldValidate: true,
});
}
}, [versioning, form]);

// build order model
const result = useMemo(() => {
const s3: cloud.StorageContainerCreation & { region: string } = {
Expand All @@ -142,6 +154,9 @@ export function useOrderFunnel({
s3.versioning = {
status: versioning,
};
s3.objectLock = {
status: objectLock,
};
}
if (replication?.enabled) {
s3.replication = {
Expand Down Expand Up @@ -176,6 +191,7 @@ export function useOrderFunnel({
user,
encryption,
versioning,
objectLock,
region,
replication,
containerType,
Expand All @@ -188,6 +204,7 @@ export function useOrderFunnel({
currentRegion,
replication,
},
versioning,
availableRegions,
pricings,
result,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,6 @@ const Dashboard = () => {
</CardContent>
</Card>
<div className="space-y-2">
<Card className="w-full">
<CardHeader>
<h4>
<FolderLock className="size-4 inline mr-2" />
<span>{t('objectLockTitle')}</span>
</h4>
</CardHeader>
<CardContent>
<ObjectLock />
</CardContent>
</Card>
{!isLocaleZone && (
<Card className="w-full">
<CardHeader>
Expand All @@ -60,6 +49,17 @@ const Dashboard = () => {
</CardContent>
</Card>
)}
<Card className="w-full">
<CardHeader>
<h4>
<FolderLock className="size-4 inline mr-2" />
<span>{t('objectLockTitle')}</span>
</h4>
</CardHeader>
<CardContent>
<ObjectLock />
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<h4>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,48 @@
import { useTranslation } from 'react-i18next';
import { Badge } from '@datatr-ux/uxlib';
import { Badge, Button } from '@datatr-ux/uxlib';
import { useNavigate } from 'react-router-dom';
import { Settings } from 'lucide-react';
import { useS3Data } from '../../S3.context';
import storages from '@/types/Storages';

const ObjectLock = () => {
const navigate = useNavigate();
const { s3 } = useS3Data();
const { t } = useTranslation('pci-object-storage/storages/s3/dashboard');
const prefix =
s3.objectLock.status === storages.ObjectLockStatusEnum.enabled
? 'enable'
: 'disable';
const isObjectLockEnabled =
s3.objectLock.status === storages.ObjectLockStatusEnum.enabled;
const prefix = isObjectLockEnabled ? 'enable' : 'disable';

return (
<div className="space-y-2">
<div className="flex flex-row justify-between">
<Badge
variant={
s3.objectLock.status === storages.ObjectLockStatusEnum.enabled
? 'success'
: 'warning'
}
>
{t(`${prefix}Label`)}
</Badge>
<Badge variant="neutral">{t('comingSoonLabel')}</Badge>
<>
<div className="space-y-2">
<div className="flex flex-row justify-between">
<Badge
variant={
s3.objectLock.status === storages.ObjectLockStatusEnum.enabled
? 'success'
: 'warning'
}
>
{t(`${prefix}Label`)}
</Badge>
{isObjectLockEnabled && (
<Button
data-testid="label-object-lock-options-button"
mode="outline"
size="sm"
className="h-6"
onClick={() => navigate('./object-lock-options')}
>
<Settings className="size-4" />
<span className="font-semibold">
{t('objectLockOptionsButton')}
</span>
</Button>
)}
</div>
</div>
</div>
</>
);
};

Expand Down
Loading
Loading