stackport 0.3.8__tar.gz → 0.4.0__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 (130) hide show
  1. {stackport-0.3.8/stackport.egg-info → stackport-0.4.0}/PKG-INFO +6 -17
  2. {stackport-0.3.8 → stackport-0.4.0}/README.md +5 -16
  3. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/logs.py +94 -1
  4. stackport-0.4.0/backend/schemas/logs.py +21 -0
  5. {stackport-0.3.8 → stackport-0.4.0}/pyproject.toml +1 -1
  6. {stackport-0.3.8 → stackport-0.4.0/stackport.egg-info}/PKG-INFO +6 -17
  7. {stackport-0.3.8 → stackport-0.4.0}/stackport.egg-info/SOURCES.txt +43 -6
  8. {stackport-0.3.8 → stackport-0.4.0}/tests/test_logs_routes.py +159 -0
  9. stackport-0.4.0/ui/dist/assets/CloudscapeAbout-BtS8xfBQ.js +1 -0
  10. stackport-0.4.0/ui/dist/assets/CloudscapeDashboard-XTN3dZMK.js +1 -0
  11. stackport-0.4.0/ui/dist/assets/CloudscapeResourceBrowser-BW5xV4W2.css +1 -0
  12. stackport-0.4.0/ui/dist/assets/CloudscapeResourceBrowser-CcOhO1RI.js +16 -0
  13. stackport-0.4.0/ui/dist/assets/CloudscapeSettings-BRB1cWqz.js +1 -0
  14. stackport-0.4.0/ui/dist/assets/CloudscapeShell-C1R1SIYZ.css +1 -0
  15. stackport-0.4.0/ui/dist/assets/CloudscapeShell-DojdksJr.js +200 -0
  16. stackport-0.4.0/ui/dist/assets/StateMachineGraph-BOEoyAZx.js +21 -0
  17. stackport-0.4.0/ui/dist/assets/index-BjtNHAFy.css +1 -0
  18. stackport-0.4.0/ui/dist/assets/index-BqhwBjJc.js +1 -0
  19. stackport-0.4.0/ui/dist/assets/index-C7soqJ2K.js +1 -0
  20. stackport-0.4.0/ui/dist/assets/index-CCLJtnX8.js +1 -0
  21. stackport-0.4.0/ui/dist/assets/index-Cah5DqCf.css +1 -0
  22. stackport-0.4.0/ui/dist/assets/index-DE--A2EE.css +1 -0
  23. stackport-0.4.0/ui/dist/assets/index-DOVP6iM8.js +52 -0
  24. stackport-0.4.0/ui/dist/assets/index-DRZKHoZl.css +1 -0
  25. stackport-0.4.0/ui/dist/assets/index-DaXIyw62.css +1 -0
  26. stackport-0.4.0/ui/dist/assets/index-Dj8ImrkQ.js +1 -0
  27. stackport-0.4.0/ui/dist/assets/index-lN483PN7.js +1 -0
  28. stackport-0.4.0/ui/dist/assets/utils-Cfmwk2UT.js +1 -0
  29. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/index.html +2 -2
  30. stackport-0.4.0/ui/dist/stackport-logo.svg +68 -0
  31. stackport-0.3.8/ui/dist/assets/StateMachineGraph-CmFIj15P.js +0 -6
  32. stackport-0.3.8/ui/dist/assets/index-BZr5kWvs.css +0 -1
  33. stackport-0.3.8/ui/dist/assets/index-Dvr-Ft0R.js +0 -652
  34. {stackport-0.3.8 → stackport-0.4.0}/LICENSE +0 -0
  35. {stackport-0.3.8 → stackport-0.4.0}/MANIFEST.in +0 -0
  36. {stackport-0.3.8 → stackport-0.4.0}/backend/__init__.py +0 -0
  37. {stackport-0.3.8 → stackport-0.4.0}/backend/aws_client.py +0 -0
  38. {stackport-0.3.8 → stackport-0.4.0}/backend/cache.py +0 -0
  39. {stackport-0.3.8 → stackport-0.4.0}/backend/cli.py +0 -0
  40. {stackport-0.3.8 → stackport-0.4.0}/backend/config.py +0 -0
  41. {stackport-0.3.8 → stackport-0.4.0}/backend/endpoint_store.py +0 -0
  42. {stackport-0.3.8 → stackport-0.4.0}/backend/main.py +0 -0
  43. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/__init__.py +0 -0
  44. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/common.py +0 -0
  45. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/dynamodb.py +0 -0
  46. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/ec2.py +0 -0
  47. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/endpoints.py +0 -0
  48. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/iam.py +0 -0
  49. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/lambda_svc.py +0 -0
  50. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/rds.py +0 -0
  51. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/resources.py +0 -0
  52. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/s3.py +0 -0
  53. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/secretsmanager.py +0 -0
  54. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/sqs.py +0 -0
  55. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/stats.py +0 -0
  56. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/stepfunctions.py +0 -0
  57. {stackport-0.3.8 → stackport-0.4.0}/backend/routes/tags.py +0 -0
  58. {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/__init__.py +0 -0
  59. {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/dynamodb.py +0 -0
  60. {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/endpoints.py +0 -0
  61. {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/lambda_svc.py +0 -0
  62. {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/s3.py +0 -0
  63. {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/secretsmanager.py +0 -0
  64. {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/sqs.py +0 -0
  65. {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/stepfunctions.py +0 -0
  66. {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/tags.py +0 -0
  67. {stackport-0.3.8 → stackport-0.4.0}/backend/websocket.py +0 -0
  68. {stackport-0.3.8 → stackport-0.4.0}/setup.cfg +0 -0
  69. {stackport-0.3.8 → stackport-0.4.0}/stackport.egg-info/dependency_links.txt +0 -0
  70. {stackport-0.3.8 → stackport-0.4.0}/stackport.egg-info/entry_points.txt +0 -0
  71. {stackport-0.3.8 → stackport-0.4.0}/stackport.egg-info/requires.txt +0 -0
  72. {stackport-0.3.8 → stackport-0.4.0}/stackport.egg-info/top_level.txt +0 -0
  73. {stackport-0.3.8 → stackport-0.4.0}/tests/test_cache.py +0 -0
  74. {stackport-0.3.8 → stackport-0.4.0}/tests/test_cli.py +0 -0
  75. {stackport-0.3.8 → stackport-0.4.0}/tests/test_client.py +0 -0
  76. {stackport-0.3.8 → stackport-0.4.0}/tests/test_config.py +0 -0
  77. {stackport-0.3.8 → stackport-0.4.0}/tests/test_dynamodb_routes.py +0 -0
  78. {stackport-0.3.8 → stackport-0.4.0}/tests/test_ec2_routes.py +0 -0
  79. {stackport-0.3.8 → stackport-0.4.0}/tests/test_endpoint_store.py +0 -0
  80. {stackport-0.3.8 → stackport-0.4.0}/tests/test_endpoints.py +0 -0
  81. {stackport-0.3.8 → stackport-0.4.0}/tests/test_iam_routes.py +0 -0
  82. {stackport-0.3.8 → stackport-0.4.0}/tests/test_lambda_routes.py +0 -0
  83. {stackport-0.3.8 → stackport-0.4.0}/tests/test_multi_endpoint.py +0 -0
  84. {stackport-0.3.8 → stackport-0.4.0}/tests/test_rds_routes.py +0 -0
  85. {stackport-0.3.8 → stackport-0.4.0}/tests/test_readonly_middleware.py +0 -0
  86. {stackport-0.3.8 → stackport-0.4.0}/tests/test_registries.py +0 -0
  87. {stackport-0.3.8 → stackport-0.4.0}/tests/test_routes.py +0 -0
  88. {stackport-0.3.8 → stackport-0.4.0}/tests/test_s3_routes.py +0 -0
  89. {stackport-0.3.8 → stackport-0.4.0}/tests/test_s3_upload_limit_env.py +0 -0
  90. {stackport-0.3.8 → stackport-0.4.0}/tests/test_secretsmanager_routes.py +0 -0
  91. {stackport-0.3.8 → stackport-0.4.0}/tests/test_sqs_routes.py +0 -0
  92. {stackport-0.3.8 → stackport-0.4.0}/tests/test_tags_routes.py +0 -0
  93. {stackport-0.3.8 → stackport-0.4.0}/tests/test_websocket.py +0 -0
  94. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
  95. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/acm.svg +0 -0
  96. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/apigateway.svg +0 -0
  97. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/appsync.svg +0 -0
  98. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/athena.svg +0 -0
  99. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/cloudformation.svg +0 -0
  100. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/cloudfront.svg +0 -0
  101. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/cognito-idp.svg +0 -0
  102. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/dynamodb.svg +0 -0
  103. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/ec2.svg +0 -0
  104. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/ecr.svg +0 -0
  105. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/ecs.svg +0 -0
  106. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/elasticache.svg +0 -0
  107. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
  108. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
  109. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
  110. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/events.svg +0 -0
  111. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/firehose.svg +0 -0
  112. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/glue.svg +0 -0
  113. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/iam.svg +0 -0
  114. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/kinesis.svg +0 -0
  115. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/kms.svg +0 -0
  116. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/lambda.svg +0 -0
  117. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/logs.svg +0 -0
  118. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/monitoring.svg +0 -0
  119. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/rds.svg +0 -0
  120. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/route53.svg +0 -0
  121. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/s3.svg +0 -0
  122. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/secretsmanager.svg +0 -0
  123. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/ses.svg +0 -0
  124. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/sns.svg +0 -0
  125. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/sqs.svg +0 -0
  126. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/ssm.svg +0 -0
  127. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/stepfunctions.svg +0 -0
  128. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/wafv2.svg +0 -0
  129. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/favicon.png +0 -0
  130. {stackport-0.3.8 → stackport-0.4.0}/ui/dist/favicon.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.3.8
3
+ Version: 0.4.0
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -55,13 +55,13 @@ Dynamic: license-file
55
55
  ## Screenshots
56
56
 
57
57
  **Dashboard** — Service overview with resource counts and health status
58
- ![StackPort Dashboard](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/dashboard.jpeg?v=1.0)
58
+ ![StackPort Dashboard](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/dashboard.jpeg?v=2.1)
59
59
 
60
60
  **Lambda Browser** — Function list with config, aliases, versions, event sources, and invocation panel
61
- ![Lambda Resources](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/lambda.jpeg?v=1.0)
61
+ ![Lambda Resources](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/lambda.jpeg?v=2.1)
62
62
 
63
63
  **S3 Browser** — File browser with upload, download, folder navigation, and tagging
64
- ![S3 Browser](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/s3.jpeg?v=1.0)
64
+ ![S3 Browser](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/s3.jpeg?v=2.1)
65
65
 
66
66
  ## Features
67
67
 
@@ -326,24 +326,13 @@ backend/
326
326
  secretsmanager.py Secret value retrieval
327
327
 
328
328
  ui/src/
329
- pages/ Dashboard, ResourceBrowser, About
329
+ pages/ CloudscapeDashboard, CloudscapeResourceBrowser, CloudscapeSettings, CloudscapeAbout
330
330
  components/
331
- service-views/ S3Browser, DynamoDBBrowser, LambdaBrowser, ...
332
- ui/ shadcn/ui components (Radix-based)
331
+ cloudscape/ AppLayout shell + service views (Cloudscape Design System)
333
332
  hooks/ useFetch, useWebSocket, useKeyboardShortcuts, ...
334
333
  lib/ API client, types, service icons, utils
335
334
  ```
336
335
 
337
- ## Star History
338
-
339
- <a href="https://star-history.com/#DaviReisVieira/stackport&Date">
340
- <picture>
341
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=DaviReisVieira/stackport&type=Date&theme=dark" />
342
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=DaviReisVieira/stackport&type=Date" />
343
- <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=DaviReisVieira/stackport&type=Date" width="100%" />
344
- </picture>
345
- </a>
346
-
347
336
  ## Contributors
348
337
 
349
338
  <a href="https://github.com/DaviReisVieira/stackport/graphs/contributors">
@@ -19,13 +19,13 @@
19
19
  ## Screenshots
20
20
 
21
21
  **Dashboard** — Service overview with resource counts and health status
22
- ![StackPort Dashboard](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/dashboard.jpeg?v=1.0)
22
+ ![StackPort Dashboard](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/dashboard.jpeg?v=2.1)
23
23
 
24
24
  **Lambda Browser** — Function list with config, aliases, versions, event sources, and invocation panel
25
- ![Lambda Resources](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/lambda.jpeg?v=1.0)
25
+ ![Lambda Resources](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/lambda.jpeg?v=2.1)
26
26
 
27
27
  **S3 Browser** — File browser with upload, download, folder navigation, and tagging
28
- ![S3 Browser](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/s3.jpeg?v=1.0)
28
+ ![S3 Browser](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/s3.jpeg?v=2.1)
29
29
 
30
30
  ## Features
31
31
 
@@ -290,24 +290,13 @@ backend/
290
290
  secretsmanager.py Secret value retrieval
291
291
 
292
292
  ui/src/
293
- pages/ Dashboard, ResourceBrowser, About
293
+ pages/ CloudscapeDashboard, CloudscapeResourceBrowser, CloudscapeSettings, CloudscapeAbout
294
294
  components/
295
- service-views/ S3Browser, DynamoDBBrowser, LambdaBrowser, ...
296
- ui/ shadcn/ui components (Radix-based)
295
+ cloudscape/ AppLayout shell + service views (Cloudscape Design System)
297
296
  hooks/ useFetch, useWebSocket, useKeyboardShortcuts, ...
298
297
  lib/ API client, types, service icons, utils
299
298
  ```
300
299
 
301
- ## Star History
302
-
303
- <a href="https://star-history.com/#DaviReisVieira/stackport&Date">
304
- <picture>
305
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=DaviReisVieira/stackport&type=Date&theme=dark" />
306
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=DaviReisVieira/stackport&type=Date" />
307
- <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=DaviReisVieira/stackport&type=Date" width="100%" />
308
- </picture>
309
- </a>
310
-
311
300
  ## Contributors
312
301
 
313
302
  <a href="https://github.com/DaviReisVieira/stackport/graphs/contributors">
@@ -2,16 +2,19 @@ import logging
2
2
  from datetime import datetime
3
3
  from typing import Optional
4
4
 
5
- from fastapi import APIRouter, Depends, Query
5
+ from fastapi import APIRouter, Depends, HTTPException, Query
6
6
 
7
7
  from backend.aws_client import get_client
8
8
  from backend.cache import cache
9
9
  from backend.routes.common import EndpointInfo, get_endpoint_info
10
+ from backend.schemas.logs import CreateLogGroupBody, SetRetentionBody
10
11
 
11
12
  logger = logging.getLogger(__name__)
12
13
 
13
14
  router = APIRouter()
14
15
 
16
+ VALID_RETENTION_DAYS = {1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653}
17
+
15
18
 
16
19
  def _epoch_millis_to_iso(timestamp: Optional[int]) -> Optional[str]:
17
20
  """Convert epoch milliseconds to ISO string."""
@@ -218,3 +221,93 @@ def get_log_events(
218
221
  "events": events,
219
222
  "next_token": response.get("nextForwardToken"),
220
223
  }
224
+
225
+
226
+ @router.post("/groups", status_code=201)
227
+ def create_log_group(body: CreateLogGroupBody, ep: EndpointInfo = Depends(get_endpoint_info)):
228
+ """Create a new CloudWatch log group."""
229
+ if body.retention_in_days is not None and body.retention_in_days not in VALID_RETENTION_DAYS:
230
+ raise HTTPException(
231
+ status_code=400,
232
+ detail=f"retention_in_days must be one of {sorted(VALID_RETENTION_DAYS)}",
233
+ )
234
+
235
+ logs = get_client("logs", **ep.client_kwargs())
236
+ try:
237
+ kwargs: dict = {"logGroupName": body.name}
238
+ if body.tags:
239
+ kwargs["tags"] = body.tags
240
+ logs.create_log_group(**kwargs)
241
+
242
+ if body.retention_in_days is not None:
243
+ logs.put_retention_policy(
244
+ logGroupName=body.name, retentionInDays=body.retention_in_days
245
+ )
246
+ except logs.exceptions.ResourceAlreadyExistsException:
247
+ raise HTTPException(status_code=409, detail=f"Log group '{body.name}' already exists")
248
+ except Exception as e:
249
+ raise HTTPException(status_code=500, detail=str(e))
250
+
251
+ cache.delete_by_prefix(f"{ep.url}:logs:groups:")
252
+ return {"name": body.name, "retention_in_days": body.retention_in_days}
253
+
254
+
255
+ @router.delete("/groups/{name:path}/streams/{stream:path}")
256
+ def delete_log_stream(name: str, stream: str, ep: EndpointInfo = Depends(get_endpoint_info)):
257
+ """Delete a log stream from a log group.
258
+
259
+ Registered before delete_log_group: {name:path} matches slashes greedily,
260
+ so if the bare /groups/{name:path} DELETE route were checked first it would
261
+ swallow this whole path (including "/streams/{stream}") as the group name
262
+ and never reach this handler. The more specific route has to come first.
263
+ """
264
+ logs = get_client("logs", **ep.client_kwargs())
265
+ try:
266
+ logs.delete_log_stream(logGroupName=name, logStreamName=stream)
267
+ except logs.exceptions.ResourceNotFoundException:
268
+ raise HTTPException(status_code=404, detail=f"Log stream '{stream}' not found")
269
+ except Exception as e:
270
+ raise HTTPException(status_code=500, detail=str(e))
271
+
272
+ cache.delete_by_prefix(f"{ep.url}:logs:streams:{name}:")
273
+ return {"success": True, "message": f"Log stream '{stream}' deleted"}
274
+
275
+
276
+ @router.put("/groups/{name:path}/retention")
277
+ def set_log_group_retention(name: str, body: SetRetentionBody, ep: EndpointInfo = Depends(get_endpoint_info)):
278
+ """Set or remove a log group's retention policy."""
279
+ if body.retention_in_days is not None and body.retention_in_days not in VALID_RETENTION_DAYS:
280
+ raise HTTPException(
281
+ status_code=400,
282
+ detail=f"retention_in_days must be one of {sorted(VALID_RETENTION_DAYS)}",
283
+ )
284
+
285
+ logs = get_client("logs", **ep.client_kwargs())
286
+ try:
287
+ if body.retention_in_days is None:
288
+ logs.delete_retention_policy(logGroupName=name)
289
+ else:
290
+ logs.put_retention_policy(logGroupName=name, retentionInDays=body.retention_in_days)
291
+ except logs.exceptions.ResourceNotFoundException:
292
+ raise HTTPException(status_code=404, detail=f"Log group '{name}' not found")
293
+ except Exception as e:
294
+ raise HTTPException(status_code=500, detail=str(e))
295
+
296
+ cache.delete_by_prefix(f"{ep.url}:logs:groups:")
297
+ return {"name": name, "retention_in_days": body.retention_in_days}
298
+
299
+
300
+ @router.delete("/groups/{name:path}")
301
+ def delete_log_group(name: str, ep: EndpointInfo = Depends(get_endpoint_info)):
302
+ """Delete a CloudWatch log group."""
303
+ logs = get_client("logs", **ep.client_kwargs())
304
+ try:
305
+ logs.delete_log_group(logGroupName=name)
306
+ except logs.exceptions.ResourceNotFoundException:
307
+ raise HTTPException(status_code=404, detail=f"Log group '{name}' not found")
308
+ except Exception as e:
309
+ raise HTTPException(status_code=500, detail=str(e))
310
+
311
+ cache.delete_by_prefix(f"{ep.url}:logs:groups:")
312
+ cache.delete_by_prefix(f"{ep.url}:logs:streams:{name}:")
313
+ return {"success": True, "message": f"Log group '{name}' deleted"}
@@ -0,0 +1,21 @@
1
+ """Pydantic schemas for CloudWatch Logs API requests."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class CreateLogGroupBody(BaseModel):
7
+ """Request body for creating a new log group."""
8
+
9
+ model_config = ConfigDict(populate_by_name=True)
10
+
11
+ name: str = Field(..., description="Log group name")
12
+ retention_in_days: int | None = Field(None, alias="retentionInDays", description="Retention period in days")
13
+ tags: dict[str, str] = Field(default_factory=dict, description="Resource tags")
14
+
15
+
16
+ class SetRetentionBody(BaseModel):
17
+ """Request body for setting a log group's retention policy."""
18
+
19
+ model_config = ConfigDict(populate_by_name=True)
20
+
21
+ retention_in_days: int | None = Field(None, alias="retentionInDays", description="Retention period in days, or null to never expire")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackport"
3
- version = "0.3.8"
3
+ version = "0.4.0"
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.3.8
3
+ Version: 0.4.0
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -55,13 +55,13 @@ Dynamic: license-file
55
55
  ## Screenshots
56
56
 
57
57
  **Dashboard** — Service overview with resource counts and health status
58
- ![StackPort Dashboard](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/dashboard.jpeg?v=1.0)
58
+ ![StackPort Dashboard](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/dashboard.jpeg?v=2.1)
59
59
 
60
60
  **Lambda Browser** — Function list with config, aliases, versions, event sources, and invocation panel
61
- ![Lambda Resources](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/lambda.jpeg?v=1.0)
61
+ ![Lambda Resources](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/lambda.jpeg?v=2.1)
62
62
 
63
63
  **S3 Browser** — File browser with upload, download, folder navigation, and tagging
64
- ![S3 Browser](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/s3.jpeg?v=1.0)
64
+ ![S3 Browser](https://raw.githubusercontent.com/DaviReisVieira/stackport/main/docs/images/s3.jpeg?v=2.1)
65
65
 
66
66
  ## Features
67
67
 
@@ -326,24 +326,13 @@ backend/
326
326
  secretsmanager.py Secret value retrieval
327
327
 
328
328
  ui/src/
329
- pages/ Dashboard, ResourceBrowser, About
329
+ pages/ CloudscapeDashboard, CloudscapeResourceBrowser, CloudscapeSettings, CloudscapeAbout
330
330
  components/
331
- service-views/ S3Browser, DynamoDBBrowser, LambdaBrowser, ...
332
- ui/ shadcn/ui components (Radix-based)
331
+ cloudscape/ AppLayout shell + service views (Cloudscape Design System)
333
332
  hooks/ useFetch, useWebSocket, useKeyboardShortcuts, ...
334
333
  lib/ API client, types, service icons, utils
335
334
  ```
336
335
 
337
- ## Star History
338
-
339
- <a href="https://star-history.com/#DaviReisVieira/stackport&Date">
340
- <picture>
341
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=DaviReisVieira/stackport&type=Date&theme=dark" />
342
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=DaviReisVieira/stackport&type=Date" />
343
- <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=DaviReisVieira/stackport&type=Date" width="100%" />
344
- </picture>
345
- </a>
346
-
347
336
  ## Contributors
348
337
 
349
338
  <a href="https://github.com/DaviReisVieira/stackport/graphs/contributors">
@@ -13,10 +13,28 @@ 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/StateMachineGraph-CmFIj15P.js
17
- backend/../ui/dist/assets/index-BZr5kWvs.css
18
- backend/../ui/dist/assets/index-Dvr-Ft0R.js
16
+ backend/../ui/dist/stackport-logo.svg
17
+ backend/../ui/dist/assets/CloudscapeAbout-BtS8xfBQ.js
18
+ backend/../ui/dist/assets/CloudscapeDashboard-XTN3dZMK.js
19
+ backend/../ui/dist/assets/CloudscapeResourceBrowser-BW5xV4W2.css
20
+ backend/../ui/dist/assets/CloudscapeResourceBrowser-CcOhO1RI.js
21
+ backend/../ui/dist/assets/CloudscapeSettings-BRB1cWqz.js
22
+ backend/../ui/dist/assets/CloudscapeShell-C1R1SIYZ.css
23
+ backend/../ui/dist/assets/CloudscapeShell-DojdksJr.js
24
+ backend/../ui/dist/assets/StateMachineGraph-BOEoyAZx.js
25
+ backend/../ui/dist/assets/index-BjtNHAFy.css
26
+ backend/../ui/dist/assets/index-BqhwBjJc.js
27
+ backend/../ui/dist/assets/index-C7soqJ2K.js
28
+ backend/../ui/dist/assets/index-CCLJtnX8.js
29
+ backend/../ui/dist/assets/index-Cah5DqCf.css
30
+ backend/../ui/dist/assets/index-DE--A2EE.css
31
+ backend/../ui/dist/assets/index-DOVP6iM8.js
32
+ backend/../ui/dist/assets/index-DRZKHoZl.css
33
+ backend/../ui/dist/assets/index-DaXIyw62.css
34
+ backend/../ui/dist/assets/index-Dj8ImrkQ.js
35
+ backend/../ui/dist/assets/index-lN483PN7.js
19
36
  backend/../ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
37
+ backend/../ui/dist/assets/utils-Cfmwk2UT.js
20
38
  backend/../ui/dist/aws-icons/acm.svg
21
39
  backend/../ui/dist/aws-icons/apigateway.svg
22
40
  backend/../ui/dist/aws-icons/appsync.svg
@@ -71,6 +89,7 @@ backend/schemas/__init__.py
71
89
  backend/schemas/dynamodb.py
72
90
  backend/schemas/endpoints.py
73
91
  backend/schemas/lambda_svc.py
92
+ backend/schemas/logs.py
74
93
  backend/schemas/s3.py
75
94
  backend/schemas/secretsmanager.py
76
95
  backend/schemas/sqs.py
@@ -107,10 +126,28 @@ tests/test_websocket.py
107
126
  ui/dist/favicon.png
108
127
  ui/dist/favicon.svg
109
128
  ui/dist/index.html
110
- ui/dist/assets/StateMachineGraph-CmFIj15P.js
111
- ui/dist/assets/index-BZr5kWvs.css
112
- ui/dist/assets/index-Dvr-Ft0R.js
129
+ ui/dist/stackport-logo.svg
130
+ ui/dist/assets/CloudscapeAbout-BtS8xfBQ.js
131
+ ui/dist/assets/CloudscapeDashboard-XTN3dZMK.js
132
+ ui/dist/assets/CloudscapeResourceBrowser-BW5xV4W2.css
133
+ ui/dist/assets/CloudscapeResourceBrowser-CcOhO1RI.js
134
+ ui/dist/assets/CloudscapeSettings-BRB1cWqz.js
135
+ ui/dist/assets/CloudscapeShell-C1R1SIYZ.css
136
+ ui/dist/assets/CloudscapeShell-DojdksJr.js
137
+ ui/dist/assets/StateMachineGraph-BOEoyAZx.js
138
+ ui/dist/assets/index-BjtNHAFy.css
139
+ ui/dist/assets/index-BqhwBjJc.js
140
+ ui/dist/assets/index-C7soqJ2K.js
141
+ ui/dist/assets/index-CCLJtnX8.js
142
+ ui/dist/assets/index-Cah5DqCf.css
143
+ ui/dist/assets/index-DE--A2EE.css
144
+ ui/dist/assets/index-DOVP6iM8.js
145
+ ui/dist/assets/index-DRZKHoZl.css
146
+ ui/dist/assets/index-DaXIyw62.css
147
+ ui/dist/assets/index-Dj8ImrkQ.js
148
+ ui/dist/assets/index-lN483PN7.js
113
149
  ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
150
+ ui/dist/assets/utils-Cfmwk2UT.js
114
151
  ui/dist/aws-icons/acm.svg
115
152
  ui/dist/aws-icons/apigateway.svg
116
153
  ui/dist/aws-icons/appsync.svg
@@ -272,3 +272,162 @@ class TestGetLogEvents:
272
272
  assert resp.status_code == 200
273
273
  data = resp.json()
274
274
  assert data["events"] == []
275
+
276
+
277
+ class TestCreateLogGroup:
278
+ @patch("backend.routes.logs.cache")
279
+ @patch("backend.routes.logs.get_client")
280
+ def test_create_log_group(self, mock_get_client, mock_cache):
281
+ mock_logs = MagicMock()
282
+ mock_get_client.return_value = mock_logs
283
+
284
+ resp = client.post("/api/logs/groups", json={"name": "/aws/lambda/new-fn"})
285
+ assert resp.status_code == 201
286
+ data = resp.json()
287
+ assert data["name"] == "/aws/lambda/new-fn"
288
+ assert data["retention_in_days"] is None
289
+ mock_logs.create_log_group.assert_called_once_with(logGroupName="/aws/lambda/new-fn")
290
+ mock_logs.put_retention_policy.assert_not_called()
291
+
292
+ @patch("backend.routes.logs.cache")
293
+ @patch("backend.routes.logs.get_client")
294
+ def test_create_log_group_with_retention_and_tags(self, mock_get_client, mock_cache):
295
+ mock_logs = MagicMock()
296
+ mock_get_client.return_value = mock_logs
297
+
298
+ resp = client.post(
299
+ "/api/logs/groups",
300
+ json={"name": "/aws/lambda/new-fn", "retentionInDays": 14, "tags": {"env": "prod"}},
301
+ )
302
+ assert resp.status_code == 201
303
+ data = resp.json()
304
+ assert data["retention_in_days"] == 14
305
+
306
+ mock_logs.create_log_group.assert_called_once_with(
307
+ logGroupName="/aws/lambda/new-fn", tags={"env": "prod"}
308
+ )
309
+ mock_logs.put_retention_policy.assert_called_once_with(
310
+ logGroupName="/aws/lambda/new-fn", retentionInDays=14
311
+ )
312
+
313
+ @patch("backend.routes.logs.get_client")
314
+ def test_create_log_group_invalid_retention(self, mock_get_client):
315
+ resp = client.post(
316
+ "/api/logs/groups", json={"name": "/aws/lambda/new-fn", "retentionInDays": 13}
317
+ )
318
+ assert resp.status_code == 400
319
+
320
+ @patch("backend.routes.logs.get_client")
321
+ def test_create_log_group_already_exists(self, mock_get_client):
322
+ mock_logs = MagicMock()
323
+ mock_get_client.return_value = mock_logs
324
+ mock_logs.exceptions.ResourceAlreadyExistsException = type(
325
+ "ResourceAlreadyExistsException", (Exception,), {}
326
+ )
327
+ mock_logs.create_log_group.side_effect = mock_logs.exceptions.ResourceAlreadyExistsException()
328
+
329
+ resp = client.post("/api/logs/groups", json={"name": "existing-group"})
330
+ assert resp.status_code == 409
331
+
332
+
333
+ class TestDeleteLogGroup:
334
+ @patch("backend.routes.logs.cache")
335
+ @patch("backend.routes.logs.get_client")
336
+ def test_delete_log_group(self, mock_get_client, mock_cache):
337
+ mock_logs = MagicMock()
338
+ mock_get_client.return_value = mock_logs
339
+
340
+ resp = client.delete("/api/logs/groups/my-log-group")
341
+ assert resp.status_code == 200
342
+ assert resp.json()["success"] is True
343
+ mock_logs.delete_log_group.assert_called_once_with(logGroupName="my-log-group")
344
+
345
+ @patch("backend.routes.logs.get_client")
346
+ def test_delete_log_group_not_found(self, mock_get_client):
347
+ mock_logs = MagicMock()
348
+ mock_get_client.return_value = mock_logs
349
+ mock_logs.exceptions.ResourceNotFoundException = type(
350
+ "ResourceNotFoundException", (Exception,), {}
351
+ )
352
+ mock_logs.delete_log_group.side_effect = mock_logs.exceptions.ResourceNotFoundException()
353
+
354
+ resp = client.delete("/api/logs/groups/missing-group")
355
+ assert resp.status_code == 404
356
+
357
+
358
+ class TestSetLogGroupRetention:
359
+ @patch("backend.routes.logs.cache")
360
+ @patch("backend.routes.logs.get_client")
361
+ def test_set_retention(self, mock_get_client, mock_cache):
362
+ mock_logs = MagicMock()
363
+ mock_get_client.return_value = mock_logs
364
+
365
+ resp = client.put(
366
+ "/api/logs/groups/my-log-group/retention", json={"retentionInDays": 30}
367
+ )
368
+ assert resp.status_code == 200
369
+ assert resp.json()["retention_in_days"] == 30
370
+ mock_logs.put_retention_policy.assert_called_once_with(
371
+ logGroupName="my-log-group", retentionInDays=30
372
+ )
373
+
374
+ @patch("backend.routes.logs.cache")
375
+ @patch("backend.routes.logs.get_client")
376
+ def test_clear_retention(self, mock_get_client, mock_cache):
377
+ mock_logs = MagicMock()
378
+ mock_get_client.return_value = mock_logs
379
+
380
+ resp = client.put(
381
+ "/api/logs/groups/my-log-group/retention", json={"retentionInDays": None}
382
+ )
383
+ assert resp.status_code == 200
384
+ assert resp.json()["retention_in_days"] is None
385
+ mock_logs.delete_retention_policy.assert_called_once_with(logGroupName="my-log-group")
386
+
387
+ @patch("backend.routes.logs.get_client")
388
+ def test_set_retention_invalid_value(self, mock_get_client):
389
+ resp = client.put(
390
+ "/api/logs/groups/my-log-group/retention", json={"retentionInDays": 999}
391
+ )
392
+ assert resp.status_code == 400
393
+
394
+ @patch("backend.routes.logs.get_client")
395
+ def test_set_retention_group_not_found(self, mock_get_client):
396
+ mock_logs = MagicMock()
397
+ mock_get_client.return_value = mock_logs
398
+ mock_logs.exceptions.ResourceNotFoundException = type(
399
+ "ResourceNotFoundException", (Exception,), {}
400
+ )
401
+ mock_logs.put_retention_policy.side_effect = mock_logs.exceptions.ResourceNotFoundException()
402
+
403
+ resp = client.put(
404
+ "/api/logs/groups/missing-group/retention", json={"retentionInDays": 30}
405
+ )
406
+ assert resp.status_code == 404
407
+
408
+
409
+ class TestDeleteLogStream:
410
+ @patch("backend.routes.logs.cache")
411
+ @patch("backend.routes.logs.get_client")
412
+ def test_delete_log_stream(self, mock_get_client, mock_cache):
413
+ mock_logs = MagicMock()
414
+ mock_get_client.return_value = mock_logs
415
+
416
+ resp = client.delete("/api/logs/groups/my-log-group/streams/my-stream")
417
+ assert resp.status_code == 200
418
+ assert resp.json()["success"] is True
419
+ mock_logs.delete_log_stream.assert_called_once_with(
420
+ logGroupName="my-log-group", logStreamName="my-stream"
421
+ )
422
+
423
+ @patch("backend.routes.logs.get_client")
424
+ def test_delete_log_stream_not_found(self, mock_get_client):
425
+ mock_logs = MagicMock()
426
+ mock_get_client.return_value = mock_logs
427
+ mock_logs.exceptions.ResourceNotFoundException = type(
428
+ "ResourceNotFoundException", (Exception,), {}
429
+ )
430
+ mock_logs.delete_log_stream.side_effect = mock_logs.exceptions.ResourceNotFoundException()
431
+
432
+ resp = client.delete("/api/logs/groups/my-log-group/streams/missing-stream")
433
+ assert resp.status_code == 404
@@ -0,0 +1 @@
1
+ import{r as j,aX as m,j as e}from"./index-DOVP6iM8.js";import{a as f,u as y,C as b,S as o,H as a,B as x,A as v,f as i,g as d,e as u}from"./CloudscapeShell-DojdksJr.js";import{C as S,L as C}from"./index-CCLJtnX8.js";import{C as h}from"./index-BqhwBjJc.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 B(){const{activeEndpoint:t}=f(),n=j.useCallback(()=>m(t),[t]),{data:s,error:c,refresh:p}=y(n,1e4);return e.jsx(b,{activeHref:"/about",children:e.jsxs(o,{size:"l",children:[e.jsx(a,{variant:"h1",description:"System information and project details",actions:e.jsx(x,{iconName:"refresh",onClick:()=>p(),ariaLabel:"Refresh system info"}),children:"About"}),c&&!s&&e.jsx(v,{type:"error",header:"Failed to load system info",action:e.jsx(x,{onClick:()=>p(),children:"Retry"}),children:c}),!s&&!c&&e.jsx(i,{type:"loading",children:"Loading system info"}),s&&e.jsxs(S,{columns:3,children:[e.jsx(h,{header:e.jsx(a,{variant:"h3",children:"Project"}),children:e.jsxs(o,{size:"s",children:[r("Name","StackPort"),r("Version",e.jsx(d,{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(h,{header:e.jsx(a,{variant:"h3",children:"Connection"}),children:e.jsxs(o,{size:"s",children:[r("Mode",s.connection_type==="local"?e.jsx(d,{color:"grey",children:"Local Emulator"}):e.jsx(d,{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(h,{header:e.jsx(a,{variant:"h3",children:"Links"}),children:e.jsx(o,{size:"s",children:k.map(l=>e.jsx(C,{href:l.url,external:!0,children:l.label},l.url))})})]})]})})}export{B as default};
@@ -0,0 +1 @@
1
+ import{r as a,u as L,f as W,j as t}from"./index-DOVP6iM8.js";import{u as O,a as A,b as D,c as E,d as M,H as z,C as P,S as $,A as H,B as R,e as d,f as F,g as U,h as _}from"./CloudscapeShell-DojdksJr.js";import{S as V,C as J,a as G,T as K}from"./index-Dj8ImrkQ.js";import{C as q,L as Q}from"./index-CCLJtnX8.js";import{T as X}from"./index-C7soqJ2K.js";import{f as Y}from"./utils-Cfmwk2UT.js";function Z({fallbackFetcher:n,fallbackInterval:r=5e3,messageType:i,endpoint:m}){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 x=window.location.protocol==="https:"?"wss:":"ws:",e=new WebSocket(`${x}//${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:m??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 x;v=!0,b.current&&clearTimeout(b.current),(x=h.current)==null||x.close(),g(null),C(!1)}},[i,m]);const u=O(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 B(n,r){const i=_(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(Q,{fontSize:"heading-m",href:`/resources/${n.name}`,onFollow:m=>{m.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(U,{color:"grey",children:[r,": ",i]},r))})}function oe(){const n=L(),{activeEndpoint:r}=A(),{data:i}=D(),m=a.useCallback(()=>W(r),[r]),{data:s,loading:g,error:y,connected:j,refresh:p}=Z({fallbackFetcher:m,fallbackInterval:5e3,messageType:"stats",endpoint:r}),{toggleFavorite:C,isFavorite:h}=E(),[c,b]=a.useState(""),[l,k]=a.useState(ee);a.useEffect(()=>{localStorage.setItem("stackport:view-mode",l)},[l]),M([{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(z,{variant:"h2",counter:s?`(${u.length})`:void 0,actions:t.jsx(V,{selectedId:l,onChange:({detail:e})=>k(e.selectedId),label:"View mode",options:[{id:"grid",text:"Grid"},{id:"list",text:"List"}]}),children:"Services"}),x=t.jsx(K,{filteringText:c,filteringPlaceholder:"Find a service",onChange:({detail:e})=>b(e.filteringText)});return t.jsx(P,{activeHref:"/",children:t.jsx(J,{header:t.jsx(z,{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(H,{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:x,cardDefinition:{header:e=>t.jsxs("div",{className:"flex items-center justify-between gap-2",children:[B(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:x,variant:"container",columnDefinitions:[{id:"favorite",header:"",width:60,cell:e=>v(e)},{id:"service",header:"Service",cell:e=>B(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{oe as default};