diff --git a/apps/opik-frontend/src/plugins/comet/PermissionsProvider.tsx b/apps/opik-frontend/src/plugins/comet/PermissionsProvider.tsx index 08e2eb7c734..add20b908e5 100644 --- a/apps/opik-frontend/src/plugins/comet/PermissionsProvider.tsx +++ b/apps/opik-frontend/src/plugins/comet/PermissionsProvider.tsx @@ -33,6 +33,7 @@ const PermissionsProvider: React.FC<{ children: ReactNode }> = ({ canUpdateAlerts, canAnnotateTraceSpanThread, canTagTrace, + canUsePlayground, isPending, } = useUserPermission(); @@ -65,6 +66,7 @@ const PermissionsProvider: React.FC<{ children: ReactNode }> = ({ canUpdateAlerts, canAnnotateTraceSpanThread, canTagTrace, + canUsePlayground, }, isPending, }), @@ -95,6 +97,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 70c24a15b99..30380f3df3b 100644 --- a/apps/opik-frontend/src/plugins/comet/types.ts +++ b/apps/opik-frontend/src/plugins/comet/types.ts @@ -87,6 +87,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 0972af6751f..d243b7bdd3f 100644 --- a/apps/opik-frontend/src/plugins/comet/useUserPermission.ts +++ b/apps/opik-frontend/src/plugins/comet/useUserPermission.ts @@ -228,6 +228,11 @@ const useUserPermission = (config?: { enabled?: boolean }) => { [checkNullablePermission], ); + const canUsePlayground = useMemo( + () => checkNullablePermission(ManagementPermissionsNames.PLAYGROUND_USE), + [checkNullablePermission], + ); + return { canInviteMembers, isWorkspaceOwner, @@ -257,6 +262,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 2fceca3c464..b1ea1b21a43 100644 --- a/apps/opik-frontend/src/types/permissions.ts +++ b/apps/opik-frontend/src/types/permissions.ts @@ -25,6 +25,7 @@ export interface Permissions { canUpdateAlerts: boolean; canAnnotateTraceSpanThread: boolean; canTagTrace: boolean; + canUsePlayground: boolean; } export interface PermissionsContextValue { @@ -60,6 +61,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 acab13b8142..c0b2a91c543 100644 --- a/apps/opik-frontend/src/v2/layout/SideBar/SideBarMenuItems.tsx +++ b/apps/opik-frontend/src/v2/layout/SideBar/SideBarMenuItems.tsx @@ -14,13 +14,14 @@ interface SideBarMenuItemsProps { const SideBarMenuItems: React.FC = ({ expanded }) => { 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 632b146fd10..0debc9a7370 100644 --- a/apps/opik-frontend/src/v2/layout/SideBar/helpers/getMenuItems.ts +++ b/apps/opik-frontend/src/v2/layout/SideBar/helpers/getMenuItems.ts @@ -25,10 +25,12 @@ const getMenuItems = ({ projectId, canViewExperiments, canViewDatasets, + canUsePlayground, }: { projectId: string | null; canViewExperiments: boolean; canViewDatasets: boolean; + canUsePlayground: boolean; }): MenuItemGroup[] => { const projectPrefix = projectId ? "/$workspaceName/projects/$projectId" @@ -110,14 +112,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 ffbaa5c20d5..30cf3b7c1cb 100644 --- a/apps/opik-frontend/src/v2/pages/EvaluationSuiteItemsPage/UseEvaluationSuiteDropdown.tsx +++ b/apps/opik-frontend/src/v2/pages/EvaluationSuiteItemsPage/UseEvaluationSuiteDropdown.tsx @@ -38,9 +38,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(); @@ -60,6 +62,8 @@ function UseEvaluationSuiteDropdown({ } }; + if (!hasAnyAction) return null; + return ( <> {canViewExperiments && ( @@ -71,19 +75,21 @@ function UseEvaluationSuiteDropdown({ projectId={projectId} /> )} - + {canUsePlayground && ( + + )} @@ -100,20 +106,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, }); @@ -554,7 +561,7 @@ const routeTree = rootRoute.addChildren([ evaluationSuiteRoute.addChildren([evaluationSuiteItemsRoute]), ]), promptsRoute.addChildren([promptsListRoute, promptRoute]), - playgroundRoute, + playgroundRoute.addChildren([playgroundIndexRoute]), optimizationsRoute.addChildren([ optimizationsListRoute, optimizationsNewRoute,