@lukoweb/apitogo 0.2.43 → 0.2.45
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/cli/cli.js +2961 -2904
- package/dist/declarations/lib/plugins/openapi/playground/result-panel/ResultPanel.d.ts +3 -1
- package/dist/declarations/lib/plugins/openapi/playground/warmUpRetry.d.ts +5 -0
- package/package.json +1 -1
- package/src/config/v2/full-server.ts +5 -0
- package/src/config/v2/services.ts +35 -7
- package/src/lib/plugins/openapi/playground/Playground.tsx +619 -602
- package/src/lib/plugins/openapi/playground/result-panel/ResultPanel.tsx +179 -106
- package/src/lib/plugins/openapi/playground/warmUpRetry.ts +43 -0
- package/src/vite/plugin-modules.ts +190 -188
|
@@ -1,106 +1,179 @@
|
|
|
1
|
-
import { Spinner } from "@lukoweb/apitogo/components";
|
|
2
|
-
import {
|
|
3
|
-
Alert,
|
|
4
|
-
AlertDescription,
|
|
5
|
-
AlertTitle,
|
|
6
|
-
} from "@lukoweb/apitogo/ui/Alert.js";
|
|
7
|
-
import type { UseMutationResult } from "@tanstack/react-query";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
/>
|
|
64
|
-
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
1
|
+
import { Spinner } from "@lukoweb/apitogo/components";
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
AlertDescription,
|
|
5
|
+
AlertTitle,
|
|
6
|
+
} from "@lukoweb/apitogo/ui/Alert.js";
|
|
7
|
+
import type { UseMutationResult } from "@tanstack/react-query";
|
|
8
|
+
import {
|
|
9
|
+
HourglassIcon,
|
|
10
|
+
KeyRoundIcon,
|
|
11
|
+
SendIcon,
|
|
12
|
+
UnplugIcon,
|
|
13
|
+
} from "lucide-react";
|
|
14
|
+
import { Button } from "../../../../ui/Button.js";
|
|
15
|
+
import { cn } from "../../../../util/cn.js";
|
|
16
|
+
import type { PlaygroundResult } from "../Playground.js";
|
|
17
|
+
import { WARM_UP_STATUSES } from "../warmUpRetry.js";
|
|
18
|
+
import ResponseStatusBar from "./ResponseStatusBar.js";
|
|
19
|
+
import { ResponseTab } from "./ResponseTab.js";
|
|
20
|
+
|
|
21
|
+
/** Where a signed-in developer gets an API key on an APItoGo portal with plans. */
|
|
22
|
+
const API_KEYS_PATH = "/account/credentials";
|
|
23
|
+
|
|
24
|
+
/** What a refused or failed answer means, and what to do about it. */
|
|
25
|
+
const ResultHint = ({
|
|
26
|
+
status,
|
|
27
|
+
hasIdentities,
|
|
28
|
+
}: {
|
|
29
|
+
status: number;
|
|
30
|
+
hasIdentities: boolean;
|
|
31
|
+
}) => {
|
|
32
|
+
if (status === 401 || status === 403)
|
|
33
|
+
return (
|
|
34
|
+
<Alert className="mx-4 mt-3 w-auto">
|
|
35
|
+
<KeyRoundIcon size={20} strokeWidth={1.5} />
|
|
36
|
+
<AlertTitle>This API needs an API key</AlertTitle>
|
|
37
|
+
<AlertDescription>
|
|
38
|
+
{hasIdentities ? (
|
|
39
|
+
<>
|
|
40
|
+
Choose your key under Authentication above and send the request
|
|
41
|
+
again. No key yet? Create one on the{" "}
|
|
42
|
+
<a href={API_KEYS_PATH} className="underline">
|
|
43
|
+
API keys page
|
|
44
|
+
</a>
|
|
45
|
+
: a key you create there is offered here in this browser.
|
|
46
|
+
</>
|
|
47
|
+
) : (
|
|
48
|
+
<>
|
|
49
|
+
Create a key on the{" "}
|
|
50
|
+
<a href={API_KEYS_PATH} className="underline">
|
|
51
|
+
API keys page
|
|
52
|
+
</a>{" "}
|
|
53
|
+
(sign in first if you have not). It is then offered under
|
|
54
|
+
Authentication here.
|
|
55
|
+
</>
|
|
56
|
+
)}
|
|
57
|
+
</AlertDescription>
|
|
58
|
+
</Alert>
|
|
59
|
+
);
|
|
60
|
+
if (WARM_UP_STATUSES.includes(status))
|
|
61
|
+
return (
|
|
62
|
+
<Alert className="mx-4 mt-3 w-auto">
|
|
63
|
+
<HourglassIcon size={20} strokeWidth={1.5} />
|
|
64
|
+
<AlertTitle>The API is still starting</AlertTitle>
|
|
65
|
+
<AlertDescription>
|
|
66
|
+
An API that was idle takes a few seconds to wake up. Send the request
|
|
67
|
+
again in a moment.
|
|
68
|
+
</AlertDescription>
|
|
69
|
+
</Alert>
|
|
70
|
+
);
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const ResultPanel = ({
|
|
75
|
+
queryMutation,
|
|
76
|
+
showLongRunningWarning,
|
|
77
|
+
isWarmingUp = false,
|
|
78
|
+
hasIdentities = false,
|
|
79
|
+
onCancel,
|
|
80
|
+
tip,
|
|
81
|
+
isFinished,
|
|
82
|
+
progress,
|
|
83
|
+
}: {
|
|
84
|
+
// biome-ignore lint/suspicious/noExplicitAny: Allow any type
|
|
85
|
+
queryMutation: UseMutationResult<PlaygroundResult, Error, any>;
|
|
86
|
+
showLongRunningWarning?: boolean;
|
|
87
|
+
/** The API said it is still starting, and the request is sent again in a moment. */
|
|
88
|
+
isWarmingUp?: boolean;
|
|
89
|
+
/** The playground offers at least one identity (a key or the signed-in user) under Authentication. */
|
|
90
|
+
hasIdentities?: boolean;
|
|
91
|
+
onCancel?: () => void;
|
|
92
|
+
isFinished: boolean;
|
|
93
|
+
progress: number;
|
|
94
|
+
tip?: React.ReactNode;
|
|
95
|
+
}) => {
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex flex-col overflow-y-auto h-[80vh] bg-muted/50">
|
|
98
|
+
{(queryMutation.isPending || queryMutation.data) && (
|
|
99
|
+
<ResponseStatusBar
|
|
100
|
+
status={queryMutation.data?.status}
|
|
101
|
+
time={queryMutation.data?.time}
|
|
102
|
+
size={queryMutation.data?.size}
|
|
103
|
+
isFinished={isFinished}
|
|
104
|
+
progress={progress}
|
|
105
|
+
/>
|
|
106
|
+
)}
|
|
107
|
+
{queryMutation.error ? (
|
|
108
|
+
<div className="max-w-2/3 mx-auto mt-20">
|
|
109
|
+
<Alert>
|
|
110
|
+
<UnplugIcon size={24} strokeWidth={1.5} className="me-5" />
|
|
111
|
+
<AlertTitle>Request failed</AlertTitle>
|
|
112
|
+
<AlertDescription>
|
|
113
|
+
{queryMutation.error.message ||
|
|
114
|
+
String(queryMutation.error) ||
|
|
115
|
+
"Unexpected error"}
|
|
116
|
+
</AlertDescription>
|
|
117
|
+
</Alert>
|
|
118
|
+
</div>
|
|
119
|
+
) : queryMutation.data ? (
|
|
120
|
+
<>
|
|
121
|
+
<ResultHint
|
|
122
|
+
status={queryMutation.data.status}
|
|
123
|
+
hasIdentities={hasIdentities}
|
|
124
|
+
/>
|
|
125
|
+
<ResponseTab
|
|
126
|
+
request={queryMutation.data.request}
|
|
127
|
+
size={queryMutation.data.size}
|
|
128
|
+
headers={queryMutation.data.headers}
|
|
129
|
+
body={queryMutation.data.body}
|
|
130
|
+
isBinary={queryMutation.data.isBinary}
|
|
131
|
+
fileName={queryMutation.data.fileName}
|
|
132
|
+
blob={queryMutation.data.blob}
|
|
133
|
+
/>
|
|
134
|
+
</>
|
|
135
|
+
) : queryMutation.isPending ? (
|
|
136
|
+
<div className="grid place-items-center h-full">
|
|
137
|
+
<div className="flex flex-col gap-2 items-center mt-20">
|
|
138
|
+
<Spinner />
|
|
139
|
+
<div
|
|
140
|
+
className={cn(
|
|
141
|
+
"opacity-0 pointer-events-none transition-opacity h-20 text-sm text-muted-foreground duration-300 flex flex-col gap-2 items-center",
|
|
142
|
+
showLongRunningWarning && "opacity-100 pointer-events-auto",
|
|
143
|
+
)}
|
|
144
|
+
>
|
|
145
|
+
{isWarmingUp
|
|
146
|
+
? "The API is waking up. Trying again in a few seconds…"
|
|
147
|
+
: "Looks like the request is taking longer than expected."}
|
|
148
|
+
<Button
|
|
149
|
+
type="button"
|
|
150
|
+
onClick={onCancel}
|
|
151
|
+
size="sm"
|
|
152
|
+
className="w-fit"
|
|
153
|
+
variant="outline"
|
|
154
|
+
>
|
|
155
|
+
Cancel
|
|
156
|
+
</Button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
) : (
|
|
161
|
+
<div className="h-full grid place-items-center">
|
|
162
|
+
<div className="flex flex-col gap-4 items-center">
|
|
163
|
+
<SendIcon
|
|
164
|
+
size={64}
|
|
165
|
+
className="text-muted-foreground"
|
|
166
|
+
strokeWidth={1.2}
|
|
167
|
+
/>
|
|
168
|
+
<span className="text-[16px] font-semibold text-muted-foreground">
|
|
169
|
+
Send your first request
|
|
170
|
+
</span>
|
|
171
|
+
{tip}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export default ResultPanel;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** What a gateway answers while the API behind it is still starting: it is not ready yet, or it did not answer in time. */
|
|
2
|
+
export const WARM_UP_STATUSES = [502, 503, 504];
|
|
3
|
+
|
|
4
|
+
const WARM_UP_RETRY_DELAY_MS = 5000;
|
|
5
|
+
|
|
6
|
+
const wait = (ms: number, signal?: AbortSignal) =>
|
|
7
|
+
new Promise<void>((resolve, reject) => {
|
|
8
|
+
const timer = setTimeout(resolve, ms);
|
|
9
|
+
signal?.addEventListener("abort", () => {
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
reject(signal.reason);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sends the request, and once more after a short wait when the answer says the API is still starting. A hosted API
|
|
17
|
+
* that slept goes on answering 502/504 (or, behind an older gateway, a response the browser reports as a network
|
|
18
|
+
* error) until it is up, which takes a few seconds. A cancelled request is never retried.
|
|
19
|
+
*/
|
|
20
|
+
export async function fetchWithWarmUpRetry(
|
|
21
|
+
request: Request,
|
|
22
|
+
init: RequestInit,
|
|
23
|
+
options: { delayMs?: number; onRetry?: () => void } = {},
|
|
24
|
+
): Promise<Response> {
|
|
25
|
+
const retry = async () => {
|
|
26
|
+
options.onRetry?.();
|
|
27
|
+
await wait(
|
|
28
|
+
options.delayMs ?? WARM_UP_RETRY_DELAY_MS,
|
|
29
|
+
init.signal ?? undefined,
|
|
30
|
+
);
|
|
31
|
+
return fetch(request, init);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// A request body can be read once, so the first try sends a copy.
|
|
35
|
+
let response: Response;
|
|
36
|
+
try {
|
|
37
|
+
response = await fetch(request.clone(), init);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (init.signal?.aborted || !(error instanceof TypeError)) throw error;
|
|
40
|
+
return retry();
|
|
41
|
+
}
|
|
42
|
+
return WARM_UP_STATUSES.includes(response.status) ? retry() : response;
|
|
43
|
+
}
|