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';
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;
34 hasInvalidShareId: boolean;
37 interface SortConfig {
39 direction: SORT_DIRECTION;
42 const Description = ({ shareId, itemId, event, hasInvalidShareId }: DescriptionProps) => {
43 if (hasInvalidShareId) {
45 <Tooltip title={c('Info').t`Vault either deleted or requires access permissions`}>
46 <span className="">{getDesciptionText(event)}</span>
52 <AppLink key="link" toApp={APPS.PROTONPASS} to={`/share/${shareId}/item/${itemId}`}>{c('Link')
55 return <span>{getDescriptionTextWithLink(event, vaultLink)}</span>;
59 <AppLink key="link" toApp={APPS.PROTONPASS} to={`/share/${shareId}`}>{c('Link').t`Vault`}</AppLink>
61 return <span>{getDescriptionTextWithLink(event, vaultLink)}</span>;
64 <div className="flex flex-column">
65 <span className="">{getDesciptionText(event)}</span>
70 const PassEventsTable = ({ events, loading, onEventClick, onTimeClick, onEmailOrIpClick, onToggleSort }: Props) => {
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))];
79 const fetchShareIds = async () => {
80 const idsMap: { [key: string]: string | null } = {};
81 const newInvalidIds = new Set(invalidShareIds);
84 vaultIds.map(async (vaultId) => {
85 if (cache.current[vaultId]) {
86 idsMap[vaultId] = cache.current[vaultId];
87 newInvalidIds.delete(vaultId);
90 const { Share } = await api(getShareID(vaultId));
91 idsMap[vaultId] = Share.ShareID;
92 cache.current[vaultId] = Share.ShareID;
93 newInvalidIds.delete(vaultId);
95 newInvalidIds.add(vaultId);
100 setShareIds((prev) => ({ ...prev, ...idsMap }));
101 setInvalidShareIds(newInvalidIds);
104 if (vaultIds.length > 0) {
109 const [sortConfig, setSortConfig] = useState<SortConfig>({
111 direction: SORT_DIRECTION.DESC,
114 const toggleSort = () => {
117 direction: sortConfig.direction === SORT_DIRECTION.ASC ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC,
119 onToggleSort(sortConfig.direction);
123 <Table responsive="cards">
126 onToggleSort={toggleSort}
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' },
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;
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);
149 <div className="flex flex-row items-center my-2">
150 <Avatar className="mr-2" color="weak">
154 className="flex flex-column cursor-pointer w-2/3"
155 onClick={() => onEmailOrIpClick(email)}
157 <span title={name} className="text-ellipsis max-w-full">
160 <span title={email} className="color-weak text-ellipsis max-w-full">
165 <div className="flex flex-column cursor-pointer">
167 className="flex flex-row mb-1 text-semibold"
168 onClick={() => onEventClick(eventType)}
172 <Time format="PPp" className="color-weak" onClick={() => onTimeClick(time)}>
180 hasInvalidShareId={hasInvalidShareId}
182 <span onClick={() => onEmailOrIpClick(ip)} className="cursor-pointer">
194 export default PassEventsTable;