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.
- {stackport-0.3.8/stackport.egg-info → stackport-0.4.0}/PKG-INFO +6 -17
- {stackport-0.3.8 → stackport-0.4.0}/README.md +5 -16
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/logs.py +94 -1
- stackport-0.4.0/backend/schemas/logs.py +21 -0
- {stackport-0.3.8 → stackport-0.4.0}/pyproject.toml +1 -1
- {stackport-0.3.8 → stackport-0.4.0/stackport.egg-info}/PKG-INFO +6 -17
- {stackport-0.3.8 → stackport-0.4.0}/stackport.egg-info/SOURCES.txt +43 -6
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_logs_routes.py +159 -0
- stackport-0.4.0/ui/dist/assets/CloudscapeAbout-BtS8xfBQ.js +1 -0
- stackport-0.4.0/ui/dist/assets/CloudscapeDashboard-XTN3dZMK.js +1 -0
- stackport-0.4.0/ui/dist/assets/CloudscapeResourceBrowser-BW5xV4W2.css +1 -0
- stackport-0.4.0/ui/dist/assets/CloudscapeResourceBrowser-CcOhO1RI.js +16 -0
- stackport-0.4.0/ui/dist/assets/CloudscapeSettings-BRB1cWqz.js +1 -0
- stackport-0.4.0/ui/dist/assets/CloudscapeShell-C1R1SIYZ.css +1 -0
- stackport-0.4.0/ui/dist/assets/CloudscapeShell-DojdksJr.js +200 -0
- stackport-0.4.0/ui/dist/assets/StateMachineGraph-BOEoyAZx.js +21 -0
- stackport-0.4.0/ui/dist/assets/index-BjtNHAFy.css +1 -0
- stackport-0.4.0/ui/dist/assets/index-BqhwBjJc.js +1 -0
- stackport-0.4.0/ui/dist/assets/index-C7soqJ2K.js +1 -0
- stackport-0.4.0/ui/dist/assets/index-CCLJtnX8.js +1 -0
- stackport-0.4.0/ui/dist/assets/index-Cah5DqCf.css +1 -0
- stackport-0.4.0/ui/dist/assets/index-DE--A2EE.css +1 -0
- stackport-0.4.0/ui/dist/assets/index-DOVP6iM8.js +52 -0
- stackport-0.4.0/ui/dist/assets/index-DRZKHoZl.css +1 -0
- stackport-0.4.0/ui/dist/assets/index-DaXIyw62.css +1 -0
- stackport-0.4.0/ui/dist/assets/index-Dj8ImrkQ.js +1 -0
- stackport-0.4.0/ui/dist/assets/index-lN483PN7.js +1 -0
- stackport-0.4.0/ui/dist/assets/utils-Cfmwk2UT.js +1 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/index.html +2 -2
- stackport-0.4.0/ui/dist/stackport-logo.svg +68 -0
- stackport-0.3.8/ui/dist/assets/StateMachineGraph-CmFIj15P.js +0 -6
- stackport-0.3.8/ui/dist/assets/index-BZr5kWvs.css +0 -1
- stackport-0.3.8/ui/dist/assets/index-Dvr-Ft0R.js +0 -652
- {stackport-0.3.8 → stackport-0.4.0}/LICENSE +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/MANIFEST.in +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/__init__.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/aws_client.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/cache.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/cli.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/config.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/endpoint_store.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/main.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/__init__.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/common.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/dynamodb.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/ec2.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/endpoints.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/iam.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/lambda_svc.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/rds.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/resources.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/s3.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/secretsmanager.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/sqs.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/stats.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/stepfunctions.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/routes/tags.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/__init__.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/dynamodb.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/endpoints.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/lambda_svc.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/s3.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/secretsmanager.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/sqs.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/stepfunctions.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/schemas/tags.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/backend/websocket.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/setup.cfg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/stackport.egg-info/dependency_links.txt +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/stackport.egg-info/entry_points.txt +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/stackport.egg-info/requires.txt +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/stackport.egg-info/top_level.txt +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_cache.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_cli.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_client.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_config.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_dynamodb_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_ec2_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_endpoint_store.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_endpoints.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_iam_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_lambda_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_multi_endpoint.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_rds_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_readonly_middleware.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_registries.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_s3_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_s3_upload_limit_env.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_secretsmanager_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_sqs_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_tags_routes.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/tests/test_websocket.py +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/acm.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/apigateway.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/appsync.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/athena.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/cloudformation.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/cloudfront.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/cognito-idp.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/dynamodb.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/ec2.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/ecr.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/ecs.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/elasticache.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/events.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/firehose.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/glue.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/iam.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/kinesis.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/kms.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/lambda.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/logs.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/monitoring.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/rds.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/route53.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/s3.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/secretsmanager.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/ses.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/sns.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/sqs.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/ssm.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/stepfunctions.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/aws-icons/wafv2.svg +0 -0
- {stackport-0.3.8 → stackport-0.4.0}/ui/dist/favicon.png +0 -0
- {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
|
+
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
|
-

|
|
59
59
|
|
|
60
60
|
**Lambda Browser** — Function list with config, aliases, versions, event sources, and invocation panel
|
|
61
|
-

|
|
62
62
|
|
|
63
63
|
**S3 Browser** — File browser with upload, download, folder navigation, and tagging
|
|
64
|
-

|
|
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/
|
|
329
|
+
pages/ CloudscapeDashboard, CloudscapeResourceBrowser, CloudscapeSettings, CloudscapeAbout
|
|
330
330
|
components/
|
|
331
|
-
service
|
|
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
|
-

|
|
23
23
|
|
|
24
24
|
**Lambda Browser** — Function list with config, aliases, versions, event sources, and invocation panel
|
|
25
|
-

|
|
26
26
|
|
|
27
27
|
**S3 Browser** — File browser with upload, download, folder navigation, and tagging
|
|
28
|
-

|
|
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/
|
|
293
|
+
pages/ CloudscapeDashboard, CloudscapeResourceBrowser, CloudscapeSettings, CloudscapeAbout
|
|
294
294
|
components/
|
|
295
|
-
service
|
|
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
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackport
|
|
3
|
-
Version: 0.
|
|
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
|
-

|
|
59
59
|
|
|
60
60
|
**Lambda Browser** — Function list with config, aliases, versions, event sources, and invocation panel
|
|
61
|
-

|
|
62
62
|
|
|
63
63
|
**S3 Browser** — File browser with upload, download, folder navigation, and tagging
|
|
64
|
-

|
|
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/
|
|
329
|
+
pages/ CloudscapeDashboard, CloudscapeResourceBrowser, CloudscapeSettings, CloudscapeAbout
|
|
330
330
|
components/
|
|
331
|
-
service
|
|
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/
|
|
17
|
-
backend/../ui/dist/assets/
|
|
18
|
-
backend/../ui/dist/assets/
|
|
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/
|
|
111
|
-
ui/dist/assets/
|
|
112
|
-
ui/dist/assets/
|
|
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};
|