Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: #1542 User details link and breadcrumb change. #1589

Merged
merged 8 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
craigyu marked this conversation as resolved.
Show resolved Hide resolved
@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]);
craigyu marked this conversation as resolved.
Show resolved Hide resolved
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
Loading