diff --git a/app/src/pages/settings/APIKeysCard.tsx b/app/src/pages/settings/APIKeysCard.tsx index 22cb82f411..a68d13e932 100644 --- a/app/src/pages/settings/APIKeysCard.tsx +++ b/app/src/pages/settings/APIKeysCard.tsx @@ -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( graphql` query APIKeysCardQuery { ...SystemAPIKeysTableFragment + ...UserAPIKeysTableFragment } `, {} @@ -39,7 +41,7 @@ function APIKeysCardContent() { -

Coming Soon

+
); diff --git a/app/src/pages/settings/DeleteSystemAPIKeyButton.tsx b/app/src/pages/settings/DeleteAPIKeyButton.tsx similarity index 54% rename from app/src/pages/settings/DeleteSystemAPIKeyButton.tsx rename to app/src/pages/settings/DeleteAPIKeyButton.tsx index 454d9923b2..e38e376278 100644 --- a/app/src/pages/settings/DeleteSystemAPIKeyButton.tsx +++ b/app/src/pages/settings/DeleteAPIKeyButton.tsx @@ -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, @@ -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(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( - {`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.`} { + 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]); @@ -75,17 +114,9 @@ export function SystemAPIKeysTable({ cell: ({ row }) => { return ( - { - startTransition(() => { - refetch( - {}, - { - fetchPolicy: "network-only", - } - ); - }); + { + handleDelete(row.original.id); }} /> @@ -97,7 +128,7 @@ export function SystemAPIKeysTable({ }, ]; return cols; - }, [refetch]); + }, [handleDelete]); const table = useReactTable({ columns, data: tableData, diff --git a/app/src/pages/settings/UserAPIKeysTable.tsx b/app/src/pages/settings/UserAPIKeysTable.tsx new file mode 100644 index 0000000000..c1eedccb7c --- /dev/null +++ b/app/src/pages/settings/UserAPIKeysTable.tsx @@ -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[] = [ + { + 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 ( + + { + // TODO(parker): implement handle delete when https://github.com/Arize-ai/phoenix/issues/4059 is done + startTransition(() => { + refetch( + {}, + { + fetchPolicy: "network-only", + } + ); + }); + }} + /> + + ); + }, + meta: { + textAlign: "right", + }, + }, + ]; + return cols; + }, [refetch]); + const table = useReactTable({ + columns, + data: tableData, + getCoreRowModel: getCoreRowModel(), + }); + const rows = table.getRowModel().rows; + const isEmpty = table.getRowModel().rows.length === 0; + return ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + {isEmpty ? ( + + ) : ( + + {rows.map((row) => { + return ( + + {row.getVisibleCells().map((cell) => { + return ( + + ); + })} + + ); + })} + + )} +
+ {header.isPlaceholder ? null : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {header.column.getIsSorted() ? ( + + ) : ( + + ) + } + /> + ) : null} +
+ )} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+ ); +} diff --git a/app/src/pages/settings/__generated__/APIKeysCardQuery.graphql.ts b/app/src/pages/settings/__generated__/APIKeysCardQuery.graphql.ts index 2100e44dc8..19c733c817 100644 --- a/app/src/pages/settings/__generated__/APIKeysCardQuery.graphql.ts +++ b/app/src/pages/settings/__generated__/APIKeysCardQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<314a46b0f443b9e1e188e126d3b94d20>> + * @generated SignedSource<<87527227e2af250a58dfb79cbd126a32>> * @lightSyntaxTransform * @nogrep */ @@ -12,14 +12,52 @@ import { ConcreteRequest, Query } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type APIKeysCardQuery$variables = Record; export type APIKeysCardQuery$data = { - readonly " $fragmentSpreads": FragmentRefs<"SystemAPIKeysTableFragment">; + readonly " $fragmentSpreads": FragmentRefs<"SystemAPIKeysTableFragment" | "UserAPIKeysTableFragment">; }; export type APIKeysCardQuery = { response: APIKeysCardQuery$data; variables: APIKeysCardQuery$variables; }; -const node: ConcreteRequest = { +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + } +]; +return { "fragment": { "argumentDefinitions": [], "kind": "Fragment", @@ -30,6 +68,11 @@ const node: ConcreteRequest = { "args": null, "kind": "FragmentSpread", "name": "SystemAPIKeysTableFragment" + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "UserAPIKeysTableFragment" } ], "type": "Query", @@ -48,57 +91,32 @@ const node: ConcreteRequest = { "kind": "LinkedField", "name": "systemApiKeys", "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "description", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "createdAt", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "expiresAt", - "storageKey": null - } - ], + "selections": (v0/*: any*/), + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "UserApiKey", + "kind": "LinkedField", + "name": "userApiKeys", + "plural": true, + "selections": (v0/*: any*/), "storageKey": null } ] }, "params": { - "cacheID": "07048107c95469bc12ff425c6ba264ea", + "cacheID": "b5ca39f5901adfac7e884cb2555989fa", "id": null, "metadata": {}, "name": "APIKeysCardQuery", "operationKind": "query", - "text": "query APIKeysCardQuery {\n ...SystemAPIKeysTableFragment\n}\n\nfragment SystemAPIKeysTableFragment on Query {\n systemApiKeys {\n id\n name\n description\n createdAt\n expiresAt\n }\n}\n" + "text": "query APIKeysCardQuery {\n ...SystemAPIKeysTableFragment\n ...UserAPIKeysTableFragment\n}\n\nfragment SystemAPIKeysTableFragment on Query {\n systemApiKeys {\n id\n name\n description\n createdAt\n expiresAt\n }\n}\n\nfragment UserAPIKeysTableFragment on Query {\n userApiKeys {\n id\n name\n description\n createdAt\n expiresAt\n }\n}\n" } }; +})(); -(node as any).hash = "c3d10193f41d6556ae921b604ec89d8c"; +(node as any).hash = "ad967afd45af0d5976982c70eaadb330"; export default node; diff --git a/app/src/pages/settings/__generated__/DeleteSystemAPIKeyButtonMutation.graphql.ts b/app/src/pages/settings/__generated__/DeleteSystemAPIKeyButtonMutation.graphql.ts deleted file mode 100644 index 9ac6fe592a..0000000000 --- a/app/src/pages/settings/__generated__/DeleteSystemAPIKeyButtonMutation.graphql.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @generated SignedSource<> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ConcreteRequest, Mutation } from 'relay-runtime'; -export type DeleteApiKeyInput = { - id: string; -}; -export type DeleteSystemAPIKeyButtonMutation$variables = { - input: DeleteApiKeyInput; -}; -export type DeleteSystemAPIKeyButtonMutation$data = { - readonly deleteSystemApiKey: { - readonly __typename: "DeleteSystemApiKeyMutationPayload"; - readonly id: string; - }; -}; -export type DeleteSystemAPIKeyButtonMutation = { - response: DeleteSystemAPIKeyButtonMutation$data; - variables: DeleteSystemAPIKeyButtonMutation$variables; -}; - -const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "input" - } -], -v1 = [ - { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } - ], - "concreteType": "DeleteSystemApiKeyMutationPayload", - "kind": "LinkedField", - "name": "deleteSystemApiKey", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - } - ], - "storageKey": null - } -]; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "DeleteSystemAPIKeyButtonMutation", - "selections": (v1/*: any*/), - "type": "Mutation", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Operation", - "name": "DeleteSystemAPIKeyButtonMutation", - "selections": (v1/*: any*/) - }, - "params": { - "cacheID": "50908d8736ce33ae7dcbed1f16299d0f", - "id": null, - "metadata": {}, - "name": "DeleteSystemAPIKeyButtonMutation", - "operationKind": "mutation", - "text": "mutation DeleteSystemAPIKeyButtonMutation(\n $input: DeleteApiKeyInput!\n) {\n deleteSystemApiKey(input: $input) {\n __typename\n id\n }\n}\n" - } -}; -})(); - -(node as any).hash = "69aa0f424589544652508e589882593b"; - -export default node;