From 7019dbfd193a15476172fc0224f7a3ba4044dc0e Mon Sep 17 00:00:00 2001 From: Mikyo King Date: Sun, 18 Aug 2024 12:58:37 -0600 Subject: [PATCH 1/4] get system keys wired up --- app/src/pages/project/AnnotationSummary.tsx | 2 +- app/src/pages/settings/APIKeysCard.tsx | 60 ++++++++ app/src/pages/settings/SettingsPage.tsx | 7 +- app/src/pages/settings/SystemAPIKeysTable.tsx | 139 ++++++++++++++++++ .../__generated__/APIKeysCardQuery.graphql.ts | 97 ++++++++++++ .../SystemAPIKeysTableFragment.graphql.ts | 87 +++++++++++ .../SystemAPIKeysTableRefetchQuery.graphql.ts | 97 ++++++++++++ 7 files changed, 483 insertions(+), 6 deletions(-) create mode 100644 app/src/pages/settings/APIKeysCard.tsx create mode 100644 app/src/pages/settings/SystemAPIKeysTable.tsx create mode 100644 app/src/pages/settings/__generated__/APIKeysCardQuery.graphql.ts create mode 100644 app/src/pages/settings/__generated__/SystemAPIKeysTableFragment.graphql.ts create mode 100644 app/src/pages/settings/__generated__/SystemAPIKeysTableRefetchQuery.graphql.ts diff --git a/app/src/pages/project/AnnotationSummary.tsx b/app/src/pages/project/AnnotationSummary.tsx index 27cec6732e..bb213301cb 100644 --- a/app/src/pages/project/AnnotationSummary.tsx +++ b/app/src/pages/project/AnnotationSummary.tsx @@ -74,7 +74,7 @@ function AnnotationSummaryValue(props: { }) { const { project, annotationName } = props; const { fetchKey } = useStreamState(); - const [data, refetch] = useRefetchableFragment< + const [data, refetch] = chableFragment< AnnotationSummaryQuery, AnnotationSummaryValueFragment$key >( diff --git a/app/src/pages/settings/APIKeysCard.tsx b/app/src/pages/settings/APIKeysCard.tsx new file mode 100644 index 0000000000..e712995687 --- /dev/null +++ b/app/src/pages/settings/APIKeysCard.tsx @@ -0,0 +1,60 @@ +import React, { Suspense } from "react"; +import { graphql, useLazyLoadQuery } from "react-relay"; + +import { + Button, + Icon, + Icons, + TabbedCard, + TabPane, + Tabs, +} from "@arizeai/components"; + +import { Loading } from "@phoenix/components"; + +import { APIKeysCardQuery } from "./__generated__/APIKeysCardQuery.graphql"; +import { SystemAPIKeysTable } from "./SystemAPIKeysTable"; + +function APIKeysCardContent() { + const query = useLazyLoadQuery( + graphql` + query APIKeysCardQuery { + ...SystemAPIKeysTableFragment + } + `, + {} + ); + + return ( + + + + + +

Create API Key

+
+
+ ); +} + +export function APIKeysCard() { + return ( + } />} + > + System Key + + } + > + }> + + + + ); +} diff --git a/app/src/pages/settings/SettingsPage.tsx b/app/src/pages/settings/SettingsPage.tsx index 37a18f8dad..5cb71b65c6 100644 --- a/app/src/pages/settings/SettingsPage.tsx +++ b/app/src/pages/settings/SettingsPage.tsx @@ -7,6 +7,7 @@ import { CopyToClipboardButton, Loading } from "@phoenix/components"; import { BASE_URL, VERSION } from "@phoenix/config"; import { useFunctionality } from "@phoenix/contexts/FunctionalityContext"; +import { APIKeysCard } from "./APIKeysCard"; import { UsersTable } from "./UsersTable"; const settingsPageCSS = css` @@ -62,11 +63,7 @@ export function SettingsPage() { - {authenticationEnabled && ( - - API settings go here - - )} + {authenticationEnabled && } {authenticationEnabled && ( }> diff --git a/app/src/pages/settings/SystemAPIKeysTable.tsx b/app/src/pages/settings/SystemAPIKeysTable.tsx new file mode 100644 index 0000000000..d70ec4b7bb --- /dev/null +++ b/app/src/pages/settings/SystemAPIKeysTable.tsx @@ -0,0 +1,139 @@ +import React, { useMemo } from "react"; +import { graphql, useRefetchableFragment } from "react-relay"; +import { + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { Icon, Icons } from "@arizeai/components"; + +import { tableCSS } from "@phoenix/components/table/styles"; +import { TableEmpty } from "@phoenix/components/table/TableEmpty"; +import { TimestampCell } from "@phoenix/components/table/TimestampCell"; + +import { SystemAPIKeysTableFragment$key } from "./__generated__/SystemAPIKeysTableFragment.graphql"; +import { SystemAPIKeysTableRefetchQuery } from "./__generated__/SystemAPIKeysTableRefetchQuery.graphql"; + +export function SystemAPIKeysTable({ + query, +}: { + query: SystemAPIKeysTableFragment$key; +}) { + const [data] = useRefetchableFragment< + SystemAPIKeysTableRefetchQuery, + SystemAPIKeysTableFragment$key + >( + graphql` + fragment SystemAPIKeysTableFragment on Query + @refetchable(queryName: "SystemAPIKeysTableRefetchQuery") { + systemApiKeys { + name + description + createdAt + expiresAt + } + } + `, + query + ); + + const tableData = useMemo(() => { + return [...data.systemApiKeys]; + }, [data]); + + type TableRow = (typeof tableData)[number]; + const table = useReactTable({ + columns: [ + { + header: "Name", + accessorKey: "name", + }, + { + header: "Description", + accessorKey: "description", + }, + { + header: "Created At", + accessorKey: "createdAt", + cell: TimestampCell, + }, + { + header: "Expires At", + accessorKey: "expiresAt", + cell: TimestampCell, + }, + ], + 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 new file mode 100644 index 0000000000..ce15c08795 --- /dev/null +++ b/app/src/pages/settings/__generated__/APIKeysCardQuery.graphql.ts @@ -0,0 +1,97 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Query } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type APIKeysCardQuery$variables = Record; +export type APIKeysCardQuery$data = { + readonly " $fragmentSpreads": FragmentRefs<"SystemAPIKeysTableFragment">; +}; +export type APIKeysCardQuery = { + response: APIKeysCardQuery$data; + variables: APIKeysCardQuery$variables; +}; + +const node: ConcreteRequest = { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "APIKeysCardQuery", + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "SystemAPIKeysTableFragment" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "APIKeysCardQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SystemApiKey", + "kind": "LinkedField", + "name": "systemApiKeys", + "plural": true, + "selections": [ + { + "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 + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "f1b81e2adefe303b9fef38d2dcbad2b9", + "id": null, + "metadata": {}, + "name": "APIKeysCardQuery", + "operationKind": "query", + "text": "query APIKeysCardQuery {\n ...SystemAPIKeysTableFragment\n}\n\nfragment SystemAPIKeysTableFragment on Query {\n systemApiKeys {\n name\n description\n createdAt\n expiresAt\n }\n}\n" + } +}; + +(node as any).hash = "c3d10193f41d6556ae921b604ec89d8c"; + +export default node; diff --git a/app/src/pages/settings/__generated__/SystemAPIKeysTableFragment.graphql.ts b/app/src/pages/settings/__generated__/SystemAPIKeysTableFragment.graphql.ts new file mode 100644 index 0000000000..04c3f02b77 --- /dev/null +++ b/app/src/pages/settings/__generated__/SystemAPIKeysTableFragment.graphql.ts @@ -0,0 +1,87 @@ +/** + * @generated SignedSource<<2f9e63ef0040e4c11fb576435e5e5fcc>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment, RefetchableFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type SystemAPIKeysTableFragment$data = { + readonly systemApiKeys: ReadonlyArray<{ + readonly createdAt: string; + readonly description: string | null; + readonly expiresAt: string | null; + readonly name: string; + }>; + readonly " $fragmentType": "SystemAPIKeysTableFragment"; +}; +export type SystemAPIKeysTableFragment$key = { + readonly " $data"?: SystemAPIKeysTableFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"SystemAPIKeysTableFragment">; +}; + +import SystemAPIKeysTableRefetchQuery_graphql from './SystemAPIKeysTableRefetchQuery.graphql'; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "refetch": { + "connection": null, + "fragmentPathInResult": [], + "operation": SystemAPIKeysTableRefetchQuery_graphql + } + }, + "name": "SystemAPIKeysTableFragment", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SystemApiKey", + "kind": "LinkedField", + "name": "systemApiKeys", + "plural": true, + "selections": [ + { + "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 + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null +}; + +(node as any).hash = "5b1b306f178fa0e790415c5e375cba5f"; + +export default node; diff --git a/app/src/pages/settings/__generated__/SystemAPIKeysTableRefetchQuery.graphql.ts b/app/src/pages/settings/__generated__/SystemAPIKeysTableRefetchQuery.graphql.ts new file mode 100644 index 0000000000..a39583fbb6 --- /dev/null +++ b/app/src/pages/settings/__generated__/SystemAPIKeysTableRefetchQuery.graphql.ts @@ -0,0 +1,97 @@ +/** + * @generated SignedSource<<27a738645f32bdb25f49fac2071d291a>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Query } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type SystemAPIKeysTableRefetchQuery$variables = Record; +export type SystemAPIKeysTableRefetchQuery$data = { + readonly " $fragmentSpreads": FragmentRefs<"SystemAPIKeysTableFragment">; +}; +export type SystemAPIKeysTableRefetchQuery = { + response: SystemAPIKeysTableRefetchQuery$data; + variables: SystemAPIKeysTableRefetchQuery$variables; +}; + +const node: ConcreteRequest = { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "SystemAPIKeysTableRefetchQuery", + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "SystemAPIKeysTableFragment" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "SystemAPIKeysTableRefetchQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SystemApiKey", + "kind": "LinkedField", + "name": "systemApiKeys", + "plural": true, + "selections": [ + { + "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 + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "454b9397a44c2b06b1f20ccf7f0b077e", + "id": null, + "metadata": {}, + "name": "SystemAPIKeysTableRefetchQuery", + "operationKind": "query", + "text": "query SystemAPIKeysTableRefetchQuery {\n ...SystemAPIKeysTableFragment\n}\n\nfragment SystemAPIKeysTableFragment on Query {\n systemApiKeys {\n name\n description\n createdAt\n expiresAt\n }\n}\n" + } +}; + +(node as any).hash = "5b1b306f178fa0e790415c5e375cba5f"; + +export default node; From a5e95f72ecf2b9f59108d68a4c90e9e36df372f3 Mon Sep 17 00:00:00 2001 From: Mikyo King Date: Mon, 19 Aug 2024 10:34:35 -0600 Subject: [PATCH 2/4] WIP --- app/src/pages/settings/APIKeysCard.tsx | 48 ++++++++++++------- .../settings/CreateSystemAPIKeyDialog.tsx | 23 +++++++++ 2 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 app/src/pages/settings/CreateSystemAPIKeyDialog.tsx diff --git a/app/src/pages/settings/APIKeysCard.tsx b/app/src/pages/settings/APIKeysCard.tsx index e712995687..9b47733043 100644 --- a/app/src/pages/settings/APIKeysCard.tsx +++ b/app/src/pages/settings/APIKeysCard.tsx @@ -1,8 +1,9 @@ -import React, { Suspense } from "react"; +import React, { ReactNode, Suspense, useState } from "react"; import { graphql, useLazyLoadQuery } from "react-relay"; import { Button, + DialogContainer, Icon, Icons, TabbedCard, @@ -13,6 +14,7 @@ import { import { Loading } from "@phoenix/components"; import { APIKeysCardQuery } from "./__generated__/APIKeysCardQuery.graphql"; +import { CreateSystemAPIKeyDialog } from "./CreateSystemAPIKeyDialog"; import { SystemAPIKeysTable } from "./SystemAPIKeysTable"; function APIKeysCardContent() { @@ -38,23 +40,33 @@ function APIKeysCardContent() { } export function APIKeysCard() { + const [dialog, setDialog] = useState(null); + const showCreateSystemAPIKeyDialog = () => { + setDialog(); + }; return ( - } />} - > - System Key - - } - > - }> - - - +
+ } />} + onClick={showCreateSystemAPIKeyDialog} + > + System Key + + } + > + }> + + + + setDialog(null)}> + {dialog} + +
); } diff --git a/app/src/pages/settings/CreateSystemAPIKeyDialog.tsx b/app/src/pages/settings/CreateSystemAPIKeyDialog.tsx new file mode 100644 index 0000000000..d9a2e1d581 --- /dev/null +++ b/app/src/pages/settings/CreateSystemAPIKeyDialog.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { Form } from "react-router-dom"; + +import { Button, Dialog, Flex, TextField, View } from "@arizeai/components"; + +export function CreateSystemAPIKeyDialog() { + return ( + +
+ + + + + + + + + +
+ ); +} From f8e553a3f1d8d9518ddce2be7c340138d08a38e3 Mon Sep 17 00:00:00 2001 From: Mikyo King Date: Mon, 19 Aug 2024 14:54:40 -0600 Subject: [PATCH 3/4] feat(auth): create system api key ui --- app/src/pages/dataset/DatasetCodeDropdown.tsx | 7 +- app/src/pages/settings/APIKeysCard.tsx | 45 +++- .../settings/CreateSystemAPIKeyDialog.tsx | 150 ++++++++++++-- ...reateSystemAPIKeyDialogMutation.graphql.ts | 194 ++++++++++++++++++ 4 files changed, 380 insertions(+), 16 deletions(-) create mode 100644 app/src/pages/settings/__generated__/CreateSystemAPIKeyDialogMutation.graphql.ts diff --git a/app/src/pages/dataset/DatasetCodeDropdown.tsx b/app/src/pages/dataset/DatasetCodeDropdown.tsx index c9669163d3..6148057739 100644 --- a/app/src/pages/dataset/DatasetCodeDropdown.tsx +++ b/app/src/pages/dataset/DatasetCodeDropdown.tsx @@ -65,7 +65,12 @@ export function DatasetCodeDropdown() { >
- + diff --git a/app/src/pages/settings/APIKeysCard.tsx b/app/src/pages/settings/APIKeysCard.tsx index 9b47733043..1543ac1875 100644 --- a/app/src/pages/settings/APIKeysCard.tsx +++ b/app/src/pages/settings/APIKeysCard.tsx @@ -1,17 +1,23 @@ 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, + View, } from "@arizeai/components"; -import { Loading } from "@phoenix/components"; +import { CopyToClipboardButton, Loading } from "@phoenix/components"; import { APIKeysCardQuery } from "./__generated__/APIKeysCardQuery.graphql"; import { CreateSystemAPIKeyDialog } from "./CreateSystemAPIKeyDialog"; @@ -33,7 +39,7 @@ function APIKeysCardContent() { -

Create API Key

+

Coming Soon

); @@ -41,8 +47,13 @@ function APIKeysCardContent() { export function APIKeysCard() { const [dialog, setDialog] = useState(null); + const showOneTimeAPIKeyDialog = (jwt: string) => { + setDialog(); + }; const showCreateSystemAPIKeyDialog = () => { - setDialog(); + setDialog( + + ); }; return (
@@ -70,3 +81,31 @@ export function APIKeysCard() {
); } + +/** + * Displays the key one time for the user to copy. + */ +function OneTimeAPIKeyDialog(props: { jwt: string }) { + const { jwt } = props; + return ( + + + 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. + +
+ + + + +
+
+ ); +} diff --git a/app/src/pages/settings/CreateSystemAPIKeyDialog.tsx b/app/src/pages/settings/CreateSystemAPIKeyDialog.tsx index d9a2e1d581..b4c05df104 100644 --- a/app/src/pages/settings/CreateSystemAPIKeyDialog.tsx +++ b/app/src/pages/settings/CreateSystemAPIKeyDialog.tsx @@ -1,19 +1,145 @@ -import React from "react"; +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 { Button, Dialog, Flex, TextField, View } from "@arizeai/components"; +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(null); + const { + control, + handleSubmit, + formState: { isDirty, isValid }, + } = useForm({ + defaultValues: { + name: "System", + description: "", + }, + }); + + const [commit, isCommitting] = useMutation( + 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] + ); -export function CreateSystemAPIKeyDialog() { return ( - - - - - - - -