stackport 0.3.1__tar.gz → 0.3.3__tar.gz
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.
- {stackport-0.3.1/stackport.egg-info → stackport-0.3.3}/PKG-INFO +1 -1
- {stackport-0.3.1 → stackport-0.3.3}/backend/main.py +4 -2
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/lambda_svc.py +52 -0
- stackport-0.3.3/backend/routes/stepfunctions.py +181 -0
- stackport-0.3.3/backend/schemas/lambda_svc.py +23 -0
- stackport-0.3.3/backend/schemas/stepfunctions.py +21 -0
- {stackport-0.3.1 → stackport-0.3.3}/pyproject.toml +1 -1
- {stackport-0.3.1 → stackport-0.3.3/stackport.egg-info}/PKG-INFO +1 -1
- {stackport-0.3.1 → stackport-0.3.3}/stackport.egg-info/SOURCES.txt +11 -4
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_lambda_routes.py +181 -0
- stackport-0.3.3/ui/dist/assets/StateMachineGraph-DKisycYa.js +6 -0
- stackport-0.3.3/ui/dist/assets/index-CCOvZYbr.css +1 -0
- stackport-0.3.3/ui/dist/assets/index-XBYym2zo.js +652 -0
- stackport-0.3.3/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +1 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/index.html +2 -2
- stackport-0.3.1/ui/dist/assets/index-CAZXHF4B.js +0 -639
- stackport-0.3.1/ui/dist/assets/index-DLd-xBZq.css +0 -1
- {stackport-0.3.1 → stackport-0.3.3}/LICENSE +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/MANIFEST.in +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/README.md +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/__init__.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/aws_client.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/cache.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/cli.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/config.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/endpoint_store.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/__init__.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/common.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/dynamodb.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/ec2.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/endpoints.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/iam.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/logs.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/resources.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/s3.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/secretsmanager.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/sqs.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/stats.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/routes/tags.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/schemas/__init__.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/schemas/dynamodb.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/schemas/endpoints.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/schemas/s3.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/schemas/secretsmanager.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/schemas/sqs.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/schemas/tags.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/backend/websocket.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/setup.cfg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/stackport.egg-info/dependency_links.txt +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/stackport.egg-info/entry_points.txt +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/stackport.egg-info/requires.txt +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/stackport.egg-info/top_level.txt +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_cache.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_cli.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_client.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_config.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_dynamodb_routes.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_ec2_routes.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_endpoint_store.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_endpoints.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_iam_routes.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_logs_routes.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_multi_endpoint.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_readonly_middleware.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_registries.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_routes.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_s3_routes.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_s3_upload_limit_env.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_secretsmanager_routes.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_sqs_routes.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_tags_routes.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/tests/test_websocket.py +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/acm.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/apigateway.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/appsync.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/athena.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/cloudformation.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/cloudfront.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/cognito-idp.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/dynamodb.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/ec2.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/ecr.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/ecs.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/elasticache.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/events.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/firehose.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/glue.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/iam.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/kinesis.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/kms.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/lambda.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/logs.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/monitoring.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/rds.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/route53.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/s3.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/secretsmanager.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/ses.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/sns.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/sqs.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/ssm.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/stepfunctions.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/aws-icons/wafv2.svg +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/favicon.png +0 -0
- {stackport-0.3.1 → stackport-0.3.3}/ui/dist/favicon.svg +0 -0
|
@@ -17,7 +17,7 @@ from botocore.exceptions import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
from backend.config import LOG_LEVEL, STACKPORT_ALLOW_WRITES, STACKPORT_PORT
|
|
20
|
-
from backend.routes import dynamodb, ec2, endpoints, iam, lambda_svc, logs, resources, s3, secretsmanager, sqs, stats, tags
|
|
20
|
+
from backend.routes import dynamodb, ec2, endpoints, iam, lambda_svc, logs, resources, s3, secretsmanager, sqs, stats, stepfunctions, tags
|
|
21
21
|
from backend.websocket import probe_loop, websocket_endpoint
|
|
22
22
|
|
|
23
23
|
|
|
@@ -71,6 +71,7 @@ class ReadOnlyMiddleware(BaseHTTPMiddleware):
|
|
|
71
71
|
return True
|
|
72
72
|
if path.startswith("/api/lambda/functions/") and path.endswith("/invoke"):
|
|
73
73
|
return True
|
|
74
|
+
# Step Functions start/stop execution are writes, not reads
|
|
74
75
|
return False
|
|
75
76
|
|
|
76
77
|
async def dispatch(self, request: Request, call_next):
|
|
@@ -103,7 +104,7 @@ app.add_middleware(ReadOnlyMiddleware)
|
|
|
103
104
|
@app.exception_handler(SSOTokenLoadError)
|
|
104
105
|
@app.exception_handler(UnauthorizedSSOTokenError)
|
|
105
106
|
@app.exception_handler(SSOError)
|
|
106
|
-
async def sso_error_handler(
|
|
107
|
+
async def sso_error_handler(_request: Request, exc: Exception):
|
|
107
108
|
"""Handle expired/missing SSO tokens with actionable guidance."""
|
|
108
109
|
return JSONResponse(
|
|
109
110
|
status_code=401,
|
|
@@ -122,6 +123,7 @@ app.include_router(iam.router, prefix="/api/iam", tags=["iam"])
|
|
|
122
123
|
app.include_router(ec2.router, prefix="/api/ec2", tags=["ec2"])
|
|
123
124
|
app.include_router(logs.router, prefix="/api/logs", tags=["logs"])
|
|
124
125
|
app.include_router(secretsmanager.router, prefix="/api/secretsmanager", tags=["secretsmanager"])
|
|
126
|
+
app.include_router(stepfunctions.router, prefix="/api/stepfunctions", tags=["stepfunctions"])
|
|
125
127
|
app.include_router(tags.router, prefix="/api", tags=["tags"])
|
|
126
128
|
app.include_router(resources.router, prefix="/api")
|
|
127
129
|
|
|
@@ -9,6 +9,7 @@ from fastapi.responses import RedirectResponse
|
|
|
9
9
|
|
|
10
10
|
from backend.aws_client import get_client
|
|
11
11
|
from backend.routes.common import EndpointInfo, get_endpoint_info
|
|
12
|
+
from backend.schemas.lambda_svc import UpdateFunctionConfigRequest
|
|
12
13
|
|
|
13
14
|
router = APIRouter()
|
|
14
15
|
|
|
@@ -184,3 +185,54 @@ def list_versions(function_name: str, ep: EndpointInfo = Depends(get_endpoint_in
|
|
|
184
185
|
return {"versions": versions}
|
|
185
186
|
except Exception as e:
|
|
186
187
|
raise HTTPException(status_code=500, detail=str(e))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@router.patch("/functions/{function_name}/configuration")
|
|
191
|
+
def update_function_configuration(
|
|
192
|
+
function_name: str,
|
|
193
|
+
body: UpdateFunctionConfigRequest,
|
|
194
|
+
ep: EndpointInfo = Depends(get_endpoint_info)
|
|
195
|
+
) -> dict[str, Any]:
|
|
196
|
+
"""Update Lambda function configuration (partial updates supported).
|
|
197
|
+
|
|
198
|
+
All body fields are optional — only specified fields will be updated.
|
|
199
|
+
Returns the updated function configuration.
|
|
200
|
+
"""
|
|
201
|
+
try:
|
|
202
|
+
client = get_client("lambda", **ep.client_kwargs())
|
|
203
|
+
|
|
204
|
+
# Build boto3 kwargs from request body, skipping None values
|
|
205
|
+
update_kwargs: dict[str, Any] = {"FunctionName": function_name}
|
|
206
|
+
|
|
207
|
+
if body.description is not None:
|
|
208
|
+
update_kwargs["Description"] = body.description
|
|
209
|
+
|
|
210
|
+
if body.handler is not None:
|
|
211
|
+
update_kwargs["Handler"] = body.handler
|
|
212
|
+
|
|
213
|
+
if body.runtime is not None:
|
|
214
|
+
update_kwargs["Runtime"] = body.runtime
|
|
215
|
+
|
|
216
|
+
if body.memory_size is not None:
|
|
217
|
+
update_kwargs["MemorySize"] = body.memory_size
|
|
218
|
+
|
|
219
|
+
if body.timeout is not None:
|
|
220
|
+
update_kwargs["Timeout"] = body.timeout
|
|
221
|
+
|
|
222
|
+
if body.environment is not None:
|
|
223
|
+
update_kwargs["Environment"] = {"Variables": body.environment}
|
|
224
|
+
|
|
225
|
+
if body.layers is not None:
|
|
226
|
+
update_kwargs["Layers"] = body.layers
|
|
227
|
+
|
|
228
|
+
# Call update_function_configuration
|
|
229
|
+
response = client.update_function_configuration(**update_kwargs)
|
|
230
|
+
|
|
231
|
+
return {"configuration": response}
|
|
232
|
+
|
|
233
|
+
except client.exceptions.ResourceNotFoundException:
|
|
234
|
+
raise HTTPException(status_code=404, detail=f"Function {function_name} not found")
|
|
235
|
+
except client.exceptions.InvalidParameterValueException as e:
|
|
236
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
237
|
+
except Exception as e:
|
|
238
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Step Functions service-specific routes."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import unquote
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
8
|
+
|
|
9
|
+
from backend.aws_client import get_client
|
|
10
|
+
from backend.routes.common import EndpointInfo, get_endpoint_info
|
|
11
|
+
from backend.schemas.stepfunctions import StartExecutionRequest, StopExecutionRequest
|
|
12
|
+
|
|
13
|
+
router = APIRouter()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.get("/state-machines")
|
|
17
|
+
def list_state_machines(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
|
|
18
|
+
"""List all Step Functions state machines."""
|
|
19
|
+
try:
|
|
20
|
+
client = get_client("stepfunctions", **ep.client_kwargs())
|
|
21
|
+
response = client.list_state_machines()
|
|
22
|
+
return {"stateMachines": response.get("stateMachines", [])}
|
|
23
|
+
except Exception as e:
|
|
24
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@router.get("/state-machines/{arn}")
|
|
28
|
+
def get_state_machine_detail(
|
|
29
|
+
arn: str,
|
|
30
|
+
ep: EndpointInfo = Depends(get_endpoint_info),
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
"""Get state machine detail including ASL definition."""
|
|
33
|
+
decoded_arn = unquote(arn)
|
|
34
|
+
try:
|
|
35
|
+
client = get_client("stepfunctions", **ep.client_kwargs())
|
|
36
|
+
response = client.describe_state_machine(stateMachineArn=decoded_arn)
|
|
37
|
+
|
|
38
|
+
# Parse definition JSON string into dict
|
|
39
|
+
if "definition" in response:
|
|
40
|
+
try:
|
|
41
|
+
response["definition"] = json.loads(response["definition"])
|
|
42
|
+
except (json.JSONDecodeError, TypeError):
|
|
43
|
+
pass # Keep as string if parsing fails
|
|
44
|
+
|
|
45
|
+
return response
|
|
46
|
+
except client.exceptions.StateMachineDoesNotExist as e:
|
|
47
|
+
raise HTTPException(status_code=404, detail=f"State machine not found: {decoded_arn}") from e
|
|
48
|
+
except Exception as e:
|
|
49
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.get("/state-machines/{arn}/executions")
|
|
53
|
+
def list_executions(
|
|
54
|
+
arn: str,
|
|
55
|
+
status_filter: str | None = Query(None, alias="status_filter"),
|
|
56
|
+
max_results: int = Query(50, alias="max_results", ge=1, le=1000),
|
|
57
|
+
ep: EndpointInfo = Depends(get_endpoint_info),
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""List executions for a state machine."""
|
|
60
|
+
decoded_arn = unquote(arn)
|
|
61
|
+
try:
|
|
62
|
+
client = get_client("stepfunctions", **ep.client_kwargs())
|
|
63
|
+
kwargs: dict[str, Any] = {
|
|
64
|
+
"stateMachineArn": decoded_arn,
|
|
65
|
+
"maxResults": max_results,
|
|
66
|
+
}
|
|
67
|
+
if status_filter:
|
|
68
|
+
kwargs["statusFilter"] = status_filter
|
|
69
|
+
|
|
70
|
+
response = client.list_executions(**kwargs)
|
|
71
|
+
return {"executions": response.get("executions", [])}
|
|
72
|
+
except Exception as e:
|
|
73
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@router.post("/state-machines/{arn}/executions")
|
|
77
|
+
def start_execution(
|
|
78
|
+
arn: str,
|
|
79
|
+
request: StartExecutionRequest,
|
|
80
|
+
ep: EndpointInfo = Depends(get_endpoint_info),
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""Start a new execution of a state machine."""
|
|
83
|
+
decoded_arn = unquote(arn)
|
|
84
|
+
try:
|
|
85
|
+
client = get_client("stepfunctions", **ep.client_kwargs())
|
|
86
|
+
kwargs: dict[str, Any] = {"stateMachineArn": decoded_arn}
|
|
87
|
+
|
|
88
|
+
if request.name:
|
|
89
|
+
kwargs["name"] = request.name
|
|
90
|
+
if request.input is not None:
|
|
91
|
+
kwargs["input"] = json.dumps(request.input)
|
|
92
|
+
|
|
93
|
+
response = client.start_execution(**kwargs)
|
|
94
|
+
return {
|
|
95
|
+
"executionArn": response["executionArn"],
|
|
96
|
+
"startDate": response["startDate"].isoformat(),
|
|
97
|
+
}
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@router.get("/executions/{arn}")
|
|
103
|
+
def get_execution_detail(
|
|
104
|
+
arn: str,
|
|
105
|
+
ep: EndpointInfo = Depends(get_endpoint_info),
|
|
106
|
+
) -> dict[str, Any]:
|
|
107
|
+
"""Get execution detail."""
|
|
108
|
+
decoded_arn = unquote(arn)
|
|
109
|
+
try:
|
|
110
|
+
client = get_client("stepfunctions", **ep.client_kwargs())
|
|
111
|
+
response = client.describe_execution(executionArn=decoded_arn)
|
|
112
|
+
|
|
113
|
+
# Parse input/output JSON strings into dicts
|
|
114
|
+
for field in ["input", "output"]:
|
|
115
|
+
if field in response:
|
|
116
|
+
try:
|
|
117
|
+
response[field] = json.loads(response[field])
|
|
118
|
+
except (json.JSONDecodeError, TypeError):
|
|
119
|
+
pass # Keep as string if parsing fails
|
|
120
|
+
|
|
121
|
+
# Convert datetime objects to ISO strings
|
|
122
|
+
for date_field in ["startDate", "stopDate"]:
|
|
123
|
+
if date_field in response and response[date_field]:
|
|
124
|
+
response[date_field] = response[date_field].isoformat()
|
|
125
|
+
|
|
126
|
+
return response
|
|
127
|
+
except client.exceptions.ExecutionDoesNotExist as e:
|
|
128
|
+
raise HTTPException(status_code=404, detail=f"Execution not found: {decoded_arn}") from e
|
|
129
|
+
except Exception as e:
|
|
130
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@router.get("/executions/{arn}/history")
|
|
134
|
+
def get_execution_history(
|
|
135
|
+
arn: str,
|
|
136
|
+
max_results: int = Query(100, alias="max_results", ge=1, le=1000),
|
|
137
|
+
reverse_order: bool = Query(False, alias="reverse_order"),
|
|
138
|
+
ep: EndpointInfo = Depends(get_endpoint_info),
|
|
139
|
+
) -> dict[str, Any]:
|
|
140
|
+
"""Get execution history events."""
|
|
141
|
+
decoded_arn = unquote(arn)
|
|
142
|
+
try:
|
|
143
|
+
client = get_client("stepfunctions", **ep.client_kwargs())
|
|
144
|
+
response = client.get_execution_history(
|
|
145
|
+
executionArn=decoded_arn,
|
|
146
|
+
maxResults=max_results,
|
|
147
|
+
reverseOrder=reverse_order,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Convert datetime objects to ISO strings in events
|
|
151
|
+
events = response.get("events", [])
|
|
152
|
+
for event in events:
|
|
153
|
+
if "timestamp" in event:
|
|
154
|
+
event["timestamp"] = event["timestamp"].isoformat()
|
|
155
|
+
|
|
156
|
+
return {"events": events}
|
|
157
|
+
except Exception as e:
|
|
158
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@router.post("/executions/{arn}/stop")
|
|
162
|
+
def stop_execution(
|
|
163
|
+
arn: str,
|
|
164
|
+
request: StopExecutionRequest,
|
|
165
|
+
ep: EndpointInfo = Depends(get_endpoint_info),
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Stop a running execution."""
|
|
168
|
+
decoded_arn = unquote(arn)
|
|
169
|
+
try:
|
|
170
|
+
client = get_client("stepfunctions", **ep.client_kwargs())
|
|
171
|
+
kwargs: dict[str, Any] = {"executionArn": decoded_arn}
|
|
172
|
+
|
|
173
|
+
if request.error:
|
|
174
|
+
kwargs["error"] = request.error
|
|
175
|
+
if request.cause:
|
|
176
|
+
kwargs["cause"] = request.cause
|
|
177
|
+
|
|
178
|
+
response = client.stop_execution(**kwargs)
|
|
179
|
+
return {"stopDate": response["stopDate"].isoformat()}
|
|
180
|
+
except Exception as e:
|
|
181
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Pydantic schemas for Lambda API requests."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UpdateFunctionConfigRequest(BaseModel):
|
|
7
|
+
"""Request body for updating Lambda function configuration.
|
|
8
|
+
|
|
9
|
+
All fields are optional — only specified fields will be updated.
|
|
10
|
+
Validation follows AWS Lambda limits:
|
|
11
|
+
- Memory: 128-10240 MB
|
|
12
|
+
- Timeout: 1-900 seconds
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
16
|
+
|
|
17
|
+
description: str | None = Field(None, description="Function description")
|
|
18
|
+
handler: str | None = Field(None, description="Handler path (e.g., index.handler)")
|
|
19
|
+
runtime: str | None = Field(None, description="Runtime identifier (e.g., python3.12, nodejs20.x)")
|
|
20
|
+
memory_size: int | None = Field(None, alias="memorySize", ge=128, le=10240, description="Memory in MB")
|
|
21
|
+
timeout: int | None = Field(None, ge=1, le=900, description="Timeout in seconds")
|
|
22
|
+
environment: dict[str, str] | None = Field(None, description="Environment variables as key-value dict")
|
|
23
|
+
layers: list[str] | None = Field(None, description="Layer ARNs")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Pydantic schemas for Step Functions API requests."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StartExecutionRequest(BaseModel):
|
|
7
|
+
"""Request body for starting a Step Functions execution."""
|
|
8
|
+
|
|
9
|
+
name: str | None = Field(None, alias="name")
|
|
10
|
+
input: dict | None = Field(None, alias="input")
|
|
11
|
+
|
|
12
|
+
model_config = {"populate_by_name": True}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StopExecutionRequest(BaseModel):
|
|
16
|
+
"""Request body for stopping a Step Functions execution."""
|
|
17
|
+
|
|
18
|
+
error: str | None = Field(None, alias="error")
|
|
19
|
+
cause: str | None = Field(None, alias="cause")
|
|
20
|
+
|
|
21
|
+
model_config = {"populate_by_name": True}
|
|
@@ -13,8 +13,10 @@ backend/websocket.py
|
|
|
13
13
|
backend/../ui/dist/favicon.png
|
|
14
14
|
backend/../ui/dist/favicon.svg
|
|
15
15
|
backend/../ui/dist/index.html
|
|
16
|
-
backend/../ui/dist/assets/
|
|
17
|
-
backend/../ui/dist/assets/index-
|
|
16
|
+
backend/../ui/dist/assets/StateMachineGraph-DKisycYa.js
|
|
17
|
+
backend/../ui/dist/assets/index-CCOvZYbr.css
|
|
18
|
+
backend/../ui/dist/assets/index-XBYym2zo.js
|
|
19
|
+
backend/../ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
|
|
18
20
|
backend/../ui/dist/aws-icons/acm.svg
|
|
19
21
|
backend/../ui/dist/aws-icons/apigateway.svg
|
|
20
22
|
backend/../ui/dist/aws-icons/appsync.svg
|
|
@@ -62,13 +64,16 @@ backend/routes/s3.py
|
|
|
62
64
|
backend/routes/secretsmanager.py
|
|
63
65
|
backend/routes/sqs.py
|
|
64
66
|
backend/routes/stats.py
|
|
67
|
+
backend/routes/stepfunctions.py
|
|
65
68
|
backend/routes/tags.py
|
|
66
69
|
backend/schemas/__init__.py
|
|
67
70
|
backend/schemas/dynamodb.py
|
|
68
71
|
backend/schemas/endpoints.py
|
|
72
|
+
backend/schemas/lambda_svc.py
|
|
69
73
|
backend/schemas/s3.py
|
|
70
74
|
backend/schemas/secretsmanager.py
|
|
71
75
|
backend/schemas/sqs.py
|
|
76
|
+
backend/schemas/stepfunctions.py
|
|
72
77
|
backend/schemas/tags.py
|
|
73
78
|
stackport.egg-info/PKG-INFO
|
|
74
79
|
stackport.egg-info/SOURCES.txt
|
|
@@ -100,8 +105,10 @@ tests/test_websocket.py
|
|
|
100
105
|
ui/dist/favicon.png
|
|
101
106
|
ui/dist/favicon.svg
|
|
102
107
|
ui/dist/index.html
|
|
103
|
-
ui/dist/assets/
|
|
104
|
-
ui/dist/assets/index-
|
|
108
|
+
ui/dist/assets/StateMachineGraph-DKisycYa.js
|
|
109
|
+
ui/dist/assets/index-CCOvZYbr.css
|
|
110
|
+
ui/dist/assets/index-XBYym2zo.js
|
|
111
|
+
ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
|
|
105
112
|
ui/dist/aws-icons/acm.svg
|
|
106
113
|
ui/dist/aws-icons/apigateway.svg
|
|
107
114
|
ui/dist/aws-icons/appsync.svg
|
|
@@ -269,3 +269,184 @@ class TestListVersions:
|
|
|
269
269
|
data = resp.json()
|
|
270
270
|
assert len(data["versions"]) == 2
|
|
271
271
|
assert data["versions"][0]["Version"] == "$LATEST"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TestUpdateFunctionConfiguration:
|
|
275
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
276
|
+
def test_update_environment_variables(self, mock_get_client):
|
|
277
|
+
mock_lambda = MagicMock()
|
|
278
|
+
mock_get_client.return_value = mock_lambda
|
|
279
|
+
mock_lambda.update_function_configuration.return_value = {
|
|
280
|
+
"FunctionName": "my-func",
|
|
281
|
+
"Runtime": "python3.12",
|
|
282
|
+
"Handler": "handler.main",
|
|
283
|
+
"MemorySize": 256,
|
|
284
|
+
"Timeout": 30,
|
|
285
|
+
"Environment": {
|
|
286
|
+
"Variables": {
|
|
287
|
+
"KEY1": "value1",
|
|
288
|
+
"KEY2": "value2",
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
resp = client.patch(
|
|
294
|
+
"/api/lambda/functions/my-func/configuration",
|
|
295
|
+
json={
|
|
296
|
+
"environment": {
|
|
297
|
+
"KEY1": "value1",
|
|
298
|
+
"KEY2": "value2",
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
assert resp.status_code == 200
|
|
303
|
+
data = resp.json()
|
|
304
|
+
assert data["configuration"]["Environment"]["Variables"]["KEY1"] == "value1"
|
|
305
|
+
mock_lambda.update_function_configuration.assert_called_once()
|
|
306
|
+
call_args = mock_lambda.update_function_configuration.call_args[1]
|
|
307
|
+
assert call_args["FunctionName"] == "my-func"
|
|
308
|
+
assert call_args["Environment"]["Variables"] == {"KEY1": "value1", "KEY2": "value2"}
|
|
309
|
+
|
|
310
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
311
|
+
def test_update_memory_and_timeout(self, mock_get_client):
|
|
312
|
+
mock_lambda = MagicMock()
|
|
313
|
+
mock_get_client.return_value = mock_lambda
|
|
314
|
+
mock_lambda.update_function_configuration.return_value = {
|
|
315
|
+
"FunctionName": "my-func",
|
|
316
|
+
"MemorySize": 512,
|
|
317
|
+
"Timeout": 60,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
resp = client.patch(
|
|
321
|
+
"/api/lambda/functions/my-func/configuration",
|
|
322
|
+
json={"memorySize": 512, "timeout": 60},
|
|
323
|
+
)
|
|
324
|
+
assert resp.status_code == 200
|
|
325
|
+
data = resp.json()
|
|
326
|
+
assert data["configuration"]["MemorySize"] == 512
|
|
327
|
+
assert data["configuration"]["Timeout"] == 60
|
|
328
|
+
mock_lambda.update_function_configuration.assert_called_once_with(
|
|
329
|
+
FunctionName="my-func",
|
|
330
|
+
MemorySize=512,
|
|
331
|
+
Timeout=60,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
335
|
+
def test_update_handler_and_runtime(self, mock_get_client):
|
|
336
|
+
mock_lambda = MagicMock()
|
|
337
|
+
mock_get_client.return_value = mock_lambda
|
|
338
|
+
mock_lambda.update_function_configuration.return_value = {
|
|
339
|
+
"FunctionName": "my-func",
|
|
340
|
+
"Handler": "new_handler.handler",
|
|
341
|
+
"Runtime": "python3.13",
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
resp = client.patch(
|
|
345
|
+
"/api/lambda/functions/my-func/configuration",
|
|
346
|
+
json={"handler": "new_handler.handler", "runtime": "python3.13"},
|
|
347
|
+
)
|
|
348
|
+
assert resp.status_code == 200
|
|
349
|
+
data = resp.json()
|
|
350
|
+
assert data["configuration"]["Handler"] == "new_handler.handler"
|
|
351
|
+
assert data["configuration"]["Runtime"] == "python3.13"
|
|
352
|
+
|
|
353
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
354
|
+
def test_update_description(self, mock_get_client):
|
|
355
|
+
mock_lambda = MagicMock()
|
|
356
|
+
mock_get_client.return_value = mock_lambda
|
|
357
|
+
mock_lambda.update_function_configuration.return_value = {
|
|
358
|
+
"FunctionName": "my-func",
|
|
359
|
+
"Description": "Updated description",
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
resp = client.patch(
|
|
363
|
+
"/api/lambda/functions/my-func/configuration",
|
|
364
|
+
json={"description": "Updated description"},
|
|
365
|
+
)
|
|
366
|
+
assert resp.status_code == 200
|
|
367
|
+
data = resp.json()
|
|
368
|
+
assert data["configuration"]["Description"] == "Updated description"
|
|
369
|
+
|
|
370
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
371
|
+
def test_update_partial(self, mock_get_client):
|
|
372
|
+
"""Test that only specified fields are included in the update call."""
|
|
373
|
+
mock_lambda = MagicMock()
|
|
374
|
+
mock_get_client.return_value = mock_lambda
|
|
375
|
+
mock_lambda.update_function_configuration.return_value = {
|
|
376
|
+
"FunctionName": "my-func",
|
|
377
|
+
"Timeout": 120,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
resp = client.patch(
|
|
381
|
+
"/api/lambda/functions/my-func/configuration",
|
|
382
|
+
json={"timeout": 120},
|
|
383
|
+
)
|
|
384
|
+
assert resp.status_code == 200
|
|
385
|
+
# Verify only Timeout and FunctionName were passed
|
|
386
|
+
mock_lambda.update_function_configuration.assert_called_once()
|
|
387
|
+
call_args = mock_lambda.update_function_configuration.call_args[1]
|
|
388
|
+
assert set(call_args.keys()) == {"FunctionName", "Timeout"}
|
|
389
|
+
assert call_args["Timeout"] == 120
|
|
390
|
+
|
|
391
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
392
|
+
def test_update_function_not_found(self, mock_get_client):
|
|
393
|
+
mock_lambda = MagicMock()
|
|
394
|
+
mock_get_client.return_value = mock_lambda
|
|
395
|
+
# Set up both exception types
|
|
396
|
+
mock_lambda.exceptions.ResourceNotFoundException = type("ResourceNotFoundException", (Exception,), {})
|
|
397
|
+
mock_lambda.exceptions.InvalidParameterValueException = type("InvalidParameterValueException", (Exception,), {})
|
|
398
|
+
mock_lambda.update_function_configuration.side_effect = mock_lambda.exceptions.ResourceNotFoundException()
|
|
399
|
+
|
|
400
|
+
resp = client.patch(
|
|
401
|
+
"/api/lambda/functions/nonexistent/configuration",
|
|
402
|
+
json={"timeout": 60},
|
|
403
|
+
)
|
|
404
|
+
assert resp.status_code == 404
|
|
405
|
+
|
|
406
|
+
@patch("backend.routes.lambda_svc.get_client")
|
|
407
|
+
def test_update_invalid_parameter_boto3(self, mock_get_client):
|
|
408
|
+
"""Test AWS InvalidParameterValueException returns 400."""
|
|
409
|
+
mock_lambda = MagicMock()
|
|
410
|
+
mock_get_client.return_value = mock_lambda
|
|
411
|
+
# Set up both exception types
|
|
412
|
+
mock_lambda.exceptions.ResourceNotFoundException = type("ResourceNotFoundException", (Exception,), {})
|
|
413
|
+
mock_lambda.exceptions.InvalidParameterValueException = type("InvalidParameterValueException", (Exception,), {})
|
|
414
|
+
mock_lambda.update_function_configuration.side_effect = mock_lambda.exceptions.InvalidParameterValueException("Invalid runtime")
|
|
415
|
+
|
|
416
|
+
resp = client.patch(
|
|
417
|
+
"/api/lambda/functions/my-func/configuration",
|
|
418
|
+
json={"runtime": "invalid_runtime"},
|
|
419
|
+
)
|
|
420
|
+
assert resp.status_code == 400
|
|
421
|
+
|
|
422
|
+
def test_update_validation_memory_too_low(self):
|
|
423
|
+
"""Test Pydantic validation rejects memory < 128."""
|
|
424
|
+
resp = client.patch(
|
|
425
|
+
"/api/lambda/functions/my-func/configuration",
|
|
426
|
+
json={"memorySize": 64},
|
|
427
|
+
)
|
|
428
|
+
assert resp.status_code == 422
|
|
429
|
+
|
|
430
|
+
def test_update_validation_memory_too_high(self):
|
|
431
|
+
"""Test Pydantic validation rejects memory > 10240."""
|
|
432
|
+
resp = client.patch(
|
|
433
|
+
"/api/lambda/functions/my-func/configuration",
|
|
434
|
+
json={"memorySize": 20480},
|
|
435
|
+
)
|
|
436
|
+
assert resp.status_code == 422
|
|
437
|
+
|
|
438
|
+
def test_update_validation_timeout_too_low(self):
|
|
439
|
+
"""Test Pydantic validation rejects timeout < 1."""
|
|
440
|
+
resp = client.patch(
|
|
441
|
+
"/api/lambda/functions/my-func/configuration",
|
|
442
|
+
json={"timeout": 0},
|
|
443
|
+
)
|
|
444
|
+
assert resp.status_code == 422
|
|
445
|
+
|
|
446
|
+
def test_update_validation_timeout_too_high(self):
|
|
447
|
+
"""Test Pydantic validation rejects timeout > 900."""
|
|
448
|
+
resp = client.patch(
|
|
449
|
+
"/api/lambda/functions/my-func/configuration",
|
|
450
|
+
json={"timeout": 1000},
|
|
451
|
+
)
|
|
452
|
+
assert resp.status_code == 422
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{c as T,C as E,a as P,b as C,G as D,j as r,r as f,B as k}from"./index-XBYym2zo.js";import{T as v}from"./stepfunctions-graph-B6nvU4OB.js";/**
|
|
2
|
+
* @license lucide-react v1.7.0 - ISC
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the ISC license.
|
|
5
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/const A=[["path",{d:"m17 2 4 4-4 4",key:"nntrym"}],["path",{d:"M3 11v-1a4 4 0 0 1 4-4h14",key:"84bu3i"}],["path",{d:"m7 22-4-4 4-4",key:"1wqhfi"}],["path",{d:"M21 13v1a4 4 0 0 1-4 4H3",key:"1rx37r"}]],q=T("repeat",A);function I(e){const t=typeof e=="string"?JSON.parse(e):e,n=t.StartAt||"",d=t.States||{},a=[],i=[];for(const[c,s]of Object.entries(d)){const l=s.Type||"Pass",u=l==="Succeed"||l==="Fail"||s.End===!0;if(a.push({id:c,type:l,isTerminal:u,metadata:s}),s.Next&&typeof s.Next=="string"&&i.push({source:c,target:s.Next,type:"next"}),l==="Choice"&&Array.isArray(s.Choices)){for(const h of s.Choices)if(h.Next&&typeof h.Next=="string"){const m=W(h);i.push({source:c,target:h.Next,label:m,type:"choice"})}s.Default&&typeof s.Default=="string"&&i.push({source:c,target:s.Default,label:"Default",type:"default"})}if(Array.isArray(s.Catch)){for(const h of s.Catch)if(h.Next&&typeof h.Next=="string"){const m=Array.isArray(h.ErrorEquals)?h.ErrorEquals.join(", "):"Error";i.push({source:c,target:h.Next,label:m,type:"catch"})}}}return{nodes:a,edges:i,startAt:n}}function W(e){const t=e.Variable,n=t?t.replace("$.",""):"",d=["StringEquals","StringEqualsPath","StringLessThan","StringGreaterThan","StringMatches","NumericEquals","NumericLessThan","NumericGreaterThan","NumericLessThanEquals","NumericGreaterThanEquals","BooleanEquals","TimestampEquals","TimestampLessThan","TimestampGreaterThan","IsPresent","IsNull","IsString","IsNumeric","IsBoolean","IsTimestamp"];for(const a of d)if(a in e){const i=e[a],c=a.replace("StringEquals","==").replace("NumericEquals","==").replace("NumericLessThan","<").replace("NumericGreaterThan",">").replace("BooleanEquals","==");return c!==a?`${n} ${c} ${i}`:`${n} ${a}`}return n||"?"}const b=160,y=48,L={Task:{bg:"fill-blue-500/10",border:"stroke-blue-500",text:"text-blue-400"},Pass:{bg:"fill-muted/50",border:"stroke-muted-foreground/50",text:"text-muted-foreground"},Choice:{bg:"fill-amber-500/10",border:"stroke-amber-500",text:"text-amber-400"},Wait:{bg:"fill-secondary/50",border:"stroke-secondary-foreground/50",text:"text-secondary-foreground"},Parallel:{bg:"fill-purple-500/10",border:"stroke-purple-500",text:"text-purple-400"},Map:{bg:"fill-indigo-500/10",border:"stroke-indigo-500",text:"text-indigo-400"},Succeed:{bg:"fill-green-500/10",border:"stroke-green-500",text:"text-green-400"},Fail:{bg:"fill-red-500/10",border:"stroke-red-500",text:"text-red-400"}},G={Choice:D,Wait:C,Map:q,Succeed:P,Fail:E};function z(e){switch(e){case"succeeded":return"stroke-green-500 stroke-[3]";case"failed":return"stroke-red-500 stroke-[3]";case"in-progress":return"stroke-blue-500 stroke-[3] animate-pulse";default:return""}}function B({node:e,x:t,y:n,traceStatus:d,isStart:a}){const i=L[e.type],c=G[e.type],s=z(d),l=d===void 0&&s===""||d?"":"opacity-40",u=e.type==="Succeed"||e.type==="Fail"?y/2:8;return r.jsxs("g",{transform:`translate(${t-b/2}, ${n-y/2})`,className:l,children:[a&&r.jsx("circle",{cx:-12,cy:y/2,r:5,className:"fill-green-500"}),r.jsx("rect",{width:b,height:y,rx:u,className:`${i.bg} ${s||i.border} stroke-[1.5]`,strokeDasharray:e.type==="Pass"?"4 2":void 0}),r.jsx("foreignObject",{width:b,height:y,children:r.jsxs("div",{className:"flex items-center justify-center gap-1.5 h-full px-2",children:[c&&r.jsx(c,{className:`h-3.5 w-3.5 flex-shrink-0 ${i.text}`}),r.jsx("span",{className:`text-xs font-medium truncate ${i.text}`,children:e.id})]})}),r.jsx("text",{x:b/2,y:y+14,textAnchor:"middle",fontSize:10,className:"fill-zinc-400 opacity-60",children:e.type})]})}function O({edge:e,points:t,highlighted:n}){if(t.length<2)return null;const d=Y(t),a=t.slice(-2),i=Math.atan2(a[1].y-a[0].y,a[1].x-a[0].x),c=F(e.type,n),s=e.type==="catch"?"4 3":e.type==="default"?"2 2":void 0,l=Math.floor(t.length/2),u=t[l];return r.jsxs("g",{children:[r.jsx("path",{d,fill:"none",stroke:c.stroke,strokeWidth:c.strokeWidth,opacity:c.opacity,strokeDasharray:s}),r.jsx("polygon",{points:_(a[1],i),fill:c.stroke,opacity:c.opacity}),e.label&&u&&r.jsxs("g",{children:[r.jsx("rect",{x:u.x-(e.label.length*3+12),y:u.y-18,width:e.label.length*6+24,height:18,rx:4,fill:"#18181b",fillOpacity:.92,stroke:e.type==="catch"?"#f8717140":e.type==="choice"?"#fbbf2440":"#3f3f46",strokeWidth:.75}),r.jsx("text",{x:u.x,y:u.y-6,textAnchor:"middle",fontSize:11,fontFamily:"monospace",fill:e.type==="catch"?"#f87171":e.type==="choice"?"#fbbf24":"#a1a1aa",children:e.label})]})]})}function F(e,t){if(t)return{stroke:"#22c55e",strokeWidth:2.5,opacity:1};switch(e){case"catch":return{stroke:"#f87171",strokeWidth:1.5,opacity:.8};case"choice":return{stroke:"#fbbf24",strokeWidth:1.5,opacity:.8};case"default":return{stroke:"#a1a1aa",strokeWidth:1.5,opacity:.6};default:return{stroke:"#a1a1aa",strokeWidth:1.5,opacity:.7}}}function Y(e){if(e.length<2)return"";let t=`M ${e[0].x} ${e[0].y}`;for(let n=1;n<e.length;n++)t+=` L ${e[n].x} ${e[n].y}`;return t}function _(e,t){const d=e.x-6*Math.cos(t-Math.PI/6),a=e.y-6*Math.sin(t-Math.PI/6),i=e.x-6*Math.cos(t+Math.PI/6),c=e.y-6*Math.sin(t+Math.PI/6);return`${e.x},${e.y} ${d},${a} ${i},${c}`}function H(e){const t=new v.graphlib.Graph;t.setGraph({rankdir:"TB",nodesep:80,ranksep:90,marginx:40,marginy:40}),t.setDefaultEdgeLabel(()=>({}));for(const s of e.nodes)t.setNode(s.id,{width:b,height:y+20});for(const s of e.edges)t.setEdge(s.source,s.target);v.layout(t);const n=new Map;for(const s of e.nodes){const l=t.node(s.id);l&&n.set(s.id,{x:l.x,y:l.y})}const d=new Map;for(const s of e.edges){const l=`${s.source}->${s.target}`,u=t.edge(s.source,s.target);u!=null&&u.points&&d.set(l,u.points)}const a=t.graph(),i=((a==null?void 0:a.width)||400)+80,c=((a==null?void 0:a.height)||300)+80;return{graph:e,nodePositions:n,edgePoints:d,width:i,height:c}}function U({definition:e,trace:t,onNodeClick:n}){const d=f.useRef(null),[a,i]=f.useState({x:0,y:0}),[c,s]=f.useState(1),[l,u]=f.useState(!1),[h,m]=f.useState({x:0,y:0}),g=f.useMemo(()=>{const o=I(e);return H(o)},[e]),S=f.useCallback(o=>{o.preventDefault();const x=o.deltaY>0?.9:1.1;s(p=>Math.max(.3,Math.min(3,p*x)))},[]),M=f.useCallback(o=>{o.button===0&&(u(!0),m({x:o.clientX-a.x,y:o.clientY-a.y}))},[a]),w=f.useCallback(o=>{l&&i({x:o.clientX-h.x,y:o.clientY-h.y})},[l,h]),N=f.useCallback(()=>{u(!1)},[]),$=f.useMemo(()=>{if(!t)return new Set;const o=new Set,x=Array.from(t.visitedStates.keys());for(let p=0;p<x.length-1;p++)o.add(`${x[p]}->${x[p+1]}`);return o},[t]),j=t&&t.visitedStates.size>0;return r.jsxs("div",{className:"relative w-full h-full min-h-[400px] border rounded-md bg-background/50 overflow-hidden select-none",children:[r.jsx("svg",{ref:d,className:"w-full h-full cursor-grab active:cursor-grabbing",viewBox:`0 0 ${g.width} ${g.height}`,onWheel:S,onMouseDown:M,onMouseMove:w,onMouseUp:N,onMouseLeave:N,children:r.jsxs("g",{transform:`translate(${a.x}, ${a.y}) scale(${c})`,children:[g.graph.edges.map(o=>{const x=`${o.source}->${o.target}`,p=g.edgePoints.get(x);return p?r.jsx(O,{edge:o,points:p,highlighted:j?$.has(x):void 0},x):null}),g.graph.nodes.map(o=>{const x=g.nodePositions.get(o.id);return x?r.jsx("g",{onClick:()=>n==null?void 0:n(o.id),className:n?"cursor-pointer":"",children:r.jsx(B,{node:o,x:x.x,y:x.y,isStart:o.id===g.graph.startAt,traceStatus:j?t.visitedStates.get(o.id):void 0})},o.id):null})]})}),r.jsxs("div",{className:"absolute bottom-2 left-2 flex flex-wrap gap-1",children:[r.jsxs(k,{variant:"outline",className:"text-[10px] px-1.5 py-0 bg-background/80",children:[r.jsx("div",{className:"w-2 h-2 rounded-sm bg-blue-500 mr-1"}),"Task"]}),r.jsxs(k,{variant:"outline",className:"text-[10px] px-1.5 py-0 bg-background/80",children:[r.jsx("div",{className:"w-2 h-2 rounded-sm bg-amber-500 mr-1"}),"Choice"]}),r.jsxs(k,{variant:"outline",className:"text-[10px] px-1.5 py-0 bg-background/80",children:[r.jsx("div",{className:"w-2 h-2 rounded-sm bg-green-500 mr-1"}),"Succeed"]}),r.jsxs(k,{variant:"outline",className:"text-[10px] px-1.5 py-0 bg-background/80",children:[r.jsx("div",{className:"w-2 h-2 rounded-sm bg-red-500 mr-1"}),"Fail"]})]}),r.jsxs("div",{className:"absolute top-2 right-2 flex gap-1",children:[r.jsx("button",{className:"w-6 h-6 rounded border bg-background/80 text-xs flex items-center justify-center hover:bg-accent",onClick:()=>s(o=>Math.min(3,o*1.2)),children:"+"}),r.jsx("button",{className:"w-6 h-6 rounded border bg-background/80 text-xs flex items-center justify-center hover:bg-accent",onClick:()=>s(o=>Math.max(.3,o*.8)),children:"−"}),r.jsx("button",{className:"w-6 h-6 rounded border bg-background/80 text-xs flex items-center justify-center hover:bg-accent",onClick:()=>{s(1),i({x:0,y:0})},children:"⟲"})]})]})}export{U as default};
|