mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 12:50:05 +01:00
annotations added to nodepools page
This commit is contained in:
261
docs/docs.go
261
docs/docs.go
@@ -2417,6 +2417,242 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/node-pools/{id}/annotations": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"node-pools"
|
||||||
|
],
|
||||||
|
"summary": "List annotations attached to a node pool (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Node Pool ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/nodepools.annotationBrief"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"node-pools"
|
||||||
|
],
|
||||||
|
"summary": "Attach annotations to a node pool (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Node Pool ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Annotation IDs to attach",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/nodepools.attachAnnotationsRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "invalid id / invalid annotation_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/node-pools/{id}/annotations/{annotationId}": {
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"node-pools"
|
||||||
|
],
|
||||||
|
"summary": "Detach one annotation from a node pool (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Node Pool ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Annotation ID (UUID)",
|
||||||
|
"name": "annotationId",
|
||||||
|
"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/node-pools/{id}/labels": {
|
"/api/v1/node-pools/{id}/labels": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -5271,6 +5507,31 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodepools.annotationBrief": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nodepools.attachAnnotationsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"annotation_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"nodepools.attachLabelsRequest": {
|
"nodepools.attachLabelsRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -2413,6 +2413,242 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/node-pools/{id}/annotations": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"node-pools"
|
||||||
|
],
|
||||||
|
"summary": "List annotations attached to a node pool (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Node Pool ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/nodepools.annotationBrief"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"node-pools"
|
||||||
|
],
|
||||||
|
"summary": "Attach annotations to a node pool (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Node Pool ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Annotation IDs to attach",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/nodepools.attachAnnotationsRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "invalid id / invalid annotation_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/node-pools/{id}/annotations/{annotationId}": {
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"node-pools"
|
||||||
|
],
|
||||||
|
"summary": "Detach one annotation from a node pool (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Node Pool ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Annotation ID (UUID)",
|
||||||
|
"name": "annotationId",
|
||||||
|
"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/node-pools/{id}/labels": {
|
"/api/v1/node-pools/{id}/labels": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -5267,6 +5503,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodepools.annotationBrief": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nodepools.attachAnnotationsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"annotation_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"nodepools.attachLabelsRequest": {
|
"nodepools.attachLabelsRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -319,6 +319,22 @@ definitions:
|
|||||||
updated_at:
|
updated_at:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
nodepools.annotationBrief:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
nodepools.attachAnnotationsRequest:
|
||||||
|
properties:
|
||||||
|
annotation_ids:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
nodepools.attachLabelsRequest:
|
nodepools.attachLabelsRequest:
|
||||||
properties:
|
properties:
|
||||||
label_ids:
|
label_ids:
|
||||||
@@ -2163,6 +2179,159 @@ paths:
|
|||||||
summary: Update node pool (org scoped)
|
summary: Update node pool (org scoped)
|
||||||
tags:
|
tags:
|
||||||
- node-pools
|
- node-pools
|
||||||
|
/api/v1/node-pools/{id}/annotations:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Node Pool ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/nodepools.annotationBrief'
|
||||||
|
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 annotations attached to a node pool (org scoped)
|
||||||
|
tags:
|
||||||
|
- node-pools
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Node Pool ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Annotation IDs to attach
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/nodepools.attachAnnotationsRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No Content
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"400":
|
||||||
|
description: invalid id / invalid annotation_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 annotations to a node pool (org scoped)
|
||||||
|
tags:
|
||||||
|
- node-pools
|
||||||
|
/api/v1/node-pools/{id}/annotations/{annotationId}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Node Pool ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Annotation ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: annotationId
|
||||||
|
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 one annotation from a node pool (org scoped)
|
||||||
|
tags:
|
||||||
|
- node-pools
|
||||||
/api/v1/node-pools/{id}/labels:
|
/api/v1/node-pools/{id}/labels:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ func RegisterRoutes(r chi.Router) {
|
|||||||
np.Get("/{id}/labels", nodepools.ListNodePoolLabels)
|
np.Get("/{id}/labels", nodepools.ListNodePoolLabels)
|
||||||
np.Post("/{id}/labels", nodepools.AttachNodePoolLabels)
|
np.Post("/{id}/labels", nodepools.AttachNodePoolLabels)
|
||||||
np.Delete("/{id}/labels/{labelId}", nodepools.DetachNodePoolLabel)
|
np.Delete("/{id}/labels/{labelId}", nodepools.DetachNodePoolLabel)
|
||||||
|
|
||||||
|
// annotations
|
||||||
|
np.Get("/{id}/annotations", nodepools.ListNodePoolAnnotations)
|
||||||
|
np.Post("/{id}/annotations", nodepools.AttachNodePoolAnnotations)
|
||||||
|
np.Delete("/{id}/annotations/{annotationId}", nodepools.DetachNodePoolAnnotation)
|
||||||
})
|
})
|
||||||
|
|
||||||
v1.Route("/taints", func(t chi.Router) {
|
v1.Route("/taints", func(t chi.Router) {
|
||||||
|
|||||||
@@ -51,3 +51,13 @@ type taintBrief struct {
|
|||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
Effect string `json:"effect"`
|
Effect string `json:"effect"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type annotationBrief struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type attachAnnotationsRequest struct {
|
||||||
|
AnnotationIDs []string `json:"annotation_ids"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,3 +78,19 @@ func ensureTaintsBelongToOrg(orgID uuid.UUID, ids []uuid.UUID) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureAnnotationsBelongToOrg(orgID uuid.UUID, ids []uuid.UUID) error {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return errors.New("empty ids")
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
if err := db.DB.Model(&models.Annotation{}).
|
||||||
|
Where("organization_id = ? AND id IN ?", orgID, ids).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count != int64(len(ids)) {
|
||||||
|
return errors.New("some annotations not in org")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -806,3 +806,184 @@ func DetachNodePoolLabel(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
response.NoContent(w)
|
response.NoContent(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListNodePoolAnnotations godoc
|
||||||
|
// @Summary List annotations attached to a node pool (org scoped)
|
||||||
|
// @Tags node-pools
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string true "Organization UUID"
|
||||||
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {array} annotationBrief
|
||||||
|
// @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/node-pools/{id}/annotations [get]
|
||||||
|
func ListNodePoolAnnotations(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
|
||||||
|
}
|
||||||
|
|
||||||
|
poolID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ng models.NodePool
|
||||||
|
if err := db.DB.Where("id = ? AND organization_id = ?", poolID, ac.OrganizationID).
|
||||||
|
Preload("Annotations").
|
||||||
|
First(&ng).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
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]annotationBrief, 0, len(ng.Annotations))
|
||||||
|
for _, a := range ng.Annotations {
|
||||||
|
out = append(out, annotationBrief{
|
||||||
|
ID: a.ID,
|
||||||
|
Name: a.Name,
|
||||||
|
Value: a.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ = response.JSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachNodePoolAnnotations godoc
|
||||||
|
// @Summary Attach annotations to a node pool (org scoped)
|
||||||
|
// @Tags node-pools
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string true "Organization UUID"
|
||||||
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
|
// @Param body body attachAnnotationsRequest true "Annotation IDs to attach"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 204 {string} string "No Content"
|
||||||
|
// @Failure 400 {string} string "invalid id / invalid annotation_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/node-pools/{id}/annotations [post]
|
||||||
|
func AttachNodePoolAnnotations(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
|
||||||
|
}
|
||||||
|
|
||||||
|
poolID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ng models.NodePool
|
||||||
|
if err := db.DB.Where("id = ? AND organization_id = ?", poolID, ac.OrganizationID).First(&ng).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 attachAnnotationsRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil || len(in.AnnotationIDs) == 0 {
|
||||||
|
http.Error(w, "invalid annotation_ids", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, err := parseUUIDs(in.AnnotationIDs)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid annotation_ids", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ensureAnnotationsBelongToOrg(ac.OrganizationID, ids); err != nil {
|
||||||
|
http.Error(w, "invalid annotation_ids for this organization", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var annotations []models.Annotation
|
||||||
|
if err := db.DB.Where("id IN ? AND organization_id = ?", ids, ac.OrganizationID).
|
||||||
|
Find(&annotations).Error; err != nil {
|
||||||
|
http.Error(w, "attach failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := db.DB.Model(&ng).Association("Annotations").Append(&annotations); err != nil {
|
||||||
|
http.Error(w, "attach failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetachNodePoolAnnotation godoc
|
||||||
|
// @Summary Detach one annotation from a node pool (org scoped)
|
||||||
|
// @Tags node-pools
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string true "Organization UUID"
|
||||||
|
// @Param id path string true "Node Pool ID (UUID)"
|
||||||
|
// @Param annotationId 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 404 {string} string "not found"
|
||||||
|
// @Failure 500 {string} string "detach failed"
|
||||||
|
// @Router /api/v1/node-pools/{id}/annotations/{annotationId} [delete]
|
||||||
|
func DetachNodePoolAnnotation(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
|
||||||
|
}
|
||||||
|
|
||||||
|
poolID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
annID, err := uuid.Parse(chi.URLParam(r, "annotationId"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ng models.NodePool
|
||||||
|
if err := db.DB.Where("id = ? AND organization_id = ?", poolID, ac.OrganizationID).First(&ng).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 ann models.Annotation
|
||||||
|
if err := db.DB.Where("id = ? AND organization_id = ?", annID, ac.OrganizationID).
|
||||||
|
First(&ann).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(&ng).Association("Annotations").Delete(&ann); err != nil {
|
||||||
|
http.Error(w, "detach failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.NoContent(w)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { z } from "zod"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import {
|
import {
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
@@ -8,10 +6,12 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Search,
|
Search,
|
||||||
|
ServerIcon,
|
||||||
Trash,
|
Trash,
|
||||||
UnlinkIcon,
|
UnlinkIcon,
|
||||||
ServerIcon,
|
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
import { api, ApiError } from "@/lib/api"
|
import { api, ApiError } from "@/lib/api"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
@@ -137,8 +137,7 @@ export const AnnotationsPage = () => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
const msg =
|
const msg = e instanceof ApiError ? e.message : "Failed to load annotations / node pools"
|
||||||
e instanceof ApiError ? e.message : "Failed to load annotations / node pools"
|
|
||||||
setErr(msg)
|
setErr(msg)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -149,14 +148,11 @@ export const AnnotationsPage = () => {
|
|||||||
setAttachedLoading(true)
|
setAttachedLoading(true)
|
||||||
setAttachedErr(null)
|
setAttachedErr(null)
|
||||||
try {
|
try {
|
||||||
const data = await api.get<NodePoolBrief[]>(
|
const data = await api.get<NodePoolBrief[]>(`/api/v1/annotations/${annotationId}/node_pools`)
|
||||||
`/api/v1/annotations/${annotationId}/node_pools`
|
|
||||||
)
|
|
||||||
setAttachedPools(data || [])
|
setAttachedPools(data || [])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
const msg =
|
const msg = e instanceof ApiError ? e.message : "Failed to load pools for annotation"
|
||||||
e instanceof ApiError ? e.message : "Failed to load pools for annotation"
|
|
||||||
setAttachedErr(msg)
|
setAttachedErr(msg)
|
||||||
} finally {
|
} finally {
|
||||||
setAttachedLoading(false)
|
setAttachedLoading(false)
|
||||||
@@ -302,7 +298,10 @@ export const AnnotationsPage = () => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="cluster-autoscaler.kubernetes.io/safe-to-evict" {...field} />
|
<Input
|
||||||
|
placeholder="cluster-autoscaler.kubernetes.io/safe-to-evict"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -329,7 +328,9 @@ export const AnnotationsPage = () => {
|
|||||||
<FormLabel>Initial node pools (optional)</FormLabel>
|
<FormLabel>Initial node pools (optional)</FormLabel>
|
||||||
<div className="max-h-56 space-y-2 overflow-auto rounded-xl border p-2">
|
<div className="max-h-56 space-y-2 overflow-auto rounded-xl border p-2">
|
||||||
{allPools.length === 0 && (
|
{allPools.length === 0 && (
|
||||||
<div className="text-muted-foreground p-2 text-sm">No node pools available</div>
|
<div className="text-muted-foreground p-2 text-sm">
|
||||||
|
No node pools available
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{allPools.map((p) => {
|
{allPools.map((p) => {
|
||||||
const checked = field.value?.includes(p.id) || false
|
const checked = field.value?.includes(p.id) || false
|
||||||
@@ -504,8 +505,7 @@ export const AnnotationsPage = () => {
|
|||||||
<DialogContent className="sm:max-w-2xl">
|
<DialogContent className="sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Manage node pools for{" "}
|
Manage node pools for <span className="font-mono">{managePoolsTarget?.name}</span>
|
||||||
<span className="font-mono">{managePoolsTarget?.name}</span>
|
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -559,7 +559,10 @@ export const AnnotationsPage = () => {
|
|||||||
{/* Attach pools */}
|
{/* Attach pools */}
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<Form {...attachPoolsForm}>
|
<Form {...attachPoolsForm}>
|
||||||
<form onSubmit={attachPoolsForm.handleSubmit(submitAttachPools)} className="space-y-3">
|
<form
|
||||||
|
onSubmit={attachPoolsForm.handleSubmit(submitAttachPools)}
|
||||||
|
className="space-y-3"
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={attachPoolsForm.control}
|
control={attachPoolsForm.control}
|
||||||
name="node_pool_ids"
|
name="node_pool_ids"
|
||||||
|
|||||||
@@ -82,6 +82,16 @@ type TaintWithPools = TaintBrief & {
|
|||||||
node_groups?: { id: string; name: string }[]
|
node_groups?: { id: string; name: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AnnotationBrief = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnnotationWithPools = AnnotationBrief & {
|
||||||
|
node_pools?: { id: string; name: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
type NodePool = {
|
type NodePool = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -90,7 +100,7 @@ type NodePool = {
|
|||||||
|
|
||||||
const CreatePoolSchema = z.object({
|
const CreatePoolSchema = z.object({
|
||||||
name: z.string().trim().min(1, "Name is required").max(120, "Max 120 chars"),
|
name: z.string().trim().min(1, "Name is required").max(120, "Max 120 chars"),
|
||||||
server_ids: z.array(z.uuid()).optional().default([]),
|
server_ids: z.array(z.string().uuid()).optional().default([]),
|
||||||
})
|
})
|
||||||
type CreatePoolInput = z.input<typeof CreatePoolSchema>
|
type CreatePoolInput = z.input<typeof CreatePoolSchema>
|
||||||
type CreatePoolValues = z.output<typeof CreatePoolSchema>
|
type CreatePoolValues = z.output<typeof CreatePoolSchema>
|
||||||
@@ -101,20 +111,25 @@ const UpdatePoolSchema = z.object({
|
|||||||
type UpdatePoolValues = z.output<typeof UpdatePoolSchema>
|
type UpdatePoolValues = z.output<typeof UpdatePoolSchema>
|
||||||
|
|
||||||
const AttachServersSchema = z.object({
|
const AttachServersSchema = z.object({
|
||||||
server_ids: z.array(z.uuid()).min(1, "Pick at least one server"),
|
server_ids: z.array(z.string().uuid()).min(1, "Pick at least one server"),
|
||||||
})
|
})
|
||||||
type AttachServersValues = z.output<typeof AttachServersSchema>
|
type AttachServersValues = z.output<typeof AttachServersSchema>
|
||||||
|
|
||||||
const AttachLabelsSchema = z.object({
|
const AttachLabelsSchema = z.object({
|
||||||
label_ids: z.array(z.uuid()).min(1, "Pick at least one label"),
|
label_ids: z.array(z.string().uuid()).min(1, "Pick at least one label"),
|
||||||
})
|
})
|
||||||
type AttachLabelsValues = z.output<typeof AttachLabelsSchema>
|
type AttachLabelsValues = z.output<typeof AttachLabelsSchema>
|
||||||
|
|
||||||
const AttachTaintsSchema = z.object({
|
const AttachTaintsSchema = z.object({
|
||||||
taint_ids: z.array(z.uuid()).min(1, "Pick at least one taint"),
|
taint_ids: z.array(z.string().uuid()).min(1, "Pick at least one taint"),
|
||||||
})
|
})
|
||||||
type AttachTaintsValues = z.output<typeof AttachTaintsSchema>
|
type AttachTaintsValues = z.output<typeof AttachTaintsSchema>
|
||||||
|
|
||||||
|
const AttachAnnotationsSchema = z.object({
|
||||||
|
annotation_ids: z.array(z.string().uuid()).min(1, "Pick at least one annotation"),
|
||||||
|
})
|
||||||
|
type AttachAnnotationsValues = z.output<typeof AttachAnnotationsSchema>
|
||||||
|
|
||||||
/* --------------------------------- Utils --------------------------------- */
|
/* --------------------------------- Utils --------------------------------- */
|
||||||
|
|
||||||
function StatusBadge({ status }: { status?: string }) {
|
function StatusBadge({ status }: { status?: string }) {
|
||||||
@@ -150,11 +165,14 @@ function labelKV(l: LabelBrief) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function taintText(t: TaintBrief) {
|
function taintText(t: TaintBrief) {
|
||||||
// Kubernetes-ish: key[=value]:effect
|
|
||||||
const kv = t.value ? `${t.key}=${t.value}` : t.key
|
const kv = t.value ? `${t.key}=${t.value}` : t.key
|
||||||
return `${kv}:${t.effect}`
|
return `${kv}:${t.effect}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function annotationKV(a: AnnotationBrief) {
|
||||||
|
return `${a.name}=${a.value}`
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------- Page ---------------------------------- */
|
/* --------------------------------- Page ---------------------------------- */
|
||||||
|
|
||||||
export const NodePoolPage = () => {
|
export const NodePoolPage = () => {
|
||||||
@@ -162,11 +180,10 @@ export const NodePoolPage = () => {
|
|||||||
const [pools, setPools] = useState<NodePool[]>([])
|
const [pools, setPools] = useState<NodePool[]>([])
|
||||||
const [allServers, setAllServers] = useState<ServerBrief[]>([])
|
const [allServers, setAllServers] = useState<ServerBrief[]>([])
|
||||||
|
|
||||||
// Labels
|
// Labels / Taints / Annotations
|
||||||
const [allLabels, setAllLabels] = useState<LabelWithPools[]>([])
|
const [allLabels, setAllLabels] = useState<LabelWithPools[]>([])
|
||||||
|
|
||||||
// Taints
|
|
||||||
const [allTaints, setAllTaints] = useState<TaintWithPools[]>([])
|
const [allTaints, setAllTaints] = useState<TaintWithPools[]>([])
|
||||||
|
const [allAnnotations, setAllAnnotations] = useState<AnnotationWithPools[]>([])
|
||||||
|
|
||||||
const [err, setErr] = useState<string | null>(null)
|
const [err, setErr] = useState<string | null>(null)
|
||||||
const [q, setQ] = useState("")
|
const [q, setQ] = useState("")
|
||||||
@@ -189,22 +206,30 @@ export const NodePoolPage = () => {
|
|||||||
const [taintsLoading, setTaintsLoading] = useState(false)
|
const [taintsLoading, setTaintsLoading] = useState(false)
|
||||||
const [taintsErr, setTaintsErr] = useState<string | null>(null)
|
const [taintsErr, setTaintsErr] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Annotations dialog state
|
||||||
|
const [manageAnnotationsTarget, setManageAnnotationsTarget] = useState<NodePool | null>(null)
|
||||||
|
const [attachedAnnotations, setAttachedAnnotations] = useState<AnnotationBrief[]>([])
|
||||||
|
const [annotationsLoading, setAnnotationsLoading] = useState(false)
|
||||||
|
const [annotationsErr, setAnnotationsErr] = useState<string | null>(null)
|
||||||
|
|
||||||
/* ------------------------------- Data Load ------------------------------ */
|
/* ------------------------------- Data Load ------------------------------ */
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setErr(null)
|
setErr(null)
|
||||||
try {
|
try {
|
||||||
const [poolsData, serversData, labelsData, taintsData] = await Promise.all([
|
const [poolsData, serversData, labelsData, taintsData, annotationsData] = await Promise.all([
|
||||||
api.get<NodePool[]>("/api/v1/node-pools?include=servers"),
|
api.get<NodePool[]>("/api/v1/node-pools?include=servers"),
|
||||||
api.get<ServerBrief[]>("/api/v1/servers"),
|
api.get<ServerBrief[]>("/api/v1/servers"),
|
||||||
api.get<LabelWithPools[]>("/api/v1/labels?include=node_pools"),
|
api.get<LabelWithPools[]>("/api/v1/labels?include=node_pools"),
|
||||||
api.get<TaintWithPools[]>("/api/v1/taints?include=node_pools"),
|
api.get<TaintWithPools[]>("/api/v1/taints?include=node_pools"),
|
||||||
|
api.get<AnnotationWithPools[]>("/api/v1/annotations?include=node_pools"),
|
||||||
])
|
])
|
||||||
setPools(poolsData || [])
|
setPools(poolsData || [])
|
||||||
setAllServers(serversData || [])
|
setAllServers(serversData || [])
|
||||||
setAllLabels(labelsData || [])
|
setAllLabels(labelsData || [])
|
||||||
setAllTaints(taintsData || [])
|
setAllTaints(taintsData || [])
|
||||||
|
setAllAnnotations(annotationsData || [])
|
||||||
|
|
||||||
if (manageTarget) {
|
if (manageTarget) {
|
||||||
const refreshed = (poolsData || []).find((p) => p.id === manageTarget.id) || null
|
const refreshed = (poolsData || []).find((p) => p.id === manageTarget.id) || null
|
||||||
@@ -220,10 +245,15 @@ export const NodePoolPage = () => {
|
|||||||
if (manageTaintsTarget) {
|
if (manageTaintsTarget) {
|
||||||
await loadAttachedTaints(manageTaintsTarget.id)
|
await loadAttachedTaints(manageTaintsTarget.id)
|
||||||
}
|
}
|
||||||
|
if (manageAnnotationsTarget) {
|
||||||
|
await loadAttachedAnnotations(manageAnnotationsTarget.id)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
const msg =
|
const msg =
|
||||||
e instanceof ApiError ? e.message : "Failed to load node pools / servers / labels / taints"
|
e instanceof ApiError
|
||||||
|
? e.message
|
||||||
|
: "Failed to load node pools / servers / labels / taints / annotations"
|
||||||
setErr(msg)
|
setErr(msg)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -260,14 +290,28 @@ export const NodePoolPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadAttachedAnnotations(poolId: string) {
|
||||||
|
setAnnotationsLoading(true)
|
||||||
|
setAnnotationsErr(null)
|
||||||
|
try {
|
||||||
|
const data = await api.get<AnnotationBrief[]>(`/api/v1/node-pools/${poolId}/annotations`)
|
||||||
|
setAttachedAnnotations(data || [])
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
const msg = e instanceof ApiError ? e.message : "Failed to load annotations for pool"
|
||||||
|
setAnnotationsErr(msg)
|
||||||
|
} finally {
|
||||||
|
setAnnotationsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadAll()
|
void loadAll()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
/* ---------------------------- Labels/Taints per Pool --------------------------- */
|
/* --------------------- Labels/Taints/Annotations per Pool --------------------- */
|
||||||
|
|
||||||
// poolId -> LabelBrief[]
|
|
||||||
const labelsByPool = useMemo(() => {
|
const labelsByPool = useMemo(() => {
|
||||||
const map = new Map<string, LabelBrief[]>()
|
const map = new Map<string, LabelBrief[]>()
|
||||||
for (const l of allLabels) {
|
for (const l of allLabels) {
|
||||||
@@ -280,7 +324,6 @@ export const NodePoolPage = () => {
|
|||||||
return map
|
return map
|
||||||
}, [allLabels])
|
}, [allLabels])
|
||||||
|
|
||||||
// poolId -> TaintBrief[]
|
|
||||||
const taintsByPool = useMemo(() => {
|
const taintsByPool = useMemo(() => {
|
||||||
const map = new Map<string, TaintBrief[]>()
|
const map = new Map<string, TaintBrief[]>()
|
||||||
for (const t of allTaints) {
|
for (const t of allTaints) {
|
||||||
@@ -293,6 +336,18 @@ export const NodePoolPage = () => {
|
|||||||
return map
|
return map
|
||||||
}, [allTaints])
|
}, [allTaints])
|
||||||
|
|
||||||
|
const annotationsByPool = useMemo(() => {
|
||||||
|
const map = new Map<string, AnnotationBrief[]>()
|
||||||
|
for (const a of allAnnotations) {
|
||||||
|
for (const ng of a.node_pools || []) {
|
||||||
|
const arr = map.get(ng.id) || []
|
||||||
|
arr.push({ id: a.id, name: a.name, value: a.value })
|
||||||
|
map.set(ng.id, arr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, [allAnnotations])
|
||||||
|
|
||||||
/* -------------------------------- Filters ------------------------------- */
|
/* -------------------------------- Filters ------------------------------- */
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
@@ -318,9 +373,21 @@ export const NodePoolPage = () => {
|
|||||||
kv.includes(needle)
|
kv.includes(needle)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
return p.name.toLowerCase().includes(needle) || serversMatch || labelsMatch || taintsMatch
|
const annotationsMatch = (annotationsByPool.get(p.id) || []).some(
|
||||||
|
(a) =>
|
||||||
|
a.name.toLowerCase().includes(needle) ||
|
||||||
|
(a.value || "").toLowerCase().includes(needle) ||
|
||||||
|
`${a.name}=${a.value}`.toLowerCase().includes(needle)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
p.name.toLowerCase().includes(needle) ||
|
||||||
|
serversMatch ||
|
||||||
|
labelsMatch ||
|
||||||
|
taintsMatch ||
|
||||||
|
annotationsMatch
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}, [pools, q, labelsByPool, taintsByPool])
|
}, [pools, q, labelsByPool, taintsByPool, annotationsByPool])
|
||||||
|
|
||||||
/* ------------------------------ Mutations ------------------------------- */
|
/* ------------------------------ Mutations ------------------------------- */
|
||||||
|
|
||||||
@@ -411,7 +478,7 @@ export const NodePoolPage = () => {
|
|||||||
})
|
})
|
||||||
attachLabelsForm.reset({ label_ids: [] })
|
attachLabelsForm.reset({ label_ids: [] })
|
||||||
await loadAttachedLabels(manageLabelsTarget.id)
|
await loadAttachedLabels(manageLabelsTarget.id)
|
||||||
await loadAll() // refresh badges in table
|
await loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detachLabel(labelId: string) {
|
async function detachLabel(labelId: string) {
|
||||||
@@ -419,7 +486,7 @@ export const NodePoolPage = () => {
|
|||||||
if (!confirm("Detach this label from the pool?")) return
|
if (!confirm("Detach this label from the pool?")) return
|
||||||
await api.delete(`/api/v1/node-pools/${manageLabelsTarget.id}/labels/${labelId}`)
|
await api.delete(`/api/v1/node-pools/${manageLabelsTarget.id}/labels/${labelId}`)
|
||||||
await loadAttachedLabels(manageLabelsTarget.id)
|
await loadAttachedLabels(manageLabelsTarget.id)
|
||||||
await loadAll() // refresh badges in table
|
await loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach / Detach Taints
|
// Attach / Detach Taints
|
||||||
@@ -441,7 +508,7 @@ export const NodePoolPage = () => {
|
|||||||
})
|
})
|
||||||
attachTaintsForm.reset({ taint_ids: [] })
|
attachTaintsForm.reset({ taint_ids: [] })
|
||||||
await loadAttachedTaints(manageTaintsTarget.id)
|
await loadAttachedTaints(manageTaintsTarget.id)
|
||||||
await loadAll() // refresh taint badges in table
|
await loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function detachTaint(taintId: string) {
|
async function detachTaint(taintId: string) {
|
||||||
@@ -452,6 +519,36 @@ export const NodePoolPage = () => {
|
|||||||
await loadAll()
|
await loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attach / Detach Annotations
|
||||||
|
const attachAnnotationsForm = useForm<AttachAnnotationsValues>({
|
||||||
|
resolver: zodResolver(AttachAnnotationsSchema),
|
||||||
|
defaultValues: { annotation_ids: [] },
|
||||||
|
})
|
||||||
|
|
||||||
|
function openManageAnnotations(p: NodePool) {
|
||||||
|
setManageAnnotationsTarget(p)
|
||||||
|
attachAnnotationsForm.reset({ annotation_ids: [] })
|
||||||
|
void loadAttachedAnnotations(p.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitAttachAnnotations = async (values: AttachAnnotationsValues) => {
|
||||||
|
if (!manageAnnotationsTarget) return
|
||||||
|
await api.post(`/api/v1/node-pools/${manageAnnotationsTarget.id}/annotations`, {
|
||||||
|
annotation_ids: values.annotation_ids,
|
||||||
|
})
|
||||||
|
attachAnnotationsForm.reset({ annotation_ids: [] })
|
||||||
|
await loadAttachedAnnotations(manageAnnotationsTarget.id)
|
||||||
|
await loadAll() // refresh badges in table
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detachAnnotation(annotationId: string) {
|
||||||
|
if (!manageAnnotationsTarget) return
|
||||||
|
if (!confirm("Detach this annotation from the pool?")) return
|
||||||
|
await api.delete(`/api/v1/node-pools/${manageAnnotationsTarget.id}/annotations/${annotationId}`)
|
||||||
|
await loadAttachedAnnotations(manageAnnotationsTarget.id)
|
||||||
|
await loadAll() // refresh badges in table
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------- Render -------------------------------- */
|
/* --------------------------------- Render -------------------------------- */
|
||||||
|
|
||||||
if (loading) return <div className="p-6">Loading node pools…</div>
|
if (loading) return <div className="p-6">Loading node pools…</div>
|
||||||
@@ -468,7 +565,7 @@ export const NodePoolPage = () => {
|
|||||||
<Input
|
<Input
|
||||||
value={q}
|
value={q}
|
||||||
onChange={(e) => setQ(e.target.value)}
|
onChange={(e) => setQ(e.target.value)}
|
||||||
placeholder="Search pools, servers, labels, taints…"
|
placeholder="Search pools, servers, labels, taints, annotations…"
|
||||||
className="w-72 pl-8"
|
className="w-72 pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -579,6 +676,7 @@ export const NodePoolPage = () => {
|
|||||||
{filtered.map((p) => {
|
{filtered.map((p) => {
|
||||||
const labels = labelsByPool.get(p.id) || []
|
const labels = labelsByPool.get(p.id) || []
|
||||||
const taints = taintsByPool.get(p.id) || []
|
const taints = taintsByPool.get(p.id) || []
|
||||||
|
const annotations = annotationsByPool.get(p.id) || []
|
||||||
return (
|
return (
|
||||||
<TableRow key={p.id}>
|
<TableRow key={p.id}>
|
||||||
<TableCell className="font-medium">{p.name}</TableCell>
|
<TableCell className="font-medium">{p.name}</TableCell>
|
||||||
@@ -612,15 +710,30 @@ export const NodePoolPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{/* Annotations placeholder */}
|
{/* Annotations cell */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-wrap gap-2">Annotations</div>
|
<div className="mb-2 flex flex-wrap gap-2">
|
||||||
<Button variant="outline" size="sm" disabled>
|
{annotations.slice(0, 6).map((a) => (
|
||||||
|
<Badge key={a.id} variant="outline" className="font-mono">
|
||||||
|
<Tag className="mr-1 h-3 w-3" />
|
||||||
|
{annotationKV(a)}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{annotations.length === 0 && (
|
||||||
|
<span className="text-muted-foreground">No annotations</span>
|
||||||
|
)}
|
||||||
|
{annotations.length > 6 && (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
+{annotations.length - 6} more
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => openManageAnnotations(p)}>
|
||||||
<LinkIcon className="mr-2 h-4 w-4" /> Manage Annotations
|
<LinkIcon className="mr-2 h-4 w-4" /> Manage Annotations
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{/* Labels cell with badges */}
|
{/* Labels cell */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="mb-2 flex flex-wrap gap-2">
|
<div className="mb-2 flex flex-wrap gap-2">
|
||||||
{labels.slice(0, 6).map((l) => (
|
{labels.slice(0, 6).map((l) => (
|
||||||
@@ -808,7 +921,6 @@ export const NodePoolPage = () => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Attach more servers</FormLabel>
|
<FormLabel>Attach more servers</FormLabel>
|
||||||
<div className="grid max-h-64 grid-cols-1 gap-2 overflow-auto rounded-xl border p-2 md:grid-cols-2">
|
<div className="grid max-h-64 grid-cols-1 gap-2 overflow-auto rounded-xl border p-2 md:grid-cols-2">
|
||||||
{/* options */}
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const attachedIds = new Set(
|
const attachedIds = new Set(
|
||||||
(manageTarget?.servers || []).map((s) => s.id)
|
(manageTarget?.servers || []).map((s) => s.id)
|
||||||
@@ -1122,6 +1234,140 @@ export const NodePoolPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Manage annotations dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={!!manageAnnotationsTarget}
|
||||||
|
onOpenChange={(o) => !o && setManageAnnotationsTarget(null)}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Manage annotations for{" "}
|
||||||
|
<span className="font-mono">{manageAnnotationsTarget?.name}</span>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Attached annotations list */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-sm font-medium">Attached annotations</div>
|
||||||
|
|
||||||
|
{annotationsLoading ? (
|
||||||
|
<div className="text-muted-foreground rounded-md border p-3 text-sm">Loading…</div>
|
||||||
|
) : annotationsErr ? (
|
||||||
|
<div className="rounded-md border p-3 text-sm text-red-500">{annotationsErr}</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-hidden rounded-xl border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Value</TableHead>
|
||||||
|
<TableHead className="w-[120px] text-right">Detach</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{attachedAnnotations.map((a) => (
|
||||||
|
<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="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => detachAnnotation(a.id)}
|
||||||
|
>
|
||||||
|
<UnlinkIcon className="mr-2 h-4 w-4" /> Detach
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{attachedAnnotations.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={3} className="text-muted-foreground py-8 text-center">
|
||||||
|
No annotations attached yet.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Attach annotations */}
|
||||||
|
<div className="pt-4">
|
||||||
|
<Form {...attachAnnotationsForm}>
|
||||||
|
<form
|
||||||
|
onSubmit={attachAnnotationsForm.handleSubmit(submitAttachAnnotations)}
|
||||||
|
className="space-y-3"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={attachAnnotationsForm.control}
|
||||||
|
name="annotation_ids"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Attach more annotations</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(attachedAnnotations.map((a) => a.id))
|
||||||
|
const attachable = (
|
||||||
|
allAnnotations as unknown as AnnotationBrief[]
|
||||||
|
).filter((a) => !attachedIds.has(a.id))
|
||||||
|
if (attachable.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-muted-foreground p-2 text-sm">
|
||||||
|
No more annotations available to attach
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return attachable.map((a) => {
|
||||||
|
const checked = field.value?.includes(a.id) || false
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={a.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(a.id)
|
||||||
|
else next.delete(a.id)
|
||||||
|
field.onChange(Array.from(next))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="leading-tight">
|
||||||
|
<div className="text-sm font-medium">{annotationKV(a)}</div>
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
{truncateMiddle(a.id, 8)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button type="submit" disabled={attachAnnotationsForm.formState.isSubmitting}>
|
||||||
|
<LinkIcon className="mr-2 h-4 w-4" />{" "}
|
||||||
|
{attachAnnotationsForm.formState.isSubmitting
|
||||||
|
? "Attaching…"
|
||||||
|
: "Attach selected"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user