-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4260 from bcgov/feat/3972
- Loading branch information
Showing
28 changed files
with
771 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { CommonComponentsOptions, ProjectStatus } from '@prisma/client'; | ||
import _sum from 'lodash-es/sum'; | ||
import { GlobalPermissions } from '@/constants'; | ||
import createApiHandler from '@/core/api-handler'; | ||
import prisma from '@/core/prisma'; | ||
import { CsvResponse } from '@/core/responses'; | ||
|
||
const apiHandler = createApiHandler({ | ||
permissions: [GlobalPermissions.ViewGeneralAnalytics], | ||
}); | ||
|
||
function commonComponentToText(item: CommonComponentsOptions) { | ||
if (item.implemented) return 'implemented'; | ||
if (item.planningToUse) return 'planning to use'; | ||
return ''; | ||
} | ||
export const GET = apiHandler(async () => { | ||
const products = await prisma.privateCloudProject.findMany({ | ||
where: { status: ProjectStatus.ACTIVE }, | ||
select: { | ||
name: true, | ||
licencePlate: true, | ||
commonComponents: true, | ||
}, | ||
}); | ||
|
||
return CsvResponse( | ||
products.map((row) => { | ||
return { | ||
'Product name': row.name, | ||
'Licence Plate': row.licencePlate, | ||
'Address and Geolocation': commonComponentToText(row.commonComponents.addressAndGeolocation), | ||
'Workflow Management': commonComponentToText(row.commonComponents.workflowManagement), | ||
'Form Design and Submission': commonComponentToText(row.commonComponents.formDesignAndSubmission), | ||
'Identity management': commonComponentToText(row.commonComponents.identityManagement), | ||
'Payment services': commonComponentToText(row.commonComponents.paymentServices), | ||
'Document Management': commonComponentToText(row.commonComponents.documentManagement), | ||
'End user notification and subscription service': commonComponentToText( | ||
row.commonComponents.endUserNotificationAndSubscription, | ||
), | ||
Publishing: commonComponentToText(row.commonComponents.publishing), | ||
'Business Intelligence Dashboard and Metrics reporting': commonComponentToText( | ||
row.commonComponents.businessIntelligence, | ||
), | ||
Other: row.commonComponents.other, | ||
}; | ||
}), | ||
'common-components.csv', | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { Session } from 'next-auth'; | ||
import { AUTH_RESOURCE } from '@/config'; | ||
import { OkResponse, BadRequestResponse } from '@/core/responses'; | ||
import { listUsers, listUsersByRole } from '@/services/keycloak/app-realm'; | ||
|
||
export default async function listOp({ session }: { session: Session }) { | ||
const roles = await listUsersByRole('a'); | ||
|
||
return OkResponse(roles); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { z } from 'zod'; | ||
import { GlobalRole } from '@/constants'; | ||
import createApiHandler from '@/core/api-handler'; | ||
import listOp from './_operations/list'; | ||
|
||
export const GET = createApiHandler({ | ||
// roles: [GlobalRole.Admin], | ||
})(async ({ session }) => { | ||
const res = await listOp({ session }); | ||
return res; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import _isString from 'lodash-es/isString'; | ||
import { string, z } from 'zod'; | ||
import { GlobalRole } from '@/constants'; | ||
import createApiHandler from '@/core/api-handler'; | ||
import prisma from '@/core/prisma'; | ||
import { OkResponse } from '@/core/responses'; | ||
import { prepareUserData } from '@/services/db'; | ||
import { listUsersByEmail } from '@/services/msgraph'; | ||
|
||
const userSearchBodySchema = z.object({ | ||
email: z.string().max(40), | ||
}); | ||
|
||
export const POST = createApiHandler({ | ||
roles: [GlobalRole.User], | ||
validations: { body: userSearchBodySchema }, | ||
})(async ({ session, body }) => { | ||
const { email } = body; | ||
if (email.length < 3) { | ||
return OkResponse({ data: [], totalCount: 0 }); | ||
} | ||
|
||
const users = await listUsersByEmail(email); | ||
|
||
const dbUsers = await Promise.all( | ||
users.map(async (user) => { | ||
const data = await prepareUserData(user); | ||
// The upsert method returns { count: x } when updating data instead of the document. | ||
// Related issue: https://github.com/prisma/prisma/issues/10935 | ||
await prisma.user.upsert({ | ||
where: { email: data.email }, | ||
update: data, | ||
create: data, | ||
}); | ||
|
||
return prisma.user.findUnique({ | ||
where: { email: data.email }, | ||
select: { | ||
id: true, | ||
firstName: true, | ||
lastName: true, | ||
email: true, | ||
upn: true, | ||
idir: true, | ||
officeLocation: true, | ||
jobTitle: true, | ||
image: true, | ||
ministry: true, | ||
archived: true, | ||
createdAt: true, | ||
updatedAt: true, | ||
lastSeen: true, | ||
}, | ||
}); | ||
}), | ||
); | ||
|
||
return OkResponse({ data: dbUsers, totalCount: users.length }); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,59 +1,63 @@ | ||
import { Prisma } from '@prisma/client'; | ||
import _compact from 'lodash-es/compact'; | ||
import _isString from 'lodash-es/isString'; | ||
import { string, z } from 'zod'; | ||
import { GlobalRole } from '@/constants'; | ||
import { GlobalPermissions, GlobalRole } from '@/constants'; | ||
import createApiHandler from '@/core/api-handler'; | ||
import prisma from '@/core/prisma'; | ||
import { OkResponse } from '@/core/responses'; | ||
import { prepareUserData } from '@/services/db'; | ||
import { listUsersByEmail } from '@/services/msgraph'; | ||
|
||
const userSearchBodySchema = z.object({ | ||
email: z.string().max(40), | ||
}); | ||
import { searchUsers } from '@/services/db'; | ||
import { listUsersByRoles, findUserByEmail, getKcAdminClient } from '@/services/keycloak/app-realm'; | ||
import { userSearchBodySchema } from '@/validation-schemas'; | ||
|
||
export const POST = createApiHandler({ | ||
roles: [GlobalRole.User], | ||
permissions: [GlobalPermissions.ViewUsers], | ||
validations: { body: userSearchBodySchema }, | ||
})(async ({ session, body }) => { | ||
const { email } = body; | ||
if (email.length < 3) { | ||
return OkResponse({ data: [], totalCount: 0 }); | ||
})(async ({ body }) => { | ||
const kcAdminClient = await getKcAdminClient(); | ||
|
||
let roleEmails: string[] = []; | ||
|
||
if (body.roles.length > 0) { | ||
const roleKcUsers = await listUsersByRoles(body.roles, kcAdminClient); | ||
roleEmails = _compact(roleKcUsers.map((user) => user.email?.toLocaleLowerCase())); | ||
if (roleEmails.length === 0) return OkResponse({ data: [], totalCount: 0 }); | ||
} | ||
|
||
const users = await listUsersByEmail(email); | ||
|
||
const dbUsers = await Promise.all( | ||
users.map(async (user) => { | ||
const data = await prepareUserData(user); | ||
// The upsert method returns { count: x } when updating data instead of the document. | ||
// Related issue: https://github.com/prisma/prisma/issues/10935 | ||
await prisma.user.upsert({ | ||
where: { email: data.email }, | ||
update: data, | ||
create: data, | ||
}); | ||
|
||
return prisma.user.findUnique({ | ||
where: { email: data.email }, | ||
select: { | ||
id: true, | ||
firstName: true, | ||
lastName: true, | ||
email: true, | ||
upn: true, | ||
idir: true, | ||
officeLocation: true, | ||
jobTitle: true, | ||
image: true, | ||
ministry: true, | ||
archived: true, | ||
createdAt: true, | ||
updatedAt: true, | ||
lastSeen: true, | ||
}, | ||
}); | ||
const result = await searchUsers({ ...body, extraFilter: roleEmails.length ? { email: { in: roleEmails } } : {} }); | ||
|
||
const kcProfiles = await Promise.all(result.data.map((v) => findUserByEmail(v.email, kcAdminClient))); | ||
|
||
result.data = await Promise.all( | ||
result.data.map(async (user, index) => { | ||
const [privateProducts, publicProducts] = await Promise.all([ | ||
prisma.privateCloudProject.findMany({ | ||
where: { | ||
OR: [ | ||
{ projectOwnerId: user.id }, | ||
{ primaryTechnicalLeadId: user.id }, | ||
{ secondaryTechnicalLeadId: user.id }, | ||
{ members: { some: { userId: user.id } } }, | ||
], | ||
}, | ||
select: { licencePlate: true, name: true }, | ||
}), | ||
prisma.publicCloudProject.findMany({ | ||
where: { | ||
OR: [ | ||
{ projectOwnerId: user.id }, | ||
{ primaryTechnicalLeadId: user.id }, | ||
{ secondaryTechnicalLeadId: user.id }, | ||
{ expenseAuthorityId: user.id }, | ||
{ members: { some: { userId: user.id } } }, | ||
], | ||
}, | ||
select: { licencePlate: true, name: true }, | ||
}), | ||
]); | ||
|
||
return { ...user, privateProducts, publicProducts, roles: kcProfiles[index]?.authRoleNames ?? [] }; | ||
}), | ||
); | ||
|
||
return OkResponse({ data: dbUsers, totalCount: users.length }); | ||
return OkResponse(result); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { Button, LoadingOverlay, Box } from '@mantine/core'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
import { useSnapshot } from 'valtio'; | ||
import FormMultiSelect from '@/components/generic/select/FormMultiSelect'; | ||
import { listKeycloakAuthRoles } from '@/services/backend/keycloak'; | ||
import { pageState } from './state'; | ||
|
||
export default function FilterPanel() { | ||
const pageSnapshot = useSnapshot(pageState); | ||
const { data: authRoles, isFetching: isAuthRolesFetching } = useQuery({ | ||
queryKey: ['roles'], | ||
queryFn: () => listKeycloakAuthRoles(), | ||
}); | ||
|
||
const authRoleNames = (authRoles || []).map((role) => role.name ?? '').sort(); | ||
|
||
return ( | ||
<Box pos="relative"> | ||
<LoadingOverlay | ||
visible={!authRoles || isAuthRolesFetching} | ||
zIndex={1000} | ||
overlayProps={{ radius: 'sm', blur: 2 }} | ||
loaderProps={{ color: 'pink', type: 'bars' }} | ||
/> | ||
<div className="grid grid-cols-1 gap-y-2 md:grid-cols-12 md:gap-x-3"> | ||
<div className="col-span-12"> | ||
<FormMultiSelect | ||
name="roles" | ||
label="Global Roles" | ||
value={pageSnapshot.roles ?? []} | ||
data={authRoleNames} | ||
onChange={(value) => { | ||
pageState.roles = value; | ||
pageState.page = 1; | ||
}} | ||
classNames={{ wrapper: '' }} | ||
/> | ||
<div className="text-right"> | ||
<Button | ||
color="primary" | ||
size="compact-md" | ||
className="mt-1" | ||
onClick={() => { | ||
pageState.roles = authRoleNames; | ||
pageState.page = 1; | ||
}} | ||
> | ||
Select All | ||
</Button> | ||
</div> | ||
</div> | ||
</div> | ||
</Box> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { HoverCard, Group } from '@mantine/core'; | ||
import { useDisclosure } from '@mantine/hooks'; | ||
import ExternalLink from '@/components/generic/button/ExternalLink'; | ||
import { cn } from '@/utils'; | ||
|
||
export default function PrivateCloudProductsCard({ | ||
products = [], | ||
className = '', | ||
context, | ||
children, | ||
}: { | ||
products: { name: string; licencePlate: string }[]; | ||
className?: string; | ||
context: 'private-cloud' | 'public-cloud'; | ||
children: React.ReactNode; | ||
}) { | ||
const [opened, { close, open }] = useDisclosure(false); | ||
|
||
const title = context === 'private-cloud' ? 'Private Cloud Products' : 'Public Cloud Products'; | ||
|
||
const emptyMessage = | ||
context === 'private-cloud' ? 'No private cloud products associated.' : 'No public cloud products associated.'; | ||
|
||
const baseUrl = `/${context}/products/`; | ||
|
||
return ( | ||
<HoverCard shadow="md" position="top"> | ||
<HoverCard.Target> | ||
<div className={cn('cursor-help inline-block', className)} onMouseEnter={open} onMouseLeave={close}> | ||
{children} | ||
</div> | ||
</HoverCard.Target> | ||
<HoverCard.Dropdown> | ||
<div className="overflow-y-auto max-h-80"> | ||
{products.length === 0 ? ( | ||
<div className="italic text-sm">{emptyMessage}</div> | ||
) : ( | ||
<> | ||
<div className="mb-2 underline">{title}</div> | ||
{products.map((product) => { | ||
return ( | ||
<div | ||
key={product.licencePlate} | ||
className="hover:bg-gray-100 transition-colors duration-200 grid grid-cols-5 gap-4 px-2 py-1 text-sm" | ||
> | ||
<div className="col-span-4"> | ||
<div> | ||
<span className="font-semibold">{product.name}</span> | ||
</div> | ||
</div> | ||
<div className="col-span-1 text-right"> | ||
<ExternalLink href={`${baseUrl}/${product.licencePlate}/edit`} /> | ||
</div> | ||
</div> | ||
); | ||
})} | ||
</> | ||
)} | ||
</div> | ||
</HoverCard.Dropdown> | ||
</HoverCard> | ||
); | ||
} |
Oops, something went wrong.