@rulebricks/cli 2.0.0 → 2.0.1
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/README.md +3 -3
- package/dist/commands/benchmark.d.ts +11 -0
- package/dist/commands/benchmark.js +173 -0
- package/dist/commands/deploy.js +3 -3
- package/dist/commands/destroy.js +2 -2
- package/dist/commands/logs.js +1 -0
- package/dist/components/Wizard/steps/BenchmarkSteps.d.ts +31 -0
- package/dist/components/Wizard/steps/BenchmarkSteps.js +304 -0
- package/dist/components/Wizard/steps/DatabaseStep.js +49 -35
- package/dist/index.js +42 -6
- package/dist/lib/benchmark.d.ts +63 -0
- package/dist/lib/benchmark.js +466 -0
- package/dist/lib/dns.d.ts +3 -1
- package/dist/lib/dns.js +138 -56
- package/dist/lib/helm.d.ts +14 -1
- package/dist/lib/helm.js +36 -1
- package/dist/lib/kubernetes.js +2 -0
- package/dist/types/index.d.ts +90 -0
- package/dist/types/index.js +51 -0
- package/package.json +5 -5
- package/terraform/aws/main.tf +22 -0
- package/terraform/azure/main.tf +45 -0
- package/terraform/gcp/main.tf +34 -0
- /package/{email-templates → templates}/email_change.html +0 -0
- /package/{email-templates → templates}/invite.html +0 -0
- /package/{email-templates → templates}/password_change.html +0 -0
- /package/{email-templates → templates}/verify.html +0 -0
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ rulebricks init
|
|
|
45
45
|
rulebricks deploy my-deployment
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
## Commands
|
|
48
|
+
## Main Commands
|
|
49
49
|
|
|
50
50
|
| Command | Description |
|
|
51
51
|
| --------------------------- | -------------------------------------- |
|
|
@@ -57,10 +57,10 @@ rulebricks deploy my-deployment
|
|
|
57
57
|
| `rulebricks logs [name]` | Inspect services |
|
|
58
58
|
| `rulebricks open [name]` | Open the generated configuration files |
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
Use `rulebricks -h` to explore all commands, and add `-h` to any command to learn more about a particular command's options.
|
|
61
61
|
|
|
62
62
|
## Notes
|
|
63
63
|
|
|
64
64
|
There are a uniquely wide variety of customization options this CLI makes available (multi-cloud, hybrid vs. self-hosted database deployment, custom email templates, etc.), and not all combinations have been validated.
|
|
65
65
|
|
|
66
|
-
If you encounter any issue deploying your private Rulebricks cluster, please [email us](mailto:support@rulebricks.com) or [open an issue](https://github.com/rulebricks/cli/issues) and we will follow up promptly.
|
|
66
|
+
If you encounter any issue deploying your private Rulebricks cluster, please [email us](mailto:support@rulebricks.com) or [open an issue](https://github.com/rulebricks/cli/issues) and we will follow up promptly. If you are particularly familiar with helm/k8s, you are also free to review generated values.yaml files and reconcile them with our [Helm chart](https://github.com/rulebricks/helm).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark Command
|
|
3
|
+
*
|
|
4
|
+
* Interactive wizard for configuring and running k6 load tests
|
|
5
|
+
* against Rulebricks deployments.
|
|
6
|
+
*/
|
|
7
|
+
interface BenchmarkCommandProps {
|
|
8
|
+
name?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function BenchmarkCommand(props: BenchmarkCommandProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Benchmark Command
|
|
4
|
+
*
|
|
5
|
+
* Interactive wizard for configuring and running k6 load tests
|
|
6
|
+
* against Rulebricks deployments.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, useEffect, useCallback } from "react";
|
|
9
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
10
|
+
import { BorderBox, Spinner, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
|
|
11
|
+
import { DeploymentSelectStep, ApiKeyStep, FlowSlugStep, TestModeStep, PresetsStep, ReviewStep, createInitialBenchmarkState, } from "../components/Wizard/steps/BenchmarkSteps.js";
|
|
12
|
+
import { isK6Installed, getK6InstallInstructions, runBenchmark, buildApiUrl, openInBrowser, formatDuration, } from "../lib/benchmark.js";
|
|
13
|
+
function BenchmarkCommandInner({ name }) {
|
|
14
|
+
const { exit } = useApp();
|
|
15
|
+
const { colors } = useTheme();
|
|
16
|
+
const [step, setStep] = useState("preflight");
|
|
17
|
+
const [wizardState, setWizardState] = useState(createInitialBenchmarkState());
|
|
18
|
+
const [error, setError] = useState(null);
|
|
19
|
+
const [k6Output, setK6Output] = useState([]);
|
|
20
|
+
const [result, setResult] = useState(null);
|
|
21
|
+
// Preflight check for k6
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (step !== "preflight")
|
|
24
|
+
return;
|
|
25
|
+
(async () => {
|
|
26
|
+
const installed = await isK6Installed();
|
|
27
|
+
if (!installed) {
|
|
28
|
+
setError(`k6 is not installed.\n\n${getK6InstallInstructions()}\n\nVisit https://k6.io/docs/get-started/installation/ for more options.`);
|
|
29
|
+
setStep("error");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// If a deployment name was provided via CLI, skip the selection step
|
|
33
|
+
if (name) {
|
|
34
|
+
setWizardState((s) => ({ ...s, deploymentName: name }));
|
|
35
|
+
// We still need to load the deployment URL, which the DeploymentSelectStep does
|
|
36
|
+
// So we'll go to that step anyway to validate and load the URL
|
|
37
|
+
}
|
|
38
|
+
setStep("select-deployment");
|
|
39
|
+
})();
|
|
40
|
+
}, [step, name]);
|
|
41
|
+
// Handle wizard step completion
|
|
42
|
+
const handleStepComplete = useCallback((data) => {
|
|
43
|
+
setWizardState((s) => ({ ...s, ...data }));
|
|
44
|
+
// Progress to next step
|
|
45
|
+
switch (step) {
|
|
46
|
+
case "select-deployment":
|
|
47
|
+
setStep("api-key");
|
|
48
|
+
break;
|
|
49
|
+
case "api-key":
|
|
50
|
+
setStep("flow-slug");
|
|
51
|
+
break;
|
|
52
|
+
case "flow-slug":
|
|
53
|
+
setStep("test-mode");
|
|
54
|
+
break;
|
|
55
|
+
case "test-mode":
|
|
56
|
+
setStep("presets");
|
|
57
|
+
break;
|
|
58
|
+
case "presets":
|
|
59
|
+
setStep("review");
|
|
60
|
+
break;
|
|
61
|
+
case "review":
|
|
62
|
+
setStep("running");
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}, [step]);
|
|
66
|
+
// Handle going back
|
|
67
|
+
const handleBack = useCallback(() => {
|
|
68
|
+
switch (step) {
|
|
69
|
+
case "select-deployment":
|
|
70
|
+
exit();
|
|
71
|
+
break;
|
|
72
|
+
case "api-key":
|
|
73
|
+
setStep("select-deployment");
|
|
74
|
+
break;
|
|
75
|
+
case "flow-slug":
|
|
76
|
+
setStep("api-key");
|
|
77
|
+
break;
|
|
78
|
+
case "test-mode":
|
|
79
|
+
setStep("flow-slug");
|
|
80
|
+
break;
|
|
81
|
+
case "presets":
|
|
82
|
+
setStep("test-mode");
|
|
83
|
+
break;
|
|
84
|
+
case "review":
|
|
85
|
+
setStep("presets");
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}, [step, exit]);
|
|
89
|
+
// Run the benchmark
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (step !== "running")
|
|
92
|
+
return;
|
|
93
|
+
(async () => {
|
|
94
|
+
const config = {
|
|
95
|
+
deploymentName: wizardState.deploymentName,
|
|
96
|
+
apiUrl: buildApiUrl(wizardState.deploymentUrl, wizardState.flowSlug),
|
|
97
|
+
apiKey: wizardState.apiKey,
|
|
98
|
+
testMode: wizardState.testMode,
|
|
99
|
+
testDuration: wizardState.testDuration,
|
|
100
|
+
targetRps: wizardState.targetRps,
|
|
101
|
+
bulkSize: wizardState.testMode === "throughput"
|
|
102
|
+
? wizardState.bulkSize
|
|
103
|
+
: undefined,
|
|
104
|
+
};
|
|
105
|
+
const benchmarkResult = await runBenchmark(config, {
|
|
106
|
+
onOutput: (line) => {
|
|
107
|
+
setK6Output((prev) => {
|
|
108
|
+
// Keep only last 15 lines to avoid memory issues
|
|
109
|
+
const newOutput = [...prev, line];
|
|
110
|
+
if (newOutput.length > 15) {
|
|
111
|
+
return newOutput.slice(-15);
|
|
112
|
+
}
|
|
113
|
+
return newOutput;
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
setResult(benchmarkResult);
|
|
118
|
+
if (benchmarkResult.success) {
|
|
119
|
+
setStep("complete");
|
|
120
|
+
// Try to open report in browser
|
|
121
|
+
try {
|
|
122
|
+
await openInBrowser(benchmarkResult.reportPath);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Ignore browser open errors
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
setError(benchmarkResult.error || "Benchmark failed");
|
|
130
|
+
setStep("error");
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
}, [step, wizardState]);
|
|
134
|
+
// Handle key input for error/complete screens
|
|
135
|
+
useInput((input, key) => {
|
|
136
|
+
if (key.escape && (step === "error" || step === "complete")) {
|
|
137
|
+
exit();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Render preflight check
|
|
141
|
+
if (step === "preflight") {
|
|
142
|
+
return (_jsx(BorderBox, { title: "Benchmark", children: _jsx(Box, { marginY: 1, children: _jsx(Spinner, { label: "Checking prerequisites..." }) }) }));
|
|
143
|
+
}
|
|
144
|
+
// Render error screen
|
|
145
|
+
if (step === "error") {
|
|
146
|
+
return (_jsx(BorderBox, { title: "Benchmark Failed", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.error, bold: true, children: "Error" }), _jsx(Text, { color: colors.error, children: error }), result?.outputDir && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Output directory: ", result.outputDir] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Press Esc to exit" }) })] }) }));
|
|
147
|
+
}
|
|
148
|
+
// Render running screen
|
|
149
|
+
if (step === "running") {
|
|
150
|
+
const expectedThroughput = wizardState.testMode === "throughput"
|
|
151
|
+
? wizardState.targetRps * wizardState.bulkSize
|
|
152
|
+
: wizardState.targetRps;
|
|
153
|
+
return (_jsx(BorderBox, { title: "Running Benchmark", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Spinner, { label: `Running ${wizardState.testMode.toUpperCase()} test against ${wizardState.deploymentName}...` }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.muted, children: ["Target: ", expectedThroughput.toLocaleString(), " ", wizardState.testMode === "throughput" ? "solutions" : "requests", "/sec"] }), _jsxs(Text, { color: colors.muted, children: ["Duration: 1m warm-up + ", formatDuration(wizardState.testDuration)] })] }), k6Output.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: k6Output.map((line, i) => (_jsx(Text, { color: colors.muted, dimColor: true, children: line.length > 80 ? line.slice(0, 77) + "..." : line }, i))) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, dimColor: true, children: "This may take several minutes. Please wait..." }) })] }) }));
|
|
154
|
+
}
|
|
155
|
+
// Render complete screen
|
|
156
|
+
if (step === "complete" && result) {
|
|
157
|
+
const metrics = result.metrics;
|
|
158
|
+
return (_jsx(BorderBox, { title: "Benchmark Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "Benchmark completed successfully!" }), metrics && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Results Summary:" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Success Rate: " }), _jsxs(Text, { color: metrics.successRate >= 99
|
|
159
|
+
? colors.success
|
|
160
|
+
: metrics.successRate >= 95
|
|
161
|
+
? colors.warning
|
|
162
|
+
: colors.error, bold: true, children: [metrics.successRate.toFixed(1), "%"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Actual RPS: " }), _jsx(Text, { color: colors.accent, bold: true, children: metrics.actualRps.toFixed(1) })] }), metrics.actualThroughput && (_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Throughput: " }), _jsxs(Text, { color: colors.accent, bold: true, children: [metrics.actualThroughput.toFixed(0), " solutions/sec"] })] })), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "P95 Latency: " }), _jsxs(Text, { color: metrics.p95Latency < 200
|
|
163
|
+
? colors.success
|
|
164
|
+
: metrics.p95Latency < 500
|
|
165
|
+
? colors.warning
|
|
166
|
+
: colors.error, children: [metrics.p95Latency.toFixed(0), "ms"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "P99 Latency: " }), _jsxs(Text, { children: [metrics.p99Latency.toFixed(0), "ms"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Total Requests: " }), _jsx(Text, { children: metrics.totalRequests.toLocaleString() })] })] })] })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: colors.muted, children: "Results saved to:" }), _jsx(Text, { color: colors.accent, children: result.outputDir })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, children: ["Report: ", result.reportPath.split("/").pop()] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "The HTML report should open in your browser automatically." }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Press Esc to exit" }) })] }) }));
|
|
167
|
+
}
|
|
168
|
+
// Render wizard steps
|
|
169
|
+
return (_jsxs(_Fragment, { children: [step === "select-deployment" && (_jsx(DeploymentSelectStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "api-key" && (_jsx(ApiKeyStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "flow-slug" && (_jsx(FlowSlugStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "test-mode" && (_jsx(TestModeStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "presets" && (_jsx(PresetsStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "review" && (_jsx(ReviewStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState }))] }));
|
|
170
|
+
}
|
|
171
|
+
export function BenchmarkCommand(props) {
|
|
172
|
+
return (_jsxs(ThemeProvider, { theme: "status", children: [_jsx(Logo, {}), _jsx(BenchmarkCommandInner, { ...props })] }));
|
|
173
|
+
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -5,7 +5,7 @@ import { BorderBox, Spinner, StatusLine, ThemeProvider, useTheme, Logo, } from "
|
|
|
5
5
|
import { DNSWaitScreen } from "../components/DNSWaitScreen.js";
|
|
6
6
|
import { loadDeploymentConfig, loadDeploymentState, saveDeploymentState, updateDeploymentStatus, } from "../lib/config.js";
|
|
7
7
|
import { setupTerraformWorkspace, terraformInit, terraformPlan, terraformApply, terraformDestroy, updateKubeconfig, hasTerraformState, isTerraformInstalled, } from "../lib/terraform.js";
|
|
8
|
-
import {
|
|
8
|
+
import { installOrUpgradeChart, upgradeChart, isHelmInstalled, } from "../lib/helm.js";
|
|
9
9
|
import { isKubectlInstalled, checkClusterAccessible, } from "../lib/kubernetes.js";
|
|
10
10
|
import { generateHelmValues, updateHelmValuesForTLS, } from "../lib/helmValues.js";
|
|
11
11
|
import { isSupportedDnsProvider, getNamespace, getReleaseName, } from "../types/index.js";
|
|
@@ -194,7 +194,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
|
|
|
194
194
|
// SINGLE-PHASE DEPLOYMENT (External DNS)
|
|
195
195
|
// Install with TLS enabled from the start - external-dns handles DNS records
|
|
196
196
|
await generateHelmValues(cfg, { tlsEnabled: true });
|
|
197
|
-
await
|
|
197
|
+
await installOrUpgradeChart(name, {
|
|
198
198
|
releaseName,
|
|
199
199
|
namespace,
|
|
200
200
|
version,
|
|
@@ -223,7 +223,7 @@ function DeployCommandInner({ name, skipInfra, skipDns, version, }) {
|
|
|
223
223
|
// TWO-PHASE DEPLOYMENT (Manual DNS)
|
|
224
224
|
// Phase 1: Install without TLS
|
|
225
225
|
await generateHelmValues(cfg, { tlsEnabled: false });
|
|
226
|
-
await
|
|
226
|
+
await installOrUpgradeChart(name, {
|
|
227
227
|
releaseName,
|
|
228
228
|
namespace,
|
|
229
229
|
version,
|
package/dist/commands/destroy.js
CHANGED
|
@@ -194,7 +194,7 @@ function DestroyCommandInner({ name, cluster, config, force, }) {
|
|
|
194
194
|
setError(err instanceof Error ? err.message : "Destruction failed");
|
|
195
195
|
setStep("error");
|
|
196
196
|
}
|
|
197
|
-
}, [name, cluster, exit]);
|
|
197
|
+
}, [name, cluster, config, exit]);
|
|
198
198
|
// Loading screen
|
|
199
199
|
if (step === "loading") {
|
|
200
200
|
return (_jsx(BorderBox, { title: `Destroying ${name}`, children: _jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Spinner, { label: "Checking deployment state..." }) }) }));
|
|
@@ -243,7 +243,7 @@ function DestroyCommandInner({ name, cluster, config, force, }) {
|
|
|
243
243
|
// Only cleaning local files (with --config)
|
|
244
244
|
_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.warning, bold: true, children: "\u2139 Local Cleanup" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { children: "No cluster resources found to clean up." }), _jsx(Text, { children: "This will delete local configuration files." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, children: "Press Enter to confirm, Esc to cancel" }) })] })) : (
|
|
245
245
|
// Full destruction
|
|
246
|
-
_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, bold: true, children: "\u26A0 WARNING" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "This will permanently delete:" }), (scope?.hasHelmRelease || scope?.hasNamespace) && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.muted, children: " \u2022 Rulebricks application" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 All databases and stored data"] }), _jsx(Text, { color: colors.muted, children: " \u2022 All persistent volumes" }), _jsx(Text, { color: colors.muted, children: " \u2022 Monitoring stack" }), _jsx(Text, { color: colors.muted, children: " \u2022 Kubernetes namespace" })] })), needsInfraConfirm && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, children: " \u2022 Kubernetes cluster" }), _jsx(Text, { color: colors.accent, children: " \u2022 All cloud infrastructure" })] })), willDeleteConfig && (_jsx(Text, { color: colors.muted, children: " \u2022 Local configuration files" })), !
|
|
246
|
+
_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, bold: true, children: "\u26A0 WARNING" }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "This will permanently delete:" }), (scope?.hasHelmRelease || scope?.hasNamespace) && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.muted, children: " \u2022 Rulebricks application" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 All databases and stored data"] }), _jsx(Text, { color: colors.muted, children: " \u2022 All persistent volumes" }), _jsx(Text, { color: colors.muted, children: " \u2022 Monitoring stack" }), _jsx(Text, { color: colors.muted, children: " \u2022 Kubernetes namespace" })] })), needsInfraConfirm && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.accent, children: " \u2022 Kubernetes cluster" }), _jsx(Text, { color: colors.accent, children: " \u2022 All cloud infrastructure" })] })), willDeleteConfig && (_jsx(Text, { color: colors.muted, children: " \u2022 Local configuration files" })), !cluster && scope?.hasInfrastructure && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Cloud infrastructure will be preserved. Use --cluster to remove it." }) })), cluster && !scope?.hasInfrastructure && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "No CLI managed infrastructure found for this deployment." }) })), !willDeleteConfig && (_jsx(Box, { marginTop: !needsInfraConfirm && !cluster ? 0 : 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Local config files will be preserved. Use --config to remove them." }) })), !scope?.clusterAccessible && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, dimColor: true, children: "\u26A0 Cluster is not accessible. Cluster resources may need manual cleanup." }) }))] }), needsInfraConfirm ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Type", " ", _jsx(Text, { color: colors.accent, bold: true, children: "destroy-all" }), " ", "to confirm:"] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(Text, { children: confirmText }), _jsx(Text, { color: colors.muted, children: "\u2588" })] })] })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, children: "Press Enter to confirm, Esc to cancel" }) }))] })) }) }));
|
|
247
247
|
}
|
|
248
248
|
export function DestroyCommand(props) {
|
|
249
249
|
return (_jsxs(ThemeProvider, { theme: "destroy", children: [_jsx(Logo, {}), _jsx(DestroyCommandInner, { ...props })] }));
|
package/dist/commands/logs.js
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark Wizard Steps
|
|
3
|
+
*
|
|
4
|
+
* These components provide the interactive wizard flow for configuring
|
|
5
|
+
* and running benchmark tests against Rulebricks deployments.
|
|
6
|
+
*/
|
|
7
|
+
import { BenchmarkTestMode, BenchmarkPreset } from "../../../types/index.js";
|
|
8
|
+
export interface BenchmarkWizardState {
|
|
9
|
+
deploymentName: string;
|
|
10
|
+
deploymentUrl: string;
|
|
11
|
+
apiKey: string;
|
|
12
|
+
flowSlug: string;
|
|
13
|
+
testMode: BenchmarkTestMode;
|
|
14
|
+
preset: BenchmarkPreset;
|
|
15
|
+
targetRps: number;
|
|
16
|
+
testDuration: string;
|
|
17
|
+
bulkSize: number;
|
|
18
|
+
}
|
|
19
|
+
interface StepProps {
|
|
20
|
+
onComplete: (data: Partial<BenchmarkWizardState>) => void;
|
|
21
|
+
onBack: () => void;
|
|
22
|
+
state: BenchmarkWizardState;
|
|
23
|
+
}
|
|
24
|
+
export declare function DeploymentSelectStep({ onComplete, onBack, state }: StepProps): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export declare function ApiKeyStep({ onComplete, onBack, state }: StepProps): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export declare function FlowSlugStep({ onComplete, onBack, state }: StepProps): import("react/jsx-runtime").JSX.Element;
|
|
27
|
+
export declare function TestModeStep({ onComplete, onBack, state }: StepProps): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export declare function PresetsStep({ onComplete, onBack, state }: StepProps): import("react/jsx-runtime").JSX.Element;
|
|
29
|
+
export declare function ReviewStep({ onComplete, onBack, state }: StepProps): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
export declare function createInitialBenchmarkState(): BenchmarkWizardState;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Benchmark Wizard Steps
|
|
4
|
+
*
|
|
5
|
+
* These components provide the interactive wizard flow for configuring
|
|
6
|
+
* and running benchmark tests against Rulebricks deployments.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, useEffect } from "react";
|
|
9
|
+
import { Box, Text, useInput } from "ink";
|
|
10
|
+
import SelectInput from "ink-select-input";
|
|
11
|
+
import TextInput from "ink-text-input";
|
|
12
|
+
import { BorderBox, useTheme, Spinner } from "../../common/index.js";
|
|
13
|
+
import { QPS_PRESETS, THROUGHPUT_PRESETS, } from "../../../types/index.js";
|
|
14
|
+
import { listDeployments, loadDeploymentState } from "../../../lib/config.js";
|
|
15
|
+
import { buildApiUrl, checkDeploymentHealth } from "../../../lib/benchmark.js";
|
|
16
|
+
export function DeploymentSelectStep({ onComplete, onBack, state }) {
|
|
17
|
+
const { colors } = useTheme();
|
|
18
|
+
const [deployments, setDeployments] = useState([]);
|
|
19
|
+
const [loading, setLoading] = useState(true);
|
|
20
|
+
const [loadingStatus, setLoadingStatus] = useState("Loading deployments...");
|
|
21
|
+
const [error, setError] = useState(null);
|
|
22
|
+
useInput((input, key) => {
|
|
23
|
+
if (key.escape) {
|
|
24
|
+
onBack();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
(async () => {
|
|
29
|
+
try {
|
|
30
|
+
const names = await listDeployments();
|
|
31
|
+
const candidates = [];
|
|
32
|
+
// First, collect all deployments that have a URL
|
|
33
|
+
for (const name of names) {
|
|
34
|
+
try {
|
|
35
|
+
const deploymentState = await loadDeploymentState(name);
|
|
36
|
+
if (deploymentState?.application?.url) {
|
|
37
|
+
candidates.push({
|
|
38
|
+
name,
|
|
39
|
+
url: deploymentState.application.url,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Skip deployments without state
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (candidates.length === 0) {
|
|
48
|
+
setError("No deployments found. Deploy a Rulebricks instance first with 'rulebricks deploy'.");
|
|
49
|
+
setLoading(false);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Now check health of each candidate
|
|
53
|
+
setLoadingStatus(`Checking health of ${candidates.length} deployment(s)...`);
|
|
54
|
+
const healthyDeployments = [];
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
setLoadingStatus(`Checking ${candidate.name}...`);
|
|
57
|
+
const isHealthy = await checkDeploymentHealth(candidate.url);
|
|
58
|
+
if (isHealthy) {
|
|
59
|
+
healthyDeployments.push({
|
|
60
|
+
name: candidate.name,
|
|
61
|
+
url: candidate.url,
|
|
62
|
+
healthy: true,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
setDeployments(healthyDeployments);
|
|
67
|
+
if (healthyDeployments.length === 0) {
|
|
68
|
+
setError("No healthy deployments found.\n\nAll configured deployments failed the health check (/api/health).\nMake sure your deployment is running and accessible.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
setError("Failed to load deployments");
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
setLoading(false);
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
}, []);
|
|
79
|
+
if (loading) {
|
|
80
|
+
return (_jsx(BorderBox, { title: "Select Deployment", children: _jsx(Box, { marginY: 1, children: _jsx(Spinner, { label: loadingStatus }) }) }));
|
|
81
|
+
}
|
|
82
|
+
if (error) {
|
|
83
|
+
return (_jsx(BorderBox, { title: "Select Deployment", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.error, children: error }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back" }) })] }) }));
|
|
84
|
+
}
|
|
85
|
+
const items = deployments.map((d) => ({
|
|
86
|
+
label: d.name,
|
|
87
|
+
value: d.name,
|
|
88
|
+
url: d.url,
|
|
89
|
+
}));
|
|
90
|
+
const handleSelect = (item) => {
|
|
91
|
+
const deployment = deployments.find((d) => d.name === item.value);
|
|
92
|
+
onComplete({
|
|
93
|
+
deploymentName: item.value,
|
|
94
|
+
deploymentUrl: deployment?.url || "",
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
return (_jsxs(BorderBox, { title: "Select Deployment", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Choose a deployment to benchmark:" }), _jsx(Text, { color: colors.muted, dimColor: true, children: "Only healthy, accessible deployments are shown" })] }), _jsx(SelectInput, { items: items, onSelect: handleSelect, indicatorComponent: () => null, itemComponent: ({ isSelected, label }) => {
|
|
98
|
+
const deployment = deployments.find((d) => d.name === label);
|
|
99
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: isSelected ? 1 : 0, children: [_jsxs(Text, { color: isSelected ? colors.accent : undefined, bold: isSelected, children: [isSelected ? "❯ " : " ", label] }), isSelected && deployment?.url && (_jsxs(Text, { color: colors.muted, dimColor: true, children: [" ", deployment.url] }))] }));
|
|
100
|
+
} }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back \u2022 Enter to select" }) })] }));
|
|
101
|
+
}
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Step 2: API Key Input
|
|
104
|
+
// ============================================================================
|
|
105
|
+
export function ApiKeyStep({ onComplete, onBack, state }) {
|
|
106
|
+
const { colors } = useTheme();
|
|
107
|
+
const [apiKey, setApiKey] = useState(state.apiKey || "");
|
|
108
|
+
useInput((input, key) => {
|
|
109
|
+
if (key.escape) {
|
|
110
|
+
onBack();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const handleSubmit = () => {
|
|
114
|
+
if (!apiKey.trim())
|
|
115
|
+
return;
|
|
116
|
+
onComplete({ apiKey: apiKey.trim() });
|
|
117
|
+
};
|
|
118
|
+
return (_jsxs(BorderBox, { title: "API Key", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter your Rulebricks API key:" }), _jsx(Text, { color: colors.muted, dimColor: true, children: "This key is used to authenticate benchmark requests" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(TextInput, { value: apiKey, onChange: setApiKey, onSubmit: handleSubmit, placeholder: "Enter your API key", mask: "*" })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back \u2022 Enter to continue" }) })] }));
|
|
119
|
+
}
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Step 3: Flow Slug Input
|
|
122
|
+
// ============================================================================
|
|
123
|
+
export function FlowSlugStep({ onComplete, onBack, state }) {
|
|
124
|
+
const { colors } = useTheme();
|
|
125
|
+
const [flowSlug, setFlowSlug] = useState(state.flowSlug || "");
|
|
126
|
+
useInput((input, key) => {
|
|
127
|
+
if (key.escape) {
|
|
128
|
+
onBack();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const handleSubmit = () => {
|
|
132
|
+
if (!flowSlug.trim())
|
|
133
|
+
return;
|
|
134
|
+
onComplete({ flowSlug: flowSlug.trim() });
|
|
135
|
+
};
|
|
136
|
+
return (_jsxs(BorderBox, { title: "Benchmarking Flow", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Enter the slug of your benchmarking flow:" }), _jsx(Text, { color: colors.muted, dimColor: true, children: "Create a flow in Rulebricks using the \"Benchmarking Flow\" template, then find its slug (ex: \"oryoRqvOV1\")" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Expected payload schema:" }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 req_id: string (auto-generated)"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 alpha: number (0-100)"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 beta: string"] }), _jsxs(Text, { color: colors.muted, children: [" ", "\u2022 charlie: boolean"] })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.accent, children: "\u276F " }), _jsx(TextInput, { value: flowSlug, onChange: setFlowSlug, onSubmit: handleSubmit, placeholder: "e.g., benchmark-flow" })] }), state.deploymentUrl && flowSlug && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Will test: ", buildApiUrl(state.deploymentUrl, flowSlug)] }) }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back \u2022 Enter to continue" }) })] }));
|
|
137
|
+
}
|
|
138
|
+
// ============================================================================
|
|
139
|
+
// Step 4: Test Mode Selection
|
|
140
|
+
// ============================================================================
|
|
141
|
+
export function TestModeStep({ onComplete, onBack, state }) {
|
|
142
|
+
const { colors } = useTheme();
|
|
143
|
+
useInput((input, key) => {
|
|
144
|
+
if (key.escape) {
|
|
145
|
+
onBack();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
const items = [
|
|
149
|
+
{
|
|
150
|
+
label: "QPS Test",
|
|
151
|
+
value: "qps",
|
|
152
|
+
description: "Measures requests per second - tests API responsiveness",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
label: "Throughput Test",
|
|
156
|
+
value: "throughput",
|
|
157
|
+
description: "Measures solutions per second with bulk requests - tests engine capacity",
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
const handleSelect = (item) => {
|
|
161
|
+
// Set default presets based on mode
|
|
162
|
+
const defaults = item.value === "qps"
|
|
163
|
+
? {
|
|
164
|
+
targetRps: QPS_PRESETS.medium.targetRps,
|
|
165
|
+
testDuration: QPS_PRESETS.medium.testDuration,
|
|
166
|
+
}
|
|
167
|
+
: {
|
|
168
|
+
targetRps: THROUGHPUT_PRESETS.medium.targetRps,
|
|
169
|
+
testDuration: THROUGHPUT_PRESETS.medium.testDuration,
|
|
170
|
+
bulkSize: THROUGHPUT_PRESETS.medium.bulkSize,
|
|
171
|
+
};
|
|
172
|
+
onComplete({
|
|
173
|
+
testMode: item.value,
|
|
174
|
+
preset: "medium",
|
|
175
|
+
...defaults,
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
return (_jsxs(BorderBox, { title: "Test Mode", children: [_jsx(Box, { flexDirection: "column", marginY: 1, children: _jsx(Text, { children: "Select the type of benchmark to run:" }) }), _jsx(SelectInput, { items: items, onSelect: handleSelect, itemComponent: ({ isSelected, label }) => {
|
|
179
|
+
const item = items.find((i) => i.label === label);
|
|
180
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: isSelected ? 1 : 0, children: [_jsxs(Text, { color: isSelected ? colors.accent : undefined, bold: isSelected, children: [isSelected ? "❯ " : " ", label] }), isSelected && item && (_jsxs(Text, { color: colors.muted, dimColor: true, children: [" ", item.description] }))] }));
|
|
181
|
+
} }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back \u2022 Enter to select" }) })] }));
|
|
182
|
+
}
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Step 5: Presets Selection
|
|
185
|
+
// ============================================================================
|
|
186
|
+
export function PresetsStep({ onComplete, onBack, state }) {
|
|
187
|
+
const { colors } = useTheme();
|
|
188
|
+
const [customMode, setCustomMode] = useState(false);
|
|
189
|
+
const [customRps, setCustomRps] = useState(state.targetRps.toString());
|
|
190
|
+
const [customDuration, setCustomDuration] = useState(state.testDuration);
|
|
191
|
+
const [customBulkSize, setCustomBulkSize] = useState(state.bulkSize?.toString() || "50");
|
|
192
|
+
const [activeField, setActiveField] = useState("rps");
|
|
193
|
+
const presets = state.testMode === "qps" ? QPS_PRESETS : THROUGHPUT_PRESETS;
|
|
194
|
+
useInput((input, key) => {
|
|
195
|
+
if (key.escape) {
|
|
196
|
+
if (customMode) {
|
|
197
|
+
setCustomMode(false);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
onBack();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (customMode && key.tab) {
|
|
204
|
+
// Cycle through fields
|
|
205
|
+
if (state.testMode === "throughput") {
|
|
206
|
+
setActiveField((prev) => prev === "rps"
|
|
207
|
+
? "duration"
|
|
208
|
+
: prev === "duration"
|
|
209
|
+
? "bulkSize"
|
|
210
|
+
: "rps");
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
setActiveField((prev) => (prev === "rps" ? "duration" : "rps"));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
const items = [
|
|
218
|
+
...Object.entries(presets).map(([key, value]) => ({
|
|
219
|
+
label: value.label,
|
|
220
|
+
value: key,
|
|
221
|
+
description: value.description,
|
|
222
|
+
})),
|
|
223
|
+
{
|
|
224
|
+
label: "Custom",
|
|
225
|
+
value: "custom",
|
|
226
|
+
description: "Define your own test parameters",
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
const handleSelect = (item) => {
|
|
230
|
+
if (item.value === "custom") {
|
|
231
|
+
setCustomMode(true);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const preset = presets[item.value];
|
|
235
|
+
const data = {
|
|
236
|
+
preset: item.value,
|
|
237
|
+
targetRps: preset.targetRps,
|
|
238
|
+
testDuration: preset.testDuration,
|
|
239
|
+
};
|
|
240
|
+
if (state.testMode === "throughput" && "bulkSize" in preset) {
|
|
241
|
+
data.bulkSize = preset.bulkSize;
|
|
242
|
+
}
|
|
243
|
+
onComplete(data);
|
|
244
|
+
};
|
|
245
|
+
const handleCustomSubmit = () => {
|
|
246
|
+
const rps = parseInt(customRps, 10);
|
|
247
|
+
const bulkSize = parseInt(customBulkSize, 10);
|
|
248
|
+
if (isNaN(rps) || rps < 1)
|
|
249
|
+
return;
|
|
250
|
+
if (state.testMode === "throughput" && (isNaN(bulkSize) || bulkSize < 1))
|
|
251
|
+
return;
|
|
252
|
+
const data = {
|
|
253
|
+
preset: "custom",
|
|
254
|
+
targetRps: rps,
|
|
255
|
+
testDuration: customDuration,
|
|
256
|
+
};
|
|
257
|
+
if (state.testMode === "throughput") {
|
|
258
|
+
data.bulkSize = bulkSize;
|
|
259
|
+
}
|
|
260
|
+
onComplete(data);
|
|
261
|
+
};
|
|
262
|
+
if (customMode) {
|
|
263
|
+
return (_jsxs(BorderBox, { title: "Custom Configuration", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: "Configure your custom benchmark parameters:" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: activeField === "rps" ? colors.accent : undefined, children: [activeField === "rps" ? "❯ " : " ", "Target RPS:", " "] }), activeField === "rps" ? (_jsx(TextInput, { value: customRps, onChange: setCustomRps, onSubmit: handleCustomSubmit, placeholder: "e.g., 500" })) : (_jsx(Text, { children: customRps }))] }), _jsxs(Box, { children: [_jsxs(Text, { color: activeField === "duration" ? colors.accent : undefined, children: [activeField === "duration" ? "❯ " : " ", "Duration:", " "] }), activeField === "duration" ? (_jsx(TextInput, { value: customDuration, onChange: setCustomDuration, onSubmit: handleCustomSubmit, placeholder: "e.g., 4m" })) : (_jsx(Text, { children: customDuration }))] }), state.testMode === "throughput" && (_jsxs(Box, { children: [_jsxs(Text, { color: activeField === "bulkSize" ? colors.accent : undefined, children: [activeField === "bulkSize" ? "❯ " : " ", "Bulk Size:", " "] }), activeField === "bulkSize" ? (_jsx(TextInput, { value: customBulkSize, onChange: setCustomBulkSize, onSubmit: handleCustomSubmit, placeholder: "e.g., 50" })) : (_jsx(Text, { children: customBulkSize }))] }))] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Tab to switch fields \u2022 Esc to go back \u2022 Enter to continue" }) })] }));
|
|
264
|
+
}
|
|
265
|
+
return (_jsxs(BorderBox, { title: "Test Presets", children: [_jsx(Box, { flexDirection: "column", marginY: 1, children: _jsxs(Text, { children: ["Select a preset for your ", state.testMode.toUpperCase(), " test:"] }) }), _jsx(SelectInput, { items: items, onSelect: handleSelect, itemComponent: ({ isSelected, label }) => {
|
|
266
|
+
const item = items.find((i) => i.label === label);
|
|
267
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: isSelected ? 1 : 0, children: [_jsxs(Text, { color: isSelected ? colors.accent : undefined, bold: isSelected, children: [isSelected ? "❯ " : " ", label] }), isSelected && item && (_jsxs(Text, { color: colors.muted, dimColor: true, children: [" ", item.description] }))] }));
|
|
268
|
+
} }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back \u2022 Enter to select" }) })] }));
|
|
269
|
+
}
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Step 6: Review and Confirm
|
|
272
|
+
// ============================================================================
|
|
273
|
+
export function ReviewStep({ onComplete, onBack, state }) {
|
|
274
|
+
const { colors } = useTheme();
|
|
275
|
+
useInput((input, key) => {
|
|
276
|
+
if (key.escape) {
|
|
277
|
+
onBack();
|
|
278
|
+
}
|
|
279
|
+
if (key.return) {
|
|
280
|
+
onComplete({});
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
const apiUrl = buildApiUrl(state.deploymentUrl, state.flowSlug);
|
|
284
|
+
const expectedThroughput = state.testMode === "throughput"
|
|
285
|
+
? state.targetRps * state.bulkSize
|
|
286
|
+
: state.targetRps;
|
|
287
|
+
return (_jsxs(BorderBox, { title: "Review Configuration", children: [_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, children: "Ready to run benchmark" }), _jsx(Text, { color: colors.muted, dimColor: true, children: "Review your configuration before starting the test" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Deployment: " }), _jsx(Text, { bold: true, children: state.deploymentName })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Target URL: " }), _jsx(Text, { children: apiUrl })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Test Mode: " }), _jsx(Text, { bold: true, children: state.testMode.toUpperCase() })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Preset: " }), _jsx(Text, { children: state.preset })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Target RPS: " }), _jsxs(Text, { children: [state.targetRps, " requests/sec"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Duration: " }), _jsxs(Text, { children: [state.testDuration, " (+ 1m warm-up)"] })] }), state.testMode === "throughput" && (_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Bulk Size: " }), _jsxs(Text, { children: [state.bulkSize, " payloads/request"] })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.accent, children: ["Expected ", state.testMode === "throughput" ? "throughput" : "load", ": ~", expectedThroughput.toLocaleString(), " ", state.testMode === "throughput" ? "solutions" : "requests", "/sec"] }) })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Esc to go back \u2022 Enter to start benchmark" }) })] }));
|
|
288
|
+
}
|
|
289
|
+
// ============================================================================
|
|
290
|
+
// Initial State Factory
|
|
291
|
+
// ============================================================================
|
|
292
|
+
export function createInitialBenchmarkState() {
|
|
293
|
+
return {
|
|
294
|
+
deploymentName: "",
|
|
295
|
+
deploymentUrl: "",
|
|
296
|
+
apiKey: "",
|
|
297
|
+
flowSlug: "",
|
|
298
|
+
testMode: "qps",
|
|
299
|
+
preset: "medium",
|
|
300
|
+
targetRps: QPS_PRESETS.medium.targetRps,
|
|
301
|
+
testDuration: QPS_PRESETS.medium.testDuration,
|
|
302
|
+
bulkSize: THROUGHPUT_PRESETS.medium.bulkSize,
|
|
303
|
+
};
|
|
304
|
+
}
|