From 677ed24cd885d8971d6927ec17634a87c7b7f839 Mon Sep 17 00:00:00 2001 From: Dilan Induwara <153802063+Induwara04@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:36:23 +0530 Subject: [PATCH 1/5] Add API name, Identifier and version validation --- .../src/context/validationContext.tsx | 190 +++++++++-- .../management-portal/src/hooks/validation.ts | 92 +++++- .../src/pages/apis/ApiOverview.tsx | 189 ++++++++--- .../GithubCreationFlow.tsx | 75 ++--- .../ContractCreationFlows/URLCreationFlow.tsx | 11 +- .../UploadCreationFlow.tsx | 164 +++++++--- .../apis/CreationFlows/CreationMetaData.tsx | 306 ++++++++++++++++-- .../CreationFlows/EndPointCreationFlow.tsx | 47 ++- 8 files changed, 863 insertions(+), 211 deletions(-) diff --git a/portals/management-portal/src/context/validationContext.tsx b/portals/management-portal/src/context/validationContext.tsx index d8936ae9d..b99540736 100644 --- a/portals/management-portal/src/context/validationContext.tsx +++ b/portals/management-portal/src/context/validationContext.tsx @@ -10,8 +10,11 @@ import React, { } from "react"; import { useGithubProjectValidation, + useApiUniquenessValidation, type GithubProjectValidationRequest, type GithubProjectValidationResponse, + type ApiNameVersionValidationRequest, + type ApiUniquenessValidationResponse, } from "../hooks/validation"; /** Keep the name “githubprojectvalidation” semantic in value keys & exports */ @@ -27,21 +30,39 @@ type GithubProjectValidationContextValue = { setBranch: (b: string | null) => void; setPath: (p: string | null) => void; - // state + // state (github project validation) loading: boolean; error: string | null; result: GithubProjectValidationResponse | null; - // actions + // actions (github project validation) validate: ( override?: Partial ) => Promise; - // convenience + // convenience (github project validation) isValid: boolean | null; // null when no result yet errors: string[]; // [] when valid or no result - // NEW: allow consumers to clear error/result/loading + // name+version uniqueness validation + nameVersionLoading: boolean; + nameVersionError: string | null; + nameVersionResult: ApiUniquenessValidationResponse | null; + validateNameVersion: ( + payload: ApiNameVersionValidationRequest + ) => Promise; + isNameVersionUnique: boolean | null; + + // identifier uniqueness validation + identifierLoading: boolean; + identifierError: string | null; + identifierResult: ApiUniquenessValidationResponse | null; + validateIdentifier: ( + identifier: string + ) => Promise; + isIdentifierUnique: boolean | null; + + // reset reset: () => void; }; @@ -51,8 +72,20 @@ const Ctx = createContext( type Props = { children: ReactNode }; -export const GithubProjectValidationProvider: React.FC = ({ children }) => { +/** --- Type guards (avoid any / avoid TS2367) --- */ +const isGithubValidationErr = ( + r: GithubProjectValidationResponse +): r is Extract< + GithubProjectValidationResponse, + { isAPIProjectValid: false } +> => r.isAPIProjectValid === false; + +export const GithubProjectValidationProvider: React.FC = ({ + children, +}) => { const { validateGithubApiProject } = useGithubProjectValidation(); + const { validateApiNameVersion, validateApiIdentifier } = + useApiUniquenessValidation(); // inputs const [repoUrl, setRepoUrl] = useState(""); @@ -60,22 +93,39 @@ export const GithubProjectValidationProvider: React.FC = ({ children }) = const [branch, setBranch] = useState(null); const [path, setPath] = useState(null); - // state + // state (github project) const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [result, setResult] = useState(null); + const [result, setResult] = useState( + null + ); + + // state (name+version) + const [nameVersionLoading, setNameVersionLoading] = useState(false); + const [nameVersionError, setNameVersionError] = useState(null); + const [nameVersionResult, setNameVersionResult] = + useState(null); + + // state (identifier) + const [identifierLoading, setIdentifierLoading] = useState(false); + const [identifierError, setIdentifierError] = useState(null); + const [identifierResult, setIdentifierResult] = + useState(null); // lifecycle guards const mountedRef = useRef(true); const pendingRef = useRef(0); + const begin = () => { pendingRef.current += 1; setLoading(true); }; + const end = () => { pendingRef.current = Math.max(0, pendingRef.current - 1); if (pendingRef.current === 0 && mountedRef.current) setLoading(false); }; + useEffect(() => { mountedRef.current = true; return () => { @@ -101,7 +151,9 @@ export const GithubProjectValidationProvider: React.FC = ({ children }) = setError(null); try { - const res = await validateGithubApiProject(effective, { signal: ac.signal }); + const res = await validateGithubApiProject(effective, { + signal: ac.signal, + }); if (mountedRef.current) setResult(res); return res; } catch (e) { @@ -119,27 +171,96 @@ export const GithubProjectValidationProvider: React.FC = ({ children }) = [repoUrl, provider, branch, path, validateGithubApiProject] ); + const validateNameVersion = useCallback( + async (payload: ApiNameVersionValidationRequest) => { + setNameVersionLoading(true); + setNameVersionError(null); + + try { + const res = await validateApiNameVersion(payload); + if (mountedRef.current) setNameVersionResult(res); + return res; + } catch (e) { + const msg = + e instanceof Error + ? e.message + : "Failed to validate API name & version"; + if (mountedRef.current) { + setNameVersionError(msg); + setNameVersionResult(null); + } + throw e; + } finally { + if (mountedRef.current) setNameVersionLoading(false); + } + }, + [validateApiNameVersion] + ); + + const validateIdentifier = useCallback( + async (identifier: string) => { + setIdentifierLoading(true); + setIdentifierError(null); + + try { + const res = await validateApiIdentifier(identifier); + if (mountedRef.current) setIdentifierResult(res); + return res; + } catch (e) { + const msg = + e instanceof Error ? e.message : "Failed to validate API identifier"; + if (mountedRef.current) { + setIdentifierError(msg); + setIdentifierResult(null); + } + throw e; + } finally { + if (mountedRef.current) setIdentifierLoading(false); + } + }, + [validateApiIdentifier] + ); + + /** ✅ FIX: no impossible comparisons; use discriminant narrowing */ const isValid = useMemo(() => { if (!result) return null; - return ( - !!(result as any).isAPIProjectValid && - !!(result as any).isAPIConfigValid && - !!(result as any).isAPIDefinitionValid - ); + return !isGithubValidationErr(result); }, [result]); + /** ✅ Type-safe errors extraction */ const errors = useMemo(() => { if (!result) return []; - return Array.isArray((result as any).errors) ? (result as any).errors : []; + return isGithubValidationErr(result) ? result.errors : []; }, [result]); - // NEW: reset helper + const isNameVersionUnique = useMemo(() => { + if (!nameVersionResult) return null; + return nameVersionResult.valid; + }, [nameVersionResult]); + + const isIdentifierUnique = useMemo(() => { + if (!identifierResult) return null; + return identifierResult.valid; + }, [identifierResult]); + const reset = useCallback(() => { pendingRef.current = 0; if (!mountedRef.current) return; + + // github project setLoading(false); setError(null); setResult(null); + + // name+version + setNameVersionLoading(false); + setNameVersionError(null); + setNameVersionResult(null); + + // identifier + setIdentifierLoading(false); + setIdentifierError(null); + setIdentifierResult(null); }, []); const value = useMemo( @@ -153,16 +274,34 @@ export const GithubProjectValidationProvider: React.FC = ({ children }) = setProvider, setBranch, setPath, - // state + + // github project state loading, error, result, - // actions + + // github project actions validate, - // convenience + + // github project convenience isValid, errors, - // new + + // name+version uniqueness + nameVersionLoading, + nameVersionError, + nameVersionResult, + validateNameVersion, + isNameVersionUnique, + + // identifier uniqueness + identifierLoading, + identifierError, + identifierResult, + validateIdentifier, + isIdentifierUnique, + + // reset reset, }), [ @@ -176,6 +315,16 @@ export const GithubProjectValidationProvider: React.FC = ({ children }) = validate, isValid, errors, + nameVersionLoading, + nameVersionError, + nameVersionResult, + validateNameVersion, + isNameVersionUnique, + identifierLoading, + identifierError, + identifierResult, + validateIdentifier, + isIdentifierUnique, reset, ] ); @@ -185,9 +334,10 @@ export const GithubProjectValidationProvider: React.FC = ({ children }) = export const useGithubProjectValidationContext = () => { const ctx = useContext(Ctx); - if (!ctx) + if (!ctx) { throw new Error( "useGithubProjectValidationContext must be used within GithubProjectValidationProvider" ); + } return ctx; }; diff --git a/portals/management-portal/src/hooks/validation.ts b/portals/management-portal/src/hooks/validation.ts index 0ed052595..6015f5ba7 100644 --- a/portals/management-portal/src/hooks/validation.ts +++ b/portals/management-portal/src/hooks/validation.ts @@ -30,7 +30,7 @@ export type GithubProjectValidationResponse = | GithubProjectValidationOK | GithubProjectValidationErr; - export type OpenApiValidationOK = { +export type OpenApiValidationOK = { isAPIDefinitionValid: true; api: Record; }; @@ -44,6 +44,23 @@ export type OpenApiValidationResponse = | OpenApiValidationOK | OpenApiValidationErr; +/** NEW: Uniqueness validation (name+version / identifier) */ + +export type ApiValidateError = { + code: string; + message: string; +}; + +export type ApiUniquenessValidationResponse = { + valid: boolean; + error: ApiValidateError | null; +}; + +export type ApiNameVersionValidationRequest = { + name: string; + version: string; +}; + /** ----- Helpers ----- */ const parseError = async (res: Response) => { @@ -99,8 +116,7 @@ export const useGithubProjectValidation = () => { ); } - const data = (await res.json()) as GithubProjectValidationResponse; - return data; + return (await res.json()) as GithubProjectValidationResponse; }, [] ); @@ -121,9 +137,7 @@ export const useOpenApiValidation = () => { const res = await fetch(`${baseUrl}/api/v1/validate/open-api`, { method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, + headers: { Authorization: `Bearer ${token}` }, body: formData, signal: opts?.signal, }); @@ -134,8 +148,7 @@ export const useOpenApiValidation = () => { ); } - const data = (await res.json()) as OpenApiValidationResponse; - return data; + return (await res.json()) as OpenApiValidationResponse; }, [] ); @@ -152,9 +165,7 @@ export const useOpenApiValidation = () => { const res = await fetch(`${baseUrl}/api/v1/validate/open-api`, { method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, + headers: { Authorization: `Bearer ${token}` }, body: formData, signal: opts?.signal, }); @@ -165,11 +176,66 @@ export const useOpenApiValidation = () => { ); } - const data = (await res.json()) as OpenApiValidationResponse; - return data; + return (await res.json()) as OpenApiValidationResponse; }, [] ); return { validateOpenApiUrl, validateOpenApiFile }; }; + +/** NEW: API uniqueness validation hook */ +export const useApiUniquenessValidation = () => { + /** GET: /api/v1/apis/validate?name=...&version=... */ + const validateApiNameVersion = useCallback( + async ( + payload: ApiNameVersionValidationRequest, + opts?: { signal?: AbortSignal } + ): Promise => { + const qs = new URLSearchParams({ + name: payload.name, + version: payload.version, + }).toString(); + + const res = await authedFetch(`/api/v1/apis/validate?${qs}`, { + method: "GET", + signal: opts?.signal, + }); + + if (!res.ok) { + throw new Error( + `Failed to validate API name & version: ${await parseError(res)}` + ); + } + + return (await res.json()) as ApiUniquenessValidationResponse; + }, + [] + ); + + /** GET: /api/v1/apis/validate?identifier=... */ + const validateApiIdentifier = useCallback( + async ( + identifier: string, + opts?: { signal?: AbortSignal } + ): Promise => { + const qs = new URLSearchParams({ identifier }).toString(); + + const res = await authedFetch(`/api/v1/apis/validate?${qs}`, { + method: "GET", + signal: opts?.signal, + }); + + if (!res.ok) { + throw new Error( + `Failed to validate API identifier: ${await parseError(res)}` + ); + } + + return (await res.json()) as ApiUniquenessValidationResponse; + }, + [] + ); + + return { validateApiNameVersion, validateApiIdentifier }; +}; diff --git a/portals/management-portal/src/pages/apis/ApiOverview.tsx b/portals/management-portal/src/pages/apis/ApiOverview.tsx index 56c7fd368..6b8aa1031 100644 --- a/portals/management-portal/src/pages/apis/ApiOverview.tsx +++ b/portals/management-portal/src/pages/apis/ApiOverview.tsx @@ -51,11 +51,13 @@ const GatewayListItem: React.FC = ({ gateway, api }) => { const isDeployed = gateway.isDeployed === true; const vhost = gateway.vhost || ""; - + // Construct URLs - const httpUrl = vhost && api ? `http://${vhost}:8080${api.context}/${api.version}` : null; - const httpsUrl = vhost && api ? `https://${vhost}:5443${api.context}/${api.version}` : null; - + const httpUrl = + vhost && api ? `http://${vhost}:8080${api.context}/${api.version}` : null; + const httpsUrl = + vhost && api ? `https://${vhost}:5443${api.context}/${api.version}` : null; + // Get upstream URL (first default backend endpoint) const upstreamUrl = api?.backendServices?.find((s) => s.isDefault)?.endpoints?.[0]?.url ?? @@ -63,10 +65,13 @@ const GatewayListItem: React.FC = ({ gateway, api }) => { ""; const handleCopyUrl = (url: string) => { - navigator.clipboard?.writeText(url).then(() => { - setCopiedUrl(url); - setTimeout(() => setCopiedUrl(null), 2000); - }).catch(() => {}); + navigator.clipboard + ?.writeText(url) + .then(() => { + setCopiedUrl(url); + setTimeout(() => setCopiedUrl(null), 2000); + }) + .catch(() => {}); }; return ( @@ -87,7 +92,10 @@ const GatewayListItem: React.FC = ({ gateway, api }) => { gap: 2, cursor: "pointer", "&:hover": { - bgcolor: (t) => t.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)", + bgcolor: (t) => + t.palette.mode === "dark" + ? "rgba(255,255,255,0.03)" + : "rgba(0,0,0,0.02)", }, }} onClick={() => setExpanded(!expanded)} @@ -100,12 +108,22 @@ const GatewayListItem: React.FC = ({ gateway, api }) => { variant={isDeployed ? "filled" : "outlined"} sx={{ minWidth: 85 }} /> - - - + + + Gateway: - + {gateway.name} @@ -141,7 +159,9 @@ const GatewayListItem: React.FC = ({ gateway, api }) => { {httpsUrl} - + { @@ -169,12 +189,30 @@ const GatewayListItem: React.FC = ({ gateway, api }) => { - t.palette.mode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.01)" }}> + + t.palette.mode === "dark" + ? "rgba(255,255,255,0.02)" + : "rgba(0,0,0,0.01)", + }} + > {/* Main URLs header + HTTP/HTTPS rows (prefix label + url + copy) */} {(httpUrl || httpsUrl) && ( - + Main URLs @@ -186,12 +224,22 @@ const GatewayListItem: React.FC = ({ gateway, api }) => { alignItems: "center", gap: 1, p: 1.25, - bgcolor: (t) => (t.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "#F9FAFB"), + bgcolor: (t) => + t.palette.mode === "dark" + ? "rgba(255,255,255,0.03)" + : "#F9FAFB", borderRadius: 1, border: (t) => `1px solid ${t.palette.divider}`, }} > - + HTTP URL @@ -211,8 +259,15 @@ const GatewayListItem: React.FC = ({ gateway, api }) => { - - handleCopyUrl(httpUrl)}> + + handleCopyUrl(httpUrl)} + > @@ -228,12 +283,22 @@ const GatewayListItem: React.FC = ({ gateway, api }) => { alignItems: "center", gap: 1, p: 1.25, - bgcolor: (t) => (t.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "#F9FAFB"), + bgcolor: (t) => + t.palette.mode === "dark" + ? "rgba(255,255,255,0.03)" + : "#F9FAFB", borderRadius: 1, border: (t) => `1px solid ${t.palette.divider}`, }} > - + HTTPS URL @@ -253,8 +318,15 @@ const GatewayListItem: React.FC = ({ gateway, api }) => { - - handleCopyUrl(httpsUrl)}> + + handleCopyUrl(httpsUrl)} + > @@ -267,10 +339,20 @@ const GatewayListItem: React.FC = ({ gateway, api }) => { {upstreamUrl && ( <> - t.palette.divider }} /> + t.palette.divider }} + /> - + Upstream URL = ({ gateway, api }) => { alignItems: "center", gap: 1, p: 1.5, - bgcolor: (t) => t.palette.mode === "dark" ? "rgba(255,255,255,0.05)" : "#F9FAFB", + bgcolor: (t) => + t.palette.mode === "dark" + ? "rgba(255,255,255,0.05)" + : "#F9FAFB", borderRadius: 1, border: (t) => `1px solid ${t.palette.divider}`, }} @@ -299,8 +384,17 @@ const GatewayListItem: React.FC = ({ gateway, api }) => { {upstreamUrl} - - handleCopyUrl(upstreamUrl)}> + + handleCopyUrl(upstreamUrl)} + > @@ -329,20 +423,23 @@ const ApiOverviewContent: React.FC = () => { }>(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); - const { apis, fetchApiById, fetchGatewaysForApi, loading, selectApi } = useApisContext(); + const { apis, fetchApiById, fetchGatewaysForApi, loading, selectApi } = + useApisContext(); const [apiId, setApiId] = React.useState( searchParams.get("apiId") ?? legacyApiId ?? null ); const [api, setApi] = React.useState(null); - const [associatedGateways, setAssociatedGateways] = React.useState([]); + const [associatedGateways, setAssociatedGateways] = React.useState< + ApiGatewaySummary[] + >([]); const [gatewaysLoading, setGatewaysLoading] = React.useState(false); const [detailsLoading, setDetailsLoading] = React.useState(false); const [error, setError] = React.useState(null); const sortedGateways = React.useMemo(() => { - return [...associatedGateways].sort((a, b) => - +(b.isDeployed === true) - +(a.isDeployed === true) + return [...associatedGateways].sort( + (a, b) => +(b.isDeployed === true) - +(a.isDeployed === true) ); }, [associatedGateways]); @@ -470,6 +567,12 @@ const ApiOverviewContent: React.FC = () => { return `${day} day${day > 1 ? "s" : ""} ago`; }; + const initials = (name: string) => { + const letters = name.replace(/[^A-Za-z]/g, ""); + if (!letters) return "API"; + return (letters[0] + (letters[1] || "")).toUpperCase(); + }; + const ProtocolBadge = ({ label }: { label: string }) => ( { } }, [navigate, orgHandle, projectHandle]); - if (loading || detailsLoading) { return ( @@ -572,7 +674,7 @@ const ApiOverviewContent: React.FC = () => { lineHeight: 1, }} > - {api.displayName || api.name} + {initials(api.name)} @@ -708,7 +810,11 @@ const ApiOverviewContent: React.FC = () => { p: 2.25, }} > - + { {gatewaysLoading ? ( - + ) : associatedGateways.length === 0 ? ( @@ -892,8 +1003,6 @@ const ApiOverviewContent: React.FC = () => { ); }; -const ApiOverview: React.FC = () => ( - -); +const ApiOverview: React.FC = () => ; export default ApiOverview; diff --git a/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/GithubCreationFlow.tsx b/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/GithubCreationFlow.tsx index 224baba07..b9b95f062 100644 --- a/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/GithubCreationFlow.tsx +++ b/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/GithubCreationFlow.tsx @@ -19,10 +19,11 @@ import Refresh from "../../../../components/src/Icons/generated/Refresh"; import { IconButton } from "../../../../components/src/components/IconButton"; import Edit from "../../../../components/src/Icons/generated/Edit"; import CreationMetaData from "../CreationMetaData"; -import { isValidMajorMinorVersion, formatVersionToMajorMinor } from "../../../../helpers/openApiHelpers"; +import { + isValidMajorMinorVersion, + formatVersionToMajorMinor, +} from "../../../../helpers/openApiHelpers"; import type { ApiSummary } from "../../../../hooks/apis"; - -// Contexts import { useGithubAPICreationContext } from "../../../../context/GithubAPICreationContext"; import { useCreateComponentBuildpackContext } from "../../../../context/CreateComponentBuildpackContext"; import { useGithubProjectValidationContext } from "../../../../context/validationContext"; @@ -31,18 +32,16 @@ import { ApiOperationsList } from "../../../../components/src/components/Common/ import { useGithubAPICreation } from "../../../../hooks/GithubAPICreation"; import { useNotifications } from "../../../../context/NotificationContext"; -/* ---------- Types ---------- */ type Props = { open: boolean; onClose: () => void; - selectedProjectId?: string; // must be provided to enable Create + selectedProjectId?: string; refreshApis: (projectId?: string) => Promise; }; type BranchOption = { label: string; value: string }; type Step = "form" | "details"; -/* ---------- Utils ---------- */ const isLikelyGithubRepo = (url: string) => /^https:\/\/github\.com\/[^\/\s]+\/[^\/\s#]+$/i.test(url.trim()); @@ -78,11 +77,8 @@ const GithubCreationFlow: React.FC = ({ reset: resetValidation, } = useGithubProjectValidationContext(); - // 👇 We will read meta (name, context, version, target, description) for the POST const { contractMeta, setContractMeta } = useCreateComponentBuildpackContext(); - - // 👇 POST /api/v1/import/api-project const { importApiProject } = useGithubAPICreation(); const { showNotification } = useNotifications(); @@ -92,6 +88,7 @@ const GithubCreationFlow: React.FC = ({ const [dirError, setDirError] = React.useState(null); const [isDirValid, setIsDirValid] = React.useState(false); + const [metaHasErrors, setMetaHasErrors] = React.useState(false); const [validatedOps, setValidatedOps] = React.useState< Array<{ @@ -136,7 +133,6 @@ const GithubCreationFlow: React.FC = ({ [selectedBranch] ); - // Debounced fetch-branches on repoUrl change React.useEffect(() => { if (!repoUrl || !isLikelyGithubRepo(repoUrl)) return; const t = setTimeout(() => { @@ -146,7 +142,6 @@ const GithubCreationFlow: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [repoUrl]); - // Auto-select default branch (or first) once branches are available React.useEffect(() => { if (!branches.length || selectedBranch) return; const def = @@ -155,7 +150,6 @@ const GithubCreationFlow: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [branches, selectedBranch]); - // Fetch content when branch changes and clear selections React.useEffect(() => { if (!selectedBranch) return; setApiDir("/"); @@ -202,7 +196,6 @@ const GithubCreationFlow: React.FC = ({ setSelectedBranch(opt ? opt.value : null); }; - // ----- Local directory validation for config.yaml presence ----- const normalizePath = (p: string) => p.replace(/^\/+/, "").replace(/\/+$/, ""); @@ -249,7 +242,9 @@ const GithubCreationFlow: React.FC = ({ } } setIsDirValid(found); - setDirError(found ? null : 'Selected directory must contain a "config.yaml".'); + setDirError( + found ? null : 'Selected directory must contain a "config.yaml".' + ); return; } @@ -264,14 +259,8 @@ const GithubCreationFlow: React.FC = ({ setDirError(ok ? null : 'Selected directory must contain a "config.yaml".'); }, [apiDir, content, findNodeByPath, nodeHasConfigYaml]); - // ----- Validate on Next, prefill meta, move to details ----- const onNext = async () => { - if ( - !repoUrl.trim() || - !selectedBranch || - !apiDir || - !isDirValid - ) { + if (!repoUrl.trim() || !selectedBranch || !apiDir || !isDirValid) { return; } @@ -293,7 +282,9 @@ const GithubCreationFlow: React.FC = ({ ...prev, name: api.name || prev?.name || "", context: api.context || prev?.context || "", - version: formatVersionToMajorMinor(api.version ?? prev?.version ?? "1.0.0"), + version: formatVersionToMajorMinor( + api.version ?? prev?.version ?? "1.0.0" + ), description: api.description || prev?.description || "", target: target || prev?.target || "", })); @@ -309,11 +300,8 @@ const GithubCreationFlow: React.FC = ({ } }; - // ----- Create: POST /api/v1/import/api-project ----- const onCreate = async () => { setCreateError(null); - - // Guard required fields const name = (contractMeta?.name || "").trim(); const context = (contractMeta?.context || "").trim(); const version = (contractMeta?.version || "").trim(); @@ -338,16 +326,14 @@ const GithubCreationFlow: React.FC = ({ setCreateError("Project is required (missing projectId)."); return; } - - // build payload const payload = { repoUrl: repoUrl.trim(), provider: "github" as const, branch: selectedBranch, - path: apiDir === "/" ? "/" : normalizePath(apiDir), // e.g., "/" or "apis/test-api" + path: apiDir === "/" ? "/" : normalizePath(apiDir), api: { name, - displayName: name, // or customize if you prefer a separate display name + displayName: name, description, context: context.startsWith("/") ? context : `/${context}`, version, @@ -399,7 +385,6 @@ const GithubCreationFlow: React.FC = ({ return ( - {/* ------------ Initial card ------------ */} {showInitial && step === "form" && ( @@ -434,10 +419,8 @@ const GithubCreationFlow: React.FC = ({ )} - {/* ------------ Form (URL/Branch/Dir) ------------ */} {!showInitial && step === "form" && ( - {/* Row 1: URL | Branch */} = ({ } /> - - {/* Row 2: API directory | Edit */} @@ -553,7 +534,6 @@ const GithubCreationFlow: React.FC = ({ )} - {/* Actions row */} - - + {/* */} + {/* */} @@ -665,8 +648,6 @@ const GithubCreationFlow: React.FC = ({ )} - - {/* Directory picker modal */} = ({ open, selectedProjectId, @@ -54,6 +51,7 @@ const URLCreationFlow: React.FC = ({ useCreateComponentBuildpackContext(); const { validateOpenApiUrl } = useOpenApiValidation(); const abortControllerRef = React.useRef(null); + const [metaHasErrors, setMetaHasErrors] = React.useState(false); React.useEffect(() => { return () => { @@ -299,7 +297,11 @@ const URLCreationFlow: React.FC = ({ {/* */} - + setMetaHasErrors(hasError)} + /> = ({ variant="contained" disabled={ creating || + metaHasErrors || !(contractMeta?.name || "").trim() || !(contractMeta?.context || "").trim() || !isValidMajorMinorVersion( diff --git a/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/UploadCreationFlow.tsx b/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/UploadCreationFlow.tsx index 86c39d057..40068ba40 100644 --- a/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/UploadCreationFlow.tsx +++ b/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/UploadCreationFlow.tsx @@ -5,19 +5,30 @@ import { Button } from "../../../../components/src/components/Button"; import { IconButton } from "../../../../components/src/components/IconButton"; import Delete from "../../../../components/src/Icons/generated/Delete"; import CreationMetaData from "../CreationMetaData"; +import { useCreateComponentBuildpackContext } from "../../../../context/CreateComponentBuildpackContext"; import { - useCreateComponentBuildpackContext, -} from "../../../../context/CreateComponentBuildpackContext"; -import { useOpenApiValidation, type OpenApiValidationResponse } from "../../../../hooks/validation"; + useOpenApiValidation, + type OpenApiValidationResponse, +} from "../../../../hooks/validation"; import { ApiOperationsList } from "../../../../components/src/components/Common/ApiOperationsList"; import type { ImportOpenApiRequest, ApiSummary } from "../../../../hooks/apis"; -import { defaultServiceName, firstServerUrl, deriveContext, mapOperations, formatVersionToMajorMinor, isValidMajorMinorVersion } from "../../../../helpers/openApiHelpers"; +import { + defaultServiceName, + firstServerUrl, + deriveContext, + mapOperations, + formatVersionToMajorMinor, + isValidMajorMinorVersion, +} from "../../../../helpers/openApiHelpers"; /* ---------- Types ---------- */ type Props = { open: boolean; selectedProjectId: string; - importOpenApi: (payload: ImportOpenApiRequest, opts?: { signal?: AbortSignal }) => Promise; + importOpenApi: ( + payload: ImportOpenApiRequest, + opts?: { signal?: AbortSignal } + ) => Promise; refreshApis: (projectId?: string) => Promise; onClose: () => void; }; @@ -25,17 +36,26 @@ type Props = { type Step = "upload" | "details"; /* ---------- component ---------- */ -const UploadCreationFlow: React.FC = ({ open, selectedProjectId, importOpenApi, refreshApis, onClose }) => { +const UploadCreationFlow: React.FC = ({ + open, + selectedProjectId, + importOpenApi, + refreshApis, + onClose, +}) => { const [step, setStep] = React.useState("upload"); const [uploadedFile, setUploadedFile] = React.useState(null); - const [validationResult, setValidationResult] = React.useState(null); + const [validationResult, setValidationResult] = + React.useState(null); const [fileName, setFileName] = React.useState(""); const [error, setError] = React.useState(null); const [validating, setValidating] = React.useState(false); const [creating, setCreating] = React.useState(false); - const { contractMeta, setContractMeta, resetContractMeta } = useCreateComponentBuildpackContext(); + const { contractMeta, setContractMeta, resetContractMeta } = + useCreateComponentBuildpackContext(); const { validateOpenApiFile } = useOpenApiValidation(); + const [metaHasErrors, setMetaHasErrors] = React.useState(false); // Always-mounted input + stable id/label wiring const fileInputRef = React.useRef(null); @@ -63,26 +83,29 @@ const UploadCreationFlow: React.FC = ({ open, selectedProjectId, importOp } }, [open, resetContractMeta]); - const autoFill = React.useCallback((api: any) => { - const title = api?.name?.trim() || api?.displayName?.trim() || ""; - const version = formatVersionToMajorMinor(api?.version); - const description = api?.description || ""; - const targetUrl = firstServerUrl(api); - - setContractMeta((prev: any) => ({ - ...prev, - name: title || prev?.name || "Sample API", - version, - description, - context: deriveContext(api), - target: prev?.target || targetUrl || "", - })); - }, [setContractMeta]); + const autoFill = React.useCallback( + (api: any) => { + const title = api?.name?.trim() || api?.displayName?.trim() || ""; + const version = formatVersionToMajorMinor(api?.version); + const description = api?.description || ""; + const targetUrl = firstServerUrl(api); + + setContractMeta((prev: any) => ({ + ...prev, + name: title || prev?.name || "Sample API", + version, + description, + context: deriveContext(api), + target: prev?.target || targetUrl || "", + })); + }, + [setContractMeta] + ); const handleFiles = React.useCallback( - async (files: FileList | null) => { + async (files: FileList | null) => { if (!files || !files[0]) return; - if (validating) return; + if (validating) return; const file = files[0]; abortControllerRef.current?.abort(); @@ -97,17 +120,20 @@ const UploadCreationFlow: React.FC = ({ open, selectedProjectId, importOp setUploadedFile(file); setFileName(file.name); - const result = await validateOpenApiFile(file, { signal: abortController.signal }); + const result = await validateOpenApiFile(file, { + signal: abortController.signal, + }); setValidationResult(result); if (result.isAPIDefinitionValid) { autoFill(result.api); } else { - const errorMsg = result.errors?.join(", ") || "Invalid OpenAPI definition"; + const errorMsg = + result.errors?.join(", ") || "Invalid OpenAPI definition"; setError(errorMsg); } } catch (e: any) { - if (e.name === 'AbortError') return; + if (e.name === "AbortError") return; setError(e?.message || "Failed to validate OpenAPI definition"); setValidationResult(null); } finally { @@ -171,17 +197,16 @@ const UploadCreationFlow: React.FC = ({ open, selectedProjectId, importOp setError(null); const serviceName = defaultServiceName(name); - const backendServices = - target - ? [ - { - name: serviceName, - isDefault: true, - retries: 2, - endpoints: [{ url: target, description: "Primary backend" }], - }, - ] - : []; + const backendServices = target + ? [ + { + name: serviceName, + isDefault: true, + retries: 2, + endpoints: [{ url: target, description: "Primary backend" }], + }, + ] + : []; try { await importOpenApi({ @@ -238,7 +263,11 @@ const UploadCreationFlow: React.FC = ({ open, selectedProjectId, importOp htmlFor={inputId} onDragOver={(e) => e.preventDefault()} onDrop={onDrop} - sx={{ display: "block", cursor: validating ? "not-allowed" : "pointer", opacity: validating ? 0.6 : 1 }} + sx={{ + display: "block", + cursor: validating ? "not-allowed" : "pointer", + opacity: validating ? 0.6 : 1, + }} > = ({ open, selectedProjectId, importOp Drag & drop your file or click to upload - @@ -290,7 +323,8 @@ const UploadCreationFlow: React.FC = ({ open, selectedProjectId, importOp setValidationResult(null); setFileName(""); setError(null); - if (fileInputRef.current) fileInputRef.current.value = ""; + if (fileInputRef.current) + fileInputRef.current.value = ""; setFileKey((k) => k + 1); }} > @@ -308,7 +342,11 @@ const UploadCreationFlow: React.FC = ({ open, selectedProjectId, importOp - + {!!dirError && ( {dirError} @@ -564,6 +602,7 @@ const GithubCreationFlow: React.FC = ({ {validating ? "Validating…" : "Next"} + {!!validateError && ( {validateError} @@ -597,45 +636,39 @@ const GithubCreationFlow: React.FC = ({ )} - {/* */} - {/* */} - - setMetaHasErrors(hasError) - } - /> + setMetaHasErrors(hasError)} + /> - {!!createError && ( - - {createError} - - )} + {!!createError && ( + + {createError} + + )} - - - - - {/* */} - {/* */} + + + + @@ -646,6 +679,7 @@ const GithubCreationFlow: React.FC = ({ )} + + (val || "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .trim(); + +const majorFromVersion = (v: string) => { + const m = (v || "").trim().match(/\d+/); + return m?.[0] ?? ""; +}; + +const buildIdentifierFromNameAndVersion = (name: string, version: string) => { + const base = slugify(name); + const major = majorFromVersion(version); + return major ? `${base}-v${major}` : base; +}; + type Props = { open: boolean; selectedProjectId: string; @@ -78,13 +97,23 @@ const URLCreationFlow: React.FC = ({ const description = api?.description || ""; const targetUrl = firstServerUrl(api); - setContractMeta((prev: any) => ({ - ...prev, - name: title || prev?.name || "Sample API", + const identifier = buildIdentifierFromNameAndVersion(title, version); + + const nextMeta = { + name: title || "Sample API", + displayName: title || "Sample API", version, description, context: deriveContext(api), - target: prev?.target || targetUrl || "", + target: targetUrl || "", + identifier, + identifierEdited: false, + }; + + setContractMeta((prev: any) => ({ + ...prev, + ...nextMeta, + target: prev?.target || nextMeta.target || "", })); }, [setContractMeta] @@ -105,6 +134,7 @@ const URLCreationFlow: React.FC = ({ const result = await validateOpenApiUrl(specUrl.trim(), { signal: abortController.signal, }); + setValidationResult(result); if (result.isAPIDefinitionValid) { @@ -142,16 +172,28 @@ const URLCreationFlow: React.FC = ({ }, [validationResult]); const onCreate = async () => { - const name = (contractMeta?.name || "").trim(); + const displayName = ( + contractMeta?.displayName || + contractMeta?.name || + "" + ).trim(); const context = (contractMeta?.context || "").trim(); const version = (contractMeta?.version || "").trim(); const description = (contractMeta?.description || "").trim() || undefined; const target = (contractMeta?.target || "").trim(); - if (!name || !context || !version) { + const identifier = + (contractMeta as any)?.identifier?.trim() || + buildIdentifierFromNameAndVersion(displayName, version); + + if (!displayName || !context || !version) { setError("Please complete all required fields."); return; } + if (!identifier) { + setError("Identifier is required."); + return; + } if (target) { try { if (/^https?:\/\//i.test(target)) new URL(target); @@ -160,7 +202,6 @@ const URLCreationFlow: React.FC = ({ return; } } - if (!validationResult?.isAPIDefinitionValid) { setError("Please fetch and validate the OpenAPI definition first."); return; @@ -169,7 +210,7 @@ const URLCreationFlow: React.FC = ({ setCreating(true); setError(null); - const serviceName = defaultServiceName(name); + const serviceName = defaultServiceName(displayName); const backendServices = target ? [ { @@ -181,19 +222,22 @@ const URLCreationFlow: React.FC = ({ ] : []; + const payload: ImportOpenApiRequest = { + api: { + name: identifier, + displayName, + context, + version, + projectId: selectedProjectId, + target, + description, + backendServices, + } as any, + url: specUrl.trim(), + }; + try { - await importOpenApi({ - api: { - name, - context, - version, - projectId: selectedProjectId, - target, - description, - backendServices, - }, - url: specUrl.trim(), - }); + await importOpenApi(payload); } catch (e: any) { setError(e?.message || "Failed to create API"); setCreating(false); @@ -297,12 +341,12 @@ const URLCreationFlow: React.FC = ({ {step === "details" && ( - {/* */} setMetaHasErrors(hasError)} /> + = ({ > Back + + - - - - {error && ( - - {error} - - )} - {/* */} + {creating ? "Creating..." : "Create"} + + + + {error && ( + + {error} + + )} diff --git a/portals/management-portal/src/pages/apis/CreationFlows/CreationMetaData.tsx b/portals/management-portal/src/pages/apis/CreationFlows/CreationMetaData.tsx index ec2a07f3e..80f4527bd 100644 --- a/portals/management-portal/src/pages/apis/CreationFlows/CreationMetaData.tsx +++ b/portals/management-portal/src/pages/apis/CreationFlows/CreationMetaData.tsx @@ -13,14 +13,23 @@ import Edit from "../../../components/src/Icons/generated/Edit"; import { useGithubProjectValidationContext } from "../../../context/validationContext"; const slugify = (val: string) => - val + (val || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .trim(); -const buildIdentifierFromName = (name: string) => slugify(name); +const majorFromVersion = (v: string) => { + const m = (v || "").trim().match(/\d+/); + return m?.[0] ?? ""; +}; + +const buildIdentifierFromNameAndVersion = (name: string, version: string) => { + const base = slugify(name); + const major = majorFromVersion(version); + return major ? `${base}-v${major}` : base; +}; type Scope = "contract" | "endpoint"; @@ -95,7 +104,6 @@ const CreationMetaData: React.FC = ({ } | null>(null); const lastCheckedIdentifierRef = React.useRef(null); - const nameVersionTimerRef = React.useRef(null); const identifierTimerRef = React.useRef(null); @@ -108,20 +116,6 @@ const CreationMetaData: React.FC = ({ onValidationChangeRef.current = onValidationChange; }, [onValidationChange]); - React.useEffect(() => { - if ( - meta.name && - !meta.identifier && - !meta.identifierEdited && - !isIdentifierEditing - ) { - change({ - identifier: buildIdentifierFromName(meta.name), - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const handleContextChange = (v: string) => { change({ context: v, contextEdited: true }); }; @@ -138,20 +132,9 @@ const CreationMetaData: React.FC = ({ change({ identifierEdited: true }); }; - const versionMajor = React.useMemo(() => { - const v = (meta.version || "").trim(); - const m = v.match(/\d+/); - return m?.[0] ?? ""; - }, [meta.version]); - const identifierDisplayValue = React.useMemo(() => { - const base = (meta.identifier ?? "").trim(); - if (!base) return ""; - if (meta.identifierEdited) return base; - if (!versionMajor) return base; - if (/-v\d+$/i.test(base)) return base; - return `${base}-v${versionMajor}`; - }, [meta.identifier, meta.identifierEdited, versionMajor]); + return (meta.identifier ?? "").trim(); + }, [meta.identifier]); const identifierToValidate = React.useMemo(() => { return identifierDisplayValue.trim(); @@ -205,7 +188,6 @@ const CreationMetaData: React.FC = ({ } } catch (e) { lastCheckedNameVersionRef.current = null; - const msg = e instanceof Error ? e.message @@ -330,8 +312,8 @@ const CreationMetaData: React.FC = ({ }, []); const handleNameChange = (v: string) => { - const slug = slugify(v); const trimmed = v.trim(); + const slug = slugify(v); const nextPatch: Partial & { identifier?: string; @@ -344,8 +326,12 @@ const CreationMetaData: React.FC = ({ if (!meta.contextEdited) { nextPatch.context = slug ? `/${slug}` : ""; } + if (!meta.identifierEdited && !isIdentifierEditing) { - nextPatch.identifier = buildIdentifierFromName(v); + nextPatch.identifier = buildIdentifierFromNameAndVersion( + v, + meta.version || "" + ); } setNameVersionError(null); @@ -372,6 +358,28 @@ const CreationMetaData: React.FC = ({ identifierValidating, ]); + React.useEffect(() => { + if (meta.identifierEdited || isIdentifierEditing) return; + + const nameForId = (meta.displayName || meta.name || "").trim(); + const verForId = (meta.version || "").trim(); + + if (!nameForId || !verForId) return; + + const expected = buildIdentifierFromNameAndVersion(nameForId, verForId); + + if ((meta.identifier || "").trim() !== expected) { + change({ identifier: expected }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + meta.displayName, + meta.name, + meta.version, + meta.identifierEdited, + isIdentifierEditing, + ]); + return ( {title ? {title} : null} @@ -414,7 +422,6 @@ const CreationMetaData: React.FC = ({ variant="link" onClick={handleIdentifierEditClick} disabled={!!readOnlyFields?.["identifier"]} - style={{ marginBottom: 4 }} aria-label="Edit identifier" /> @@ -425,7 +432,16 @@ const CreationMetaData: React.FC = ({ value={meta.version} onChange={(v: string) => { setNameVersionError(null); - change({ version: v }); + + const shouldAuto = !meta.identifierEdited && !isIdentifierEditing; + const nextIdentifier = shouldAuto + ? buildIdentifierFromNameAndVersion( + meta.displayName || meta.name || "", + v + ) + : meta.identifier; + + change({ version: v, identifier: nextIdentifier }); scheduleNameVersionValidation({ version: v }); setIdentifierError(null); diff --git a/portals/management-portal/src/pages/apis/CreationFlows/EndPointCreationFlow.tsx b/portals/management-portal/src/pages/apis/CreationFlows/EndPointCreationFlow.tsx index 85494dad3..497326975 100644 --- a/portals/management-portal/src/pages/apis/CreationFlows/EndPointCreationFlow.tsx +++ b/portals/management-portal/src/pages/apis/CreationFlows/EndPointCreationFlow.tsx @@ -16,6 +16,25 @@ import { useCreateComponentBuildpackContext } from "../../../context/CreateCompo import { type CreateApiPayload } from "../../../hooks/apis"; import CreationMetaData from "./CreationMetaData"; +const slugify = (val: string) => + (val || "") + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .trim(); + +const majorFromVersion = (v: string) => { + const m = (v || "").trim().match(/\d+/); + return m?.[0] ?? ""; +}; + +const buildIdentifierFromNameAndVersion = (name: string, version: string) => { + const base = slugify(name); + const major = majorFromVersion(version); + return major ? `${base}-v${major}` : base; +}; + type EndpointWizardStep = "endpoint" | "details"; type EndpointCreationState = { @@ -51,13 +70,13 @@ const EndPointCreationFlow: React.FC = ({ const [creating, setCreating] = React.useState(false); const [metaHasErrors, setMetaHasErrors] = React.useState(false); - // Reset this flow's slice when opened React.useEffect(() => { if (open) { resetEndpointMeta(); setWizardStep("endpoint"); setWizardState({ endpointUrl: "" }); setWizardError(null); + setMetaHasErrors(false); } }, [open, resetEndpointMeta]); @@ -87,31 +106,46 @@ const EndPointCreationFlow: React.FC = ({ const handleStepChange = React.useCallback( (next: EndpointWizardStep) => { if (next === "details") { - const inferred = inferNameFromEndpoint(wizardState.endpointUrl); - const needsName = !(endpointMeta?.name || "").trim(); - const needsContext = !(endpointMeta?.context || "").trim(); + const inferredDisplayName = inferNameFromEndpoint( + wizardState.endpointUrl + ); + setEndpointMeta((prev: any) => { const base = prev || {}; const nextMeta = { ...base }; - if (needsName) nextMeta.name = inferred; - if (needsContext && !base?.contextEdited) { - const slug = inferred - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, ""); + + const hasName = !!(base?.name || "").trim(); + const hasDisplayName = !!(base?.displayName || "").trim(); + const hasContext = !!(base?.context || "").trim(); + const hasVersion = !!(base?.version || "").trim(); + + const version = hasVersion ? base.version : "1.0.0"; + + if (!hasDisplayName) nextMeta.displayName = inferredDisplayName; + if (!hasName) nextMeta.name = inferredDisplayName; + + if (!hasContext && !base?.contextEdited) { + const slug = slugify(inferredDisplayName); nextMeta.context = slug ? `/${slug}` : ""; } + + if (!hasVersion) nextMeta.version = version; + + if (!base?.identifierEdited) { + nextMeta.identifier = buildIdentifierFromNameAndVersion( + inferredDisplayName, + version + ); + nextMeta.identifierEdited = false; + } + return nextMeta; }); } + setWizardStep(next); }, - [ - inferNameFromEndpoint, - endpointMeta, - setEndpointMeta, - wizardState.endpointUrl, - ] + [inferNameFromEndpoint, setEndpointMeta, wizardState.endpointUrl] ); const handleCreate = React.useCallback(async () => { @@ -121,11 +155,8 @@ const EndPointCreationFlow: React.FC = ({ endpointMeta?.name || "" ).trim(); - const identifier = ( - endpointMeta?.identifier || - endpointMeta?.name || - "" - ).trim(); + + const identifier = (endpointMeta?.identifier || "").trim(); const context = (endpointMeta?.context || "").trim(); const version = (endpointMeta?.version || "").trim() || "1.0.0"; @@ -137,11 +168,12 @@ const EndPointCreationFlow: React.FC = ({ try { setWizardError(null); setCreating(true); + const uniqueBackendName = `default-backend-${Date.now().toString( 36 )}${Math.random().toString(36).slice(2, 8)}`; - await createApi({ + const payload: CreateApiPayload = { name: identifier, displayName, context: context.startsWith("/") ? context : `/${context}`, @@ -158,9 +190,10 @@ const EndPointCreationFlow: React.FC = ({ retries: 0, }, ], - }); + }; + + await createApi(payload); - // fresh state for the next time it's opened resetEndpointMeta(); setWizardState({ endpointUrl: "" }); onClose(); @@ -260,7 +293,8 @@ const EndPointCreationFlow: React.FC = ({ disabled={ creating || metaHasErrors || - !(endpointMeta?.name || "").trim() || + !(endpointMeta?.displayName || endpointMeta?.name || "").trim() || + !(endpointMeta?.identifier || "").trim() || !(endpointMeta?.context || "").trim() || !(endpointMeta?.version || "").trim() } From 6308636884cd970e61ed3cd320c4d35e4a3b1430 Mon Sep 17 00:00:00 2001 From: Dilan Induwara <153802063+Induwara04@users.noreply.github.com> Date: Sat, 13 Dec 2025 09:37:05 +0530 Subject: [PATCH 5/5] Resolve Comments --- portals/management-portal/src/hooks/apis.ts | 1 + .../ContractCreationFlows/URLCreationFlow.tsx | 14 ++-- .../UploadCreationFlow.tsx | 14 ++-- .../CreationFlows/EndPointCreationFlow.tsx | 65 ++++++++++++------- 4 files changed, 54 insertions(+), 40 deletions(-) diff --git a/portals/management-portal/src/hooks/apis.ts b/portals/management-portal/src/hooks/apis.ts index c60b76666..4937c170d 100644 --- a/portals/management-portal/src/hooks/apis.ts +++ b/portals/management-portal/src/hooks/apis.ts @@ -75,6 +75,7 @@ export type ImportOpenApiRequest = { context: string; version: string; projectId: string; + displayName?: string; target?: string; description?: string; backendServices?: ApiBackendService[]; diff --git a/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/URLCreationFlow.tsx b/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/URLCreationFlow.tsx index c12e4b399..fc0afdc51 100644 --- a/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/URLCreationFlow.tsx +++ b/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/URLCreationFlow.tsx @@ -177,6 +177,7 @@ const URLCreationFlow: React.FC = ({ contractMeta?.name || "" ).trim(); + const context = (contractMeta?.context || "").trim(); const version = (contractMeta?.version || "").trim(); const description = (contractMeta?.description || "").trim() || undefined; @@ -232,7 +233,7 @@ const URLCreationFlow: React.FC = ({ target, description, backendServices, - } as any, + }, url: specUrl.trim(), }; @@ -357,6 +358,7 @@ const URLCreationFlow: React.FC = ({ variant="outlined" onClick={() => setStep("url")} sx={{ textTransform: "none" }} + disabled={creating} > Back @@ -366,16 +368,10 @@ const URLCreationFlow: React.FC = ({ disabled={ creating || metaHasErrors || - !( - contractMeta?.displayName || - contractMeta?.name || - "" - ).trim() || + !(contractMeta?.displayName || contractMeta?.name || "").trim() || !(contractMeta as any)?.identifier?.trim() || !(contractMeta?.context || "").trim() || - !isValidMajorMinorVersion( - (contractMeta?.version || "").trim() - ) + !isValidMajorMinorVersion((contractMeta?.version || "").trim()) } onClick={onCreate} sx={{ textTransform: "none" }} diff --git a/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/UploadCreationFlow.tsx b/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/UploadCreationFlow.tsx index 86769d94f..fdc6a085c 100644 --- a/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/UploadCreationFlow.tsx +++ b/portals/management-portal/src/pages/apis/CreationFlows/ContractCreationFlows/UploadCreationFlow.tsx @@ -209,10 +209,8 @@ const UploadCreationFlow: React.FC = ({ const description = (contractMeta?.description || "").trim() || undefined; const target = (contractMeta?.target || "").trim(); - // ✅ this is the important fix: - // send name as identifier (with -v{major}), not the display name const identifier = - (contractMeta as any)?.identifier?.trim() || + ((contractMeta as any)?.identifier || "").trim() || buildIdentifierFromNameAndVersion(displayName, version); if (!displayName || !context || !version) { @@ -253,15 +251,15 @@ const UploadCreationFlow: React.FC = ({ const payload: ImportOpenApiRequest = { api: { - name: identifier, // ✅ now: dilan-api-v1 - displayName, // ✅ now: Dilan API + name: identifier, + displayName, context, version, projectId: selectedProjectId, target, description, backendServices, - } as any, + }, definition: uploadedFile, }; @@ -427,7 +425,9 @@ const UploadCreationFlow: React.FC = ({ setMetaHasErrors(hasError)} + onValidationChange={({ hasError }) => + setMetaHasErrors(hasError) + } /> void; }; +type EndpointMeta = ProxyMetadata & { + identifier?: string; + identifierEdited?: boolean; +}; + const EndPointCreationFlow: React.FC = ({ open, selectedProjectId, @@ -110,28 +118,29 @@ const EndPointCreationFlow: React.FC = ({ wizardState.endpointUrl ); - setEndpointMeta((prev: any) => { - const base = prev || {}; - const nextMeta = { ...base }; + setEndpointMeta((prev) => { + const base: EndpointMeta = + (prev as EndpointMeta) || ({} as EndpointMeta); + const nextMeta: EndpointMeta = { ...base }; - const hasName = !!(base?.name || "").trim(); - const hasDisplayName = !!(base?.displayName || "").trim(); - const hasContext = !!(base?.context || "").trim(); - const hasVersion = !!(base?.version || "").trim(); + const hasName = !!(base.name || "").trim(); + const hasDisplayName = !!(base.displayName || "").trim(); + const hasContext = !!(base.context || "").trim(); + const hasVersion = !!(base.version || "").trim(); const version = hasVersion ? base.version : "1.0.0"; if (!hasDisplayName) nextMeta.displayName = inferredDisplayName; if (!hasName) nextMeta.name = inferredDisplayName; - if (!hasContext && !base?.contextEdited) { + if (!hasContext && !base.contextEdited) { const slug = slugify(inferredDisplayName); nextMeta.context = slug ? `/${slug}` : ""; } if (!hasVersion) nextMeta.version = version; - if (!base?.identifierEdited) { + if (!base.identifierEdited) { nextMeta.identifier = buildIdentifierFromNameAndVersion( inferredDisplayName, version @@ -139,7 +148,7 @@ const EndPointCreationFlow: React.FC = ({ nextMeta.identifierEdited = false; } - return nextMeta; + return nextMeta as any; // <-- if your context setter expects ProxyMetadata only, see note below }); } @@ -150,15 +159,13 @@ const EndPointCreationFlow: React.FC = ({ const handleCreate = React.useCallback(async () => { const endpointUrl = wizardState.endpointUrl.trim(); - const displayName = ( - endpointMeta?.displayName || - endpointMeta?.name || - "" - ).trim(); - const identifier = (endpointMeta?.identifier || "").trim(); - const context = (endpointMeta?.context || "").trim(); - const version = (endpointMeta?.version || "").trim() || "1.0.0"; + const meta = endpointMeta as EndpointMeta | undefined; + + const displayName = (meta?.displayName || meta?.name || "").trim(); + const identifier = (meta?.identifier || "").trim(); + const context = (meta?.context || "").trim(); + const version = (meta?.version || "").trim() || "1.0.0"; if (!endpointUrl || !displayName || !identifier || !context) { setWizardError("Please complete all required fields."); @@ -178,7 +185,7 @@ const EndPointCreationFlow: React.FC = ({ displayName, context: context.startsWith("/") ? context : `/${context}`, version, - description: endpointMeta?.description?.trim() || undefined, + description: meta?.description?.trim() || undefined, projectId: selectedProjectId, backendServices: [ { @@ -293,10 +300,20 @@ const EndPointCreationFlow: React.FC = ({ disabled={ creating || metaHasErrors || - !(endpointMeta?.displayName || endpointMeta?.name || "").trim() || - !(endpointMeta?.identifier || "").trim() || - !(endpointMeta?.context || "").trim() || - !(endpointMeta?.version || "").trim() + !( + (endpointMeta as EndpointMeta | undefined)?.displayName || + (endpointMeta as EndpointMeta | undefined)?.name || + "" + ).trim() || + !( + (endpointMeta as EndpointMeta | undefined)?.identifier || "" + ).trim() || + !( + (endpointMeta as EndpointMeta | undefined)?.context || "" + ).trim() || + !( + (endpointMeta as EndpointMeta | undefined)?.version || "" + ).trim() } sx={{ textTransform: "none" }} >