Skip to content

Commit

Permalink
feat: geo 656 user management screen (#552)
Browse files Browse the repository at this point in the history
Co-authored-by: Sukanya Rath <98050194+sukanya-rath@users.noreply.github.com>
  • Loading branch information
goemen and sukanya-rath committed Jun 24, 2024
1 parent b768e39 commit 6396a05
Show file tree
Hide file tree
Showing 32 changed files with 741 additions and 19 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ jobs:
--set-string global.secrets.bceidWsAuthPassword="${{ secrets.BCEID_WS_BASIC_AUTH_PASSWORD }}" \
--set-string global.secrets.bceidWsAuthUserName="${{ secrets.BCEID_WS_BASIC_AUTH_USERNAME }}" \
--set-string global.secrets.bceidWsOnlineServiceId="${{ secrets.BCEID_WS_ONLINE_SERVICE_ID }}" \
--set-string global.secrets.cssAppApiClientId="${{ secrets.CSS_APP_API_CLIENT_ID }}" \
--set-string global.secrets.cssAppApiClientSecret="${{ secrets.CSS_APP_API_CLIENT_SECRET }}" \
--set-string global.secrets.cssAppApiIntegrationId="${{ secrets.CSS_APP_API_INTEGRATION_ID }}" \
--set-string global.secrets.cssAppApiEnvironment="${{ inputs.environment }}" \
--set-string global.secrets.externalConsumerApiKey="${{ secrets.EXTERNAL_CONSUMER_API_KEY }}" \
--set-string global.secrets.externalConsumerDeleteReportsApiKey="${{ secrets.EXTERNAL_CONSUMER_DELETE_REPORTS_API_KEY }}" \
--set-string global.secrets.externalConsumerErrorReportsApiKey="${{ secrets.EXTERNAL_CONSUMER_ERROR_REPORTS_API_KEY }}" \
Expand Down
7 changes: 4 additions & 3 deletions admin-frontend/e2e/pages/admin-portal-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ export class AdminPortalPage {
await expect(
this.page.getByRole('link', { name: 'Announcements' }),
).toBeVisible();
await expect(
this.page.getByRole('link', { name: 'User Management' }),
).toBeVisible();
// TODO: bring it back after the
// await expect(
// this.page.getByRole('link', { name: 'User Management' }),
// ).toBeVisible();
await expect(
this.page.getByRole('link', { name: 'Analytics' }),
).toBeVisible();
Expand Down
6 changes: 6 additions & 0 deletions admin-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions admin-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"papaparse": "^5.4.1",
"pinia": "^2.1.7",
"regenerator-runtime": "^0.14.0",
"resize-observer-polyfill": "^1.5.1",
"rfdc": "^1.3.0",
"sass": "^1.58.3",
"tiny-emitter": "^2.1.0",
Expand Down
1 change: 1 addition & 0 deletions admin-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ h1 {
.v-btn {
text-transform: none !important;
font-weight: 600 !important;
}
button:disabled.v-btn {
Expand Down
4 changes: 4 additions & 0 deletions admin-frontend/src/components/SideBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
to="user-management"
title="User Management"
:class="{ active: activeRoute == 'user-management' }"
v-if="userInfo?.role == ADMIN_ROLE_NAME"
>
<template v-slot:prepend>
<v-icon icon="mdi-account-multiple"></v-icon>
Expand Down Expand Up @@ -88,7 +89,10 @@ export default {
<script setup>
import { watch, ref } from 'vue';
import { useRoute } from 'vue-router';
import { authStore } from '../store/modules/auth';
import { ADMIN_ROLE_NAME } from '../constants';
const { userInfo } = authStore();
const route = useRoute();
const activeRoute = ref();
const isExpanded = ref(true);
Expand Down
143 changes: 140 additions & 3 deletions admin-frontend/src/components/UserManagementPage.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,142 @@
<template>User Management placeholder</template>
<template>
<div v-if="loading" class="loader-root">
<v-progress-circular color="primary" indeterminate></v-progress-circular>
<span class="mt-2">Loading users</span>
</div>
<div v-if="!loading && users !== undefined" class="main-root">
<div class="toolbar">
<div class="title">Users ({{ users.length }})</div>
<span style="flex: 1 1 auto" />
<v-btn prepend-icon="mdi-account-plus" color="primary" variant="elevated"
>Add New User</v-btn
>
</div>
<v-row class="users-grid" no-gutters>
<v-col
class="user-card-wrapper"
cols="12"
sm="12"
md="3"
v-for="user of users"
>
<v-card class="pa-4 ma-2 user-card">
<div class="actions d-flex">
<v-avatar color="primary">
<span class="text-h6">{{ getUserInitials(user) }}</span>
</v-avatar>
<span style="flex: 1 1 auto" />

<script lang="ts"></script>
<v-btn
density="compact"
variant="text"
color="error"
icon="mdi-trash-can-outline"
aria-label="Delete user"
></v-btn>
</div>
<div class="display-name mt-2">
{{ user.displayName }}
</div>
<v-menu location="bottom">
<template v-slot:activator="{ props }">
<v-btn
color="primary"
variant="text"
append-icon="mdi-chevron-down"
class="role-menu-button"
:aria-label="`Role ${RoleLabels[user.role]}`"
v-bind="props"
>{{ RoleLabels[user.role] }}</v-btn
>
</template>

<style></style>
<v-list>
<v-list-item
:variant="user.role === item.value ? 'tonal' : 'plain'"
v-for="(item, index) in RoleOptions"
:key="index"
:class="user.role === item.value ? 'selected-role' : undefined"
>
<v-list-item-title v-text="item.label"></v-list-item-title>
<template v-slot:append v-if="user.role === item.value">
<v-icon size="x-small" icon="mdi-check" />
</template>
</v-list-item>
</v-list>
</v-menu>
</v-card>
</v-col>
</v-row>
</div>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { onMounted } from 'vue';
import { useUsersStore } from '../store/modules/usersStore';
import { RoleOptions, RoleLabels } from '../constants';
const usersStore = useUsersStore();
const { users, loading } = storeToRefs(usersStore);
const getUserInitials = (user) => {
const tokens = user.displayName.split(' ');
return `${tokens[0][0]}${tokens[1][0]}`;
};
onMounted(async () => {
await usersStore.getUsers();
});
</script>

<style scoped lang="scss">
.loader-root {
width: 100%;
height: calc(100vh - 200px);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.main-root {
width: 100%;
min-height: calc(100vh - 200px);
display: flex;
flex-direction: column;
.toolbar {
width: 100%;
display: flex;
flex-direction: row;
margin-bottom: 10px;
.title {
font-weight: bold;
font-size: large;
}
}
}
.users-grid {
flex: none;
.user-card-wrapper {
.user-card {
.actions {
width: 100%;
}
.display-name {
font-weight: 700;
color: #000;
}
}
}
}
.role-menu-button {
padding: 5px;
padding-right: 5px;
}
.selected-role {
color: #003366;
font-weight: 600;
}
</style>
6 changes: 5 additions & 1 deletion admin-frontend/src/components/__tests__/SideBar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
import SideBar from '../SideBar.vue';
import { createTestingPinia } from '@pinia/testing';

// Mock the ResizeObserver
const ResizeObserverMock = vi.fn(() => ({
Expand All @@ -24,6 +25,9 @@ describe('SideBar', () => {
components,
directives,
});
const pinia = createTestingPinia({
initialState: {},
});

wrapper = mount(
{
Expand All @@ -41,7 +45,7 @@ describe('SideBar', () => {
components: {
SideBar,
},
plugins: [vuetify],
plugins: [vuetify, pinia],
},
},
);
Expand Down
104 changes: 104 additions & 0 deletions admin-frontend/src/components/__tests__/UserManagementPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { vi, beforeEach, describe, it, expect } from 'vitest';
import UserManagementPage from '../UserManagementPage.vue';
import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
import { createTestingPinia } from '@pinia/testing';
import { useUsersStore } from '../../store/modules/usersStore';
import { render, waitFor, fireEvent } from '@testing-library/vue';

global.ResizeObserver = require('resize-observer-polyfill');

const mockGetUsers = vi.fn();
vi.mock('../../../services/apiService', () => ({
default: {
getUsers: () => mockGetUsers(),
},
}));

const vuetify = createVuetify({
components,
directives,
});

const pinia = createTestingPinia({
initialState: {},
});

const wrappedRender = () => {
return render(UserManagementPage, {
global: {
plugins: [pinia, vuetify],
},
});
};

const store = useUsersStore();

describe('UserManagementPage', () => {
beforeEach(() => {
store.$patch({
loading: false,
users: [],
});
});

it('should display loading spinner', async () => {
const wrapper = await wrappedRender();
store.$patch({
loading: true,
users: [],
});

await waitFor(() => {
wrapper.getByText('Loading users');
});
});
it('should add user button', async () => {
const wrapper = await wrappedRender();
store.$patch({
loading: false,
users: [],
});

await waitFor(() => {
wrapper.getByRole('button', { name: 'Add New User' });
});
});
it('should display the number of users found', async () => {
const wrapper = await wrappedRender();
store.$patch({
loading: false,
users: [
{
userName: 'testusername',
displayName: 'Test display name',
role: 'PTRT-ADMIN',
},
],
});

await waitFor(() => {
wrapper.getByText('Users (1)');
});
});
it('should user cards', async () => {
const wrapper = await wrappedRender();
store.$patch({
loading: false,
users: [
{
userName: 'testusername',
displayName: 'Test Display FIN:EX',
role: 'PTRT-ADMIN',
},
],
});

await waitFor(() => {
expect(wrapper.getByRole('button', { name: 'Delete user' }));
expect(wrapper.getByText('Test Display FIN:EX')).toBeInTheDocument();
expect(wrapper.getByRole('button', { name: 'Role Manager' }));
});
});
});
12 changes: 12 additions & 0 deletions admin-frontend/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const ADMIN_ROLE_NAME = 'PTRT-ADMIN';
export const USER_ROLE_NAME = 'PTRT-USER';

export const RoleLabels = {
'PTRT-ADMIN': 'Manager',
'PTRT-USER': 'User'
}

export const RoleOptions = [
{ label: RoleLabels['PTRT-ADMIN'], value: 'PTRT-ADMIN' },
{ label: RoleLabels['PTRT-USER'], value: 'PTRT-USER' },
];
5 changes: 5 additions & 0 deletions admin-frontend/src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PAGE_TITLES } from './utils/constant';
import Login from './components/Login.vue';
import { authStore } from './store/modules/auth';
import Logout from './components/Logout.vue';
import { ADMIN_ROLE_NAME } from './constants';

const router = createRouter({
history: createWebHistory(),
Expand Down Expand Up @@ -68,6 +69,7 @@ const router = createRouter({
meta: {
pageTitle: PAGE_TITLES.USER_MANAGEMENT,
requiresAuth: true,
role: ADMIN_ROLE_NAME,
isTitleVisible: true,
isBreadcrumbTrailVisible: true,
},
Expand Down Expand Up @@ -153,6 +155,9 @@ router.beforeEach((to, _from, next) => {
aStore
.getUserInfo()
.then(() => {
if (to.meta.role && aStore.userInfo?.role !== to.meta.role) {
next('notfound')
}
next();
})
.catch(() => {
Expand Down
Loading

0 comments on commit 6396a05

Please sign in to comment.