diff --git a/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.jsx b/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.jsx index ed9ee06a0285d77200d207017e3de4aa5a059b63..27b494827d0cfa98c968120eafbce7b4e8332cee 100644 --- a/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.jsx +++ b/enterprise/frontend/src/metabase-enterprise/moderation/components/ModerationReviewBanner/ModerationReviewBanner.jsx @@ -2,7 +2,7 @@ import PropTypes from "prop-types"; import { connect } from "react-redux"; import _ from "underscore"; -import User from "metabase/entities/users"; +import Users from "metabase/entities/users"; import { color, alpha } from "metabase/lib/colors"; import { getRelativeTime } from "metabase/lib/time"; import { getUser } from "metabase/selectors/user"; @@ -26,7 +26,7 @@ const mapStateToProps = (state, props) => ({ }); export default _.compose( - User.load({ + Users.load({ id: (state, props) => props.moderationReview.moderator_id, loadingAndErrorWrapper: false, }), diff --git a/frontend/src/metabase-types/api/user.ts b/frontend/src/metabase-types/api/user.ts index 435f3f295702ee26b208b823a799899dbd0e2228..f367f10a893e6e7a86bbd53aaf6fc35f90bb5c13 100644 --- a/frontend/src/metabase-types/api/user.ts +++ b/frontend/src/metabase-types/api/user.ts @@ -79,3 +79,43 @@ export type UserLoginHistoryItem = { }; export type UserLoginHistory = UserLoginHistoryItem[]; + +export type CreateUserRequest = { + email: string; + first_name?: string; + last_name?: string; + user_group_memberships?: { id: number; is_group_manager: boolean }[]; + login_attributes?: Record<UserAttribute, UserAttribute>; +}; + +export type UpdatePasswordRequest = { + id: UserId; + password: string; + old_password?: string; +}; + +export type ListUsersRequest = { + status?: "deactivated" | "all"; + query?: string; + group_id?: number; + include_deactivated?: boolean; +}; + +export type ListUsersResponse = { + data: User[]; + total: number; + limit: number | null; + offset: number | null; +}; + +export type UpdateUserRequest = { + id: UserId; + email?: string | null; + first_name?: string | null; + last_name?: string | null; + locale?: string | null; + is_group_manager?: boolean; + is_superuser?: boolean; + login_attributes?: Record<UserAttribute, UserAttribute> | null; + user_group_memberships?: { id: number; is_group_manager: boolean }[]; +}; diff --git a/frontend/src/metabase/account/password/actions.ts b/frontend/src/metabase/account/password/actions.ts index 34a0b00ebf55bc1bf39bfe2fce18e834cc318d3a..a706d087b880ad4c8998da3765220b2b345103a4 100644 --- a/frontend/src/metabase/account/password/actions.ts +++ b/frontend/src/metabase/account/password/actions.ts @@ -1,10 +1,7 @@ import { getIn } from "icepick"; import MetabaseSettings from "metabase/lib/settings"; -import { UserApi, UtilApi } from "metabase/services"; -import type { User } from "metabase-types/api"; - -import type { UserPasswordData } from "./types"; +import { UtilApi } from "metabase/services"; export const validatePassword = async (password: string) => { const error = MetabaseSettings.passwordComplexityDescription(password); @@ -18,11 +15,3 @@ export const validatePassword = async (password: string) => { return getIn(error, ["data", "errors", "password"]); } }; - -export const updatePassword = async (user: User, data: UserPasswordData) => { - await UserApi.update_password({ - id: user.id, - password: data.password, - old_password: data.old_password, - }); -}; diff --git a/frontend/src/metabase/account/password/components/UserPasswordForm/UserPasswordForm.tsx b/frontend/src/metabase/account/password/components/UserPasswordForm/UserPasswordForm.tsx index 2f5962640ec03aaf852837b83e8db8e87119f6dd..3d11e45b3e27b82ae616756d31e3822cf068a4ec 100644 --- a/frontend/src/metabase/account/password/components/UserPasswordForm/UserPasswordForm.tsx +++ b/frontend/src/metabase/account/password/components/UserPasswordForm/UserPasswordForm.tsx @@ -3,6 +3,7 @@ import { t } from "ttag"; import _ from "underscore"; import * as Yup from "yup"; +import { useUpdatePasswordMutation } from "metabase/api"; import { Form, FormProvider, @@ -34,13 +35,11 @@ const USER_PASSWORD_SCHEMA = Yup.object({ export interface UserPasswordFormProps { user: User; onValidatePassword: (password: string) => Promise<string | undefined>; - onSubmit: (user: User, data: UserPasswordData) => void; } export const UserPasswordForm = ({ user, onValidatePassword, - onSubmit, }: UserPasswordFormProps): JSX.Element => { const initialValues = useMemo(() => { return USER_PASSWORD_SCHEMA.getDefault(); @@ -51,11 +50,18 @@ export const UserPasswordForm = ({ [onValidatePassword], ); + const [updatePassword] = useUpdatePasswordMutation(); + const handleSubmit = useCallback( - (data: UserPasswordData) => { - return onSubmit(user, data); + async (data: UserPasswordData) => { + const { old_password, password } = data; + return await updatePassword({ + id: user.id, + old_password, + password, + }).unwrap(); }, - [user, onSubmit], + [user, updatePassword], ); return ( diff --git a/frontend/src/metabase/account/password/containers/UserPasswordApp/UserPasswordApp.tsx b/frontend/src/metabase/account/password/containers/UserPasswordApp/UserPasswordApp.tsx index 656e49841b07498dc4bb613858d48e09863cc95b..3c67481550601a50ccb5bd6b896684dbb1307251 100644 --- a/frontend/src/metabase/account/password/containers/UserPasswordApp/UserPasswordApp.tsx +++ b/frontend/src/metabase/account/password/containers/UserPasswordApp/UserPasswordApp.tsx @@ -4,13 +4,12 @@ import { checkNotNull } from "metabase/lib/types"; import { getUser } from "metabase/selectors/user"; import type { State } from "metabase-types/store"; -import { updatePassword, validatePassword } from "../../actions"; +import { validatePassword } from "../../actions"; import { UserPasswordForm } from "../../components/UserPasswordForm"; const mapStateToProps = (state: State) => ({ user: checkNotNull(getUser(state)), onValidatePassword: validatePassword, - onSubmit: updatePassword, }); // eslint-disable-next-line import/no-default-export -- deprecated usage diff --git a/frontend/src/metabase/admin/people/components/GroupMembersTable/GroupMembersTable.tsx b/frontend/src/metabase/admin/people/components/GroupMembersTable/GroupMembersTable.tsx index a108f5684f08df6e0d28ddb4cb7bec6bfa43aa43..1e24b196ff50c75556927fb5d8fb13d65d0bd3c7 100644 --- a/frontend/src/metabase/admin/people/components/GroupMembersTable/GroupMembersTable.tsx +++ b/frontend/src/metabase/admin/people/components/GroupMembersTable/GroupMembersTable.tsx @@ -8,7 +8,7 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; import PaginationControls from "metabase/components/PaginationControls"; import Link from "metabase/core/components/Link"; import CS from "metabase/css/core/index.css"; -import User from "metabase/entities/users"; +import Users from "metabase/entities/users"; import { isAdminGroup, isDefaultGroup } from "metabase/lib/groups"; import { isNotNull } from "metabase/lib/types"; import { getFullName } from "metabase/lib/user"; @@ -150,7 +150,7 @@ function GroupMembersTable({ } // eslint-disable-next-line import/no-default-export -- deprecated usage -export default User.loadList({ +export default Users.loadList({ reload: true, pageSize: 25, listName: "groupUsers", diff --git a/frontend/src/metabase/admin/people/components/PeopleList.jsx b/frontend/src/metabase/admin/people/components/PeopleList.jsx index 9792e800b10014f6fe7378913ed1864c306fd075..58f16d11716e2ed34028c631f7414da0e85a7f22 100644 --- a/frontend/src/metabase/admin/people/components/PeopleList.jsx +++ b/frontend/src/metabase/admin/people/components/PeopleList.jsx @@ -10,7 +10,7 @@ import PaginationControls from "metabase/components/PaginationControls"; import AdminS from "metabase/css/admin.module.css"; import CS from "metabase/css/core/index.css"; import Group from "metabase/entities/groups"; -import User from "metabase/entities/users"; +import Users from "metabase/entities/users"; import { useConfirmation } from "metabase/hooks/use-confirmation"; import { PLUGIN_GROUP_MANAGERS } from "metabase/plugins"; import { getUser, getUserIsAdmin } from "metabase/selectors/user"; @@ -302,7 +302,7 @@ export default _.compose( Group.loadList({ reload: true, }), - User.loadList({ + Users.loadList({ reload: true, query: (_, { query }) => ({ query: query.searchText, diff --git a/frontend/src/metabase/admin/people/containers/EditUserModal.tsx b/frontend/src/metabase/admin/people/containers/EditUserModal.tsx index 554f75cdc6efb4ea95b810f60a5e3e3a61aec48f..ee913ed2b93bd12e4af91e39ddb02e4f33d98045 100644 --- a/frontend/src/metabase/admin/people/containers/EditUserModal.tsx +++ b/frontend/src/metabase/admin/people/containers/EditUserModal.tsx @@ -4,7 +4,7 @@ import type { Params } from "react-router/lib/Router"; import { useUserQuery } from "metabase/common/hooks"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; import ModalContent from "metabase/components/ModalContent"; -import User from "metabase/entities/users"; +import Users from "metabase/entities/users"; import { useDispatch } from "metabase/lib/redux"; import type { User as UserType } from "metabase-types/api"; @@ -25,7 +25,7 @@ export const EditUserModal = ({ onClose, params }: EditUserModalProps) => { const initialValues = useMemo(() => getInitialValues(user), [user]); const handleSubmit = async (val: Partial<UserType>) => { - await dispatch(User.actions.update({ id: user?.id, ...val })); + await dispatch(Users.actions.update({ id: user?.id, ...val })); onClose(); }; diff --git a/frontend/src/metabase/admin/people/containers/GroupDetailApp.jsx b/frontend/src/metabase/admin/people/containers/GroupDetailApp.jsx index 4bc135020a6c45d8d02dfed8a576309f7fb59d2a..c9d4b9b7d93a5a03cac1d37adbe53318da95885d 100644 --- a/frontend/src/metabase/admin/people/containers/GroupDetailApp.jsx +++ b/frontend/src/metabase/admin/people/containers/GroupDetailApp.jsx @@ -2,7 +2,7 @@ import { Component } from "react"; import _ from "underscore"; import Group from "metabase/entities/groups"; -import User from "metabase/entities/users"; +import Users from "metabase/entities/users"; import GroupDetail from "../components/GroupDetail"; @@ -13,6 +13,6 @@ class GroupDetailApp extends Component { } export default _.compose( - User.loadList(), + Users.loadList(), Group.load({ id: (_state, props) => props.params.groupId, reload: true }), )(GroupDetailApp); diff --git a/frontend/src/metabase/admin/people/containers/NewUserModal.tsx b/frontend/src/metabase/admin/people/containers/NewUserModal.tsx index e470388785faafd1d705f92c4670e51d35d687d1..2573bec824544ba3f22f01b2cde2564d2a1bb281 100644 --- a/frontend/src/metabase/admin/people/containers/NewUserModal.tsx +++ b/frontend/src/metabase/admin/people/containers/NewUserModal.tsx @@ -2,7 +2,7 @@ import { push } from "react-router-redux"; import { t } from "ttag"; import ModalContent from "metabase/components/ModalContent"; -import User from "metabase/entities/users"; +import Users from "metabase/entities/users"; import { useDispatch } from "metabase/lib/redux"; import * as Urls from "metabase/lib/urls"; import type { User as UserType } from "metabase-types/api"; @@ -19,7 +19,7 @@ export const NewUserModal = ({ onClose }: NewUserModalProps) => { const handleSubmit = async (vals: Partial<UserType>) => { const { payload: { id: userId }, - } = await dispatch(User.actions.create(vals)); + } = await dispatch(Users.actions.create(vals)); await dispatch(push(Urls.newUserSuccess(userId))); }; diff --git a/frontend/src/metabase/admin/people/containers/UserActivationModal.jsx b/frontend/src/metabase/admin/people/containers/UserActivationModal.jsx index c31cda5bd7ed0311d58c1283168b45df6f1b59fe..48fa9d693430a5d258325e6d691dbcbb420509cc 100644 --- a/frontend/src/metabase/admin/people/containers/UserActivationModal.jsx +++ b/frontend/src/metabase/admin/people/containers/UserActivationModal.jsx @@ -8,7 +8,7 @@ import ModalContent from "metabase/components/ModalContent"; import Text from "metabase/components/type/Text"; import Button from "metabase/core/components/Button"; import CS from "metabase/css/core/index.css"; -import User from "metabase/entities/users"; +import Users from "metabase/entities/users"; // NOTE: we have to load the list of users because /api/user/:id doesn't return deactivated users // but that's ok because it's probably already loaded through the people PeopleListingApp @@ -58,7 +58,7 @@ class UserActivationModalInner extends Component { } const UserActivationModal = _.compose( - User.loadList({ + Users.loadList({ query: { include_deactivated: true }, wrapped: true, }), diff --git a/frontend/src/metabase/admin/people/containers/UserPasswordResetModal.jsx b/frontend/src/metabase/admin/people/containers/UserPasswordResetModal.jsx index 2751a85083a834d404e6f8ef81bc5d9a7803e66b..a936f01067e91b97f0f23edc19b82499f328e9cd 100644 --- a/frontend/src/metabase/admin/people/containers/UserPasswordResetModal.jsx +++ b/frontend/src/metabase/admin/people/containers/UserPasswordResetModal.jsx @@ -10,7 +10,7 @@ import ModalContent from "metabase/components/ModalContent"; import PasswordReveal from "metabase/components/PasswordReveal"; import Button from "metabase/core/components/Button"; import CS from "metabase/css/core/index.css"; -import User from "metabase/entities/users"; +import Users from "metabase/entities/users"; import MetabaseSettings from "metabase/lib/settings"; import { clearTemporaryPassword } from "../people"; @@ -77,7 +77,7 @@ class UserPasswordResetModal extends Component { } export default _.compose( - User.load({ + Users.load({ id: (state, props) => props.params.userId, wrapped: true, }), diff --git a/frontend/src/metabase/admin/people/containers/UserSuccessModal.jsx b/frontend/src/metabase/admin/people/containers/UserSuccessModal.jsx index 60dea02d411295cf116556452f44f0291b197f2c..1eab57646ab977df5fc0cd37b880ab671c98ab3a 100644 --- a/frontend/src/metabase/admin/people/containers/UserSuccessModal.jsx +++ b/frontend/src/metabase/admin/people/containers/UserSuccessModal.jsx @@ -11,7 +11,7 @@ import PasswordReveal from "metabase/components/PasswordReveal"; import Button from "metabase/core/components/Button"; import Link from "metabase/core/components/Link"; import CS from "metabase/css/core/index.css"; -import User from "metabase/entities/users"; +import Users from "metabase/entities/users"; import MetabaseSettings from "metabase/lib/settings"; import { clearTemporaryPassword } from "../people"; @@ -88,7 +88,7 @@ const PasswordSuccess = ({ user, temporaryPassword }) => ( ); export default _.compose( - User.load({ + Users.load({ id: (state, props) => props.params.userId, }), connect( diff --git a/frontend/src/metabase/api/index.ts b/frontend/src/metabase/api/index.ts index d92d76958861d8ab1f17bae7f216b7003c6c93b0..57b5a083a165ebb5034fd56920eda93a6bb1f998 100644 --- a/frontend/src/metabase/api/index.ts +++ b/frontend/src/metabase/api/index.ts @@ -17,3 +17,4 @@ export * from "./session"; export * from "./table"; export * from "./timeline"; export * from "./timeline-event"; +export * from "./user"; diff --git a/frontend/src/metabase/api/session.ts b/frontend/src/metabase/api/session.ts index 7fc7b55df1ee0fe78a62da24096ce1031201ab6c..6f999771a60e5365be68897443ed8b3ede518d6d 100644 --- a/frontend/src/metabase/api/session.ts +++ b/frontend/src/metabase/api/session.ts @@ -14,7 +14,15 @@ export const sessionApi = Api.injectEndpoints({ body: { token }, }), }), + forgotPassword: builder.query<void, string>({ + query: email => ({ + method: "POST", + url: "/api/session/forgot_password", + body: { email }, + }), + }), }), }); -export const { useGetPasswordResetTokenStatusQuery } = sessionApi; +export const { useGetPasswordResetTokenStatusQuery, useForgotPasswordQuery } = + sessionApi; diff --git a/frontend/src/metabase/api/tags/utils.ts b/frontend/src/metabase/api/tags/utils.ts index 5257e44ffe44c3a22ee7ce6705fc7128145bbac8..d00a19569670715a975122f8bd2cf14cbbb8547c 100644 --- a/frontend/src/metabase/api/tags/utils.ts +++ b/frontend/src/metabase/api/tags/utils.ts @@ -323,6 +323,12 @@ export function provideTimelineTags( ]; } +export function provideUserListTags( + users: UserInfo[], +): TagDescription<TagType>[] { + return [listTag("user"), ...users.flatMap(user => provideUserTags(user))]; +} + export function provideUserTags(user: UserInfo): TagDescription<TagType>[] { return [idTag("user", user.id)]; } diff --git a/frontend/src/metabase/api/user.ts b/frontend/src/metabase/api/user.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a1919446706930007625caa281896f97e5a2c9e --- /dev/null +++ b/frontend/src/metabase/api/user.ts @@ -0,0 +1,100 @@ +import type { + CreateUserRequest, + ListUsersRequest, + ListUsersResponse, + UpdatePasswordRequest, + UserId, + User, + UpdateUserRequest, +} from "metabase-types/api"; + +import { Api } from "./api"; +import { + idTag, + listTag, + invalidateTags, + provideUserTags, + provideUserListTags, +} from "./tags"; + +export const userApi = Api.injectEndpoints({ + endpoints: builder => ({ + listUsers: builder.query<ListUsersResponse, ListUsersRequest | void>({ + query: body => ({ + method: "GET", + url: "/api/user", + body, + }), + providesTags: response => + response ? provideUserListTags(response.data) : [], + }), + listUserRecipients: builder.query<ListUsersResponse, void>({ + query: () => ({ + method: "GET", + url: "/api/user/recipients", + }), + providesTags: response => + response ? provideUserListTags(response.data) : [], + }), + getUser: builder.query<User, UserId>({ + query: id => ({ + method: "GET", + url: `/api/user/${id}`, + }), + providesTags: user => (user ? provideUserTags(user) : []), + }), + createUser: builder.mutation<User, CreateUserRequest>({ + query: body => ({ + method: "POST", + url: "/api/user", + body, + }), + invalidatesTags: (_, error) => invalidateTags(error, [listTag("user")]), + }), + updatePassword: builder.mutation<void, UpdatePasswordRequest>({ + query: ({ id, old_password, password }) => ({ + method: "PUT", + url: `/api/user/${id}/password`, + body: { old_password, password }, + }), + invalidatesTags: (_, error, { id }) => + invalidateTags(error, [listTag("user"), idTag("user", id)]), + }), + deactivateUser: builder.mutation<void, UserId>({ + query: id => ({ + method: "DELETE", + url: `/api/user/${id}`, + }), + invalidatesTags: (_, error, id) => + invalidateTags(error, [listTag("user"), idTag("user", id)]), + }), + reactivateUser: builder.mutation<User, UserId>({ + query: id => ({ + method: "PUT", + url: `/api/user/${id}/reactivate`, + }), + invalidatesTags: (_, error, id) => + invalidateTags(error, [listTag("user"), idTag("user", id)]), + }), + updateUser: builder.mutation<User, UpdateUserRequest>({ + query: ({ id, ...body }) => ({ + method: "PUT", + url: `/api/user/${id}`, + body, + }), + invalidatesTags: (_, error, { id }) => + invalidateTags(error, [listTag("user"), idTag("user", id)]), + }), + }), +}); + +export const { + useListUsersQuery, + useListUserRecipientsQuery, + useGetUserQuery, + useCreateUserMutation, + useUpdatePasswordMutation, + useDeactivateUserMutation, + useReactivateUserMutation, + useUpdateUserMutation, +} = userApi; diff --git a/frontend/src/metabase/common/components/EntityPicker/components/EntityPickerModal/EntityPickerModal.unit.spec.tsx b/frontend/src/metabase/common/components/EntityPicker/components/EntityPickerModal/EntityPickerModal.unit.spec.tsx index 16c29ce73cc1d8f7c6a9018be6da2174d233d442..c2db194980766a39902736694b43d1769b7ad8fc 100644 --- a/frontend/src/metabase/common/components/EntityPicker/components/EntityPickerModal/EntityPickerModal.unit.spec.tsx +++ b/frontend/src/metabase/common/components/EntityPicker/components/EntityPickerModal/EntityPickerModal.unit.spec.tsx @@ -149,7 +149,7 @@ describe("EntityPickerModal", () => { }), ); - fetchMock.get("path:/api/user/recipients", []); + fetchMock.get("path:/api/user/recipients", { data: [] }); const onItemSelect = jest.fn(); const onConfirm = jest.fn(); diff --git a/frontend/src/metabase/containers/UserCollectionList.unit.spec.tsx b/frontend/src/metabase/containers/UserCollectionList.unit.spec.tsx index bceddc0a434700e623cc48cce2e0b666efa24249..442c0051e733f3821daac8ef0be1d2f3628b8e14 100644 --- a/frontend/src/metabase/containers/UserCollectionList.unit.spec.tsx +++ b/frontend/src/metabase/containers/UserCollectionList.unit.spec.tsx @@ -19,7 +19,7 @@ const setup = () => { const params = new URL(request.url).searchParams; const limit = parseInt(params.get("limit") ?? "0"); const offset = parseInt(params.get("offset") ?? "0"); - return MockUsers.slice(offset, offset + limit); + return { data: MockUsers.slice(offset, offset + limit) }; }); renderWithProviders(<UserCollectionList />); }; diff --git a/frontend/src/metabase/entities/users.js b/frontend/src/metabase/entities/users.js index 08cfedc5334ce8be63ca717d2eda5afc1dc79952..e34ee73f01dc0efd588bd9ffba3bb5600c5cadc9 100644 --- a/frontend/src/metabase/entities/users.js +++ b/frontend/src/metabase/entities/users.js @@ -1,12 +1,11 @@ import { assocIn } from "icepick"; +import { userApi, sessionApi } from "metabase/api"; import * as MetabaseAnalytics from "metabase/lib/analytics"; -import { GET } from "metabase/lib/api"; -import { createEntity } from "metabase/lib/entities"; +import { createEntity, entityCompatibleQuery } from "metabase/lib/entities"; import { generatePassword } from "metabase/lib/security"; import MetabaseSettings from "metabase/lib/settings"; import { UserSchema } from "metabase/schema"; -import { UserApi, SessionApi } from "metabase/services"; export const DEACTIVATE = "metabase/entities/users/DEACTIVATE"; export const REACTIVATE = "metabase/entities/users/REACTIVATE"; @@ -14,15 +13,16 @@ export const PASSWORD_RESET_EMAIL = "metabase/entities/users/PASSWORD_RESET_EMAIL"; export const PASSWORD_RESET_MANUAL = "metabase/entities/users/RESET_PASSWORD_MANUAL"; -export const RESEND_INVITE = "metabase/entities/users/RESEND_INVITE"; // TODO: It'd be nice to import loadMemberships, but we need to resolve a circular dependency function loadMemberships() { return require("metabase/admin/people/people").loadMemberships(); } -const getUserList = GET("/api/user"); -const getRecipientsList = GET("/api/user/recipients"); +const getUserList = (query = {}, dispatch) => + entityCompatibleQuery(query, dispatch, userApi.endpoints.listUsers); +const getRecipientsList = (query = {}, dispatch) => + entityCompatibleQuery(query, dispatch, userApi.endpoints.listUserRecipients); /** * @deprecated use "metabase/api" instead @@ -35,8 +35,31 @@ const Users = createEntity({ path: "/api/user", api: { - list: ({ recipients = false, ...args }) => - recipients ? getRecipientsList() : getUserList(args), + list: ({ recipients = false, ...args }, dispatch) => + recipients + ? getRecipientsList({}, dispatch) + : getUserList(args, dispatch), + create: (entityQuery, dispatch) => + entityCompatibleQuery( + entityQuery, + dispatch, + userApi.endpoints.createUser, + ), + get: (entityQuery, options, dispatch) => + entityCompatibleQuery( + entityQuery.id, + dispatch, + userApi.endpoints.getUser, + ), + update: (entityQuery, dispatch) => + entityCompatibleQuery( + entityQuery, + dispatch, + userApi.endpoints.updateUser, + ), + delete: () => { + throw new TypeError("Users.api.delete is not supported"); + }, }, objectSelectors: { @@ -48,7 +71,6 @@ const Users = createEntity({ REACTIVATE, PASSWORD_RESET_EMAIL, PASSWORD_RESET_MANUAL, - RESEND_INVITE, }, actionDecorators: { @@ -80,50 +102,74 @@ const Users = createEntity({ }, objectActions: { - resentInvite: async ({ id }) => { - MetabaseAnalytics.trackStructEvent("People Admin", "Resent Invite"); - await UserApi.send_invite({ id }); - return { type: RESEND_INVITE }; - }, - resetPasswordEmail: async ({ email }) => { - MetabaseAnalytics.trackStructEvent( - "People Admin", - "Trigger User Password Reset", - ); - await SessionApi.forgot_password({ email }); - return { type: PASSWORD_RESET_EMAIL }; - }, - resetPasswordManual: async ({ id }, password = generatePassword()) => { - MetabaseAnalytics.trackStructEvent( - "People Admin", - "Manual Password Reset", - ); - await UserApi.update_password({ id, password }); - return { type: PASSWORD_RESET_MANUAL, payload: { id, password } }; - }, - deactivate: async ({ id }) => { - MetabaseAnalytics.trackStructEvent("People Admin", "User Removed"); - // TODO: move these APIs from services to this file - await UserApi.delete({ userId: id }); - return { type: DEACTIVATE, payload: { id } }; - }, - reactivate: async ({ id }) => { - MetabaseAnalytics.trackStructEvent("People Admin", "User Reactivated"); - // TODO: move these APIs from services to this file - const user = await UserApi.reactivate({ userId: id }); - return { type: REACTIVATE, payload: user }; - }, + resetPasswordEmail: + ({ email }) => + async dispatch => { + MetabaseAnalytics.trackStructEvent( + "People Admin", + "Trigger User Password Reset", + ); + await entityCompatibleQuery( + email, + dispatch, + sessionApi.endpoints.forgotPassword, + ); + dispatch({ type: PASSWORD_RESET_EMAIL }); + }, + resetPasswordManual: + async ({ id }, password = generatePassword()) => + async dispatch => { + MetabaseAnalytics.trackStructEvent( + "People Admin", + "Manual Password Reset", + ); + await entityCompatibleQuery( + { id, password }, + dispatch, + userApi.endpoints.updatePassword, + ); + dispatch({ type: PASSWORD_RESET_MANUAL, payload: { id, password } }); + }, + deactivate: + ({ id }) => + async dispatch => { + MetabaseAnalytics.trackStructEvent("People Admin", "User Removed"); + + await entityCompatibleQuery( + id, + dispatch, + userApi.endpoints.deactivateUser, + ); + dispatch({ type: DEACTIVATE, payload: { id } }); + }, + reactivate: + ({ id }) => + async dispatch => { + MetabaseAnalytics.trackStructEvent("People Admin", "User Reactivated"); + + const user = await entityCompatibleQuery( + id, + dispatch, + userApi.endpoints.reactivateUser, + ); + dispatch({ type: REACTIVATE, payload: user }); + }, }, reducer: (state = {}, { type, payload, error }) => { - if (type === DEACTIVATE && !error) { - return assocIn(state, [payload.id, "is_active"], false); - } else if (type === REACTIVATE && !error) { - return assocIn(state, [payload.id, "is_active"], true); - } else if (type === PASSWORD_RESET_MANUAL && !error) { - return assocIn(state, [payload.id, "password"], payload.password); + if (error) { + return state; + } + switch (type) { + case DEACTIVATE: + return assocIn(state, [payload.id, "is_active"], false); + case REACTIVATE: + return assocIn(state, [payload.id, "is_active"], true); + case PASSWORD_RESET_MANUAL: + return assocIn(state, [payload.id, "password"], payload.password); + default: + return state; } - return state; }, }); diff --git a/frontend/src/metabase/query_builder/components/AlertModals/AlertModals.jsx b/frontend/src/metabase/query_builder/components/AlertModals/AlertModals.jsx index 88c2e2b20cadcb7a478de680be479aac76393e24..5917dd2e83c3f24470072e253eda795151dd3708 100644 --- a/frontend/src/metabase/query_builder/components/AlertModals/AlertModals.jsx +++ b/frontend/src/metabase/query_builder/components/AlertModals/AlertModals.jsx @@ -17,7 +17,7 @@ import Button from "metabase/core/components/Button"; import Radio from "metabase/core/components/Radio"; import ButtonsS from "metabase/css/components/buttons.module.css"; import CS from "metabase/css/core/index.css"; -import User from "metabase/entities/users"; +import Users from "metabase/entities/users"; import { alertIsValid } from "metabase/lib/alert"; import * as MetabaseAnalytics from "metabase/lib/analytics"; import MetabaseCookies from "metabase/lib/cookies"; @@ -636,7 +636,7 @@ class AlertEditChannelsInner extends Component { } export const AlertEditChannels = _.compose( - User.loadList(), + Users.loadList(), connect( (state, props) => ({ user: getUser(state), diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js index c78b3afa2623254440ff0d09052971d1105341d3..c799f8328c9afdda7f0c7e84fbbcc7c5b885a6b9 100644 --- a/frontend/src/metabase/services.js +++ b/frontend/src/metabase/services.js @@ -444,11 +444,7 @@ export const SetupApi = { export const UserApi = { list: GET("/api/user/recipients"), current: GET("/api/user/current"), - update_password: PUT("/api/user/:id/password"), update_qbnewb: PUT("/api/user/:id/modal/qbnewb"), - delete: DELETE("/api/user/:userId"), - reactivate: PUT("/api/user/:userId/reactivate"), - send_invite: POST("/api/user/:id/send_invite"), }; export const UtilApi = { diff --git a/frontend/test/__support__/server-mocks/user.ts b/frontend/test/__support__/server-mocks/user.ts index 3dd4831faec89d22580a2e877d64e5bebeed315f..f49dd1eb1b5e183e5fcc2413ebf22844dc36e791 100644 --- a/frontend/test/__support__/server-mocks/user.ts +++ b/frontend/test/__support__/server-mocks/user.ts @@ -8,7 +8,7 @@ export function setupUserEndpoints(user: UserListResult) { export function setupUsersEndpoints(users: UserListResult[]) { users.forEach(user => setupUserEndpoints(user)); - return fetchMock.get("path:/api/user", users); + return fetchMock.get("path:/api/user", { data: users }); } export function setupCurrentUserEndpoint(user: User) {