Merge branch 'pass-lifetime-fixes' into 'main'
[ProtonMail-WebClient.git] / packages / components / containers / vpn / gateways / GatewayModal.tsx
blob335955ab5e5eae87fc738a1da078046243d5b584
1 import { useMemo, useState } from 'react';
3 import { c } from 'ttag';
5 import { Button } from '@proton/atoms';
6 import Form from '@proton/components/components/form/Form';
7 import Loader from '@proton/components/components/loader/Loader';
8 import type { ModalProps } from '@proton/components/components/modalTwo/Modal';
9 import ModalTwo from '@proton/components/components/modalTwo/Modal';
10 import ModalTwoContent from '@proton/components/components/modalTwo/ModalContent';
11 import ModalTwoFooter from '@proton/components/components/modalTwo/ModalFooter';
12 import ModalTwoHeader from '@proton/components/components/modalTwo/ModalHeader';
13 import { useModalTwoStatic } from '@proton/components/components/modalTwo/useModalTwo';
14 import useFormErrors from '@proton/components/components/v2/useFormErrors';
15 import useApiResult from '@proton/components/hooks/useApiResult';
17 import type { CountryOptions } from '../../../helpers/countries';
18 import AddServerConfirmationModal from './AddServerConfirmationModal';
19 import type { DeletedDedicatedIp } from './DeletedDedicatedIp';
20 import { GatewayCountrySelection } from './GatewayCountrySelection';
21 import type { GatewayDto } from './GatewayDto';
22 import type { GatewayLocation } from './GatewayLocation';
23 import type { GatewayModel } from './GatewayModel';
24 import { GatewayNameField } from './GatewayNameField';
25 import type { GatewayUser } from './GatewayUser';
26 import { GatewayUserSelection } from './GatewayUserSelection';
27 import { queryDeletedDedicatedIPs } from './api';
28 import { getInitialModel } from './helpers';
29 import { useAddedQuantities, useUnassigningAddedQuantities } from './useAddedQuantities';
30 import { useSpecificCountryCount } from './useSpecificCountryCount';
32 interface Props extends ModalProps<typeof Form> {
33     locations: readonly GatewayLocation[];
34     deletedInCountries: Record<string, number>;
35     ownedCount: number;
36     usedCount: number;
37     users: readonly GatewayUser[];
38     countryOptions: CountryOptions;
39     isEditing?: boolean;
40     singleServer?: boolean;
41     showCancelButton?: boolean;
42     onSubmitDone: (server: GatewayModel) => Promise<void>;
45 enum STEP {
46     NAME,
47     COUNTRIES,
48     MEMBERS,
51 const GatewayModal = ({
52     locations,
53     deletedInCountries,
54     ownedCount,
55     usedCount,
56     users,
57     countryOptions,
58     onSubmitDone,
59     isEditing = false,
60     singleServer = false,
61     showCancelButton = false,
62     ...rest
63 }: Props) => {
64     const { loading: deletedDedicatedIPsLoading, result } = useApiResult<
65         { DedicatedIps: DeletedDedicatedIp[] },
66         typeof queryDeletedDedicatedIPs
67     >(queryDeletedDedicatedIPs, []);
69     const deletedDedicatedIPs = result?.DedicatedIps;
71     const [addServerConfirmationModal, showAddServerConfirmationModal] = useModalTwoStatic(AddServerConfirmationModal);
73     const { validator, onFormSubmit } = useFormErrors();
74     const [step, setStep] = useState(STEP.NAME);
75     const [model, setModel] = useState(getInitialModel(locations));
76     const [loading, setLoading] = useState(false);
77     const remainingCount = useMemo(() => ownedCount - usedCount, [ownedCount, usedCount]);
78     const addedCount = useAddedQuantities(model);
79     const totalAddedCount = addedCount + useUnassigningAddedQuantities(model);
80     const totalCountExceeded = useMemo(
81         () => addedCount > remainingCount - (deletedDedicatedIPs?.length || 0),
82         [addedCount, remainingCount]
83     );
84     const specificCountryCount = useSpecificCountryCount(model, remainingCount, deletedInCountries);
85     const needUpsell = useMemo(
86         () => totalCountExceeded || specificCountryCount > 0,
87         [totalCountExceeded, specificCountryCount]
88     );
89     const canContinue = useMemo(
90         () => step !== STEP.COUNTRIES || !(needUpsell || (!singleServer && totalAddedCount < 1)),
91         [step, needUpsell, singleServer, totalAddedCount]
92     );
94     const changeModel = (diff: Partial<GatewayDto>) => setModel((model: GatewayDto) => ({ ...model, ...diff }));
96     const stepBack = () => {
97         if (step === STEP.MEMBERS) {
98             setStep(locations.length > 1 ? STEP.COUNTRIES : STEP.NAME);
100             return;
101         }
103         if (step === STEP.COUNTRIES) {
104             setStep(STEP.NAME);
106             return;
107         }
109         rest.onClose?.();
110     };
112     const handleSubmit = async () => {
113         if (!onFormSubmit()) {
114             return;
115         }
117         if (step === STEP.NAME) {
118             setStep(STEP.COUNTRIES);
120             return;
121         }
123         const quantities: Record<string, number> = {};
124         let total = 0;
126         Object.keys(model.quantities || {}).forEach((locationId) => {
127             const count = model.quantities?.[locationId] || 0;
129             if (count > 0) {
130                 quantities[locationId] = count;
131                 total += count;
132             }
133         });
135         Object.keys(model.unassignedIpQuantities || {}).forEach((locationId) => {
136             const count = model.unassignedIpQuantities?.[locationId] || 0;
138             if (count > 0) {
139                 quantities[locationId] = (quantities[locationId] || 0) + count;
140                 total += count;
141             }
142         });
144         if (step === STEP.COUNTRIES) {
145             showAddServerConfirmationModal({
146                 onSubmitDone: () => {
147                     setStep(STEP.MEMBERS);
148                 },
149                 totalQuantities: quantities,
150                 countryOptions,
151             });
153             return;
154         }
156         const dtoBody: GatewayModel =
157             singleServer || total === 1
158                 ? {
159                       Name: model.name,
160                       Location: model.location,
161                       Features: model.features,
162                       UserIds: model.userIds,
163                   }
164                 : {
165                       Name: model.name,
166                       Features: model.features,
167                       UserIds: model.userIds,
168                       Quantities: quantities,
169                   };
170         try {
171             setLoading(true);
172             await onSubmitDone(dtoBody);
173             rest.onClose?.();
174         } finally {
175             setLoading(false);
176         }
177     };
179     // Add checked locations in the model
180     const handleUpdateCheckedLocations = (updatedCheckedLocations: GatewayLocation[]) => {
181         setModel((prevModel) => ({
182             ...prevModel,
183             checkedLocations: updatedCheckedLocations,
184         }));
185     };
187     return (
188         <>
189             <ModalTwo size={step === STEP.MEMBERS ? 'xlarge' : 'large'} as={Form} onSubmit={handleSubmit} {...rest}>
190                 <ModalTwoHeader
191                     title={(() => {
192                         if (step === STEP.NAME) {
193                             return isEditing ? c('Action').t`Edit Gateway` : c('Title').t`Create Gateway`;
194                         }
196                         if (step === STEP.COUNTRIES) {
197                             return c('Title').t`Add dedicated servers`;
198                         }
200                         return c('Title').t`Add users`;
201                     })()}
202                 />
203                 <ModalTwoContent>
204                     {deletedDedicatedIPsLoading ? (
205                         <Loader />
206                     ) : (
207                         <>
208                             {step === STEP.NAME && (
209                                 <GatewayNameField model={model} changeModel={changeModel} validator={validator} />
210                             )}
211                             {step === STEP.COUNTRIES && (
212                                 <GatewayCountrySelection
213                                     singleServer={singleServer}
214                                     locations={locations}
215                                     ownedCount={ownedCount}
216                                     usedCount={usedCount}
217                                     addedCount={addedCount}
218                                     deletedDedicatedIPs={deletedDedicatedIPs}
219                                     countryOptions={countryOptions}
220                                     loading={loading}
221                                     model={model}
222                                     onUpdateCheckedLocations={handleUpdateCheckedLocations}
223                                     changeModel={changeModel}
224                                 />
225                             )}
226                             {step === STEP.MEMBERS && (
227                                 <GatewayUserSelection
228                                     loading={loading}
229                                     users={users}
230                                     model={model}
231                                     changeModel={changeModel}
232                                 />
233                             )}
234                         </>
235                     )}
236                 </ModalTwoContent>
237                 <ModalTwoFooter>
238                     {showCancelButton || step !== STEP.NAME ? (
239                         <Button color="weak" onClick={stepBack}>
240                             {step === STEP.NAME
241                                 ? c('Action').t`Cancel`
242                                 : /* button to go back to previous step of gateway creation */ c('Action').t`Back`}
243                         </Button>
244                     ) : (
245                         <div />
246                     )}
247                     <Button color="norm" type="submit" loading={loading} disabled={!canContinue}>
248                         {step === STEP.MEMBERS
249                             ? /* final step submit button of the creation, if not clean translation possible, it can also simply be "Create" */ c(
250                                   'Action'
251                               ).t`Done`
252                             : /* button to continue to the next step of gateway creation */ c('Action').t`Continue`}
253                     </Button>
254                 </ModalTwoFooter>
255             </ModalTwo>
256             {addServerConfirmationModal}
257         </>
258     );
261 export default GatewayModal;