From 07e6436f4f81f0ff131197501cd01039d25c0b3c Mon Sep 17 00:00:00 2001 From: Timofei Ponomarev Date: Mon, 27 Jan 2025 17:07:39 +0100 Subject: [PATCH 1/2] refactor: rename dialog hooks and reorganize file structure - Rename dialog hooks to use kebab-case filenames - Remove nested index files for dialog hooks - Simplify export structure for dialog hooks fix: standardize dialog content width and error text classes - Remove `w-full` class from Dialog.Content components - Reorder error text classes for consistency across user management dialogs fix: adjust user dialog UI details - Modify info icon positioning in create user dialog - Reorder error text classes for consistency - Use useEffect to reset form values in edit user dialog refactor: remove form components from user management dialogs - Inline dialog form logic directly into dialog components - Remove separate form components for create, edit, delete, reset password, and remove admin dialogs - Simplify dialog structure by consolidating form and dialog logic - Update locales to support new dialog translations fixes dialogs width decapitalize fix cancels buttons refactor: standardize import paths in user management components - Replace relative imports with absolute paths using @/ prefix - Maintain relative imports in index.ts files - Update import paths in dialog components and providers added locales fixes added empty state added error state added skeleton and nodata refactor dialogs added state provider caption props added no search results state paddings refactor remove unnecesary props drilling remove classes sorting refactor remove unnecessary props drilling fix-margins first versions of modals replace text with span change eslint rule due to conflict between prettier and eslint, prettier already sort classnames Adds promisifyMutation helper to simplify mutation handling Updates user management handlers to use the helper Adds proper typing for handlers using IDataHandlers interface Removes duplicate type definitions added search query make pagination using usePaginationQueryStateWithStore fixes code-review: remove onErrors callbacks fix enter fix pathes refactoring dialogs refactoring types commit: refactor: use store provider for user management hooks This commit refactors the user management page to use a centralized store provider by: Creating UserManagementStoreProvider to manage shared hooks Removing hook props drilling through components Accessing hooks via useUserManagementStore hook in child components Splitting UserManagementPage into container and content components This change improves code organization and reduces prop drilling while maintaining existing functionality. commit: refactor: move dialog context to providers directory This commit reorganizes the dialog-related code by: Moving DialogsProvider from context/ to providers/ directory Updating import paths across components to use the new location 3. Removing unused context files and types Consolidating dialog-related code for better maintainability This is a structural change that improves code organization while maintaining existing functionality. remove redundant index from dialog commit: refactor: migrate dialog state management to UI layer The commit refactors the user management dialog state handling by: Moving dialog state management from container to UI layer using DialogsProvider Converting mutation handlers to return Promises for proper async handling Removing redundant dialog state management code from container Consolidating error and loading states into dedicated objects Moving EActiveTab enum to UI package for better organization This change improves separation of concerns by keeping UI state management in the UI layer while maintaining the same functionality. refactor: reorganize validation schemas and types - Move Zod schemas to dedicated schema files - Update type definitions to use schema inference - Fix import paths to use absolute imports - Remove duplicate schema definitions from components - Centralize types in dedicated type files refactor(user-management): reorganize dialogs structure - Move all dialogs to dedicated folders with proper structure - Add types and index files for each dialog - Update imports and exports - Add new translations for empty state - Improve code organization in UserManagementPage first version of filters and sorting remove unnecessary fragment added tabs fixes after design review --- .eslintrc.json | 2 - .../user-management-container.tsx | 195 +++++++----------- .../utils/promisify-mutation.ts | 15 ++ packages/ui/locales/en/views.json | 75 +++++++ packages/ui/locales/es/views.json | 45 ++++ packages/ui/locales/fr/views.json | 45 ++++ .../components/admin-dialog.tsx | 57 ----- .../components/create-user-dialog.tsx | 145 ------------- .../components/delete-user-dialog.tsx | 46 ----- .../components/create-user/create-user.tsx | 87 ++++++++ .../dialogs/components/create-user/index.ts | 2 + .../dialogs/components/create-user/schemas.ts | 7 + .../dialogs/components/create-user/types.ts | 15 ++ .../components/delete-user/delete-user.tsx | 63 ++++++ .../dialogs/components/delete-user/index.ts | 2 + .../dialogs/components/delete-user/types.ts | 10 + .../components/edit-user/edit-user.tsx | 106 ++++++++++ .../dialogs/components/edit-user/index.ts | 2 + .../dialogs/components/edit-user/schemas.ts | 7 + .../dialogs/components/edit-user/types.ts | 15 ++ .../dialogs/components/remove-admin/index.ts | 2 + .../components/remove-admin/remove-admin.tsx | 71 +++++++ .../dialogs/components/remove-admin/types.ts | 10 + .../components/reset-password/index.ts | 2 + .../reset-password/reset-password.tsx | 76 +++++++ .../components/reset-password/types.ts | 10 + .../components/dialogs/dialogs.tsx | 45 ++++ .../components/dialogs/hooks/index.ts | 2 + .../dialogs/hooks/use-dialog-data.ts | 40 ++++ .../dialogs/hooks/use-dialog-handlers.ts | 46 +++++ .../components/dialogs/index.ts | 2 + .../components/dialogs/types.ts | 15 ++ .../components/edit-user-dialog.tsx | 119 ----------- .../components/empty-state/empty-state.tsx | 31 +++ .../components/empty-state/index.ts | 1 + .../views/user-management/components/index.ts | 1 + .../user-management/components/index.tsx | 5 - .../views/user-management/components/main.tsx | 17 ++ .../page-components/actions/actions.tsx | 33 +++ .../page-components/actions/index.ts | 1 + .../components/error-state/error-state.tsx | 33 +++ .../components/error-state/index.ts | 1 + .../components/no-search-results/index.ts | 1 + .../no-search-results/no-search-results.tsx | 29 +++ .../content/components/users-list/index.ts | 1 + .../components/users-list/users-list.tsx | 111 ++++++++++ .../page-components/content/content.tsx | 43 ++++ .../page-components/content/index.ts | 2 + .../page-components/content/types.ts | 5 + .../components/reset-password-dialog.tsx | 71 ------- .../user-management/components/users-list.tsx | 95 --------- .../ui/src/views/user-management/index.ts | 3 +- .../dialogs-provider/dialogs-provider.tsx | 34 +++ .../dialogs-provider/hooks/use-dialogs.ts | 13 ++ .../providers/dialogs-provider/index.ts | 3 + .../providers/dialogs-provider/types.ts | 11 + .../search-provider/hooks/use-search.ts | 13 ++ .../providers/search-provider/index.ts | 3 + .../search-provider/search-provider.tsx | 30 +++ .../providers/search-provider/types.ts | 12 ++ .../state-provider/hooks/use-states.ts | 13 ++ .../providers/state-provider/index.ts | 3 + .../state-provider/state-provider.tsx | 18 ++ .../providers/state-provider/types.ts | 26 +++ .../store-provider/hooks/use-store.ts | 13 ++ .../providers/store-provider/index.ts | 3 + .../store-provider/store-provider.tsx | 15 ++ .../providers/store-provider/types.ts | 27 +++ .../ui/src/views/user-management/types.ts | 71 ------- .../user-management/types/data-handlers.ts | 19 ++ .../src/views/user-management/types/index.ts | 2 + .../src/views/user-management/types/main.ts | 25 +++ .../user-management/user-management-page.tsx | 62 +++--- 73 files changed, 1508 insertions(+), 773 deletions(-) create mode 100644 apps/gitness/src/pages-v2/user-management/utils/promisify-mutation.ts delete mode 100644 packages/ui/src/views/user-management/components/admin-dialog.tsx delete mode 100644 packages/ui/src/views/user-management/components/create-user-dialog.tsx delete mode 100644 packages/ui/src/views/user-management/components/delete-user-dialog.tsx create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/create-user/create-user.tsx create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/create-user/index.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/create-user/schemas.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/create-user/types.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/delete-user/delete-user.tsx create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/delete-user/index.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/delete-user/types.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/edit-user/edit-user.tsx create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/edit-user/index.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/edit-user/schemas.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/edit-user/types.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/remove-admin/index.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/remove-admin/remove-admin.tsx create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/remove-admin/types.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/reset-password/index.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/reset-password/reset-password.tsx create mode 100644 packages/ui/src/views/user-management/components/dialogs/components/reset-password/types.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/dialogs.tsx create mode 100644 packages/ui/src/views/user-management/components/dialogs/hooks/index.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/hooks/use-dialog-data.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/hooks/use-dialog-handlers.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/index.ts create mode 100644 packages/ui/src/views/user-management/components/dialogs/types.ts delete mode 100644 packages/ui/src/views/user-management/components/edit-user-dialog.tsx create mode 100644 packages/ui/src/views/user-management/components/empty-state/empty-state.tsx create mode 100644 packages/ui/src/views/user-management/components/empty-state/index.ts create mode 100644 packages/ui/src/views/user-management/components/index.ts delete mode 100644 packages/ui/src/views/user-management/components/index.tsx create mode 100644 packages/ui/src/views/user-management/components/main.tsx create mode 100644 packages/ui/src/views/user-management/components/page-components/actions/actions.tsx create mode 100644 packages/ui/src/views/user-management/components/page-components/actions/index.ts create mode 100644 packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/error-state.tsx create mode 100644 packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/index.ts create mode 100644 packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/index.ts create mode 100644 packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/no-search-results.tsx create mode 100644 packages/ui/src/views/user-management/components/page-components/content/components/users-list/index.ts create mode 100644 packages/ui/src/views/user-management/components/page-components/content/components/users-list/users-list.tsx create mode 100644 packages/ui/src/views/user-management/components/page-components/content/content.tsx create mode 100644 packages/ui/src/views/user-management/components/page-components/content/index.ts create mode 100644 packages/ui/src/views/user-management/components/page-components/content/types.ts delete mode 100644 packages/ui/src/views/user-management/components/reset-password-dialog.tsx delete mode 100644 packages/ui/src/views/user-management/components/users-list.tsx create mode 100644 packages/ui/src/views/user-management/providers/dialogs-provider/dialogs-provider.tsx create mode 100644 packages/ui/src/views/user-management/providers/dialogs-provider/hooks/use-dialogs.ts create mode 100644 packages/ui/src/views/user-management/providers/dialogs-provider/index.ts create mode 100644 packages/ui/src/views/user-management/providers/dialogs-provider/types.ts create mode 100644 packages/ui/src/views/user-management/providers/search-provider/hooks/use-search.ts create mode 100644 packages/ui/src/views/user-management/providers/search-provider/index.ts create mode 100644 packages/ui/src/views/user-management/providers/search-provider/search-provider.tsx create mode 100644 packages/ui/src/views/user-management/providers/search-provider/types.ts create mode 100644 packages/ui/src/views/user-management/providers/state-provider/hooks/use-states.ts create mode 100644 packages/ui/src/views/user-management/providers/state-provider/index.ts create mode 100644 packages/ui/src/views/user-management/providers/state-provider/state-provider.tsx create mode 100644 packages/ui/src/views/user-management/providers/state-provider/types.ts create mode 100644 packages/ui/src/views/user-management/providers/store-provider/hooks/use-store.ts create mode 100644 packages/ui/src/views/user-management/providers/store-provider/index.ts create mode 100644 packages/ui/src/views/user-management/providers/store-provider/store-provider.tsx create mode 100644 packages/ui/src/views/user-management/providers/store-provider/types.ts delete mode 100644 packages/ui/src/views/user-management/types.ts create mode 100644 packages/ui/src/views/user-management/types/data-handlers.ts create mode 100644 packages/ui/src/views/user-management/types/index.ts create mode 100644 packages/ui/src/views/user-management/types/main.ts diff --git a/.eslintrc.json b/.eslintrc.json index e3a1a4bd52..8bc6adebaa 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -43,10 +43,8 @@ "import/no-unresolved": "off", "react/prop-types": "off", "react/react-in-jsx-scope": "off", - "jsx-a11y/click-events-have-key-events": "warn", "jsx-a11y/no-autofocus": "off", - "@typescript-eslint/no-unused-vars": [ "error", { diff --git a/apps/gitness/src/pages-v2/user-management/user-management-container.tsx b/apps/gitness/src/pages-v2/user-management/user-management-container.tsx index 29cdf39146..92f735ef87 100644 --- a/apps/gitness/src/pages-v2/user-management/user-management-container.tsx +++ b/apps/gitness/src/pages-v2/user-management/user-management-container.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import { useQueryClient } from '@tanstack/react-query' @@ -9,65 +9,32 @@ import { useAdminUpdateUserMutation, useUpdateUserAdminMutation } from '@harnessio/code-service-client' -import { - AdminDialog, - CreateUserDialog, - DeleteUserDialog, - DialogLabels, - EditUserDialog, - ResetPasswordDialog, - UserManagementPage, - UsersProps -} from '@harnessio/ui/views' - -import { parseAsInteger, useQueryState } from '../../framework/hooks/useQueryState' +import { IDataHandlers, UserManagementPage } from '@harnessio/ui/views' + +import { useQueryState } from '../../framework/hooks/useQueryState' +import usePaginationQueryStateWithStore from '../../hooks/use-pagination-query-state-with-store' import { useTranslationStore } from '../../i18n/stores/i18n-store' -import { generateAlphaNumericHash } from '../pull-request/pull-request-utils' import { useAdminListUsersStore } from './stores/admin-list-store' +import { promisifyMutation } from './utils/promisify-mutation' export const UserManagementPageContainer = () => { - const [queryPage, setQueryPage] = useQueryState('page', parseAsInteger.withDefault(1)) - const { setUsers, setTotalPages, setPage, page, password, setUser, setPassword, setGeteneratePassword } = - useAdminListUsersStore() const queryClient = useQueryClient() - const [isDeleteUserDialogOpen, setDeleteUserDialogOpen] = useState(false) - const [isEditUserDialogOpen, setEditUserDialogOpen] = useState(false) - const [isAdminDialogOpen, setAdminDialogOpen] = useState(false) - const [isResetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false) - const [isCreateUserDialogOpen, setCreateUserDialogOpen] = useState(false) - - const handleDialogOpen = (user: UsersProps | null, dialogTypeLabel: string) => { - if (user) setUser(user) - - switch (dialogTypeLabel) { - case DialogLabels.DELETE_USER: - setDeleteUserDialogOpen(true) - break - case DialogLabels.EDIT_USER: - setEditUserDialogOpen(true) - break - case DialogLabels.TOGGLE_ADMIN: - setAdminDialogOpen(true) - break - case DialogLabels.RESET_PASSWORD: - setGeteneratePassword(false) - setPassword(generateAlphaNumericHash(10)) - setResetPasswordDialogOpen(true) - break - case DialogLabels.CREATE_USER: - setPassword(generateAlphaNumericHash(10)) - setCreateUserDialogOpen(true) - setGeteneratePassword(true) - break - default: - break - } - } + const { setUsers, setTotalPages, setPage, page, password } = useAdminListUsersStore() - const { data: { body: userData, headers } = {} } = useAdminListUsersQuery({ + const [query, setQuery] = useQueryState('query') + const { queryPage } = usePaginationQueryStateWithStore({ page, setPage }) + + const { + isFetching, + error, + data: { body: userData, headers } = {} + } = useAdminListUsersQuery({ queryParams: { - page: queryPage + page: queryPage, + // TODO: add search functionality by query parameter + //@ts-expect-error - query is not typed + query: query ?? '' } }) @@ -80,45 +47,41 @@ export const UserManagementPageContainer = () => { } }, [userData, setUsers, setTotalPages, headers]) - useEffect(() => { - setQueryPage(page) - }, [queryPage, page, setPage]) - - const { mutate: updateUser, isLoading: isUpdatingUser } = useAdminUpdateUserMutation( + const { + mutate: updateUser, + isLoading: isUpdatingUser, + error: updateUserError + } = useAdminUpdateUserMutation( {}, { onSuccess: () => { - setEditUserDialogOpen(false) queryClient.invalidateQueries({ queryKey: ['adminListUsers'] }) - }, - onError: error => { - console.error(error) } } ) - const { mutate: deleteUser, isLoading: isDeletingUser } = useAdminDeleteUserMutation( + const { + mutate: deleteUser, + isLoading: isDeletingUser, + error: deleteUserError + } = useAdminDeleteUserMutation( {}, { onSuccess: () => { - setDeleteUserDialogOpen(false) queryClient.invalidateQueries({ queryKey: ['adminListUsers'] }) - }, - onError: error => { - console.error(error) } } ) - const { mutate: updateUserAdmin, isLoading: isUpdatingUserAdmin } = useUpdateUserAdminMutation( + const { + mutate: updateUserAdmin, + isLoading: isUpdatingUserAdmin, + error: updateUserAdminError + } = useUpdateUserAdminMutation( {}, { onSuccess: () => { - setAdminDialogOpen(false) queryClient.invalidateQueries({ queryKey: ['adminListUsers'] }) - }, - onError: error => { - console.error(error) } } ) @@ -131,18 +94,13 @@ export const UserManagementPageContainer = () => { {}, { onSuccess: () => { - setCreateUserDialogOpen(false) - setResetPasswordDialogOpen(true) queryClient.invalidateQueries({ queryKey: ['adminListUsers'] }) - }, - onError: error => { - console.error(error) } } ) - const handleCreateUser = (data: { uid: string; email: string; display_name: string }) => { - createUser({ + const handleCreateUser: IDataHandlers['handleCreateUser'] = data => { + return promisifyMutation(createUser, { body: { uid: data.uid, email: data.email, @@ -152,8 +110,8 @@ export const UserManagementPageContainer = () => { }) } - const handleUpdateUser = (data: { email: string; displayName: string; userID: string }) => { - updateUser({ + const handleUpdateUser: IDataHandlers['handleUpdateUser'] = data => { + return promisifyMutation(updateUser, { user_uid: data.userID, body: { email: data.email, @@ -162,14 +120,14 @@ export const UserManagementPageContainer = () => { }) } - const handleDeleteUser = (userUid: string) => { - deleteUser({ + const handleDeleteUser: IDataHandlers['handleDeleteUser'] = userUid => { + return promisifyMutation(deleteUser, { user_uid: userUid }) } - const handleUpdateUserAdmin = (userUid: string, isAdmin: boolean) => { - updateUserAdmin({ + const handleUpdateUserAdmin: IDataHandlers['handleUpdateUserAdmin'] = (userUid, isAdmin) => { + return promisifyMutation(updateUserAdmin, { user_uid: userUid, body: { admin: isAdmin @@ -177,8 +135,8 @@ export const UserManagementPageContainer = () => { }) } - const handleUpdatePassword = (userId: string) => { - updateUser({ + const handleUpdatePassword: IDataHandlers['handleUpdatePassword'] = userId => { + return promisifyMutation(updateUser, { user_uid: userId, body: { password: password @@ -186,47 +144,40 @@ export const UserManagementPageContainer = () => { }) } + const handlers = { + handleUpdateUser, + handleDeleteUser, + handleUpdateUserAdmin, + handleUpdatePassword, + handleCreateUser + } + + const loadingStates = { + isFetchingUsers: isFetching, + isUpdatingUser, + isDeletingUser, + isUpdatingUserAdmin, + isCreatingUser + } + + const errorStates = { + fetchUsersError: error?.message?.toString() ?? '', + updateUserError: updateUserError?.message?.toString() ?? '', + deleteUserError: deleteUserError?.message?.toString() ?? '', + updateUserAdminError: updateUserAdminError?.message?.toString() ?? '', + createUserError: createUserError?.message?.toString() ?? '' + } + return ( <> - - setDeleteUserDialogOpen(false)} - isDeleting={isDeletingUser} - handleDeleteUser={handleDeleteUser} - /> - setEditUserDialogOpen(false)} - handleUpdateUser={handleUpdateUser} - /> - setAdminDialogOpen(false)} - isLoading={isUpdatingUserAdmin} - updateUserAdmin={handleUpdateUserAdmin} - /> - setResetPasswordDialogOpen(false)} - handleUpdatePassword={handleUpdatePassword} - /> - setCreateUserDialogOpen(false)} - isLoading={isCreatingUser} - apiError={createUserError?.message?.toString() ?? ''} - handleCreateUser={handleCreateUser} + handlers={handlers} + loadingStates={loadingStates} + errorStates={errorStates} + searchQuery={query} + setSearchQuery={setQuery} /> ) diff --git a/apps/gitness/src/pages-v2/user-management/utils/promisify-mutation.ts b/apps/gitness/src/pages-v2/user-management/utils/promisify-mutation.ts new file mode 100644 index 0000000000..e403846c6c --- /dev/null +++ b/apps/gitness/src/pages-v2/user-management/utils/promisify-mutation.ts @@ -0,0 +1,15 @@ +import { UsererrorError } from '@harnessio/code-service-client' + +export type MutationFn = ( + params: T, + options: { onSuccess: () => void; onError: (error: UsererrorError) => void } +) => void + +export const promisifyMutation = (mutation: MutationFn, params: T): Promise => { + return new Promise((resolve, reject) => { + mutation(params, { + onSuccess: () => resolve(), + onError: error => reject(error) + }) + }) +} diff --git a/packages/ui/locales/en/views.json b/packages/ui/locales/en/views.json index d2ca8b256a..62148f3d07 100644 --- a/packages/ui/locales/en/views.json +++ b/packages/ui/locales/en/views.json @@ -213,6 +213,8 @@ "noWebhookExecutionsDescription": "Your webhook executions will appear here once they're completed. Trigger your webhook to see the results.", "noWebhooks": "No webhooks yet", "noWebhooksDescription": "Add or manage webhooks to automate tasks and connect external services to your project.", + "noUsers": "No Users Found", + "noUsersDescription": "There are no users in this scope. Click on the button below to start adding them.", "commit": "Commit", "noLabels": "No labels yet", "noLabelsDescription": "Use labels to organize, prioritize, and categorize tasks efficiently." @@ -476,5 +478,78 @@ "create": "Create webhook", "edit": "Edit webhook", "delete": "Delete webhook" + }, + "userManagement": { + "addNewUser": "Add a new user", + "userId": "User ID", + "userIdHint": "User ID cannot be changed once created", + "enterUsername": "Enter user name", + "enterEmail": "Enter email address", + "email": "Email", + "enterDisplayName": "Enter display name", + "displayName": "Display name", + "cancel": "Cancel", + "inviting": "Inviting...", + "inviteNewUser": "Invite new user", + "deleteConfirmation": "Are you sure you want to delete {{name}}?", + "deleteWarning": "This will permanently delete the user \"{{name}}\" from the system.", + "deletingUser": "Deleting user...", + "confirmDelete": "Yes, delete user", + "updateUser": "Update user", + "updateUserMessage": "Update information for {{name}} and confirm changes.", + "saving": "Saving...", + "save": "Save", + "updateAdminRights": "Are you sure you want to remove {{name}} as an admin?", + "removeAdminMessage": "This will remove the admin tag for \"{{name}}\"", + "grantAdminMessage": "This will grant admin privileges to \"{{name}}\"", + "removingAdmin": "Removing admin...", + "grantingAdmin": "Granting admin...", + "removeAdmin": "Yes, remove admin", + "grantAdmin": "Yes, grant admin", + "resetPassword": "Are you sure you want to reset password for {{name}}?", + "passwordGeneratedMessage": "Your password has been generated. Please make sure to copy and store your password somewhere safe, you won't be able to see it again.", + "resetPasswordMessage": "A new password will be generated to assist {{name}} in resetting their current password.", + "close": "Close", + "resettingPassword": "Resetting Password...", + "resetPasswordConfirm": "Confirm", + "newUserButton": "New user", + "searchPlaceholder": "Search", + "usersHeader": "Users", + "deleteUser": "Delete user", + "deleteUserMessage": "Are you sure you want to delete \"{{name}}\"?", + "tabs": { + "active": "Active users", + "inactive": "Pending users" + }, + "usersList": { + "user": "User", + "email": "Email", + "roleBinding": "Role binding" + }, + "roles": { + "admin": "Admin", + "user": "User" + }, + "actions": { + "removeAdmin": "Remove admin", + "setAsAdmin": "Set as admin", + "resetPassword": "Reset password", + "editUser": "Edit user", + "deleteUser": "Delete user" + }, + "forms": { + "userIdRequired": "Please provide a user ID", + "emailRequired": "Please enter a valid email address", + "displayNameRequired": "Please provide a display name", + "passwordGenerateSuccess": "Your password has been generated. Please make sure to copy and store your password somewhere safe.", + "saving": "Saving...", + "generating": "Generating...", + "close": "Close" + }, + "validation": { + "emailInvalid": "Please enter a valid email", + "displayNameRequired": "Display name is required", + "userIdRequired": "User ID is required" + } } } diff --git a/packages/ui/locales/es/views.json b/packages/ui/locales/es/views.json index f5e899cea2..acb198b4b0 100644 --- a/packages/ui/locales/es/views.json +++ b/packages/ui/locales/es/views.json @@ -213,6 +213,8 @@ "noWebhookExecutionsDescription": "Your webhook executions will appear here once they're completed. Trigger your webhook to see the results.", "noWebhooks": "No webhooks yet", "noWebhooksDescription": "Add or manage webhooks to automate tasks and connect external services to your project.", + "noUsers": "No Users Found", + "noUsersDescription": "There are no users in this scope. Click on the button below to start adding them.", "commit": "Commit", "noLabels": "No labels yet", "noLabelsDescription": "Use labels to organize, prioritize, and categorize tasks efficiently." @@ -465,6 +467,49 @@ "edit": "Edit webhook", "delete": "Delete webhook" }, + "userManagement": { + "addNewUser": "Add a new user", + "userId": "User ID", + "userIdHint": "User ID cannot be changed once created", + "enterUsername": "Enter user name", + "enterEmail": "Enter email address", + "email": "Email", + "enterDisplayName": "Enter display name", + "displayName": "Display Name", + "cancel": "Cancel", + "inviting": "Inviting...", + "inviteNewUser": "Invite New User", + "deleteConfirmation": "Are you sure you want to delete {{name}}?", + "deleteWarning": "This will permanently delete the user \"{{name}}\" from the system.", + "deletingUser": "Deleting user...", + "confirmDelete": "Yes, delete user", + "updateUser": "Update User", + "updateUserMessage": "Update information for {name} and confirm changes.", + "saving": "Saving...", + "save": "Save", + "updateAdminRights": "Update admin rights", + "removeAdminMessage": "This will remove the admin tag for \"{{name}}\"", + "grantAdminMessage": "This will grant admin privileges to \"{{name}}\"", + "removingAdmin": "Removing admin...", + "grantingAdmin": "Granting admin...", + "removeAdmin": "Yes, remove admin", + "grantAdmin": "Yes, grant admin", + "resetPassword": "Reset password for {{name}}", + "passwordGeneratedMessage": "Your password has been generated. Please make sure to copy and store your password somewhere safe, you won't be able to see it again.", + "resetPasswordMessage": "A new password will be generated to assist {{name}} in resetting their current password.", + "close": "Close", + "resettingPassword": "Resetting Password...", + "resetPasswordConfirm": "Confirm", + "newUserButton": "New user", + "searchPlaceholder": "Search", + "usersHeader": "Users", + "deleteUser": "Delete user", + "deleteUserMessage": "Are you sure you want to delete \"{{name}}\"?", + "tabs": { + "active": "Active users", + "inactive": "Pending users" + } + }, "profileSettingsTableHeader": { "token": "Token", "status": "Status" diff --git a/packages/ui/locales/fr/views.json b/packages/ui/locales/fr/views.json index 31349e3434..b443e8acc8 100644 --- a/packages/ui/locales/fr/views.json +++ b/packages/ui/locales/fr/views.json @@ -213,6 +213,8 @@ "noWebhookExecutionsDescription": "Your webhook executions will appear here once they're completed. Trigger your webhook to see the results.", "noWebhooks": "No webhooks yet", "noWebhooksDescription": "Add or manage webhooks to automate tasks and connect external services to your project.", + "noUsers": "No Users Found", + "noUsersDescription": "There are no users in this scope. Click on the button below to start adding them.", "commit": "Validation", "noLabels": "No labels yet", "noLabelsDescription": "Use labels to organize, prioritize, and categorize tasks efficiently." @@ -470,5 +472,48 @@ "create": "Create webhook", "edit": "Edit webhook", "delete": "Delete webhook" + }, + "userManagement": { + "addNewUser": "Add a new user", + "userId": "User ID", + "userIdHint": "User ID cannot be changed once created", + "enterUsername": "Enter user name", + "enterEmail": "Enter email address", + "email": "Email", + "enterDisplayName": "Enter display name", + "displayName": "Display Name", + "cancel": "Cancel", + "inviting": "Inviting...", + "inviteNewUser": "Invite New User", + "deleteConfirmation": "Are you sure you want to delete {{name}}?", + "deleteWarning": "This will permanently delete the user \"{{name}}\" from the system.", + "deletingUser": "Deleting user...", + "confirmDelete": "Yes, delete user", + "updateUser": "Update User", + "updateUserMessage": "Update information for {name} and confirm changes.", + "saving": "Saving...", + "save": "Save", + "updateAdminRights": "Update admin rights", + "removeAdminMessage": "This will remove the admin tag for \"{{name}}\"", + "grantAdminMessage": "This will grant admin privileges to \"{{name}}\"", + "removingAdmin": "Removing admin...", + "grantingAdmin": "Granting admin...", + "removeAdmin": "Yes, remove admin", + "grantAdmin": "Yes, grant admin", + "resetPassword": "Reset password for {{name}}", + "passwordGeneratedMessage": "Your password has been generated. Please make sure to copy and store your password somewhere safe, you won't be able to see it again.", + "resetPasswordMessage": "A new password will be generated to assist {{name}} in resetting their current password.", + "close": "Close", + "resettingPassword": "Resetting Password...", + "resetPasswordConfirm": "Confirm", + "newUserButton": "New user", + "searchPlaceholder": "Search", + "usersHeader": "Users", + "deleteUser": "Delete user", + "deleteUserMessage": "Are you sure you want to delete \"{{name}}\"?", + "tabs": { + "active": "Active users", + "inactive": "Pending users" + } } } diff --git a/packages/ui/src/views/user-management/components/admin-dialog.tsx b/packages/ui/src/views/user-management/components/admin-dialog.tsx deleted file mode 100644 index c00e5e633e..0000000000 --- a/packages/ui/src/views/user-management/components/admin-dialog.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { AlertDialog, Button } from '@/components' - -import { IRemoveAdminDialogProps } from '../types' - -// Form Remove/Add Admin Dialog -export const AdminDialog: React.FC = ({ - useAdminListUsersStore, - open, - onClose, - isLoading, - updateUserAdmin -}) => { - const { user } = useAdminListUsersStore() - const isAdmin = user?.admin ?? false - - return ( - - - - - - {isAdmin ? `Are you sure you want to remove ` : `Are you sure you want to grant `} - {user?.display_name} - {isAdmin ? ` as an admin?` : ` admin privileges?`} - - - {isAdmin - ? `This will remove the admin tag for "${user?.display_name}".` - : `This will grant admin privileges to "${user?.display_name}".`} - - - - - - - - - - ) -} diff --git a/packages/ui/src/views/user-management/components/create-user-dialog.tsx b/packages/ui/src/views/user-management/components/create-user-dialog.tsx deleted file mode 100644 index 17c662854e..0000000000 --- a/packages/ui/src/views/user-management/components/create-user-dialog.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { SubmitHandler, useForm } from 'react-hook-form' - -import { - AlertDialog, - Button, - ButtonGroup, - ControlGroup, - Fieldset, - FormWrapper, - Icon, - Input, - Label, - Spacer, - Text -} from '@/components' -import { zodResolver } from '@hookform/resolvers/zod' -import { z } from 'zod' - -const newUserSchema = z.object({ - uid: z.string().min(1, { message: 'Please provide a user ID' }), - email: z.string().email({ message: 'Please enter a valid email address' }), - display_name: z.string().min(1, { message: 'Please provide a display name' }) -}) - -export type NewUserFields = z.infer - -export function CreateUserDialog({ - handleCreateUser, - isLoading, - apiError, - open, - onClose -}: { - handleCreateUser: (data: NewUserFields) => void - isLoading: boolean - apiError: string | null - open: boolean - onClose: () => void -}) { - const { - register, - handleSubmit, - formState: { errors } - } = useForm({ - resolver: zodResolver(newUserSchema), - mode: 'onChange', - defaultValues: { - uid: '', - email: '', - display_name: '' - } - }) - - const onSubmit: SubmitHandler = data => { - handleCreateUser(data) - } - - return ( - - - - Add a new user - - - -
- {/* USER ID */} - - - - - - User ID cannot be changed once created - - - - - - - - {/* EMAIL */} - - - - - {/* ROLE */} - - - - - {apiError && ( - <> - - {apiError?.toString()} - - - )} - {/* SAVE BUTTON */} - - - - <> - - - - - - -
-
-
-
-
- ) -} diff --git a/packages/ui/src/views/user-management/components/delete-user-dialog.tsx b/packages/ui/src/views/user-management/components/delete-user-dialog.tsx deleted file mode 100644 index c6a3a839ff..0000000000 --- a/packages/ui/src/views/user-management/components/delete-user-dialog.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { AlertDialog, Button, Spacer } from '@/components' - -import { IDeleteDialogProps } from '../types' - -export const DeleteUserDialog: React.FC = ({ - useAdminListUsersStore, - onClose, - isDeleting, - handleDeleteUser, - open -}) => { - const { user } = useAdminListUsersStore() - return ( - - - - - Are you sure you want to delete {user?.display_name}? - - This will permanently delete the user "{user?.display_name}" from the system. - - - - - {!isDeleting && ( - - )} - - - - - - ) -} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/create-user/create-user.tsx b/packages/ui/src/views/user-management/components/dialogs/components/create-user/create-user.tsx new file mode 100644 index 0000000000..46f1571ec4 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/create-user/create-user.tsx @@ -0,0 +1,87 @@ +import { SubmitHandler, useForm } from 'react-hook-form' + +import { Button, ButtonGroup, Dialog, Input } from '@/components' +import { useStates } from '@/views/user-management/providers/state-provider' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' +import { zodResolver } from '@hookform/resolvers/zod' + +import { newUserSchema } from './schemas' +import { ICreateUserDialogProps, NewUserFields } from './types' + +export function CreateUserDialog({ handleCreateUser, open, onClose }: ICreateUserDialogProps) { + const { useTranslationStore } = useUserManagementStore() + const { t } = useTranslationStore() + + const { loadingStates, errorStates } = useStates() + const { isCreatingUser } = loadingStates + const { createUserError } = errorStates + + const { + register, + handleSubmit, + formState: { errors } + } = useForm({ + resolver: zodResolver(newUserSchema), + mode: 'onChange', + defaultValues: { uid: '', email: '', display_name: '' } + }) + + const onSubmit: SubmitHandler = data => { + handleCreateUser(data) + } + + return ( + + + + {t('views:userManagement.addNewUser', 'Add a new user')} + + +
+ + + + + + + {createUserError && {createUserError}} +
+ + + + + + + +
+
+ ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/create-user/index.ts b/packages/ui/src/views/user-management/components/dialogs/components/create-user/index.ts new file mode 100644 index 0000000000..4b38bf2520 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/create-user/index.ts @@ -0,0 +1,2 @@ +export { CreateUserDialog } from './create-user' +export type { ICreateUserDialogProps } from './types' diff --git a/packages/ui/src/views/user-management/components/dialogs/components/create-user/schemas.ts b/packages/ui/src/views/user-management/components/dialogs/components/create-user/schemas.ts new file mode 100644 index 0000000000..067878d3b7 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/create-user/schemas.ts @@ -0,0 +1,7 @@ +import { z } from 'zod' + +export const newUserSchema = z.object({ + uid: z.string().min(1, { message: 'Please provide a user ID' }), + email: z.string().email({ message: 'Please enter a valid email address' }), + display_name: z.string().min(1, { message: 'Please provide a display name' }) +}) diff --git a/packages/ui/src/views/user-management/components/dialogs/components/create-user/types.ts b/packages/ui/src/views/user-management/components/dialogs/components/create-user/types.ts new file mode 100644 index 0000000000..c201e649a5 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/create-user/types.ts @@ -0,0 +1,15 @@ +import { newUserSchema } from '@/views/user-management/components/dialogs/components/create-user/schemas' +import { z } from 'zod' + +export type NewUserFields = z.infer + +export interface ICreateUserDialogProps { + handleCreateUser: (data: NewUserFields) => void + open: boolean + onClose: () => void +} + +export interface ICreateUserFormProps { + handleCreateUser: (data: NewUserFields) => void + onClose: () => void +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/delete-user/delete-user.tsx b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/delete-user.tsx new file mode 100644 index 0000000000..ef9398ab93 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/delete-user.tsx @@ -0,0 +1,63 @@ +import { Button, ButtonGroup, Dialog } from '@/components' +import { IDeleteDialogProps } from '@/views/user-management/components/dialogs/components/delete-user/types' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' +import { useStates } from '@views/user-management/providers/state-provider' + +export function DeleteUserDialog({ onClose, handleDeleteUser, open }: IDeleteDialogProps) { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + + const { t } = useTranslationStore() + const { user } = useAdminListUsersStore() + + const { loadingStates, errorStates } = useStates() + + const { isDeletingUser } = loadingStates + const { deleteUserError } = errorStates + + return ( + + + + + {t('views:userManagement.deleteConfirmation', 'Are you sure you want to delete {{name}}?', { + name: user?.display_name + })} + + + {/* TODO: investigate how to make user login bold with font-weight: 600 */} + {t( + 'views:userManagement.deleteWarning', + 'This will permanently delete the user "{{name}}" from the system.', + { + name: user?.display_name + } + )} + + + + {deleteUserError && {deleteUserError}} + + + + {!isDeletingUser && ( + + )} + + + + + + + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/delete-user/index.ts b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/index.ts new file mode 100644 index 0000000000..a3fb5c9ae0 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/index.ts @@ -0,0 +1,2 @@ +export { DeleteUserDialog } from './delete-user' +export type { IDeleteDialogProps } from './types' diff --git a/packages/ui/src/views/user-management/components/dialogs/components/delete-user/types.ts b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/types.ts new file mode 100644 index 0000000000..8cf707c679 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/delete-user/types.ts @@ -0,0 +1,10 @@ +export interface IDeleteDialogProps { + open: boolean + onClose: () => void + handleDeleteUser: (userUid: string) => void +} + +export interface IDeleteUserFormProps { + handleDeleteUser: (userUid: string) => void + onClose: () => void +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/edit-user/edit-user.tsx b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/edit-user.tsx new file mode 100644 index 0000000000..e246ff4541 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/edit-user.tsx @@ -0,0 +1,106 @@ +import { useEffect } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' + +import { Button, ButtonGroup, Dialog, Input } from '@/components' +import { editUserSchema } from '@/views/user-management/components/dialogs/components/edit-user/schemas' +import { + EditUserFormType, + IEditUserDialogProps +} from '@/views/user-management/components/dialogs/components/edit-user/types' +import { useStates } from '@/views/user-management/providers/state-provider' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' +import { zodResolver } from '@hookform/resolvers/zod' + +export function EditUserDialog({ handleUpdateUser, open, onClose }: IEditUserDialogProps) { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + + const { t } = useTranslationStore() + const { user } = useAdminListUsersStore() + + const { loadingStates, errorStates } = useStates() + const { isUpdatingUser } = loadingStates + const { updateUserError } = errorStates + + const { + register, + handleSubmit, + reset, + formState: { errors } + } = useForm({ + resolver: zodResolver(editUserSchema), + mode: 'onChange' + }) + + useEffect(() => { + if (user) { + reset({ + userID: user.uid, + email: user.email, + displayName: user.display_name + }) + } + }, [user, reset]) + + const onSubmit: SubmitHandler = data => { + handleUpdateUser(data) + } + + return ( + + + + {t('views:userManagement.updateUser', 'Update user')} + + + {/* TODO: investigate how to make user login bold with font-weight: 600 */} + + {t('views:userManagement.updateUserMessage', 'Update information for {name} and confirm changes.', { + name: user?.display_name + })} + + +
+ + + + + + + {updateUserError && {updateUserError}} +
+ + + + + + + +
+
+ ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/edit-user/index.ts b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/index.ts new file mode 100644 index 0000000000..52b9ce0467 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/index.ts @@ -0,0 +1,2 @@ +export { EditUserDialog } from './edit-user' +export type { IEditUserDialogProps } from './types' diff --git a/packages/ui/src/views/user-management/components/dialogs/components/edit-user/schemas.ts b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/schemas.ts new file mode 100644 index 0000000000..6a1edbe3de --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/schemas.ts @@ -0,0 +1,7 @@ +import { z } from 'zod' + +export const editUserSchema = z.object({ + userID: z.string(), + email: z.string().email({ message: 'Please enter a valid email' }), + displayName: z.string().min(1, { message: 'Display name is required' }) +}) diff --git a/packages/ui/src/views/user-management/components/dialogs/components/edit-user/types.ts b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/types.ts new file mode 100644 index 0000000000..82b760dc01 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/edit-user/types.ts @@ -0,0 +1,15 @@ +import { editUserSchema } from '@/views/user-management/components/dialogs/components/edit-user/schemas' +import { z } from 'zod' + +export type EditUserFormType = z.infer + +export interface IEditUserFormProps { + handleUpdateUser: (data: EditUserFormType) => void + onClose: () => void +} + +export interface IEditUserDialogProps { + open: boolean + onClose: () => void + handleUpdateUser: (data: EditUserFormType) => void +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/index.ts b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/index.ts new file mode 100644 index 0000000000..71a738e62e --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/index.ts @@ -0,0 +1,2 @@ +export { RemoveAdminDialog } from './remove-admin' +export type { IRemoveAdminDialogProps } from './types' diff --git a/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/remove-admin.tsx b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/remove-admin.tsx new file mode 100644 index 0000000000..6f940a5c7b --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/remove-admin.tsx @@ -0,0 +1,71 @@ +import { Button, ButtonGroup, Dialog } from '@/components' +import { IRemoveAdminDialogProps } from '@/views/user-management/components/dialogs/components/remove-admin/types' +import { useStates } from '@/views/user-management/providers/state-provider' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' + +export function RemoveAdminDialog({ handleUpdateUserAdmin, open, onClose }: IRemoveAdminDialogProps) { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + const { t } = useTranslationStore() + const { user } = useAdminListUsersStore() + + const { loadingStates, errorStates } = useStates() + const { isUpdatingUserAdmin } = loadingStates + const { updateUserAdminError } = errorStates + + const isAdmin = user?.admin ?? false + + const handleSubmit = () => { + handleUpdateUserAdmin(user?.uid || '', !isAdmin) + } + + return ( + + + + + {t('views:userManagement.updateAdminRights', 'Are you sure you want to remove {name} as an admin?', { + name: user?.display_name + })} + + + + {/* TODO: investigate how to make user login bold with font-weight: 600 */} +
+ + {isAdmin + ? t('views:userManagement.removeAdminMessage', 'This will remove the admin tag for "{{name}}"', { + name: user?.display_name + }) + : t('views:userManagement.grantAdminMessage', 'This will grant admin privileges to "{{name}}"', { + name: user?.display_name + })} + + + {updateUserAdminError && {updateUserAdminError}} +
+ + + + + + + +
+
+ ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/types.ts b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/types.ts new file mode 100644 index 0000000000..9b608caa87 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/remove-admin/types.ts @@ -0,0 +1,10 @@ +export interface IRemoveAdminFormProps { + handleUpdateUserAdmin: (userId: string, isAdmin: boolean) => void + onClose: () => void +} + +export interface IRemoveAdminDialogProps { + open: boolean + onClose: () => void + handleUpdateUserAdmin: (userId: string, isAdmin: boolean) => void +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/reset-password/index.ts b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/index.ts new file mode 100644 index 0000000000..91436678e9 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/index.ts @@ -0,0 +1,2 @@ +export { ResetPasswordDialog } from './reset-password' +export type { IResetPasswordDialogProps } from './types' diff --git a/packages/ui/src/views/user-management/components/dialogs/components/reset-password/reset-password.tsx b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/reset-password.tsx new file mode 100644 index 0000000000..d4cd0772fe --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/reset-password.tsx @@ -0,0 +1,76 @@ +import { Button, ButtonGroup, CopyButton, Dialog, Input } from '@/components' +import { IResetPasswordDialogProps } from '@/views/user-management/components/dialogs/components/reset-password/types' +import { useStates } from '@/views/user-management/providers/state-provider' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' + +export function ResetPasswordDialog({ handleUpdatePassword, open, onClose }: IResetPasswordDialogProps) { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + const { t } = useTranslationStore() + const { user, generatePassword, setGeteneratePassword, password } = useAdminListUsersStore() + + const { loadingStates, errorStates } = useStates() + const { isUpdatingUser } = loadingStates + const { updateUserError } = errorStates + + const onSubmit = () => { + handleUpdatePassword(user?.uid || '') + setGeteneratePassword(true) + } + + return ( + + + + + {t('views:userManagement.resetPassword', 'Are you sure you want to reset password for {name}?', { + name: user?.display_name || '' + })} + + + + {/* TODO: investigate how to make user login bold with font-weight: 600 */} +
+ + {generatePassword + ? t( + 'views:userManagement.passwordGeneratedMessage', + "Your password has been generated. Please make sure to copy and store your password somewhere safe, you won't be able to see it again." + ) + : t( + 'views:userManagement.resetPasswordMessage', + 'A new password will be generated to assist {name} in resetting their current password.', + { name: user?.display_name || '' } + )} + + + {generatePassword && ( + } + /> + )} + + {updateUserError && {updateUserError}} +
+ + + + + {!generatePassword && ( + + )} + + +
+
+ ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/components/reset-password/types.ts b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/types.ts new file mode 100644 index 0000000000..696fbd6813 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/components/reset-password/types.ts @@ -0,0 +1,10 @@ +export interface IResetPasswordFormProps { + handleUpdatePassword: (userId: string) => void + onClose: () => void +} + +export interface IResetPasswordDialogProps { + open: boolean + onClose: () => void + handleUpdatePassword: (userId: string) => void +} diff --git a/packages/ui/src/views/user-management/components/dialogs/dialogs.tsx b/packages/ui/src/views/user-management/components/dialogs/dialogs.tsx new file mode 100644 index 0000000000..ce254a1209 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/dialogs.tsx @@ -0,0 +1,45 @@ +import { CreateUserDialog } from '@/views/user-management/components/dialogs/components/create-user' +import { DeleteUserDialog } from '@/views/user-management/components/dialogs/components/delete-user' +import { EditUserDialog } from '@/views/user-management/components/dialogs/components/edit-user' +import { RemoveAdminDialog } from '@/views/user-management/components/dialogs/components/remove-admin' +import { ResetPasswordDialog } from '@/views/user-management/components/dialogs/components/reset-password' +import { useDialogHandlers } from '@/views/user-management/components/dialogs/hooks' +import { DialogLabels, IDialogsProps } from '@/views/user-management/components/dialogs/types' +import { useDialogs } from '@/views/user-management/providers/dialogs-provider' + +export const Dialogs = ({ handlers }: IDialogsProps) => { + const { dialogsOpenState, closeDialog } = useDialogs() + + const { handleDeleteUser, handleUpdateUser, handleUpdatePassword, handleUpdateUserAdmin, handleCreateUser } = + useDialogHandlers(handlers) + + return ( + <> + closeDialog(DialogLabels.DELETE_USER)} + handleDeleteUser={handleDeleteUser} + /> + closeDialog(DialogLabels.EDIT_USER)} + handleUpdateUser={handleUpdateUser} + /> + closeDialog(DialogLabels.TOGGLE_ADMIN)} + handleUpdateUserAdmin={handleUpdateUserAdmin} + /> + closeDialog(DialogLabels.RESET_PASSWORD)} + handleUpdatePassword={handleUpdatePassword} + /> + closeDialog(DialogLabels.CREATE_USER)} + handleCreateUser={handleCreateUser} + /> + + ) +} diff --git a/packages/ui/src/views/user-management/components/dialogs/hooks/index.ts b/packages/ui/src/views/user-management/components/dialogs/hooks/index.ts new file mode 100644 index 0000000000..0f4fa602e4 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './use-dialog-handlers' +export * from './use-dialog-data' diff --git a/packages/ui/src/views/user-management/components/dialogs/hooks/use-dialog-data.ts b/packages/ui/src/views/user-management/components/dialogs/hooks/use-dialog-data.ts new file mode 100644 index 0000000000..8b94425ca5 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/hooks/use-dialog-data.ts @@ -0,0 +1,40 @@ +import { generateAlphaNumericHash } from '@/utils/utils' +import { DialogLabels } from '@/views/user-management/components/dialogs' +import { useDialogs } from '@/views/user-management/providers/dialogs-provider' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' +import { UsersProps } from '@/views/user-management/types' + +export const useDialogData = () => { + const { useAdminListUsersStore } = useUserManagementStore() + + const { setUser, setPassword, setGeteneratePassword } = useAdminListUsersStore() + + const { openDialog } = useDialogs() + + const prepareDialogData = (user: UsersProps | null, dialogType: DialogLabels) => { + if (user) setUser(user) + + if (dialogType === DialogLabels.RESET_PASSWORD) { + setGeteneratePassword(false) + setPassword(generateAlphaNumericHash(10)) + + return + } + + if (dialogType === DialogLabels.CREATE_USER) { + setGeteneratePassword(true) + setPassword(generateAlphaNumericHash(10)) + + return + } + } + + const handleDialogOpen = (user: UsersProps | null, dialogType: DialogLabels) => { + prepareDialogData(user, dialogType) + openDialog(dialogType) + } + + return { + handleDialogOpen + } +} diff --git a/packages/ui/src/views/user-management/components/dialogs/hooks/use-dialog-handlers.ts b/packages/ui/src/views/user-management/components/dialogs/hooks/use-dialog-handlers.ts new file mode 100644 index 0000000000..194c37aab7 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/hooks/use-dialog-handlers.ts @@ -0,0 +1,46 @@ +import { DialogLabels, IDialogHandlers } from '@/views/user-management/components/dialogs' +import { useDialogs } from '@/views/user-management/providers/dialogs-provider' + +export const useDialogHandlers = (handlers: IDialogHandlers) => { + const { closeDialog } = useDialogs() + + return { + handleCreateUser: async (...args: Parameters) => { + const result = await handlers.handleCreateUser(...args) + + closeDialog(DialogLabels.CREATE_USER) + + return result + }, + + handleUpdateUser: async (...args: Parameters) => { + const result = await handlers.handleUpdateUser(...args) + + closeDialog(DialogLabels.EDIT_USER) + + return result + }, + + handleDeleteUser: async (...args: Parameters) => { + const result = await handlers.handleDeleteUser(...args) + + closeDialog(DialogLabels.DELETE_USER) + + return result + }, + + handleUpdateUserAdmin: async (...args: Parameters) => { + const result = await handlers.handleUpdateUserAdmin(...args) + + closeDialog(DialogLabels.TOGGLE_ADMIN) + + return result + }, + + handleUpdatePassword: async (...args: Parameters) => { + const result = await handlers.handleUpdatePassword(...args) + + return result + } + } +} diff --git a/packages/ui/src/views/user-management/components/dialogs/index.ts b/packages/ui/src/views/user-management/components/dialogs/index.ts new file mode 100644 index 0000000000..65c7d8cb0b --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/index.ts @@ -0,0 +1,2 @@ +export * from './dialogs' +export * from './types' diff --git a/packages/ui/src/views/user-management/components/dialogs/types.ts b/packages/ui/src/views/user-management/components/dialogs/types.ts new file mode 100644 index 0000000000..be0fd2ce81 --- /dev/null +++ b/packages/ui/src/views/user-management/components/dialogs/types.ts @@ -0,0 +1,15 @@ +import { IDataHandlers } from '@/views/user-management/types/data-handlers' + +export interface IDialogHandlers extends IDataHandlers {} + +export enum DialogLabels { + DELETE_USER = 'deleteUser', + EDIT_USER = 'editUser', + TOGGLE_ADMIN = 'toggleAdmin', + RESET_PASSWORD = 'resetPassword', + CREATE_USER = 'createUser' +} + +export interface IDialogsProps { + handlers: IDialogHandlers +} diff --git a/packages/ui/src/views/user-management/components/edit-user-dialog.tsx b/packages/ui/src/views/user-management/components/edit-user-dialog.tsx deleted file mode 100644 index c770b1256a..0000000000 --- a/packages/ui/src/views/user-management/components/edit-user-dialog.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useEffect } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' - -import { AlertDialog, Button, ButtonGroup, ControlGroup, Fieldset, FormWrapper, Input } from '@/components' -import { zodResolver } from '@hookform/resolvers/zod' -import { z } from 'zod' - -import { IEditUserDialogProps } from '../types' - -export const EditUserDialog: React.FC = ({ - useAdminListUsersStore, - onClose, - isSubmitting, - handleUpdateUser, - open -}) => { - const { user } = useAdminListUsersStore() - const newUserSchema = z.object({ - userID: z.string(), - email: z.string().email({ message: 'Please provide a valid email, ex: example@yourcompany.com' }), - displayName: z.string().min(1, { message: 'Please provide a display name' }) - }) - - type MemberFields = z.infer - - const { - handleSubmit, - register, - reset: resetNewMemberForm, - formState: { errors } - } = useForm({ - resolver: zodResolver(newUserSchema), - mode: 'onChange' - }) - - // Form edit submit handler - const onSubmit: SubmitHandler = data => { - handleUpdateUser(data) - resetNewMemberForm(data) - } - - useEffect(() => { - resetNewMemberForm({ - userID: user?.uid, - email: user?.email, - displayName: user?.display_name - }) - }, [user, resetNewMemberForm]) - - return ( - - - - Update User - - - {/* Accessibility: Add Description */} - Update information for {user?.uid} and confirm changes. - -
- {/* User ID */} - - - - - {/* EMAIL */} - - - - - {/* Display Name */} - - - - - {/* Footer */} - {/* */} - - - - <> - - - - - - -
-
-
-
- ) -} diff --git a/packages/ui/src/views/user-management/components/empty-state/empty-state.tsx b/packages/ui/src/views/user-management/components/empty-state/empty-state.tsx new file mode 100644 index 0000000000..466a4df706 --- /dev/null +++ b/packages/ui/src/views/user-management/components/empty-state/empty-state.tsx @@ -0,0 +1,31 @@ +import { NoData } from '@/components' +import { DialogLabels } from '@/views/user-management' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' + +import { useDialogData } from '../dialogs/hooks' + +export const EmptyState = () => { + const { useTranslationStore } = useUserManagementStore() + + const { t } = useTranslationStore() + + const { handleDialogOpen } = useDialogData() + + return ( + handleDialogOpen(null, DialogLabels.CREATE_USER) + }} + /> + ) +} diff --git a/packages/ui/src/views/user-management/components/empty-state/index.ts b/packages/ui/src/views/user-management/components/empty-state/index.ts new file mode 100644 index 0000000000..ff47926401 --- /dev/null +++ b/packages/ui/src/views/user-management/components/empty-state/index.ts @@ -0,0 +1 @@ +export { EmptyState } from './empty-state' diff --git a/packages/ui/src/views/user-management/components/index.ts b/packages/ui/src/views/user-management/components/index.ts new file mode 100644 index 0000000000..48d1a756fc --- /dev/null +++ b/packages/ui/src/views/user-management/components/index.ts @@ -0,0 +1 @@ +export * from './main' diff --git a/packages/ui/src/views/user-management/components/index.tsx b/packages/ui/src/views/user-management/components/index.tsx deleted file mode 100644 index ac53434fc6..0000000000 --- a/packages/ui/src/views/user-management/components/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export * from './delete-user-dialog' -export * from './edit-user-dialog' -export * from './admin-dialog' -export * from './reset-password-dialog' -export * from './create-user-dialog' diff --git a/packages/ui/src/views/user-management/components/main.tsx b/packages/ui/src/views/user-management/components/main.tsx new file mode 100644 index 0000000000..ecda557a1b --- /dev/null +++ b/packages/ui/src/views/user-management/components/main.tsx @@ -0,0 +1,17 @@ +import { IUserManagementPageProps, SandboxLayout } from '@/views/index' +import { Dialogs } from '@/views/user-management/components/dialogs' +import { Content } from '@/views/user-management/components/page-components/content' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' + +export const UserManagementPageContent = ({ handlers }: Pick) => { + const { useAdminListUsersStore } = useUserManagementStore() + + const { totalPages, page: currentPage, setPage } = useAdminListUsersStore() + + return ( + + + + + ) +} diff --git a/packages/ui/src/views/user-management/components/page-components/actions/actions.tsx b/packages/ui/src/views/user-management/components/page-components/actions/actions.tsx new file mode 100644 index 0000000000..76bd89a5a3 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/actions/actions.tsx @@ -0,0 +1,33 @@ +import { Button, ListActions, SearchBox } from '@/components' +import { DialogLabels } from '@/views/user-management/components/dialogs' +import { useDialogData } from '@/views/user-management/components/dialogs/hooks' +import { useSearch } from '@/views/user-management/providers/search-provider' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' + +export const Actions = () => { + const { useTranslationStore } = useUserManagementStore() + + const { t } = useTranslationStore() + + const { searchInput, handleInputChange } = useSearch() + + const { handleDialogOpen } = useDialogData() + + return ( + + + + + + + + + ) +} diff --git a/packages/ui/src/views/user-management/components/page-components/actions/index.ts b/packages/ui/src/views/user-management/components/page-components/actions/index.ts new file mode 100644 index 0000000000..d04c018e24 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/actions/index.ts @@ -0,0 +1 @@ +export { Actions } from './actions' diff --git a/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/error-state.tsx b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/error-state.tsx new file mode 100644 index 0000000000..40ea14b268 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/error-state.tsx @@ -0,0 +1,33 @@ +import { NoData } from '@/components' +import { useStates } from '@/views/user-management/providers/state-provider' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' + +export const ErrorState = () => { + const { useTranslationStore } = useUserManagementStore() + + const { t } = useTranslationStore() + + const { errorStates } = useStates() + const { fetchUsersError } = errorStates + + return ( + { + window.location.reload() + } + }} + /> + ) +} diff --git a/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/index.ts b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/index.ts new file mode 100644 index 0000000000..3c3607299c --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/error-state/index.ts @@ -0,0 +1 @@ +export * from './error-state' diff --git a/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/index.ts b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/index.ts new file mode 100644 index 0000000000..3c22761807 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/index.ts @@ -0,0 +1 @@ +export * from './no-search-results' diff --git a/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/no-search-results.tsx b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/no-search-results.tsx new file mode 100644 index 0000000000..365320b470 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/components/no-search-results/no-search-results.tsx @@ -0,0 +1,29 @@ +import { NoData } from '@/components' +import { useSearch } from '@/views/user-management/providers/search-provider' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' + +export const NoSearchResults = () => { + const { useTranslationStore } = useUserManagementStore() + + const { t } = useTranslationStore() + + const { handleResetSearch } = useSearch() + + return ( + + ) +} diff --git a/packages/ui/src/views/user-management/components/page-components/content/components/users-list/index.ts b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/index.ts new file mode 100644 index 0000000000..be5b630d99 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/index.ts @@ -0,0 +1 @@ +export { UsersList } from './users-list' diff --git a/packages/ui/src/views/user-management/components/page-components/content/components/users-list/users-list.tsx b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/users-list.tsx new file mode 100644 index 0000000000..744bde4378 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/components/users-list/users-list.tsx @@ -0,0 +1,111 @@ +import { Avatar, Badge, MoreActionsTooltip, SkeletonList, Table } from '@/components' +import { getInitials } from '@/utils/utils' +import { DialogLabels } from '@/views/user-management/components/dialogs' +import { useDialogData } from '@/views/user-management/components/dialogs/hooks' +import { ErrorState } from '@/views/user-management/components/page-components/content/components/users-list/components/error-state' +import { NoSearchResults } from '@/views/user-management/components/page-components/content/components/users-list/components/no-search-results' +import { useSearch } from '@/views/user-management/providers/search-provider' +import { useStates } from '@/views/user-management/providers/state-provider/hooks/use-states' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' + +export const UsersList = () => { + const { useAdminListUsersStore } = useUserManagementStore() + + const { users } = useAdminListUsersStore() + const { searchQuery } = useSearch() + const { handleDialogOpen } = useDialogData() + + const { loadingStates, errorStates } = useStates() + const { isFetchingUsers } = loadingStates + const { fetchUsersError } = errorStates + + if (isFetchingUsers) { + return + } + + if (fetchUsersError) { + return + } + + // here should be additional check for users.length === 0, + // but until backend is not ready I leave only searchQuery to make it possible to see how this component works + // TODO: add additional check for users.length === 0 when backend will be ready + if (searchQuery) { + return + } + + return ( + + + + User + Email + Role binding + + <> + + + + + {users && + users.map(user => { + return ( + + {/* NAME */} + +
+ + {user.avatarUrl && } + {getInitials(user.uid!, 2)} + + {user.uid} +
+
+ + {/* EMAIL */} + +
+ {user.email} +
+
+ + {/* ROLE BINDING */} + +
+ + {user.admin ? 'Admin' : 'User'} + +
+
+ + + handleDialogOpen(user, DialogLabels.TOGGLE_ADMIN) + }, + { + title: 'Reset password', + onClick: () => handleDialogOpen(user, DialogLabels.RESET_PASSWORD) + }, + { + title: 'Edit user', + onClick: () => handleDialogOpen(user, DialogLabels.EDIT_USER) + }, + { + isDanger: true, + title: 'Delete user', + onClick: () => handleDialogOpen(user, DialogLabels.DELETE_USER) + } + ]} + /> + +
+ ) + })} +
+
+ ) +} diff --git a/packages/ui/src/views/user-management/components/page-components/content/content.tsx b/packages/ui/src/views/user-management/components/page-components/content/content.tsx new file mode 100644 index 0000000000..ae18e707a8 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/content.tsx @@ -0,0 +1,43 @@ +import { Pagination, Spacer } from '@/components' +import { SandboxLayout } from '@/views' +import { EmptyState } from '@/views/user-management/components/empty-state/empty-state' +import { Actions } from '@/views/user-management/components/page-components/actions' +import { UsersList } from '@/views/user-management/components/page-components/content/components/users-list' +import { ContentProps } from '@/views/user-management/components/page-components/content/types' +import { useStates } from '@/views/user-management/providers/state-provider' +import { useUserManagementStore } from '@/views/user-management/providers/store-provider' + +export const Content = ({ totalPages, currentPage, setPage }: ContentProps) => { + const { useTranslationStore, useAdminListUsersStore } = useUserManagementStore() + + const { users } = useAdminListUsersStore() + + const { loadingStates } = useStates() + const { isFetchingUsers } = loadingStates + + const { t } = useTranslationStore() + + if (!isFetchingUsers && users?.length === 0) { + return + } + + return ( + + +

+ {t('views:userManagement.usersHeader', 'Users')}{' '} + ({users?.length || 0}) +

+ + + + + setPage(pageNum)} + t={t} + /> +
+ ) +} diff --git a/packages/ui/src/views/user-management/components/page-components/content/index.ts b/packages/ui/src/views/user-management/components/page-components/content/index.ts new file mode 100644 index 0000000000..fa11a30216 --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/index.ts @@ -0,0 +1,2 @@ +export { Content } from './content' +export type { ContentProps } from './types' diff --git a/packages/ui/src/views/user-management/components/page-components/content/types.ts b/packages/ui/src/views/user-management/components/page-components/content/types.ts new file mode 100644 index 0000000000..e571fae3be --- /dev/null +++ b/packages/ui/src/views/user-management/components/page-components/content/types.ts @@ -0,0 +1,5 @@ +export interface ContentProps { + totalPages: number + currentPage: number + setPage: (page: number) => void +} diff --git a/packages/ui/src/views/user-management/components/reset-password-dialog.tsx b/packages/ui/src/views/user-management/components/reset-password-dialog.tsx deleted file mode 100644 index 831e9f65ba..0000000000 --- a/packages/ui/src/views/user-management/components/reset-password-dialog.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { FC } from 'react' - -import { AlertDialog, Button, CopyButton, Input, Text } from '@/components' - -import { IResetPasswordDialogProps } from '../types' - -export const ResetPasswordDialog: FC = ({ - open, - useAdminListUsersStore, - onClose, - handleUpdatePassword -}) => { - const { user, generatePassword, setGeteneratePassword, password } = useAdminListUsersStore() - - const handleResetPassword = () => { - handleUpdatePassword(user?.uid ?? '') - } - - return ( - - - - - {generatePassword ? ( - Reset Password - ) : ( - Are you sure you want to reset password for {user?.display_name}? - )} - - {generatePassword ? ( - - Your password has been generated. Please make sure to copy and store your password somewhere safe, you - won't be able to see it again. - - ) : ( - - A new password will be generated to assist {user?.display_name} in resetting their current password. - - )} - {generatePassword && ( - } - /> - )} - - - - - {!generatePassword && ( - - )} - - - - ) -} diff --git a/packages/ui/src/views/user-management/components/users-list.tsx b/packages/ui/src/views/user-management/components/users-list.tsx deleted file mode 100644 index c90d9ecad9..0000000000 --- a/packages/ui/src/views/user-management/components/users-list.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Avatar, Badge, MoreActionsTooltip, Table, Text } from '@/components' -import { getInitials } from '@/utils/utils' - -import { DialogLabels, UsersProps } from '../types' - -interface PageProps { - users: UsersProps[] - handleDialogOpen: (user: UsersProps | null, dialogLabel: string) => void -} - -export const UsersList = ({ users, handleDialogOpen }: PageProps) => { - return ( - - - - User - Display Name - Email - - - - - {users && - users.map(user => { - return ( - - {/* NAME */} - -
- - {!!user?.avatarUrl && } - {getInitials(user.uid!)} - - - {user.uid} - {user.admin && ( - - Admin - - )} - -
-
- - {/* DISPLAY NAME */} - - - {user.display_name} - - - - {/* EMAIL */} - -
- - {user.email} - -
-
- - - handleDialogOpen(user, DialogLabels.TOGGLE_ADMIN) - }, - { - title: 'Reset Password', - onClick: () => handleDialogOpen(user, DialogLabels.RESET_PASSWORD) - }, - { - title: 'Edit User', - onClick: () => handleDialogOpen(user, DialogLabels.EDIT_USER) - }, - { - isDanger: true, - title: 'Delete User', - onClick: () => handleDialogOpen(user, DialogLabels.DELETE_USER) - } - ]} - /> - -
- ) - })} -
-
- ) -} diff --git a/packages/ui/src/views/user-management/index.ts b/packages/ui/src/views/user-management/index.ts index ac8483be3d..1dfe9f6948 100644 --- a/packages/ui/src/views/user-management/index.ts +++ b/packages/ui/src/views/user-management/index.ts @@ -1,3 +1,4 @@ export * from './user-management-page' -export * from './components' export * from './types' +export * from './components/dialogs/types' +export * from './providers/store-provider/types' diff --git a/packages/ui/src/views/user-management/providers/dialogs-provider/dialogs-provider.tsx b/packages/ui/src/views/user-management/providers/dialogs-provider/dialogs-provider.tsx new file mode 100644 index 0000000000..e82916c291 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/dialogs-provider/dialogs-provider.tsx @@ -0,0 +1,34 @@ +import { createContext, ReactNode, useState } from 'react' + +import { DialogLabels } from '@/views/user-management/components/dialogs' +import { DialogsContextType, DialogState } from '@/views/user-management/providers/dialogs-provider' + +export const DialogsContext = createContext(undefined) + +export const DialogsProvider = ({ children }: { children: ReactNode }) => { + const [dialogsOpenState, setDialogsOpenState] = useState({ + [DialogLabels.DELETE_USER]: false, + [DialogLabels.EDIT_USER]: false, + [DialogLabels.TOGGLE_ADMIN]: false, + [DialogLabels.RESET_PASSWORD]: false, + [DialogLabels.CREATE_USER]: false + }) + + const openDialog = (dialogType: DialogLabels) => { + setDialogsOpenState(prev => ({ + ...prev, + [dialogType]: true + })) + } + + const closeDialog = (dialogType: DialogLabels) => { + setDialogsOpenState(prev => ({ + ...prev, + [dialogType]: false + })) + } + + return ( + {children} + ) +} diff --git a/packages/ui/src/views/user-management/providers/dialogs-provider/hooks/use-dialogs.ts b/packages/ui/src/views/user-management/providers/dialogs-provider/hooks/use-dialogs.ts new file mode 100644 index 0000000000..f3a6afb0fc --- /dev/null +++ b/packages/ui/src/views/user-management/providers/dialogs-provider/hooks/use-dialogs.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' + +import { DialogsContext } from '@/views/user-management/providers/dialogs-provider' + +export const useDialogs = () => { + const context = useContext(DialogsContext) + + if (!context) { + throw new Error('useDialogs must be used within DialogsProvider') + } + + return context +} diff --git a/packages/ui/src/views/user-management/providers/dialogs-provider/index.ts b/packages/ui/src/views/user-management/providers/dialogs-provider/index.ts new file mode 100644 index 0000000000..e33cee4b0b --- /dev/null +++ b/packages/ui/src/views/user-management/providers/dialogs-provider/index.ts @@ -0,0 +1,3 @@ +export * from './dialogs-provider' +export * from './hooks/use-dialogs' +export * from './types' diff --git a/packages/ui/src/views/user-management/providers/dialogs-provider/types.ts b/packages/ui/src/views/user-management/providers/dialogs-provider/types.ts new file mode 100644 index 0000000000..b8725b062e --- /dev/null +++ b/packages/ui/src/views/user-management/providers/dialogs-provider/types.ts @@ -0,0 +1,11 @@ +import { DialogLabels } from '@/views/user-management/components/dialogs' + +export type DialogState = { + [K in DialogLabels]: boolean +} + +export interface DialogsContextType { + dialogsOpenState: DialogState + openDialog: (dialogType: DialogLabels) => void + closeDialog: (dialogType: DialogLabels) => void +} diff --git a/packages/ui/src/views/user-management/providers/search-provider/hooks/use-search.ts b/packages/ui/src/views/user-management/providers/search-provider/hooks/use-search.ts new file mode 100644 index 0000000000..8f813b2519 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/search-provider/hooks/use-search.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' + +import { SearchContext } from '@/views/user-management/providers/search-provider' + +export const useSearch = () => { + const context = useContext(SearchContext) + + if (!context) { + throw new Error('useSearch must be used within SearchProvider') + } + + return context +} diff --git a/packages/ui/src/views/user-management/providers/search-provider/index.ts b/packages/ui/src/views/user-management/providers/search-provider/index.ts new file mode 100644 index 0000000000..4be52468f3 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/search-provider/index.ts @@ -0,0 +1,3 @@ +export * from './search-provider' +export * from './hooks/use-search' +export * from './types' diff --git a/packages/ui/src/views/user-management/providers/search-provider/search-provider.tsx b/packages/ui/src/views/user-management/providers/search-provider/search-provider.tsx new file mode 100644 index 0000000000..4524b00d88 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/search-provider/search-provider.tsx @@ -0,0 +1,30 @@ +import { createContext } from 'react' + +import { SearchContextType, SearchProviderProps } from '@/views/user-management/providers/search-provider' +import { useDebounceSearch } from '@hooks/use-debounce-search' + +export const SearchContext = createContext(undefined) + +export const SearchProvider = ({ children, searchQuery, setSearchQuery }: SearchProviderProps) => { + const { + search: searchInput, + handleSearchChange: handleInputChange, + handleResetSearch + } = useDebounceSearch({ + handleChangeSearchValue: (val: string) => setSearchQuery(val.length ? val : null), + searchValue: searchQuery || '' + }) + + return ( + + {children} + + ) +} diff --git a/packages/ui/src/views/user-management/providers/search-provider/types.ts b/packages/ui/src/views/user-management/providers/search-provider/types.ts new file mode 100644 index 0000000000..a23ea18a97 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/search-provider/types.ts @@ -0,0 +1,12 @@ +export interface SearchContextType { + searchQuery: string | null + searchInput: string | null + handleInputChange: (e: React.ChangeEvent) => void + handleResetSearch: () => void +} + +export interface SearchProviderProps { + searchQuery: string | null + setSearchQuery: (query: string | null) => void + children: React.ReactNode +} diff --git a/packages/ui/src/views/user-management/providers/state-provider/hooks/use-states.ts b/packages/ui/src/views/user-management/providers/state-provider/hooks/use-states.ts new file mode 100644 index 0000000000..17bc3b64b9 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/state-provider/hooks/use-states.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' + +import { StateContext } from '@/views/user-management/providers/state-provider' + +export const useStates = () => { + const context = useContext(StateContext) + + if (!context) { + throw new Error('useStates must be used within StateProvider') + } + + return context +} diff --git a/packages/ui/src/views/user-management/providers/state-provider/index.ts b/packages/ui/src/views/user-management/providers/state-provider/index.ts new file mode 100644 index 0000000000..a2822544f8 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/state-provider/index.ts @@ -0,0 +1,3 @@ +export * from './state-provider' +export * from './hooks/use-states' +export * from './types' diff --git a/packages/ui/src/views/user-management/providers/state-provider/state-provider.tsx b/packages/ui/src/views/user-management/providers/state-provider/state-provider.tsx new file mode 100644 index 0000000000..a9bbccf574 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/state-provider/state-provider.tsx @@ -0,0 +1,18 @@ +import { createContext } from 'react' + +import { StateContextType, StateProviderProps } from '@/views/user-management/providers/state-provider' + +export const StateContext = createContext(undefined) + +export const StateProvider = ({ children, loadingStates, errorStates }: StateProviderProps) => { + return ( + + {children} + + ) +} diff --git a/packages/ui/src/views/user-management/providers/state-provider/types.ts b/packages/ui/src/views/user-management/providers/state-provider/types.ts new file mode 100644 index 0000000000..40cfa78cdf --- /dev/null +++ b/packages/ui/src/views/user-management/providers/state-provider/types.ts @@ -0,0 +1,26 @@ +export interface ILoadingStates { + isFetchingUsers: boolean + isUpdatingUser: boolean + isDeletingUser: boolean + isUpdatingUserAdmin: boolean + isCreatingUser: boolean +} + +export interface IErrorStates { + fetchUsersError: string + updateUserError: string + deleteUserError: string + updateUserAdminError: string + createUserError: string +} + +export interface StateContextType { + loadingStates: ILoadingStates + errorStates: IErrorStates +} + +export interface StateProviderProps { + loadingStates: StateContextType['loadingStates'] + errorStates: StateContextType['errorStates'] + children: React.ReactNode +} diff --git a/packages/ui/src/views/user-management/providers/store-provider/hooks/use-store.ts b/packages/ui/src/views/user-management/providers/store-provider/hooks/use-store.ts new file mode 100644 index 0000000000..42d1a836f1 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/store-provider/hooks/use-store.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' + +import { StoreContext } from '@/views/user-management/providers/store-provider/store-provider' + +export const useUserManagementStore = () => { + const context = useContext(StoreContext) + + if (!context) { + throw new Error('useUserManagementStore must be used within UserManagementStoreProvider') + } + + return context +} diff --git a/packages/ui/src/views/user-management/providers/store-provider/index.ts b/packages/ui/src/views/user-management/providers/store-provider/index.ts new file mode 100644 index 0000000000..2a5eb71164 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/store-provider/index.ts @@ -0,0 +1,3 @@ +export * from './hooks/use-store' +export * from './store-provider' +export * from './types' diff --git a/packages/ui/src/views/user-management/providers/store-provider/store-provider.tsx b/packages/ui/src/views/user-management/providers/store-provider/store-provider.tsx new file mode 100644 index 0000000000..f2d35fe2b0 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/store-provider/store-provider.tsx @@ -0,0 +1,15 @@ +import { createContext } from 'react' + +import { StoreContextType, StoreProviderProps } from '@/views/user-management/providers/store-provider' + +export const StoreContext = createContext(undefined) + +export const UserManagementStoreProvider = ({ + children, + useAdminListUsersStore, + useTranslationStore +}: StoreProviderProps) => { + return ( + {children} + ) +} diff --git a/packages/ui/src/views/user-management/providers/store-provider/types.ts b/packages/ui/src/views/user-management/providers/store-provider/types.ts new file mode 100644 index 0000000000..fe371bd099 --- /dev/null +++ b/packages/ui/src/views/user-management/providers/store-provider/types.ts @@ -0,0 +1,27 @@ +import { TranslationStore, UsersProps } from '@/views' + +export interface IAdminListUsersStore { + users: UsersProps[] + totalPages: number + page: number + password: string | null + user: UsersProps | null + generatePassword: boolean + setPassword: (password: string) => void + setUser: (user: UsersProps) => void + setPage: (data: number) => void + setUsers: (data: UsersProps[]) => void + setTotalPages: (data: Headers) => void + setGeteneratePassword: (data: boolean) => void +} + +export interface StoreProviderProps { + useAdminListUsersStore: () => IAdminListUsersStore + useTranslationStore: () => TranslationStore + children: React.ReactNode +} + +export interface StoreContextType { + useAdminListUsersStore: () => IAdminListUsersStore + useTranslationStore: () => TranslationStore +} diff --git a/packages/ui/src/views/user-management/types.ts b/packages/ui/src/views/user-management/types.ts deleted file mode 100644 index a290a2d028..0000000000 --- a/packages/ui/src/views/user-management/types.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { TranslationStore } from '@/views' - -export interface UsersProps { - admin?: boolean - uid?: string - display_name?: string | undefined - email?: string - created?: number - updated?: number - avatarUrl?: string - blocked?: boolean -} - -export interface IUserManagementPageProps { - useAdminListUsersStore: () => IAdminListUsersStore - useTranslationStore: () => TranslationStore - handleDialogOpen: (user: UsersProps | null, dialogLabel: string) => void -} -export interface IAdminListUsersStore { - users: UsersProps[] - totalPages: number - page: number - password: string | null - user: UsersProps | null - generatePassword: boolean - setPassword: (password: string) => void - setUser: (user: UsersProps) => void - setPage: (data: number) => void - setUsers: (data: UsersProps[]) => void - setTotalPages: (data: Headers) => void - setGeteneratePassword: (data: boolean) => void -} - -export interface IDeleteDialogProps { - open: boolean - onClose: () => void - isDeleting: boolean - handleDeleteUser: (userUid: string) => void - useAdminListUsersStore: () => IAdminListUsersStore -} - -export interface IEditUserDialogProps { - isSubmitting: boolean - onClose: () => void - handleUpdateUser: (data: { email: string; displayName: string; userID: string }) => void - open: boolean - useAdminListUsersStore: () => IAdminListUsersStore -} - -export interface IRemoveAdminDialogProps { - open: boolean - onClose: () => void - isLoading: boolean - updateUserAdmin: (uid: string, admin: boolean) => void - useAdminListUsersStore: () => IAdminListUsersStore -} - -export interface IResetPasswordDialogProps { - onClose: () => void - open: boolean - handleUpdatePassword: (userId: string) => void - useAdminListUsersStore: () => IAdminListUsersStore -} - -export enum DialogLabels { - DELETE_USER = 'deleteUser', - EDIT_USER = 'editUser', - TOGGLE_ADMIN = 'toggleAdmin', - RESET_PASSWORD = 'resetPassword', - CREATE_USER = 'createUser' -} diff --git a/packages/ui/src/views/user-management/types/data-handlers.ts b/packages/ui/src/views/user-management/types/data-handlers.ts new file mode 100644 index 0000000000..2946d17ed8 --- /dev/null +++ b/packages/ui/src/views/user-management/types/data-handlers.ts @@ -0,0 +1,19 @@ +export interface IUpdateUserData { + email: string + displayName: string + userID: string +} + +export interface ICreateUserData { + uid: string + email: string + display_name: string +} + +export interface IDataHandlers { + handleUpdateUser: (data: IUpdateUserData) => Promise + handleDeleteUser: (userUid: string) => Promise + handleUpdateUserAdmin: (userUid: string, isAdmin: boolean) => Promise + handleUpdatePassword: (userId: string) => Promise + handleCreateUser: (data: ICreateUserData) => Promise +} diff --git a/packages/ui/src/views/user-management/types/index.ts b/packages/ui/src/views/user-management/types/index.ts new file mode 100644 index 0000000000..8e9802e57b --- /dev/null +++ b/packages/ui/src/views/user-management/types/index.ts @@ -0,0 +1,2 @@ +export * from './main' +export * from './data-handlers' diff --git a/packages/ui/src/views/user-management/types/main.ts b/packages/ui/src/views/user-management/types/main.ts new file mode 100644 index 0000000000..fdc1b00b48 --- /dev/null +++ b/packages/ui/src/views/user-management/types/main.ts @@ -0,0 +1,25 @@ +import { TranslationStore } from '@/views' +import { IDialogHandlers } from '@/views/user-management/components/dialogs' +import { IErrorStates, ILoadingStates } from '@/views/user-management/providers/state-provider/types' +import { IAdminListUsersStore } from '@/views/user-management/providers/store-provider' + +export interface UsersProps { + admin?: boolean + uid?: string + display_name?: string | undefined + email?: string + created?: number + updated?: number + avatarUrl?: string + blocked?: boolean +} + +export interface IUserManagementPageProps { + useAdminListUsersStore: () => IAdminListUsersStore + useTranslationStore: () => TranslationStore + handlers: IDialogHandlers + loadingStates: ILoadingStates + errorStates: IErrorStates + searchQuery: string | null + setSearchQuery: (query: string | null) => void +} diff --git a/packages/ui/src/views/user-management/user-management-page.tsx b/packages/ui/src/views/user-management/user-management-page.tsx index 32e23ff88e..645c1f8b3d 100644 --- a/packages/ui/src/views/user-management/user-management-page.tsx +++ b/packages/ui/src/views/user-management/user-management-page.tsx @@ -1,48 +1,32 @@ -import { Button, ListActions, Pagination, SearchBox, Spacer, Text } from '@/components' -import { SandboxLayout } from '@/views' +import { DialogsProvider } from '@/views/user-management/providers/dialogs-provider' +import { SearchProvider } from '@/views/user-management/providers/search-provider' +import { StateProvider } from '@/views/user-management/providers/state-provider' +import { UserManagementStoreProvider } from '@/views/user-management/providers/store-provider' +import { IUserManagementPageProps } from '@/views/user-management/types' -import { UsersList } from './components/users-list' -import { DialogLabels, IUserManagementPageProps, UsersProps } from './types' +import { UserManagementPageContent } from './components' export const UserManagementPage: React.FC = ({ useAdminListUsersStore, useTranslationStore, - handleDialogOpen + handlers, + loadingStates, + errorStates, + searchQuery, + setSearchQuery }) => { - const { users: userData, totalPages, page: currentPage, setPage } = useAdminListUsersStore() - const { t } = useTranslationStore() - - const renderUserListContent = () => { - return ( - <> - - - ) - } - return ( - - - - - Users - - - - - - - - - - - - {renderUserListContent()} - - - - + + + + + + + + + ) } From c9f36a27de830e99a1df115a7f0b1692fcff1add Mon Sep 17 00:00:00 2001 From: Timofei Ponomarev Date: Thu, 20 Feb 2025 13:40:01 +0100 Subject: [PATCH 2/2] added user-management to design-system --- .../src/pages/view-preview/view-preview.tsx | 6 ++ .../views/user-management/user-management.tsx | 62 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 apps/design-system/src/subjects/views/user-management/user-management.tsx diff --git a/apps/design-system/src/pages/view-preview/view-preview.tsx b/apps/design-system/src/pages/view-preview/view-preview.tsx index 6a87886f18..53fecad03c 100644 --- a/apps/design-system/src/pages/view-preview/view-preview.tsx +++ b/apps/design-system/src/pages/view-preview/view-preview.tsx @@ -41,6 +41,7 @@ import { RepoWebhooksList } from '@subjects/views/repo-webhooks-list/repo-webhoo import { SignInView } from '@subjects/views/signin' import { SignUpView } from '@subjects/views/signup' import { SpaceSettingsMembers } from '@subjects/views/space-settings-members/space-settings-members' +import UserManagementWrapper from '@subjects/views/user-management/user-management' import { useTranslationStore } from '@utils/viewUtils' import { ChatEmptyPreviewWrapper, ChatPreviewWrapper } from '@harnessio/ui/components' @@ -286,6 +287,11 @@ export const viewPreviews: Record = { + ), + 'user-management': ( + + + ) } diff --git a/apps/design-system/src/subjects/views/user-management/user-management.tsx b/apps/design-system/src/subjects/views/user-management/user-management.tsx new file mode 100644 index 0000000000..9a05b71621 --- /dev/null +++ b/apps/design-system/src/subjects/views/user-management/user-management.tsx @@ -0,0 +1,62 @@ +import { FC } from 'react' + +import { noop, useTranslationStore } from '@utils/viewUtils' + +import { IUserManagementPageProps, UserManagementPage } from '@harnessio/ui/views' + +const UserManagementWrapper: FC> = () => { + const user = { + uid: 'user-1', + email: 'user-1@example.com', + display_name: 'User 1', + admin: true, + blocked: false + } + + const useAdminListUsersStore = () => ({ + users: [user], + totalPages: 1, + page: 1, + password: null, + user: user, + generatePassword: false, + setPage: noop, + setUser: noop, + setUsers: noop, + setTotalPages: noop, + setPassword: noop, + setGeteneratePassword: noop + }) + + return ( + {}, + handleDeleteUser: async () => {}, + handleUpdateUserAdmin: async () => {}, + handleUpdatePassword: async () => {}, + handleCreateUser: async () => {} + }} + loadingStates={{ + isFetchingUsers: false, + isUpdatingUser: false, + isDeletingUser: false, + isUpdatingUserAdmin: false, + isCreatingUser: false + }} + errorStates={{ + fetchUsersError: '', + updateUserError: '', + deleteUserError: '', + updateUserAdminError: '', + createUserError: '' + }} + searchQuery={''} + setSearchQuery={noop} + /> + ) +} + +export default UserManagementWrapper