/** @jsx jsx */ import { jsx } from '@emotion/core' import css from '@emotion/css/macro' import { useModal } from '@sumup/circuit-ui' import SelectorGroup from '@sumup/circuit-ui/dist/es/components/SelectorGroup' import { useTranslation } from 'react-i18next' import { ListRowRenderer } from 'react-virtualized/dist/es/List' import tw from 'twin.macro' import omit from 'lodash-es/omit' import React, { useState, useCallback, useEffect, ChangeEvent, useMemo, } from 'react' import useSWR, { mutate } from 'swr' import { List, AutoSizer } from 'react-virtualized' import { useLocation } from 'react-router-dom' import FixedFullscreenContainer from '../../components/FixedFullscreenContainer' import PageTitle from '../../components/PageTitle' import { useProfile } from '../../models/profile' import { RecentRequests, RequestItem } from '../../types' import fetcher from '../../utils/fetcher' import ListItem from './components/ListItem' import RequestModal from './components/RequestModal' const LIST_ITEMS_MAX = 150 function useQuery() { return new URLSearchParams(useLocation().search) } const Page: React.FC = () => { const { setModal } = useModal() const { t } = useTranslation() const profile = useProfile() const [isAutoRefresh, setIsAutoRefresh] = useState<boolean>(true) const [group, setGroup] = useState<'recent' | 'active'>('recent') const { data: recentRequestsResponse, error: requestsError } = useSWR<RecentRequests>(() => '/requests/' + group, fetcher, { revalidateOnFocus: false, revalidateOnReconnect: false, dedupingInterval: 1000, refreshInterval: isAutoRefresh ? profile?.platform === 'macos' ? 2000 : 4000 : 0, }) const [requestList, setRequestList] = useState<Array<RequestItem>>([]) const [activeRequestList, setActiveRequestList] = useState< Array<RequestItem> >([]) const currentList = useMemo( () => (group === 'recent' ? requestList : activeRequestList), [group, requestList, activeRequestList], ) const query = useQuery() const sourceIp = useMemo<string | null>(() => query.get('source'), [query]) useEffect( () => { if (!recentRequestsResponse?.requests) return const pendingList = [...recentRequestsResponse.requests] const now = new Date() let newList = [...currentList] while (pendingList.length) { const request = pendingList.pop() as RequestItem const existingIndex = newList.findIndex( (item) => item.id === request.id, ) if (existingIndex >= 0) { Object.assign(newList[existingIndex], { ...omit(request, ['id']), lastUpdated: now, }) } else { if (newList.length && request.id > newList[0].id) { newList.unshift({ ...request, lastUpdated: now, }) } else { newList.push({ ...request, lastUpdated: now, }) } } } if (group === 'recent') { newList = newList .filter((request) => { if (sourceIp) { return sourceIp === request.sourceAddress } return true }) .slice(0, LIST_ITEMS_MAX) } else { newList = newList .filter((request) => { if (sourceIp) { return ( request.lastUpdated === now && sourceIp === request.sourceAddress ) } return request.lastUpdated === now }) .sort((a, b) => b.id - a.id) } if (group === 'recent') { setRequestList(newList) setActiveRequestList([]) } else { setRequestList([]) setActiveRequestList(newList) } }, // eslint-disable-next-line react-hooks/exhaustive-deps [recentRequestsResponse, group, sourceIp], ) const openRequestDetail = useCallback( (req: RequestItem) => { setModal({ children({ onClose }) { return onClose ? ( <RequestModal req={req} onClose={onClose} /> ) : ( <React.Fragment /> ) }, onClose() { // noop }, }) }, [setModal], ) const rowRenderer: ListRowRenderer = useCallback( ({ key, // Unique key within array of rows index, // Index of row within collection isScrolling, // The List is currently being scrolled isVisible, // This row is visible within the List (eg it is not an overscanned row) style, // Style object to be applied to row (to position it) }) => { const req = currentList[index] return ( <div key={key} style={style} onClick={() => openRequestDetail(req)} tw="flex flex-col justify-center py-2 cursor-pointer hover:bg-gray-100" css={css` padding-left: calc(env(safe-area-inset-left) + 0.75rem); padding-right: calc(env(safe-area-inset-right) + 0.75rem); `} > <ListItem req={req} /> </div> ) }, [currentList, openRequestDetail], ) return ( <FixedFullscreenContainer> <React.Fragment> <PageTitle title={t('home.requests')} hasAutoRefresh={true} defaultAutoRefreshState={true} onAuthRefreshStateChange={(newState) => setIsAutoRefresh(newState)} /> <div tw="flex-1"> {recentRequestsResponse ? ( currentList.length ? ( <AutoSizer> {({ width, height }) => { return ( <List width={width} height={height} rowCount={currentList.length} rowHeight={64} rowRenderer={rowRenderer} style={{ outline: 'none', }} css={css` & > div { ${tw`divide-y divide-gray-200`} } `} /> ) }} </AutoSizer> ) : ( <div tw="h-full flex items-center justify-center text-base text-gray-500"> {t('common.no_data')} </div> ) ) : ( <div tw="h-full flex items-center justify-center text-base text-gray-500"> {t('common.is_loading')}... </div> )} </div> <div css={[ tw`flex divide-x divide-gray-200 border-t border-solid border-gray-200 py-2 px-2`, css` & > div { ${tw`mx-2`} } & > div:first-of-type { margin-left: 0; } `, ]} > <SelectorGroup css={[ tw`flex justify-center items-center`, css` & label { ${tw`py-2 px-4 ml-2 my-1 text-sm`} } & label:first-of-type { margin-left: 0; } `, ]} label="choose the dns result group" name="selector-group" onChange={(event: ChangeEvent<HTMLInputElement>) => { setGroup(() => { const newState = event.target.value as 'recent' | 'active' mutate('/requests/' + newState) return newState }) }} options={[ { children: t('requests.recent'), value: 'recent', }, { children: t('requests.active'), value: 'active', }, ]} value={group} /> </div> </React.Fragment> </FixedFullscreenContainer> ) } export default Page