Merge branch 'fix/isloading-photos' into 'main'
[ProtonMail-WebClient.git] / packages / components / containers / b2bDashboard / Pass / PassEventsTable.tsx
blob08eaf737eb7e13c135d660dd3a13cf5fa83bc08c
1 import { useEffect, useRef, useState } from 'react';
3 import { c } from 'ttag';
5 import { Avatar } from '@proton/atoms';
6 import AppLink from '@proton/components/components/link/AppLink';
7 import { SortingTableHeader } from '@proton/components/components/table/SortingTableHeader';
8 import Table from '@proton/components/components/table/Table';
9 import TableBody from '@proton/components/components/table/TableBody';
10 import TableRow from '@proton/components/components/table/TableRow';
11 import Time from '@proton/components/components/time/Time';
12 import Tooltip from '@proton/components/components/tooltip/Tooltip';
13 import useApi from '@proton/components/hooks/useApi';
14 import { getShareID } from '@proton/shared/lib/api/b2bevents';
15 import { APPS, SORT_DIRECTION } from '@proton/shared/lib/constants';
16 import { getInitials } from '@proton/shared/lib/helpers/string';
18 import { getDesciptionText, getDescriptionTextWithLink } from './helpers';
19 import type { PassEvent } from './interface';
21 interface Props {
22     events: PassEvent[];
23     loading: boolean;
24     onEventClick: (event: string) => void;
25     onTimeClick: (time: string) => void;
26     onEmailOrIpClick: (keyword: string) => void;
27     onToggleSort: (direction: SORT_DIRECTION) => void;
30 interface DescriptionProps {
31     shareId: string | null;
32     itemId: string | null;
33     event: string;
34     hasInvalidShareId: boolean;
37 interface SortConfig {
38     key: keyof PassEvent;
39     direction: SORT_DIRECTION;
42 const Description = ({ shareId, itemId, event, hasInvalidShareId }: DescriptionProps) => {
43     if (hasInvalidShareId) {
44         return (
45             <Tooltip title={c('Info').t`Vault either deleted or requires access permissions`}>
46                 <span className="">{getDesciptionText(event)}</span>
47             </Tooltip>
48         );
49     }
50     if (itemId) {
51         const vaultLink = (
52             <AppLink key="link" toApp={APPS.PROTONPASS} to={`/share/${shareId}/item/${itemId}`}>{c('Link')
53                 .t`Vault`}</AppLink>
54         );
55         return <span>{getDescriptionTextWithLink(event, vaultLink)}</span>;
56     }
57     if (shareId) {
58         const vaultLink = (
59             <AppLink key="link" toApp={APPS.PROTONPASS} to={`/share/${shareId}`}>{c('Link').t`Vault`}</AppLink>
60         );
61         return <span>{getDescriptionTextWithLink(event, vaultLink)}</span>;
62     }
63     return (
64         <div className="flex flex-column">
65             <span className="">{getDesciptionText(event)}</span>
66         </div>
67     );
70 const PassEventsTable = ({ events, loading, onEventClick, onTimeClick, onEmailOrIpClick, onToggleSort }: Props) => {
71     const api = useApi();
72     const [shareIds, setShareIds] = useState<{ [key: string]: string | null }>({});
73     const [invalidShareIds, setInvalidShareIds] = useState<Set<string>>(new Set());
74     const cache = useRef<{ [key: string]: string | null }>({});
76     const vaultIds = [...new Set(events.map((event) => event.eventData?.vaultId))];
78     useEffect(() => {
79         const fetchShareIds = async () => {
80             const idsMap: { [key: string]: string | null } = {};
81             const newInvalidIds = new Set(invalidShareIds);
83             await Promise.all(
84                 vaultIds.map(async (vaultId) => {
85                     if (cache.current[vaultId]) {
86                         idsMap[vaultId] = cache.current[vaultId];
87                         newInvalidIds.delete(vaultId);
88                     } else {
89                         try {
90                             const { Share } = await api(getShareID(vaultId));
91                             idsMap[vaultId] = Share.ShareID;
92                             cache.current[vaultId] = Share.ShareID;
93                             newInvalidIds.delete(vaultId);
94                         } catch (e) {
95                             newInvalidIds.add(vaultId);
96                         }
97                     }
98                 })
99             );
100             setShareIds((prev) => ({ ...prev, ...idsMap }));
101             setInvalidShareIds(newInvalidIds);
102         };
104         if (vaultIds.length > 0) {
105             fetchShareIds();
106         }
107     }, [events, api]);
109     const [sortConfig, setSortConfig] = useState<SortConfig>({
110         key: 'time',
111         direction: SORT_DIRECTION.DESC,
112     });
114     const toggleSort = () => {
115         setSortConfig({
116             ...sortConfig,
117             direction: sortConfig.direction === SORT_DIRECTION.ASC ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC,
118         });
119         onToggleSort(sortConfig.direction);
120     };
122     return (
123         <Table responsive="cards">
124             <SortingTableHeader
125                 config={sortConfig}
126                 onToggleSort={toggleSort}
127                 cells={[
128                     { content: c('Title').t`User`, className: 'w-1/4' },
129                     { key: 'time', content: c('TableHeader').t`Event`, sorting: true, className: 'w-1/4' },
130                     { content: c('Title').t`Description`, className: 'w-1/4' },
131                     { content: c('Title').t`IP`, className: 'w-1/4' },
132                 ]}
133             />
134             <TableBody colSpan={5} loading={loading}>
135                 {events.map(({ time, user, eventType, eventTypeName, ip, eventData }, index) => {
136                     const { name, email } = user;
137                     const { vaultId, itemId } = eventData;
138                     const key = index;
140                     const unixTime = new Date(time).getTime() / 1000;
141                     const shareId = shareIds[vaultId];
142                     const hasInvalidShareId = vaultId ? invalidShareIds.has(vaultId) : false;
143                     const initials = name ? getInitials(name) : email.charAt(0);
145                     return (
146                         <TableRow
147                             key={key}
148                             cells={[
149                                 <div className="flex flex-row items-center my-2">
150                                     <Avatar className="mr-2" color="weak">
151                                         {initials}
152                                     </Avatar>
153                                     <div
154                                         className="flex flex-column cursor-pointer w-2/3"
155                                         onClick={() => onEmailOrIpClick(email)}
156                                     >
157                                         <span title={name} className="text-ellipsis max-w-full">
158                                             {name}
159                                         </span>
160                                         <span title={email} className="color-weak text-ellipsis max-w-full">
161                                             {email}
162                                         </span>
163                                     </div>
164                                 </div>,
165                                 <div className="flex flex-column cursor-pointer">
166                                     <div
167                                         className="flex flex-row mb-1 text-semibold"
168                                         onClick={() => onEventClick(eventType)}
169                                     >
170                                         {eventTypeName}
171                                     </div>
172                                     <Time format="PPp" className="color-weak" onClick={() => onTimeClick(time)}>
173                                         {unixTime}
174                                     </Time>
175                                 </div>,
176                                 <Description
177                                     shareId={shareId}
178                                     itemId={itemId}
179                                     event={eventType}
180                                     hasInvalidShareId={hasInvalidShareId}
181                                 />,
182                                 <span onClick={() => onEmailOrIpClick(ip)} className="cursor-pointer">
183                                     {ip}
184                                 </span>,
185                             ]}
186                         />
187                     );
188                 })}
189             </TableBody>
190         </Table>
191     );
194 export default PassEventsTable;