elder_docs 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +43 -0
- data/frontend/index.html +18 -0
- data/frontend/package.json +28 -0
- data/frontend/postcss.config.js +7 -0
- data/frontend/public/data.js +2 -0
- data/frontend/src/App.jsx +154 -0
- data/frontend/src/UiConfigApp.jsx +11 -0
- data/frontend/src/components/ApiExplorer.jsx +323 -0
- data/frontend/src/components/ContentPanel.jsx +228 -0
- data/frontend/src/components/Sidebar.jsx +157 -0
- data/frontend/src/components/UiConfigurator.jsx +470 -0
- data/frontend/src/contexts/ApiKeyContext.jsx +41 -0
- data/frontend/src/index.css +215 -0
- data/frontend/src/main.jsx +11 -0
- data/frontend/tailwind.config.js +40 -0
- data/frontend/vite.config.js +17 -0
- data/lib/elder_docs/generator.rb +19 -3
- data/lib/elder_docs/version.rb +1 -1
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20f0c58e7d2f5a6497d153f05cc9127730b8897b34a0a9ac8a42959e49f263f4
|
|
4
|
+
data.tar.gz: e9230ebcf9b18e2e3b2809dd2cd7d2880e6fe346e0d251ea32f3011a579a42f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4b30353dd92332b3a017ca6df2598daeaf96ca4c5ce67b2d0100eaeda4d26b61dd949168440fa8aa60bd3914c81fbfa4c8fed0a303b2e0bbbda16bef3b8fa03
|
|
7
|
+
data.tar.gz: e13f040aa72ecea558922a99c678975b324f5860d5e8bd035268b6337eee23bb4ef17d879388f7ce5edaf71fa2e01a53ec95874d9eb647e013bf31adbc3f048a
|
data/README.md
CHANGED
|
@@ -145,6 +145,49 @@ bundle exec elderdocs deploy \
|
|
|
145
145
|
- Rails >= 6.0
|
|
146
146
|
- Node.js >= 16.0 (for building assets)
|
|
147
147
|
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
### Quick Development Build
|
|
151
|
+
|
|
152
|
+
For rapid iteration during development:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
./dev_build.sh
|
|
156
|
+
# or
|
|
157
|
+
rake elderdocs:dev
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This rebuilds and reinstalls the gem locally without publishing.
|
|
161
|
+
|
|
162
|
+
### Full Build and Deploy
|
|
163
|
+
|
|
164
|
+
To build, test, and optionally publish a new version:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
./build_and_deploy.sh
|
|
168
|
+
# or
|
|
169
|
+
rake elderdocs:deploy
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Options:
|
|
173
|
+
- `--skip-publish` - Build and test but don't publish to RubyGems
|
|
174
|
+
- `--skip-tests` - Skip running tests
|
|
175
|
+
|
|
176
|
+
### Manual Build
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Build gem
|
|
180
|
+
rake elderdocs:build
|
|
181
|
+
# or
|
|
182
|
+
gem build elder_docs.gemspec
|
|
183
|
+
|
|
184
|
+
# Install locally
|
|
185
|
+
gem install elder_docs-*.gem --local
|
|
186
|
+
|
|
187
|
+
# Publish (when ready)
|
|
188
|
+
gem push elder_docs-*.gem
|
|
189
|
+
```
|
|
190
|
+
|
|
148
191
|
## License
|
|
149
192
|
|
|
150
193
|
MIT
|
data/frontend/index.html
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
10
|
+
<title>ElderDocs - API Documentation</title>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script src="/data.js"></script>
|
|
15
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
18
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "elder-docs-frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"react": "^18.2.0",
|
|
13
|
+
"react-dom": "^18.2.0",
|
|
14
|
+
"react-markdown": "^9.0.0",
|
|
15
|
+
"react-syntax-highlighter": "^15.5.0",
|
|
16
|
+
"prismjs": "^1.29.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/react": "^18.2.0",
|
|
20
|
+
"@types/react-dom": "^18.2.0",
|
|
21
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
22
|
+
"autoprefixer": "^10.4.14",
|
|
23
|
+
"postcss": "^8.4.24",
|
|
24
|
+
"tailwindcss": "^3.3.0",
|
|
25
|
+
"vite": "^4.4.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
window.ElderDocsData = {"openapi":{"openapi":"3.0.0","info":{"title":"Test API - ElderDocs Demo","version":"1.0.0","description":"A comprehensive test API showcasing various endpoints, methods, and payloads for ElderDocs demonstration"},"servers":[{"url":"https://jsonplaceholder.typicode.com","description":"Test server (JSONPlaceholder)"},{"url":"https://api.example.com","description":"Production server"}],"paths":{"/posts":{"get":{"summary":"Get all posts","description":"Retrieve a list of all blog posts with optional filtering and pagination","parameters":[{"name":"userId","in":"query","description":"Filter posts by user ID","required":false,"schema":{"type":"integer"}},{"name":"_limit","in":"query","description":"Limit the number of results","required":false,"schema":{"type":"integer","default":10}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Post"}}}}}}},"post":{"summary":"Create a new post","description":"Create a new blog post with title, body, and user ID","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePostRequest"},"example":{"title":"My New Post","body":"This is the content of my new post","userId":1}}}},"responses":{"201":{"description":"Post created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}},"400":{"description":"Bad request - invalid input"}}}},"/posts/{id}":{"get":{"summary":"Get post by ID","description":"Retrieve a specific post by its unique identifier","parameters":[{"name":"id","in":"path","required":true,"description":"Post ID","schema":{"type":"integer"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}},"404":{"description":"Post not found"}}},"put":{"summary":"Update entire post","description":"Replace all fields of an existing post","parameters":[{"name":"id","in":"path","required":true,"description":"Post ID","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePostRequest"},"example":{"id":1,"title":"Updated Post Title","body":"Updated post content","userId":1}}}},"responses":{"200":{"description":"Post updated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}}}},"patch":{"summary":"Partially update post","description":"Update only specified fields of a post","parameters":[{"name":"id","in":"path","required":true,"description":"Post ID","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"body":{"type":"string"}}},"example":{"title":"Partially Updated Title"}}}},"responses":{"200":{"description":"Post partially updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"}}}}}},"delete":{"summary":"Delete a post","description":"Permanently delete a post by ID","parameters":[{"name":"id","in":"path","required":true,"description":"Post ID","schema":{"type":"integer"}}],"responses":{"200":{"description":"Post deleted successfully"},"404":{"description":"Post not found"}}}},"/users":{"get":{"summary":"List all users","description":"Get a list of all registered users","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}}}},"/users/{id}":{"get":{"summary":"Get user by ID","description":"Retrieve detailed information about a specific user","parameters":[{"name":"id","in":"path","required":true,"description":"User ID","schema":{"type":"integer"}}],"responses":{"200":{"description":"User found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"404":{"description":"User not found"}}}},"/comments":{"get":{"summary":"Get all comments","description":"Retrieve comments with optional filtering","parameters":[{"name":"postId","in":"query","description":"Filter comments by post ID","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"List of comments","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Comment"}}}}}}},"post":{"summary":"Create a comment","description":"Add a new comment to a post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCommentRequest"},"example":{"postId":1,"name":"John Doe","email":"john@example.com","body":"This is a great post!"}}}},"responses":{"201":{"description":"Comment created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Comment"}}}}}}}},"components":{"schemas":{"Post":{"type":"object","properties":{"id":{"type":"integer","description":"Unique post identifier"},"title":{"type":"string","description":"Post title"},"body":{"type":"string","description":"Post content"},"userId":{"type":"integer","description":"ID of the user who created the post"}}},"CreatePostRequest":{"type":"object","required":["title","body","userId"],"properties":{"title":{"type":"string","description":"Post title","minLength":1,"maxLength":200},"body":{"type":"string","description":"Post content"},"userId":{"type":"integer","description":"User ID"}}},"UpdatePostRequest":{"type":"object","required":["id","title","body","userId"],"properties":{"id":{"type":"integer"},"title":{"type":"string"},"body":{"type":"string"},"userId":{"type":"integer"}}},"User":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"username":{"type":"string"},"email":{"type":"string","format":"email"},"address":{"type":"object","properties":{"street":{"type":"string"},"city":{"type":"string"},"zipcode":{"type":"string"}}},"phone":{"type":"string"},"website":{"type":"string"}}},"Comment":{"type":"object","properties":{"id":{"type":"integer"},"postId":{"type":"integer"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"body":{"type":"string"}}},"CreateCommentRequest":{"type":"object","required":["postId","name","email","body"],"properties":{"postId":{"type":"integer"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"body":{"type":"string"}}}}}},"articles":[{"id":"getting_started","title":"🚀 Getting Started","markdown_content":"# Welcome to ElderDocs!\n\nThis is a **dead simple** API documentation platform.\n\n## Quick Start\n\n1. **Select an endpoint** from the sidebar\n2. **Fill in parameters** (if needed)\n3. **Click SEND REQUEST**\n4. **See the response** instantly\n\nThat's it! No complicated setup needed.\n\n## Features\n\n- ✅ **Interactive API Explorer** - Try endpoints right in your browser\n- ✅ **Multiple Auth Types** - Bearer tokens, API keys, Basic auth, OAuth2\n- ✅ **Beautiful Design** - Bright yellow and white with sharp corners\n- ✅ **Big Fonts** - Easy to read, easy to use\n- ✅ **Real-time Testing** - See responses immediately\n\n## Authentication\n\nChoose your authentication method from the dropdown:\n\n- **Bearer Token**: Standard OAuth2 bearer tokens\n- **API Key Header**: Custom API key headers\n- **Basic Auth**: Username:password format\n- **OAuth 2.0**: OAuth token support\n\nEnter your credentials once, and they'll be saved for all requests!"},{"id":"api_basics","title":"📖 API Basics","markdown_content":"## Understanding HTTP Methods\n\n### GET\n\nUse GET to **retrieve** data. It's safe and doesn't modify anything.\n\n```bash\nGET /posts\n```\n\n### POST\n\nUse POST to **create** new resources.\n\n```bash\nPOST /posts\nContent-Type: application/json\n\n{\n \"title\": \"My Post\",\n \"body\": \"Content here\",\n \"userId\": 1\n}\n```\n\n### PUT\n\nUse PUT to **replace** an entire resource.\n\n```bash\nPUT /posts/1\n```\n\n### PATCH\n\nUse PATCH to **partially update** a resource.\n\n```bash\nPATCH /posts/1\n```\n\n### DELETE\n\nUse DELETE to **remove** a resource.\n\n```bash\nDELETE /posts/1\n```\n\n## Response Codes\n\n- **200 OK**: Success!\n- **201 Created**: Resource created\n- **400 Bad Request**: Invalid input\n- **404 Not Found**: Resource doesn't exist\n- **500 Server Error**: Something went wrong"},{"id":"testing_tips","title":"💡 Testing Tips","markdown_content":"## Pro Tips for Testing\n\n### 1. Start Simple\n\nBegin with GET requests - they're the easiest!\n\n### 2. Check Required Fields\n\nRequired parameters are marked with a red asterisk (*).\n\n### 3. Use Examples\n\nMany endpoints have example payloads. Copy and modify them!\n\n### 4. Read the Response\n\nThe response shows:\n\n- **Status code** (200, 404, etc.)\n- **Response time** (how fast the API responded)\n- **Response body** (the actual data)\n\n### 5. Try Different Auth Methods\n\nSwitch between authentication types to see which works best for your API.\n\n### 6. Path Parameters\n\nFor endpoints like `/posts/{id}`, enter the ID in the Path Parameters section.\n\n### 7. Query Parameters\n\nAdd filters and pagination using Query Parameters.\n\n## Common Issues\n\n**CORS Error?**\n\nSome APIs block browser requests. Use a CORS proxy or test from your backend.\n\n**401 Unauthorized?**\n\nCheck your authentication credentials.\n\n**404 Not Found?**\n\nVerify the endpoint path and any path parameters."}],"api_server":"https://jsonplaceholder.typicode.com","api_servers":[],"auth_types":["bearer","api_key","basic","oauth2"],"ui_config":{},"generated_at":"2025-11-24T14:09:15+05:30"};
|
|
2
|
+
window.dispatchEvent(new Event('elderdocs:data_loaded'));
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import Sidebar from './components/Sidebar'
|
|
3
|
+
import ContentPanel from './components/ContentPanel'
|
|
4
|
+
import ApiExplorer from './components/ApiExplorer'
|
|
5
|
+
import { ApiKeyProvider } from './contexts/ApiKeyContext'
|
|
6
|
+
|
|
7
|
+
function App() {
|
|
8
|
+
const [data, setData] = useState(null)
|
|
9
|
+
const [loading, setLoading] = useState(true)
|
|
10
|
+
const [activeView, setActiveView] = useState('api') // 'api' or 'articles'
|
|
11
|
+
const [selectedItem, setSelectedItem] = useState(null)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (data?.ui_config) {
|
|
15
|
+
const root = document.documentElement
|
|
16
|
+
const ui = data.ui_config
|
|
17
|
+
|
|
18
|
+
if (ui.colors) {
|
|
19
|
+
if (ui.colors.primary) root.style.setProperty('--bd-primary', ui.colors.primary)
|
|
20
|
+
if (ui.colors.secondary) {
|
|
21
|
+
root.style.setProperty('--bd-secondary', ui.colors.secondary)
|
|
22
|
+
}
|
|
23
|
+
if (ui.colors.background) root.style.setProperty('--bd-background', ui.colors.background)
|
|
24
|
+
if (ui.colors.surface) root.style.setProperty('--bd-surface', ui.colors.surface)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (ui.corner_radius) {
|
|
28
|
+
root.style.setProperty('--bd-corner-radius', ui.corner_radius)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (ui.font_heading) {
|
|
32
|
+
root.style.setProperty('--bd-font-heading', `'${ui.font_heading}', sans-serif`)
|
|
33
|
+
// Dynamically load font
|
|
34
|
+
const link = document.createElement('link')
|
|
35
|
+
link.href = `https://fonts.googleapis.com/css2?family=${ui.font_heading.replace(/\s/g, '+')}:wght@500;600;700&display=swap`
|
|
36
|
+
link.rel = 'stylesheet'
|
|
37
|
+
if (!document.querySelector(`link[href*="${ui.font_heading}"]`)) {
|
|
38
|
+
document.head.appendChild(link)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (ui.font_body) {
|
|
43
|
+
root.style.setProperty('--bd-font-body', `'${ui.font_body}', sans-serif`)
|
|
44
|
+
// Dynamically load font
|
|
45
|
+
const link = document.createElement('link')
|
|
46
|
+
link.href = `https://fonts.googleapis.com/css2?family=${ui.font_body.replace(/\s/g, '+')}:wght@400;500;600&display=swap`
|
|
47
|
+
link.rel = 'stylesheet'
|
|
48
|
+
if (!document.querySelector(`link[href*="${ui.font_body}"]`)) {
|
|
49
|
+
document.head.appendChild(link)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}, [data])
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
document.body.dataset.mounted = 'false'
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
document.body.dataset.mounted = 'true'
|
|
59
|
+
}, 0)
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
clearTimeout(timeout)
|
|
63
|
+
delete document.body.dataset.mounted
|
|
64
|
+
}
|
|
65
|
+
}, [])
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const initializeData = (payload) => {
|
|
69
|
+
setData(payload)
|
|
70
|
+
setLoading(false)
|
|
71
|
+
|
|
72
|
+
if (payload.openapi?.paths) {
|
|
73
|
+
const firstPath = Object.keys(payload.openapi.paths)[0]
|
|
74
|
+
const firstMethod = Object.keys(payload.openapi.paths[firstPath])[0]
|
|
75
|
+
setSelectedItem({ type: 'endpoint', path: firstPath, method: firstMethod })
|
|
76
|
+
} else if (payload.articles?.length > 0) {
|
|
77
|
+
setSelectedItem({ type: 'article', id: payload.articles[0].id })
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const tryInitialize = () => {
|
|
82
|
+
if (window.ElderDocsData) {
|
|
83
|
+
initializeData(window.ElderDocsData)
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!tryInitialize()) {
|
|
90
|
+
const handleDataLoaded = () => {
|
|
91
|
+
if (window.ElderDocsData) {
|
|
92
|
+
initializeData(window.ElderDocsData)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
window.addEventListener('elderdocs:data_loaded', handleDataLoaded)
|
|
97
|
+
|
|
98
|
+
// Fallback timeout so we don't wait forever
|
|
99
|
+
const timeoutId = setTimeout(() => {
|
|
100
|
+
if (!window.ElderDocsData) {
|
|
101
|
+
console.error('ElderDocsData not found. Make sure data.js is loaded.')
|
|
102
|
+
setLoading(false)
|
|
103
|
+
}
|
|
104
|
+
}, 5000)
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
window.removeEventListener('elderdocs:data_loaded', handleDataLoaded)
|
|
108
|
+
clearTimeout(timeoutId)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}, [])
|
|
112
|
+
|
|
113
|
+
if (loading) {
|
|
114
|
+
return (
|
|
115
|
+
<div className="flex items-center justify-center h-screen bg-white">
|
|
116
|
+
<div className="text-3xl font-black text-black">Loading documentation...</div>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!data) {
|
|
122
|
+
return (
|
|
123
|
+
<div className="flex items-center justify-center h-screen bg-white">
|
|
124
|
+
<div className="text-3xl font-black text-red-600">Error: Documentation data not found</div>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<ApiKeyProvider>
|
|
131
|
+
<div className="app-shell">
|
|
132
|
+
<Sidebar
|
|
133
|
+
data={data}
|
|
134
|
+
activeView={activeView}
|
|
135
|
+
setActiveView={setActiveView}
|
|
136
|
+
selectedItem={selectedItem}
|
|
137
|
+
setSelectedItem={setSelectedItem}
|
|
138
|
+
/>
|
|
139
|
+
<ContentPanel
|
|
140
|
+
data={data}
|
|
141
|
+
activeView={activeView}
|
|
142
|
+
selectedItem={selectedItem}
|
|
143
|
+
/>
|
|
144
|
+
<ApiExplorer
|
|
145
|
+
data={data}
|
|
146
|
+
selectedItem={selectedItem}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
</ApiKeyProvider>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default App
|
|
154
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import ReactDOM from 'react-dom/client'
|
|
3
|
+
import UiConfigurator from './components/UiConfigurator'
|
|
4
|
+
import './index.css'
|
|
5
|
+
|
|
6
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
7
|
+
<React.StrictMode>
|
|
8
|
+
<UiConfigurator />
|
|
9
|
+
</React.StrictMode>,
|
|
10
|
+
)
|
|
11
|
+
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { useApiKey } from '../contexts/ApiKeyContext'
|
|
3
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
|
4
|
+
import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism'
|
|
5
|
+
|
|
6
|
+
const ApiExplorer = ({ data, selectedItem }) => {
|
|
7
|
+
const { authConfig, setAuthConfig } = useApiKey()
|
|
8
|
+
const [params, setParams] = useState({})
|
|
9
|
+
const [headers, setHeaders] = useState({})
|
|
10
|
+
const [body, setBody] = useState('')
|
|
11
|
+
const [loading, setLoading] = useState(false)
|
|
12
|
+
const [response, setResponse] = useState(null)
|
|
13
|
+
const [isOpen, setIsOpen] = useState(true)
|
|
14
|
+
|
|
15
|
+
const authTypes = data?.auth_types || ['bearer', 'api_key', 'basic', 'oauth2']
|
|
16
|
+
const currentAuthType = authConfig?.type || 'bearer'
|
|
17
|
+
const isEndpoint = selectedItem && selectedItem.type === 'endpoint'
|
|
18
|
+
const selectedPath = isEndpoint ? selectedItem.path : ''
|
|
19
|
+
const selectedMethod = isEndpoint ? selectedItem.method : ''
|
|
20
|
+
const operation = isEndpoint ? data.openapi.paths[selectedPath]?.[selectedMethod] : null
|
|
21
|
+
const servers = data.api_servers?.length > 0 ? data.api_servers : (data.openapi.servers || [])
|
|
22
|
+
const defaultServerUrl = data.api_server || servers[0]?.url || ''
|
|
23
|
+
const [selectedServer, setSelectedServer] = useState(defaultServerUrl)
|
|
24
|
+
const [isCustomUrl, setIsCustomUrl] = useState(false)
|
|
25
|
+
const [customUrl, setCustomUrl] = useState('')
|
|
26
|
+
const fullUrl = `${isCustomUrl ? customUrl : selectedServer}${selectedPath}`
|
|
27
|
+
|
|
28
|
+
if (!isEndpoint) {
|
|
29
|
+
return (
|
|
30
|
+
<div className={`w-[360px] surface border-l-0 border-black/10 flex flex-col transition-all bg-white ${isOpen ? '' : 'hidden'}`} style={{ borderLeftWidth: '3px' }}>
|
|
31
|
+
<div className="p-6 border-b-0 border-black/10" style={{ borderBottomWidth: '3px' }}>
|
|
32
|
+
<div className="pill text-black bg-yellow-400 border-black mb-4">API Explorer</div>
|
|
33
|
+
<div className="text-base font-bold text-black">Select an endpoint to light up the live runner.</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!operation) return null
|
|
40
|
+
|
|
41
|
+
const handleParamChange = (name, value) => {
|
|
42
|
+
setParams(prev => ({ ...prev, [name]: value }))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const handleHeaderChange = (name, value) => {
|
|
46
|
+
setHeaders(prev => ({ ...prev, [name]: value }))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handleAuthChange = (type, value) => {
|
|
50
|
+
setAuthConfig({ type, value })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const buildUrl = () => {
|
|
54
|
+
let url = fullUrl
|
|
55
|
+
const queryParams = []
|
|
56
|
+
|
|
57
|
+
// First, replace path parameters
|
|
58
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
59
|
+
if (value) {
|
|
60
|
+
const param = operation.parameters?.find(p => p.name === key && p.in === 'path')
|
|
61
|
+
if (param) {
|
|
62
|
+
url = url.replace(`{${key}}`, encodeURIComponent(value))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Then, add query parameters
|
|
68
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
69
|
+
if (value) {
|
|
70
|
+
const param = operation.parameters?.find(p => p.name === key && p.in === 'query')
|
|
71
|
+
if (param) {
|
|
72
|
+
queryParams.push(`${key}=${encodeURIComponent(value)}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (queryParams.length > 0) {
|
|
78
|
+
url += '?' + queryParams.join('&')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return url
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const executeRequest = async () => {
|
|
85
|
+
setLoading(true)
|
|
86
|
+
setResponse(null)
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const url = buildUrl()
|
|
90
|
+
const requestHeaders = {
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
...headers
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Apply authentication based on type
|
|
96
|
+
if (authConfig?.value) {
|
|
97
|
+
switch (authConfig.type) {
|
|
98
|
+
case 'bearer':
|
|
99
|
+
requestHeaders['Authorization'] = `Bearer ${authConfig.value}`
|
|
100
|
+
break
|
|
101
|
+
case 'api_key':
|
|
102
|
+
requestHeaders['X-API-Key'] = authConfig.value
|
|
103
|
+
break
|
|
104
|
+
case 'basic':
|
|
105
|
+
requestHeaders['Authorization'] = `Basic ${btoa(authConfig.value)}`
|
|
106
|
+
break
|
|
107
|
+
case 'oauth2':
|
|
108
|
+
requestHeaders['Authorization'] = `Bearer ${authConfig.value}`
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const requestOptions = {
|
|
114
|
+
method: selectedMethod.toUpperCase(),
|
|
115
|
+
headers: requestHeaders
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (['post', 'put', 'patch'].includes(selectedMethod.toLowerCase()) && body) {
|
|
119
|
+
requestOptions.body = body
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const startTime = Date.now()
|
|
123
|
+
const res = await fetch(url, requestOptions)
|
|
124
|
+
const endTime = Date.now()
|
|
125
|
+
|
|
126
|
+
const responseText = await res.text()
|
|
127
|
+
let responseData
|
|
128
|
+
try {
|
|
129
|
+
responseData = JSON.parse(responseText)
|
|
130
|
+
} catch {
|
|
131
|
+
responseData = responseText
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setResponse({
|
|
135
|
+
status: res.status,
|
|
136
|
+
statusText: res.statusText,
|
|
137
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
138
|
+
body: responseData,
|
|
139
|
+
time: endTime - startTime,
|
|
140
|
+
url: url
|
|
141
|
+
})
|
|
142
|
+
} catch (error) {
|
|
143
|
+
setResponse({
|
|
144
|
+
error: error.message
|
|
145
|
+
})
|
|
146
|
+
} finally {
|
|
147
|
+
setLoading(false)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const queryParams = operation.parameters?.filter(p => p.in === 'query') || []
|
|
152
|
+
const pathParams = operation.parameters?.filter(p => p.in === 'path') || []
|
|
153
|
+
const headerParams = operation.parameters?.filter(p => p.in === 'header') || []
|
|
154
|
+
const hasRequestBody = operation.requestBody && ['post', 'put', 'patch'].includes(selectedMethod.toLowerCase())
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className={`w-[360px] surface border-l-0 border-black/10 flex flex-col transition-all bg-white ${isOpen ? '' : 'hidden'}`} style={{ borderLeftWidth: '3px' }}>
|
|
158
|
+
<div className="p-6 border-b-0 border-black/10 flex items-center justify-between" style={{ borderBottomWidth: '3px' }}>
|
|
159
|
+
<div>
|
|
160
|
+
<div className="pill text-black bg-yellow-400 border-black">Live Runner</div>
|
|
161
|
+
<h2 className="text-xl font-black text-black mt-4 font-['Syne'] uppercase">Send real traffic</h2>
|
|
162
|
+
</div>
|
|
163
|
+
<button
|
|
164
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
165
|
+
className="md:hidden btn-secondary px-3 py-2"
|
|
166
|
+
>
|
|
167
|
+
{isOpen ? 'Close' : 'Open'}
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className="flex-1 overflow-y-auto p-6 space-y-6 text-black">
|
|
172
|
+
<div className="reveal" style={{ animationDelay: '140ms' }}>
|
|
173
|
+
<p className="text-[0.65rem] uppercase tracking-[0.3em] text-black/60 mb-2 font-bold">Base URL</p>
|
|
174
|
+
{!isCustomUrl ? (
|
|
175
|
+
<select
|
|
176
|
+
value={selectedServer}
|
|
177
|
+
onChange={(e) => {
|
|
178
|
+
if (e.target.value === '__custom__') {
|
|
179
|
+
setIsCustomUrl(true)
|
|
180
|
+
setCustomUrl(selectedServer)
|
|
181
|
+
} else {
|
|
182
|
+
setSelectedServer(e.target.value)
|
|
183
|
+
}
|
|
184
|
+
}}
|
|
185
|
+
className="input-field w-full text-sm bg-white border-black text-black font-bold"
|
|
186
|
+
>
|
|
187
|
+
{servers.map((server, idx) => (
|
|
188
|
+
<option key={idx} value={server.url}>
|
|
189
|
+
{server.description ? `${server.description} (${server.url})` : server.url}
|
|
190
|
+
</option>
|
|
191
|
+
))}
|
|
192
|
+
<option value="__custom__">➕ Custom URL...</option>
|
|
193
|
+
</select>
|
|
194
|
+
) : (
|
|
195
|
+
<div className="space-y-2">
|
|
196
|
+
<input
|
|
197
|
+
type="text"
|
|
198
|
+
value={customUrl}
|
|
199
|
+
onChange={(e) => setCustomUrl(e.target.value)}
|
|
200
|
+
placeholder="Enter custom base URL"
|
|
201
|
+
className="input-field w-full text-sm bg-white border-black text-black font-medium"
|
|
202
|
+
/>
|
|
203
|
+
<button
|
|
204
|
+
onClick={() => {
|
|
205
|
+
setIsCustomUrl(false)
|
|
206
|
+
if (customUrl && servers.some(s => s.url === customUrl)) {
|
|
207
|
+
setSelectedServer(customUrl)
|
|
208
|
+
}
|
|
209
|
+
}}
|
|
210
|
+
className="btn-secondary text-xs px-3 py-1"
|
|
211
|
+
>
|
|
212
|
+
Use Preset
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div className="reveal" style={{ animationDelay: '140ms' }}>
|
|
219
|
+
<p className="text-[0.65rem] uppercase tracking-[0.3em] text-black/60 mb-2 font-bold">Authentication</p>
|
|
220
|
+
<select
|
|
221
|
+
value={currentAuthType}
|
|
222
|
+
onChange={(e) => handleAuthChange(e.target.value, authConfig?.value || '')}
|
|
223
|
+
className="input-field w-full text-sm bg-white border-black text-black font-bold"
|
|
224
|
+
>
|
|
225
|
+
{authTypes.map(type => (
|
|
226
|
+
<option key={type} value={type}>
|
|
227
|
+
{type === 'bearer' ? 'Bearer Token' :
|
|
228
|
+
type === 'api_key' ? 'API Key Header' :
|
|
229
|
+
type === 'basic' ? 'Basic Auth' :
|
|
230
|
+
type === 'oauth2' ? 'OAuth 2.0' : type}
|
|
231
|
+
</option>
|
|
232
|
+
))}
|
|
233
|
+
</select>
|
|
234
|
+
<input
|
|
235
|
+
type={currentAuthType === 'basic' ? 'text' : 'password'}
|
|
236
|
+
value={authConfig?.value || ''}
|
|
237
|
+
onChange={(e) => handleAuthChange(currentAuthType, e.target.value)}
|
|
238
|
+
placeholder={
|
|
239
|
+
currentAuthType === 'bearer' ? 'Enter Bearer token' :
|
|
240
|
+
currentAuthType === 'api_key' ? 'Enter API key' :
|
|
241
|
+
currentAuthType === 'basic' ? 'username:password' :
|
|
242
|
+
currentAuthType === 'oauth2' ? 'Enter OAuth token' : 'Enter credentials'
|
|
243
|
+
}
|
|
244
|
+
className="input-field w-full mt-3 text-sm bg-white border-black text-black font-medium"
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{[{ label: 'Path Parameters', items: pathParams, handler: handleParamChange, values: params },
|
|
249
|
+
{ label: 'Query Parameters', items: queryParams, handler: handleParamChange, values: params },
|
|
250
|
+
{ label: 'Headers', items: headerParams, handler: handleHeaderChange, values: headers },
|
|
251
|
+
].map((section, idx) => (
|
|
252
|
+
section.items.length > 0 && (
|
|
253
|
+
<div key={section.label} className="reveal" style={{ animationDelay: `${160 + idx * 60}ms` }}>
|
|
254
|
+
<p className="text-[0.65rem] uppercase tracking-[0.3em] text-black/60 mb-2 font-bold">{section.label}</p>
|
|
255
|
+
{section.items.map((param) => (
|
|
256
|
+
<div key={param.name} className="mb-3">
|
|
257
|
+
<label className="block text-[0.6rem] font-bold uppercase tracking-[0.25em] text-black/80 mb-1">
|
|
258
|
+
{param.name} {param.required && <span className="text-red-500">*</span>}
|
|
259
|
+
</label>
|
|
260
|
+
<input
|
|
261
|
+
type="text"
|
|
262
|
+
value={section.values[param.name] || ''}
|
|
263
|
+
onChange={(e) => section.handler(param.name, e.target.value)}
|
|
264
|
+
placeholder={param.schema?.default || ''}
|
|
265
|
+
className="input-field w-full text-sm bg-white border-black text-black font-medium"
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
268
|
+
))}
|
|
269
|
+
</div>
|
|
270
|
+
)
|
|
271
|
+
))}
|
|
272
|
+
|
|
273
|
+
{hasRequestBody && (
|
|
274
|
+
<div className="reveal" style={{ animationDelay: '260ms' }}>
|
|
275
|
+
<p className="text-[0.65rem] uppercase tracking-[0.3em] text-black/60 mb-2 font-bold">Request Body</p>
|
|
276
|
+
<textarea
|
|
277
|
+
value={body}
|
|
278
|
+
onChange={(e) => setBody(e.target.value)}
|
|
279
|
+
placeholder="Enter JSON body"
|
|
280
|
+
className="input-field w-full h-32 font-mono text-sm bg-white border-black text-black font-medium"
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
<button
|
|
286
|
+
onClick={executeRequest}
|
|
287
|
+
disabled={loading}
|
|
288
|
+
className="btn-primary w-full reveal"
|
|
289
|
+
style={{ animationDelay: '300ms' }}
|
|
290
|
+
>
|
|
291
|
+
{loading ? 'Sending...' : 'Send Request'}
|
|
292
|
+
</button>
|
|
293
|
+
|
|
294
|
+
{response && (
|
|
295
|
+
<div className="reveal" style={{ animationDelay: '320ms' }}>
|
|
296
|
+
<p className="text-[0.65rem] uppercase tracking-[0.3em] text-black/60 mb-2 font-bold">Response</p>
|
|
297
|
+
{response.error ? (
|
|
298
|
+
<div className="surface surface--highlight border-2 border-black bg-red-50 text-red-900 text-sm p-4 font-bold rounded-none">
|
|
299
|
+
{response.error}
|
|
300
|
+
</div>
|
|
301
|
+
) : (
|
|
302
|
+
<div className="space-y-3">
|
|
303
|
+
<div className="flex items-center gap-3 text-sm text-black">
|
|
304
|
+
<span className="chip bg-white border-black text-black">
|
|
305
|
+
{response.status} {response.statusText}
|
|
306
|
+
</span>
|
|
307
|
+
<span className="font-bold">{response.time} ms</span>
|
|
308
|
+
</div>
|
|
309
|
+
<div className="rounded-none border-2 border-black bg-white">
|
|
310
|
+
<SyntaxHighlighter language="json" style={oneLight} customStyle={{ margin: 0, padding: '16px', fontSize: '14px', backgroundColor: '#fff' }}>
|
|
311
|
+
{JSON.stringify(response.body, null, 2)}
|
|
312
|
+
</SyntaxHighlighter>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export default ApiExplorer
|