Repository: metabase/metabase
Pull request #57365: [Admin] Modernize components for People + Groups pages
Author: login: sloansparger
Labels: name: no-backport
description: Do not backport this PR to any branch
name: .Team/AdminWebapp
description: DEPRECATED  Please use .Team/UXWest instead
Created: 2025-04-28T22:52:48Z | Merged: 2025-05-08T16:50:58Z

Closes ADM-735

### Description

Parts of the People / Group pages were first committed in 2016 and there's been a lot of pattern changes in our UI components since this. This PR upgrades these pages and their components to use a Mantine based component instead. I also try to replace usage of global styles with Mantine components. 

I've also made some changes to the ConfirmModal so that it supports async confirm actions + disables the confirm button while the promise is pending.

Lastly I make some adjustments to how colors work for users and groups. Color palletes are the same for groups, but the color is calculated from the group id so it's consistent (previously the color would change if a new group was added alphabetically before it). Also adding users to a group would have avatar colors change as you were search for users to add. Now they're consistent with the people page where normal users are blue and admins are purple.

### How to verify

This is a huge diff, but besides some small mantine vs. old component style differences, this should be a 1:1 refactor.

### Checklist

- [ ] Tests have been added/updated to cover changes in this PR



Diff:
diff --git a/enterprise/frontend/src/metabase-enterprise/audit_app/components/UnsubscribeUserForm/UnsubscribeUserForm.tsx b/enterprise/frontend/src/metabase-enterprise/audit_app/components/UnsubscribeUserForm/UnsubscribeUserForm.tsx
index cb7a3ce83f4f..cf111c3bf500 100644
--- a/enterprise/frontend/src/metabase-enterprise/audit_app/components/UnsubscribeUserForm/UnsubscribeUserForm.tsx
+++ b/enterprise/frontend/src/metabase-enterprise/audit_app/components/UnsubscribeUserForm/UnsubscribeUserForm.tsx
@@ -1,9 +1,8 @@
 import { useCallback, useState } from "react";
 import { t } from "ttag";
 
-import ModalContent from "metabase/components/ModalContent";
-import Button from "metabase/core/components/Button";
-import { FormMessage } from "metabase/forms";
+import { ConfirmModal } from "metabase/components/ConfirmModal";
+import { getResponseErrorMessage } from "metabase/lib/errors";
 import type { User } from "metabase-types/api";
 
 import { ModalMessage } from "./UnsubscribeUserForm.styled";
@@ -19,38 +18,37 @@ export const UnsubscribeUserForm = ({
   onUnsubscribe,
   onClose,
 }: UnsubscribeUserFormProps) => {
-  const [error, setError] = useState<any>();
+  const [errorMessage, setErrorMessage] = useState<string>();
 
   const handleConfirmClick = useCallback(async () => {
     try {
       await onUnsubscribe(user);
       onClose();
     } catch (error) {
-      setError(error);
+      const msg = getResponseErrorMessage(error);
+      setErrorMessage(msg ?? t`Unknown error encountered`);
     }
   }, [user, onUnsubscribe, onClose]);
 
   return (
-    <ModalContent
-      title={t`Unsubscribe ${user.common_name} from all subscriptions and alerts?`}
-      footer={[
-        error ? <FormMessage key="message" formError={error} /> : null,
-        <Button key="cancel" onClick={onClose}>{t`Cancel`}</Button>,
-        <Button
-          key="submit"
-          danger
-          onClick={handleConfirmClick}
-        >{t`Unsubscribe`}</Button>,
-      ]}
+    <ConfirmModal
+      opened
+      errorMessage={errorMessage}
       onClose={onClose}
-    >
-      <ModalMessage>
-        {t`This will delete any dashboard subscriptions or alerts ${user.common_name} has created, and remove them as a recipient from any other subscriptions or alerts.`}
-      </ModalMessage>
-      <ModalMessage>
-        {/* eslint-disable-next-line no-literal-metabase-strings -- Metabase settings */}
-        {t`This does not affect email distribution lists that are managed outside of Metabase.`}
-      </ModalMessage>
-    </ModalContent>
+      onConfirm={handleConfirmClick}
+      title={t`Unsubscribe ${user.common_name} from all subscriptions and alerts?`}
+      confirmButtonText={t`Unsubscribe`}
+      message={
+        <>
+          <ModalMessage>
+            {t`This will delete any dashboard subscriptions or alerts ${user.common_name} has created, and remove them as a recipient from any other subscriptions or alerts.`}
+          </ModalMessage>
+          <ModalMessage>
+            {/* eslint-disable-next-line no-literal-metabase-strings -- Metabase settings */}
+            {t`This does not affect email distribution lists that are managed outside of Metabase.`}
+          </ModalMessage>
+        </>
+      }
+    />
   );
 };
diff --git a/enterprise/frontend/src/metabase-enterprise/audit_app/index.js b/enterprise/frontend/src/metabase-enterprise/audit_app/index.js
index f8279e0e0adf..42adbe844db5 100644
--- a/enterprise/frontend/src/metabase-enterprise/audit_app/index.js
+++ b/enterprise/frontend/src/metabase-enterprise/audit_app/index.js
@@ -1,10 +1,12 @@
 import { t } from "ttag";
 
+import { ForwardRefLink } from "metabase/core/components/Link";
 import {
   PLUGIN_ADMIN_USER_MENU_ITEMS,
   PLUGIN_ADMIN_USER_MENU_ROUTES,
   PLUGIN_AUDIT,
 } from "metabase/plugins";
+import { Menu } from "metabase/ui";
 import { hasPremiumFeature } from "metabase-enterprise/settings";
 
 import { InsightsLink } from "./components/InsightsLink";
@@ -13,10 +15,13 @@ import { isAuditDb } from "./utils";
 
 if (hasPremiumFeature("audit_app")) {
   PLUGIN_ADMIN_USER_MENU_ITEMS.push((user) => [
-    {
-      title: t`Unsubscribe from all subscriptions / alerts`,
-      link: `/admin/people/${user.id}/unsubscribe`,
-    },
+    <Menu.Item
+      component={ForwardRefLink}
+      to={`/admin/people/${user.id}/unsubscribe`}
+      key="unsubscribe"
+    >
+      {t`Unsubscribe from all subscriptions / alerts`}
+    </Menu.Item>,
   ]);
 
   PLUGIN_ADMIN_USER_MENU_ROUTES.push(getUserMenuRotes);
diff --git a/enterprise/frontend/src/metabase-enterprise/audit_app/routes.jsx b/enterprise/frontend/src/metabase-enterprise/audit_app/routes.jsx
index 216197eccd05..24f8569f037a 100644
--- a/enterprise/frontend/src/metabase-enterprise/audit_app/routes.jsx
+++ b/enterprise/frontend/src/metabase-enterprise/audit_app/routes.jsx
@@ -3,5 +3,5 @@ import { ModalRoute } from "metabase/hoc/ModalRoute";
 import { UnsubscribeUserModal } from "./containers/UnsubscribeUserModal/UnsubscribeUserModal";
 
 export const getUserMenuRotes = () => (
-  <ModalRoute path="unsubscribe" modal={UnsubscribeUserModal} />
+  <ModalRoute path="unsubscribe" modal={UnsubscribeUserModal} noWrap />
 );
diff --git a/frontend/src/metabase/admin/people/colors.ts b/frontend/src/metabase/admin/people/colors.ts
new file mode 100644
index 000000000000..ece104214fd5
--- /dev/null
+++ b/frontend/src/metabase/admin/people/colors.ts
@@ -0,0 +1,18 @@
+import { color } from "metabase/lib/colors";
+import type { GroupId, User } from "metabase-types/api";
+
+export const userToColor = (user: User) => {
+  return user.is_superuser ? color("accent2") : color("brand");
+};
+
+const groupColorPalette = [
+  color("error"),
+  color("accent2"),
+  color("brand"),
+  color("accent4"),
+  color("accent1"),
+];
+
+export const groupIdToColor = (groupId: GroupId) => {
+  return groupColorPalette[groupId % groupColorPalette.length];
+};
diff --git a/frontend/src/metabase/admin/people/components/AddMemberRow.tsx b/frontend/src/metabase/admin/people/components/AddMemberRow.tsx
index a66bc14b89ec..b9f924d52551 100644
--- a/frontend/src/metabase/admin/people/components/AddMemberRow.tsx
+++ b/frontend/src/metabase/admin/people/components/AddMemberRow.tsx
@@ -1,15 +1,12 @@
-import cx from "classnames";
-import { useMemo, useRef, useState } from "react";
+import { useMemo, useState } from "react";
 import { t } from "ttag";
 
-import TippyPopover from "metabase/components/Popover/TippyPopover";
 import UserAvatar from "metabase/components/UserAvatar";
-import CS from "metabase/css/core/index.css";
-import Typeahead from "metabase/hoc/Typeahead";
-import { color } from "metabase/lib/colors";
-import { Icon, UnstyledButton } from "metabase/ui";
+import { Flex, Pill, Popover, Text, UnstyledButton } from "metabase/ui";
 import type { User } from "metabase-types/api";
 
+import { userToColor } from "../colors";
+
 import { AddRow } from "./AddRow";
 
 interface AddMemberRowProps {
@@ -25,7 +22,6 @@ export function AddMemberRow({
   onCancel,
   onDone,
 }: AddMemberRowProps) {
-  const rowRef = useRef<HTMLDivElement>(null);
   const [text, setText] = useState("");
   const [selectedUsersById, setSelectedUsersById] = useState<Map<number, User>>(
     new Map(),
@@ -56,131 +52,70 @@ export function AddMemberRow({
     [selectedUsersById, excludeIds, users],
   );
 
+  const suggestedUsers = useMemo(() => {
+    const input = text.toLowerCase();
+    return availableToSelectUsers.filter((user) =>
+      (user.common_name || "").toLowerCase().includes(input),
+    );
+  }, [availableToSelectUsers, text]);
+
   return (
     <tr>
       <td colSpan={4} style={{ padding: 0 }}>
-        <AddRow
-          ref={rowRef}
-          value={text}
-          isValid={selectedUsersById.size > 0}
-          placeholder={t`Julie McMemberson`}
-          onChange={(e) => setText(e.target.value)}
-          onDone={handleDone}
-          onCancel={onCancel}
+        <Popover
+          opened={suggestedUsers.length > 0}
+          position="bottom-start"
+          withArrow
+          shadow="md"
         >
-          {Array.from(selectedUsersById.values()).map((user) => (
-            <div
-              key={user.id}
-              className={cx(
-                CS.bgMedium,
-                CS.p1,
-                CS.px2,
-                CS.mr1,
-                CS.rounded,
-                CS.flex,
-                CS.alignCenter,
-              )}
-            >
-              {user.common_name}
-              <Icon
-                className={cx(
-                  CS.pl1,
-                  CS.cursorPointer,
-                  CS.textSlate,
-                  CS.textMediumHover,
-                )}
-                name="close"
-                onClick={() => handleRemoveUser(user)}
-              />
+          <Popover.Target>
+            <div>
+              <AddRow
+                value={text}
+                isValid={selectedUsersById.size > 0}
+                placeholder={t`Julie McMemberson`}
+                onChange={(e) => setText(e.target.value)}
+                onDone={handleDone}
+                onCancel={onCancel}
+              >
+                {Array.from(selectedUsersById.values()).map((user, index) => (
+                  <Pill
+                    key={user.id}
+                    size="xl"
+                    bg="bg-medium"
+                    c="text-primary"
+                    ms={index > 0 ? "sm" : ""}
+                    withRemoveButton
+                    onRemove={() => handleRemoveUser(user)}
+                  >
+                    {user.common_name}
+                  </Pill>
+                ))}
+              </AddRow>
             </div>
-          ))}
-          <AddMemberTypeaheadPopover
-            value={text}
-            options={availableToSelectUsers}
-            onSuggestionAccepted={handleAddUser}
-            target={rowRef}
-          />
-        </AddRow>
+          </Popover.Target>
+
+          <Popover.Dropdown>
+            <Flex direction="column" p="xs" miw="15rem">
+              {suggestedUsers.map((user: User, index: number) => (
+                <Flex
+                  key={index}
+                  component={UnstyledButton}
+                  align="center"
+                  gap="md"
+                  p="0.5rem 1rem"
+                  onClick={() => handleAddUser(user)}
+                >
+                  <UserAvatar bg={userToColor(user)} user={user} />
+                  <Text fw="bold" size="lg">
+                    {user.common_name}
+                  </Text>
+                </Flex>
+              ))}
+            </Flex>
+          </Popover.Dropdown>
+        </Popover>
       </td>
     </tr>
   );
 }
-
-const getColorPalette = () => [
-  color("brand"),
-  color("accent1"),
-  color("accent2"),
-  color("accent3"),
-  color("accent4"),
-];
-
-interface AddMemberTypeaheadPopoverProps {
-  suggestions: User[];
-  selectedSuggestion: User | null;
-  onSuggestionAccepted: (user: User) => void;
-  target: React.RefObject<HTMLDivElement>;
-}
-
-const AddMemberTypeaheadPopover = Typeahead({
-  optionFilter: (text: string, user: User) =>
-    (user.common_name || "").toLowerCase().includes(text.toLowerCase()),
-  optionIsEqual: (userA: User, userB: User) => userA.id === userB.id,
-})(function AddMemberTypeaheadPopover({
-  suggestions,
-  selectedSuggestion,
-  onSuggestionAccepted,
-  target,
-}: AddMemberTypeaheadPopoverProps) {
-  const colors = useMemo(getColorPalette, []);
-
-  return (
-    <TippyPopover
-      className={CS.bordered}
-      offset={[0, 0]}
-      placement="bottom-start"
-      visible={suggestions.length > 0}
-      reference={target}
-      content={
-        suggestions &&
-        suggestions.map((user: User, index: number) => (
-          <AddMemberAutocompleteSuggestion
-            key={index}
-            user={user}
-            color={colors[index % colors.length]}
-            selected={!!selectedSuggestion && user.id === selectedSuggestion.id}
-            onClick={onSuggestionAccepted.bind(null, user)}
-          />
-        ))
-      }
-    />
-  );
-});
-
-interface AddMemberAutocompleteSuggestionProps {
-  user: User;
-  color: string;
-  selected: boolean;
-  onClick: () => void;
-}
-
-function AddMemberAutocompleteSuggestion({
-  user,
-  color,
-  selected,
-  onClick,
-}: AddMemberAutocompleteSuggestionProps) {
-  return (
-    <UnstyledButton
-      p="0.5rem 1rem"
-      bg={selected ? "brand" : ""}
-      onClick={onClick}
-    >
-      <span className={cx(CS.inlineBlock, CS.mr2)}>
-        <UserAvatar bg={color} user={user} />
-      </span>
-      <span className={cx(CS.h3, { [CS.textWhite]: selected })}>
-        {user.common_name}
-      </span>
-    </UnstyledButton>
-  );
-}
diff --git a/frontend/src/metabase/admin/people/components/AddRow.tsx b/frontend/src/metabase/admin/people/components/AddRow.tsx
index b46bde0e3479..01c6a1a10550 100644
--- a/frontend/src/metabase/admin/people/components/AddRow.tsx
+++ b/frontend/src/metabase/admin/people/components/AddRow.tsx
@@ -1,9 +1,7 @@
-import cx from "classnames";
-import { type ChangeEvent, type Ref, forwardRef } from "react";
+import type { ChangeEvent } from "react";
 import { t } from "ttag";
 
-import ButtonsS from "metabase/css/components/buttons.module.css";
-import CS from "metabase/css/core/index.css";
+import { Button, Flex, Input } from "metabase/ui";
 
 interface AddRowProps {
   value: string;
@@ -16,56 +14,47 @@ interface AddRowProps {
   children?: React.ReactNode;
 }
 
-export const AddRow = forwardRef(function AddRow(
-  {
-    value,
-    isValid,
-    placeholder,
-    onKeyDown,
-    onChange,
-    onDone,
-    onCancel,
-    children,
-  }: AddRowProps,
-  ref: Ref<HTMLDivElement>,
-) {
-  return (
-    <div
-      ref={ref}
-      className={cx(
-        CS.my2,
-        CS.pl1,
-        CS.p1,
-        CS.bordered,
-        CS.borderBrand,
-        CS.rounded,
-        CS.relative,
-        CS.flex,
-        CS.alignCenter,
-      )}
+export const AddRow = ({
+  value,
+  isValid,
+  placeholder,
+  onKeyDown,
+  onChange,
+  onDone,
+  onCancel,
+  children,
+}: AddRowProps) => (
+  <Flex
+    my="1rem"
+    p="0.5rem"
+    display="relative"
+    align="center"
+    bd="1px solid var(--mb-color-brand)"
+    style={{ borderRadius: "0.5rem" }}
+  >
+    {children}
+    <Input
+      type="text"
+      variant="unstyled"
+      flex="1 0 auto"
+      fz="lg"
+      styles={{ input: { background: "transparent" } }}
+      value={value}
+      placeholder={placeholder}
+      autoFocus
+      onKeyDown={onKeyDown}
+      onChange={onChange}
+    />
+    <Button variant="subtle" bg="transparent" onClick={onCancel} mr="sm">
+      {t`Cancel`}
+    </Button>
+    <Button
+      variant={isValid ? "filled" : "outline"}
+      bg={!isValid ? "transparent" : ""}
+      disabled={!isValid}
+      onClick={onDone}
     >
-      {children}
-      <input
-        className={cx(CS.inputBorderless, CS.h3, CS.ml1, CS.flexFull)}
-        type="text"
-        value={value}
-        placeholder={placeholder}
-        autoFocus
-        onKeyDown={onKeyDown}
-        onChange={onChange}
-      />
-      <span className={CS.link} onClick={onCancel}>
-        {t`Cancel`}
-      </span>
-      <button
-        className={cx(ButtonsS.Button, CS.ml2, {
-          [ButtonsS.ButtonPrimary]: !!isValid,
-        })}
-        disabled={!isValid}
-        onClick={onDone}
-      >
-        {t`Add`}
-      </button>
-    </div>
-  );
-});
+      {t`Add`}
+    </Button>
+  </Flex>
+);
diff --git a/frontend/src/metabase/admin/people/components/GroupDetail.tsx b/frontend/src/metabase/admin/people/components/GroupDetail.tsx
index 322c544b75c9..8d8fc14d04ad 100644
--- a/frontend/src/metabase/admin/people/components/GroupDetail.tsx
+++ b/frontend/src/metabase/admin/people/components/GroupDetail.tsx
@@ -1,10 +1,8 @@
-import cx from "classnames";
 import { Fragment, useEffect, useState } from "react";
 import { msgid, ngettext, t } from "ttag";
 
 import { AdminPaneLayout } from "metabase/components/AdminPaneLayout";
 import Alert from "metabase/components/Alert";
-import CS from "metabase/css/core/index.css";
 import { useConfirmation } from "metabase/hooks/use-confirmation";
 import {
   canEditMembership,
@@ -15,6 +13,7 @@ import {
 import { connect } from "metabase/lib/redux";
 import { PLUGIN_GROUP_MANAGERS } from "metabase/plugins";
 import { getUser } from "metabase/selectors/user";
+import { Box } from "metabase/ui";
 import type { Group, Member, User } from "metabase-types/api";
 import type { State } from "metabase-types/store";
 
@@ -26,33 +25,42 @@ import {
 } from "../people";
 import { getGroupMemberships, getMembershipsByUser } from "../selectors";
 
-import GroupMembersTable from "./GroupMembersTable";
+import { GroupMembersTable } from "./GroupMembersTable";
 
 interface GroupDescriptionProps {
   group: Group;
 }
 
-const GroupDescription = ({ group }: GroupDescriptionProps) =>
-  isDefaultGroup(group) ? (
-    <div className={cx(CS.px2, CS.textMeasure)}>
-      <p>
-        {t`All users belong to the ${getGroupNameLocalized(
-          group,
-        )} group and can't be removed from it. Setting permissions for this group is a great way to
-                make sure you know what new Metabase users will be able to see.`}
-      </p>
-    </div>
-  ) : isAdminGroup(group) ? (
-    <div className={cx(CS.px2, CS.textMeasure)}>
-      <p>
-        {t`This is a special group whose members can see everything in the Metabase instance, and who can access and make changes to the
-                settings in the Admin Panel, including changing permissions! So, add people to this group with care.`}
-      </p>
-      <p>
-        {t`To make sure you don't get locked out of Metabase, there always has to be at least one user in this group.`}
-      </p>
-    </div>
-  ) : null;
+const GroupDescription = ({ group }: GroupDescriptionProps) => {
+  if (isDefaultGroup(group)) {
+    return (
+      <Box maw="38rem" px="1rem">
+        <p>
+          {t`All users belong to the ${getGroupNameLocalized(
+            group,
+          )} group and can't be removed from it. Setting permissions for this group is a great way to
+        make sure you know what new Metabase users will be able to see.`}
+        </p>
+      </Box>
+    );
+  }
+
+  if (isAdminGroup(group)) {
+    return (
+      <Box maw="38rem" px="1rem">
+        <p>
+          {t`This is a special group whose members can see everything in the Metabase instance, and who can access and make changes to the
+        settings in the Admin Panel, including changing permissions! So, add people to this group with care.`}
+        </p>
+        <p>
+          {t`To make sure you don't get locked out of Metabase, there always has to be at least one user in this group.`}
+        </p>
+      </Box>
+    );
+  }
+
+  return null;
+};
 
 interface GroupDetailStateProps {
   groupMemberships: Member[];
@@ -142,12 +150,12 @@ const GroupDetailInner = ({
     setAddUserVisible(false);
     try {
       await Promise.all(
-        userIds.map(async (userId) => {
-          await createMembership({
+        userIds.map((userId) =>
+          createMembership({
             groupId: group.id,
             userId,
-          });
-        }),
+          }),
+        ),
       );
     } catch (error) {
       const errorMessage =
@@ -159,7 +167,6 @@ const GroupDetailInner = ({
   const handleChange = async (membership: Member) => {
     if (!currentUser) {
       throw new Error("currentUser is not defined");
-      return;
     }
 
     const confirmation = PLUGIN_GROUP_MANAGERS.getChangeMembershipConfirmation(
@@ -185,7 +192,6 @@ const GroupDetailInner = ({
   const handleRemove = async (membershipId: number) => {
     if (!currentUser) {
       throw new Error("currentUser is not defined");
-      return;
     }
 
     const confirmation = PLUGIN_GROUP_MANAGERS.getRemoveMembershipConfirmation(
@@ -214,13 +220,13 @@ const GroupDetailInner = ({
       title={
         <Fragment>
           {getGroupNameLocalized(group ?? {})}
-          <span className={cx(CS.textLight, CS.ml1)}>
+          <Box component="span" c="text-light" ms="sm">
             {ngettext(
               msgid`${group.members.length} member`,
               `${group.members.length} members`,
               group.members.length,
             )}
-          </span>
+          </Box>
         </Fragment>
       }
       buttonText={t`Add members`}
diff --git a/frontend/src/metabase/admin/people/components/GroupMembersTable/GroupMembersTable.tsx b/frontend/src/metabase/admin/people/components/GroupMembersTable/GroupMembersTable.tsx
index 201f7b3db943..98d95773d174 100644
--- a/frontend/src/metabase/admin/people/components/GroupMembersTable/GroupMembersTable.tsx
+++ b/frontend/src/metabase/admin/people/components/GroupMembersTable/GroupMembersTable.tsx
@@ -1,4 +1,3 @@
-import cx from "classnames";
 import { useMemo } from "react";
 import { t } from "ttag";
 
@@ -7,13 +6,12 @@ import { AdminContentTable } from "metabase/components/AdminContentTable";
 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 Users from "metabase/entities/users";
 import { isAdminGroup, isDefaultGroup } from "metabase/lib/groups";
 import { isNotNull } from "metabase/lib/types";
 import { getFullName } from "metabase/lib/user";
 import { PLUGIN_GROUP_MANAGERS } from "metabase/plugins";
-import { Icon, Text, Tooltip } from "metabase/ui";
+import { Box, Flex, Icon, Text, Tooltip, UnstyledButton } from "metabase/ui";
 import type { ApiKey, Group, User as IUser, Member } from "metabase-types/api";
 import type { State } from "metabase-types/store";
 
@@ -31,8 +29,8 @@ interface GroupMembersTableProps {
   showAddUser: any;
   selectedUsers: IUser[];
   onAddUserCancel: () => void;
-  onAddUserDone: (userIds: number[]) => void;
-  onMembershipRemove: (membershipId: number) => void;
+  onAddUserDone: (userIds: number[]) => Promise<void>;
+  onMembershipRemove: (membershipId: number) => Promise<void>;
   onMembershipUpdate: (member: Member) => void;
   reload: () => void;
   groupUsers: IUser[];
@@ -42,7 +40,7 @@ interface GroupMembersTableProps {
   onPreviousPage: () => void;
 }
 
-function GroupMembersTable({
+function GroupMembersTableInner({
   group,
   groupMemberships,
   membershipsByUser,
@@ -113,22 +111,20 @@ function GroupMembersTable({
         {groupApiKeys?.map((apiKey: ApiKey) => (
           <ApiKeyRow key={`apiKey-${apiKey.id}`} apiKey={apiKey} />
         ))}
-        {groupUsers.map((user: IUser) => {
-          return (
-            <UserRow
-              key={user.id}
-              group={group}
-              user={user}
-              memberships={membershipsByUser[user.id]}
-              canRemove={canRemove(user)}
-              onMembershipRemove={handleRemoveUser}
-              onMembershipUpdate={onMembershipUpdate}
-            />
-          );
-        })}
+        {groupUsers.map((user: IUser) => (
+          <UserRow
+            key={user.id}
+            group={group}
+            user={user}
+            memberships={membershipsByUser[user.id]}
+            canRemove={canRemove(user)}
+            onMembershipRemove={handleRemoveUser}
+            onMembershipUpdate={onMembershipUpdate}
+          />
+        ))}
       </AdminContentTable>
       {hasMembers && (
-        <div className={cx(CS.flex, CS.alignCenter, CS.justifyEnd, CS.p2)}>
+        <Flex align="center" justify="flex-end" p="md">
           <PaginationControls
             page={page}
             pageSize={pageSize}
@@ -137,28 +133,25 @@ function GroupMembersTable({
             onNextPage={onNextPage}
             onPreviousPage={onPreviousPage}
           />
-        </div>
+        </Flex>
       )}
       {!hasMembers && (
-        <div className={cx(CS.mt4, CS.pt4, CS.flex, CS.layoutCentered)}>
-          <h2
-            className={CS.textMedium}
-          >{t`A group is only as good as its members.`}</h2>
-        </div>
+        <Text size="lg" fw="700" ta="center" mt="4rem">
+          {t`A group is only as good as its members.`}
+        </Text>
       )}
     </>
   );
 }
 
-// eslint-disable-next-line import/no-default-export -- deprecated usage
-export default Users.loadList({
+export const GroupMembersTable = Users.loadList({
   reload: true,
   pageSize: 25,
   listName: "groupUsers",
   query: (_state: State, props: GroupMembersTableProps) => ({
     group_id: props.group.id,
   }),
-})(GroupMembersTable);
+})(GroupMembersTableInner);
 
 interface UserRowProps {
   user: IUser;
@@ -194,7 +187,9 @@ const UserRow = ({
 
   return (
     <tr>
-      <td className={CS.textBold}>{getName(user)}</td>
+      <td>
+        <Text fw={700}>{getName(user)}</Text>
+      </td>
       {canEditMembership(group) && PLUGIN_GROUP_MANAGERS.UserTypeCell && (
         <PLUGIN_GROUP_MANAGERS.UserTypeCell
           isManager={groupMembership.is_group_manager}
@@ -204,17 +199,37 @@ const UserRow = ({
       )}
       <td>{user.email}</td>
       {canRemove ? (
-        <td
-          className={cx(CS.textRight, CS.cursorPointer)}
-          onClick={() => onMembershipRemove(groupMembership?.membership_id)}
-        >
-          <Icon name="close" className={CS.textLight} size={16} />
-        </td>
+        <Box component="td" ta="right">
+          <UnstyledButton
+            onClick={() => onMembershipRemove(groupMembership?.membership_id)}
+          >
+            <Icon name="close" c="text-light" size={16} />
+          </UnstyledButton>
+        </Box>
       ) : null}
     </tr>
   );
 };
 
+const ApiKeyRow = ({ apiKey }: { apiKey: ApiKey }) => (
+  <tr>
+    <td>
+      <Text fw="bold">{apiKey.name}</Text>
+    </td>
+    <td>
+      <Text fw="bold" c="text-medium">{t`API Key`}</Text>
+    </td>
+    <td>{/* api keys don't have real emails */}</td>
+    <Box component="td" ta="right">
+      <Link to="/admin/settings/authentication/api-keys">
+        <Tooltip label={t`Manage API keys`} position="left">
+          <Icon name="link" c="text-light" size={16} />
+        </Tooltip>
+      </Link>
+    </Box>
+  </tr>
+);
+
 function getName(user: IUser): string {
   const name = getFullName(user);
 
@@ -224,24 +239,3 @@ function getName(user: IUser): string {
 
   return name;
 }
-
-const ApiKeyRow = ({ apiKey }: { apiKey: ApiKey }) => {
-  return (
-    <tr>
-      <td>
-        <Text fw="bold">{apiKey.name}</Text>
-      </td>
-      <td>
-        <Text fw="bold" color="text-medium">{t`API Key`}</Text>
-      </td>
-      <td>{/* api keys don't have real emails */}</td>
-      <td className={CS.textRight}>
-        <Link to="/admin/settings/authentication/api-keys">
-          <Tooltip label={t`Manage API keys`} position="left">
-            <Icon name="link" size={16} />
-          </Tooltip>
-        </Link>
-      </td>
-    </tr>
-  );
-};
diff --git a/frontend/src/metabase/admin/people/components/GroupMembersTable/index.ts b/frontend/src/metabase/admin/people/components/GroupMembersTable/index.ts
index aa6c5c505ef0..9e686937f494 100644
--- a/frontend/src/metabase/admin/people/components/GroupMembersTable/index.ts
+++ b/frontend/src/metabase/admin/people/components/GroupMembersTable/index.ts
@@ -1,2 +1 @@
-// eslint-disable-next-line import/no-default-export -- deprecated usage
-export { default } from "./GroupMembersTable";
+export * from "./GroupMembersTable";
diff --git a/frontend/src/metabase/admin/people/components/GroupSummary.tsx b/frontend/src/metabase/admin/people/components/GroupSummary.tsx
index cba70ccf79fc..2ade4d817fc3 100644
--- a/frontend/src/metabase/admin/people/components/GroupSummary.tsx
+++ b/frontend/src/metabase/admin/people/components/GroupSummary.tsx
@@ -1,11 +1,10 @@
 import { msgid, ngettext, t } from "ttag";
 
 import type { UserGroupType } from "metabase/admin/types";
-import CS from "metabase/css/core/index.css";
 import { isAdminGroup, isDefaultGroup } from "metabase/lib/groups";
-import { Box } from "metabase/ui";
+import { Box, type BoxProps } from "metabase/ui";
 
-interface GroupSummaryProps {
+interface GroupSummaryProps extends BoxProps {
   groups: UserGroupType[];
   selectedGroupIds: number[];
 }
@@ -13,6 +12,7 @@ interface GroupSummaryProps {
 export const GroupSummary = ({
   groups,
   selectedGroupIds,
+  ...props
 }: GroupSummaryProps) => {
   const adminGroup = groups.find(isAdminGroup);
   const otherGroups = groups.filter(
@@ -22,31 +22,35 @@ export const GroupSummary = ({
 
   if (adminGroup && selectedGroupIds.includes(adminGroup.id)) {
     return (
-      <span>
+      <Box component="span" {...props}>
         <Box component="span" c="filter">
           {t`Admin`}
         </Box>
         {otherGroups.length > 0 && " " + t`and` + " "}
         {otherGroups.length > 0 && (
-          <span className={CS.textBrand}>
+          <Box component="span" c="brand">
             {((n) => ngettext(msgid`${n} other group`, `${n} other groups`, n))(
               otherGroups.length,
             )}
-          </span>
+          </Box>
         )}
-      </span>
+      </Box>
     );
   } else if (otherGroups.length === 1) {
-    return <span className={CS.textBrand}>{otherGroups[0].name}</span>;
+    return (
+      <Box component="span" c="brand" {...props}>
+        {otherGroups[0].name}
+      </Box>
+    );
   } else if (otherGroups.length > 1) {
     return (
-      <span className={CS.textBrand}>
+      <Box component="span" c="brand" {...props}>
         {((n) => ngettext(msgid`${n} other group`, `${n} other groups`, n))(
           otherGroups.length,
         )}
-      </span>
+      </Box>
     );
   } else {
-    return <span>{t`Default`}</span>;
+    return <Box {...props}>{t`Default`}</Box>;
   }
 };
diff --git a/frontend/src/metabase/admin/people/components/GroupsListing.tsx b/frontend/src/metabase/admin/people/components/GroupsListing.tsx
index 4da11e5d68c8..229c270bd3f3 100644
--- a/frontend/src/metabase/admin/people/components/GroupsListing.tsx
+++ b/frontend/src/metabase/admin/people/components/GroupsListing.tsx
@@ -1,5 +1,4 @@
 import { useDisclosure } from "@mantine/hooks";
-import cx from "classnames";
 import { Component } from "react";
 import { jt, t } from "ttag";
 import _ from "underscore";
@@ -11,20 +10,27 @@ import Alert from "metabase/components/Alert";
 import { ConfirmModal } from "metabase/components/ConfirmModal";
 import { LoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper";
 import UserAvatar from "metabase/components/UserAvatar";
-import Input from "metabase/core/components/Input";
 import Link from "metabase/core/components/Link";
-import ButtonsS from "metabase/css/components/buttons.module.css";
 import CS from "metabase/css/core/index.css";
-import { color } from "metabase/lib/colors";
 import {
   getGroupNameLocalized,
   isAdminGroup,
   isDefaultGroup,
 } from "metabase/lib/groups";
 import { KEYCODE_ENTER } from "metabase/lib/keyboard";
-import { Icon, Menu, UnstyledButton } from "metabase/ui";
+import {
+  Box,
+  Button,
+  Flex,
+  Icon,
+  Input,
+  Menu,
+  UnstyledButton,
+} from "metabase/ui";
 import type { ApiKey, Group as IGroup } from "metabase-types/api";
 
+import { groupIdToColor } from "../colors";
+
 import { AddRow } from "./AddRow";
 
 // ------------------------------------------------------------ Add Group ------------------------------------------------------------
@@ -177,31 +183,31 @@ function EditingGroupRow({
   onDoneClicked,
 }: EditingGroupRowProps) {
   const textIsValid = group.name && group.name.length;
+
   return (
-    <tr className={cx(CS.bordered, CS.borderBrand, CS.rounded)}>
+    <Box component="tr" bd="1px solid var(--mb-color-brand)">
       <td>
         <Input
-          className={CS.h3}
+          fz="lg"
           type="text"
-          autoFocus={true}
+          autoFocus
           value={group.name}
           onChange={(e) => onTextChange(e.target.value)}
         />
       </td>
       <td />
-      <td className={CS.textRight}>
-        <span className={CS.link} onClick={onCancelClicked}>{t`Cancel`}</span>
-        <button
-          className={cx(ButtonsS.Button, CS.ml2, {
-            [ButtonsS.ButtonPrimary]: textIsValid && textHasChanged,
-          })}
+      <Box component="td" ta="right">
+        <Button variant="subtle" onClick={onCancelClicked}>{t`Cancel`}</Button>
+        <Button
+          ml="1rem"
+          variant={textIsValid && textHasChanged ? "filled" : "outline"}
           disabled={!textIsValid || !textHasChanged}
           onClick={onDoneClicked}
         >
           {t`Done`}
-        </button>
-      </td>
-    </tr>
+        </Button>
+      </Box>
+    </Box>
   );
 }
 
@@ -210,7 +216,6 @@ function EditingGroupRow({
 interface GroupRowProps {
   group: IGroup;
   groupBeingEdited: IGroup | null;
-  index: number;
   apiKeys: ApiKey[];
   onEditGroupClicked: (group: IGroup) => void;
   onDeleteGroupClicked: (group: IGroup) => void;
@@ -222,7 +227,6 @@ interface GroupRowProps {
 function GroupRow({
   group,
   groupBeingEdited,
-  index,
   apiKeys,
   onEditGroupClicked,
   onDeleteGroupClicked,
@@ -230,8 +234,7 @@ function GroupRow({
   onEditGroupCancelClicked,
   onEditGroupDoneClicked,
 }: GroupRowProps) {
-  const colors = getGroupRowColors();
-  const backgroundColor = colors[index % colors.length];
+  const backgroundColor = groupIdToColor(group.id);
   const showActionsButton = !isDefaultGroup(group) && !isAdminGroup(group);
   const editing = groupBeingEdited && groupBeingEdited.id === group.id;
 
@@ -246,26 +249,27 @@ function GroupRow({
   ) : (
     <tr>
       <td>
-        <Link
+        <Flex
+          component={Link}
+          align="center"
           to={"/admin/people/groups/" + group.id}
-          className={cx(CS.link, CS.flex, CS.alignCenter)}
+          className={CS.link}
+          gap="md"
         >
-          <span className={CS.textWhite}>
-            <UserAvatar
-              user={{ first_name: getGroupNameLocalized(group) }}
-              bg={backgroundColor}
-            />
-          </span>
-          <span className={cx(CS.ml2, CS.textBold)}>
+          <UserAvatar
+            user={{ first_name: getGroupNameLocalized(group) }}
+            bg={backgroundColor}
+          />
+          <Box component="span" fw={700} c="brand">
             {getGroupNameLocalized(group)}
-          </span>
-        </Link>
+          </Box>
+        </Flex>
       </td>
       <td>
         {group.member_count || 0}
         <ApiKeyCount apiKeys={apiKeys} />
       </td>
-      <td className={CS.textRight}>
+      <Box component="td" ta="end">
         {showActionsButton ? (
           <ActionsPopover
             group={group}
@@ -274,7 +278,7 @@ function GroupRow({
             onDeleteGroupClicked={onDeleteGroupClicked}
           />
         ) : null}
-      </td>
+      </Box>
     </tr>
   );
 }
@@ -284,22 +288,14 @@ const ApiKeyCount = ({ apiKeys }: { apiKeys: ApiKey[] }) => {
     return null;
   }
   return (
-    <span className={CS.textLight}>
+    <Box component="span" c="text-light">
       {apiKeys.length === 1
         ? t` (includes 1 API key)`
         : t` (includes ${apiKeys.length} API keys)`}
-    </span>
+    </Box>
   );
 };
 
-const getGroupRowColors = () => [
-  color("error"),
-  color("accent2"),
-  color("brand"),
-  color("accent4"),
-  color("accent1"),
-];
-
 interface GroupsTableProps {
   groups: IGroup[];
   text: string;
@@ -346,11 +342,10 @@ function GroupsTable({
         />
       ) : null}
       {groups &&
-        groups.map((group: IGroup, index: number) => (
+        groups.map((group: IGroup) => (
           <GroupRow
             key={group.id}
             group={group}
-            index={index}
             apiKeys={
               isDefaultGroup(group)
                 ? (apiKeys ?? [])
diff --git a/frontend/src/metabase/admin/people/components/MembershipSelect/MembershipSelect.module.css b/frontend/src/metabase/admin/people/components/MembershipSelect/MembershipSelect.module.css
index b3b0b5db9b86..c01006f10f27 100644
--- a/frontend/src/metabase/admin/people/components/MembershipSelect/MembershipSelect.module.css
+++ b/frontend/src/metabase/admin/people/components/MembershipSelect/MembershipSelect.module.css
@@ -18,7 +18,7 @@
     color: var(--mb-color-text-white);
     background-color: var(--mb-color-filter);
 
-    &:global(.Icon) {
+    :global(.Icon) {
       color: var(--mb-color-text-white) !important;
     }
   }
diff --git a/frontend/src/metabase/admin/people/components/MembershipSelect/MembershipSelect.tsx b/frontend/src/metabase/admin/people/components/MembershipSelect/MembershipSelect.tsx
index c4e3b74d1440..9bf2cb2970de 100644
--- a/frontend/src/metabase/admin/people/components/MembershipSelect/MembershipSelect.tsx
+++ b/frontend/src/metabase/admin/people/components/MembershipSelect/MembershipSelect.tsx
@@ -3,7 +3,6 @@ import cx from "classnames";
 import { Fragment } from "react";
 import { t } from "ttag";
 
-import CS from "metabase/css/core/index.css";
 import {
   getGroupNameLocalized,
   isAdminGroup,
@@ -11,7 +10,7 @@ import {
 } from "metabase/lib/groups";
 import { isNotNull } from "metabase/lib/types";
 import { PLUGIN_GROUP_MANAGERS } from "metabase/plugins";
-import { Icon, Popover } from "metabase/ui";
+import { Box, Flex, Icon, Popover } from "metabase/ui";
 import type { Group, Member } from "metabase-types/api";
 
 import { GroupSummary } from "../GroupSummary";
@@ -87,22 +86,28 @@ export const MembershipSelect = ({
       // prevent clicks on the confirm modal from closing this popover
       closeOnClickOutside={!isConfirmModalOpen}
       onChange={togglePopover}
+      position="bottom-end"
     >
       <Popover.Target>
-        <div
+        <Flex
+          display="inline-flex"
           onClick={openPopover}
-          className={cx(CS.flex, CS.alignCenter)}
+          align="center"
           aria-label="group-summary"
         >
-          <span className={cx(CS.mr1, CS.textMedium)}>
-            <GroupSummary groups={groups} selectedGroupIds={selectedGroupIds} />
-          </span>
-          <Icon className={CS.textLight} name="chevrondown" size={10} />
-        </div>
+          <GroupSummary
+            me="sm"
+            groups={groups}
+            selectedGroupIds={selectedGroupIds}
+          />
+          <Icon c="text-light" name="chevrondown" size={10} />
+        </Flex>
       </Popover.Target>
       <Popover.Dropdown>
         {groups.length === 0 && (
-          <span className={CS.p1}>{emptyListMessage}</span>
+          <Box component="span" p="sm">
+            {emptyListMessage}
+          </Box>
         )}
         {groups.length > 0 && (
           <ul className={S.membershipSelectContainer}>
diff --git a/frontend/src/metabase/admin/people/components/PeopleList.tsx b/frontend/src/metabase/admin/people/components/PeopleList.tsx
index 6b675535205a..af2c3e139b6b 100644
--- a/frontend/src/metabase/admin/people/components/PeopleList.tsx
+++ b/frontend/src/metabase/admin/people/components/PeopleList.tsx
@@ -14,7 +14,7 @@ import { useConfirmation } from "metabase/hooks/use-confirmation";
 import { connect } from "metabase/lib/redux";
 import { PLUGIN_GROUP_MANAGERS } from "metabase/plugins";
 import { getUser, getUserIsAdmin } from "metabase/selectors/user";
-import { Icon } from "metabase/ui";
+import { Box, Flex, Icon, Text } from "metabase/ui";
 import type {
   GroupId,
   Group as IGroup,
@@ -265,7 +265,7 @@ const PeopleListInner = ({
 
   return (
     <LoadingAndErrorWrapper loading={isLoading} error={error} noWrapper>
-      <section className={CS.pb4}>
+      <Box component="section" pb="xl">
         <table
           data-testid="admin-people-list-table"
           className={cx(AdminS.ContentTable, CS.borderBottom)}
@@ -315,17 +315,19 @@ const PeopleListInner = ({
         </table>
 
         {hasUsers && (
-          <div
-            className={cx(CS.flex, CS.alignCenter, CS.justifyBetween, CS.p2)}
+          <Flex
+            align="center"
+            justify="space-between"
+            p="md"
             data-testid="people-list-footer"
           >
-            <div className={cx(CS.textMedium, CS.textBold)}>
+            <Box fw={700}>
               {ngettext(
                 msgid`${total} person found`,
                 `${total} people found`,
                 total,
               )}
-            </div>
+            </Box>
             <PaginationControls
               page={page}
               pageSize={pageSize}
@@ -334,29 +336,26 @@ const PeopleListInner = ({
               onNextPage={onNextPage}
               onPreviousPage={onPreviousPage}
             />
-          </div>
+          </Flex>
         )}
 
         {!hasUsers && (
-          <div
-            className={cx(
-              CS.flex,
-              CS.flexColumn,
-              CS.alignCenter,
-              CS.justifyCenter,
-              CS.p4,
-              CS.textMedium,
-              CS.textCentered,
-            )}
+          <Flex
+            align="center"
+            justify="center"
+            direction="column"
+            p="xl"
+            ta="center"
           >
-            <div className={CS.my3}>
-              <Icon name="search" className={CS.mb1} size={32} />
-              <h3 className={CS.textLight}>{t`No results found`}</h3>
-            </div>
-          </div>
+            <Box my="lg">
+              <Icon name="search" mb="sm" size={32} />
+              <Text c="text-light" fz="lg" fw={700}>{t`No results found`}</Text>
+            </Box>
+          </Flex>
         )}
+
         {modalContent}
-      </section>
+      </Box>
     </LoadingAndErrorWrapper>
   );
 };
diff --git a/frontend/src/metabase/admin/people/components/PeopleListRow.tsx b/frontend/src/metabase/admin/people/components/PeopleListRow.tsx
index 948c745cbaaf..188b194bfaaa 100644
--- a/frontend/src/metabase/admin/people/components/PeopleListRow.tsx
+++ b/frontend/src/metabase/admin/people/components/PeopleListRow.tsx
@@ -1,22 +1,28 @@
-import cx from "classnames";
-import moment from "moment-timezone"; // eslint-disable-line no-restricted-imports -- deprecated usage
+import dayjs from "dayjs";
 import { Fragment, useMemo } from "react";
 import { t } from "ttag";
 
-import EntityMenu from "metabase/components/EntityMenu";
 import LoadingSpinner from "metabase/components/LoadingSpinner";
 import UserAvatar from "metabase/components/UserAvatar";
-import Link from "metabase/core/components/Link";
-import CS from "metabase/css/core/index.css";
-import { color } from "metabase/lib/colors";
+import Link, { ForwardRefLink } from "metabase/core/components/Link";
 import { useSelector } from "metabase/lib/redux";
 import * as Urls from "metabase/lib/urls";
 import { getFullName } from "metabase/lib/user";
 import { PLUGIN_ADMIN_USER_MENU_ITEMS } from "metabase/plugins";
 import { getSetting } from "metabase/selectors/settings";
-import { Icon, Tooltip } from "metabase/ui";
+import {
+  Box,
+  Flex,
+  Icon,
+  Menu,
+  Text,
+  Tooltip,
+  UnstyledButton,
+} from "metabase/ui";
 import type { Group, GroupId, Member, User } from "metabase-types/api";
 
+import { userToColor } from "../colors";
+
 import { MembershipSelect } from "./MembershipSelect";
 import S from "./PeopleListRow.module.css";
 
@@ -64,15 +70,10 @@ export const PeopleListRow = ({
 
   return (
     <tr key={user.id}>
-      <td className={cx(CS.flex, CS.alignCenter)}>
-        <span className={cx(CS.textWhite, CS.inlineBlock)}>
-          <UserAvatar
-            bg={user.is_superuser ? color("accent2") : color("brand")}
-            user={user}
-          />
-        </span>{" "}
-        <span className={cx(CS.ml2, CS.textBold)}>{getName(user)}</span>
-      </td>
+      <Flex component="td" align="center" gap="md" c="text-white">
+        <UserAvatar bg={userToColor(user)} user={user} />
+        <Text fw="700">{getName(user)}</Text>
+      </Flex>
       <td>
         {user.sso_source === "google" ? (
           <Tooltip label={t`Signed up via Google`}>
@@ -88,7 +89,7 @@ export const PeopleListRow = ({
       <td>{user.email}</td>
       {showDeactivated ? (
         <Fragment>
-          <td>{moment(user.updated_at).fromNow()}</td>
+          <td>{dayjs(user.updated_at).fromNow()}</td>
           <td>
             <Tooltip label={t`Reactivate this account`}>
               <Link to={Urls.reactivateUser(user.id)} className={S.refreshLink}>
@@ -116,32 +117,50 @@ export const PeopleListRow = ({
             )}
           </td>
           <td>
-            {user.last_login ? moment(user.last_login).fromNow() : t`Never`}
+            {user.last_login ? dayjs(user.last_login).fromNow() : t`Never`}
           </td>
-          <td className={CS.textRight}>
+          <Box component="td" ta="right">
             {isAdmin && (
-              <EntityMenu
-                triggerIcon="ellipsis"
-                items={[
-                  {
-                    title: t`Edit user`,
-                    link: Urls.editUser(user.id),
-                  },
-                  isPasswordLoginEnabled && {
-                    title: t`Reset password`,
-                    link: Urls.resetPassword(user.id),
-                  },
-                  ...PLUGIN_ADMIN_USER_MENU_ITEMS.flatMap((getItems) =>
+              <Menu shadow="md" position="bottom-end">
+                <Menu.Target>
+                  <UnstyledButton>
+                    <Icon name="ellipsis" />
+                  </UnstyledButton>
+                </Menu.Target>
+
+                <Menu.Dropdown>
+                  <Menu.Item
+                    component={ForwardRefLink}
+                    to={Urls.editUser(user.id)}
+                  >
+                    {t`Edit user`}
+                  </Menu.Item>
+
+                  {isPasswordLoginEnabled && (
+                    <Menu.Item
+                      component={ForwardRefLink}
+                      to={Urls.resetPassword(user.id)}
+                    >
+                      {t`Reset password`}
+                    </Menu.Item>
+                  )}
+
+                  {PLUGIN_ADMIN_USER_MENU_ITEMS.flatMap((getItems) =>
                     getItems(user),
-                  ),
-                  !isCurrentUser && {
-                    title: t`Deactivate user`,
-                    link: Urls.deactivateUser(user.id),
-                  },
-                ]}
-              />
+                  )}
+
+                  {!isCurrentUser && (
+                    <Menu.Item
+                      component={ForwardRefLink}
+                      to={Urls.deactivateUser(user.id)}
+                    >
+                      {t`Deactivate user`}
+                    </Menu.Item>
+                  )}
+                </Menu.Dropdown>
+              </Menu>
             )}
-          </td>
+          </Box>
         </Fragment>
       )}
     </tr>
diff --git a/frontend/src/metabase/admin/people/containers/EditUserModal/EditUserModal.tsx b/frontend/src/metabase/admin/people/containers/EditUserModal/EditUserModal.tsx
index c6fe81280b2b..d187d6b93ec7 100644
--- a/frontend/src/metabase/admin/people/containers/EditUserModal/EditUserModal.tsx
+++ b/frontend/src/metabase/admin/people/containers/EditUserModal/EditUserModal.tsx
@@ -7,7 +7,7 @@ import {
   useUpdateUserMutation,
 } from "metabase/api";
 import { LoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper";
-import ModalContent from "metabase/components/ModalContent";
+import { Modal } from "metabase/ui";
 import type { User } from "metabase-types/api";
 
 import { UserForm } from "../../forms/UserForm";
@@ -19,9 +19,19 @@ interface EditUserModalProps {
 
 export const EditUserModal = ({ onClose, params }: EditUserModalProps) => {
   const userId = params.userId ? parseInt(params.userId) : null;
-  const { data: user, isLoading } = useGetUserQuery(userId ?? skipToken);
+  const { data: user, isLoading, error } = useGetUserQuery(userId ?? skipToken);
   const [updateUser] = useUpdateUserMutation();
-  const initialValues = useMemo(() => getInitialValues(user), [user]);
+
+  const initialValues = useMemo(
+    () => ({
+      first_name: user?.first_name,
+      last_name: user?.last_name,
+      email: user?.email,
+      user_group_memberships: user?.user_group_memberships || [],
+      login_attributes: user?.login_attributes || {},
+    }),
+    [user],
+  );
 
   const handleSubmit = async (newValues: Partial<User>) => {
     if (userId == null) {
@@ -29,39 +39,22 @@ export const EditUserModal = ({ onClose, params }: EditUserModalProps) => {
     }
 
     // first name and last name keys need to be present, so they can potentially be removed
-    await updateUser({
-      id: userId,
-      first_name: null,
-      last_name: null,
-      ...newValues,
-    }).unwrap();
+    const defaultValues = { id: userId, first_name: null, last_name: null };
+    const updatedUser = { ...defaultValues, ...newValues };
+    await updateUser(updatedUser).unwrap();
 
     onClose();
   };
 
   return (
-    <ModalContent title="Edit user" onClose={onClose}>
-      <LoadingAndErrorWrapper loading={isLoading}>
-        {() => {
-          return (
-            <UserForm
-              onCancel={onClose}
-              initialValues={initialValues}
-              onSubmit={handleSubmit}
-            />
-          );
-        }}
+    <Modal opened title="Edit user" padding="xl" onClose={onClose}>
+      <LoadingAndErrorWrapper loading={isLoading} error={error}>
+        <UserForm
+          onCancel={onClose}
+          initialValues={initialValues}
+          onSubmit={handleSubmit}
+        />
       </LoadingAndErrorWrapper>
-    </ModalContent>
+    </Modal>
   );
 };
-
-const getInitialValues = (user?: User) => {
-  return {
-    first_name: user?.first_name,
-    last_name: user?.last_name,
-    email: user?.email,
-    user_group_memberships: user?.user_group_memberships || [],
-    login_attributes: user?.login_attributes || {},
-  };
-};
diff --git a/frontend/src/metabase/admin/people/containers/NewUserModal.tsx b/frontend/src/metabase/admin/people/containers/NewUserModal.tsx
index 2573bec82454..4db71d94e5c5 100644
--- a/frontend/src/metabase/admin/people/containers/NewUserModal.tsx
+++ b/frontend/src/metabase/admin/people/containers/NewUserModal.tsx
@@ -1,10 +1,10 @@
 import { push } from "react-router-redux";
 import { t } from "ttag";
 
-import ModalContent from "metabase/components/ModalContent";
 import Users from "metabase/entities/users";
 import { useDispatch } from "metabase/lib/redux";
 import * as Urls from "metabase/lib/urls";
+import { Modal } from "metabase/ui";
 import type { User as UserType } from "metabase-types/api";
 
 import { UserForm } from "../forms/UserForm";
@@ -25,13 +25,13 @@ export const NewUserModal = ({ onClose }: NewUserModalProps) => {
   };
 
   return (
-    <ModalContent title="Create user" onClose={onClose}>
+    <Modal opened title="Create user" padding="xl" onClose={onClose}>
       <UserForm
         initialValues={{}}
         submitText={t`Create`}
         onCancel={onClose}
         onSubmit={handleSubmit}
       />
-    </ModalContent>
+    </Modal>
   );
 };
diff --git a/frontend/src/metabase/admin/people/containers/PeopleListingApp.tsx b/frontend/src/metabase/admin/people/containers/PeopleListingApp.tsx
index cd813c6b1302..bc0877d369b0 100644
--- a/frontend/src/metabase/admin/people/containers/PeopleListingApp.tsx
+++ b/frontend/src/metabase/admin/people/containers/PeopleListingApp.tsx
@@ -1,12 +1,10 @@
-import cx from "classnames";
 import { t } from "ttag";
 
 import { AdminPaneLayout } from "metabase/components/AdminPaneLayout";
-import CS from "metabase/css/core/index.css";
 import { useSelector } from "metabase/lib/redux";
 import * as Urls from "metabase/lib/urls";
 import { getUserIsAdmin } from "metabase/selectors/user";
-import { Group, Icon, Input, Radio } from "metabase/ui";
+import { Flex, Group, Icon, Input, Radio } from "metabase/ui";
 
 import { PeopleList } from "../components/PeopleList";
 import { USER_STATUS } from "../constants";
@@ -32,10 +30,10 @@ export function PeopleListingApp({ children }: { children: React.ReactNode }) {
   };
 
   const headingContent = (
-    <div className={cx(CS.mb2, CS.flex, CS.alignCenter)}>
+    <Flex align="center" mb="xl">
       <Input
         miw="14rem"
-        mr="2rem"
+        mr="xl"
         fz="sm"
         type="text"
         placeholder={t`Find someone`}
@@ -62,7 +60,7 @@ export function PeopleListingApp({ children }: { children: React.ReactNode }) {
           </Group>
         </Radio.Group>
       )}
-    </div>
+    </Flex>
   );
 
   const buttonText =
diff --git a/frontend/src/metabase/admin/people/containers/UserActivationModal.tsx b/frontend/src/metabase/admin/people/containers/UserActivationModal.tsx
index 0a8f8f76e3fa..ca0ff85e6824 100644
--- a/frontend/src/metabase/admin/people/containers/UserActivationModal.tsx
+++ b/frontend/src/metabase/admin/people/containers/UserActivationModal.tsx
@@ -1,10 +1,7 @@
 import { t } from "ttag";
 import _ from "underscore";
 
-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 { ConfirmModal } from "metabase/components/ConfirmModal";
 import Users from "metabase/entities/users";
 import { connect } from "metabase/lib/redux";
 import type { User } from "metabase-types/api";
@@ -29,39 +26,33 @@ const UserActivationModalInner = ({
 
   if (user.is_active) {
     return (
-      <ModalContent
+      <ConfirmModal
+        opened
         title={t`Deactivate ${user.common_name}?`}
+        message={t`${user.common_name} won't be able to log in anymore.`}
+        confirmButtonText={t`Deactivate`}
         onClose={onClose}
-      >
-        <Text>{t`${user.common_name} won't be able to log in anymore.`}</Text>
-        <Button
-          className={CS.mlAuto}
-          danger
-          onClick={() => user.deactivate() && onClose()}
-        >
-          {t`Deactivate`}
-        </Button>
-      </ModalContent>
-    );
-  } else {
-    return (
-      <ModalContent
-        title={t`Reactivate ${user.common_name}?`}
-        onClose={onClose}
-      >
-        <Text>
-          {t`They'll be able to log in again, and they'll be placed back into the groups they were in before their account was deactivated.`}
-        </Text>
-        <Button
-          className={CS.mlAuto}
-          danger
-          onClick={() => user.reactivate() && onClose()}
-        >
-          {t`Reactivate`}
-        </Button>
-      </ModalContent>
+        onConfirm={() => {
+          user.deactivate();
+          onClose();
+        }}
+      />
     );
   }
+
+  return (
+    <ConfirmModal
+      opened
+      title={t`Reactivate ${user.common_name}?`}
+      message={t`They'll be able to log in again, and they'll be placed back into the groups they were in before their account was deactivated.`}
+      confirmButtonText={t`Reactivate`}
+      onClose={onClose}
+      onConfirm={() => {
+        user.reactivate();
+        onClose();
+      }}
+    />
+  );
 };
 
 export const UserActivationModal = _.compose(
diff --git a/frontend/src/metabase/admin/people/containers/UserPasswordResetModal.tsx b/frontend/src/metabase/admin/people/containers/UserPasswordResetModal.tsx
index 07eca5feb90f..79369e2f1d9a 100644
--- a/frontend/src/metabase/admin/people/containers/UserPasswordResetModal.tsx
+++ b/frontend/src/metabase/admin/people/containers/UserPasswordResetModal.tsx
@@ -1,17 +1,14 @@
-import cx from "classnames";
-import { Component } from "react";
 import { goBack } from "react-router-redux";
+import { useUnmount } from "react-use";
 import { t } from "ttag";
 import _ from "underscore";
 
-import ModalContent from "metabase/components/ModalContent";
+import { ConfirmModal } from "metabase/components/ConfirmModal";
 import PasswordReveal from "metabase/components/PasswordReveal";
-import Button from "metabase/core/components/Button";
-import CS from "metabase/css/core/index.css";
 import Users from "metabase/entities/users";
 import { connect } from "metabase/lib/redux";
 import MetabaseSettings from "metabase/lib/settings";
-import { Flex } from "metabase/ui";
+import { Text } from "metabase/ui";
 import type { User } from "metabase-types/api";
 import type { State } from "metabase-types/store";
 
@@ -33,64 +30,57 @@ interface UserPasswordResetModalProps extends UserPasswordResetModalParams {
   temporaryPassword: string;
 }
 
-class UserPasswordResetModalInner extends Component<UserPasswordResetModalProps> {
-  state = {
-    resetButtonDisabled: false,
-  };
-
-  componentWillUnmount() {
-    this.props.clearTemporaryPassword(this.props.params.userId);
-  }
+const UserPasswordResetModalInner = ({
+  clearTemporaryPassword,
+  emailConfigured,
+  onClose,
+  params,
+  temporaryPassword,
+  user,
+}: UserPasswordResetModalProps) => {
+  useUnmount(() => {
+    clearTemporaryPassword(params.userId);
+  });
 
-  handleClose = () => {
-    this.setState({ resetButtonDisabled: false });
-    this.props.onClose();
+  const handleResetConfirm = async () => {
+    if (emailConfigured) {
+      await user.resetPasswordEmail();
+    } else {
+      await user.resetPasswordManual();
+    }
   };
 
-  render() {
-    const { user, emailConfigured, temporaryPassword } = this.props;
-
-    return temporaryPassword ? (
-      <ModalContent
+  if (temporaryPassword) {
+    return (
+      <ConfirmModal
+        opened
         title={t`${user.common_name}'s password has been reset`}
-        footer={<Button primary onClick={this.handleClose}>{t`Done`}</Button>}
-        onClose={this.handleClose}
-      >
-        <span
-          className={cx(CS.pb3, CS.block)}
-        >{t`Here’s a temporary password they can use to log in and then change their password.`}</span>
-
-        <PasswordReveal password={temporaryPassword} />
-      </ModalContent>
-    ) : (
-      <ModalContent
-        title={t`Reset ${user.common_name}'s password?`}
-        onClose={this.handleClose}
-      >
-        <p>{t`Are you sure you want to do this?`}</p>
-
-        <Flex>
-          <Button
-            className={CS.mlAuto}
-            disabled={this.state.resetButtonDisabled}
-            onClick={async () => {
-              this.setState({ resetButtonDisabled: true });
-              if (emailConfigured) {
-                await user.resetPasswordEmail();
-                this.handleClose();
-              } else {
-                await user.resetPasswordManual();
-              }
-            }}
-            danger
-          >
-            {t`Reset password`}
-          </Button>
-        </Flex>
-      </ModalContent>
+        onConfirm={onClose}
+        confirmButtonProps={{ color: "brand", variant: "filled" }}
+        confirmButtonText={t`Done`}
+        closeButtonText={null}
+        onClose={onClose}
+        message={
+          <>
+            <Text pb="lg">{t`Here’s a temporary password they can use to log in and then change their password.`}</Text>
+            <PasswordReveal password={temporaryPassword} />
+          </>
+        }
+      />
     );
   }
-}
+
+  return (
+    <ConfirmModal
+      opened
+      title={t`Reset ${user.common_name}'s password?`}
+      onClose={onClose}
+      confirmButtonText={t`Reset password`}
+      onConfirm={handleResetConfirm}
+      message={t`Are you sure you want to do this?`}
+    />
+  );
+};
 
 export const UserPasswordResetModal = _.compose(
   Users.load({
diff --git a/frontend/src/metabase/admin/people/containers/UserSuccessModal.tsx b/frontend/src/metabase/admin/people/containers/UserSuccessModal.tsx
index ea8fe917da08..f923fdcb07ab 100644
--- a/frontend/src/metabase/admin/people/containers/UserSuccessModal.tsx
+++ b/frontend/src/metabase/admin/people/containers/UserSuccessModal.tsx
@@ -4,10 +4,9 @@ import { push } from "react-router-redux";
 import { jt, t } from "ttag";
 
 import { useGetUserQuery } from "metabase/api";
+import { ConfirmModal } from "metabase/components/ConfirmModal";
 import { LoadingAndErrorWrapper } from "metabase/components/LoadingAndErrorWrapper";
-import ModalContent from "metabase/components/ModalContent";
 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 { useDispatch, useSelector } from "metabase/lib/redux";
@@ -50,20 +49,26 @@ export function UserSuccessModal({ params }: UserSuccessModalProps) {
   }
 
   return (
-    <ModalContent
+    <ConfirmModal
+      opened
       title={t`${user.common_name} has been added`}
-      footer={<Button primary onClick={handleClose}>{t`Done`}</Button>}
+      padding="xl"
       onClose={handleClose}
-    >
-      {temporaryPassword ? (
-        <PasswordSuccess user={user} temporaryPassword={temporaryPassword} />
-      ) : (
-        <EmailSuccess
-          isSsoEnabled={hasSsoEnabled && !hasPasswordLoginEnabled}
-          user={user}
-        />
-      )}
-    </ModalContent>
+      onConfirm={handleClose}
+      closeButtonText={null}
+      confirmButtonProps={{ color: "brand" }}
+      confirmButtonText={t`Done`}
+      message={
+        temporaryPassword ? (
+          <PasswordSuccess user={user} temporaryPassword={temporaryPassword} />
+        ) : (
+          <EmailSuccess
+            isSsoEnabled={hasSsoEnabled && !hasPasswordLoginEnabled}
+            user={user}
+          />
+        )
+      }
+    />
   );
 }
 
@@ -109,10 +114,7 @@ const PasswordSuccess = ({
     </Box>
 
     <PasswordReveal password={temporaryPassword} />
-    <div
-      style={{ paddingLeft: "5em", paddingRight: "5em" }}
-      className={cx(CS.pt4, CS.textCentered)}
-    >
+    <Box ta="center" p="2rem 4.5rem 0">
       {jt`If you want to be able to send email invites, just go to the ${(
         <Link
           key="link"
@@ -122,6 +124,6 @@ const PasswordSuccess = ({
           Email Settings
         </Link>
       )} page.`}
-    </div>
+    </Box>
   </div>
 );
diff --git a/frontend/src/metabase/admin/people/forms/UserForm.tsx b/frontend/src/metabase/admin/people/forms/UserForm.tsx
index 2963d19571f2..3649bd5aeb98 100644
--- a/frontend/src/metabase/admin/people/forms/UserForm.tsx
+++ b/frontend/src/metabase/admin/people/forms/UserForm.tsx
@@ -48,14 +48,14 @@ export const UserForm = ({
             title={t`First name`}
             placeholder={t`Johnny`}
             label={t`First name`}
-            mb="1rem"
+            mb="md"
           />
           <FormTextInput
             name="last_name"
             title={t`Last name`}
             placeholder={t`Appleseed`}
             label={t`Last name`}
-            mb="1rem"
+            mb="md"
           />
           <FormTextInput
             name="email"
@@ -64,7 +64,7 @@ export const UserForm = ({
             placeholder="nicetoseeyou@email.com"
             label={t`Email`}
             required
-            mb="1rem"
+            mb="md"
           />
           <FormGroupsWidget name="user_group_memberships" />
           <PLUGIN_ADMIN_USER_FORM_FIELDS.FormLoginAttributes />
diff --git a/frontend/src/metabase/admin/routes.jsx b/frontend/src/metabase/admin/routes.jsx
index a628b6a419f7..6dcb09c85952 100644
--- a/frontend/src/metabase/admin/routes.jsx
+++ b/frontend/src/metabase/admin/routes.jsx
@@ -98,16 +98,16 @@ const getRoutes = (store, CanAccessSettings, IsAdmin) => (
           </Route>
 
           <Route path="" component={PeopleListingApp}>
-            <ModalRoute path="new" modal={NewUserModal} />
+            <ModalRoute path="new" modal={NewUserModal} noWrap />
           </Route>
 
           <Route path=":userId" component={PeopleListingApp}>
             <IndexRedirect to="/admin/people" />
-            <ModalRoute path="edit" modal={EditUserModal} />
-            <ModalRoute path="success" modal={UserSuccessModal} />
-            <ModalRoute path="reset" modal={UserPasswordResetModal} />
-            <ModalRoute path="deactivate" modal={UserActivationModal} />
-            <ModalRoute path="reactivate" modal={UserActivationModal} />
+            <ModalRoute path="edit" modal={EditUserModal} noWrap />
+            <ModalRoute path="success" modal={UserSuccessModal} noWrap />
+            <ModalRoute path="reset" modal={UserPasswordResetModal} noWrap />
+            <ModalRoute path="deactivate" modal={UserActivationModal} noWrap />
+            <ModalRoute path="reactivate" modal={UserActivationModal} noWrap />
             {PLUGIN_ADMIN_USER_MENU_ROUTES.map((getRoutes, index) => (
               <Fragment key={index}>{getRoutes(store)}</Fragment>
             ))}
diff --git a/frontend/src/metabase/admin/settings/components/widgets/GroupMappingsWidget/GroupSelect/GroupSelect.tsx b/frontend/src/metabase/admin/settings/components/widgets/GroupMappingsWidget/GroupSelect/GroupSelect.tsx
index 6c2993bbee9d..e6f605e0b372 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/GroupMappingsWidget/GroupSelect/GroupSelect.tsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/GroupMappingsWidget/GroupSelect/GroupSelect.tsx
@@ -60,9 +60,11 @@ export const GroupSelect = ({
 }: GroupSelectProps) => {
   const triggerElement = (
     <div className={cx(CS.flex, CS.alignCenter)}>
-      <span className={cx(CS.mr1, CS.textMedium)}>
-        <GroupSummary groups={groups} selectedGroupIds={selectedGroupIds} />
-      </span>
+      <GroupSummary
+        mr="0.5rem"
+        groups={groups}
+        selectedGroupIds={selectedGroupIds}
+      />
       <Icon className={CS.textLight} name="chevrondown" size={10} />
     </div>
   );
diff --git a/frontend/src/metabase/components/ConfirmModal/ConfirmModal.tsx b/frontend/src/metabase/components/ConfirmModal/ConfirmModal.tsx
index 2215f5d50e09..ed75ff98a09e 100644
--- a/frontend/src/metabase/components/ConfirmModal/ConfirmModal.tsx
+++ b/frontend/src/metabase/components/ConfirmModal/ConfirmModal.tsx
@@ -1,52 +1,76 @@
-import type { ReactNode } from "react";
+import { type ReactNode, useState } from "react";
 import { t } from "ttag";
 import _ from "underscore";
 
-import { Button, Flex, Modal, type ModalProps, Text } from "metabase/ui";
+import {
+  Button,
+  type ButtonProps,
+  Flex,
+  Modal,
+  type ModalProps,
+  Text,
+} from "metabase/ui";
 
 interface ConfirmModal extends ModalProps {
-  "data-testid"?: string;
   title: string | ReactNode;
   content?: string;
   message?: string | ReactNode;
-  onConfirm?: () => void;
+  onConfirm?: () => void | Promise<void>;
   confirmButtonText?: string;
-  confirmButtonPrimary?: boolean;
-  closeButtonText?: string;
+  confirmButtonProps?: Omit<ButtonProps, "onClick" | "children">;
+  closeButtonText?: string | null;
+  closeButtonProps?: Omit<ButtonProps, "onClick" | "children">;
+  errorMessage?: string;
 }
 
 export const ConfirmModal = ({
-  "data-testid": dataTestId,
   title,
   content,
   message = t`Are you sure you want to do this?`,
   onClose,
   onConfirm = _.noop,
   confirmButtonText = t`Yes`,
-  confirmButtonPrimary = false,
+  confirmButtonProps = {},
   closeButtonText = t`Cancel`,
+  closeButtonProps = {},
+  errorMessage,
   ...props
-}: ConfirmModal) => (
-  <Modal
-    data-testid={dataTestId}
-    title={title}
-    onClose={onClose}
-    size="lg"
-    {...props}
-  >
-    <Flex direction="column" gap="lg" mt="lg">
-      {content ? <Text>{content}</Text> : null}
-      <Text>{message}</Text>
-      <Flex justify="flex-end" gap="md">
-        <Button onClick={onClose}>{closeButtonText}</Button>
-        <Button
-          color={confirmButtonPrimary ? "primary" : "danger"}
-          variant={confirmButtonPrimary ? "primary" : "filled"}
-          onClick={onConfirm}
-        >
-          {confirmButtonText}
-        </Button>
+}: ConfirmModal) => {
+  const [confirming, setConfirming] = useState(false);
+  const handleConfirm = async () => {
+    const confirm = onConfirm();
+    if (confirm instanceof Promise) {
+      setConfirming(true);
+      await confirm;
+    }
+    setConfirming(false);
+  };
+
+  return (
+    <Modal title={title} onClose={onClose} size="lg" {...props}>
+      <Flex direction="column" gap="lg" mt="lg">
+        {content ? <Text>{content}</Text> : null}
+        <Text>{message}</Text>
+        <Flex align="center" justify="space-between" gap="md">
+          {errorMessage ? <Text c="danger">{errorMessage} </Text> : <div />}
+          <Flex align="center" justify="flex-end" gap="md">
+            {closeButtonText && (
+              <Button {...closeButtonProps} onClick={onClose}>
+                {closeButtonText}
+              </Button>
+            )}
+            <Button
+              color="danger"
+              variant="filled"
+              {...confirmButtonProps}
+              disabled={confirming}
+              onClick={handleConfirm}
+            >
+              {confirmButtonText}
+            </Button>
+          </Flex>
+        </Flex>
       </Flex>
-    </Flex>
-  </Modal>
-);
+    </Modal>
+  );
+};
diff --git a/frontend/src/metabase/plugins/index.ts b/frontend/src/metabase/plugins/index.ts
index 5f90aa22b6f0..fd847b70f50e 100644
--- a/frontend/src/metabase/plugins/index.ts
+++ b/frontend/src/metabase/plugins/index.ts
@@ -193,7 +193,7 @@ export const PLUGIN_ADMIN_USER_FORM_FIELDS = {
 
 // menu items in people management tab
 export const PLUGIN_ADMIN_USER_MENU_ITEMS = [] as Array<
-  (user: User) => { title: string; link: string }
+  (user: User) => React.ReactNode
 >;
 export const PLUGIN_ADMIN_USER_MENU_ROUTES = [];
 
diff --git a/frontend/src/metabase/query_builder/components/MoveQuestionModal/MoveQuestionModal.tsx b/frontend/src/metabase/query_builder/components/MoveQuestionModal/MoveQuestionModal.tsx
index a28075f753fd..411360c6ae95 100644
--- a/frontend/src/metabase/query_builder/components/MoveQuestionModal/MoveQuestionModal.tsx
+++ b/frontend/src/metabase/query_builder/components/MoveQuestionModal/MoveQuestionModal.tsx
@@ -189,7 +189,7 @@ export const MoveQuestionModal = ({
             </Radio.Group>
           </>
         }
-        confirmButtonPrimary
+        confirmButtonProps={{ color: "brand", variant: "filled" }}
         confirmButtonText={t`Done`}
       />
     );
@@ -210,8 +210,8 @@ export const MoveQuestionModal = ({
           </Title>
         }
         message={t`You can move it to a collection if you want to use it in both dashboards.`}
-        confirmButtonPrimary
         confirmButtonText={t`Okay`}
+        confirmButtonProps={{ color: "brand", variant: "filled" }}
       />
     );
   }
diff --git a/frontend/src/metabase/query_builder/components/expressions/Editor/CloseModal/CloseModal.tsx b/frontend/src/metabase/query_builder/components/expressions/Editor/CloseModal/CloseModal.tsx
index bf0250d4b7d2..27f62bf9e925 100644
--- a/frontend/src/metabase/query_builder/components/expressions/Editor/CloseModal/CloseModal.tsx
+++ b/frontend/src/metabase/query_builder/components/expressions/Editor/CloseModal/CloseModal.tsx
@@ -20,7 +20,7 @@ export function CloseModal({
       onConfirm={onDiscardChanges}
       onClose={onKeepEditing || _.noop}
       confirmButtonText={t`Discard changes`}
-      confirmButtonPrimary
+      confirmButtonProps={{ color: "brand", variant: "filled" }}
       closeButtonText={t`Keep editing`}
     />
   );
diff --git a/frontend/src/metabase/ui/components/inputs/Pill/index.ts b/frontend/src/metabase/ui/components/inputs/Pill/index.ts
index d65f86df4010..798464a4469f 100644
--- a/frontend/src/metabase/ui/components/inputs/Pill/index.ts
+++ b/frontend/src/metabase/ui/components/inputs/Pill/index.ts
@@ -1 +1,2 @@
+export { Pill } from "@mantine/core";
 export { pillOverrides } from "./Pill.config";


Reviews:
user: login: rafpaf
body: Looks great! I noted some nits
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T17:10:06Z
user: login: sloansparger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:41:40Z
user: login: sloansparger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:44:04Z
user: login: sloansparger
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:46:25Z

Review comments:
diff_hunk: @@ -19,38 +18,37 @@ export const UnsubscribeUserForm = ({
   onUnsubscribe,
   onClose,
 }: UnsubscribeUserFormProps) => {
-  const [error, setError] = useState<any>();
+  const [errorMessage, setErrorMessage] = useState<string>();
 
   const handleConfirmClick = useCallback(async () => {
     try {
       await onUnsubscribe(user);
       onClose();
     } catch (error) {
-      setError(error);
+      const msg = getResponseErrorMessage(error);
+      setErrorMessage(msg ?? t`Unknown error encountered`);
     }
   }, [user, onUnsubscribe, onClose]);
 
   return (
-    <ModalContent
-      title={t`Unsubscribe ${user.common_name} from all subscriptions and alerts?`}
-      footer={[
-        error ? <FormMessage key="message" formError={error} /> : null,
-        <Button key="cancel" onClick={onClose}>{t`Cancel`}</Button>,
-        <Button
-          key="submit"
-          danger
-          onClick={handleConfirmClick}
-        >{t`Unsubscribe`}</Button>,
-      ]}
+    <ConfirmModal
+      opened
+      errorMessage={errorMessage}
       onClose={onClose}
-    >
-      <ModalMessage>
-        {t`This will delete any dashboard subscriptions or alerts ${user.common_name} has created, and remove them as a recipient from any other subscriptions or alerts.`}
-      </ModalMessage>
-      <ModalMessage>
-        {/* eslint-disable-next-line no-literal-metabase-strings -- Metabase settings */}
-        {t`This does not affect email distribution lists that are managed outside of Metabase.`}
-      </ModalMessage>
-    </ModalContent>
+      onConfirm={handleConfirmClick}
+      title={t`Unsubscribe ${user.common_name} from all subscriptions and alerts?`}
+      confirmButtonText={t`Unsubscribe`}
+      message={
+        <>
+          <ModalMessage>
+            {t`This will delete any dashboard subscriptions or alerts ${user.common_name} has created, and remove them as a recipient from any other subscriptions or alerts.`}
+          </ModalMessage>
+          <ModalMessage>
+            {/* eslint-disable-next-line no-literal-metabase-strings -- Metabase settings */}
path: enterprise/frontend/src/metabase-enterprise/audit_app/components/UnsubscribeUserForm/UnsubscribeUserForm.tsx
user: login: rafpaf
body: nit:

```suggestion
            {/* eslint-disable-next-line no-literal-metabase-strings -- This is only shown to admins */}
```

I know you didn't add this line but I think the doc could be a little clearer
created_at: 2025-05-05T16:40:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,131 +52,70 @@ export function AddMemberRow({
     [selectedUsersById, excludeIds, users],
   );
 
+  const suggestedUsers = useMemo(() => {
+    const input = text.toLowerCase();
+    return availableToSelectUsers.filter((user) =>
+      (user.common_name || "").toLowerCase().includes(input),
+    );
+  }, [availableToSelectUsers, text]);
+
   return (
     <tr>
       <td colSpan={4} style={{ padding: 0 }}>
-        <AddRow
-          ref={rowRef}
-          value={text}
-          isValid={selectedUsersById.size > 0}
-          placeholder={t`Julie McMemberson`}
-          onChange={(e) => setText(e.target.value)}
-          onDone={handleDone}
-          onCancel={onCancel}
+        <Popover
+          opened={suggestedUsers.length > 0}
+          position="bottom-start"
+          withArrow
+          shadow="md"
         >
-          {Array.from(selectedUsersById.values()).map((user) => (
-            <div
-              key={user.id}
-              className={cx(
-                CS.bgMedium,
-                CS.p1,
-                CS.px2,
-                CS.mr1,
-                CS.rounded,
-                CS.flex,
-                CS.alignCenter,
-              )}
-            >
-              {user.common_name}
-              <Icon
-                className={cx(
-                  CS.pl1,
-                  CS.cursorPointer,
-                  CS.textSlate,
-                  CS.textMediumHover,
-                )}
-                name="close"
-                onClick={() => handleRemoveUser(user)}
-              />
+          <Popover.Target>
+            <div>
+              <AddRow
+                value={text}
+                isValid={selectedUsersById.size > 0}
+                placeholder={t`Julie McMemberson`}
path: frontend/src/metabase/admin/people/components/AddMemberRow.tsx
user: login: rafpaf
body: Are we localizing this string so that we can swap in a culturally specific placeholder name? If so, some ttag context would help
created_at: 2025-05-05T16:46:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,131 +52,70 @@ export function AddMemberRow({
     [selectedUsersById, excludeIds, users],
   );
 
+  const suggestedUsers = useMemo(() => {
+    const input = text.toLowerCase();
+    return availableToSelectUsers.filter((user) =>
+      (user.common_name || "").toLowerCase().includes(input),
+    );
+  }, [availableToSelectUsers, text]);
+
   return (
     <tr>
       <td colSpan={4} style={{ padding: 0 }}>
-        <AddRow
-          ref={rowRef}
-          value={text}
-          isValid={selectedUsersById.size > 0}
-          placeholder={t`Julie McMemberson`}
-          onChange={(e) => setText(e.target.value)}
-          onDone={handleDone}
-          onCancel={onCancel}
+        <Popover
+          opened={suggestedUsers.length > 0}
+          position="bottom-start"
+          withArrow
+          shadow="md"
         >
-          {Array.from(selectedUsersById.values()).map((user) => (
-            <div
-              key={user.id}
-              className={cx(
-                CS.bgMedium,
-                CS.p1,
-                CS.px2,
-                CS.mr1,
-                CS.rounded,
-                CS.flex,
-                CS.alignCenter,
-              )}
-            >
-              {user.common_name}
-              <Icon
-                className={cx(
-                  CS.pl1,
-                  CS.cursorPointer,
-                  CS.textSlate,
-                  CS.textMediumHover,
-                )}
-                name="close"
-                onClick={() => handleRemoveUser(user)}
-              />
+          <Popover.Target>
+            <div>
+              <AddRow
+                value={text}
+                isValid={selectedUsersById.size > 0}
+                placeholder={t`Julie McMemberson`}
+                onChange={(e) => setText(e.target.value)}
+                onDone={handleDone}
+                onCancel={onCancel}
+              >
+                {Array.from(selectedUsersById.values()).map((user, index) => (
+                  <Pill
+                    key={user.id}
+                    size="xl"
+                    bg="bg-medium"
+                    c="text-primary"
+                    ml={index > 0 ? "sm" : ""}
path: frontend/src/metabase/admin/people/components/AddMemberRow.tsx
user: login: rafpaf
body: nit: Maybe `ms` for margin-inline-start?
created_at: 2025-05-05T16:47:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,131 +52,70 @@ export function AddMemberRow({
     [selectedUsersById, excludeIds, users],
   );
 
+  const suggestedUsers = useMemo(() => {
+    const input = text.toLowerCase();
+    return availableToSelectUsers.filter((user) =>
+      (user.common_name || "").toLowerCase().includes(input),
+    );
+  }, [availableToSelectUsers, text]);
+
   return (
     <tr>
       <td colSpan={4} style={{ padding: 0 }}>
-        <AddRow
-          ref={rowRef}
-          value={text}
-          isValid={selectedUsersById.size > 0}
-          placeholder={t`Julie McMemberson`}
-          onChange={(e) => setText(e.target.value)}
-          onDone={handleDone}
-          onCancel={onCancel}
+        <Popover
+          opened={suggestedUsers.length > 0}
+          position="bottom-start"
+          withArrow
+          shadow="md"
         >
-          {Array.from(selectedUsersById.values()).map((user) => (
-            <div
-              key={user.id}
-              className={cx(
-                CS.bgMedium,
-                CS.p1,
-                CS.px2,
-                CS.mr1,
-                CS.rounded,
-                CS.flex,
-                CS.alignCenter,
-              )}
-            >
-              {user.common_name}
-              <Icon
-                className={cx(
-                  CS.pl1,
-                  CS.cursorPointer,
-                  CS.textSlate,
-                  CS.textMediumHover,
-                )}
-                name="close"
-                onClick={() => handleRemoveUser(user)}
-              />
+          <Popover.Target>
+            <div>
+              <AddRow
+                value={text}
+                isValid={selectedUsersById.size > 0}
+                placeholder={t`Julie McMemberson`}
+                onChange={(e) => setText(e.target.value)}
+                onDone={handleDone}
+                onCancel={onCancel}
+              >
+                {Array.from(selectedUsersById.values()).map((user, index) => (
+                  <Pill
+                    key={user.id}
+                    size="xl"
+                    bg="bg-medium"
+                    c="text-primary"
+                    ml={index > 0 ? "sm" : ""}
path: frontend/src/metabase/admin/people/components/AddMemberRow.tsx
user: login: rafpaf
body: Or would `<Group gap="sm">` work here, as an alternative to the index-based approach?
created_at: 2025-05-05T16:48:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,131 +52,70 @@ export function AddMemberRow({
     [selectedUsersById, excludeIds, users],
   );
 
+  const suggestedUsers = useMemo(() => {
+    const input = text.toLowerCase();
+    return availableToSelectUsers.filter((user) =>
+      (user.common_name || "").toLowerCase().includes(input),
+    );
+  }, [availableToSelectUsers, text]);
+
   return (
     <tr>
       <td colSpan={4} style={{ padding: 0 }}>
-        <AddRow
-          ref={rowRef}
-          value={text}
-          isValid={selectedUsersById.size > 0}
-          placeholder={t`Julie McMemberson`}
-          onChange={(e) => setText(e.target.value)}
-          onDone={handleDone}
-          onCancel={onCancel}
+        <Popover
+          opened={suggestedUsers.length > 0}
+          position="bottom-start"
+          withArrow
+          shadow="md"
         >
-          {Array.from(selectedUsersById.values()).map((user) => (
-            <div
-              key={user.id}
-              className={cx(
-                CS.bgMedium,
-                CS.p1,
-                CS.px2,
-                CS.mr1,
-                CS.rounded,
-                CS.flex,
-                CS.alignCenter,
-              )}
-            >
-              {user.common_name}
-              <Icon
-                className={cx(
-                  CS.pl1,
-                  CS.cursorPointer,
-                  CS.textSlate,
-                  CS.textMediumHover,
-                )}
-                name="close"
-                onClick={() => handleRemoveUser(user)}
-              />
+          <Popover.Target>
+            <div>
+              <AddRow
+                value={text}
+                isValid={selectedUsersById.size > 0}
+                placeholder={t`Julie McMemberson`}
+                onChange={(e) => setText(e.target.value)}
+                onDone={handleDone}
+                onCancel={onCancel}
+              >
+                {Array.from(selectedUsersById.values()).map((user, index) => (
+                  <Pill
+                    key={user.id}
+                    size="xl"
+                    bg="bg-medium"
+                    c="text-primary"
+                    ml={index > 0 ? "sm" : ""}
+                    withRemoveButton
+                    onRemove={() => handleRemoveUser(user)}
+                  >
+                    {user.common_name}
+                  </Pill>
+                ))}
+              </AddRow>
             </div>
-          ))}
-          <AddMemberTypeaheadPopover
-            value={text}
-            options={availableToSelectUsers}
-            onSuggestionAccepted={handleAddUser}
-            target={rowRef}
-          />
-        </AddRow>
+          </Popover.Target>
+
+          <Popover.Dropdown>
+            <Flex direction="column" p="xs" miw="15rem">
+              {suggestedUsers.map((user: User, index: number) => (
+                <Flex
+                  key={index}
+                  component={UnstyledButton}
+                  align="center"
+                  gap="md"
+                  p="0.5rem 1rem"
+                  onClick={() => handleAddUser(user)}
+                >
+                  <UserAvatar bg={userToColor(user)} user={user} />
+                  <Text fw="700" size="lg">
path: frontend/src/metabase/admin/people/components/AddMemberRow.tsx
user: login: rafpaf
body: Out of curiosity, when should we use `700`, and when `"bold"`?
created_at: 2025-05-05T16:49:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,56 +14,47 @@ interface AddRowProps {
   children?: React.ReactNode;
 }
 
-export const AddRow = forwardRef(function AddRow(
-  {
-    value,
-    isValid,
-    placeholder,
-    onKeyDown,
-    onChange,
-    onDone,
-    onCancel,
-    children,
-  }: AddRowProps,
-  ref: Ref<HTMLDivElement>,
-) {
-  return (
-    <div
-      ref={ref}
-      className={cx(
-        CS.my2,
-        CS.pl1,
-        CS.p1,
-        CS.bordered,
-        CS.borderBrand,
-        CS.rounded,
-        CS.relative,
-        CS.flex,
-        CS.alignCenter,
-      )}
+export const AddRow = ({
+  value,
+  isValid,
+  placeholder,
+  onKeyDown,
+  onChange,
+  onDone,
+  onCancel,
+  children,
+}: AddRowProps) => (
+  <Flex
+    my="1rem"
+    p="0.5rem"
+    display="relative"
+    align="center"
+    bd="1px solid var(--mb-color-brand)"
+    style={{ borderRadius: "0.5rem" }}
path: frontend/src/metabase/admin/people/components/AddRow.tsx
user: login: rafpaf
body: minor nit: `Paper` is perhaps more conventional here, and then you could use the `radius` prop
created_at: 2025-05-05T16:50:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -214,13 +220,13 @@ const GroupDetailInner = ({
       title={
         <Fragment>
           {getGroupNameLocalized(group ?? {})}
-          <span className={cx(CS.textLight, CS.ml1)}>
+          <Box component="span" c="text-light" ml="0.5rem">
path: frontend/src/metabase/admin/people/components/GroupDetail.tsx
user: login: rafpaf
body: nit:
```suggestion
          <Box component="span" c="text-light" ms="sm">
```
created_at: 2025-05-05T16:53:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -246,26 +249,26 @@ function GroupRow({
   ) : (
     <tr>
       <td>
-        <Link
+        <Flex
+          component={Link}
+          align="center"
           to={"/admin/people/groups/" + group.id}
-          className={cx(CS.link, CS.flex, CS.alignCenter)}
+          className={CS.link}
         >
-          <span className={CS.textWhite}>
-            <UserAvatar
-              user={{ first_name: getGroupNameLocalized(group) }}
-              bg={backgroundColor}
-            />
-          </span>
-          <span className={cx(CS.ml2, CS.textBold)}>
+          <UserAvatar
+            user={{ first_name: getGroupNameLocalized(group) }}
+            bg={backgroundColor}
+          />
+          <Box component="span" ml="1rem" fw={700} c="brand">
path: frontend/src/metabase/admin/people/components/GroupsListing.tsx
user: login: rafpaf
body: nit: `ms="md"`
created_at: 2025-05-05T17:01:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -246,26 +249,26 @@ function GroupRow({
   ) : (
     <tr>
       <td>
-        <Link
+        <Flex
+          component={Link}
+          align="center"
           to={"/admin/people/groups/" + group.id}
-          className={cx(CS.link, CS.flex, CS.alignCenter)}
+          className={CS.link}
         >
-          <span className={CS.textWhite}>
-            <UserAvatar
-              user={{ first_name: getGroupNameLocalized(group) }}
-              bg={backgroundColor}
-            />
-          </span>
-          <span className={cx(CS.ml2, CS.textBold)}>
+          <UserAvatar
+            user={{ first_name: getGroupNameLocalized(group) }}
+            bg={backgroundColor}
+          />
+          <Box component="span" ml="1rem" fw={700} c="brand">
             {getGroupNameLocalized(group)}
-          </span>
-        </Link>
+          </Box>
+        </Flex>
       </td>
       <td>
         {group.member_count || 0}
         <ApiKeyCount apiKeys={apiKeys} />
       </td>
-      <td className={CS.textRight}>
+      <Box component="td" ta="right">
path: frontend/src/metabase/admin/people/components/GroupsListing.tsx
user: login: rafpaf
body: nit: `ta="end"`
created_at: 2025-05-05T17:02:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,22 +86,28 @@ export const MembershipSelect = ({
       // prevent clicks on the confirm modal from closing this popover
       closeOnClickOutside={!isConfirmModalOpen}
       onChange={togglePopover}
+      position="bottom-end"
     >
       <Popover.Target>
-        <div
+        <Flex
+          display="inline-flex"
           onClick={openPopover}
-          className={cx(CS.flex, CS.alignCenter)}
+          align="center"
           aria-label="group-summary"
         >
-          <span className={cx(CS.mr1, CS.textMedium)}>
-            <GroupSummary groups={groups} selectedGroupIds={selectedGroupIds} />
-          </span>
-          <Icon className={CS.textLight} name="chevrondown" size={10} />
-        </div>
+          <GroupSummary
+            mr="sm"
path: frontend/src/metabase/admin/people/components/MembershipSelect/MembershipSelect.tsx
user: login: rafpaf
body: nit: `me="sm"`
created_at: 2025-05-05T17:02:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,131 +52,70 @@ export function AddMemberRow({
     [selectedUsersById, excludeIds, users],
   );
 
+  const suggestedUsers = useMemo(() => {
+    const input = text.toLowerCase();
+    return availableToSelectUsers.filter((user) =>
+      (user.common_name || "").toLowerCase().includes(input),
+    );
+  }, [availableToSelectUsers, text]);
+
   return (
     <tr>
       <td colSpan={4} style={{ padding: 0 }}>
-        <AddRow
-          ref={rowRef}
-          value={text}
-          isValid={selectedUsersById.size > 0}
-          placeholder={t`Julie McMemberson`}
-          onChange={(e) => setText(e.target.value)}
-          onDone={handleDone}
-          onCancel={onCancel}
+        <Popover
+          opened={suggestedUsers.length > 0}
+          position="bottom-start"
+          withArrow
+          shadow="md"
         >
-          {Array.from(selectedUsersById.values()).map((user) => (
-            <div
-              key={user.id}
-              className={cx(
-                CS.bgMedium,
-                CS.p1,
-                CS.px2,
-                CS.mr1,
-                CS.rounded,
-                CS.flex,
-                CS.alignCenter,
-              )}
-            >
-              {user.common_name}
-              <Icon
-                className={cx(
-                  CS.pl1,
-                  CS.cursorPointer,
-                  CS.textSlate,
-                  CS.textMediumHover,
-                )}
-                name="close"
-                onClick={() => handleRemoveUser(user)}
-              />
+          <Popover.Target>
+            <div>
+              <AddRow
+                value={text}
+                isValid={selectedUsersById.size > 0}
+                placeholder={t`Julie McMemberson`}
path: frontend/src/metabase/admin/people/components/AddMemberRow.tsx
user: login: sloansparger
body: Yeah that's a great point but looks like something that's been around so I'm inclined to leave it...
created_at: 2025-05-07T14:41:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,131 +52,70 @@ export function AddMemberRow({
     [selectedUsersById, excludeIds, users],
   );
 
+  const suggestedUsers = useMemo(() => {
+    const input = text.toLowerCase();
+    return availableToSelectUsers.filter((user) =>
+      (user.common_name || "").toLowerCase().includes(input),
+    );
+  }, [availableToSelectUsers, text]);
+
   return (
     <tr>
       <td colSpan={4} style={{ padding: 0 }}>
-        <AddRow
-          ref={rowRef}
-          value={text}
-          isValid={selectedUsersById.size > 0}
-          placeholder={t`Julie McMemberson`}
-          onChange={(e) => setText(e.target.value)}
-          onDone={handleDone}
-          onCancel={onCancel}
+        <Popover
+          opened={suggestedUsers.length > 0}
+          position="bottom-start"
+          withArrow
+          shadow="md"
         >
-          {Array.from(selectedUsersById.values()).map((user) => (
-            <div
-              key={user.id}
-              className={cx(
-                CS.bgMedium,
-                CS.p1,
-                CS.px2,
-                CS.mr1,
-                CS.rounded,
-                CS.flex,
-                CS.alignCenter,
-              )}
-            >
-              {user.common_name}
-              <Icon
-                className={cx(
-                  CS.pl1,
-                  CS.cursorPointer,
-                  CS.textSlate,
-                  CS.textMediumHover,
-                )}
-                name="close"
-                onClick={() => handleRemoveUser(user)}
-              />
+          <Popover.Target>
+            <div>
+              <AddRow
+                value={text}
+                isValid={selectedUsersById.size > 0}
+                placeholder={t`Julie McMemberson`}
+                onChange={(e) => setText(e.target.value)}
+                onDone={handleDone}
+                onCancel={onCancel}
+              >
+                {Array.from(selectedUsersById.values()).map((user, index) => (
+                  <Pill
+                    key={user.id}
+                    size="xl"
+                    bg="bg-medium"
+                    c="text-primary"
+                    ml={index > 0 ? "sm" : ""}
+                    withRemoveButton
+                    onRemove={() => handleRemoveUser(user)}
+                  >
+                    {user.common_name}
+                  </Pill>
+                ))}
+              </AddRow>
             </div>
-          ))}
-          <AddMemberTypeaheadPopover
-            value={text}
-            options={availableToSelectUsers}
-            onSuggestionAccepted={handleAddUser}
-            target={rowRef}
-          />
-        </AddRow>
+          </Popover.Target>
+
+          <Popover.Dropdown>
+            <Flex direction="column" p="xs" miw="15rem">
+              {suggestedUsers.map((user: User, index: number) => (
+                <Flex
+                  key={index}
+                  component={UnstyledButton}
+                  align="center"
+                  gap="md"
+                  p="0.5rem 1rem"
+                  onClick={() => handleAddUser(user)}
+                >
+                  <UserAvatar bg={userToColor(user)} user={user} />
+                  <Text fw="700" size="lg">
path: frontend/src/metabase/admin/people/components/AddMemberRow.tsx
user: login: sloansparger
body: habit? i guess it's what comes to mind. i'll change it.
created_at: 2025-05-07T14:44:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,56 +14,47 @@ interface AddRowProps {
   children?: React.ReactNode;
 }
 
-export const AddRow = forwardRef(function AddRow(
-  {
-    value,
-    isValid,
-    placeholder,
-    onKeyDown,
-    onChange,
-    onDone,
-    onCancel,
-    children,
-  }: AddRowProps,
-  ref: Ref<HTMLDivElement>,
-) {
-  return (
-    <div
-      ref={ref}
-      className={cx(
-        CS.my2,
-        CS.pl1,
-        CS.p1,
-        CS.bordered,
-        CS.borderBrand,
-        CS.rounded,
-        CS.relative,
-        CS.flex,
-        CS.alignCenter,
-      )}
+export const AddRow = ({
+  value,
+  isValid,
+  placeholder,
+  onKeyDown,
+  onChange,
+  onDone,
+  onCancel,
+  children,
+}: AddRowProps) => (
+  <Flex
+    my="1rem"
+    p="0.5rem"
+    display="relative"
+    align="center"
+    bd="1px solid var(--mb-color-brand)"
+    style={{ borderRadius: "0.5rem" }}
path: frontend/src/metabase/admin/people/components/AddRow.tsx
user: login: sloansparger
body: Looks like I have to remove some styles if I use Paper (there's a box shadow). I think this ends up about the same.

Honestly I'd like to get ride of this custom component all together in favor of a large mantine multi-select. So I don't think this is worth over-investing in.
created_at: 2025-05-07T14:46:25Z
author_association: CONTRIBUTOR

PR comments:
user: login: trunk-io[bot]
created_at: 2025-04-28T23:56:26Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://raster.shields.io/badge/24485_tests-42fa2abf-purple)](https://app.trunk.io/metabase/flaky-tests/pr/57365?repo=metabase/metabase&commitHash=42fa2abf51f450672a4df2f3efb2100e9c6468bf&utm_source=gh-comment) &ensp; [![Static Badge](https://raster.shields.io/badge/2-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/57365?repo=metabase/metabase&commitHash=42fa2abf51f450672a4df2f3efb2100e9c6468bf&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://raster.shields.io/badge/0-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/57365?repo=metabase/metabase&commitHash=42fa2abf51f450672a4df2f3efb2100e9c6468bf&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://raster.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/57365?repo=metabase/metabase&commitHash=42fa2abf51f450672a4df2f3efb2100e9c6468bf&utm_source=gh-comment&runConclusion=QUARANTINED)


| Failed Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>scenarios > setup should send snowplow events</code> | The 'event' property is undefined when trying to read it. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/57365?repo=metabase/metabase&commitHash=42fa2abf51f450672a4df2f3efb2100e9c6468bf&utm_source=gh-comment&runConclusion=FAILURE&runId=621078a9-deee-5ce4-a73b-30953d18e0dc) |
| <code>scenarios > setup should ignore snowplow failures and work as normal</code> | Snowplow retry timed out due to null value. | [Logs ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/57365?repo=metabase/metabase&commitHash=42fa2abf51f450672a4df2f3efb2100e9c6468bf&utm_source=gh-comment&runConclusion=FAILURE&runId=bc56c3af-c633-5d9b-ba20-b82ed2009b3d) |
<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/57365?repo=metabase/metabase&commitHash=42fa2abf51f450672a4df2f3efb2100e9c6468bf&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-08T15:16:37Z
body: 
  ## e2e tests failed on `42fa2abf51f450672a4df2f3efb2100e9c6468bf-1`

  [e2e test run](https://github.com/metabase/metabase/actions/runs/14909565564?pr=57365)

  File | Test Name
  ---- | ---------
  `setup.cy.spec.ts` | (_flaky_) scenarios > setup > should send snowplow events
`setup.cy.spec.ts` | (_flaky_) scenarios > setup > should ignore snowplow failures and work as normal

author_association: CONTRIBUTOR