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

Commit dc6eaf2

Browse files
Add user management page (#113)
1 parent 54b734c commit dc6eaf2

22 files changed

+1418
-33
lines changed

src/api/constants.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
const API_V1 = 'api/v1';
22

3-
export const HEALTH_LIVENESS_URL = `${API_V1}/liveness`;
3+
4+
// Streams Management
45
export const LOG_STREAM_LIST_URL = `${API_V1}/logstream`;
56
export const LOG_STREAMS_SCHEMA_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}/schema`;
67
export const LOG_QUERY_URL = `${API_V1}/query`;
78
export const LOG_STREAMS_ALERTS_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}/alert`;
89
export const LOG_STREAMS_RETRNTION_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}/retention`;
910
export const LOG_STREAMS_STATS_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}/stats`;
1011
export const DELETE_STREAMS_URL = (streamName: string) => `${LOG_STREAM_LIST_URL}/${streamName}`;
12+
13+
// About Parsable Instance
1114
export const ABOUT_URL = `${API_V1}/about`;
15+
16+
// Users Management
17+
export const USERS_LIST_URL = `${API_V1}/user`;
18+
export const USER_URL = (username: string) => `${USERS_LIST_URL}/${username}`;
19+
export const USER_ROLES_URL = (username: string) => `${USER_URL(username)}/role`;

src/api/users.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Axios } from "./axios";
2+
import { USERS_LIST_URL, USER_ROLES_URL, USER_URL } from "./constants";
3+
4+
export const getUsers = () => {
5+
return Axios().get(USERS_LIST_URL);
6+
}
7+
8+
export const putUser = (username: string, roles?: object[]) => {
9+
return Axios().put(USER_URL(username), roles );
10+
}
11+
12+
export const deleteUser = (username: string) => {
13+
return Axios().delete(USER_URL(username));
14+
}
15+
16+
export const putUserRoles = (username: string, roles: object[]) => {
17+
return Axios().put(USER_ROLES_URL(username), roles);
18+
}
19+
20+
export const getUserRoles = (username: string) => {
21+
return Axios().get(USER_ROLES_URL(username));
22+
}

src/components/Header/CreateUser.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Button, Tooltip, px } from '@mantine/core';
2+
import { IconUserPlus } from '@tabler/icons-react';
3+
import type { FC } from 'react';
4+
import { useLogQueryStyles } from './styles';
5+
import { useHeaderContext } from '@/layouts/MainLayout/Context';
6+
7+
const CreateUser: FC = () => {
8+
const { classes } = useLogQueryStyles();
9+
const { refreshNowBtn } = classes;
10+
const {
11+
state: { subCreateUserModalTogle }
12+
} = useHeaderContext();
13+
14+
15+
const handleOpen = () => {
16+
subCreateUserModalTogle.set(true);
17+
};
18+
19+
20+
return (
21+
<Tooltip
22+
label={'Create New User'}
23+
sx={{ color: 'white', backgroundColor: 'black' }}
24+
withArrow
25+
onClick={handleOpen}
26+
position="left">
27+
<Button variant="default" className={refreshNowBtn} aria-label="create user">
28+
<IconUserPlus size={px('1.2rem')} stroke={1.5} />
29+
</Button>
30+
</Tooltip>
31+
);
32+
};
33+
34+
export default CreateUser;

src/components/Header/ReloadUser.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Button, Tooltip, px } from '@mantine/core';
2+
import { IconReload } from '@tabler/icons-react';
3+
import type { FC } from 'react';
4+
import { useLogQueryStyles } from './styles';
5+
import { useNavigate } from 'react-router-dom';
6+
7+
const ReloadUser: FC = () => {
8+
const navigate = useNavigate();
9+
const { classes } = useLogQueryStyles();
10+
const { refreshNowBtn } = classes;
11+
12+
return (
13+
<Tooltip label="Refresh" sx={{ color: 'white', backgroundColor: 'black' }} withArrow position="left">
14+
<Button className={refreshNowBtn} onClick={()=>navigate(0)}>
15+
<IconReload size={px('1.2rem')} stroke={1.5} />
16+
</Button>
17+
</Tooltip>
18+
);
19+
};
20+
21+
export default ReloadUser;

src/components/Header/SubHeader.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import RefreshNow from './RefreshNow';
66
import Search from './Search';
77
import TimeRange from './TimeRange';
88
import { useLogQueryStyles } from './styles';
9+
import ReloadUser from './ReloadUser';
10+
import DocsUser from './UserDocs';
11+
import CreateUser from './CreateUser';
912

1013
export const StatsHeader: FC = () => {
1114
const { classes } = useLogQueryStyles();
@@ -88,3 +91,25 @@ export const ConfigHeader: FC = () => {
8891
</Box>
8992
);
9093
};
94+
95+
export const UsersManagementHeader: FC = () => {
96+
const { classes } = useLogQueryStyles();
97+
const { container, innerContainer } = classes;
98+
99+
return (
100+
<Box className={container}>
101+
<Box>
102+
<Box className={innerContainer}>
103+
<HeaderBreadcrumbs crumbs={['User Management']} />
104+
</Box>
105+
</Box>
106+
<Box>
107+
<Box className={innerContainer}>
108+
<ReloadUser />
109+
<DocsUser />
110+
<CreateUser />
111+
</Box>
112+
</Box>
113+
</Box>
114+
);
115+
};

src/components/Header/UserDocs.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Button, Tooltip, px } from '@mantine/core';
2+
import { IconBook2 } from '@tabler/icons-react';
3+
import type { FC } from 'react';
4+
import { useLogQueryStyles } from './styles';
5+
6+
const DocsUser: FC = () => {
7+
const { classes } = useLogQueryStyles();
8+
const { refreshNowBtn } = classes;
9+
10+
return (
11+
<Tooltip label={'Docs'} sx={{ color: 'white', backgroundColor: 'black' }} withArrow position="left">
12+
<Button
13+
variant="default"
14+
className={refreshNowBtn}
15+
onClick={() => {
16+
window.open('https://www.parseable.io/docs/rbac', '_blank');
17+
}}>
18+
<IconBook2 size={px('1.2rem')} stroke={1.5} />
19+
</Button>
20+
</Tooltip>
21+
);
22+
};
23+
24+
export default DocsUser;

src/components/Header/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type { HeaderProps as MantineHeaderProps } from '@mantine/core';
22
import { FC } from 'react';
33
import { Route, Routes } from 'react-router-dom';
44
import HeaderLayout from './Layout';
5-
import { ConfigHeader, LogsHeader, QueryHeader, StatsHeader } from './SubHeader';
6-
import { CONFIG_ROUTE, LOGS_ROUTE, QUERY_ROUTE, STATS_ROUTE } from '@/constants/routes';
5+
import { ConfigHeader, LogsHeader, QueryHeader, StatsHeader, UsersManagementHeader } from './SubHeader';
6+
import { CONFIG_ROUTE, LOGS_ROUTE, QUERY_ROUTE, STATS_ROUTE, USERS_MANAGEMENT_ROUTE } from '@/constants/routes';
77

88
type HeaderProps = Omit<MantineHeaderProps, 'children' | 'height' | 'className'>;
99

@@ -15,6 +15,7 @@ const Header: FC<HeaderProps> = (props) => {
1515
<Route path={QUERY_ROUTE} element={<QueryHeader />} />
1616
<Route path={STATS_ROUTE} element={<StatsHeader />} />
1717
<Route path={CONFIG_ROUTE} element={<ConfigHeader />} />
18+
<Route path={USERS_MANAGEMENT_ROUTE} element={<UsersManagementHeader />} />
1819
</Route>
1920
</Routes>
2021
);

src/components/Navbar/index.tsx

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
import type { NavbarProps as MantineNavbarProps } from '@mantine/core';
2-
import {
3-
Navbar as MantineNavbar,
4-
NavLink,
5-
Select,
6-
Box,
7-
Modal,
8-
Text,
9-
Button,
10-
TextInput,
11-
} from '@mantine/core';
2+
import { Navbar as MantineNavbar, NavLink, Select, Box, Modal, Text, Button, TextInput } from '@mantine/core';
123
import {
134
IconZoomCode,
145
IconReportAnalytics,
@@ -21,6 +12,7 @@ import {
2112
IconSettings,
2213
IconTrash,
2314
IconInfoCircle,
15+
IconUserCog,
2416
} from '@tabler/icons-react';
2517
import { FC, useEffect } from 'react';
2618
import { useNavbarStyles } from './styles';
@@ -32,9 +24,10 @@ import { DEFAULT_FIXED_DURATIONS, useHeaderContext } from '@/layouts/MainLayout/
3224
import useMountedState from '@/hooks/useMountedState';
3325
import dayjs from 'dayjs';
3426
import { useDisclosure, useLocalStorage } from '@mantine/hooks';
35-
import { LOGIN_ROUTE } from '@/constants/routes';
27+
import { LOGIN_ROUTE, USERS_MANAGEMENT_ROUTE } from '@/constants/routes';
3628
import { useDeleteLogStream } from '@/hooks/useDeleteLogStream';
3729
import InfoModal from './infoModal';
30+
import { useGetUserRole } from '@/hooks/useGetUserRoles';
3831

3932
const links = [
4033
{ icon: IconZoomCode, label: 'Query', pathname: '/query' },
@@ -49,12 +42,11 @@ const Navbar: FC<NavbarProps> = (props) => {
4942
const navigate = useNavigate();
5043
const { streamName } = useParams();
5144
const location = useLocation();
52-
45+
5346
const [username] = useLocalStorage({ key: 'username', getInitialValueInEffect: false });
5447
const [, , removeCredentials] = useLocalStorage({ key: 'credentials' });
5548
const [, , removeUsername] = useLocalStorage({ key: 'username' });
56-
57-
49+
5850
const {
5951
state: { subNavbarTogle },
6052
} = useHeaderContext();
@@ -63,6 +55,7 @@ const Navbar: FC<NavbarProps> = (props) => {
6355
const [searchValue, setSearchValue] = useMountedState('');
6456
const [currentPage, setCurrentPage] = useMountedState('/query');
6557
const [deleteStream, setDeleteStream] = useMountedState('');
58+
const [isAdmin, setIsAdmin] = useMountedState(false);
6659

6760
const [disableLink, setDisableLink] = useMountedState(false);
6861
const [isSubNavbarOpen, setIsSubNavbarOpen] = useMountedState(false);
@@ -103,8 +96,7 @@ const Navbar: FC<NavbarProps> = (props) => {
10396
setSearchValue('');
10497
setDisableLink(true);
10598
navigate(`/`);
106-
}
107-
else if (streamName) {
99+
} else if (streamName) {
108100
if (streamName === deleteStream && streams) {
109101
setDeleteStream('');
110102
handleChange(streams[0].name);
@@ -122,14 +114,24 @@ const Navbar: FC<NavbarProps> = (props) => {
122114
handleChange(streamName);
123115
}
124116
} else if (streams && Boolean(streams.length)) {
125-
handleChange(streams[0].name);
117+
if (location.pathname === USERS_MANAGEMENT_ROUTE) {
118+
handleChangeWithoutRiderection(streams[0].name, location.pathname);
119+
} else {
120+
handleChange(streams[0].name);
121+
}
126122
}
127123
}, [streams]);
128124

129125
const handleChange = (value: string) => {
126+
handleChangeWithoutRiderection(value);
127+
navigate(`/${value}${currentPage}`);
128+
};
129+
const handleChangeWithoutRiderection = (value: string, page: string = currentPage) => {
130130
setActiveStream(value);
131131
setSearchValue(value);
132+
setCurrentPage(page);
132133
const now = dayjs();
134+
133135
subLogQuery.set((state) => {
134136
state.streamName = value || '';
135137
state.startTime = now.subtract(DEFAULT_FIXED_DURATIONS.milliseconds, 'milliseconds').toDate();
@@ -138,16 +140,14 @@ const Navbar: FC<NavbarProps> = (props) => {
138140
subLogSelectedTimeRange.set((state) => {
139141
state.state = 'fixed';
140142
state.value = DEFAULT_FIXED_DURATIONS.name;
141-
});
143+
});
142144
subLogSearch.set((state) => {
143145
state.search = '';
144146
state.filters = {};
145147
});
146148
subRefreshInterval.set(null);
147149
setDisableLink(false);
148-
navigate(`/${value}${currentPage}`);
149150
};
150-
151151
const handleCloseDelete = () => {
152152
closeDelete();
153153
setDeleteStream('');
@@ -166,6 +166,22 @@ const Navbar: FC<NavbarProps> = (props) => {
166166
}
167167
}, [deleteData]);
168168

169+
//isAdmin
170+
const { data: adminData, getRoles, resetData } = useGetUserRole();
171+
useEffect(() => {
172+
if (username) {
173+
getRoles(username);
174+
}
175+
return () => {
176+
resetData();
177+
};
178+
}, [username]);
179+
180+
useEffect(() => {
181+
if (adminData) {
182+
setIsAdmin(true);
183+
}
184+
}, [adminData]);
169185

170186
const { classes } = useNavbarStyles();
171187
const {
@@ -177,10 +193,13 @@ const Navbar: FC<NavbarProps> = (props) => {
177193
lowerContainer,
178194
actionBtn,
179195
userBtn,
196+
userManagementBtn,
197+
userManagementBtnActive,
180198
} = classes;
181199
return (
182200
<MantineNavbar {...props} withBorder zIndex={1} hiddenBreakpoint={window.outerWidth + 20} hidden={isSubNavbarOpen}>
183201
<MantineNavbar.Section grow className={container}>
202+
184203
<NavLink label="Log Streams" icon={<IconBinaryTree2 size="1.5rem" stroke={1.3} />} className={streamsBtn} />
185204
<Select
186205
placeholder="Pick one"
@@ -208,8 +227,7 @@ const Navbar: FC<NavbarProps> = (props) => {
208227
setCurrentPage(link.pathname);
209228
}}
210229
key={link.label}
211-
212-
className={(currentPage === link.pathname && linkBtnActive ) || linkBtn}
230+
className={(currentPage === link.pathname && linkBtnActive) || linkBtn}
213231
/>
214232
);
215233
})}
@@ -231,6 +249,18 @@ const Navbar: FC<NavbarProps> = (props) => {
231249
sx={{ paddingLeft: 0 }}
232250
/>
233251
)}
252+
{isAdmin && (
253+
<NavLink
254+
pt={24}
255+
className={(currentPage === USERS_MANAGEMENT_ROUTE && userManagementBtnActive) || userManagementBtn}
256+
label="Users"
257+
icon={<IconUserCog size="1.5rem" stroke={1.3} />}
258+
onClick={() => {
259+
navigate(`/users`);
260+
setCurrentPage(USERS_MANAGEMENT_ROUTE);
261+
}}
262+
/>
263+
)}
234264
</MantineNavbar.Section>
235265
<MantineNavbar.Section className={lowerContainer}>
236266
<NavLink label={username} icon={<IconUser size="1.3rem" stroke={1.3} />} className={userBtn} component="a" />
@@ -256,15 +286,15 @@ const Navbar: FC<NavbarProps> = (props) => {
256286
onChange={(e) => {
257287
setDeleteStream(e.target.value);
258288
}}
259-
placeholder={`Type the name of the stream to confirm. i.e: ${streamName}`}
289+
placeholder={`Type the name of the stream to confirm. i.e: ${activeStream}`}
260290
/>
261291

262292
<Box mt={10} display="flex" sx={{ justifyContent: 'end' }}>
263293
<Button
264294
variant="filled"
265295
color="red"
266296
sx={{ margin: '12px' }}
267-
disabled={deleteStream === streamName ? false : true}
297+
disabled={deleteStream === activeStream ? false : true}
268298
onClick={handleDelete}>
269299
Delete
270300
</Button>
@@ -273,11 +303,9 @@ const Navbar: FC<NavbarProps> = (props) => {
273303
</Button>
274304
</Box>
275305
</Modal>
276-
<InfoModal opened={opened} close={close}/>
306+
<InfoModal opened={opened} close={close} />
277307
</MantineNavbar>
278308
);
279309
};
280310

281-
282-
283311
export default Navbar;

0 commit comments

Comments
 (0)