Skip to content

Commit

Permalink
User management - Front-end
Browse files Browse the repository at this point in the history
  • Loading branch information
sanjaytkbabu committed Jul 23, 2024
1 parent b0bd1fa commit b318a9f
Show file tree
Hide file tree
Showing 10 changed files with 502 additions and 3 deletions.
1 change: 1 addition & 0 deletions frontend/src/components/layout/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ onMounted(() => {
},
{
label: 'User Management',
route: RouteName.USER_MANAGEMENT,
access: Permissions.NAVIGATION_HOUSING_USER_MANAGEMENT
},
{
Expand Down
103 changes: 103 additions & 0 deletions frontend/src/components/user/UserCreateModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Button, Column, DataTable, Dialog, FilterMatchMode, IconField, InputIcon, InputText } from '@/lib/primevue';
import { Dropdown } from '@/components/form';
import { ROLES } from '@/utils/constants/application';
import type { Ref } from 'vue';
import type { User } from '@/types';
// Emits
const emit = defineEmits(['userCreate:request']);
// State
const visible = defineModel<boolean>('visible');
const users: Ref<Array<User>> = ref([]);
const selection: Ref<User | undefined> = ref(undefined);
// Datatable filter(s)
const filters = ref({
global: { value: null, matchMode: FilterMatchMode.CONTAINS }
});
</script>

<template>
<Dialog
v-model:visible="visible"
:draggable="false"
:modal="true"
class="app-info-dialog w-5"
>
<template #header>
<span class="p-dialog-title">Create new user</span>
</template>
<IconField
icon-position="left"
class="mt-1"
>
<InputIcon class="pi pi-search" />
<InputText
v-model="filters['global'].value"
placeholder="Search by first name, last name, or email"
class="col-12 pl-5"
/>
</IconField>
<DataTable
v-model:selection="selection"
v-model:filters="filters"
:row-hover="true"
class="datatable mt-3 mb-2"
:value="users"
selection-mode="single"
data-key="userId"
>
<template #empty>
<div class="flex justify-content-center">
<h5 class="m-0">No users found.</h5>
</div>
</template>
<Column
field="username"
header="Username"
sortable
/>
<Column
field="firstName"
header="First Name"
sortable
/>
<Column
field="lastName"
header="Last Name"
sortable
/>
</DataTable>
<Dropdown
class="col-12"
name="assignRole"
label="Assign role"
:options="ROLES"
/>
<div class="flex-auto">
<Button
class="mr-2"
label="Request approval"
type="submit"
icon="pi pi-check"
@click="
() => {
emit('userCreate:request', selection);
visible = false;
}
"
/>
<Button
class="p-button-outlined mr-2"
label="Cancel"
icon="pi pi-times"
@click="visible = false"
/>
</div>
</Dialog>
</template>
52 changes: 52 additions & 0 deletions frontend/src/components/user/UserManageModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { Button, Dialog } from '@/lib/primevue';
import { RadioList } from '@/components/form';
import { ROLES } from '@/utils/constants/application';
// Emits
const emit = defineEmits(['userManage:save']);
// State
const visible = defineModel<boolean>('visible');
</script>

<template>
<Dialog
v-model:visible="visible"
:draggable="false"
:modal="true"
class="app-info-dialog w-3"
>
<template #header>
<span class="p-dialog-title">Manage user role</span>
</template>
<div>Select role</div>
<RadioList
name="role"
:bold="false"
:options="ROLES"
class="mt-3 mb-4"
/>
<div class="flex-auto">
<Button
class="mr-2"
label="Save"
type="submit"
icon="pi pi-check"
@click="
() => {
emit('userManage:save');
visible = false;
}
"
/>
<Button
class="p-button-outlined mr-2"
label="Cancel"
icon="pi pi-times"
@click="visible = false"
/>
</div>
</Dialog>
</template>
147 changes: 147 additions & 0 deletions frontend/src/components/user/UserTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Button, Column, DataTable } from '@/lib/primevue';
import PermissionService, { Permissions } from '@/services/permissionService';
import type { Ref } from 'vue';
import type { User } from '@/types';
// Props
type Props = {
users: Array<User>;
revocation?: boolean;
};
const props = withDefaults(defineProps<Props>(), {
users: undefined
});
// Constants
const USER_STATUS = {
APPROVED: 'Approved'
};
// Emits
const emit = defineEmits(['userTable:delete', 'userTable:approve', 'userTable:manage', 'userTable:revoke']);
// State
const selection: Ref<User | undefined> = ref(undefined);
// Actions
const permissionService = new PermissionService();
</script>

<template>
<DataTable
v-model:selection="selection"
:row-hover="true"
class="datatable"
:value="props.users"
selection-mode="single"
>
<template #empty>
<div class="flex justify-content-center">
<h5 class="m-0">No users found.</h5>
</div>
</template>
<Column
field="username"
header="Username"
sortable
/>
<Column
field="firstName"
header="First Name"
sortable
/>
<Column
field="lastName"
header="Last Name"
sortable
/>
<Column
field="status"
header="Status"
sortable
/>
<Column
field="role"
header="Role"
sortable
/>
<Column
v-if="!revocation"
field="manage"
header="Manage"
header-class="header-right"
class="text-right"
style="min-width: 150px"
>
<template #body="{ data }">
<Button
class="p-button-lg p-button-text p-0 pr-3"
aria-label="Manage user"
:disabled="revocation || data.status !== USER_STATUS.APPROVED"
@click="
() => {
selection = data;
emit('userTable:manage', data);
}
"
>
<font-awesome-icon icon="fa-solid fa-pen-to-square" />
</Button>
</template>
</Column>
<Column
v-if="!revocation && permissionService.can(Permissions.NAVIGATION_HOUSING_USER_MANAGEMENT_ADMIN)"
field="approve"
header="Approve"
header-class="header-right"
class="text-right"
style="min-width: 150px"
>
<template #body="{ data }">
<Button
class="p-button-lg p-button-text p-0 pr-3"
aria-label="Approve user"
:disabled="revocation || data.status === USER_STATUS.APPROVED"
@click="
() => {
selection = data;
emit('userTable:approve', data);
}
"
>
<font-awesome-icon icon="fa-solid fa-check-to-slot" />
</Button>
</template>
</Column>
<Column
field="action"
header="Action"
header-class="header-right"
class="text-right"
style="min-width: 150px"
>
<template #body="{ data }">
<Button
class="p-button-lg p-button-text p-button-danger p-0 pr-3"
aria-label="Delete user"
:disabled="data.status !== USER_STATUS.APPROVED"
@click="
() => {
selection = data;
permissionService.can(Permissions.NAVIGATION_HOUSING_USER_MANAGEMENT_ADMIN)
? emit('userTable:delete', data)
: emit('userTable:revoke', data);
}
"
>
<font-awesome-icon icon="fa-solid fa-trash" />
</Button>
</template>
</Column>
</DataTable>
</template>
5 changes: 5 additions & 0 deletions frontend/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ const routes: Array<RouteRecordRaw> = [
}
]
},
{
path: '/user',
name: RouteName.USER_MANAGEMENT,
component: () => import('@/views/user/UserManagementView.vue')
},
{
path: '/:pathMatch(.*)*',
name: RouteName.NOT_FOUND,
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/services/permissionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export enum Permissions {
NAVIGATION_HOUSING_SUBMISSIONS_SUB = 'housing.submissions.sub',
NAVIGATION_HOUSING_STATUS_TRACKER = 'housing.status.tracker',
NAVIGATION_HOUSING_USER_MANAGEMENT = 'housing.usermanagement',
NAVIGATION_HOUSING_USER_MANAGEMENT_ADMIN = 'housing.usermanagementadmin',
NAVIGATION_DEVELOPER = 'developer',

TESTING_ROLE_OVERRIDE = 'testing.role.override'
Expand All @@ -46,7 +47,8 @@ const PermissionMap = [
Permissions.NAVIGATION_HOUSING_STATUS_TRACKER,
Permissions.NAVIGATION_HOUSING_SUBMISSION,
Permissions.NAVIGATION_HOUSING_SUBMISSIONS,
Permissions.NAVIGATION_HOUSING_USER_MANAGEMENT
Permissions.NAVIGATION_HOUSING_USER_MANAGEMENT,
Permissions.NAVIGATION_HOUSING_USER_MANAGEMENT_ADMIN
]
},
{
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/types/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type User = {
identityId: string | null;
idp: string;
lastName: string;
role?: string;
status?: string;
userId: string;
username: string;
elevatedRights: boolean;
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/utils/constants/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import { NIL } from 'uuid';
import { AccessRole, BasicResponse } from '../enums/application';
import { AccessRole, BasicResponse, Roles } from '../enums/application';

export const ACCESS_ROLES_LIST = [
AccessRole.PCNS_ADMIN,
Expand All @@ -20,6 +20,8 @@ export const PCNS_CONTACT = {
subject: 'Reporting an Issue with PCNS'
};

export const ROLES = [Roles.NAVIGATOR, Roles.READ_ONLY];

export const SYSTEM_USER = NIL;

export const YES_NO_LIST = [BasicResponse.YES, BasicResponse.NO];
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/utils/enums/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@ export enum RouteName {

OIDC_CALLBACK = 'oidc_callback',
OIDC_LOGIN = 'oidc_login',
OIDC_LOGOUT = 'oidc_logout'
OIDC_LOGOUT = 'oidc_logout',

USER_MANAGEMENT = 'user_management'
}

export enum Roles {
NAVIGATOR = 'Navigator',
READ_ONLY = 'Read-only'
}

export enum StorageKey {
Expand Down
Loading

0 comments on commit b318a9f

Please sign in to comment.