Skip to content

Commit

Permalink
feat(auth): add admin user management ui (#4631)
Browse files Browse the repository at this point in the history
* feat(auth): add admin user management ui

* block changing admins role, block resetting password when not using local auth

* remove user form changes

* remove blocked wording for user delete
  • Loading branch information
Parker-Stafford authored Sep 18, 2024
1 parent 79cc9c3 commit cc453e0
Show file tree
Hide file tree
Showing 17 changed files with 817 additions and 156 deletions.
5 changes: 5 additions & 0 deletions app/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ interface ApiKey {
expiresAt: DateTime
}

enum AuthMethod {
LOCAL
}

union Bin = NominalBin | IntervalBin | MissingValueBin

input ClearProjectInput {
Expand Down Expand Up @@ -1465,6 +1469,7 @@ type User implements Node {
email: String!
username: String
createdAt: DateTime!
authMethod: AuthMethod!
role: UserRole!
apiKeys: [UserApiKey!]!
}
Expand Down
27 changes: 19 additions & 8 deletions app/src/components/settings/RolePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import React from "react";
import { css } from "@emotion/react";

import { Item, Picker, PickerProps } from "@arizeai/components";

import { UserRole } from "@phoenix/constants";
import { isUserRole, normalizeUserRole, UserRole } from "@phoenix/constants";

const UserRoles = Object.values(UserRole);

function isUserRole(role: unknown): role is UserRole {
return typeof role === "string" && role in UserRole;
}
const hiddenLabelCSS = css`
.ac-field-label {
display: none;
}
`;

type RolePickerProps<T> = {
onChange: (role: UserRole) => void;
role: UserRole;
role?: UserRole;
/**
* Whether to display a label for the picker
* This may be set to false in cases where the picker is rendered in a table for instance
* @default true
*/
includeLabel?: boolean;
} & Omit<
PickerProps<T>,
"children" | "onSelectionChange" | "defaultSelectedKey"
Expand All @@ -21,13 +30,15 @@ type RolePickerProps<T> = {
export function RolePicker<T>({
onChange,
role,
includeLabel = true,
...pickerProps
}: RolePickerProps<T>) {
return (
<Picker
label="Role"
css={!includeLabel ? hiddenLabelCSS : undefined}
label={"Role"}
className="role-picker"
defaultSelectedKey={role}
defaultSelectedKey={role ?? undefined}
aria-label="User Role"
onSelectionChange={(key) => {
if (isUserRole(key)) {
Expand All @@ -38,7 +49,7 @@ export function RolePicker<T>({
{...pickerProps}
>
{UserRoles.map((role) => {
return <Item key={role}>{role.toLocaleLowerCase()}</Item>;
return <Item key={role}>{normalizeUserRole(role)}</Item>;
})}
</Picker>
);
Expand Down
8 changes: 8 additions & 0 deletions app/src/constants/userConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,11 @@ export enum UserRole {
ADMIN = "ADMIN",
MEMBER = "MEMBER",
}

export function normalizeUserRole(role: string) {
return role.toLocaleLowerCase();
}

export function isUserRole(role: unknown): role is UserRole {
return typeof role === "string" && role in UserRole;
}
112 changes: 0 additions & 112 deletions app/src/pages/settings/DeleteUserButton.tsx

This file was deleted.

80 changes: 80 additions & 0 deletions app/src/pages/settings/DeleteUserDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useCallback } from "react";
import { graphql, useMutation } from "react-relay";

import { Button, Dialog, Flex, Text, View } from "@arizeai/components";

import { useNotifyError, useNotifySuccess } from "@phoenix/contexts";

export function DeleteUserDialog({
userId,
onDeleted,
onClose,
}: {
userId: string;
onDeleted: () => void;
onClose: () => void;
}) {
const [commit, isCommitting] = useMutation(graphql`
mutation DeleteUserDialogMutation($input: DeleteUsersInput!) {
deleteUsers(input: $input)
}
`);

const notifySuccess = useNotifySuccess();
const notifyError = useNotifyError();

const handleDelete = useCallback(() => {
commit({
variables: {
input: {
userIds: [userId],
},
},
onCompleted: () => {
notifySuccess({
title: "User deleted",
message: "User has been deleted.",
});
onDeleted();
onClose();
},
onError: (error) => {
notifyError({
title: "Failed to delete user",
message: error.message,
});
},
});
}, [commit, notifyError, notifySuccess, onClose, onDeleted, userId]);
return (
<Dialog title="Delete User" isDismissable onDismiss={onClose}>
<View padding="size-200">
<Text color="danger">
{`Are you sure you want to delete this user? This action cannot be undone.`}
</Text>
</View>
<View
paddingEnd="size-200"
paddingTop="size-100"
paddingBottom="size-100"
borderTopColor="light"
borderTopWidth="thin"
>
<Flex direction="row" justifyContent="end" gap={"size-100"}>
<Button variant="default" onClick={onClose}>
Cancel
</Button>
<Button
variant="danger"
onClick={() => {
handleDelete();
}}
disabled={isCommitting}
>
Delete user
</Button>
</Flex>
</View>
</Dialog>
);
}
Loading

0 comments on commit cc453e0

Please sign in to comment.