Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
F
frontend
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
vicotor
frontend
Commits
872cf535
Unverified
Commit
872cf535
authored
Jun 21, 2023
by
Igor Stuev
Committed by
GitHub
Jun 21, 2023
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #932 from blockscout/csv-filters
csv export filters
parents
e1011e30
6db2fd57
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
136 additions
and
26 deletions
+136
-26
buildUrl.ts
lib/api/buildUrl.ts
+1
-1
address.ts
types/client/address.ts
+11
-1
AddressCsvExportLink.tsx
ui/address/AddressCsvExportLink.tsx
+4
-4
AddressInternalTxs.tsx
ui/address/AddressInternalTxs.tsx
+6
-1
AddressLogs.tsx
ui/address/AddressLogs.tsx
+5
-1
AddressTokenTransfers.tsx
ui/address/AddressTokenTransfers.tsx
+1
-1
AddressTxs.tsx
ui/address/AddressTxs.tsx
+11
-8
CsvExportForm.tsx
ui/csvExport/CsvExportForm.tsx
+12
-3
CsvExport.pw.tsx
ui/pages/CsvExport.pw.tsx
+35
-0
CsvExport.tsx
ui/pages/CsvExport.tsx
+38
-4
CsvExport.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png
...t.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png
+0
-0
CsvExport.pw.tsx_default_base-view-mobile-dark-mode-1.png
...CsvExport.pw.tsx_default_base-view-mobile-dark-mode-1.png
+0
-0
CsvExport.pw.tsx_mobile_base-view-mobile-dark-mode-1.png
.../CsvExport.pw.tsx_mobile_base-view-mobile-dark-mode-1.png
+0
-0
TxsContent.tsx
ui/txs/TxsContent.tsx
+12
-2
No files found.
lib/api/buildUrl.ts
View file @
872cf535
...
...
@@ -9,7 +9,7 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources'
export
default
function
buildUrl
<
R
extends
ResourceName
>
(
resourceName
:
R
,
pathParams
?:
ResourcePathParams
<
R
>
,
queryParams
?:
Record
<
string
,
string
|
Array
<
string
>
|
number
|
undefined
>
,
queryParams
?:
Record
<
string
,
string
|
Array
<
string
>
|
number
|
null
|
undefined
>
,
):
string
{
const
resource
:
ApiResource
=
RESOURCES
[
resourceName
];
const
baseUrl
=
isNeedProxy
()
?
appConfig
.
baseUrl
:
(
resource
.
endpoint
||
appConfig
.
api
.
endpoint
);
...
...
types/client/address.ts
View file @
872cf535
export
type
CsvExportType
=
'
transactions
'
|
'
internal-transactions
'
|
'
token-transfers
'
|
'
logs
'
;
import
type
{
AddressFromToFilter
}
from
'
types/api/address
'
;
export
type
CsvExportParams
=
{
type
:
'
transactions
'
|
'
internal-transactions
'
|
'
token-transfers
'
;
filterType
?:
'
address
'
;
filterValue
?:
AddressFromToFilter
;
}
|
{
type
:
'
logs
'
;
filterType
?:
'
topic
'
;
filterValue
?:
string
;
}
ui/address/AddressCsvExportLink.tsx
View file @
872cf535
...
...
@@ -2,7 +2,7 @@ import { chakra, Icon, Tooltip, Hide, Skeleton, Flex } from '@chakra-ui/react';
import
{
route
}
from
'
nextjs-routes
'
;
import
React
from
'
react
'
;
import
type
{
CsvExport
Type
}
from
'
types/client/address
'
;
import
type
{
CsvExport
Params
}
from
'
types/client/address
'
;
import
appConfig
from
'
configs/app/config
'
;
import
svgFileIcon
from
'
icons/files/csv.svg
'
;
...
...
@@ -12,12 +12,12 @@ import LinkInternal from 'ui/shared/LinkInternal';
interface
Props
{
address
:
string
;
type
:
CsvExportType
;
params
:
CsvExportParams
;
className
?:
string
;
isLoading
?:
boolean
;
}
const
AddressCsvExportLink
=
({
className
,
address
,
type
,
isLoading
}:
Props
)
=>
{
const
AddressCsvExportLink
=
({
className
,
address
,
params
,
isLoading
}:
Props
)
=>
{
const
isMobile
=
useIsMobile
();
const
isInitialLoading
=
useIsInitialLoading
(
isLoading
);
...
...
@@ -43,7 +43,7 @@ const AddressCsvExportLink = ({ className, address, type, isLoading }: Props) =>
display=
"inline-flex"
alignItems=
"center"
whiteSpace=
"nowrap"
href=
{
route
({
pathname
:
'
/csv-export
'
,
query
:
{
type
,
address
}
})
}
href=
{
route
({
pathname
:
'
/csv-export
'
,
query
:
{
...
params
,
address
}
})
}
flexShrink=
{
0
}
>
<
Icon
as=
{
svgFileIcon
}
boxSize=
{
{
base
:
'
30px
'
,
lg
:
6
}
}
/>
...
...
ui/address/AddressInternalTxs.tsx
View file @
872cf535
...
...
@@ -74,7 +74,12 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
isActive=
{
Boolean
(
filterValue
)
}
isLoading=
{
pagination
.
isLoading
}
/>
<
AddressCsvExportLink
address=
{
hash
}
isLoading=
{
pagination
.
isLoading
}
type=
"internal-transactions"
ml=
{
{
base
:
2
,
lg
:
'
auto
'
}
}
/>
<
AddressCsvExportLink
address=
{
hash
}
isLoading=
{
pagination
.
isLoading
}
params=
{
{
type
:
'
internal-transactions
'
,
filterType
:
'
address
'
,
filterValue
}
}
ml=
{
{
base
:
2
,
lg
:
'
auto
'
}
}
/>
<
Pagination
ml=
{
{
base
:
'
auto
'
,
lg
:
8
}
}
{
...
pagination
}
/>
</
ActionBar
>
);
...
...
ui/address/AddressLogs.tsx
View file @
872cf535
...
...
@@ -32,7 +32,11 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
const
actionBar
=
(
<
ActionBar
mt=
{
-
6
}
showShadow
justifyContent=
{
{
base
:
'
space-between
'
,
lg
:
'
end
'
}
}
>
<
AddressCsvExportLink
address=
{
hash
}
isLoading=
{
pagination
.
isLoading
}
type=
"logs"
/>
<
AddressCsvExportLink
address=
{
hash
}
isLoading=
{
pagination
.
isLoading
}
params=
{
{
type
:
'
logs
'
}
}
/>
<
Pagination
ml=
{
{
base
:
0
,
lg
:
8
}
}
{
...
pagination
}
/>
</
ActionBar
>
);
...
...
ui/address/AddressTokenTransfers.tsx
View file @
872cf535
...
...
@@ -272,7 +272,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
{
currentAddress
&&
(
<
AddressCsvExportLink
address=
{
currentAddress
}
type=
"token-transfers"
params=
{
{
type
:
'
token-transfers
'
,
filterType
:
'
address
'
,
filterValue
:
filters
.
filter
}
}
ml=
{
{
base
:
2
,
lg
:
'
auto
'
}
}
isLoading=
{
isPlaceholderData
}
/>
...
...
ui/address/AddressTxs.tsx
View file @
872cf535
...
...
@@ -159,24 +159,27 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
/>
);
const csvExportLink = (
<AddressCsvExportLink
address={ currentAddress }
params={{ type: 'transactions', filterType: 'address', filterValue }}
ml="auto"
isLoading={ addressTxsQuery.pagination.isLoading }
/>
);
return (
<>
{ !isMobile && (
<ActionBar mt={ -6 }>
{ filter }
{ currentAddress && (
<AddressCsvExportLink
address={ currentAddress }
type="transactions"
ml="auto"
isLoading={ addressTxsQuery.pagination.isLoading }
/>
) }
{ currentAddress && csvExportLink }
<Pagination { ...addressTxsQuery.pagination } ml={ 8 }/>
</ActionBar>
) }
<TxsContent
filter={ filter }
filterValue={ filterValue }
query={ addressTxsQuery }
currentAddress={ typeof currentAddress === 'string' ? currentAddress : undefined }
enableTimeIncrement
...
...
ui/csvExport/CsvExportForm.tsx
View file @
872cf535
...
...
@@ -4,6 +4,7 @@ import type { SubmitHandler } from 'react-hook-form';
import
{
useForm
,
FormProvider
}
from
'
react-hook-form
'
;
import
type
{
FormFields
}
from
'
./types
'
;
import
type
{
CsvExportParams
}
from
'
types/client/address
'
;
import
buildUrl
from
'
lib/api/buildUrl
'
;
import
type
{
ResourceName
}
from
'
lib/api/resources
'
;
...
...
@@ -17,10 +18,12 @@ import CsvExportFormReCaptcha from './CsvExportFormReCaptcha';
interface
Props
{
hash
:
string
;
resource
:
ResourceName
;
filterType
?:
CsvExportParams
[
'
filterType
'
]
|
null
;
filterValue
?:
CsvExportParams
[
'
filterValue
'
]
|
null
;
fileNameTemplate
:
string
;
}
const
CsvExportForm
=
({
hash
,
resource
,
fileNameTemplate
}:
Props
)
=>
{
const
CsvExportForm
=
({
hash
,
resource
,
fil
terType
,
filterValue
,
fil
eNameTemplate
}:
Props
)
=>
{
const
formApi
=
useForm
<
FormFields
>
({
mode
:
'
onBlur
'
,
defaultValues
:
{
...
...
@@ -37,6 +40,8 @@ const CsvExportForm = ({ hash, resource, fileNameTemplate }: Props) => {
address_id
:
hash
,
from_period
:
data
.
from
,
to_period
:
data
.
to
,
filter_type
:
filterType
,
filter_value
:
filterValue
,
recaptcha_response
:
data
.
reCaptcha
,
});
...
...
@@ -51,7 +56,11 @@ const CsvExportForm = ({ hash, resource, fileNameTemplate }: Props) => {
}
const
blob
=
await
response
.
blob
();
downloadBlob
(
blob
,
`
${
fileNameTemplate
}
_
${
hash
}
_
${
data
.
from
}
_
${
data
.
to
}
.csv`
);
downloadBlob
(
blob
,
`
${
fileNameTemplate
}
_
${
hash
}
_
${
data
.
from
}
_
${
data
.
to
}
${
filterType
&&
filterValue
?
'
_with_filter_type_
'
+
filterType
+
'
_value_
'
+
filterValue
:
''
}
.csv`
,
);
}
catch
(
error
)
{
toast
({
...
...
@@ -64,7 +73,7 @@ const CsvExportForm = ({ hash, resource, fileNameTemplate }: Props) => {
});
}
},
[
fileNameTemplate
,
hash
,
resource
,
toast
]);
},
[
fileNameTemplate
,
hash
,
resource
,
filterType
,
filterValue
,
toast
]);
return
(
<
FormProvider
{
...
formApi
}
>
...
...
ui/pages/CsvExport.pw.tsx
0 → 100644
View file @
872cf535
import
{
test
,
expect
}
from
'
@playwright/experimental-ct-react
'
;
import
React
from
'
react
'
;
import
*
as
addressMock
from
'
mocks/address/address
'
;
import
TestApp
from
'
playwright/TestApp
'
;
import
buildApiUrl
from
'
playwright/utils/buildApiUrl
'
;
import
CsvExport
from
'
./CsvExport
'
;
const
ADDRESS_API_URL
=
buildApiUrl
(
'
address
'
,
{
hash
:
addressMock
.
hash
});
const
hooksConfig
=
{
router
:
{
query
:
{
address
:
addressMock
.
hash
,
type
:
'
transactions
'
,
filterType
:
'
address
'
,
filterValue
:
'
from
'
},
isReady
:
true
,
},
};
test
.
beforeEach
(
async
({
page
})
=>
{
await
page
.
route
(
ADDRESS_API_URL
,
(
route
)
=>
route
.
fulfill
({
status
:
200
,
body
:
JSON
.
stringify
(
addressMock
.
withName
),
}));
});
test
(
'
base view +@mobile +@dark-mode
'
,
async
({
mount
})
=>
{
const
component
=
await
mount
(
<
TestApp
>
<
CsvExport
/>
</
TestApp
>,
{
hooksConfig
},
);
await
expect
(
component
).
toHaveScreenshot
();
});
ui/pages/CsvExport.tsx
View file @
872cf535
...
...
@@ -2,7 +2,8 @@ import { Flex } from '@chakra-ui/react';
import
{
useRouter
}
from
'
next/router
'
;
import
React
from
'
react
'
;
import
type
{
CsvExportType
}
from
'
types/client/address
'
;
import
{
AddressFromToFilterValues
}
from
'
types/api/address
'
;
import
type
{
CsvExportParams
}
from
'
types/client/address
'
;
import
type
{
ResourceName
}
from
'
lib/api/resources
'
;
import
useApiQuery
from
'
lib/api/useApiQuery
'
;
...
...
@@ -21,32 +22,41 @@ interface ExportTypeEntity {
text
:
string
;
resource
:
ResourceName
;
fileNameTemplate
:
string
;
filterType
?:
CsvExportParams
[
'
filterType
'
];
filterValues
?:
Readonly
<
Array
<
CsvExportParams
[
'
filterValue
'
]
>>
;
}
const
EXPORT_TYPES
:
Record
<
CsvExport
Type
,
ExportTypeEntity
>
=
{
const
EXPORT_TYPES
:
Record
<
CsvExport
Params
[
'
type
'
]
,
ExportTypeEntity
>
=
{
transactions
:
{
text
:
'
transactions
'
,
resource
:
'
csv_export_txs
'
,
fileNameTemplate
:
'
transactions
'
,
filterType
:
'
address
'
,
filterValues
:
AddressFromToFilterValues
,
},
'
internal-transactions
'
:
{
text
:
'
internal transactions
'
,
resource
:
'
csv_export_internal_txs
'
,
fileNameTemplate
:
'
internal_transactions
'
,
filterType
:
'
address
'
,
filterValues
:
AddressFromToFilterValues
,
},
'
token-transfers
'
:
{
text
:
'
token transfers
'
,
resource
:
'
csv_export_token_transfers
'
,
fileNameTemplate
:
'
token_transfers
'
,
filterType
:
'
address
'
,
filterValues
:
AddressFromToFilterValues
,
},
logs
:
{
text
:
'
logs
'
,
resource
:
'
csv_export_logs
'
,
fileNameTemplate
:
'
logs
'
,
filterType
:
'
topic
'
,
},
};
const
isCorrectExportType
=
(
type
:
string
):
type
is
CsvExport
Type
=>
Object
.
keys
(
EXPORT_TYPES
).
includes
(
type
);
const
isCorrectExportType
=
(
type
:
string
):
type
is
CsvExport
Params
[
'
type
'
]
=>
Object
.
keys
(
EXPORT_TYPES
).
includes
(
type
);
const
CsvExport
=
()
=>
{
const
router
=
useRouter
();
...
...
@@ -55,6 +65,8 @@ const CsvExport = () => {
const
addressHash
=
router
.
query
.
address
?.
toString
()
||
''
;
const
exportType
=
router
.
query
.
type
?.
toString
()
||
''
;
const
filterTypeFromQuery
=
router
.
query
.
filterType
?.
toString
()
||
null
;
const
filterValueFromQuery
=
router
.
query
.
filterValue
?.
toString
();
const
addressQuery
=
useApiQuery
(
'
address
'
,
{
pathParams
:
{
hash
:
addressHash
},
...
...
@@ -80,6 +92,19 @@ const CsvExport = () => {
throw
Error
(
'
Not found
'
,
{
cause
:
{
status
:
404
}
});
}
const
filterType
=
filterTypeFromQuery
===
EXPORT_TYPES
[
exportType
].
filterType
?
filterTypeFromQuery
:
null
;
const
filterValue
=
(()
=>
{
if
(
!
filterType
||
!
filterValueFromQuery
)
{
return
null
;
}
if
(
EXPORT_TYPES
[
exportType
].
filterValues
&&
!
EXPORT_TYPES
[
exportType
].
filterValues
?.
includes
(
filterValueFromQuery
))
{
return
null
;
}
return
filterValueFromQuery
;
})();
const
content
=
(()
=>
{
if
(
addressQuery
.
isError
)
{
return
<
DataFetchAlert
/>;
...
...
@@ -89,7 +114,15 @@ const CsvExport = () => {
return
<
ContentLoader
/>;
}
return
<
CsvExportForm
hash=
{
addressHash
}
resource=
{
EXPORT_TYPES
[
exportType
].
resource
}
fileNameTemplate=
{
EXPORT_TYPES
[
exportType
].
fileNameTemplate
}
/>;
return
(
<
CsvExportForm
hash=
{
addressHash
}
resource=
{
EXPORT_TYPES
[
exportType
].
resource
}
filterType=
{
filterType
}
filterValue=
{
filterValue
}
fileNameTemplate=
{
EXPORT_TYPES
[
exportType
].
fileNameTemplate
}
/>
);
})();
return
(
...
...
@@ -104,6 +137,7 @@ const CsvExport = () => {
<
AddressIcon
address=
{
{
hash
:
addressHash
,
is_contract
:
true
,
implementation_name
:
null
}
}
/>
<
AddressLink
hash=
{
addressHash
}
type=
"address"
ml=
{
2
}
truncation=
{
isMobile
?
'
constant
'
:
'
dynamic
'
}
/>
</
Address
>
{
filterType
&&
filterValue
&&
<
span
>
with applied filter by
{
filterType
}
(
{
filterValue
}
)
</
span
>
}
<
span
>
to CSV file
</
span
>
</
Flex
>
{
content
}
...
...
ui/pages/__screenshots__/CsvExport.pw.tsx_dark-color-mode_base-view-mobile-dark-mode-1.png
0 → 100644
View file @
872cf535
60.9 KB
ui/pages/__screenshots__/CsvExport.pw.tsx_default_base-view-mobile-dark-mode-1.png
0 → 100644
View file @
872cf535
59.6 KB
ui/pages/__screenshots__/CsvExport.pw.tsx_mobile_base-view-mobile-dark-mode-1.png
0 → 100644
View file @
872cf535
51.3 KB
ui/txs/TxsContent.tsx
View file @
872cf535
import
{
Box
,
Show
,
Hide
}
from
'
@chakra-ui/react
'
;
import
React
from
'
react
'
;
import
type
{
AddressFromToFilter
}
from
'
types/api/address
'
;
import
useIsMobile
from
'
lib/hooks/useIsMobile
'
;
import
AddressCsvExportLink
from
'
ui/address/AddressCsvExportLink
'
;
import
DataListDisplay
from
'
ui/shared/DataListDisplay
'
;
...
...
@@ -20,12 +22,14 @@ type Props = {
socketInfoNum
?:
number
;
currentAddress
?:
string
;
filter
?:
React
.
ReactNode
;
filterValue
?:
AddressFromToFilter
;
enableTimeIncrement
?:
boolean
;
top
?:
number
;
}
const
TxsContent
=
({
filter
,
filterValue
,
query
,
showBlockInfo
=
true
,
showSocketInfo
=
true
,
...
...
@@ -88,8 +92,14 @@ const TxsContent = ({
paginationProps=
{
query
.
pagination
}
showPagination=
{
query
.
pagination
.
isVisible
}
filterComponent=
{
filter
}
linkSlot=
{
currentAddress
?
<
AddressCsvExportLink
address=
{
currentAddress
}
type=
"transactions"
ml=
{
2
}
isLoading=
{
query
.
pagination
.
isLoading
}
/>
:
null
linkSlot=
{
currentAddress
?
(
<
AddressCsvExportLink
address=
{
currentAddress
}
params=
{
{
type
:
'
transactions
'
,
filterType
:
'
address
'
,
filterValue
}
}
ml=
{
2
}
isLoading=
{
query
.
pagination
.
isLoading
}
/>
)
:
null
}
/>
)
:
null
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment