Skip to content

Commit aa74bd6

Browse files
authored
Merge pull request #8 from API-200/better-api-import
Better api import
2 parents a50d0ce + d49607e commit aa74bd6

File tree

5 files changed

+338
-115
lines changed

5 files changed

+338
-115
lines changed

packages/dashboard/next.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import type { NextConfig } from "next";
33

44
const nextConfig: NextConfig = {
55
serverExternalPackages: ['cache-manager', '@keyv/redis', 'redis'],
6-
output: 'standalone'
6+
output: 'standalone',
7+
images: {
8+
domains: ['res.cloudinary.com'],
9+
},
710
};
811

912
export default withSentryConfig(withSentryConfig(nextConfig, {
Lines changed: 147 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
"use client"
22

3-
import {useRef, useState} from "react"
4-
import {Button} from "@/components/ui/button"
5-
import {Card, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
6-
import {AlertCircle, FileJson, FileSymlink} from "lucide-react"
7-
import {captureException} from "@sentry/nextjs"
8-
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert"
9-
import {ParsedSwaggerResult, parseSwagger} from "@/app/(layout)/services/import/parseSwagger";
10-
import {parsePostman} from "@/app/(layout)/services/import/parsePostman";
11-
3+
import type React from "react"
4+
5+
import { useRef, useState } from "react"
6+
import { Button } from "@/components/ui/button"
7+
import { Card, CardDescription, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
8+
import { AlertCircle, FileJson, FileSymlink, Search } from "lucide-react"
9+
import { captureException } from "@sentry/nextjs"
10+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
11+
import { type ParsedSwaggerResult, parseSwagger } from "@/app/(layout)/services/import/parseSwagger"
12+
import { parsePostman } from "@/app/(layout)/services/import/parsePostman"
13+
import { Input } from "@/components/ui/input"
14+
import Image from "next/image"
15+
import { apiServices } from "@/utils/data/apiServices"
1216

1317
interface ImportOptionsProps {
14-
onImportComplete: (data: ParsedSwaggerResult) => void;
18+
onImportComplete: (data: ParsedSwaggerResult) => void
1519
}
1620

17-
export default function ImportOptions({onImportComplete}: ImportOptionsProps) {
21+
export default function ImportOptions({ onImportComplete }: ImportOptionsProps) {
1822
const [importMethod, setImportMethod] = useState<"postman" | "url" | "openapi" | null>(null)
1923
const [isLoading, setIsLoading] = useState(false)
2024
const [error, setError] = useState<string | null>(null)
25+
const [searchQuery, setSearchQuery] = useState("")
2126

2227
const openApiFileRef = useRef<HTMLInputElement>(null)
2328
const postmanFileRef = useRef<HTMLInputElement>(null)
@@ -38,129 +43,187 @@ export default function ImportOptions({onImportComplete}: ImportOptionsProps) {
3843
}
3944
}
4045

41-
42-
const processImport = async (file: File | null, method: string | null) => {
46+
const processImport = async (file: File | null, method: string | null, fileContent?: string) => {
4347
setIsLoading(true)
4448
setError(null)
4549

4650
try {
4751
if (method === "openapi" && file) {
48-
const fileText = await file.text();
49-
50-
const parsedResult = parseSwagger(fileText);
51-
console.log('parsedResult')
52+
const fileText = fileContent || await file.text()
53+
const parsedResult = parseSwagger(fileText)
54+
console.log("parsedResult")
5255
console.log(parsedResult)
5356
return parsedResult
54-
} else if (method === "postman" && file) {
55-
56-
const fileText = await file.text();
57-
58-
const parsedResult = parsePostman(fileText);
59-
57+
} else if (method === "postman" && (file || fileContent)) {
58+
const fileText = fileContent || await file!.text()
59+
const parsedResult = parsePostman(fileText)
6060
return parsedResult
61-
6261
} else if (method === "url") {
63-
62+
// URL import logic
6463
} else {
65-
throw new Error("Invalid import method or missing file");
64+
throw new Error("Invalid import method or missing file")
6665
}
6766
} catch (e) {
68-
captureException(e);
69-
const errorMessage = e instanceof Error ? e.message : "Unknown error occurred during import";
70-
setError(errorMessage);
71-
throw e;
67+
captureException(e)
68+
const errorMessage = e instanceof Error ? e.message : "Unknown error occurred during import"
69+
setError(errorMessage)
70+
throw e
7271
} finally {
73-
setIsLoading(false);
72+
setIsLoading(false)
7473
}
7574
}
7675

77-
const handleImport = async (file: File | null, method: string | null) => {
76+
const handleImport = async (file: File | null, method: string | null, fileContent?: string) => {
7877
try {
79-
const data = await processImport(file, method)
78+
const data = await processImport(file, method, fileContent)
8079
onImportComplete(data!)
8180
} catch (error) {
8281
console.error("Error processing import:", error)
8382
// Error is already set in processImport
8483
}
8584
}
8685

86+
const handleApiServiceSelect = async (service: typeof apiServices[0]) => {
87+
setIsLoading(true)
88+
setError(null)
89+
90+
try {
91+
// Fetch the Postman collection from the configUrl
92+
const response = await fetch(service.configUrl)
93+
94+
if (!response.ok) {
95+
throw new Error(`Failed to fetch API service configuration: ${response.statusText}`)
96+
}
97+
98+
const collectionData = await response.text()
99+
100+
// Process the fetched Postman collection
101+
await handleImport(null, "postman", collectionData)
102+
103+
} catch (error) {
104+
const errorMessage = error instanceof Error ? error.message : "Failed to import API service"
105+
setError(errorMessage)
106+
console.error("Error importing API service:", error)
107+
} finally {
108+
setIsLoading(false)
109+
}
110+
}
111+
112+
const filteredApiServices = apiServices.filter((service) =>
113+
service.name.toLowerCase().includes(searchQuery.toLowerCase()),
114+
)
87115

88116
return (
89117
<div>
90-
<h1 className="text-3xl font-bold mb-8 tracking-tight">Import endpoints</h1>
91118

92-
<div className="grid gap-6">
93-
{/* OpenAPI/Swagger Import Option */}
94-
<Card>
95-
<div className="flex flex-col md:flex-row">
96-
<div className="flex-grow">
119+
<div className="mb-12">
120+
<h2 className="text-2xl font-semibold">Import Your Endpoints</h2>
121+
<p className="text-muted-foreground mb-4">
122+
Upload your API specification files to import endpoints directly into your project.
123+
</p>
124+
125+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
126+
<Card>
127+
<div className="flex flex-col h-full">
97128
<CardHeader>
98129
<div className="flex items-center gap-4">
99-
<FileJson className="h-10 w-10 text-primary"/>
130+
<FileJson className="h-10 w-10 text-primary" />
100131
<div>
101132
<CardTitle className="mb-1">OpenAPI / Swagger</CardTitle>
102-
<CardDescription>Import API endpoints from OpenAPI or Swagger specification
103-
files</CardDescription>
133+
<CardDescription>Import API endpoints from OpenAPI or Swagger specification files</CardDescription>
104134
</div>
105135
</div>
106136
</CardHeader>
137+
<div className="mt-auto p-6 pt-0">
138+
<Button variant="outline" onClick={handleOpenApiClick} disabled={isLoading}>
139+
Select File
140+
</Button>
141+
</div>
107142
</div>
108-
<div className="flex items-center p-6">
109-
<Button variant="outline" onClick={handleOpenApiClick} disabled={isLoading}>
110-
Select File
111-
</Button>
112-
</div>
113-
</div>
114-
<input
115-
type="file"
116-
ref={openApiFileRef}
117-
className="hidden"
118-
accept=".json,.yaml,.yml"
119-
onChange={handleFileChange}
120-
/>
121-
</Card>
122-
123-
{/* Postman Import Option */}
124-
<Card>
125-
<div className="flex flex-col md:flex-row">
126-
<div className="flex-grow">
143+
<input
144+
type="file"
145+
ref={openApiFileRef}
146+
className="hidden"
147+
accept=".json,.yaml,.yml"
148+
onChange={handleFileChange}
149+
/>
150+
</Card>
151+
152+
{/* Postman Import Option */}
153+
<Card>
154+
<div className="flex flex-col h-full">
127155
<CardHeader>
128156
<div className="flex items-center gap-4">
129-
<FileSymlink className="h-10 w-10 text-primary"/>
157+
<FileSymlink className="h-10 w-10 text-primary" />
130158
<div>
131159
<CardTitle className="mb-1">Postman (Experimental)</CardTitle>
132-
<CardDescription>Import API endpoints from Postman collection
133-
files</CardDescription>
160+
<CardDescription>Import API endpoints from Postman collection files</CardDescription>
134161
</div>
135162
</div>
136163
</CardHeader>
164+
<div className="mt-auto p-6 pt-0">
165+
<Button variant="outline" onClick={handlePostmanClick} disabled={isLoading}>
166+
Select File
167+
</Button>
168+
</div>
137169
</div>
138-
<div className="flex items-center p-6">
139-
<Button variant="outline" onClick={handlePostmanClick} disabled={isLoading}>
140-
Select File
141-
</Button>
142-
</div>
143-
</div>
144-
<input
145-
type="file"
146-
ref={postmanFileRef}
147-
className="hidden"
148-
accept=".json"
149-
onChange={handleFileChange}
150-
/>
151-
</Card>
170+
<input type="file" ref={postmanFileRef} className="hidden" accept=".json" onChange={handleFileChange} />
171+
</Card>
172+
</div>
152173

153-
{/* Error Alert */}
154174
{error && (
155-
<Alert variant="destructive">
156-
<AlertCircle className="h-4 w-4"/>
175+
<Alert variant="destructive" className="mt-6">
176+
<AlertCircle className="h-4 w-4" />
157177
<AlertTitle>Error</AlertTitle>
158-
<AlertDescription>
159-
{error}
160-
</AlertDescription>
178+
<AlertDescription>{error}</AlertDescription>
161179
</Alert>
162180
)}
163181
</div>
182+
183+
<div>
184+
<h2 className="text-2xl font-semibold">Browse API Services</h2>
185+
<p className="text-muted-foreground mb-4">Explore and import from our collection of popular API services.</p>
186+
187+
<div className="relative mb-6">
188+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
189+
<Input
190+
type="search"
191+
placeholder="Search API services..."
192+
className="pl-10"
193+
value={searchQuery}
194+
onChange={(e) => setSearchQuery(e.target.value)}
195+
/>
196+
</div>
197+
198+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
199+
{filteredApiServices.map((service) => (
200+
<Card key={service.name} className="overflow-hidden">
201+
<div className="flex flex-col md:flex-row">
202+
<div className="p-6 flex items-center justify-center md:justify-start">
203+
<Image
204+
src={service.image || "/placeholder.svg"}
205+
alt={`${service.name} logo`}
206+
width={64}
207+
height={64}
208+
className="rounded-md"
209+
/>
210+
</div>
211+
<CardContent className="flex-1 p-6 pl-0">
212+
<CardTitle className="mb-2">{service.name}</CardTitle>
213+
<CardDescription className="mb-2">{service.description}</CardDescription>
214+
<Button
215+
variant="outline"
216+
onClick={() => handleApiServiceSelect(service)}
217+
disabled={isLoading}
218+
>
219+
{isLoading ? "Importing..." : "Import"}
220+
</Button>
221+
</CardContent>
222+
</div>
223+
</Card>
224+
))}
225+
</div>
226+
</div>
164227
</div>
165228
)
166229
}

0 commit comments

Comments
 (0)