Skip to content

Commit

Permalink
feat(console): manage users support listing users and change user sta…
Browse files Browse the repository at this point in the history
…te (#810)
  • Loading branch information
jialeicui authored Jul 26, 2022
1 parent e880fb4 commit 9ed8329
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 7 deletions.
20 changes: 17 additions & 3 deletions console/src/components/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import { useCurrentThemeType } from '@/hooks/useCurrentThemeType'
import User from '@/domain/user/components/User'
import { simulationJump } from '@/utils'
import { BsChevronDown } from 'react-icons/bs'
import { Link } from 'react-router-dom'
import { Link, useHistory } from 'react-router-dom'
import PasswordForm from '@user/components/PasswordForm'
import { IChangePasswordSchema } from '@user/schemas/user'
import { changePassword } from '@user/services/user'
import { toaster } from 'baseui/toast'
import { AiOutlineSetting, AiOutlineSecurityScan } from 'react-icons/ai'
import IconFont from '../IconFont'

const useHeaderStyles = createUseStyles({
Expand Down Expand Up @@ -148,10 +149,11 @@ const useStyles = createUseStyles({
userMenuItem: (props: IThemedStyleProps) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
justifyContent: 'left',
alignSelf: 'normal',
gap: '10px',
height: '32px',
paddingLeft: '10px',
color: props.theme.colors.contentPrimary,
backgroundColor: 'var(--color-brandMenuItemBackground)',
}),
Expand All @@ -175,6 +177,7 @@ export default function Header() {
const { currentUser } = useCurrentUser()

const [t] = useTranslation()
const history = useHistory()

const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false)
const handleChangePassword = useCallback(
Expand Down Expand Up @@ -224,6 +227,17 @@ export default function Header() {
<BsChevronDown size={14} />
</div>
<div className={styles.userMenu}>
<div
role='button'
tabIndex={0}
className={styles.userMenuItem}
onClick={() => {
history.push('/admin')
}}
>
<AiOutlineSetting size={18} />
<span>{t('Admin Settings')}</span>
</div>
<div
role='button'
tabIndex={0}
Expand All @@ -232,7 +246,7 @@ export default function Header() {
setIsChangePasswordOpen(true)
}}
>
<IconFont type='password' />
<AiOutlineSecurityScan size={18} />
<span>{t('Change Password')}</span>
</div>
<div
Expand Down
2 changes: 1 addition & 1 deletion console/src/components/data-table/stateful-data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function useResizeObserver(
}, [ref, callback])
}

function QueryInput(props: any) {
export function QueryInput(props: any) {
const [css, theme] = useStyletron()
const locale = React.useContext(LocaleContext)
const [value, setValue] = React.useState('')
Expand Down
8 changes: 8 additions & 0 deletions console/src/domain/user/hooks/useUser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { useQuery } from 'react-query'
import qs from 'qs'
import useGlobalState from '@/hooks/global'
import { IListQuerySchema } from '@base/schemas/list'
import { listUsers } from '@user/services/user'

export const useUser = () => {
const [user, setUser] = useGlobalState('user')
Expand All @@ -17,3 +21,7 @@ export const useUserLoading = () => {
setUserLoading,
}
}

export function useFetchUsers(query: IListQuerySchema) {
return useQuery(['fetch users', qs.stringify(query)], () => listUsers(query), { refetchOnWindowFocus: false })
}
8 changes: 8 additions & 0 deletions console/src/domain/user/schemas/user.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { IResourceSchema } from '@/domain/base/schemas/resource'

export interface IUserRoleSchema {
id: string
name: string
nameEn: string
}

export interface IUserSchema extends IResourceSchema {
id: string
name: string
isEnabled: string
role: IUserRoleSchema
}

export interface IRegisterUserSchema {
Expand Down
13 changes: 12 additions & 1 deletion console/src/domain/user/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function fetchCurrentUser(): Promise<IUserSchema> {
}

export async function listUsers(query: IListQuerySchema): Promise<IListSchema<IUserSchema>> {
const resp = await axios.get<IListSchema<IUserSchema>>('/api/v1/users', {
const resp = await axios.get<IListSchema<IUserSchema>>('/api/v1/user', {
params: query,
})
return resp.data
Expand All @@ -45,3 +45,14 @@ export async function changePassword(data: IChangePasswordSchema) {

return resp.data
}

export async function changeUserState(userId: string, enable: boolean) {
const resp = await axios({
method: 'put',
url: `/api/v1/user/${userId}/state`,
data: JSON.stringify({ isEnabled: enable }),
headers: { 'Content-Type': 'application/json' },
})

return resp.data
}
28 changes: 26 additions & 2 deletions console/src/i18n/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,36 @@ const locales0 = {
'User': {
en: 'User',
},
'Create User': {
en: 'Create User',
'Admin Settings': {
en: 'Admin Settings',
},
'Manage Users': {
en: 'Manage Users',
},
'Add User': {
en: 'Add User',
},
'User List': {
en: 'User List',
},
'Disable User': {
en: 'Disable',
},
'Enable User': {
en: 'Enable',
},
'Disable User Success': {
en: 'Disable User Success',
},
'Enable User Success': {
en: 'Enable User Success',
},
'Disabled User': {
en: 'Disabled',
},
'Enabled User': {
en: 'Enabled',
},
'Username': {
en: 'Username',
},
Expand Down
11 changes: 11 additions & 0 deletions console/src/pages/Admin/AdminLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react'
import BaseSubLayout from '@/pages/BaseSubLayout'
import AdminSidebar from '@/pages/Admin/AdminSidebar'

export interface IAdminLayoutProps {
children: React.ReactNode
}

export default function AdminLayout({ children }: IAdminLayoutProps) {
return <BaseSubLayout sidebar={AdminSidebar}>{children}</BaseSubLayout>
}
28 changes: 28 additions & 0 deletions console/src/pages/Admin/AdminSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import useTranslation from '@/hooks/useTranslation'
import React, { useMemo } from 'react'
import BaseSidebar, { IComposedSidebarProps, INavItem } from '@/components/BaseSidebar'
import { AiOutlineSetting, AiOutlineUser } from 'react-icons/ai'

export default function AdminSidebar({ style }: IComposedSidebarProps) {
const [t] = useTranslation()

const navItems: INavItem[] = useMemo(() => {
return [
{
title: t('Manage Users'),
path: '/admin/users',
icon: <AiOutlineUser size={20} />,
},
]
}, [t])

return (
<BaseSidebar
navItems={navItems}
style={style}
title={t('Admin Settings')}
titleLink='/admin'
icon={<AiOutlineSetting style={{ color: '#fff' }} size={20} />}
/>
)
}
100 changes: 100 additions & 0 deletions console/src/pages/Admin/UserManagement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useState, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import { Button, SIZE as ButtonSize } from 'baseui/button'
import { StyledLink } from 'baseui/link'
import Card from '@/components/Card'
import IconFont from '@/components/IconFont'
import Table from '@/components/Table'
import { usePage } from '@/hooks/usePage'
import useTranslation from '@/hooks/useTranslation'
import { formatTimestampDateTime } from '@/utils/datetime'
import { useFetchUsers } from '@user/hooks/useUser'
import { QueryInput } from '@/components/data-table/stateful-data-table'
import { useStyletron } from 'baseui'
import { IUserSchema } from '@user/schemas/user'
import { changeUserState } from '@user/services/user'
import { toaster } from 'baseui/toast'

interface IActionProps {
title: string
marginRight?: boolean
onClick: () => Promise<void>
}

const ActionButton: React.FC<IActionProps> = ({ title, marginRight = false, onClick }: IActionProps) => {
const style = {
textDecoration: 'none',
marginRight: marginRight ? '10px' : '0px',
}

return (
<StyledLink style={style} onClick={onClick}>
{title}
</StyledLink>
)
}

export default function UserManagement() {
const [page] = usePage()
const [t] = useTranslation()
const users = useFetchUsers(page)
const history = useHistory()
const [css] = useStyletron()
const [data, updateData] = useState<IUserSchema[]>([])
const [filter, updateFilter] = useState('')

useEffect(() => {
const items = users.data?.list ?? []
updateData(items.filter((i) => (filter && i.name.includes(filter)) || filter === ''))
}, [filter, users.data])

const changUserState = async (userId: string, enable: boolean): Promise<void> => {
await changeUserState(userId, enable)
toaster.positive(enable ? t('Enable User Success') : t('Disable User Success'), { autoHideDuration: 1000 })
await users.refetch()
return Promise.resolve()
}

return (
<Card
title={t('Manage Users')}
extra={
<Button
startEnhancer={<IconFont type='add' kind='white' />}
size={ButtonSize.compact}
onClick={() => history.push('new_job')}
>
{t('Add User')}
</Button>
}
>
<div className={css({ marginBottom: '20px' })}>
<QueryInput
onChange={(val: string) => {
updateFilter(val.trim())
}}
/>
</div>
<Table
isLoading={users.isLoading}
columns={[t('sth name', [t('User')]), t('Status'), t('Created'), t('Action')]}
data={
data.map((user) => [
user.name,
user.isEnabled ? t('Enabled User') : t('Disabled User'),
user.createdTime && formatTimestampDateTime(user.createdTime),
<div key={user.id}>
<ActionButton
marginRight
title={user.isEnabled ? t('Disable User') : t('Enable User')}
onClick={() => changUserState(user.id, !user.isEnabled)}
/>
&nbsp; {/* make segmenter works well when double click */}
<ActionButton title={t('Change Password')} onClick={async (): Promise<void> => {}} />
</div>,
]) ?? []
}
/>
</Card>
)
}
11 changes: 11 additions & 0 deletions console/src/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import AdminLayout from '@/pages/Admin/AdminLayout'
import UserManagement from '@/pages/Admin/UserManagement'
import React from 'react'
import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom'
import ProjectLayout from '@/pages/Project/ProjectLayout'
Expand Down Expand Up @@ -197,6 +199,15 @@ const Routes = () => {
</Switch>
</ModelLayout>
</Route>
{/* admin */}
<Route exact path='/admin/:path?'>
<AdminLayout>
<Switch>
<Route exact path='/admin/users' component={UserManagement} />
<Redirect exact from='/admin' to='/admin/users' />
</Switch>
</AdminLayout>
</Route>
{/* other */}
<Route exact path='/login' component={Login} />
<Route exact path='/logout' component={Pending} />
Expand Down

0 comments on commit 9ed8329

Please sign in to comment.