Skip to content

Commit

Permalink
feat(auth): add user api keys table (#4473)
Browse files Browse the repository at this point in the history
* feat(auth): add user api keys table

* add todos for rbac support

* fix imports

* fix lint

* import order
  • Loading branch information
Parker-Stafford authored Sep 3, 2024
1 parent cd64a99 commit 7c1334d
Show file tree
Hide file tree
Showing 8 changed files with 504 additions and 107 deletions.
5 changes: 4 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 @@ -38,8 +40,9 @@ function APIKeysCardContent() {
<TabPane title="System Keys" name="System Keys">
<SystemAPIKeysTable query={query} />
</TabPane>
{/* TODO(parker): do not render this table for non admins once https://github.com/Arize-ai/phoenix/issues/4454 is done*/}
<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, setDialog]);

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]
);

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]);
}, [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

0 comments on commit 7c1334d

Please sign in to comment.