Skip to content

Commit 8380ecf

Browse files
boston008Kris Rane
andauthored
Openapi import advanced option tenant and prefix (#176)
Openapi import advanced option tenant and prefix Co-authored-by: Kris Rane <kris.rane@e2open.com>
1 parent 07c2176 commit 8380ecf

4 files changed

Lines changed: 104 additions & 12 deletions

File tree

internal/apiserver/handler/openapi.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package handler
22

33
import (
4+
"github.com/amoylab/unla/internal/common/config"
45
"github.com/gin-gonic/gin"
56
"github.com/amoylab/unla/internal/apiserver/database"
67
"github.com/amoylab/unla/internal/i18n"
@@ -65,13 +66,20 @@ func (h *OpenAPI) HandleImport(c *gin.Context) {
6566
return
6667
}
6768

68-
// Create converter
69+
// Read tenant and prefix from form
70+
tenant := c.PostForm("tenantId")
71+
prefix := c.PostForm("prefix")
72+
6973
h.logger.Debug("creating OpenAPI converter")
7074
converter := openapi.NewConverter()
7175

72-
// Convert the OpenAPI specification
73-
h.logger.Debug("converting OpenAPI specification")
74-
config, err := converter.Convert(content)
76+
// Use provided tenant/prefix if not empty, else use default logic
77+
var config *config.MCPConfig
78+
if tenant == "" && prefix == "" {
79+
config, err = converter.Convert(content)
80+
} else {
81+
config, err = converter.ConvertWithOptions(content, tenant, prefix)
82+
}
7583
if err != nil {
7684
h.logger.Error("failed to convert OpenAPI specification", zap.Error(err))
7785
i18n.RespondWithError(c, i18n.ErrBadRequest.WithParam("Reason", "Failed to convert OpenAPI specification: "+err.Error()))

pkg/openapi/converter.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,38 @@ func (c *Converter) ConvertFromYAML(yamlData []byte) (*config.MCPConfig, error)
330330
return c.Convert(yamlData)
331331
}
332332

333+
// ConvertWithOptions converts OpenAPI specification to MCP configuration, using tenant and prefix if provided
334+
func (c *Converter) ConvertWithOptions(specData []byte, tenant, prefix string) (*config.MCPConfig, error) {
335+
config, err := c.Convert(specData)
336+
if err != nil {
337+
return nil, err
338+
}
339+
// Remove leading / from tenant if present
340+
cleanTenant := tenant
341+
if strings.HasPrefix(cleanTenant, "/") {
342+
cleanTenant = strings.TrimPrefix(cleanTenant, "/")
343+
}
344+
if tenant != "" && prefix != "" {
345+
config.Tenant = cleanTenant
346+
if len(config.Routers) > 0 {
347+
config.Routers[0].Prefix = cleanTenant + "/" + prefix
348+
}
349+
} else if tenant != "" {
350+
config.Tenant = cleanTenant
351+
if len(config.Routers) > 0 {
352+
// Autogenerate prefix as in default logic
353+
rs := lol.RandomString(4)
354+
config.Routers[0].Prefix = cleanTenant + "/" + rs
355+
}
356+
} else if prefix != "" {
357+
config.Tenant = "default"
358+
if len(config.Routers) > 0 {
359+
config.Routers[0].Prefix = "/default/" + prefix
360+
}
361+
}
362+
return config, nil
363+
}
364+
333365
// contains checks if a string is in a slice
334366
func contains(slice []string, str string) bool {
335367
for _, v := range slice {

web/src/pages/gateway/components/OpenAPIImport.tsx

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
1-
import { Card, CardBody, Button } from '@heroui/react';
1+
import { Card, CardBody, Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Input } from '@heroui/react';
22
import { t } from 'i18next';
3-
import React, { useCallback } from 'react';
3+
import React, { useCallback, useState, useEffect } from 'react';
44
import { useDropzone } from 'react-dropzone';
55

66
import LocalIcon from '@/components/LocalIcon';
7-
import { importOpenAPI } from '@/services/api';
7+
import { importOpenAPI, getTenants } from '@/services/api';
88
import { toast } from "@/utils/toast.ts";
9+
import type { Tenant } from '@/types/gateway';
910

1011
interface OpenAPIImportProps {
1112
onSuccess?: () => void;
1213
}
1314

1415
const OpenAPIImport: React.FC<OpenAPIImportProps> = ({ onSuccess }) => {
16+
const [showAdvanced, setShowAdvanced] = useState(false);
17+
const [tenants, setTenants] = useState<Tenant[]>([]);
18+
const [selectedTenant, setSelectedTenant] = useState<string>('');
19+
const [prefix, setPrefix] = useState('');
20+
const [loadingTenants, setLoadingTenants] = useState(false);
21+
22+
useEffect(() => {
23+
if (showAdvanced && tenants.length === 0) {
24+
setLoadingTenants(true);
25+
getTenants()
26+
.then((data) => setTenants(data))
27+
.catch(() => toast.error(t('errors.fetch_tenants')))
28+
.finally(() => setLoadingTenants(false));
29+
}
30+
}, [showAdvanced, tenants.length]);
31+
1532
const onDrop = useCallback(async (acceptedFiles: globalThis.File[]) => {
1633
if (acceptedFiles.length === 0) {
1734
toast.error(t('errors.invalid_openapi_file'), {
@@ -21,7 +38,11 @@ const OpenAPIImport: React.FC<OpenAPIImportProps> = ({ onSuccess }) => {
2138
}
2239

2340
try {
24-
await importOpenAPI(acceptedFiles[0]);
41+
// Find the selected tenant object
42+
const tenantObj = tenants.find((t: Tenant) => t.id.toString() === selectedTenant);
43+
// Use tenant.prefix if available, otherwise empty string
44+
const tenantPrefix = tenantObj ? tenantObj.prefix : '';
45+
await importOpenAPI(acceptedFiles[0], tenantPrefix, prefix);
2546
toast.success(t('errors.import_openapi_success'), {
2647
duration: 3000,
2748
});
@@ -31,7 +52,7 @@ const OpenAPIImport: React.FC<OpenAPIImportProps> = ({ onSuccess }) => {
3152
duration: 3000,
3253
})
3354
}
34-
}, [onSuccess]);
55+
}, [onSuccess, selectedTenant, prefix, tenants]);
3556

3657
const { getRootProps, getInputProps, isDragActive } = useDropzone({
3758
onDrop,
@@ -52,15 +73,15 @@ const OpenAPIImport: React.FC<OpenAPIImportProps> = ({ onSuccess }) => {
5273
isDragActive ? 'bg-primary/10 border-primary' : 'bg-content2 border-divider'
5374
}`}
5475
>
55-
<input {...getInputProps()} />
76+
<input {...getInputProps()} style={{ display: 'none' }} />
5677
<LocalIcon icon="lucide:upload" className="text-4xl mb-4 text-primary" />
5778
{isDragActive ? (
5879
<p className="text-lg text-primary">Drop the OpenAPI specification file here...</p>
5980
) : (
6081
<div className="text-center">
6182
<p className="text-lg">Drag and drop an OpenAPI specification file here</p>
6283
<p className="text-sm text-default-500 mt-2">or</p>
63-
<Button color="primary" variant="flat" className="mt-2">
84+
<Button color="primary" variant="flat" className="mt-2" onClick={e => { e.stopPropagation(); document.querySelector<HTMLInputElement>('input[type="file"]')?.click(); }}>
6485
Select a file
6586
</Button>
6687
</div>
@@ -69,6 +90,34 @@ const OpenAPIImport: React.FC<OpenAPIImportProps> = ({ onSuccess }) => {
6990
Supported formats: JSON (.json), YAML (.yaml, .yml)
7091
</p>
7192
</div>
93+
<div className="mt-4 w-full flex flex-col items-center">
94+
<Button size="sm" variant="light" onClick={() => setShowAdvanced((v) => !v)}>
95+
{showAdvanced ? t('common.hide_advanced_options', 'Hide Advanced Options') : t('common.show_advanced_options', 'Show Advanced Options')}
96+
</Button>
97+
{showAdvanced && (
98+
<div className="w-full mt-4 flex flex-col gap-4 items-center">
99+
<div className="w-full max-w-xs">
100+
<label className="block text-sm font-medium mb-1">{t('gateway.tenant', 'Tenant')}</label>
101+
<Dropdown isDisabled={loadingTenants || tenants.length === 0} className="w-full">
102+
<DropdownTrigger>
103+
<Button variant="bordered" className="w-full">
104+
{selectedTenant ? tenants.find(t => t.id.toString() === selectedTenant)?.name : t('gateway.select_tenant', 'Select Tenant')}
105+
</Button>
106+
</DropdownTrigger>
107+
<DropdownMenu aria-label="Tenant List" selectionMode="single" selectedKeys={selectedTenant ? [selectedTenant] : []} onAction={key => setSelectedTenant(key as string)}>
108+
{tenants.map(tenant => (
109+
<DropdownItem key={tenant.id.toString()}>{tenant.name}</DropdownItem>
110+
))}
111+
</DropdownMenu>
112+
</Dropdown>
113+
</div>
114+
<div className="w-full max-w-xs">
115+
<label className="block text-sm font-medium mb-1">{t('gateway.prefix', 'Prefix')}</label>
116+
<Input value={prefix} onChange={e => setPrefix(e.target.value)} placeholder={t('gateway.prefix_placeholder', 'Enter prefix (optional)')} />
117+
</div>
118+
</div>
119+
)}
120+
</div>
72121
</CardBody>
73122
</Card>
74123
);

web/src/services/api.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,13 @@ export const deleteChatSession = async (sessionId: string) => {
168168
}
169169
};
170170

171-
export const importOpenAPI = async (file: File) => {
171+
export const importOpenAPI = async (file: File, tenantId?: string, prefix?: string) => {
172172
try {
173173
const formData = new globalThis.FormData();
174174
formData.append('file', file);
175+
// Always send tenantId and prefix, even if empty
176+
formData.append('tenantId', tenantId ?? '');
177+
formData.append('prefix', prefix ?? '');
175178

176179
const response = await api.post('/openapi/import', formData, {
177180
headers: {

0 commit comments

Comments
 (0)