Skip to content
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

fix(auth): add back user api keys table #4494

Merged
merged 1 commit into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/src/pages/settings/APIKeysCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ import { CopyToClipboardButton, Loading } from "@phoenix/components";
import { APIKeysCardQuery } from "./__generated__/APIKeysCardQuery.graphql";
import { CreateSystemAPIKeyDialog } from "./CreateSystemAPIKeyDialog";
import { SystemAPIKeysTable } from "./SystemAPIKeysTable";
import { UserAPIKeysTable } from "./UserAPIKeysTable";

function APIKeysCardContent() {
const query = useLazyLoadQuery<APIKeysCardQuery>(
graphql`
query APIKeysCardQuery {
...SystemAPIKeysTableFragment
...UserAPIKeysTableFragment
}
`,
{}
Expand All @@ -39,7 +41,7 @@ function APIKeysCardContent() {
<SystemAPIKeysTable query={query} />
</TabPane>
<TabPane title="User Keys" name="User Keys">
<p>Coming Soon</p>
<UserAPIKeysTable query={query} />
</TabPane>
</Tabs>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { ReactNode, useCallback, useState } from "react";
import { graphql, useMutation } from "react-relay";
import React, { ReactNode, useState } from "react";

import {
Button,
Expand All @@ -12,48 +11,19 @@ import {
View,
} from "@arizeai/components";

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

export function DeleteSystemAPIKeyButton({
id,
onDeleted,
export function DeleteAPIKeyButton({
handleDelete,
}: {
id: string;
onDeleted: () => void;
handleDelete: () => void;
}) {
const [dialog, setDialog] = useState<ReactNode>(null);
const notifySuccess = useNotifySuccess();
const [commit] = useMutation(graphql`
mutation DeleteSystemAPIKeyButtonMutation($input: DeleteApiKeyInput!) {
deleteSystemApiKey(input: $input) {
__typename
id
}
}
`);
const handleDelete = useCallback(() => {
commit({
variables: {
input: {
id,
},
},
onCompleted: () => {
notifySuccess({
title: "System key deleted",
message: "The system key has been deleted and is no longer active.",
});
setDialog(null);
onDeleted();
},
});
}, [commit, id, notifySuccess, onDeleted]);

const onDelete = () => {
setDialog(
<Dialog title="Delete System Key">
<View padding="size-200">
<Text color="danger">
{`Are you sure you want to delete this system key? This cannot be undone and will disable all services using this key.`}
{`Are you sure you want to delete this key? This cannot be undone and will disable all uses of this key.`}
</Text>
</View>
<View
Expand Down
61 changes: 46 additions & 15 deletions app/src/pages/settings/SystemAPIKeysTable.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { startTransition, useMemo } from "react";
import { graphql, useRefetchableFragment } from "react-relay";
import React, { startTransition, useCallback, useMemo } from "react";
import { graphql, useMutation, useRefetchableFragment } from "react-relay";
import {
ColumnDef,
flexRender,
Expand All @@ -13,10 +13,11 @@ import { TextCell } from "@phoenix/components/table";
import { tableCSS } from "@phoenix/components/table/styles";
import { TableEmpty } from "@phoenix/components/table/TableEmpty";
import { TimestampCell } from "@phoenix/components/table/TimestampCell";
import { useNotifySuccess } from "@phoenix/contexts";

import { SystemAPIKeysTableFragment$key } from "./__generated__/SystemAPIKeysTableFragment.graphql";
import { SystemAPIKeysTableQuery } from "./__generated__/SystemAPIKeysTableQuery.graphql";
import { DeleteSystemAPIKeyButton } from "./DeleteSystemAPIKeyButton";
import { DeleteAPIKeyButton } from "./DeleteAPIKeyButton";

export function SystemAPIKeysTable({
query,
Expand All @@ -42,6 +43,44 @@ export function SystemAPIKeysTable({
query
);

const notifySuccess = useNotifySuccess();
const [commit] = useMutation(graphql`
mutation SystemAPIKeysTableDeleteAPIKeyMutation(
$input: DeleteApiKeyInput!
) {
deleteSystemApiKey(input: $input) {
__typename
id
}
}
`);
const handleDelete = useCallback(
(id: string) => {
commit({
variables: {
input: {
id,
},
},
onCompleted: () => {
notifySuccess({
title: "System key deleted",
message: "The system key has been deleted and is no longer active.",
});
startTransition(() => {
refetch(
{},
{
fetchPolicy: "network-only",
}
);
});
},
});
},
[commit, notifySuccess, refetch]
);

const tableData = useMemo(() => {
return [...data.systemApiKeys];
}, [data]);
Expand Down Expand Up @@ -75,17 +114,9 @@ export function SystemAPIKeysTable({
cell: ({ row }) => {
return (
<Flex direction="row" justifyContent="end" width="100%">
<DeleteSystemAPIKeyButton
id={row.original.id}
onDeleted={() => {
startTransition(() => {
refetch(
{},
{
fetchPolicy: "network-only",
}
);
});
<DeleteAPIKeyButton
handleDelete={() => {
handleDelete(row.original.id);
}}
/>
</Flex>
Expand All @@ -97,7 +128,7 @@ export function SystemAPIKeysTable({
},
];
return cols;
}, [refetch]);
}, [handleDelete]);
const table = useReactTable<TableRow>({
columns,
data: tableData,
Expand Down
176 changes: 176 additions & 0 deletions app/src/pages/settings/UserAPIKeysTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import React, { startTransition, useMemo } from "react";
import { graphql, useRefetchableFragment } from "react-relay";
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";

import { Flex, Icon, Icons } from "@arizeai/components";

import { TextCell } from "@phoenix/components/table";
import { tableCSS } from "@phoenix/components/table/styles";
import { TableEmpty } from "@phoenix/components/table/TableEmpty";
import { TimestampCell } from "@phoenix/components/table/TimestampCell";

import { UserAPIKeysTableFragment$key } from "./__generated__/UserAPIKeysTableFragment.graphql";
import { UserAPIKeysTableQuery } from "./__generated__/UserAPIKeysTableQuery.graphql";
import { DeleteAPIKeyButton } from "./DeleteAPIKeyButton";

export function UserAPIKeysTable({
query,
}: {
query: UserAPIKeysTableFragment$key;
}) {
const [data, refetch] = useRefetchableFragment<
UserAPIKeysTableQuery,
UserAPIKeysTableFragment$key
>(
graphql`
fragment UserAPIKeysTableFragment on Query
@refetchable(queryName: "UserAPIKeysTableQuery") {
userApiKeys {
id
name
description
createdAt
expiresAt
}
}
`,
query
);

const tableData = useMemo(() => {
return [...data.userApiKeys];
}, [data]);

type TableRow = (typeof tableData)[number];
const columns = useMemo(() => {
const cols: ColumnDef<TableRow>[] = [
{
header: "Name",
accessorKey: "name",
},
{
header: "Description",
accessorKey: "description",
cell: TextCell,
},
{
header: "Created At",
accessorKey: "createdAt",
cell: TimestampCell,
},
{
header: "Expires At",
accessorKey: "expiresAt",
cell: TimestampCell,
},
// TODO(parker): Do not render this column for non admins once https://github.com/Arize-ai/phoenix/issues/4454 is done
{
header: "",
accessorKey: "id",
size: 10,
cell: () => {
return (
<Flex direction="row" justifyContent="end" width="100%">
<DeleteAPIKeyButton
handleDelete={() => {
// TODO(parker): implement handle delete when https://github.com/Arize-ai/phoenix/issues/4059 is done
startTransition(() => {
refetch(
{},
{
fetchPolicy: "network-only",
}
);
});
}}
/>
</Flex>
);
},
meta: {
textAlign: "right",
},
},
];
return cols;
}, [refetch]);
const table = useReactTable<TableRow>({
columns,
data: tableData,
getCoreRowModel: getCoreRowModel(),
});
const rows = table.getRowModel().rows;
const isEmpty = table.getRowModel().rows.length === 0;
return (
<table css={tableCSS}>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th colSpan={header.colSpan} key={header.id}>
{header.isPlaceholder ? null : (
<div
{...{
className: header.column.getCanSort()
? "cursor-pointer"
: "",
onClick: header.column.getToggleSortingHandler(),
style: {
left: header.getStart(),
width: header.getSize(),
},
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{header.column.getIsSorted() ? (
<Icon
className="sort-icon"
svg={
header.column.getIsSorted() === "asc" ? (
<Icons.ArrowUpFilled />
) : (
<Icons.ArrowDownFilled />
)
}
/>
) : null}
</div>
)}
</th>
))}
</tr>
))}
</thead>
{isEmpty ? (
<TableEmpty message="No Keys" />
) : (
<tbody>
{rows.map((row) => {
return (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => {
return (
<td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
);
})}
</tr>
);
})}
</tbody>
)}
</table>
);
}
Loading
Loading