From 45c16e78484f8d83c3d66bf3e9738087d8a6adbf Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Tue, 25 Jun 2019 14:44:21 -0700 Subject: [PATCH] Kelp UI: Edit bots part 3 (#189) addresses part of issue #67 * 1 - remove hardcoded error message * 2 - bot is only editable when stopped * 3 - disable editing names for now * 4 - streamline frontend save logic workflow * 5 - added /updateBotConfig endpoint and integrated to frontend * 6 - fix checks around null values of _asyncRequests and .cancel() calls --- gui/backend/api_server.go | 2 +- gui/backend/routes.go | 1 + gui/backend/update_bot_config.go | 73 +++++++++++++++++++ gui/web/src/App.js | 16 ++-- .../components/molecules/BotCard/BotCard.js | 56 +++++++++----- gui/web/src/components/molecules/Form/Form.js | 32 +++----- .../PriceFeedAsset/PriceFeedAsset.js | 10 ++- gui/web/src/components/screens/Bots/Bots.js | 29 ++++++-- .../src/components/screens/NewBot/NewBot.js | 51 ++++++++++--- gui/web/src/kelp-ops-api/updateBotConfig.js | 8 ++ 10 files changed, 215 insertions(+), 63 deletions(-) create mode 100644 gui/backend/update_bot_config.go create mode 100644 gui/web/src/kelp-ops-api/updateBotConfig.js diff --git a/gui/backend/api_server.go b/gui/backend/api_server.go index f048a1fbe..5d853752e 100644 --- a/gui/backend/api_server.go +++ b/gui/backend/api_server.go @@ -61,7 +61,7 @@ type ErrorResponse struct { } func (s *APIServer) writeErrorJson(w http.ResponseWriter, message string) { - log.Print(message) + log.Println(message) w.WriteHeader(http.StatusInternalServerError) marshalledJson, e := json.MarshalIndent(ErrorResponse{Error: message}, "", " ") diff --git a/gui/backend/routes.go b/gui/backend/routes.go index 169817e8a..007287f18 100644 --- a/gui/backend/routes.go +++ b/gui/backend/routes.go @@ -21,5 +21,6 @@ func SetRoutes(r *chi.Mux, s *APIServer) { r.Post("/getBotInfo", http.HandlerFunc(s.getBotInfo)) r.Post("/getBotConfig", http.HandlerFunc(s.getBotConfig)) r.Post("/fetchPrice", http.HandlerFunc(s.fetchPrice)) + r.Post("/updateBotConfig", http.HandlerFunc(s.updateBotConfig)) }) } diff --git a/gui/backend/update_bot_config.go b/gui/backend/update_bot_config.go new file mode 100644 index 000000000..49752479f --- /dev/null +++ b/gui/backend/update_bot_config.go @@ -0,0 +1,73 @@ +package backend + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/stellar/kelp/gui/model" + "github.com/stellar/kelp/plugins" + "github.com/stellar/kelp/support/kelpos" + "github.com/stellar/kelp/support/toml" + "github.com/stellar/kelp/trader" +) + +type updateBotConfigRequest struct { + Name string `json:"name"` + Strategy string `json:"strategy"` + TraderConfig trader.BotConfig `json:"trader_config"` + StrategyConfig plugins.BuySellConfig `json:"strategy_config"` +} + +type updateBotConfigResponse struct { + Success bool `json:"success"` +} + +func (s *APIServer) updateBotConfig(w http.ResponseWriter, r *http.Request) { + bodyBytes, e := ioutil.ReadAll(r.Body) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error reading request input: %s", e)) + return + } + log.Printf("updateBotConfig requestJson: %s\n", string(bodyBytes)) + + var req updateBotConfigRequest + e = json.Unmarshal(bodyBytes, &req) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) + return + } + + botState, e := s.kos.QueryBotState(req.Name) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error getting bot state for bot '%s': %s", req.Name, e)) + return + } + if botState != kelpos.BotStateStopped { + s.writeErrorJson(w, fmt.Sprintf("bot state needs to be '%s' when updating bot config, but was '%s'\n", kelpos.BotStateStopped, botState)) + return + } + + filenamePair := model.GetBotFilenames(req.Name, req.Strategy) + traderFilePath := fmt.Sprintf("%s/%s", s.configsDir, filenamePair.Trader) + botConfig := req.TraderConfig + log.Printf("updating bot config to file: %s\n", traderFilePath) + e = toml.WriteFile(traderFilePath, &botConfig) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error writing trader botConfig toml file for bot '%s': %s", req.Name, e)) + return + } + + strategyFilePath := fmt.Sprintf("%s/%s", s.configsDir, filenamePair.Strategy) + strategyConfig := req.StrategyConfig + log.Printf("updating strategy config to file: %s\n", strategyFilePath) + e = toml.WriteFile(strategyFilePath, &strategyConfig) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error writing strategy toml file for bot '%s': %s", req.Name, e)) + return + } + + s.writeJson(w, updateBotConfigResponse{Success: true}) +} diff --git a/gui/web/src/App.js b/gui/web/src/App.js index 49c6809ff..f1e029933 100644 --- a/gui/web/src/App.js +++ b/gui/web/src/App.js @@ -25,20 +25,26 @@ class App extends Component { this.state = { version: "" }; + + this._asyncRequests = {}; } componentDidMount() { var _this = this - this._asyncRequest = version(baseUrl).then(resp => { - _this._asyncRequest = null; + this._asyncRequests["version"] = version(baseUrl).then(resp => { + if (!_this._asyncRequests["version"]) { + // if it has been deleted it means we don't want to process the result + return + } + + delete _this._asyncRequests["version"]; _this.setState({version: resp}); }); } componentWillUnmount() { - if (this._asyncRequest) { - this._asyncRequest.cancel(); - this._asyncRequest = null; + if (this._asyncRequests["version"]) { + delete this._asyncRequests["version"]; } } diff --git a/gui/web/src/components/molecules/BotCard/BotCard.js b/gui/web/src/components/molecules/BotCard/BotCard.js index daa8c02d2..37a4237a7 100644 --- a/gui/web/src/components/molecules/BotCard/BotCard.js +++ b/gui/web/src/components/molecules/BotCard/BotCard.js @@ -84,10 +84,15 @@ class BotCard extends Component { }; checkState() { - if (this._asyncRequests["state"] == null) { + if (!this._asyncRequests["state"]) { var _this = this; this._asyncRequests["state"] = getState(this.props.baseUrl, this.props.name).then(resp => { - _this._asyncRequests["state"] = null; + if (!_this._asyncRequests["state"]) { + // if it has been deleted it means we don't want to process the result + return + } + + delete _this._asyncRequests["state"]; let state = resp.trim(); if (_this.state.state !== state) { _this.setState({ @@ -99,10 +104,15 @@ class BotCard extends Component { } checkBotInfo() { - if (this._asyncRequests["botInfo"] == null) { + if (!this._asyncRequests["botInfo"]) { var _this = this; this._asyncRequests["botInfo"] = getBotInfo(this.props.baseUrl, this.props.name).then(resp => { - _this._asyncRequests["botInfo"] = null; + if (!_this._asyncRequests["botInfo"]) { + // if it has been deleted it means we don't want to process the result + return + } + + delete _this._asyncRequests["botInfo"]; if (JSON.stringify(resp) !== "{}") { _this.setState({ botInfo: resp, @@ -140,28 +150,23 @@ class BotCard extends Component { } if (this._asyncRequests["state"]) { - this._asyncRequests["state"].cancel(); - this._asyncRequests["state"] = null; + delete this._asyncRequests["state"]; } if (this._asyncRequests["start"]) { - this._asyncRequests["start"].cancel(); - this._asyncRequests["start"] = null; + delete this._asyncRequests["start"]; } if (this._asyncRequests["stop"]) { - this._asyncRequests["stop"].cancel(); - this._asyncRequests["stop"] = null; + delete this._asyncRequests["stop"]; } if (this._asyncRequests["delete"]) { - this._asyncRequests["delete"].cancel(); - this._asyncRequests["delete"] = null; + delete this._asyncRequests["delete"]; } if (this._asyncRequests["botInfo"]) { - this._asyncRequests["botInfo"].cancel(); - this._asyncRequests["botInfo"] = null; + delete this._asyncRequests["botInfo"]; } } @@ -178,7 +183,12 @@ class BotCard extends Component { startBot() { var _this = this; this._asyncRequests["start"] = start(this.props.baseUrl, this.props.name).then(resp => { - _this._asyncRequests["start"] = null; + if (!_this._asyncRequests["start"]) { + // if it has been deleted it means we don't want to process the result + return + } + + delete _this._asyncRequests["start"]; _this.setState({ timeStarted: new Date(), @@ -194,7 +204,12 @@ class BotCard extends Component { stopBot() { var _this = this; this._asyncRequests["stop"] = stop(this.props.baseUrl, this.props.name).then(resp => { - _this._asyncRequests["stop"] = null; + if (!_this._asyncRequests["stop"]) { + // if it has been deleted it means we don't want to process the result + return + } + + delete _this._asyncRequests["stop"]; _this.setState({ timeStarted: null, }); @@ -206,7 +221,12 @@ class BotCard extends Component { callDeleteBot() { var _this = this; this._asyncRequests["delete"] = deleteBot(this.props.baseUrl, this.props.name).then(resp => { - _this._asyncRequests["delete"] = null; + if (!_this._asyncRequests["delete"]) { + // if it has been deleted it means we don't want to process the result + return + } + + delete _this._asyncRequests["delete"]; clearTimeout(_this._tickTimer); _this._tickTimer = null; // reload parent view @@ -245,7 +265,7 @@ class BotCard extends Component {