Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion portals/ai-workspace/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
services:

platform-api:
image: ghcr.io/wso2/api-platform/platform-api:0.16.0-SNAPSHOT
image: ghcr.io/wso2/api-platform/platform-api:0.17.0-SNAPSHOT
restart: unless-stopped
profiles: ["platform-api"]
command: ["-config", "/etc/platform-api/config.toml"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@

import React, { useMemo, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Box, Button, CircularProgress, Typography } from '@wso2/oxygen-ui';
import {
Alert,
Box,
Button,
CircularProgress,
Typography,
} from '@wso2/oxygen-ui';
import { Plus } from '@wso2/oxygen-ui-icons-react';
import { FormattedMessage } from 'react-intl';
import type { Gateway } from '../../apis/gateway/types';
Expand All @@ -39,7 +45,8 @@ interface GatewayDeployMainSectionProps {
export default function GatewayDeployMainSection({
showConfigureOption = true,
}: GatewayDeployMainSectionProps = {}) {
const { gateways, isLoading, error } = useGatewayDeploy();
const { gateways, isLoading, error, canDeploy, canViewDeployments } =
useGatewayDeploy();
const { currentOrganization } = useAppShell();
const [searchQuery, setSearchQuery] = useState('');
const [configDrawerOpen, setConfigDrawerOpen] = useState(false);
Expand Down Expand Up @@ -78,6 +85,22 @@ export default function GatewayDeployMainSection({
);
}

// Without the deployment-read scope the gateway list is empty by design, not
// because the organization has no gateways — say so instead of inviting the
// user to create one they wouldn't be able to deploy to or even see.
if (!canViewDeployments) {
return (
<Box sx={{ mt: 2 }}>
<Alert severity="info">
<FormattedMessage
id="aiWorkspace.components.GatewayDeploy.GatewayDeployMainSection.no.deployment.read.access"
defaultMessage="You do not have access to view deployments for this artifact. Please contact your admin."
/>
</Alert>
</Box>
);
}

if (gateways.length === 0) {
return (
<Box
Expand Down Expand Up @@ -123,6 +146,17 @@ export default function GatewayDeployMainSection({

return (
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
{/* The deploy/redeploy/restore/undeploy controls below are all disabled in
this case; without this the page would read as broken rather than as a
permission boundary. */}
{!canDeploy && (
<Alert severity="info" sx={{ mb: 2 }}>
<FormattedMessage
id="aiWorkspace.components.GatewayDeploy.GatewayDeployMainSection.read.only.deployments"
defaultMessage="You have read-only access to deployments. Please contact your admin to deploy this artifact."
/>
</Alert>
)}
<GatewayDeployList
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default function GatewayDeploymentHistory({
deploymentsError,
refetchDeployments,
deleteDeployment,
canDelete,
} = useGatewayDeploy();

const gatewayDeployments =
Expand Down Expand Up @@ -169,7 +170,7 @@ export default function GatewayDeploymentHistory({
deployment.deploymentId === currentDeploymentId
}
isInDrawer
onDelete={handleDeleteDeployment}
onDelete={canDelete ? handleDeleteDeployment : undefined}
/>
))}
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { Alert, Button } from '@wso2/oxygen-ui';
import { RefreshCcw } from '@wso2/oxygen-ui-icons-react';

interface PartialLoadWarningProps {
/** Short, user-facing note about which source could not be loaded. */
message: string;
onRetry: () => void;
retryLabel?: string;
}

/**
* Non-blocking notice for a list assembled from several independent sources
* (for example, Policy Hub policies plus the organization's custom policies).
* One source failing must not hide the rest, so the caller keeps rendering
* whatever loaded and shows this above it with a retry for the failed source.
*/
export default function PartialLoadWarning({
message,
onRetry,
retryLabel = 'Retry',
}: PartialLoadWarningProps) {
return (
<Alert
severity="warning"
sx={{ mb: 1 }}
action={
<Button
color="inherit"
size="small"
startIcon={<RefreshCcw size={14} />}
onClick={onRetry}
>
{retryLabel}
</Button>
}
>
{message}
</Alert>
);
}
27 changes: 27 additions & 0 deletions portals/ai-workspace/src/auth/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,33 @@ export const SCOPES = {
SECRET_MANAGE: 'ap:secret:manage',
} as const;

/**
* Deployment scopes per deployable AI artifact kind, keyed by the same resource
* type `GatewayDeployProvider` takes. Shared by the deploy page/context and the
* "Deploy to Gateway" entry points on the overview pages, so one map decides
* both whether the deploy actions render and whether the button leading to them
* is reachable.
*/
export const DEPLOYMENT_SCOPES = {
provider: {
read: SCOPES.LLM_PROVIDER_DEPLOYMENT_READ,
create: SCOPES.LLM_PROVIDER_DEPLOYMENT_CREATE,
delete: SCOPES.LLM_PROVIDER_DEPLOYMENT_DELETE,
},
proxy: {
read: SCOPES.LLM_PROXY_DEPLOYMENT_READ,
create: SCOPES.LLM_PROXY_DEPLOYMENT_CREATE,
delete: SCOPES.LLM_PROXY_DEPLOYMENT_DELETE,
},
'mcp-server': {
read: SCOPES.MCP_PROXY_DEPLOYMENT_READ,
create: SCOPES.MCP_PROXY_DEPLOYMENT_CREATE,
delete: SCOPES.MCP_PROXY_DEPLOYMENT_DELETE,
},
} as const;

export type DeployableResourceType = keyof typeof DEPLOYMENT_SCOPES;

/**
* Scopes that must be held explicitly and are never derived from a broader
* `:manage`. `ap:api_key:all:manage` is an ownership override (it widens which
Expand Down
Loading
Loading