mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-14 05:10:05 +01:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dac28d3ea5 | ||
|
|
dd0cefc08a | ||
|
|
842f7c9be6 | ||
|
|
c15311a5a1 | ||
|
|
25ced343c4 | ||
|
|
b72a8d384d | ||
|
|
c786a79b60 | ||
|
|
01b1434842 | ||
|
|
e8c9cde474 | ||
|
|
ae92d05cd4 | ||
|
|
67d50d2b15 | ||
|
|
e5a664b812 | ||
|
|
f722ba8dca | ||
|
|
20e6d8d186 |
76
cmd/serve.go
76
cmd/serve.go
@@ -116,46 +116,48 @@ var serveCmd = &cobra.Command{
|
||||
log.Printf("failed to enqueue bootstrap_bastion: %v", err)
|
||||
}
|
||||
|
||||
_, err = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"prepare_cluster",
|
||||
bg.ClusterPrepareArgs{IntervalS: 120},
|
||||
archer.WithMaxRetries(3),
|
||||
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("failed to enqueue prepare_cluster: %v", err)
|
||||
}
|
||||
/*
|
||||
_, err = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"prepare_cluster",
|
||||
bg.ClusterPrepareArgs{IntervalS: 120},
|
||||
archer.WithMaxRetries(3),
|
||||
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("failed to enqueue prepare_cluster: %v", err)
|
||||
}
|
||||
|
||||
_, err = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"cluster_setup",
|
||||
bg.ClusterSetupArgs{
|
||||
IntervalS: 120,
|
||||
},
|
||||
archer.WithMaxRetries(3),
|
||||
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
||||
)
|
||||
_, err = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"cluster_setup",
|
||||
bg.ClusterSetupArgs{
|
||||
IntervalS: 120,
|
||||
},
|
||||
archer.WithMaxRetries(3),
|
||||
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("failed to enqueue cluster setup: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("failed to enqueue cluster setup: %v", err)
|
||||
}
|
||||
|
||||
_, err = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"cluster_bootstrap",
|
||||
bg.ClusterBootstrapArgs{
|
||||
IntervalS: 120,
|
||||
},
|
||||
archer.WithMaxRetries(3),
|
||||
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("failed to enqueue cluster bootstrap: %v", err)
|
||||
}
|
||||
_, err = jobs.Enqueue(
|
||||
context.Background(),
|
||||
uuid.NewString(),
|
||||
"cluster_bootstrap",
|
||||
bg.ClusterBootstrapArgs{
|
||||
IntervalS: 120,
|
||||
},
|
||||
archer.WithMaxRetries(3),
|
||||
archer.WithScheduleTime(time.Now().Add(60*time.Second)),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("failed to enqueue cluster bootstrap: %v", err)
|
||||
}
|
||||
*/
|
||||
|
||||
_, err = jobs.Enqueue(
|
||||
context.Background(),
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,23 @@
|
||||
components:
|
||||
schemas:
|
||||
dto.ActionResponse:
|
||||
properties:
|
||||
created_at:
|
||||
format: date-time
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
id:
|
||||
format: uuid
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
make_target:
|
||||
type: string
|
||||
updated_at:
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
dto.AnnotationResponse:
|
||||
properties:
|
||||
created_at:
|
||||
@@ -128,6 +146,42 @@ components:
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
dto.ClusterRunResponse:
|
||||
properties:
|
||||
action:
|
||||
type: string
|
||||
cluster_id:
|
||||
format: uuid
|
||||
type: string
|
||||
created_at:
|
||||
format: date-time
|
||||
type: string
|
||||
error:
|
||||
type: string
|
||||
finished_at:
|
||||
format: date-time
|
||||
type: string
|
||||
id:
|
||||
format: uuid
|
||||
type: string
|
||||
organization_id:
|
||||
format: uuid
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
updated_at:
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
dto.CreateActionRequest:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
make_target:
|
||||
type: string
|
||||
type: object
|
||||
dto.CreateAnnotationRequest:
|
||||
properties:
|
||||
key:
|
||||
@@ -716,6 +770,15 @@ components:
|
||||
example: Bearer
|
||||
type: string
|
||||
type: object
|
||||
dto.UpdateActionRequest:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
make_target:
|
||||
type: string
|
||||
type: object
|
||||
dto.UpdateAnnotationRequest:
|
||||
properties:
|
||||
key:
|
||||
@@ -1202,6 +1265,222 @@ paths:
|
||||
summary: Get JWKS
|
||||
tags:
|
||||
- Auth
|
||||
/admin/actions:
|
||||
get:
|
||||
description: Returns all admin-configured actions.
|
||||
operationId: ListActions
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/components/schemas/dto.ActionResponse'
|
||||
type: array
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: Unauthorized
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: db error
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: List available actions
|
||||
tags:
|
||||
- Actions
|
||||
post:
|
||||
description: Creates a new admin-configured action.
|
||||
operationId: CreateAction
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/dto.CreateActionRequest'
|
||||
description: payload
|
||||
required: true
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/dto.ActionResponse'
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: bad request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: Unauthorized
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: db error
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Create an action
|
||||
tags:
|
||||
- Actions
|
||||
/admin/actions/{actionID}:
|
||||
delete:
|
||||
description: Deletes an action.
|
||||
operationId: DeleteAction
|
||||
parameters:
|
||||
- description: Action ID
|
||||
in: path
|
||||
name: actionID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: deleted
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: bad request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: Unauthorized
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: not found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: db error
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Delete an action
|
||||
tags:
|
||||
- Actions
|
||||
get:
|
||||
description: Returns a single action.
|
||||
operationId: GetAction
|
||||
parameters:
|
||||
- description: Action ID
|
||||
in: path
|
||||
name: actionID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/dto.ActionResponse'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: bad request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: Unauthorized
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: not found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: db error
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Get a single action by ID
|
||||
tags:
|
||||
- Actions
|
||||
patch:
|
||||
description: Updates an action. Only provided fields are modified.
|
||||
operationId: UpdateAction
|
||||
parameters:
|
||||
- description: Action ID
|
||||
in: path
|
||||
name: actionID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/dto.UpdateActionRequest'
|
||||
description: payload
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/dto.ActionResponse'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: bad request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: Unauthorized
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: not found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: db error
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Update an action
|
||||
tags:
|
||||
- Actions
|
||||
/admin/archer/jobs:
|
||||
get:
|
||||
description: Paginated background jobs with optional filters. Search `q` may
|
||||
@@ -2124,6 +2403,73 @@ paths:
|
||||
summary: Update basic cluster details (org scoped)
|
||||
tags:
|
||||
- Clusters
|
||||
/clusters/{clusterID}/actions/{actionID}/runs:
|
||||
post:
|
||||
description: Creates a ClusterRun record for the cluster/action. Execution is
|
||||
handled asynchronously by workers.
|
||||
operationId: RunClusterAction
|
||||
parameters:
|
||||
- description: Organization UUID
|
||||
in: header
|
||||
name: X-Org-ID
|
||||
schema:
|
||||
type: string
|
||||
- description: Cluster ID
|
||||
in: path
|
||||
name: clusterID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- description: Action ID
|
||||
in: path
|
||||
name: actionID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/dto.ClusterRunResponse'
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: bad request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: organization required
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: cluster or action not found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: db error
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- OrgKeyAuth: []
|
||||
- OrgSecretAuth: []
|
||||
summary: Run an admin-configured action on a cluster (org scoped)
|
||||
tags:
|
||||
- ClusterRuns
|
||||
/clusters/{clusterID}/apps-load-balancer:
|
||||
delete:
|
||||
description: Clears apps_load_balancer_id on the cluster.
|
||||
@@ -3017,6 +3363,128 @@ paths:
|
||||
summary: Detach a node pool from a cluster
|
||||
tags:
|
||||
- Clusters
|
||||
/clusters/{clusterID}/runs:
|
||||
get:
|
||||
description: Returns runs for a cluster within the organization in X-Org-ID.
|
||||
operationId: ListClusterRuns
|
||||
parameters:
|
||||
- description: Organization UUID
|
||||
in: header
|
||||
name: X-Org-ID
|
||||
schema:
|
||||
type: string
|
||||
- description: Cluster ID
|
||||
in: path
|
||||
name: clusterID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/components/schemas/dto.ClusterRunResponse'
|
||||
type: array
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: organization required
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: cluster not found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: db error
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- OrgKeyAuth: []
|
||||
- OrgSecretAuth: []
|
||||
summary: List cluster runs (org scoped)
|
||||
tags:
|
||||
- ClusterRuns
|
||||
/clusters/{clusterID}/runs/{runID}:
|
||||
get:
|
||||
description: Returns a single run for a cluster within the organization in X-Org-ID.
|
||||
operationId: GetClusterRun
|
||||
parameters:
|
||||
- description: Organization UUID
|
||||
in: header
|
||||
name: X-Org-ID
|
||||
schema:
|
||||
type: string
|
||||
- description: Cluster ID
|
||||
in: path
|
||||
name: clusterID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- description: Run ID
|
||||
in: path
|
||||
name: runID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/dto.ClusterRunResponse'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: bad request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: organization required
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: not found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
description: db error
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- OrgKeyAuth: []
|
||||
- OrgSecretAuth: []
|
||||
summary: Get a cluster run (org scoped)
|
||||
tags:
|
||||
- ClusterRuns
|
||||
/credentials:
|
||||
get:
|
||||
description: Returns credential metadata for the current org. Secrets are never
|
||||
|
||||
7
go.mod
7
go.mod
@@ -8,7 +8,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.5
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/dyaksa/archer v1.1.5
|
||||
github.com/fergusstrange/embedded-postgres v1.33.0
|
||||
@@ -16,7 +16,7 @@ require (
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/go-chi/httprate v0.15.0
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/go-playground/validator/v10 v10.29.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
@@ -31,7 +31,6 @@ require (
|
||||
gorm.io/datatypes v1.2.7
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
github.com/swaggo/swag/v2 v2.0.0-rc4
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -62,7 +61,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@@ -40,6 +40,8 @@ github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 h1:80pDB3Tpmb2RCSZORrK9/3iQ
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0/go.mod h1:6EZUGGNLPLh5Unt30uEoA+KQcByERfXIkax9qrc80nA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2 h1:U3ygWUhCpiSPYSHOrRhb3gOl9T5Y3kB8k5Vjs//57bE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw=
|
||||
@@ -81,6 +83,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
@@ -114,6 +118,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-playground/validator/v10 v10.29.0 h1:lQlF5VNJWNlRbRZNeOIkWElR+1LL/OuHcc0Kp14w1xk=
|
||||
github.com/go-playground/validator/v10 v10.29.0/go.mod h1:D6QxqeMlgIPuT02L66f2ccrZ7AGgHkzKmmTMZhk/Kc4=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
|
||||
@@ -22,5 +22,16 @@ func mountAdminRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs, authUser func(ht
|
||||
archer.Post("/jobs/{id}/cancel", handlers.AdminCancelArcherJob(db))
|
||||
archer.Get("/queues", handlers.AdminListArcherQueues(db))
|
||||
})
|
||||
admin.Route("/actions", func(action chi.Router) {
|
||||
action.Use(authUser)
|
||||
action.Use(httpmiddleware.RequirePlatformAdmin())
|
||||
|
||||
action.Get("/", handlers.ListActions(db))
|
||||
action.Post("/", handlers.CreateAction(db))
|
||||
|
||||
action.Get("/{actionID}", handlers.GetAction(db))
|
||||
action.Patch("/{actionID}", handlers.UpdateAction(db))
|
||||
action.Delete("/{actionID}", handlers.DeleteAction(db))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func mountAPIRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs) {
|
||||
mountNodePoolRoutes(v1, db, authOrg)
|
||||
mountDNSRoutes(v1, db, authOrg)
|
||||
mountLoadBalancerRoutes(v1, db, authOrg)
|
||||
mountClusterRoutes(v1, db, authOrg)
|
||||
mountClusterRoutes(v1, db, jobs, authOrg)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ package api
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/bg"
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountClusterRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||
func mountClusterRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs, authOrg func(http.Handler) http.Handler) {
|
||||
r.Route("/clusters", func(c chi.Router) {
|
||||
c.Use(authOrg)
|
||||
c.Get("/", handlers.ListClusters(db))
|
||||
@@ -36,6 +37,10 @@ func mountClusterRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) ht
|
||||
c.Delete("/{clusterID}/kubeconfig", handlers.ClearClusterKubeconfig(db))
|
||||
|
||||
c.Post("/{clusterID}/node-pools", handlers.AttachNodePool(db))
|
||||
c.Delete("/{clusterID}/node-pools/{nodePoolID}", handlers.DeleteNodePool(db))
|
||||
c.Delete("/{clusterID}/node-pools/{nodePoolID}", handlers.DetachNodePool(db))
|
||||
|
||||
c.Get("/{clusterID}/runs", handlers.ListClusterRuns(db))
|
||||
c.Get("/{clusterID}/runs/{runID}", handlers.GetClusterRun(db))
|
||||
c.Post("/{clusterID}/actions/{actionID}/runs", handlers.RunClusterAction(db, jobs))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ func NewRuntime() *Runtime {
|
||||
&models.RecordSet{},
|
||||
&models.LoadBalancer{},
|
||||
&models.Cluster{},
|
||||
&models.Action{},
|
||||
&models.Cluster{},
|
||||
&models.ClusterRun{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -107,27 +107,29 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(2*time.Minute),
|
||||
)
|
||||
/*
|
||||
c.Register(
|
||||
"prepare_cluster",
|
||||
ClusterPrepareWorker(gdb, jobs),
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(2*time.Minute),
|
||||
)
|
||||
|
||||
c.Register(
|
||||
"prepare_cluster",
|
||||
ClusterPrepareWorker(gdb, jobs),
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(2*time.Minute),
|
||||
)
|
||||
c.Register(
|
||||
"cluster_setup",
|
||||
ClusterSetupWorker(gdb, jobs),
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(2*time.Minute),
|
||||
)
|
||||
|
||||
c.Register(
|
||||
"cluster_setup",
|
||||
ClusterSetupWorker(gdb, jobs),
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(2*time.Minute),
|
||||
)
|
||||
c.Register(
|
||||
"cluster_bootstrap",
|
||||
ClusterBootstrapWorker(gdb, jobs),
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(60*time.Minute),
|
||||
)
|
||||
|
||||
c.Register(
|
||||
"cluster_bootstrap",
|
||||
ClusterBootstrapWorker(gdb, jobs),
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(60*time.Minute),
|
||||
)
|
||||
*/
|
||||
|
||||
c.Register(
|
||||
"org_key_sweeper",
|
||||
@@ -135,6 +137,8 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) {
|
||||
archer.WithInstances(1),
|
||||
archer.WithTimeout(5*time.Minute),
|
||||
)
|
||||
|
||||
c.Register("cluster_action", ClusterActionWorker(gdb))
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
|
||||
166
internal/bg/cluster_action.go
Normal file
166
internal/bg/cluster_action.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package bg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dyaksa/archer"
|
||||
"github.com/dyaksa/archer/job"
|
||||
"github.com/glueops/autoglue/internal/mapper"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ClusterActionArgs struct {
|
||||
OrgID uuid.UUID `json:"org_id"`
|
||||
ClusterID uuid.UUID `json:"cluster_id"`
|
||||
Action string `json:"action"`
|
||||
MakeTarget string `json:"make_target"`
|
||||
}
|
||||
|
||||
type ClusterActionResult struct {
|
||||
Status string `json:"status"`
|
||||
Action string `json:"action"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
ElapsedMs int `json:"elapsed_ms"`
|
||||
}
|
||||
|
||||
func ClusterActionWorker(db *gorm.DB) archer.WorkerFn {
|
||||
return func(ctx context.Context, j job.Job) (any, error) {
|
||||
start := time.Now()
|
||||
var args ClusterActionArgs
|
||||
_ = j.ParseArguments(&args)
|
||||
|
||||
logger := log.With().
|
||||
Str("job", j.ID).
|
||||
Str("cluster_id", args.ClusterID.String()).
|
||||
Str("action", args.Action).
|
||||
Logger()
|
||||
|
||||
var c models.Cluster
|
||||
if err := db.
|
||||
Preload("BastionServer.SshKey").
|
||||
Preload("CaptainDomain").
|
||||
Preload("ControlPlaneRecordSet").
|
||||
Preload("AppsLoadBalancer").
|
||||
Preload("GlueOpsLoadBalancer").
|
||||
Preload("NodePools").
|
||||
Preload("NodePools.Labels").
|
||||
Preload("NodePools.Annotations").
|
||||
Preload("NodePools.Taints").
|
||||
Preload("NodePools.Servers.SshKey").
|
||||
Where("id = ? AND organization_id = ?", args.ClusterID, args.OrgID).
|
||||
First(&c).Error; err != nil {
|
||||
|
||||
return nil, fmt.Errorf("load cluster: %w", err)
|
||||
}
|
||||
|
||||
// ---- Step 1: Prepare (mostly lifted from ClusterPrepareWorker)
|
||||
if err := setClusterStatus(db, c.ID, clusterStatusBootstrapping, ""); err != nil {
|
||||
return nil, fmt.Errorf("mark bootstrapping: %w", err)
|
||||
}
|
||||
c.Status = clusterStatusBootstrapping
|
||||
|
||||
if err := validateClusterForPrepare(&c); err != nil {
|
||||
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||
return nil, fmt.Errorf("validate: %w", err)
|
||||
}
|
||||
|
||||
allServers := flattenClusterServers(&c)
|
||||
keyPayloads, sshConfig, err := buildSSHAssetsForCluster(db, &c, allServers)
|
||||
if err != nil {
|
||||
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||
return nil, fmt.Errorf("build ssh assets: %w", err)
|
||||
}
|
||||
|
||||
dtoCluster := mapper.ClusterToDTO(c)
|
||||
|
||||
if c.EncryptedKubeconfig != "" && c.KubeIV != "" && c.KubeTag != "" {
|
||||
kubeconfig, err := utils.DecryptForOrg(
|
||||
c.OrganizationID,
|
||||
c.EncryptedKubeconfig,
|
||||
c.KubeIV,
|
||||
c.KubeTag,
|
||||
db,
|
||||
)
|
||||
if err != nil {
|
||||
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||
return nil, fmt.Errorf("decrypt kubeconfig: %w", err)
|
||||
}
|
||||
dtoCluster.Kubeconfig = &kubeconfig
|
||||
}
|
||||
|
||||
orgKey, orgSecret, err := findOrCreateClusterAutomationKey(db, c.OrganizationID, c.ID, 24*time.Hour)
|
||||
if err != nil {
|
||||
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||
return nil, fmt.Errorf("org key: %w", err)
|
||||
}
|
||||
dtoCluster.OrgKey = &orgKey
|
||||
dtoCluster.OrgSecret = &orgSecret
|
||||
|
||||
payloadJSON, err := json.MarshalIndent(dtoCluster, "", " ")
|
||||
if err != nil {
|
||||
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||
return nil, fmt.Errorf("marshal payload: %w", err)
|
||||
}
|
||||
|
||||
{
|
||||
runCtx, cancel := context.WithTimeout(ctx, 8*time.Minute)
|
||||
err := pushAssetsToBastion(runCtx, db, &c, sshConfig, keyPayloads, payloadJSON)
|
||||
cancel()
|
||||
if err != nil {
|
||||
_ = setClusterStatus(db, c.ID, clusterStatusFailed, err.Error())
|
||||
return nil, fmt.Errorf("push assets: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := setClusterStatus(db, c.ID, clusterStatusPending, ""); err != nil {
|
||||
return nil, fmt.Errorf("mark pending: %w", err)
|
||||
}
|
||||
c.Status = clusterStatusPending
|
||||
|
||||
// ---- Step 2: Setup (ping-servers)
|
||||
{
|
||||
runCtx, cancel := context.WithTimeout(ctx, 30*time.Minute)
|
||||
out, err := runMakeOnBastion(runCtx, db, &c, "ping-servers")
|
||||
cancel()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("output", out).Msg("ping-servers failed")
|
||||
_ = setClusterStatus(db, c.ID, clusterStatusFailed, fmt.Sprintf("make ping-servers: %v", err))
|
||||
return nil, fmt.Errorf("ping-servers: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := setClusterStatus(db, c.ID, clusterStatusProvisioning, ""); err != nil {
|
||||
return nil, fmt.Errorf("mark provisioning: %w", err)
|
||||
}
|
||||
c.Status = clusterStatusProvisioning
|
||||
|
||||
// ---- Step 3: Bootstrap (parameterized target)
|
||||
{
|
||||
runCtx, cancel := context.WithTimeout(ctx, 60*time.Minute)
|
||||
out, err := runMakeOnBastion(runCtx, db, &c, args.MakeTarget)
|
||||
cancel()
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("output", out).Msg("bootstrap target failed")
|
||||
_ = setClusterStatus(db, c.ID, clusterStatusFailed, fmt.Sprintf("make %s: %v", args.MakeTarget, err))
|
||||
return nil, fmt.Errorf("make %s: %w", args.MakeTarget, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := setClusterStatus(db, c.ID, clusterStatusReady, ""); err != nil {
|
||||
return nil, fmt.Errorf("mark ready: %w", err)
|
||||
}
|
||||
return ClusterActionResult{
|
||||
Status: "ok",
|
||||
Action: args.Action,
|
||||
ClusterID: c.ID.String(),
|
||||
ElapsedMs: int(time.Since(start).Milliseconds()),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func ClusterBootstrapWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||
var clusters []models.Cluster
|
||||
if err := db.
|
||||
Preload("BastionServer.SshKey").
|
||||
Where("status = ?", clusterStatusPending).
|
||||
Where("status = ?", clusterStatusProvisioning).
|
||||
Find(&clusters).Error; err != nil {
|
||||
log.Error().Err(err).Msg("[cluster_bootstrap] query clusters failed")
|
||||
return nil, err
|
||||
|
||||
@@ -74,8 +74,8 @@ func ClusterSetupWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||
if err != nil {
|
||||
failCount++
|
||||
failedIDs = append(failedIDs, c.ID)
|
||||
logger.Error().Err(err).Str("output", out).Msg("[cluster_setup] make setup failed")
|
||||
_ = setClusterStatus(db, c.ID, clusterStatusFailed, fmt.Sprintf("make setup: %v", err))
|
||||
logger.Error().Err(err).Str("output", out).Msg("[cluster_setup] make ping-servers failed")
|
||||
_ = setClusterStatus(db, c.ID, clusterStatusFailed, fmt.Sprintf("make ping-servers: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -45,11 +45,12 @@ type ClusterPrepareResult struct {
|
||||
|
||||
// Alias the status constants from models to avoid string drift.
|
||||
const (
|
||||
clusterStatusPrePending = models.ClusterStatusPrePending
|
||||
clusterStatusPending = models.ClusterStatusPending
|
||||
clusterStatusProvisioning = models.ClusterStatusProvisioning
|
||||
clusterStatusReady = models.ClusterStatusReady
|
||||
clusterStatusFailed = models.ClusterStatusFailed
|
||||
clusterStatusPrePending = models.ClusterStatusPrePending
|
||||
clusterStatusPending = models.ClusterStatusPending
|
||||
clusterStatusProvisioning = models.ClusterStatusProvisioning
|
||||
clusterStatusReady = models.ClusterStatusReady
|
||||
clusterStatusFailed = models.ClusterStatusFailed
|
||||
clusterStatusBootstrapping = models.ClusterStatusBootstrapping
|
||||
)
|
||||
|
||||
func ClusterPrepareWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||
@@ -97,6 +98,13 @@ func ClusterPrepareWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := setClusterStatus(db, c.ID, clusterStatusBootstrapping, ""); err != nil {
|
||||
log.Error().Err(err).Msg("[cluster_prepare] failed to mark cluster bootstrapping")
|
||||
continue
|
||||
}
|
||||
|
||||
c.Status = clusterStatusBootstrapping
|
||||
|
||||
clusterLog := log.With().
|
||||
Str("job", jobID).
|
||||
Str("cluster_id", c.ID.String()).
|
||||
|
||||
256
internal/handlers/actions.go
Normal file
256
internal/handlers/actions.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ListActions godoc
|
||||
//
|
||||
// @ID ListActions
|
||||
// @Summary List available actions
|
||||
// @Description Returns all admin-configured actions.
|
||||
// @Tags Actions
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.ActionResponse
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 500 {string} string "db error"
|
||||
// @Router /admin/actions [get]
|
||||
// @Security BearerAuth
|
||||
func ListActions(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var rows []models.Action
|
||||
if err := db.Order("label ASC").Find(&rows).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]dto.ActionResponse, 0, len(rows))
|
||||
for _, a := range rows {
|
||||
out = append(out, actionToDTO(a))
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAction godoc
|
||||
//
|
||||
// @ID GetAction
|
||||
// @Summary Get a single action by ID
|
||||
// @Description Returns a single action.
|
||||
// @Tags Actions
|
||||
// @Produce json
|
||||
// @Param actionID path string true "Action ID"
|
||||
// @Success 200 {object} dto.ActionResponse
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "db error"
|
||||
// @Router /admin/actions/{actionID} [get]
|
||||
// @Security BearerAuth
|
||||
func GetAction(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
actionID, err := uuid.Parse(chi.URLParam(r, "actionID"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_action_id", "invalid action id")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.Action
|
||||
if err := db.Where("id = ?", actionID).First(&row).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "action not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, actionToDTO(row))
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAction godoc
|
||||
//
|
||||
// @ID CreateAction
|
||||
// @Summary Create an action
|
||||
// @Description Creates a new admin-configured action.
|
||||
// @Tags Actions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body dto.CreateActionRequest true "payload"
|
||||
// @Success 201 {object} dto.ActionResponse
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 500 {string} string "db error"
|
||||
// @Router /admin/actions [post]
|
||||
// @Security BearerAuth
|
||||
func CreateAction(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var in dto.CreateActionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
label := strings.TrimSpace(in.Label)
|
||||
desc := strings.TrimSpace(in.Description)
|
||||
target := strings.TrimSpace(in.MakeTarget)
|
||||
|
||||
if label == "" {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", "label is required")
|
||||
return
|
||||
}
|
||||
if desc == "" {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", "description is required")
|
||||
return
|
||||
}
|
||||
if target == "" {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", "make_target is required")
|
||||
return
|
||||
}
|
||||
|
||||
row := models.Action{
|
||||
Label: label,
|
||||
Description: desc,
|
||||
MakeTarget: target,
|
||||
}
|
||||
|
||||
if err := db.Create(&row).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusCreated, actionToDTO(row))
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateAction godoc
|
||||
//
|
||||
// @ID UpdateAction
|
||||
// @Summary Update an action
|
||||
// @Description Updates an action. Only provided fields are modified.
|
||||
// @Tags Actions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param actionID path string true "Action ID"
|
||||
// @Param body body dto.UpdateActionRequest true "payload"
|
||||
// @Success 200 {object} dto.ActionResponse
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "db error"
|
||||
// @Router /admin/actions/{actionID} [patch]
|
||||
// @Security BearerAuth
|
||||
func UpdateAction(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
actionID, err := uuid.Parse(chi.URLParam(r, "actionID"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_action_id", "invalid action id")
|
||||
return
|
||||
}
|
||||
|
||||
var in dto.UpdateActionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var row models.Action
|
||||
if err := db.Where("id = ?", actionID).First(&row).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "action not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
|
||||
if in.Label != nil {
|
||||
v := strings.TrimSpace(*in.Label)
|
||||
if v == "" {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", "label cannot be empty")
|
||||
return
|
||||
}
|
||||
row.Label = v
|
||||
}
|
||||
if in.Description != nil {
|
||||
v := strings.TrimSpace(*in.Description)
|
||||
if v == "" {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", "description cannot be empty")
|
||||
return
|
||||
}
|
||||
row.Description = v
|
||||
}
|
||||
if in.MakeTarget != nil {
|
||||
v := strings.TrimSpace(*in.MakeTarget)
|
||||
if v == "" {
|
||||
utils.WriteError(w, http.StatusBadRequest, "validation_error", "make_target cannot be empty")
|
||||
return
|
||||
}
|
||||
row.MakeTarget = v
|
||||
}
|
||||
|
||||
if err := db.Save(&row).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
|
||||
utils.WriteJSON(w, http.StatusOK, actionToDTO(row))
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteAction godoc
|
||||
//
|
||||
// @ID DeleteAction
|
||||
// @Summary Delete an action
|
||||
// @Description Deletes an action.
|
||||
// @Tags Actions
|
||||
// @Produce json
|
||||
// @Param actionID path string true "Action ID"
|
||||
// @Success 204 {string} string "deleted"
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "db error"
|
||||
// @Router /admin/actions/{actionID} [delete]
|
||||
// @Security BearerAuth
|
||||
func DeleteAction(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
actionID, err := uuid.Parse(chi.URLParam(r, "actionID"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_action_id", "invalid action id")
|
||||
return
|
||||
}
|
||||
|
||||
tx := db.Where("id = ?", actionID).Delete(&models.Action{})
|
||||
if tx.Error != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
if tx.RowsAffected == 0 {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "action not found")
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func actionToDTO(a models.Action) dto.ActionResponse {
|
||||
return dto.ActionResponse{
|
||||
ID: a.ID,
|
||||
Label: a.Label,
|
||||
Description: a.Description,
|
||||
MakeTarget: a.MakeTarget,
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: a.UpdatedAt,
|
||||
}
|
||||
}
|
||||
257
internal/handlers/cluster_runs.go
Normal file
257
internal/handlers/cluster_runs.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dyaksa/archer"
|
||||
"github.com/glueops/autoglue/internal/api/httpmiddleware"
|
||||
"github.com/glueops/autoglue/internal/bg"
|
||||
"github.com/glueops/autoglue/internal/handlers/dto"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ListClusterRuns godoc
|
||||
//
|
||||
// @ID ListClusterRuns
|
||||
// @Summary List cluster runs (org scoped)
|
||||
// @Description Returns runs for a cluster within the organization in X-Org-ID.
|
||||
// @Tags ClusterRuns
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param clusterID path string true "Cluster ID"
|
||||
// @Success 200 {array} dto.ClusterRunResponse
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "cluster not found"
|
||||
// @Failure 500 {string} string "db error"
|
||||
// @Router /clusters/{clusterID}/runs [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func ListClusterRuns(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
|
||||
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure cluster exists + org scoped
|
||||
if err := db.Select("id").
|
||||
Where("id = ? AND organization_id = ?", clusterID, orgID).
|
||||
First(&models.Cluster{}).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
|
||||
var rows []models.ClusterRun
|
||||
if err := db.
|
||||
Where("organization_id = ? AND cluster_id = ?", orgID, clusterID).
|
||||
Order("created_at DESC").
|
||||
Find(&rows).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]dto.ClusterRunResponse, 0, len(rows))
|
||||
for _, cr := range rows {
|
||||
out = append(out, clusterRunToDTO(cr))
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
// GetClusterRun godoc
|
||||
//
|
||||
// @ID GetClusterRun
|
||||
// @Summary Get a cluster run (org scoped)
|
||||
// @Description Returns a single run for a cluster within the organization in X-Org-ID.
|
||||
// @Tags ClusterRuns
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param clusterID path string true "Cluster ID"
|
||||
// @Param runID path string true "Run ID"
|
||||
// @Success 200 {object} dto.ClusterRunResponse
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "db error"
|
||||
// @Router /clusters/{clusterID}/runs/{runID} [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func GetClusterRun(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
|
||||
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
||||
return
|
||||
}
|
||||
|
||||
runID, err := uuid.Parse(chi.URLParam(r, "runID"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_run_id", "invalid run id")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.ClusterRun
|
||||
if err := db.
|
||||
Where("id = ? AND organization_id = ? AND cluster_id = ?", runID, orgID, clusterID).
|
||||
First(&row).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "run not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
|
||||
utils.WriteJSON(w, http.StatusOK, clusterRunToDTO(row))
|
||||
}
|
||||
}
|
||||
|
||||
// RunClusterAction godoc
|
||||
//
|
||||
// @ID RunClusterAction
|
||||
// @Summary Run an admin-configured action on a cluster (org scoped)
|
||||
// @Description Creates a ClusterRun record for the cluster/action. Execution is handled asynchronously by workers.
|
||||
// @Tags ClusterRuns
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param clusterID path string true "Cluster ID"
|
||||
// @Param actionID path string true "Action ID"
|
||||
// @Success 201 {object} dto.ClusterRunResponse
|
||||
// @Failure 400 {string} string "bad request"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "cluster or action not found"
|
||||
// @Failure 500 {string} string "db error"
|
||||
// @Router /clusters/{clusterID}/actions/{actionID}/runs [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func RunClusterAction(db *gorm.DB, jobs *bg.Jobs) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
if !ok {
|
||||
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
|
||||
return
|
||||
}
|
||||
|
||||
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
||||
return
|
||||
}
|
||||
|
||||
actionID, err := uuid.Parse(chi.URLParam(r, "actionID"))
|
||||
if err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_action_id", "invalid action id")
|
||||
return
|
||||
}
|
||||
|
||||
// cluster must exist + org scoped
|
||||
var cluster models.Cluster
|
||||
if err := db.Select("id", "organization_id").
|
||||
Where("id = ? AND organization_id = ?", clusterID, orgID).
|
||||
First(&cluster).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
|
||||
// action is global/admin-configured (not org scoped)
|
||||
var action models.Action
|
||||
if err := db.Where("id = ?", actionID).First(&action).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "action_not_found", "action not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
|
||||
run := models.ClusterRun{
|
||||
OrganizationID: orgID,
|
||||
ClusterID: clusterID,
|
||||
Action: action.MakeTarget, // this is what you actually execute
|
||||
Status: models.ClusterRunStatusQueued,
|
||||
Error: "",
|
||||
FinishedAt: time.Time{},
|
||||
}
|
||||
|
||||
if err := db.Create(&run).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
|
||||
// Enqueue with run.ID as the job ID so the worker can look it up.
|
||||
_, enqueueErr := jobs.Enqueue(
|
||||
r.Context(),
|
||||
run.ID.String(),
|
||||
"cluster_action",
|
||||
bg.ClusterActionWorker,
|
||||
archer.WithMaxRetries(3),
|
||||
)
|
||||
|
||||
if enqueueErr != nil {
|
||||
_ = db.Model(&models.ClusterRun{}).
|
||||
Where("id = ?", run.ID).
|
||||
Updates(map[string]any{
|
||||
"status": models.ClusterRunStatusFailed,
|
||||
"error": "failed to enqueue job",
|
||||
"finished_at": time.Now().UTC(),
|
||||
}).Error
|
||||
|
||||
utils.WriteError(w, http.StatusInternalServerError, "job_error", "failed to enqueue cluster action")
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusCreated, clusterRunToDTO(run))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func clusterRunToDTO(cr models.ClusterRun) dto.ClusterRunResponse {
|
||||
var finished *time.Time
|
||||
if !cr.FinishedAt.IsZero() {
|
||||
t := cr.FinishedAt
|
||||
finished = &t
|
||||
}
|
||||
return dto.ClusterRunResponse{
|
||||
ID: cr.ID,
|
||||
OrganizationID: cr.OrganizationID,
|
||||
ClusterID: cr.ClusterID,
|
||||
Action: cr.Action,
|
||||
Status: cr.Status,
|
||||
Error: cr.Error,
|
||||
CreatedAt: cr.CreatedAt,
|
||||
UpdatedAt: cr.UpdatedAt,
|
||||
FinishedAt: finished,
|
||||
}
|
||||
}
|
||||
28
internal/handlers/dto/actions.go
Normal file
28
internal/handlers/dto/actions.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ActionResponse struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
Label string `json:"label"`
|
||||
Description string `json:"description"`
|
||||
MakeTarget string `json:"make_target"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||
}
|
||||
|
||||
type CreateActionRequest struct {
|
||||
Label string `json:"label"`
|
||||
Description string `json:"description"`
|
||||
MakeTarget string `json:"make_target"`
|
||||
}
|
||||
|
||||
type UpdateActionRequest struct {
|
||||
Label *string `json:"label,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
MakeTarget *string `json:"make_target,omitempty"`
|
||||
}
|
||||
19
internal/handlers/dto/cluster_runs.go
Normal file
19
internal/handlers/dto/cluster_runs.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ClusterRunResponse struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
|
||||
ClusterID uuid.UUID `json:"cluster_id" format:"uuid"`
|
||||
Action string `json:"action"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||
FinishedAt *time.Time `json:"finished_at,omitempty" format:"date-time"`
|
||||
}
|
||||
@@ -828,16 +828,16 @@ func ListNodePoolLabels(db *gorm.DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
out := make([]dto.LabelResponse, 0, len(np.Taints))
|
||||
for _, taint := range np.Taints {
|
||||
for _, label := range np.Labels {
|
||||
out = append(out, dto.LabelResponse{
|
||||
AuditFields: common.AuditFields{
|
||||
ID: taint.ID,
|
||||
OrganizationID: taint.OrganizationID,
|
||||
CreatedAt: taint.CreatedAt,
|
||||
UpdatedAt: taint.UpdatedAt,
|
||||
ID: label.ID,
|
||||
OrganizationID: label.OrganizationID,
|
||||
CreatedAt: label.CreatedAt,
|
||||
UpdatedAt: label.UpdatedAt,
|
||||
},
|
||||
Key: taint.Key,
|
||||
Value: taint.Value,
|
||||
Key: label.Key,
|
||||
Value: label.Value,
|
||||
})
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, out)
|
||||
|
||||
16
internal/models/action.go
Normal file
16
internal/models/action.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Action struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
||||
Label string `gorm:"type:varchar(255);not null;uniqueIndex" json:"label"`
|
||||
Description string `gorm:"type:text;not null" json:"description"`
|
||||
MakeTarget string `gorm:"type:varchar(255);not null;uniqueIndex" json:"make_target"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" format:"date-time"`
|
||||
}
|
||||
@@ -7,12 +7,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ClusterStatusPrePending = "pre_pending" // needs validation
|
||||
ClusterStatusIncomplete = "incomplete" // invalid/missing shape
|
||||
ClusterStatusPending = "pending" // valid shape, waiting for provisioning
|
||||
ClusterStatusProvisioning = "provisioning"
|
||||
ClusterStatusReady = "ready"
|
||||
ClusterStatusFailed = "failed" // provisioning/runtime failure
|
||||
ClusterStatusPrePending = "pre_pending" // needs validation
|
||||
ClusterStatusIncomplete = "incomplete" // invalid/missing shape
|
||||
ClusterStatusPending = "pending" // valid shape, waiting for provisioning
|
||||
ClusterStatusProvisioning = "provisioning"
|
||||
ClusterStatusReady = "ready"
|
||||
ClusterStatusFailed = "failed" // provisioning/runtime failure
|
||||
ClusterStatusBootstrapping = "bootstrapping"
|
||||
)
|
||||
|
||||
type Cluster struct {
|
||||
|
||||
27
internal/models/cluster_runs.go
Normal file
27
internal/models/cluster_runs.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
ClusterRunStatusQueued = "queued"
|
||||
ClusterRunStatusRunning = "running"
|
||||
ClusterRunStatusSuccess = "success"
|
||||
ClusterRunStatusFailed = "failed"
|
||||
ClusterRunStatusCanceled = "canceled"
|
||||
)
|
||||
|
||||
type ClusterRun struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id" format:"uuid"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" gorm:"type:uuid;index"`
|
||||
ClusterID uuid.UUID `json:"cluster_id" gorm:"type:uuid;index"`
|
||||
Action string `json:"action" gorm:"type:text;not null"`
|
||||
Status string `json:"status" gorm:"type:text;not null"`
|
||||
Error string `json:"error" gorm:"type:text;not null"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()" format:"date-time"`
|
||||
FinishedAt time.Time `json:"finished_at,omitempty" gorm:"type:timestamptz" format:"date-time"`
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -46,13 +46,13 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.557.0",
|
||||
"lucide-react": "^0.561.0",
|
||||
"motion": "^12.23.26",
|
||||
"next-themes": "^0.4.6",
|
||||
"rapidoc": "^9.3.8",
|
||||
"react": "^19.2.1",
|
||||
"react": "^19.2.3",
|
||||
"react-day-picker": "^9.12.0",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hook-form": "^7.68.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
@@ -60,27 +60,27 @@
|
||||
"recharts": "2.15.4",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.1",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@ianvs/prettier-plugin-sort-imports": "4.7.0",
|
||||
"@types/node": "24.10.2",
|
||||
"@types/node": "25.0.1",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "5.1.2",
|
||||
"eslint": "9.39.1",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
"eslint-plugin-react-refresh": "0.4.24",
|
||||
"globals": "16.5.0",
|
||||
"prettier": "3.7.4",
|
||||
"prettier-plugin-tailwindcss": "0.7.2",
|
||||
"shadcn": "3.5.2",
|
||||
"shadcn": "3.6.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.49.0",
|
||||
"typescript-eslint": "8.50.0",
|
||||
"vite": "7.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AppShell } from "@/layouts/app-shell.tsx"
|
||||
import { Route, Routes } from "react-router-dom"
|
||||
|
||||
import { ProtectedRoute } from "@/components/protected-route.tsx"
|
||||
import { ActionsPage } from "@/pages/actions-page.tsx"
|
||||
import { AnnotationPage } from "@/pages/annotation-page.tsx"
|
||||
import { ClustersPage } from "@/pages/cluster-page"
|
||||
import { CredentialPage } from "@/pages/credential-page.tsx"
|
||||
@@ -46,6 +47,7 @@ export default function App() {
|
||||
<Route path="/clusters" element={<ClustersPage />} />
|
||||
|
||||
<Route path="/admin/jobs" element={<JobsPage />} />
|
||||
<Route path="/admin/actions" element={<ActionsPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="*" element={<Login />} />
|
||||
|
||||
30
ui/src/api/actions.ts
Normal file
30
ui/src/api/actions.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { withRefresh } from "@/api/with-refresh.ts"
|
||||
import type { DtoCreateActionRequest, DtoUpdateActionRequest } from "@/sdk"
|
||||
import { makeActionsApi } from "@/sdkClient.ts"
|
||||
|
||||
const actions = makeActionsApi()
|
||||
export const actionsApi = {
|
||||
listActions: () =>
|
||||
withRefresh(async () => {
|
||||
return await actions.listActions()
|
||||
}),
|
||||
createAction: (body: DtoCreateActionRequest) =>
|
||||
withRefresh(async () => {
|
||||
return await actions.createAction({
|
||||
dtoCreateActionRequest: body,
|
||||
})
|
||||
}),
|
||||
updateAction: (id: string, body: DtoUpdateActionRequest) =>
|
||||
withRefresh(async () => {
|
||||
return await actions.updateAction({
|
||||
actionID: id,
|
||||
dtoUpdateActionRequest: body,
|
||||
})
|
||||
}),
|
||||
deleteAction: (id: string) =>
|
||||
withRefresh(async () => {
|
||||
await actions.deleteAction({
|
||||
actionID: id,
|
||||
})
|
||||
}),
|
||||
}
|
||||
@@ -8,9 +8,10 @@ import type {
|
||||
DtoSetKubeconfigRequest,
|
||||
DtoUpdateClusterRequest,
|
||||
} from "@/sdk"
|
||||
import { makeClusterApi } from "@/sdkClient"
|
||||
import { makeClusterApi, makeClusterRunsApi } from "@/sdkClient"
|
||||
|
||||
const clusters = makeClusterApi()
|
||||
const clusterRuns = makeClusterRunsApi()
|
||||
|
||||
export const clustersApi = {
|
||||
// --- basic CRUD ---
|
||||
@@ -147,4 +148,20 @@ export const clustersApi = {
|
||||
withRefresh(async () => {
|
||||
return await clusters.detachNodePool({ clusterID, nodePoolID })
|
||||
}),
|
||||
|
||||
// --- cluster runs / actions ---
|
||||
listClusterRuns: (clusterID: string) =>
|
||||
withRefresh(async () => {
|
||||
return await clusterRuns.listClusterRuns({ clusterID })
|
||||
}),
|
||||
|
||||
getClusterRun: (clusterID: string, runID: string) =>
|
||||
withRefresh(async () => {
|
||||
return await clusterRuns.getClusterRun({ clusterID, runID })
|
||||
}),
|
||||
|
||||
runClusterAction: (clusterID: string, actionID: string) =>
|
||||
withRefresh(async () => {
|
||||
return await clusterRuns.runClusterAction({ clusterID, actionID })
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FileKey2Icon,
|
||||
KeyRound,
|
||||
LockKeyholeIcon,
|
||||
PickaxeIcon,
|
||||
ServerIcon,
|
||||
SprayCanIcon,
|
||||
TagsIcon,
|
||||
@@ -49,5 +50,6 @@ export const userNav: NavItem[] = [{ to: "/me", label: "Profile", icon: User2 }]
|
||||
export const adminNav: NavItem[] = [
|
||||
{ to: "/admin/users", label: "Users Admin", icon: Users },
|
||||
{ to: "/admin/jobs", label: "Jobs Admin", icon: GrUserWorker },
|
||||
{ to: "/admin/actions", label: "Actions Admin", icon: PickaxeIcon},
|
||||
{ to: "/docs", label: "API Docs ", icon: SiSwagger, target: "_blank" },
|
||||
]
|
||||
|
||||
433
ui/src/pages/actions-page.tsx
Normal file
433
ui/src/pages/actions-page.tsx
Normal file
@@ -0,0 +1,433 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import { actionsApi } from "@/api/actions.ts"
|
||||
import type { DtoActionResponse } from "@/sdk"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { AlertCircle, CircleSlash2, Loader2, Pencil, Plus, Search, Trash2 } from "lucide-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Badge } from "@/components/ui/badge.tsx"
|
||||
import { Button } from "@/components/ui/button.tsx"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog.tsx"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form.tsx"
|
||||
import { Input } from "@/components/ui/input.tsx"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table.tsx"
|
||||
import { Textarea } from "@/components/ui/textarea.tsx"
|
||||
|
||||
const createActionSchema = z.object({
|
||||
label: z.string().trim().min(1, "Label is required").max(255, "Max 255 chars"),
|
||||
description: z.string().trim().min(1, "Description is required"),
|
||||
make_target: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Make target is required")
|
||||
.max(255, "Max 255 chars")
|
||||
// keep client-side fairly strict to avoid surprises; server should also validate
|
||||
.regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/, "Invalid make target (allowed: a-z A-Z 0-9 . _ -)"),
|
||||
})
|
||||
type CreateActionInput = z.input<typeof createActionSchema>
|
||||
|
||||
const updateActionSchema = createActionSchema.partial()
|
||||
type UpdateActionInput = z.input<typeof updateActionSchema>
|
||||
|
||||
function TargetBadge({ target }: { target?: string | null }) {
|
||||
if (!target) {
|
||||
return (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
—
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Badge variant="secondary" className="font-mono text-xs">
|
||||
{target}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
export const ActionsPage = () => {
|
||||
const qc = useQueryClient()
|
||||
|
||||
const [filter, setFilter] = useState("")
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [updateOpen, setUpdateOpen] = useState(false)
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||
const [editing, setEditing] = useState<DtoActionResponse | null>(null)
|
||||
|
||||
const actionsQ = useQuery({
|
||||
queryKey: ["admin-actions"],
|
||||
queryFn: () => actionsApi.listActions(),
|
||||
})
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const data: DtoActionResponse[] = actionsQ.data ?? []
|
||||
const q = filter.trim().toLowerCase()
|
||||
if (!q) return data
|
||||
|
||||
return data.filter((a) => {
|
||||
return (
|
||||
(a.label ?? "").toLowerCase().includes(q) ||
|
||||
(a.description ?? "").toLowerCase().includes(q) ||
|
||||
(a.make_target ?? "").toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
}, [filter, actionsQ.data])
|
||||
|
||||
const createForm = useForm<CreateActionInput>({
|
||||
resolver: zodResolver(createActionSchema),
|
||||
defaultValues: {
|
||||
label: "",
|
||||
description: "",
|
||||
make_target: "",
|
||||
},
|
||||
})
|
||||
|
||||
const createMut = useMutation({
|
||||
mutationFn: (values: CreateActionInput) => actionsApi.createAction(values),
|
||||
onSuccess: async () => {
|
||||
await qc.invalidateQueries({ queryKey: ["admin-actions"] })
|
||||
createForm.reset()
|
||||
setCreateOpen(false)
|
||||
toast.success("Action created.")
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast.error(err?.message ?? "Failed to create action.")
|
||||
},
|
||||
})
|
||||
|
||||
const updateForm = useForm<UpdateActionInput>({
|
||||
resolver: zodResolver(updateActionSchema),
|
||||
defaultValues: {},
|
||||
})
|
||||
|
||||
const updateMut = useMutation({
|
||||
mutationFn: ({ id, values }: { id: string; values: UpdateActionInput }) =>
|
||||
actionsApi.updateAction(id, values),
|
||||
onSuccess: async () => {
|
||||
await qc.invalidateQueries({ queryKey: ["admin-actions"] })
|
||||
updateForm.reset()
|
||||
setUpdateOpen(false)
|
||||
setEditing(null)
|
||||
toast.success("Action updated.")
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast.error(err?.message ?? "Failed to update action.")
|
||||
},
|
||||
})
|
||||
|
||||
const openEdit = (a: DtoActionResponse) => {
|
||||
if (!a.id) return
|
||||
setEditing(a)
|
||||
updateForm.reset({
|
||||
label: a.label ?? "",
|
||||
description: a.description ?? "",
|
||||
make_target: a.make_target ?? "",
|
||||
})
|
||||
setUpdateOpen(true)
|
||||
}
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: (id: string) => actionsApi.deleteAction(id),
|
||||
onSuccess: async () => {
|
||||
await qc.invalidateQueries({ queryKey: ["admin-actions"] })
|
||||
setDeleteId(null)
|
||||
toast.success("Action deleted.")
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast.error(err?.message ?? "Failed to delete action.")
|
||||
},
|
||||
})
|
||||
|
||||
if (actionsQ.isLoading) return <div className="p-6">Loading actions…</div>
|
||||
if (actionsQ.error) return <div className="p-6 text-red-500">Error loading actions.</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-6">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<h1 className="text-2xl font-bold">Admin Actions</h1>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute top-2.5 left-2 h-4 w-4 opacity-60" />
|
||||
<Input
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
placeholder="Search actions"
|
||||
className="w-72 pl-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={() => setCreateOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Action
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Action</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...createForm}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={createForm.handleSubmit((v) => createMut.mutate(v))}
|
||||
>
|
||||
<FormField
|
||||
control={createForm.control}
|
||||
name="label"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Label</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Setup" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={createForm.control}
|
||||
name="make_target"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Make Target</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="setup" className="font-mono" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={createForm.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
rows={4}
|
||||
placeholder="Runs prepare, ping-servers, then make setup on the bastion."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setCreateOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={createMut.isPending}>
|
||||
{createMut.isPending ? "Creating…" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-background overflow-hidden rounded-2xl border shadow-sm">
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Label</TableHead>
|
||||
<TableHead>Make Target</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead className="w-[260px] text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map((a) => (
|
||||
<TableRow key={a.id}>
|
||||
<TableCell className="font-medium">{a.label}</TableCell>
|
||||
<TableCell>
|
||||
<TargetBadge target={a.make_target} />
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground max-w-[680px] truncate">
|
||||
{a.description}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => openEdit(a)}>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => a.id && setDeleteId(a.id)}
|
||||
disabled={deleteMut.isPending && deleteId === a.id}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{deleteMut.isPending && deleteId === a.id ? "Deleting…" : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-muted-foreground py-10 text-center">
|
||||
<CircleSlash2 className="mx-auto mb-2 h-6 w-6 opacity-60" />
|
||||
No actions match your search.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Update dialog */}
|
||||
<Dialog
|
||||
open={updateOpen}
|
||||
onOpenChange={(open) => {
|
||||
setUpdateOpen(open)
|
||||
if (!open) setEditing(null)
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Action</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{editing ? (
|
||||
<Form {...updateForm}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={updateForm.handleSubmit((values) => {
|
||||
if (!editing.id) return
|
||||
updateMut.mutate({ id: editing.id, values })
|
||||
})}
|
||||
>
|
||||
<FormField
|
||||
control={updateForm.control}
|
||||
name="label"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Label</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={updateForm.control}
|
||||
name="make_target"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Make Target</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="font-mono" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={updateForm.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea rows={4} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setUpdateOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={updateMut.isPending}>
|
||||
{updateMut.isPending ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Saving…
|
||||
</span>
|
||||
) : (
|
||||
"Save changes"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
) : (
|
||||
<div className="text-muted-foreground text-sm">No action selected.</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete confirm dialog */}
|
||||
<Dialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete action</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="mt-0.5 h-5 w-5 text-red-500" />
|
||||
<p className="text-muted-foreground text-sm">
|
||||
This action cannot be undone. Are you sure you want to delete it?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" onClick={() => setDeleteId(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => deleteId && deleteMut.mutate(deleteId)}
|
||||
disabled={deleteMut.isPending}
|
||||
>
|
||||
{deleteMut.isPending ? "Deleting…" : "Delete"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,56 +1,34 @@
|
||||
// src/pages/ClustersPage.tsx
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { actionsApi } from "@/api/actions";
|
||||
import { clustersApi } from "@/api/clusters";
|
||||
import { dnsApi } from "@/api/dns";
|
||||
import { loadBalancersApi } from "@/api/loadbalancers";
|
||||
import { nodePoolsApi } from "@/api/node_pools";
|
||||
import { serversApi } from "@/api/servers";
|
||||
import type { DtoActionResponse, DtoClusterResponse, DtoClusterRunResponse, DtoDomainResponse, DtoLoadBalancerResponse, DtoNodePoolResponse, DtoRecordSetResponse, DtoServerResponse } from "@/sdk";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AlertCircle, CheckCircle2, CircleSlash2, FileCode2, Globe2, Loader2, MapPin, Pencil, Plus, Search, Server, Wrench } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
|
||||
|
||||
import { truncateMiddle } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
|
||||
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { clustersApi } from "@/api/clusters"
|
||||
import { dnsApi } from "@/api/dns"
|
||||
import { loadBalancersApi } from "@/api/loadbalancers"
|
||||
import { nodePoolsApi } from "@/api/node_pools"
|
||||
import { serversApi } from "@/api/servers"
|
||||
import type {
|
||||
DtoClusterResponse,
|
||||
DtoDomainResponse,
|
||||
DtoLoadBalancerResponse,
|
||||
DtoNodePoolResponse,
|
||||
DtoRecordSetResponse,
|
||||
DtoServerResponse,
|
||||
} from "@/sdk"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
CircleSlash2,
|
||||
FileCode2,
|
||||
Globe2,
|
||||
Loader2,
|
||||
MapPin,
|
||||
Pencil,
|
||||
Plus,
|
||||
Search,
|
||||
Server,
|
||||
Wrench,
|
||||
} from "lucide-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
import { truncateMiddle } from "@/lib/utils"
|
||||
import { Badge } from "@/components/ui/badge.tsx"
|
||||
import { Button } from "@/components/ui/button.tsx"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog.tsx"
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form.tsx"
|
||||
import { Input } from "@/components/ui/input.tsx"
|
||||
import { Label } from "@/components/ui/label.tsx"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select.tsx"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table.tsx"
|
||||
import { Textarea } from "@/components/ui/textarea.tsx"
|
||||
|
||||
// --- Schemas ---
|
||||
|
||||
@@ -66,6 +44,22 @@ type CreateClusterInput = z.input<typeof createClusterSchema>
|
||||
const updateClusterSchema = createClusterSchema.partial()
|
||||
type UpdateClusterValues = z.infer<typeof updateClusterSchema>
|
||||
|
||||
// --- Data normalization helpers (fixes rows.some is not a function) ---
|
||||
|
||||
function asArray<T>(res: any): T[] {
|
||||
if (Array.isArray(res)) return res as T[]
|
||||
if (Array.isArray(res?.data)) return res.data as T[]
|
||||
if (Array.isArray(res?.body)) return res.body as T[]
|
||||
if (Array.isArray(res?.result)) return res.result as T[]
|
||||
return []
|
||||
}
|
||||
|
||||
function asObject<T>(res: any): T {
|
||||
// for get endpoints that might return {data: {...}}
|
||||
if (res?.data && typeof res.data === "object") return res.data as T
|
||||
return res as T
|
||||
}
|
||||
|
||||
// --- UI helpers ---
|
||||
|
||||
function StatusBadge({ status }: { status?: string | null }) {
|
||||
@@ -122,6 +116,61 @@ function StatusBadge({ status }: { status?: string | null }) {
|
||||
)
|
||||
}
|
||||
|
||||
function RunStatusBadge({ status }: { status?: string | null }) {
|
||||
const s = (status ?? "").toLowerCase()
|
||||
|
||||
if (!s)
|
||||
return (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
unknown
|
||||
</Badge>
|
||||
)
|
||||
|
||||
if (s === "succeeded" || s === "success") {
|
||||
return (
|
||||
<Badge variant="default" className="flex items-center gap-1 text-xs">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
succeeded
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
if (s === "failed" || s === "error") {
|
||||
return (
|
||||
<Badge variant="destructive" className="flex items-center gap-1 text-xs">
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
failed
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
if (s === "queued" || s === "running") {
|
||||
return (
|
||||
<Badge variant="secondary" className="flex items-center gap-1 text-xs">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
{s}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{s}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
function fmtTime(v: any): string {
|
||||
if (!v) return "-"
|
||||
try {
|
||||
const d = v instanceof Date ? v : new Date(v)
|
||||
if (Number.isNaN(d.getTime())) return "-"
|
||||
return d.toLocaleString()
|
||||
} catch {
|
||||
return "-"
|
||||
}
|
||||
}
|
||||
|
||||
function ClusterSummary({ c }: { c: DtoClusterResponse }) {
|
||||
return (
|
||||
<div className="text-muted-foreground flex flex-col gap-1 text-xs">
|
||||
@@ -162,7 +211,7 @@ export const ClustersPage = () => {
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
|
||||
// Config dialog state
|
||||
// Configure dialog state
|
||||
const [configCluster, setConfigCluster] = useState<DtoClusterResponse | null>(null)
|
||||
|
||||
const [captainDomainId, setCaptainDomainId] = useState("")
|
||||
@@ -182,36 +231,69 @@ export const ClustersPage = () => {
|
||||
|
||||
const clustersQ = useQuery({
|
||||
queryKey: ["clusters"],
|
||||
queryFn: () => clustersApi.listClusters(),
|
||||
queryFn: async () => asArray<DtoClusterResponse>(await clustersApi.listClusters()),
|
||||
})
|
||||
|
||||
const lbsQ = useQuery({
|
||||
queryKey: ["load-balancers"],
|
||||
queryFn: () => loadBalancersApi.listLoadBalancers(),
|
||||
queryFn: async () =>
|
||||
asArray<DtoLoadBalancerResponse>(await loadBalancersApi.listLoadBalancers()),
|
||||
})
|
||||
|
||||
const domainsQ = useQuery({
|
||||
queryKey: ["domains"],
|
||||
queryFn: () => dnsApi.listDomains(),
|
||||
queryFn: async () => asArray<DtoDomainResponse>(await dnsApi.listDomains()),
|
||||
})
|
||||
|
||||
// record sets fetched per captain domain
|
||||
const recordSetsQ = useQuery({
|
||||
queryKey: ["record-sets", captainDomainId],
|
||||
enabled: !!captainDomainId,
|
||||
queryFn: () => dnsApi.listRecordSetsByDomain(captainDomainId),
|
||||
queryFn: async () =>
|
||||
asArray<DtoRecordSetResponse>(await dnsApi.listRecordSetsByDomain(captainDomainId)),
|
||||
})
|
||||
|
||||
const serversQ = useQuery({
|
||||
queryKey: ["servers"],
|
||||
queryFn: () => serversApi.listServers(),
|
||||
queryFn: async () => asArray<DtoServerResponse>(await serversApi.listServers()),
|
||||
})
|
||||
|
||||
const npQ = useQuery({
|
||||
queryKey: ["node-pools"],
|
||||
queryFn: () => nodePoolsApi.listNodePools(),
|
||||
queryFn: async () => asArray<DtoNodePoolResponse>(await nodePoolsApi.listNodePools()),
|
||||
})
|
||||
|
||||
const actionsQ = useQuery({
|
||||
queryKey: ["actions"],
|
||||
queryFn: async () => asArray<DtoActionResponse>(await actionsApi.listActions()),
|
||||
})
|
||||
|
||||
const runsQ = useQuery({
|
||||
queryKey: ["cluster-runs", configCluster?.id],
|
||||
enabled: !!configCluster?.id,
|
||||
queryFn: async () =>
|
||||
asArray<DtoClusterRunResponse>(await clustersApi.listClusterRuns(configCluster!.id!)),
|
||||
refetchInterval: (data) => {
|
||||
// IMPORTANT: data might not be array if queryFn isn't normalizing. But it is here anyway.
|
||||
const rows = Array.isArray(data) ? data : []
|
||||
const active = rows.some((r: any) => {
|
||||
const s = String(r?.status ?? "").toLowerCase()
|
||||
return s === "queued" || s === "running"
|
||||
})
|
||||
return active ? 2000 : false
|
||||
},
|
||||
})
|
||||
|
||||
const actionLabelByTarget = useMemo(() => {
|
||||
const m = new Map<string, string>()
|
||||
;(actionsQ.data ?? []).forEach((a) => {
|
||||
if (a.make_target) m.set(a.make_target, a.label ?? a.make_target)
|
||||
})
|
||||
return m
|
||||
}, [actionsQ.data])
|
||||
|
||||
const runDisplayName = (r: DtoClusterRunResponse) =>
|
||||
actionLabelByTarget.get(r.action ?? "") ?? r.action ?? "unknown"
|
||||
|
||||
// --- Create ---
|
||||
|
||||
const createForm = useForm<CreateClusterInput>({
|
||||
@@ -233,15 +315,10 @@ export const ClustersPage = () => {
|
||||
setCreateOpen(false)
|
||||
toast.success("Cluster created successfully.")
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast.error(err?.message ?? "There was an error while creating the cluster")
|
||||
},
|
||||
onError: (err: any) =>
|
||||
toast.error(err?.message ?? "There was an error while creating the cluster"),
|
||||
})
|
||||
|
||||
const onCreateSubmit = (values: CreateClusterInput) => {
|
||||
createMut.mutate(values)
|
||||
}
|
||||
|
||||
// --- Update basic details ---
|
||||
|
||||
const updateForm = useForm<UpdateClusterValues>({
|
||||
@@ -258,9 +335,8 @@ export const ClustersPage = () => {
|
||||
setUpdateOpen(false)
|
||||
toast.success("Cluster updated successfully.")
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast.error(err?.message ?? "There was an error while updating the cluster")
|
||||
},
|
||||
onError: (err: any) =>
|
||||
toast.error(err?.message ?? "There was an error while updating the cluster"),
|
||||
})
|
||||
|
||||
const openEdit = (cluster: DtoClusterResponse) => {
|
||||
@@ -285,11 +361,32 @@ export const ClustersPage = () => {
|
||||
setDeleteId(null)
|
||||
toast.success("Cluster deleted successfully.")
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast.error(err?.message ?? "There was an error while deleting the cluster")
|
||||
},
|
||||
onError: (err: any) =>
|
||||
toast.error(err?.message ?? "There was an error while deleting the cluster"),
|
||||
})
|
||||
|
||||
// --- Run Action ---
|
||||
|
||||
const runActionMut = useMutation({
|
||||
mutationFn: ({ clusterID, actionID }: { clusterID: string; actionID: string }) =>
|
||||
clustersApi.runClusterAction(clusterID, actionID),
|
||||
onSuccess: async () => {
|
||||
await qc.invalidateQueries({ queryKey: ["cluster-runs", configCluster?.id] })
|
||||
toast.success("Action enqueued.")
|
||||
},
|
||||
onError: (err: any) => toast.error(err?.message ?? "Failed to enqueue action."),
|
||||
})
|
||||
|
||||
async function handleRunAction(actionID: string) {
|
||||
if (!configCluster?.id) return
|
||||
setBusyKey(`run:${actionID}`)
|
||||
try {
|
||||
await runActionMut.mutateAsync({ clusterID: configCluster.id, actionID })
|
||||
} finally {
|
||||
setBusyKey(null)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Filter ---
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
@@ -322,30 +419,23 @@ export const ClustersPage = () => {
|
||||
return
|
||||
}
|
||||
|
||||
// Prefill IDs from current attachments
|
||||
if (configCluster.captain_domain?.id) {
|
||||
setCaptainDomainId(configCluster.captain_domain.id)
|
||||
}
|
||||
if (configCluster.control_plane_record_set?.id) {
|
||||
if (configCluster.captain_domain?.id) setCaptainDomainId(configCluster.captain_domain.id)
|
||||
if (configCluster.control_plane_record_set?.id)
|
||||
setRecordSetId(configCluster.control_plane_record_set.id)
|
||||
}
|
||||
if (configCluster.apps_load_balancer?.id) {
|
||||
setAppsLbId(configCluster.apps_load_balancer.id)
|
||||
}
|
||||
if (configCluster.glueops_load_balancer?.id) {
|
||||
if (configCluster.apps_load_balancer?.id) setAppsLbId(configCluster.apps_load_balancer.id)
|
||||
if (configCluster.glueops_load_balancer?.id)
|
||||
setGlueopsLbId(configCluster.glueops_load_balancer.id)
|
||||
}
|
||||
if (configCluster.bastion_server?.id) {
|
||||
setBastionId(configCluster.bastion_server.id)
|
||||
}
|
||||
if (configCluster.bastion_server?.id) setBastionId(configCluster.bastion_server.id)
|
||||
}, [configCluster])
|
||||
|
||||
async function refreshConfigCluster() {
|
||||
if (!configCluster?.id) return
|
||||
try {
|
||||
const updated = await clustersApi.getCluster(configCluster.id)
|
||||
const updatedRaw = await clustersApi.getCluster(configCluster.id)
|
||||
const updated = asObject<DtoClusterResponse>(updatedRaw)
|
||||
setConfigCluster(updated)
|
||||
await qc.invalidateQueries({ queryKey: ["clusters"] })
|
||||
await qc.invalidateQueries({ queryKey: ["cluster-runs", configCluster.id] })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -353,15 +443,10 @@ export const ClustersPage = () => {
|
||||
|
||||
async function handleAttachCaptain() {
|
||||
if (!configCluster?.id) return
|
||||
if (!captainDomainId) {
|
||||
toast.error("Domain is required")
|
||||
return
|
||||
}
|
||||
if (!captainDomainId) return toast.error("Domain is required")
|
||||
setBusyKey("captain")
|
||||
try {
|
||||
await clustersApi.attachCaptainDomain(configCluster.id, {
|
||||
domain_id: captainDomainId,
|
||||
})
|
||||
await clustersApi.attachCaptainDomain(configCluster.id, { domain_id: captainDomainId })
|
||||
toast.success("Captain domain attached.")
|
||||
await refreshConfigCluster()
|
||||
} catch (err: any) {
|
||||
@@ -387,10 +472,7 @@ export const ClustersPage = () => {
|
||||
|
||||
async function handleAttachRecordSet() {
|
||||
if (!configCluster?.id) return
|
||||
if (!recordSetId) {
|
||||
toast.error("Record set is required")
|
||||
return
|
||||
}
|
||||
if (!recordSetId) return toast.error("Record set is required")
|
||||
setBusyKey("recordset")
|
||||
try {
|
||||
await clustersApi.attachControlPlaneRecordSet(configCluster.id, {
|
||||
@@ -421,15 +503,10 @@ export const ClustersPage = () => {
|
||||
|
||||
async function handleAttachAppsLb() {
|
||||
if (!configCluster?.id) return
|
||||
if (!appsLbId) {
|
||||
toast.error("Load balancer is required")
|
||||
return
|
||||
}
|
||||
if (!appsLbId) return toast.error("Load balancer is required")
|
||||
setBusyKey("apps-lb")
|
||||
try {
|
||||
await clustersApi.attachAppsLoadBalancer(configCluster.id, {
|
||||
load_balancer_id: appsLbId,
|
||||
})
|
||||
await clustersApi.attachAppsLoadBalancer(configCluster.id, { load_balancer_id: appsLbId })
|
||||
toast.success("Apps load balancer attached.")
|
||||
await refreshConfigCluster()
|
||||
} catch (err: any) {
|
||||
@@ -455,10 +532,7 @@ export const ClustersPage = () => {
|
||||
|
||||
async function handleAttachGlueopsLb() {
|
||||
if (!configCluster?.id) return
|
||||
if (!glueopsLbId) {
|
||||
toast.error("Load balancer is required")
|
||||
return
|
||||
}
|
||||
if (!glueopsLbId) return toast.error("Load balancer is required")
|
||||
setBusyKey("glueops-lb")
|
||||
try {
|
||||
await clustersApi.attachGlueOpsLoadBalancer(configCluster.id, {
|
||||
@@ -489,15 +563,10 @@ export const ClustersPage = () => {
|
||||
|
||||
async function handleAttachBastion() {
|
||||
if (!configCluster?.id) return
|
||||
if (!bastionId) {
|
||||
toast.error("Server is required")
|
||||
return
|
||||
}
|
||||
if (!bastionId) return toast.error("Server is required")
|
||||
setBusyKey("bastion")
|
||||
try {
|
||||
await clustersApi.attachBastion(configCluster.id, {
|
||||
server_id: bastionId,
|
||||
})
|
||||
await clustersApi.attachBastion(configCluster.id, { server_id: bastionId })
|
||||
toast.success("Bastion server attached.")
|
||||
await refreshConfigCluster()
|
||||
} catch (err: any) {
|
||||
@@ -523,10 +592,7 @@ export const ClustersPage = () => {
|
||||
|
||||
async function handleAttachNodePool() {
|
||||
if (!configCluster?.id) return
|
||||
if (!nodePoolId) {
|
||||
toast.error("Node pool is required")
|
||||
return
|
||||
}
|
||||
if (!nodePoolId) return toast.error("Node pool is required")
|
||||
setBusyKey("nodepool")
|
||||
try {
|
||||
await clustersApi.attachNodePool(configCluster.id, nodePoolId)
|
||||
@@ -556,15 +622,10 @@ export const ClustersPage = () => {
|
||||
|
||||
async function handleSetKubeconfig() {
|
||||
if (!configCluster?.id) return
|
||||
if (!kubeconfigText.trim()) {
|
||||
toast.error("Kubeconfig is required")
|
||||
return
|
||||
}
|
||||
if (!kubeconfigText.trim()) return toast.error("Kubeconfig is required")
|
||||
setBusyKey("kubeconfig")
|
||||
try {
|
||||
await clustersApi.setKubeconfig(configCluster.id, {
|
||||
kubeconfig: kubeconfigText,
|
||||
})
|
||||
await clustersApi.setKubeconfig(configCluster.id, { kubeconfig: kubeconfigText })
|
||||
toast.success("Kubeconfig updated.")
|
||||
setKubeconfigText("")
|
||||
await refreshConfigCluster()
|
||||
@@ -625,7 +686,10 @@ export const ClustersPage = () => {
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...createForm}>
|
||||
<form className="space-y-4" onSubmit={createForm.handleSubmit(onCreateSubmit)}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={createForm.handleSubmit((v) => createMut.mutate(v))}
|
||||
>
|
||||
<FormField
|
||||
control={createForm.control}
|
||||
name="name"
|
||||
@@ -739,7 +803,7 @@ export const ClustersPage = () => {
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{c.docker_image + ":" + c.docker_tag}</TableCell>
|
||||
<TableCell>{(c.docker_image ?? "") + ":" + (c.docker_tag ?? "")}</TableCell>
|
||||
<TableCell>
|
||||
<ClusterSummary c={c} />
|
||||
{c.id && (
|
||||
@@ -771,7 +835,7 @@ export const ClustersPage = () => {
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-muted-foreground py-10 text-center">
|
||||
<TableCell colSpan={7} className="text-muted-foreground py-10 text-center">
|
||||
<CircleSlash2 className="mx-auto mb-2 h-6 w-6 opacity-60" />
|
||||
No clusters match your search.
|
||||
</TableCell>
|
||||
@@ -788,6 +852,7 @@ export const ClustersPage = () => {
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Cluster</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...updateForm}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
@@ -879,7 +944,7 @@ export const ClustersPage = () => {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Configure dialog (attachments + kubeconfig + node pools) */}
|
||||
{/* Configure dialog (attachments + kubeconfig + node pools + actions/runs) */}
|
||||
<Dialog open={!!configCluster} onOpenChange={(open) => !open && setConfigCluster(null)}>
|
||||
<DialogContent className="max-h-[90vh] w-full max-w-3xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
@@ -890,26 +955,144 @@ export const ClustersPage = () => {
|
||||
|
||||
{configCluster && (
|
||||
<div className="space-y-6 py-2">
|
||||
{/* Kubeconfig */}
|
||||
{/* Cluster Actions */}
|
||||
<section className="space-y-2 rounded-xl border p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileCode2 className="h-4 w-4" />
|
||||
<h3 className="text-sm font-semibold">Kubeconfig</h3>
|
||||
<Wrench className="h-4 w-4" />
|
||||
<h3 className="text-sm font-semibold">Cluster Actions</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Paste the kubeconfig for this cluster. It will be stored encrypted and never
|
||||
returned by the API.
|
||||
Run admin-configured actions on this cluster. Actions are executed
|
||||
asynchronously.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => runsQ.refetch()}
|
||||
disabled={runsQ.isFetching || !configCluster?.id}
|
||||
>
|
||||
{runsQ.isFetching ? "Refreshing…" : "Refresh runs"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{actionsQ.isLoading ? (
|
||||
<p className="text-muted-foreground text-xs">Loading actions…</p>
|
||||
) : (actionsQ.data ?? []).length === 0 ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
No actions configured yet. Create actions in Admin → Actions.
|
||||
</p>
|
||||
) : (
|
||||
<div className="divide-border rounded-md border">
|
||||
{(actionsQ.data ?? []).map((a: DtoActionResponse) => (
|
||||
<div
|
||||
key={a.id}
|
||||
className="flex items-center justify-between gap-3 px-3 py-2"
|
||||
>
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{a.label}</span>
|
||||
{a.make_target && (
|
||||
<code className="text-muted-foreground text-xs">
|
||||
{a.make_target}
|
||||
</code>
|
||||
)}
|
||||
</div>
|
||||
{a.description && (
|
||||
<p className="text-muted-foreground line-clamp-2 text-xs">
|
||||
{a.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => a.id && handleRunAction(a.id)}
|
||||
disabled={!a.id || isBusy(`run:${a.id}`)}
|
||||
>
|
||||
{a.id && isBusy(`run:${a.id}`) ? "Enqueueing…" : "Run"}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-1">
|
||||
<Label className="text-xs">Recent Runs</Label>
|
||||
|
||||
{runsQ.isLoading ? (
|
||||
<p className="text-muted-foreground text-xs">Loading runs…</p>
|
||||
) : (runsQ.data ?? []).length === 0 ? (
|
||||
<p className="text-muted-foreground text-xs">No runs yet for this cluster.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Action</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Finished</TableHead>
|
||||
<TableHead>Error</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(runsQ.data ?? []).slice(0, 20).map((r) => (
|
||||
<TableRow key={r.id}>
|
||||
<TableCell className="min-w-[220px]">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">{runDisplayName(r)}</span>
|
||||
{r.id && (
|
||||
<code className="text-muted-foreground text-xs">
|
||||
{truncateMiddle(r.id, 8)}
|
||||
</code>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<RunStatusBadge status={r.status} />
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{fmtTime((r as any).created_at)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{fmtTime((r as any).finished_at)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{r.error ? truncateMiddle(r.error, 80) : "-"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Kubeconfig */}
|
||||
<section className="space-y-2 rounded-xl border p-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileCode2 className="h-4 w-4" />
|
||||
<h3 className="text-sm font-semibold">Kubeconfig</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Paste the kubeconfig for this cluster. It will be stored encrypted and never
|
||||
returned by the API.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Textarea
|
||||
value={kubeconfigText}
|
||||
onChange={(e) => setKubeconfigText(e.target.value)}
|
||||
rows={6}
|
||||
placeholder="apiVersion: v1 clusters: - cluster: ..."
|
||||
placeholder={"apiVersion: v1\nclusters:\n - cluster: ..."}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
|
||||
@@ -994,7 +1177,7 @@ export const ClustersPage = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Control Plane Record Set (shown once we have a captainDomainId) */}
|
||||
{/* Control Plane Record Set */}
|
||||
{captainDomainId && (
|
||||
<section className="space-y-2 rounded-xl border p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
@@ -1231,14 +1414,12 @@ export const ClustersPage = () => {
|
||||
|
||||
{/* Node Pools */}
|
||||
<section className="space-y-2 rounded-xl border p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">Node Pools</h3>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Attach node pools to this cluster. Each node pool may have its own labels,
|
||||
taints, and backing servers.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">Node Pools</h3>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Attach node pools to this cluster. Each node pool may have its own labels,
|
||||
taints, and backing servers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-end">
|
||||
@@ -1337,8 +1518,6 @@ export const ClustersPage = () => {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<pre>{JSON.stringify(clustersQ.data, null, 2)}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { orgStore } from "@/auth/org.ts"
|
||||
import { authStore } from "@/auth/store.ts"
|
||||
import {
|
||||
ActionsApi,
|
||||
AnnotationsApi,
|
||||
ArcherAdminApi,
|
||||
AuthApi,
|
||||
ClusterRunsApi,
|
||||
ClustersApi,
|
||||
Configuration,
|
||||
CredentialsApi,
|
||||
@@ -133,3 +135,11 @@ export function makeLoadBalancerApi() {
|
||||
export function makeClusterApi() {
|
||||
return makeApiClient(ClustersApi)
|
||||
}
|
||||
|
||||
export function makeActionsApi() {
|
||||
return makeApiClient(ActionsApi)
|
||||
}
|
||||
|
||||
export function makeClusterRunsApi() {
|
||||
return makeApiClient(ClusterRunsApi)
|
||||
}
|
||||
585
ui/yarn.lock
585
ui/yarn.lock
@@ -291,9 +291,9 @@
|
||||
integrity sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==
|
||||
|
||||
"@dotenvx/dotenvx@^1.48.4":
|
||||
version "1.51.1"
|
||||
resolved "https://registry.yarnpkg.com/@dotenvx/dotenvx/-/dotenvx-1.51.1.tgz#d43150952ba38c3eaaf7f8dbf3efa13f776a9e25"
|
||||
integrity sha512-fqcQxcxC4LOaUlW8IkyWw8x0yirlLUkbxohz9OnWvVWjf73J5yyw7jxWnkOJaUKXZotcGEScDox9MU6rSkcDgg==
|
||||
version "1.51.2"
|
||||
resolved "https://registry.yarnpkg.com/@dotenvx/dotenvx/-/dotenvx-1.51.2.tgz#6701c5a7a4105a14a47666b0ea2056b87fddb79e"
|
||||
integrity sha512-+693mNflujDZxudSEqSNGpn92QgFhJlBn9q2mDQ9yGWyHuz3hZ8B5g3EXCwdAz4DMJAI+OFCIbfEFZS+YRdrEA==
|
||||
dependencies:
|
||||
commander "^11.1.0"
|
||||
dotenv "^17.2.1"
|
||||
@@ -512,10 +512,10 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@9.39.1":
|
||||
version "9.39.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.1.tgz#0dd59c3a9f40e3f1882975c321470969243e0164"
|
||||
integrity sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==
|
||||
"@eslint/js@9.39.2":
|
||||
version "9.39.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599"
|
||||
integrity sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==
|
||||
|
||||
"@eslint/object-schema@^2.1.7":
|
||||
version "2.1.7"
|
||||
@@ -557,6 +557,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
|
||||
integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==
|
||||
|
||||
"@hono/node-server@^1.19.7":
|
||||
version "1.19.7"
|
||||
resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.7.tgz#ecb2d3a7af40d1d378e53ce1fc1219f199fbcd6f"
|
||||
integrity sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==
|
||||
|
||||
"@hookform/resolvers@^5.2.2":
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-5.2.2.tgz#5ac16cd89501ca31671e6e9f0f5c5d762a99aa12"
|
||||
@@ -694,10 +699,11 @@
|
||||
"@lit-labs/ssr-dom-shim" "^1.4.0"
|
||||
|
||||
"@modelcontextprotocol/sdk@^1.17.2":
|
||||
version "1.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.24.3.tgz#81a3fcc919cb4ce8630e2bcecf59759176eb331a"
|
||||
integrity sha512-YgSHW29fuzKKAHTGe9zjNoo+yF8KaQPzDC2W9Pv41E7/57IfY+AMGJ/aDFlgTLcVVELoggKE4syABCE75u3NCw==
|
||||
version "1.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.25.0.tgz#ce65b7ae4631f76c7f0b899c28cbbfa4d9337240"
|
||||
integrity sha512-z0Zhn/LmQ3yz91dEfd5QgS7DpSjA4pk+3z2++zKgn5L6iDFM9QapsVoAQSbKLvlrFsZk9+ru6yHHWNq2lCYJKQ==
|
||||
dependencies:
|
||||
"@hono/node-server" "^1.19.7"
|
||||
ajv "^8.17.1"
|
||||
ajv-formats "^3.0.1"
|
||||
content-type "^1.0.5"
|
||||
@@ -708,6 +714,7 @@
|
||||
express "^5.0.1"
|
||||
express-rate-limit "^7.5.0"
|
||||
jose "^6.1.1"
|
||||
json-schema-typed "^8.0.2"
|
||||
pkce-challenge "^5.0.0"
|
||||
raw-body "^3.0.0"
|
||||
zod "^3.25 || ^4.0"
|
||||
@@ -1397,115 +1404,115 @@
|
||||
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz#c57a5234ae122671aff6fe72e673a7ed90f03f87"
|
||||
integrity sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz#7e478b66180c5330429dd161bf84dad66b59c8eb"
|
||||
integrity sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==
|
||||
"@rollup/rollup-android-arm-eabi@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.4.tgz#c02c6fcd53ebae26feff7bfdcfb3b6b9015ff56f"
|
||||
integrity sha512-PWU3Y92H4DD0bOqorEPp1Y0tbzwAurFmIYpjcObv5axGVOtcTlB0b2UKMd2echo08MgN7jO8WQZSSysvfisFSQ==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz#2b025510c53a5e3962d3edade91fba9368c9d71c"
|
||||
integrity sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==
|
||||
"@rollup/rollup-android-arm64@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.4.tgz#c00bab78a44dbcd5d124c99b1f964dfdb19e3fa0"
|
||||
integrity sha512-Gw0/DuVm3rGsqhMGYkSOXXIx20cC3kTlivZeuaGt4gEgILivykNyBWxeUV5Cf2tDA2nPLah26vq3emlRrWVbng==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz#3577c38af68ccf34c03e84f476bfd526abca10a0"
|
||||
integrity sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==
|
||||
"@rollup/rollup-darwin-arm64@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.4.tgz#c0fcb8a3ec5ba5947ac6638013e667f0168e7d05"
|
||||
integrity sha512-+w06QvXsgzKwdVg5qRLZpTHh1bigHZIqoIUPtiqh05ZiJVUQ6ymOxaPkXTvRPRLH88575ZCRSRM3PwIoNma01Q==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz#2bf5f2520a1f3b551723d274b9669ba5b75ed69c"
|
||||
integrity sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==
|
||||
"@rollup/rollup-darwin-x64@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.4.tgz#d76162c6dab079695d73857824530a7b03bc90de"
|
||||
integrity sha512-EB4Na9G2GsrRNRNFPuxfwvDRDUwQEzJPpiK1vo2zMVhEeufZ1k7J1bKnT0JYDfnPC7RNZ2H5YNQhW6/p2QKATw==
|
||||
|
||||
"@rollup/rollup-freebsd-arm64@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz#4bb9cc80252564c158efc0710153c71633f1927c"
|
||||
integrity sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==
|
||||
"@rollup/rollup-freebsd-arm64@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.4.tgz#679732db3eb5ba50253c6f7cf1ed8742f3a12e49"
|
||||
integrity sha512-bldA8XEqPcs6OYdknoTMaGhjytnwQ0NClSPpWpmufOuGPN5dDmvIa32FygC2gneKK4A1oSx86V1l55hyUWUYFQ==
|
||||
|
||||
"@rollup/rollup-freebsd-x64@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz#2301289094d49415a380cf942219ae9d8b127440"
|
||||
integrity sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==
|
||||
"@rollup/rollup-freebsd-x64@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.4.tgz#f686a16e2d6ea871062ea332f02f9c3685511773"
|
||||
integrity sha512-3T8GPjH6mixCd0YPn0bXtcuSXi1Lj+15Ujw2CEb7dd24j9thcKscCf88IV7n76WaAdorOzAgSSbuVRg4C8V8Qw==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz#1d03d776f2065e09fc141df7d143476e94acca88"
|
||||
integrity sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.4.tgz#b0f0956625530b93bbe7e8e795c1684e09fd3292"
|
||||
integrity sha512-UPMMNeC4LXW7ZSHxeP3Edv09aLsFUMaD1TSVW6n1CWMECnUIJMFFB7+XC2lZTdPtvB36tYC0cJWc86mzSsaviw==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz#8623de0e040b2fd52a541c602688228f51f96701"
|
||||
integrity sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.4.tgz#cb68ce30bec02ec60bec236137a073e6b336c7fb"
|
||||
integrity sha512-H8uwlV0otHs5Q7WAMSoyvjV9DJPiy5nJ/xnHolY0QptLPjaSsuX7tw+SPIfiYH6cnVx3fe4EWFafo6gH6ekZKA==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz#ce2d1999bc166277935dde0301cde3dd0417fb6e"
|
||||
integrity sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==
|
||||
"@rollup/rollup-linux-arm64-gnu@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.4.tgz#fd0011bd694062f6e93bc56d26f81da2a2a28f46"
|
||||
integrity sha512-BLRwSRwICXz0TXkbIbqJ1ibK+/dSBpTJqDClF61GWIrxTXZWQE78ROeIhgl5MjVs4B4gSLPCFeD4xML9vbzvCQ==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz#88c2523778444da952651a2219026416564a4899"
|
||||
integrity sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==
|
||||
"@rollup/rollup-linux-arm64-musl@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.4.tgz#9fca5891d43758c2c90618096bf0204059f17900"
|
||||
integrity sha512-6bySEjOTbmVcPJAywjpGLckK793A0TJWSbIa0sVwtVGfe/Nz6gOWHOwkshUIAp9j7wg2WKcA4Snu7Y1nUZyQew==
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz#578ca2220a200ac4226c536c10c8cc6e4f276714"
|
||||
integrity sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==
|
||||
"@rollup/rollup-linux-loong64-gnu@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.4.tgz#84b99ebbeb1a77cf4bcb005c1337ef91f2a50f00"
|
||||
integrity sha512-U0ow3bXYJZ5MIbchVusxEycBw7bO6C2u5UvD31i5IMTrnt2p4Fh4ZbHSdc/31TScIJQYHwxbj05BpevB3201ug==
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz#aa338d3effd4168a20a5023834a74ba2c3081293"
|
||||
integrity sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==
|
||||
"@rollup/rollup-linux-ppc64-gnu@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.4.tgz#fed9b3637e1c6ae31a6731987a4875d1ca344b21"
|
||||
integrity sha512-iujDk07ZNwGLVn0YIWM80SFN039bHZHCdCCuX9nyx3Jsa2d9V/0Y32F+YadzwbvDxhSeVo9zefkoPnXEImnM5w==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz#16ba582f9f6cff58119aa242782209b1557a1508"
|
||||
integrity sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.4.tgz#0fc62a9dbdf0e169d2738486517badb8b012b692"
|
||||
integrity sha512-MUtAktiOUSu+AXBpx1fkuG/Bi5rhlorGs3lw5QeJ2X3ziEGAq7vFNdWVde6XGaVqi0LGSvugwjoxSNJfHFTC0g==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz#e404a77ebd6378483888b8064c703adb011340ab"
|
||||
integrity sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==
|
||||
"@rollup/rollup-linux-riscv64-musl@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.4.tgz#0ae1a8833ba4decd0c315eee33b475b657c91e82"
|
||||
integrity sha512-btm35eAbDfPtcFEgaXCI5l3c2WXyzwiE8pArhd66SDtoLWmgK5/M7CUxmUglkwtniPzwvWioBKKl6IXLbPf2sQ==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz#92ad52d306227c56bec43d96ad2164495437ffe6"
|
||||
integrity sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==
|
||||
"@rollup/rollup-linux-s390x-gnu@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.4.tgz#16f13008a4bec6ac3c9b6334761f989c78ff740f"
|
||||
integrity sha512-uJlhKE9ccUTCUlK+HUz/80cVtx2RayadC5ldDrrDUFaJK0SNb8/cCmC9RhBhIWuZ71Nqj4Uoa9+xljKWRogdhA==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz#fd0dea3bb9aa07e7083579f25e1c2285a46cb9fa"
|
||||
integrity sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==
|
||||
"@rollup/rollup-linux-x64-gnu@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.4.tgz#ffeed1084d83da0c38143311fa2a63f56a0054bb"
|
||||
integrity sha512-jjEMkzvASQBbzzlzf4os7nzSBd/cvPrpqXCUOqoeCh1dQ4BP3RZCJk8XBeik4MUln3m+8LeTJcY54C/u8wb3DQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz#37a3efb09f18d555f8afc490e1f0444885de8951"
|
||||
integrity sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==
|
||||
"@rollup/rollup-linux-x64-musl@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.4.tgz#4797fbcf5c231be2ace9b76de75c4f683ac8a5f2"
|
||||
integrity sha512-lu90KG06NNH19shC5rBPkrh6mrTpq5kviFylPBXQVpdEu0yzb0mDgyxLr6XdcGdBIQTH/UAhDJnL+APZTBu1aQ==
|
||||
|
||||
"@rollup/rollup-openharmony-arm64@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz#c489bec9f4f8320d42c9b324cca220c90091c1f7"
|
||||
integrity sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==
|
||||
"@rollup/rollup-openharmony-arm64@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.4.tgz#ee4ede9da9a31b002627cb449000962217bcffbe"
|
||||
integrity sha512-dFDcmLwsUzhAm/dn0+dMOQZoONVYBtgik0VuY/d5IJUUb787L3Ko/ibvTvddqhb3RaB7vFEozYevHN4ox22R/w==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz#152832b5f79dc22d1606fac3db946283601b7080"
|
||||
integrity sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==
|
||||
"@rollup/rollup-win32-arm64-msvc@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.4.tgz#ce034c5f3ab670fa1af02f90fa83f2a7779d80fb"
|
||||
integrity sha512-WvUpUAWmUxZKtRnQWpRKnLW2DEO8HB/l8z6oFFMNuHndMzFTJEXzaYJ5ZAmzNw0L21QQJZsUQFt2oPf3ykAD/w==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz#54d91b2bb3bf3e9f30d32b72065a4e52b3a172a5"
|
||||
integrity sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==
|
||||
"@rollup/rollup-win32-ia32-msvc@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.4.tgz#e1f431955c502fce472ecd398960bde03fa7ac94"
|
||||
integrity sha512-JGbeF2/FDU0x2OLySw/jgvkwWUo05BSiJK0dtuI4LyuXbz3wKiC1xHhLB1Tqm5VU6ZZDmAorj45r/IgWNWku5g==
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz#df9df03e61a003873efec8decd2034e7f135c71e"
|
||||
integrity sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==
|
||||
"@rollup/rollup-win32-x64-gnu@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.4.tgz#7151c8af5ec4b78c401ae2a1c1aa5fa573fce692"
|
||||
integrity sha512-zuuC7AyxLWLubP+mlUwEyR8M1ixW1ERNPHJfXm8x7eQNP4Pzkd7hS3qBuKBR70VRiQ04Kw8FNfRMF5TNxuZq2g==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.53.3":
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz#38ae84f4c04226c1d56a3b17296ef1e0460ecdfe"
|
||||
integrity sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==
|
||||
"@rollup/rollup-win32-x64-msvc@4.53.4":
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.4.tgz#f5f616f008888c205e08d739873cfa8ac548142e"
|
||||
integrity sha512-Sbx45u/Lbb5RyptSbX7/3deP+/lzEmZ0BTSHxwxN/IMOZDZf8S0AGo0hJD5n/LQssxb5Z3B4og4P2X6Dd8acCA==
|
||||
|
||||
"@scarf/scarf@=1.4.0":
|
||||
version "1.4.0"
|
||||
@@ -2101,7 +2108,7 @@
|
||||
"@tailwindcss/oxide-win32-arm64-msvc" "4.1.18"
|
||||
"@tailwindcss/oxide-win32-x64-msvc" "4.1.18"
|
||||
|
||||
"@tailwindcss/vite@^4.1.17":
|
||||
"@tailwindcss/vite@^4.1.18":
|
||||
version "4.1.18"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/vite/-/vite-4.1.18.tgz#614b9d5483559518c72d31bca05d686f8df28e9a"
|
||||
integrity sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==
|
||||
@@ -2240,10 +2247,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
|
||||
"@types/node@24.10.2":
|
||||
version "24.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.2.tgz#82a57476a19647d8f2c7750d0924788245e39b26"
|
||||
integrity sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==
|
||||
"@types/node@25.0.1":
|
||||
version "25.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.1.tgz#9c41c277a1b16491174497cd075f8de7c27a1ac4"
|
||||
integrity sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==
|
||||
dependencies:
|
||||
undici-types "~7.16.0"
|
||||
|
||||
@@ -2276,100 +2283,100 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.49.0":
|
||||
version "8.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz#8ed8736b8415a9193989220eadb6031dbcd2260a"
|
||||
integrity sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==
|
||||
"@typescript-eslint/eslint-plugin@8.50.0":
|
||||
version "8.50.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz#a6ce899690542e2affa9543306d2d3935740abb7"
|
||||
integrity sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.49.0"
|
||||
"@typescript-eslint/type-utils" "8.49.0"
|
||||
"@typescript-eslint/utils" "8.49.0"
|
||||
"@typescript-eslint/visitor-keys" "8.49.0"
|
||||
"@typescript-eslint/scope-manager" "8.50.0"
|
||||
"@typescript-eslint/type-utils" "8.50.0"
|
||||
"@typescript-eslint/utils" "8.50.0"
|
||||
"@typescript-eslint/visitor-keys" "8.50.0"
|
||||
ignore "^7.0.0"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@8.49.0":
|
||||
version "8.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.49.0.tgz#0ede412d59e99239b770f0f08c76c42fba717fa2"
|
||||
integrity sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==
|
||||
"@typescript-eslint/parser@8.50.0":
|
||||
version "8.50.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.50.0.tgz#c35b28f686dbe08e81b9d6208ebc08912549f4ba"
|
||||
integrity sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.49.0"
|
||||
"@typescript-eslint/types" "8.49.0"
|
||||
"@typescript-eslint/typescript-estree" "8.49.0"
|
||||
"@typescript-eslint/visitor-keys" "8.49.0"
|
||||
"@typescript-eslint/scope-manager" "8.50.0"
|
||||
"@typescript-eslint/types" "8.50.0"
|
||||
"@typescript-eslint/typescript-estree" "8.50.0"
|
||||
"@typescript-eslint/visitor-keys" "8.50.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/project-service@8.49.0":
|
||||
version "8.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.49.0.tgz#ce220525c88cb2d23792b391c07e14cb9697651a"
|
||||
integrity sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==
|
||||
"@typescript-eslint/project-service@8.50.0":
|
||||
version "8.50.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.50.0.tgz#1422366b7cc11fef8c6d87770884e608093423a4"
|
||||
integrity sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.49.0"
|
||||
"@typescript-eslint/types" "^8.49.0"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.50.0"
|
||||
"@typescript-eslint/types" "^8.50.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.49.0":
|
||||
version "8.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz#a3496765b57fb48035d671174552e462e5bffa63"
|
||||
integrity sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==
|
||||
"@typescript-eslint/scope-manager@8.50.0":
|
||||
version "8.50.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz#e0d6c838dc9044bc679724611b138cb34c81bddf"
|
||||
integrity sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.49.0"
|
||||
"@typescript-eslint/visitor-keys" "8.49.0"
|
||||
"@typescript-eslint/types" "8.50.0"
|
||||
"@typescript-eslint/visitor-keys" "8.50.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.49.0", "@typescript-eslint/tsconfig-utils@^8.49.0":
|
||||
version "8.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz#857777c8e35dd1e564505833d8043f544442fbf4"
|
||||
integrity sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==
|
||||
"@typescript-eslint/tsconfig-utils@8.50.0", "@typescript-eslint/tsconfig-utils@^8.50.0":
|
||||
version "8.50.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz#5c17537ad4c8a13bf6d7393035edaf91a1e13191"
|
||||
integrity sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==
|
||||
|
||||
"@typescript-eslint/type-utils@8.49.0":
|
||||
version "8.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz#d8118a0c1896a78a22f01d3c176e9945409b085b"
|
||||
integrity sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==
|
||||
"@typescript-eslint/type-utils@8.50.0":
|
||||
version "8.50.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz#feb6f54f876980a258b14f1cb033f54fc545d37b"
|
||||
integrity sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.49.0"
|
||||
"@typescript-eslint/typescript-estree" "8.49.0"
|
||||
"@typescript-eslint/utils" "8.49.0"
|
||||
"@typescript-eslint/types" "8.50.0"
|
||||
"@typescript-eslint/typescript-estree" "8.50.0"
|
||||
"@typescript-eslint/utils" "8.50.0"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@8.49.0", "@typescript-eslint/types@^8.49.0":
|
||||
version "8.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.49.0.tgz#c1bd3ebf956d9e5216396349ca23c58d74f06aee"
|
||||
integrity sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==
|
||||
"@typescript-eslint/types@8.50.0", "@typescript-eslint/types@^8.50.0":
|
||||
version "8.50.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.50.0.tgz#ad8f1ad88ae0096f548c9cdf60da9b92832db96e"
|
||||
integrity sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.49.0":
|
||||
version "8.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz#99c5a53275197ccb4e849786dad68344e9924135"
|
||||
integrity sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==
|
||||
"@typescript-eslint/typescript-estree@8.50.0":
|
||||
version "8.50.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz#2871d36617f81a127db905fa91b16d1a0251411b"
|
||||
integrity sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.49.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.49.0"
|
||||
"@typescript-eslint/types" "8.49.0"
|
||||
"@typescript-eslint/visitor-keys" "8.49.0"
|
||||
"@typescript-eslint/project-service" "8.50.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.50.0"
|
||||
"@typescript-eslint/types" "8.50.0"
|
||||
"@typescript-eslint/visitor-keys" "8.50.0"
|
||||
debug "^4.3.4"
|
||||
minimatch "^9.0.4"
|
||||
semver "^7.6.0"
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/utils@8.49.0":
|
||||
version "8.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.49.0.tgz#43b3b91d30afd6f6114532cf0b228f1790f43aff"
|
||||
integrity sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==
|
||||
"@typescript-eslint/utils@8.50.0":
|
||||
version "8.50.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.50.0.tgz#107f20a5747eab5db988c5f6ad462b59851cdd1f"
|
||||
integrity sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.7.0"
|
||||
"@typescript-eslint/scope-manager" "8.49.0"
|
||||
"@typescript-eslint/types" "8.49.0"
|
||||
"@typescript-eslint/typescript-estree" "8.49.0"
|
||||
"@typescript-eslint/scope-manager" "8.50.0"
|
||||
"@typescript-eslint/types" "8.50.0"
|
||||
"@typescript-eslint/typescript-estree" "8.50.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.49.0":
|
||||
version "8.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz#8e450cc502c0d285cad9e84d400cf349a85ced6c"
|
||||
integrity sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==
|
||||
"@typescript-eslint/visitor-keys@8.50.0":
|
||||
version "8.50.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz#79d1c95474e08f844dbe13370715cfb9b7e21363"
|
||||
integrity sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.49.0"
|
||||
"@typescript-eslint/types" "8.50.0"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
"@vitejs/plugin-react@5.1.2":
|
||||
@@ -2510,9 +2517,9 @@ base64-js@^1.3.1:
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
baseline-browser-mapping@^2.9.0:
|
||||
version "2.9.6"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz#82de0f7ee5860df86d60daf0d9524ae7227eeee7"
|
||||
integrity sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==
|
||||
version "2.9.7"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz#d36ce64f2a2c468f6f743c8db495d319120007db"
|
||||
integrity sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==
|
||||
|
||||
body-parser@^2.2.1:
|
||||
version "2.2.1"
|
||||
@@ -2570,6 +2577,13 @@ buffer@^6.0.3:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.2.1"
|
||||
|
||||
bundle-name@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889"
|
||||
integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==
|
||||
dependencies:
|
||||
run-applescript "^7.0.0"
|
||||
|
||||
bytes@^3.1.2, bytes@~3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||
@@ -2763,6 +2777,11 @@ cross-spawn@^7.0.3, cross-spawn@^7.0.5, cross-spawn@^7.0.6:
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
cssesc@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
|
||||
|
||||
csstype@^3.0.2, csstype@^3.2.2:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
|
||||
@@ -2854,7 +2873,7 @@ date-fns@^4.1.0:
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14"
|
||||
integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==
|
||||
|
||||
debug@4, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@^4.4.3:
|
||||
debug@4, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0, debug@^4.4.3:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
|
||||
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
|
||||
@@ -2881,6 +2900,24 @@ deepmerge@^4.3.1, deepmerge@~4.3.0:
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
||||
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
||||
|
||||
default-browser-id@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.1.tgz#f7a7ccb8f5104bf8e0f71ba3b1ccfa5eafdb21e8"
|
||||
integrity sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==
|
||||
|
||||
default-browser@^5.4.0:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.4.0.tgz#b55cf335bb0b465dd7c961a02cd24246aa434287"
|
||||
integrity sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==
|
||||
dependencies:
|
||||
bundle-name "^4.1.0"
|
||||
default-browser-id "^5.0.0"
|
||||
|
||||
define-lazy-prop@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f"
|
||||
integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
@@ -3114,10 +3151,10 @@ eslint-visitor-keys@^4.2.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
|
||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||
|
||||
eslint@9.39.1:
|
||||
version "9.39.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.1.tgz#be8bf7c6de77dcc4252b5a8dcb31c2efff74a6e5"
|
||||
integrity sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==
|
||||
eslint@9.39.2:
|
||||
version "9.39.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.2.tgz#cb60e6d16ab234c0f8369a3fe7cc87967faf4b6c"
|
||||
integrity sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.8.0"
|
||||
"@eslint-community/regexpp" "^4.12.1"
|
||||
@@ -3125,7 +3162,7 @@ eslint@9.39.1:
|
||||
"@eslint/config-helpers" "^0.4.2"
|
||||
"@eslint/core" "^0.17.0"
|
||||
"@eslint/eslintrc" "^3.3.1"
|
||||
"@eslint/js" "9.39.1"
|
||||
"@eslint/js" "9.39.2"
|
||||
"@eslint/plugin-kit" "^0.4.1"
|
||||
"@humanfs/node" "^0.16.6"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
@@ -3292,9 +3329,9 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-equals@^5.0.1:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.3.3.tgz#e55f96198269278533348c22f1ab1a0fb957e22a"
|
||||
integrity sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.4.0.tgz#b60073b8764f27029598447f05773c7534ba7f1e"
|
||||
integrity sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==
|
||||
|
||||
fast-glob@^3.3.3:
|
||||
version "3.3.3"
|
||||
@@ -3614,7 +3651,7 @@ hermes-parser@^0.25.1:
|
||||
dependencies:
|
||||
hermes-estree "0.25.1"
|
||||
|
||||
http-errors@^2.0.0, http-errors@~2.0.1:
|
||||
http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b"
|
||||
integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==
|
||||
@@ -3703,6 +3740,11 @@ is-arrayish@^0.2.1:
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||
integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
|
||||
|
||||
is-docker@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200"
|
||||
integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==
|
||||
|
||||
is-extglob@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||
@@ -3720,6 +3762,18 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
|
||||
dependencies:
|
||||
is-extglob "^2.1.1"
|
||||
|
||||
is-in-ssh@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-in-ssh/-/is-in-ssh-1.0.0.tgz#8eb73c1cabba77748d389588eeea132a63057622"
|
||||
integrity sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==
|
||||
|
||||
is-inside-container@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4"
|
||||
integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==
|
||||
dependencies:
|
||||
is-docker "^3.0.0"
|
||||
|
||||
is-interactive@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-2.0.0.tgz#40c57614593826da1100ade6059778d597f16e90"
|
||||
@@ -3775,6 +3829,13 @@ is-unicode-supported@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz#09f0ab0de6d3744d48d265ebb98f65d11f2a9b3a"
|
||||
integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==
|
||||
|
||||
is-wsl@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2"
|
||||
integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==
|
||||
dependencies:
|
||||
is-inside-container "^1.0.0"
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
@@ -3832,6 +3893,11 @@ json-schema-traverse@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
|
||||
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||
|
||||
json-schema-typed@^8.0.2:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz#e98ee7b1899ff4a184534d1f167c288c66bbeff4"
|
||||
integrity sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==
|
||||
|
||||
json-stable-stringify-without-jsonify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
@@ -4019,10 +4085,10 @@ lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
lucide-react@^0.557.0:
|
||||
version "0.557.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.557.0.tgz#372f6069b227fcca816962d5bbbd9c8dee5e97bd"
|
||||
integrity sha512-x6LrJAuYGywdjH4rBgI8ygWnCsn8GTvS9/BhSORNWsuv3LNLV39ZOUg6UTJa9nFUl0fHY8bytDSThH12pNHyLQ==
|
||||
lucide-react@^0.561.0:
|
||||
version "0.561.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.561.0.tgz#8eb440395cf01b27da9c65cb014eb2c71f77e656"
|
||||
integrity sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==
|
||||
|
||||
magic-string@^0.30.21:
|
||||
version "0.30.21"
|
||||
@@ -4086,7 +4152,7 @@ mime-types@^2.1.12:
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
mime-types@^3.0.0, mime-types@^3.0.1:
|
||||
mime-types@^3.0.0, mime-types@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab"
|
||||
integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==
|
||||
@@ -4322,6 +4388,18 @@ onetime@^7.0.0:
|
||||
dependencies:
|
||||
mimic-function "^5.0.0"
|
||||
|
||||
open@^11.0.0:
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/open/-/open-11.0.0.tgz#897e6132f994d3554cbcf72e0df98f176a7e5f62"
|
||||
integrity sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==
|
||||
dependencies:
|
||||
default-browser "^5.4.0"
|
||||
define-lazy-prop "^3.0.0"
|
||||
is-in-ssh "^1.0.0"
|
||||
is-inside-container "^1.0.0"
|
||||
powershell-utils "^0.1.0"
|
||||
wsl-utils "^0.3.0"
|
||||
|
||||
openapi-path-templating@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/openapi-path-templating/-/openapi-path-templating-2.2.1.tgz#57026767530667096d33d7362382a93d75d497f6"
|
||||
@@ -4464,6 +4542,14 @@ pkce-challenge@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz#3b4446865b17b1745e9ace2016a31f48ddf6230d"
|
||||
integrity sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==
|
||||
|
||||
postcss-selector-parser@^7.1.0:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz#e75d2e0d843f620e5df69076166f4e16f891cb9f"
|
||||
integrity sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
util-deprecate "^1.0.2"
|
||||
|
||||
postcss@^8.5.6:
|
||||
version "8.5.6"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
|
||||
@@ -4473,6 +4559,11 @@ postcss@^8.5.6:
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
powershell-utils@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/powershell-utils/-/powershell-utils-0.1.0.tgz#5a42c9a824fb4f2f251ccb41aaae73314f5d6ac2"
|
||||
integrity sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
@@ -4608,7 +4699,7 @@ react-day-picker@^9.12.0:
|
||||
date-fns "^4.1.0"
|
||||
date-fns-jalali "^4.1.0-0"
|
||||
|
||||
react-dom@^19.2.1:
|
||||
react-dom@^19.2.3:
|
||||
version "19.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17"
|
||||
integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==
|
||||
@@ -4706,7 +4797,7 @@ react-transition-group@^4.4.5:
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react@^19.2.1:
|
||||
react@^19.2.3:
|
||||
version "19.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8"
|
||||
integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==
|
||||
@@ -4787,34 +4878,34 @@ reusify@^1.0.4:
|
||||
integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
|
||||
|
||||
rollup@^4.43.0:
|
||||
version "4.53.3"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.53.3.tgz#dbc8cd8743b38710019fb8297e8d7a76e3faa406"
|
||||
integrity sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==
|
||||
version "4.53.4"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.53.4.tgz#5517de2593624928ac18f041b269f3b79cb64e09"
|
||||
integrity sha512-YpXaaArg0MvrnJpvduEDYIp7uGOqKXbH9NsHGQ6SxKCOsNAjZF018MmxefFUulVP2KLtiGw1UvZbr+/ekjvlDg==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.8"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.53.3"
|
||||
"@rollup/rollup-android-arm64" "4.53.3"
|
||||
"@rollup/rollup-darwin-arm64" "4.53.3"
|
||||
"@rollup/rollup-darwin-x64" "4.53.3"
|
||||
"@rollup/rollup-freebsd-arm64" "4.53.3"
|
||||
"@rollup/rollup-freebsd-x64" "4.53.3"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.53.3"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.53.3"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.53.3"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.53.3"
|
||||
"@rollup/rollup-linux-loong64-gnu" "4.53.3"
|
||||
"@rollup/rollup-linux-ppc64-gnu" "4.53.3"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.53.3"
|
||||
"@rollup/rollup-linux-riscv64-musl" "4.53.3"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.53.3"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.53.3"
|
||||
"@rollup/rollup-linux-x64-musl" "4.53.3"
|
||||
"@rollup/rollup-openharmony-arm64" "4.53.3"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.53.3"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.53.3"
|
||||
"@rollup/rollup-win32-x64-gnu" "4.53.3"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.53.3"
|
||||
"@rollup/rollup-android-arm-eabi" "4.53.4"
|
||||
"@rollup/rollup-android-arm64" "4.53.4"
|
||||
"@rollup/rollup-darwin-arm64" "4.53.4"
|
||||
"@rollup/rollup-darwin-x64" "4.53.4"
|
||||
"@rollup/rollup-freebsd-arm64" "4.53.4"
|
||||
"@rollup/rollup-freebsd-x64" "4.53.4"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.53.4"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.53.4"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.53.4"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.53.4"
|
||||
"@rollup/rollup-linux-loong64-gnu" "4.53.4"
|
||||
"@rollup/rollup-linux-ppc64-gnu" "4.53.4"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.53.4"
|
||||
"@rollup/rollup-linux-riscv64-musl" "4.53.4"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.53.4"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.53.4"
|
||||
"@rollup/rollup-linux-x64-musl" "4.53.4"
|
||||
"@rollup/rollup-openharmony-arm64" "4.53.4"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.53.4"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.53.4"
|
||||
"@rollup/rollup-win32-x64-gnu" "4.53.4"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.53.4"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
router@^2.2.0:
|
||||
@@ -4828,6 +4919,11 @@ router@^2.2.0:
|
||||
parseurl "^1.3.3"
|
||||
path-to-regexp "^8.0.0"
|
||||
|
||||
run-applescript@^7.0.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911"
|
||||
integrity sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||
@@ -4856,26 +4952,26 @@ semver@^7.5.2, semver@^7.6.0:
|
||||
integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
|
||||
|
||||
send@^1.1.0, send@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-1.2.0.tgz#32a7554fb777b831dfa828370f773a3808d37212"
|
||||
integrity sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed"
|
||||
integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==
|
||||
dependencies:
|
||||
debug "^4.3.5"
|
||||
debug "^4.4.3"
|
||||
encodeurl "^2.0.0"
|
||||
escape-html "^1.0.3"
|
||||
etag "^1.8.1"
|
||||
fresh "^2.0.0"
|
||||
http-errors "^2.0.0"
|
||||
mime-types "^3.0.1"
|
||||
http-errors "^2.0.1"
|
||||
mime-types "^3.0.2"
|
||||
ms "^2.1.3"
|
||||
on-finished "^2.4.1"
|
||||
range-parser "^1.2.1"
|
||||
statuses "^2.0.1"
|
||||
statuses "^2.0.2"
|
||||
|
||||
serve-static@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9"
|
||||
integrity sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.1.tgz#7f186a4a4e5f5b663ad7a4294ff1bf37cf0e98a9"
|
||||
integrity sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==
|
||||
dependencies:
|
||||
encodeurl "^2.0.0"
|
||||
escape-html "^1.0.3"
|
||||
@@ -4892,10 +4988,10 @@ setprototypeof@~1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||
|
||||
shadcn@3.5.2:
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/shadcn/-/shadcn-3.5.2.tgz#6664116548439f2ec598b6bc8ccb5480f823b9cd"
|
||||
integrity sha512-pVHVaNPeRXOH5FzIOc+qNaQ5RbuRNrXkL9bOuAtTagKIuwHLqzcgFb/krxaz1uzA2DHY9xJotAOd6UwMG1jc6w==
|
||||
shadcn@3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/shadcn/-/shadcn-3.6.0.tgz#bc5216bcc79c1bc5d8893838c7034c68be157733"
|
||||
integrity sha512-6uXnm38JWRtVHnULsTxV3J1IUsI1efZM5qqKhab1j32IFxAhKWIOoIFwtjrMYRgtcBrKwPXKaVfyhs1JPHRXpA==
|
||||
dependencies:
|
||||
"@antfu/ni" "^25.0.0"
|
||||
"@babel/core" "^7.28.0"
|
||||
@@ -4918,8 +5014,10 @@ shadcn@3.5.2:
|
||||
kleur "^4.1.5"
|
||||
msw "^2.10.4"
|
||||
node-fetch "^3.3.2"
|
||||
open "^11.0.0"
|
||||
ora "^8.2.0"
|
||||
postcss "^8.5.6"
|
||||
postcss-selector-parser "^7.1.0"
|
||||
prompts "^2.4.2"
|
||||
recast "^0.23.11"
|
||||
stringify-object "^5.0.0"
|
||||
@@ -5132,7 +5230,7 @@ tailwind-merge@^3.4.0:
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.4.0.tgz#5a264e131a096879965f1175d11f8c36e6b64eca"
|
||||
integrity sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==
|
||||
|
||||
tailwindcss@4.1.18, tailwindcss@^4.1.17:
|
||||
tailwindcss@4.1.18, tailwindcss@^4.1.18:
|
||||
version "4.1.18"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.1.18.tgz#f488ba47853abdb5354daf9679d3e7791fc4f4e3"
|
||||
integrity sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==
|
||||
@@ -5287,15 +5385,15 @@ types-ramda@^0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@8.49.0:
|
||||
version "8.49.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.49.0.tgz#4a8b608ae48c0db876c8fb2a2724839fc5a7147c"
|
||||
integrity sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==
|
||||
typescript-eslint@8.50.0:
|
||||
version "8.50.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.50.0.tgz#b91e73eea65edf46e10425dbeb0dc1ddb0d7fea5"
|
||||
integrity sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.49.0"
|
||||
"@typescript-eslint/parser" "8.49.0"
|
||||
"@typescript-eslint/typescript-estree" "8.49.0"
|
||||
"@typescript-eslint/utils" "8.49.0"
|
||||
"@typescript-eslint/eslint-plugin" "8.50.0"
|
||||
"@typescript-eslint/parser" "8.50.0"
|
||||
"@typescript-eslint/typescript-estree" "8.50.0"
|
||||
"@typescript-eslint/utils" "8.50.0"
|
||||
|
||||
typescript@5.9.3:
|
||||
version "5.9.3"
|
||||
@@ -5367,6 +5465,11 @@ use-sync-external-store@^1.5.0:
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d"
|
||||
integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==
|
||||
|
||||
util-deprecate@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||
|
||||
vary@^1, vary@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
@@ -5465,6 +5568,14 @@ wrappy@1:
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
wsl-utils@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/wsl-utils/-/wsl-utils-0.3.0.tgz#197049b93b34b822703bf4ccde8256660651205f"
|
||||
integrity sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ==
|
||||
dependencies:
|
||||
is-wsl "^3.1.0"
|
||||
powershell-utils "^0.1.0"
|
||||
|
||||
xml-but-prettier@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xml-but-prettier/-/xml-but-prettier-1.0.1.tgz#f5a33267ed42ccd4e355c62557a5e39b01fb40f3"
|
||||
@@ -5531,6 +5642,6 @@ zod@^3.24.1:
|
||||
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
|
||||
|
||||
"zod@^3.25 || ^4.0", "zod@^3.25.0 || ^4.0.0", zod@^4.1.13:
|
||||
version "4.1.13"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.13.tgz#93699a8afe937ba96badbb0ce8be6033c0a4b6b1"
|
||||
integrity sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.2.0.tgz#01e86f2c2b6d525a1b9fa6dbe78beccad082118f"
|
||||
integrity sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw==
|
||||
|
||||
Reference in New Issue
Block a user