diff --git a/apps/opik-frontend/src/plugins/comet/PermissionsProvider.tsx b/apps/opik-frontend/src/plugins/comet/PermissionsProvider.tsx index d3879c40d2c..4193a7abdc7 100644 --- a/apps/opik-frontend/src/plugins/comet/PermissionsProvider.tsx +++ b/apps/opik-frontend/src/plugins/comet/PermissionsProvider.tsx @@ -32,6 +32,7 @@ const PermissionsProvider: React.FC<{ children: ReactNode }> = ({ canUpdateAlerts, canAnnotateTraceSpanThread, canTagTrace, + canUsePlayground, isPending, } = useUserPermission(); @@ -63,6 +64,7 @@ const PermissionsProvider: React.FC<{ children: ReactNode }> = ({ canUpdateAlerts, canAnnotateTraceSpanThread, canTagTrace, + canUsePlayground, }, isPending, }), @@ -92,6 +94,7 @@ const PermissionsProvider: React.FC<{ children: ReactNode }> = ({ canUpdateAlerts, canAnnotateTraceSpanThread, canTagTrace, + canUsePlayground, isPending, ], ); diff --git a/apps/opik-frontend/src/plugins/comet/types.ts b/apps/opik-frontend/src/plugins/comet/types.ts index 9d099eaebf4..ef0606ef36a 100644 --- a/apps/opik-frontend/src/plugins/comet/types.ts +++ b/apps/opik-frontend/src/plugins/comet/types.ts @@ -86,6 +86,7 @@ export enum ManagementPermissionsNames { COMMENT_WRITE = "comment_write", ONLINE_EVALUATION_RULE_UPDATE = "online_evaluation_rule_update", ALERT_UPDATE = "alert_update", + PLAYGROUND_USE = "playground_use", } export interface UserPermission { diff --git a/apps/opik-frontend/src/plugins/comet/useUserPermission.ts b/apps/opik-frontend/src/plugins/comet/useUserPermission.ts index 5aee7cbf97d..244c765b48b 100644 --- a/apps/opik-frontend/src/plugins/comet/useUserPermission.ts +++ b/apps/opik-frontend/src/plugins/comet/useUserPermission.ts @@ -223,6 +223,11 @@ const useUserPermission = (config?: { enabled?: boolean }) => { [checkNullablePermission], ); + const canUsePlayground = useMemo( + () => checkNullablePermission(ManagementPermissionsNames.PLAYGROUND_USE), + [checkNullablePermission], + ); + return { canInviteMembers, isWorkspaceOwner, @@ -251,6 +256,7 @@ const useUserPermission = (config?: { enabled?: boolean }) => { canUpdateAlerts, canAnnotateTraceSpanThread, canTagTrace, + canUsePlayground, isPending: isEnabled && isPending, }; }; diff --git a/apps/opik-frontend/src/types/permissions.ts b/apps/opik-frontend/src/types/permissions.ts index 19b78996869..cde6350acb2 100644 --- a/apps/opik-frontend/src/types/permissions.ts +++ b/apps/opik-frontend/src/types/permissions.ts @@ -24,6 +24,7 @@ export interface Permissions { canUpdateAlerts: boolean; canAnnotateTraceSpanThread: boolean; canTagTrace: boolean; + canUsePlayground: boolean; } export interface PermissionsContextValue { @@ -58,6 +59,7 @@ export const DEFAULT_PERMISSIONS: PermissionsContextValue = { canUpdateAlerts: true, canAnnotateTraceSpanThread: true, canTagTrace: true, + canUsePlayground: true, }, isPending: false, }; diff --git a/apps/opik-frontend/src/v1/layout/PlaygroundPageGuard/index.tsx b/apps/opik-frontend/src/v1/layout/PlaygroundPageGuard/index.tsx new file mode 100644 index 00000000000..2f984e55a76 --- /dev/null +++ b/apps/opik-frontend/src/v1/layout/PlaygroundPageGuard/index.tsx @@ -0,0 +1,17 @@ +import { usePermissions } from "@/contexts/PermissionsContext"; +import NoAccessPageGuard from "@/v1/layout/NoAccessPageGuard/NoAccessPageGuard"; + +const PlaygroundPageGuard = () => { + const { + permissions: { canUsePlayground }, + } = usePermissions(); + + return ( + + ); +}; + +export default PlaygroundPageGuard; diff --git a/apps/opik-frontend/src/v1/layout/SideBar/SideBarMenuItems.tsx b/apps/opik-frontend/src/v1/layout/SideBar/SideBarMenuItems.tsx index da3f366be92..7639c1c1120 100644 --- a/apps/opik-frontend/src/v1/layout/SideBar/SideBarMenuItems.tsx +++ b/apps/opik-frontend/src/v1/layout/SideBar/SideBarMenuItems.tsx @@ -27,13 +27,19 @@ interface SideBarMenuItemsProps { const SideBarMenuItems: React.FC = ({ expanded }) => { const { activeWorkspaceName: workspaceName } = useAppStore(); const { - permissions: { canViewExperiments, canViewDashboards, canViewDatasets }, + permissions: { + canViewExperiments, + canViewDashboards, + canViewDatasets, + canUsePlayground, + }, } = usePermissions(); const menuItems = getMenuItems({ canViewExperiments, canViewDashboards, canViewDatasets, + canUsePlayground, }); const { data: projectData } = useProjectsList( diff --git a/apps/opik-frontend/src/v1/layout/SideBar/helpers/getMenuItems.ts b/apps/opik-frontend/src/v1/layout/SideBar/helpers/getMenuItems.ts index 50eaab88a0f..fc0cd65d86b 100644 --- a/apps/opik-frontend/src/v1/layout/SideBar/helpers/getMenuItems.ts +++ b/apps/opik-frontend/src/v1/layout/SideBar/helpers/getMenuItems.ts @@ -21,10 +21,12 @@ const getMenuItems = ({ canViewExperiments, canViewDashboards, canViewDatasets, + canUsePlayground, }: { canViewExperiments: boolean; canViewDashboards: boolean; canViewDatasets: boolean; + canUsePlayground: boolean; }): MenuItemGroup[] => { return [ { @@ -115,13 +117,17 @@ const getMenuItems = ({ label: "Prompt library", count: "prompts", }, - { - id: "playground", - path: "/$workspaceName/playground", - type: MENU_ITEM_TYPE.router, - icon: Blocks, - label: "Playground", - }, + ...(canUsePlayground + ? [ + { + id: "playground", + path: "/$workspaceName/playground", + type: MENU_ITEM_TYPE.router as const, + icon: Blocks, + label: "Playground", + }, + ] + : []), ], }, { diff --git a/apps/opik-frontend/src/v1/pages-shared/traces/TraceDetailsPanel/TraceDataViewer/PromptsTab.tsx b/apps/opik-frontend/src/v1/pages-shared/traces/TraceDetailsPanel/TraceDataViewer/PromptsTab.tsx index 8fd55a3a982..124fee65345 100644 --- a/apps/opik-frontend/src/v1/pages-shared/traces/TraceDetailsPanel/TraceDataViewer/PromptsTab.tsx +++ b/apps/opik-frontend/src/v1/pages-shared/traces/TraceDetailsPanel/TraceDataViewer/PromptsTab.tsx @@ -12,6 +12,7 @@ import get from "lodash/get"; import { FileTerminal, GitCommitVertical } from "lucide-react"; import useAppStore from "@/store/AppStore"; import { useIsFeatureEnabled } from "@/contexts/feature-toggles-provider"; +import { usePermissions } from "@/contexts/PermissionsContext"; import { FeatureToggleKeys } from "@/types/feature-toggles"; import TryInPlaygroundButton from "@/v1/pages/PromptPage/TryInPlaygroundButton"; import PromptContentView, { @@ -71,6 +72,10 @@ const PromptsTab: React.FunctionComponent = ({ data, search, }) => { + const { + permissions: { canUsePlayground }, + } = usePermissions(); + const rawPrompts = get( data.metadata as Record, "opik_prompts", @@ -129,10 +134,12 @@ const PromptsTab: React.FunctionComponent = ({ search={search} templateStructure={rawPrompts[index]?.template_structure} playgroundButton={ - + canUsePlayground ? ( + + ) : null } /> diff --git a/apps/opik-frontend/src/v1/pages/EvaluationSuiteItemsPage/UseEvaluationSuiteDropdown.tsx b/apps/opik-frontend/src/v1/pages/EvaluationSuiteItemsPage/UseEvaluationSuiteDropdown.tsx index 3b4da694052..f2ee2224d3c 100644 --- a/apps/opik-frontend/src/v1/pages/EvaluationSuiteItemsPage/UseEvaluationSuiteDropdown.tsx +++ b/apps/opik-frontend/src/v1/pages/EvaluationSuiteItemsPage/UseEvaluationSuiteDropdown.tsx @@ -33,9 +33,11 @@ function UseEvaluationSuiteDropdown({ const [openConfirmDialog, setOpenConfirmDialog] = useState(false); const { - permissions: { canViewExperiments, canCreateExperiments }, + permissions: { canViewExperiments, canCreateExperiments, canUsePlayground }, } = usePermissions(); + const hasAnyAction = canUsePlayground || canCreateExperiments; + const { loadPlayground, isPlaygroundEmpty, isPendingProviderKeys } = useLoadPlayground(); @@ -55,6 +57,8 @@ function UseEvaluationSuiteDropdown({ } }; + if (!hasAnyAction) return null; + return ( <> {canViewExperiments && ( @@ -65,19 +69,21 @@ function UseEvaluationSuiteDropdown({ datasetName={datasetName} /> )} - + {canUsePlayground && ( + + )} @@ -86,20 +92,22 @@ function UseEvaluationSuiteDropdown({ - - - - Open in Playground - - Test prompts over your{" "} - {isEvalSuite ? "evaluation suite" : "dataset"} and run - evaluations interactively - - - + {canUsePlayground && ( + + + + Open in Playground + + Test prompts over your{" "} + {isEvalSuite ? "evaluation suite" : "dataset"} and run + evaluations interactively + + + + )} {canCreateExperiments && ( { diff --git a/apps/opik-frontend/src/v1/pages/PromptPage/PromptTab/PromptTab.tsx b/apps/opik-frontend/src/v1/pages/PromptPage/PromptTab/PromptTab.tsx index e44594f1e11..a994bbbeace 100644 --- a/apps/opik-frontend/src/v1/pages/PromptPage/PromptTab/PromptTab.tsx +++ b/apps/opik-frontend/src/v1/pages/PromptPage/PromptTab/PromptTab.tsx @@ -26,12 +26,17 @@ import ChatPromptView from "./ChatPromptView"; import TextPromptView from "./TextPromptView"; import TagListRenderer from "@/shared/TagListRenderer/TagListRenderer"; import usePromptVersionsUpdateMutation from "@/api/prompts/usePromptVersionsUpdateMutation"; +import { usePermissions } from "@/contexts/PermissionsContext"; interface PromptTabInterface { prompt?: PromptWithLatestVersion; } const PromptTab = ({ prompt }: PromptTabInterface) => { + const { + permissions: { canUsePlayground }, + } = usePermissions(); + const [openUseThisPrompt, setOpenUseThisPrompt] = useState(false); const [openEditPrompt, setOpenEditPrompt] = useState(false); const [versionToRestore, setVersionToRestore] = @@ -120,8 +125,13 @@ const PromptTab = ({ prompt }: PromptTabInterface) => { Use this prompt - - {!isChatPrompt && ( + {canUsePlayground && ( + + )} + {canUsePlayground && !isChatPrompt && ( playgroundRoute, component: PlaygroundPage, }); @@ -561,7 +568,7 @@ const routeTree = rootRoute.addChildren([ redirectProjectsRoute, redirectDatasetsRoute, ]), - playgroundRoute, + playgroundRoute.addChildren([playgroundIndexRoute]), configurationRoute, alertsRoute.addChildren([alertNewRoute, alertEditRoute]), onlineEvaluationRoute, diff --git a/apps/opik-frontend/src/v2/layout/PlaygroundPageGuard/index.tsx b/apps/opik-frontend/src/v2/layout/PlaygroundPageGuard/index.tsx new file mode 100644 index 00000000000..88c584887f2 --- /dev/null +++ b/apps/opik-frontend/src/v2/layout/PlaygroundPageGuard/index.tsx @@ -0,0 +1,17 @@ +import { usePermissions } from "@/contexts/PermissionsContext"; +import NoAccessPageGuard from "@/v2/layout/NoAccessPageGuard/NoAccessPageGuard"; + +const PlaygroundPageGuard = () => { + const { + permissions: { canUsePlayground }, + } = usePermissions(); + + return ( + + ); +}; + +export default PlaygroundPageGuard; diff --git a/apps/opik-frontend/src/v2/layout/SideBar/SideBarMenuItems.tsx b/apps/opik-frontend/src/v2/layout/SideBar/SideBarMenuItems.tsx index 4be2838c864..af52d4d8200 100644 --- a/apps/opik-frontend/src/v2/layout/SideBar/SideBarMenuItems.tsx +++ b/apps/opik-frontend/src/v2/layout/SideBar/SideBarMenuItems.tsx @@ -9,13 +9,14 @@ import { usePermissions } from "@/contexts/PermissionsContext"; const SideBarMenuItems: React.FC = () => { const activeProjectId = useActiveProjectId(); const { - permissions: { canViewExperiments, canViewDatasets }, + permissions: { canViewExperiments, canViewDatasets, canUsePlayground }, } = usePermissions(); const menuItems = getMenuItems({ projectId: activeProjectId, canViewExperiments, canViewDatasets, + canUsePlayground, }); const renderItems = (items: MenuItem[]) => { diff --git a/apps/opik-frontend/src/v2/layout/SideBar/helpers/getMenuItems.ts b/apps/opik-frontend/src/v2/layout/SideBar/helpers/getMenuItems.ts index 7edb215422f..9f3aee5d1ca 100644 --- a/apps/opik-frontend/src/v2/layout/SideBar/helpers/getMenuItems.ts +++ b/apps/opik-frontend/src/v2/layout/SideBar/helpers/getMenuItems.ts @@ -22,10 +22,12 @@ const getMenuItems = ({ projectId, canViewExperiments, canViewDatasets, + canUsePlayground, }: { projectId: string | null; canViewExperiments: boolean; canViewDatasets: boolean; + canUsePlayground: boolean; }): MenuItemGroup[] => { const projectPrefix = projectId ? "/$workspaceName/projects/$projectId" @@ -107,14 +109,18 @@ const getMenuItems = ({ label: "Prompt library", disabled: !projectPrefix, }, - { - id: "playground", - path: projectPath("/playground"), - type: MENU_ITEM_TYPE.router, - icon: Blocks, - label: "Playground", - disabled: !projectPrefix, - }, + ...(canUsePlayground + ? [ + { + id: "playground", + path: projectPath("/playground"), + type: MENU_ITEM_TYPE.router as const, + icon: Blocks, + label: "Playground", + disabled: !projectPrefix, + }, + ] + : []), ], }, { diff --git a/apps/opik-frontend/src/v2/pages-shared/traces/TraceDetailsPanel/TraceDataViewer/PromptsTab.tsx b/apps/opik-frontend/src/v2/pages-shared/traces/TraceDetailsPanel/TraceDataViewer/PromptsTab.tsx index c8cd84e7441..7bb3d1dca1b 100644 --- a/apps/opik-frontend/src/v2/pages-shared/traces/TraceDetailsPanel/TraceDataViewer/PromptsTab.tsx +++ b/apps/opik-frontend/src/v2/pages-shared/traces/TraceDetailsPanel/TraceDataViewer/PromptsTab.tsx @@ -12,6 +12,7 @@ import get from "lodash/get"; import { FileTerminal, GitCommitVertical } from "lucide-react"; import useAppStore from "@/store/AppStore"; import { useIsFeatureEnabled } from "@/contexts/feature-toggles-provider"; +import { usePermissions } from "@/contexts/PermissionsContext"; import { FeatureToggleKeys } from "@/types/feature-toggles"; import TryInPlaygroundButton from "@/v2/pages/PromptPage/TryInPlaygroundButton"; import PromptContentView, { @@ -71,6 +72,10 @@ const PromptsTab: React.FunctionComponent = ({ data, search, }) => { + const { + permissions: { canUsePlayground }, + } = usePermissions(); + const rawPrompts = get( data.metadata as Record, "opik_prompts", @@ -129,10 +134,12 @@ const PromptsTab: React.FunctionComponent = ({ search={search} templateStructure={rawPrompts[index]?.template_structure} playgroundButton={ - + canUsePlayground ? ( + + ) : null } /> diff --git a/apps/opik-frontend/src/v2/pages/EvaluationSuiteItemsPage/UseEvaluationSuiteDropdown.tsx b/apps/opik-frontend/src/v2/pages/EvaluationSuiteItemsPage/UseEvaluationSuiteDropdown.tsx index 4fcfe904c14..e6f4b1b2b03 100644 --- a/apps/opik-frontend/src/v2/pages/EvaluationSuiteItemsPage/UseEvaluationSuiteDropdown.tsx +++ b/apps/opik-frontend/src/v2/pages/EvaluationSuiteItemsPage/UseEvaluationSuiteDropdown.tsx @@ -35,9 +35,11 @@ function UseEvaluationSuiteDropdown({ const [openConfirmDialog, setOpenConfirmDialog] = useState(false); const { - permissions: { canViewExperiments, canCreateExperiments }, + permissions: { canViewExperiments, canCreateExperiments, canUsePlayground }, } = usePermissions(); + const hasAnyAction = canUsePlayground || canCreateExperiments; + const { loadPlayground, isPlaygroundEmpty, isPendingProviderKeys } = useLoadPlayground(); @@ -57,6 +59,8 @@ function UseEvaluationSuiteDropdown({ } }; + if (!hasAnyAction) return null; + return ( <> {canViewExperiments && ( @@ -68,19 +72,21 @@ function UseEvaluationSuiteDropdown({ projectId={projectId} /> )} - + {canUsePlayground && ( + + )} @@ -89,20 +95,22 @@ function UseEvaluationSuiteDropdown({ - - - - Open in Playground - - Test prompts over your{" "} - {isEvalSuite ? "evaluation suite" : "dataset"} and run - evaluations interactively - - - + {canUsePlayground && ( + + + + Open in Playground + + Test prompts over your{" "} + {isEvalSuite ? "evaluation suite" : "dataset"} and run + evaluations interactively + + + + )} {canCreateExperiments && ( { diff --git a/apps/opik-frontend/src/v2/pages/PromptPage/PromptTab/PromptTab.tsx b/apps/opik-frontend/src/v2/pages/PromptPage/PromptTab/PromptTab.tsx index a61d1006438..f656571247d 100644 --- a/apps/opik-frontend/src/v2/pages/PromptPage/PromptTab/PromptTab.tsx +++ b/apps/opik-frontend/src/v2/pages/PromptPage/PromptTab/PromptTab.tsx @@ -26,12 +26,17 @@ import ChatPromptView from "./ChatPromptView"; import TextPromptView from "./TextPromptView"; import TagListRenderer from "@/shared/TagListRenderer/TagListRenderer"; import usePromptVersionsUpdateMutation from "@/api/prompts/usePromptVersionsUpdateMutation"; +import { usePermissions } from "@/contexts/PermissionsContext"; interface PromptTabInterface { prompt?: PromptWithLatestVersion; } const PromptTab = ({ prompt }: PromptTabInterface) => { + const { + permissions: { canUsePlayground }, + } = usePermissions(); + const [openUseThisPrompt, setOpenUseThisPrompt] = useState(false); const [openEditPrompt, setOpenEditPrompt] = useState(false); const [versionToRestore, setVersionToRestore] = @@ -120,8 +125,13 @@ const PromptTab = ({ prompt }: PromptTabInterface) => { Use this prompt - - {!isChatPrompt && ( + {canUsePlayground && ( + + )} + {canUsePlayground && !isChatPrompt && ( playgroundRoute, component: PlaygroundPage, }); @@ -544,7 +551,7 @@ const routeTree = rootRoute.addChildren([ evaluationSuiteRoute.addChildren([evaluationSuiteItemsRoute]), ]), promptsRoute.addChildren([promptsListRoute, promptRoute]), - playgroundRoute, + playgroundRoute.addChildren([playgroundIndexRoute]), optimizationsRoute.addChildren([ optimizationsListRoute, optimizationsNewRoute,