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
add0e9bb
Commit
add0e9bb
authored
Feb 21, 2025
by
tom
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
stats pages
parent
8fb28abe
Changes
14
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
209 additions
and
92 deletions
+209
-92
[id].tsx
pages/stats/[id].tsx
+2
-2
index.tsx
pages/stats/index.tsx
+2
-2
tag.tsx
toolkit/chakra/tag.tsx
+7
-1
semanticTokens.ts
toolkit/theme/foundations/semanticTokens.ts
+7
-0
tag.recipe.ts
toolkit/theme/recipes/tag.recipe.ts
+36
-0
Chart.tsx
ui/pages/Chart.tsx
+47
-32
EmptySearchResult.tsx
ui/shared/EmptySearchResult.tsx
+3
-2
ChartIntervalSelect.tsx
ui/shared/chart/ChartIntervalSelect.tsx
+35
-19
ChartMenu.tsx
ui/shared/chart/ChartMenu.tsx
+4
-4
FullscreenChartModal.tsx
ui/shared/chart/FullscreenChartModal.tsx
+4
-3
TagGroupSelect.tsx
ui/shared/tagGroupSelect/TagGroupSelect.tsx
+5
-3
Tag.tsx
ui/showcases/Tag.tsx
+16
-0
ChartsWidgetsList.tsx
ui/stats/ChartsWidgetsList.tsx
+9
-8
StatsFilters.tsx
ui/stats/StatsFilters.tsx
+32
-16
No files found.
pages/stats/[id].tsx
View file @
add0e9bb
...
@@ -13,14 +13,14 @@ import config from 'configs/app';
...
@@ -13,14 +13,14 @@ import config from 'configs/app';
import
dayjs
from
'
lib/date/dayjs
'
;
import
dayjs
from
'
lib/date/dayjs
'
;
import
getQueryParamString
from
'
lib/router/getQueryParamString
'
;
import
getQueryParamString
from
'
lib/router/getQueryParamString
'
;
//
const Chart = dynamic(() => import('ui/pages/Chart'), { ssr: false });
const
Chart
=
dynamic
(()
=>
import
(
'
ui/pages/Chart
'
),
{
ssr
:
false
});
const
pathname
:
Route
[
'
pathname
'
]
=
'
/stats/[id]
'
;
const
pathname
:
Route
[
'
pathname
'
]
=
'
/stats/[id]
'
;
const
Page
:
NextPage
<
Props
<
typeof
pathname
>>
=
(
props
:
Props
<
typeof
pathname
>
)
=>
{
const
Page
:
NextPage
<
Props
<
typeof
pathname
>>
=
(
props
:
Props
<
typeof
pathname
>
)
=>
{
return
(
return
(
<
PageNextJs
pathname=
{
pathname
}
query=
{
props
.
query
}
apiData=
{
props
.
apiData
}
>
<
PageNextJs
pathname=
{
pathname
}
query=
{
props
.
query
}
apiData=
{
props
.
apiData
}
>
{
/* <Chart/> */
}
<
Chart
/>
</
PageNextJs
>
</
PageNextJs
>
);
);
};
};
...
...
pages/stats.tsx
→
pages/stats
/index
.tsx
View file @
add0e9bb
...
@@ -3,12 +3,12 @@ import React from 'react';
...
@@ -3,12 +3,12 @@ import React from 'react';
import
PageNextJs
from
'
nextjs/PageNextJs
'
;
import
PageNextJs
from
'
nextjs/PageNextJs
'
;
//
import Stats from 'ui/pages/Stats';
import
Stats
from
'
ui/pages/Stats
'
;
const
Page
:
NextPage
=
()
=>
{
const
Page
:
NextPage
=
()
=>
{
return
(
return
(
<
PageNextJs
pathname=
"/stats"
>
<
PageNextJs
pathname=
"/stats"
>
{
/* <Stats/> */
}
<
Stats
/>
</
PageNextJs
>
</
PageNextJs
>
);
);
};
};
...
...
toolkit/chakra/tag.tsx
View file @
add0e9bb
...
@@ -13,6 +13,7 @@ export interface TagProps extends ChakraTag.RootProps {
...
@@ -13,6 +13,7 @@ export interface TagProps extends ChakraTag.RootProps {
closable
?:
boolean
;
closable
?:
boolean
;
truncated
?:
boolean
;
truncated
?:
boolean
;
loading
?:
boolean
;
loading
?:
boolean
;
selected
?:
boolean
;
}
}
export
const
Tag
=
React
.
forwardRef
<
HTMLSpanElement
,
TagProps
>
(
export
const
Tag
=
React
.
forwardRef
<
HTMLSpanElement
,
TagProps
>
(
...
@@ -26,6 +27,7 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
...
@@ -26,6 +27,7 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
children
,
children
,
truncated
=
false
,
truncated
=
false
,
loading
,
loading
,
selected
,
...
rest
...
rest
}
=
props
;
}
=
props
;
...
@@ -37,7 +39,11 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
...
@@ -37,7 +39,11 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
return
(
return
(
<
Skeleton
loading=
{
loading
}
asChild
>
<
Skeleton
loading=
{
loading
}
asChild
>
<
ChakraTag
.
Root
ref=
{
ref
}
{
...
rest
}
>
<
ChakraTag
.
Root
ref=
{
ref
}
{
...
(
selected
&&
{
'
data
-
selected
':
true
})
}
{
...
rest
}
>
{
startElement
&&
(
{
startElement
&&
(
<
ChakraTag
.
StartElement
>
{
startElement
}
</
ChakraTag
.
StartElement
>
<
ChakraTag
.
StartElement
>
{
startElement
}
</
ChakraTag
.
StartElement
>
)
}
)
}
...
...
toolkit/theme/foundations/semanticTokens.ts
View file @
add0e9bb
...
@@ -358,6 +358,13 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
...
@@ -358,6 +358,13 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
bg
:
{
value
:
{
_light
:
'
{colors.gray.100}
'
,
_dark
:
'
{colors.gray.800}
'
}
},
bg
:
{
value
:
{
_light
:
'
{colors.gray.100}
'
,
_dark
:
'
{colors.gray.800}
'
}
},
fg
:
{
value
:
{
_light
:
'
{colors.blackAlpha.800}
'
,
_dark
:
'
{colors.whiteAlpha.800}
'
}
},
fg
:
{
value
:
{
_light
:
'
{colors.blackAlpha.800}
'
,
_dark
:
'
{colors.whiteAlpha.800}
'
}
},
},
},
select
:
{
bg
:
{
DEFAULT
:
{
value
:
{
_light
:
'
{colors.gray.100}
'
,
_dark
:
'
{colors.gray.800}
'
}
},
selected
:
{
value
:
{
_light
:
'
{colors.blue.500}
'
,
_dark
:
'
{colors.blue.900}
'
}
},
},
fg
:
{
value
:
{
_light
:
'
{colors.gray.500}
'
,
_dark
:
'
{colors.whiteAlpha.800}
'
}
},
},
},
},
closeTrigger
:
{
closeTrigger
:
{
color
:
{
value
:
{
_light
:
'
{colors.gray.400}
'
,
_dark
:
'
{colors.gray.500}
'
}
},
color
:
{
value
:
{
_light
:
'
{colors.gray.400}
'
,
_dark
:
'
{colors.gray.500}
'
}
},
...
...
toolkit/theme/recipes/tag.recipe.ts
View file @
add0e9bb
...
@@ -79,6 +79,20 @@ export const recipe = defineSlotRecipe({
...
@@ -79,6 +79,20 @@ export const recipe = defineSlotRecipe({
textStyle
:
'
sm
'
,
textStyle
:
'
sm
'
,
},
},
},
},
lg
:
{
root
:
{
px
:
'
6px
'
,
py
:
'
6px
'
,
minH
:
'
8
'
,
gap
:
'
1
'
,
'
--tag-avatar-size
'
:
'
spacing.4
'
,
'
--tag-element-size
'
:
'
spacing.3
'
,
'
--tag-element-offset
'
:
'
0px
'
,
},
label
:
{
textStyle
:
'
sm
'
,
},
},
},
},
variant
:
{
variant
:
{
...
@@ -104,6 +118,28 @@ export const recipe = defineSlotRecipe({
...
@@ -104,6 +118,28 @@ export const recipe = defineSlotRecipe({
},
},
},
},
},
},
select
:
{
root
:
{
cursor
:
'
pointer
'
,
bgColor
:
'
tag.root.select.bg
'
,
color
:
'
tag.root.select.fg
'
,
'
&:not([data-loading], [aria-busy=true])
'
:
{
bgColor
:
'
tag.root.select.bg
'
,
},
_hover
:
{
color
:
'
blue.400
'
,
opacity
:
0.76
,
},
_selected
:
{
bgColor
:
'
tag.root.select.bg.selected
'
,
color
:
'
whiteAlpha.800
'
,
_hover
:
{
color
:
'
whiteAlpha.800
'
,
opacity
:
0.76
,
},
},
},
},
},
},
},
},
...
...
ui/pages/Chart.tsx
View file @
add0e9bb
import
{
Button
,
Flex
,
Link
,
Text
}
from
'
@chakra-ui/react
'
;
import
{
createListCollection
,
Flex
,
Text
}
from
'
@chakra-ui/react
'
;
import
type
{
NextRouter
}
from
'
next/router
'
;
import
type
{
NextRouter
}
from
'
next/router
'
;
import
{
useRouter
}
from
'
next/router
'
;
import
{
useRouter
}
from
'
next/router
'
;
import
React
from
'
react
'
;
import
React
from
'
react
'
;
...
@@ -15,8 +15,11 @@ import isBrowser from 'lib/isBrowser';
...
@@ -15,8 +15,11 @@ import isBrowser from 'lib/isBrowser';
import
*
as
metadata
from
'
lib/metadata
'
;
import
*
as
metadata
from
'
lib/metadata
'
;
import
*
as
mixpanel
from
'
lib/mixpanel/index
'
;
import
*
as
mixpanel
from
'
lib/mixpanel/index
'
;
import
getQueryParamString
from
'
lib/router/getQueryParamString
'
;
import
getQueryParamString
from
'
lib/router/getQueryParamString
'
;
import
{
Button
}
from
'
toolkit/chakra/button
'
;
import
{
IconButton
}
from
'
toolkit/chakra/icon-button
'
;
import
{
SelectContent
,
SelectControl
,
SelectItem
,
SelectRoot
,
SelectValueText
}
from
'
toolkit/chakra/select
'
;
import
{
Skeleton
}
from
'
toolkit/chakra/skeleton
'
;
import
isCustomAppError
from
'
ui/shared/AppError/isCustomAppError
'
;
import
isCustomAppError
from
'
ui/shared/AppError/isCustomAppError
'
;
import
Skeleton
from
'
ui/shared/chakra/Skeleton
'
;
import
ChartIntervalSelect
from
'
ui/shared/chart/ChartIntervalSelect
'
;
import
ChartIntervalSelect
from
'
ui/shared/chart/ChartIntervalSelect
'
;
import
ChartMenu
from
'
ui/shared/chart/ChartMenu
'
;
import
ChartMenu
from
'
ui/shared/chart/ChartMenu
'
;
import
ChartWidgetContent
from
'
ui/shared/chart/ChartWidgetContent
'
;
import
ChartWidgetContent
from
'
ui/shared/chart/ChartWidgetContent
'
;
...
@@ -25,7 +28,6 @@ import useZoom from 'ui/shared/chart/useZoom';
...
@@ -25,7 +28,6 @@ import useZoom from 'ui/shared/chart/useZoom';
import
CopyToClipboard
from
'
ui/shared/CopyToClipboard
'
;
import
CopyToClipboard
from
'
ui/shared/CopyToClipboard
'
;
import
IconSvg
from
'
ui/shared/IconSvg
'
;
import
IconSvg
from
'
ui/shared/IconSvg
'
;
import
PageTitle
from
'
ui/shared/Page/PageTitle
'
;
import
PageTitle
from
'
ui/shared/Page/PageTitle
'
;
import
Select
from
'
ui/shared/select/Select
'
;
import
{
STATS_RESOLUTIONS
}
from
'
ui/stats/constants
'
;
import
{
STATS_RESOLUTIONS
}
from
'
ui/stats/constants
'
;
const
DEFAULT_RESOLUTION
=
Resolution
.
DAY
;
const
DEFAULT_RESOLUTION
=
Resolution
.
DAY
;
...
@@ -108,11 +110,11 @@ const Chart = () => {
...
@@ -108,11 +110,11 @@ const Chart = () => {
);
);
},
[
setIntervalState
,
router
]);
},
[
setIntervalState
,
router
]);
const
onResolutionChange
=
React
.
useCallback
((
resolution
:
Resolution
)
=>
{
const
onResolutionChange
=
React
.
useCallback
((
{
value
}:
{
value
:
Array
<
string
>
}
)
=>
{
setResolution
(
r
esolution
);
setResolution
(
value
[
0
]
as
R
esolution
);
router
.
push
({
router
.
push
({
pathname
:
router
.
pathname
,
pathname
:
router
.
pathname
,
query
:
{
...
router
.
query
,
resolution
},
query
:
{
...
router
.
query
,
resolution
:
value
[
0
]
},
},
},
undefined
,
undefined
,
{
shallow
:
true
},
{
shallow
:
true
},
...
@@ -121,7 +123,7 @@ const Chart = () => {
...
@@ -121,7 +123,7 @@ const Chart = () => {
const
handleReset
=
React
.
useCallback
(()
=>
{
const
handleReset
=
React
.
useCallback
(()
=>
{
handleZoomReset
();
handleZoomReset
();
onResolutionChange
(
DEFAULT_RESOLUTION
);
onResolutionChange
(
{
value
:
[
DEFAULT_RESOLUTION
]
}
);
},
[
handleZoomReset
,
onResolutionChange
]);
},
[
handleZoomReset
,
onResolutionChange
]);
const
{
items
,
info
,
lineQuery
}
=
useChartQuery
(
id
,
resolution
,
interval
);
const
{
items
,
info
,
lineQuery
}
=
useChartQuery
(
id
,
resolution
,
interval
);
...
@@ -155,22 +157,24 @@ const Chart = () => {
...
@@ -155,22 +157,24 @@ const Chart = () => {
const
shareButton
=
(
const
shareButton
=
(
<
Button
<
Button
leftIcon=
{
<
IconSvg
name=
"share"
w=
{
4
}
h=
{
4
}
/>
}
colorScheme=
"blue"
size=
"sm"
size=
"sm"
variant=
"outline"
variant=
"outline"
onClick=
{
onShare
}
onClick=
{
onShare
}
ml=
{
6
}
ml=
{
6
}
loadingSkeleton=
{
lineQuery
.
isPlaceholderData
}
>
>
<
IconSvg
name=
"share"
w=
{
4
}
h=
{
4
}
/>
Share
Share
</
Button
>
</
Button
>
);
);
const
resolution
Options
=
React
.
useMemo
(()
=>
{
const
resolution
Collection
=
React
.
useMemo
(()
=>
{
const
resolutions
=
lineQuery
.
data
?.
info
?.
resolutions
||
[];
const
resolutions
=
lineQuery
.
data
?.
info
?.
resolutions
||
[];
return
STATS_RESOLUTIONS
const
items
=
STATS_RESOLUTIONS
.
filter
((
resolution
)
=>
resolutions
.
includes
(
resolution
.
id
))
.
filter
((
resolution
)
=>
resolutions
.
includes
(
resolution
.
id
))
.
map
((
resolution
)
=>
({
value
:
resolution
.
id
,
label
:
resolution
.
title
}));
.
map
((
resolution
)
=>
({
value
:
resolution
.
id
,
label
:
resolution
.
title
}));
return
createListCollection
({
items
});
},
[
lineQuery
.
data
?.
info
?.
resolutions
]);
},
[
lineQuery
.
data
?.
info
?.
resolutions
]);
return
(
return
(
...
@@ -194,21 +198,32 @@ const Chart = () => {
...
@@ -194,21 +198,32 @@ const Chart = () => {
(
!
info
&&
lineQuery
.
data
?.
info
?.
resolutions
&&
lineQuery
.
data
?.
info
?.
resolutions
.
length
>
1
)
(
!
info
&&
lineQuery
.
data
?.
info
?.
resolutions
&&
lineQuery
.
data
?.
info
?.
resolutions
.
length
>
1
)
)
&&
(
)
&&
(
<
Flex
alignItems=
"center"
gap=
{
3
}
>
<
Flex
alignItems=
"center"
gap=
{
3
}
>
<
Skeleton
isLoaded=
{
!
isInfoLoading
}
>
<
Skeleton
loading=
{
isInfoLoading
}
>
{
isMobile
?
'
Res.
'
:
'
Resolution
'
}
{
isMobile
?
'
Res.
'
:
'
Resolution
'
}
</
Skeleton
>
</
Skeleton
>
<
Select
<
Select
Root
options=
{
resolutionOptions
}
collection=
{
resolutionCollection
}
defaultValue=
{
defaultResolution
}
variant=
"outline"
onChange=
{
onResolutionChange
}
defaultValue=
{
[
defaultResolution
]
}
isLoading=
{
isInfoLoading
}
onValueChange=
{
onResolutionChange
}
w=
{
{
base
:
'
fit-content
'
,
lg
:
'
160px
'
}
}
w=
{
{
base
:
'
fit-content
'
,
lg
:
'
160px
'
}
}
fontWeight=
{
600
}
>
/>
<
SelectControl
loading=
{
isInfoLoading
}
>
<
SelectValueText
placeholder=
"Select resolution"
/>
</
SelectControl
>
<
SelectContent
>
{
resolutionCollection
.
items
.
map
((
item
)
=>
(
<
SelectItem
item=
{
item
}
key=
{
item
.
value
}
>
{
item
.
label
}
</
SelectItem
>
))
}
</
SelectContent
>
</
SelectRoot
>
</
Flex
>
</
Flex
>
)
}
)
}
{
(
Boolean
(
zoomRange
))
&&
(
{
(
Boolean
(
zoomRange
))
&&
(
<
Link
<
Button
variant=
"link"
onClick=
{
handleReset
}
onClick=
{
handleReset
}
display=
"flex"
display=
"flex"
alignItems=
"center"
alignItems=
"center"
...
@@ -216,7 +231,7 @@ const Chart = () => {
...
@@ -216,7 +231,7 @@ const Chart = () => {
>
>
<
IconSvg
name=
"repeat"
w=
{
5
}
h=
{
5
}
/>
<
IconSvg
name=
"repeat"
w=
{
5
}
h=
{
5
}
/>
{
!
isMobile
&&
'
Reset
'
}
{
!
isMobile
&&
'
Reset
'
}
</
Link
>
</
Button
>
)
}
)
}
</
Flex
>
</
Flex
>
<
Flex
alignItems=
"center"
gap=
{
3
}
>
<
Flex
alignItems=
"center"
gap=
{
3
}
>
...
@@ -225,23 +240,23 @@ const Chart = () => {
...
@@ -225,23 +240,23 @@ const Chart = () => {
{
!
isMobile
&&
(
isInBrowser
&&
((
window
.
navigator
.
share
as
any
)
?
{
!
isMobile
&&
(
isInBrowser
&&
((
window
.
navigator
.
share
as
any
)
?
shareButton
:
shareButton
:
(
(
<
IconButton
variant=
"outline"
size=
"sm"
asChild
p=
{
1
}
>
<
CopyToClipboard
<
CopyToClipboard
text=
{
config
.
app
.
baseUrl
+
router
.
asPath
}
text=
{
config
.
app
.
baseUrl
+
router
.
asPath
}
size=
{
5
}
type=
"link"
type=
"link"
variant=
"outline"
boxSize=
{
8
}
colorScheme=
"blue"
color=
"button.outline.fg"
display=
"flex"
ml=
{
0
}
borderRadius=
"8px"
borderRadius=
"base"
width=
{
8
}
height=
{
8
}
/>
/>
</
IconButton
>
)
)
))
}
))
}
{
(
hasItems
||
lineQuery
.
isPlaceholderData
)
&&
(
{
(
hasItems
||
lineQuery
.
isPlaceholderData
)
&&
(
<
ChartMenu
<
ChartMenu
items=
{
items
}
items=
{
items
}
title=
{
info
?.
title
||
''
}
title=
{
info
?.
title
||
''
}
description=
{
info
?.
description
||
''
}
isLoading=
{
lineQuery
.
isPlaceholderData
}
isLoading=
{
lineQuery
.
isPlaceholderData
}
chartRef=
{
ref
}
chartRef=
{
ref
}
resolution=
{
resolution
}
resolution=
{
resolution
}
...
...
ui/shared/EmptySearchResult.tsx
View file @
add0e9bb
import
{
Box
,
Heading
,
Icon
}
from
'
@chakra-ui/react
'
;
import
{
Box
,
Icon
}
from
'
@chakra-ui/react
'
;
import
React
from
'
react
'
;
import
React
from
'
react
'
;
// This icon doesn't work properly when it is in the sprite
// This icon doesn't work properly when it is in the sprite
// Probably because of radial gradient
// Probably because of radial gradient
// eslint-disable-next-line no-restricted-imports
// eslint-disable-next-line no-restricted-imports
import
emptySearchResultIcon
from
'
icons/empty_search_result.svg
'
;
import
emptySearchResultIcon
from
'
icons/empty_search_result.svg
'
;
import
{
Heading
}
from
'
toolkit/chakra/heading
'
;
interface
Props
{
interface
Props
{
text
:
string
|
React
.
JSX
.
Element
;
text
:
string
|
React
.
JSX
.
Element
;
...
@@ -26,7 +27,7 @@ const EmptySearchResult = ({ text }: Props) => {
...
@@ -26,7 +27,7 @@ const EmptySearchResult = ({ text }: Props) => {
mb=
{
{
base
:
4
,
sm
:
6
}
}
mb=
{
{
base
:
4
,
sm
:
6
}
}
/>
/>
<
Heading
as=
"h4"
size=
"sm
"
mb=
{
2
}
>
<
Heading
level=
"3
"
mb=
{
2
}
>
No results
No results
</
Heading
>
</
Heading
>
...
...
ui/shared/chart/ChartIntervalSelect.tsx
View file @
add0e9bb
import
type
{
TagProps
}
from
'
@chakra-ui/react
'
;
import
{
createListCollection
}
from
'
@chakra-ui/react
'
;
import
React
from
'
react
'
;
import
React
from
'
react
'
;
import
type
{
StatsInterval
,
StatsIntervalIds
}
from
'
types/client/stats
'
;
import
type
{
StatsInterval
,
StatsIntervalIds
}
from
'
types/client/stats
'
;
import
type
{
SelectOption
}
from
'
toolkit/chakra/select
'
;
import
{
SelectContent
,
SelectControl
,
SelectItem
,
SelectRoot
,
SelectValueText
}
from
'
toolkit/chakra/select
'
;
import
Skeleton
from
'
ui/shared/chakra/S
keleton
'
;
import
{
Skeleton
}
from
'
toolkit/chakra/s
keleton
'
;
import
Select
from
'
ui/shared/select/Select
'
;
import
type
{
TagProps
}
from
'
toolkit/chakra/tag
'
;
import
TagGroupSelect
from
'
ui/shared/tagGroupSelect/TagGroupSelect
'
;
import
TagGroupSelect
from
'
ui/shared/tagGroupSelect/TagGroupSelect
'
;
import
{
STATS_INTERVALS
}
from
'
ui/stats/constants
'
;
import
{
STATS_INTERVALS
}
from
'
ui/stats/constants
'
;
const
intervalList
=
Object
.
keys
(
STATS_INTERVALS
).
map
((
id
:
string
)
=>
({
const
intervalCollection
=
createListCollection
({
items
:
Object
.
keys
(
STATS_INTERVALS
).
map
((
id
:
string
)
=>
({
value
:
id
,
value
:
id
,
label
:
STATS_INTERVALS
[
id
as
StatsIntervalIds
].
title
,
label
:
STATS_INTERVALS
[
id
as
StatsIntervalIds
].
shortTitle
,
}))
as
Array
<
SelectOption
>
;
})),
});
const
intervalListShort
=
Object
.
keys
(
STATS_INTERVALS
).
map
((
id
:
string
)
=>
({
const
intervalListShort
=
Object
.
keys
(
STATS_INTERVALS
).
map
((
id
:
string
)
=>
({
id
:
id
,
id
:
id
,
...
@@ -27,21 +29,35 @@ type Props = {
...
@@ -27,21 +29,35 @@ type Props = {
};
};
const
ChartIntervalSelect
=
({
interval
,
onIntervalChange
,
isLoading
,
selectTagSize
}:
Props
)
=>
{
const
ChartIntervalSelect
=
({
interval
,
onIntervalChange
,
isLoading
,
selectTagSize
}:
Props
)
=>
{
const
handleItemSelect
=
React
.
useCallback
(({
value
}:
{
value
:
Array
<
string
>
})
=>
{
onIntervalChange
(
value
[
0
]
as
StatsIntervalIds
);
},
[
onIntervalChange
]);
return
(
return
(
<>
<>
<
Skeleton
display=
{
{
base
:
'
none
'
,
lg
:
'
flex
'
}
}
borderRadius=
"base"
isLoaded=
{
!
isLoading
}
>
<
Skeleton
hideBelow=
"lg"
borderRadius=
"base"
loading=
{
isLoading
}
>
<
TagGroupSelect
<
StatsIntervalIds
>
items=
{
intervalListShort
}
onChange=
{
onIntervalChange
}
value=
{
interval
}
tagSize=
{
selectTagSize
}
/
>
<
TagGroupSelect
<
StatsIntervalIds
>
items=
{
intervalListShort
}
onChange=
{
onIntervalChange
}
value=
{
interval
}
tagSize=
{
selectTagSize
}
/
>
</
Skeleton
>
</
Skeleton
>
<
Select
<
SelectRoot
options=
{
intervalList
}
collection=
{
intervalCollection
}
defaultValue=
{
interval
}
variant=
"outline"
onChange=
{
onIntervalChange
}
defaultValue=
{
[
interval
]
}
isLoading=
{
isLoading
}
onValueChange=
{
handleItemSelect
}
w=
{
{
base
:
'
100%
'
,
lg
:
'
136px
'
}
}
hideFrom=
"lg"
display=
{
{
base
:
'
flex
'
,
lg
:
'
none
'
}
}
w=
"100%"
flexShrink=
{
0
}
>
fontWeight=
{
600
}
<
SelectControl
loading=
{
isLoading
}
>
/>
<
SelectValueText
placeholder=
"Select interval"
/>
</
SelectControl
>
<
SelectContent
>
{
intervalCollection
.
items
.
map
((
item
)
=>
(
<
SelectItem
item=
{
item
}
key=
{
item
.
value
}
>
{
item
.
label
}
</
SelectItem
>
))
}
</
SelectContent
>
</
SelectRoot
>
</>
</>
);
);
};
};
...
...
ui/shared/chart/ChartMenu.tsx
View file @
add0e9bb
...
@@ -130,7 +130,7 @@ const ChartMenu = ({
...
@@ -130,7 +130,7 @@ const ChartMenu = ({
onClick=
{
hasShare
?
handleShare
:
handleCopy
}
onClick=
{
hasShare
?
handleShare
:
handleCopy
}
closeOnSelect=
{
hasShare
?
false
:
true
}
closeOnSelect=
{
hasShare
?
false
:
true
}
>
>
<
IconSvg
name=
{
hasShare
?
'
share
'
:
'
copy
'
}
boxSize=
{
5
}
mr=
{
3
}
/>
<
IconSvg
name=
{
hasShare
?
'
share
'
:
'
copy
'
}
boxSize=
{
5
}
/>
{
hasShare
?
'
Share
'
:
'
Copy link
'
}
{
hasShare
?
'
Share
'
:
'
Copy link
'
}
</
MenuItem
>
</
MenuItem
>
)
}
)
}
...
@@ -138,21 +138,21 @@ const ChartMenu = ({
...
@@ -138,21 +138,21 @@ const ChartMenu = ({
value=
"fullscreen"
value=
"fullscreen"
onClick=
{
showChartFullscreen
}
onClick=
{
showChartFullscreen
}
>
>
<
IconSvg
name=
"scope"
boxSize=
{
5
}
mr=
{
3
}
/>
<
IconSvg
name=
"scope"
boxSize=
{
5
}
/>
View fullscreen
View fullscreen
</
MenuItem
>
</
MenuItem
>
<
MenuItem
<
MenuItem
value=
"save-png"
value=
"save-png"
onClick=
{
handleFileSaveClick
}
onClick=
{
handleFileSaveClick
}
>
>
<
IconSvg
name=
"files/image"
boxSize=
{
5
}
mr=
{
3
}
/>
<
IconSvg
name=
"files/image"
boxSize=
{
5
}
/>
Save as PNG
Save as PNG
</
MenuItem
>
</
MenuItem
>
<
MenuItem
<
MenuItem
value=
"save-csv"
value=
"save-csv"
onClick=
{
handleSVGSavingClick
}
onClick=
{
handleSVGSavingClick
}
>
>
<
IconSvg
name=
"files/csv"
boxSize=
{
5
}
mr=
{
3
}
/>
<
IconSvg
name=
"files/csv"
boxSize=
{
5
}
/>
Save as CSV
Save as CSV
</
MenuItem
>
</
MenuItem
>
</
MenuContent
>
</
MenuContent
>
...
...
ui/shared/chart/FullscreenChartModal.tsx
View file @
add0e9bb
...
@@ -40,12 +40,13 @@ const FullscreenChartModal = ({
...
@@ -40,12 +40,13 @@ const FullscreenChartModal = ({
<
DialogRoot
<
DialogRoot
open=
{
open
}
open=
{
open
}
onOpenChange=
{
onOpenChange
}
onOpenChange=
{
onOpenChange
}
size=
"full"
// FIXME: with size="full" the chart will not be expanded to the full height of the modal
size=
"cover"
>
>
<
DialogContent
>
<
DialogContent
>
<
DialogHeader
/>
<
DialogHeader
/>
<
DialogBody
pt=
{
6
}
display=
"flex"
flexDir=
"column"
>
<
DialogBody
pt=
{
6
}
display=
"flex"
flexDir=
"column"
>
<
Grid
gridColumnGap=
{
2
}
>
<
Grid
gridColumnGap=
{
2
}
mb=
{
4
}
>
<
Heading
mb=
{
1
}
level=
"2"
>
<
Heading
mb=
{
1
}
level=
"2"
>
{
title
}
{
title
}
</
Heading
>
</
Heading
>
...
@@ -54,7 +55,7 @@ const FullscreenChartModal = ({
...
@@ -54,7 +55,7 @@ const FullscreenChartModal = ({
<
Text
<
Text
gridColumn=
{
1
}
gridColumn=
{
1
}
color=
"text.secondary"
color=
"text.secondary"
fontSize=
"xs
"
textStyle=
"sm
"
>
>
{
description
}
{
description
}
</
Text
>
</
Text
>
...
...
ui/shared/tagGroupSelect/TagGroupSelect.tsx
View file @
add0e9bb
import
type
{
TagProps
}
from
'
@chakra-ui/react
'
;
import
{
HStack
}
from
'
@chakra-ui/react
'
;
import
{
HStack
,
Tag
}
from
'
@chakra-ui/react
'
;
import
React
from
'
react
'
;
import
React
from
'
react
'
;
import
type
{
TagProps
}
from
'
toolkit/chakra/tag
'
;
import
{
Tag
}
from
'
toolkit/chakra/tag
'
;
type
Props
<
T
extends
string
>
=
{
type
Props
<
T
extends
string
>
=
{
items
:
Array
<
{
id
:
T
;
title
:
string
}
>
;
items
:
Array
<
{
id
:
T
;
title
:
string
}
>
;
tagSize
?:
TagProps
[
'
size
'
];
tagSize
?:
TagProps
[
'
size
'
];
...
@@ -42,7 +44,7 @@ const TagGroupSelect = <T extends string>({ items, value, isMulti, onChange, tag
...
@@ -42,7 +44,7 @@ const TagGroupSelect = <T extends string>({ items, value, isMulti, onChange, tag
variant=
"select"
variant=
"select"
key=
{
item
.
id
}
key=
{
item
.
id
}
data
-
id=
{
item
.
id
}
data
-
id=
{
item
.
id
}
data
-
selected=
{
isSelected
}
selected=
{
isSelected
}
fontWeight=
{
500
}
fontWeight=
{
500
}
onClick=
{
onItemClick
}
onClick=
{
onItemClick
}
size=
{
tagSize
}
size=
{
tagSize
}
...
...
ui/showcases/Tag.tsx
View file @
add0e9bb
...
@@ -19,6 +19,22 @@ const TagShowcase = () => {
...
@@ -19,6 +19,22 @@ const TagShowcase = () => {
<
Sample
label=
"variant: clickable"
>
<
Sample
label=
"variant: clickable"
>
<
Tag
variant=
"clickable"
>
My tag
</
Tag
>
<
Tag
variant=
"clickable"
>
My tag
</
Tag
>
</
Sample
>
</
Sample
>
<
Sample
label=
"variant: select"
>
<
Tag
variant=
"select"
>
Default
</
Tag
>
<
Tag
variant=
"select"
selected
>
Selected
</
Tag
>
</
Sample
>
</
SamplesStack
>
</
Section
>
<
Section
>
<
SectionHeader
>
Size
</
SectionHeader
>
<
SamplesStack
>
<
Sample
label=
"size: md"
>
<
Tag
size=
"md"
>
My tag
</
Tag
>
</
Sample
>
<
Sample
label=
"size: lg"
>
<
Tag
size=
"lg"
>
My tag
</
Tag
>
</
Sample
>
</
SamplesStack
>
</
SamplesStack
>
</
Section
>
</
Section
>
...
...
ui/stats/ChartsWidgetsList.tsx
View file @
add0e9bb
import
{
Box
,
Grid
,
Heading
,
List
,
ListItem
}
from
'
@chakra-ui/react
'
;
import
{
Box
,
Grid
}
from
'
@chakra-ui/react
'
;
import
React
,
{
useCallback
,
useState
}
from
'
react
'
;
import
React
,
{
useCallback
,
useState
}
from
'
react
'
;
import
type
*
as
stats
from
'
@blockscout/stats-types
'
;
import
type
*
as
stats
from
'
@blockscout/stats-types
'
;
...
@@ -6,7 +6,8 @@ import type { StatsIntervalIds } from 'types/client/stats';
...
@@ -6,7 +6,8 @@ import type { StatsIntervalIds } from 'types/client/stats';
import
useApiQuery
from
'
lib/api/useApiQuery
'
;
import
useApiQuery
from
'
lib/api/useApiQuery
'
;
import
{
apos
}
from
'
lib/html-entities
'
;
import
{
apos
}
from
'
lib/html-entities
'
;
import
Skeleton
from
'
ui/shared/chakra/Skeleton
'
;
import
{
Heading
}
from
'
toolkit/chakra/heading
'
;
import
{
Skeleton
}
from
'
toolkit/chakra/skeleton
'
;
import
EmptySearchResult
from
'
ui/shared/EmptySearchResult
'
;
import
EmptySearchResult
from
'
ui/shared/EmptySearchResult
'
;
import
GasInfoTooltip
from
'
ui/shared/gas/GasInfoTooltip
'
;
import
GasInfoTooltip
from
'
ui/shared/gas/GasInfoTooltip
'
;
import
IconSvg
from
'
ui/shared/IconSvg
'
;
import
IconSvg
from
'
ui/shared/IconSvg
'
;
...
@@ -61,18 +62,18 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
...
@@ -61,18 +62,18 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
<
ChartsLoadingErrorAlert
/>
<
ChartsLoadingErrorAlert
/>
)
}
)
}
<
List
ref=
{
sectionRef
}
>
<
section
ref=
{
sectionRef
}
>
{
{
charts
?.
map
((
section
)
=>
(
charts
?.
map
((
section
)
=>
(
<
ListItem
<
Box
key=
{
section
.
id
}
key=
{
section
.
id
}
mb=
{
8
}
mb=
{
8
}
_last=
{
{
_last=
{
{
marginBottom
:
0
,
marginBottom
:
0
,
}
}
}
}
>
>
<
Skeleton
isLoaded=
{
!
isPlaceholderData
}
mb=
{
4
}
display=
"inline-flex"
alignItems=
"center"
columnGap=
{
2
}
id=
{
section
.
id
}
>
<
Skeleton
loading=
{
isPlaceholderData
}
mb=
{
4
}
display=
"inline-flex"
alignItems=
"center"
columnGap=
{
2
}
id=
{
section
.
id
}
>
<
Heading
size=
"md
"
id=
{
section
.
id
}
>
<
Heading
level=
"2
"
id=
{
section
.
id
}
>
{
section
.
title
}
{
section
.
title
}
</
Heading
>
</
Heading
>
{
section
.
id
===
'
gas
'
&&
homeStatsQuery
.
data
&&
homeStatsQuery
.
data
.
gas_prices
&&
(
{
section
.
id
===
'
gas
'
&&
homeStatsQuery
.
data
&&
homeStatsQuery
.
data
.
gas_prices
&&
(
...
@@ -100,10 +101,10 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
...
@@ -100,10 +101,10 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
/>
/>
))
}
))
}
</
Grid
>
</
Grid
>
</
ListItem
>
</
Box
>
))
))
}
}
</
List
>
</
section
>
</
Box
>
</
Box
>
);
);
};
};
...
...
ui/stats/StatsFilters.tsx
View file @
add0e9bb
import
{
Grid
,
GridItem
}
from
'
@chakra-ui/react
'
;
import
{
createListCollection
,
Grid
,
GridItem
}
from
'
@chakra-ui/react
'
;
import
React
from
'
react
'
;
import
React
from
'
react
'
;
import
type
*
as
stats
from
'
@blockscout/stats-types
'
;
import
type
*
as
stats
from
'
@blockscout/stats-types
'
;
import
type
{
StatsIntervalIds
}
from
'
types/client/stats
'
;
import
type
{
StatsIntervalIds
}
from
'
types/client/stats
'
;
import
{
SelectContent
,
SelectControl
,
SelectItem
,
SelectRoot
,
SelectValueText
}
from
'
toolkit/chakra/select
'
;
import
ChartIntervalSelect
from
'
ui/shared/chart/ChartIntervalSelect
'
;
import
ChartIntervalSelect
from
'
ui/shared/chart/ChartIntervalSelect
'
;
import
FilterInput
from
'
ui/shared/filters/FilterInput
'
;
import
FilterInput
from
'
ui/shared/filters/FilterInput
'
;
import
Select
from
'
ui/shared/select/Select
'
;
type
Props
=
{
type
Props
=
{
sections
?:
Array
<
stats
.
LineChartSection
>
;
sections
?:
Array
<
stats
.
LineChartSection
>
;
...
@@ -30,13 +30,19 @@ const StatsFilters = ({
...
@@ -30,13 +30,19 @@ const StatsFilters = ({
initialFilterValue
,
initialFilterValue
,
}:
Props
)
=>
{
}:
Props
)
=>
{
const
options
=
React
.
useMemo
(()
=>
{
const
collection
=
React
.
useMemo
(()
=>
{
return
[
return
createListCollection
({
items
:
[
{
value
:
'
all
'
,
label
:
'
All stats
'
},
{
value
:
'
all
'
,
label
:
'
All stats
'
},
...(
sections
||
[]).
map
((
section
)
=>
({
value
:
section
.
id
,
label
:
section
.
title
})),
...(
sections
||
[]).
map
((
section
)
=>
({
value
:
section
.
id
,
label
:
section
.
title
})),
];
],
});
},
[
sections
]);
},
[
sections
]);
const
handleItemSelect
=
React
.
useCallback
(({
value
}:
{
value
:
Array
<
string
>
})
=>
{
onSectionChange
(
value
[
0
]);
},
[
onSectionChange
]);
return
(
return
(
<
Grid
<
Grid
gap=
{
{
base
:
2
,
lg
:
6
}
}
gap=
{
{
base
:
2
,
lg
:
6
}
}
...
@@ -52,14 +58,24 @@ const StatsFilters = ({
...
@@ -52,14 +58,24 @@ const StatsFilters = ({
w=
{
{
base
:
'
100%
'
,
lg
:
'
auto
'
}
}
w=
{
{
base
:
'
100%
'
,
lg
:
'
auto
'
}
}
area=
"section"
area=
"section"
>
>
<
Select
<
Select
Root
options=
{
options
}
collection=
{
collection
}
defaultValue=
{
currentSection
}
variant=
"outline"
onChange=
{
onSectionChange
}
defaultValue=
{
[
currentSection
]
}
isLoading=
{
isLoading
}
onValueChange=
{
handleItemSelect
}
w=
{
{
base
:
'
100%
'
,
lg
:
'
136px
'
}
}
w=
{
{
base
:
'
100%
'
,
lg
:
'
136px
'
}
}
fontWeight=
{
600
}
>
/>
<
SelectControl
loading=
{
isLoading
}
>
<
SelectValueText
placeholder=
"Select section"
/>
</
SelectControl
>
<
SelectContent
>
{
collection
.
items
.
map
((
item
)
=>
(
<
SelectItem
item=
{
item
}
key=
{
item
.
value
}
>
{
item
.
label
}
</
SelectItem
>
))
}
</
SelectContent
>
</
SelectRoot
>
</
GridItem
>
</
GridItem
>
<
GridItem
<
GridItem
...
@@ -75,11 +91,11 @@ const StatsFilters = ({
...
@@ -75,11 +91,11 @@ const StatsFilters = ({
>
>
<
FilterInput
<
FilterInput
key=
{
initialFilterValue
}
key=
{
initialFilterValue
}
isL
oading=
{
isLoading
}
l
oading=
{
isLoading
}
onChange=
{
onFilterInputChange
}
onChange=
{
onFilterInputChange
}
placeholder=
"Find chart, metric..."
placeholder=
"Find chart, metric..."
initialValue=
{
initialFilterValue
}
initialValue=
{
initialFilterValue
}
size=
"
xs
"
size=
"
sm
"
/>
/>
</
GridItem
>
</
GridItem
>
</
Grid
>
</
Grid
>
...
...
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