Remove payments API routing initialization
[ProtonMail-WebClient.git] / packages / components / containers / b2bDashboard / VPN / VPNEvents.tsx
blob4049d7c2b36dbf7d95578d5a5e24ed24ad0028fc
1 import type { ReactNode } from 'react';
2 import { useEffect, useState } from 'react';
4 import { endOfDay, isAfter, isBefore, startOfDay } from 'date-fns';
5 import { c } from 'ttag';
7 import { useUserSettings } from '@proton/account/userSettings/hooks';
8 import { Pagination, usePaginationAsync } from '@proton/components';
9 import Icon from '@proton/components/components/icon/Icon';
10 import useModalState from '@proton/components/components/modalTwo/useModalState';
11 import TimeIntl from '@proton/components/components/time/TimeIntl';
12 import Toggle from '@proton/components/components/toggle/Toggle';
13 import SettingsSectionWide from '@proton/components/containers/account/SettingsSectionWide';
14 import useApi from '@proton/components/hooks/useApi';
15 import useErrorHandler from '@proton/components/hooks/useErrorHandler';
16 import useNotifications from '@proton/components/hooks/useNotifications';
17 import { useLoading } from '@proton/hooks';
18 import { getVPNLogDownload, getVPNLogs, getVpnEventTypes } from '@proton/shared/lib/api/b2bevents';
19 import { SORT_DIRECTION } from '@proton/shared/lib/constants';
20 import type { B2BLogsQuery } from '@proton/shared/lib/interfaces/B2BLogs';
21 import noop from '@proton/utils/noop';
23 import { getCountryOptions } from '../../../helpers/countries';
24 import { toCamelCase } from '../../credentialLeak/helpers';
25 import GenericError from '../../error/GenericError';
26 import { FilterAndSortEventsBlock } from '../FilterAndSortEventBlock';
27 import { ALL_EVENTS_DEFAULT, PAGINATION_LIMIT, getLocalTimeStringFromDate, getSearchType } from '../Pass/helpers';
28 import TogglingMonitoringModal from './TogglingMonitoringModal';
29 import VPNEventsTable from './VPNEventsTable';
30 import type { OrganizationSettings } from './api';
31 import { getMonitoringSetting, updateMonitoringSetting } from './api';
32 import type { Event as EventObject } from './helpers';
33 import { downloadVPNEvents, getConnectionEvents } from './helpers';
34 import type { VPNEvent } from './interface';
36 export interface FilterModel {
37     eventType: string;
38     start: Date | undefined;
39     end: Date | undefined;
42 const initialFilter = {
43     eventType: ALL_EVENTS_DEFAULT,
44     start: undefined,
45     end: undefined,
48 const getQueryParams = (filter: FilterModel, searchType: 'ip' | 'email' | 'empty', keyword: string) => {
49     const { eventType, start, end } = filter;
50     const Event = eventType === ALL_EVENTS_DEFAULT ? undefined : eventType;
51     const StartTime = start ? getLocalTimeStringFromDate(start) : undefined;
52     const EndTime = end ? getLocalTimeStringFromDate(endOfDay(end)) : undefined;
53     const Email = searchType === 'email' ? keyword : undefined;
54     const Ip = searchType === 'ip' ? keyword : undefined;
55     return { Email, Ip, Event, StartTime, EndTime };
58 export const VPNEvents = () => {
59     const api = useApi();
60     const handleError = useErrorHandler();
61     const [userSettings] = useUserSettings();
63     const countryOptions = getCountryOptions(userSettings);
65     const { page, onNext, onPrevious, onSelect, reset } = usePaginationAsync(1);
66     const { createNotification } = useNotifications();
67     const [loading, withLoading] = useLoading();
68     // const [downloading, withDownloading] = useLoading();
69     const [filter, setFilter] = useState<FilterModel>(initialFilter);
70     const [events, setEvents] = useState<VPNEvent[] | []>([]);
71     const [connectionEvents, setConnectionEvents] = useState([]);
72     const [keyword, setKeyword] = useState<string>('');
73     const [total, setTotal] = useState<number>(0);
74     const [monitoringLoading, setMonitoringLoading] = useState<boolean>(true);
75     const [monitoringEnabling, setMonitoringEnabling] = useState<boolean>(false);
76     const [monitoring, setMonitoring] = useState<boolean>(false);
77     const [monitoringLastChange, setMonitoringLastChange] = useState<number | null>(null);
78     const [togglingMonitoringModalProps, setTogglingMonitoringModalOpen, togglingMonitoringModalRender] =
79         useModalState();
80     const [businessSettingsAvailable, setBusinessSettingsAvailable] = useState<boolean>(true);
81     const [togglingMonitoringLoading, setTogglingMonitoringLoading] = useState<boolean>(false);
82     const [togglingMonitoringInitializing, withMonitoringInitializing] = useLoading();
83     const [query, setQuery] = useState({});
84     const [error, setError] = useState<string | null>(null);
85     const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.DESC);
87     const fetchVpnConnectionEvents = async () => {
88         const { Items } = await api(getVpnEventTypes());
89         const filteredItems = Items.filter((item: EventObject) => item.EventType !== '' && item.EventTypeName !== '');
90         const sortedItems = filteredItems.sort((a: EventObject, b: EventObject) =>
91             a.EventTypeName.localeCompare(b.EventTypeName)
92         );
93         sortedItems.forEach((item: { EventType: string; EventTypeName: string }) => {
94             if (item.EventType === 'session_roaming') {
95                 item.EventTypeName = 'Network change';
96             }
97         });
98         setConnectionEvents(sortedItems);
99     };
101     useEffect(() => {
102         fetchVpnConnectionEvents();
103     }, []);
105     const fetchVPNLogs = async (params: B2BLogsQuery) => {
106         try {
107             const { Items, Total } = await api(
108                 getVPNLogs({ ...params, Sort: sortDirection === SORT_DIRECTION.DESC ? 'desc' : 'asc' })
109             );
110             const connectionEvents = toCamelCase(Items);
111             connectionEvents.forEach((item: VPNEvent) => {
112                 if (item.eventType === 'session_roaming') {
113                     item.eventTypeName = 'Network change';
114                 }
115             });
117             setEvents(connectionEvents);
118             setTotal(Total | 0);
119         } catch (e) {
120             handleError(e);
121             setError(c('Error').t`Please try again in a few moments.`);
122         }
123     };
125     useEffect(() => {
126         withLoading(fetchVPNLogs({ ...query, Page: page - 1 }).catch(noop));
127     }, [page, query, sortDirection]);
129     useEffect(() => {
130         withMonitoringInitializing(
131             new Promise(async (resolve) => {
132                 const timeout = setTimeout(() => {
133                     setMonitoringLoading(false);
134                     setMonitoringLastChange(null);
135                     setBusinessSettingsAvailable(false);
136                     resolve();
137                 }, 10_000);
139                 try {
140                     const organizationSettings = await api<OrganizationSettings>(getMonitoringSetting());
142                     if (organizationSettings.GatewayMonitoring) {
143                         setMonitoring(true);
144                     }
146                     setMonitoringLastChange(organizationSettings.GatewayMonitoringLastUpdate || null);
147                 } catch (e) {
148                     setBusinessSettingsAvailable(false);
149                     setMonitoringLastChange(null);
150                 } finally {
151                     setMonitoringLoading(false);
152                     clearTimeout(timeout);
153                     resolve();
154                 }
155             })
156         );
157     }, []);
159     const handleSearchSubmit = () => {
160         setError(null);
161         const searchType = getSearchType(keyword);
162         if (searchType === 'invalid') {
163             createNotification({ text: c('Notification').t`Invalid input. Search an email or IP address.` });
164             return;
165         }
166         const queryParams = getQueryParams(filter, searchType, keyword);
167         setQuery({ ...queryParams });
168         reset();
169     };
171     const handleDownloadClick = async () => {
172         const response = await api({
173             ...getVPNLogDownload({ ...query, Page: page - 1 }),
174             output: 'raw',
175         });
177         const responseCode = response.headers.get('x-pm-code') || '';
178         if (response.status === 429) {
179             createNotification({
180                 text: c('Notification').t`Too many recent API requests`,
181             });
182         } else if (responseCode !== '1000') {
183             createNotification({
184                 text: c('Notification').t`Number of records exceeds the download limit of 10000`,
185             });
186         }
188         downloadVPNEvents(response);
189     };
191     const handleStartDateChange = (start: Date | undefined) => {
192         if (!start) {
193             return;
194         }
195         if (!filter.end || isBefore(start, filter.end)) {
196             setFilter({ ...filter, start });
197         } else {
198             setFilter({ ...filter, start, end: start });
199         }
200     };
202     const handleEndDateChange = (end: Date | undefined) => {
203         if (!end) {
204             return;
205         }
206         if (!filter.start || isAfter(end, filter.start)) {
207             setFilter({ ...filter, end });
208         } else {
209             setFilter({ ...filter, start: end, end });
210         }
211     };
213     const handleSetEventType = (eventType: string) => {
214         setFilter({ ...filter, eventType });
215     };
217     const handleClickableEvent = (eventType: string) => {
218         setFilter({ ...filter, eventType });
219         setQuery({ ...query, Event: eventType });
220         reset();
221     };
223     const handleClickableTime = (time: string) => {
224         const date = new Date(time);
225         const start = getLocalTimeStringFromDate(startOfDay(date));
226         const end = getLocalTimeStringFromDate(endOfDay(date));
228         setFilter({ ...filter, start: date, end: date });
229         setQuery({ ...query, StartTime: start, EndTime: end });
230         reset();
231     };
233     const handleClickableEmailOrIP = (keyword: string) => {
234         const searchType = getSearchType(keyword);
236         if (searchType !== 'email' && searchType !== 'ip') {
237             return;
238         }
240         const updatedQuery = { ...query, [searchType === 'email' ? 'Email' : 'Ip']: keyword };
241         setQuery(updatedQuery);
242         setKeyword(keyword);
243         reset();
244     };
246     const setGatewayMonitoring = async () => {
247         try {
248             const enabling = !monitoring;
249             const newSettings = await api<OrganizationSettings>(updateMonitoringSetting(enabling));
251             setMonitoring(enabling);
252             setMonitoringLastChange(newSettings.GatewayMonitoringLastUpdate || Date.now() / 1000);
253             setTogglingMonitoringModalOpen(false);
254             setTogglingMonitoringLoading(false);
255         } catch (e) {
256             setTogglingMonitoringModalOpen(false);
258             throw e;
259         } finally {
260             setTogglingMonitoringLoading(false);
261         }
262     };
264     const toggleMonitoring = async () => {
265         if (togglingMonitoringLoading || togglingMonitoringInitializing) {
266             return;
267         }
269         const enabling = !monitoring;
270         setMonitoringEnabling(enabling);
271         setTogglingMonitoringModalOpen(true);
272     };
274     const getMonitoringInfoText = (): string => {
275         if (monitoringLoading) {
276             return c('Info').t`Loading gateways monitoring current status.`;
277         }
279         if (!businessSettingsAvailable) {
280             return c('Info').t`Unable to check gateways monitoring current status.`;
281         }
283         return monitoring
284             ? c('Info').t`Gateways monitoring is enabled.`
285             : c('Info').t`Gateways monitoring is disabled.`;
286     };
288     const getMonitoringLastChangeText = (): ReactNode => {
289         if (!monitoringLastChange) {
290             return <>&nbsp;</>;
291         }
293         const formattedDateAndTime = (
294             <TimeIntl
295                 options={{
296                     year: 'numeric',
297                     day: 'numeric',
298                     month: 'short',
299                     hour: 'numeric',
300                     minute: 'numeric',
301                 }}
302             >
303                 {monitoringLastChange}
304             </TimeIntl>
305         );
307         return monitoring
308             ? /** translator: formattedDateAndTime be like "25 Sep 2023, 15:37" or just "15:37" if it's on the same day */ c(
309                   'Info'
310               ).jt`Enabled on ${formattedDateAndTime}`
311             : /** translator: formattedDateAndTime be like "25 Sep 2023, 15:37" or just "15:37" if it's on the same day */ c(
312                   'Info'
313               ).jt`Disabled on ${formattedDateAndTime}`;
314     };
316     const handleToggleSort = (direction: SORT_DIRECTION) => {
317         setSortDirection(direction);
318     };
320     const handleResetFilter = () => {
321         setError(null);
322         setFilter(initialFilter);
323         setKeyword('');
324         setQuery({});
325         reset();
326     };
328     return (
329         <SettingsSectionWide customWidth="90em">
330             {togglingMonitoringModalRender && (
331                 <TogglingMonitoringModal
332                     {...togglingMonitoringModalProps}
333                     enabling={monitoringEnabling}
334                     onChange={setGatewayMonitoring}
335                 />
336             )}
337             <div className="mb-8 flex *:min-size-auto flex-column sm:flex-row gap-2">
338                 <div className="flex flex-row flex-nowrap items-center gap-2">
339                     <Toggle
340                         loading={togglingMonitoringLoading || togglingMonitoringInitializing}
341                         checked={monitoring}
342                         disabled={
343                             !togglingMonitoringLoading && !togglingMonitoringInitializing && !businessSettingsAvailable
344                         }
345                         onChange={toggleMonitoring}
346                     />
347                     <span>
348                         <span className="text-bold">{getMonitoringInfoText()}</span>
349                         <span className="block color-weak text-sm">{getMonitoringLastChangeText()}</span>
350                     </span>
351                 </div>
352             </div>
354             <FilterAndSortEventsBlock
355                 filter={filter}
356                 keyword={keyword}
357                 setKeyword={setKeyword}
358                 handleSetEventType={handleSetEventType}
359                 handleStartDateChange={handleStartDateChange}
360                 handleEndDateChange={handleEndDateChange}
361                 handleDownloadClick={handleDownloadClick}
362                 eventTypesList={getConnectionEvents(connectionEvents)}
363                 handleSearchSubmit={handleSearchSubmit}
364                 hasFilterEvents={true}
365                 resetFilter={handleResetFilter}
366             />
367             {error ? (
368                 <GenericError className="text-center">{error}</GenericError>
369             ) : (
370                 <>
371                     <div className="content-center my-3">
372                         <Icon name="info-circle" size={4.5} className="mr-1 mb-1" />
373                         <span>{c('Title').t`Click a value in the table to use it as filter`}</span>
374                     </div>
375                     <div className="flex justify-center">
376                         <VPNEventsTable
377                             events={events}
378                             loading={loading}
379                             onTimeClick={handleClickableTime}
380                             onEventClick={handleClickableEvent}
381                             onEmailOrIpClick={handleClickableEmailOrIP}
382                             onToggleSort={handleToggleSort}
383                             countryOptions={countryOptions}
384                         />
385                         <Pagination
386                             page={page}
387                             total={total}
388                             limit={PAGINATION_LIMIT}
389                             onSelect={onSelect}
390                             onNext={onNext}
391                             onPrevious={onPrevious}
392                         />
393                     </div>
394                 </>
395             )}
396         </SettingsSectionWide>
397     );