Merge branch 'fix/isloading-photos' into 'main'
[ProtonMail-WebClient.git] / packages / components / containers / b2bDashboard / Pass / PassEvents.tsx
blobf17103abab994529b00cb35303af04e90e1f3f2b
1 import { useEffect, useState } from 'react';
3 import { endOfDay, isAfter, isBefore, startOfDay } from 'date-fns';
4 import { c } from 'ttag';
6 import Icon from '@proton/components/components/icon/Icon';
7 import Pagination from '@proton/components/components/pagination/Pagination';
8 import usePaginationAsync from '@proton/components/components/pagination/usePaginationAsync';
9 import PassEventsTable from '@proton/components/containers/b2bDashboard/Pass/PassEventsTable';
10 import useApi from '@proton/components/hooks/useApi';
11 import useErrorHandler from '@proton/components/hooks/useErrorHandler';
12 import useNotifications from '@proton/components/hooks/useNotifications';
13 import { useLoading } from '@proton/hooks';
14 import { getPassEventTypes, getPassLogs, getPassLogsDownload } from '@proton/shared/lib/api/b2bevents';
15 import { SORT_DIRECTION } from '@proton/shared/lib/constants';
16 import type { B2BLogsQuery } from '@proton/shared/lib/interfaces/B2BLogs';
17 import noop from '@proton/utils/noop';
19 import SettingsSectionWide from '../../../containers/account/SettingsSectionWide';
20 import GenericError from '../../../containers/error/GenericError';
21 import { toCamelCase } from '../../credentialLeak/helpers';
22 import { FilterAndSortEventsBlock } from '../FilterAndSortEventBlock';
23 import type { EventObject } from './helpers';
24 import {
25     ALL_EVENTS_DEFAULT,
26     PAGINATION_LIMIT,
27     downloadPassEvents,
28     getConnectionEvents,
29     getLocalTimeStringFromDate,
30     getSearchType,
31 } from './helpers';
32 import type { PassEvent } from './interface';
34 export interface FilterModel {
35     eventType: string;
36     start: Date | undefined;
37     end: Date | undefined;
39 const initialFilter = {
40     eventType: ALL_EVENTS_DEFAULT,
41     start: undefined,
42     end: undefined,
45 const getQueryParams = (filter: FilterModel, searchType: 'ip' | 'email' | 'empty', keyword: string) => {
46     const { eventType, start, end } = filter;
47     const Event = eventType === ALL_EVENTS_DEFAULT ? undefined : eventType;
48     const StartTime = start ? getLocalTimeStringFromDate(start) : undefined;
49     const EndTime = end ? getLocalTimeStringFromDate(endOfDay(end)) : undefined;
50     const Email = searchType === 'email' ? keyword : undefined;
51     const Ip = searchType === 'ip' ? keyword : undefined;
52     return { Email, Ip, Event, StartTime, EndTime };
55 export const PassEvents = () => {
56     const api = useApi();
57     const handleError = useErrorHandler();
58     const { page, onNext, onPrevious, onSelect, reset } = usePaginationAsync(1);
59     const { createNotification } = useNotifications();
60     const [loading, withLoading] = useLoading();
61     const [filter, setFilter] = useState<FilterModel>(initialFilter);
62     const [events, setEvents] = useState<PassEvent[]>([]);
63     const [connectionEvents, setConnectionEvents] = useState<EventObject[]>([]);
64     const [keyword, setKeyword] = useState<string>('');
65     const [total, setTotal] = useState<number>(0);
66     const [query, setQuery] = useState({});
67     const [error, setError] = useState<string | null>(null);
68     const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.DESC);
70     const fetchPassLogs = async (params: B2BLogsQuery) => {
71         try {
72             const { Items, Total } = await api(getPassLogs(params));
73             const passEvents = toCamelCase(Items);
74             setEvents(passEvents);
75             setTotal(Total | 0);
76         } catch (e) {
77             handleError(e);
78             setError(c('Error').t`Please try again in a few moments.`);
79         }
80     };
82     const fetchPassConnectionEvents = async () => {
83         const { Items } = await api<{ Items: EventObject[] }>(getPassEventTypes());
84         const filteredItems = Items.filter((item: EventObject) => item.EventType !== '' && item.EventTypeName !== '');
85         const sortedItems = filteredItems.sort((a: EventObject, b: EventObject) =>
86             a.EventTypeName.localeCompare(b.EventTypeName)
87         );
88         setConnectionEvents(sortedItems);
89     };
91     useEffect(() => {
92         fetchPassConnectionEvents();
93     }, []);
95     useEffect(() => {
96         withLoading(
97             fetchPassLogs({
98                 ...query,
99                 Page: page - 1,
100                 Sort: sortDirection === SORT_DIRECTION.DESC ? 'desc' : 'asc',
101             }).catch(noop)
102         );
103     }, [page, query, sortDirection]);
105     const handleSearchSubmit = () => {
106         setError(null);
107         const searchType = getSearchType(keyword);
108         if (searchType === 'invalid') {
109             createNotification({ text: c('Notification').t`Invalid input. Search an email or IP address.` });
110             return;
111         }
112         const queryParams = getQueryParams(filter, searchType, keyword);
113         setQuery({ ...queryParams });
114         reset();
115     };
117     const handleDownloadClick = async () => {
118         const response = await api({
119             ...getPassLogsDownload({ ...query, Page: page - 1 }),
120             output: 'raw',
121         });
123         const responseCode = response.headers.get('x-pm-code') || '';
124         if (response.status === 429) {
125             createNotification({
126                 text: c('Notification').t`Too many recent API requests`,
127             });
128         } else if (responseCode !== '1000') {
129             createNotification({
130                 text: c('Notification').t`Number of records exceeds the download limit of 10000`,
131             });
132         }
134         downloadPassEvents(response);
135     };
137     const handleStartDateChange = (start: Date | undefined) => {
138         if (!start) {
139             return;
140         }
141         if (!filter.end || isBefore(start, filter.end)) {
142             setFilter({ ...filter, start });
143         } else {
144             setFilter({ ...filter, start, end: start });
145         }
146     };
148     const handleEndDateChange = (end: Date | undefined) => {
149         if (!end) {
150             return;
151         }
152         if (!filter.start || isAfter(end, filter.start)) {
153             setFilter({ ...filter, end });
154         } else {
155             setFilter({ ...filter, start: end, end });
156         }
157     };
159     const handleSetEventType = (eventType: string) => {
160         setFilter({ ...filter, eventType });
161     };
163     const handleClickableEvent = (eventType: string) => {
164         setFilter({ ...filter, eventType });
165         setQuery({ ...query, Event: eventType });
166         reset();
167     };
169     const handleClickableTime = (time: string) => {
170         const date = new Date(time);
171         const start = getLocalTimeStringFromDate(startOfDay(date));
172         const end = getLocalTimeStringFromDate(endOfDay(date));
173         setFilter({ ...filter, start: date, end: date });
174         setQuery({ ...query, StartTime: start, EndTime: end });
175         reset();
176     };
178     const handleClickableEmailOrIP = (keyword: string) => {
179         const searchType = getSearchType(keyword);
181         if (searchType !== 'email' && searchType !== 'ip') {
182             return;
183         }
185         const updatedQuery = { ...query, [searchType === 'email' ? 'Email' : 'Ip']: keyword };
186         setQuery(updatedQuery);
187         setKeyword(keyword);
188         reset();
189     };
191     const handleToggleSort = (direction: SORT_DIRECTION) => {
192         setSortDirection(direction);
193     };
195     const handleResetFilter = () => {
196         setError(null);
197         setFilter(initialFilter);
198         setKeyword('');
199         setQuery({});
200         reset();
201     };
203     return (
204         <SettingsSectionWide customWidth="90em">
205             <FilterAndSortEventsBlock
206                 filter={filter}
207                 keyword={keyword}
208                 setKeyword={setKeyword}
209                 handleSetEventType={handleSetEventType}
210                 handleStartDateChange={handleStartDateChange}
211                 handleEndDateChange={handleEndDateChange}
212                 eventTypesList={getConnectionEvents(connectionEvents) || []}
213                 handleSearchSubmit={handleSearchSubmit}
214                 handleDownloadClick={handleDownloadClick}
215                 resetFilter={handleResetFilter}
216                 hasFilterEvents={true}
217             />
218             <div className="content-center my-3">
219                 <Icon name="info-circle" size={4.5} className="mr-1 mb-1" />
220                 <span>{c('Title').t`Click a value in the table to use it as filter`}</span>
221             </div>
222             {error ? (
223                 <GenericError className="text-center">{error}</GenericError>
224             ) : (
225                 <div className="flex justify-center">
226                     <PassEventsTable
227                         events={events}
228                         loading={loading}
229                         onEventClick={handleClickableEvent}
230                         onTimeClick={handleClickableTime}
231                         onEmailOrIpClick={handleClickableEmailOrIP}
232                         onToggleSort={handleToggleSort}
233                     />
234                     <Pagination
235                         page={page}
236                         total={total}
237                         limit={PAGINATION_LIMIT}
238                         onSelect={onSelect}
239                         onNext={onNext}
240                         onPrevious={onPrevious}
241                     />
242                 </div>
243             )}
244         </SettingsSectionWide>
245     );