Skip to content

Commit

Permalink
feat(auth): add create user modal to ui (#4319)
Browse files Browse the repository at this point in the history
* feat(auth): add create user modal to ui

* add create user form and modal

* fix overflow

* feedback

* default to member, lowercase roles in table

---------

Co-authored-by: Mikyo King <mikyo@arize.com>
  • Loading branch information
Parker-Stafford and mikeldking authored Aug 22, 2024
1 parent beb04a8 commit e77a390
Show file tree
Hide file tree
Showing 16 changed files with 800 additions and 188 deletions.
1 change: 1 addition & 0 deletions app/src/components/dataset/CreateDatasetForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function CreateDatasetForm(props: CreateDatasetFormProps) {
isSubmitting={isCommitting}
onSubmit={onSubmit}
submitButtonText={isCommitting ? "Creating..." : "Create Dataset"}
formMode="create"
/>
);
}
6 changes: 5 additions & 1 deletion app/src/components/dataset/DatasetForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ export function DatasetForm({
onSubmit,
isSubmitting,
submitButtonText,
formMode,
}: {
datasetName?: string | null;
datasetDescription?: string | null;
datasetMetadata?: Record<string, unknown> | null;
onSubmit: (params: DatasetFormParams) => void;
isSubmitting: boolean;
submitButtonText: string;
formMode: "create" | "edit";
}) {
const {
control,
Expand Down Expand Up @@ -125,7 +127,9 @@ export function DatasetForm({
>
<Flex direction="row" justifyContent="end">
<Button
disabled={!isDirty}
// Only allow submission if the form is dirty for edits
// When creating allow the user to click create without any changes as the form will be prefilled with valid values
disabled={formMode === "edit" ? !isDirty : false}
variant={isDirty ? "primary" : "default"}
size="compact"
loading={isSubmitting}
Expand Down
1 change: 1 addition & 0 deletions app/src/components/dataset/EditDatasetForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function EditDatasetForm({
onSubmit={onSubmit}
isSubmitting={isCommitting}
submitButtonText={isCommitting ? "Saving..." : "Save"}
formMode="edit"
/>
);
}
45 changes: 45 additions & 0 deletions app/src/components/settings/RolePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from "react";

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

import { UserRole } from "@phoenix/constants";

const UserRoles = Object.values(UserRole);

function isUserRole(role: unknown): role is UserRole {
return typeof role === "string" && role in UserRole;
}

type RolePickerProps<T> = {
onChange: (role: UserRole) => void;
role: UserRole;
} & Omit<
PickerProps<T>,
"children" | "onSelectionChange" | "defaultSelectedKey"
>;

export function RolePicker<T>({
onChange,
role,
...pickerProps
}: RolePickerProps<T>) {
return (
<Picker
label="Role"
className="role-picker"
defaultSelectedKey={role}
aria-label="User Role"
onSelectionChange={(key) => {
if (isUserRole(key)) {
onChange(key);
}
}}
width={"100%"}
{...pickerProps}
>
{UserRoles.map((role) => {
return <Item key={role}>{role.toLocaleLowerCase()}</Item>;
})}
</Picker>
);
}
169 changes: 169 additions & 0 deletions app/src/components/settings/UserForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React from "react";
import { Controller, useForm } from "react-hook-form";
import { css } from "@emotion/react";

import { Button, Flex, Form, TextField, View } from "@arizeai/components";

import { UserRole } from "@phoenix/constants";

import { RolePicker } from "./RolePicker";

const MIN_PASSWORD_LENGTH = 4;

export type UserFormParams = {
email: string;
username: string | null;
password: string;
role: UserRole;
};

export function UserForm({
onSubmit,
email,
username,
password,
role,
isSubmitting,
}: {
onSubmit: (data: UserFormParams) => void;
isSubmitting: boolean;
} & Partial<UserFormParams>) {
const {
control,
handleSubmit,
formState: { isDirty },
} = useForm<UserFormParams>({
defaultValues: {
email: email ?? "",
username: username ?? null,
password: password ?? "",
role: role ?? UserRole.MEMBER,
},
});
return (
<div
css={css`
.role-picker {
width: 100%;
.ac-dropdown--picker,
.ac-dropdown-button {
width: 100%;
}
}
`}
>
<Form onSubmit={handleSubmit(onSubmit)}>
<View padding="size-200">
<Controller
name="email"
control={control}
rules={{
required: "Email is required",
pattern: {
value: /^[^@\s]+@[^@\s]+[.][^@\s]+$/,
message: "Invalid email format",
},
}}
render={({
field: { name, onChange, onBlur, value },
fieldState: { invalid, error },
}) => (
<TextField
label="Email"
type="email"
name={name}
isRequired
description="The user's email address. Must be unique."
errorMessage={error?.message}
validationState={invalid ? "invalid" : "valid"}
onChange={onChange}
onBlur={onBlur}
value={value}
/>
)}
/>
<Controller
name="username"
control={control}
render={({
field: { name, onChange, onBlur, value },
fieldState: { invalid, error },
}) => (
<TextField
label="Username"
name={name}
description="The user's username. Optional."
errorMessage={error?.message}
validationState={invalid ? "invalid" : "valid"}
onChange={onChange}
onBlur={onBlur}
value={value?.toString()}
/>
)}
/>
<Controller
name="password"
control={control}
rules={{
required: "Password is required",
minLength: {
value: MIN_PASSWORD_LENGTH,
message: `Password must be at least ${MIN_PASSWORD_LENGTH} characters`,
},
}}
render={({
field: { name, onChange, onBlur, value },
fieldState: { invalid, error },
}) => (
<TextField
label="Password"
type="password"
description="Password must be at least 4 characters"
name={name}
errorMessage={error?.message}
validationState={invalid ? "invalid" : "valid"}
onChange={onChange}
onBlur={onBlur}
defaultValue={value}
/>
)}
/>
<Controller
name="role"
control={control}
render={({
field: { onChange, value },
fieldState: { invalid, error },
}) => (
<RolePicker
onChange={onChange}
role={value}
validationState={invalid ? "invalid" : "valid"}
errorMessage={error?.message}
/>
)}
/>
</View>
<View
paddingStart="size-200"
paddingEnd="size-200"
paddingTop="size-100"
paddingBottom="size-100"
borderColor="dark"
borderTopWidth="thin"
>
<Flex direction="row" gap="size-100" justifyContent="end">
<Button
variant={isDirty ? "primary" : "default"}
type="submit"
size="compact"
disabled={isSubmitting}
>
{isSubmitting ? "Adding..." : "Add User"}
</Button>
</Flex>
</View>
</Form>
</div>
);
}
4 changes: 4 additions & 0 deletions app/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./userConstants";
export * from "./timeConstants";
export * from "./numberConstants";
export * from "./pointCloudConstants";
4 changes: 4 additions & 0 deletions app/src/constants/userConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum UserRole {
ADMIN = "ADMIN",
MEMBER = "MEMBER",
}
62 changes: 62 additions & 0 deletions app/src/pages/settings/NewUserDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { useCallback } from "react";
import { graphql, useMutation } from "react-relay";

import { Dialog, DialogContainer } from "@arizeai/components";

import {
UserForm,
UserFormParams,
} from "@phoenix/components/settings/UserForm";

import { NewUserDialogMutation } from "./__generated__/NewUserDialogMutation.graphql";

export function NewUserDialog({
onNewUserCreated,
onNewUserCreationError,
onDismiss,
}: {
onNewUserCreated: (email: string) => void;
onNewUserCreationError: (error: Error) => void;
onDismiss: () => void;
}) {
const [commit, isCommitting] = useMutation<NewUserDialogMutation>(graphql`
mutation NewUserDialogMutation($input: CreateUserInput!) {
createUser(input: $input) {
user {
id
email
}
}
}
`);

const onSubmit = useCallback(
(data: UserFormParams) => {
commit({
variables: {
input: data,
},
onCompleted: (response) => {
onNewUserCreated(response.createUser.user.email);
},
onError: (error) => {
onNewUserCreationError(error);
},
});
},
[commit, onNewUserCreated, onNewUserCreationError]
);

return (
<DialogContainer
onDismiss={onDismiss}
isDismissable
type="modal"
isKeyboardDismissDisabled
>
<Dialog title="Add user">
<UserForm onSubmit={onSubmit} isSubmitting={isCommitting} />
</Dialog>
</DialogContainer>
);
}
Loading

0 comments on commit e77a390

Please sign in to comment.