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> {
34 countries: readonly string[];
35 locations: readonly GatewayLocation[];
36 countryOptions: CountryOptions;
37 deletedInCountries: Record<string, number>;
38 users: readonly GatewayUser[];
41 showDeleted?: boolean;
45 showCancelButton?: boolean;
46 singleServer?: boolean;
47 isDeleted: (logical: GatewayLogical) => boolean;
48 onSubmitDone: (deletedLogicalIds: readonly string[], addedQuantities: Record<string, number>) => Promise<void>;
52 const GatewayServersModal = ({
65 showCancelButton = false,
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;
94 newAdded[country] = newQuantity;
99 delete newAdded[country];
105 const addQuantities = (quantities: Record<string, number>) => {
106 const entries = Object.entries(quantities);
108 if (!entries.length) {
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])
119 const recoverableQuantity = serversFromTrash.length;
121 if (recoverableQuantity) {
122 deletedIds = deletedIds.filter((id) => serversFromTrash.indexOf(id) === -1);
125 const addedQuantity = quantity - recoverableQuantity;
127 if (addedQuantity > 0) {
128 newAdded[country] = (newAdded[country] || 0) + addedQuantity;
132 const newDeleted: Record<string, true> = {};
134 deletedIds.forEach((id) => {
135 newDeleted[id] = true;
139 setDeleted(newDeleted);
142 const addServers = () =>
143 showAddServersModal({
148 usedCount: usedCount - deletedServerCount + addedServerCount,
153 onSubmitDone: addQuantities,
157 const handleDelete = (logical: GatewayLogical) => async () => {
158 const onRemoveConfirmed = () => {
159 if (added[logical.ExitCountry] > 0) {
160 decreaseQuantities({ [logical.ExitCountry]: 1 });
167 [logical.ID]: !deleted[logical.ID],
171 if (!deleted[logical.ID]) {
172 showRemoveServerConfirmation({ onSubmitDone: onRemoveConfirmed });
178 const handleSubmit = async () => {
179 const idsToDelete = Object.keys(deleted).filter((id) => deleted[id]);
181 if (getTotalAdded(added) < 1 && idsToDelete.length < 1) {
189 await onSubmitDone(idsToDelete, added);
198 <ModalTwo size="xlarge" {...rest} as={Form} onSubmit={handleSubmit}>
199 <ModalTwoHeader title={c('Title').t`Edit servers`} />
201 <Table className="my-2" responsive="cards">
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`}
209 <TableCell key="status" type="header" className="w-1/6">
210 {c('Header').t`Status`}
213 <TableCell key="ip" type="header" className="w-1/6">
214 {c('Header').t`IP address`}
218 <TableCell key="load" type="header" className="w-1/6">
219 {c('Header').t`Server load`}
222 <TableCell key="manage" type="header" className="w-1/10">
227 <TableBody colSpan={4 + Number(showIP) + Number(showLoad)} loading={loading}>
228 {gateway.Logicals.filter((l) => l.Servers?.length && l.Visible).map((logical) => (
230 key={'logical-' + logical.ID}
231 className={deleted[logical.ID] ? 'opacity-50' : undefined}
234 countryCode={logical.ExitCountry}
236 getLocalizedCountryByAbbr(logical.ExitCountry, countryOptions) +
242 getSuffix(logical.Name),
243 deleted[logical.ID] ? (
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`
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`
263 showIPv4 && logical.Servers[0].ExitIPv4,
264 showIPv6 && logical.Servers[0].ExitIPv6,
270 className="text-ellipsis"
271 title={ip || undefined}
279 ...(showLoad ? [getFormattedLoad(logical.Servers)] : []),
280 <Button icon size="small" key="delete" onClick={handleDelete(logical)}>
281 <Icon name="trash" />
287 gateway.Logicals.filter((l) => l.Servers?.length && !l.Visible).map((logical) => (
289 key={'logical-' + logical.ID}
290 className="opacity-50"
293 countryCode={logical.ExitCountry}
295 getLocalizedCountryByAbbr(logical.ExitCountry, countryOptions) +
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`
309 {[logical.Servers[0].ExitIPv4, logical.Servers[0].ExitIPv6]
312 <div key={'ip-' + ip}>{ip}</div>
317 ...(showLoad ? [getFormattedLoad(logical.Servers)] : []),
322 {gateway.Logicals.filter((l) => !l.Servers?.length).map((logical) => (
324 key={'logical-' + logical.ID}
325 className={deleted[logical.ID] ? 'opacity-50' : undefined}
328 countryCode={logical.ExitCountry}
330 getLocalizedCountryByAbbr(logical.ExitCountry, countryOptions) +
336 getSuffix(logical.Name),
337 deleted[logical.ID] ? (
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`
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`
353 ...(showIP ? ['-'] : []),
354 ...(showLoad ? ['-'] : []),
355 <Button icon size="small" key="delete" onClick={handleDelete(logical)}>
356 <Icon name="trash" />
361 {Object.keys(added).map((locationId) =>
362 range(0, added[locationId]).map((index) => {
363 const location = getLocationFromId(locationId);
366 key={'future-logical-' + locationId + '-' + index}
369 countryCode={location.Country}
371 getLocalizedCountryByAbbr(location.Country, countryOptions) +
373 location.TranslatedCity
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`
385 1 + Number(showIP) + Number(showLoad)
392 decreaseQuantities({ [locationId]: 1 });
395 <Icon name="trash" />
404 {availableAddedCount >= 1 ? (
405 <Button shape="ghost" className="color-primary" onClick={addServers}>
406 <Icon name="plus-circle-filled" className="mr-2" />
408 msgid`Add server (${availableAddedCount} available)`,
409 `Add server (${availableAddedCount} available)`,
414 ownedCount < MAX_IPS_ADDON && (
417 className="color-primary"
423 <Icon name="plus-circle-filled" className="mr-2" />
424 {c('Action').t`Get more servers`}
430 {showCancelButton ? (
431 <Button color="weak" onClick={rest.onClose}>
432 {c('Action').t`Cancel`}
437 <Button color={newNumberOfServers ? 'norm' : 'danger'} type="submit" loading={loading}>
438 {newNumberOfServers ? c('Action').t`Save` : c('Action').t`Delete Gateway`}
443 {removeServerConfirmation}
448 export default GatewayServersModal;