Skip to content

Commit

Permalink
feat(console): Groups and group user management (#2461)
Browse files Browse the repository at this point in the history
* feat(console): Groups and group user management

* Added group details page

* Updated group member model

* Updated routing & context

* Promisified

* Added invitations in DO

* Mermelated list & list item

* Added group listing page

* Added group page skeleton

* Added pending invites

* Added invite member modal

* Added nice dropdown

* Added migration

* Replaced null with undefined for provider icons

* Added false divving instead of comments

* Removed explicit undefined cast

* Added IdentityNode helper
  • Loading branch information
Cosmin-Parvulescu authored Jul 10, 2023
1 parent e3a24a7 commit 5bd9f8d
Show file tree
Hide file tree
Showing 29 changed files with 1,456 additions and 34 deletions.
26 changes: 21 additions & 5 deletions apps/console/app/components/SiteMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
HiOutlineBookOpen,
HiOutlineExternalLink,
HiOutlineLogout,
HiUserGroup,
} from 'react-icons/hi'
import {
TbScan,
Expand Down Expand Up @@ -340,18 +341,33 @@ function ExternalLinks({ PASSPORT_URL, docsURL }: ExternalLinksProps) {
return (
<div className="mt-2 border-t border-gray-700">
<div className="px-2 p-2 hover:bg-gray-800">
{/* <NavLink
to={'/billing'}
<NavLink
to={'/groups'}
className={({ isActive }) => `${menuItemClass(isActive, false)} `}
>
<TbReceipt2 size={24} className="mr-2" />
<HiUserGroup size={24} className="mr-2" />
<div className="flex flex-row w-full items-center justify-between">
<Text size="sm" weight="medium">
Billing & Invoicing
Groups
</Text>
</div>
</NavLink> */}
</NavLink>
</div>
{false && (
<div className="px-2 p-2 hover:bg-gray-800">
<NavLink
to={'/billing'}
className={({ isActive }) => `${menuItemClass(isActive, false)} `}
>
<TbReceipt2 size={24} className="mr-2" />
<div className="flex flex-row w-full items-center justify-between">
<Text size="sm" weight="medium">
Billing & Invoicing
</Text>
</div>
</NavLink>
</div>
)}
<div className="px-2 p-2 hover:bg-gray-800">
<NavLink
to={PASSPORT_URL}
Expand Down
98 changes: 98 additions & 0 deletions apps/console/app/routes/__layout/groups.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { generateTraceContextHeaders } from '@proofzero/platform-middleware/trace'
import { AccountURN } from '@proofzero/urns/account'
import { getRollupReqFunctionErrorWrapper } from '@proofzero/utils/errors'
import { LoaderFunction, json } from '@remix-run/cloudflare'
import { parseJwt, requireJWT } from '~/utilities/session.server'
import createAccountClient from '@proofzero/platform-clients/account'
import createAddressClient from '@proofzero/platform-clients/address'
import { getAuthzHeaderConditionallyFromToken } from '@proofzero/utils'
import { PlatformAddressURNHeader } from '@proofzero/types/headers'
import { NO_OP_ADDRESS_PLACEHOLDER } from '@proofzero/platform/address/src/constants'
import { AddressURN } from '@proofzero/urns/address'
import { Outlet, useLoaderData } from '@remix-run/react'

type GroupMemberModel = {
URN: AddressURN
iconURL: string
title: string
address: string
joinTimestamp: number
}

type GroupModel = {
URN: string
name: string
members: GroupMemberModel[]
}

type GroupRootLoaderData = {
groups: GroupModel[]
}

export type GroupRootContextData = GroupRootLoaderData

export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper(
async ({ request, context }) => {
const jwt = await requireJWT(request)
const parsedJwt = parseJwt(jwt!)
const accountURN = parsedJwt.sub as AccountURN

const traceHeader = generateTraceContextHeaders(context.traceSpan)

const accountClient = createAccountClient(Account, {
...getAuthzHeaderConditionallyFromToken(jwt),
...traceHeader,
})

const addressClient = createAddressClient(Address, {
[PlatformAddressURNHeader]: NO_OP_ADDRESS_PLACEHOLDER,
...getAuthzHeaderConditionallyFromToken(jwt),
...traceHeader,
})

const groups = await accountClient.listIdentityGroups.query({
accountURN,
})

const mappedGroups = await Promise.all(
groups.map(async (group) => {
const memberMap = group.members
.filter((m) => m.joinTimestamp != null)
.reduce(
(acc, curr) => ({ ...acc, [curr.URN]: curr }),
{} as Record<AddressURN, { URN: AddressURN; joinTimestamp: number }>
)

const memberProfiles = await addressClient.getAddressProfileBatch.query(
group.members.map((m) => m.URN)
)

const memberModels: GroupMemberModel[] = memberProfiles.map(
(profile) => ({
URN: profile.id,
iconURL: profile.icon!,
title: profile.title,
address: profile.address,
joinTimestamp: memberMap[profile.id].joinTimestamp,
})
)

return {
URN: group.URN,
name: group.name,
members: memberModels,
}
})
)

return json<GroupRootLoaderData>({
groups: mappedGroups,
})
}
)

export default () => {
const data = useLoaderData<GroupRootLoaderData>()

return <Outlet context={data} />
}
Loading

0 comments on commit 5bd9f8d

Please sign in to comment.