@djangocfg/ui-tools 2.1.270 → 2.1.271
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.
- package/dist/{PlaygroundLayout-FRKIMYVN.mjs → PlaygroundLayout-G325I6HM.mjs} +65 -13
- package/dist/PlaygroundLayout-G325I6HM.mjs.map +1 -0
- package/dist/{PlaygroundLayout-LIAN63CZ.cjs → PlaygroundLayout-ZO2LO7M5.cjs} +73 -21
- package/dist/PlaygroundLayout-ZO2LO7M5.cjs.map +1 -0
- package/dist/{chunk-FX3GCEUL.mjs → chunk-QZ55LYK2.mjs} +139 -146
- package/dist/chunk-QZ55LYK2.mjs.map +1 -0
- package/dist/{chunk-VAL2LCQD.cjs → chunk-WM4RT5KX.cjs} +138 -145
- package/dist/chunk-WM4RT5KX.cjs.map +1 -0
- package/dist/index.cjs +7 -7
- package/dist/index.mjs +4 -4
- package/package.json +6 -6
- package/src/tools/OpenapiViewer/components/PlaygroundLayout/EndpointList.tsx +11 -4
- package/src/tools/OpenapiViewer/components/PlaygroundLayout/RequestPanel.tsx +33 -6
- package/src/tools/OpenapiViewer/components/PlaygroundLayout/ResponsePanel.tsx +17 -2
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +199 -209
- package/src/tools/OpenapiViewer/types.ts +1 -0
- package/dist/PlaygroundLayout-FRKIMYVN.mjs.map +0 -1
- package/dist/PlaygroundLayout-LIAN63CZ.cjs.map +0 -1
- package/dist/chunk-FX3GCEUL.mjs.map +0 -1
- package/dist/chunk-VAL2LCQD.cjs.map +0 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import consola from 'consola';
|
|
4
4
|
import React, {
|
|
5
|
-
createContext, ReactNode, useCallback, useContext, useEffect,
|
|
5
|
+
createContext, ReactNode, useCallback, useContext, useEffect, useReducer, useRef
|
|
6
6
|
} from 'react';
|
|
7
7
|
|
|
8
8
|
import type {
|
|
@@ -12,18 +12,15 @@ import type {
|
|
|
12
12
|
import { parseRequestHeaders, substituteUrlParameters } from '../utils';
|
|
13
13
|
import { getDefaultVersion } from '../utils/versionManager';
|
|
14
14
|
|
|
15
|
+
// ─── Initial state ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
15
17
|
const createInitialState = (): PlaygroundState => ({
|
|
16
|
-
// Step management
|
|
17
18
|
currentStep: 'endpoints',
|
|
18
19
|
steps: ['endpoints', 'request', 'response'],
|
|
19
|
-
|
|
20
|
-
// Endpoint selection
|
|
21
20
|
selectedEndpoint: null,
|
|
22
21
|
selectedCategory: 'All',
|
|
23
22
|
searchTerm: '',
|
|
24
23
|
selectedVersion: getDefaultVersion().id,
|
|
25
|
-
|
|
26
|
-
// Request configuration
|
|
27
24
|
requestUrl: '',
|
|
28
25
|
requestMethod: 'GET',
|
|
29
26
|
requestHeaders: '{\n "Content-Type": "application/json"\n}',
|
|
@@ -31,197 +28,197 @@ const createInitialState = (): PlaygroundState => ({
|
|
|
31
28
|
selectedApiKey: null,
|
|
32
29
|
manualApiToken: '',
|
|
33
30
|
parameters: {},
|
|
34
|
-
|
|
35
|
-
// Response
|
|
36
31
|
response: null,
|
|
37
32
|
loading: false,
|
|
38
|
-
|
|
39
|
-
// UI state
|
|
40
|
-
sidebarOpen: false, // kept for API compat
|
|
33
|
+
sidebarOpen: false,
|
|
41
34
|
});
|
|
42
35
|
|
|
36
|
+
// ─── Actions ──────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
type Action =
|
|
39
|
+
| { type: 'SET_STEP'; step: PlaygroundStep }
|
|
40
|
+
| { type: 'NEXT_STEP' }
|
|
41
|
+
| { type: 'PREV_STEP' }
|
|
42
|
+
| { type: 'SELECT_ENDPOINT'; endpoint: ApiEndpoint | null }
|
|
43
|
+
| { type: 'SET_CATEGORY'; category: string }
|
|
44
|
+
| { type: 'SET_SEARCH'; term: string }
|
|
45
|
+
| { type: 'SET_VERSION'; version: string }
|
|
46
|
+
| { type: 'SET_REQUEST_URL'; url: string }
|
|
47
|
+
| { type: 'SET_REQUEST_METHOD'; method: string }
|
|
48
|
+
| { type: 'SET_REQUEST_HEADERS'; headers: string }
|
|
49
|
+
| { type: 'SET_REQUEST_BODY'; body: string }
|
|
50
|
+
| { type: 'SET_API_KEY'; apiKeyId: string | null }
|
|
51
|
+
| { type: 'SET_MANUAL_TOKEN'; token: string }
|
|
52
|
+
| { type: 'SET_PARAMETERS'; parameters: Record<string, string> }
|
|
53
|
+
| { type: 'SET_RESPONSE'; response: ApiResponse | null }
|
|
54
|
+
| { type: 'SET_LOADING'; loading: boolean }
|
|
55
|
+
// Batched: set loading + clear response atomically (avoids two renders on send)
|
|
56
|
+
| { type: 'REQUEST_START' }
|
|
57
|
+
// Batched: set response + loading=false + advance step atomically (avoids three renders)
|
|
58
|
+
| { type: 'REQUEST_SUCCESS'; response: ApiResponse }
|
|
59
|
+
// Batched: set error response + loading=false atomically
|
|
60
|
+
| { type: 'REQUEST_ERROR'; response: ApiResponse }
|
|
61
|
+
| { type: 'SET_SIDEBAR'; open: boolean }
|
|
62
|
+
| { type: 'SYNC_API_KEY_HEADER'; headers: string }
|
|
63
|
+
| { type: 'CLEAR_API_KEY_SELECTION' }
|
|
64
|
+
| { type: 'SYNC_URL'; url: string }
|
|
65
|
+
| { type: 'RESET' };
|
|
66
|
+
|
|
67
|
+
// ─── Reducer ──────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function reducer(state: PlaygroundState, action: Action): PlaygroundState {
|
|
70
|
+
switch (action.type) {
|
|
71
|
+
case 'SET_STEP':
|
|
72
|
+
return { ...state, currentStep: action.step };
|
|
73
|
+
|
|
74
|
+
case 'NEXT_STEP': {
|
|
75
|
+
const i = state.steps.indexOf(state.currentStep);
|
|
76
|
+
return i < state.steps.length - 1
|
|
77
|
+
? { ...state, currentStep: state.steps[i + 1]! }
|
|
78
|
+
: state;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'PREV_STEP': {
|
|
82
|
+
const i = state.steps.indexOf(state.currentStep);
|
|
83
|
+
return i > 0 ? { ...state, currentStep: state.steps[i - 1]! } : state;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case 'SELECT_ENDPOINT':
|
|
87
|
+
if (!action.endpoint) return { ...state, selectedEndpoint: null };
|
|
88
|
+
return {
|
|
89
|
+
...state,
|
|
90
|
+
selectedEndpoint: action.endpoint,
|
|
91
|
+
requestMethod: action.endpoint.method,
|
|
92
|
+
requestUrl: action.endpoint.path,
|
|
93
|
+
parameters: {},
|
|
94
|
+
currentStep: 'request',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
case 'SET_CATEGORY': return { ...state, selectedCategory: action.category };
|
|
98
|
+
case 'SET_SEARCH': return { ...state, searchTerm: action.term };
|
|
99
|
+
case 'SET_VERSION': return { ...state, selectedVersion: action.version };
|
|
100
|
+
case 'SET_REQUEST_URL': return { ...state, requestUrl: action.url };
|
|
101
|
+
case 'SET_REQUEST_METHOD': return { ...state, requestMethod: action.method };
|
|
102
|
+
case 'SET_REQUEST_HEADERS': return { ...state, requestHeaders: action.headers };
|
|
103
|
+
case 'SET_REQUEST_BODY': return { ...state, requestBody: action.body };
|
|
104
|
+
case 'SET_API_KEY': return { ...state, selectedApiKey: action.apiKeyId };
|
|
105
|
+
case 'SET_MANUAL_TOKEN': return { ...state, manualApiToken: action.token };
|
|
106
|
+
case 'SET_PARAMETERS': return { ...state, parameters: action.parameters };
|
|
107
|
+
case 'SET_RESPONSE': return { ...state, response: action.response };
|
|
108
|
+
case 'SET_LOADING': return { ...state, loading: action.loading };
|
|
109
|
+
|
|
110
|
+
case 'REQUEST_START':
|
|
111
|
+
return { ...state, loading: true, response: null };
|
|
112
|
+
|
|
113
|
+
case 'REQUEST_SUCCESS':
|
|
114
|
+
return { ...state, loading: false, response: action.response, currentStep: 'response' };
|
|
115
|
+
|
|
116
|
+
case 'REQUEST_ERROR':
|
|
117
|
+
return { ...state, loading: false, response: action.response };
|
|
118
|
+
|
|
119
|
+
case 'SET_SIDEBAR': return { ...state, sidebarOpen: action.open };
|
|
120
|
+
case 'SYNC_API_KEY_HEADER': return { ...state, requestHeaders: action.headers };
|
|
121
|
+
case 'CLEAR_API_KEY_SELECTION': return { ...state, selectedApiKey: null };
|
|
122
|
+
case 'SYNC_URL': return { ...state, requestUrl: action.url };
|
|
123
|
+
case 'RESET': return createInitialState();
|
|
124
|
+
|
|
125
|
+
default: return state;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
43
131
|
const PlaygroundContext = createContext<PlaygroundContextType | undefined>(undefined);
|
|
44
132
|
|
|
45
133
|
export const usePlaygroundContext = () => {
|
|
46
134
|
const context = useContext(PlaygroundContext);
|
|
47
|
-
if (!context)
|
|
48
|
-
throw new Error('usePlaygroundContext must be used within a PlaygroundProvider');
|
|
49
|
-
}
|
|
135
|
+
if (!context) throw new Error('usePlaygroundContext must be used within a PlaygroundProvider');
|
|
50
136
|
return context;
|
|
51
137
|
};
|
|
52
138
|
|
|
139
|
+
// ─── Provider ─────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
53
141
|
interface PlaygroundProviderProps {
|
|
54
142
|
children: ReactNode;
|
|
55
143
|
config: PlaygroundConfig;
|
|
56
144
|
}
|
|
57
145
|
|
|
58
146
|
export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({ children, config }) => {
|
|
59
|
-
const [state,
|
|
147
|
+
const [state, dispatch] = useReducer(reducer, undefined, createInitialState);
|
|
148
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
60
149
|
|
|
61
|
-
// TODO: Get API keys from CFG context - temporarily disabled
|
|
62
|
-
// const { apiKeys: apiKeysResponse, isLoadingApiKeys } = useApiKeysContext();
|
|
63
|
-
// const apiKeys = (apiKeysResponse && apiKeysResponse.results) ? apiKeysResponse.results : [];
|
|
64
150
|
const apiKeys = React.useMemo(() => [], []);
|
|
65
151
|
const isLoadingApiKeys = false;
|
|
66
152
|
|
|
67
|
-
|
|
68
|
-
setState((prev) => ({ ...prev, ...updates }));
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Auto-select first API key when available
|
|
153
|
+
// Auto-select first API key
|
|
72
154
|
useEffect(() => {
|
|
73
155
|
if (!isLoadingApiKeys && apiKeys.length > 0 && !state.selectedApiKey) {
|
|
74
|
-
|
|
156
|
+
dispatch({ type: 'SET_API_KEY', apiKeyId: apiKeys[0]?.id || null });
|
|
75
157
|
}
|
|
76
158
|
}, [apiKeys, isLoadingApiKeys, state.selectedApiKey]);
|
|
77
159
|
|
|
78
|
-
//
|
|
160
|
+
// Sync X-API-Key header when selected key changes
|
|
79
161
|
useEffect(() => {
|
|
80
162
|
try {
|
|
81
|
-
|
|
82
|
-
const headers = parseRequestHeaders(prev.requestHeaders);
|
|
83
|
-
let hasChanged = false;
|
|
84
|
-
|
|
85
|
-
if (prev.selectedApiKey) {
|
|
86
|
-
const apiKey = apiKeys.find(k => k.id === prev.selectedApiKey);
|
|
87
|
-
|
|
88
|
-
if (apiKey) {
|
|
89
|
-
// Add API key to headers only if it changed
|
|
90
|
-
if (headers['X-API-Key'] !== apiKey.id) {
|
|
91
|
-
headers['X-API-Key'] = apiKey.id;
|
|
92
|
-
hasChanged = true;
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
// Selected API key no longer exists, clear selection
|
|
96
|
-
return { ...prev, selectedApiKey: null };
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
// Remove API key from headers if no key is selected
|
|
100
|
-
if (headers['X-API-Key']) {
|
|
101
|
-
delete headers['X-API-Key'];
|
|
102
|
-
hasChanged = true;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
163
|
+
const headers = parseRequestHeaders(state.requestHeaders);
|
|
105
164
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
165
|
+
if (state.selectedApiKey) {
|
|
166
|
+
const apiKey = apiKeys.find((k) => k.id === state.selectedApiKey);
|
|
167
|
+
if (!apiKey) {
|
|
168
|
+
dispatch({ type: 'CLEAR_API_KEY_SELECTION' });
|
|
169
|
+
return;
|
|
110
170
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
171
|
+
if (headers['X-API-Key'] !== apiKey.id) {
|
|
172
|
+
headers['X-API-Key'] = apiKey.id;
|
|
173
|
+
dispatch({ type: 'SYNC_API_KEY_HEADER', headers: JSON.stringify(headers, null, 2) });
|
|
174
|
+
}
|
|
175
|
+
} else if (headers['X-API-Key']) {
|
|
176
|
+
delete headers['X-API-Key'];
|
|
177
|
+
dispatch({ type: 'SYNC_API_KEY_HEADER', headers: JSON.stringify(headers, null, 2) });
|
|
178
|
+
}
|
|
114
179
|
} catch (error) {
|
|
115
180
|
consola.error('Error updating headers:', error);
|
|
116
181
|
}
|
|
117
|
-
}, [state.selectedApiKey, apiKeys]); //
|
|
182
|
+
}, [state.selectedApiKey, apiKeys]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
118
183
|
|
|
119
|
-
//
|
|
184
|
+
// Sync URL when path parameters change
|
|
120
185
|
useEffect(() => {
|
|
121
|
-
if (state.selectedEndpoint
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Only update if URL actually changed to avoid infinite loop
|
|
126
|
-
if (updatedUrl !== state.requestUrl) {
|
|
127
|
-
updateState({ requestUrl: updatedUrl });
|
|
128
|
-
}
|
|
186
|
+
if (!state.selectedEndpoint) return;
|
|
187
|
+
const updated = substituteUrlParameters(state.selectedEndpoint.path, state.parameters);
|
|
188
|
+
if (updated !== state.requestUrl) {
|
|
189
|
+
dispatch({ type: 'SYNC_URL', url: updated });
|
|
129
190
|
}
|
|
130
|
-
}, [state.parameters, state.selectedEndpoint
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
} else {
|
|
162
|
-
updateState({ selectedEndpoint: endpoint });
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const setSelectedCategory = (category: string) => {
|
|
167
|
-
updateState({ selectedCategory: category });
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const setSearchTerm = (term: string) => {
|
|
171
|
-
updateState({ searchTerm: term });
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const setSelectedVersion = (version: string) => {
|
|
175
|
-
updateState({ selectedVersion: version });
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// Request management
|
|
179
|
-
const setRequestUrl = (url: string) => {
|
|
180
|
-
updateState({ requestUrl: url });
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const setRequestMethod = (method: string) => {
|
|
184
|
-
updateState({ requestMethod: method });
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const setRequestHeaders = (headers: string) => {
|
|
188
|
-
updateState({ requestHeaders: headers });
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const setRequestBody = (body: string) => {
|
|
192
|
-
updateState({ requestBody: body });
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const setSelectedApiKey = (apiKeyId: string | null) => {
|
|
196
|
-
updateState({ selectedApiKey: apiKeyId });
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const setManualApiToken = (manualApiToken: string) => {
|
|
200
|
-
updateState({ manualApiToken });
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const setParameters = (parameters: Record<string, string>) => {
|
|
204
|
-
updateState({ parameters });
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
// Response management
|
|
208
|
-
const setResponse = (response: ApiResponse | null) => {
|
|
209
|
-
updateState({ response });
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const setLoading = (loading: boolean) => {
|
|
213
|
-
updateState({ loading });
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
// UI management
|
|
217
|
-
const setSidebarOpen = (sidebarOpen: boolean) => {
|
|
218
|
-
updateState({ sidebarOpen });
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
// Actions
|
|
222
|
-
const clearAll = useCallback(() => {
|
|
223
|
-
setState(createInitialState());
|
|
224
|
-
}, []);
|
|
191
|
+
}, [state.parameters, state.selectedEndpoint]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
192
|
+
|
|
193
|
+
// ── Stable action dispatchers ─────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
const setCurrentStep = useCallback((step: PlaygroundStep) => dispatch({ type: 'SET_STEP', step }), []);
|
|
196
|
+
const goToNextStep = useCallback(() => dispatch({ type: 'NEXT_STEP' }), []);
|
|
197
|
+
const goToPreviousStep = useCallback(() => dispatch({ type: 'PREV_STEP' }), []);
|
|
198
|
+
|
|
199
|
+
const setSelectedEndpoint = useCallback((endpoint: ApiEndpoint | null) =>
|
|
200
|
+
dispatch({ type: 'SELECT_ENDPOINT', endpoint }), []);
|
|
201
|
+
|
|
202
|
+
const setSelectedCategory = useCallback((category: string) =>
|
|
203
|
+
dispatch({ type: 'SET_CATEGORY', category }), []);
|
|
204
|
+
|
|
205
|
+
const setSearchTerm = useCallback((term: string) => dispatch({ type: 'SET_SEARCH', term }), []);
|
|
206
|
+
const setSelectedVersion = useCallback((version: string) => dispatch({ type: 'SET_VERSION', version }), []);
|
|
207
|
+
const setRequestUrl = useCallback((url: string) => dispatch({ type: 'SET_REQUEST_URL', url }), []);
|
|
208
|
+
const setRequestMethod = useCallback((method: string) => dispatch({ type: 'SET_REQUEST_METHOD', method }), []);
|
|
209
|
+
const setRequestHeaders = useCallback((headers: string) => dispatch({ type: 'SET_REQUEST_HEADERS', headers }), []);
|
|
210
|
+
const setRequestBody = useCallback((body: string) => dispatch({ type: 'SET_REQUEST_BODY', body }), []);
|
|
211
|
+
const setSelectedApiKey = useCallback((apiKeyId: string | null) => dispatch({ type: 'SET_API_KEY', apiKeyId }), []);
|
|
212
|
+
const setManualApiToken = useCallback((token: string) => dispatch({ type: 'SET_MANUAL_TOKEN', token }), []);
|
|
213
|
+
const setParameters = useCallback((parameters: Record<string, string>) =>
|
|
214
|
+
dispatch({ type: 'SET_PARAMETERS', parameters }), []);
|
|
215
|
+
const setResponse = useCallback((response: ApiResponse | null) =>
|
|
216
|
+
dispatch({ type: 'SET_RESPONSE', response }), []);
|
|
217
|
+
const setLoading = useCallback((loading: boolean) => dispatch({ type: 'SET_LOADING', loading }), []);
|
|
218
|
+
const setSidebarOpen = useCallback((open: boolean) => dispatch({ type: 'SET_SIDEBAR', open }), []);
|
|
219
|
+
const clearAll = useCallback(() => dispatch({ type: 'RESET' }), []);
|
|
220
|
+
|
|
221
|
+
// ── Send request ──────────────────────────────────────────────────────────
|
|
225
222
|
|
|
226
223
|
const sendRequest = useCallback(async () => {
|
|
227
224
|
if (!state.requestUrl) {
|
|
@@ -229,32 +226,31 @@ export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({ children
|
|
|
229
226
|
return;
|
|
230
227
|
}
|
|
231
228
|
|
|
232
|
-
|
|
233
|
-
|
|
229
|
+
abortControllerRef.current?.abort();
|
|
230
|
+
const controller = new AbortController();
|
|
231
|
+
abortControllerRef.current = controller;
|
|
232
|
+
|
|
233
|
+
// Single dispatch: loading=true + clear response
|
|
234
|
+
dispatch({ type: 'REQUEST_START' });
|
|
235
|
+
|
|
236
|
+
const startTime = Date.now();
|
|
234
237
|
|
|
235
238
|
try {
|
|
236
239
|
const headers = parseRequestHeaders(state.requestHeaders);
|
|
237
240
|
|
|
238
|
-
// Bearer token priority: manual token → JWT token from localStorage
|
|
239
241
|
let bearerToken: string | null = null;
|
|
240
|
-
|
|
241
242
|
if (state.manualApiToken) {
|
|
242
|
-
// Use manual token if provided
|
|
243
243
|
bearerToken = state.manualApiToken;
|
|
244
|
-
} else {
|
|
245
|
-
|
|
246
|
-
if (typeof window !== 'undefined') {
|
|
247
|
-
bearerToken = window.localStorage.getItem('auth_token');
|
|
248
|
-
}
|
|
244
|
+
} else if (typeof window !== 'undefined') {
|
|
245
|
+
bearerToken = window.localStorage.getItem('auth_token');
|
|
249
246
|
}
|
|
250
247
|
|
|
251
|
-
if (bearerToken) {
|
|
252
|
-
headers['Authorization'] = `Bearer ${bearerToken}`;
|
|
253
|
-
}
|
|
248
|
+
if (bearerToken) headers['Authorization'] = `Bearer ${bearerToken}`;
|
|
254
249
|
|
|
255
250
|
const requestOptions: RequestInit = {
|
|
256
251
|
method: state.requestMethod,
|
|
257
252
|
headers,
|
|
253
|
+
signal: controller.signal,
|
|
258
254
|
};
|
|
259
255
|
|
|
260
256
|
if (state.requestBody && state.requestMethod !== 'GET') {
|
|
@@ -262,55 +258,55 @@ export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({ children
|
|
|
262
258
|
}
|
|
263
259
|
|
|
264
260
|
const response = await fetch(state.requestUrl, requestOptions);
|
|
261
|
+
const duration = Date.now() - startTime;
|
|
265
262
|
const responseText = await response.text();
|
|
266
263
|
|
|
267
|
-
let responseData;
|
|
268
|
-
try {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
264
|
+
let responseData: unknown;
|
|
265
|
+
try { responseData = JSON.parse(responseText); }
|
|
266
|
+
catch { responseData = responseText; }
|
|
267
|
+
|
|
268
|
+
// Single dispatch: response + loading=false + step='response'
|
|
269
|
+
dispatch({
|
|
270
|
+
type: 'REQUEST_SUCCESS',
|
|
271
|
+
response: {
|
|
272
|
+
status: response.status,
|
|
273
|
+
statusText: response.statusText,
|
|
274
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
275
|
+
data: responseData,
|
|
276
|
+
duration,
|
|
277
|
+
},
|
|
279
278
|
});
|
|
280
279
|
|
|
281
|
-
consola.success(
|
|
282
|
-
|
|
283
|
-
// Auto-advance to response step
|
|
284
|
-
updateState({ currentStep: 'response' });
|
|
280
|
+
consola.success(`${state.requestMethod} ${state.requestUrl} → ${response.status} (${duration}ms)`);
|
|
285
281
|
} catch (error) {
|
|
282
|
+
if (error instanceof DOMException && error.name === 'AbortError') return;
|
|
286
283
|
consola.error('Request failed:', error);
|
|
287
|
-
|
|
288
|
-
|
|
284
|
+
|
|
285
|
+
// Single dispatch: error response + loading=false
|
|
286
|
+
dispatch({
|
|
287
|
+
type: 'REQUEST_ERROR',
|
|
288
|
+
response: {
|
|
289
|
+
error: error instanceof Error ? error.message : 'Request failed',
|
|
290
|
+
duration: Date.now() - startTime,
|
|
291
|
+
},
|
|
289
292
|
});
|
|
290
|
-
} finally {
|
|
291
|
-
setLoading(false);
|
|
292
293
|
}
|
|
293
|
-
}, [state,
|
|
294
|
+
}, [state.requestUrl, state.requestHeaders, state.manualApiToken, state.requestMethod, state.requestBody]);
|
|
295
|
+
|
|
296
|
+
// ── Context value ─────────────────────────────────────────────────────────
|
|
294
297
|
|
|
295
298
|
const contextValue: PlaygroundContextType = {
|
|
296
|
-
// State
|
|
297
299
|
state,
|
|
298
300
|
config,
|
|
299
301
|
apiKeys,
|
|
300
302
|
apiKeysLoading: isLoadingApiKeys,
|
|
301
|
-
|
|
302
|
-
// Step management
|
|
303
303
|
setCurrentStep,
|
|
304
304
|
goToNextStep,
|
|
305
305
|
goToPreviousStep,
|
|
306
|
-
|
|
307
|
-
// Endpoint management
|
|
308
306
|
setSelectedEndpoint,
|
|
309
307
|
setSelectedCategory,
|
|
310
308
|
setSearchTerm,
|
|
311
309
|
setSelectedVersion,
|
|
312
|
-
|
|
313
|
-
// Request management
|
|
314
310
|
setRequestUrl,
|
|
315
311
|
setRequestMethod,
|
|
316
312
|
setRequestHeaders,
|
|
@@ -318,18 +314,12 @@ export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({ children
|
|
|
318
314
|
setSelectedApiKey,
|
|
319
315
|
setManualApiToken,
|
|
320
316
|
setParameters,
|
|
321
|
-
|
|
322
|
-
// Response management
|
|
323
317
|
setResponse,
|
|
324
318
|
setLoading,
|
|
325
|
-
|
|
326
|
-
// UI management
|
|
327
319
|
setSidebarOpen,
|
|
328
|
-
|
|
329
|
-
// Actions
|
|
330
320
|
clearAll,
|
|
331
321
|
sendRequest,
|
|
332
322
|
};
|
|
333
323
|
|
|
334
324
|
return <PlaygroundContext.Provider value={contextValue}>{children}</PlaygroundContext.Provider>;
|
|
335
|
-
};
|
|
325
|
+
};
|