From 618fba68e0323dd7836bfe4f4c0872eb50515c04 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Wed, 18 Feb 2026 11:48:49 +0530 Subject: [PATCH 1/2] feat: export users page --- .../src/pages/organizations/list/index.tsx | 4 +- web/apps/admin/src/pages/users/UsersPage.tsx | 32 +++ web/apps/admin/src/routes.tsx | 11 +- web/lib/admin/assets/icons/UsersIcon.tsx | 24 ++ web/lib/admin/components/AssignRole.tsx | 213 ++++++++++++++++++ web/lib/admin/index.ts | 1 + web/lib/admin/utils/connect-timestamp.ts | 6 + web/lib/admin/utils/constants.ts | 13 ++ web/lib/admin/views/users/UsersView.tsx | 27 +++ .../admin/views}/users/details/index.ts | 0 .../views}/users/details/layout/index.ts | 0 .../users/details/layout/layout.module.css | 0 .../views}/users/details/layout/layout.tsx | 0 .../details/layout/membership-dropdown.tsx | 4 +- .../users/details/layout/navbar.module.css | 0 .../views}/users/details/layout/navbar.tsx | 2 +- .../details/layout/side-panel-details.tsx | 2 +- .../details/layout/side-panel-membership.tsx | 2 +- .../details/layout/side-panel.module.css | 0 .../users/details/layout/side-panel.tsx | 0 .../users/details/layout/suspend-user.tsx | 0 .../users/details/security/block-user.tsx | 0 .../views}/users/details/security/index.ts | 0 .../details/security/security.module.css | 0 .../users/details/security/security.tsx | 4 +- .../users/details/security/sessions/index.tsx | 2 +- .../sessions/revoke-session-confirm.tsx | 0 .../sessions/revoke-session-final-confirm.tsx | 0 .../security/sessions/session-skeleton.tsx | 0 .../security/sessions/sessions.module.css | 0 .../views}/users/details/user-context.tsx | 0 .../views}/users/details/user-details.tsx | 51 +++-- .../admin/views}/users/list/columns.tsx | 22 +- .../admin/views}/users/list/index.ts | 0 .../views}/users/list/invite-users.module.css | 0 .../admin/views}/users/list/invite-users.tsx | 2 +- .../admin/views}/users/list/list.module.css | 0 .../admin/views}/users/list/list.tsx | 21 +- .../admin/views}/users/list/navbar.tsx | 14 +- .../pages => lib/admin/views}/users/util.ts | 0 40 files changed, 395 insertions(+), 62 deletions(-) create mode 100644 web/apps/admin/src/pages/users/UsersPage.tsx create mode 100644 web/lib/admin/assets/icons/UsersIcon.tsx create mode 100644 web/lib/admin/components/AssignRole.tsx create mode 100644 web/lib/admin/utils/constants.ts create mode 100644 web/lib/admin/views/users/UsersView.tsx rename web/{apps/admin/src/pages => lib/admin/views}/users/details/index.ts (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/layout/index.ts (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/layout/layout.module.css (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/layout/layout.tsx (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/layout/membership-dropdown.tsx (97%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/layout/navbar.module.css (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/layout/navbar.tsx (97%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/layout/side-panel-details.tsx (95%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/layout/side-panel-membership.tsx (96%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/layout/side-panel.module.css (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/layout/side-panel.tsx (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/layout/suspend-user.tsx (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/security/block-user.tsx (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/security/index.ts (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/security/security.module.css (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/security/security.tsx (89%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/security/sessions/index.tsx (98%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/security/sessions/revoke-session-confirm.tsx (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/security/sessions/revoke-session-final-confirm.tsx (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/security/sessions/session-skeleton.tsx (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/security/sessions/sessions.module.css (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/user-context.tsx (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/details/user-details.tsx (52%) rename web/{apps/admin/src/pages => lib/admin/views}/users/list/columns.tsx (80%) rename web/{apps/admin/src/pages => lib/admin/views}/users/list/index.ts (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/list/invite-users.module.css (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/list/invite-users.tsx (99%) rename web/{apps/admin/src/pages => lib/admin/views}/users/list/list.module.css (100%) rename web/{apps/admin/src/pages => lib/admin/views}/users/list/list.tsx (87%) rename web/{apps/admin/src/pages => lib/admin/views}/users/list/navbar.tsx (83%) rename web/{apps/admin/src/pages => lib/admin/views}/users/util.ts (100%) diff --git a/web/apps/admin/src/pages/organizations/list/index.tsx b/web/apps/admin/src/pages/organizations/list/index.tsx index b73edb36e..c76cb87a6 100644 --- a/web/apps/admin/src/pages/organizations/list/index.tsx +++ b/web/apps/admin/src/pages/organizations/list/index.tsx @@ -185,9 +185,7 @@ export const OrganizationList = () => { } rowHeight={48} diff --git a/web/apps/admin/src/pages/users/UsersPage.tsx b/web/apps/admin/src/pages/users/UsersPage.tsx new file mode 100644 index 000000000..3561b8f6e --- /dev/null +++ b/web/apps/admin/src/pages/users/UsersPage.tsx @@ -0,0 +1,32 @@ +import { UsersView } from "@raystack/frontier/admin"; +import { useCallback } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { clients } from "~/connect/clients"; +import { exportCsvFromStream } from "~/utils/helper"; + +const adminClient = clients.admin({ useBinary: true }); + +export function UsersPage() { + const { userId } = useParams(); + const navigate = useNavigate(); + + const onExportUsers = useCallback(async () => { + await exportCsvFromStream(adminClient.exportUsers, {}, "users.csv"); + }, []); + + const onNavigateToUser = useCallback( + (id: string) => { + navigate(`/users/${id}/security`); + }, + [navigate], + ); + + return ( + navigate("/users")} + onExportUsers={onExportUsers} + onNavigateToUser={onNavigateToUser} + /> + ); +} diff --git a/web/apps/admin/src/routes.tsx b/web/apps/admin/src/routes.tsx index 47c5b1190..c2aef33c0 100644 --- a/web/apps/admin/src/routes.tsx +++ b/web/apps/admin/src/routes.tsx @@ -31,9 +31,7 @@ import { OrganizationInvoicesPage } from "./pages/organizations/details/invoices import { OrganizationTokensPage } from "./pages/organizations/details/tokens"; import { OrganizationApisPage } from "./pages/organizations/details/apis"; -import { UsersList } from "./pages/users/list"; -import { UserDetails } from "./pages/users/details"; -import { UserDetailsSecurityPage } from "./pages/users/details/security"; +import { UsersPage } from "./pages/users/UsersPage"; import { InvoicesPage } from "./pages/invoices/InvoicesPage"; import { AuditLogsPage } from "./pages/audit-logs/AuditLogsPage"; @@ -71,10 +69,9 @@ export default memo(function AppRoutes() { } /> } /> - } /> - }> - } /> - } /> + }> + } /> + } /> } /> diff --git a/web/lib/admin/assets/icons/UsersIcon.tsx b/web/lib/admin/assets/icons/UsersIcon.tsx new file mode 100644 index 000000000..d65298bc4 --- /dev/null +++ b/web/lib/admin/assets/icons/UsersIcon.tsx @@ -0,0 +1,24 @@ +import type { SVGProps } from "react"; + +export function UsersIcon(props: SVGProps) { + return ( + + + + + + ); +} + +export default UsersIcon; diff --git a/web/lib/admin/components/AssignRole.tsx b/web/lib/admin/components/AssignRole.tsx new file mode 100644 index 000000000..482c6dabf --- /dev/null +++ b/web/lib/admin/components/AssignRole.tsx @@ -0,0 +1,213 @@ +import { + Button, + Checkbox, + Dialog, + Flex, + Label, + Text, + toast, +} from "@raystack/apsara"; +import { useCallback } from "react"; +import type { + SearchOrganizationUsersResponse_OrganizationUser, + Role, + Policy, +} from "@raystack/proton/frontier"; +import { + FrontierService, + FrontierServiceQueries, + ListPoliciesRequestSchema, + DeletePolicyRequestSchema, + CreatePolicyRequestSchema, +} from "@raystack/proton/frontier"; +import { create } from "@bufbuild/protobuf"; +import { useMutation, useTransport } from "@connectrpc/connect-query"; +import { createClient } from "@connectrpc/connect"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +interface AssignRoleProps { + organizationId: string; + roles: Role[]; + user?: SearchOrganizationUsersResponse_OrganizationUser; + onRoleUpdate: () => void; + onClose: () => void; +} + +const formSchema = z.object({ + roleIds: z.instanceof(Set).refine((set) => set.size > 0, { + message: "At least one role must be selected", + }), +}); + +type FormData = z.infer; + +export const AssignRole = ({ + roles = [], + user, + organizationId, + onRoleUpdate, + onClose, +}: AssignRoleProps) => { + const transport = useTransport(); + + const { + handleSubmit, + watch, + setValue, + formState: { isSubmitting, errors }, + } = useForm({ + defaultValues: { + roleIds: new Set(user?.roleIds || []), + }, + resolver: zodResolver(formSchema), + }); + + const { mutateAsync: deletePolicy } = useMutation( + FrontierServiceQueries.deletePolicy, + ); + + const { mutateAsync: createPolicy } = useMutation( + FrontierServiceQueries.createPolicy, + ); + + const roleIds = watch("roleIds"); + + function onCheckedChange(value: boolean | string, roleId?: string) { + if (!roleId) return; + const currentRoles = new Set(roleIds); + + if (value) { + currentRoles.add(roleId); + } else { + currentRoles.delete(roleId); + } + + setValue("roleIds", currentRoles); + } + + const checkRole = useCallback( + (roleId?: string) => { + if (!roleId) return false; + return roleIds?.has(roleId) || false; + }, + [roleIds], + ); + + const onSubmit = async (data: FormData) => { + try { + const client = createClient(FrontierService, transport); + const policiesResp = await client.listPolicies( + create(ListPoliciesRequestSchema, { + orgId: organizationId, + userId: user?.id, + }), + ); + const policies = policiesResp.policies || []; + + const removedRolesPolicies = policies.filter( + (policy: Policy) => !(policy.roleId && data.roleIds.has(policy.roleId)), + ); + await Promise.all( + removedRolesPolicies.map((policy: Policy) => + deletePolicy( + create(DeletePolicyRequestSchema, { id: policy.id || "" }), + ), + ), + ); + + const resource = `app/organization:${organizationId}`; + const principal = `app/user:${user?.id}`; + + const assignedRolesArr = Array.from(data.roleIds); + await Promise.all( + assignedRolesArr.map((roleId) => + createPolicy( + create(CreatePolicyRequestSchema, { + body: { + roleId, + resource, + principal, + }, + }), + ), + ), + ); + + if (onRoleUpdate) { + onRoleUpdate(); + } + + toast.success("Role assigned successfully"); + } catch (error) { + toast.error("Failed to assign role"); + console.error(error); + } + }; + + return ( + + + + Assign Role + + +
+ + + + Taking this action may result in changes in the role which might + lead to changes in access of the user. + +
+ + {roles.map((role) => { + const htmlId = `role-${role.id}`; + const checked = checkRole(role.id); + return ( + + + onCheckedChange(value, role.id) + } + /> + + + ); + })} + {errors.roleIds && ( + {errors.roleIds.message} + )} + +
+
+
+ + + + + + +
+
+
+ ); +}; diff --git a/web/lib/admin/index.ts b/web/lib/admin/index.ts index d28c42360..8293a00db 100644 --- a/web/lib/admin/index.ts +++ b/web/lib/admin/index.ts @@ -9,6 +9,7 @@ export { default as AdminsView } from "./views/admins"; export { default as PlansView } from "./views/plans"; export { default as WebhooksView } from "./views/webhooks/webhooks"; export { default as PreferencesView } from "./views/preferences/PreferencesView"; +export { default as UsersView } from "./views/users/UsersView"; // utils exports export { diff --git a/web/lib/admin/utils/connect-timestamp.ts b/web/lib/admin/utils/connect-timestamp.ts index f08af0d37..aef04cdb5 100644 --- a/web/lib/admin/utils/connect-timestamp.ts +++ b/web/lib/admin/utils/connect-timestamp.ts @@ -1,10 +1,16 @@ import { timestampDate, type Timestamp } from "@bufbuild/protobuf/wkt"; +import dayjs, { type Dayjs } from "dayjs"; export function timestampToDate(timestamp?: Timestamp): Date | null { if (!timestamp) return null; return timestampDate(timestamp); } +export function timestampToDayjs(timestamp?: Timestamp): Dayjs | null { + const date = timestampToDate(timestamp); + return date ? dayjs(date) : null; +} + /** * Checks if a ConnectRPC Timestamp is the null time (0001-01-01T00:00:00Z) */ diff --git a/web/lib/admin/utils/constants.ts b/web/lib/admin/utils/constants.ts new file mode 100644 index 000000000..0ca14cd03 --- /dev/null +++ b/web/lib/admin/utils/constants.ts @@ -0,0 +1,13 @@ +export const SCOPES = { + ORG: "app/organization", + PROJECT: "app/project", + GROUP: "app/group", +} as const; + +export const DEFAULT_ROLES = { + ORG_MANAGER: "app_organization_manager", + ORG_OWNER: "app_organization_owner", + ORG_BILLING_MANAGER: "app_billing_manager", + ORG_VIEWER: "app_organization_viewer", + PROJECT_VIEWER: "app_project_viewer", +} as const; diff --git a/web/lib/admin/views/users/UsersView.tsx b/web/lib/admin/views/users/UsersView.tsx new file mode 100644 index 000000000..2c016e22d --- /dev/null +++ b/web/lib/admin/views/users/UsersView.tsx @@ -0,0 +1,27 @@ +import { UsersList } from "./list/list"; +import { UserDetailsByUserId } from "./details/user-details"; + +export type UsersViewProps = { + selectedUserId?: string; + onCloseDetail?: () => void; + onExportUsers?: () => Promise; + onNavigateToUser?: (userId: string) => void; +}; + +export default function UsersView({ + selectedUserId, + onCloseDetail, + onExportUsers, + onNavigateToUser, +}: UsersViewProps = {}) { + if (selectedUserId) { + return ; + } + + return ( + + ); +} diff --git a/web/apps/admin/src/pages/users/details/index.ts b/web/lib/admin/views/users/details/index.ts similarity index 100% rename from web/apps/admin/src/pages/users/details/index.ts rename to web/lib/admin/views/users/details/index.ts diff --git a/web/apps/admin/src/pages/users/details/layout/index.ts b/web/lib/admin/views/users/details/layout/index.ts similarity index 100% rename from web/apps/admin/src/pages/users/details/layout/index.ts rename to web/lib/admin/views/users/details/layout/index.ts diff --git a/web/apps/admin/src/pages/users/details/layout/layout.module.css b/web/lib/admin/views/users/details/layout/layout.module.css similarity index 100% rename from web/apps/admin/src/pages/users/details/layout/layout.module.css rename to web/lib/admin/views/users/details/layout/layout.module.css diff --git a/web/apps/admin/src/pages/users/details/layout/layout.tsx b/web/lib/admin/views/users/details/layout/layout.tsx similarity index 100% rename from web/apps/admin/src/pages/users/details/layout/layout.tsx rename to web/lib/admin/views/users/details/layout/layout.tsx diff --git a/web/apps/admin/src/pages/users/details/layout/membership-dropdown.tsx b/web/lib/admin/views/users/details/layout/membership-dropdown.tsx similarity index 97% rename from web/apps/admin/src/pages/users/details/layout/membership-dropdown.tsx rename to web/lib/admin/views/users/details/layout/membership-dropdown.tsx index b997d7d4d..ff74b30a0 100644 --- a/web/apps/admin/src/pages/users/details/layout/membership-dropdown.tsx +++ b/web/lib/admin/views/users/details/layout/membership-dropdown.tsx @@ -11,8 +11,8 @@ import { } from "@raystack/proton/frontier"; import { create } from "@bufbuild/protobuf"; import { useQuery } from "@connectrpc/connect-query"; -import { SCOPES } from "~/utils/constants"; -import { AssignRole } from "~/components/assign-role"; +import { SCOPES } from "../../../../utils/constants"; +import { AssignRole } from "../../../../components/AssignRole"; import { useUser } from "../user-context"; import { SuspendUser } from "./suspend-user"; diff --git a/web/apps/admin/src/pages/users/details/layout/navbar.module.css b/web/lib/admin/views/users/details/layout/navbar.module.css similarity index 100% rename from web/apps/admin/src/pages/users/details/layout/navbar.module.css rename to web/lib/admin/views/users/details/layout/navbar.module.css diff --git a/web/apps/admin/src/pages/users/details/layout/navbar.tsx b/web/lib/admin/views/users/details/layout/navbar.tsx similarity index 97% rename from web/apps/admin/src/pages/users/details/layout/navbar.tsx rename to web/lib/admin/views/users/details/layout/navbar.tsx index da3595ca9..cfcd0f0b3 100644 --- a/web/apps/admin/src/pages/users/details/layout/navbar.tsx +++ b/web/lib/admin/views/users/details/layout/navbar.tsx @@ -8,7 +8,7 @@ import { } from "@raystack/apsara"; import { NavLink } from "react-router-dom"; import { SidebarIcon } from "@raystack/apsara/icons"; -import UserIcon from "~/assets/icons/users.svg?react"; +import UserIcon from "../../../../assets/icons/UsersIcon"; import styles from "./navbar.module.css"; import { getUserName } from "../../util"; import { useUser } from "../user-context"; diff --git a/web/apps/admin/src/pages/users/details/layout/side-panel-details.tsx b/web/lib/admin/views/users/details/layout/side-panel-details.tsx similarity index 95% rename from web/apps/admin/src/pages/users/details/layout/side-panel-details.tsx rename to web/lib/admin/views/users/details/layout/side-panel-details.tsx index 00ea304aa..58805c576 100644 --- a/web/apps/admin/src/pages/users/details/layout/side-panel-details.tsx +++ b/web/lib/admin/views/users/details/layout/side-panel-details.tsx @@ -3,7 +3,7 @@ import { CalendarIcon } from "@radix-ui/react-icons"; import styles from "./side-panel.module.css"; import { UserState, USER_STATES } from "../../util"; import { useUser } from "../user-context"; -import { timestampToDayjs } from "~/utils/connect-timestamp"; +import { timestampToDayjs } from "../../../../utils/connect-timestamp"; export const SidePanelDetails = () => { const { user } = useUser(); diff --git a/web/apps/admin/src/pages/users/details/layout/side-panel-membership.tsx b/web/lib/admin/views/users/details/layout/side-panel-membership.tsx similarity index 96% rename from web/apps/admin/src/pages/users/details/layout/side-panel-membership.tsx rename to web/lib/admin/views/users/details/layout/side-panel-membership.tsx index 5654e4fc7..3dc0831c3 100644 --- a/web/apps/admin/src/pages/users/details/layout/side-panel-membership.tsx +++ b/web/lib/admin/views/users/details/layout/side-panel-membership.tsx @@ -5,7 +5,7 @@ import Skeleton from "react-loading-skeleton"; import { type SearchUserOrganizationsResponse_UserOrganization } from "@raystack/proton/frontier"; import styles from "./side-panel.module.css"; import { MembershipDropdown } from "./membership-dropdown"; -import { timestampToDate, isNullTimestamp } from "~/utils/connect-timestamp"; +import { timestampToDate, isNullTimestamp } from "../../../../utils/connect-timestamp"; interface SidePanelMembershipProps { data?: SearchUserOrganizationsResponse_UserOrganization; diff --git a/web/apps/admin/src/pages/users/details/layout/side-panel.module.css b/web/lib/admin/views/users/details/layout/side-panel.module.css similarity index 100% rename from web/apps/admin/src/pages/users/details/layout/side-panel.module.css rename to web/lib/admin/views/users/details/layout/side-panel.module.css diff --git a/web/apps/admin/src/pages/users/details/layout/side-panel.tsx b/web/lib/admin/views/users/details/layout/side-panel.tsx similarity index 100% rename from web/apps/admin/src/pages/users/details/layout/side-panel.tsx rename to web/lib/admin/views/users/details/layout/side-panel.tsx diff --git a/web/apps/admin/src/pages/users/details/layout/suspend-user.tsx b/web/lib/admin/views/users/details/layout/suspend-user.tsx similarity index 100% rename from web/apps/admin/src/pages/users/details/layout/suspend-user.tsx rename to web/lib/admin/views/users/details/layout/suspend-user.tsx diff --git a/web/apps/admin/src/pages/users/details/security/block-user.tsx b/web/lib/admin/views/users/details/security/block-user.tsx similarity index 100% rename from web/apps/admin/src/pages/users/details/security/block-user.tsx rename to web/lib/admin/views/users/details/security/block-user.tsx diff --git a/web/apps/admin/src/pages/users/details/security/index.ts b/web/lib/admin/views/users/details/security/index.ts similarity index 100% rename from web/apps/admin/src/pages/users/details/security/index.ts rename to web/lib/admin/views/users/details/security/index.ts diff --git a/web/apps/admin/src/pages/users/details/security/security.module.css b/web/lib/admin/views/users/details/security/security.module.css similarity index 100% rename from web/apps/admin/src/pages/users/details/security/security.module.css rename to web/lib/admin/views/users/details/security/security.module.css diff --git a/web/apps/admin/src/pages/users/details/security/security.tsx b/web/lib/admin/views/users/details/security/security.tsx similarity index 89% rename from web/apps/admin/src/pages/users/details/security/security.tsx rename to web/lib/admin/views/users/details/security/security.tsx index 60f6f487d..57598d028 100644 --- a/web/apps/admin/src/pages/users/details/security/security.tsx +++ b/web/lib/admin/views/users/details/security/security.tsx @@ -1,11 +1,11 @@ import { Flex, Separator, Text } from "@raystack/apsara"; -import PageTitle from "~/components/page-title"; +import { PageTitle } from "../../../../components/PageTitle"; import { useUser } from "../user-context"; import { BlockUserDialog } from "./block-user"; import { UserSessions } from "./sessions"; import styles from "./security.module.css"; -export const UserDetailsSecurityPage = () => { +export const UserDetailsSecurityContent = () => { const { user } = useUser(); const title = `Security | ${user?.email} | Users`; diff --git a/web/apps/admin/src/pages/users/details/security/sessions/index.tsx b/web/lib/admin/views/users/details/security/sessions/index.tsx similarity index 98% rename from web/apps/admin/src/pages/users/details/security/sessions/index.tsx rename to web/lib/admin/views/users/details/security/sessions/index.tsx index 1dd5442cb..a8480a43a 100644 --- a/web/apps/admin/src/pages/users/details/security/sessions/index.tsx +++ b/web/lib/admin/views/users/details/security/sessions/index.tsx @@ -8,7 +8,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { AdminServiceQueries, Session } from "@raystack/proton/frontier"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; -import { timestampToDate } from "~/utils/connect-timestamp"; +import { timestampToDate } from "../../../../../utils/connect-timestamp"; import styles from "./sessions.module.css"; /** diff --git a/web/apps/admin/src/pages/users/details/security/sessions/revoke-session-confirm.tsx b/web/lib/admin/views/users/details/security/sessions/revoke-session-confirm.tsx similarity index 100% rename from web/apps/admin/src/pages/users/details/security/sessions/revoke-session-confirm.tsx rename to web/lib/admin/views/users/details/security/sessions/revoke-session-confirm.tsx diff --git a/web/apps/admin/src/pages/users/details/security/sessions/revoke-session-final-confirm.tsx b/web/lib/admin/views/users/details/security/sessions/revoke-session-final-confirm.tsx similarity index 100% rename from web/apps/admin/src/pages/users/details/security/sessions/revoke-session-final-confirm.tsx rename to web/lib/admin/views/users/details/security/sessions/revoke-session-final-confirm.tsx diff --git a/web/apps/admin/src/pages/users/details/security/sessions/session-skeleton.tsx b/web/lib/admin/views/users/details/security/sessions/session-skeleton.tsx similarity index 100% rename from web/apps/admin/src/pages/users/details/security/sessions/session-skeleton.tsx rename to web/lib/admin/views/users/details/security/sessions/session-skeleton.tsx diff --git a/web/apps/admin/src/pages/users/details/security/sessions/sessions.module.css b/web/lib/admin/views/users/details/security/sessions/sessions.module.css similarity index 100% rename from web/apps/admin/src/pages/users/details/security/sessions/sessions.module.css rename to web/lib/admin/views/users/details/security/sessions/sessions.module.css diff --git a/web/apps/admin/src/pages/users/details/user-context.tsx b/web/lib/admin/views/users/details/user-context.tsx similarity index 100% rename from web/apps/admin/src/pages/users/details/user-context.tsx rename to web/lib/admin/views/users/details/user-context.tsx diff --git a/web/apps/admin/src/pages/users/details/user-details.tsx b/web/lib/admin/views/users/details/user-details.tsx similarity index 52% rename from web/apps/admin/src/pages/users/details/user-details.tsx rename to web/lib/admin/views/users/details/user-details.tsx index 92a522046..cb2b1df19 100644 --- a/web/apps/admin/src/pages/users/details/user-details.tsx +++ b/web/lib/admin/views/users/details/user-details.tsx @@ -1,16 +1,33 @@ -import { Outlet, useParams } from "react-router-dom"; -import { Flex, EmptyState } from "@raystack/apsara"; -import LoadingState from "~/components/states/Loading"; -import PageTitle from "~/components/page-title"; -import UserIcon from "~/assets/icons/users.svg?react"; +import { Flex, EmptyState, Spinner } from "@raystack/apsara"; +import { PageTitle } from "../../../components/PageTitle"; +import UserIcon from "../../../assets/icons/UsersIcon"; import { UserDetailsLayout } from "./layout"; import { UserProvider } from "./user-context"; import { useQuery } from "@connectrpc/connect-query"; +import type { User } from "@raystack/proton/frontier"; import { AdminServiceQueries } from "@raystack/proton/frontier"; +import { UserDetailsSecurityContent } from "./security/security"; -export const UserDetails = () => { - const { userId } = useParams(); +interface UserDetailContentProps { + user: User; + refetch: () => void; +} +export const UserDetailContent = ({ user, refetch }: UserDetailContentProps) => { + return ( + + + + + + ); +}; + +interface UserDetailsByUserIdProps { + userId: string; +} + +export const UserDetailsByUserId = ({ userId }: UserDetailsByUserIdProps) => { const { data, isLoading, refetch } = useQuery( AdminServiceQueries.searchUsers, { query: { search: userId } }, @@ -25,15 +42,20 @@ export const UserDetails = () => { const user = data?.users?.[0]; if (isLoading) { - return ; + return ( + + + + ); } - if (!user?.id) + if (!user?.id) { return ( + justify="center" + > } @@ -42,12 +64,7 @@ export const UserDetails = () => { /> ); + } - return ( - - - - - - ); + return ; }; diff --git a/web/apps/admin/src/pages/users/list/columns.tsx b/web/lib/admin/views/users/list/columns.tsx similarity index 80% rename from web/apps/admin/src/pages/users/list/columns.tsx rename to web/lib/admin/views/users/list/columns.tsx index 40c440202..122ea7abb 100644 --- a/web/apps/admin/src/pages/users/list/columns.tsx +++ b/web/lib/admin/views/users/list/columns.tsx @@ -6,6 +6,7 @@ import { getAvatarColor, Text, } from "@raystack/apsara"; +import { Link } from "react-router-dom"; import dayjs from "dayjs"; import styles from "./list.module.css"; import { getUserName, USER_STATES, UserState } from "../util"; @@ -14,7 +15,7 @@ import { isNullTimestamp, TimeStamp, timestampToDate, -} from "~/utils/connect-timestamp"; +} from "../../../utils/connect-timestamp"; interface getColumnsOptions { groupCountMap: Record>; @@ -34,15 +35,18 @@ export const getColumns = ({ cell: ({ row }) => { const avatarColor = getAvatarColor(row?.original?.id || ""); const name = getUserName(row.original); + const userId = row.original.id; return ( - - - {name} - + + + + {name} + + ); }, enableColumnFilter: true, diff --git a/web/apps/admin/src/pages/users/list/index.ts b/web/lib/admin/views/users/list/index.ts similarity index 100% rename from web/apps/admin/src/pages/users/list/index.ts rename to web/lib/admin/views/users/list/index.ts diff --git a/web/apps/admin/src/pages/users/list/invite-users.module.css b/web/lib/admin/views/users/list/invite-users.module.css similarity index 100% rename from web/apps/admin/src/pages/users/list/invite-users.module.css rename to web/lib/admin/views/users/list/invite-users.module.css diff --git a/web/apps/admin/src/pages/users/list/invite-users.tsx b/web/lib/admin/views/users/list/invite-users.tsx similarity index 99% rename from web/apps/admin/src/pages/users/list/invite-users.tsx rename to web/lib/admin/views/users/list/invite-users.tsx index 8cd9da965..306e049b8 100644 --- a/web/apps/admin/src/pages/users/list/invite-users.tsx +++ b/web/lib/admin/views/users/list/invite-users.tsx @@ -13,7 +13,7 @@ import { PlusIcon } from "@radix-ui/react-icons"; import * as z from "zod"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { SCOPES, DEFAULT_ROLES } from "~/utils/constants"; +import { SCOPES, DEFAULT_ROLES } from "../../../utils/constants"; import styles from "./invite-users.module.css"; import Skeleton from "react-loading-skeleton"; import { useMutation, useQuery } from "@connectrpc/connect-query"; diff --git a/web/apps/admin/src/pages/users/list/list.module.css b/web/lib/admin/views/users/list/list.module.css similarity index 100% rename from web/apps/admin/src/pages/users/list/list.module.css rename to web/lib/admin/views/users/list/list.module.css diff --git a/web/apps/admin/src/pages/users/list/list.tsx b/web/lib/admin/views/users/list/list.tsx similarity index 87% rename from web/apps/admin/src/pages/users/list/list.tsx rename to web/lib/admin/views/users/list/list.tsx index 05d6d3a5b..96ce9516c 100644 --- a/web/apps/admin/src/pages/users/list/list.tsx +++ b/web/lib/admin/views/users/list/list.tsx @@ -3,17 +3,16 @@ import type { DataTableQuery, DataTableSort } from "@raystack/apsara"; import Navbar from "./navbar"; import styles from "./list.module.css"; import { getColumns } from "./columns"; -import { useNavigate } from "react-router-dom"; -import PageTitle from "~/components/page-title"; -import UserIcon from "~/assets/icons/users.svg?react"; +import { PageTitle } from "../../../components/PageTitle"; +import UserIcon from "../../../assets/icons/UsersIcon"; import { useInfiniteQuery } from "@connectrpc/connect-query"; import { AdminServiceQueries, type User } from "@raystack/proton/frontier"; import { getConnectNextPageParam, getGroupCountMapFromFirstPage, DEFAULT_PAGE_SIZE, - transformDataTableQueryToRQLRequest, -} from "@raystack/frontier/admin"; +} from "../../../utils/connect-pagination"; +import { transformDataTableQueryToRQLRequest } from "../../../utils/transform-query"; import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; import { useDebouncedState } from "@raystack/apsara/hooks"; @@ -37,8 +36,12 @@ const INITIAL_QUERY: DataTableQuery = { limit: DEFAULT_PAGE_SIZE, }; -export const UsersList = () => { - const navigate = useNavigate(); +interface UsersListProps { + onExportUsers?: () => Promise; + onNavigateToUser?: (userId: string) => void; +} + +export const UsersList = ({ onExportUsers, onNavigateToUser }: UsersListProps) => { const [tableQuery, setTableQuery] = useDebouncedState( INITIAL_QUERY, 200, @@ -100,7 +103,7 @@ export const UsersList = () => { const loading = isLoading || isFetchingNextPage; const onRowClick = (row: User) => { - navigate(`/users/${row.id}`); + onNavigateToUser?.(row.id); }; if (isError) { @@ -137,7 +140,7 @@ export const UsersList = () => { onLoadMore={handleLoadMore} onRowClick={onRowClick}> - + Promise; } -const Navbar = ({ searchQuery }: NavbarProps) => { +const Navbar = ({ searchQuery, onExportUsers }: NavbarProps) => { const [showSearch, setShowSearch] = useState(searchQuery ? true : false); const [isDownloading, setIsDownloading] = useState(false); @@ -36,9 +33,10 @@ const Navbar = ({ searchQuery }: NavbarProps) => { } async function onDownloadClick() { + if (!onExportUsers) return; try { setIsDownloading(true); - await exportCsvFromStream(adminClient.exportUsers, {}, "users.csv"); + await onExportUsers(); } catch (error) { console.error(error); } finally { @@ -79,7 +77,7 @@ const Navbar = ({ searchQuery }: NavbarProps) => { aria-label="Download" data-test-id="admin-download-users-list-btn" onClick={onDownloadClick} - disabled={isDownloading} + disabled={isDownloading || !onExportUsers} > {isDownloading ? : } diff --git a/web/apps/admin/src/pages/users/util.ts b/web/lib/admin/views/users/util.ts similarity index 100% rename from web/apps/admin/src/pages/users/util.ts rename to web/lib/admin/views/users/util.ts From d591f5586ae5bb533e163dcdfdb51a4c52426ee9 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 19 Feb 2026 08:21:51 +0530 Subject: [PATCH 2/2] fix: add additional class names for DataTable styling in organization list --- web/apps/admin/src/pages/organizations/list/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/apps/admin/src/pages/organizations/list/index.tsx b/web/apps/admin/src/pages/organizations/list/index.tsx index c76cb87a6..b73edb36e 100644 --- a/web/apps/admin/src/pages/organizations/list/index.tsx +++ b/web/apps/admin/src/pages/organizations/list/index.tsx @@ -185,7 +185,9 @@ export const OrganizationList = () => { } rowHeight={48}