Merge branch 'fix/isloading-photos' into 'main'
[ProtonMail-WebClient.git] / packages / components / containers / vpn / gateways / GatewayServersModal.tsx
blobed3f4943568a1e2e036403ae0ae12622747c4bb9
1 import { useState } from 'react';
3 import { c, msgid } from 'ttag';
5 import { Button } from '@proton/atoms';
6 import Form from '@proton/components/components/form/Form';
7 import Icon from '@proton/components/components/icon/Icon';
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 Cell from '@proton/components/components/table/Cell';
15 import Table from '@proton/components/components/table/Table';
16 import TableBody from '@proton/components/components/table/TableBody';
17 import TableCell from '@proton/components/components/table/TableCell';
18 import TableRow from '@proton/components/components/table/TableRow';
19 import { MAX_IPS_ADDON } from '@proton/payments';
20 import range from '@proton/utils/range';
22 import { type CountryOptions, getLocalizedCountryByAbbr } from '../../../helpers/countries';
23 import { CountryFlagAndName } from './CountryFlagAndName';
24 import type { Gateway } from './Gateway';
25 import GatewayAddServersModal from './GatewayAddServersModal';
26 import type { GatewayLocation } from './GatewayLocation';
27 import type { GatewayLogical } from './GatewayLogical';
28 import type { GatewayUser } from './GatewayUser';
29 import RemoveServerConfirmationModal from './RemoveServerConfirmationModal';
30 import { getFormattedLoad, getLocationFromId, getSuffix, getTotalAdded } from './helpers';
32 interface Props extends ModalProps<typeof Form> {
33     gateway: Gateway;
34     countries: readonly string[];
35     locations: readonly GatewayLocation[];
36     countryOptions: CountryOptions;
37     deletedInCountries: Record<string, number>;
38     users: readonly GatewayUser[];
39     ownedCount: number;
40     usedCount: number;
41     showDeleted?: boolean;
42     showIPv4?: boolean;
43     showIPv6?: boolean;
44     showLoad?: boolean;
45     showCancelButton?: boolean;
46     singleServer?: boolean;
47     isDeleted: (logical: GatewayLogical) => boolean;
48     onSubmitDone: (deletedLogicalIds: readonly string[], addedQuantities: Record<string, number>) => Promise<void>;
49     onUpsell: () => void;
52 const GatewayServersModal = ({
53     gateway,
54     countries,
55     locations,
56     countryOptions,
57     deletedInCountries,
58     users,
59     ownedCount,
60     usedCount,
61     showDeleted = false,
62     showIPv4 = true,
63     showIPv6 = true,
64     showLoad = true,
65     showCancelButton = false,
66     singleServer = false,
67     isDeleted,
68     onSubmitDone,
69     onUpsell,
70     ...rest
71 }: Props) => {
72     const [addServersModal, showAddServersModal] = useModalTwoStatic(GatewayAddServersModal);
73     const [removeServerConfirmation, showRemoveServerConfirmation] = useModalTwoStatic(RemoveServerConfirmationModal);
74     const [deleted, setDeleted] = useState<Record<string, boolean>>({});
75     const [added, setAdded] = useState<Record<string, number>>({});
76     const [loading, setLoading] = useState(false);
77     const remainingCount = ownedCount - usedCount;
78     const previousServers = gateway.Logicals.filter((l) => !l.Servers?.length || l.Visible);
79     const previousNumberOfServers = previousServers.length;
80     const deletedServers = previousServers.filter((l) => deleted[l.ID]);
81     const deletedServerCount = deletedServers.length;
82     const addedServerCount = Object.entries(added).reduce((total, [, quantity]) => total + quantity, 0);
83     const availableAddedCount = remainingCount + deletedServerCount - addedServerCount;
84     const newNumberOfServers = previousNumberOfServers + addedServerCount - deletedServerCount;
85     const showIP = showIPv4 || showIPv6;
87     const decreaseQuantities = (quantities: Record<string, number>) => {
88         const newAdded = { ...added };
90         Object.entries(quantities).forEach(([country, quantity]) => {
91             const newQuantity = (newAdded[country] || 0) - quantity;
93             if (newQuantity) {
94                 newAdded[country] = newQuantity;
96                 return;
97             }
99             delete newAdded[country];
100         });
102         setAdded(newAdded);
103     };
105     const addQuantities = (quantities: Record<string, number>) => {
106         const entries = Object.entries(quantities);
108         if (!entries.length) {
109             return;
110         }
112         const newAdded = { ...added };
113         let deletedIds = Object.keys(deleted).filter((id) => deleted[id]);
115         entries.forEach(([country, quantity]) => {
116             const serversFromTrash = gateway.Logicals.filter((l) => l.ExitCountry === country && l.ID && deleted[l.ID])
117                 .map((l) => l.ID)
118                 .slice(0, quantity);
119             const recoverableQuantity = serversFromTrash.length;
121             if (recoverableQuantity) {
122                 deletedIds = deletedIds.filter((id) => serversFromTrash.indexOf(id) === -1);
123             }
125             const addedQuantity = quantity - recoverableQuantity;
127             if (addedQuantity > 0) {
128                 newAdded[country] = (newAdded[country] || 0) + addedQuantity;
129             }
130         });
132         const newDeleted: Record<string, true> = {};
134         deletedIds.forEach((id) => {
135             newDeleted[id] = true;
136         });
138         setAdded(newAdded);
139         setDeleted(newDeleted);
140     };
142     const addServers = () =>
143         showAddServersModal({
144             countries,
145             locations,
146             deletedInCountries,
147             ownedCount,
148             usedCount: usedCount - deletedServerCount + addedServerCount,
149             users,
150             countryOptions,
151             singleServer,
152             showCancelButton,
153             onSubmitDone: addQuantities,
154             onUpsell,
155         });
157     const handleDelete = (logical: GatewayLogical) => async () => {
158         const onRemoveConfirmed = () => {
159             if (added[logical.ExitCountry] > 0) {
160                 decreaseQuantities({ [logical.ExitCountry]: 1 });
162                 return;
163             }
165             setDeleted({
166                 ...deleted,
167                 [logical.ID]: !deleted[logical.ID],
168             });
169         };
171         if (!deleted[logical.ID]) {
172             showRemoveServerConfirmation({ onSubmitDone: onRemoveConfirmed });
173         } else {
174             onRemoveConfirmed();
175         }
176     };
178     const handleSubmit = async () => {
179         const idsToDelete = Object.keys(deleted).filter((id) => deleted[id]);
181         if (getTotalAdded(added) < 1 && idsToDelete.length < 1) {
182             rest.onClose?.();
184             return;
185         }
187         try {
188             setLoading(true);
189             await onSubmitDone(idsToDelete, added);
190             rest.onClose?.();
191         } finally {
192             setLoading(false);
193         }
194     };
196     return (
197         <>
198             <ModalTwo size="xlarge" {...rest} as={Form} onSubmit={handleSubmit}>
199                 <ModalTwoHeader title={c('Title').t`Edit servers`} />
200                 <ModalTwoContent>
201                     <Table className="my-2" responsive="cards">
202                         <thead>
203                             <tr>
204                                 <TableCell key="country" type="header" className="w-1/4">{c('Header')
205                                     .t`Location`}</TableCell>
206                                 <TableCell key="server" type="header" className="w-1/10">
207                                     {c('Header').t`Server`}
208                                 </TableCell>
209                                 <TableCell key="status" type="header" className="w-1/6">
210                                     {c('Header').t`Status`}
211                                 </TableCell>
212                                 {showIP && (
213                                     <TableCell key="ip" type="header" className="w-1/6">
214                                         {c('Header').t`IP address`}
215                                     </TableCell>
216                                 )}
217                                 {showLoad && (
218                                     <TableCell key="load" type="header" className="w-1/6">
219                                         {c('Header').t`Server load`}
220                                     </TableCell>
221                                 )}
222                                 <TableCell key="manage" type="header" className="w-1/10">
223                                     &nbsp;
224                                 </TableCell>
225                             </tr>
226                         </thead>
227                         <TableBody colSpan={4 + Number(showIP) + Number(showLoad)} loading={loading}>
228                             {gateway.Logicals.filter((l) => l.Servers?.length && l.Visible).map((logical) => (
229                                 <TableRow
230                                     key={'logical-' + logical.ID}
231                                     className={deleted[logical.ID] ? 'opacity-50' : undefined}
232                                     cells={[
233                                         <CountryFlagAndName
234                                             countryCode={logical.ExitCountry}
235                                             countryName={
236                                                 getLocalizedCountryByAbbr(logical.ExitCountry, countryOptions) +
237                                                 ' - ' +
238                                                 logical.City
239                                             }
240                                             className="mb-1"
241                                         />,
242                                         getSuffix(logical.Name),
243                                         deleted[logical.ID] ? (
244                                             new Cell(
245                                                 (
246                                                     <span className="py-1 px-2 rounded text-uppercase bg-danger">{
247                                                         /** translator: status of the server: will be deleted when user click "Save" */
248                                                         c('Server-Info').t`to be deleted`
249                                                     }</span>
250                                                 ),
251                                                 1
252                                             )
253                                         ) : (
254                                             <span className="py-1 px-2 rounded text-uppercase bg-success">{
255                                                 /** translator: status of the server: people can connect to it */
256                                                 c('Server-Info').t`active`
257                                             }</span>
258                                         ),
259                                         ...(showIP
260                                             ? [
261                                                   <>
262                                                       {[
263                                                           showIPv4 && logical.Servers[0].ExitIPv4,
264                                                           showIPv6 && logical.Servers[0].ExitIPv6,
265                                                       ]
266                                                           .filter(Boolean)
267                                                           .map((ip) => (
268                                                               <div
269                                                                   key={'ip-' + ip}
270                                                                   className="text-ellipsis"
271                                                                   title={ip || undefined}
272                                                               >
273                                                                   {ip}
274                                                               </div>
275                                                           ))}
276                                                   </>,
277                                               ]
278                                             : []),
279                                         ...(showLoad ? [getFormattedLoad(logical.Servers)] : []),
280                                         <Button icon size="small" key="delete" onClick={handleDelete(logical)}>
281                                             <Icon name="trash" />
282                                         </Button>,
283                                     ]}
284                                 />
285                             ))}
286                             {showDeleted &&
287                                 gateway.Logicals.filter((l) => l.Servers?.length && !l.Visible).map((logical) => (
288                                     <TableRow
289                                         key={'logical-' + logical.ID}
290                                         className="opacity-50"
291                                         cells={[
292                                             <CountryFlagAndName
293                                                 countryCode={logical.ExitCountry}
294                                                 countryName={
295                                                     getLocalizedCountryByAbbr(logical.ExitCountry, countryOptions) +
296                                                     ' - ' +
297                                                     logical.City
298                                                 }
299                                                 className="mb-1"
300                                             />,
301                                             getSuffix(logical.Name),
302                                             <span className="py-1 px-2 rounded text-uppercase bg-weak color-weak">{
303                                                 /** translator: status of the server: people cannot connect to it */
304                                                 c('Server-Info').t`inactive`
305                                             }</span>,
306                                             ...(showIP
307                                                 ? [
308                                                       <>
309                                                           {[logical.Servers[0].ExitIPv4, logical.Servers[0].ExitIPv6]
310                                                               .filter(Boolean)
311                                                               .map((ip) => (
312                                                                   <div key={'ip-' + ip}>{ip}</div>
313                                                               ))}
314                                                       </>,
315                                                   ]
316                                                 : []),
317                                             ...(showLoad ? [getFormattedLoad(logical.Servers)] : []),
318                                             '',
319                                         ]}
320                                     />
321                                 ))}
322                             {gateway.Logicals.filter((l) => !l.Servers?.length).map((logical) => (
323                                 <TableRow
324                                     key={'logical-' + logical.ID}
325                                     className={deleted[logical.ID] ? 'opacity-50' : undefined}
326                                     cells={[
327                                         <CountryFlagAndName
328                                             countryCode={logical.ExitCountry}
329                                             countryName={
330                                                 getLocalizedCountryByAbbr(logical.ExitCountry, countryOptions) +
331                                                 ' - ' +
332                                                 logical.City
333                                             }
334                                             className="mb-1"
335                                         />,
336                                         getSuffix(logical.Name),
337                                         deleted[logical.ID] ? (
338                                             new Cell(
339                                                 (
340                                                     <span className="py-1 px-2 rounded text-uppercase bg-danger">{
341                                                         /** translator: status of the server: will be deleted when user click "Save" */
342                                                         c('Server-Info').t`to be deleted`
343                                                     }</span>
344                                                 ),
345                                                 1
346                                             )
347                                         ) : (
348                                             <span className="py-1 px-2 rounded text-uppercase bg-info">{
349                                                 /** translator: status of the server: people cannot yet use it */
350                                                 c('Server-Info').t`pending`
351                                             }</span>
352                                         ),
353                                         ...(showIP ? ['-'] : []),
354                                         ...(showLoad ? ['-'] : []),
355                                         <Button icon size="small" key="delete" onClick={handleDelete(logical)}>
356                                             <Icon name="trash" />
357                                         </Button>,
358                                     ]}
359                                 />
360                             ))}
361                             {Object.keys(added).map((locationId) =>
362                                 range(0, added[locationId]).map((index) => {
363                                     const location = getLocationFromId(locationId);
364                                     return (
365                                         <TableRow
366                                             key={'future-logical-' + locationId + '-' + index}
367                                             cells={[
368                                                 <CountryFlagAndName
369                                                     countryCode={location.Country}
370                                                     countryName={
371                                                         getLocalizedCountryByAbbr(location.Country, countryOptions) +
372                                                         ' - ' +
373                                                         location.TranslatedCity
374                                                     }
375                                                     className="mb-1"
376                                                 />,
377                                                 '',
378                                                 new Cell(
379                                                     (
380                                                         <span className="py-1 px-2 rounded text-uppercase bg-weak">{
381                                                             /** translator: status of the server: will be created when user click "Save" */
382                                                             c('Server-Info').t`to be created`
383                                                         }</span>
384                                                     ),
385                                                     1 + Number(showIP) + Number(showLoad)
386                                                 ),
387                                                 <Button
388                                                     icon
389                                                     size="small"
390                                                     key="delete"
391                                                     onClick={() => {
392                                                         decreaseQuantities({ [locationId]: 1 });
393                                                     }}
394                                                 >
395                                                     <Icon name="trash" />
396                                                 </Button>,
397                                             ]}
398                                         />
399                                     );
400                                 })
401                             )}
402                         </TableBody>
403                     </Table>
404                     {availableAddedCount >= 1 ? (
405                         <Button shape="ghost" className="color-primary" onClick={addServers}>
406                             <Icon name="plus-circle-filled" className="mr-2" />
407                             {c('Info').ngettext(
408                                 msgid`Add server (${availableAddedCount} available)`,
409                                 `Add server (${availableAddedCount} available)`,
410                                 availableAddedCount
411                             )}
412                         </Button>
413                     ) : (
414                         ownedCount < MAX_IPS_ADDON && (
415                             <Button
416                                 shape="ghost"
417                                 className="color-primary"
418                                 onClick={() => {
419                                     onUpsell();
420                                     rest.onClose?.();
421                                 }}
422                             >
423                                 <Icon name="plus-circle-filled" className="mr-2" />
424                                 {c('Action').t`Get more servers`}
425                             </Button>
426                         )
427                     )}
428                 </ModalTwoContent>
429                 <ModalTwoFooter>
430                     {showCancelButton ? (
431                         <Button color="weak" onClick={rest.onClose}>
432                             {c('Action').t`Cancel`}
433                         </Button>
434                     ) : (
435                         <div />
436                     )}
437                     <Button color={newNumberOfServers ? 'norm' : 'danger'} type="submit" loading={loading}>
438                         {newNumberOfServers ? c('Action').t`Save` : c('Action').t`Delete Gateway`}
439                     </Button>
440                 </ModalTwoFooter>
441             </ModalTwo>
442             {addServersModal}
443             {removeServerConfirmation}
444         </>
445     );
448 export default GatewayServersModal;