Merge pull request #2007 from shijilin0116/console

feat: Kubekey Web Console
This commit is contained in:
Xiao Liu 2023-10-13 15:40:41 +08:00 committed by GitHub
commit 4e4e00b763
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
117 changed files with 5953 additions and 82 deletions

6
.gitignore vendored
View File

@ -35,4 +35,8 @@ _artifacts
# Used during parts of the build process. Files _should_ get cleaned up automatically.
# This is also a good location for any temporary manfiests used during development
tmp
tmp
# kubekey-console node_modules and package-lock.json files
console/node_modules
console/package-lock.json

View File

@ -112,7 +112,7 @@ type RegistryConfig struct {
DataRoot string `yaml:"dataRoot" json:"dataRoot,omitempty"`
NamespaceOverride string `yaml:"namespaceOverride" json:"namespaceOverride,omitempty"`
BridgeIP string `yaml:"bridgeIP" json:"bridgeIP,omitempty"`
Auths runtime.RawExtension `yaml:"auths" json:"auths,omitempty"`
Auths runtime.RawExtension `yaml:"auths,omitempty" json:"auths,omitempty"`
}
// KubeSphere defines the configuration information of the KubeSphere.
@ -157,7 +157,7 @@ func (cfg *ClusterSpec) GenerateCertSANs() []string {
}
// GroupHosts is used to group hosts according to the configuration file.s
func (cfg *ClusterSpec) GroupHosts() map[string][]*KubeHost {
func (cfg *ClusterSpec) GroupHosts(isBackend bool) (map[string][]*KubeHost, error) {
hostMap := make(map[string]*KubeHost)
for _, hostCfg := range cfg.Hosts {
host := toHosts(hostCfg)
@ -168,10 +168,22 @@ func (cfg *ClusterSpec) GroupHosts() map[string][]*KubeHost {
//Check that the parameters under roleGroups are incorrect
if len(roleGroups[Master]) == 0 && len(roleGroups[ControlPlane]) == 0 {
logger.Log.Fatal(errors.New("The number of master/control-plane cannot be 0"))
err := errors.New("The number of master/control-plane cannot be 0")
if isBackend {
logger.Log.Errorln(err)
return nil, err
} else {
logger.Log.Fatal(err)
}
}
if len(roleGroups[Etcd]) == 0 && cfg.Etcd.Type == KubeKey {
logger.Log.Fatal(errors.New("The number of etcd cannot be 0"))
err := errors.New("The number of etcd cannot be 0")
if isBackend {
logger.Log.Errorln(err)
return nil, err
} else {
logger.Log.Fatal(err)
}
}
if len(roleGroups[Registry]) > 1 {
logger.Log.Fatal(errors.New("The number of registry node cannot be greater than 1."))
@ -181,8 +193,7 @@ func (cfg *ClusterSpec) GroupHosts() map[string][]*KubeHost {
host.SetRole(Master)
roleGroups[Master] = append(roleGroups[Master], host)
}
return roleGroups
return roleGroups, nil
}
// +kubebuilder:object:generate=false

View File

@ -104,13 +104,16 @@ const (
DefaultKubeVipMode = "ARP"
)
func (cfg *ClusterSpec) SetDefaultClusterSpec() (*ClusterSpec, map[string][]*KubeHost) {
func (cfg *ClusterSpec) SetDefaultClusterSpec(isBackend bool) (*ClusterSpec, map[string][]*KubeHost, error) {
clusterCfg := ClusterSpec{}
clusterCfg.Hosts = SetDefaultHostsCfg(cfg)
clusterCfg.RoleGroups = cfg.RoleGroups
clusterCfg.Etcd = SetDefaultEtcdCfg(cfg)
roleGroups := clusterCfg.GroupHosts()
roleGroups, err := clusterCfg.GroupHosts(isBackend)
if err != nil {
return nil, nil, err
}
clusterCfg.ControlPlaneEndpoint = SetDefaultLBCfg(cfg, roleGroups[Master])
clusterCfg.Network = SetDefaultNetworkCfg(cfg)
clusterCfg.Storage = SetDefaultStorageCfg(cfg)
@ -139,7 +142,7 @@ func (cfg *ClusterSpec) SetDefaultClusterSpec() (*ClusterSpec, map[string][]*Kub
if cfg.Kubernetes.ProxyMode == "" {
clusterCfg.Kubernetes.ProxyMode = DefaultProxyMode
}
return &clusterCfg, roleGroups
return &clusterCfg, roleGroups, nil
}
func SetDefaultHostsCfg(cfg *ClusterSpec) []HostCfg {

View File

@ -44,8 +44,8 @@ type Kubernetes struct {
KubeletArgs []string `yaml:"kubeletArgs" json:"kubeletArgs,omitempty"`
KubeProxyArgs []string `yaml:"kubeProxyArgs" json:"kubeProxyArgs,omitempty"`
FeatureGates map[string]bool `yaml:"featureGates" json:"featureGates,omitempty"`
KubeletConfiguration runtime.RawExtension `yaml:"kubeletConfiguration" json:"kubeletConfiguration,omitempty"`
KubeProxyConfiguration runtime.RawExtension `yaml:"kubeProxyConfiguration" json:"kubeProxyConfiguration,omitempty"`
KubeletConfiguration runtime.RawExtension `yaml:"kubeletConfiguration,omitempty" json:"kubeletConfiguration,omitempty"`
KubeProxyConfiguration runtime.RawExtension `yaml:"kubeProxyConfiguration,omitempty" json:"kubeProxyConfiguration,omitempty"`
Audit Audit `yaml:"audit" json:"audit,omitempty"`
}

View File

@ -0,0 +1,65 @@
package controllers
import (
"bufio"
"fmt"
"github.com/gorilla/websocket"
kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"net/http"
"os"
"strings"
)
type Cluster struct {
Kind string `yaml:"kind,omitempty" json:"kind,omitempty"`
ApiVersion string `yaml:"apiVersion,omitempty" json:"apiVersion,omitempty"`
metav1.ObjectMeta `yaml:"metadata,omitempty" json:"metadata,omitempty"`
Spec kubekeyapiv1alpha2.ClusterSpec `yaml:"spec,omitempty" json:"spec,omitempty"`
}
var Upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type WebSocketWriter struct {
WsConn *websocket.Conn
}
//var clientConn *websocket.Conn
func SetupCaptureBuffer() *WebSocketWriter {
captureBuffer := &WebSocketWriter{}
readerOut, writerOut, _ := os.Pipe()
os.Stdout = writerOut
outReader := bufio.NewReader(readerOut)
// 新开线程监听管道信息并输出到websocket
go func() {
for {
line, _, err := outReader.ReadLine()
if err != nil {
break
}
if captureBuffer.WsConn != nil {
captureBuffer.WsConn.WriteMessage(websocket.TextMessage, line)
}
}
}()
return captureBuffer
}
func FormatErrorMessage(err error) string {
msg := err.Error()
if !strings.HasPrefix(msg, "error: ") {
msg = fmt.Sprintf("error: %s", msg)
}
if !strings.HasSuffix(msg, "\n") {
msg += "\n"
}
return msg
}

View File

@ -0,0 +1,28 @@
package console
import (
"github.com/spf13/cobra"
"github.com/kubesphere/kubekey/v3/cmd/kk/cmd/options"
)
type ConsoleOptions struct {
CommonOptions *options.CommonOptions
}
func NewConsoleOptions() *ConsoleOptions {
return &ConsoleOptions{
CommonOptions: options.NewCommonOptions(),
}
}
func NewCmdConsole() *cobra.Command {
o := NewConsoleOptions()
cmd := &cobra.Command{
Use: "console",
Short: "Start a web console of kubekey",
}
o.CommonOptions.AddCommonFlag(cmd)
cmd.AddCommand(NewCmdConsoleStart())
return cmd
}

View File

@ -0,0 +1,65 @@
package console_common
import (
"bufio"
"fmt"
"github.com/gorilla/websocket"
kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"net/http"
"os"
"strings"
)
type Cluster struct {
Kind string `yaml:"kind,omitempty" json:"kind,omitempty"`
ApiVersion string `yaml:"apiVersion,omitempty" json:"apiVersion,omitempty"`
metav1.ObjectMeta `yaml:"metadata,omitempty" json:"metadata,omitempty"`
Spec kubekeyapiv1alpha2.ClusterSpec `yaml:"spec,omitempty" json:"spec,omitempty"`
}
var Upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type WebSocketWriter struct {
WsConn *websocket.Conn
}
//var clientConn *websocket.Conn
func SetupCaptureBuffer() *WebSocketWriter {
captureBuffer := &WebSocketWriter{}
readerOut, writerOut, _ := os.Pipe()
os.Stdout = writerOut
outReader := bufio.NewReader(readerOut)
// 新开线程监听管道信息并输出到websocket
go func() {
for {
line, _, err := outReader.ReadLine()
if err != nil {
break
}
if captureBuffer.WsConn != nil {
captureBuffer.WsConn.WriteMessage(websocket.TextMessage, line)
}
}
}()
return captureBuffer
}
func FormatErrorMessage(err error) string {
msg := err.Error()
if !strings.HasPrefix(msg, "error: ") {
msg = fmt.Sprintf("error: %s", msg)
}
if !strings.HasSuffix(msg, "\n") {
msg += "\n"
}
return msg
}

View File

@ -0,0 +1,89 @@
package operators
import (
"fmt"
"github.com/gin-gonic/gin"
kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2"
"github.com/kubesphere/kubekey/v3/cmd/kk/cmd/console/console_common"
"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common"
"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/pipelines"
"gopkg.in/yaml.v3"
"log"
"os"
)
func AddNode(c *gin.Context, targetDir string, tmpDir string) {
// 升级连接
clusterName := c.DefaultQuery("clusterName", "")
clientConn, err := console_common.Upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Fatalf("Failed to set websocket upgrade: %v", err)
return
}
// 建立管道stdout->websocket监听信息
captureBuffer := console_common.SetupCaptureBuffer()
captureBuffer.WsConn = clientConn
for {
_, readMsg, readErr := clientConn.ReadMessage()
if readErr != nil {
fmt.Println("websocket后台读取消息出错:", err)
fmt.Println("添加节点失败")
break
}
go func(message []byte) {
// 写入文件
mkdirErr := os.MkdirAll(fmt.Sprintf("./%s/%s", tmpDir, clusterName), 0755)
if mkdirErr != nil {
fmt.Println("创建目录时出错:", err)
fmt.Println("添加节点失败")
return
}
filePath := fmt.Sprintf("./%s/%s/Cluster-addNode.yaml", tmpDir, clusterName)
err := os.WriteFile(filePath, readMsg, 0644)
if err != nil {
fmt.Println("写入文件时出错:", err)
fmt.Println("添加节点失败")
return
}
// 解析yaml数据到data
var data kubekeyapiv1alpha2.Cluster
unmarshalErr := yaml.Unmarshal(readMsg, &data)
if unmarshalErr != nil {
fmt.Println("websocket解析yaml出错:", unmarshalErr)
fmt.Println("添加节点失败")
return
}
arg := common.Argument{
FilePath: filePath,
Debug: false,
IgnoreErr: false,
SkipConfirmCheck: true,
SkipPullImages: false,
ContainerManager: data.Spec.Kubernetes.ContainerManager,
Artifact: "",
InstallPackages: false,
}
actionErr := pipelines.AddNodes(arg, "")
if actionErr != nil {
msg := console_common.FormatErrorMessage(actionErr)
fmt.Println(msg)
fmt.Println("添加节点失败")
} else {
fmt.Println("添加节点成功")
mkdirErr := os.MkdirAll(fmt.Sprintf("./%s/%s", targetDir, clusterName), 0755)
if mkdirErr != nil {
fmt.Println("创建目录时出错:", err)
return
}
filePath := fmt.Sprintf("./%s/%s/Cluster.yaml", targetDir, clusterName)
err := os.WriteFile(filePath, readMsg, 0644)
if err != nil {
fmt.Println("写入文件时出错:", err)
return
}
}
}(readMsg)
}
}

View File

@ -0,0 +1,97 @@
package operators
import (
"fmt"
"github.com/gin-gonic/gin"
kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2"
"github.com/kubesphere/kubekey/v3/cmd/kk/cmd/console/console_common"
"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common"
"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/pipelines"
"gopkg.in/yaml.v3"
"log"
"os"
)
func CreateCluster(c *gin.Context, targetDir string, tmpDir string) {
// 升级连接
KubekeyNamespace := c.DefaultQuery("KubekeyNamespace", "")
clusterName := c.DefaultQuery("clusterName", "")
ksVersion := c.DefaultQuery("ksVersion", "")
clientConn, err := console_common.Upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Fatalf("Failed to set websocket upgrade: %v", err)
return
}
// 建立管道stdout->websocket监听信息
captureBuffer := console_common.SetupCaptureBuffer()
captureBuffer.WsConn = clientConn
for {
_, readMsg, readErr := clientConn.ReadMessage()
if readErr != nil {
fmt.Println("websocket后台读取消息出错:", err)
fmt.Println("安装集群失败")
break
}
go func(message []byte) {
// 写入文件
mkdirErr := os.MkdirAll(fmt.Sprintf("./%s/%s", tmpDir, clusterName), 0755)
if mkdirErr != nil {
fmt.Println("创建目录时出错:", err)
fmt.Println("安装集群失败")
return
}
filePath := fmt.Sprintf("./%s/%s/Cluster.yaml", tmpDir, clusterName)
err := os.WriteFile(filePath, readMsg, 0644)
if err != nil {
fmt.Println("写入文件时出错:", err)
fmt.Println("安装集群失败")
return
}
var data kubekeyapiv1alpha2.Cluster
unmarshalErr := yaml.Unmarshal(readMsg, &data)
if unmarshalErr != nil {
fmt.Println("websocket解析yaml出错:", unmarshalErr)
fmt.Println("安装集群失败")
return
}
arg := common.Argument{
FilePath: filePath,
KubernetesVersion: data.Spec.Kubernetes.Version,
KsEnable: ksVersion != "",
KsVersion: ksVersion,
SkipPullImages: false,
SkipPushImages: false,
SecurityEnhancement: false,
Debug: false,
IgnoreErr: false,
SkipConfirmCheck: true,
ContainerManager: data.Spec.Kubernetes.ContainerManager,
Artifact: "",
InstallPackages: false,
Namespace: KubekeyNamespace,
}
localStorage := true
arg.DeployLocalStorage = &localStorage
actionErr := pipelines.CreateCluster(arg, "curl -s -L -o %s %s")
if actionErr != nil {
msg := console_common.FormatErrorMessage(actionErr)
fmt.Println(msg)
fmt.Println("安装集群失败")
} else {
fmt.Println("安装集群成功")
mkdirErr := os.MkdirAll(fmt.Sprintf("./%s/%s", targetDir, clusterName), 0755)
if mkdirErr != nil {
fmt.Println("创建目录时出错:", err)
return
}
filePath := fmt.Sprintf("./%s/%s/Cluster.yaml", targetDir, clusterName)
err := os.WriteFile(filePath, readMsg, 0644)
if err != nil {
fmt.Println("写入文件时出错:", err)
return
}
}
}(readMsg)
}
}

View File

@ -0,0 +1,92 @@
package operators
import (
"fmt"
"github.com/gin-gonic/gin"
kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2"
"github.com/kubesphere/kubekey/v3/cmd/kk/cmd/console/console_common"
"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common"
"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/pipelines"
"gopkg.in/yaml.v3"
"log"
"os"
)
func DeleteCluster(c *gin.Context, targetDir string, tmpDir string) {
// 升级连接
clusterName := c.DefaultQuery("clusterName", "")
deleteCRI := c.DefaultQuery("deleteCRI", "no")
clientConn, err := console_common.Upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Fatalf("Failed to set websocket upgrade: %v", err)
return
}
// 建立管道stdout->websocket监听信息
captureBuffer := console_common.SetupCaptureBuffer()
captureBuffer.WsConn = clientConn
for {
_, readMsg, readErr := clientConn.ReadMessage()
if readErr != nil {
fmt.Println("websocket后台读取消息出错:", err)
fmt.Println("删除集群失败")
break
}
go func(message []byte) {
// 写入文件
mkdirErr := os.MkdirAll(fmt.Sprintf("./%s/%s", tmpDir, clusterName), 0755)
if mkdirErr != nil {
fmt.Println("创建目录时出错:", err)
fmt.Println("删除集群失败")
return
}
//fmt.Println("ClusterName is:::::", clusterName)
filePath := fmt.Sprintf("./%s/%s/Cluster-deleteCluster.yaml", tmpDir, clusterName)
err := os.WriteFile(filePath, readMsg, 0644)
if err != nil {
fmt.Println("写入文件时出错:", err)
fmt.Println("删除集群失败")
return
}
// 解析yaml数据到data
var data kubekeyapiv1alpha2.Cluster
unmarshalErr := yaml.Unmarshal(readMsg, &data)
if unmarshalErr != nil {
fmt.Println("websocket解析yaml出错:", unmarshalErr)
fmt.Println("删除集群失败")
return
}
arg := common.Argument{
FilePath: filePath,
Debug: false,
KubernetesVersion: "",
DeleteCRI: deleteCRI == "yes",
SkipConfirmCheck: true,
}
actionErr := pipelines.DeleteCluster(arg)
if actionErr != nil {
msg := console_common.FormatErrorMessage(actionErr)
fmt.Println(msg)
fmt.Println("删除集群失败")
} else {
deleteFilePath := fmt.Sprintf("./%s/%s/Cluster.yaml", targetDir, clusterName)
// 检查文件是否存在
if _, err := os.Stat(deleteFilePath); err == nil {
// 文件存在,进行删除操作
err := os.Remove(deleteFilePath)
if err != nil {
fmt.Println("删除文件时出错:", err)
return
}
} else if os.IsNotExist(err) {
fmt.Println("文件不存在,不需要删除")
} else {
// 其他错误
fmt.Println("检查文件时出错:", err)
return
}
fmt.Println("删除集群成功")
}
}(readMsg)
}
}

View File

@ -0,0 +1,99 @@
package operators
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/kubesphere/kubekey/v3/cmd/kk/cmd/console/console_common"
"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common"
"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/pipelines"
"gopkg.in/yaml.v3"
"log"
"os"
)
func DeleteNode(c *gin.Context, targetDir string, tmpDir string) {
// 升级连接
clusterName := c.DefaultQuery("clusterName", "")
nodeName := c.DefaultQuery("nodeName", "")
clientConn, err := console_common.Upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Fatalf("Failed to set websocket upgrade: %v", err)
return
}
// 建立管道stdout->websocket监听信息
captureBuffer := console_common.SetupCaptureBuffer()
captureBuffer.WsConn = clientConn
for {
_, readMsg, readErr := clientConn.ReadMessage()
if readErr != nil {
fmt.Println("websocket后台读取消息出错:", err)
fmt.Println("删除节点失败")
break
}
go func(message []byte) {
// 写入文件
mkdirErr := os.MkdirAll(fmt.Sprintf("./%s/%s", tmpDir, clusterName), 0755)
if mkdirErr != nil {
fmt.Println("创建目录时出错:", err)
fmt.Println("删除节点失败")
return
}
filePath := fmt.Sprintf("./%s/%s/Cluster-deleteNode.yaml", tmpDir, clusterName)
err := os.WriteFile(filePath, readMsg, 0644)
if err != nil {
fmt.Println("写入文件时出错:", err)
fmt.Println("删除节点失败")
return
}
// 解析yaml数据到data
var data console_common.Cluster
unmarshalErr := yaml.Unmarshal(readMsg, &data)
if unmarshalErr != nil {
fmt.Println("websocket解析yaml出错:", unmarshalErr)
fmt.Println("删除节点失败")
return
}
arg := common.Argument{
FilePath: filePath,
Debug: false,
NodeName: nodeName,
SkipConfirmCheck: true,
}
actionErr := pipelines.DeleteNode(arg)
if actionErr != nil {
msg := console_common.FormatErrorMessage(actionErr)
fmt.Println(msg)
fmt.Println("删除节点失败")
} else {
fmt.Println("删除节点成功")
mkdirErr := os.MkdirAll(fmt.Sprintf("./%s/%s", targetDir, clusterName), 0755)
if mkdirErr != nil {
fmt.Println("创建目录时出错:", err)
return
}
filePath := fmt.Sprintf("./%s/%s/Cluster.yaml", targetDir, clusterName)
for i, host := range data.Spec.Hosts {
if host.Name == nodeName {
data.Spec.Hosts = append(data.Spec.Hosts[:i], data.Spec.Hosts[i+1:]...)
break
}
}
for role, nodes := range data.Spec.RoleGroups {
for i, node := range nodes {
if node == nodeName {
data.Spec.RoleGroups[role] = append(data.Spec.RoleGroups[role][:i], data.Spec.RoleGroups[role][i+1:]...)
break
}
}
}
newData, err := yaml.Marshal(&data)
err = os.WriteFile(filePath, newData, 0644)
if err != nil {
fmt.Println("写入文件时出错:", err)
return
}
}
}(readMsg)
}
}

View File

@ -0,0 +1,91 @@
package operators
import (
"fmt"
"github.com/gin-gonic/gin"
kubekeyapiv1alpha2 "github.com/kubesphere/kubekey/v3/cmd/kk/apis/kubekey/v1alpha2"
"github.com/kubesphere/kubekey/v3/cmd/kk/cmd/console/console_common"
"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/common"
"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/pipelines"
"gopkg.in/yaml.v3"
"log"
"os"
)
func UpgradeCluster(c *gin.Context, targetDir string, tmpDir string) {
// 升级连接
clusterName := c.DefaultQuery("clusterName", "")
ksVersion := c.DefaultQuery("ksVersion", "")
clientConn, err := console_common.Upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Fatalf("Failed to set websocket upgrade: %v", err)
return
}
// 建立管道stdout->websocket监听信息
captureBuffer := console_common.SetupCaptureBuffer()
captureBuffer.WsConn = clientConn
for {
_, readMsg, readErr := clientConn.ReadMessage()
if readErr != nil {
fmt.Println("websocket后台读取消息出错:", err)
fmt.Println("升级集群失败")
break
}
go func(message []byte) {
// 写入文件 前端传过来的是什么就写入什么比kubekeyapiv1alpha2.Cluster的参数少
mkdirErr := os.MkdirAll(fmt.Sprintf("./%s/%s", tmpDir, clusterName), 0755)
if mkdirErr != nil {
fmt.Println("创建目录时出错:", err)
fmt.Println("升级集群失败")
return
}
filePath := fmt.Sprintf("./%s/%s/Cluster-upgradeCluster.yaml", tmpDir, clusterName)
err := os.WriteFile(filePath, readMsg, 0644)
if err != nil {
fmt.Println("写入文件时出错:", err)
fmt.Println("升级集群失败")
return
}
var data kubekeyapiv1alpha2.Cluster
unmarshalErr := yaml.Unmarshal(readMsg, &data)
if unmarshalErr != nil {
fmt.Println("websocket解析yaml出错:", unmarshalErr)
fmt.Println("升级集群失败")
return
}
arg := common.Argument{
FilePath: filePath,
KubernetesVersion: data.Spec.Kubernetes.Version,
KsEnable: ksVersion != "",
KsVersion: ksVersion,
SkipPullImages: false,
Debug: false,
SkipConfirmCheck: true,
Artifact: "",
}
localStorage := true
arg.DeployLocalStorage = &localStorage
actionErr := pipelines.UpgradeCluster(arg, "curl -s -L -o %s %s")
if actionErr != nil {
msg := console_common.FormatErrorMessage(actionErr)
fmt.Println(msg)
fmt.Println("升级集群失败")
} else {
fmt.Println("升级集群成功")
mkdirErr := os.MkdirAll(fmt.Sprintf("./%s/%s", targetDir, clusterName), 0755)
if mkdirErr != nil {
fmt.Println("创建目录时出错:", err)
return
}
filePath := fmt.Sprintf("./%s/%s/Cluster.yaml", targetDir, clusterName)
err := os.WriteFile(filePath, readMsg, 0644)
if err != nil {
fmt.Println("写入文件时出错:", err)
return
}
}
}(readMsg)
}
}

View File

@ -0,0 +1,145 @@
package router
import (
"embed"
"fmt"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/kubesphere/kubekey/v3/cmd/kk/cmd/console/console_common"
"github.com/kubesphere/kubekey/v3/cmd/kk/cmd/console/controllers/operators"
"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/version/kubernetes"
"github.com/kubesphere/kubekey/v3/cmd/kk/pkg/version/kubesphere"
"gopkg.in/yaml.v3"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"sort"
)
//go:embed templates/*
var embeddedTemplates embed.FS
//go:embed templates/static/*
var embeddedStatic embed.FS
func Router(LogFilePath string, ConfigFileDirPath string) *gin.Engine {
fmt.Println(fmt.Sprintf("服务器日志存放地址:%s", LogFilePath))
file, err := os.OpenFile(LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatal("无法打开日志文件:", err)
}
defer file.Close()
// 设置 gin 的日志输出为标准日志
gin.DefaultWriter = file
gin.SetMode(gin.ReleaseMode) // 设置 Gin 的模式为发布模式,以减少日志输出
ginServer := gin.Default()
staticFS, err := fs.Sub(embeddedStatic, "templates/static")
if err != nil {
// 处理错误
}
ginServer.StaticFS("/static", http.FS(staticFS))
ginConfig := cors.DefaultConfig()
ginConfig.AllowAllOrigins = true
ginConfig.AllowMethods = []string{"GET", "POST"}
ginConfig.AllowHeaders = []string{"Origin", "Content-Type"}
ginServer.Use(cors.New(ginConfig))
targetDir := ConfigFileDirPath
tmpDir := "./tmp_config"
ginServer.GET("/", func(context *gin.Context) {
data, _ := embeddedTemplates.ReadFile("templates/index.html")
context.Data(http.StatusOK, "text/html", data)
})
ginServer.GET("/scanCluster", func(c *gin.Context) {
//fmt.Println("进入scanCluster")
var clusterList []console_common.Cluster
err := filepath.Walk(targetDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relativePath, err := filepath.Rel(targetDir, path)
if err != nil {
return err
}
// 只检查一级子目录
if info.IsDir() && filepath.Dir(relativePath) == "." && relativePath != "." {
filePath := filepath.Join(path, "Cluster.yaml")
if _, err := os.Stat(filePath); !os.IsNotExist(err) {
// 文件存在
fileContent, err := os.ReadFile(filePath)
if err != nil {
return err
}
var cluster console_common.Cluster
if err := yaml.Unmarshal(fileContent, &cluster); err != nil {
fmt.Println("Error parsing YAML:", err)
} else {
clusterList = append(clusterList, cluster)
}
}
return filepath.SkipDir // 跳过此目录的其他文件和子目录
}
return nil
})
if err != nil {
fmt.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"clusterData": clusterList})
})
ginServer.GET("/clusterVersionOptions", func(context *gin.Context) {
//context.JSON 返回JSON
context.JSON(200, gin.H{"clusterVersionOptions": kubernetes.SupportedK8sVersionList()})
})
ginServer.GET("/ksVersionOptions/:clusterVersion", func(context *gin.Context) {
clusterVersion := context.Param("clusterVersion")
if len(clusterVersion) < 5 {
context.JSON(200, "invalid k8s version format")
}
compatibleKSVersions := []string{}
k8sVersionMajorMinor := clusterVersion[:5]
for _, installer := range kubesphere.VersionMap {
for _, supportVersion := range installer.K8sSupportVersions {
//fmt.Println(supportVersion)
if supportVersion == k8sVersionMajorMinor {
compatibleKSVersions = append(compatibleKSVersions, installer.Version)
break
}
}
}
sort.Strings(compatibleKSVersions)
sort.Sort(sort.Reverse(sort.StringSlice(compatibleKSVersions)))
context.JSON(200, gin.H{"ksVersionOptions": compatibleKSVersions})
})
ginServer.GET("/createCluster", func(c *gin.Context) {
operators.CreateCluster(c, targetDir, tmpDir)
})
ginServer.GET("/upgradeCluster", func(c *gin.Context) {
operators.UpgradeCluster(c, targetDir, tmpDir)
})
ginServer.GET("/deleteNode", func(c *gin.Context) {
operators.DeleteNode(c, targetDir, tmpDir)
})
ginServer.GET("/deleteCluster", func(c *gin.Context) {
operators.DeleteCluster(c, targetDir, tmpDir)
})
ginServer.GET("/addNode", func(c *gin.Context) {
operators.AddNode(c, targetDir, tmpDir)
})
return ginServer
}

View File

@ -0,0 +1,14 @@
{
"files": {
"main.css": "/static/css/main.273632b7.css",
"main.js": "/static/js/main.e3625d59.js",
"static/media/kubekey-logo.svg": "/static/media/kubekey-logo.953aed6d9c1c243565265d037e993403.svg",
"index.html": "/index.html",
"main.273632b7.css.map": "/static/css/main.273632b7.css.map",
"main.e3625d59.js.map": "/static/js/main.e3625d59.js.map"
},
"entrypoints": [
"static/css/main.273632b7.css",
"static/js/main.e3625d59.js"
]
}

View File

@ -0,0 +1 @@
<!doctype html><html lang="zh"><head><meta charset="utf-8"/><link rel="icon" href="./src/assets/favicon.png"/><title>Kubekey Web Console</title><script defer="defer" src="/static/js/main.e3625d59.js"></script><link href="/static/css/main.273632b7.css" rel="stylesheet"></head><body><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,100 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/*!
* cookie
* Copyright(c) 2012-2014 Roman Shtylman
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* escape-html
* Copyright(c) 2012-2013 TJ Holowaychuk
* Copyright(c) 2015 Andreas Lubbe
* Copyright(c) 2015 Tiancheng "Timothy" Gu
* MIT Licensed
*/
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* @license
* Lodash <https://lodash.com/>
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
/**
* @license React
* react-dom-server-legacy.browser.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-dom-server.browser.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 454.34 79.33" width="454.3399963378906" height="79.33000183105469"><defs><style>.cls-1{fill:#3e3d49;}.cls-2{fill:#00a971;}</style></defs><g id="图层_2" data-name="图层 2"><g id="图层_1-2" data-name="图层 1"><path class="cls-1" d="M172.92,79.16c-7.3,0-21.58-9.84-21.58-17.82V7.79a2.55,2.55,0,0,1,2.55-2.43h5.86a2.48,2.48,0,0,1,2.54,2.43V59.91c0,3.88,7.2,9.17,10.63,9.17s10.63-5.29,10.63-9.17V7.79a2.36,2.36,0,0,1,2.33-2.43H192a2.55,2.55,0,0,1,2.55,2.43V61.34C194.5,69.08,180.23,79.16,172.92,79.16Z"/><path class="cls-2" d="M400.73,70.65a2.5,2.5,0,0,0-2.47-2.48H372V47.84h26.27a2.42,2.42,0,0,0,2.47-2.48v-6.2a2.5,2.5,0,0,0-2.48-2.48H372V16.35h26.26a2.43,2.43,0,0,0,2.48-2.48V7.67a2.5,2.5,0,0,0-2.48-2.48H363.32a2.5,2.5,0,0,0-2.48,2.48V76.85a2.5,2.5,0,0,0,2.48,2.48h34.94a2.42,2.42,0,0,0,2.47-2.48Z"/><path class="cls-1" d="M295.56,70.65a2.5,2.5,0,0,0-2.48-2.48H266.82V47.84h26.26a2.42,2.42,0,0,0,2.48-2.48v-6.2a2.5,2.5,0,0,0-2.48-2.48H266.82V16.35h26.26a2.44,2.44,0,0,0,2.48-2.48V7.67a2.51,2.51,0,0,0-2.48-2.48H258.15a2.5,2.5,0,0,0-2.48,2.48V76.85a2.5,2.5,0,0,0,2.48,2.48h34.94a2.42,2.42,0,0,0,2.47-2.48Z"/><path class="cls-1" d="M236.58,41c5.11-3.36,10.25-9.9,10.25-15.32,0-8.4-11.43-20.31-19.61-20.31H209.68c-2.47,0-5.49,2.58-5.49,5.15V76.66a2.5,2.5,0,0,0,2.57,2.46H227.6c8.18,0,19.23-13.76,19.23-22.16C246.83,51.53,241.69,44.43,236.58,41Zm-21.3-26h10.49c4.48,0,10.08,6.41,10.08,10.67s-5.38,11.51-10,11.51h-10.6Zm10.49,54.45H215.28V44.84h10.6c4.59,0,10,7.75,10,12.12S230.25,69.49,225.77,69.49Z"/><path class="cls-2" d="M323.88,42.67l24.53-28.41a2.42,2.42,0,0,0-.25-3.49l-4.69-4A2.5,2.5,0,0,0,340,7L318.52,31.81V7.67A2.43,2.43,0,0,0,316,5.19h-6.2a2.51,2.51,0,0,0-2.48,2.48V76.85a2.51,2.51,0,0,0,2.48,2.48H316a2.43,2.43,0,0,0,2.48-2.48V53.53L340,78.37a2.51,2.51,0,0,0,3.5.26l4.69-4a2.43,2.43,0,0,0,.25-3.5Z"/><path class="cls-1" d="M116,42.67l24.53-28.41a2.42,2.42,0,0,0-.26-3.49l-4.69-4a2.49,2.49,0,0,0-3.49.25L110.66,31.81V7.67a2.44,2.44,0,0,0-2.48-2.48H102A2.51,2.51,0,0,0,99.5,7.67V76.85A2.51,2.51,0,0,0,102,79.33h6.2a2.44,2.44,0,0,0,2.48-2.48V53.53l21.45,24.84a2.5,2.5,0,0,0,3.49.26l4.69-4a2.44,2.44,0,0,0,.26-3.5Z"/><rect class="cls-2" x="427.63" y="39.99" width="11.16" height="38.95" rx="2.48"/><path class="cls-2" d="M451.82,5.59h-6.28A2.53,2.53,0,0,0,443,8.1V23.27c0,3.91-5.36,13.25-9.66,13.62-4.3-.37-9.65-9.71-9.65-13.62V8.1a2.54,2.54,0,0,0-2.52-2.51h-6.28A2.53,2.53,0,0,0,412.4,8.1V25c0,5.6,5.65,15.06,9.79,17.7,2.36,1.52,5.78,3.88,10.9,4h.56c5.12-.17,8.54-2.53,10.91-4,4.13-2.64,9.78-12.1,9.78-17.7V8.1A2.53,2.53,0,0,0,451.82,5.59Z"/><path class="cls-2" d="M76.63,22.12V66.36L54.54,79.12,44.91,49.23a13.63,13.63,0,1,0-13.48.22L21.91,79,0,66.36V22.12L38.31,0Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,70 @@
package console
import (
"fmt"
"github.com/kubesphere/kubekey/v3/cmd/kk/cmd/console/router"
"github.com/kubesphere/kubekey/v3/cmd/kk/cmd/options"
"github.com/spf13/cobra"
"log"
"os/exec"
"runtime"
)
type ConsoleStartOptions struct {
CommonOptions *options.CommonOptions
LogFilePath string
ConfigFileDirPath string
}
func NewConsoleStartOptions() *ConsoleStartOptions {
return &ConsoleStartOptions{
CommonOptions: options.NewCommonOptions(),
}
}
func openBrowser(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}
func NewCmdConsoleStart() *cobra.Command {
o := NewConsoleStartOptions()
// 这里的cmd只是声明其中的Run字段要等输入了kk create cluster时才会运行
cmd := &cobra.Command{
Use: "start",
Short: "Start a web console of kubekey",
Run: func(cmd *cobra.Command, args []string) {
go func() {
fmt.Println(fmt.Sprintf("web控制台已启动请访问[localhost:8082]或[公网ip:8082]查看"))
err := openBrowser("localhost:8082")
if err != nil {
}
}()
ginServer := router.Router(o.LogFilePath, o.ConfigFileDirPath)
err := ginServer.Run(":8082")
if err != nil {
log.Fatal("无法启动服务器:", err)
} else {
}
},
}
o.CommonOptions.AddCommonFlag(cmd)
o.AddFlags(cmd)
return cmd
}
func (o *ConsoleStartOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringVarP(&o.LogFilePath, "log-path", "", "./server.log", "the path to the log of web server")
cmd.Flags().StringVarP(&o.ConfigFileDirPath, "config-Path", "", "./config_from_console", "the path of the directory of config files")
}

View File

@ -18,6 +18,7 @@ package cmd
import (
"fmt"
"github.com/kubesphere/kubekey/v3/cmd/kk/cmd/console"
"os"
"os/exec"
"runtime"
@ -48,6 +49,7 @@ type KubeKeyOptions struct {
}
func NewDefaultKubeKeyCommand() *cobra.Command {
//fmt.Println("进入root.go中的NewDefaultKubeKeyCommand")
return NewDefaultKubeKeyCommandWithArgs(KubeKeyOptions{
PluginHandler: NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes),
Arguments: os.Args,
@ -109,6 +111,8 @@ func NewKubeKeyCommand(o KubeKeyOptions) *cobra.Command {
cmds.AddCommand(alpha.NewAlphaCmd())
cmds.AddCommand(console.NewCmdConsole())
cmds.AddCommand(create.NewCmdCreate())
cmds.AddCommand(delete.NewCmdDelete())
cmds.AddCommand(add.NewCmdAdd())

View File

@ -86,16 +86,25 @@ func (i *InstallationConfirm) Execute(runtime connector.Runtime) error {
for _, host := range results {
if host.Sudo == "" {
logger.Log.Errorf("%s: sudo is required.", host.Name)
if runtime.GetIsBackend() {
fmt.Printf("%s: sudo is required.\n", host.Name)
}
stopFlag = true
}
if host.Conntrack == "" {
logger.Log.Errorf("%s: conntrack is required.", host.Name)
if runtime.GetIsBackend() {
fmt.Printf("%s: conntrack is required.\n", host.Name)
}
stopFlag = true
}
if host.Socat == "" {
logger.Log.Errorf("%s: socat is required.", host.Name)
if runtime.GetIsBackend() {
fmt.Printf("%s: socat is required.\n", host.Name)
}
stopFlag = true
}
}
@ -122,7 +131,11 @@ func (i *InstallationConfirm) Execute(runtime connector.Runtime) error {
}
if stopFlag {
os.Exit(1)
if runtime.GetIsBackend() {
return errors.New("安装失败!")
} else {
os.Exit(1)
}
}
if i.KubeConf.Arg.SkipConfirmCheck {

View File

@ -62,10 +62,18 @@ func NewKubeRuntime(flag string, arg Argument) (*KubeRuntime, error) {
return nil, err
}
base := connector.NewBaseRuntime(cluster.Name, connector.NewDialer(), arg.Debug, arg.IgnoreErr)
var isBackend bool
_, ok := cluster.Labels["type.kubekey.kubesphere.io/backend"]
if ok {
isBackend = true
}
base := connector.NewBaseRuntime(cluster.Name, connector.NewDialer(), arg.Debug, arg.IgnoreErr, isBackend)
clusterSpec := &cluster.Spec
defaultCluster, roleGroups := clusterSpec.SetDefaultClusterSpec()
defaultCluster, roleGroups, err := clusterSpec.SetDefaultClusterSpec(isBackend)
if err != nil {
return nil, err
}
hostSet := make(map[string]struct{})
for _, role := range roleGroups {

View File

@ -54,7 +54,7 @@ func NewLocalRuntime(debug, ingoreErr bool) (LocalRuntime, error) {
if err != nil {
return localRuntime, err
}
base := connector.NewBaseRuntime(name, connector.NewDialer(), debug, ingoreErr)
base := connector.NewBaseRuntime(name, connector.NewDialer(), debug, ingoreErr, false)
host := connector.NewHost()
host.Name = name

View File

@ -62,6 +62,7 @@ type Runtime interface {
RemoteHost() Host
Copy() Runtime
ModuleRuntime
GetIsBackend() bool
}
type Host interface {

View File

@ -1,7 +1,7 @@
/*
Copyright 2021 The KubeSphere Authors.
Licensed under the Apache License, Version 2.0 (the "License");
Licensed under the Apache License, Version 2.0 (the "License")
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
@ -38,9 +38,14 @@ type BaseRuntime struct {
allHosts []Host
roleHosts map[string][]Host
deprecatedHosts map[string]string
isBackend bool
}
func NewBaseRuntime(name string, connector Connector, verbose bool, ignoreErr bool) BaseRuntime {
func (b *BaseRuntime) GetIsBackend() bool {
return b.isBackend
}
func NewBaseRuntime(name string, connector Connector, verbose bool, ignoreErr bool, isBackend bool) BaseRuntime {
base := BaseRuntime{
ObjName: name,
connector: connector,
@ -49,6 +54,7 @@ func NewBaseRuntime(name string, connector Connector, verbose bool, ignoreErr bo
allHosts: make([]Host, 0, 0),
roleHosts: make(map[string][]Host),
deprecatedHosts: make(map[string]string),
isBackend: isBackend,
}
if err := base.GenerateWorkDir(); err != nil {
fmt.Printf("[ERRO]: Failed to create KubeKey work dir: %s\n", err)
@ -178,7 +184,7 @@ func (b *BaseRuntime) InitLogger() error {
}
}
logDir := filepath.Join(b.GetWorkDir(), "logs")
logger.Log = logger.NewLogger(logDir, b.verbose)
logger.Log = logger.NewLogger(logDir, b.verbose, b.isBackend)
return nil
}

View File

@ -18,6 +18,8 @@ package logger
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
@ -36,7 +38,7 @@ type KubeKeyLog struct {
Verbose bool
}
func NewLogger(outputPath string, verbose bool) *KubeKeyLog {
func NewLogger(outputPath string, verbose bool, isBackend bool) *KubeKeyLog {
logger := logrus.New()
formatter := &Formatter{
@ -54,18 +56,24 @@ func NewLogger(outputPath string, verbose bool) *KubeKeyLog {
rotatelogs.WithLinkName(path),
rotatelogs.WithRotationTime(24*time.Hour),
)
var logWriter io.Writer
if isBackend {
logWriter = io.MultiWriter(os.Stdout, writer)
} else {
logWriter = writer
}
logWriters := lfshook.WriterMap{
logrus.InfoLevel: writer,
logrus.WarnLevel: writer,
logrus.ErrorLevel: writer,
logrus.FatalLevel: writer,
logrus.PanicLevel: writer,
logrus.InfoLevel: logWriter,
logrus.WarnLevel: logWriter,
logrus.ErrorLevel: logWriter,
logrus.FatalLevel: logWriter,
logrus.PanicLevel: logWriter,
}
if verbose {
logger.SetLevel(logrus.DebugLevel)
logWriters[logrus.DebugLevel] = writer
logWriters[logrus.DebugLevel] = logWriter
} else {
logger.SetLevel(logrus.InfoLevel)
}

View File

@ -250,8 +250,9 @@ type Check struct {
func (c *Check) Execute(runtime connector.Runtime) error {
var (
position = 1
notes = "Please wait for the installation to complete: "
position = 1
notes = "Please wait for the installation to complete: "
isBackendNotes = "Please wait for the installation to complete. "
)
ch := make(chan string)
@ -260,38 +261,49 @@ func (c *Check) Execute(runtime connector.Runtime) error {
go CheckKubeSphereStatus(ctx, runtime, ch)
stop := false
for !stop {
select {
case res := <-ch:
fmt.Printf("\033[%dA\033[K", position)
fmt.Println(res)
stop = true
default:
for i := 0; i < 10; i++ {
if i < 5 {
fmt.Printf("\033[%dA\033[K", position)
if runtime.GetIsBackend() {
fmt.Println(isBackendNotes)
for !stop {
select {
case res := <-ch:
fmt.Println(res)
stop = true
}
}
} else {
for !stop {
select {
case res := <-ch:
fmt.Printf("\033[%dA\033[K", position)
fmt.Println(res)
stop = true
default:
for i := 0; i < 10; i++ {
if i < 5 {
fmt.Printf("\033[%dA\033[K", position)
output := fmt.Sprintf(
"%s%s%s",
notes,
strings.Repeat(" ", i),
">>--->",
)
output := fmt.Sprintf(
"%s%s%s",
notes,
strings.Repeat(" ", i),
">>--->",
)
fmt.Printf("%s \033[K\n", output)
time.Sleep(time.Duration(200) * time.Millisecond)
} else {
fmt.Printf("\033[%dA\033[K", position)
fmt.Printf("%s \033[K\n", output)
time.Sleep(time.Duration(200) * time.Millisecond)
} else {
fmt.Printf("\033[%dA\033[K", position)
output := fmt.Sprintf(
"%s%s%s",
notes,
strings.Repeat(" ", 10-i),
"<---<<",
)
output := fmt.Sprintf(
"%s%s%s",
notes,
strings.Repeat(" ", 10-i),
"<---<<",
)
fmt.Printf("%s \033[K\n", output)
time.Sleep(time.Duration(200) * time.Millisecond)
fmt.Printf("%s \033[K\n", output)
time.Sleep(time.Duration(200) * time.Millisecond)
}
}
}
}

View File

@ -0,0 +1,14 @@
{
"files": {
"main.css": "/static/css/main.273632b7.css",
"main.js": "/static/js/main.e3625d59.js",
"static/media/kubekey-logo.svg": "/static/media/kubekey-logo.953aed6d9c1c243565265d037e993403.svg",
"index.html": "/index.html",
"main.273632b7.css.map": "/static/css/main.273632b7.css.map",
"main.e3625d59.js.map": "/static/js/main.e3625d59.js.map"
},
"entrypoints": [
"static/css/main.273632b7.css",
"static/js/main.e3625d59.js"
]
}

1
console/build/index.html Normal file
View File

@ -0,0 +1 @@
<!doctype html><html lang="zh"><head><meta charset="utf-8"/><link rel="icon" href="./src/assets/favicon.png"/><title>Kubekey Web Console</title><script defer="defer" src="/static/js/main.e3625d59.js"></script><link href="/static/css/main.273632b7.css" rel="stylesheet"></head><body><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,100 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/*!
* cookie
* Copyright(c) 2012-2014 Roman Shtylman
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* escape-html
* Copyright(c) 2012-2013 TJ Holowaychuk
* Copyright(c) 2015 Andreas Lubbe
* Copyright(c) 2015 Tiancheng "Timothy" Gu
* MIT Licensed
*/
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* @license
* Lodash <https://lodash.com/>
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
/**
* @license React
* react-dom-server-legacy.browser.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-dom-server.browser.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 454.34 79.33" width="454.3399963378906" height="79.33000183105469"><defs><style>.cls-1{fill:#3e3d49;}.cls-2{fill:#00a971;}</style></defs><g id="图层_2" data-name="图层 2"><g id="图层_1-2" data-name="图层 1"><path class="cls-1" d="M172.92,79.16c-7.3,0-21.58-9.84-21.58-17.82V7.79a2.55,2.55,0,0,1,2.55-2.43h5.86a2.48,2.48,0,0,1,2.54,2.43V59.91c0,3.88,7.2,9.17,10.63,9.17s10.63-5.29,10.63-9.17V7.79a2.36,2.36,0,0,1,2.33-2.43H192a2.55,2.55,0,0,1,2.55,2.43V61.34C194.5,69.08,180.23,79.16,172.92,79.16Z"/><path class="cls-2" d="M400.73,70.65a2.5,2.5,0,0,0-2.47-2.48H372V47.84h26.27a2.42,2.42,0,0,0,2.47-2.48v-6.2a2.5,2.5,0,0,0-2.48-2.48H372V16.35h26.26a2.43,2.43,0,0,0,2.48-2.48V7.67a2.5,2.5,0,0,0-2.48-2.48H363.32a2.5,2.5,0,0,0-2.48,2.48V76.85a2.5,2.5,0,0,0,2.48,2.48h34.94a2.42,2.42,0,0,0,2.47-2.48Z"/><path class="cls-1" d="M295.56,70.65a2.5,2.5,0,0,0-2.48-2.48H266.82V47.84h26.26a2.42,2.42,0,0,0,2.48-2.48v-6.2a2.5,2.5,0,0,0-2.48-2.48H266.82V16.35h26.26a2.44,2.44,0,0,0,2.48-2.48V7.67a2.51,2.51,0,0,0-2.48-2.48H258.15a2.5,2.5,0,0,0-2.48,2.48V76.85a2.5,2.5,0,0,0,2.48,2.48h34.94a2.42,2.42,0,0,0,2.47-2.48Z"/><path class="cls-1" d="M236.58,41c5.11-3.36,10.25-9.9,10.25-15.32,0-8.4-11.43-20.31-19.61-20.31H209.68c-2.47,0-5.49,2.58-5.49,5.15V76.66a2.5,2.5,0,0,0,2.57,2.46H227.6c8.18,0,19.23-13.76,19.23-22.16C246.83,51.53,241.69,44.43,236.58,41Zm-21.3-26h10.49c4.48,0,10.08,6.41,10.08,10.67s-5.38,11.51-10,11.51h-10.6Zm10.49,54.45H215.28V44.84h10.6c4.59,0,10,7.75,10,12.12S230.25,69.49,225.77,69.49Z"/><path class="cls-2" d="M323.88,42.67l24.53-28.41a2.42,2.42,0,0,0-.25-3.49l-4.69-4A2.5,2.5,0,0,0,340,7L318.52,31.81V7.67A2.43,2.43,0,0,0,316,5.19h-6.2a2.51,2.51,0,0,0-2.48,2.48V76.85a2.51,2.51,0,0,0,2.48,2.48H316a2.43,2.43,0,0,0,2.48-2.48V53.53L340,78.37a2.51,2.51,0,0,0,3.5.26l4.69-4a2.43,2.43,0,0,0,.25-3.5Z"/><path class="cls-1" d="M116,42.67l24.53-28.41a2.42,2.42,0,0,0-.26-3.49l-4.69-4a2.49,2.49,0,0,0-3.49.25L110.66,31.81V7.67a2.44,2.44,0,0,0-2.48-2.48H102A2.51,2.51,0,0,0,99.5,7.67V76.85A2.51,2.51,0,0,0,102,79.33h6.2a2.44,2.44,0,0,0,2.48-2.48V53.53l21.45,24.84a2.5,2.5,0,0,0,3.49.26l4.69-4a2.44,2.44,0,0,0,.26-3.5Z"/><rect class="cls-2" x="427.63" y="39.99" width="11.16" height="38.95" rx="2.48"/><path class="cls-2" d="M451.82,5.59h-6.28A2.53,2.53,0,0,0,443,8.1V23.27c0,3.91-5.36,13.25-9.66,13.62-4.3-.37-9.65-9.71-9.65-13.62V8.1a2.54,2.54,0,0,0-2.52-2.51h-6.28A2.53,2.53,0,0,0,412.4,8.1V25c0,5.6,5.65,15.06,9.79,17.7,2.36,1.52,5.78,3.88,10.9,4h.56c5.12-.17,8.54-2.53,10.91-4,4.13-2.64,9.78-12.1,9.78-17.7V8.1A2.53,2.53,0,0,0,451.82,5.59Z"/><path class="cls-2" d="M76.63,22.12V66.36L54.54,79.12,44.91,49.23a13.63,13.63,0,1,0-13.48.22L21.91,79,0,66.36V22.12L38.31,0Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

46
console/package.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "console",
"version": "0.1.0",
"private": true,
"dependencies": {
"@kube-design/components": "^1.27.1",
"@kubed/components": "0.0.81",
"@kubed/hooks": "0.0.14",
"@kubed/icons": "0.0.13",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"classnames": "^2.3.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^5.3.4",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

11
console/public/index.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<link rel="icon" href="./src/assets/favicon.png" />
<title>Kubekey Web Console</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

74
console/src/App.js Normal file
View File

@ -0,0 +1,74 @@
import React from 'react';
import {Column, Columns} from "@kube-design/components";
import logo from './assets/kubekey-logo.svg';
import Install from "./Components/Install/Install";
import {Route, Switch} from "react-router-dom";
import Cluster from "./Components/Cluster/Cluster";
import DeleteNode from "./Components/DeleteNode/DeleteNode";
import {ClusterTableProvider} from "./context/ClusterTableContext";
import {DeleteNodeFormProvider} from "./context/DeleteNodeFormContext";
import AddNode from "./Components/AddNode/AddNode";
import {AddNodeFormProvider} from "./context/AddNodeFormContext";
import {InstallFormProvider} from "./context/InstallFormContext";
import UpgradeCluster from "./Components/UpgradeCluster/UpgradeCluster";
import {GlobalProvider} from "./context/GlobalContext";
import {DeleteClusterProvider} from "./context/DeleteClusterContext";
import DeleteCluster from "./Components/DeleteCluster/DeleteCluster";
import {UpgradeClusterFormProvider} from "./context/UpgradeClusterFormContext";
const App = () => {
return (
<div>
<GlobalProvider>
<Columns>
<Column className={'is-1'}></Column>
<Column className={'is-2'}>
<img src={logo} alt='logo' style={{width:'70%',height:'70%',marginTop: '10px'}}></img>
</Column>
</Columns>
<Switch>
<Route exact path="/">
<Cluster/>
</Route>
<Route path="/install">
<InstallFormProvider>
<Install/>
</InstallFormProvider>
</Route>
<Route path="/DeleteCluster/:clusterName">
<ClusterTableProvider>
<DeleteClusterProvider>
<DeleteCluster/>
</DeleteClusterProvider>
</ClusterTableProvider>
</Route>
<Route path="/DeleteNode/:clusterName">
<ClusterTableProvider>
<DeleteNodeFormProvider>
<DeleteNode/>
</DeleteNodeFormProvider>
</ClusterTableProvider>
</Route>
<Route path="/AddNode/:clusterName">
<ClusterTableProvider>
<AddNodeFormProvider>
<AddNode/>
</AddNodeFormProvider>
</ClusterTableProvider>
</Route>
<Route path="/UpgradeCluster/:clusterName">
<ClusterTableProvider>
<UpgradeClusterFormProvider>
<UpgradeCluster/>
</UpgradeClusterFormProvider>
</ClusterTableProvider>
</Route>
{/* TODO 增加用户输入/AddNode/xxx错误集群名时的报错处理*/}
<Route path="*">
<div>路径错误</div>
</Route>
</Switch>
</GlobalProvider>
</div>
)
}
export default App;

View File

@ -0,0 +1,54 @@
import React, {useEffect} from 'react';
import {Link, useParams} from "react-router-dom";
import {Button, Column, Columns} from "@kube-design/components";
import AddNodeProgressBar from "./AddNodeProgressBar";
import AddNodeForm from "./AddNodeForm";
import useClusterTableContext from "../../hooks/useClusterTableContext";
import useAddNodeFormContext from "../../hooks/useAddNodeFormContext";
const AddNode = () => {
const {clusterName} = useParams()
const {clusterData} = useClusterTableContext()
const {setCurCluster,canToHome} = useAddNodeFormContext()
useEffect(() => {
if (clusterData.length > 0) {
setCurCluster(clusterData.find(item=>item.metadata.name===clusterName))
}
}, [clusterData]);
return (
<>
<Columns>
<Column className="is-1"></Column>
<Column className="is-2">
<h2>新增节点</h2>
</Column>
<Column className={'is-8'}>
<Columns>
<Column className={'is-10'}>
</Column>
<Column>
{canToHome ? (
<Link to='/'>
<Button disabled={!canToHome}>集群列表</Button>
</Link>
) : (
<Button disabled={!canToHome}>集群列表</Button>
)}
</Column>
</Columns>
</Column>
</Columns>
<Columns>
<Column className={'is-1'}></Column>
<Column className={'is-2'}>
<AddNodeProgressBar></AddNodeProgressBar>
</Column>
<Column className={'is-8'}>
<AddNodeForm/>
</Column>
</Columns>
</>
);
};
export default AddNode;

View File

@ -0,0 +1,94 @@
import React from 'react';
import {Button, Column, Columns} from "@kube-design/components";
import useAddNodeFormContext from "../../hooks/useAddNodeFormContext";
import AddNodeFormInputs from "./AddNodeFormInputs";
import {Modal} from "@kubed/components";
const AddNodeForm = () => {
const [visible, setVisible] = React.useState(false);
const ref = React.createRef();
const openModal = () => {
setVisible(true);
};
const closeModal = () => {
setVisible(false);
};
const onOKHandler = () => {
setVisible(false);
}
const textStyle={
fontSize:"15px",
minHeight: '50px',
margin: 0, /* 清除默认的外边距 */
display: 'flex',
alignItems: 'center'
}
const {
page,
setPage,
title,
canSubmit,
disablePrev,
addHandler,
disableNext
} = useAddNodeFormContext()
const handlePrev = () => {
setPage(prev => {
return +prev-1
})
}
const handleNext = () => {
setPage(prev => {
return +prev+1
})
}
return (
<>
<Columns>
<Column className='is-10'>
<h3>{title[page]}</h3>
</Column>
</Columns>
<Columns>
<Column>
<AddNodeFormInputs/>
</Column>
</Columns>
<Columns>
<Column className='is-8'/>
<Column className='is-2'>
<Columns>
<Column className='is-5'/>
<Column>
{page !== 0 && <Button onClick={handlePrev} disabled={disablePrev}>上一步</Button>}
</Column>
</Columns>
</Column>
<Column className='is-1'>
{page !== Object.keys(title).length - 1 && <Button onClick={handleNext} disabled={disableNext}>下一步</Button>}
{page === Object.keys(title).length - 1 && <Button disabled={!canSubmit} onClick={()=>{addHandler(); openModal();}} >新增</Button>}
<Modal
ref={ref}
visible={visible}
title="开始添加节点"
onCancel={closeModal}
onOk={onOKHandler}
>
<Columns>
<Column style={{display:`flex`, alignItems: 'center' }}>
<p style={textStyle}>添加节点已开始关闭该提示后可查看实时日志期间请勿进行其他操作</p>
</Column>
</Columns>
</Modal>
</Column>
</Columns>
</>
);
};
export default AddNodeForm;

View File

@ -0,0 +1,23 @@
import React from 'react';
import InputNodeInfoSetting from "./AddNodeSettings/InputNodeInfoSetting";
import ConfirmAddNodeSetting from "./AddNodeSettings/ConfirmAddNodeSetting";
import useAddNodeFormContext from "../../hooks/useAddNodeFormContext";
import AddNodeEtcdSetting from "./AddNodeSettings/AddNodeEtcdSetting";
const AddNodeFormInputs = () => {
const { page } = useAddNodeFormContext()
const display = {
0: <InputNodeInfoSetting/>,
1: <AddNodeEtcdSetting/>,
2: <ConfirmAddNodeSetting/>
}
return (
<div>
{display[page]}
</div>
)
};
export default AddNodeFormInputs;

View File

@ -0,0 +1,69 @@
import React from 'react';
import {Button} from "@kubed/components";
import useAddNodeFormContext from "../../hooks/useAddNodeFormContext";
const AddNodeProgressBar = () => {
const {page,setPage,title,buttonDisabled} = useAddNodeFormContext();
const ongoingIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
backgroundColor: '#1C8FFC', // 背景色为蓝色
color: 'white', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
};
const finishedIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
border: '1px solid #7eb8dc',
backgroundColor: 'white', // 背景色为蓝色
color: '#7eb8dc', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
};
const unfinishedIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
border: '1px solid #abb4be',
backgroundColor: 'white', // 背景色为蓝色
color: '#abb4be', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
}
const changePageHandler= e => {
const index = Object.keys(title).find(key => title[key] === e.target.innerText)
setPage(index)
}
const IndexCircleItem = (step) => {
if(step<page) return <div style={finishedIndexCircleStyle}></div>
else if(step===page) return <div style={ongoingIndexCircleStyle}>{step+1}</div>
else return <div style={unfinishedIndexCircleStyle}>{step+1}</div>
}
const steps = Object.keys(title).map((step,index) => {
return (
<div style={{display:`flex`, alignItems: 'center' }} key={index}>
{IndexCircleItem(+step)}
<Button style={{height:'50px'}} variant="link" color="default" onClick={changePageHandler} disabled={+step>page||buttonDisabled}>{title[step]}</Button>
</div>
)
})
return (
<div>
{steps}
</div>
);
};
export default AddNodeProgressBar;

View File

@ -0,0 +1,64 @@
import React from 'react';
import {Column, Columns, RadioGroup} from "@kube-design/components";
import {Select, Tag} from "@kubed/components"
import useAddNodeFormContext from "../../../hooks/useAddNodeFormContext";
const AddNodeEtcdSetting = () => {
const { curCluster, setCurCluster } = useAddNodeFormContext()
const ETCDTypeOptions = [{
value: 'kubekey',
label: 'kubekey'
}]
const ETCDChangeHandler = (e) => {
setCurCluster(prev=>{
const newCluster = {...prev}
newCluster.spec.roleGroups.etcd = e
return newCluster
})
}
const ETCDTypeChangeHandler = e => {
setCurCluster(prev=>{
const newCluster = {...prev}
newCluster.spec.etcd.type = e
return newCluster
})
}
const ETCDOptionContent = (item) => {
return (
<Select.Option key={item.name} value={item.name} label={item.name}>
<div style={{display:`flex`}}>
<div style={{width:"200px"}}>{item.name}</div>
<div style={{display:`flex`}}>
{curCluster.spec.roleGroups.master.includes(item.name) && <Tag style={{marginRight:"10px"}} color="error">MASTER</Tag>}
{curCluster.spec.roleGroups.worker.includes(item.name) && <Tag color="secondary">WORKER</Tag>}
</div>
</div>
</Select.Option>
)
}
return (
<div>
<Columns>
<Column className={'is-2'}>ETCD部署节点</Column>
<Column>
<div style={{display:`flex`}}>
<Select style={{minWidth:'400px'}} value={curCluster.spec.roleGroups.etcd} onChange={ETCDChangeHandler} placeholder="请选择ETCD部署节点" mode="multiple" showSearch allowClear showArrow optionLabelProp="label">
{curCluster.spec.hosts.map(host=>ETCDOptionContent(host))}
</Select>
</div>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>ETCD类型</Column>
<Column >
<RadioGroup options={ETCDTypeOptions} value={curCluster.spec.etcd.type} onChange={ETCDTypeChangeHandler} />
</Column>
</Columns>
</div>
)
};
export default AddNodeEtcdSetting;

View File

@ -0,0 +1,66 @@
import React from 'react';
import {Button, InputSearch, Pagination, Table} from "@kube-design/components";
import AddNodeTableDataWrapper from "./AddNodeTableDataWrapper";
const AddNodeTable = () => {
return (
<AddNodeTableDataWrapper>
{({
fetchList,
list: {
pagination,
filters,
sorter,
data,
isLoading,
// selectedRowKeys
},
setSelectedRowKeys,
columns
}) => {
// const rowSelection = {
// // selectedRowKeys,
// onSelect: (record, checked, rowKeys) => {
// setSelectedRowKeys(rowKeys);
// },
// onSelectAll: (checked, rowKeys) => {
// setSelectedRowKeys(rowKeys);
// },
// getCheckboxProps: record => ({
// // disabled: record.name === 'node3'
// })
// };
const title = <div style={{
display: "flex"
}}>
<InputSearch style={{
flex: 1
}} placeholder="please input a word" onSearch={name => fetchList({
name
})} />
<Button style={{
marginLeft: 12
}} icon="refresh" type="flat" onClick={() => fetchList({
pagination,
filters,
sorter
})} />
</div>;
const footer = <Pagination {...pagination} onChange={page => fetchList({
pagination: { ...pagination,
page
},
filters,
sorter
})} />;
// console.log("data is :",data)
return <Table rowKey="name" columns={columns} filters={filters} sorter={sorter} dataSource={data} loading={isLoading} title={title} footer={footer} onChange={(filters, sorter) => fetchList({
filters,
sorter
})} />;
}}
</AddNodeTableDataWrapper>
)
}
export default AddNodeTable;

View File

@ -0,0 +1,116 @@
import React, {useEffect, useState} from 'react';
import {Tag} from "@kube-design/components";
import {sortBy} from "lodash";
import useAddNodeFormContext from "../../../hooks/useAddNodeFormContext";
const AddNodeTableDataWrapper = ({ children }) => {
const {curCluster} = useAddNodeFormContext();
const [initialData,setInitialData] = useState([])
const [initialColumns,setInitialColumns]=useState([])
useEffect(()=>{
if(Object.keys(curCluster).length > 0){
setInitialData(curCluster.spec.hosts)
setInitialColumns([
{
children: [
{ title: 'Name', dataIndex: 'name', sorter: true, search: true ,width: '10%'},
{ title: 'Address', dataIndex: 'address', width: '10%' },
{ title: 'InternalAddress', dataIndex: 'internalAddress', width: '13%' },
{
title: '角色',
dataIndex: 'role',
width: '20%',
search: true,
render:roleColumn
},
{ title: '用户名', dataIndex: 'user', width: '12%' },
{ title: '密码', dataIndex: 'password', width: '15%' },
{ title: 'id_rsa路径', dataIndex: 'privateKeyPath', width: '20%' },
],
}
])
}
},[curCluster])
useEffect(() => {
if(initialData.length>0){
fetchList();
}
}, [initialData]);
const roleColumn = (_,record) => {
return(
<div style={{display: `flex`}}>
{curCluster.spec.roleGroups.master.includes(record.name) && <Tag type="warning">MASTER</Tag>}
{curCluster.spec.roleGroups.master.includes(record.name) && <div style={{width:'10px'}}/>}
{curCluster.spec.roleGroups.worker.includes(record.name) && <Tag type="primary">WORKER</Tag>}
</div>
)
}
const [list, setList] = useState({
data: [],
isLoading: false,
selectedRowKeys: [],
filters: {},
sorter: {},
pagination: { page: 1, total: 0, limit: 10 },
});
// const setSelectedRowKeys = (value) => {
// setList((prevState) => ({ ...prevState, selectedRowKeys: value }));
// console.log('setSelectedRowKeys',value)
// };
const fetchList = ({ name, pagination = {}, filters = {}, sorter = {} } = {}) => {
setList((prevState) => ({ ...prevState, isLoading: true }));
setTimeout(() => {
let data = [...initialData];
if (name) {
data = data.filter((item) => item.name.indexOf(name) !== -1);
}
const filterKeys = Object.keys(filters);
if (filterKeys.length > 0) {
data = data.filter((item) =>
filterKeys.every((key) => filters[key] === item[key])
);
}
if (sorter.field && sorter.order) {
data = sortBy(data, [sorter.field]);
if (sorter.order === 'descend') {
data = data.reverse();
}
}
const total = data.length;
const { page = 1, limit = 10 } = pagination;
data = data.slice((page - 1) * limit, page * limit);
setList({
data,
filters,
sorter,
pagination: { total, page, limit },
isLoading: false,
selectedRowKeys: [],
});
}, 300);
};
return (
<div>
{children({
list,
columns: initialColumns,
fetchList,
// setSelectedRowKeys,
})}
</div>
);
}
export default AddNodeTableDataWrapper;

View File

@ -0,0 +1,31 @@
import React, {useRef} from 'react';
import useAddNodeFormContext from "../../../hooks/useAddNodeFormContext";
const ConfirmAddNodeSetting = () => {
const logContainerRef = useRef(null);
const { logs} = useAddNodeFormContext();
return (
<div>
<div ref={logContainerRef} style={{
backgroundColor: '#1e1e1e',
color: '#ffffff',
padding: '10px',
borderRadius: '5px',
maxHeight: '500px',
maxWidth: '850px',
overflowY: 'scroll',
fontFamily: 'Consolas, "Courier New", monospace',
fontSize: '14px',
lineHeight: '1.5'
}}>
{logs.map((log, index) => (
<div key={index} style={{ whiteSpace: 'pre-wrap' }}>
{log}
</div>
))}
</div>
</div>
);
};
export default ConfirmAddNodeSetting;

View File

@ -0,0 +1,14 @@
import React from 'react';
import AddNodeTable from "./AddNodeTable";
import AddNodeModal from "../../Modal/AddNodeModal";
const InputNodeInfoSetting = () => {
return (
<div>
<AddNodeModal ></AddNodeModal>
<AddNodeTable />
</div>
);
};
export default InputNodeInfoSetting;

View File

@ -0,0 +1,39 @@
import React from 'react';
import {Button, Column, Columns} from "@kube-design/components";
import {Link} from "react-router-dom";
import ClusterTable from "./ClusterTable";
import {ClusterTableProvider} from "../../context/ClusterTableContext";
const Cluster = () => {
return (
<div>
<Columns>
<Column className={'is-1'}></Column>
<Column className={'is-2'}>
<h2>集群列表</h2>
</Column>
<Column className={'is-8'}>
<Columns>
<Column className={'is-10'}>
</Column>
<Column>
<Link to='/install'>
<Button>安装集群</Button>
</Link>
</Column>
</Columns>
</Column>
</Columns>
<Columns>
<Column className={'is-1'}></Column>
<Column className={'is-10'}>
<ClusterTableProvider>
<ClusterTable/>
</ClusterTableProvider>
</Column>
</Columns>
</div>
);
};
export default Cluster;

View File

@ -0,0 +1,63 @@
import React from 'react';
import {Button, InputSearch, Pagination, Table} from "@kube-design/components";
import ClusterTableDataWrapper from "./ClusterTableDataWrapper";
import EmbeddedNodeTable from "./EmbeddedNodeTable";
import useClusterTableContext from "../../hooks/useClusterTableContext";
const ClusterTable = () => {
const {clusterData} = useClusterTableContext()
const embeddedNodeTable= record => {
const curClusterData = record
return <EmbeddedNodeTable curClusterData={curClusterData}/>
}
return (
<div>
<ClusterTableDataWrapper clusterData={clusterData}>
{({
fetchList,
list: {
pagination,
filters,
sorter,
data,
isLoading,
selectedRowKeys
},
setSelectedRowKeys,
columns
}) => {
const title = <div style={{
display: "flex"
}}>
<InputSearch style={{
flex: 1
}} placeholder="please input a word" onSearch={name => fetchList({
name
})} />
<Button style={{
marginLeft: 12
}} icon="refresh" type="flat" onClick={() => fetchList({
pagination,
filters,
sorter
})} />
</div>;
const footer = <Pagination {...pagination} onChange={page => fetchList({
pagination: { ...pagination,
page
},
filters,
sorter
})} />;
return <Table rowKey="name" columns={columns} filters={filters} sorter={sorter} dataSource={data} loading={isLoading} title={title} footer={footer} onChange={(filters, sorter) => fetchList({
filters,
sorter
})} expandedRowRender={embeddedNodeTable} />;
}}
</ClusterTableDataWrapper>
</div>
)
};
export default ClusterTable;

View File

@ -0,0 +1,144 @@
import React, { useState, useEffect } from 'react';
import { sortBy } from 'lodash';
import {Button, Dropdown,Menu} from "@kube-design/components";
import {Link} from "react-router-dom";
const ClusterTableDataWrapper= ({ children,clusterData }) => {
const autoRenewCertColumn = (_,record) => {
return(
<div style={{display: `flex`}}>
{record.spec.kubernetes.autoRenewCerts && <div></div>}
{!record.spec.kubernetes.autoRenewCerts && <div></div>}
</div>
)
}
const MenuColumn = (_, record) => {
return <Dropdown content={
<Menu>
<Menu.MenuItem key="upgradeCluster" >
<Link to={`/UpgradeCluster/${record.metadata.name}`}>
升级集群
</Link>
</Menu.MenuItem>
<Menu.MenuItem key="deleteCluster" >
<Link to={`/DeleteCluster/${record.metadata.name}`}>
删除集群
</Link>
</Menu.MenuItem>
<Menu.MenuItem key="addNode">
<Link to={`/addNode/${record.metadata.name}`}>
增加节点
</Link>
</Menu.MenuItem>
<Menu.MenuItem key="deleteNode">
<Link to={`/deleteNode/${record.metadata.name}`}>
删除节点
</Link>
</Menu.MenuItem>
</Menu>}>
<Button type="control" size='small'>操作</Button>
</Dropdown>
}
const storageColumn = (_,record) =>{
if(record.spec && record.spec.storage && record.spec.storage.openebs !== undefined) {
return <p>开启</p>
} else {
return <p>关闭</p>
}
}
const initialColumns = [
{
children: [
{ title: '集群名', width: '14%',dataIndex: 'metadata.name', sorter: true, search: true },
{ title: '节点数', width: '8%',render:(_, record) => record.spec.hosts.length},
{ title: 'Kubernetes 版本', dataIndex: 'spec.kubernetes.version', width: '13%' },
{
title: '自动续费证书',
width: '14%',
dataIndex: 'spec.kubernetes.autoRenewCert',
// filters: [
// { text: '是', value: true },
// { text: '否', value: false },
// ],
search: true,
render:autoRenewCertColumn
},
// {title: '证书有效期', dataIndex: '', width: '14%',render:certExpiraionColumn},
{title: '网络插件', dataIndex: 'spec.network.plugin', width: '13%'},
{title: '容器运行时', dataIndex: 'spec.kubernetes.containerManager', width: '13%'},
{title: '本地存储', width: '18%',render:storageColumn},
{title: '操作', dataIndex: '', width: '18%', render: MenuColumn},
],
},
];
const [list, setList] = useState({
data: [],
isLoading: false,
selectedRowKeys: [],
filters: {},
sorter: {},
pagination: { page: 1, total: 0, limit: 10 },
});
useEffect(() => {
fetchList();
}, [clusterData]);
const setSelectedRowKeys = (value) => {
setList((prevState) => ({ ...prevState, selectedRowKeys: value }));
};
const fetchList = ({ name, pagination = {}, filters = {}, sorter = {} } = {}) => {
setList((prevState) => ({ ...prevState, isLoading: true }));
setTimeout(() => {
let data = [...clusterData];
if (name) {
data = data.filter((item) => item.clusterName.indexOf(name) !== -1);
}
const filterKeys = Object.keys(filters);
if (filterKeys.length > 0) {
data = data.filter((item) =>
filterKeys.every((key) => filters[key] === item[key])
);
}
if (sorter.field && sorter.order) {
data = sortBy(data, [sorter.field]);
if (sorter.order === 'descend') {
data = data.reverse();
}
}
const total = data.length;
const { page = 1, limit = 10 } = pagination;
data = data.slice((page - 1) * limit, page * limit);
setList({
data,
filters,
sorter,
pagination: { total, page, limit },
isLoading: false,
});
}, 300);
};
return (
<div>
{children({
list,
columns: initialColumns,
fetchList,
setSelectedRowKeys,
})}
</div>
);
}
export default ClusterTableDataWrapper;

View File

@ -0,0 +1,52 @@
import {Table, Button, InputSearch, Pagination} from "@kube-design/components";
import EmbeddedNodeTableDataWrapper from "./EmbeddedNodeTableDataWrapper";
const EmbeddedNodeTable = ({curClusterData}) => {
return (
<EmbeddedNodeTableDataWrapper curClusterData={curClusterData} >
{({
fetchList,
list: {
pagination,
filters,
sorter,
data,
isLoading,
// selectedRowKeys
},
// setSelectedRowKeys,
columns
}) => {
const title = <div style={{
display: "flex"
}}>
<InputSearch style={{
flex: 1
}} placeholder="输入节点名搜索" onSearch={name => fetchList({
name
})} />
<Button style={{
marginLeft: 12
}} icon="refresh" type="flat" onClick={() => fetchList({
pagination,
filters,
sorter
})} />
</div>;
const footer = <Pagination {...pagination} onChange={page => fetchList({
pagination: { ...pagination,
page
},
filters,
sorter
})} />;
return <Table rowKey="name" columns={columns} filters={filters} sorter={sorter} dataSource={data} loading={isLoading} title={title} footer={footer} onChange={(filters, sorter) => fetchList({
filters,
sorter
})} />;
}}
</EmbeddedNodeTableDataWrapper>
)
}
export default EmbeddedNodeTable

View File

@ -0,0 +1,106 @@
import React, { useState, useEffect } from 'react';
import { sortBy } from 'lodash';
import {Tag} from "@kube-design/components";
const EmbeddedNodeTableDataWrapper= ({ children,curClusterData }) => {
const initialData = curClusterData.spec.hosts
const roleColumn = (_,record) => {
return(
<div style={{display: `flex`}}>
{curClusterData.spec.roleGroups.master.includes(record.name) && <Tag type="warning">MASTER</Tag>}
{curClusterData.spec.roleGroups.master.includes(record.name) && <div style={{width:'10px'}}/>}
{curClusterData.spec.roleGroups.worker.includes(record.name) && <Tag type="primary">WORKER</Tag>}
</div>
)
}
const initialColumns = [
{
children: [
{ title: 'Name', dataIndex: 'name',width:'13%', sorter: true, search: true },
{ title: 'Address', dataIndex: 'address', width: '12%' },
{ title: 'InternalAddress', dataIndex: 'internalAddress', width: '12%' },
{
title: '角色',
dataIndex: 'role',
width: '15%',
// filters: [
// { text: 'MASTER', value: 'Master' },
// { text: 'WORKER', value: ['Worker'] },
// ],
search: true,
render:roleColumn
},
{ title: '用户名', dataIndex: 'user', width: '12%' },
{ title: '密码', dataIndex: 'password', width: '15%' },
{ title: 'id_rsa路径', dataIndex: 'privateKeyPath', width: '20%' },
],
},
];
const [list, setList] = useState({
data: [],
isLoading: false,
selectedRowKeys: [],
filters: {},
sorter: {},
pagination: { page: 1, total: 0, limit: 10 },
});
useEffect(() => {
fetchList();
}, [curClusterData.spec.hosts]);
const setSelectedRowKeys = (value) => {
setList((prevState) => ({ ...prevState, selectedRowKeys: value }));
};
const fetchList = ({ name, pagination = {}, filters = {}, sorter = {} } = {}) => {
setList((prevState) => ({ ...prevState, isLoading: true }));
setTimeout(() => {
let data = [...initialData];
if (name) {
data = data.filter((item) => item.nodeName.indexOf(name) !== -1);
}
const filterKeys = Object.keys(filters);
if (filterKeys.length > 0) {
data = data.filter((item) =>
filterKeys.every((key) => filters[key] === item[key])
);
}
if (sorter.field && sorter.order) {
data = sortBy(data, [sorter.field]);
if (sorter.order === 'descend') {
data = data.reverse();
}
}
const total = data.length;
const { page = 1, limit = 5 } = pagination;
data = data.slice((page - 1) * limit, page * limit);
setList({
data,
filters,
sorter,
pagination: { total, page, limit },
isLoading: false,
});
}, 300);
};
return (
<div>
{children({
list,
columns: initialColumns,
fetchList,
setSelectedRowKeys,
})}
</div>
);
}
export default EmbeddedNodeTableDataWrapper;

View File

@ -0,0 +1,54 @@
import React, {useEffect} from 'react';
import {Link, useParams} from "react-router-dom";
import useClusterTableContext from "../../hooks/useClusterTableContext";
import {Button, Column, Columns} from "@kube-design/components";
import useDeleteClusterContext from "../../hooks/useDeleteClusterContext";
import DeleteClusterProgressBar from "./DeleteClusterProgressBar";
import DeleteClusterForm from "./DeleteClusterForm";
const DeleteCluster = () => {
const {clusterName} = useParams()
const {clusterData} = useClusterTableContext()
const {setCurCluster,canToHome} = useDeleteClusterContext()
useEffect(() => {
if (clusterData.length > 0) {
setCurCluster(clusterData.find(item=>item.metadata.name===clusterName))
}
}, [clusterData]);
return (
<>
<Columns>
<Column className="is-1"></Column>
<Column className="is-2">
<h2>删除集群</h2>
</Column>
<Column className={'is-8'}>
<Columns>
<Column className={'is-10'}>
</Column>
<Column>
{canToHome ? (
<Link to='/'>
<Button disabled={!canToHome}>集群列表</Button>
</Link>
) : (
<Button disabled={!canToHome}>集群列表</Button>
)}
</Column>
</Columns>
</Column>
</Columns>
<Columns>
<Column className={'is-1'}></Column>
<Column className={'is-2'}>
<DeleteClusterProgressBar></DeleteClusterProgressBar>
</Column>
<Column className={'is-8'}>
<DeleteClusterForm/>
</Column>
</Columns>
</>
);
};
export default DeleteCluster;

View File

@ -0,0 +1,95 @@
import React from 'react';
import {Button, Column, Columns} from "@kube-design/components";
import {Modal} from "@kubed/components";
import useDeleteClusterContext from "../../hooks/useDeleteClusterContext";
import DeleteClusterFormInputs from "./DeleteClusterFormInputs";
const DeleteClusterForm = () => {
const [visible, setVisible] = React.useState(false);
const ref = React.createRef();
const openModal = () => {
setVisible(true);
};
const closeModal = () => {
setVisible(false);
};
const onOKHandler = () => {
setVisible(false);
}
const textStyle={
fontSize:"15px",
minHeight: '50px',
margin: 0, /* 清除默认的外边距 */
display: 'flex',
alignItems: 'center'
}
const {
page,
setPage,
title,
canSubmit,
deleteHandler,
disableNext,
disablePrev
} = useDeleteClusterContext()
const handlePrev = () => {
setPage(prev => {
return +prev-1
})
}
const handleNext = () => {
setPage(prev => {
return +prev+1
})
}
return (
<>
<Columns>
<Column className='is-10'>
<h3>{title[page]}</h3>
</Column>
</Columns>
<Columns>
<Column>
<DeleteClusterFormInputs/>
</Column>
</Columns>
<Columns>
<Column className='is-8'/>
<Column className='is-2'>
<Columns>
<Column className='is-5'/>
<Column>
{page !== 0 && <Button onClick={handlePrev} disabled={disablePrev}>上一步</Button>}
</Column>
</Columns>
</Column>
<Column className='is-1'>
{page !== Object.keys(title).length - 1 && <Button onClick={handleNext} disabled={disableNext}>下一步</Button>}
{page === Object.keys(title).length - 1 && <Button disabled={!canSubmit} onClick={()=>{deleteHandler(); openModal();}} >删除</Button>}
<Modal
ref={ref}
visible={visible}
title="开始删除集群"
onCancel={closeModal}
onOk={onOKHandler}
>
<Columns>
<Column style={{display:`flex`, alignItems: 'center' }}>
<p style={textStyle}>删除集群已开始关闭该提示后可查看实时日志期间请勿进行其他操作</p>
</Column>
</Columns>
</Modal>
</Column>
</Columns>
</>
);
};
export default DeleteClusterForm;

View File

@ -0,0 +1,21 @@
import React from 'react';
import useDeleteClusterContext from "../../hooks/useDeleteClusterContext";
import ConfirmDeleteClusterSetting from "./DeleteClusterSettings/ConfirmDeleteClusterSetting";
import DeleteCriSetting from "./DeleteClusterSettings/DeleteCRISetting";
const DeleteClusterFormInputs = () => {
const { page } = useDeleteClusterContext()
const display = {
0: <DeleteCriSetting/>,
1: <ConfirmDeleteClusterSetting/>,
}
return (
<div>
{display[page]}
</div>
)
};
export default DeleteClusterFormInputs;

View File

@ -0,0 +1,69 @@
import React from 'react';
import {Button} from "@kubed/components";
import useDeleteClusterContext from "../../hooks/useDeleteClusterContext";
const DeleteClusterProgressBar = () => {
const {page,setPage,title,buttonDisabled} = useDeleteClusterContext();
const ongoingIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
backgroundColor: '#1C8FFC', // 背景色为蓝色
color: 'white', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
};
const finishedIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
border: '1px solid #7eb8dc',
backgroundColor: 'white', // 背景色为蓝色
color: '#7eb8dc', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
};
const unfinishedIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
border: '1px solid #abb4be',
backgroundColor: 'white', // 背景色为蓝色
color: '#abb4be', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
}
const changePageHandler= e => {
const index = Object.keys(title).find(key => title[key] === e.target.innerText)
setPage(index)
}
const IndexCircleItem = (step) => {
if(step<page) return <div style={finishedIndexCircleStyle}></div>
else if(step===page) return <div style={ongoingIndexCircleStyle}>{step+1}</div>
else return <div style={unfinishedIndexCircleStyle}>{step+1}</div>
}
const steps = Object.keys(title).map((step,index) => {
return (
<div style={{display:`flex`, alignItems: 'center' }} key={index}>
{IndexCircleItem(+step)}
<Button style={{height:'50px'}} variant="link" color="default" onClick={changePageHandler} disabled={+step>page || buttonDisabled}>{title[step]}</Button>
</div>
)
})
return (
<div>
{steps}
</div>
);
};
export default DeleteClusterProgressBar;

View File

@ -0,0 +1,31 @@
import React, {useRef} from 'react';
import useDeleteClusterContext from "../../../hooks/useDeleteClusterContext";
const ConfirmDeleteClusterSetting = () => {
const {logs} = useDeleteClusterContext();
const logContainerRef = useRef(null);
return (
<div>
<div ref={logContainerRef} style={{
backgroundColor: '#1e1e1e',
color: '#ffffff',
padding: '10px',
borderRadius: '5px',
maxHeight: '500px',
maxWidth: '850px',
overflowY: 'scroll',
fontFamily: 'Consolas, "Courier New", monospace',
fontSize: '14px',
lineHeight: '1.5'
}}>
{logs.map((log, index) => (
<div key={index} style={{ whiteSpace: 'pre-wrap' }}>
{log}
</div>
))}
</div>
</div>
);
};
export default ConfirmDeleteClusterSetting;

View File

@ -0,0 +1,24 @@
import React from 'react';
import useDeleteClusterContext from "../../../hooks/useDeleteClusterContext";
import {Column, Columns, Toggle} from "@kube-design/components";
const DeleteCriSetting = () => {
const { deleteCRI,setDeleteCRI } = useDeleteClusterContext()
const deleteCRIHandler = () => {
setDeleteCRI(prev=>!prev)
}
return (
<>
<Columns>
<Column>
是否删除CRI:
</Column>
<Column>
<Toggle checked={deleteCRI} onChange={deleteCRIHandler} onText="是" offText="否"></Toggle>
</Column>
</Columns>
</>
);
};
export default DeleteCriSetting;

View File

@ -0,0 +1,54 @@
import React, {useEffect} from 'react';
import {Link, useParams} from "react-router-dom";
import {Button, Column, Columns} from "@kube-design/components";
import DeleteNodeProgressBar from "./DeleteNodeProgressBar";
import DeleteNodeForm from "./DeleteNodeForm";
import useDeleteNodeFormContext from "../../hooks/useDeleteNodeFormContext";
import useClusterTableContext from "../../hooks/useClusterTableContext";
const DeleteNode = () => {
const {clusterName} = useParams()
const {clusterData} = useClusterTableContext()
const {setCurCluster,canToHome} = useDeleteNodeFormContext()
useEffect(() => {
if (clusterData.length > 0) {
setCurCluster(clusterData.find(item=>item.metadata.name===clusterName))
}
}, [clusterData]);
return (
<>
<Columns>
<Column className="is-1"></Column>
<Column className="is-2">
<h2>删除节点</h2>
</Column>
<Column className={'is-8'}>
<Columns>
<Column className={'is-10'}>
</Column>
<Column>
{canToHome ? (
<Link to='/'>
<Button disabled={!canToHome}>集群列表</Button>
</Link>
) : (
<Button disabled={!canToHome}>集群列表</Button>
)}
</Column>
</Columns>
</Column>
</Columns>
<Columns>
<Column className={'is-1'}></Column>
<Column className={'is-2'}>
<DeleteNodeProgressBar></DeleteNodeProgressBar>
</Column>
<Column className={'is-8'}>
<DeleteNodeForm/>
</Column>
</Columns>
</>
);
};
export default DeleteNode;

View File

@ -0,0 +1,95 @@
import React from 'react';
import useDeleteNodeFormContext from "../../hooks/useDeleteNodeFormContext";
import {Button, Column, Columns} from "@kube-design/components";
import DeleteNodeFormInputs from "./DeleteNodeFormInputs";
import {Modal} from "@kubed/components";
const DeleteNodeForm = () => {
const [visible, setVisible] = React.useState(false);
const ref = React.createRef();
const openModal = () => {
setVisible(true);
};
const closeModal = () => {
setVisible(false);
};
const onOKHandler = () => {
setVisible(false);
}
const textStyle={
fontSize:"15px",
minHeight: '50px',
margin: 0, /* 清除默认的外边距 */
display: 'flex',
alignItems: 'center'
}
const {
page,
setPage,
title,
canSubmit,
disablePrev,
disableNext,
deleteHandler
} = useDeleteNodeFormContext()
const handlePrev = () => {
setPage(prev => {
return +prev-1
})
}
const handleNext = () => {
setPage(prev => {
return +prev+1
})
}
return (
<>
<Columns>
<Column className='is-10'>
<h3>{title[page]}</h3>
</Column>
</Columns>
<Columns>
<Column>
<DeleteNodeFormInputs/>
</Column>
</Columns>
<Columns>
<Column className='is-8'/>
<Column className='is-2'>
<Columns>
<Column className='is-5'/>
<Column>
{page !== 0 && <Button onClick={handlePrev} disabled={disablePrev}>上一步</Button>}
</Column>
</Columns>
</Column>
<Column className='is-1'>
{page !== Object.keys(title).length - 1 && <Button onClick={handleNext} disabled={disableNext}>下一步</Button>}
{page === Object.keys(title).length - 1 && <Button onClick={()=>{deleteHandler(); openModal();}} disabled={!canSubmit} >删除</Button>}
<Modal
ref={ref}
visible={visible}
title="开始删除节点"
onCancel={closeModal}
onOk={onOKHandler}
>
<Columns>
<Column style={{display:`flex`, alignItems: 'center' }}>
<p style={textStyle}>删除节点已开始关闭该提示后可查看实时日志期间请勿进行其他操作</p>
</Column>
</Columns>
</Modal>
</Column>
</Columns>
</>
);
};
export default DeleteNodeForm;

View File

@ -0,0 +1,21 @@
import React from 'react';
import useDeleteNodeFormContext from "../../hooks/useDeleteNodeFormContext";
import SelectNodeSetting from "./DeleteNodeSettings/SelectNodeSetting/SelectNodeSetting";
import ConfirmDeleteNodeSetting from "./DeleteNodeSettings/ConfirmDeleteNodeSetting";
const DeleteNodeFormInputs = () => {
const { page } = useDeleteNodeFormContext()
const display = {
0: <SelectNodeSetting />,
1: <ConfirmDeleteNodeSetting />
}
return (
<div>
{display[page]}
</div>
)
};
export default DeleteNodeFormInputs;

View File

@ -0,0 +1,70 @@
import React from 'react';
import {Button} from "@kubed/components";
import useDeleteNodeFormContext from "../../hooks/useDeleteNodeFormContext";
const DeleteNodeProgressBar = () => {
const {page,setPage,title,buttonDisabled} = useDeleteNodeFormContext()
const ongoingIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
backgroundColor: '#1C8FFC', // 背景色为蓝色
color: 'white', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
};
const finishedIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
border: '1px solid #7eb8dc',
backgroundColor: 'white', // 背景色为蓝色
color: '#7eb8dc', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
};
const unfinishedIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
border: '1px solid #abb4be',
backgroundColor: 'white', // 背景色为蓝色
color: '#abb4be', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
}
const changePageHandler= e => {
const index = Object.keys(title).find(key => title[key] === e.target.innerText)
setPage(index)
}
const IndexCircleItem = (step) => {
if(step<page) return <div style={finishedIndexCircleStyle}></div>
else if(step===page) return <div style={ongoingIndexCircleStyle}>{step+1}</div>
else return <div style={unfinishedIndexCircleStyle}>{step+1}</div>
}
const steps = Object.keys(title).map((step,index) => {
return (
<div style={{display:`flex`, alignItems: 'center' }} key={index}>
{IndexCircleItem(+step)}
<Button style={{height:'50px'}} variant="link" color="default" onClick={changePageHandler} disabled={+step>page || buttonDisabled}>{title[step]}</Button>
</div>
)
})
return (
<div>
{steps}
</div>
);
};
export default DeleteNodeProgressBar;

View File

@ -0,0 +1,32 @@
import React, {useRef} from 'react';
import useDeleteNodeFormContext from "../../../hooks/useDeleteNodeFormContext";
const ConfirmDeleteNodeSetting = () => {
const {logs} = useDeleteNodeFormContext();
const logContainerRef = useRef(null);
return (
<div>
<div ref={logContainerRef} style={{
backgroundColor: '#1e1e1e',
color: '#ffffff',
padding: '10px',
borderRadius: '5px',
maxHeight: '500px',
maxWidth: '850px',
overflowY: 'scroll',
fontFamily: 'Consolas, "Courier New", monospace',
fontSize: '14px',
lineHeight: '1.5'
}}>
{logs.map((log, index) => (
<div key={index} style={{ whiteSpace: 'pre-wrap' }}>
{log}
</div>
))}
</div>
</div>
);
};
export default ConfirmDeleteNodeSetting;

View File

@ -0,0 +1,65 @@
import {Table, Button, InputSearch, Pagination} from "@kube-design/components";
import DeleteNodeTableDataWrapper from "./DeleteNodeTableDataWrapper";
import useDeleteNodeFormContext from "../../../../hooks/useDeleteNodeFormContext";
const DeleteNodeTable = () => {
const {curSelectedNodeName} = useDeleteNodeFormContext();
return (
<DeleteNodeTableDataWrapper >
{({
fetchList,
list: {
pagination,
filters,
sorter,
data,
isLoading,
selectedRowKeys
},
setSelectedRowKeys,
columns
}) => {
const rowSelection = {
selectedRowKeys,
onSelect: (record, checked, rowKeys) => {
setSelectedRowKeys(rowKeys);
},
onSelectAll: (checked, rowKeys) => {
// setSelectedRowKeys(rowKeys);
},
getCheckboxProps: record => ({
disabled: record.name!==curSelectedNodeName && curSelectedNodeName!==''
})
};
const title = <div style={{
display: "flex"
}}>
<InputSearch style={{
flex: 1
}} placeholder="please input a word" onSearch={name => fetchList({
name
})} />
<Button style={{
marginLeft: 12
}} icon="refresh" type="flat" onClick={() => fetchList({
pagination,
filters,
sorter
})} />
</div>;
const footer = <Pagination {...pagination} onChange={page => fetchList({
pagination: { ...pagination,
page
},
filters,
sorter
})} />;
return <Table rowKey="name" columns={columns} filters={filters} sorter={sorter} dataSource={data} loading={isLoading} title={title} footer={footer} rowSelection={rowSelection} onChange={(filters, sorter) => fetchList({
filters,
sorter
})} />;
}}
</DeleteNodeTableDataWrapper>
)
}
export default DeleteNodeTable

View File

@ -0,0 +1,119 @@
import React, { useState, useEffect } from 'react';
import { sortBy } from 'lodash';
import {Tag} from "@kube-design/components";
import useDeleteNodeFormContext from "../../../../hooks/useDeleteNodeFormContext";
const DeleteNodeTableDataWrapper= ({ children }) => {
const {curCluster, setCurSelectedNodeName} = useDeleteNodeFormContext();
const [initialData,setInitialData] = useState([])
const [initialColumns,setInitialColumns]=useState([])
useEffect(()=>{
if(Object.keys(curCluster).length > 0) {
setInitialData(curCluster.spec.hosts)
setInitialColumns([
{
children: [
{title: 'Name', dataIndex: 'name', sorter: true, search: true},
{title: 'Address', dataIndex: 'address', width: '15%'},
{title: 'InternalAddress', dataIndex: 'internalAddress', width: '15%'},
{
title: '角色',
dataIndex: 'role',
width: '20%',
search: true,
render: roleColumn
},
],
},
])
}
},[curCluster])
useEffect(()=>{
setCurSelectedNodeName('')
},[])
useEffect(() => {
if(initialData.length>0){
fetchList();
}
}, [initialData]);
const [list, setList] = useState({
data: [],
isLoading: false,
selectedRowKeys: [],
filters: {},
sorter: {},
pagination: { page: 1, total: 0, limit: 10 },
});
const roleColumn = (_,record) => {
return(
<div style={{display: `flex`}}>
{curCluster.spec.roleGroups.master.includes(record.name) && <Tag type="warning">MASTER</Tag>}
{curCluster.spec.roleGroups.master.includes(record.name) && <div style={{width:'10px'}}/>}
{curCluster.spec.roleGroups.worker.includes(record.name) && <Tag type="primary">WORKER</Tag>}
</div>
)
}
const setSelectedRowKeys = (value) => {
setList((prevState) => ({ ...prevState, selectedRowKeys: value }));
setCurSelectedNodeName(value.length>0?value[0]:'')
};
//
const fetchList = ({ name, pagination = {}, filters = {}, sorter = {} } = {}) => {
setList((prevState) => ({ ...prevState, isLoading: true }));
setTimeout(() => {
let data = [...initialData];
if (name) {
data = data.filter((item) => item.name.indexOf(name) !== -1);
}
const filterKeys = Object.keys(filters);
if (filterKeys.length > 0) {
data = data.filter((item) =>
filterKeys.every((key) => filters[key] === item[key])
);
}
if (sorter.field && sorter.order) {
data = sortBy(data, [sorter.field]);
if (sorter.order === 'descend') {
data = data.reverse();
}
}
const total = data.length;
const { page = 1, limit = 10 } = pagination;
data = data.slice((page - 1) * limit, page * limit);
setList({
data,
filters,
sorter,
pagination: { total, page, limit },
isLoading: false,
selectedRowKeys: [],
});
}, 300);
};
//
return (
<div>
{children({
list,
columns: initialColumns,
fetchList,
setSelectedRowKeys,
})}
</div>
);
}
export default DeleteNodeTableDataWrapper;

View File

@ -0,0 +1,12 @@
import React from 'react';
import DeleteNodeTable from "./DeleteNodeTable";
const SelectNodeSetting = () => {
return (
<div>
<DeleteNodeTable></DeleteNodeTable>
</div>
);
};
export default SelectNodeSetting;

View File

@ -0,0 +1,46 @@
import React from 'react';
import {Button, Column, Columns} from "@kube-design/components";
import InstallProgressBar from "./InstallProgressBar";
import InstallForm from "./InstallForm";
import {Link} from "react-router-dom";
import useInstallFormContext from "../../hooks/useInstallFormContext";
const Install = () => {
const {canToHome} = useInstallFormContext()
return (
<>
<Columns>
<Column className="is-1"></Column>
<Column className="is-2">
<h2>安装集群</h2>
</Column>
<Column className={'is-8'}>
<Columns>
<Column className={'is-10'}>
</Column>
<Column>
{canToHome ? (
<Link to='/'>
<Button disabled={!canToHome}>集群列表</Button>
</Link>
) : (
<Button disabled={!canToHome}>集群列表</Button>
)}
</Column>
</Columns>
</Column>
</Columns>
<Columns>
<Column className={'is-1'}></Column>
<Column className={'is-2'}>
<InstallProgressBar></InstallProgressBar>
</Column>
<Column className={'is-8'}>
<InstallForm />
</Column>
</Columns>
</>
);
};
export default Install;

View File

@ -0,0 +1,96 @@
import InstallFormInputs from './InstallFormInputs'
import useInstallFormContext from "../../hooks/useInstallFormContext"
import {Button, Columns, Column} from "@kube-design/components";
import React from "react";
import {Modal} from "@kubed/components";
const InstallForm = () => {
const [visible, setVisible] = React.useState(false);
const ref = React.createRef();
const openModal = () => {
setVisible(true);
};
const closeModal = () => {
setVisible(false);
};
const onOKHandler = () => {
setVisible(false);
}
const textStyle={
fontSize:"15px",
minHeight: '50px',
margin: 0, /* 清除默认的外边距 */
display: 'flex',
alignItems: 'center'
}
const {
page,
setPage,
title,
canSubmit,
disablePrev,
disableNext,
installHandler
} = useInstallFormContext()
const handlePrev = () => {
setPage(prev => {
return +prev-1
})
}
const handleNext = () => {
setPage(prev => {
return +prev+1
})
}
return (
<>
<Columns>
<Column className='is-10'>
<h3>{title[page]}</h3>
</Column>
</Columns>
<Columns>
<Column>
<InstallFormInputs />
</Column>
</Columns>
<Columns>
<Column className='is-8'/>
<Column className='is-2'>
<Columns>
<Column className='is-5'/>
<Column>
{page !== 0 && <Button onClick={handlePrev} disabled={disablePrev}>上一步</Button>}
</Column>
</Columns>
</Column>
<Column className='is-1'>
{page !== Object.keys(title).length - 1 && <Button onClick={handleNext} disabled={disableNext}>下一步</Button>}
{page === Object.keys(title).length - 1 && <Button disabled={!canSubmit} onClick={()=>{installHandler();openModal();}}>安装</Button>}
<Modal
ref={ref}
visible={visible}
title="开始安装集群"
onCancel={closeModal}
onOk={onOKHandler}
>
<Columns>
<Column style={{display:`flex`, alignItems: 'center' }}>
<p style={textStyle}>集群安装已开始关闭该提示后可查看实时日志期间请勿进行其他操作</p>
</Column>
</Columns>
</Modal>
</Column>
</Columns>
</>
)
}
export default InstallForm

View File

@ -0,0 +1,32 @@
import useInstallFormContext from "../../hooks/useInstallFormContext"
import EtcdSetting from "./InstallSettings/ETCDSetting";
import ClusterSetting from "./InstallSettings/ClusterSetting";
import HostSetting from "./InstallSettings/HostSetting/HostSetting";
import NetworkSetting from "./InstallSettings/NetworkSetting";
import StorageSetting from "./InstallSettings/StorageSetting";
import RegistrySetting from "./InstallSettings/RegistrySetting";
import KubesphereSetting from "./InstallSettings/KubesphereSetting";
import ConfirmInstallSetting from "./InstallSettings/ConfirmInstallSetting";
const InstallFormInputs = () => {
const { page } = useInstallFormContext()
const display = {
0: <HostSetting/>,
1: <EtcdSetting/>,
2: <ClusterSetting/>,
3: <NetworkSetting/>,
4: <StorageSetting/>,
5: <RegistrySetting/>,
6: <KubesphereSetting/>,
7: <ConfirmInstallSetting/>
}
return (
<div>
{display[page]}
</div>
)
}
export default InstallFormInputs

View File

@ -0,0 +1,70 @@
import React from 'react';
import useInstallFormContext from "../../hooks/useInstallFormContext";
import {Button} from "@kubed/components";
const InstallProgressBar = () => {
const {page,setPage,title,buttonDisabled} = useInstallFormContext()
const ongoingIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
backgroundColor: '#1C8FFC', // 背景色为蓝色
color: 'white', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
};
const finishedIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
border: '1px solid #7eb8dc',
backgroundColor: 'white', // 背景色为蓝色
color: '#7eb8dc', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
};
const unfinishedIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
border: '1px solid #abb4be',
backgroundColor: 'white', // 背景色为蓝色
color: '#abb4be', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
}
const changePageHandler= e => {
const index = Object.keys(title).find(key => title[key] === e.target.innerText)
setPage(index)
}
const IndexCircleItem = (step) => {
if(step<page) return <div style={finishedIndexCircleStyle}></div>
else if(step===page) return <div style={ongoingIndexCircleStyle}>{step+1}</div>
else return <div style={unfinishedIndexCircleStyle}>{step+1}</div>
}
const steps = Object.keys(title).map((step,index) => {
return (
<div style={{display:`flex`, alignItems: 'center' }} key={index}>
{IndexCircleItem(+step)}
<Button style={{height:'50px'}} variant="link" color="default" onClick={changePageHandler} disabled={+step>page||buttonDisabled}>{title[step]}</Button>
</div>
)
})
return (
<div>
{steps}
</div>
);
};
export default InstallProgressBar;

View File

@ -0,0 +1,91 @@
import React, {useEffect, useState} from 'react';
import useInstallFormContext from "../../../hooks/useInstallFormContext";
import {Column, Input, Columns, Select, Toggle, Radio, Tooltip} from "@kube-design/components";
import useGlobalContext from "../../../hooks/useGlobalContext";
const ClusterSetting = () => {
const {backendIP} = useGlobalContext();
const [clusterVersionOptions,setClusterVersionOptions] = useState([])
const { data, handleChange, KubekeyNamespace, setKubekeyNamespace} = useInstallFormContext()
const changeClusterVersionHandler = e => {
handleChange('spec.kubernetes.version',e)
handleChange('spec.kubernetes.containerManager','')
}
const changeClusterNameHandler = e => {
handleChange('spec.kubernetes.clusterName',e.target.value)
handleChange('metadata.name',e.target.value)
}
const changeAutoRenewHandler = e => {
handleChange('spec.kubernetes.autoRenewCerts',e)
}
const changeContainerManagerHandler = e => {
handleChange('spec.kubernetes.containerManager',e.target.name)
}
const changeKubekeyNamespaceHandler = e => {
setKubekeyNamespace(e.target.value)
}
useEffect(()=>{
if(backendIP!=='') {
fetch(`http://${backendIP}:8082/clusterVersionOptions`)
// fetch('http://139.196.14.61:8082/clusterVersionOptions')
.then((res)=>{
return res.json()
}).then(data => {
setClusterVersionOptions(data.clusterVersionOptions.map(item => ({ value: item, label: item })))
}).catch(()=>{
})
}
},[backendIP])
return (
<div>
<Columns>
<Column className={'is-2'}>
Kubernetes 版本
</Column>
<Column>
<Select value={data.spec.kubernetes.version} options={clusterVersionOptions} onChange={changeClusterVersionHandler} />
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>Kubernetes 集群名称:</Column>
<Column >
<Input onChange={changeClusterNameHandler} value={data.spec.kubernetes.clusterName} placeholder="请输入要创建的 Kubernetes 集群名称" />
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>是否自动续费证书:</Column>
<Column>
<Toggle checked={data.spec.kubernetes.autoRenewCerts} onChange={changeAutoRenewHandler} onText="开启" offText="关闭" />
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
容器运行时
</Column>
<Column>
<Tooltip content={"v1.24.0 及以上版本集群不支持 docker 作为容器运行时"}>
<Radio name="docker" checked={data.spec.kubernetes.containerManager === 'docker'} onChange={changeContainerManagerHandler} disabled={data.spec.kubernetes.version>='v1.24.0'}>
Docker
</Radio>
</Tooltip>
<Radio name="containerd" checked={data.spec.kubernetes.containerManager === 'containerd'} onChange={changeContainerManagerHandler}>
Containerd
</Radio>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
Kubekey 命名空间
</Column>
<Column>
<Input placeholder="默认为 kubekey-system" value={KubekeyNamespace} onChange={changeKubekeyNamespaceHandler} />
</Column>
</Columns>
</div>
)
};
export default ClusterSetting;

View File

@ -0,0 +1,35 @@
import React, { useEffect, useRef } from 'react';
import useInstallFormContext from "../../../hooks/useInstallFormContext";
const ConfirmInstallSetting = () => {
const logContainerRef = useRef(null);
const { logs } = useInstallFormContext();
return (
<div>
<div ref={logContainerRef} style={{
backgroundColor: '#1e1e1e',
color: '#ffffff',
padding: '10px',
borderRadius: '5px',
maxHeight: '500px',
maxWidth: '850px',
overflowY: 'scroll',
fontFamily: 'Consolas, "Courier New", monospace',
fontSize: '14px',
lineHeight: '1.5'
}}>
{logs.map((log, index) => (
<div key={index} style={{ whiteSpace: 'pre-wrap' }}>
{log}
</div>
))}
</div>
</div>
);
};
export default ConfirmInstallSetting;

View File

@ -0,0 +1,57 @@
import React from 'react';
import useInstallFormContext from "../../../hooks/useInstallFormContext";
import {Column, Columns, RadioGroup} from "@kube-design/components";
import {Select, Tag} from "@kubed/components"
const EtcdSetting = () => {
const { data, handleChange } = useInstallFormContext()
const ETCDTypeOptions = [{
value: 'kubekey',
label: 'kubekey'
}]
const ETCDChangeHandler = (e) => {
handleChange('spec.roleGroups.etcd',e)
}
const ETCDTypeChangeHandler = e => {
handleChange('spec.etcd.type',e)
}
const ETCDOptionContent = (item) => {
return (
<Select.Option key={item.name} value={item.name} label={item.name}>
<div style={{display:`flex`}}>
<div style={{width:"200px"}}>{item.name}</div>
<div style={{display:`flex`}}>
{data.spec.roleGroups.master.includes(item.name) && <Tag style={{marginRight:"10px"}} color="error">MASTER</Tag>}
{data.spec.roleGroups.worker.includes(item.name) && <Tag color="secondary">WORKER</Tag>}
</div>
</div>
</Select.Option>
)
}
return (
<div>
<Columns>
<Column className={'is-2'}>ETCD部署节点</Column>
<Column>
<div style={{display:`flex`}}>
<Select style={{minWidth:'400px'}} value={data.spec.roleGroups.etcd} onChange={ETCDChangeHandler} placeholder="请选择ETCD部署节点" mode="multiple" showSearch allowClear showArrow optionLabelProp="label">
{data.spec.hosts.map(host=>ETCDOptionContent(host))}
</Select>
</div>
{/*<Select options={ETCDOptions} value={data.ETCD} onChange={ETCDChangeHandler} searchable multi />*/}
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>ETCD类型</Column>
<Column >
<RadioGroup options={ETCDTypeOptions} value={data.spec.etcd.type} onChange={ETCDTypeChangeHandler} />
</Column>
</Columns>
</div>
)
};
export default EtcdSetting;

View File

@ -0,0 +1,15 @@
import React from 'react';
import HostTable from "./HostTable";
import HostAddModal from "../../../Modal/HostAddModal";
const HostSetting = () => {
return (
<div>
<HostAddModal></HostAddModal>
<HostTable></HostTable>
</div>
)
};
export default HostSetting;

View File

@ -0,0 +1,51 @@
import {Table, Button, InputSearch, Pagination} from "@kube-design/components";
import HostTableDataWrapper from "./HostTableDataWrapper";
const HostTable = () => {
return (
<HostTableDataWrapper>
{({
fetchList,
list: {
pagination,
filters,
sorter,
data,
isLoading,
selectedRowKeys
},
setSelectedRowKeys,
columns
}) => {
const title = <div style={{
display: "flex"
}}>
<InputSearch style={{
flex: 1
}} placeholder="please input a word" onSearch={name => fetchList({
name
})} />
<Button style={{
marginLeft: 12
}} icon="refresh" type="flat" onClick={() => fetchList({
pagination,
filters,
sorter
})} />
</div>;
const footer = <Pagination {...pagination} onChange={page => fetchList({
pagination: { ...pagination,
page
},
filters,
sorter
})} />;
return <Table rowKey="name" columns={columns} filters={filters} sorter={sorter} dataSource={data} loading={isLoading} title={title} footer={footer} onChange={(filters, sorter) => fetchList({
filters,
sorter
})} />;
}}
</HostTableDataWrapper>
)
}
export default HostTable

View File

@ -0,0 +1,119 @@
import React, { useState, useEffect } from 'react';
import { sortBy } from 'lodash';
import useInstallFormContext from "../../../../hooks/useInstallFormContext";
import {Tag} from "@kube-design/components";
import HostEditModal from "../../../Modal/HostEditModal";
import HostDeleteConfirmModal from "../../../Modal/HostDeleteConfirmModal";
const HostTableDataWrapper= ({ children }) => {
const {data} = useInstallFormContext()
const initialData = data.spec.hosts;
const menuColumn = (_,record) => {
return (
<div style={{display: `flex`}}>
<HostEditModal record={record}/>
<HostDeleteConfirmModal record={record}/>
</div>
)
}
const roleColumn = (_,record) => {
return(
<div style={{display: `flex`}}>
{data.spec.roleGroups.master.includes(record.name) && <Tag type="warning">MASTER</Tag>}
{data.spec.roleGroups.master.includes(record.name) && <div style={{width:'10px'}}/>}
{data.spec.roleGroups.worker.includes(record.name) && <Tag type="primary">WORKER</Tag>}
</div>
)
}
const initialColumns = [
{
children: [
{ title: 'Name', dataIndex: 'name', sorter: true, search: true ,width: '10%'},
{ title: 'Address', dataIndex: 'address', width: '10%' },
{ title: 'InternalAddress', dataIndex: 'internalAddress', width: '13%' },
{
title: '角色',
dataIndex: 'role',
width: '20%',
search: true,
render:roleColumn
},
{ title: '用户名', dataIndex: 'user', width: '12%' },
{ title: '密码', dataIndex: 'password', width: '15%' },
{ title: 'id_rsa路径', dataIndex: 'privateKeyPath', width: '20%' },
{title:'操作', dataIndex:'', width: '13%', render:menuColumn}
],
},
];
const [list, setList] = useState({
data: [],
isLoading: false,
selectedRowKeys: [],
filters: {},
sorter: {},
pagination: { page: 1, total: 0, limit: 10 },
});
useEffect(() => {
fetchList();
}, [data.spec.hosts]);
const setSelectedRowKeys = (value) => {
setList((prevState) => ({ ...prevState, selectedRowKeys: value }));
};
const fetchList = ({ name, pagination = {}, filters = {}, sorter = {} } = {}) => {
setList((prevState) => ({ ...prevState, isLoading: true }));
setTimeout(() => {
let data = [...initialData];
if (name) {
data = data.filter((item) => item.name.indexOf(name) !== -1);
}
const filterKeys = Object.keys(filters);
if (filterKeys.length > 0) {
data = data.filter((item) =>
filterKeys.every((key) => filters[key] === item[key])
);
}
if (sorter.field && sorter.order) {
data = sortBy(data, [sorter.field]);
if (sorter.order === 'descend') {
data = data.reverse();
}
}
const total = data.length;
const { page = 1, limit = 10 } = pagination;
data = data.slice((page - 1) * limit, page * limit);
setList({
data,
filters,
sorter,
pagination: { total, page, limit },
isLoading: false,
});
}, 300);
};
return (
<div>
{children({
list,
columns: initialColumns,
fetchList,
setSelectedRowKeys,
})}
</div>
);
}
export default HostTableDataWrapper;

View File

@ -0,0 +1,58 @@
import React, {useEffect, useState} from 'react';
import {Column, Columns, Select, Toggle, Tooltip} from "@kube-design/components";
import useInstallFormContext from "../../../hooks/useInstallFormContext";
import useGlobalContext from "../../../hooks/useGlobalContext";
const KubesphereSetting = () => {
const {backendIP} = useGlobalContext();
const { data, ksEnable, setKsEnable, ksVersion, setKsVersion} = useInstallFormContext()
const [KubesphereVersionOptions,setKubesphereVersionOptions] = useState([])
useEffect(()=>{
if(backendIP!=='') {
fetch(`http://${backendIP}:8082/ksVersionOptions/${data.spec.kubernetes.version}`)
.then((res)=>{
return res.json()
}).then(data => {
setKubesphereVersionOptions(data.ksVersionOptions.map(item => ({ value: item, label: item })))
}).catch(()=>{
})
}
},[backendIP])
const changeInstallKubesphereHandler = (e) => {
setKsVersion('')
setKsEnable(e)
}
const changeKubesphereVersionHandler = e => {
setKsVersion(e)
}
return (
<div>
<Columns>
<Column className={'is-2'}>是否安装 KubeSphere:</Column>
<Column>
<Tooltip content="安装 KubeSphere 需要在存储设置中开启本地存储" placement="right" >
<Toggle checked={ksEnable} onChange={changeInstallKubesphereHandler}
disabled={!(data.spec.storage
&& data.spec.storage.openebs
&& data.spec.storage.openebs.basePath
&& data.spec.storage.openebs.basePath!=='')}
onText="开启" offText="关闭"/>
</Tooltip>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
KubeSphere 版本
</Column>
<Column>
<Select placeholder="KubeSphere 可选版本与 K8s 集群版本有关" value={ksVersion} options={KubesphereVersionOptions} disabled={!ksEnable} onChange={changeKubesphereVersionHandler} />
</Column>
</Columns>
</div>
);
};
export default KubesphereSetting;

View File

@ -0,0 +1,91 @@
import React from 'react';
import useInstallFormContext from "../../../hooks/useInstallFormContext";
import {Column, Input, RadioGroup, Columns} from "@kube-design/components";
const NetworkSetting = () => {
const networkPluginOptions = [
{
value:'calico',
label:'calico'
},
// {
// value:'flannel',
// label:'flannel'
// },
// {
// value:'cilium',
// label:'cilium'
// },
// {
// value:'hybridnet',
// label:'hybridnet'
// },
// {
// value:'Kube-OVN',
// label:'Kube-OVN'
// },
// {
// value:'',
// label:'不启用'
// }
]
const { data, handleChange } = useInstallFormContext()
const networkPluginChangeHandler = (e) => {
handleChange('spec.network.plugin',e)
if(e==='none') {
handleChange('enableMultusCNI',false)
}
}
const kubePodsCIDRChangeHandler = (e) => {
handleChange('spec.network.kubePodsCIDR',e.target.value)
}
const kubeServiceCIDRChangeHandler = (e) => {
handleChange('spec.network.kubeServiceCIDR',e.target.value)
}
// const changEnableMultusCNIHandler = e => {
// handleChange('enableMultusCNI',e)
// }
return (
<div>
<Columns>
<Column className={'is-2'}>网络插件</Column>
<Column>
<RadioGroup value={data.spec.network.plugin} options={networkPluginOptions} onChange={networkPluginChangeHandler}>
</RadioGroup>
</Column>
</Columns>
<Columns >
{/*TODO 要改*/}
<Column className={'is-2'}>kubePodsCIDR:</Column>
<Column>
<Input placeholder={'10.233.64.0/18'} name='kubePodsCIDRPrefix' value={data.spec.network.kubePodsCIDR} onChange={kubePodsCIDRChangeHandler}></Input>
</Column>
</Columns>
<Columns >
<Column className={'is-2'}>kubeServiceCIDR:</Column>
<Column>
<Input placeholder={'10.233.0.0/18'} name='kubeServiceCIDRPrefix' value={data.spec.network.kubeServiceCIDR} onChange={kubeServiceCIDRChangeHandler}></Input>
</Column>
</Columns>
{/*<Columns>*/}
{/* <Column className={'is-2'}>是否开启Multus CNI:</Column>*/}
{/* <Column>*/}
{/* <Tooltip content="Multus 不能独立部署。它总是需要至少一个传统的 CNI 插件,以满足 Kubernetes 集群的网络要求。该 CNI 插件成为 Multus 的默认插件,并将被用来为所有的 pod 提供主接口。">*/}
{/*
{/* <Toggle checked={data.enableMultusCNI} onChange={changEnableMultusCNIHandler} onText="开启" offText="关闭" disabled={data.spec.network.plugin==='none' || data.spec.network.plugin===''}/>*/}
{/* </Tooltip>*/}
{/* </Column>*/}
{/*</Columns>*/}
</div>
);
};
export default NetworkSetting;

View File

@ -0,0 +1,41 @@
import React from 'react';
import useInstallFormContext from "../../../hooks/useInstallFormContext";
import {Column, Input, Columns, TextArea} from "@kube-design/components";
const RegistrySetting = () => {
const { data, handleChange } = useInstallFormContext()
const changeInsecureRegistriesHandler = e => {
handleChange('spec.registry.insecureRegistries',e.split('\n'))
}
const changeRegistryMirrorsHandler = e => {
handleChange('spec.registry.registryMirrors',e.split('\n'))
}
const changePrivateRegistryUrlHandler= e => {
handleChange('spec.registry.privateRegistry',e.target.value)
}
return (
<div>
<Columns >
<Column className={'is-2'}>私有镜像仓库Url:</Column>
<Column>
<Input placeholder={"请输入私有镜像仓库Url留空代表不使用"} style={{width:'100%'}} value={data.spec.registry.privateRegistry} onChange={changePrivateRegistryUrlHandler} />
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>非安全仓库:</Column>
<Column >
<TextArea style={{width:'100%'}} onChange={changeInsecureRegistriesHandler} value={data.spec.registry.insecureRegistries.join('\n')} autoResize maxHeight={200} placeholder="请输入非安全仓库,每行一个,留空代表不使用" />
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>仓库镜像Url:</Column>
<Column >
<TextArea style={{width:'100%'}} placeholder={"请输入镜像仓库Url每行一个,留空代表不使用"} onChange={changeRegistryMirrorsHandler} value={data.spec.registry.registryMirrors.join('\n')} autoResize maxHeight={200} />
</Column>
</Columns>
</div>
)
};
export default RegistrySetting;

View File

@ -0,0 +1,59 @@
import React from 'react';
import useInstallFormContext from "../../../hooks/useInstallFormContext";
import {Column, Columns, Input, Toggle} from "@kube-design/components";
const StorageSetting = () => {
const { data, handleChange,setKsEnable, setKsVersion } = useInstallFormContext()
const changeEnableLocalStorageHandler = e => {
if(e) {
handleChange('spec.storage',{
openebs: {
basePath: '/var/openebs/local',
},
})
} else {
setKsEnable(false)
setKsVersion('')
handleChange('spec.storage',{})
}
}
const changeLocalStoragePathHandler = (e)=> {
handleChange('spec.storage.openebs.basePath', e.target.value)
}
return (
// TODO 待处理
<div>
<Columns >
<Column className={'is-2'}>开启openebs本地存储:</Column>
<Column>
<Toggle checked={data.spec.storage
&& data.spec.storage.openebs
&& data.spec.storage.openebs.basePath
&& data.spec.storage.openebs.basePath!==''}
onChange={changeEnableLocalStorageHandler} onText="开启" offText="关闭" />
</Column>
</Columns>
<Columns >
<Column className={'is-2'}>openebs路径:</Column>
<Column>
<Input value={(data.spec.storage
&& data.spec.storage.openebs
&& data.spec.storage.openebs.basePath
&& data.spec.storage.openebs.basePath!=='')?data.spec.storage.openebs.basePath:''}
onChange={changeLocalStoragePathHandler}
disabled={!(data.spec.storage
&& data.spec.storage.openebs
&& data.spec.storage.openebs.basePath
&& data.spec.storage.openebs.basePath!=='')}></Input>
</Column>
</Columns>
</div>
)
};
export default StorageSetting;

View File

@ -0,0 +1,163 @@
import React, {useState} from 'react';
import {Button, CheckboxGroup, Column, Columns, Input, InputPassword} from "@kube-design/components";
import {Modal} from "@kubed/components";
import useAddNodeFormContext from "../../hooks/useAddNodeFormContext";
const AddNodeModal = () => {
const {setCurCluster} = useAddNodeFormContext()
const [visible, setVisible] = useState(false);
const [curRole, setCurRole] = useState([]);
const [newHost,setNewHost] = useState({
name : '',
address : '',
internalAddress : '',
user : '',
password : '',
privateKeyPath : ''
})
const ref = React.createRef();
const openModal = () => {
setVisible(true);
};
const closeModal = () => {
setNewHost({
name : '',
address : '',
internalAddress : '',
user : '',
password : '',
privateKeyPath : ''
})
setCurRole([])
setVisible(false);
};
const roleOptions = [
{
value:'Master',
label:'Master'
},
{
value:'Worker',
label:'Worker'
}
]
const onChangeHandler = e => {
if(Array.isArray(e)) {
setCurRole(e)
} else {
setNewHost(prevState => {
// console.log({...prevState,[e.target.name]:e.target.value})
return ({...prevState,[e.target.name]:e.target.value})
})
}
}
const onOKHandler = () => {
setCurCluster(prev=>{
const newCluster = {...prev}
newCluster.spec.hosts = [...prev.spec.hosts,newHost]
if(curRole.length===2){
newCluster.spec.roleGroups.master = [...prev.spec.roleGroups.master,newHost.name]
newCluster.spec.roleGroups.worker = [...prev.spec.roleGroups.worker,newHost.name]
} else if(curRole[0]==='Master') {
newCluster.spec.roleGroups.master = [...prev.spec.roleGroups.master,newHost.name]
} else if(curRole[0]==='Worker') {
newCluster.spec.roleGroups.worker = [...prev.spec.roleGroups.worker,newHost.name]
}
return newCluster
})
setNewHost({
name : '',
address : '',
internalAddress : '',
user : '',
password : '',
privateKeyPath : ''
})
setCurRole([])
setVisible(false);
}
const modalContent = (
<div>
<Columns>
<Column className={'is-2'}>
主机名
</Column>
<Column>
<Input name='name' value={newHost.name} onChange={onChangeHandler}></Input>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
Address
</Column>
<Column>
<Input name='address' value={newHost.address} onChange={onChangeHandler}></Input>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
InternalAddress
</Column>
<Column>
<Input name='internalAddress' value={newHost.internalAddress} onChange={onChangeHandler}></Input>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
角色
</Column>
<Column>
<CheckboxGroup name='role' value={curRole} options={roleOptions} onChange={onChangeHandler} ></CheckboxGroup>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
用户名
</Column>
<Column>
<Input name='user' value={newHost.user} onChange={onChangeHandler}></Input>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
密码
</Column>
<Column>
<InputPassword name='password' value={newHost.password} onChange={onChangeHandler}></InputPassword>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
id_rsa路径
</Column>
<Column>
<Input name='privateKeyPath' value={newHost.privateKeyPath} onChange={onChangeHandler}></Input>
</Column>
</Columns>
</div>
)
return (
<>
<Button onClick={openModal}>添加节点</Button>
<Modal
ref={ref}
visible={visible}
title="添加节点"
onCancel={closeModal}
onOk={onOKHandler}
>
{modalContent}
</Modal>
</>
);
}
export default AddNodeModal;

View File

@ -0,0 +1,48 @@
import React from 'react';
import {Button, Modal} from "@kubed/components";
import {Column, Columns} from "@kube-design/components";
const AlertModal = () => {
const [visible, setVisible] = React.useState(false);
const ref = React.createRef();
const openModal = () => {
setVisible(true);
};
const closeModal = () => {
setVisible(false);
};
const onOKHandler = () => {
setVisible(false);
}
const textStyle={
fontSize:"20px",
height: '30px',
margin: 0, /* 清除默认的外边距 */
display: 'flex',
alignItems: 'center'
}
return (
<div>
<Button variant="link" onClick={openModal}></Button>
<Modal
ref={ref}
visible={visible}
title="开始安装集群"
onCancel={closeModal}
onOk={onOKHandler}
>
<Columns>
<Column className='is-1'></Column>
<Column style={{display:`flex`, alignItems: 'center' }}>
<p style={textStyle}>集群安装已开始关闭该提示后可查看实时日志期间请勿进行其他操作</p>
</Column>
</Columns>
</Modal>
</div>
);
};
export default AlertModal;

View File

@ -0,0 +1,159 @@
import React, {useState} from 'react';
import {Modal} from "@kubed/components";
import { CheckboxGroup, Column, Columns, Input, InputPassword, Button} from "@kube-design/components";
import useInstallFormContext from "../../hooks/useInstallFormContext";
const HostAddModal = () => {
const { data, handleChange } = useInstallFormContext()
const [visible, setVisible] = useState(false);
const [curRole, setCurRole] = useState([]);
const [newHost,setNewHost] = useState({
name : '',
address : '',
internalAddress : '',
user : '',
password : '',
privateKeyPath : ''
})
const ref = React.createRef();
const openModal = () => {
setVisible(true);
};
const closeModal = () => {
setNewHost({
name : '',
address : '',
internalAddress : '',
user : '',
password : '',
privateKeyPath : ''
})
setCurRole([])
setVisible(false);
};
const roleOptions = [
{
value:'Master',
label:'Master'
},
{
value:'Worker',
label:'Worker'
}
]
const onChangeHandler = e => {
if(Array.isArray(e)) {
setCurRole(e)
} else {
setNewHost(prevState => {
return ({...prevState,[e.target.name]:e.target.value})
})
}
}
const onOKHandler = () => {
handleChange('spec.hosts',[...data.spec.hosts,newHost])
if(curRole.length===2){
handleChange("spec.roleGroups.master",[...data.spec.roleGroups.master,newHost.name])
handleChange("spec.roleGroups.worker",[...data.spec.roleGroups.worker,newHost.name])
}
else if(curRole[0]==='Master') {
handleChange("spec.roleGroups.master",[...data.spec.roleGroups.master,newHost.name])
}
else if(curRole[0]==='Worker') {
handleChange("spec.roleGroups.worker",[...data.spec.roleGroups.worker,newHost.name])
}
setNewHost({
name : '',
address : '',
internalAddress : '',
user : '',
password : '',
privateKeyPath : ''
})
setCurRole([])
setVisible(false);
}
const modalContent = (
<div>
<Columns>
<Column className={'is-2'}>
主机名
</Column>
<Column>
<Input name='name' value={newHost.name} onChange={onChangeHandler}></Input>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
Address
</Column>
<Column>
<Input name='address' value={newHost.address} onChange={onChangeHandler}></Input>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
InternalAddress
</Column>
<Column>
<Input name='internalAddress' value={newHost.internalAddress} onChange={onChangeHandler}></Input>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
角色
</Column>
<Column>
<CheckboxGroup name='role' value={curRole} options={roleOptions} onChange={onChangeHandler} ></CheckboxGroup>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
用户名
</Column>
<Column>
<Input name='user' value={newHost.user} onChange={onChangeHandler}></Input>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
密码
</Column>
<Column>
<InputPassword name='password' value={newHost.password} onChange={onChangeHandler}></InputPassword>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
id_rsa路径
</Column>
<Column>
<Input name='privateKeyPath' value={newHost.privateKeyPath} onChange={onChangeHandler}></Input>
</Column>
</Columns>
</div>
)
return (
<>
<Button onClick={openModal}>添加节点</Button>
<Modal
ref={ref}
visible={visible}
title="添加节点"
onCancel={closeModal}
onOk={onOKHandler}
>
{modalContent}
</Modal>
</>
);
}
export default HostAddModal;

View File

@ -0,0 +1,66 @@
import React from 'react';
import {Button, Modal} from "@kubed/components";
import useInstallFormContext from "../../hooks/useInstallFormContext";
import {Column, Columns} from "@kube-design/components";
const HostDeleteConfirmModal = ({record}) => {
const { data, handleChange } = useInstallFormContext()
const [visible, setVisible] = React.useState(false);
const ref = React.createRef();
const openModal = () => {
setVisible(true);
};
const closeModal = () => {
setVisible(false);
};
const onOKHandler = () => {
const newHosts = data.spec.hosts.filter(host => host.name !== record.name);
handleChange('spec.hosts',newHosts)
if(data.spec.roleGroups.master.includes(record.name)){
const newMasters = data.spec.roleGroups.master.filter(name=>name!==record.name)
handleChange('spec.roleGroups.master',newMasters)
}
if(data.spec.roleGroups.worker.includes(record.name)){
const newWorkers = data.spec.roleGroups.worker.filter(name => name!==record.name)
handleChange('spec.roleGroups.worker',newWorkers)
}
setVisible(false);
}
const textStyle={
fontSize:"20px",
height: '30px',
margin: 0, /* 清除默认的外边距 */
display: 'flex',
alignItems: 'center'
}
return (
<div>
<Button variant="link" onClick={openModal}>删除</Button>
<Modal
ref={ref}
visible={visible}
title="删除节点"
onCancel={closeModal}
onOk={onOKHandler}
>
<Columns>
<Column className='is-1'></Column>
<Column style={{display:`flex`, alignItems: 'center' }}>
<p style={textStyle}>确定删除吗</p>
</Column>
</Columns>
</Modal>
</div>
);
};
export default HostDeleteConfirmModal;

View File

@ -0,0 +1,155 @@
import React, {useState} from 'react';
import useInstallFormContext from "../../hooks/useInstallFormContext";
import {CheckboxGroup, Column, Columns, Input, InputPassword} from "@kube-design/components";
import {Modal,Button} from "@kubed/components";
const HostEditModal = ({record}) => {
const recordCopy = record
const { data, handleChange } = useInstallFormContext()
const roleCopy = []
if (data.spec.roleGroups.master.includes(record.name)) roleCopy.push('Master');
if (data.spec.roleGroups.worker.includes(record.name)) roleCopy.push('Worker');
const [curRole,setCurRole] = useState(roleCopy)
const [visible, setVisible] = React.useState(false);
const [curHost,setCurHost] = useState(record)
const ref = React.createRef();
const openModal = () => {
setVisible(true);
};
const closeModal = () => {
setCurHost(recordCopy)
// 可以不要这行
setCurRole(roleCopy)
setVisible(false);
};
const roleOptions = [
{
value:'Master',
label:'Master'
},
{
value:'Worker',
label:'Worker'
}
]
const onChangeHandler = e => {
if(Array.isArray(e)) {
setCurRole(e)
} else {
setCurHost(prevState => {
return ({...prevState,[e.target.name]:e.target.value})
})
}
}
const onOKHandler = () => {
const newHosts = data.spec.hosts.map(host => {
if (host.name === recordCopy.name) {
return curHost;
} else {
return host;
}
});
handleChange('spec.hosts',newHosts)
// 无论改没改名都在master和worker中删掉原名
const otherMasters = data.spec.roleGroups.master.filter(name => name!==recordCopy.name)
const otherWorkers = data.spec.roleGroups.worker.filter(name => name!==recordCopy.name)
// 再加回去
if(curRole.length===2){
handleChange("spec.roleGroups.master",[...otherMasters,curHost.name])
handleChange("spec.roleGroups.worker",[...otherWorkers,curHost.name])
}
else if(curRole[0]==='Master') {
handleChange("spec.roleGroups.master",[...otherMasters,curHost.name])
handleChange("spec.roleGroups.worker",[...otherWorkers])
}
else if(curRole[0]==='Worker') {
handleChange("spec.roleGroups.worker",[...otherWorkers,curHost.name])
handleChange("spec.roleGroups.master",[...otherMasters])
}
setVisible(false);
}
const modalContent = (
<div>
<Columns>
<Column className={'is-2'}>
主机名
</Column>
<Column>
<Input name='name' value={curHost.name} onChange={onChangeHandler}></Input>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
Address
</Column>
<Column>
<Input name='address' value={curHost.address} onChange={onChangeHandler}></Input>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
InternalAddress
</Column>
<Column>
<Input name='internalAddress' value={curHost.internalAddress} onChange={onChangeHandler}></Input>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
角色
</Column>
<Column>
<CheckboxGroup name='role' value={curRole} options={roleOptions} onChange={onChangeHandler} ></CheckboxGroup>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
用户名
</Column>
<Column>
<Input name='user' value={curHost.user} onChange={onChangeHandler}></Input>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
密码
</Column>
<Column>
<InputPassword name='password' value={curHost.password} onChange={onChangeHandler}></InputPassword>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
id_rsa路径
</Column>
<Column>
<Input name='privateKeyPath' value={curHost.privateKeyPath} onChange={onChangeHandler}></Input>
</Column>
</Columns>
</div>
)
return (
<>
<Button variant="link" style={{marginRight:'20px'}} onClick={openModal}>编辑</Button>
<Modal
ref={ref}
visible={visible}
title="编辑节点"
onCancel={closeModal}
onOk={onOKHandler}
okText={"保存"}
cancelText={"取消"}
>
{modalContent}
</Modal>
</>
);
}
export default HostEditModal;

View File

@ -0,0 +1,57 @@
import React, {useEffect} from 'react';
import {Link, useParams} from "react-router-dom";
import {Button, Column, Columns} from "@kube-design/components";
import UpgradeClusterProgressBar from "./UpgradeClusterProgressBar";
import UpgradeClusterForm from "./UpgradeClusterForm";
import useUpgradeClusterFormContext from "../../hooks/useUpgradeClusterFormContext";
import useClusterTableContext from "../../hooks/useClusterTableContext";
const UpgradeCluster = () => {
const {clusterName} = useParams()
const {clusterData} = useClusterTableContext()
const {canToHome,setCurCluster,setOriginalClusterVersion} = useUpgradeClusterFormContext()
useEffect(() => {
if (clusterData.length > 0) {
const newV = clusterData.find(item=>item.metadata.name===clusterName)
setOriginalClusterVersion(newV.spec.kubernetes.version)
newV.spec.kubernetes.version = ''
setCurCluster(newV)
}
}, [clusterData]);
return (
<>
<Columns>
<Column className="is-1"></Column>
<Column className="is-2">
<h2>升级集群</h2>
</Column>
<Column className={'is-8'}>
<Columns>
<Column className={'is-10'}>
</Column>
<Column>
{canToHome ? (
<Link to='/'>
<Button disabled={!canToHome}>集群列表</Button>
</Link>
) : (
<Button disabled={!canToHome}>集群列表</Button>
)}
</Column>
</Columns>
</Column>
</Columns>
<Columns>
<Column className={'is-1'}></Column>
<Column className={'is-2'}>
<UpgradeClusterProgressBar></UpgradeClusterProgressBar>
</Column>
<Column className={'is-8'}>
<UpgradeClusterForm/>
</Column>
</Columns>
</>
);
};
export default UpgradeCluster;

View File

@ -0,0 +1,96 @@
import React from 'react';
import {Button, Column, Columns} from "@kube-design/components";
import useUpgradeClusterFormContext from "../../hooks/useUpgradeClusterFormContext";
import UpgradeClusterFormInputs from "./UpgradeClusterFormInputs";
import {Modal} from "@kubed/components";
const UpgradeClusterForm = () => {
const [visible, setVisible] = React.useState(false);
const ref = React.createRef();
const openModal = () => {
setVisible(true);
};
const closeModal = () => {
setVisible(false);
};
const onOKHandler = () => {
setVisible(false);
}
const textStyle={
fontSize:"15px",
minHeight: '50px',
margin: 0, /* 清除默认的外边距 */
display: 'flex',
alignItems: 'center'
}
const {
page,
setPage,
title,
canSubmit,
disablePrev,
upgradeHandler,
disableNext
} = useUpgradeClusterFormContext()
const handlePrev = () => {
setPage(prev => {
return +prev-1
})
}
const handleNext = () => {
setPage(prev => {
return +prev+1
})
}
return (
<>
<Columns>
<Column className='is-10'>
<h3>{title[page]}</h3>
</Column>
</Columns>
<Columns>
<Column>
<UpgradeClusterFormInputs/>
</Column>
</Columns>
<Columns>
<Column className='is-8'/>
<Column className='is-2'>
<Columns>
<Column className='is-5'/>
<Column>
{page !== 0 && <Button onClick={handlePrev} disabled={disablePrev}>上一步</Button>}
</Column>
</Columns>
</Column>
<Column className='is-1'>
{page !== Object.keys(title).length - 1 && <Button onClick={handleNext} disabled={disableNext}>下一步</Button>}
{page === Object.keys(title).length - 1 && <Button onClick={()=>{upgradeHandler(); openModal();}} disabled={!canSubmit} >升级</Button>}
<Modal
ref={ref}
visible={visible}
title="开始升级节点"
onCancel={closeModal}
onOk={onOKHandler}
>
<Columns>
<Column style={{display:`flex`, alignItems: 'center' }}>
<p style={textStyle}>升级节点已开始关闭该提示后可查看实时日志期间请勿进行其他操作</p>
</Column>
</Columns>
</Modal>
</Column>
</Columns>
</>
);
};
export default UpgradeClusterForm;

View File

@ -0,0 +1,26 @@
import React from 'react';
import useUpgradeClusterFormContext from "../../hooks/useUpgradeClusterFormContext";
import UpgradeRegistrySetting from "./UpgradeSettings/UpgradeRegistrySetting";
import UpgradeKubesphereSetting from "./UpgradeSettings/UpgradeKubesphereSetting";
import UpgradeClusterSetting from "./UpgradeSettings/UpgradeClusterSetting";
import ConfirmUpgradeSetting from "./UpgradeSettings/ConfirmUpgradeSetting";
import UpgradeStorageSetting from "./UpgradeSettings/UpgradeStorageSetting";
const UpgradeClusterFormInputs = () => {
const { page } = useUpgradeClusterFormContext()
const display = {
0: <UpgradeClusterSetting/>,
1: <UpgradeRegistrySetting/>,
2: <UpgradeKubesphereSetting/>,
3: <UpgradeStorageSetting/>,
4: <ConfirmUpgradeSetting/>
}
return (
<div>
{display[page]}
</div>
)
};
export default UpgradeClusterFormInputs;

View File

@ -0,0 +1,70 @@
import React from 'react';
import {Button} from "@kubed/components";
import useUpgradeClusterFormContext from "../../hooks/useUpgradeClusterFormContext";
const UpgradeClusterProgressBar = () => {
const {page,setPage,title,buttonDisabled} = useUpgradeClusterFormContext()
const ongoingIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
backgroundColor: '#1C8FFC', // 背景色为蓝色
color: 'white', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
};
const finishedIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
border: '1px solid #7eb8dc',
backgroundColor: 'white', // 背景色为蓝色
color: '#7eb8dc', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
};
const unfinishedIndexCircleStyle = {
marginRight: '10px',
width: '20px', // 正方形的大小
height: '20px', // 正方形的大小
borderRadius: '50%', // 边框为 50% 得到圆角
border: '1px solid #abb4be',
backgroundColor: 'white', // 背景色为蓝色
color: '#abb4be', // 数字的颜色为白色
display: 'flex', // 使用flex布局来居中数字
alignItems: 'center', // 垂直居中
justifyContent: 'center', // 水平居中
fontSize: '13px' // 设置字体大小
}
const changePageHandler= e => {
const index = Object.keys(title).find(key => title[key] === e.target.innerText)
setPage(index)
}
const IndexCircleItem = (step) => {
if(step<page) return <div style={finishedIndexCircleStyle}></div>
else if(step===page) return <div style={ongoingIndexCircleStyle}>{step+1}</div>
else return <div style={unfinishedIndexCircleStyle}>{step+1}</div>
}
const steps = Object.keys(title).map((step,index) => {
return (
<div style={{display:`flex`, alignItems: 'center' }} key={index}>
{IndexCircleItem(+step)}
<Button style={{height:'50px'}} variant="link" color="default" onClick={changePageHandler} disabled={+step>page ||buttonDisabled}>{title[step]}</Button>
</div>
)
})
return (
<div>
{steps}
</div>
);
};
export default UpgradeClusterProgressBar;

View File

@ -0,0 +1,31 @@
import React, { useRef } from 'react';
import useUpgradeClusterFormContext from "../../../hooks/useUpgradeClusterFormContext";
const ConfirmUpgradeClusterSetting = () => {
const logContainerRef = useRef(null);
const { logs} = useUpgradeClusterFormContext();
return (
<div>
<div ref={logContainerRef} style={{
backgroundColor: '#1e1e1e',
color: '#ffffff',
padding: '10px',
borderRadius: '5px',
maxHeight: '500px',
maxWidth: '850px',
overflowY: 'scroll',
fontFamily: 'Consolas, "Courier New", monospace',
fontSize: '14px',
lineHeight: '1.5'
}}>
{logs.map((log, index) => (
<div key={index} style={{ whiteSpace: 'pre-wrap' }}>
{log}
</div>
))}
</div>
</div>
);
};
export default ConfirmUpgradeClusterSetting;

View File

@ -0,0 +1,103 @@
import React, {useEffect, useState} from 'react';
import {Column, Input, Columns, Select, Toggle, Radio, Tooltip} from "@kube-design/components";
import useUpgradeClusterFormContext from "../../../hooks/useUpgradeClusterFormContext";
import useGlobalContext from "../../../hooks/useGlobalContext";
const UpgradeClusterSetting = () => {
const [clusterVersionOptions,setClusterVersionOptions] = useState([])
const { curCluster, handleChange ,originalClusterVersion} = useUpgradeClusterFormContext()
const {backendIP} = useGlobalContext();
const changeClusterVersionHandler = e => {
handleChange('spec.kubernetes.version',e)
handleChange('spec.kubernetes.containerManager','')
}
const changeClusterNameHandler = e => {
handleChange('spec.kubernetes.clusterName',e.target.value)
handleChange('metadata.name',e.target.value)
}
const changeAutoRenewHandler = e => {
handleChange('spec.kubernetes.autoRenewCerts',e)
}
const changeContainerManagerHandler = e => {
handleChange('spec.kubernetes.containerManager',e.target.name)
}
const compareVersion = (a, b) => {
// 去除"v"并按点拆分版本字符串
const partsA = a.substring(1).split('.').map(Number);
const partsB = b.substring(1).split('.').map(Number);
// 找到最长的版本长度
const maxLength = Math.max(partsA.length, partsB.length);
// 遍历并比较
for (let i = 0; i < maxLength; i++) {
const partA = partsA[i] || 0;
const partB = partsB[i] || 0;
if (partA > partB) {
return true;
} else if (partA < partB) {
return false;
}
}
// 如果到这里,说明版本完全相同
return false;
}
useEffect(()=>{
if(Object.keys(curCluster).length>0 && backendIP!==''){
fetch(`http://${backendIP}:8082/clusterVersionOptions`)
.then((res)=>{
return res.json()
}).then(data => {
const targetClusterVersionList = data.clusterVersionOptions.filter(item=>(compareVersion(item,originalClusterVersion)))
setClusterVersionOptions(targetClusterVersionList.map(item => ({ value: item, label: item })))
}).catch(()=>{
})
}
},[curCluster,backendIP])
return (
<div>
<Columns>
<Column className={'is-2'}>
Kubernetes目标升级版本
</Column>
<Column>
<Select value={Object.keys(curCluster).length>0?curCluster.spec.kubernetes.version:''} options={clusterVersionOptions} onChange={changeClusterVersionHandler} />
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>Kubernetes集群名称:</Column>
<Column >
<Input onChange={changeClusterNameHandler} value={Object.keys(curCluster).length>0?curCluster.metadata.name:''} placeholder="请输入要创建的Kubernetes集群名称" />
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>是否自动续费证书:</Column>
<Column>
<Toggle checked={Object.keys(curCluster).length>0?curCluster.spec.kubernetes.autoRenewCerts:false} onChange={changeAutoRenewHandler} onText="开启" offText="关闭" />
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
容器运行时
</Column>
<Column>
<Tooltip content={"v1.24.0及以上版本集群不支持docker作为容器运行时"}>
<Radio name="docker" checked={Object.keys(curCluster).length>0?curCluster.spec.kubernetes.containerManager === 'docker':false} onChange={changeContainerManagerHandler} disabled={Object.keys(curCluster).length>0?curCluster.spec.kubernetes.version>='v1.24.0':true}>
Docker
</Radio>
</Tooltip>
<Radio name="containerd" checked={Object.keys(curCluster).length>0?curCluster.spec.kubernetes.containerManager === 'containerd':false} onChange={changeContainerManagerHandler}>
Containerd
</Radio>
</Column>
</Columns>
</div>
)
};
export default UpgradeClusterSetting;

View File

@ -0,0 +1,38 @@
import React from 'react';
import {Column, Columns, Select, Toggle, Tooltip} from "@kube-design/components";
import useUpgradeClusterFormContext from "../../../hooks/useUpgradeClusterFormContext";
const UpgradeKubesphereSetting = () => {
const { ksEnable, setKsEnable, ksVersion, setKsVersion} = useUpgradeClusterFormContext()
const KubesphereVersionOptions = [{label:"v3.4.0",value:"v3.4.0"}]
const changeInstallKubesphereHandler = (e) => {
setKsVersion('')
setKsEnable(e)
}
const changeKubesphereVersionHandler = e => {
setKsVersion(e)
}
return (
<div>
<Columns>
<Column className={'is-2'}>是否安装Kubesphere:</Column>
<Column>
<Tooltip content="升级k8s必须同时安装v3.4.0版本KubeSphere" placement="right" >
<Toggle checked={ksEnable} onChange={changeInstallKubesphereHandler} onText="开启" offText="关闭" disabled={true}/>
</Tooltip>
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>
Kubesphere版本
</Column>
<Column>
<Select placeholder="Kubesphere可选版本与K8s集群版本有关" value={ksVersion} options={KubesphereVersionOptions} disabled={true} onChange={changeKubesphereVersionHandler} />
</Column>
</Columns>
</div>
);
};
export default UpgradeKubesphereSetting;

View File

@ -0,0 +1,50 @@
import React from 'react';
import {Column, Input, Columns, TextArea} from "@kube-design/components";
import useUpgradeClusterFormContext from "../../../hooks/useUpgradeClusterFormContext";
const UpgradeRegistrySetting = () => {
const { curCluster, handleChange } = useUpgradeClusterFormContext()
const changeInsecureRegistriesHandler = e => {
handleChange('spec.registry.insecureRegistries',e.split('\n'))
}
const changeRegistryMirrorsHandler = e => {
handleChange('spec.registry.registryMirrors',e.split('\n'))
}
const changePrivateRegistryUrlHandler= e => {
handleChange('spec.registry.privateRegistry',e.target.value)
}
return (
<div>
<Columns >
<Column className={'is-2'}>私有镜像仓库Url:</Column>
<Column>
<Input placeholder={"请输入私有镜像仓库Url留空代表不使用"} style={{width:'100%'}} value={curCluster.spec.registry.privateRegistry} onChange={changePrivateRegistryUrlHandler} />
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>非安全仓库:</Column>
<Column >
<TextArea style={{width:'100%'}} onChange={changeInsecureRegistriesHandler}
value={(curCluster && curCluster.spec && curCluster.spec.registry && 'insecureRegistries' in curCluster.spec.registry)
? curCluster.spec.registry.insecureRegistries.join('\n')
: ''
}
autoResize maxHeight={200} placeholder="请输入非安全仓库,每行一个,留空代表不使用" />
</Column>
</Columns>
<Columns>
<Column className={'is-2'}>仓库镜像Url:</Column>
<Column >
<TextArea style={{width:'100%'}} placeholder={"请输入镜像仓库Url每行一个,留空代表不使用"} onChange={changeRegistryMirrorsHandler}
value={(curCluster && curCluster.spec && curCluster.spec.registry && 'registryMirrors' in curCluster.spec.registry)
? curCluster.spec.registry.registryMirrors.join('\n')
: ''
} autoResize maxHeight={200} />
</Column>
</Columns>
</div>
)
};
export default UpgradeRegistrySetting;

View File

@ -0,0 +1,21 @@
import React from 'react';
import {Column, Columns, Toggle, Tooltip} from "@kube-design/components";
const UpgradeStorageSetting = () => {
return (
// TODO 待处理 开启本地存储
<div>
<Columns >
<Column className={'is-2'}>是否开启本地存储:</Column>
<Column>
<Tooltip content={'安装kubesphere必须开启本地存储'}>
<Toggle checked={true} disabled={true} onText="开启" offText="关闭" />
</Tooltip>
</Column>
</Columns>
</div>
)
};
export default UpgradeStorageSetting;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,3 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 454.34 79.33" width="454.3399963378906" height="79.33000183105469"><defs><style>.cls-1{fill:#3e3d49;}.cls-2{fill:#00a971;}</style></defs><g id="图层_2" data-name="图层 2"><g id="图层_1-2" data-name="图层 1"><path class="cls-1" d="M172.92,79.16c-7.3,0-21.58-9.84-21.58-17.82V7.79a2.55,2.55,0,0,1,2.55-2.43h5.86a2.48,2.48,0,0,1,2.54,2.43V59.91c0,3.88,7.2,9.17,10.63,9.17s10.63-5.29,10.63-9.17V7.79a2.36,2.36,0,0,1,2.33-2.43H192a2.55,2.55,0,0,1,2.55,2.43V61.34C194.5,69.08,180.23,79.16,172.92,79.16Z"/><path class="cls-2" d="M400.73,70.65a2.5,2.5,0,0,0-2.47-2.48H372V47.84h26.27a2.42,2.42,0,0,0,2.47-2.48v-6.2a2.5,2.5,0,0,0-2.48-2.48H372V16.35h26.26a2.43,2.43,0,0,0,2.48-2.48V7.67a2.5,2.5,0,0,0-2.48-2.48H363.32a2.5,2.5,0,0,0-2.48,2.48V76.85a2.5,2.5,0,0,0,2.48,2.48h34.94a2.42,2.42,0,0,0,2.47-2.48Z"/><path class="cls-1" d="M295.56,70.65a2.5,2.5,0,0,0-2.48-2.48H266.82V47.84h26.26a2.42,2.42,0,0,0,2.48-2.48v-6.2a2.5,2.5,0,0,0-2.48-2.48H266.82V16.35h26.26a2.44,2.44,0,0,0,2.48-2.48V7.67a2.51,2.51,0,0,0-2.48-2.48H258.15a2.5,2.5,0,0,0-2.48,2.48V76.85a2.5,2.5,0,0,0,2.48,2.48h34.94a2.42,2.42,0,0,0,2.47-2.48Z"/><path class="cls-1" d="M236.58,41c5.11-3.36,10.25-9.9,10.25-15.32,0-8.4-11.43-20.31-19.61-20.31H209.68c-2.47,0-5.49,2.58-5.49,5.15V76.66a2.5,2.5,0,0,0,2.57,2.46H227.6c8.18,0,19.23-13.76,19.23-22.16C246.83,51.53,241.69,44.43,236.58,41Zm-21.3-26h10.49c4.48,0,10.08,6.41,10.08,10.67s-5.38,11.51-10,11.51h-10.6Zm10.49,54.45H215.28V44.84h10.6c4.59,0,10,7.75,10,12.12S230.25,69.49,225.77,69.49Z"/><path class="cls-2" d="M323.88,42.67l24.53-28.41a2.42,2.42,0,0,0-.25-3.49l-4.69-4A2.5,2.5,0,0,0,340,7L318.52,31.81V7.67A2.43,2.43,0,0,0,316,5.19h-6.2a2.51,2.51,0,0,0-2.48,2.48V76.85a2.51,2.51,0,0,0,2.48,2.48H316a2.43,2.43,0,0,0,2.48-2.48V53.53L340,78.37a2.51,2.51,0,0,0,3.5.26l4.69-4a2.43,2.43,0,0,0,.25-3.5Z"/><path class="cls-1" d="M116,42.67l24.53-28.41a2.42,2.42,0,0,0-.26-3.49l-4.69-4a2.49,2.49,0,0,0-3.49.25L110.66,31.81V7.67a2.44,2.44,0,0,0-2.48-2.48H102A2.51,2.51,0,0,0,99.5,7.67V76.85A2.51,2.51,0,0,0,102,79.33h6.2a2.44,2.44,0,0,0,2.48-2.48V53.53l21.45,24.84a2.5,2.5,0,0,0,3.49.26l4.69-4a2.44,2.44,0,0,0,.26-3.5Z"/><rect class="cls-2" x="427.63" y="39.99" width="11.16" height="38.95" rx="2.48"/><path class="cls-2" d="M451.82,5.59h-6.28A2.53,2.53,0,0,0,443,8.1V23.27c0,3.91-5.36,13.25-9.66,13.62-4.3-.37-9.65-9.71-9.65-13.62V8.1a2.54,2.54,0,0,0-2.52-2.51h-6.28A2.53,2.53,0,0,0,412.4,8.1V25c0,5.6,5.65,15.06,9.79,17.7,2.36,1.52,5.78,3.88,10.9,4h.56c5.12-.17,8.54-2.53,10.91-4,4.13-2.64,9.78-12.1,9.78-17.7V8.1A2.53,2.53,0,0,0,451.82,5.59Z"/><path class="cls-2" d="M76.63,22.12V66.36L54.54,79.12,44.91,49.23a13.63,13.63,0,1,0-13.48.22L21.91,79,0,66.36V22.12L38.31,0Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="428px" height="90px" viewBox="0 0 428 90" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 54 (76480) - https://sketchapp.com -->
<title>KubeSphere®.svg</title>
<desc>Created with Sketch.</desc>
<g id="KubeSphere商标®.ai" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="编组" transform="translate(98.999600, 22.647000)" fill="#443D4E">
<path d="M208.533917,0.353 L204.86864,0.353 C204.067471,0.353 203.400296,1.0373925 203.400296,1.85732717 L203.400296,19.3623119 L191.601435,19.3623119 L191.601435,1.85732717 C191.601435,1.0373925 191.001256,0.353 190.134952,0.353 L186.467814,0.353 C185.667575,0.353 185.0004,1.0373925 185.0004,1.85732717 L185.0004,43.8477183 C185.0004,44.669562 185.667575,45.353 186.467814,45.353 L190.134952,45.353 C191.001256,45.353 191.601435,44.669562 191.601435,43.8477183 L191.601435,26.1327387 L203.400296,26.1327387 L203.400296,43.8477183 C203.400296,44.669562 204.067471,45.353 204.86864,45.353 L208.533917,45.353 C209.401152,45.353 210.0004,44.669562 210.0004,43.8477183 L210.0004,1.85732717 C210.0004,1.0373925 209.401152,0.353 208.533917,0.353" id="Fill-5"></path>
<path d="M119.0004,40.0869004 C119.0004,39.2660112 118.323014,38.5816187 117.511472,38.5816187 L100.701519,38.5825732 L100.701519,26.2377361 L117.511472,26.2377361 C118.323014,26.2377361 118.999455,25.6230238 118.999455,24.733409 L118.999455,20.972591 C118.999455,20.1507473 118.322069,19.4673094 117.510528,19.4673094 L100.701519,19.4673094 L100.701519,7.12342678 L117.510528,7.12342678 C118.322069,7.12342678 118.999455,6.50775988 118.999455,5.61909961 L118.999455,1.85828169 C118.999455,1.03643798 118.322069,0.353 117.510528,0.353 L99.2154253,0.353 C99.2144806,0.353 99.2135358,0.353 99.2125911,0.353 L95.4902723,0.353 C94.6768417,0.353 94.0004,1.03643798 94.0004,1.85732717 L94.0004,43.8477183 C94.0004,44.6686075 94.6768417,45.353 95.4902723,45.353 L97.1029622,45.353 L97.1057964,45.353 L117.511472,45.353 C118.323014,45.353 119.0004,44.7373331 119.0004,43.8477183 L119.0004,40.0869004 Z" id="Fill-7"></path>
<path d="M241.0004,40.0869004 C241.0004,39.2660112 240.323014,38.5816187 239.511472,38.5816187 L222.701519,38.5825732 L222.701519,26.2377361 L239.510528,26.2377361 C240.322069,26.2377361 240.999455,25.6230238 240.999455,24.733409 L240.999455,20.972591 C240.999455,20.1507473 240.322069,19.4673094 239.510528,19.4673094 L222.701519,19.4673094 L222.701519,7.12342678 L239.510528,7.12342678 C240.322069,7.12342678 240.999455,6.50775988 240.999455,5.61909961 L240.999455,1.85828169 C240.999455,1.03643798 240.322069,0.353 239.510528,0.353 L221.215425,0.353 C221.214481,0.353 221.213536,0.353 221.212591,0.353 L217.490272,0.353 C216.676842,0.353 216.0004,1.03643798 216.0004,1.85732717 L216.0004,43.8477183 C216.0004,44.6686075 216.676842,45.353 217.490272,45.353 L219.102962,45.353 L219.105796,45.353 L239.511472,45.353 C240.323014,45.353 241.0004,44.7373331 241.0004,43.8477183 L241.0004,40.0869004 Z" id="Fill-9"></path>
<path d="M305.0004,40.0869004 C305.0004,39.2660112 304.323014,38.5816187 303.511472,38.5816187 L286.701519,38.5825732 L286.701519,26.2377361 L303.511472,26.2377361 C304.323014,26.2377361 304.999455,25.6230238 304.999455,24.733409 L304.999455,20.972591 C304.999455,20.1507473 304.322069,19.4673094 303.510528,19.4673094 L286.701519,19.4673094 L286.701519,7.12342678 L303.510528,7.12342678 C304.322069,7.12342678 304.999455,6.50775988 304.999455,5.61909961 L304.999455,1.85828169 C304.999455,1.03643798 304.322069,0.353 303.510528,0.353 L285.215425,0.353 C285.214481,0.353 285.213536,0.353 285.212591,0.353 L281.490272,0.353 C280.677786,0.353 280.0004,1.03643798 280.0004,1.85732717 L280.0004,43.8477183 C280.0004,44.6686075 280.677786,45.353 281.490272,45.353 L283.102962,45.353 L283.105796,45.353 L303.511472,45.353 C304.323014,45.353 305.0004,44.7373331 305.0004,43.8477183 L305.0004,40.0869004 Z" id="Fill-11"></path>
<path d="M130.467167,12.1249562 L130.467167,14.7109427 C130.467167,16.6847874 132.334047,17.8408416 134.467355,18.7259005 L143.332917,22.5361704 C146.666565,24.034332 149.0004,26.6866258 149.0004,30.7698129 L149.0004,35.6006393 C149.0004,39.6155971 140.732654,46.353 136.332353,46.353 C133.333859,46.353 128.800814,44.1754283 125.268523,42.2015836 C124.467355,41.7941298 123.934499,40.8398806 124.401454,39.7520557 L125.534951,37.4380255 C126.000965,36.4856983 127.000777,36.2810104 127.867845,36.7576545 C130.733595,38.2548551 134.533256,40.0922412 136.332353,40.0922412 C138.332917,40.0922412 142.533633,36.6221568 142.533633,34.6483121 L142.533633,31.6548718 C142.533633,29.3408416 140.800438,28.0483288 138.400701,27.0960016 L130.133897,23.5567269 C126.933934,22.1959849 124.0004,19.4062715 124.0004,15.5960016 L124.0004,11.172629 C124.0004,6.74925637 132.733219,0.353 136.933934,0.353 C139.733784,0.353 144.266828,2.46234236 147.267205,4.09600159 C148.200174,4.57264569 148.466602,5.66143152 148.065548,6.47730016 L146.999835,8.79133041 C146.666565,9.60719905 145.59991,9.88011624 144.666941,9.47074045 C142.399948,8.3829156 138.600287,6.61375875 136.933934,6.61375875 C135.000212,6.61375875 130.467167,9.88011624 130.467167,12.1249562" id="Fill-13"></path>
<path d="M260.452541,6.2347665 L253.794962,6.2347665 L253.794962,21.2809017 L260.520553,21.2809017 C263.334937,21.2809017 266.629241,16.28875 266.629241,13.6208602 C266.629241,11.0226504 263.196996,6.2347665 260.452541,6.2347665 M260.177616,27.1617137 L253.794962,27.1617137 L253.794962,43.8496274 C253.794962,44.7382876 253.1771,45.353 252.215344,45.353 L248.57906,45.353 C247.755245,45.353 247.0004,44.7382876 247.0004,43.8496274 L247.0004,3.49910555 C247.0004,1.9270073 248.853985,0.353 250.363675,0.353 L261.345327,0.353 C266.354316,0.353 273.0004,8.138084 273.0004,13.2667324 C273.0004,16.6867858 269.992516,21.8268885 266.767182,23.8113404 C269.374654,25.1782164 272.942925,28.8034921 272.942925,32.15482 L272.942925,43.8496274 C272.942925,44.669562 272.258008,45.353 271.433235,45.353 L267.65901,45.353 C266.835195,45.353 266.14932,44.669562 266.14932,43.8496274 L266.14932,33.1799769 C266.14932,30.786035 262.787004,27.1617137 260.177616,27.1617137" id="Fill-15"></path>
<path d="M168.088005,6.2347665 L161.61177,6.2347665 L161.61177,21.2809017 L168.154174,21.2809017 C170.893224,21.2809017 173.715218,16.28875 173.715218,13.6208602 C173.715218,11.0226504 170.758089,6.2347665 168.088005,6.2347665 M167.821462,27.1617137 L161.61177,27.1617137 L161.61177,43.8496274 C161.61177,44.7382876 161.00972,45.353 160.074957,45.353 L156.536281,45.353 C155.73479,45.353 155.0004,44.7382876 155.0004,43.8496274 L155.0004,3.49910555 C155.0004,1.9270073 156.803755,0.353 158.272534,0.353 L168.956598,0.353 C173.82985,0.353 180.0004,8.38053267 180.0004,13.5091811 C180.0004,16.9292345 177.526961,21.5290775 174.669552,24.1893312 C173.265078,25.4970268 171.033951,27.1617137 167.821462,27.1617137" id="Fill-17"></path>
<path d="M43.4842698,46.353 C39.1364838,46.353 31.0004,40.5896007 31.0004,35.6161579 L31.0004,2.23711395 C31.0004,1.40852873 31.7221122,0.722869628 32.5175626,0.722869628 L36.0026121,0.722869628 C36.8542878,0.722869628 37.5124009,1.40852873 37.5124009,2.23711395 L37.5124009,34.7219039 C37.5124009,37.1400594 41.4417229,40.0710106 43.4842698,40.0710106 C45.5268167,40.0710106 49.4828688,36.769224 49.4828688,34.3510686 L49.4828688,1.86724433 C49.4828688,1.03769338 50.0746175,0.353 50.8700678,0.353 L54.4832374,0.353 C55.2786878,0.353 56.0004,1.03769338 56.0004,1.86724433 L56.0004,35.2462883 C56.0004,40.0710106 47.8274472,46.353 43.4842698,46.353" id="Fill-19"></path>
<path d="M75.1577157,39.4720633 L68.7630696,39.4720633 L68.7630696,24.4256089 L75.2254092,24.4256089 C78.0265826,24.4256089 81.3063773,29.1553673 81.3063773,31.8223592 C81.3063773,34.4215786 77.8902423,39.4720633 75.1577157,39.4720633 L75.1577157,39.4720633 Z M68.7630696,6.23393672 L75.1577157,6.23393672 C77.8902423,6.23393672 81.3063773,10.1513794 81.3063773,12.7505988 C81.3063773,15.4175907 78.0265826,19.7731684 75.2254092,19.7731684 L68.7630696,19.7731684 L68.7630696,6.23393672 Z M81.7516285,22.1003432 C84.8645731,20.0480767 88.0004,16.0609524 88.0004,12.7505988 C88.0004,7.62184161 81.0327432,0.353 76.0463113,0.353 L65.3469347,0.353 C63.8452845,0.353 62.0004,1.92608614 62.0004,3.49821774 L62.0004,19.9860314 L62.0004,42.2068277 L62.0004,43.8495955 C62.0004,44.7392291 62.7517018,45.353 63.5716505,45.353 L65.3469347,45.353 L67.1918191,45.353 L76.277041,45.353 C81.263473,45.353 88.0004,36.9520709 88.0004,31.8223592 C88.0004,28.5139147 84.8645731,24.1755187 81.7516285,22.1003432 L81.7516285,22.1003432 Z" id="Fill-21"></path>
<path d="M9.9094313,23.1021303 L24.6262721,5.85868471 C25.1564897,5.23729068 25.1385642,4.31808569 24.4734336,3.73678161 L21.6591291,1.27888665 C21.0449446,0.741490582 20.091119,0.812125233 19.5609014,1.43351926 L6.69227461,16.5121083 L6.69227461,1.85828169 C6.69227461,1.0373925 6.08375083,0.353 5.20540101,0.353 L1.48727359,0.353 C0.67590856,0.353 0.0004,1.0373925 0.0004,1.85828169 L0.0004,43.8496274 C0.0004,44.669562 0.67590856,45.353 1.48727359,45.353 L5.20540101,45.353 C6.08375083,45.353 6.69227461,44.669562 6.69227461,43.8496274 L6.69227461,29.6921524 L19.5609014,44.7716959 C20.091119,45.3921354 21.0449446,45.4627701 21.6591291,44.9263285 L24.4734336,42.4684336 C25.1385642,41.886175 25.1564897,40.96697 24.6262721,40.3465305 L9.9094313,23.1021303 Z" id="Fill-23"></path>
</g>
<polygon id="Fill-1" fill="#00A971" points="64.9996 71.6465174 45.9996 60.647 45.9996 82.647"></polygon>
<polygon id="Fill-2" fill="#00A971" points="64.9996 19.647 45.9996 8.647 45.9996 30.647"></polygon>
<polygon id="Fill-3" fill="#00A971" points="19.6777987 45.6474704 36.9996 35.5556175 36.9996 3.647 0.9996 24.6199548 0.9996 66.6749859 36.9996 87.647 36.9996 55.7393232"></polygon>
<polygon id="Fill-4" fill="#00A971" points="36.9996 45.647 73.9996 66.647 73.9996 24.647"></polygon>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.9 KiB

Some files were not shown because too many files have changed in this diff Show More