-
Notifications
You must be signed in to change notification settings - Fork 285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(auth): system api keys ui #4270
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import React, { ReactNode, Suspense, useState } from "react"; | ||
import { graphql, useLazyLoadQuery } from "react-relay"; | ||
import { css } from "@emotion/react"; | ||
|
||
import { | ||
Alert, | ||
Button, | ||
Dialog, | ||
DialogContainer, | ||
Flex, | ||
Icon, | ||
Icons, | ||
TabbedCard, | ||
TabPane, | ||
Tabs, | ||
TextField, | ||
} from "@arizeai/components"; | ||
|
||
import { CopyToClipboardButton, Loading } from "@phoenix/components"; | ||
|
||
import { APIKeysCardQuery } from "./__generated__/APIKeysCardQuery.graphql"; | ||
import { CreateSystemAPIKeyDialog } from "./CreateSystemAPIKeyDialog"; | ||
import { SystemAPIKeysTable } from "./SystemAPIKeysTable"; | ||
|
||
function APIKeysCardContent() { | ||
const query = useLazyLoadQuery<APIKeysCardQuery>( | ||
graphql` | ||
query APIKeysCardQuery { | ||
...SystemAPIKeysTableFragment | ||
} | ||
`, | ||
{} | ||
); | ||
|
||
return ( | ||
<Tabs> | ||
<TabPane title="System Keys" name="System Keys"> | ||
<SystemAPIKeysTable query={query} /> | ||
</TabPane> | ||
<TabPane title="User Keys" name="User Keys"> | ||
<p>Coming Soon</p> | ||
</TabPane> | ||
</Tabs> | ||
); | ||
} | ||
|
||
export function APIKeysCard() { | ||
const [dialog, setDialog] = useState<ReactNode>(null); | ||
const showOneTimeAPIKeyDialog = (jwt: string) => { | ||
setDialog(<OneTimeAPIKeyDialog jwt={jwt} />); | ||
}; | ||
const showCreateSystemAPIKeyDialog = () => { | ||
setDialog( | ||
<CreateSystemAPIKeyDialog onSystemKeyCreated={showOneTimeAPIKeyDialog} /> | ||
); | ||
}; | ||
return ( | ||
<div> | ||
<TabbedCard | ||
title="API Keys" | ||
variant="compact" | ||
extra={ | ||
<Button | ||
variant="default" | ||
size="compact" | ||
icon={<Icon svg={<Icons.PlusCircleOutline />} />} | ||
onClick={showCreateSystemAPIKeyDialog} | ||
> | ||
System Key | ||
</Button> | ||
} | ||
> | ||
<Suspense fallback={<Loading />}> | ||
<APIKeysCardContent /> | ||
</Suspense> | ||
</TabbedCard> | ||
<DialogContainer onDismiss={() => setDialog(null)}> | ||
{dialog} | ||
</DialogContainer> | ||
</div> | ||
); | ||
} | ||
|
||
/** | ||
* Displays the key one time for the user to copy. | ||
*/ | ||
function OneTimeAPIKeyDialog(props: { jwt: string }) { | ||
const { jwt } = props; | ||
return ( | ||
<Dialog title="New API Key Created" isDismissable> | ||
<Alert variant="success" banner> | ||
You have successfully created a new API key. The API key will only be | ||
displayed once below. Please copy and save it in a secure location. | ||
</Alert> | ||
<div | ||
css={css` | ||
padding: var(--ac-global-dimension-size-200); | ||
.ac-field { | ||
width: 100%; | ||
} | ||
`} | ||
> | ||
<Flex direction="row" gap="size-100" alignItems="end"> | ||
<TextField label="API Key" isReadOnly value={jwt} minWidth="100%" /> | ||
<CopyToClipboardButton text={jwt} size="normal" /> | ||
</Flex> | ||
</div> | ||
</Dialog> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import React, { useCallback, useState } from "react"; | ||
import { Controller, useForm } from "react-hook-form"; | ||
import { useMutation } from "react-relay"; | ||
import { Form } from "react-router-dom"; | ||
import { graphql } from "relay-runtime"; | ||
|
||
import { | ||
Alert, | ||
Button, | ||
Dialog, | ||
Flex, | ||
TextArea, | ||
TextField, | ||
View, | ||
} from "@arizeai/components"; | ||
|
||
import { CreateSystemAPIKeyDialogMutation } from "./__generated__/CreateSystemAPIKeyDialogMutation.graphql"; | ||
|
||
export type SystemKeyFormParams = { | ||
name: string; | ||
description: string | null; | ||
}; | ||
|
||
/** | ||
* A dialog that allows admin users to create a system API key. | ||
* TODO: Add expiry date field | ||
*/ | ||
export function CreateSystemAPIKeyDialog(props: { | ||
onSystemKeyCreated: (jwt: string) => void; | ||
}) { | ||
const { onSystemKeyCreated } = props; | ||
const [error, setError] = useState<string | null>(null); | ||
const { | ||
control, | ||
handleSubmit, | ||
formState: { isDirty, isValid }, | ||
} = useForm<SystemKeyFormParams>({ | ||
defaultValues: { | ||
name: "System", | ||
description: "", | ||
}, | ||
}); | ||
|
||
const [commit, isCommitting] = useMutation<CreateSystemAPIKeyDialogMutation>( | ||
graphql` | ||
mutation CreateSystemAPIKeyDialogMutation( | ||
$name: String! | ||
$description: String = null | ||
) { | ||
createSystemApiKey(input: { name: $name, description: $description }) { | ||
jwt | ||
query { | ||
...SystemAPIKeysTableFragment | ||
} | ||
} | ||
} | ||
` | ||
); | ||
|
||
const onSubmit = useCallback( | ||
(data: SystemKeyFormParams) => { | ||
setError(null); | ||
commit({ | ||
variables: data, | ||
onCompleted: (response) => { | ||
onSystemKeyCreated(response.createSystemApiKey.jwt); | ||
}, | ||
onError: (error) => { | ||
setError(error.message); | ||
}, | ||
}); | ||
}, | ||
[commit, onSystemKeyCreated] | ||
); | ||
|
||
return ( | ||
<Dialog title="Create a System Key" isDismissable> | ||
{error && ( | ||
<Alert variant="danger" banner> | ||
{error} | ||
</Alert> | ||
)} | ||
<Form onSubmit={handleSubmit(onSubmit)}> | ||
<View padding="size-200"> | ||
<Controller | ||
name="name" | ||
control={control} | ||
rules={{ | ||
required: "System key name is required", | ||
}} | ||
Comment on lines
+85
to
+90
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for my product level understanding - there can be multiple system keys, and they can all have different names, correct? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, and they can ultimately have the same name if they want |
||
render={({ | ||
field: { onChange, onBlur, value }, | ||
fieldState: { invalid, error }, | ||
}) => ( | ||
<TextField | ||
label="Name" | ||
description="A short name to identify this key" | ||
errorMessage={error?.message} | ||
validationState={invalid ? "invalid" : "valid"} | ||
onChange={onChange} | ||
onBlur={onBlur} | ||
value={value.toString()} | ||
/> | ||
)} | ||
/> | ||
<Controller | ||
name="description" | ||
control={control} | ||
render={({ | ||
field: { onChange, onBlur, value }, | ||
fieldState: { invalid, error }, | ||
}) => ( | ||
<TextArea | ||
label="description" | ||
description={`A description of the system key`} | ||
isRequired={false} | ||
height={100} | ||
errorMessage={error?.message} | ||
validationState={invalid ? "invalid" : "valid"} | ||
onChange={onChange} | ||
onBlur={onBlur} | ||
value={value?.toString()} | ||
/> | ||
)} | ||
/> | ||
</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={!isValid || isCommitting} | ||
> | ||
{isCommitting ? "Creating..." : "Create Key"} | ||
</Button> | ||
</Flex> | ||
</View> | ||
</Form> | ||
</Dialog> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we want to obscure it even though it's only displayed once - **** with copy to clipboard - change messaging a bit: "The API key will only be copyable once below..."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typically these are displayed once. it's similar to how github does thsee