Skip to content

Commit

Permalink
ui-feat - homepage with quick stats of all streams (#184)
Browse files Browse the repository at this point in the history
  • Loading branch information
balaji-jr authored Jan 23, 2024
1 parent 7375d1b commit 33031ec
Show file tree
Hide file tree
Showing 10 changed files with 382 additions and 78 deletions.
10 changes: 10 additions & 0 deletions src/@types/parseable/api/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,13 @@ export type LogSelectedTimeRange = {
state : "fixed"| "custom";
value : string;
};

export type UserRoles = {
roleName: {
privilege: string;
resource?: {
stream: string;
tag: string;
};
}[];
};
14 changes: 14 additions & 0 deletions src/components/Header/SubHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@ export const LogsHeader: FC = () => {
);
};

export const HomeHeader: FC = () => {
const { classes } = useLogQueryStyles();
const { container, innerContainer } = classes;
return (
<Box className={container}>
<Box>
<Box className={innerContainer}>
<HeaderBreadcrumbs crumbs={['My Streams']} />
</Box>
</Box>
</Box>
);
};

export const ConfigHeader: FC = () => {
const { classes } = useLogQueryStyles();
const { container, innerContainer } = classes;
Expand Down
71 changes: 31 additions & 40 deletions src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ import { useNavbarStyles } from './styles';
import { useLocation, useParams } from 'react-router-dom';
import { notifications } from '@mantine/notifications';
import { useNavigate } from 'react-router-dom';
import { DEFAULT_FIXED_DURATIONS, useHeaderContext } from '@/layouts/MainLayout/Context';
import { useHeaderContext } from '@/layouts/MainLayout/Context';
import useMountedState from '@/hooks/useMountedState';
import dayjs from 'dayjs';
import { useDisclosure } from '@mantine/hooks';
import { USERS_MANAGEMENT_ROUTE } from '@/constants/routes';
import InfoModal from './infoModal';
Expand All @@ -36,7 +35,9 @@ const baseURL = import.meta.env.VITE_PARSEABLE_URL ?? '/';
const isSecureConnection = window.location.protocol === 'https:';
const links = [
{ icon: IconTableShortcut, label: 'Explore', pathname: '/logs', requiredAccess: ['Query', 'GetSchema'] },
...(!isSecureConnection ? [{ icon: IconTimelineEvent, label: 'Live tail', pathname: '/live-tail', requiredAccess: ['GetLiveTail'] }] : []),
...(!isSecureConnection
? [{ icon: IconTimelineEvent, label: 'Live tail', pathname: '/live-tail', requiredAccess: ['GetLiveTail'] }]
: []),
{ icon: IconReportAnalytics, label: 'Stats', pathname: '/stats', requiredAccess: ['GetStats'] },
{ icon: IconSettings, label: 'Config', pathname: '/config', requiredAccess: ['PutAlert'] },
];
Expand All @@ -51,12 +52,13 @@ const Navbar: FC<NavbarProps> = (props) => {
const username = Cookies.get('username');

const {
state: { subNavbarTogle },
state: { subNavbarTogle, subAppContext },
methods: { streamChangeCleanup, setUserRoles, setSelectedStream },
} = useHeaderContext();

const [activeStream, setActiveStream] = useMountedState('');
const selectedStream = subAppContext.get().selectedStream;
const [searchValue, setSearchValue] = useMountedState('');
const [currentPage, setCurrentPage] = useMountedState('/logs');
const [currentPage, setCurrentPage] = useMountedState('/');
const [deleteStream, setDeleteStream] = useMountedState('');
const [userSepecficStreams, setUserSepecficStreams] = useMountedState<LogStreamData | null>(null);
const [userSepecficAccess, setUserSepecficAccess] = useMountedState<string[] | null>(null);
Expand Down Expand Up @@ -85,16 +87,16 @@ const Navbar: FC<NavbarProps> = (props) => {
window.location.href = `${baseURL}api/v1/o/logout?redirect=${window.location.origin}/login`;
};

const {
state: { subLogQuery, subLogSelectedTimeRange, subLogSearch, subRefreshInterval },
} = useHeaderContext();

useEffect(() => {
if (location.pathname.split('/')[2]) {
setCurrentPage(`/${location.pathname.split('/')[2]}`);
}
if (userSepecficStreams && userSepecficStreams.length === 0) {
setActiveStream('');
if (location.pathname === '/') {
setSelectedStream('');
setCurrentPage('/');
setUserSepecficAccess(getStreamsSepcificAccess(getUserRolesData?.data));
} else if (userSepecficStreams && userSepecficStreams.length === 0) {
setSelectedStream('');
setSearchValue('');
setDisableLink(true);
navigate('/');
Expand Down Expand Up @@ -126,33 +128,19 @@ const Navbar: FC<NavbarProps> = (props) => {
}, [userSepecficStreams]);

const handleChange = (value: string, page: string = currentPage) => {
handleChangeWithoutRiderection(value, page);
const targetPage = page === '/' ? '/logs' : page;
handleChangeWithoutRiderection(value, targetPage);
setUserSepecficAccess(getStreamsSepcificAccess(getUserRolesData?.data, value));
if (page !== '/users') {
navigate(`/${value}${page}`);
navigate(`/${value}${targetPage}`);
}
};

const handleChangeWithoutRiderection = (value: string, page: string = currentPage) => {
setActiveStream(value);
setSelectedStream(value);
setSearchValue(value);
setCurrentPage(page);
const now = dayjs();
setUserSepecficAccess(getStreamsSepcificAccess(getUserRolesData?.data, value));
subLogQuery.set((state) => {
state.streamName = value || '';
state.startTime = now.subtract(DEFAULT_FIXED_DURATIONS.milliseconds, 'milliseconds').toDate();
state.endTime = now.toDate();
state.access = getStreamsSepcificAccess(getUserRolesData?.data, value);
});
subLogSelectedTimeRange.set((state) => {
state.state = 'fixed';
state.value = DEFAULT_FIXED_DURATIONS.name;
});
subLogSearch.set((state) => {
state.search = '';
state.filters = {};
});
subRefreshInterval.set(null);
streamChangeCleanup(value);
setDisableLink(false);
};
const handleCloseDelete = () => {
Expand All @@ -167,6 +155,7 @@ const Navbar: FC<NavbarProps> = (props) => {

useEffect(() => {
if (getLogStreamListData?.data && getLogStreamListData?.data.length > 0 && getUserRolesData?.data) {
getUserRolesData?.data && setUserRoles(getUserRolesData?.data); // TODO: move user context main context
const userStreams = getUserSepcificStreams(getUserRolesData?.data, getLogStreamListData?.data as any);
setUserSepecficStreams(userStreams as any);
} else {
Expand Down Expand Up @@ -209,10 +198,10 @@ const Navbar: FC<NavbarProps> = (props) => {
placeholder="Pick one"
onChange={(value) => handleChange(value || '')}
nothingFound="No options"
value={activeStream}
value={selectedStream}
searchValue={searchValue}
onSearchChange={(value) => setSearchValue(value)}
onDropdownClose={() => setSearchValue(activeStream)}
onDropdownClose={() => setSearchValue(selectedStream)}
onDropdownOpen={() => setSearchValue('')}
data={userSepecficStreams?.map((stream: any) => ({ value: stream.name, label: stream.name })) ?? []}
searchable
Expand All @@ -231,8 +220,9 @@ const Navbar: FC<NavbarProps> = (props) => {
)}
{links.map((link) => {
if (
link.requiredAccess &&
!userSepecficAccess?.some((access: string) => link.requiredAccess.includes(access))
(link.requiredAccess &&
!userSepecficAccess?.some((access: string) => link.requiredAccess.includes(access))) ||
selectedStream === ''
) {
return null;
}
Expand All @@ -243,14 +233,15 @@ const Navbar: FC<NavbarProps> = (props) => {
sx={{ paddingLeft: 53 }}
disabled={disableLink}
onClick={() => {
handleChange(activeStream, link.pathname);
handleChange(selectedStream, link.pathname);
}}
key={link.label}
className={(currentPage === link.pathname && linkBtnActive) || linkBtn}
/>
);
})}
{!userSepecficAccess?.some((access: string) => ['DeleteStream'].includes(access)) ? null : (
{!userSepecficAccess?.some((access: string) => ['DeleteStream'].includes(access)) ||
selectedStream === '' ? null : (
<NavLink
label={'Delete'}
icon={<IconTrash size="1.3rem" stroke={1.2} />}
Expand Down Expand Up @@ -305,14 +296,14 @@ const Navbar: FC<NavbarProps> = (props) => {
onChange={(e) => {
setDeleteStream(e.target.value);
}}
placeholder={`Type the name of the stream to confirm. i.e. ${activeStream}`}
placeholder={`Type the name of the stream to confirm. i.e. ${selectedStream}`}
required
/>

<Group mt={10} position="right">
<Button
className={modalActionBtn}
disabled={deleteStream === activeStream ? false : true}
disabled={deleteStream === selectedStream ? false : true}
onClick={handleDelete}>
Delete
</Button>
Expand Down
6 changes: 4 additions & 2 deletions src/components/Navbar/rolesHandler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UserRoles } from '@/layouts/MainLayout/Context';

const adminAccess = [
'Ingest',
'Query',
Expand Down Expand Up @@ -43,11 +45,11 @@ const writerAccess = [
const readerAccess = ['Query', 'ListStream', 'GetSchema', 'GetStats', 'GetRetention', 'GetAlert', 'GetLiveTail'];
const ingesterAccess = ['Ingest'];

const getStreamsSepcificAccess = (rolesWithRoleName: object[], stream?: string) => {
const getStreamsSepcificAccess = (rolesWithRoleName: UserRoles, stream?: string) => {
let access: string[] = [];
let roles: any[] = [];
for (var prop in rolesWithRoleName) {
roles = [...roles, ...(rolesWithRoleName[prop] as any)];
roles = [...roles, ...rolesWithRoleName[prop]];
}
roles.forEach((role: any) => {
if (role.privilege === 'admin') {
Expand Down
50 changes: 50 additions & 0 deletions src/hooks/useGetStreamMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { LogStreamRetention, LogStreamStat } from '@/@types/parseable/api/stream';
import { getLogStreamRetention, getLogStreamStats } from '@/api/logStream';
import { useCallback, useState } from 'react';

type MetaData = {
[key: string]: {
stats: LogStreamStat | {};
retention: LogStreamRetention | [];
};
};

// until dedicated endpoint been provided - fetch one by one
export const useGetStreamMetadata = () => {
const [isLoading, setLoading] = useState<Boolean>(false);
const [error, setError] = useState<Boolean>(false);
const [metaData, setMetadata] = useState<MetaData | null>(null);

const getStreamMetadata = useCallback(async (streams: string[]) => {
setLoading(true);
try {
// stats
const allStatsReqs = streams.map((stream) => getLogStreamStats(stream));
const allStatsRes = await Promise.all(allStatsReqs);

// retention
const allretentionReqs = streams.map((stream) => getLogStreamRetention(stream));
const allretentionRes = await Promise.all(allretentionReqs);

const metadata = streams.reduce((acc, stream, index) => {
return {
...acc,
[stream]: { stats: allStatsRes[index]?.data || {}, retention: allretentionRes[index]?.data || [] },
};
}, {});
setMetadata(metadata);
} catch {
setError(true);
setMetadata(null);
} finally {
setLoading(false);
}
}, []);

return {
isLoading,
error,
getStreamMetadata,
metaData,
};
};
49 changes: 44 additions & 5 deletions src/layouts/MainLayout/Context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AboutData } from '@/@types/parseable/api/about';
import { SortOrder, type LogsQuery, type LogsSearch, type LogSelectedTimeRange } from '@/@types/parseable/api/query';
import { LogStreamData } from '@/@types/parseable/api/stream';
import { getStreamsSepcificAccess } from '@/components/Navbar/rolesHandler';
import { FIXED_DURATIONS } from '@/constants/timeConstants';
import useSubscribeState, { SubData } from '@/hooks/useSubscribeState';
import dayjs from 'dayjs';
Expand Down Expand Up @@ -35,7 +36,7 @@ interface HeaderContextState {
}

export type UserRoles = {
roleName: {
[roleName: string]: {
privilege: string;
resource?: {
stream: string;
Expand All @@ -47,7 +48,7 @@ export type UserRoles = {
export type PageOption = '/' | '/explore' | '/sql' | '/management' | '/team';

export type AppContext = {
selectedStream: string | null;
selectedStream: string;
activePage: PageOption | null;
action: string[] | null;
userSpecificStreams: string[] | null;
Expand All @@ -57,6 +58,9 @@ export type AppContext = {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface HeaderContextMethods {
resetTimeInterval: () => void;
streamChangeCleanup: (streamName: string) => void;
setUserRoles: (userRoles: UserRoles) => void;
setSelectedStream: (stream: string) => void;
}

interface HeaderContextValue {
Expand All @@ -70,7 +74,7 @@ interface HeaderProviderProps {

const MainLayoutPageProvider: FC<HeaderProviderProps> = ({ children }) => {
const subAppContext = useSubscribeState<AppContext>({
selectedStream: null,
selectedStream: '',
activePage: null,
action: null,
userSpecificStreams: null,
Expand Down Expand Up @@ -118,7 +122,7 @@ const MainLayoutPageProvider: FC<HeaderProviderProps> = ({ children }) => {
};

const resetTimeInterval = useCallback(() => {
if (subLogSelectedTimeRange.get().state==='fixed') {
if (subLogSelectedTimeRange.get().state === 'fixed') {
const now = dayjs();
const timeDiff = subLogQuery.get().endTime.getTime() - subLogQuery.get().startTime.getTime();
subLogQuery.set((state) => {
Expand All @@ -127,7 +131,42 @@ const MainLayoutPageProvider: FC<HeaderProviderProps> = ({ children }) => {
});
}
}, []);
const methods: HeaderContextMethods = {resetTimeInterval};

const streamChangeCleanup = useCallback((stream: string) => {
const now = dayjs();
subLogQuery.set((state) => {
state.streamName = stream;
state.startTime = now.subtract(DEFAULT_FIXED_DURATIONS.milliseconds, 'milliseconds').toDate();
state.endTime = now.toDate();
state.access = getStreamsSepcificAccess(subAppContext.get().userRoles || {}, stream);
});
subLogSelectedTimeRange.set((state) => {
state.state = 'fixed';
state.value = DEFAULT_FIXED_DURATIONS.name;
});
subLogSearch.set((state) => {
state.search = '';
state.filters = {};
});
subRefreshInterval.set(null);
subAppContext.set((state) => {
state.selectedStream = stream;
});
}, []);

const setUserRoles = useCallback((userRoles: UserRoles) => {
subAppContext.set((state) => {
state.userRoles = userRoles;
});
}, []);

const setSelectedStream = useCallback((stream: string) => {
subAppContext.set((state) => {
state.selectedStream = stream;
});
}, []);

const methods: HeaderContextMethods = { resetTimeInterval, streamChangeCleanup, setUserRoles, setSelectedStream };

return <Provider value={{ state, methods }}>{children}</Provider>;
};
Expand Down
Loading

0 comments on commit 33031ec

Please sign in to comment.