Skip to content

Commit

Permalink
feat(explorer): reorder columns. RequestKey is the relevant first col…
Browse files Browse the repository at this point in the history
…umn when showing transactions. Moved sender to the end (#2754)

* feat(explorer): reorder columns. RequestKey is the relevant first column when showing transactions. Moved sender to the end

* feat(explorer): use proper layout for no results found

* feat(explorer): do not close truncated parts
  • Loading branch information
alber70g authored Dec 20, 2024
1 parent ce9f188 commit f87b1f7
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 77 deletions.
8 changes: 8 additions & 0 deletions .changeset/soft-tomatoes-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@kadena/explorer': patch
---

Reordering columns to present the first column as the one to link to

Remove toggling close of truncated content. Will always open so users can no
select and copy
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,7 @@ export const BlockTransactions: FC<IProps> = ({ hash }) => {
loaderVariant: 'icon',
},
{
label: 'Sender',
key: 'cmd.meta.sender',
variant: 'code',
width: '25%',
render: FormatLinkWrapper({ url: '/account/:value' }),
},
{
label: 'RequestKey',
label: 'Request Key',
key: 'hash',
variant: 'code',
width: '25%',
Expand All @@ -114,6 +107,13 @@ export const BlockTransactions: FC<IProps> = ({ hash }) => {
width: '40%',
render: CompactTableFormatters.FormatJsonParse(),
},
{
label: 'Sender',
key: 'cmd.meta.sender',
variant: 'code',
width: '25%',
render: FormatLinkWrapper({ url: '/account/:value' }),
},
]}
data={innerData.node.transactions.edges.map(
(edge) => edge.node as Transaction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CopyButton } from '@/components/CopyButton/CopyButton';
import { Stack, Text } from '@kadena/kode-ui';
import classNames from 'classnames';
import type { FC } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useState } from 'react';
import {
descriptionDetailsClass,
descriptionDetailsExpandedClass,
Expand All @@ -14,57 +14,12 @@ interface IProps {
field: IDataRenderComponentField;
}

const storageKey = 'expandedfields';

export const ExpandTruncatedField: FC<IProps> = ({ field }) => {
const [isExpanded, setIsExpanded] = useState(false);

const toggleExpand = (): void => {
let storage: string[] = JSON.parse(
localStorage.getItem(storageKey) ?? '[]',
);

const key = field.key ? field.key : field.id ?? '';

if (isExpanded) {
storage = storage.filter((v) => v !== key);
} else {
storage.push(key);
}

localStorage.setItem(storageKey, JSON.stringify(storage));

window.dispatchEvent(new Event(storageKey));
setIsExpanded((v) => !isExpanded);
};

const checkStorage = () => {
const storage: string[] = JSON.parse(
localStorage.getItem(storageKey) ?? '[]',
);

setIsExpanded(storage.includes(field.key));
};
const storageListener = useCallback((event: StorageEvent | Event) => {
if (event.type !== storageKey && 'key' in event && event.key !== storageKey)
return;

checkStorage();
}, []);

useEffect(() => {
checkStorage();
window.addEventListener(storageKey, storageListener);
window.addEventListener('storage', storageListener);
return () => {
window.removeEventListener(storageKey, storageListener);
window.removeEventListener('storage', storageListener);
};
}, [storageListener]);

return (
<Stack
onClick={toggleExpand}
onClick={() => setIsExpanded(true)}
as="dd"
gap="xs"
className={classNames(descriptionDetailsClass, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,35 @@
import { Heading, Stack } from '@kadena/kode-ui';
import { Heading, Text } from '@kadena/kode-ui';
import type { FC } from 'react';
import React from 'react';

interface NoSearchResultsProps {
interface INoSearchResultsProps {
type?: 'requestKey' | 'accountName' | 'blockhash';
value?: string;
}

export const NoSearchResults: FC<NoSearchResultsProps> = ({ type, value }) => {
export const NoSearchResults: FC<INoSearchResultsProps> = ({ type, value }) => {
// TODO: add buttons to navigate to other networks testnet/mainnet
// Use url from router to query other networks to see if the search result is
// there

switch (true) {
case type === 'requestKey' && value !== undefined:
return (
<Stack justifyContent="center" width="100%">
<Heading as="h3">No search results for request key: {value}</Heading>
</Stack>
<Heading as="h3">No search results for request key: {value}</Heading>
);

case type === 'accountName' && value !== undefined:
return (
<Stack justifyContent="center" flexDirection={'column'} width="100%">
<>
<Heading as="h3">No search results for account: {value}</Heading>
<Heading as="h4">Please check the account name and try again</Heading>
</Stack>
<Text>Please check the network and account name and try again</Text>
</>
);

case type === 'blockhash' && value !== undefined:
return (
<Stack justifyContent="center" width="100%">
<Heading as="h3">No search results for block: {value}</Heading>
</Stack>
);
return <Heading as="h3">No search results for block: {value}</Heading>;

default:
return (
<Stack justifyContent="center" width="100%">
<Heading as="h3">No search results</Heading>
</Stack>
);
return <Heading as="h3">No search results</Heading>;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ const Height: React.FC = () => {
key={`chain${chainData.chainId}`}
>
{chainData.data.edges.length === 0 ? (
<NoSearchResults />
<LayoutBody>
<NoSearchResults />
</LayoutBody>
) : (
<CompactTable
isLoading={isLoading}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const Transaction: React.FC = () => {
}, [loading, data, error, setIsLoading]);

return (
<Layout>
<Layout layout="full">
{innerData && innerData.transaction ? (
<>
<LayoutHeader>
Expand Down Expand Up @@ -120,7 +120,12 @@ const Transaction: React.FC = () => {
</>
) : (
!Array.isArray(router.query.requestKey) && (
<NoSearchResults type="requestKey" value={router.query.requestKey} />
<LayoutBody>
<NoSearchResults
type="requestKey"
value={router.query.requestKey}
/>
</LayoutBody>
)
)}
</Layout>
Expand Down
79 changes: 79 additions & 0 deletions packages/apps/explorer/src/utils/__test__/condenseString.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { condenseStrings } from '../condenseStrings';

describe('condenseString', () => {
const singleWord =
'thisisaverylongstringthatshouldbecondensedthisisaverylongstringthatshouldbecondensed';
const multiWord =
'thisisaverylongstringthatshouldbecond ensedthisisaverylongstringthatshouldbecondensed thisIsAshorterString';

describe('with default options', () => {
it('condenses a single string', () => {
expect(condenseStrings(singleWord)).toBe('thisi…ensed');
});

it('condenses a string with multiple words', () => {
expect(condenseStrings(multiWord)).toBe(
'thisi…econd ensed…ensed thisIsAshorterString',
);
});
});
describe('with custom options', () => {
it('minLength 10', () => {
expect(condenseStrings(singleWord, { minLength: 10 })).toBe(
'thisi…ensed',
);
expect(condenseStrings(multiWord, { minLength: 10 })).toBe(
'thisi…econd ensed…ensed thisI…tring',
);
});

it('replacement "..."', () => {
expect(condenseStrings(singleWord, { replacement: '...' })).toBe(
'thisi...ensed',
);
expect(condenseStrings(multiWord, { replacement: '...' })).toBe(
'thisi...econd ensed...ensed thisIsAshorterString',
);
});

it('startLength 10', () => {
expect(condenseStrings(singleWord, { startLength: 10 })).toBe(
'thisisaver…ensed',
);
expect(condenseStrings(multiWord, { startLength: 10 })).toBe(
'thisisaver…econd ensedthisi…ensed thisIsAshorterString',
);
});

it('endLength 10', () => {
expect(condenseStrings(singleWord, { endLength: 10 })).toBe(
'thisi…econdensed',
);
expect(condenseStrings(multiWord, { endLength: 10 })).toBe(
'thisi…ouldbecond ensed…econdensed thisIsAshorterString',
);
});

it('all options', () => {
expect(
condenseStrings(singleWord, {
minLength: 40,
replacement: '*.*',
startLength: 5,
endLength: 20,
}),
).toBe('thisi*.*hatshouldbecondensed');

expect(
condenseStrings(multiWord, {
minLength: 40,
replacement: '*.*',
startLength: 5,
endLength: 20,
}),
).toBe(
'thisisaverylongstringthatshouldbecond ensed*.*hatshouldbecondensed thisIsAshorterString',
);
});
});
});
43 changes: 43 additions & 0 deletions packages/apps/explorer/src/utils/condenseStrings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Shortens long sections in a string by keeping the first 5 and last 5 characters.
* @param str - string to shorten
* @returns {[string, string]} - shortened string and original string
*/
export function condenseStrings(
str: string,
options?: {
minLength?: number;
replacement?: string;
startLength?: number;
endLength?: number;
},
): string {
// set default options and override with provided options
let config;
if (options) {
config = {
...{ minLength: 22, replacement: '…', startLength: 5, endLength: 5 },
...options,
};
} else {
config = {
minLength: 22,
replacement: '…',
startLength: 5,
endLength: 5,
};
}

// Regex explanation:
// Split on sequences of characters that are NOT letters, digits, '_' or '-'.
const parts = str.split(/([^\w\d]+)/u);

return parts
.map((part) => {
if (/^[\w\d]+$/u.test(part) && part.length > config.minLength) {
return `${part.slice(0, config.startLength)}${config.replacement}${part.slice(-config.endLength)}`;
}
return part;
})
.join('');
}

0 comments on commit f87b1f7

Please sign in to comment.