mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 04:40:05 +01:00
annotations api and page
This commit is contained in:
691
docs/docs.go
691
docs/docs.go
@@ -282,6 +282,627 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/annotations": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns annotations for the organization in X-Org-ID. Filters: ` + "`" + `name` + "`" + `, ` + "`" + `value` + "`" + `, and ` + "`" + `q` + "`" + ` (name contains). Add ` + "`" + `include=node_pools` + "`" + ` to include linked node pools.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "List annotations (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Exact name",
|
||||
"name": "name",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Exact value",
|
||||
"name": "value",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name contains (case-insensitive)",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Optional: node_pools",
|
||||
"name": "include",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/annotations.annotationResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "failed to list annotations",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Creates an annotation. Optionally link to node pools.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "Create annotation (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Annotation payload",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.createAnnotationRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.annotationResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid json / missing fields / invalid node_pool_ids",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "create failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/annotations/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns one annotation. Add ` + "`" + `include=node_pools` + "`" + ` to include node pools.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "Get annotation by ID (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Annotation ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Optional: node_pools",
|
||||
"name": "include",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.annotationResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "fetch failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Permanently deletes the annotation.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "Delete annotation (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Annotation ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "delete failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Partially update annotation fields.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "Update annotation (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Annotation ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Fields to update",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.updateAnnotationRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.annotationResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid id / invalid json",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "update failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/annotations/{id}/node_pools": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns node pools attached to the annotation. Supports ` + "`" + `q` + "`" + ` (name contains, case-insensitive).",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "List node pools linked to an annotation (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Annotation ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name contains (case-insensitive)",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/annotations.nodePoolBrief"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "fetch failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Links the annotation to one or more node pools in the same organization.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "Attach annotation to node pools (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Annotation ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "IDs to attach",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.addAnnotationToNodePool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Optional: node_pools",
|
||||
"name": "include",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.annotationResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid id / invalid json / invalid node_pool_ids",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "attach failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/annotations/{id}/node_pools/{poolId}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Unlinks the annotation from the specified node pool.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "Detach annotation from a node pool (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Annotation ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "detach failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/introspect": {
|
||||
"post": {
|
||||
"description": "Returns whether the token is active and basic metadata",
|
||||
@@ -4172,6 +4793,76 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"annotations.addAnnotationToNodePool": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"node_pool_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"annotations.annotationResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"node_pools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/annotations.nodePoolBrief"
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"annotations.createAnnotationRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"node_pool_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"annotations.nodePoolBrief": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"annotations.updateAnnotationRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"authn.AdminCreateUserRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -278,6 +278,627 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/annotations": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns annotations for the organization in X-Org-ID. Filters: `name`, `value`, and `q` (name contains). Add `include=node_pools` to include linked node pools.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "List annotations (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Exact name",
|
||||
"name": "name",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Exact value",
|
||||
"name": "value",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name contains (case-insensitive)",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Optional: node_pools",
|
||||
"name": "include",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/annotations.annotationResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "failed to list annotations",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Creates an annotation. Optionally link to node pools.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "Create annotation (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Annotation payload",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.createAnnotationRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.annotationResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid json / missing fields / invalid node_pool_ids",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "create failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/annotations/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns one annotation. Add `include=node_pools` to include node pools.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "Get annotation by ID (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Annotation ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Optional: node_pools",
|
||||
"name": "include",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.annotationResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "fetch failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Permanently deletes the annotation.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "Delete annotation (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Annotation ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "delete failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Partially update annotation fields.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "Update annotation (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Annotation ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Fields to update",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.updateAnnotationRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.annotationResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid id / invalid json",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "update failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/annotations/{id}/node_pools": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns node pools attached to the annotation. Supports `q` (name contains, case-insensitive).",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "List node pools linked to an annotation (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Annotation ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Name contains (case-insensitive)",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/annotations.nodePoolBrief"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "fetch failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Links the annotation to one or more node pools in the same organization.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "Attach annotation to node pools (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Annotation ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "IDs to attach",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.addAnnotationToNodePool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Optional: node_pools",
|
||||
"name": "include",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/annotations.annotationResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid id / invalid json / invalid node_pool_ids",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "attach failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/annotations/{id}/node_pools/{poolId}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Unlinks the annotation from the specified node pool.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"annotations"
|
||||
],
|
||||
"summary": "Detach annotation from a node pool (org scoped)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Organization UUID",
|
||||
"name": "X-Org-ID",
|
||||
"in": "header",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Annotation ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "invalid id",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "organization required",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "not found",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "detach failed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/introspect": {
|
||||
"post": {
|
||||
"description": "Returns whether the token is active and basic metadata",
|
||||
@@ -4168,6 +4789,76 @@
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"annotations.addAnnotationToNodePool": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"node_pool_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"annotations.annotationResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"node_pools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/annotations.nodePoolBrief"
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"annotations.createAnnotationRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"node_pool_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"annotations.nodePoolBrief": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"annotations.updateAnnotationRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"authn.AdminCreateUserRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
basePath: /
|
||||
definitions:
|
||||
annotations.addAnnotationToNodePool:
|
||||
properties:
|
||||
node_pool_ids:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
annotations.annotationResponse:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
node_pools:
|
||||
items:
|
||||
$ref: '#/definitions/annotations.nodePoolBrief'
|
||||
type: array
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
annotations.createAnnotationRequest:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
node_pool_ids:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
annotations.nodePoolBrief:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
annotations.updateAnnotationRequest:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
authn.AdminCreateUserRequest:
|
||||
properties:
|
||||
email:
|
||||
@@ -726,6 +771,414 @@ paths:
|
||||
summary: 'Admin: update user'
|
||||
tags:
|
||||
- admin
|
||||
/api/v1/annotations:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 'Returns annotations for the organization in X-Org-ID. Filters:
|
||||
`name`, `value`, and `q` (name contains). Add `include=node_pools` to include
|
||||
linked node pools.'
|
||||
parameters:
|
||||
- description: Organization UUID
|
||||
in: header
|
||||
name: X-Org-ID
|
||||
required: true
|
||||
type: string
|
||||
- description: Exact name
|
||||
in: query
|
||||
name: name
|
||||
type: string
|
||||
- description: Exact value
|
||||
in: query
|
||||
name: value
|
||||
type: string
|
||||
- description: name contains (case-insensitive)
|
||||
in: query
|
||||
name: q
|
||||
type: string
|
||||
- description: 'Optional: node_pools'
|
||||
in: query
|
||||
name: include
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/annotations.annotationResponse'
|
||||
type: array
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: organization required
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: failed to list annotations
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: List annotations (org scoped)
|
||||
tags:
|
||||
- annotations
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Creates an annotation. Optionally link to node pools.
|
||||
parameters:
|
||||
- description: Organization UUID
|
||||
in: header
|
||||
name: X-Org-ID
|
||||
required: true
|
||||
type: string
|
||||
- description: Annotation payload
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/annotations.createAnnotationRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
schema:
|
||||
$ref: '#/definitions/annotations.annotationResponse'
|
||||
"400":
|
||||
description: invalid json / missing fields / invalid node_pool_ids
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: organization required
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: create failed
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Create annotation (org scoped)
|
||||
tags:
|
||||
- annotations
|
||||
/api/v1/annotations/{id}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Permanently deletes the annotation.
|
||||
parameters:
|
||||
- description: Organization UUID
|
||||
in: header
|
||||
name: X-Org-ID
|
||||
required: true
|
||||
type: string
|
||||
- description: Annotation ID (UUID)
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: invalid id
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: organization required
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: delete failed
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Delete annotation (org scoped)
|
||||
tags:
|
||||
- annotations
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns one annotation. Add `include=node_pools` to include node
|
||||
pools.
|
||||
parameters:
|
||||
- description: Organization UUID
|
||||
in: header
|
||||
name: X-Org-ID
|
||||
required: true
|
||||
type: string
|
||||
- description: Annotation ID (UUID)
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: 'Optional: node_pools'
|
||||
in: query
|
||||
name: include
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/annotations.annotationResponse'
|
||||
"400":
|
||||
description: invalid id
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: organization required
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: fetch failed
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Get annotation by ID (org scoped)
|
||||
tags:
|
||||
- annotations
|
||||
patch:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Partially update annotation fields.
|
||||
parameters:
|
||||
- description: Organization UUID
|
||||
in: header
|
||||
name: X-Org-ID
|
||||
required: true
|
||||
type: string
|
||||
- description: Annotation ID (UUID)
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Fields to update
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/annotations.updateAnnotationRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/annotations.annotationResponse'
|
||||
"400":
|
||||
description: invalid id / invalid json
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: organization required
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: update failed
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Update annotation (org scoped)
|
||||
tags:
|
||||
- annotations
|
||||
/api/v1/annotations/{id}/node_pools:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns node pools attached to the annotation. Supports `q` (name
|
||||
contains, case-insensitive).
|
||||
parameters:
|
||||
- description: Organization UUID
|
||||
in: header
|
||||
name: X-Org-ID
|
||||
required: true
|
||||
type: string
|
||||
- description: Annotation ID (UUID)
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Name contains (case-insensitive)
|
||||
in: query
|
||||
name: q
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/annotations.nodePoolBrief'
|
||||
type: array
|
||||
"400":
|
||||
description: invalid id
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: organization required
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: fetch failed
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: List node pools linked to an annotation (org scoped)
|
||||
tags:
|
||||
- annotations
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Links the annotation to one or more node pools in the same organization.
|
||||
parameters:
|
||||
- description: Organization UUID
|
||||
in: header
|
||||
name: X-Org-ID
|
||||
required: true
|
||||
type: string
|
||||
- description: Annotation ID (UUID)
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: IDs to attach
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/annotations.addAnnotationToNodePool'
|
||||
- description: 'Optional: node_pools'
|
||||
in: query
|
||||
name: include
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/annotations.annotationResponse'
|
||||
"400":
|
||||
description: invalid id / invalid json / invalid node_pool_ids
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: organization required
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: attach failed
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Attach annotation to node pools (org scoped)
|
||||
tags:
|
||||
- annotations
|
||||
/api/v1/annotations/{id}/node_pools/{poolId}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Unlinks the annotation from the specified node pool.
|
||||
parameters:
|
||||
- description: Organization UUID
|
||||
in: header
|
||||
name: X-Org-ID
|
||||
required: true
|
||||
type: string
|
||||
- description: Annotation ID (UUID)
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: invalid id
|
||||
schema:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: organization required
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: not found
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: detach failed
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Detach annotation from a node pool (org scoped)
|
||||
tags:
|
||||
- annotations
|
||||
/api/v1/auth/introspect:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
httpPprof "net/http/pprof"
|
||||
|
||||
"github.com/glueops/autoglue/internal/config"
|
||||
"github.com/glueops/autoglue/internal/handlers/annotations"
|
||||
"github.com/glueops/autoglue/internal/handlers/authn"
|
||||
"github.com/glueops/autoglue/internal/handlers/health"
|
||||
"github.com/glueops/autoglue/internal/handlers/labels"
|
||||
@@ -54,6 +55,18 @@ func RegisterRoutes(r chi.Router) {
|
||||
})
|
||||
})
|
||||
|
||||
v1.Route("/annotations", func(a chi.Router) {
|
||||
a.Use(authMW)
|
||||
a.Get("/", annotations.ListAnnotations)
|
||||
a.Post("/", annotations.CreateAnnotation)
|
||||
a.Get("/{id}", annotations.GetAnnotation)
|
||||
a.Patch("/{id}", annotations.UpdateAnnotation)
|
||||
a.Delete("/{id}", annotations.DeleteAnnotation)
|
||||
a.Get("/{id}/node_pools", annotations.ListNodePoolsWithAnnotation)
|
||||
a.Post("/{id}/node_pools", annotations.AddAnnotationToNodePools)
|
||||
a.Delete("/{id}/node_pools/{poolId}", annotations.RemoveAnnotationFromNodePool)
|
||||
})
|
||||
|
||||
v1.Route("/orgs", func(o chi.Router) {
|
||||
o.Use(authMW)
|
||||
o.Post("/", orgs.CreateOrganization)
|
||||
|
||||
@@ -1 +1,559 @@
|
||||
package annotations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/glueops/autoglue/internal/db"
|
||||
"github.com/glueops/autoglue/internal/db/models"
|
||||
"github.com/glueops/autoglue/internal/middleware"
|
||||
"github.com/glueops/autoglue/internal/response"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
/* ------------------------------- DTOs ----------------------------------- */
|
||||
|
||||
type nodePoolBrief struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type annotationResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
NodePools []nodePoolBrief `json:"node_pools,omitempty"`
|
||||
}
|
||||
|
||||
type createAnnotationRequest struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
NodePoolIDs []string `json:"node_pool_ids"`
|
||||
}
|
||||
|
||||
type updateAnnotationRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Value *string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type addAnnotationToNodePool struct {
|
||||
NodePoolIDs []string `json:"node_pool_ids"`
|
||||
}
|
||||
|
||||
/* ------------------------------- Helpers -------------------------------- */
|
||||
|
||||
func toResp(a models.Annotation, includePools bool) annotationResponse {
|
||||
out := annotationResponse{
|
||||
ID: a.ID,
|
||||
Name: a.Name,
|
||||
Value: a.Value,
|
||||
}
|
||||
if includePools {
|
||||
for _, p := range a.NodePools {
|
||||
out.NodePools = append(out.NodePools, nodePoolBrief{ID: p.ID, Name: p.Name})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseUUIDs(in []string) ([]uuid.UUID, error) {
|
||||
out := make([]uuid.UUID, 0, len(in))
|
||||
for _, s := range in {
|
||||
id, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, id)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ensureNodePoolsBelongToOrg(orgID uuid.UUID, ids []uuid.UUID) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
var count int64
|
||||
if err := db.DB.Model(&models.NodePool{}).
|
||||
Where("id IN ? AND organization_id = ?", ids, orgID).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count != int64(len(ids)) {
|
||||
return errors.New("one or more node pools do not belong to organization")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/* -------------------------------- Routes -------------------------------- */
|
||||
|
||||
// ListAnnotations godoc
|
||||
// @Summary List annotations (org scoped)
|
||||
// @Description Returns annotations for the organization in X-Org-ID. Filters: `name`, `value`, and `q` (name contains). Add `include=node_pools` to include linked node pools.
|
||||
// @Tags annotations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string true "Organization UUID"
|
||||
// @Param name query string false "Exact name"
|
||||
// @Param value query string false "Exact value"
|
||||
// @Param q query string false "name contains (case-insensitive)"
|
||||
// @Param include query string false "Optional: node_pools"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {array} annotationResponse
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "failed to list annotations"
|
||||
// @Router /api/v1/annotations [get]
|
||||
func ListAnnotations(w http.ResponseWriter, r *http.Request) {
|
||||
ac := middleware.GetAuthContext(r)
|
||||
if ac == nil || ac.OrganizationID == uuid.Nil {
|
||||
http.Error(w, "organization required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
q := db.DB.Where("organization_id = ?", ac.OrganizationID)
|
||||
if name := strings.TrimSpace(r.URL.Query().Get("name")); name != "" {
|
||||
q = q.Where(`name = ?`, name)
|
||||
}
|
||||
if val := strings.TrimSpace(r.URL.Query().Get("value")); val != "" {
|
||||
q = q.Where(`value = ?`, val)
|
||||
}
|
||||
if needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
|
||||
q = q.Where(`name ILIKE ?`, "%"+needle+"%")
|
||||
}
|
||||
|
||||
includePools := strings.EqualFold(strings.TrimSpace(r.URL.Query().Get("include")), "node_pools")
|
||||
if includePools {
|
||||
q = q.Preload("NodePools")
|
||||
}
|
||||
|
||||
var rows []models.Annotation
|
||||
if err := q.Order("created_at DESC").Find(&rows).Error; err != nil {
|
||||
http.Error(w, "failed to list annotations", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]annotationResponse, 0, len(rows))
|
||||
for _, a := range rows {
|
||||
out = append(out, toResp(a, includePools))
|
||||
}
|
||||
_ = response.JSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// GetAnnotation godoc
|
||||
// @Summary Get annotation by ID (org scoped)
|
||||
// @Description Returns one annotation. Add `include=node_pools` to include node pools.
|
||||
// @Tags annotations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string true "Organization UUID"
|
||||
// @Param id path string true "Annotation ID (UUID)"
|
||||
// @Param include query string false "Optional: node_pools"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} annotationResponse
|
||||
// @Failure 400 {string} string "invalid id"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "fetch failed"
|
||||
// @Router /api/v1/annotations/{id} [get]
|
||||
func GetAnnotation(w http.ResponseWriter, r *http.Request) {
|
||||
ac := middleware.GetAuthContext(r)
|
||||
if ac == nil || ac.OrganizationID == uuid.Nil {
|
||||
http.Error(w, "organization required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
includePools := strings.EqualFold(strings.TrimSpace(r.URL.Query().Get("include")), "node_pools")
|
||||
|
||||
var a models.Annotation
|
||||
q := db.DB.Where("id = ? AND organization_id = ?", id, ac.OrganizationID)
|
||||
if includePools {
|
||||
q = q.Preload("NodePools")
|
||||
}
|
||||
|
||||
if err := q.First(&a).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "fetch failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_ = response.JSON(w, http.StatusOK, toResp(a, includePools))
|
||||
}
|
||||
|
||||
// CreateAnnotation godoc
|
||||
// @Summary Create annotation (org scoped)
|
||||
// @Description Creates an annotation. Optionally link to node pools.
|
||||
// @Tags annotations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string true "Organization UUID"
|
||||
// @Param body body createAnnotationRequest true "Annotation payload"
|
||||
// @Security BearerAuth
|
||||
// @Success 201 {object} annotationResponse
|
||||
// @Failure 400 {string} string "invalid json / missing fields / invalid node_pool_ids"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "create failed"
|
||||
// @Router /api/v1/annotations [post]
|
||||
func CreateAnnotation(w http.ResponseWriter, r *http.Request) {
|
||||
ac := middleware.GetAuthContext(r)
|
||||
if ac == nil || ac.OrganizationID == uuid.Nil {
|
||||
http.Error(w, "organization required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var req createAnnotationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Value) == "" {
|
||||
http.Error(w, "invalid json or missing name/value", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
a := models.Annotation{
|
||||
OrganizationID: ac.OrganizationID,
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Value: strings.TrimSpace(req.Value),
|
||||
}
|
||||
|
||||
if err := db.DB.Create(&a).Error; err != nil {
|
||||
http.Error(w, "create failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.NodePoolIDs) > 0 {
|
||||
ids, err := parseUUIDs(req.NodePoolIDs)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid node_pool_ids", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := ensureNodePoolsBelongToOrg(ac.OrganizationID, ids); err != nil {
|
||||
http.Error(w, "invalid node_pool_ids for this organization", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var pools []models.NodePool
|
||||
if err := db.DB.Where("id IN ? AND organization_id = ?", ids, ac.OrganizationID).Find(&pools).Error; err != nil {
|
||||
http.Error(w, "attach failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := db.DB.Model(&a).Association("NodePools").Append(&pools); err != nil {
|
||||
http.Error(w, "attach failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_ = response.JSON(w, http.StatusCreated, toResp(a, false))
|
||||
}
|
||||
|
||||
// UpdateAnnotation godoc
|
||||
// @Summary Update annotation (org scoped)
|
||||
// @Description Partially update annotation fields.
|
||||
// @Tags annotations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string true "Organization UUID"
|
||||
// @Param id path string true "Annotation ID (UUID)"
|
||||
// @Param body body updateAnnotationRequest true "Fields to update"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} annotationResponse
|
||||
// @Failure 400 {string} string "invalid id / invalid json"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "update failed"
|
||||
// @Router /api/v1/annotations/{id} [patch]
|
||||
func UpdateAnnotation(w http.ResponseWriter, r *http.Request) {
|
||||
ac := middleware.GetAuthContext(r)
|
||||
if ac == nil || ac.OrganizationID == uuid.Nil {
|
||||
http.Error(w, "organization required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var a models.Annotation
|
||||
if err := db.DB.Where("id = ? AND organization_id = ?", id, ac.OrganizationID).First(&a).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "fetch failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req updateAnnotationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Name != nil {
|
||||
a.Name = strings.TrimSpace(*req.Name)
|
||||
}
|
||||
if req.Value != nil {
|
||||
a.Value = strings.TrimSpace(*req.Value)
|
||||
}
|
||||
|
||||
if err := db.DB.Save(&a).Error; err != nil {
|
||||
http.Error(w, "update failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_ = response.JSON(w, http.StatusOK, toResp(a, false))
|
||||
}
|
||||
|
||||
// DeleteAnnotation godoc
|
||||
// @Summary Delete annotation (org scoped)
|
||||
// @Description Permanently deletes the annotation.
|
||||
// @Tags annotations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string true "Organization UUID"
|
||||
// @Param id path string true "Annotation ID (UUID)"
|
||||
// @Security BearerAuth
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 400 {string} string "invalid id"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "delete failed"
|
||||
// @Router /api/v1/annotations/{id} [delete]
|
||||
func DeleteAnnotation(w http.ResponseWriter, r *http.Request) {
|
||||
ac := middleware.GetAuthContext(r)
|
||||
if ac == nil || ac.OrganizationID == uuid.Nil {
|
||||
http.Error(w, "organization required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB.Where("id = ? AND organization_id = ?", id, ac.OrganizationID).Delete(&models.Annotation{}).Error; err != nil {
|
||||
http.Error(w, "delete failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response.NoContent(w)
|
||||
}
|
||||
|
||||
// AddAnnotationToNodePools godoc
|
||||
// @Summary Attach annotation to node pools (org scoped)
|
||||
// @Description Links the annotation to one or more node pools in the same organization.
|
||||
// @Tags annotations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string true "Organization UUID"
|
||||
// @Param id path string true "Annotation ID (UUID)"
|
||||
// @Param body body addAnnotationToNodePool true "IDs to attach"
|
||||
// @Param include query string false "Optional: node_pools"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} annotationResponse
|
||||
// @Failure 400 {string} string "invalid id / invalid json / invalid node_pool_ids"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "attach failed"
|
||||
// @Router /api/v1/annotations/{id}/node_pools [post]
|
||||
func AddAnnotationToNodePools(w http.ResponseWriter, r *http.Request) {
|
||||
ac := middleware.GetAuthContext(r)
|
||||
if ac == nil || ac.OrganizationID == uuid.Nil {
|
||||
http.Error(w, "organization required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
annID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var a models.Annotation
|
||||
if err := db.DB.Where("id = ? AND organization_id = ?", annID, ac.OrganizationID).First(&a).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "fetch failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var in struct {
|
||||
NodePoolIDs []string `json:"node_pool_ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil || len(in.NodePoolIDs) == 0 {
|
||||
http.Error(w, "invalid json or empty node_pool_ids", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ids, err := parseUUIDs(in.NodePoolIDs)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid node_pool_ids", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := ensureNodePoolsBelongToOrg(ac.OrganizationID, ids); err != nil {
|
||||
http.Error(w, "invalid node_pool_ids for this organization", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var pools []models.NodePool
|
||||
if err := db.DB.Where("id IN ? AND organization_id = ?", ids, ac.OrganizationID).Find(&pools).Error; err != nil {
|
||||
http.Error(w, "attach failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := db.DB.Model(&a).Association("NodePools").Append(&pools); err != nil {
|
||||
http.Error(w, "attach failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
includePools := strings.EqualFold(strings.TrimSpace(r.URL.Query().Get("include")), "node_pools")
|
||||
if includePools {
|
||||
if err := db.DB.Preload("NodePools").First(&a, "id = ? AND organization_id = ?", annID, ac.OrganizationID).Error; err != nil {
|
||||
http.Error(w, "fetch failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_ = response.JSON(w, http.StatusOK, toResp(a, includePools))
|
||||
}
|
||||
|
||||
// RemoveAnnotationFromNodePool godoc
|
||||
// @Summary Detach annotation from a node pool (org scoped)
|
||||
// @Description Unlinks the annotation from the specified node pool.
|
||||
// @Tags annotations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string true "Organization UUID"
|
||||
// @Param id path string true "Annotation ID (UUID)"
|
||||
// / @Param poolId path string true "Node Pool ID (UUID)"
|
||||
// @Security BearerAuth
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 400 {string} string "invalid id"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "detach failed"
|
||||
// @Router /api/v1/annotations/{id}/node_pools/{poolId} [delete]
|
||||
func RemoveAnnotationFromNodePool(w http.ResponseWriter, r *http.Request) {
|
||||
ac := middleware.GetAuthContext(r)
|
||||
if ac == nil || ac.OrganizationID == uuid.Nil {
|
||||
http.Error(w, "organization required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
annID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
poolID, err := uuid.Parse(chi.URLParam(r, "poolId"))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var a models.Annotation
|
||||
if err := db.DB.Where("id = ? AND organization_id = ?", annID, ac.OrganizationID).First(&a).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "fetch failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var p models.NodePool
|
||||
if err := db.DB.Where("id = ? AND organization_id = ?", poolID, ac.OrganizationID).First(&p).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "fetch failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB.Model(&a).Association("NodePools").Delete(&p); err != nil {
|
||||
http.Error(w, "detach failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response.NoContent(w)
|
||||
}
|
||||
|
||||
// ListNodePoolsWithAnnotation godoc
|
||||
// @Summary List node pools linked to an annotation (org scoped)
|
||||
// @Description Returns node pools attached to the annotation. Supports `q` (name contains, case-insensitive).
|
||||
// @Tags annotations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string true "Organization UUID"
|
||||
// @Param id path string true "Annotation ID (UUID)"
|
||||
// @Param q query string false "Name contains (case-insensitive)"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {array} nodePoolBrief
|
||||
// @Failure 400 {string} string "invalid id"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "fetch failed"
|
||||
// @Router /api/v1/annotations/{id}/node_pools [get]
|
||||
func ListNodePoolsWithAnnotation(w http.ResponseWriter, r *http.Request) {
|
||||
ac := middleware.GetAuthContext(r)
|
||||
if ac == nil || ac.OrganizationID == uuid.Nil {
|
||||
http.Error(w, "organization required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
annID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the annotation exists within this org
|
||||
var a models.Annotation
|
||||
if err := db.DB.Where("id = ? AND organization_id = ?", annID, ac.OrganizationID).First(&a).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "fetch failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Find pools joined via the M2M table "node_annotations"
|
||||
q := db.DB.Model(&models.NodePool{}).
|
||||
Joins("JOIN node_annotations na ON na.node_pool_id = node_pools.id").
|
||||
Where("na.annotation_id = ? AND node_pools.organization_id = ?", annID, ac.OrganizationID)
|
||||
|
||||
if needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
|
||||
q = q.Where("node_pools.name ILIKE ?", "%"+needle+"%")
|
||||
}
|
||||
|
||||
var pools []models.NodePool
|
||||
if err := q.Order("node_pools.created_at DESC").Find(&pools).Error; err != nil {
|
||||
http.Error(w, "fetch failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]nodePoolBrief, 0, len(pools))
|
||||
for _, p := range pools {
|
||||
out = append(out, nodePoolBrief{ID: p.ID, Name: p.Name})
|
||||
}
|
||||
|
||||
_ = response.JSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,625 @@
|
||||
export const AnnotationsPage = () => {
|
||||
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="mb-4 text-2xl font-bold">Annotations</h1>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { z } from "zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import {
|
||||
LinkIcon,
|
||||
Pencil,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Trash,
|
||||
UnlinkIcon,
|
||||
ServerIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { api, ApiError } from "@/lib/api"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
|
||||
/* ----------------------------- Types & Schemas ---------------------------- */
|
||||
|
||||
type NodePoolBrief = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type Annotation = {
|
||||
id: string
|
||||
name: string
|
||||
value: string
|
||||
node_pools?: NodePoolBrief[]
|
||||
}
|
||||
|
||||
const CreateSchema = z.object({
|
||||
name: z.string().trim().min(1, "Name is required").max(120, "Max 120 chars"),
|
||||
value: z.string().trim().min(1, "Value is required").max(512, "Max 512 chars"),
|
||||
node_pool_ids: z.array(z.string().uuid()).optional().default([]),
|
||||
})
|
||||
type CreateInput = z.input<typeof CreateSchema>
|
||||
type CreateValues = z.output<typeof CreateSchema>
|
||||
|
||||
const UpdateSchema = z.object({
|
||||
name: z.string().trim().min(1, "Name is required").max(120, "Max 120 chars"),
|
||||
value: z.string().trim().min(1, "Value is required").max(512, "Max 512 chars"),
|
||||
})
|
||||
type UpdateValues = z.output<typeof UpdateSchema>
|
||||
|
||||
const AttachPoolsSchema = z.object({
|
||||
node_pool_ids: z.array(z.string().uuid()).min(1, "Pick at least one node pool"),
|
||||
})
|
||||
type AttachPoolsValues = z.output<typeof AttachPoolsSchema>
|
||||
|
||||
/* --------------------------------- Utils --------------------------------- */
|
||||
|
||||
function truncateMiddle(str: string, keep = 12) {
|
||||
if (!str || str.length <= keep * 2 + 3) return str
|
||||
return `${str.slice(0, keep)}…${str.slice(-keep)}`
|
||||
}
|
||||
|
||||
/* --------------------------------- Page ---------------------------------- */
|
||||
|
||||
export const AnnotationsPage = () => {
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [err, setErr] = useState<string | null>(null)
|
||||
|
||||
const [annotations, setAnnotations] = useState<Annotation[]>([])
|
||||
const [allPools, setAllPools] = useState<NodePoolBrief[]>([])
|
||||
|
||||
const [q, setQ] = useState("")
|
||||
|
||||
// Dialog state
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [editTarget, setEditTarget] = useState<Annotation | null>(null)
|
||||
const [managePoolsTarget, setManagePoolsTarget] = useState<Annotation | null>(null)
|
||||
|
||||
// Attached pools (for manage dialog)
|
||||
const [attachedPools, setAttachedPools] = useState<NodePoolBrief[]>([])
|
||||
const [attachedLoading, setAttachedLoading] = useState(false)
|
||||
const [attachedErr, setAttachedErr] = useState<string | null>(null)
|
||||
|
||||
/* ------------------------------- Data Load ------------------------------ */
|
||||
|
||||
async function loadAll() {
|
||||
setLoading(true)
|
||||
setErr(null)
|
||||
try {
|
||||
const [ann, pools] = await Promise.all([
|
||||
api.get<Annotation[]>("/api/v1/annotations?include=node_pools"),
|
||||
api.get<NodePoolBrief[]>("/api/v1/node-pools"),
|
||||
])
|
||||
setAnnotations(ann || [])
|
||||
setAllPools(pools || [])
|
||||
|
||||
// keep dialog targets in sync
|
||||
if (editTarget) {
|
||||
const refreshed = (ann || []).find((a) => a.id === editTarget.id) || null
|
||||
setEditTarget(refreshed)
|
||||
}
|
||||
if (managePoolsTarget) {
|
||||
const refreshed = (ann || []).find((a) => a.id === managePoolsTarget.id) || null
|
||||
setManagePoolsTarget(refreshed)
|
||||
if (refreshed) {
|
||||
void loadAttachedPools(refreshed.id)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
const msg =
|
||||
e instanceof ApiError ? e.message : "Failed to load annotations / node pools"
|
||||
setErr(msg)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAttachedPools(annotationId: string) {
|
||||
setAttachedLoading(true)
|
||||
setAttachedErr(null)
|
||||
try {
|
||||
const data = await api.get<NodePoolBrief[]>(
|
||||
`/api/v1/annotations/${annotationId}/node_pools`
|
||||
)
|
||||
setAttachedPools(data || [])
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
const msg =
|
||||
e instanceof ApiError ? e.message : "Failed to load pools for annotation"
|
||||
setAttachedErr(msg)
|
||||
} finally {
|
||||
setAttachedLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadAll()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
/* -------------------------------- Filters ------------------------------- */
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const needle = q.trim().toLowerCase()
|
||||
if (!needle) return annotations
|
||||
return annotations.filter(
|
||||
(a) =>
|
||||
a.name.toLowerCase().includes(needle) ||
|
||||
a.value.toLowerCase().includes(needle) ||
|
||||
(a.node_pools || []).some((p) => p.name.toLowerCase().includes(needle))
|
||||
)
|
||||
}, [annotations, q])
|
||||
|
||||
/* ------------------------------ Mutations ------------------------------- */
|
||||
|
||||
async function deleteAnnotation(id: string) {
|
||||
if (!confirm("Delete this annotation? This cannot be undone.")) return
|
||||
await api.delete<void>(`/api/v1/annotations/${id}`)
|
||||
await loadAll()
|
||||
}
|
||||
|
||||
// Create
|
||||
const createForm = useForm<CreateInput, any, CreateValues>({
|
||||
resolver: zodResolver(CreateSchema),
|
||||
defaultValues: { name: "", value: "", node_pool_ids: [] },
|
||||
})
|
||||
|
||||
const submitCreate = async (values: CreateValues) => {
|
||||
const payload: any = { name: values.name.trim(), value: values.value.trim() }
|
||||
if (values.node_pool_ids && values.node_pool_ids.length > 0) {
|
||||
payload.node_pool_ids = values.node_pool_ids
|
||||
}
|
||||
await api.post("/api/v1/annotations", payload)
|
||||
setCreateOpen(false)
|
||||
createForm.reset({ name: "", value: "", node_pool_ids: [] })
|
||||
await loadAll()
|
||||
}
|
||||
|
||||
// Edit
|
||||
const editForm = useForm<UpdateValues>({
|
||||
resolver: zodResolver(UpdateSchema),
|
||||
defaultValues: { name: "", value: "" },
|
||||
})
|
||||
|
||||
function openEdit(a: Annotation) {
|
||||
setEditTarget(a)
|
||||
editForm.reset({ name: a.name, value: a.value })
|
||||
}
|
||||
|
||||
const submitEdit = async (values: UpdateValues) => {
|
||||
if (!editTarget) return
|
||||
await api.patch(`/api/v1/annotations/${editTarget.id}`, {
|
||||
name: values.name.trim(),
|
||||
value: values.value.trim(),
|
||||
})
|
||||
setEditTarget(null)
|
||||
await loadAll()
|
||||
}
|
||||
|
||||
// Manage pools (attach/detach)
|
||||
const attachPoolsForm = useForm<AttachPoolsValues>({
|
||||
resolver: zodResolver(AttachPoolsSchema),
|
||||
defaultValues: { node_pool_ids: [] },
|
||||
})
|
||||
|
||||
function openManagePools(a: Annotation) {
|
||||
setManagePoolsTarget(a)
|
||||
attachPoolsForm.reset({ node_pool_ids: [] })
|
||||
void loadAttachedPools(a.id)
|
||||
}
|
||||
|
||||
const submitAttachPools = async (values: AttachPoolsValues) => {
|
||||
if (!managePoolsTarget) return
|
||||
await api.post(`/api/v1/annotations/${managePoolsTarget.id}/node_pools`, {
|
||||
node_pool_ids: values.node_pool_ids,
|
||||
})
|
||||
attachPoolsForm.reset({ node_pool_ids: [] })
|
||||
await loadAttachedPools(managePoolsTarget.id)
|
||||
await loadAll()
|
||||
}
|
||||
|
||||
async function detachPool(poolId: string) {
|
||||
if (!managePoolsTarget) return
|
||||
if (!confirm("Detach this node pool from the annotation?")) return
|
||||
await api.delete(`/api/v1/annotations/${managePoolsTarget.id}/node_pools/${poolId}`)
|
||||
await loadAttachedPools(managePoolsTarget.id)
|
||||
await loadAll()
|
||||
}
|
||||
|
||||
/* --------------------------------- Render -------------------------------- */
|
||||
|
||||
if (loading) return <div className="p-6">Loading annotations…</div>
|
||||
if (err) return <div className="p-6 text-red-500">{err}</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="mb-4 text-2xl font-bold">Annotations</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={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="Search name, value, pool…"
|
||||
className="w-72 pl-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" onClick={loadAll}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" /> Refresh
|
||||
</Button>
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={() => setCreateOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" /> Create Annotation
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create annotation</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...createForm}>
|
||||
<form onSubmit={createForm.handleSubmit(submitCreate)} className="space-y-4">
|
||||
<FormField
|
||||
control={createForm.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="cluster-autoscaler.kubernetes.io/safe-to-evict" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={createForm.control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Value</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="true" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={createForm.control}
|
||||
name="node_pool_ids"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Initial node pools (optional)</FormLabel>
|
||||
<div className="max-h-56 space-y-2 overflow-auto rounded-xl border p-2">
|
||||
{allPools.length === 0 && (
|
||||
<div className="text-muted-foreground p-2 text-sm">No node pools available</div>
|
||||
)}
|
||||
{allPools.map((p) => {
|
||||
const checked = field.value?.includes(p.id) || false
|
||||
return (
|
||||
<label
|
||||
key={p.id}
|
||||
className="hover:bg-accent flex cursor-pointer items-start gap-2 rounded p-1"
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onCheckedChange={(v) => {
|
||||
const next = new Set(field.value || [])
|
||||
if (v === true) next.add(p.id)
|
||||
else next.delete(p.id)
|
||||
field.onChange(Array.from(next))
|
||||
}}
|
||||
/>
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-medium">{p.name}</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{truncateMiddle(p.id, 8)}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setCreateOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={createForm.formState.isSubmitting}>
|
||||
{createForm.formState.isSubmitting ? "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>Name</TableHead>
|
||||
<TableHead>Value</TableHead>
|
||||
<TableHead>Node Pools</TableHead>
|
||||
<TableHead className="w-[180px] text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map((a) => {
|
||||
const pools = a.node_pools || []
|
||||
return (
|
||||
<TableRow key={a.id}>
|
||||
<TableCell className="font-mono text-sm">{a.name}</TableCell>
|
||||
<TableCell className="font-mono text-sm">{a.value}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div className="mb-2 flex flex-wrap gap-2">
|
||||
{pools.slice(0, 6).map((p) => (
|
||||
<Badge key={p.id} variant="secondary" className="gap-1">
|
||||
<ServerIcon className="h-3 w-3" /> {p.name}
|
||||
</Badge>
|
||||
))}
|
||||
{pools.length === 0 && (
|
||||
<span className="text-muted-foreground">No node pools</span>
|
||||
)}
|
||||
{pools.length > 6 && (
|
||||
<span className="text-muted-foreground">+{pools.length - 6} more</span>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => openManagePools(a)}>
|
||||
<LinkIcon className="mr-2 h-4 w-4" /> Manage Node Pools
|
||||
</Button>
|
||||
</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>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="destructive" size="sm">
|
||||
<Trash className="mr-2 h-4 w-4" /> Delete
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => deleteAnnotation(a.id)}>
|
||||
Confirm delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-muted-foreground py-10 text-center">
|
||||
No annotations match your search.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Edit dialog */}
|
||||
<Dialog open={!!editTarget} onOpenChange={(o) => !o && setEditTarget(null)}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit annotation</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...editForm}>
|
||||
<form onSubmit={editForm.handleSubmit(submitEdit)} className="space-y-4">
|
||||
<FormField
|
||||
control={editForm.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="example.com/some" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={editForm.control}
|
||||
name="value"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Value</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="true" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setEditTarget(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={editForm.formState.isSubmitting}>
|
||||
{editForm.formState.isSubmitting ? "Saving…" : "Save changes"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Manage node pools dialog */}
|
||||
<Dialog open={!!managePoolsTarget} onOpenChange={(o) => !o && setManagePoolsTarget(null)}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Manage node pools for{" "}
|
||||
<span className="font-mono">{managePoolsTarget?.name}</span>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Attached pools list */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium">Attached node pools</div>
|
||||
|
||||
{attachedLoading ? (
|
||||
<div className="text-muted-foreground rounded-md border p-3 text-sm">Loading…</div>
|
||||
) : attachedErr ? (
|
||||
<div className="rounded-md border p-3 text-sm text-red-500">{attachedErr}</div>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-xl border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead className="w-[120px] text-right">Detach</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{attachedPools.map((p) => (
|
||||
<TableRow key={p.id}>
|
||||
<TableCell className="font-medium">{p.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => detachPool(p.id)}
|
||||
>
|
||||
<UnlinkIcon className="mr-2 h-4 w-4" /> Detach
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{attachedPools.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="text-muted-foreground py-8 text-center">
|
||||
No node pools attached yet.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Attach pools */}
|
||||
<div className="pt-4">
|
||||
<Form {...attachPoolsForm}>
|
||||
<form onSubmit={attachPoolsForm.handleSubmit(submitAttachPools)} className="space-y-3">
|
||||
<FormField
|
||||
control={attachPoolsForm.control}
|
||||
name="node_pool_ids"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Attach more node pools</FormLabel>
|
||||
<div className="grid max-h-64 grid-cols-1 gap-2 overflow-auto rounded-xl border p-2 md:grid-cols-2">
|
||||
{(() => {
|
||||
const attachedIds = new Set(attachedPools.map((p) => p.id))
|
||||
const attachable = allPools.filter((p) => !attachedIds.has(p.id))
|
||||
if (attachable.length === 0) {
|
||||
return (
|
||||
<div className="text-muted-foreground p-2 text-sm">
|
||||
No more node pools available to attach
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return attachable.map((p) => {
|
||||
const checked = field.value?.includes(p.id) || false
|
||||
return (
|
||||
<label
|
||||
key={p.id}
|
||||
className="hover:bg-accent flex cursor-pointer items-start gap-2 rounded p-1"
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onCheckedChange={(v) => {
|
||||
const next = new Set(field.value || [])
|
||||
if (v === true) next.add(p.id)
|
||||
else next.delete(p.id)
|
||||
field.onChange(Array.from(next))
|
||||
}}
|
||||
/>
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-medium">{p.name}</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{truncateMiddle(p.id, 8)}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
})
|
||||
})()}
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button type="submit" disabled={attachPoolsForm.formState.isSubmitting}>
|
||||
<LinkIcon className="mr-2 h-4 w-4" />{" "}
|
||||
{attachPoolsForm.formState.isSubmitting ? "Attaching…" : "Attach selected"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user