mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 04:40:05 +01:00
labels page, api as well as integrating labels in the node pool page
This commit is contained in:
657
docs/docs.go
657
docs/docs.go
@@ -810,7 +810,7 @@ const docTemplate = `{
|
|||||||
"BearerAuth": []
|
"BearerAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Returns node labels for the organization in X-Org-ID. Filters: ` + "`" + `name` + "`" + `, ` + "`" + `value` + "`" + `, and ` + "`" + `q` + "`" + ` (name contains). Add ` + "`" + `include=node_pools` + "`" + ` to include linked node groups.",
|
"description": "Returns node labels for the organization in X-Org-ID. Filters: ` + "`" + `key` + "`" + `, ` + "`" + `value` + "`" + `, and ` + "`" + `q` + "`" + ` (key contains). Add ` + "`" + `include=node_pools` + "`" + ` to include linked node groups.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -831,8 +831,8 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Exact name",
|
"description": "Exact key",
|
||||||
"name": "name",
|
"name": "key",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -843,7 +843,7 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Name contains (case-insensitive)",
|
"description": "Key contains (case-insensitive)",
|
||||||
"name": "q",
|
"name": "q",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
@@ -1180,6 +1180,257 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/labels/{id}/node_pools": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Returns node pools attached to the label. Supports ` + "`" + `q` + "`" + ` (name contains, case-insensitive).",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "List node pools linked to a label (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Label 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/labels.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 label to one or more node pools in the same organization.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Attach label to node pools (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Label ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "IDs to attach",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/labels.addLabelToPoolRequest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional: node_pools",
|
||||||
|
"name": "include",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/labels.labelResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/labels/{id}/node_pools/{poolId}": {
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Unlinks the label from the specified node pool.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Detach label from a node pool (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Label ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Node Pool ID (UUID)",
|
||||||
|
"name": "poolId",
|
||||||
|
"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": {
|
"/api/v1/node-pools": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1545,6 +1796,242 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/node-pools/{id}/labels": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"node-pools"
|
||||||
|
],
|
||||||
|
"summary": "List labels 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.labelBrief"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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 labels 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": "Label IDs to attach",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/nodepools.attachLabelsRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "invalid id / invalid label_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}/labels/{labelId}": {
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"node-pools"
|
||||||
|
],
|
||||||
|
"summary": "Detach one label 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": "Label ID (UUID)",
|
||||||
|
"name": "labelId",
|
||||||
|
"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}/servers": {
|
"/api/v1/node-pools/{id}/servers": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -3062,7 +3549,7 @@ const docTemplate = `{
|
|||||||
"BearerAuth": []
|
"BearerAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Returns node taints for the organization in X-Org-ID. Filters: ` + "`" + `name` + "`" + `, ` + "`" + `value` + "`" + `, and ` + "`" + `q` + "`" + ` (name contains). Add ` + "`" + `include=node_groups` + "`" + ` to include linked node groups.",
|
"description": "Returns node taints for the organization in X-Org-ID. Filters: ` + "`" + `key` + "`" + `, ` + "`" + `value` + "`" + `, and ` + "`" + `q` + "`" + ` (key contains). Add ` + "`" + `include=node_groups` + "`" + ` to include linked node groups.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -3083,8 +3570,8 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Exact name",
|
"description": "Exact key",
|
||||||
"name": "name",
|
"name": "key",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -3095,7 +3582,7 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Name contains (case-insensitive)",
|
"description": "key contains (case-insensitive)",
|
||||||
"name": "q",
|
"name": "q",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
@@ -3433,6 +3920,87 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/taints/{id}/node_pools": {
|
"/api/v1/taints/{id}/node_pools": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Returns node pools attached to the taint. Supports ` + "`" + `q` + "`" + ` (name contains, case-insensitive).",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"taints"
|
||||||
|
],
|
||||||
|
"summary": "List node pools linked to a taint (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Taint 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/taints.nodePoolResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
@@ -3826,6 +4394,17 @@ const docTemplate = `{
|
|||||||
"updated_at": {}
|
"updated_at": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"labels.addLabelToPoolRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"node_pool_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"labels.createLabelRequest": {
|
"labels.createLabelRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -4001,6 +4580,17 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodepools.attachLabelsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"nodepools.attachServersRequest": {
|
"nodepools.attachServersRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -4038,6 +4628,20 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodepools.labelBrief": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"nodepools.nodePoolResponse": {
|
"nodepools.nodePoolResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -4325,6 +4929,43 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"taints.nodePoolResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"servers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/taints.serverBrief"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"taints.serverBrief": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hostname": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"taints.taintResponse": {
|
"taints.taintResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -806,7 +806,7 @@
|
|||||||
"BearerAuth": []
|
"BearerAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Returns node labels for the organization in X-Org-ID. Filters: `name`, `value`, and `q` (name contains). Add `include=node_pools` to include linked node groups.",
|
"description": "Returns node labels for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node groups.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -827,8 +827,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Exact name",
|
"description": "Exact key",
|
||||||
"name": "name",
|
"name": "key",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -839,7 +839,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Name contains (case-insensitive)",
|
"description": "Key contains (case-insensitive)",
|
||||||
"name": "q",
|
"name": "q",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
@@ -1176,6 +1176,257 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/labels/{id}/node_pools": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Returns node pools attached to the label. Supports `q` (name contains, case-insensitive).",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "List node pools linked to a label (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Label 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/labels.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 label to one or more node pools in the same organization.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Attach label to node pools (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Label ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "IDs to attach",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/labels.addLabelToPoolRequest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional: node_pools",
|
||||||
|
"name": "include",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/labels.labelResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/labels/{id}/node_pools/{poolId}": {
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Unlinks the label from the specified node pool.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Detach label from a node pool (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Label ID (UUID)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Node Pool ID (UUID)",
|
||||||
|
"name": "poolId",
|
||||||
|
"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": {
|
"/api/v1/node-pools": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1541,6 +1792,242 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/node-pools/{id}/labels": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"node-pools"
|
||||||
|
],
|
||||||
|
"summary": "List labels 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.labelBrief"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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 labels 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": "Label IDs to attach",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/nodepools.attachLabelsRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "invalid id / invalid label_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}/labels/{labelId}": {
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"node-pools"
|
||||||
|
],
|
||||||
|
"summary": "Detach one label 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": "Label ID (UUID)",
|
||||||
|
"name": "labelId",
|
||||||
|
"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}/servers": {
|
"/api/v1/node-pools/{id}/servers": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -3058,7 +3545,7 @@
|
|||||||
"BearerAuth": []
|
"BearerAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Returns node taints for the organization in X-Org-ID. Filters: `name`, `value`, and `q` (name contains). Add `include=node_groups` to include linked node groups.",
|
"description": "Returns node taints for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_groups` to include linked node groups.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -3079,8 +3566,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Exact name",
|
"description": "Exact key",
|
||||||
"name": "name",
|
"name": "key",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -3091,7 +3578,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Name contains (case-insensitive)",
|
"description": "key contains (case-insensitive)",
|
||||||
"name": "q",
|
"name": "q",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
@@ -3429,6 +3916,87 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/taints/{id}/node_pools": {
|
"/api/v1/taints/{id}/node_pools": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Returns node pools attached to the taint. Supports `q` (name contains, case-insensitive).",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"taints"
|
||||||
|
],
|
||||||
|
"summary": "List node pools linked to a taint (org scoped)",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization UUID",
|
||||||
|
"name": "X-Org-ID",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Taint 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/taints.nodePoolResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
@@ -3822,6 +4390,17 @@
|
|||||||
"updated_at": {}
|
"updated_at": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"labels.addLabelToPoolRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"node_pool_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"labels.createLabelRequest": {
|
"labels.createLabelRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -3997,6 +4576,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodepools.attachLabelsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"nodepools.attachServersRequest": {
|
"nodepools.attachServersRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -4034,6 +4624,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodepools.labelBrief": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"nodepools.nodePoolResponse": {
|
"nodepools.nodePoolResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -4321,6 +4925,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"taints.nodePoolResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"servers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/taints.serverBrief"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"taints.serverBrief": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hostname": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"taints.taintResponse": {
|
"taints.taintResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -152,6 +152,13 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
updated_at: {}
|
updated_at: {}
|
||||||
type: object
|
type: object
|
||||||
|
labels.addLabelToPoolRequest:
|
||||||
|
properties:
|
||||||
|
node_pool_ids:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
labels.createLabelRequest:
|
labels.createLabelRequest:
|
||||||
properties:
|
properties:
|
||||||
key:
|
key:
|
||||||
@@ -267,6 +274,13 @@ definitions:
|
|||||||
updated_at:
|
updated_at:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
nodepools.attachLabelsRequest:
|
||||||
|
properties:
|
||||||
|
label_ids:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
nodepools.attachServersRequest:
|
nodepools.attachServersRequest:
|
||||||
properties:
|
properties:
|
||||||
server_ids:
|
server_ids:
|
||||||
@@ -291,6 +305,15 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
type: object
|
type: object
|
||||||
|
nodepools.labelBrief:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
key:
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
nodepools.nodePoolResponse:
|
nodepools.nodePoolResponse:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
@@ -480,6 +503,30 @@ definitions:
|
|||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
taints.nodePoolResponse:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
servers:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/taints.serverBrief'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
taints.serverBrief:
|
||||||
|
properties:
|
||||||
|
hostname:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
ip:
|
||||||
|
type: string
|
||||||
|
role:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
taints.taintResponse:
|
taints.taintResponse:
|
||||||
properties:
|
properties:
|
||||||
effect:
|
effect:
|
||||||
@@ -1019,7 +1066,7 @@ paths:
|
|||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: 'Returns node labels for the organization in X-Org-ID. Filters:
|
description: 'Returns node labels for the organization in X-Org-ID. Filters:
|
||||||
`name`, `value`, and `q` (name contains). Add `include=node_pools` to include
|
`key`, `value`, and `q` (key contains). Add `include=node_pools` to include
|
||||||
linked node groups.'
|
linked node groups.'
|
||||||
parameters:
|
parameters:
|
||||||
- description: Organization UUID
|
- description: Organization UUID
|
||||||
@@ -1027,15 +1074,15 @@ paths:
|
|||||||
name: X-Org-ID
|
name: X-Org-ID
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
- description: Exact name
|
- description: Exact key
|
||||||
in: query
|
in: query
|
||||||
name: name
|
name: key
|
||||||
type: string
|
type: string
|
||||||
- description: Exact value
|
- description: Exact value
|
||||||
in: query
|
in: query
|
||||||
name: value
|
name: value
|
||||||
type: string
|
type: string
|
||||||
- description: Name contains (case-insensitive)
|
- description: Key contains (case-insensitive)
|
||||||
in: query
|
in: query
|
||||||
name: q
|
name: q
|
||||||
type: string
|
type: string
|
||||||
@@ -1261,6 +1308,171 @@ paths:
|
|||||||
summary: Update label (org scoped)
|
summary: Update label (org scoped)
|
||||||
tags:
|
tags:
|
||||||
- labels
|
- labels
|
||||||
|
/api/v1/labels/{id}/node_pools:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns node pools attached to the label. Supports `q` (name contains,
|
||||||
|
case-insensitive).
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Label 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/labels.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 a label (org scoped)
|
||||||
|
tags:
|
||||||
|
- labels
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Links the label 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: Label ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: IDs to attach
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/labels.addLabelToPoolRequest'
|
||||||
|
- description: 'Optional: node_pools'
|
||||||
|
in: query
|
||||||
|
name: include
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/labels.labelResponse'
|
||||||
|
"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 label to node pools (org scoped)
|
||||||
|
tags:
|
||||||
|
- labels
|
||||||
|
/api/v1/labels/{id}/node_pools/{poolId}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Unlinks the label from the specified node pool.
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Label ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Node Pool ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: poolId
|
||||||
|
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 label from a node pool (org scoped)
|
||||||
|
tags:
|
||||||
|
- labels
|
||||||
/api/v1/node-pools:
|
/api/v1/node-pools:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -1499,6 +1711,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}/labels:
|
||||||
|
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.labelBrief'
|
||||||
|
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 labels 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: Label IDs to attach
|
||||||
|
in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/nodepools.attachLabelsRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No Content
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"400":
|
||||||
|
description: invalid id / invalid label_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 labels to a node pool (org scoped)
|
||||||
|
tags:
|
||||||
|
- node-pools
|
||||||
|
/api/v1/node-pools/{id}/labels/{labelId}:
|
||||||
|
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: Label ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: labelId
|
||||||
|
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 label from a node pool (org scoped)
|
||||||
|
tags:
|
||||||
|
- node-pools
|
||||||
/api/v1/node-pools/{id}/servers:
|
/api/v1/node-pools/{id}/servers:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -2483,7 +2848,7 @@ paths:
|
|||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: 'Returns node taints for the organization in X-Org-ID. Filters:
|
description: 'Returns node taints for the organization in X-Org-ID. Filters:
|
||||||
`name`, `value`, and `q` (name contains). Add `include=node_groups` to include
|
`key`, `value`, and `q` (key contains). Add `include=node_groups` to include
|
||||||
linked node groups.'
|
linked node groups.'
|
||||||
parameters:
|
parameters:
|
||||||
- description: Organization UUID
|
- description: Organization UUID
|
||||||
@@ -2491,15 +2856,15 @@ paths:
|
|||||||
name: X-Org-ID
|
name: X-Org-ID
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
- description: Exact name
|
- description: Exact key
|
||||||
in: query
|
in: query
|
||||||
name: name
|
name: key
|
||||||
type: string
|
type: string
|
||||||
- description: Exact value
|
- description: Exact value
|
||||||
in: query
|
in: query
|
||||||
name: value
|
name: value
|
||||||
type: string
|
type: string
|
||||||
- description: Name contains (case-insensitive)
|
- description: key contains (case-insensitive)
|
||||||
in: query
|
in: query
|
||||||
name: q
|
name: q
|
||||||
type: string
|
type: string
|
||||||
@@ -2726,6 +3091,60 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- taints
|
- taints
|
||||||
/api/v1/taints/{id}/node_pools:
|
/api/v1/taints/{id}/node_pools:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns node pools attached to the taint. Supports `q` (name contains,
|
||||||
|
case-insensitive).
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Taint 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/taints.nodePoolResponse'
|
||||||
|
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 a taint (org scoped)
|
||||||
|
tags:
|
||||||
|
- taints
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ func RegisterRoutes(r chi.Router) {
|
|||||||
np.Get("/{id}/taints", nodepools.ListNodePoolTaints)
|
np.Get("/{id}/taints", nodepools.ListNodePoolTaints)
|
||||||
np.Post("/{id}/taints", nodepools.AttachNodePoolTaints)
|
np.Post("/{id}/taints", nodepools.AttachNodePoolTaints)
|
||||||
np.Delete("/{id}/taints/{taintId}", nodepools.DetachNodePoolTaint)
|
np.Delete("/{id}/taints/{taintId}", nodepools.DetachNodePoolTaint)
|
||||||
|
|
||||||
|
// labels
|
||||||
|
np.Get("/{id}/labels", nodepools.ListNodePoolLabels)
|
||||||
|
np.Post("/{id}/labels", nodepools.AttachNodePoolLabels)
|
||||||
|
np.Delete("/{id}/labels/{labelId}", nodepools.DetachNodePoolLabel)
|
||||||
})
|
})
|
||||||
|
|
||||||
v1.Route("/taints", func(t chi.Router) {
|
v1.Route("/taints", func(t chi.Router) {
|
||||||
@@ -110,6 +115,7 @@ func RegisterRoutes(r chi.Router) {
|
|||||||
t.Patch("/{id}", taints.UpdateTaint)
|
t.Patch("/{id}", taints.UpdateTaint)
|
||||||
t.Delete("/{id}", taints.DeleteTaint)
|
t.Delete("/{id}", taints.DeleteTaint)
|
||||||
t.Post("/{id}/node_pools", taints.AddTaintToNodePool)
|
t.Post("/{id}/node_pools", taints.AddTaintToNodePool)
|
||||||
|
t.Get("/{id}/node_pools", taints.ListNodePoolsWithTaint)
|
||||||
t.Delete("/{id}/node_pools/{poolId}", taints.RemoveTaintFromNodePool)
|
t.Delete("/{id}/node_pools/{poolId}", taints.RemoveTaintFromNodePool)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -120,6 +126,9 @@ func RegisterRoutes(r chi.Router) {
|
|||||||
l.Get("/{id}", labels.GetLabel)
|
l.Get("/{id}", labels.GetLabel)
|
||||||
l.Patch("/{id}", labels.UpdateLabel)
|
l.Patch("/{id}", labels.UpdateLabel)
|
||||||
l.Delete("/{id}", labels.DeleteLabel)
|
l.Delete("/{id}", labels.DeleteLabel)
|
||||||
|
l.Get("/{id}/node_pools", labels.ListNodePoolsWithLabel)
|
||||||
|
l.Post("/{id}/node_pools", labels.AddLabelToNodePool)
|
||||||
|
l.Delete("/{id}/node_pools/{poolId}", labels.RemoveLabelFromNodePool)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,3 +24,7 @@ type updateLabelRequest struct {
|
|||||||
Key *string `json:"key"`
|
Key *string `json:"key"`
|
||||||
Value *string `json:"value"`
|
Value *string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type addLabelToPoolRequest struct {
|
||||||
|
NodePoolIDs []string `json:"node_pool_ids"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ import (
|
|||||||
|
|
||||||
// ListLabels godoc
|
// ListLabels godoc
|
||||||
// @Summary List node labels (org scoped)
|
// @Summary List node labels (org scoped)
|
||||||
// @Description Returns node labels for the organization in X-Org-ID. Filters: `name`, `value`, and `q` (name contains). Add `include=node_pools` to include linked node groups.
|
// @Description Returns node labels for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node groups.
|
||||||
// @Tags labels
|
// @Tags labels
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string true "Organization UUID"
|
// @Param X-Org-ID header string true "Organization UUID"
|
||||||
// @Param name query string false "Exact name"
|
// @Param key query string false "Exact key"
|
||||||
// @Param value query string false "Exact value"
|
// @Param value query string false "Exact value"
|
||||||
// @Param q query string false "Name contains (case-insensitive)"
|
// @Param q query string false "Key contains (case-insensitive)"
|
||||||
// @Param include query string false "Optional: node_pools"
|
// @Param include query string false "Optional: node_pools"
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Success 200 {array} labelResponse
|
// @Success 200 {array} labelResponse
|
||||||
@@ -39,19 +39,25 @@ func ListLabels(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
q := db.DB.Where("organization_id = ?", ac.OrganizationID)
|
qb := db.DB.Where("organization_id = ?", ac.OrganizationID)
|
||||||
|
if key := strings.TrimSpace(r.URL.Query().Get("key")); key != "" {
|
||||||
|
qb = qb.Where(`"key" = ?`, key)
|
||||||
|
}
|
||||||
|
if val := strings.TrimSpace(r.URL.Query().Get("value")); val != "" {
|
||||||
|
qb = qb.Where(`"value" = ?`, val)
|
||||||
|
}
|
||||||
if needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
|
if needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
|
||||||
q = q.Where("name ILIKE ?", "%"+needle+"%")
|
qb = qb.Where("name ILIKE ?", "%"+needle+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
includePools := strings.EqualFold(strings.TrimSpace(r.URL.Query().Get("include")), "node_pools")
|
includePools := strings.EqualFold(strings.TrimSpace(r.URL.Query().Get("include")), "node_pools")
|
||||||
if includePools {
|
if includePools {
|
||||||
q.Preload("NodePools")
|
qb.Preload("NodePools")
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows []models.Label
|
var rows []models.Label
|
||||||
if err := q.Order("created_at DESC").Find(&rows).Error; err != nil {
|
if err := qb.Order("created_at DESC").Find(&rows).Error; err != nil {
|
||||||
http.Error(w, "failed to list taints", http.StatusInternalServerError)
|
http.Error(w, "failed to list labels", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,10 +270,225 @@ func DeleteLabel(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.DB.Where("id = ? AND organization_id = ?", id, ac.OrganizationID).Delete(&models.Taint{}).Error; err != nil {
|
if err := db.DB.Where("id = ? AND organization_id = ?", id, ac.OrganizationID).Delete(&models.Label{}).Error; err != nil {
|
||||||
http.Error(w, "delete failed", http.StatusInternalServerError)
|
http.Error(w, "delete failed", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.NoContent(w)
|
response.NoContent(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListNodePoolsWithLabel godoc
|
||||||
|
// @Summary List node pools linked to a label (org scoped)
|
||||||
|
// @Description Returns node pools attached to the label. Supports `q` (name contains, case-insensitive).
|
||||||
|
// @Tags labels
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string true "Organization UUID"
|
||||||
|
// @Param id path string true "Label 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/labels/{id}/node_pools [get]
|
||||||
|
func ListNodePoolsWithLabel(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
|
||||||
|
}
|
||||||
|
|
||||||
|
labelID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the label exists and belongs to this org
|
||||||
|
var l models.Label
|
||||||
|
if err := db.DB.Where("id = ? AND organization_id = ?", labelID, ac.OrganizationID).
|
||||||
|
First(&l).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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query pools through the join table defined on the model: many2many:node_labels
|
||||||
|
q := db.DB.Model(&models.NodePool{}).
|
||||||
|
Joins("JOIN node_labels nl ON nl.node_pool_id = node_pools.id").
|
||||||
|
Where("nl.label_id = ? AND node_pools.organization_id = ?", labelID, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLabelToNodePool godoc
|
||||||
|
// @Summary Attach label to node pools (org scoped)
|
||||||
|
// @Description Links the label to one or more node pools in the same organization.
|
||||||
|
// @Tags labels
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string true "Organization UUID"
|
||||||
|
// @Param id path string true "Label ID (UUID)"
|
||||||
|
// @Param body body addLabelToPoolRequest true "IDs to attach"
|
||||||
|
// @Param include query string false "Optional: node_pools"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {object} labelResponse
|
||||||
|
// @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/labels/{id}/node_pools [post]
|
||||||
|
func AddLabelToNodePool(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
|
||||||
|
}
|
||||||
|
|
||||||
|
labelID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var t models.Label
|
||||||
|
if err := db.DB.
|
||||||
|
Where("id = ? AND organization_id = ?", labelID, ac.OrganizationID).
|
||||||
|
First(&t).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(&t).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(&t, "id = ? AND organization_id = ?", labelID, ac.OrganizationID).Error; err != nil {
|
||||||
|
http.Error(w, "fetch failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = response.JSON(w, http.StatusOK, toResp(t, includePools))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveLabelFromNodePool godoc
|
||||||
|
// @Summary Detach label from a node pool (org scoped)
|
||||||
|
// @Description Unlinks the label from the specified node pool.
|
||||||
|
// @Tags labels
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string true "Organization UUID"
|
||||||
|
// @Param id path string true "Label 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/labels/{id}/node_pools/{poolId} [delete]
|
||||||
|
func RemoveLabelFromNodePool(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
|
||||||
|
}
|
||||||
|
|
||||||
|
labelID, 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 t models.Label
|
||||||
|
if err := db.DB.Where("id = ? AND organization_id = ?", labelID, ac.OrganizationID).
|
||||||
|
First(&t).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(&t).Association("NodePools").Delete(&p); err != nil {
|
||||||
|
http.Error(w, "detach failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.NoContent(w)
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,3 +41,23 @@ type taintBrief struct {
|
|||||||
type attachTaintsRequest struct {
|
type attachTaintsRequest struct {
|
||||||
TaintIDs []string `json:"taint_ids"`
|
TaintIDs []string `json:"taint_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type labelBrief struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type attachLabelsRequest struct {
|
||||||
|
LabelIDs []string `json:"label_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type annotationBrief struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type attachAnnotationsRequest struct {
|
||||||
|
AnnotationIDs []string `json:"annotation_ids"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,3 +70,35 @@ func ensureTaintsBelongToOrg(orgID uuid.UUID, ids []uuid.UUID) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureLabelsBelongToOrg(orgID uuid.UUID, ids []uuid.UUID) error {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var cnt int64
|
||||||
|
if err := db.DB.Model(&models.Label{}).
|
||||||
|
Where("organization_id = ? AND id IN ?", orgID, ids).
|
||||||
|
Count(&cnt).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cnt != int64(len(ids)) {
|
||||||
|
return errors.New("one or more labels not in organization")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureAnnotationsBelongToOrg(orgID uuid.UUID, ids []uuid.UUID) error {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var cnt int64
|
||||||
|
if err := db.DB.Model(&models.Annotation{}).
|
||||||
|
Where("organization_id = ? AND id IN ?", orgID, ids).
|
||||||
|
Count(&cnt).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cnt != int64(len(ids)) {
|
||||||
|
return errors.New("one or more annotations not in organization")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -624,3 +624,185 @@ func DetachNodePoolTaint(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
response.NoContent(w)
|
response.NoContent(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListNodePoolLabels godoc
|
||||||
|
// @Summary List labels 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} labelBrief
|
||||||
|
// @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}/labels [get]
|
||||||
|
func ListNodePoolLabels(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
|
||||||
|
}
|
||||||
|
|
||||||
|
ngID, 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 = ?", ngID, ac.OrganizationID).
|
||||||
|
Preload("Labels").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([]labelBrief, 0, len(ng.Labels))
|
||||||
|
for _, l := range ng.Labels {
|
||||||
|
out = append(out, labelBrief{
|
||||||
|
ID: l.ID,
|
||||||
|
Key: l.Key,
|
||||||
|
Value: l.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ = response.JSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachNodePoolLabels godoc
|
||||||
|
// @Summary Attach labels 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 attachLabelsRequest true "Label IDs to attach"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 204 {string} string "No Content"
|
||||||
|
// @Failure 400 {string} string "invalid id / invalid label_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}/labels [post]
|
||||||
|
func AttachNodePoolLabels(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
|
||||||
|
}
|
||||||
|
|
||||||
|
ngID, 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 = ?", ngID, 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 body attachLabelsRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.LabelIDs) == 0 {
|
||||||
|
http.Error(w, "invalid label_ids", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, err := parseUUIDs(body.LabelIDs) // already used in this package for servers/taints
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid label_ids", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ensureLabelsBelongToOrg(ac.OrganizationID, ids); err != nil {
|
||||||
|
http.Error(w, "invalid label_ids for this organization", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var labels []models.Label
|
||||||
|
if err := db.DB.Where("id IN ? AND organization_id = ?", ids, ac.OrganizationID).
|
||||||
|
Find(&labels).Error; err != nil {
|
||||||
|
http.Error(w, "attach failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := db.DB.Model(&ng).Association("Labels").Append(&labels); err != nil {
|
||||||
|
http.Error(w, "attach failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetachNodePoolLabel godoc
|
||||||
|
// @Summary Detach one label 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 labelId path string true "Label 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}/labels/{labelId} [delete]
|
||||||
|
func DetachNodePoolLabel(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
|
||||||
|
}
|
||||||
|
|
||||||
|
ngID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lid, err := uuid.Parse(chi.URLParam(r, "labelId"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ng models.NodePool
|
||||||
|
if err := db.DB.Where("id = ? AND organization_id = ?", ngID, 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 l models.Label
|
||||||
|
if err := db.DB.Where("id = ? AND organization_id = ?", lid, ac.OrganizationID).
|
||||||
|
First(&l).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("Labels").Delete(&l); err != nil {
|
||||||
|
http.Error(w, "detach failed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.NoContent(w)
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,3 +31,17 @@ type updateTaintRequest struct {
|
|||||||
type addTaintToPoolRequest struct {
|
type addTaintToPoolRequest struct {
|
||||||
NodePoolIDs []string `json:"node_pool_ids"`
|
NodePoolIDs []string `json:"node_pool_ids"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type nodePoolResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Servers []serverBrief `json:"servers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverBrief struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ import (
|
|||||||
|
|
||||||
// ListTaints godoc
|
// ListTaints godoc
|
||||||
// @Summary List node taints (org scoped)
|
// @Summary List node taints (org scoped)
|
||||||
// @Description Returns node taints for the organization in X-Org-ID. Filters: `name`, `value`, and `q` (name contains). Add `include=node_groups` to include linked node groups.
|
// @Description Returns node taints for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_groups` to include linked node groups.
|
||||||
// @Tags taints
|
// @Tags taints
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param X-Org-ID header string true "Organization UUID"
|
// @Param X-Org-ID header string true "Organization UUID"
|
||||||
// @Param name query string false "Exact name"
|
// @Param key query string false "Exact key"
|
||||||
// @Param value query string false "Exact value"
|
// @Param value query string false "Exact value"
|
||||||
// @Param q query string false "Name contains (case-insensitive)"
|
// @Param q query string false "key contains (case-insensitive)"
|
||||||
// @Param include query string false "Optional: node_pools"
|
// @Param include query string false "Optional: node_pools"
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Success 200 {array} taintResponse
|
// @Success 200 {array} taintResponse
|
||||||
@@ -40,6 +40,12 @@ func ListTaints(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := db.DB.Where("organization_id = ?", ac.OrganizationID)
|
q := db.DB.Where("organization_id = ?", ac.OrganizationID)
|
||||||
|
if key := strings.TrimSpace(r.URL.Query().Get("key")); key != "" {
|
||||||
|
q = q.Where("key = ?", key)
|
||||||
|
}
|
||||||
|
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 != "" {
|
if needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
|
||||||
q = q.Where("name ILIKE ?", "%"+needle+"%")
|
q = q.Where("name ILIKE ?", "%"+needle+"%")
|
||||||
}
|
}
|
||||||
@@ -423,3 +429,67 @@ func RemoveTaintFromNodePool(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
response.NoContent(w)
|
response.NoContent(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListNodePoolsWithTaint godoc
|
||||||
|
// @Summary List node pools linked to a taint (org scoped)
|
||||||
|
// @Description Returns node pools attached to the taint. Supports `q` (name contains, case-insensitive).
|
||||||
|
// @Tags taints
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string true "Organization UUID"
|
||||||
|
// @Param id path string true "Taint ID (UUID)"
|
||||||
|
// @Param q query string false "Name contains (case-insensitive)"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Success 200 {array} nodePoolResponse
|
||||||
|
// @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/taints/{id}/node_pools [get]
|
||||||
|
func ListNodePoolsWithTaint(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
|
||||||
|
}
|
||||||
|
|
||||||
|
taintID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the taint exists and belongs to this org
|
||||||
|
var t models.Taint
|
||||||
|
if err := db.DB.Where("id = ? AND organization_id = ?", taintID, ac.OrganizationID).
|
||||||
|
First(&t).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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build query for pools linked via join table
|
||||||
|
q := db.DB.Model(&models.NodePool{}).
|
||||||
|
Joins("JOIN taint_node_pools tnp ON tnp.node_pool_id = node_pools.id").
|
||||||
|
Where("tnp.taint_id = ? AND node_pools.organization_id = ?", taintID, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you have a serializer like toNodePoolResp, use it; otherwise return models with JSON tags.
|
||||||
|
//out := make([]nodePoolResponse, 0, len(pools))
|
||||||
|
//for _, p := range pools { out = append(out, toNodePoolResp(p)) }
|
||||||
|
|
||||||
|
_ = response.JSON(w, http.StatusOK, pools)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { useEffect, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { PencilIcon, Plus, TrashIcon } from "lucide-react"
|
import { LinkIcon, PencilIcon, Plus, TrashIcon, UnlinkIcon } from "lucide-react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { api } from "@/lib/api.ts"
|
import { api, ApiError } from "@/lib/api.ts"
|
||||||
|
import { Badge } from "@/components/ui/badge.tsx"
|
||||||
import { Button } from "@/components/ui/button.tsx"
|
import { Button } from "@/components/ui/button.tsx"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox.tsx"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -32,34 +34,89 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table.tsx"
|
} from "@/components/ui/table.tsx"
|
||||||
|
|
||||||
|
type NodePoolBrief = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
type Label = {
|
type Label = {
|
||||||
id: string
|
id: string
|
||||||
key: string
|
key: string
|
||||||
value: string
|
value: string
|
||||||
|
node_pools?: NodePoolBrief[] // normalized from API's "node_groups"
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateLabelSchema = z.object({
|
const CreateLabelSchema = z.object({
|
||||||
key: z.string().min(2),
|
key: z.string().trim().min(2, "Key is too short"),
|
||||||
value: z.string().min(2),
|
value: z.string().trim().min(2, "Value is too short"),
|
||||||
})
|
})
|
||||||
type CreateLabelValues = z.infer<typeof CreateLabelSchema>
|
type CreateLabelValues = z.infer<typeof CreateLabelSchema>
|
||||||
|
|
||||||
|
const UpdateLabelSchema = z
|
||||||
|
.object({
|
||||||
|
key: z.string().trim().min(2, "Key is too short").optional(),
|
||||||
|
value: z.string().trim().min(2, "Value is too short").optional(),
|
||||||
|
})
|
||||||
|
.refine((v) => v.key !== undefined || v.value !== undefined, {
|
||||||
|
message: "Provide a new key or value",
|
||||||
|
path: ["key"],
|
||||||
|
})
|
||||||
|
type UpdateLabelValues = z.infer<typeof UpdateLabelSchema>
|
||||||
|
|
||||||
|
const AttachPoolsSchema = z.object({
|
||||||
|
node_pool_ids: z.array(z.string().uuid()).min(1, "Pick at least one node pool"),
|
||||||
|
})
|
||||||
|
type AttachPoolsValues = z.infer<typeof AttachPoolsSchema>
|
||||||
|
|
||||||
|
function truncateMiddle(str: string, keep = 8) {
|
||||||
|
if (!str || str.length <= keep * 2 + 3) return str
|
||||||
|
return `${str.slice(0, keep)}…${str.slice(-keep)}`
|
||||||
|
}
|
||||||
|
|
||||||
export const LabelsPage = () => {
|
export const LabelsPage = () => {
|
||||||
const [labels, setLabels] = useState<Label[]>([])
|
const [labels, setLabels] = useState<Label[]>([])
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [allPools, setAllPools] = useState<NodePoolBrief[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
const [err, setErr] = useState<string | null>(null)
|
const [err, setErr] = useState<string | null>(null)
|
||||||
|
|
||||||
const [createOpen, setCreateOpen] = useState(false)
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
|
const [editOpen, setEditOpen] = useState(false)
|
||||||
|
const [editTarget, setEditTarget] = useState<Label | null>(null)
|
||||||
|
|
||||||
|
const [manageTarget, setManageTarget] = useState<Label | null>(null)
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setErr(null)
|
setErr(null)
|
||||||
try {
|
try {
|
||||||
const labelData = await api.get<Label[]>("/api/v1/labels")
|
// include=node_pools -> backend returns "node_groups" field; normalize it.
|
||||||
console.log(JSON.stringify(labelData))
|
const [labelsRaw, poolsRaw] = await Promise.all([
|
||||||
setLabels(labelData)
|
api.get<any[]>("/api/v1/labels?include=node_pools"),
|
||||||
|
api.get<NodePoolBrief[]>("/api/v1/node-pools"),
|
||||||
|
])
|
||||||
|
|
||||||
|
const normalized: Label[] = (labelsRaw || []).map((l) => ({
|
||||||
|
id: l.id,
|
||||||
|
key: l.key,
|
||||||
|
value: l.value,
|
||||||
|
node_pools: l.node_pools ?? l.node_groups ?? [], // support either
|
||||||
|
}))
|
||||||
|
|
||||||
|
setLabels(normalized)
|
||||||
|
setAllPools(poolsRaw || [])
|
||||||
|
|
||||||
|
if (manageTarget) {
|
||||||
|
const refreshed = normalized.find((x) => x.id === manageTarget.id) || null
|
||||||
|
setManageTarget(refreshed)
|
||||||
|
}
|
||||||
|
if (editTarget) {
|
||||||
|
const refreshed = normalized.find((x) => x.id === editTarget.id) || null
|
||||||
|
setEditTarget(refreshed)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
const msg = e instanceof ApiError ? e.message : "Failed to load labels/pools"
|
||||||
|
setErr(msg)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -69,26 +126,86 @@ export const LabelsPage = () => {
|
|||||||
void loadAll()
|
void loadAll()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// CREATE
|
||||||
const createForm = useForm<CreateLabelValues>({
|
const createForm = useForm<CreateLabelValues>({
|
||||||
resolver: zodResolver(CreateLabelSchema),
|
resolver: zodResolver(CreateLabelSchema),
|
||||||
defaultValues: {
|
defaultValues: { key: "", value: "" },
|
||||||
key: "",
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const submitCreate = async (values: CreateLabelValues) => {
|
const submitCreate = async (values: CreateLabelValues) => {
|
||||||
const payload: Record<string, any> = {
|
await api.post<Label>("/api/v1/labels", {
|
||||||
key: values.key,
|
key: values.key.trim(),
|
||||||
value: values.value,
|
value: values.value.trim(),
|
||||||
}
|
})
|
||||||
await api.post<Label>("/api/v1/labels", payload)
|
|
||||||
setCreateOpen(false)
|
setCreateOpen(false)
|
||||||
createForm.reset()
|
createForm.reset()
|
||||||
await loadAll()
|
await loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return <div className="p-6">Loading servers…</div>
|
// EDIT
|
||||||
|
const editForm = useForm<UpdateLabelValues>({
|
||||||
|
resolver: zodResolver(UpdateLabelSchema),
|
||||||
|
defaultValues: { key: undefined, value: undefined },
|
||||||
|
})
|
||||||
|
|
||||||
|
function openEdit(l: Label) {
|
||||||
|
setEditTarget(l)
|
||||||
|
editForm.reset({ key: undefined, value: undefined })
|
||||||
|
setEditOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitEdit = async (values: UpdateLabelValues) => {
|
||||||
|
if (!editTarget) return
|
||||||
|
const payload: Partial<Label> = {}
|
||||||
|
if (values.key !== undefined) payload.key = values.key.trim()
|
||||||
|
if (values.value !== undefined) payload.value = values.value.trim()
|
||||||
|
await api.patch<Label>(`/api/v1/labels/${editTarget.id}`, payload)
|
||||||
|
setEditOpen(false)
|
||||||
|
setEditTarget(null)
|
||||||
|
await loadAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
async function deleteLabel(id: string) {
|
||||||
|
if (!confirm("Delete this label? This cannot be undone.")) return
|
||||||
|
await api.delete(`/api/v1/labels/${id}`)
|
||||||
|
await loadAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MANAGE NODE POOLS (attach/detach)
|
||||||
|
const attachForm = useForm<AttachPoolsValues>({
|
||||||
|
resolver: zodResolver(AttachPoolsSchema),
|
||||||
|
defaultValues: { node_pool_ids: [] },
|
||||||
|
})
|
||||||
|
|
||||||
|
function openManage(l: Label) {
|
||||||
|
setManageTarget(l)
|
||||||
|
attachForm.reset({ node_pool_ids: [] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitAttachPools = async (values: AttachPoolsValues) => {
|
||||||
|
if (!manageTarget) return
|
||||||
|
await api.post<Label>(`/api/v1/labels/${manageTarget.id}/node_pools`, {
|
||||||
|
node_pool_ids: values.node_pool_ids,
|
||||||
|
})
|
||||||
|
attachForm.reset({ node_pool_ids: [] })
|
||||||
|
await loadAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detachPool(poolId: string) {
|
||||||
|
if (!manageTarget) return
|
||||||
|
if (!confirm("Detach this label from the selected node pool?")) return
|
||||||
|
await api.delete(`/api/v1/labels/${manageTarget.id}/node_pools/${poolId}`)
|
||||||
|
await loadAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachablePools = useMemo(() => {
|
||||||
|
if (!manageTarget) return [] as NodePoolBrief[]
|
||||||
|
const attached = new Set((manageTarget.node_pools || []).map((p) => p.id))
|
||||||
|
return allPools.filter((p) => !attached.has(p.id))
|
||||||
|
}, [manageTarget, allPools])
|
||||||
|
|
||||||
|
if (loading) return <div className="p-6">Loading labels…</div>
|
||||||
if (err) return <div className="p-6 text-red-500">{err}</div>
|
if (err) return <div className="p-6 text-red-500">{err}</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -158,22 +275,44 @@ export const LabelsPage = () => {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Key</TableHead>
|
<TableHead>Key</TableHead>
|
||||||
<TableHead>Values</TableHead>
|
<TableHead>Value</TableHead>
|
||||||
<TableHead className="w-[180px] text-right">Actions</TableHead>
|
<TableHead>Node Pools</TableHead>
|
||||||
|
<TableHead className="w-[260px] text-right">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{labels.map((l) => (
|
{labels.map((l) => (
|
||||||
<TableRow key={l.id}>
|
<TableRow key={l.id}>
|
||||||
<TableCell>{l.key}</TableCell>
|
<TableCell className="font-medium">{l.key}</TableCell>
|
||||||
<TableCell>{l.value}</TableCell>
|
<TableCell>{l.value}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(l.node_pools || []).slice(0, 6).map((p) => (
|
||||||
|
<Badge key={p.id} variant="secondary">
|
||||||
|
{p.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{(l.node_pools || []).length === 0 && (
|
||||||
|
<span className="text-muted-foreground">No pools</span>
|
||||||
|
)}
|
||||||
|
{(l.node_pools || []).length > 6 && (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
+{(l.node_pools || []).length - 6} more
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm" onClick={() => openManage(l)}>
|
||||||
|
<LinkIcon className="mr-2 h-4 w-4" />
|
||||||
|
Manage node pools
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => openEdit(l)}>
|
||||||
<PencilIcon className="mr-2 h-4 w-4" />
|
<PencilIcon className="mr-2 h-4 w-4" />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="destructive" size="sm">
|
<Button variant="destructive" size="sm" onClick={() => deleteLabel(l.id)}>
|
||||||
<TrashIcon className="mr-2 h-4 w-4" />
|
<TrashIcon className="mr-2 h-4 w-4" />
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
@@ -181,10 +320,181 @@ export const LabelsPage = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{labels.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-muted-foreground py-10 text-center">
|
||||||
|
No labels yet.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Edit label */}
|
||||||
|
<Dialog open={editOpen} onOpenChange={(o) => !o && setEditOpen(false)}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Edit Label{" "}
|
||||||
|
{editTarget ? (
|
||||||
|
<span className="text-muted-foreground ml-2 font-mono text-sm">
|
||||||
|
({editTarget.key} = {editTarget.value})
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form {...editForm}>
|
||||||
|
<form onSubmit={editForm.handleSubmit(submitEdit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={editForm.control}
|
||||||
|
name="key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>New key (optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={editTarget?.key || "e.g. app"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={editForm.control}
|
||||||
|
name="value"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>New value (optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={editTarget?.value || "e.g. GlueOps"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => setEditOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={editForm.formState.isSubmitting}>
|
||||||
|
{editForm.formState.isSubmitting ? "Saving…" : "Save changes"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Manage node pools for a label */}
|
||||||
|
<Dialog open={!!manageTarget} onOpenChange={(o) => !o && setManageTarget(null)}>
|
||||||
|
<DialogContent className="sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Manage node pools for{" "}
|
||||||
|
<span className="font-mono">
|
||||||
|
{manageTarget ? `${manageTarget.key}=${manageTarget.value}` : ""}
|
||||||
|
</span>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Attached pools */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-sm font-medium">Attached node pools</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>
|
||||||
|
{(manageTarget?.node_pools || []).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>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{(manageTarget?.node_pools || []).length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={2} className="text-muted-foreground py-8 text-center">
|
||||||
|
No pools attached yet.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Attach more */}
|
||||||
|
<div className="pt-4">
|
||||||
|
<Form {...attachForm}>
|
||||||
|
<form onSubmit={attachForm.handleSubmit(submitAttachPools)} className="space-y-3">
|
||||||
|
<FormField
|
||||||
|
control={attachForm.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">
|
||||||
|
{attachablePools.length === 0 && (
|
||||||
|
<div className="text-muted-foreground p-2 text-sm">
|
||||||
|
No more node pools available to attach
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{attachablePools.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={attachForm.formState.isSubmitting}>
|
||||||
|
<LinkIcon className="mr-2 h-4 w-4" />
|
||||||
|
{attachForm.formState.isSubmitting ? "Attaching…" : "Attach selected"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
Search,
|
Search,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
|
Tag,
|
||||||
Trash,
|
Trash,
|
||||||
UnlinkIcon,
|
UnlinkIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
@@ -49,6 +50,8 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table.tsx"
|
} from "@/components/ui/table.tsx"
|
||||||
|
|
||||||
|
/* ----------------------------- Types & Schemas ---------------------------- */
|
||||||
|
|
||||||
type ServerBrief = {
|
type ServerBrief = {
|
||||||
id: string
|
id: string
|
||||||
hostname?: string | null
|
hostname?: string | null
|
||||||
@@ -58,6 +61,16 @@ type ServerBrief = {
|
|||||||
status?: string
|
status?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LabelBrief = {
|
||||||
|
id: string
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LabelWithPools = LabelBrief & {
|
||||||
|
node_groups?: { id: string; name: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
type NodePool = {
|
type NodePool = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -66,7 +79,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>
|
||||||
@@ -77,10 +90,17 @@ 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({
|
||||||
|
label_ids: z.array(z.string().uuid()).min(1, "Pick at least one label"),
|
||||||
|
})
|
||||||
|
type AttachLabelsValues = z.output<typeof AttachLabelsSchema>
|
||||||
|
|
||||||
|
/* --------------------------------- Utils --------------------------------- */
|
||||||
|
|
||||||
function StatusBadge({ status }: { status?: string }) {
|
function StatusBadge({ status }: { status?: string }) {
|
||||||
const v =
|
const v =
|
||||||
status === "ready"
|
status === "ready"
|
||||||
@@ -109,28 +129,49 @@ function serverLabel(s: ServerBrief) {
|
|||||||
return `${name}${role}`
|
return `${name}${role}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function labelKV(l: LabelBrief) {
|
||||||
|
return `${l.key}=${l.value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------------- Page ---------------------------------- */
|
||||||
|
|
||||||
export const NodePoolPage = () => {
|
export const NodePoolPage = () => {
|
||||||
const [loading, setLoading] = useState<boolean>(true)
|
const [loading, setLoading] = useState<boolean>(true)
|
||||||
const [pools, setPools] = useState<NodePool[]>([])
|
const [pools, setPools] = useState<NodePool[]>([])
|
||||||
const [allServers, setAllServers] = useState<ServerBrief[]>([])
|
const [allServers, setAllServers] = useState<ServerBrief[]>([])
|
||||||
const [err, setErr] = useState<string | null>(null)
|
|
||||||
|
|
||||||
|
// Pull labels with include=node_pools so we can map them to pools
|
||||||
|
const [allLabels, setAllLabels] = useState<LabelWithPools[]>([])
|
||||||
|
|
||||||
|
const [err, setErr] = useState<string | null>(null)
|
||||||
const [q, setQ] = useState("")
|
const [q, setQ] = useState("")
|
||||||
|
|
||||||
const [createOpen, setCreateOpen] = useState(false)
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
const [editTarget, setEditTarget] = useState<NodePool | null>(null)
|
const [editTarget, setEditTarget] = useState<NodePool | null>(null)
|
||||||
|
|
||||||
|
// Servers dialog state
|
||||||
const [manageTarget, setManageTarget] = useState<NodePool | null>(null)
|
const [manageTarget, setManageTarget] = useState<NodePool | null>(null)
|
||||||
|
|
||||||
|
// Labels dialog state
|
||||||
|
const [manageLabelsTarget, setManageLabelsTarget] = useState<NodePool | null>(null)
|
||||||
|
const [attachedLabels, setAttachedLabels] = useState<LabelBrief[]>([])
|
||||||
|
const [labelsLoading, setLabelsLoading] = useState(false)
|
||||||
|
const [labelsErr, setLabelsErr] = useState<string | null>(null)
|
||||||
|
|
||||||
|
/* ------------------------------- Data Load ------------------------------ */
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setErr(null)
|
setErr(null)
|
||||||
try {
|
try {
|
||||||
const [poolsData, serversData] = await Promise.all([
|
const [poolsData, serversData, labelsData] = 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"),
|
||||||
])
|
])
|
||||||
setPools(poolsData || [])
|
setPools(poolsData || [])
|
||||||
setAllServers(serversData || [])
|
setAllServers(serversData || [])
|
||||||
|
setAllLabels(labelsData || [])
|
||||||
|
|
||||||
if (manageTarget) {
|
if (manageTarget) {
|
||||||
const refreshed = (poolsData || []).find((p) => p.id === manageTarget.id) || null
|
const refreshed = (poolsData || []).find((p) => p.id === manageTarget.id) || null
|
||||||
@@ -140,19 +181,55 @@ export const NodePoolPage = () => {
|
|||||||
const refreshed = (poolsData || []).find((p) => p.id === editTarget.id) || null
|
const refreshed = (poolsData || []).find((p) => p.id === editTarget.id) || null
|
||||||
setEditTarget(refreshed)
|
setEditTarget(refreshed)
|
||||||
}
|
}
|
||||||
|
if (manageLabelsTarget) {
|
||||||
|
await loadAttachedLabels(manageLabelsTarget.id)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
const msg = e instanceof ApiError ? e.message : "Failed to load node pools or servers"
|
const msg = e instanceof ApiError ? e.message : "Failed to load node pools / servers / labels"
|
||||||
setErr(msg)
|
setErr(msg)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadAttachedLabels(poolId: string) {
|
||||||
|
setLabelsLoading(true)
|
||||||
|
setLabelsErr(null)
|
||||||
|
try {
|
||||||
|
const data = await api.get<LabelBrief[]>(`/api/v1/node-pools/${poolId}/labels`)
|
||||||
|
setAttachedLabels(data || [])
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
const msg = e instanceof ApiError ? e.message : "Failed to load labels for pool"
|
||||||
|
setLabelsErr(msg)
|
||||||
|
} finally {
|
||||||
|
setLabelsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadAll()
|
void loadAll()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
/* ---------------------------- Labels per Pool --------------------------- */
|
||||||
|
|
||||||
|
// Build a quick lookup: poolId -> LabelBrief[]
|
||||||
|
const labelsByPool = useMemo(() => {
|
||||||
|
const map = new Map<string, LabelBrief[]>()
|
||||||
|
for (const l of allLabels) {
|
||||||
|
for (const ng of l.node_groups || []) {
|
||||||
|
const arr = map.get(ng.id) || []
|
||||||
|
arr.push({ id: l.id, key: l.key, value: l.value })
|
||||||
|
map.set(ng.id, arr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, [allLabels])
|
||||||
|
|
||||||
|
/* -------------------------------- Filters ------------------------------- */
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
const needle = q.trim().toLowerCase()
|
const needle = q.trim().toLowerCase()
|
||||||
if (!needle) return pools
|
if (!needle) return pools
|
||||||
@@ -164,9 +241,15 @@ export const NodePoolPage = () => {
|
|||||||
(s.hostname || "").toLowerCase().includes(needle) ||
|
(s.hostname || "").toLowerCase().includes(needle) ||
|
||||||
(s.ip || s.ip_address || "").toLowerCase().includes(needle) ||
|
(s.ip || s.ip_address || "").toLowerCase().includes(needle) ||
|
||||||
(s.role || "").toLowerCase().includes(needle)
|
(s.role || "").toLowerCase().includes(needle)
|
||||||
|
) ||
|
||||||
|
(labelsByPool.get(p.id) || []).some(
|
||||||
|
(l) =>
|
||||||
|
l.key.toLowerCase().includes(needle) || (l.value || "").toLowerCase().includes(needle)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}, [pools, q])
|
}, [pools, q, labelsByPool])
|
||||||
|
|
||||||
|
/* ------------------------------ Mutations ------------------------------- */
|
||||||
|
|
||||||
async function deletePool(id: string) {
|
async function deletePool(id: string) {
|
||||||
if (!confirm("Delete this node pool? This cannot be undone.")) return
|
if (!confirm("Delete this node pool? This cannot be undone.")) return
|
||||||
@@ -174,6 +257,7 @@ export const NodePoolPage = () => {
|
|||||||
await loadAll()
|
await loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create Pool
|
||||||
const createForm = useForm<CreatePoolInput, any, CreatePoolValues>({
|
const createForm = useForm<CreatePoolInput, any, CreatePoolValues>({
|
||||||
resolver: zodResolver(CreatePoolSchema),
|
resolver: zodResolver(CreatePoolSchema),
|
||||||
defaultValues: { name: "", server_ids: [] },
|
defaultValues: { name: "", server_ids: [] },
|
||||||
@@ -190,6 +274,7 @@ export const NodePoolPage = () => {
|
|||||||
await loadAll()
|
await loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Edit Pool
|
||||||
const editForm = useForm<UpdatePoolValues>({
|
const editForm = useForm<UpdatePoolValues>({
|
||||||
resolver: zodResolver(UpdatePoolSchema),
|
resolver: zodResolver(UpdatePoolSchema),
|
||||||
defaultValues: { name: "" },
|
defaultValues: { name: "" },
|
||||||
@@ -207,6 +292,7 @@ export const NodePoolPage = () => {
|
|||||||
await loadAll()
|
await loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attach / Detach Servers
|
||||||
const attachForm = useForm<AttachServersValues>({
|
const attachForm = useForm<AttachServersValues>({
|
||||||
resolver: zodResolver(AttachServersSchema),
|
resolver: zodResolver(AttachServersSchema),
|
||||||
defaultValues: { server_ids: [] },
|
defaultValues: { server_ids: [] },
|
||||||
@@ -233,11 +319,37 @@ export const NodePoolPage = () => {
|
|||||||
await loadAll()
|
await loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachableServers = useMemo(() => {
|
// Attach / Detach Labels
|
||||||
if (!manageTarget) return [] as ServerBrief[]
|
const attachLabelsForm = useForm<AttachLabelsValues>({
|
||||||
const attachedIds = new Set((manageTarget.servers || []).map((s) => s.id))
|
resolver: zodResolver(AttachLabelsSchema),
|
||||||
return allServers.filter((s) => !attachedIds.has(s.id))
|
defaultValues: { label_ids: [] },
|
||||||
}, [manageTarget, allServers])
|
})
|
||||||
|
|
||||||
|
function openManageLabels(p: NodePool) {
|
||||||
|
setManageLabelsTarget(p)
|
||||||
|
attachLabelsForm.reset({ label_ids: [] })
|
||||||
|
void loadAttachedLabels(p.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitAttachLabels = async (values: AttachLabelsValues) => {
|
||||||
|
if (!manageLabelsTarget) return
|
||||||
|
await api.post(`/api/v1/node-pools/${manageLabelsTarget.id}/labels`, {
|
||||||
|
label_ids: values.label_ids,
|
||||||
|
})
|
||||||
|
attachLabelsForm.reset({ label_ids: [] })
|
||||||
|
await loadAttachedLabels(manageLabelsTarget.id)
|
||||||
|
await loadAll() // refresh badges in table
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detachLabel(labelId: string) {
|
||||||
|
if (!manageLabelsTarget) return
|
||||||
|
if (!confirm("Detach this label from the pool?")) return
|
||||||
|
await api.delete(`/api/v1/node-pools/${manageLabelsTarget.id}/labels/${labelId}`)
|
||||||
|
await loadAttachedLabels(manageLabelsTarget.id)
|
||||||
|
await loadAll() // refresh badges in table
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------------- Render -------------------------------- */
|
||||||
|
|
||||||
if (loading) return <div className="p-6">Loading node pools…</div>
|
if (loading) return <div className="p-6">Loading node pools…</div>
|
||||||
if (err) return <div className="p-6 text-red-500">{err}</div>
|
if (err) return <div className="p-6 text-red-500">{err}</div>
|
||||||
@@ -253,7 +365,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 or servers…"
|
placeholder="Search pools, servers, labels…"
|
||||||
className="w-72 pl-8"
|
className="w-72 pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -361,79 +473,106 @@ export const NodePoolPage = () => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filtered.map((p) => (
|
{filtered.map((p) => {
|
||||||
<TableRow key={p.id}>
|
const labels = labelsByPool.get(p.id) || []
|
||||||
<TableCell className="font-medium">{p.name}</TableCell>
|
return (
|
||||||
<TableCell>
|
<TableRow key={p.id}>
|
||||||
<div className="flex flex-wrap gap-2">
|
<TableCell className="font-medium">{p.name}</TableCell>
|
||||||
{(p.servers || []).slice(0, 6).map((s) => (
|
|
||||||
<Badge key={s.id} variant="secondary" className="gap-1">
|
|
||||||
<ServerIcon className="h-3 w-3" />{" "}
|
|
||||||
{s.hostname || s.ip || s.ip_address || truncateMiddle(s.id, 6)}
|
|
||||||
{s.status && (
|
|
||||||
<span className="ml-1">
|
|
||||||
<StatusBadge status={s.status} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
{(p.servers || []).length === 0 && (
|
|
||||||
<span className="text-muted-foreground">No servers</span>
|
|
||||||
)}
|
|
||||||
{(p.servers || []).length > 6 && (
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
+{(p.servers || []).length - 6} more
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => openManage(p)}>
|
|
||||||
<LinkIcon className="mr-2 h-4 w-4" /> Manage servers
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex flex-wrap gap-2">Annotations</div>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<LinkIcon className="mr-2 h-4 w-4" /> Manage Annotations
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex flex-wrap gap-2">Labels</div>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<LinkIcon className="mr-2 h-4 w-4" /> Manage Labels
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex flex-wrap gap-2">Taints</div>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<LinkIcon className="mr-2 h-4 w-4" /> Manage Taints
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button variant="outline" size="sm" onClick={() => openEdit(p)}>
|
|
||||||
<Pencil className="mr-2 h-4 w-4" /> Edit
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<DropdownMenu>
|
{/* Servers cell */}
|
||||||
<DropdownMenuTrigger asChild>
|
<TableCell>
|
||||||
<Button variant="destructive" size="sm">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Trash className="mr-2 h-4 w-4" /> Delete
|
{(p.servers || []).slice(0, 6).map((s) => (
|
||||||
</Button>
|
<Badge key={s.id} variant="secondary" className="gap-1">
|
||||||
</DropdownMenuTrigger>
|
<ServerIcon className="h-3 w-3" />{" "}
|
||||||
<DropdownMenuContent align="end">
|
{s.hostname || s.ip || s.ip_address || truncateMiddle(s.id, 6)}
|
||||||
<DropdownMenuItem onClick={() => deletePool(p.id)}>
|
<span className="ml-1">{s.role}</span>
|
||||||
Confirm delete
|
{s.status && (
|
||||||
</DropdownMenuItem>
|
<span className="ml-1">
|
||||||
</DropdownMenuContent>
|
<StatusBadge status={s.status} />
|
||||||
</DropdownMenu>
|
</span>
|
||||||
</div>
|
)}
|
||||||
</TableCell>
|
</Badge>
|
||||||
</TableRow>
|
))}
|
||||||
))}
|
{(p.servers || []).length === 0 && (
|
||||||
|
<span className="text-muted-foreground">No servers</span>
|
||||||
|
)}
|
||||||
|
{(p.servers || []).length > 6 && (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
+{(p.servers || []).length - 6} more
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => openManage(p)}>
|
||||||
|
<LinkIcon className="mr-2 h-4 w-4" /> Manage servers
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* Annotations placeholder */}
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-wrap gap-2">Annotations</div>
|
||||||
|
<Button variant="outline" size="sm" disabled>
|
||||||
|
<LinkIcon className="mr-2 h-4 w-4" /> Manage Annotations
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* Labels cell with badges */}
|
||||||
|
<TableCell>
|
||||||
|
<div className="mb-2 flex flex-wrap gap-2">
|
||||||
|
{labels.slice(0, 6).map((l) => (
|
||||||
|
<Badge key={l.id} variant="outline" className="font-mono">
|
||||||
|
<Tag className="mr-1 h-3 w-3" />
|
||||||
|
{l.key}={l.value}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{labels.length === 0 && (
|
||||||
|
<span className="text-muted-foreground">No labels</span>
|
||||||
|
)}
|
||||||
|
{labels.length > 6 && (
|
||||||
|
<span className="text-muted-foreground">+{labels.length - 6} more</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => openManageLabels(p)}>
|
||||||
|
<Tag className="mr-2 h-4 w-4" /> Manage Labels
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* Taints placeholder */}
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-wrap gap-2">Taints</div>
|
||||||
|
<Button variant="outline" size="sm" disabled>
|
||||||
|
<LinkIcon className="mr-2 h-4 w-4" /> Manage Taints
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => openEdit(p)}>
|
||||||
|
<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={() => deletePool(p.id)}>
|
||||||
|
Confirm delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{filtered.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} className="text-muted-foreground py-10 text-center">
|
<TableCell colSpan={6} className="text-muted-foreground py-10 text-center">
|
||||||
No node pools match your search.
|
No node pools match your search.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -541,7 +680,7 @@ export const NodePoolPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Attach section */}
|
{/* Attach servers */}
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<Form {...attachForm}>
|
<Form {...attachForm}>
|
||||||
<form onSubmit={attachForm.handleSubmit(submitAttach)} className="space-y-3">
|
<form onSubmit={attachForm.handleSubmit(submitAttach)} className="space-y-3">
|
||||||
@@ -552,36 +691,45 @@ 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">
|
||||||
{attachableServers.length === 0 && (
|
{/* options */}
|
||||||
<div className="text-muted-foreground p-2 text-sm">
|
{(() => {
|
||||||
No more servers available to attach
|
const attachedIds = new Set(
|
||||||
</div>
|
(manageTarget?.servers || []).map((s) => s.id)
|
||||||
)}
|
|
||||||
{attachableServers.map((s) => {
|
|
||||||
const checked = field.value?.includes(s.id) || false
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
key={s.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(s.id)
|
|
||||||
else next.delete(s.id)
|
|
||||||
field.onChange(Array.from(next))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="leading-tight">
|
|
||||||
<div className="text-sm font-medium">{serverLabel(s)}</div>
|
|
||||||
<div className="text-muted-foreground text-xs">
|
|
||||||
{truncateMiddle(s.id, 8)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
)
|
)
|
||||||
})}
|
const attachableServers = allServers.filter((s) => !attachedIds.has(s.id))
|
||||||
|
if (attachableServers.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-muted-foreground p-2 text-sm">
|
||||||
|
No more servers available to attach
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return attachableServers.map((s) => {
|
||||||
|
const checked = field.value?.includes(s.id) || false
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={s.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(s.id)
|
||||||
|
else next.delete(s.id)
|
||||||
|
field.onChange(Array.from(next))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="leading-tight">
|
||||||
|
<div className="text-sm font-medium">{serverLabel(s)}</div>
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
{truncateMiddle(s.id, 8)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -599,6 +747,134 @@ export const NodePoolPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Manage labels dialog */}
|
||||||
|
<Dialog open={!!manageLabelsTarget} onOpenChange={(o) => !o && setManageLabelsTarget(null)}>
|
||||||
|
<DialogContent className="sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Manage labels for <span className="font-mono">{manageLabelsTarget?.name}</span>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Attached labels list */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-sm font-medium">Attached labels</div>
|
||||||
|
|
||||||
|
{labelsLoading ? (
|
||||||
|
<div className="text-muted-foreground rounded-md border p-3 text-sm">Loading…</div>
|
||||||
|
) : labelsErr ? (
|
||||||
|
<div className="rounded-md border p-3 text-sm text-red-500">{labelsErr}</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-hidden rounded-xl border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Key</TableHead>
|
||||||
|
<TableHead>Value</TableHead>
|
||||||
|
<TableHead className="w-[120px] text-right">Detach</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{attachedLabels.map((l) => (
|
||||||
|
<TableRow key={l.id}>
|
||||||
|
<TableCell className="font-mono text-sm">{l.key}</TableCell>
|
||||||
|
<TableCell className="font-mono text-sm">{l.value}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => detachLabel(l.id)}
|
||||||
|
>
|
||||||
|
<UnlinkIcon className="mr-2 h-4 w-4" /> Detach
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{attachedLabels.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={3} className="text-muted-foreground py-8 text-center">
|
||||||
|
No labels attached yet.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Attach labels */}
|
||||||
|
<div className="pt-4">
|
||||||
|
<Form {...attachLabelsForm}>
|
||||||
|
<form
|
||||||
|
onSubmit={attachLabelsForm.handleSubmit(submitAttachLabels)}
|
||||||
|
className="space-y-3"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={attachLabelsForm.control}
|
||||||
|
name="label_ids"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Attach more labels</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(attachedLabels.map((l) => l.id))
|
||||||
|
const attachable = (allLabels as LabelBrief[]).filter(
|
||||||
|
(l) => !attachedIds.has(l.id)
|
||||||
|
)
|
||||||
|
if (attachable.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-muted-foreground p-2 text-sm">
|
||||||
|
No more labels available to attach
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return attachable.map((l) => {
|
||||||
|
const checked = field.value?.includes(l.id) || false
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={l.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(l.id)
|
||||||
|
else next.delete(l.id)
|
||||||
|
field.onChange(Array.from(next))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="leading-tight">
|
||||||
|
<div className="text-sm font-medium">{labelKV(l)}</div>
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
{truncateMiddle(l.id, 8)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button type="submit" disabled={attachLabelsForm.formState.isSubmitting}>
|
||||||
|
<LinkIcon className="mr-2 h-4 w-4" />{" "}
|
||||||
|
{attachLabelsForm.formState.isSubmitting ? "Attaching…" : "Attach selected"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user