Skip to content

Commit 2469b2c

Browse files
committed
ui-feat - homepage with quick stats of all streams
1 parent 7375d1b commit 2469b2c

File tree

10 files changed

+363
-78
lines changed

10 files changed

+363
-78
lines changed

src/@types/parseable/api/query.ts

+10
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,13 @@ export type LogSelectedTimeRange = {
3838
state : "fixed"| "custom";
3939
value : string;
4040
};
41+
42+
export type UserRoles = {
43+
roleName: {
44+
privilege: string;
45+
resource?: {
46+
stream: string;
47+
tag: string;
48+
};
49+
}[];
50+
};

src/components/Header/SubHeader.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,20 @@ export const LogsHeader: FC = () => {
134134
);
135135
};
136136

137+
export const HomeHeader: FC = () => {
138+
const { classes } = useLogQueryStyles();
139+
const { container, innerContainer } = classes;
140+
return (
141+
<Box className={container}>
142+
<Box>
143+
<Box className={innerContainer}>
144+
<HeaderBreadcrumbs crumbs={['My Streams']} />
145+
</Box>
146+
</Box>
147+
</Box>
148+
);
149+
};
150+
137151
export const ConfigHeader: FC = () => {
138152
const { classes } = useLogQueryStyles();
139153
const { container, innerContainer } = classes;

src/components/Navbar/index.tsx

+32-40
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ import { useNavbarStyles } from './styles';
1919
import { useLocation, useParams } from 'react-router-dom';
2020
import { notifications } from '@mantine/notifications';
2121
import { useNavigate } from 'react-router-dom';
22-
import { DEFAULT_FIXED_DURATIONS, useHeaderContext } from '@/layouts/MainLayout/Context';
22+
import { useHeaderContext } from '@/layouts/MainLayout/Context';
2323
import useMountedState from '@/hooks/useMountedState';
24-
import dayjs from 'dayjs';
2524
import { useDisclosure } from '@mantine/hooks';
2625
import { USERS_MANAGEMENT_ROUTE } from '@/constants/routes';
2726
import InfoModal from './infoModal';
@@ -36,7 +35,9 @@ const baseURL = import.meta.env.VITE_PARSEABLE_URL ?? '/';
3635
const isSecureConnection = window.location.protocol === 'https:';
3736
const links = [
3837
{ icon: IconTableShortcut, label: 'Explore', pathname: '/logs', requiredAccess: ['Query', 'GetSchema'] },
39-
...(!isSecureConnection ? [{ icon: IconTimelineEvent, label: 'Live tail', pathname: '/live-tail', requiredAccess: ['GetLiveTail'] }] : []),
38+
...(!isSecureConnection
39+
? [{ icon: IconTimelineEvent, label: 'Live tail', pathname: '/live-tail', requiredAccess: ['GetLiveTail'] }]
40+
: []),
4041
{ icon: IconReportAnalytics, label: 'Stats', pathname: '/stats', requiredAccess: ['GetStats'] },
4142
{ icon: IconSettings, label: 'Config', pathname: '/config', requiredAccess: ['PutAlert'] },
4243
];
@@ -51,12 +52,14 @@ const Navbar: FC<NavbarProps> = (props) => {
5152
const username = Cookies.get('username');
5253

5354
const {
54-
state: { subNavbarTogle },
55+
state: { subNavbarTogle, subAppContext },
56+
methods: { streamChangeCleanup, setUserRoles, setSelectedStream },
5557
} = useHeaderContext();
5658

57-
const [activeStream, setActiveStream] = useMountedState('');
59+
const selectedStream = subAppContext.get().selectedStream;
60+
// const [selectedStream, setSelectedStream] = useMountedState('');
5861
const [searchValue, setSearchValue] = useMountedState('');
59-
const [currentPage, setCurrentPage] = useMountedState('/logs');
62+
const [currentPage, setCurrentPage] = useMountedState('/');
6063
const [deleteStream, setDeleteStream] = useMountedState('');
6164
const [userSepecficStreams, setUserSepecficStreams] = useMountedState<LogStreamData | null>(null);
6265
const [userSepecficAccess, setUserSepecficAccess] = useMountedState<string[] | null>(null);
@@ -85,16 +88,16 @@ const Navbar: FC<NavbarProps> = (props) => {
8588
window.location.href = `${baseURL}api/v1/o/logout?redirect=${window.location.origin}/login`;
8689
};
8790

88-
const {
89-
state: { subLogQuery, subLogSelectedTimeRange, subLogSearch, subRefreshInterval },
90-
} = useHeaderContext();
91-
9291
useEffect(() => {
9392
if (location.pathname.split('/')[2]) {
9493
setCurrentPage(`/${location.pathname.split('/')[2]}`);
9594
}
96-
if (userSepecficStreams && userSepecficStreams.length === 0) {
97-
setActiveStream('');
95+
if (location.pathname === '/') {
96+
setSelectedStream('');
97+
setCurrentPage('/');
98+
setUserSepecficAccess(getStreamsSepcificAccess(getUserRolesData?.data));
99+
} else if (userSepecficStreams && userSepecficStreams.length === 0) {
100+
setSelectedStream('');
98101
setSearchValue('');
99102
setDisableLink(true);
100103
navigate('/');
@@ -126,33 +129,19 @@ const Navbar: FC<NavbarProps> = (props) => {
126129
}, [userSepecficStreams]);
127130

128131
const handleChange = (value: string, page: string = currentPage) => {
129-
handleChangeWithoutRiderection(value, page);
132+
const targetPage = page === '/' ? '/logs' : page;
133+
handleChangeWithoutRiderection(value, targetPage);
134+
setUserSepecficAccess(getStreamsSepcificAccess(getUserRolesData?.data, value));
130135
if (page !== '/users') {
131-
navigate(`/${value}${page}`);
136+
navigate(`/${value}${targetPage}`);
132137
}
133138
};
134139

135140
const handleChangeWithoutRiderection = (value: string, page: string = currentPage) => {
136-
setActiveStream(value);
141+
setSelectedStream(value);
137142
setSearchValue(value);
138143
setCurrentPage(page);
139-
const now = dayjs();
140-
setUserSepecficAccess(getStreamsSepcificAccess(getUserRolesData?.data, value));
141-
subLogQuery.set((state) => {
142-
state.streamName = value || '';
143-
state.startTime = now.subtract(DEFAULT_FIXED_DURATIONS.milliseconds, 'milliseconds').toDate();
144-
state.endTime = now.toDate();
145-
state.access = getStreamsSepcificAccess(getUserRolesData?.data, value);
146-
});
147-
subLogSelectedTimeRange.set((state) => {
148-
state.state = 'fixed';
149-
state.value = DEFAULT_FIXED_DURATIONS.name;
150-
});
151-
subLogSearch.set((state) => {
152-
state.search = '';
153-
state.filters = {};
154-
});
155-
subRefreshInterval.set(null);
144+
streamChangeCleanup(value);
156145
setDisableLink(false);
157146
};
158147
const handleCloseDelete = () => {
@@ -167,6 +156,7 @@ const Navbar: FC<NavbarProps> = (props) => {
167156

168157
useEffect(() => {
169158
if (getLogStreamListData?.data && getLogStreamListData?.data.length > 0 && getUserRolesData?.data) {
159+
getUserRolesData?.data && setUserRoles(getUserRolesData?.data); // TODO: move user context main context
170160
const userStreams = getUserSepcificStreams(getUserRolesData?.data, getLogStreamListData?.data as any);
171161
setUserSepecficStreams(userStreams as any);
172162
} else {
@@ -209,10 +199,10 @@ const Navbar: FC<NavbarProps> = (props) => {
209199
placeholder="Pick one"
210200
onChange={(value) => handleChange(value || '')}
211201
nothingFound="No options"
212-
value={activeStream}
202+
value={selectedStream}
213203
searchValue={searchValue}
214204
onSearchChange={(value) => setSearchValue(value)}
215-
onDropdownClose={() => setSearchValue(activeStream)}
205+
onDropdownClose={() => setSearchValue(selectedStream)}
216206
onDropdownOpen={() => setSearchValue('')}
217207
data={userSepecficStreams?.map((stream: any) => ({ value: stream.name, label: stream.name })) ?? []}
218208
searchable
@@ -231,8 +221,9 @@ const Navbar: FC<NavbarProps> = (props) => {
231221
)}
232222
{links.map((link) => {
233223
if (
234-
link.requiredAccess &&
235-
!userSepecficAccess?.some((access: string) => link.requiredAccess.includes(access))
224+
(link.requiredAccess &&
225+
!userSepecficAccess?.some((access: string) => link.requiredAccess.includes(access))) ||
226+
selectedStream === ''
236227
) {
237228
return null;
238229
}
@@ -243,14 +234,15 @@ const Navbar: FC<NavbarProps> = (props) => {
243234
sx={{ paddingLeft: 53 }}
244235
disabled={disableLink}
245236
onClick={() => {
246-
handleChange(activeStream, link.pathname);
237+
handleChange(selectedStream, link.pathname);
247238
}}
248239
key={link.label}
249240
className={(currentPage === link.pathname && linkBtnActive) || linkBtn}
250241
/>
251242
);
252243
})}
253-
{!userSepecficAccess?.some((access: string) => ['DeleteStream'].includes(access)) ? null : (
244+
{!userSepecficAccess?.some((access: string) => ['DeleteStream'].includes(access)) ||
245+
selectedStream === '' ? null : (
254246
<NavLink
255247
label={'Delete'}
256248
icon={<IconTrash size="1.3rem" stroke={1.2} />}
@@ -305,14 +297,14 @@ const Navbar: FC<NavbarProps> = (props) => {
305297
onChange={(e) => {
306298
setDeleteStream(e.target.value);
307299
}}
308-
placeholder={`Type the name of the stream to confirm. i.e. ${activeStream}`}
300+
placeholder={`Type the name of the stream to confirm. i.e. ${selectedStream}`}
309301
required
310302
/>
311303

312304
<Group mt={10} position="right">
313305
<Button
314306
className={modalActionBtn}
315-
disabled={deleteStream === activeStream ? false : true}
307+
disabled={deleteStream === selectedStream ? false : true}
316308
onClick={handleDelete}>
317309
Delete
318310
</Button>

src/components/Navbar/rolesHandler.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { UserRoles } from '@/layouts/MainLayout/Context';
2+
13
const adminAccess = [
24
'Ingest',
35
'Query',
@@ -43,11 +45,11 @@ const writerAccess = [
4345
const readerAccess = ['Query', 'ListStream', 'GetSchema', 'GetStats', 'GetRetention', 'GetAlert', 'GetLiveTail'];
4446
const ingesterAccess = ['Ingest'];
4547

46-
const getStreamsSepcificAccess = (rolesWithRoleName: object[], stream?: string) => {
48+
const getStreamsSepcificAccess = (rolesWithRoleName: UserRoles, stream?: string) => {
4749
let access: string[] = [];
4850
let roles: any[] = [];
4951
for (var prop in rolesWithRoleName) {
50-
roles = [...roles, ...(rolesWithRoleName[prop] as any)];
52+
roles = [...roles, ...rolesWithRoleName[prop]];
5153
}
5254
roles.forEach((role: any) => {
5355
if (role.privilege === 'admin') {

src/hooks/useGetStreamMetadata.ts

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { LogStreamRetention, LogStreamStat } from '@/@types/parseable/api/stream';
2+
import { getLogStreamRetention, getLogStreamStats } from '@/api/logStream';
3+
import { useCallback, useState } from 'react';
4+
5+
type MetaData = {
6+
[key: string]: {
7+
stats: LogStreamStat | {};
8+
retention: LogStreamRetention | [];
9+
};
10+
};
11+
12+
// until dedicated endpoint been provided - fetch one by one
13+
export const useGetStreamMetadata = () => {
14+
const [isLoading, setLoading] = useState<Boolean>(false);
15+
const [error, setError] = useState<Boolean>(false);
16+
const [metaData, setMetadata] = useState<MetaData | null>(null);
17+
18+
const getStreamMetadata = useCallback(async (streams: string[]) => {
19+
setLoading(true);
20+
try {
21+
// stats
22+
const allStatsReqs = streams.map((stream) => getLogStreamStats(stream));
23+
const allStatsRes = await Promise.all(allStatsReqs);
24+
25+
// retention
26+
const allretentionReqs = streams.map((stream) => getLogStreamRetention(stream));
27+
const allretentionRes = await Promise.all(allretentionReqs);
28+
29+
const metadata = streams.reduce((acc, stream, index) => {
30+
return {
31+
...acc,
32+
[stream]: { stats: allStatsRes[index]?.data || {}, retention: allretentionRes[index]?.data || [] },
33+
};
34+
}, {});
35+
setMetadata(metadata);
36+
} catch {
37+
setError(true);
38+
setMetadata(null);
39+
} finally {
40+
setLoading(false);
41+
}
42+
}, []);
43+
44+
return {
45+
isLoading,
46+
error,
47+
getStreamMetadata,
48+
metaData,
49+
};
50+
};

src/layouts/MainLayout/Context.tsx

+41-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AboutData } from '@/@types/parseable/api/about';
22
import { SortOrder, type LogsQuery, type LogsSearch, type LogSelectedTimeRange } from '@/@types/parseable/api/query';
33
import { LogStreamData } from '@/@types/parseable/api/stream';
4+
import { getStreamsSepcificAccess } from '@/components/Navbar/rolesHandler';
45
import { FIXED_DURATIONS } from '@/constants/timeConstants';
56
import useSubscribeState, { SubData } from '@/hooks/useSubscribeState';
67
import dayjs from 'dayjs';
@@ -35,7 +36,7 @@ interface HeaderContextState {
3536
}
3637

3738
export type UserRoles = {
38-
roleName: {
39+
[roleName: string]: {
3940
privilege: string;
4041
resource?: {
4142
stream: string;
@@ -47,7 +48,7 @@ export type UserRoles = {
4748
export type PageOption = '/' | '/explore' | '/sql' | '/management' | '/team';
4849

4950
export type AppContext = {
50-
selectedStream: string | null;
51+
selectedStream: string;
5152
activePage: PageOption | null;
5253
action: string[] | null;
5354
userSpecificStreams: string[] | null;
@@ -57,6 +58,9 @@ export type AppContext = {
5758
// eslint-disable-next-line @typescript-eslint/no-empty-interface
5859
interface HeaderContextMethods {
5960
resetTimeInterval: () => void;
61+
streamChangeCleanup: (streamName: string) => void;
62+
setUserRoles: (userRoles: UserRoles) => void;
63+
setSelectedStream: (stream: string) => void;
6064
}
6165

6266
interface HeaderContextValue {
@@ -70,7 +74,7 @@ interface HeaderProviderProps {
7074

7175
const MainLayoutPageProvider: FC<HeaderProviderProps> = ({ children }) => {
7276
const subAppContext = useSubscribeState<AppContext>({
73-
selectedStream: null,
77+
selectedStream: '',
7478
activePage: null,
7579
action: null,
7680
userSpecificStreams: null,
@@ -118,7 +122,7 @@ const MainLayoutPageProvider: FC<HeaderProviderProps> = ({ children }) => {
118122
};
119123

120124
const resetTimeInterval = useCallback(() => {
121-
if (subLogSelectedTimeRange.get().state==='fixed') {
125+
if (subLogSelectedTimeRange.get().state === 'fixed') {
122126
const now = dayjs();
123127
const timeDiff = subLogQuery.get().endTime.getTime() - subLogQuery.get().startTime.getTime();
124128
subLogQuery.set((state) => {
@@ -127,7 +131,39 @@ const MainLayoutPageProvider: FC<HeaderProviderProps> = ({ children }) => {
127131
});
128132
}
129133
}, []);
130-
const methods: HeaderContextMethods = {resetTimeInterval};
134+
135+
const streamChangeCleanup = useCallback((stream: string) => {
136+
const now = dayjs();
137+
subLogQuery.set((state) => {
138+
state.streamName = stream;
139+
state.startTime = now.subtract(DEFAULT_FIXED_DURATIONS.milliseconds, 'milliseconds').toDate();
140+
state.endTime = now.toDate();
141+
state.access = getStreamsSepcificAccess(subAppContext.get().userRoles || {}, stream);
142+
});
143+
subLogSelectedTimeRange.set((state) => {
144+
state.state = 'fixed';
145+
state.value = DEFAULT_FIXED_DURATIONS.name;
146+
});
147+
subLogSearch.set((state) => {
148+
state.search = '';
149+
state.filters = {};
150+
});
151+
subRefreshInterval.set(null);
152+
}, []);
153+
154+
const setUserRoles = useCallback((userRoles: UserRoles) => {
155+
subAppContext.set((state) => {
156+
state.userRoles = userRoles;
157+
});
158+
}, []);
159+
160+
const setSelectedStream = useCallback((stream: string) => {
161+
subAppContext.set((state) => {
162+
state.selectedStream = stream;
163+
});
164+
}, []);
165+
166+
const methods: HeaderContextMethods = { resetTimeInterval, streamChangeCleanup, setUserRoles, setSelectedStream };
131167

132168
return <Provider value={{ state, methods }}>{children}</Provider>;
133169
};

0 commit comments

Comments
 (0)