Convert message list to table

pull/16/head
J M Rossy 2 years ago
parent 83a464dd35
commit 14910020ba
  1. 10
      src/components/search/SearchBar.tsx
  2. 6
      src/features/debugger/TxDebugger.tsx
  3. 75
      src/features/messages/MessageSearch.tsx
  4. 76
      src/features/messages/MessageSummary.tsx
  5. 105
      src/features/messages/MessageTable.tsx
  6. 59
      src/features/messages/SearchFilterBar.tsx
  7. 4
      src/images/icons/funnel.svg

@ -10,10 +10,10 @@ interface Props {
value: string;
placeholder: string;
onChangeValue: (v: string) => void;
fetching: boolean;
isFetching: boolean;
}
export function SearchBar({ value, placeholder, onChangeValue, fetching }: Props) {
export function SearchBar({ value, placeholder, onChangeValue, isFetching }: Props) {
const onChange = (event: ChangeEvent<HTMLInputElement> | null) => {
const value = event?.target?.value || '';
onChangeValue(value);
@ -29,9 +29,9 @@ export function SearchBar({ value, placeholder, onChangeValue, fetching }: Props
className="p-2 sm:px-4 md:px-5 flex-1 h-10 sm:h-12 rounded placeholder:text-gray-500 focus:outline-none"
/>
<div className="h-10 sm:h-12 w-10 sm:w-12 flex items-center justify-center rounded bg-beige-300">
{fetching && <Spinner classes="scale-[30%] mr-2.5" />}
{!fetching && !value && <Image src={SearchIcon} width={20} height={20} alt="" />}
{!fetching && value && (
{isFetching && <Spinner classes="scale-[30%] mr-2.5" />}
{!isFetching && !value && <Image src={SearchIcon} width={20} height={20} alt="" />}
{!isFetching && value && (
<IconButton
imgSrc={XIcon}
title="Clear search"

@ -39,7 +39,7 @@ export function TxDebugger() {
const isValidInput = isValidSearchQuery(sanitizedInput, false);
const {
isLoading: fetching,
isLoading: isFetching,
isError: hasError,
data,
} = useQuery(
@ -60,7 +60,7 @@ export function TxDebugger() {
<SearchBar
value={searchInput}
onChangeValue={setSearchInput}
fetching={fetching}
isFetching={isFetching}
placeholder="Search transaction hash to debug message"
/>
<div className="w-full h-[38.2rem] mt-5 bg-white shadow-md border border-blue-50 rounded overflow-auto relative">
@ -75,7 +75,7 @@ export function TxDebugger() {
</div>
</Fade>
<SearchEmptyError
show={isValidInput && !hasError && !fetching && !data}
show={isValidInput && !hasError && !isFetching && !data}
hasInput={hasInput}
allowAddress={false}
/>

@ -1,29 +1,25 @@
import Image from 'next/future/image';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import { useQuery } from 'urql';
import { Fade } from '../../components/animation/Fade';
import { SelectField } from '../../components/input/SelectField';
import { SearchBar } from '../../components/search/SearchBar';
import {
SearchEmptyError,
SearchInvalidError,
SearchUnknownError,
} from '../../components/search/SearchError';
import { prodAndTestChains } from '../../consts/chains';
import { chainToDomain } from '../../consts/domains';
import ArrowRightIcon from '../../images/icons/arrow-right-short.svg';
import FunnelIcon from '../../images/icons/funnel.svg';
import { trimLeading0x } from '../../utils/addresses';
import useDebounce from '../../utils/debounce';
import { logger } from '../../utils/logger';
import { getQueryParamString } from '../../utils/queryParams';
import { sanitizeString, trimToLength } from '../../utils/string';
import { sanitizeString } from '../../utils/string';
import { useInterval } from '../../utils/timeout';
import { MessageSummary } from './MessageSummary';
import { MessageTable } from './MessageTable';
import { SearchFilterBar } from './SearchFilterBar';
import { parseMessageStubResult } from './query';
import { MessagesStubQueryResult } from './types';
import { isValidSearchQuery } from './utils';
@ -66,7 +62,6 @@ export function MessageSearch() {
}, [isValidInput, sanitizedInput]);
// Filter state and handlers
const chainOptions = useMemo(getChainOptionList, []);
const [originChainFilter, setOriginChainFilter] = useState('');
const [destinationChainFilter, setDestinationChainFilter] = useState('');
const onChangeOriginFilter = (value: string) => {
@ -87,7 +82,7 @@ export function MessageSearch() {
variables,
pause: !isValidInput,
});
const { data, fetching, error } = result;
const { data, fetching: isFetching, error } = result;
const messageList = useMemo(() => parseMessageStubResult(data), [data]);
const hasError = !!error;
const reExecutor = useCallback(() => {
@ -102,55 +97,27 @@ export function MessageSearch() {
<SearchBar
value={searchInput}
onChangeValue={setSearchInput}
fetching={fetching}
isFetching={isFetching}
placeholder="Search by address or transaction hash"
/>
<div className="w-full min-h-[38rem] max-h-[47rem] mt-5 bg-white shadow-md border border-blue-50 rounded overflow-auto relative">
{/* Content header and filter bar */}
<div className="px-2 py-3 sm:px-4 md:px-5 flex items-center justify-between border-b border-gray-100">
<h2 className="text-gray-600">{!hasInput ? 'Latest Messages' : 'Search Results'}</h2>
<div className="flex items-center space-x-1 sm:space-x-2 md:space-x-3">
<div className="w-px h-8 bg-gray-100"></div>
<Image
src={FunnelIcon}
width={22}
height={22}
className="hidden sm:block opacity-50"
alt=""
/>
<SelectField
classes="w-24 md:w-32"
options={chainOptions}
value={originChainFilter}
onValueSelect={onChangeOriginFilter}
/>
<Image src={ArrowRightIcon} width={30} height={30} className="opacity-50" alt="" />
<SelectField
classes="w-24 md:w-32"
options={chainOptions}
value={destinationChainFilter}
onValueSelect={onChangeDestinationFilter}
/>
</div>
<div className="w-full min-h-[38rem] mt-5 bg-white shadow-md border border-blue-50 rounded overflow-auto relative">
<div className="px-2 py-3 sm:px-4 md:px-5 flex items-center justify-between bg-gray-50">
<h2 className="pl-1 text-gray-700">{!hasInput ? 'Latest Messages' : 'Search Results'}</h2>
<SearchFilterBar
originChainFilter={originChainFilter}
onChangeOriginFilter={onChangeOriginFilter}
destinationChainFilter={destinationChainFilter}
onChangeDestinationFilter={onChangeDestinationFilter}
/>
</div>
{/* Message list */}
<Fade show={!hasError && isValidInput && messageList.length > 0}>
{messageList.map((m) => (
<div
key={`message-${m.id}`}
className={`px-2 py-2 sm:px-4 md:px-5 md:py-2.5 border-b border-gray-100 hover:bg-gray-50 active:bg-gray-100 ${
fetching && 'blur-xs'
} transition-all duration-500`}
>
<MessageSummary message={m} />
</div>
))}
<MessageTable messageList={messageList} isFetching={isFetching} />
</Fade>
<SearchInvalidError show={!isValidInput} allowAddress={true} />
<SearchUnknownError show={isValidInput && hasError} />
<SearchEmptyError
show={isValidInput && !hasError && !fetching && messageList.length === 0}
show={isValidInput && !hasError && !isFetching && messageList.length === 0}
hasInput={hasInput}
allowAddress={true}
/>
@ -159,16 +126,6 @@ export function MessageSearch() {
);
}
function getChainOptionList(): Array<{ value: string; display: string }> {
return [
{ value: '', display: 'All Chains' },
...prodAndTestChains.map((c) => ({
value: c.id.toString(),
display: trimToLength(c.name, 12),
})),
];
}
function assembleQuery(searchInput: string, originFilter: string, destFilter: string) {
const hasInput = !!searchInput;
const variables = {

@ -1,76 +0,0 @@
import Link from 'next/link';
import { ChainToChain } from '../../components/icons/ChainToChain';
import { MessageStatus, MessageStub } from '../../types';
import { shortenAddress } from '../../utils/addresses';
import { getHumanReadableDuration, getHumanReadableTimeString } from '../../utils/time';
export function MessageSummary({ message }: { message: MessageStub }) {
const {
id,
status,
sender,
recipient,
originChainId,
destinationChainId,
originTimestamp,
destinationTimestamp,
} = message;
let statusColor = 'bg-beige-500';
let statusText = 'Pending';
if (status === MessageStatus.Delivered) {
statusColor = 'bg-green-400 text-white';
statusText = 'Delivered';
} else if (status === MessageStatus.Failing) {
statusColor = 'bg-red-500 text-white';
statusText = 'Failing';
}
return (
<Link href={`/message/${id}`}>
<a className="flex items-center justify-between space-x-4 xs:space-x-9 sm:space-x-12 md:space-x-16">
<ChainToChain
originChainId={originChainId}
destinationChainId={destinationChainId}
size={40}
arrowSize={30}
isNarrow={true}
/>
<div className="flex items-center justify-between flex-1">
<div className="flex flex-col">
<div className={styles.label}>Sender</div>
<div className={styles.value}>{shortenAddress(sender) || 'Invalid Address'}</div>
</div>
<div className="hidden sm:flex flex-col">
<div className={styles.label}>Recipient</div>
<div className={styles.value}>{shortenAddress(recipient) || 'Invalid Address'}</div>
</div>
<div className="flex flex-col sm:w-24">
<div className={styles.label}>Time sent</div>
<div className={styles.valueTruncated}>
{getHumanReadableTimeString(originTimestamp)}
</div>
</div>
<div className="hidden lg:flex flex-col sm:w-16">
<div className={styles.label}>Duration</div>
<div className={styles.valueTruncated}>
{destinationTimestamp
? getHumanReadableDuration(destinationTimestamp - originTimestamp)
: '-'}
</div>
</div>
</div>
<div className={`w-20 md:w-[5.5rem] py-2 text-sm text-center rounded ${statusColor}`}>
{statusText}
</div>
</a>
</Link>
);
}
const styles = {
label: 'text-sm text-gray-500',
value: 'text-sm mt-1',
valueTruncated: 'text-sm mt-1 truncate',
};

@ -0,0 +1,105 @@
import { useRouter } from 'next/router';
import { ChainToChain } from '../../components/icons/ChainToChain';
import { MessageStatus, MessageStub } from '../../types';
import { shortenAddress } from '../../utils/addresses';
import { getHumanReadableDuration, getHumanReadableTimeString } from '../../utils/time';
export function MessageTable({
messageList,
isFetching,
}: {
messageList: MessageStub[];
isFetching: boolean;
}) {
const router = useRouter();
return (
<table className="w-full mb-1">
<tr className="px-2 py-2 sm:px-4 md:px-5 md:py-2.5 border-b border-gray-100 bg-gray-50">
<th className={`${styles.header} pr-1`}>Chains</th>
<th className={styles.header}>Sender</th>
<th className={`${styles.header} hidden sm:table-cell`}>Recipient</th>
<th className={styles.header}>Time Sent</th>
<th className={`${styles.header} hidden lg:table-cell`}>Duration</th>
<th className={styles.header}>Status</th>
</tr>
{messageList.map((m) => (
<tr
key={`message-${m.id}`}
className={`px-2 py-2 sm:px-4 md:px-5 md:py-2.5 cursor-pointer hover:bg-gray-100 active:bg-gray-200 border-b border-gray-100 last:border-0 ${
isFetching && 'blur-xs'
} transition-all duration-500`}
onClick={() => router.push(`/message/${m.id}`)}
>
<MessageSummaryRow message={m} />
</tr>
))}
</table>
);
}
export function MessageSummaryRow({ message }: { message: MessageStub }) {
const {
status,
sender,
recipient,
originChainId,
destinationChainId,
originTimestamp,
destinationTimestamp,
} = message;
let statusColor = 'bg-beige-500';
let statusText = 'Pending';
if (status === MessageStatus.Delivered) {
statusColor = 'bg-green-400 text-white';
statusText = 'Delivered';
} else if (status === MessageStatus.Failing) {
statusColor = 'bg-red-500 text-white';
statusText = 'Failing';
}
return (
<>
<td className="py-2.5">
<ChainToChain
originChainId={originChainId}
destinationChainId={destinationChainId}
size={38}
arrowSize={30}
isNarrow={true}
/>
</td>
<td>
<div className={styles.value}>{shortenAddress(sender) || 'Invalid Address'}</div>
</td>
<td className="hidden sm:table-cell">
<div className={styles.value}>{shortenAddress(recipient) || 'Invalid Address'}</div>
</td>
<td>
<div className={styles.valueTruncated}>{getHumanReadableTimeString(originTimestamp)}</div>
</td>
<td className="hidden lg:table-cell text-center px-4">
<div className={styles.valueTruncated}>
{destinationTimestamp
? getHumanReadableDuration(destinationTimestamp - originTimestamp)
: '-'}
</div>
</td>
<td>
<div className="flex items-center justify-center">
<div className={`text-center w-20 md:w-[5.25rem] py-1.5 text-sm rounded ${statusColor}`}>
{statusText}
</div>
</div>
</td>
</>
);
}
const styles = {
header: 'text-sm text-gray-700 font-normal pt-2 pb-3 text-center',
value: 'text-sm text-center',
valueTruncated: 'text-sm text-center truncate',
};

@ -0,0 +1,59 @@
import Image from 'next/future/image';
import { useMemo } from 'react';
import { SelectField } from '../../components/input/SelectField';
import { prodAndTestChains } from '../../consts/chains';
import ArrowRightIcon from '../../images/icons/arrow-right-short.svg';
import FunnelIcon from '../../images/icons/funnel.svg';
import { trimToLength } from '../../utils/string';
interface Props {
originChainFilter: string;
onChangeOriginFilter: (value: string) => void;
destinationChainFilter: string;
onChangeDestinationFilter: (value: string) => void;
}
export function SearchFilterBar({
originChainFilter,
onChangeOriginFilter,
destinationChainFilter,
onChangeDestinationFilter,
}: Props) {
const chainOptions = useMemo(getChainOptionList, []);
return (
<div className="flex items-center space-x-1 sm:space-x-2 md:space-x-3">
<div className="w-px h-8 bg-gray-200"></div>
<Image
src={FunnelIcon}
width={20}
height={20}
className="hidden sm:block opacity-20"
alt=""
/>
<SelectField
classes="w-24 md:w-32"
options={chainOptions}
value={originChainFilter}
onValueSelect={onChangeOriginFilter}
/>
<Image src={ArrowRightIcon} width={30} height={30} className="opacity-30" alt="" />
<SelectField
classes="w-24 md:w-32"
options={chainOptions}
value={destinationChainFilter}
onValueSelect={onChangeDestinationFilter}
/>
</div>
);
}
function getChainOptionList(): Array<{ value: string; display: string }> {
return [
{ value: '', display: 'All Chains' },
...prodAndTestChains.map((c) => ({
value: c.id.toString(),
display: trimToLength(c.name, 12),
})),
];
}

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
<path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel-fill" viewBox="0 0 16 16">
<path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2z"/>
</svg>

Before

Width:  |  Height:  |  Size: 381 B

After

Width:  |  Height:  |  Size: 313 B

Loading…
Cancel
Save