Skip to content

Commit

Permalink
feat: #1542 User details link and breadcrumb change. (#1589)
Browse files Browse the repository at this point in the history
  • Loading branch information
ianliuwk1019 authored Sep 12, 2024
1 parent 52fff70 commit fa6b3ad
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 47 deletions.
1 change: 1 addition & 0 deletions frontend/src/alltypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ declare module '@carbon/icons-vue/es/group--access/16';
declare module '@carbon/icons-vue/es/enterprise/16'
declare module '@carbon/icons-vue/es/user--profile/16'
declare module '@carbon/icons-vue/es/document/16'
declare module '@carbon/icons-vue/es/recently-viewed/16';

// medium
declare module '@carbon/icons-vue/es/login/20';
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/components/common/Icon.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
import type { PropType } from 'vue';
import { IconSize } from '@/enum/IconEnum';
import type { PropType } from 'vue';
import { defineAsyncComponent } from 'vue';
const props = defineProps({
icon: {
Expand Down Expand Up @@ -72,6 +72,9 @@ const icons = {
document16: defineAsyncComponent(
() => import('@carbon/icons-vue/es/document/16')
),
history16: defineAsyncComponent(
() => import('@carbon/icons-vue/es/recently-viewed/16')
),
// medium icons
'checkmark--filled20': defineAsyncComponent(
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/components/common/SideNav.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script setup lang="ts">
import Sidebar from 'primevue/sidebar';
import router from '@/router';
import { sideNavState } from '@/store/SideNavState';
import Sidebar from 'primevue/sidebar';
import type { PropType } from 'vue';
import type { RouteLocationRaw } from 'vue-router';
Expand Down Expand Up @@ -35,7 +34,7 @@ const props = defineProps({
item.link,
'sidenav-disabled': item.disabled,
}"
@click="router.push(item.link)"
@click="$router.push(item.link)"
>
{{ item.name }}
</li>
Expand All @@ -48,7 +47,7 @@ const props = defineProps({
child.link,
'sidenav-disabled': child.disabled,
}"
@click="router.push(child.link)"
@click="$router.push(child.link)"
>
<span>{{ child.name }}</span>
</li>
Expand Down
42 changes: 28 additions & 14 deletions frontend/src/components/managePermissions/table/UserDataTable.vue
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
<script setup lang="ts">
import { reactive, ref, computed, type PropType } from 'vue';
import { FilterMatchMode } from 'primevue/api';
import Column from 'primevue/column';
import DataTable from 'primevue/datatable';
import { useConfirm } from 'primevue/useconfirm';
import ConfirmDialog from 'primevue/confirmdialog';
import DataTable from 'primevue/datatable';
import ProgressSpinner from 'primevue/progressspinner';
import { useConfirm } from 'primevue/useconfirm';
import { computed, reactive, ref, type PropType } from 'vue';
import { IconSize } from '@/enum/IconEnum';
import { routeItems } from '@/router/routeItem';
import Button from '@/components/common/Button.vue';
import NewUserTag from '@/components/common/NewUserTag.vue';
import ConfirmDialogtext from '@/components/managePermissions/ConfirmDialogText.vue';
import DataTableHeader from '@/components/managePermissions/table/DataTableHeader.vue';
import { IconSize } from '@/enum/IconEnum';
import router from '@/router';
import { routeItems } from '@/router/routeItem';
import { EnvironmentSettings } from '@/services/EnvironmentSettings';
import { isNewAccess } from '@/services/utils';
import { selectedApplicationId } from '@/store/ApplicationState';
import {
NEW_ACCESS_STYLE_IN_TABLE,
TABLE_CURRENT_PAGE_REPORT_TEMPLATE,
TABLE_PAGINATOR_TEMPLATE,
TABLE_ROWS_PER_PAGE,
NEW_ACCESS_STYLE_IN_TABLE,
} from '@/store/Constants';
import { isNewAccess } from '@/services/utils';
import type { FamApplicationUserRoleAssignmentGet } from 'fam-app-acsctl-api';
const environmentSettings = new EnvironmentSettings();
const isDevEnvironment = environmentSettings.isDevEnvironment();
type emit = (
e: 'deleteUserRoleAssignment',
item: FamApplicationUserRoleAssignmentGet
Expand Down Expand Up @@ -78,6 +83,13 @@ function deleteAssignment(assignment: FamApplicationUserRoleAssignmentGet) {
});
}
const viewUserPermissionHistoryDetails = (user_id: number) => {
router.push({
name: routeItems.userDetails.name,
params: {userId: user_id, applicationId: selectedApplicationId.value}
});
}
const highlightNewUserAccessRow = (rowData: any) => {
if (isNewAccess(newUserAccessIds.value, rowData.user_role_xref_id)) {
return NEW_ACCESS_STYLE_IN_TABLE;
Expand Down Expand Up @@ -177,12 +189,14 @@ const highlightNewUserAccessRow = (rowData: any) => {
>
<Column header="Action">
<template #body="{ data }">
<!-- Hidden until functionality is available
<button
class="btn btn-icon"
>
<Icon icon="edit" :size="IconSize.small"/>
</button> -->
<button
title="User permission history"
class="btn btn-icon"
:disabled="!isDevEnvironment"
@click="viewUserPermissionHistoryDetails(data.user_id)">
<Icon icon="history" :size="IconSize.small" />
</button>

<button
title="Delete user"
class="btn btn-icon"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="ts">
// TODO: Currently this is a placeholder component.
</script>

<template>
Under Construction...
</template>

<style scoped lang="scss">
@import '@/assets/styles/base.scss';
</style>
31 changes: 19 additions & 12 deletions frontend/src/layouts/ProtectedLayout.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import type { ISideNavItem } from '@/components/common/SideNav.vue';
import Header from '@/components/header/Header.vue';
import SideNav, { type ISideNavItem } from '@/components/common/SideNav.vue';
import { EnvironmentSettings } from '@/services/EnvironmentSettings';
import sideNavData from '@/static/sideNav.json';
import { FAM_APPLICATION_ID } from '@/store/Constants';
import {
isApplicationSelected,
selectedApplicationId,
} from '@/store/ApplicationState';
import { FAM_APPLICATION_ID } from '@/store/Constants';
import LoginUserState from '@/store/FamLoginUserState';
import { EnvironmentSettings } from '@/services/EnvironmentSettings';
import { onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
const environmentSettings = new EnvironmentSettings();
const isDevEnvironment = environmentSettings.isDevEnvironment();
const navigationData = ref<[ISideNavItem]>(sideNavData as any);
const route = useRoute();
// Show and hide the correct sideNav btn based on the application
const setSideNavOptions = () => {
Expand Down Expand Up @@ -42,18 +43,24 @@ onMounted(() => {
}
});
watch(selectedApplicationId, () => {
// watch a ref:selectedApplicationId and a route change in order to react to sidNav difference.
watch([selectedApplicationId, route], () => {
setSideNavOptions();
});
const disableSideNavOption = (optionName: string, disabled: boolean) => {
navigationData.value.map((navItem) => {
navItem.items?.map((childNavItem: ISideNavItem) => {
if (childNavItem.name === optionName) {
childNavItem.disabled = disabled;
const disableSideNavItemsOption = (optionName: string, disabled: boolean, items: ISideNavItem[]) => {
items.forEach((navItem) => {
if (navItem.name === optionName) {
navItem.disabled = disabled;
}
if (navItem.items) {
disableSideNavItemsOption(optionName, disabled, navItem.items);
}
});
});
})
}
disableSideNavItemsOption(optionName, disabled, navigationData.value);
};
</script>
<template>
Expand Down
25 changes: 23 additions & 2 deletions frontend/src/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router';

import AuthCallback from '@/components/AuthCallbackHandler.vue';
import UserDetails from '@/components/managePermissions/userDetails/UserDetails.vue';
import NotFound from '@/components/NotFound.vue';
import {
beforeEachRouteHandler,
Expand All @@ -9,11 +10,11 @@ import {
import { routeItems } from '@/router/routeItem';
import GrantAccessView from '@/views/GrantAccessView.vue';
import GrantApplicationAdminView from '@/views/GrantApplicationAdminView.vue';
import GrantDelegatedAdminView from '@/views/GrantDelegatedAdminView.vue';
import LandingView from '@/views/LandingView.vue';
import ManagePermissionsView from '@/views/ManagePermissionsView.vue';
import { AdminRoleAuthGroup } from 'fam-admin-mgmt-api/model';
import GrantDelegatedAdminView from '@/views/GrantDelegatedAdminView.vue';
import MyPermissionsView from '@/views/MyPermissionsView.vue';
import { AdminRoleAuthGroup } from 'fam-admin-mgmt-api/model';

// WARNING: any components referenced below that themselves reference the router cannot be automatically hot-reloaded in local development due to circular dependency
// See vitejs issue https://github.com/vitejs/vite/issues/3033 for discussion.
Expand Down Expand Up @@ -114,6 +115,26 @@ const routes = [
component: GrantDelegatedAdminView,
beforeEnter: beforeEnterHandlers[routeItems.grantDelegatedAdmin.name],
},
{
path: routeItems.userDetails.path,
name: routeItems.userDetails.name,
meta: {
requiresAuth: true,
requiresAppSelected: true,
title: routeItems.userDetails.label,
layout: 'ProtectedLayout',
hasBreadcrumb: true,
},
component: UserDetails,

/* TODO: 'beforeEnter' placeholder to fetch data from backend*/
// beforeEnter: beforeEnterHandlers[routeItems.userDetails.name],
// props: (route: any) => {
// return {
// // TODO: placeholder here to supply props for the component.
// };
// },
},
{
path: routeItems.myPermissions.path,
name: routeItems.myPermissions.name,
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/router/routeHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { FamRouteError, RouteErrorName } from '@/errors/FamCustomError';
import { routeItems } from '@/router/routeItem';
import AuthService from '@/services/AuthService';
import { EnvironmentSettings } from '@/services/EnvironmentSettings';
import {
fetchApplicationAdmins,
fetchUserRoleAssignments,
fetchDelegatedAdmins,
fetchUserRoleAssignments,
} from '@/services/fetchData';
import { asyncWrap } from '@/services/utils';
import {
Expand All @@ -17,7 +18,6 @@ import LoginUserState from '@/store/FamLoginUserState';
import { setRouteToastError as emitRouteToastError } from '@/store/ToastState';
import { AdminRoleAuthGroup } from 'fam-admin-mgmt-api/model';
import type { RouteLocationNormalized } from 'vue-router';
import { EnvironmentSettings } from '@/services/EnvironmentSettings';

const environmentSettings = new EnvironmentSettings();
const isDevEnvironment = environmentSettings.isDevEnvironment();
Expand Down Expand Up @@ -84,7 +84,7 @@ const beforeEnterGrantUserPermissionRoute = async (
return { path: routeItems.dashboard.path };
}

populateBreadcrumb([routeItems.dashboard, routeItems.grantUserPermission]);
populateBreadcrumb([routeItems.dashboard]);
return true;
};

Expand All @@ -96,7 +96,7 @@ const beforeEnterGrantApplicationAdminRoute = async (
emitRouteToastError(ACCESS_RESTRICTED_ERROR);
return { path: routeItems.dashboard.path };
}
populateBreadcrumb([routeItems.dashboard, routeItems.grantAppAdmin]);
populateBreadcrumb([routeItems.dashboard]);
return true;
};

Expand All @@ -107,7 +107,7 @@ const beforeEnterGrantDelegationAdminRoute = async (
emitRouteToastError(ACCESS_RESTRICTED_ERROR);
return { path: routeItems.dashboard.path };
}
populateBreadcrumb([routeItems.dashboard, routeItems.grantDelegatedAdmin]);
populateBreadcrumb([routeItems.dashboard]);
return true;
};

Expand Down
9 changes: 7 additions & 2 deletions frontend/src/router/routeItem.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface IRouteInfo {
label: string;
label?: string;
path: string;
name: string;
}
Expand Down Expand Up @@ -38,5 +38,10 @@ export const routeItems = {
name: 'myPermissions',
path: '/my-permissions',
label: 'Check my permissions',
}
},
userDetails: {
name: 'viewUserDetails',
path: '/user-details/users/:userId/applications/:applicationId',
label: 'User details',
},
} as RouteItems;
19 changes: 19 additions & 0 deletions frontend/src/store/BreadcrumbState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@ import { ref } from 'vue';

export const breadcrumbState = ref();

// This is a special item to fit the limitation of "PrimeVue" Breadcrum that
// the very last item for breadcrum is not rendering as a link. It will be append
// at the end of the `breadcrumbItem` array
const crumbEndItem = {
name: 'endCrumb',
path: "", // deliberately empty.
label: undefined // deliberately undefined.
}

/**
* 'breadcrumbItem' items to display for current routed component.
* Note:
* - We don't need to show the current page crumb item.
* - PrmeVue has limitation that the last crumb item will not be rendered as a link.
* So `crumbEndItem` is always appended at the end to make first item always is
* a link for convenience.
* @param breadcrumbItem
*/
export const populateBreadcrumb = (breadcrumbItem: IRouteInfo[]) => {
breadcrumbItem.push(crumbEndItem);
breadcrumbState.value = breadcrumbItem;
};
12 changes: 6 additions & 6 deletions frontend/src/tests/GrantApplicationAdmin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import GrantApplicationAdmin from '@/components/grantaccess/GrantApplicationAdmin.vue';
import router, { routes } from '@/router';
import { mount, VueWrapper } from '@vue/test-utils';
import { it, describe, beforeEach, expect, afterEach, vi } from 'vitest';
import { routeItems } from '@/router/routeItem';
import { fixJsdomCssErr } from './common/fixJsdomCssErr';
import GrantApplicationAdmin from '@/components/grantaccess/GrantApplicationAdmin.vue';
import waitForExpect from 'wait-for-expect';
import { populateBreadcrumb } from '@/store/BreadcrumbState';
import { mount, VueWrapper } from '@vue/test-utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import waitForExpect from 'wait-for-expect';
import { fixJsdomCssErr } from './common/fixJsdomCssErr';

fixJsdomCssErr();
vi.mock('vue-router', async () => {
Expand All @@ -25,7 +25,7 @@ describe('GrantApplicationAdmin', () => {
const routerPushSpy = vi.spyOn(router, 'push');

//populate the breadcrumbState
const breadcrumbItems = [routeItems.dashboard, routeItems.grantAppAdmin];
const breadcrumbItems = [routeItems.dashboard];
populateBreadcrumb(breadcrumbItems);
beforeEach(async () => {
wrapper = mount(GrantApplicationAdmin, {
Expand Down

0 comments on commit fa6b3ad

Please sign in to comment.