diff --git a/app/package.json b/app/package.json index 2d8c8856e0..8e2f5c3973 100644 --- a/app/package.json +++ b/app/package.json @@ -88,7 +88,7 @@ "build:relay": "relay-compiler", "test": "jest --config ./jest.config.js", "dev": "pnpm run dev:server:init & pnpm run build:static && pnpm run build:relay && vite", - "dev:auth": "export DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH=true && export DANGEROUSLY_SET_PHOENIX_SECRET=secret && pnpm run dev:server & pnpm run build:static && pnpm run build:relay && vite", + "dev:auth": "export DANGEROUSLY_SET_PHOENIX_ENABLE_AUTH=true && export DANGEROUSLY_SET_PHOENIX_SECRET=secretsecretsecretsecretsecretsecretsecret1 && pnpm run dev:server & pnpm run build:static && pnpm run build:relay && vite", "dev:ui": "pnpm run build:static && pnpm run build:relay && vite", "dev:server:init": "python -m phoenix.server.main --dev serve --with-fixture=chatbot --with-project=demo_llama_index --force-fixture-ingestion", "dev:server": "python -m phoenix.server.main --dev serve", diff --git a/app/src/Routes.tsx b/app/src/Routes.tsx index 59658a35dc..7990fe5d7d 100644 --- a/app/src/Routes.tsx +++ b/app/src/Routes.tsx @@ -7,6 +7,8 @@ import { embeddingLoaderQuery$data } from "./pages/embedding/__generated__/embed import { projectLoaderQuery$data } from "./pages/project/__generated__/projectLoaderQuery.graphql"; import { APIsPage, + AuthenticatedRoot, + authenticatedRootLoader, datasetLoader, DatasetPage, DatasetsPage, @@ -23,7 +25,6 @@ import { experimentsLoader, ExperimentsPage, homeLoader, - Layout, LoginPage, ModelPage, ModelRoot, @@ -41,7 +42,7 @@ const router = createBrowserRouter( createRoutesFromElements( }> } /> - }> + } loader={authenticatedRootLoader}> "profile" }} diff --git a/app/src/contexts/ViewerContext.tsx b/app/src/contexts/ViewerContext.tsx new file mode 100644 index 0000000000..9ceeb521b8 --- /dev/null +++ b/app/src/contexts/ViewerContext.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { graphql, useRefetchableFragment } from "react-relay"; + +import { + ViewerContext_viewer$data, + ViewerContext_viewer$key, +} from "./__generated__/ViewerContext_viewer.graphql"; + +export type ViewerContextType = { + viewer: ViewerContext_viewer$data["viewer"]; +}; + +export const ViewerContext = React.createContext({ + viewer: null, +}); + +export function useViewer() { + const context = React.useContext(ViewerContext); + if (context == null) { + throw new Error("useViewer must be used within a ViewerProvider"); + } + return context; +} + +export function ViewerProvider({ + query, + children, +}: React.PropsWithChildren<{ + query: ViewerContext_viewer$key; +}>) { + const [data] = useRefetchableFragment( + graphql` + fragment ViewerContext_viewer on Query + @refetchable(queryName: "ViewerContextRefetchQuery") { + viewer { + id + username + email + role { + name + } + } + } + `, + query + ); + return ( + + {children} + + ); +} diff --git a/app/src/contexts/__generated__/ViewerContextRefetchQuery.graphql.ts b/app/src/contexts/__generated__/ViewerContextRefetchQuery.graphql.ts new file mode 100644 index 0000000000..72b2a8e739 --- /dev/null +++ b/app/src/contexts/__generated__/ViewerContextRefetchQuery.graphql.ts @@ -0,0 +1,108 @@ +/** + * @generated SignedSource<<493db806afb41461ac8c0992ad1c49b0>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Query } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type ViewerContextRefetchQuery$variables = Record; +export type ViewerContextRefetchQuery$data = { + readonly " $fragmentSpreads": FragmentRefs<"ViewerContext_viewer">; +}; +export type ViewerContextRefetchQuery = { + response: ViewerContextRefetchQuery$data; + variables: ViewerContextRefetchQuery$variables; +}; + +const node: ConcreteRequest = { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "ViewerContextRefetchQuery", + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "ViewerContext_viewer" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "ViewerContextRefetchQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "username", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "UserRole", + "kind": "LinkedField", + "name": "role", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "3140ec9f88a7f0af697593abfce6cac1", + "id": null, + "metadata": {}, + "name": "ViewerContextRefetchQuery", + "operationKind": "query", + "text": "query ViewerContextRefetchQuery {\n ...ViewerContext_viewer\n}\n\nfragment ViewerContext_viewer on Query {\n viewer {\n id\n username\n email\n role {\n name\n }\n }\n}\n" + } +}; + +(node as any).hash = "8010036c1e996cdcd783b5b4cd65313a"; + +export default node; diff --git a/app/src/contexts/__generated__/ViewerContext_viewer.graphql.ts b/app/src/contexts/__generated__/ViewerContext_viewer.graphql.ts new file mode 100644 index 0000000000..758488c151 --- /dev/null +++ b/app/src/contexts/__generated__/ViewerContext_viewer.graphql.ts @@ -0,0 +1,100 @@ +/** + * @generated SignedSource<<0ddf4428f6184c26d7ae515162ef392c>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment, RefetchableFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type ViewerContext_viewer$data = { + readonly viewer: { + readonly email: string; + readonly id: string; + readonly role: { + readonly name: string; + }; + readonly username: string | null; + } | null; + readonly " $fragmentType": "ViewerContext_viewer"; +}; +export type ViewerContext_viewer$key = { + readonly " $data"?: ViewerContext_viewer$data; + readonly " $fragmentSpreads": FragmentRefs<"ViewerContext_viewer">; +}; + +import ViewerContextRefetchQuery_graphql from './ViewerContextRefetchQuery.graphql'; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "refetch": { + "connection": null, + "fragmentPathInResult": [], + "operation": ViewerContextRefetchQuery_graphql + } + }, + "name": "ViewerContext_viewer", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "username", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "UserRole", + "kind": "LinkedField", + "name": "role", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null +}; + +(node as any).hash = "8010036c1e996cdcd783b5b4cd65313a"; + +export default node; diff --git a/app/src/pages/AuthenticatedRoot.tsx b/app/src/pages/AuthenticatedRoot.tsx new file mode 100644 index 0000000000..99ce6d554c --- /dev/null +++ b/app/src/pages/AuthenticatedRoot.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { useLoaderData } from "react-router"; + +import { ViewerProvider } from "@phoenix/contexts/ViewerContext"; + +import { authenticatedRootLoaderQuery$data } from "./__generated__/authenticatedRootLoaderQuery.graphql"; +import { Layout } from "./Layout"; + +/** + * The root of the authenticated application. Note that authentication might be entirely disabled + */ +export function AuthenticatedRoot() { + const loaderData = useLoaderData() as authenticatedRootLoaderQuery$data; + return ( + + + + ); +} diff --git a/app/src/pages/__generated__/authenticatedRootLoaderQuery.graphql.ts b/app/src/pages/__generated__/authenticatedRootLoaderQuery.graphql.ts new file mode 100644 index 0000000000..f5da869dd3 --- /dev/null +++ b/app/src/pages/__generated__/authenticatedRootLoaderQuery.graphql.ts @@ -0,0 +1,108 @@ +/** + * @generated SignedSource<<8accd833ec420c0b9107a450bd7ddc7c>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest, Query } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type authenticatedRootLoaderQuery$variables = Record; +export type authenticatedRootLoaderQuery$data = { + readonly " $fragmentSpreads": FragmentRefs<"ViewerContext_viewer">; +}; +export type authenticatedRootLoaderQuery = { + response: authenticatedRootLoaderQuery$data; + variables: authenticatedRootLoaderQuery$variables; +}; + +const node: ConcreteRequest = { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "authenticatedRootLoaderQuery", + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "ViewerContext_viewer" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "authenticatedRootLoaderQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "username", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "UserRole", + "kind": "LinkedField", + "name": "role", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "6c829fb3bc2f5b6f3dc7cc83990282b9", + "id": null, + "metadata": {}, + "name": "authenticatedRootLoaderQuery", + "operationKind": "query", + "text": "query authenticatedRootLoaderQuery {\n ...ViewerContext_viewer\n}\n\nfragment ViewerContext_viewer on Query {\n viewer {\n id\n username\n email\n role {\n name\n }\n }\n}\n" + } +}; + +(node as any).hash = "26f018608f21da07f218dbd5e9f3a989"; + +export default node; diff --git a/app/src/pages/authenticatedRootLoader.ts b/app/src/pages/authenticatedRootLoader.ts new file mode 100644 index 0000000000..39d1214793 --- /dev/null +++ b/app/src/pages/authenticatedRootLoader.ts @@ -0,0 +1,20 @@ +import { fetchQuery, graphql } from "react-relay"; + +import RelayEnvironment from "@phoenix/RelayEnvironment"; + +import { authenticatedRootLoaderQuery } from "./__generated__/authenticatedRootLoaderQuery.graphql"; + +/** + * Loads in the necessary data at the root of the authenticated application + */ +export async function authenticatedRootLoader() { + return await fetchQuery( + RelayEnvironment, + graphql` + query authenticatedRootLoaderQuery { + ...ViewerContext_viewer + } + `, + {} + ).toPromise(); +} diff --git a/app/src/pages/index.tsx b/app/src/pages/index.tsx index 724da9f6dd..b4298b6b8a 100644 --- a/app/src/pages/index.tsx +++ b/app/src/pages/index.tsx @@ -4,7 +4,6 @@ export * from "./embedding"; export * from "./dimension"; export * from "./trace"; export * from "./project"; -export * from "./Layout"; export * from "./ErrorElement"; export * from "./ModelRoot"; export * from "./TracingRoot"; @@ -20,3 +19,5 @@ export * from "./settings"; export * from "./apis"; export * from "./login"; export * from "./profile"; +export * from "./AuthenticatedRoot"; +export * from "./authenticatedRootLoader"; diff --git a/app/src/pages/profile/ProfilePage.tsx b/app/src/pages/profile/ProfilePage.tsx index 6cd23ca3df..d181c0ba83 100644 --- a/app/src/pages/profile/ProfilePage.tsx +++ b/app/src/pages/profile/ProfilePage.tsx @@ -1,7 +1,9 @@ import React from "react"; import { css } from "@emotion/react"; -import { Card } from "@arizeai/components"; +import { Card, Form, TextField } from "@arizeai/components"; + +import { useViewer } from "@phoenix/contexts/ViewerContext"; import { LogoutButton } from "./LogoutButton"; @@ -20,11 +22,23 @@ const profilePageInnerCSS = css` `; export function ProfilePage() { + const { viewer } = useViewer(); + if (!viewer) { + return null; + } return (
} variant="compact"> - Profile goes here +
+ + + +