1
1
import { EmptySimple } from '@/components/Empty' ;
2
- import { Text , Button , Center , Box , Group , ActionIcon , Stack , Tooltip , ScrollArea , Loader , Flex } from '@mantine/core' ;
3
- import { IconChevronRight , IconExternalLink , IconPlus } from '@tabler/icons-react' ;
4
- import { useEffect , type FC , useCallback , useMemo } from 'react' ;
2
+ import {
3
+ Text ,
4
+ Button ,
5
+ Center ,
6
+ Box ,
7
+ Group ,
8
+ ActionIcon ,
9
+ Stack ,
10
+ Tooltip ,
11
+ ScrollArea ,
12
+ Loader ,
13
+ Flex ,
14
+ TextInput ,
15
+ Kbd ,
16
+ px ,
17
+ } from '@mantine/core' ;
18
+ import { IconChevronRight , IconExternalLink , IconPlus , IconSearch } from '@tabler/icons-react' ;
19
+ import { useEffect , type FC , useCallback , useMemo , useState , useRef } from 'react' ;
5
20
import { useNavigate } from 'react-router-dom' ;
6
- import { useDocumentTitle } from '@mantine/hooks' ;
21
+ import { useDocumentTitle , useHotkeys } from '@mantine/hooks' ;
7
22
import { useGetStreamMetadata } from '@/hooks/useGetStreamMetadata' ;
8
23
import { calcCompressionRate , formatBytes , sanitizeEventsCount } from '@/utils/formatBytes' ;
9
24
import { LogStreamRetention , LogStreamStat } from '@/@types/parseable/api/stream' ;
@@ -20,41 +35,42 @@ const { changeStream, toggleCreateStreamModal } = appStoreReducers;
20
35
21
36
type NoStreamsViewProps = {
22
37
hasCreateStreamAccess : boolean ;
38
+ shouldHideFooter ?: boolean ;
23
39
openCreateStreamModal : ( ) => void ;
24
40
} ;
25
41
26
42
const NoStreamsView : FC < NoStreamsViewProps > = ( {
27
43
hasCreateStreamAccess,
44
+ shouldHideFooter = false ,
28
45
openCreateStreamModal,
29
- } : {
30
- hasCreateStreamAccess : boolean ;
31
- openCreateStreamModal : ( ) => void ;
32
- } ) => {
46
+ } : NoStreamsViewProps ) => {
33
47
const classes = homeStyles ;
34
48
const { messageStyle, btnStyle, noDataViewContainer, createStreamButton } = classes ;
35
49
return (
36
50
< Center className = { noDataViewContainer } >
37
51
< EmptySimple height = { 70 } width = { 100 } />
38
52
< Text className = { messageStyle } > No Stream found on this account</ Text >
39
- < Flex gap = "md" >
40
- < Button
41
- target = "_blank"
42
- component = "a"
43
- href = "https://www.parseable.io/docs/category/log-ingestion"
44
- className = { btnStyle }
45
- leftSection = { < IconExternalLink size = "0.9rem" /> } >
46
- Documentation
47
- </ Button >
48
- { hasCreateStreamAccess && (
53
+ { ! shouldHideFooter && (
54
+ < Flex gap = "md" >
49
55
< Button
50
- style = { { marginTop : '1rem' } }
51
- className = { createStreamButton }
52
- onClick = { openCreateStreamModal }
53
- leftSection = { < IconPlus stroke = { 2 } size = { '1rem' } /> } >
54
- Create Stream
56
+ target = "_blank"
57
+ component = "a"
58
+ href = "https://www.parseable.io/docs/category/log-ingestion"
59
+ className = { btnStyle }
60
+ leftSection = { < IconExternalLink size = "0.9rem" /> } >
61
+ Documentation
55
62
</ Button >
56
- ) }
57
- </ Flex >
63
+ { hasCreateStreamAccess && (
64
+ < Button
65
+ style = { { marginTop : '1rem' } }
66
+ className = { createStreamButton }
67
+ onClick = { openCreateStreamModal }
68
+ leftSection = { < IconPlus stroke = { 2 } size = { '1rem' } /> } >
69
+ Create Stream
70
+ </ Button >
71
+ ) }
72
+ </ Flex >
73
+ ) }
58
74
</ Center >
59
75
) ;
60
76
} ;
@@ -68,15 +84,27 @@ const Home: FC = () => {
68
84
const [ userSpecificStreams , setAppStore ] = useAppStore ( ( store ) => store . userSpecificStreams ) ;
69
85
const [ userRoles ] = useAppStore ( ( store ) => store . userRoles ) ;
70
86
const [ userAccessMap ] = useAppStore ( ( store ) => store . userAccessMap ) ;
87
+ const searchInputRef = useRef < HTMLInputElement > ( null ) ;
88
+ const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
71
89
72
90
useEffect ( ( ) => {
73
91
if ( ! Array . isArray ( userSpecificStreams ) || userSpecificStreams . length === 0 ) return ;
74
92
getStreamMetadata ( userSpecificStreams . map ( ( stream ) => stream . name ) ) ;
75
93
} , [ userSpecificStreams ] ) ;
76
94
95
+ const filteredMetaData = useMemo ( ( ) => {
96
+ if ( ! searchTerm || ! metaData ) return metaData || { } ;
97
+ return Object . fromEntries (
98
+ Object . entries ( metaData ) . filter ( ( [ stream ] ) => stream . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ) ,
99
+ ) ;
100
+ } , [ searchTerm , metaData ] ) ;
101
+
102
+ useHotkeys ( [ [ 'mod+K' , ( ) => searchInputRef . current ?. focus ( ) ] ] ) ;
103
+
77
104
const navigateToStream = useCallback ( ( stream : string ) => {
78
105
setAppStore ( ( store ) => changeStream ( store , stream ) ) ;
79
106
navigate ( `/${ stream } /explore` ) ;
107
+ setSearchTerm ( '' ) ;
80
108
} , [ ] ) ;
81
109
82
110
const displayEmptyPlaceholder = Array . isArray ( userSpecificStreams ) && userSpecificStreams . length === 0 ;
@@ -87,6 +115,14 @@ const Home: FC = () => {
87
115
const hasCreateStreamAccess = useMemo ( ( ) => userAccessMap ?. hasCreateStreamAccess , [ userAccessMap ] ) ;
88
116
89
117
const shouldDisplayEmptyPlaceholder = displayEmptyPlaceholder || isLoading || error ;
118
+ const hasEmptyStreamData = Object . keys ( filteredMetaData ) . length === 0 ;
119
+
120
+ const searchIcon = < IconSearch size = { px ( '0.8rem' ) } /> ;
121
+ const shortcutKeyElement = (
122
+ < span style = { { marginBottom : '3px' } } >
123
+ < Kbd style = { { borderBottom : '1px solid #dee2e6' } } > Ctrl + K</ Kbd >
124
+ </ span >
125
+ ) ;
90
126
91
127
return (
92
128
< >
@@ -100,8 +136,21 @@ const Home: FC = () => {
100
136
borderBottom : '1px solid var(--mantine-color-gray-3)' ,
101
137
} } >
102
138
< Text style = { { fontSize : '0.8rem' } } fw = { 500 } >
103
- All Streams ({ metaData && Object . keys ( metaData ) . length } )
139
+ All Streams ({ filteredMetaData && Object . keys ( filteredMetaData ) . length } )
104
140
</ Text >
141
+ < TextInput
142
+ style = { { width : '30%' } }
143
+ placeholder = "Search Stream"
144
+ leftSection = { searchIcon }
145
+ ref = { searchInputRef }
146
+ key = "search-stream"
147
+ value = { searchTerm }
148
+ rightSection = { shortcutKeyElement }
149
+ rightSectionWidth = { 80 }
150
+ onChange = { ( event ) => {
151
+ setSearchTerm ( event . target . value ) ;
152
+ } }
153
+ />
105
154
< Box >
106
155
{ hasCreateStreamAccess && (
107
156
< Button
@@ -120,9 +169,12 @@ const Home: FC = () => {
120
169
className = { container }
121
170
style = { {
122
171
display : 'flex' ,
123
- paddingTop : shouldDisplayEmptyPlaceholder ? '0rem' : '1rem' ,
124
- paddingBottom : shouldDisplayEmptyPlaceholder ? '0rem' : '3rem' ,
125
- height : shouldDisplayEmptyPlaceholder ? `calc(${ heights . screen } - ${ PRIMARY_HEADER_HEIGHT } px)` : 'auto' ,
172
+ paddingTop : shouldDisplayEmptyPlaceholder || hasEmptyStreamData ? '0rem' : '1rem' ,
173
+ paddingBottom : shouldDisplayEmptyPlaceholder || hasEmptyStreamData ? '0rem' : '3rem' ,
174
+ height :
175
+ shouldDisplayEmptyPlaceholder || hasEmptyStreamData
176
+ ? `calc(${ heights . screen } - ${ PRIMARY_HEADER_HEIGHT } px)`
177
+ : 'auto' ,
126
178
} } >
127
179
< CreateStreamModal />
128
180
{ isLoading ? (
@@ -131,14 +183,15 @@ const Home: FC = () => {
131
183
</ Center >
132
184
) : (
133
185
< >
134
- { displayEmptyPlaceholder || error ? (
186
+ { displayEmptyPlaceholder || error || hasEmptyStreamData ? (
135
187
< NoStreamsView
136
188
hasCreateStreamAccess = { hasCreateStreamAccess }
137
189
openCreateStreamModal = { openCreateStreamModal }
190
+ shouldHideFooter
138
191
/>
139
192
) : (
140
193
< Group style = { { margin : '0 1rem' , gap : '1rem' } } >
141
- { Object . entries ( metaData || { } ) . map ( ( [ stream , data ] ) => (
194
+ { Object . entries ( filteredMetaData || { } ) . map ( ( [ stream , data ] ) => (
142
195
< StreamInfo
143
196
key = { stream }
144
197
stream = { stream }
0 commit comments