Skip to content

Commit ece5f4b

Browse files
authored
Merge branch 'main' into nm-48798-50565
2 parents 27dc1cc + e5cc502 commit ece5f4b

17 files changed

Lines changed: 1468 additions & 139 deletions

File tree

frontend/src/pages/projects/projectPermissions/SubjectRolesTableRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const SubjectRolesTableRow: React.FC<SubjectRolesTableRowProps> = ({
4343
>
4444
<Split hasGutter>
4545
<SplitItem>
46-
<RoleDetailsLink roleRef={row.roleRef} role={row.role} showAssigneesTab />
46+
<RoleDetailsLink roleRef={row.roleRef} role={row.role} />
4747
</SplitItem>
4848
<SplitItem>
4949
<RoleLabel roleRef={row.roleRef} role={row.role} isCompact />

frontend/src/pages/projects/projectPermissions/__tests__/RoleDetailsModal.spec.tsx

Lines changed: 379 additions & 101 deletions
Large diffs are not rendered by default.

frontend/src/pages/projects/projectPermissions/components/RoleDetailsLink.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,17 @@ import RoleDetailsModal from '#~/pages/projects/projectPermissions/roleDetails/R
88
type RoleDetailsLinkProps = {
99
roleRef: RoleRef;
1010
role?: RoleKind | ClusterRoleKind;
11-
showAssigneesTab?: boolean;
1211
};
1312

14-
const RoleDetailsLink: React.FC<RoleDetailsLinkProps> = ({ roleRef, role, showAssigneesTab }) => {
13+
const RoleDetailsLink: React.FC<RoleDetailsLinkProps> = ({ roleRef, role }) => {
1514
const [isOpen, setIsOpen] = React.useState(false);
1615

1716
return (
1817
<>
1918
<Button variant="link" isInline onClick={() => setIsOpen(true)} data-testid="role-link">
2019
{getRoleDisplayName(roleRef, role)}
2120
</Button>
22-
{isOpen ? (
23-
<RoleDetailsModal
24-
roleRef={roleRef}
25-
onClose={() => setIsOpen(false)}
26-
showAssigneesTab={showAssigneesTab}
27-
/>
28-
) : null}
21+
{isOpen ? <RoleDetailsModal roleRef={roleRef} onClose={() => setIsOpen(false)} /> : null}
2922
</>
3023
);
3124
};

frontend/src/pages/projects/projectPermissions/manageRoles/ManageRolesTableRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const ManageRolesTableRow: React.FC<ManageRolesTableRowProps> = ({
8383
aria-label={`Toggle ${row.displayName}`}
8484
/>
8585
<Td dataLabel="Role">
86-
<RoleDetailsLink roleRef={row.roleRef} role={row.role} showAssigneesTab={false} />
86+
<RoleDetailsLink roleRef={row.roleRef} role={row.role} />
8787
</Td>
8888
<Td dataLabel="Description">{getRoleDescription(row.roleRef, row.role) ?? '-'}</Td>
8989
<Td dataLabel="Role type">

frontend/src/pages/projects/projectPermissions/roleDetails/RoleDetailsModal.tsx

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,12 @@ import RoleDetailsModalAssigneesTab from './RoleDetailsModalAssigneesTab';
2323
type RoleDetailsModalProps = {
2424
roleRef: RoleRef;
2525
onClose: () => void;
26-
showAssigneesTab?: boolean;
2726
};
2827

2928
type TabKey = 'details' | 'assignees';
3029
const isTabKey = (key: unknown): key is TabKey => key === 'details' || key === 'assignees';
3130

32-
const RoleDetailsModal: React.FC<RoleDetailsModalProps> = ({
33-
roleRef,
34-
onClose,
35-
showAssigneesTab = true,
36-
}) => {
31+
const RoleDetailsModal: React.FC<RoleDetailsModalProps> = ({ roleRef, onClose }) => {
3732
const { roles, clusterRoles } = usePermissionsContext();
3833

3934
const role = React.useMemo(
@@ -70,26 +65,22 @@ const RoleDetailsModal: React.FC<RoleDetailsModalProps> = ({
7065
description={getRoleDescription(roleRef, role)}
7166
/>
7267
<ModalBody>
73-
{showAssigneesTab ? (
74-
<Tabs
75-
activeKey={activeTabKey}
76-
onSelect={(_e, key) => {
77-
if (isTabKey(key)) {
78-
setActiveTabKey(key);
79-
}
80-
}}
81-
aria-label="Role details tabs"
82-
>
83-
<Tab eventKey="details" title={<TabTitleText>Role details</TabTitleText>}>
84-
<RoleDetailsModalDetailsTab roleRef={roleRef} role={role} />
85-
</Tab>
86-
<Tab eventKey="assignees" title={<TabTitleText>Assignees</TabTitleText>}>
87-
<RoleDetailsModalAssigneesTab roleRef={roleRef} />
88-
</Tab>
89-
</Tabs>
90-
) : (
91-
<RoleDetailsModalDetailsTab roleRef={roleRef} role={role} />
92-
)}
68+
<Tabs
69+
activeKey={activeTabKey}
70+
onSelect={(_e, key) => {
71+
if (isTabKey(key)) {
72+
setActiveTabKey(key);
73+
}
74+
}}
75+
aria-label="Role details tabs"
76+
>
77+
<Tab eventKey="details" title={<TabTitleText>Role details</TabTitleText>}>
78+
<RoleDetailsModalDetailsTab roleRef={roleRef} role={role} />
79+
</Tab>
80+
<Tab eventKey="assignees" title={<TabTitleText>Assignees</TabTitleText>}>
81+
<RoleDetailsModalAssigneesTab roleRef={roleRef} />
82+
</Tab>
83+
</Tabs>
9384
</ModalBody>
9485
</Modal>
9586
);

packages/gen-ai/bff/internal/api/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ func (app *App) Routes() http.Handler {
316316

317317
// AI Assets Models (Kubernetes)
318318
apiRouter.GET(constants.ModelsAAPath, app.AttachNamespace(app.RequireAccessToService(app.ModelsAAHandler)))
319+
apiRouter.POST(constants.ExternalModelsPath, app.AttachNamespace(app.RequireAccessToService(app.CreateExternalModelHandler)))
319320

320321
// Settings path namespace endpoints. This endpoint will get all the namespaces
321322
apiRouter.GET(constants.NamespacesPath, app.RequireAccessToService(app.GetNamespaceHandler))
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/julienschmidt/httprouter"
8+
"github.com/opendatahub-io/gen-ai/internal/constants"
9+
"github.com/opendatahub-io/gen-ai/internal/integrations"
10+
"github.com/opendatahub-io/gen-ai/internal/models"
11+
)
12+
13+
type CreateExternalModelEnvelope Envelope[models.AAModel, None]
14+
15+
// CreateExternalModelHandler handles the creation of external model endpoints
16+
func (app *App) CreateExternalModelHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
17+
ctx := r.Context()
18+
19+
// Get namespace from context
20+
namespace, ok := r.Context().Value(constants.NamespaceQueryParameterKey).(string)
21+
if !ok || namespace == "" {
22+
app.badRequestResponse(w, r, fmt.Errorf("missing namespace in the context"))
23+
return
24+
}
25+
26+
// Get the request identity from context
27+
identity, ok := ctx.Value(constants.RequestIdentityKey).(*integrations.RequestIdentity)
28+
if !ok || identity == nil {
29+
app.unauthorizedResponse(w, r, fmt.Errorf("missing RequestIdentity in context"))
30+
return
31+
}
32+
33+
// Parse request body
34+
var req models.ExternalModelRequest
35+
err := app.ReadJSON(w, r, &req)
36+
if err != nil {
37+
app.badRequestResponse(w, r, err)
38+
return
39+
}
40+
41+
// Validate required fields
42+
if req.ModelID == "" {
43+
app.badRequestResponse(w, r, fmt.Errorf("model_id is required"))
44+
return
45+
}
46+
if req.ModelDisplayName == "" {
47+
app.badRequestResponse(w, r, fmt.Errorf("model_display_name is required"))
48+
return
49+
}
50+
if req.BaseURL == "" {
51+
app.badRequestResponse(w, r, fmt.Errorf("base_url is required"))
52+
return
53+
}
54+
if req.SecretValue == "" {
55+
app.badRequestResponse(w, r, fmt.Errorf("secret_value is required"))
56+
return
57+
}
58+
if req.ProviderType == "" {
59+
app.badRequestResponse(w, r, fmt.Errorf("provider_type is required"))
60+
return
61+
}
62+
if req.ModelType == "" {
63+
app.badRequestResponse(w, r, fmt.Errorf("model_type is required"))
64+
return
65+
}
66+
67+
// Validate provider type
68+
validProviderTypes := map[models.ProviderTypeEnum]bool{
69+
models.ProviderTypeGemini: true,
70+
models.ProviderTypeOpenAI: true,
71+
models.ProviderTypeAnthropic: true,
72+
models.ProviderTypeVLLM: true,
73+
}
74+
if !validProviderTypes[req.ProviderType] {
75+
app.badRequestResponse(w, r, fmt.Errorf("invalid provider_type: %s", req.ProviderType))
76+
return
77+
}
78+
79+
// Validate model type
80+
validModelTypes := map[models.ModelTypeEnum]bool{
81+
models.ModelTypeEmbedding: true,
82+
models.ModelTypeLLM: true,
83+
}
84+
if !validModelTypes[req.ModelType] {
85+
app.badRequestResponse(w, r, fmt.Errorf("invalid model_type: %s", req.ModelType))
86+
return
87+
}
88+
89+
// Get Kubernetes client
90+
client, err := app.kubernetesClientFactory.GetClient(ctx)
91+
if err != nil {
92+
app.badRequestResponse(w, r, err)
93+
return
94+
}
95+
96+
// Create external model
97+
response, err := app.repositories.ExternalModels.CreateExternalModel(client, ctx, identity, namespace, req)
98+
if err != nil {
99+
app.serverErrorResponse(w, r, err)
100+
return
101+
}
102+
103+
// Return success response
104+
envelope := CreateExternalModelEnvelope{
105+
Data: *response,
106+
}
107+
err = app.WriteJSON(w, http.StatusCreated, envelope, nil)
108+
if err != nil {
109+
app.serverErrorResponse(w, r, err)
110+
return
111+
}
112+
}

0 commit comments

Comments
 (0)