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';
29 getLocalTimeStringFromDate,
32 import type { PassEvent } from './interface';
34 export interface FilterModel {
36 start: Date | undefined;
37 end: Date | undefined;
39 const initialFilter = {
40 eventType: ALL_EVENTS_DEFAULT,
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 = () => {
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) => {
72 const { Items, Total } = await api(getPassLogs(params));
73 const passEvents = toCamelCase(Items);
74 setEvents(passEvents);
78 setError(c('Error').t`Please try again in a few moments.`);
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)
88 setConnectionEvents(sortedItems);
92 fetchPassConnectionEvents();
100 Sort: sortDirection === SORT_DIRECTION.DESC ? 'desc' : 'asc',
103 }, [page, query, sortDirection]);
105 const handleSearchSubmit = () => {
107 const searchType = getSearchType(keyword);
108 if (searchType === 'invalid') {
109 createNotification({ text: c('Notification').t`Invalid input. Search an email or IP address.` });
112 const queryParams = getQueryParams(filter, searchType, keyword);
113 setQuery({ ...queryParams });
117 const handleDownloadClick = async () => {
118 const response = await api({
119 ...getPassLogsDownload({ ...query, Page: page - 1 }),
123 const responseCode = response.headers.get('x-pm-code') || '';
124 if (response.status === 429) {
126 text: c('Notification').t`Too many recent API requests`,
128 } else if (responseCode !== '1000') {
130 text: c('Notification').t`Number of records exceeds the download limit of 10000`,
134 downloadPassEvents(response);
137 const handleStartDateChange = (start: Date | undefined) => {
141 if (!filter.end || isBefore(start, filter.end)) {
142 setFilter({ ...filter, start });
144 setFilter({ ...filter, start, end: start });
148 const handleEndDateChange = (end: Date | undefined) => {
152 if (!filter.start || isAfter(end, filter.start)) {
153 setFilter({ ...filter, end });
155 setFilter({ ...filter, start: end, end });
159 const handleSetEventType = (eventType: string) => {
160 setFilter({ ...filter, eventType });
163 const handleClickableEvent = (eventType: string) => {
164 setFilter({ ...filter, eventType });
165 setQuery({ ...query, Event: eventType });
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 });
178 const handleClickableEmailOrIP = (keyword: string) => {
179 const searchType = getSearchType(keyword);
181 if (searchType !== 'email' && searchType !== 'ip') {
185 const updatedQuery = { ...query, [searchType === 'email' ? 'Email' : 'Ip']: keyword };
186 setQuery(updatedQuery);
191 const handleToggleSort = (direction: SORT_DIRECTION) => {
192 setSortDirection(direction);
195 const handleResetFilter = () => {
197 setFilter(initialFilter);
204 <SettingsSectionWide customWidth="90em">
205 <FilterAndSortEventsBlock
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}
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>
223 <GenericError className="text-center">{error}</GenericError>
225 <div className="flex justify-center">
229 onEventClick={handleClickableEvent}
230 onTimeClick={handleClickableTime}
231 onEmailOrIpClick={handleClickableEmailOrIP}
232 onToggleSort={handleToggleSort}
237 limit={PAGINATION_LIMIT}
240 onPrevious={onPrevious}
244 </SettingsSectionWide>