stackport 0.4.4__tar.gz → 0.4.5__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.
Files changed (150) hide show
  1. {stackport-0.4.4/stackport.egg-info → stackport-0.4.5}/PKG-INFO +1 -1
  2. {stackport-0.4.4 → stackport-0.4.5}/backend/main.py +2 -1
  3. stackport-0.4.5/backend/routes/kms.py +163 -0
  4. {stackport-0.4.4 → stackport-0.4.5}/pyproject.toml +1 -1
  5. {stackport-0.4.4 → stackport-0.4.5/stackport.egg-info}/PKG-INFO +1 -1
  6. {stackport-0.4.4 → stackport-0.4.5}/stackport.egg-info/SOURCES.txt +28 -26
  7. stackport-0.4.5/tests/test_kms_routes.py +275 -0
  8. stackport-0.4.4/ui/dist/assets/CloudscapeAbout-BRbsDskR.js → stackport-0.4.5/ui/dist/assets/CloudscapeAbout-D8bVx2o1.js +1 -1
  9. stackport-0.4.4/ui/dist/assets/CloudscapeDashboard-j2f4Elz2.js → stackport-0.4.5/ui/dist/assets/CloudscapeDashboard-Cb3SVBox.js +1 -1
  10. stackport-0.4.4/ui/dist/assets/CloudscapeLearn-CVW3k3X0.js → stackport-0.4.5/ui/dist/assets/CloudscapeLearn-DhnltSEL.js +1 -1
  11. stackport-0.4.4/ui/dist/assets/CloudscapeResourceBrowser-CgWdgn20.js → stackport-0.4.5/ui/dist/assets/CloudscapeResourceBrowser-BNBaU_zZ.js +11 -11
  12. stackport-0.4.4/ui/dist/assets/CloudscapeSettings-CIXFvPKv.js → stackport-0.4.5/ui/dist/assets/CloudscapeSettings-BMXbMPTT.js +1 -1
  13. stackport-0.4.4/ui/dist/assets/CloudscapeShell-DNfhSg0O.js → stackport-0.4.5/ui/dist/assets/CloudscapeShell-Dd2hVOGq.js +1 -1
  14. stackport-0.4.4/ui/dist/assets/StateMachineGraph-DvF_kIOO.js → stackport-0.4.5/ui/dist/assets/StateMachineGraph-gL9KABj0.js +1 -1
  15. stackport-0.4.4/ui/dist/assets/index-BkSgVQ0X.js → stackport-0.4.5/ui/dist/assets/index-BBLXT2u8.js +1 -1
  16. stackport-0.4.4/ui/dist/assets/index-BLdRlH9d.js → stackport-0.4.5/ui/dist/assets/index-BFIBfRr-.js +13 -13
  17. stackport-0.4.4/ui/dist/assets/index-Ba-oOcd3.js → stackport-0.4.5/ui/dist/assets/index-Blob5IHY.js +1 -1
  18. stackport-0.4.4/ui/dist/assets/index-Dw6TGweb.js → stackport-0.4.5/ui/dist/assets/index-CN6POuiI.js +1 -1
  19. stackport-0.4.4/ui/dist/assets/index-BC7E-Fm4.js → stackport-0.4.5/ui/dist/assets/index-D8EPia5i.js +1 -1
  20. stackport-0.4.4/ui/dist/assets/index-C5z3cir-.js → stackport-0.4.5/ui/dist/assets/index-u1-TTPf4.js +1 -1
  21. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/index.html +1 -1
  22. {stackport-0.4.4 → stackport-0.4.5}/LICENSE +0 -0
  23. {stackport-0.4.4 → stackport-0.4.5}/MANIFEST.in +0 -0
  24. {stackport-0.4.4 → stackport-0.4.5}/README.md +0 -0
  25. {stackport-0.4.4 → stackport-0.4.5}/backend/__init__.py +0 -0
  26. {stackport-0.4.4 → stackport-0.4.5}/backend/aws_client.py +0 -0
  27. {stackport-0.4.4 → stackport-0.4.5}/backend/cache.py +0 -0
  28. {stackport-0.4.4 → stackport-0.4.5}/backend/cli.py +0 -0
  29. {stackport-0.4.4 → stackport-0.4.5}/backend/config.py +0 -0
  30. {stackport-0.4.4 → stackport-0.4.5}/backend/endpoint_store.py +0 -0
  31. {stackport-0.4.4 → stackport-0.4.5}/backend/learn/__init__.py +0 -0
  32. {stackport-0.4.4 → stackport-0.4.5}/backend/learn/content.py +0 -0
  33. {stackport-0.4.4 → stackport-0.4.5}/backend/learn/hotspots.py +0 -0
  34. {stackport-0.4.4 → stackport-0.4.5}/backend/learn/trails/aws-first-steps.json +0 -0
  35. {stackport-0.4.4 → stackport-0.4.5}/backend/learn/verify.py +0 -0
  36. {stackport-0.4.4 → stackport-0.4.5}/backend/learn_store.py +0 -0
  37. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/__init__.py +0 -0
  38. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/common.py +0 -0
  39. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/dynamodb.py +0 -0
  40. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/ec2.py +0 -0
  41. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/endpoints.py +0 -0
  42. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/iam.py +0 -0
  43. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/lambda_svc.py +0 -0
  44. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/learn.py +0 -0
  45. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/logs.py +0 -0
  46. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/monitoring.py +0 -0
  47. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/rds.py +0 -0
  48. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/resources.py +0 -0
  49. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/s3.py +0 -0
  50. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/secretsmanager.py +0 -0
  51. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/sns.py +0 -0
  52. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/sqs.py +0 -0
  53. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/stats.py +0 -0
  54. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/stepfunctions.py +0 -0
  55. {stackport-0.4.4 → stackport-0.4.5}/backend/routes/tags.py +0 -0
  56. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/__init__.py +0 -0
  57. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/dynamodb.py +0 -0
  58. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/endpoints.py +0 -0
  59. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/lambda_svc.py +0 -0
  60. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/learn.py +0 -0
  61. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/logs.py +0 -0
  62. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/monitoring.py +0 -0
  63. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/s3.py +0 -0
  64. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/secretsmanager.py +0 -0
  65. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/sns.py +0 -0
  66. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/sqs.py +0 -0
  67. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/stepfunctions.py +0 -0
  68. {stackport-0.4.4 → stackport-0.4.5}/backend/schemas/tags.py +0 -0
  69. {stackport-0.4.4 → stackport-0.4.5}/backend/websocket.py +0 -0
  70. {stackport-0.4.4 → stackport-0.4.5}/setup.cfg +0 -0
  71. {stackport-0.4.4 → stackport-0.4.5}/stackport.egg-info/dependency_links.txt +0 -0
  72. {stackport-0.4.4 → stackport-0.4.5}/stackport.egg-info/entry_points.txt +0 -0
  73. {stackport-0.4.4 → stackport-0.4.5}/stackport.egg-info/requires.txt +0 -0
  74. {stackport-0.4.4 → stackport-0.4.5}/stackport.egg-info/top_level.txt +0 -0
  75. {stackport-0.4.4 → stackport-0.4.5}/tests/test_cache.py +0 -0
  76. {stackport-0.4.4 → stackport-0.4.5}/tests/test_cli.py +0 -0
  77. {stackport-0.4.4 → stackport-0.4.5}/tests/test_client.py +0 -0
  78. {stackport-0.4.4 → stackport-0.4.5}/tests/test_config.py +0 -0
  79. {stackport-0.4.4 → stackport-0.4.5}/tests/test_create_resources.py +0 -0
  80. {stackport-0.4.4 → stackport-0.4.5}/tests/test_dynamodb_routes.py +0 -0
  81. {stackport-0.4.4 → stackport-0.4.5}/tests/test_ec2_routes.py +0 -0
  82. {stackport-0.4.4 → stackport-0.4.5}/tests/test_endpoint_store.py +0 -0
  83. {stackport-0.4.4 → stackport-0.4.5}/tests/test_endpoints.py +0 -0
  84. {stackport-0.4.4 → stackport-0.4.5}/tests/test_iam_routes.py +0 -0
  85. {stackport-0.4.4 → stackport-0.4.5}/tests/test_lambda_routes.py +0 -0
  86. {stackport-0.4.4 → stackport-0.4.5}/tests/test_learn_content.py +0 -0
  87. {stackport-0.4.4 → stackport-0.4.5}/tests/test_learn_routes.py +0 -0
  88. {stackport-0.4.4 → stackport-0.4.5}/tests/test_learn_store.py +0 -0
  89. {stackport-0.4.4 → stackport-0.4.5}/tests/test_logs_routes.py +0 -0
  90. {stackport-0.4.4 → stackport-0.4.5}/tests/test_logs_tail.py +0 -0
  91. {stackport-0.4.4 → stackport-0.4.5}/tests/test_monitoring_routes.py +0 -0
  92. {stackport-0.4.4 → stackport-0.4.5}/tests/test_multi_endpoint.py +0 -0
  93. {stackport-0.4.4 → stackport-0.4.5}/tests/test_rds_routes.py +0 -0
  94. {stackport-0.4.4 → stackport-0.4.5}/tests/test_readonly_middleware.py +0 -0
  95. {stackport-0.4.4 → stackport-0.4.5}/tests/test_registries.py +0 -0
  96. {stackport-0.4.4 → stackport-0.4.5}/tests/test_routes.py +0 -0
  97. {stackport-0.4.4 → stackport-0.4.5}/tests/test_s3_routes.py +0 -0
  98. {stackport-0.4.4 → stackport-0.4.5}/tests/test_s3_upload_limit_env.py +0 -0
  99. {stackport-0.4.4 → stackport-0.4.5}/tests/test_secretsmanager_routes.py +0 -0
  100. {stackport-0.4.4 → stackport-0.4.5}/tests/test_sns_routes.py +0 -0
  101. {stackport-0.4.4 → stackport-0.4.5}/tests/test_sqs_routes.py +0 -0
  102. {stackport-0.4.4 → stackport-0.4.5}/tests/test_tags_routes.py +0 -0
  103. {stackport-0.4.4 → stackport-0.4.5}/tests/test_websocket.py +0 -0
  104. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/assets/CloudscapeResourceBrowser-CzwePG-V.css +0 -0
  105. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/assets/CloudscapeShell-BXHVWjDE.css +0 -0
  106. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/assets/index-DE--A2EE.css +0 -0
  107. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/assets/index-DKIdXFX0.css +0 -0
  108. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/assets/index-DNBdeKId.css +0 -0
  109. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/assets/index-DoY0HscW.css +0 -0
  110. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/assets/index-DrkkXAoG.css +0 -0
  111. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/assets/index-Vf0UFiCT.css +0 -0
  112. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
  113. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/assets/utils-Cfmwk2UT.js +0 -0
  114. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/acm.svg +0 -0
  115. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/apigateway.svg +0 -0
  116. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/appsync.svg +0 -0
  117. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/athena.svg +0 -0
  118. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/cloudformation.svg +0 -0
  119. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/cloudfront.svg +0 -0
  120. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/cognito-idp.svg +0 -0
  121. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/dynamodb.svg +0 -0
  122. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/ec2.svg +0 -0
  123. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/ecr.svg +0 -0
  124. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/ecs.svg +0 -0
  125. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/elasticache.svg +0 -0
  126. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
  127. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
  128. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
  129. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/events.svg +0 -0
  130. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/firehose.svg +0 -0
  131. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/glue.svg +0 -0
  132. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/iam.svg +0 -0
  133. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/kinesis.svg +0 -0
  134. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/kms.svg +0 -0
  135. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/lambda.svg +0 -0
  136. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/logs.svg +0 -0
  137. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/monitoring.svg +0 -0
  138. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/rds.svg +0 -0
  139. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/route53.svg +0 -0
  140. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/s3.svg +0 -0
  141. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/secretsmanager.svg +0 -0
  142. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/ses.svg +0 -0
  143. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/sns.svg +0 -0
  144. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/sqs.svg +0 -0
  145. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/ssm.svg +0 -0
  146. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/stepfunctions.svg +0 -0
  147. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/aws-icons/wafv2.svg +0 -0
  148. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/favicon.png +0 -0
  149. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/favicon.svg +0 -0
  150. {stackport-0.4.4 → stackport-0.4.5}/ui/dist/stackport-logo.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.4.4
3
+ Version: 0.4.5
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -17,7 +17,7 @@ from botocore.exceptions import (
17
17
  )
18
18
 
19
19
  from backend.config import LOG_LEVEL, STACKPORT_ALLOW_WRITES, STACKPORT_LEARN, STACKPORT_PORT
20
- from backend.routes import dynamodb, ec2, endpoints, iam, lambda_svc, learn, logs, monitoring, resources, rds, s3, secretsmanager, sns, sqs, stats, stepfunctions, tags
20
+ from backend.routes import dynamodb, ec2, endpoints, iam, lambda_svc, learn, logs, monitoring, resources, rds, s3, secretsmanager, sns, sqs, stats, stepfunctions, tags, kms
21
21
  from backend.websocket import logs_tail_endpoint, probe_loop, websocket_endpoint
22
22
 
23
23
 
@@ -137,6 +137,7 @@ app.include_router(resources.router, prefix="/api")
137
137
  app.include_router(rds.router, prefix="/api/rds", tags=["rds"])
138
138
  app.include_router(monitoring.router, prefix="/api/monitoring", tags=["monitoring"])
139
139
  app.include_router(sns.router, prefix="/api/sns", tags=["sns"])
140
+ app.include_router(kms.router, prefix="/api/kms", tags=["kms"])
140
141
  if STACKPORT_LEARN:
141
142
  app.include_router(learn.router, prefix="/api/learn", tags=["learn"])
142
143
 
@@ -0,0 +1,163 @@
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from backend.routes.common import EndpointInfo, get_endpoint_info
3
+ from backend.aws_client import get_client
4
+ import json, logging
5
+ from botocore.exceptions import ClientError
6
+
7
+ router = APIRouter()
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ @router.get("/keys")
13
+ def list_keys(ep: EndpointInfo = Depends(get_endpoint_info)):
14
+ """List all KMS keys with basic information"""
15
+ try:
16
+ client = get_client("kms", **ep.client_kwargs())
17
+ paginator = client.get_paginator("list_keys")
18
+
19
+ all_keys = []
20
+
21
+ for pages in paginator.paginate():
22
+ for key in pages["Keys"]:
23
+ key_id = key["KeyId"]
24
+
25
+ metadata = client.describe_key(KeyId=key_id)["KeyMetadata"]
26
+
27
+ all_keys.append(
28
+ {
29
+ "status": metadata["KeyState"],
30
+ "creationDate": metadata["CreationDate"],
31
+ "keySpec": metadata["KeySpec"],
32
+ "keyUsage": metadata["KeyUsage"],
33
+ "keyID": key_id,
34
+ "keyArn": metadata["Arn"],
35
+ }
36
+ )
37
+
38
+ return {"keys": all_keys}
39
+ except Exception as e:
40
+ logger.error("Failed to list keys with metadata %s", id, exc_info=True)
41
+ raise HTTPException(status_code=500, detail=str(e))
42
+
43
+
44
+ @router.get("/keys/{id}")
45
+ def get_key_detail(id: str, ep: EndpointInfo = Depends(get_endpoint_info)):
46
+ "Get detailed information for a specific kms key."
47
+ try:
48
+ client = get_client("kms", **ep.client_kwargs())
49
+ metadata = client.describe_key(KeyId=id)["KeyMetadata"]
50
+ tags = client.list_resource_tags(KeyId=id)
51
+ rotation_status = client.get_key_rotation_status(KeyId=id)
52
+ return {
53
+ "status": metadata["KeyState"],
54
+ "expiresAt": metadata.get("ValidTo"),
55
+ "origin": metadata["Origin"],
56
+ "description": metadata.get("Description"),
57
+ "tags": tags["Tags"],
58
+ "rotationStatus": {
59
+ "keyRotationEnabled": rotation_status["KeyRotationEnabled"],
60
+ "rotationPeriodInDays": rotation_status.get("RotationPeriodInDays"),
61
+ "nextRotationDate": rotation_status.get("NextRotationDate"),
62
+ "onDemandRotationStartDate": rotation_status.get(
63
+ "OnDemandRotationStartDate"
64
+ ),
65
+ },
66
+ }
67
+ except HTTPException:
68
+ raise
69
+ except ClientError as e:
70
+ error_code = e.response["Error"]["Code"]
71
+ if error_code == "NotFoundException":
72
+ raise HTTPException(status_code=404, detail=f"Key {id} not found")
73
+ raise HTTPException(status_code=500, detail=str(e))
74
+ except Exception as e:
75
+ logger.error("Failed to get details for %s", id, exc_info=True)
76
+ raise HTTPException(status_code=500, detail=str(e))
77
+
78
+
79
+ @router.get("/keys/{id}/policy")
80
+ def get_key_policy(id: str, ep: EndpointInfo = Depends(get_endpoint_info)):
81
+ "Get key policy for a specific kms key"
82
+ try:
83
+ client = get_client("kms", **ep.client_kwargs())
84
+ policy = client.get_key_policy(KeyId=id)
85
+ policy = json.loads(policy["Policy"])
86
+ return policy
87
+ except HTTPException:
88
+ raise
89
+ except ClientError as e:
90
+ error_code = e.response["Error"]["Code"]
91
+ if error_code == "NotFoundException":
92
+ raise HTTPException(status_code=404, detail=f"Key {id} not found")
93
+ raise HTTPException(status_code=500, detail=str(e))
94
+ except Exception as e:
95
+ logger.error("Failed to get policy document for %s", id, exc_info=True)
96
+ raise HTTPException(status_code=500, detail=str(e))
97
+
98
+
99
+ @router.get("/keys/{id}/grants")
100
+ def list_grants(id: str, ep: EndpointInfo = Depends(get_endpoint_info)):
101
+ "List grants for a specific key"
102
+ try:
103
+ client = get_client("kms", **ep.client_kwargs())
104
+ paginator = client.get_paginator("list_grants")
105
+
106
+ all_grants = []
107
+ for pages in paginator.paginate(KeyId=id):
108
+ for grant in pages["Grants"]:
109
+ all_grants.append(
110
+ {
111
+ "grantID": grant["GrantId"],
112
+ "grantee": grant["GranteePrincipal"],
113
+ "operations": grant["Operations"],
114
+ "creationDate": grant["CreationDate"],
115
+ }
116
+ )
117
+
118
+ return all_grants
119
+ except HTTPException:
120
+ raise
121
+ except ClientError as e:
122
+ error_code = e.response["Error"]["Code"]
123
+ if error_code == "NotFoundException":
124
+ raise HTTPException(status_code=404, detail=f"Key {id} not found")
125
+ if error_code == "InvalidAction":
126
+ logger.warning(
127
+ "ListGrants is not supported by the configured KMS backend",
128
+ exc_info=True,
129
+ )
130
+ return []
131
+ raise HTTPException(status_code=500, detail=str(e))
132
+ except Exception as e:
133
+ logger.error("Failed to list grants for %s", id, exc_info=True)
134
+ raise HTTPException(status_code=500, detail=str(e))
135
+
136
+
137
+ @router.get("/keys/{id}/aliases")
138
+ def get_aliases_detail(id: str, ep: EndpointInfo = Depends(get_endpoint_info)):
139
+ try:
140
+ client = get_client("kms", **ep.client_kwargs())
141
+ paginator = client.get_paginator("list_aliases")
142
+
143
+ all_aliases = []
144
+ for pages in paginator.paginate(KeyId=id):
145
+ for alias in pages["Aliases"]:
146
+ all_aliases.append(
147
+ {
148
+ "aliasName": alias["AliasName"],
149
+ "aliasArn": alias["AliasArn"],
150
+ "creationDate": alias.get("CreationDate"),
151
+ }
152
+ )
153
+ return all_aliases
154
+ except HTTPException:
155
+ raise
156
+ except ClientError as e:
157
+ error_code = e.response["Error"]["Code"]
158
+ if error_code == "NotFoundException":
159
+ raise HTTPException(status_code=404, detail=f"Key {id} not found")
160
+ raise HTTPException(status_code=500, detail=str(e))
161
+ except Exception as e:
162
+ logger.error("Failed to get aliases for %s", id, exc_info=True)
163
+ raise HTTPException(status_code=500, detail=str(e))
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackport"
3
- version = "0.4.4"
3
+ version = "0.4.5"
4
4
  description = "Universal AWS resource browser for local emulators"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.4.4
3
+ Version: 0.4.5
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -15,27 +15,27 @@ backend/../ui/dist/favicon.png
15
15
  backend/../ui/dist/favicon.svg
16
16
  backend/../ui/dist/index.html
17
17
  backend/../ui/dist/stackport-logo.svg
18
- backend/../ui/dist/assets/CloudscapeAbout-BRbsDskR.js
19
- backend/../ui/dist/assets/CloudscapeDashboard-j2f4Elz2.js
20
- backend/../ui/dist/assets/CloudscapeLearn-CVW3k3X0.js
21
- backend/../ui/dist/assets/CloudscapeResourceBrowser-CgWdgn20.js
18
+ backend/../ui/dist/assets/CloudscapeAbout-D8bVx2o1.js
19
+ backend/../ui/dist/assets/CloudscapeDashboard-Cb3SVBox.js
20
+ backend/../ui/dist/assets/CloudscapeLearn-DhnltSEL.js
21
+ backend/../ui/dist/assets/CloudscapeResourceBrowser-BNBaU_zZ.js
22
22
  backend/../ui/dist/assets/CloudscapeResourceBrowser-CzwePG-V.css
23
- backend/../ui/dist/assets/CloudscapeSettings-CIXFvPKv.js
23
+ backend/../ui/dist/assets/CloudscapeSettings-BMXbMPTT.js
24
24
  backend/../ui/dist/assets/CloudscapeShell-BXHVWjDE.css
25
- backend/../ui/dist/assets/CloudscapeShell-DNfhSg0O.js
26
- backend/../ui/dist/assets/StateMachineGraph-DvF_kIOO.js
27
- backend/../ui/dist/assets/index-BC7E-Fm4.js
28
- backend/../ui/dist/assets/index-BLdRlH9d.js
29
- backend/../ui/dist/assets/index-Ba-oOcd3.js
30
- backend/../ui/dist/assets/index-BkSgVQ0X.js
31
- backend/../ui/dist/assets/index-C5z3cir-.js
25
+ backend/../ui/dist/assets/CloudscapeShell-Dd2hVOGq.js
26
+ backend/../ui/dist/assets/StateMachineGraph-gL9KABj0.js
27
+ backend/../ui/dist/assets/index-BBLXT2u8.js
28
+ backend/../ui/dist/assets/index-BFIBfRr-.js
29
+ backend/../ui/dist/assets/index-Blob5IHY.js
30
+ backend/../ui/dist/assets/index-CN6POuiI.js
31
+ backend/../ui/dist/assets/index-D8EPia5i.js
32
32
  backend/../ui/dist/assets/index-DE--A2EE.css
33
33
  backend/../ui/dist/assets/index-DKIdXFX0.css
34
34
  backend/../ui/dist/assets/index-DNBdeKId.css
35
35
  backend/../ui/dist/assets/index-DoY0HscW.css
36
36
  backend/../ui/dist/assets/index-DrkkXAoG.css
37
- backend/../ui/dist/assets/index-Dw6TGweb.js
38
37
  backend/../ui/dist/assets/index-Vf0UFiCT.css
38
+ backend/../ui/dist/assets/index-u1-TTPf4.js
39
39
  backend/../ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
40
40
  backend/../ui/dist/assets/utils-Cfmwk2UT.js
41
41
  backend/../ui/dist/aws-icons/acm.svg
@@ -83,6 +83,7 @@ backend/routes/dynamodb.py
83
83
  backend/routes/ec2.py
84
84
  backend/routes/endpoints.py
85
85
  backend/routes/iam.py
86
+ backend/routes/kms.py
86
87
  backend/routes/lambda_svc.py
87
88
  backend/routes/learn.py
88
89
  backend/routes/logs.py
@@ -125,6 +126,7 @@ tests/test_ec2_routes.py
125
126
  tests/test_endpoint_store.py
126
127
  tests/test_endpoints.py
127
128
  tests/test_iam_routes.py
129
+ tests/test_kms_routes.py
128
130
  tests/test_lambda_routes.py
129
131
  tests/test_learn_content.py
130
132
  tests/test_learn_routes.py
@@ -148,27 +150,27 @@ ui/dist/favicon.png
148
150
  ui/dist/favicon.svg
149
151
  ui/dist/index.html
150
152
  ui/dist/stackport-logo.svg
151
- ui/dist/assets/CloudscapeAbout-BRbsDskR.js
152
- ui/dist/assets/CloudscapeDashboard-j2f4Elz2.js
153
- ui/dist/assets/CloudscapeLearn-CVW3k3X0.js
154
- ui/dist/assets/CloudscapeResourceBrowser-CgWdgn20.js
153
+ ui/dist/assets/CloudscapeAbout-D8bVx2o1.js
154
+ ui/dist/assets/CloudscapeDashboard-Cb3SVBox.js
155
+ ui/dist/assets/CloudscapeLearn-DhnltSEL.js
156
+ ui/dist/assets/CloudscapeResourceBrowser-BNBaU_zZ.js
155
157
  ui/dist/assets/CloudscapeResourceBrowser-CzwePG-V.css
156
- ui/dist/assets/CloudscapeSettings-CIXFvPKv.js
158
+ ui/dist/assets/CloudscapeSettings-BMXbMPTT.js
157
159
  ui/dist/assets/CloudscapeShell-BXHVWjDE.css
158
- ui/dist/assets/CloudscapeShell-DNfhSg0O.js
159
- ui/dist/assets/StateMachineGraph-DvF_kIOO.js
160
- ui/dist/assets/index-BC7E-Fm4.js
161
- ui/dist/assets/index-BLdRlH9d.js
162
- ui/dist/assets/index-Ba-oOcd3.js
163
- ui/dist/assets/index-BkSgVQ0X.js
164
- ui/dist/assets/index-C5z3cir-.js
160
+ ui/dist/assets/CloudscapeShell-Dd2hVOGq.js
161
+ ui/dist/assets/StateMachineGraph-gL9KABj0.js
162
+ ui/dist/assets/index-BBLXT2u8.js
163
+ ui/dist/assets/index-BFIBfRr-.js
164
+ ui/dist/assets/index-Blob5IHY.js
165
+ ui/dist/assets/index-CN6POuiI.js
166
+ ui/dist/assets/index-D8EPia5i.js
165
167
  ui/dist/assets/index-DE--A2EE.css
166
168
  ui/dist/assets/index-DKIdXFX0.css
167
169
  ui/dist/assets/index-DNBdeKId.css
168
170
  ui/dist/assets/index-DoY0HscW.css
169
171
  ui/dist/assets/index-DrkkXAoG.css
170
- ui/dist/assets/index-Dw6TGweb.js
171
172
  ui/dist/assets/index-Vf0UFiCT.css
173
+ ui/dist/assets/index-u1-TTPf4.js
172
174
  ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
173
175
  ui/dist/assets/utils-Cfmwk2UT.js
174
176
  ui/dist/aws-icons/acm.svg
@@ -0,0 +1,275 @@
1
+ """Tests for KMS API routes."""
2
+
3
+ import os
4
+
5
+ os.environ.setdefault("AWS_ENDPOINT_URL", "http://localhost:4566")
6
+
7
+ from datetime import datetime, timezone
8
+ from unittest.mock import MagicMock, patch
9
+
10
+ from fastapi.testclient import TestClient
11
+
12
+ from backend.main import app
13
+
14
+ client = TestClient(app)
15
+
16
+ NOW = datetime(2024, 6, 1, tzinfo=timezone.utc)
17
+ from botocore.exceptions import ClientError
18
+
19
+
20
+ class TestListKeys:
21
+ @patch("backend.routes.kms.get_client")
22
+ def test_list_keys_empty(self, mock_get_client):
23
+ mock_paginator = MagicMock()
24
+
25
+ mock_paginator.paginate.return_value = [{"Keys": []}]
26
+
27
+ response = client.get("/api/kms/keys")
28
+
29
+ assert response.status_code == 200
30
+ data = response.json()
31
+ assert data["keys"] == []
32
+
33
+ @patch("backend.routes.kms.get_client")
34
+ def test_list_keys_with_data(self, mock_get_client):
35
+ mock_kms = MagicMock()
36
+ mock_get_client.return_value = mock_kms
37
+
38
+ mock_paginator = MagicMock()
39
+
40
+ mock_paginator.paginate.return_value = [
41
+ {
42
+ "Keys": [
43
+ {
44
+ "KeyId": "test-id",
45
+ "KeyArn": "arn:aws:kms:us-east-1:000000000000:key/test-id",
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+
51
+ mock_kms.get_paginator.return_value = mock_paginator
52
+
53
+ mock_kms.describe_key.return_value = {
54
+ "KeyMetadata": {
55
+ "AWSAccountId": "000000000000",
56
+ "KeyId": "test-id",
57
+ "Arn": "arn:aws:kms:us-east-1:000000000000:key/test-id",
58
+ "CreationDate": NOW,
59
+ "Enabled": True,
60
+ "KeyUsage": "ENCRYPT_DECRYPT",
61
+ "KeyState": "Enabled",
62
+ "Origin": "AWS_KMS",
63
+ "KeyManager": "CUSTOMER",
64
+ "CustomerMasterKeySpec": "SYMMETRIC_DEFAULT",
65
+ "KeySpec": "SYMMETRIC_DEFAULT",
66
+ "EncryptionAlgorithms": ["SYMMETRIC_DEFAULT"],
67
+ }
68
+ }
69
+
70
+ response = client.get("/api/kms/keys")
71
+
72
+ assert response.status_code == 200
73
+ data = response.json()
74
+ assert data["keys"][0]["keyID"] == "test-id"
75
+ assert (
76
+ data["keys"][0]["keyArn"]
77
+ == "arn:aws:kms:us-east-1:000000000000:key/test-id"
78
+ )
79
+
80
+
81
+ class TestGetKeyDetail:
82
+ @patch("backend.routes.kms.get_client")
83
+ def test_get_key_detail(self, mock_get_client):
84
+ mock_kms = MagicMock()
85
+ mock_get_client.return_value = mock_kms
86
+ mock_kms.describe_key.return_value = {
87
+ "KeyMetadata": {
88
+ "AWSAccountId": "000000000000",
89
+ "KeyId": "test-id",
90
+ "Arn": "arn:aws:kms:us-east-1:000000000000:key/test-id",
91
+ "CreationDate": NOW,
92
+ "Enabled": True,
93
+ "KeyUsage": "ENCRYPT_DECRYPT",
94
+ "KeyState": "Enabled",
95
+ "Origin": "AWS_KMS",
96
+ "KeyManager": "CUSTOMER",
97
+ "CustomerMasterKeySpec": "SYMMETRIC_DEFAULT",
98
+ "KeySpec": "SYMMETRIC_DEFAULT",
99
+ "EncryptionAlgorithms": ["SYMMETRIC_DEFAULT"],
100
+ }
101
+ }
102
+ mock_kms.list_resource_tags.return_value = {"Tags": []}
103
+ mock_kms.get_key_rotation_status.return_value = {"KeyRotationEnabled": False}
104
+
105
+ response = client.get("/api/kms/keys/test-id")
106
+
107
+ assert response.status_code == 200
108
+ data = response.json()
109
+ assert data["status"] == "Enabled"
110
+ assert data["tags"] == []
111
+ assert data["rotationStatus"].get("keyRotationEnabled") is False
112
+
113
+ @patch("backend.routes.kms.get_client")
114
+ def test_get_key_detail_not_found(self, mock_get_client):
115
+ mock_kms = MagicMock()
116
+ mock_get_client.return_value = mock_kms
117
+ mock_kms.describe_key.side_effect = ClientError(
118
+ error_response={
119
+ "Error": {"Code": "NotFoundException"}
120
+ },
121
+ operation_name="DescribeKey",
122
+ )
123
+
124
+ response = client.get("/api/kms/keys/key-nonexistent")
125
+ print(response.text)
126
+ assert response.status_code == 404
127
+
128
+
129
+ class TestGetKeyPolicy:
130
+ @patch("backend.routes.kms.get_client")
131
+ def test_get_key_policy(self, mock_get_client):
132
+ mock_kms = MagicMock()
133
+ mock_get_client.return_value = mock_kms
134
+ mock_kms.get_key_policy.return_value = {
135
+ "Policy": '{"Version":"2012-10-17","Statement":[{"Sid":"Enable IAM User Permissions","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::000000000000:root"},"Action":"kms:*","Resource":"*"}]}',
136
+ "PolicyName": "default",
137
+ }
138
+
139
+ response = client.get("/api/kms/keys/test-id/policy")
140
+
141
+ assert response.status_code == 200
142
+ data = response.json()
143
+ assert data["Version"] == "2012-10-17"
144
+ assert data["Statement"] == [
145
+ {
146
+ "Sid": "Enable IAM User Permissions",
147
+ "Effect": "Allow",
148
+ "Principal": {"AWS": "arn:aws:iam::000000000000:root"},
149
+ "Action": "kms:*",
150
+ "Resource": "*",
151
+ }
152
+ ]
153
+
154
+ @patch("backend.routes.kms.get_client")
155
+ def test_get_key_policy_not_found(self, mock_get_client):
156
+ mock_kms = MagicMock()
157
+ mock_get_client.return_value = mock_kms
158
+ mock_kms.get_key_policy.side_effect = ClientError(
159
+ error_response={
160
+ "Error": {"Code": "NotFoundException"}
161
+ },
162
+ operation_name="GetKeyPolicy",
163
+ )
164
+ response = client.get("/api/kms/keys/test-id/policy")
165
+ assert response.status_code == 404
166
+
167
+
168
+ class TestListGrants:
169
+ @patch("backend.routes.kms.get_client")
170
+ def test_list_grants(self, mock_get_client):
171
+ mock_kms = MagicMock()
172
+ mock_get_client.return_value = mock_kms
173
+ mock_paginator = MagicMock()
174
+ mock_kms.get_paginator.return_value = mock_paginator
175
+
176
+ mock_paginator.paginate.return_value = [{
177
+ "Grants": [
178
+ {
179
+ "KeyId": "arn:aws:kms:us-east-1:000000000000:key/test-key",
180
+ "GrantId": "test-id",
181
+ "CreationDate": NOW,
182
+ "GranteePrincipal": "arn:aws:iam::000000000000:user/test-user",
183
+ "Operations": ["Encrypt", "Decrypt"],
184
+ }
185
+ ]
186
+ }]
187
+
188
+ response = client.get("/api/kms/keys/test-key/grants")
189
+ assert response.status_code == 200
190
+ data = response.json()
191
+ assert data[0]["grantID"] == "test-id"
192
+ assert data[0]["grantee"] == "arn:aws:iam::000000000000:user/test-user"
193
+
194
+ @patch("backend.routes.kms.get_client")
195
+ def test_list_grants_empty(self, mock_get_client):
196
+ mock_paginator = MagicMock()
197
+
198
+ mock_paginator.paginate.return_value = [{"Grants": []}]
199
+
200
+ response = client.get("/api/kms/keys/test-key/grants")
201
+
202
+ assert response.status_code == 200
203
+
204
+ @patch("backend.routes.kms.get_client")
205
+ def test_list_grants_not_found(self, mock_get_client):
206
+ mock_kms = MagicMock()
207
+ mock_paginator = MagicMock()
208
+ mock_kms.get_paginator.return_value = mock_paginator
209
+ mock_get_client.return_value = mock_kms
210
+ mock_paginator.paginate.side_effect = ClientError(
211
+ error_response={
212
+ "Error": {"Code": "NotFoundException"}
213
+ },
214
+ operation_name="ListGrants",
215
+ )
216
+ response = client.get("/api/kms/keys/test-key/grants")
217
+
218
+ assert response.status_code == 404
219
+
220
+
221
+ class TestGetAliasesDetail:
222
+ @patch("backend.routes.kms.get_client")
223
+ def test_get_aliases_detail(self, mock_get_client):
224
+ mock_paginator = MagicMock()
225
+
226
+ mock_kms = MagicMock()
227
+ mock_get_client.return_value = mock_kms
228
+
229
+ mock_kms.get_paginator.return_value = mock_paginator
230
+
231
+ mock_paginator.paginate.return_value = [{
232
+ "Aliases": [
233
+ {
234
+ "AliasName": "alias/test-alias",
235
+ "AliasArn": "arn:aws:kms:us-east-1:000000000000:alias/test-alias",
236
+ "TargetKeyId": "test-key",
237
+ "CreationDate": NOW,
238
+ }
239
+ ]
240
+ }]
241
+
242
+ response = client.get("/api/kms/keys/test-key/aliases")
243
+
244
+ assert response.status_code == 200
245
+ data = response.json()
246
+ assert data[0]["aliasName"] == "alias/test-alias"
247
+ assert (
248
+ data[0]["aliasArn"] == "arn:aws:kms:us-east-1:000000000000:alias/test-alias"
249
+ )
250
+
251
+ @patch("backend.routes.kms.get_client")
252
+ def test_get_aliases_detail_empty(self, mock_get_client):
253
+ mock_paginator = MagicMock()
254
+
255
+ mock_paginator.paginate.return_value = [{"Aliases": []}]
256
+
257
+ response = client.get("/api/kms/keys/test-key/aliases")
258
+
259
+ assert response.status_code == 200
260
+
261
+ @patch("backend.routes.kms.get_client")
262
+ def test_get_aliases_not_found(self, mock_get_client):
263
+ mock_kms = MagicMock()
264
+ mock_paginator = MagicMock()
265
+ mock_kms.get_paginator.return_value = mock_paginator
266
+ mock_get_client.return_value = mock_kms
267
+ mock_paginator.paginate.side_effect = ClientError(
268
+ error_response={
269
+ "Error": {"Code": "NotFoundException"}
270
+ },
271
+ operation_name="ListAliases",
272
+ )
273
+ response = client.get("/api/kms/keys/test-key/aliases")
274
+
275
+ assert response.status_code == 404
@@ -1 +1 @@
1
- import{b as j,r as m,dY as f,u as y,j as e,S as o,B as p,A as b,e as i,d as u,L as v}from"./index-BLdRlH9d.js";import{C as S,H as a,K as d,B as h}from"./CloudscapeShell-DNfhSg0O.js";import{C}from"./index-C5z3cir-.js";import{f as g}from"./utils-Cfmwk2UT.js";const k=[{label:"GitHub Repository",url:"https://github.com/DaviReisVieira/stackport"},{label:"Report an Issue",url:"https://github.com/DaviReisVieira/stackport/issues"}];function r(t,n){return e.jsxs("div",{children:[e.jsx(u,{variant:"awsui-key-label",children:t}),e.jsx(u,{fontSize:"body-s",children:n})]},t)}function z(){const{activeEndpoint:t}=j(),n=m.useCallback(()=>f(t),[t]),{data:s,error:c,refresh:x}=y(n,1e4);return e.jsx(S,{activeHref:"/about",children:e.jsxs(o,{size:"l",children:[e.jsx(a,{variant:"h1",description:"System information and project details",actions:e.jsx(p,{iconName:"refresh",onClick:()=>x(),ariaLabel:"Refresh system info"}),children:"About"}),c&&!s&&e.jsx(b,{type:"error",header:"Failed to load system info",action:e.jsx(p,{onClick:()=>x(),children:"Retry"}),children:c}),!s&&!c&&e.jsx(i,{type:"loading",children:"Loading system info"}),s&&e.jsxs(C,{columns:3,children:[e.jsx(d,{header:e.jsx(a,{variant:"h3",children:"Project"}),children:e.jsxs(o,{size:"s",children:[r("Name","StackPort"),r("Version",e.jsx(h,{color:"blue",children:s.version})),r("License","MIT"),e.jsx(u,{color:"text-body-secondary",fontSize:"body-s",children:"Universal AWS resource browser for local emulators"})]})}),e.jsx(d,{header:e.jsx(a,{variant:"h3",children:"Connection"}),children:e.jsxs(o,{size:"s",children:[r("Mode",s.connection_type==="local"?e.jsx(h,{color:"grey",children:"Local Emulator"}):e.jsx(h,{color:"severity-medium",children:"Real AWS"})),r("Endpoint",s.endpoint_url??"AWS (default)"),r("Region",s.region),r("Services",String(s.services_count)),r("Writes",s.writes_enabled?e.jsx(i,{type:"success",children:"Enabled"}):e.jsx(i,{type:"stopped",children:"Disabled"})),r("Uptime",g(s.uptime_seconds)),r("Status",s.status==="ok"?e.jsx(i,{type:"success",children:"ok"}):e.jsx(i,{type:"error",children:s.status}))]})}),e.jsx(d,{header:e.jsx(a,{variant:"h3",children:"Links"}),children:e.jsx(o,{size:"s",children:k.map(l=>e.jsx(v,{href:l.url,external:!0,children:l.label},l.url))})})]})]})})}export{z as default};
1
+ import{b as j,r as m,e1 as f,u as y,j as e,S as o,B as p,A as b,e as i,d as u,L as v}from"./index-BFIBfRr-.js";import{C as S,H as a,K as d,B as h}from"./CloudscapeShell-Dd2hVOGq.js";import{C}from"./index-u1-TTPf4.js";import{f as g}from"./utils-Cfmwk2UT.js";const k=[{label:"GitHub Repository",url:"https://github.com/DaviReisVieira/stackport"},{label:"Report an Issue",url:"https://github.com/DaviReisVieira/stackport/issues"}];function r(t,n){return e.jsxs("div",{children:[e.jsx(u,{variant:"awsui-key-label",children:t}),e.jsx(u,{fontSize:"body-s",children:n})]},t)}function z(){const{activeEndpoint:t}=j(),n=m.useCallback(()=>f(t),[t]),{data:s,error:c,refresh:x}=y(n,1e4);return e.jsx(S,{activeHref:"/about",children:e.jsxs(o,{size:"l",children:[e.jsx(a,{variant:"h1",description:"System information and project details",actions:e.jsx(p,{iconName:"refresh",onClick:()=>x(),ariaLabel:"Refresh system info"}),children:"About"}),c&&!s&&e.jsx(b,{type:"error",header:"Failed to load system info",action:e.jsx(p,{onClick:()=>x(),children:"Retry"}),children:c}),!s&&!c&&e.jsx(i,{type:"loading",children:"Loading system info"}),s&&e.jsxs(C,{columns:3,children:[e.jsx(d,{header:e.jsx(a,{variant:"h3",children:"Project"}),children:e.jsxs(o,{size:"s",children:[r("Name","StackPort"),r("Version",e.jsx(h,{color:"blue",children:s.version})),r("License","MIT"),e.jsx(u,{color:"text-body-secondary",fontSize:"body-s",children:"Universal AWS resource browser for local emulators"})]})}),e.jsx(d,{header:e.jsx(a,{variant:"h3",children:"Connection"}),children:e.jsxs(o,{size:"s",children:[r("Mode",s.connection_type==="local"?e.jsx(h,{color:"grey",children:"Local Emulator"}):e.jsx(h,{color:"severity-medium",children:"Real AWS"})),r("Endpoint",s.endpoint_url??"AWS (default)"),r("Region",s.region),r("Services",String(s.services_count)),r("Writes",s.writes_enabled?e.jsx(i,{type:"success",children:"Enabled"}):e.jsx(i,{type:"stopped",children:"Disabled"})),r("Uptime",g(s.uptime_seconds)),r("Status",s.status==="ok"?e.jsx(i,{type:"success",children:"ok"}):e.jsx(i,{type:"error",children:s.status}))]})}),e.jsx(d,{header:e.jsx(a,{variant:"h3",children:"Links"}),children:e.jsx(o,{size:"s",children:k.map(l=>e.jsx(v,{href:l.url,external:!0,children:l.label},l.url))})})]})]})})}export{z as default};
@@ -1 +1 @@
1
- import{r as a,u as L,a as W,b as O,c as A,f as D,j as t,S as $,A as E,B as R,d,e as F,L as M}from"./index-BLdRlH9d.js";import{u as P,a as H,H as B,C as U,B as _,g as V}from"./CloudscapeShell-DNfhSg0O.js";import{S as J,C as G,T as K}from"./index-BkSgVQ0X.js";import{C as q}from"./index-C5z3cir-.js";import{C as Q}from"./index-Ba-oOcd3.js";import{T as X}from"./index-BC7E-Fm4.js";import{f as Y}from"./utils-Cfmwk2UT.js";function Z({fallbackFetcher:n,fallbackInterval:r=5e3,messageType:i,endpoint:x}){const[s,g]=a.useState(null),[y,j]=a.useState(!1),[p,C]=a.useState(!1),h=a.useRef(null),c=a.useRef(0),b=a.useRef(null),l=a.useRef(),k=3;a.useEffect(()=>{let v=!1;c.current=0;function S(){var o;if(v||((o=h.current)==null?void 0:o.readyState)===WebSocket.OPEN)return;const m=window.location.protocol==="https:"?"wss:":"ws:",e=new WebSocket(`${m}//${window.location.host}/ws`);h.current=e,e.onopen=()=>{if(v){e.close();return}j(!0),c.current=0,e.send(JSON.stringify({type:"subscribe",services:["all"],endpoint:x??void 0}))},e.onmessage=w=>{try{const f=JSON.parse(w.data);f.type===i&&f.data&&g(f.data)}catch{}},e.onclose=()=>{if(j(!1),h.current=null,!v)if(c.current++,c.current>=k)C(!0);else{const w=Math.min(1e3*Math.pow(2,c.current),3e4);b.current=setTimeout(()=>{var f;return(f=l.current)==null?void 0:f.call(l)},w)}},e.onerror=()=>{e.close()}}return l.current=S,S(),()=>{var m;v=!0,b.current&&clearTimeout(b.current),(m=h.current)==null||m.close(),g(null),C(!1)}},[i,x]);const u=L(n,p?r:void 0),T=s??u.data,N=!T&&u.loading;return{data:T,loading:N,error:p?u.error:null,connected:y,refresh:u.refresh}}function ee(){return localStorage.getItem("stackport:view-mode")==="list"?"list":"grid"}function z(n,r){const i=V(n.name);return t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx(i,{className:"h-5 w-5 flex-shrink-0"}),t.jsx(M,{fontSize:"heading-m",href:`/resources/${n.name}`,onFollow:x=>{x.preventDefault(),r(n.name)},children:n.name})]})}function I(n){return n.total===0?t.jsx(d,{color:"text-status-inactive",children:"No resources"}):t.jsx($,{direction:"horizontal",size:"xs",children:Object.entries(n.resources).filter(([,r])=>r>0).map(([r,i])=>t.jsxs(_,{color:"grey",children:[r,": ",i]},r))})}function ce(){const n=W(),{activeEndpoint:r}=O(),{data:i}=A(),x=a.useCallback(()=>D(r),[r]),{data:s,loading:g,error:y,connected:j,refresh:p}=Z({fallbackFetcher:x,fallbackInterval:5e3,messageType:"stats",endpoint:r}),{toggleFavorite:C,isFavorite:h}=P(),[c,b]=a.useState(""),[l,k]=a.useState(ee);a.useEffect(()=>{localStorage.setItem("stackport:view-mode",l)},[l]),H([{key:"r",handler:()=>p()},{key:"v",handler:()=>k(e=>e==="grid"?"list":"grid")}]);const u=a.useMemo(()=>s?Object.entries(s.services).map(([e,o])=>({name:e,status:o.status,resources:o.resources,total:Object.values(o.resources).reduce((w,f)=>w+f,0),favorite:h(e)})).filter(e=>e.name.toLowerCase().includes(c.toLowerCase())).sort((e,o)=>Number(o.favorite)-Number(e.favorite)||o.total-e.total||e.name.localeCompare(o.name)):[],[s,c,h]),T=s?Object.values(s.services).filter(e=>e.status==="available").length:0,N=a.useCallback(e=>n(`/resources/${e}`),[n]),v=e=>t.jsx(R,{variant:"inline-icon",iconName:e.favorite?"star-filled":"star",ariaLabel:e.favorite?`Remove ${e.name} from favorites`:`Add ${e.name} to favorites`,onClick:()=>C(e.name)}),S=t.jsx(B,{variant:"h2",counter:s?`(${u.length})`:void 0,actions:t.jsx(J,{selectedId:l,onChange:({detail:e})=>k(e.selectedId),label:"View mode",options:[{id:"grid",text:"Grid"},{id:"list",text:"List"}]}),children:"Services"}),m=t.jsx(K,{filteringText:c,filteringPlaceholder:"Find a service",onChange:({detail:e})=>b(e.filteringText)});return t.jsx(U,{activeHref:"/",children:t.jsx(Q,{header:t.jsx(B,{variant:"h1",description:r?`Connected to ${r}`:i!=null&&i.endpoint_url?`Connected to ${i.endpoint_url}`:"Using the default endpoint",actions:t.jsxs($,{direction:"horizontal",size:"xs",children:[t.jsx(F,{type:j?"success":"in-progress",children:j?"Live":"Polling"}),t.jsx(R,{iconName:"refresh",onClick:()=>p(),loading:g,children:"Refresh"})]}),children:"Dashboard"}),children:t.jsxs($,{size:"l",children:[!s&&y&&t.jsx(E,{type:"error",header:"Could not reach the endpoint",action:t.jsx(R,{onClick:()=>p(),children:"Retry"}),children:y}),t.jsxs(q,{columns:3,variant:"text-grid",children:[t.jsxs("div",{children:[t.jsx(d,{variant:"awsui-key-label",children:"Services available"}),t.jsx(d,{fontSize:"display-l",fontWeight:"bold",children:s?`${T}/${Object.keys(s.services).length}`:"—"})]}),t.jsxs("div",{children:[t.jsx(d,{variant:"awsui-key-label",children:"Total resources"}),t.jsx(d,{fontSize:"display-l",fontWeight:"bold",children:s?s.total_resources:"—"})]}),t.jsxs("div",{children:[t.jsx(d,{variant:"awsui-key-label",children:"Uptime"}),t.jsx(d,{fontSize:"display-l",fontWeight:"bold",children:s?Y(s.uptime_seconds):"—"})]})]}),l==="grid"?t.jsx(G,{items:u,loading:g&&!s,loadingText:"Probing services",trackBy:"name",cardsPerRow:[{cards:1},{minWidth:500,cards:2},{minWidth:900,cards:3}],header:S,filter:m,cardDefinition:{header:e=>t.jsxs("div",{className:"flex items-center justify-between gap-2",children:[z(e,N),v(e)]}),sections:[{id:"status",content:e=>t.jsx(F,{type:e.status==="available"?"success":"error",children:e.status==="available"?"Available":"Unavailable"})},{id:"resources",header:"Resources",content:e=>I(e)}]},empty:t.jsx(d,{textAlign:"center",children:"No services match the filter"})}):t.jsx(X,{items:u,loading:g&&!s,loadingText:"Probing services",trackBy:"name",header:S,filter:m,variant:"container",columnDefinitions:[{id:"favorite",header:"",width:60,cell:e=>v(e)},{id:"service",header:"Service",cell:e=>z(e,N),sortingField:"name"},{id:"status",header:"Status",cell:e=>t.jsx(F,{type:e.status==="available"?"success":"error",children:e.status==="available"?"Available":"Unavailable"})},{id:"resources",header:"Resources",cell:e=>I(e)},{id:"total",header:"Total",width:90,cell:e=>e.total}],empty:t.jsx(d,{textAlign:"center",children:"No services match the filter"})})]})})})}export{ce as default};
1
+ import{r as a,u as L,a as W,b as O,c as A,f as D,j as t,S as $,A as E,B as R,d,e as F,L as M}from"./index-BFIBfRr-.js";import{u as P,a as H,H as B,C as U,B as _,g as V}from"./CloudscapeShell-Dd2hVOGq.js";import{S as J,C as G,T as K}from"./index-BBLXT2u8.js";import{C as q}from"./index-u1-TTPf4.js";import{C as Q}from"./index-Blob5IHY.js";import{T as X}from"./index-D8EPia5i.js";import{f as Y}from"./utils-Cfmwk2UT.js";function Z({fallbackFetcher:n,fallbackInterval:r=5e3,messageType:i,endpoint:x}){const[s,g]=a.useState(null),[y,j]=a.useState(!1),[p,C]=a.useState(!1),h=a.useRef(null),c=a.useRef(0),b=a.useRef(null),l=a.useRef(),k=3;a.useEffect(()=>{let v=!1;c.current=0;function S(){var o;if(v||((o=h.current)==null?void 0:o.readyState)===WebSocket.OPEN)return;const m=window.location.protocol==="https:"?"wss:":"ws:",e=new WebSocket(`${m}//${window.location.host}/ws`);h.current=e,e.onopen=()=>{if(v){e.close();return}j(!0),c.current=0,e.send(JSON.stringify({type:"subscribe",services:["all"],endpoint:x??void 0}))},e.onmessage=w=>{try{const f=JSON.parse(w.data);f.type===i&&f.data&&g(f.data)}catch{}},e.onclose=()=>{if(j(!1),h.current=null,!v)if(c.current++,c.current>=k)C(!0);else{const w=Math.min(1e3*Math.pow(2,c.current),3e4);b.current=setTimeout(()=>{var f;return(f=l.current)==null?void 0:f.call(l)},w)}},e.onerror=()=>{e.close()}}return l.current=S,S(),()=>{var m;v=!0,b.current&&clearTimeout(b.current),(m=h.current)==null||m.close(),g(null),C(!1)}},[i,x]);const u=L(n,p?r:void 0),T=s??u.data,N=!T&&u.loading;return{data:T,loading:N,error:p?u.error:null,connected:y,refresh:u.refresh}}function ee(){return localStorage.getItem("stackport:view-mode")==="list"?"list":"grid"}function z(n,r){const i=V(n.name);return t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx(i,{className:"h-5 w-5 flex-shrink-0"}),t.jsx(M,{fontSize:"heading-m",href:`/resources/${n.name}`,onFollow:x=>{x.preventDefault(),r(n.name)},children:n.name})]})}function I(n){return n.total===0?t.jsx(d,{color:"text-status-inactive",children:"No resources"}):t.jsx($,{direction:"horizontal",size:"xs",children:Object.entries(n.resources).filter(([,r])=>r>0).map(([r,i])=>t.jsxs(_,{color:"grey",children:[r,": ",i]},r))})}function ce(){const n=W(),{activeEndpoint:r}=O(),{data:i}=A(),x=a.useCallback(()=>D(r),[r]),{data:s,loading:g,error:y,connected:j,refresh:p}=Z({fallbackFetcher:x,fallbackInterval:5e3,messageType:"stats",endpoint:r}),{toggleFavorite:C,isFavorite:h}=P(),[c,b]=a.useState(""),[l,k]=a.useState(ee);a.useEffect(()=>{localStorage.setItem("stackport:view-mode",l)},[l]),H([{key:"r",handler:()=>p()},{key:"v",handler:()=>k(e=>e==="grid"?"list":"grid")}]);const u=a.useMemo(()=>s?Object.entries(s.services).map(([e,o])=>({name:e,status:o.status,resources:o.resources,total:Object.values(o.resources).reduce((w,f)=>w+f,0),favorite:h(e)})).filter(e=>e.name.toLowerCase().includes(c.toLowerCase())).sort((e,o)=>Number(o.favorite)-Number(e.favorite)||o.total-e.total||e.name.localeCompare(o.name)):[],[s,c,h]),T=s?Object.values(s.services).filter(e=>e.status==="available").length:0,N=a.useCallback(e=>n(`/resources/${e}`),[n]),v=e=>t.jsx(R,{variant:"inline-icon",iconName:e.favorite?"star-filled":"star",ariaLabel:e.favorite?`Remove ${e.name} from favorites`:`Add ${e.name} to favorites`,onClick:()=>C(e.name)}),S=t.jsx(B,{variant:"h2",counter:s?`(${u.length})`:void 0,actions:t.jsx(J,{selectedId:l,onChange:({detail:e})=>k(e.selectedId),label:"View mode",options:[{id:"grid",text:"Grid"},{id:"list",text:"List"}]}),children:"Services"}),m=t.jsx(K,{filteringText:c,filteringPlaceholder:"Find a service",onChange:({detail:e})=>b(e.filteringText)});return t.jsx(U,{activeHref:"/",children:t.jsx(Q,{header:t.jsx(B,{variant:"h1",description:r?`Connected to ${r}`:i!=null&&i.endpoint_url?`Connected to ${i.endpoint_url}`:"Using the default endpoint",actions:t.jsxs($,{direction:"horizontal",size:"xs",children:[t.jsx(F,{type:j?"success":"in-progress",children:j?"Live":"Polling"}),t.jsx(R,{iconName:"refresh",onClick:()=>p(),loading:g,children:"Refresh"})]}),children:"Dashboard"}),children:t.jsxs($,{size:"l",children:[!s&&y&&t.jsx(E,{type:"error",header:"Could not reach the endpoint",action:t.jsx(R,{onClick:()=>p(),children:"Retry"}),children:y}),t.jsxs(q,{columns:3,variant:"text-grid",children:[t.jsxs("div",{children:[t.jsx(d,{variant:"awsui-key-label",children:"Services available"}),t.jsx(d,{fontSize:"display-l",fontWeight:"bold",children:s?`${T}/${Object.keys(s.services).length}`:"—"})]}),t.jsxs("div",{children:[t.jsx(d,{variant:"awsui-key-label",children:"Total resources"}),t.jsx(d,{fontSize:"display-l",fontWeight:"bold",children:s?s.total_resources:"—"})]}),t.jsxs("div",{children:[t.jsx(d,{variant:"awsui-key-label",children:"Uptime"}),t.jsx(d,{fontSize:"display-l",fontWeight:"bold",children:s?Y(s.uptime_seconds):"—"})]})]}),l==="grid"?t.jsx(G,{items:u,loading:g&&!s,loadingText:"Probing services",trackBy:"name",cardsPerRow:[{cards:1},{minWidth:500,cards:2},{minWidth:900,cards:3}],header:S,filter:m,cardDefinition:{header:e=>t.jsxs("div",{className:"flex items-center justify-between gap-2",children:[z(e,N),v(e)]}),sections:[{id:"status",content:e=>t.jsx(F,{type:e.status==="available"?"success":"error",children:e.status==="available"?"Available":"Unavailable"})},{id:"resources",header:"Resources",content:e=>I(e)}]},empty:t.jsx(d,{textAlign:"center",children:"No services match the filter"})}):t.jsx(X,{items:u,loading:g&&!s,loadingText:"Probing services",trackBy:"name",header:S,filter:m,variant:"container",columnDefinitions:[{id:"favorite",header:"",width:60,cell:e=>v(e)},{id:"service",header:"Service",cell:e=>z(e,N),sortingField:"name"},{id:"status",header:"Status",cell:e=>t.jsx(F,{type:e.status==="available"?"success":"error",children:e.status==="available"?"Available":"Unavailable"})},{id:"resources",header:"Resources",cell:e=>I(e)},{id:"total",header:"Total",width:90,cell:e=>e.total}],empty:t.jsx(d,{textAlign:"center",children:"No services match the filter"})})]})})})}export{ce as default};
@@ -1 +1 @@
1
- import{dZ as S,c as v,r as u,j as e,S as d,A as x,d as a,B as c}from"./index-BLdRlH9d.js";import{C as L}from"./index-C5z3cir-.js";import{C as b,ab as k,K as w,U as R,H as p,ac as A,M as z}from"./CloudscapeShell-DNfhSg0O.js";import{C as B}from"./index-Ba-oOcd3.js";function T(){const{enabled:j,loading:f,trail:s,completed:y,resetTrail:g}=S(),{data:n}=v(),[m,r]=u.useState(!1),[C,h]=u.useState(!1),o=(s==null?void 0:s.lessons.reduce((t,l)=>t+l.steps.length,0))??0,i=Object.values(y).reduce((t,l)=>t+l.length,0);return e.jsxs(b,{activeHref:"/learn",children:[e.jsx(B,{header:e.jsx(p,{variant:"h1",description:"Short hands-on lessons against your own emulator. You do the work in the console or the CLI, and StackPort checks the real resource state, so a step only turns green when the thing genuinely exists.",actions:i>0&&e.jsx(c,{onClick:()=>r(!0),"data-testid":"learn-reset",children:"Reset progress"}),children:"Learn"}),children:e.jsxs(d,{size:"l",children:[(n==null?void 0:n.connection_type)==="aws"&&e.jsx(x,{type:"warning",header:"Lessons need a local emulator",children:"You are connected to real AWS. Lessons create and delete resources, so they stay disabled until you switch to a local endpoint."}),!j&&e.jsxs(x,{type:"info",header:"Learn is turned off",children:["Start StackPort with ",e.jsx(a,{variant:"code",children:"STACKPORT_LEARN=true"})," to use the lessons."]}),f&&!s&&e.jsx(a,{textAlign:"center",padding:"xxl",children:e.jsx(k,{size:"large"})}),s&&e.jsxs(e.Fragment,{children:[e.jsx(w,{header:e.jsx(p,{variant:"h2",children:s.title}),children:e.jsxs(L,{columns:2,variant:"text-grid",children:[e.jsx(a,{variant:"p",color:"text-body-secondary",children:s.description}),e.jsx(R,{value:o>0?i/o*100:0,additionalInfo:`${i} of ${o} steps done`,label:"Your progress"})]})}),e.jsx(d,{size:"l",children:s.lessons.map(t=>e.jsx(A,{lesson:t},t.id))}),e.jsx(a,{variant:"small",color:"text-body-secondary",children:"More lessons are on the way: queues, fan-out, logs and metrics, and a table you query rather than scan."})]})]})}),e.jsx(z,{visible:m,onDismiss:()=>r(!1),header:"Reset your progress?",footer:e.jsx(a,{float:"right",children:e.jsxs(d,{size:"xs",direction:"horizontal",children:[e.jsx(c,{variant:"link",onClick:()=>r(!1),children:"Cancel"}),e.jsx(c,{variant:"primary",loading:C,onClick:async()=>{h(!0);try{await g(),r(!1)}finally{h(!1)}},"data-testid":"learn-reset-confirm",children:"Reset progress"})]})}),children:"Every step goes back to not done, and the lesson picks fresh resource names. Nothing you created in your emulator is deleted."})]})}export{T as default};
1
+ import{e2 as S,c as v,r as u,j as e,S as c,A as x,d as a,B as d}from"./index-BFIBfRr-.js";import{C as L}from"./index-u1-TTPf4.js";import{C as b,ab as k,K as w,U as R,H as p,ac as A,M as z}from"./CloudscapeShell-Dd2hVOGq.js";import{C as B}from"./index-Blob5IHY.js";function T(){const{enabled:j,loading:f,trail:s,completed:y,resetTrail:g}=S(),{data:n}=v(),[m,r]=u.useState(!1),[C,h]=u.useState(!1),o=(s==null?void 0:s.lessons.reduce((t,l)=>t+l.steps.length,0))??0,i=Object.values(y).reduce((t,l)=>t+l.length,0);return e.jsxs(b,{activeHref:"/learn",children:[e.jsx(B,{header:e.jsx(p,{variant:"h1",description:"Short hands-on lessons against your own emulator. You do the work in the console or the CLI, and StackPort checks the real resource state, so a step only turns green when the thing genuinely exists.",actions:i>0&&e.jsx(d,{onClick:()=>r(!0),"data-testid":"learn-reset",children:"Reset progress"}),children:"Learn"}),children:e.jsxs(c,{size:"l",children:[(n==null?void 0:n.connection_type)==="aws"&&e.jsx(x,{type:"warning",header:"Lessons need a local emulator",children:"You are connected to real AWS. Lessons create and delete resources, so they stay disabled until you switch to a local endpoint."}),!j&&e.jsxs(x,{type:"info",header:"Learn is turned off",children:["Start StackPort with ",e.jsx(a,{variant:"code",children:"STACKPORT_LEARN=true"})," to use the lessons."]}),f&&!s&&e.jsx(a,{textAlign:"center",padding:"xxl",children:e.jsx(k,{size:"large"})}),s&&e.jsxs(e.Fragment,{children:[e.jsx(w,{header:e.jsx(p,{variant:"h2",children:s.title}),children:e.jsxs(L,{columns:2,variant:"text-grid",children:[e.jsx(a,{variant:"p",color:"text-body-secondary",children:s.description}),e.jsx(R,{value:o>0?i/o*100:0,additionalInfo:`${i} of ${o} steps done`,label:"Your progress"})]})}),e.jsx(c,{size:"l",children:s.lessons.map(t=>e.jsx(A,{lesson:t},t.id))}),e.jsx(a,{variant:"small",color:"text-body-secondary",children:"More lessons are on the way: queues, fan-out, logs and metrics, and a table you query rather than scan."})]})]})}),e.jsx(z,{visible:m,onDismiss:()=>r(!1),header:"Reset your progress?",footer:e.jsx(a,{float:"right",children:e.jsxs(c,{size:"xs",direction:"horizontal",children:[e.jsx(d,{variant:"link",onClick:()=>r(!1),children:"Cancel"}),e.jsx(d,{variant:"primary",loading:C,onClick:async()=>{h(!0);try{await g(),r(!1)}finally{h(!1)}},"data-testid":"learn-reset-confirm",children:"Reset progress"})]})}),children:"Every step goes back to not done, and the lesson picks fresh resource names. Nothing you created in your emulator is deleted."})]})}export{T as default};