diff --git a/cluster/cluster_acl.go b/cluster/cluster_acl.go index 4dac99f47..0f53b5c6f 100644 --- a/cluster/cluster_acl.go +++ b/cluster/cluster_acl.go @@ -22,7 +22,7 @@ type APIUser struct { Password string `json:"-"` GitToken string `json:"-"` GitUser string `json:"-"` - IsExternal bool `json:"-"` + IsExternal bool `json:"isExternal"` Roles map[string]bool `json:"roles"` Grants map[string]bool `json:"grants"` } @@ -818,6 +818,9 @@ func (cluster *Cluster) IsURLPassACL(strUser string, URL string, errorPrint bool if strings.Contains(URL, "/api/clusters/actions/delete") { return true } + if strings.Contains(URL, "/api/clusters/actions/rename") { + return true + } } if cluster.APIUsers[strUser].Grants[config.GrantClusterConfigGraphs] { if strings.Contains(URL, "/api/clusters/"+cluster.Name+"/settings/actions/set-graphite-filterlist") { diff --git a/cluster/cluster_set.go b/cluster/cluster_set.go index 752b7d4c4..dd4271bc9 100644 --- a/cluster/cluster_set.go +++ b/cluster/cluster_set.go @@ -13,6 +13,7 @@ import ( "fmt" "io" "net/http" + "os" "os/exec" "strconv" "strings" @@ -2228,3 +2229,34 @@ func (cluster *Cluster) SetResticVersion() error { func (cluster *Cluster) SetInRollingRestart(value bool) { cluster.InRollingRestart = value } + +func (cluster *Cluster) RenameCluster(newClusterName string) error { + + cluster.Lock() + defer func() { + cluster.Unlock() + cluster.Save() + }() + + cluster.LogModulePrintf(cluster.Conf.Verbose, config.ConstLogModGeneral, config.LvlInfo, "Initiate rename cluster %s to %s", cluster.Name, newClusterName) + + // Rename cluster directory + err := os.Rename(cluster.Conf.WorkingDir+"/"+cluster.Name, cluster.Conf.WorkingDir+"/"+newClusterName) + if err != nil { + cluster.LogModulePrintf(cluster.Conf.Verbose, config.ConstLogModGeneral, config.LvlErr, "Rename cluster working directory fail: %s", err) + return err + } + + // Rename cluster configuration file + err = os.Rename(cluster.Conf.WorkingDir+"/"+newClusterName+"/"+cluster.Name+".toml", cluster.Conf.WorkingDir+"/"+newClusterName+"/"+newClusterName+".toml") + if err != nil { + cluster.LogModulePrintf(cluster.Conf.Verbose, config.ConstLogModGeneral, config.LvlErr, "Rename cluster working directory fail: %s", err) + return err + } + + // Rename cluster name + cluster.Name = newClusterName + + return nil + +} diff --git a/cluster/prov_onpremise_db.go b/cluster/prov_onpremise_db.go index 4d41ae166..2d6973dc2 100644 --- a/cluster/prov_onpremise_db.go +++ b/cluster/prov_onpremise_db.go @@ -114,6 +114,7 @@ func (cluster *Cluster) OnPremiseGetNodes() ([]Agent, error) { client, err := cluster.OnPremiseConnect(server) if err != nil { cluster.errorChan <- err + continue } defer client.Close() diff --git a/config/config.go b/config/config.go index 47129a4fa..a7201d526 100644 --- a/config/config.go +++ b/config/config.go @@ -1034,7 +1034,6 @@ const ( GrantDBDebug string = "db-debug" GrantClusterCreate string = "cluster-create" GrantClusterDelete string = "cluster-delete" - GrantClusterDrop string = "cluster-drop" GrantClusterCreateMonitor string = "cluster-create-monitor" GrantClusterDropMonitor string = "cluster-drop-monitor" GrantClusterFailover string = "cluster-failover" @@ -2127,7 +2126,7 @@ func GetGrantType() map[string]string { GrantDBShowLogs: GrantDBShowLogs, GrantDBDebug: GrantDBDebug, GrantClusterCreate: GrantClusterCreate, - GrantClusterDrop: GrantClusterDrop, + GrantClusterDelete: GrantClusterDelete, GrantClusterCreateMonitor: GrantClusterCreateMonitor, GrantClusterDropMonitor: GrantClusterDropMonitor, GrantClusterFailover: GrantClusterFailover, @@ -2221,7 +2220,7 @@ func HasAllDBGrants(grants map[string]bool) bool { func GetGrantCluster() []string { return []string{ GrantClusterCreate, - GrantClusterDrop, + GrantClusterDelete, GrantClusterCreateMonitor, GrantClusterDropMonitor, GrantClusterFailover, diff --git a/docs/docs.go b/docs/docs.go index a23e5375e..a5121b2bb 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -155,14 +155,61 @@ const docTemplate = `{ "type": "string" } }, - "400": { - "description": "Invalid cluster name", + "500": { + "description": "Invalid cluster name\" or \"No Valid ACL", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/clusters/actions/rename/{clusterName}/{newClusterName}": { + "post": { + "description": "Renames a cluster identified by its name.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Cluster" + ], + "summary": "Rename a cluster", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Cluster Name", + "name": "clusterName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "New Cluster Name", + "name": "newClusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Cluster renamed successfully", "schema": { "type": "string" } }, "500": { - "description": "Internal server error", + "description": "Invalid cluster name\" or \"Cluster name already exists\" or \"No Valid ACL", "schema": { "type": "string" } @@ -1020,6 +1067,55 @@ const docTemplate = `{ } } }, + "/api/clusters/{clusterName}/actions/refresh-staging": { + "post": { + "description": "Refreshes the staging cluster specified by the cluster name in the URL.", + "produces": [ + "application/json" + ], + "tags": [ + "ClusterActions" + ], + "summary": "Refresh Staging Cluster", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Cluster Name", + "name": "clusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Staging cluster refresh initiated", + "schema": { + "type": "string" + } + }, + "403": { + "description": "No valid ACL", + "schema": { + "type": "string" + } + }, + "500": { + "description": "No cluster", + "schema": { + "type": "string" + } + } + } + } + }, "/api/clusters/{clusterName}/actions/replication/bootstrap/{topology}": { "post": { "description": "This endpoint triggers the bootstrap replication process for the specified cluster.", diff --git a/docs/swagger.json b/docs/swagger.json index f545ca69a..310fa4dd0 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -144,14 +144,61 @@ "type": "string" } }, - "400": { - "description": "Invalid cluster name", + "500": { + "description": "Invalid cluster name\" or \"No Valid ACL", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/clusters/actions/rename/{clusterName}/{newClusterName}": { + "post": { + "description": "Renames a cluster identified by its name.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Cluster" + ], + "summary": "Rename a cluster", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Cluster Name", + "name": "clusterName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "New Cluster Name", + "name": "newClusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Cluster renamed successfully", "schema": { "type": "string" } }, "500": { - "description": "Internal server error", + "description": "Invalid cluster name\" or \"Cluster name already exists\" or \"No Valid ACL", "schema": { "type": "string" } @@ -1009,6 +1056,55 @@ } } }, + "/api/clusters/{clusterName}/actions/refresh-staging": { + "post": { + "description": "Refreshes the staging cluster specified by the cluster name in the URL.", + "produces": [ + "application/json" + ], + "tags": [ + "ClusterActions" + ], + "summary": "Refresh Staging Cluster", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cAdd access token here\u003e", + "description": "Insert your access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Cluster Name", + "name": "clusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Staging cluster refresh initiated", + "schema": { + "type": "string" + } + }, + "403": { + "description": "No valid ACL", + "schema": { + "type": "string" + } + }, + "500": { + "description": "No cluster", + "schema": { + "type": "string" + } + } + } + } + }, "/api/clusters/{clusterName}/actions/replication/bootstrap/{topology}": { "post": { "description": "This endpoint triggers the bootstrap replication process for the specified cluster.", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d747b78f1..1024601b0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3492,6 +3492,40 @@ paths: summary: Optimize a specific cluster tags: - ClusterActions + /api/clusters/{clusterName}/actions/refresh-staging: + post: + description: Refreshes the staging cluster specified by the cluster name in + the URL. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - description: Cluster Name + in: path + name: clusterName + required: true + type: string + produces: + - application/json + responses: + "200": + description: Staging cluster refresh initiated + schema: + type: string + "403": + description: No valid ACL + schema: + type: string + "500": + description: No cluster + schema: + type: string + summary: Refresh Staging Cluster + tags: + - ClusterActions /api/clusters/{clusterName}/actions/replication/bootstrap/{topology}: post: consumes: @@ -9847,15 +9881,48 @@ paths: description: Cluster deleted successfully schema: type: string - "400": - description: Invalid cluster name + "500": + description: Invalid cluster name" or "No Valid ACL + schema: + type: string + summary: Delete a cluster + tags: + - Cluster + /api/clusters/actions/rename/{clusterName}/{newClusterName}: + post: + consumes: + - application/json + description: Renames a cluster identified by its name. + parameters: + - default: Bearer + description: Insert your access token + in: header + name: Authorization + required: true + type: string + - description: Cluster Name + in: path + name: clusterName + required: true + type: string + - description: New Cluster Name + in: path + name: newClusterName + required: true + type: string + produces: + - application/json + responses: + "200": + description: Cluster renamed successfully schema: type: string "500": - description: Internal server error + description: Invalid cluster name" or "Cluster name already exists" or "No + Valid ACL schema: type: string - summary: Delete a cluster + summary: Rename a cluster tags: - Cluster /api/clusters/for-sale: diff --git a/server/api.go b/server/api.go index 6e4552b29..80395d5b7 100644 --- a/server/api.go +++ b/server/api.go @@ -1298,14 +1298,67 @@ func (repman *ReplicationManager) handlerMuxClusterAdd(w http.ResponseWriter, r // @Param Authorization header string true "Insert your access token" default(Bearer ) // @Param clusterName path string true "Cluster Name" // @Success 200 {string} string "Cluster deleted successfully" -// @Failure 400 {string} string "Invalid cluster name" -// @Failure 500 {string} string "Internal server error" +// @Failure 500 {string} string "Invalid cluster name" or "No Valid ACL" // @Router /api/clusters/actions/delete/{clusterName} [delete] func (repman *ReplicationManager) handlerMuxClusterDelete(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") vars := mux.Vars(r) + + mycluster := repman.getClusterByName(vars["clusterName"]) + if mycluster == nil { + http.Error(w, "Invalid cluster name", 500) + return + } + + valid, _ := repman.IsValidClusterACL(r, mycluster) + if !valid { + http.Error(w, "No Valid ACL", 500) + return + } + repman.DeleteCluster(vars["clusterName"]) +} + +// handlerMuxClusterRename handles the HTTP request to rename a cluster. +// @Summary Rename a cluster +// @Description Renames a cluster identified by its name. +// @Tags Cluster +// @Accept json +// @Produce json +// @Param Authorization header string true "Insert your access token" default(Bearer ) +// @Param clusterName path string true "Cluster Name" +// @Param newClusterName path string true "New Cluster Name" +// @Success 200 {string} string "Cluster renamed successfully" +// @Failure 500 {string} string "Invalid cluster name" or "Cluster name already exists" or "No Valid ACL" +// @Router /api/clusters/actions/rename/{clusterName}/{newClusterName} [post] +func (repman *ReplicationManager) handlerMuxClusterRename(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + vars := mux.Vars(r) + + mycluster := repman.getClusterByName(vars["clusterName"]) + if mycluster == nil { + http.Error(w, "Invalid cluster name", 500) + return + } + + if slices.Contains(repman.ImmutableClusterList, mycluster.Name) { + http.Error(w, "Cluster is not dynamic", 500) + return + } + + valid, _ := repman.IsValidClusterACL(r, mycluster) + if !valid { + http.Error(w, "No Valid ACL", 500) + return + } + + // Check if new cluster name is already exist + if newcluster := repman.getClusterByName(vars["newClusterName"]); newcluster != nil { + http.Error(w, "Cluster name already exists", 500) + return + } + mycluster.RenameCluster(vars["newClusterName"]) } // handlerMuxPrometheus handles HTTP requests to fetch Prometheus metrics for all servers in all clusters. diff --git a/server/api_cluster.go b/server/api_cluster.go index 3dadf3ede..712c48ff7 100644 --- a/server/api_cluster.go +++ b/server/api_cluster.go @@ -320,6 +320,11 @@ func (repman *ReplicationManager) apiClusterProtectedHandler(router *mux.Router) negroni.Wrap(http.HandlerFunc(repman.handlerMuxClusterDelete)), )) + router.Handle("/api/clusters/actions/rename/{clusterName}/{newClusterName}", negroni.New( + negroni.HandlerFunc(repman.validateTokenMiddleware), + negroni.Wrap(http.HandlerFunc(repman.handlerMuxClusterRename)), + )) + router.Handle("/api/clusters/{clusterName}/topology/servers", negroni.New( negroni.HandlerFunc(repman.validateTokenMiddleware), negroni.Wrap(http.HandlerFunc(repman.handlerMuxServers)), diff --git a/server/server.go b/server/server.go index 90dae7d3d..6a12e9fec 100644 --- a/server/server.go +++ b/server/server.go @@ -70,30 +70,31 @@ import ( var RepMan *ReplicationManager type ReplicationManager struct { - OpenSVC opensvc.Collector `json:"-"` - Version string `json:"version"` - Fullversion string `json:"fullVersion"` - Os string `json:"os"` - OsUser *user.User `json:"osUser"` - Arch string `json:"arch"` - MemProfile string `json:"memprofile"` - CpuProfile string `json:"cpuprofile"` - Clusters map[string]*cluster.Cluster `json:"-"` - PeerClusters []config.PeerCluster `json:"-"` - PeerBooked map[string]string `json:"-"` - Partners []config.Partner `json:"partners"` - Partner config.Partner `json:"partner"` - Agents []opensvc.Host `json:"agents"` - UUID string `json:"uuid"` - Hostname string `json:"hostname"` - Status string `json:"status"` - SplitBrain bool `json:"spitBrain"` - ClusterList []string `json:"clusters"` - Tests []string `json:"tests"` - Conf config.Config `json:"config"` - ImmuableFlagMaps map[string]map[string]interface{} `json:"-"` - DynamicFlagMaps map[string]map[string]interface{} `json:"-"` - DefaultFlagMap map[string]interface{} `json:"-"` + OpenSVC opensvc.Collector `json:"-"` + Version string `json:"version"` + Fullversion string `json:"fullVersion"` + Os string `json:"os"` + OsUser *user.User `json:"osUser"` + Arch string `json:"arch"` + MemProfile string `json:"memprofile"` + CpuProfile string `json:"cpuprofile"` + Clusters map[string]*cluster.Cluster `json:"-"` + PeerClusters []config.PeerCluster `json:"-"` + PeerBooked map[string]string `json:"-"` + Partners []config.Partner `json:"partners"` + Partner config.Partner `json:"partner"` + Agents []opensvc.Host `json:"agents"` + UUID string `json:"uuid"` + Hostname string `json:"hostname"` + Status string `json:"status"` + SplitBrain bool `json:"spitBrain"` + ClusterList []string `json:"clusters"` + ImmutableClusterList []string `json:"-"` + Tests []string `json:"tests"` + Conf config.Config `json:"config"` + ImmuableFlagMaps map[string]map[string]interface{} `json:"-"` + DynamicFlagMaps map[string]map[string]interface{} `json:"-"` + DefaultFlagMap map[string]interface{} `json:"-"` //Adding default flags from AddFlags CommandLineFlag []string `json:"-"` ConfigPathList []string `json:"-"` @@ -1240,6 +1241,8 @@ func (repman *ReplicationManager) InitConfig(conf config.Config, init_git bool) repman.Logrus.Warning("No include directory in default section") } + repman.ImmutableClusterList = strings.Split(repman.DiscoverClusters(fistRead), ",") + tmp_read := fistRead.Sub("Default") if tmp_read != nil { tmp_read.Unmarshal(&conf) diff --git a/server/server_del.go b/server/server_del.go index c487ea4b5..f100b4148 100644 --- a/server/server_del.go +++ b/server/server_del.go @@ -9,6 +9,7 @@ package server import ( "os" + "github.com/signal18/replication-manager/config" log "github.com/sirupsen/logrus" ) @@ -45,13 +46,14 @@ func (repman *ReplicationManager) DeleteCluster(clusterName string) error { err := os.RemoveAll(repman.Conf.WorkingDir + "/" + clusterName) if err != nil { - log.Errorf("Delete cluster working directory fail: %s", err) - + repman.LogModulePrintf(repman.Conf.Verbose, config.ConstLogModGeneral, config.LvlErr, "Delete cluster working directory fail: %s", err) } + if repman.currentCluster == cl { repman.currentCluster = nil } - log.Warnf("Cluster %s is delete\n", clusterName) + + repman.LogModulePrintf(repman.Conf.Verbose, config.ConstLogModGeneral, config.LvlWarn, "Cluster %s is deleted\n", clusterName) return nil } diff --git a/share/dashboard_react/src/Pages/Dashboard/components/ClusterDetail.jsx b/share/dashboard_react/src/Pages/Dashboard/components/ClusterDetail.jsx index 5cb1237ac..e34aa66ad 100644 --- a/share/dashboard_react/src/Pages/Dashboard/components/ClusterDetail.jsx +++ b/share/dashboard_react/src/Pages/Dashboard/components/ClusterDetail.jsx @@ -113,7 +113,7 @@ function ClusterDetail({ selectedCluster }) { openConfirmModal() setConfirmTitle('Confirm switchover?') setConfirmHandler( - () => () => dispatch(switchOverCluster({ clustclusterName: selectedCluster?.nameerName })) + () => () => dispatch(switchOverCluster({ clusterName: selectedCluster?.name })) ) } } diff --git a/share/dashboard_react/src/Pages/Dashboard/components/Proxies/ProxyGrid/index.jsx b/share/dashboard_react/src/Pages/Dashboard/components/Proxies/ProxyGrid/index.jsx index f9bc92e86..7c8c15a0d 100644 --- a/share/dashboard_react/src/Pages/Dashboard/components/Proxies/ProxyGrid/index.jsx +++ b/share/dashboard_react/src/Pages/Dashboard/components/Proxies/ProxyGrid/index.jsx @@ -12,7 +12,7 @@ import RMIconButton from '../../../../../components/RMIconButton' import styles from './styles.module.scss' import ServerName from '../../../../../components/ServerName' -function ProxyGrid({ proxies, clusterName, showTableView, user, isDesktop, isMenuOptionsVisible }) { +function ProxyGrid({ proxies = [], clusterName, showTableView, user, isDesktop, isMenuOptionsVisible }) { return ( {proxies?.length > 0 && diff --git a/share/dashboard_react/src/Pages/Dashboard/components/Proxies/ProxyTable/index.jsx b/share/dashboard_react/src/Pages/Dashboard/components/Proxies/ProxyTable/index.jsx index 0a4be6316..81ee3e96b 100644 --- a/share/dashboard_react/src/Pages/Dashboard/components/Proxies/ProxyTable/index.jsx +++ b/share/dashboard_react/src/Pages/Dashboard/components/Proxies/ProxyTable/index.jsx @@ -12,7 +12,7 @@ import RMIconButton from '../../../../../components/RMIconButton' import styles from './styles.module.scss' import ServerName from '../../../../../components/ServerName' -function ProxyTable({ proxies, isDesktop, clusterName, showGridView, user, isMenuOptionsVisible }) { +function ProxyTable({ proxies = [], isDesktop, clusterName, showGridView, user, isMenuOptionsVisible }) { const [tableData, setTableData] = useState([]) useEffect(() => { if (proxies?.length > 0) { diff --git a/share/dashboard_react/src/Pages/Home/index.jsx b/share/dashboard_react/src/Pages/Home/index.jsx index b02865819..fd87904f2 100644 --- a/share/dashboard_react/src/Pages/Home/index.jsx +++ b/share/dashboard_react/src/Pages/Home/index.jsx @@ -237,7 +237,7 @@ function Home() { ...(isClusterOpenRef.current ? [ , - , + , , ...(selectedCluster?.config?.graphiteMetrics && user?.grants['cluster-show-graphs'] ? [] diff --git a/share/dashboard_react/src/Pages/Settings/GeneralSettings.jsx b/share/dashboard_react/src/Pages/Settings/GeneralSettings.jsx index 82fe2d844..02981f79a 100644 --- a/share/dashboard_react/src/Pages/Settings/GeneralSettings.jsx +++ b/share/dashboard_react/src/Pages/Settings/GeneralSettings.jsx @@ -7,8 +7,12 @@ import { convertObjectToArrayForDropdown } from '../../utility/common' import { useDispatch, useSelector } from 'react-redux' import TableType2 from '../../components/TableType2' import { changeTopology, switchSetting } from '../../redux/settingsSlice' +import { dropCluster, renameCluster } from '../../redux/globalClustersSlice' +import { TbTrash } from 'react-icons/tb' +import RMIconButton from '../../components/RMIconButton' +import TextForm from '../../components/TextForm' -function GeneralSettings({ selectedCluster, user }) { +function GeneralSettings({ selectedCluster, user, openConfirmModal, onTabChange }) { const [topologyOptions, setTopologyOptions] = useState([]) const dispatch = useDispatch() @@ -114,6 +118,28 @@ function GeneralSettings({ selectedCluster, user }) { onChange={() => dispatch(switchSetting({ clusterName: selectedCluster?.name, setting: 'test' }))} /> ) + }, + { + key: 'Cluster Name (alpha-numeric)', + value: ( + { + dispatch(renameCluster({ clusterName: selectedCluster?.name, newClusterName: value })).then(() => { onTabChange(0)}) + }} + /> + ) + }, + { + key: 'Drop Cluster', + value: ( + { + openConfirmModal(`Confirm drop cluster? This action can not be undone!`, () => () => { dispatch(dropCluster({ clusterName: selectedCluster?.name })).then(() => { onTabChange(0)}) + }) + }} /> + ) } ] diff --git a/share/dashboard_react/src/Pages/Settings/StagingSettings.jsx b/share/dashboard_react/src/Pages/Settings/StagingSettings.jsx index 01e44fe1c..8213bce9b 100644 --- a/share/dashboard_react/src/Pages/Settings/StagingSettings.jsx +++ b/share/dashboard_react/src/Pages/Settings/StagingSettings.jsx @@ -52,7 +52,7 @@ function StagingSettings({ selectedCluster, user, openConfirmModal }) { ) }, ...(selectedCluster?.config?.topologyStaging ? [{ - key: 'Staging post-detach script', + key: 'Refresh Staging', value: ( { openConfirmModal(`Confirm refresh-staging? This action can not be undone!`, () => () => { diff --git a/share/dashboard_react/src/Pages/Settings/index.jsx b/share/dashboard_react/src/Pages/Settings/index.jsx index 959617fab..e3ea6f5fe 100644 --- a/share/dashboard_react/src/Pages/Settings/index.jsx +++ b/share/dashboard_react/src/Pages/Settings/index.jsx @@ -16,7 +16,7 @@ import RepConfigSettings from './RepConfigSettings' import AlertSettings from './AlertSettings' import StagingSettings from './StagingSettings' -function Settings({ selectedCluster, user }) { +function Settings({ selectedCluster, user, onTabChange }) { const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false) const [confirmHandler, setConfirmHandler] = useState(null) const [confirmTitle, setConfirmTitle] = useState('') @@ -111,7 +111,7 @@ function Settings({ selectedCluster, user }) { isOpen={isGeneralOpen} headerClassName={styles.accordionHeader} panelClassName={styles.accordionPanel} - body={} + body={} /> {label && ( @@ -54,6 +56,7 @@ function TextForm({ onSave, id, label, value, loading, maxLength = 120, classNam { setIsConfirmModalOpen(true) diff --git a/share/dashboard_react/src/redux/globalClustersSlice.js b/share/dashboard_react/src/redux/globalClustersSlice.js index 2abbc23f2..5f1cd1815 100644 --- a/share/dashboard_react/src/redux/globalClustersSlice.js +++ b/share/dashboard_react/src/redux/globalClustersSlice.js @@ -3,7 +3,7 @@ import { handleError, showErrorBanner, showSuccessBanner } from '../utility/comm import { globalClustersService } from '../services/globalClustersService' import { Link } from '@chakra-ui/react' -export const getClusters = createAsyncThunk('globalClusters/getClusters', async ({}, thunkAPI) => { +export const getClusters = createAsyncThunk('globalClusters/getClusters', async ({ }, thunkAPI) => { try { const { data, status } = await globalClustersService.getClusters() return { data, status } @@ -14,16 +14,50 @@ export const getClusters = createAsyncThunk('globalClusters/getClusters', async export const addCluster = createAsyncThunk('globalClusters/addCluster', async ({ clusterName, formdata }, thunkAPI) => { try { - const { data, status } = await globalClustersService.addCluster(clusterName,formdata) - showSuccessBanner("Add cluster '"+clusterName+"' is successful!", status, thunkAPI) - return { data, status } + const { data, status } = await globalClustersService.addCluster(clusterName, formdata) + if (status === 200) { + showSuccessBanner("Add cluster '" + clusterName + "' is successful!", status, thunkAPI) + return { data, status } + } else { + throw new Error(data) + } + } catch (error) { + showErrorBanner("Add cluster '" + clusterName + "' is failed!", error, thunkAPI) + handleError(error, thunkAPI) + } +}) + +export const dropCluster = createAsyncThunk('globalClusters/dropCluster', async ({ clusterName }, thunkAPI) => { + try { + const { data, status } = await globalClustersService.dropCluster(clusterName) + if (status === 200) { + showSuccessBanner("Drop cluster '" + clusterName + "' is successful!", status, thunkAPI) + return { data, status } + } else { + throw new Error(data) + } + } catch (error) { + showErrorBanner("Drop cluster '" + clusterName + "' is failed!", error, thunkAPI) + handleError(error, thunkAPI) + } +}) + +export const renameCluster = createAsyncThunk('globalClusters/renameCluster', async ({ clusterName, newClusterName }, thunkAPI) => { + try { + const { data, status } = await globalClustersService.renameCluster(clusterName, newClusterName) + if (status === 200) { + showSuccessBanner("Rename cluster '" + clusterName + "' is successful!", status, thunkAPI) + return { data, status } + } else { + throw new Error(data) + } } catch (error) { - showErrorBanner("Add cluster '"+clusterName+"' is failed!", error, thunkAPI) + showErrorBanner("Rename cluster '" + clusterName + "' is failed!", error, thunkAPI) handleError(error, thunkAPI) } }) -export const getClusterPeers = createAsyncThunk('globalClusters/getClusterPeers', async ({}, thunkAPI) => { +export const getClusterPeers = createAsyncThunk('globalClusters/getClusterPeers', async ({ }, thunkAPI) => { try { const { data, status } = await globalClustersService.getClusterPeers() return { data, status } @@ -32,7 +66,7 @@ export const getClusterPeers = createAsyncThunk('globalClusters/getClusterPeers' } }) -export const getClusterForSale = createAsyncThunk('globalClusters/getClusterForSale', async ({}, thunkAPI) => { +export const getClusterForSale = createAsyncThunk('globalClusters/getClusterForSale', async ({ }, thunkAPI) => { try { const { data, status } = await globalClustersService.getClusterForSale() return { data, status } @@ -41,7 +75,7 @@ export const getClusterForSale = createAsyncThunk('globalClusters/getClusterForS } }) -export const getMonitoredData = createAsyncThunk('globalClusters/getMonitoredData', async ({}, thunkAPI) => { +export const getMonitoredData = createAsyncThunk('globalClusters/getMonitoredData', async ({ }, thunkAPI) => { try { const { data, status } = await globalClustersService.getMonitoredData() return { data, status } @@ -96,20 +130,20 @@ export const setGlobalSetting = createAsyncThunk( } ) -export const reloadClustersPlan = createAsyncThunk('globalClusters/reloadClustersPlan',async ({}, thunkAPI) => { - try { - const { data, status } = await globalClustersService.reloadClustersPlan() - showSuccessBanner('All clusters plan reloaded!', status, thunkAPI) - return { data, status } - } catch (error) { - console.log('error::', error) - showErrorBanner('Failed to reload clusters plans!', error, thunkAPI) - handleError(error, thunkAPI) - } +export const reloadClustersPlan = createAsyncThunk('globalClusters/reloadClustersPlan', async ({ }, thunkAPI) => { + try { + const { data, status } = await globalClustersService.reloadClustersPlan() + showSuccessBanner('All clusters plan reloaded!', status, thunkAPI) + return { data, status } + } catch (error) { + console.log('error::', error) + showErrorBanner('Failed to reload clusters plans!', error, thunkAPI) + handleError(error, thunkAPI) } +} ) -export const getTermsData = createAsyncThunk('globalClusters/getTermsData', async ({}, thunkAPI) => { +export const getTermsData = createAsyncThunk('globalClusters/getTermsData', async ({ }, thunkAPI) => { try { const { data, status } = await globalClustersService.getTermsData() return { data, status } @@ -122,8 +156,8 @@ const initialState = { loading: false, error: null, clusters: null, - isDownList: {}, - isFailableList: {}, + isDownList: {}, + isFailableList: {}, clusterPeers: null, clusterForSale: null, monitor: null, @@ -159,28 +193,28 @@ export const globalClustersSlice = createSlice({ state.loading = false state.error = action.error }) - .addCase(getMonitoredData.pending, (state) => {}) + .addCase(getMonitoredData.pending, (state) => { }) .addCase(getMonitoredData.fulfilled, (state, action) => { state.monitor = action.payload.data }) .addCase(getMonitoredData.rejected, (state, action) => { state.error = action.error }) - .addCase(getTermsData.pending, (state) => {}) + .addCase(getTermsData.pending, (state) => { }) .addCase(getTermsData.fulfilled, (state, action) => { state.terms = action.payload.data }) .addCase(getTermsData.rejected, (state, action) => { state.error = action.error }) - .addCase(getClusterPeers.pending, (state) => {}) + .addCase(getClusterPeers.pending, (state) => { }) .addCase(getClusterPeers.fulfilled, (state, action) => { state.clusterPeers = action.payload.data }) .addCase(getClusterPeers.rejected, (state, action) => { state.error = action.error }) - .addCase(getClusterForSale.pending, (state) => {}) + .addCase(getClusterForSale.pending, (state) => { }) .addCase(getClusterForSale.fulfilled, (state, action) => { state.clusterForSale = action.payload.data }) diff --git a/share/dashboard_react/src/services/globalClustersService.js b/share/dashboard_react/src/services/globalClustersService.js index 5f3513fca..26b233ac4 100644 --- a/share/dashboard_react/src/services/globalClustersService.js +++ b/share/dashboard_react/src/services/globalClustersService.js @@ -10,6 +10,8 @@ export const globalClustersService = { setGlobalSetting, clearGlobalSetting, addCluster, + dropCluster, + renameCluster, reloadClustersPlan } @@ -49,6 +51,14 @@ function addCluster(clusterName, formdata) { return getApi().post(`clusters/actions/add/${clusterName}`, formdata) } +function dropCluster(clusterName) { + return getApi().post(`clusters/actions/delete/${clusterName}`) +} + +function renameCluster(clusterName, newClusterName) { + return getApi().post(`clusters/actions/rename/${clusterName}/${newClusterName}`) +} + function reloadClustersPlan() { return getApi().get(`clusters/settings/actions/reload-clusters-plans`) }