Skip to content
This repository was archived by the owner on May 13, 2025. It is now read-only.

Commit 292e1ce

Browse files
authored
feat: search streams on landing page (#358)
1 parent ad2d65f commit 292e1ce

File tree

2 files changed

+85
-32
lines changed

2 files changed

+85
-32
lines changed

src/hooks/useGetStreamMetadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { notifyError } from '@/utils/notification';
66
import _ from 'lodash';
77
import { useCallback, useState } from 'react';
88

9-
type MetaData = {
9+
export type MetaData = {
1010
[key: string]: {
1111
stats: LogStreamStat | {};
1212
retention: LogStreamRetention | [];

src/pages/Home/index.tsx

Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
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';
520
import { useNavigate } from 'react-router-dom';
6-
import { useDocumentTitle } from '@mantine/hooks';
21+
import { useDocumentTitle, useHotkeys } from '@mantine/hooks';
722
import { useGetStreamMetadata } from '@/hooks/useGetStreamMetadata';
823
import { calcCompressionRate, formatBytes, sanitizeEventsCount } from '@/utils/formatBytes';
924
import { LogStreamRetention, LogStreamStat } from '@/@types/parseable/api/stream';
@@ -20,41 +35,42 @@ const { changeStream, toggleCreateStreamModal } = appStoreReducers;
2035

2136
type NoStreamsViewProps = {
2237
hasCreateStreamAccess: boolean;
38+
shouldHideFooter?: boolean;
2339
openCreateStreamModal: () => void;
2440
};
2541

2642
const NoStreamsView: FC<NoStreamsViewProps> = ({
2743
hasCreateStreamAccess,
44+
shouldHideFooter = false,
2845
openCreateStreamModal,
29-
}: {
30-
hasCreateStreamAccess: boolean;
31-
openCreateStreamModal: () => void;
32-
}) => {
46+
}: NoStreamsViewProps) => {
3347
const classes = homeStyles;
3448
const { messageStyle, btnStyle, noDataViewContainer, createStreamButton } = classes;
3549
return (
3650
<Center className={noDataViewContainer}>
3751
<EmptySimple height={70} width={100} />
3852
<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">
4955
<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
5562
</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+
)}
5874
</Center>
5975
);
6076
};
@@ -68,15 +84,27 @@ const Home: FC = () => {
6884
const [userSpecificStreams, setAppStore] = useAppStore((store) => store.userSpecificStreams);
6985
const [userRoles] = useAppStore((store) => store.userRoles);
7086
const [userAccessMap] = useAppStore((store) => store.userAccessMap);
87+
const searchInputRef = useRef<HTMLInputElement>(null);
88+
const [searchTerm, setSearchTerm] = useState('');
7189

7290
useEffect(() => {
7391
if (!Array.isArray(userSpecificStreams) || userSpecificStreams.length === 0) return;
7492
getStreamMetadata(userSpecificStreams.map((stream) => stream.name));
7593
}, [userSpecificStreams]);
7694

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+
77104
const navigateToStream = useCallback((stream: string) => {
78105
setAppStore((store) => changeStream(store, stream));
79106
navigate(`/${stream}/explore`);
107+
setSearchTerm('');
80108
}, []);
81109

82110
const displayEmptyPlaceholder = Array.isArray(userSpecificStreams) && userSpecificStreams.length === 0;
@@ -87,6 +115,14 @@ const Home: FC = () => {
87115
const hasCreateStreamAccess = useMemo(() => userAccessMap?.hasCreateStreamAccess, [userAccessMap]);
88116

89117
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+
);
90126

91127
return (
92128
<>
@@ -100,8 +136,21 @@ const Home: FC = () => {
100136
borderBottom: '1px solid var(--mantine-color-gray-3)',
101137
}}>
102138
<Text style={{ fontSize: '0.8rem' }} fw={500}>
103-
All Streams ({metaData && Object.keys(metaData).length})
139+
All Streams ({filteredMetaData && Object.keys(filteredMetaData).length})
104140
</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+
/>
105154
<Box>
106155
{hasCreateStreamAccess && (
107156
<Button
@@ -120,9 +169,12 @@ const Home: FC = () => {
120169
className={container}
121170
style={{
122171
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',
126178
}}>
127179
<CreateStreamModal />
128180
{isLoading ? (
@@ -131,14 +183,15 @@ const Home: FC = () => {
131183
</Center>
132184
) : (
133185
<>
134-
{displayEmptyPlaceholder || error ? (
186+
{displayEmptyPlaceholder || error || hasEmptyStreamData ? (
135187
<NoStreamsView
136188
hasCreateStreamAccess={hasCreateStreamAccess}
137189
openCreateStreamModal={openCreateStreamModal}
190+
shouldHideFooter
138191
/>
139192
) : (
140193
<Group style={{ margin: '0 1rem', gap: '1rem' }}>
141-
{Object.entries(metaData || {}).map(([stream, data]) => (
194+
{Object.entries(filteredMetaData || {}).map(([stream, data]) => (
142195
<StreamInfo
143196
key={stream}
144197
stream={stream}

0 commit comments

Comments
 (0)