Skip to content

Commit

Permalink
feat(ui): add circle progress bar (#242)
Browse files Browse the repository at this point in the history
  • Loading branch information
JeremyPansier committed Oct 27, 2023
1 parent 5537c18 commit f5e727b
Show file tree
Hide file tree
Showing 13 changed files with 623 additions and 57 deletions.
4 changes: 3 additions & 1 deletion src/ui/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/my-cloud/ruthenium/src/ui/server/index"
"github.com/my-cloud/ruthenium/src/ui/server/transaction"
"github.com/my-cloud/ruthenium/src/ui/server/transaction/info"
"github.com/my-cloud/ruthenium/src/ui/server/transaction/status"
"github.com/my-cloud/ruthenium/src/ui/server/transactions"
"github.com/my-cloud/ruthenium/src/ui/server/wallet/address"
"github.com/my-cloud/ruthenium/src/ui/server/wallet/amount"
Expand Down Expand Up @@ -46,8 +47,9 @@ func main() {
watch := tick.NewWatch()
http.Handle("/", index.NewHandler(*templatesPath, logger))
http.Handle("/transaction", transaction.NewHandler(host, logger))
http.Handle("/transaction/info", info.NewHandler(host, settings, watch, logger))
http.Handle("/transactions", transactions.NewHandler(host, logger))
http.Handle("/transaction/info", info.NewHandler(host, settings, watch, logger))
http.Handle("/transaction/status", status.NewHandler(host, settings, watch, logger))
http.Handle("/wallet/address", address.NewHandler(logger))
http.Handle("/wallet/amount", amount.NewHandler(host, settings, watch, logger))
logger.Info("user interface server is running...")
Expand Down
6 changes: 3 additions & 3 deletions src/ui/server/transaction/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ func (handler *Handler) ServeHTTP(writer http.ResponseWriter, req *http.Request)
var transaction *verification.Transaction
err := decoder.Decode(&transaction)
if err != nil {
handler.logger.Error(fmt.Errorf("failed to decode transaction request: %w", err).Error())
handler.logger.Error(fmt.Errorf("failed to decode transaction: %w", err).Error())
writer.WriteHeader(http.StatusBadRequest)
jsonWriter.Write("invalid transaction request")
jsonWriter.Write("invalid transaction")
return
}
transactionRequest := validation.NewTransactionRequest(transaction, handler.host.Target())
Expand All @@ -42,7 +42,7 @@ func (handler *Handler) ServeHTTP(writer http.ResponseWriter, req *http.Request)
}
err = handler.host.AddTransaction(marshaledTransaction)
if err != nil {
handler.logger.Error(fmt.Errorf("failed to create transaction: %w", err).Error())
handler.logger.Error(fmt.Errorf("failed to add transaction: %w", err).Error())
writer.WriteHeader(http.StatusInternalServerError)
return
}
Expand Down
1 change: 1 addition & 0 deletions src/ui/server/transaction/info/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ func (handler *Handler) ServeHTTP(writer http.ResponseWriter, req *http.Request)
return
}
writer.Header().Add("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
server.NewIoWriter(writer, handler.logger).Write(string(marshaledResponse[:]))
default:
handler.logger.Error("invalid HTTP method")
Expand Down
142 changes: 142 additions & 0 deletions src/ui/server/transaction/status/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package status

import (
"encoding/json"
"errors"
"fmt"
"github.com/my-cloud/ruthenium/src/log"
"github.com/my-cloud/ruthenium/src/node/clock"
"github.com/my-cloud/ruthenium/src/node/network"
"github.com/my-cloud/ruthenium/src/node/protocol/verification"
"github.com/my-cloud/ruthenium/src/ui/server"
"net/http"
)

type Handler struct {
host network.Neighbor
settings server.Settings
watch clock.Watch
logger log.Logger
}

func NewHandler(host network.Neighbor, settings server.Settings, watch clock.Watch, logger log.Logger) *Handler {
return &Handler{host, settings, watch, logger}
}

func (handler *Handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodPut:
jsonWriter := server.NewIoWriter(writer, handler.logger)
decoder := json.NewDecoder(req.Body)
var transaction *verification.Transaction
err := decoder.Decode(&transaction)
if err != nil {
handler.logger.Error(fmt.Errorf("failed to decode transaction: %w", err).Error())
writer.WriteHeader(http.StatusBadRequest)
jsonWriter.Write("invalid transaction")
return
} else if len(transaction.Outputs()) == 0 {
handler.logger.Error(errors.New("transaction has no output").Error())
writer.WriteHeader(http.StatusBadRequest)
jsonWriter.Write("invalid transaction")
return
}
lastOutputIndex := len(transaction.Outputs()) - 1
lastOutput := transaction.Outputs()[lastOutputIndex]
utxosBytes, err := handler.host.GetUtxos(lastOutput.Address())
if err != nil {
handler.logger.Error(fmt.Errorf("failed to get UTXOs: %w", err).Error())
writer.WriteHeader(http.StatusInternalServerError)
return
}
var utxos []*verification.Utxo
err = json.Unmarshal(utxosBytes, &utxos)
if err != nil {
handler.logger.Error(fmt.Errorf("failed to unmarshal UTXOs: %w", err).Error())
writer.WriteHeader(http.StatusInternalServerError)
return
}
genesisTimestamp, err := handler.host.GetFirstBlockTimestamp()
now := handler.watch.Now().UnixNano()
currentBlockHeight := (now - genesisTimestamp) / handler.settings.ValidationTimestamp()
currentBlockTimestamp := genesisTimestamp + currentBlockHeight*handler.settings.ValidationTimestamp()
progress := &Progress{
CurrentBlockTimestamp: currentBlockTimestamp,
ValidationTimestamp: handler.settings.ValidationTimestamp(),
}
for _, utxo := range utxos {
if utxo.TransactionId() == transaction.Id() && utxo.OutputIndex() == uint16(lastOutputIndex) {
progress.TransactionStatus = "confirmed"
handler.sendResponse(writer, progress)
return
}
}
if err != nil {
handler.logger.Error(fmt.Errorf("failed to get genesis timestamp: %w", err).Error())
writer.WriteHeader(http.StatusInternalServerError)
return
}
blocksBytes, err := handler.host.GetBlocks(uint64(currentBlockHeight))
if err != nil {
handler.logger.Error("failed to get blocks")
writer.WriteHeader(http.StatusInternalServerError)
return
}
var blocks []*verification.Block
err = json.Unmarshal(blocksBytes, &blocks)
if err != nil {
handler.logger.Error(fmt.Errorf("failed to unmarshal blocks: %w", err).Error())
writer.WriteHeader(http.StatusInternalServerError)
return
}
if len(blocks) == 0 {
handler.logger.Error("failed to get last block, get blocks returned an empty list")
writer.WriteHeader(http.StatusInternalServerError)
return
}
for _, validatedTransaction := range blocks[0].Transactions() {
if validatedTransaction.Id() == transaction.Id() {
progress.TransactionStatus = "validated"
handler.sendResponse(writer, progress)
return
}
}
transactionsBytes, err := handler.host.GetTransactions()
if err != nil {
handler.logger.Error(fmt.Errorf("failed to get transactions: %w", err).Error())
writer.WriteHeader(http.StatusInternalServerError)
return
}
var transactions []*verification.Transaction
err = json.Unmarshal(transactionsBytes, &transactions)
if err != nil {
handler.logger.Error(fmt.Errorf("failed to unmarshal transactions: %w", err).Error())
writer.WriteHeader(http.StatusInternalServerError)
return
}
for _, pendingTransaction := range transactions {
if pendingTransaction.Id() == transaction.Id() {
progress.TransactionStatus = "sent"
handler.sendResponse(writer, progress)
return
}
}
progress.TransactionStatus = "rejected"
handler.sendResponse(writer, progress)
default:
handler.logger.Error("invalid HTTP method")
writer.WriteHeader(http.StatusBadRequest)
}
}

func (handler *Handler) sendResponse(writer http.ResponseWriter, progress *Progress) {
marshaledResponse, err := json.Marshal(progress)
if err != nil {
handler.logger.Error(fmt.Errorf("failed to marshal progress: %w", err).Error())
writer.WriteHeader(http.StatusInternalServerError)
return
}
writer.Header().Add("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
server.NewIoWriter(writer, handler.logger).Write(string(marshaledResponse[:]))
}
7 changes: 7 additions & 0 deletions src/ui/server/transaction/status/progress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package status

type Progress struct {
CurrentBlockTimestamp int64 `json:"current_block_timestamp"`
TransactionStatus string `json:"transaction_status"`
ValidationTimestamp int64 `json:"validation_timestamp"`
}
1 change: 1 addition & 0 deletions src/ui/server/transactions/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func (handler *Handler) ServeHTTP(writer http.ResponseWriter, req *http.Request)
return
}
writer.Header().Add("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
server.NewIoWriter(writer, handler.logger).Write(string(transactions[:]))
default:
handler.logger.Error("invalid HTTP method")
Expand Down
1 change: 1 addition & 0 deletions src/ui/server/wallet/amount/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func (handler *Handler) ServeHTTP(writer http.ResponseWriter, req *http.Request)
return
}
writer.Header().Add("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
server.NewIoWriter(writer, handler.logger).Write(string(marshaledAmount[:]))
default:
handler.logger.Error("invalid HTTP method")
Expand Down
86 changes: 81 additions & 5 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wallet</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
Expand Down Expand Up @@ -49,8 +50,8 @@ <h1>Send Tokens</h1>
<input id="recipient_address" name="recipient_address" class="form-field">
</div>
<div>
<label for="send_amount" class="form-label">Amount:</label>
<input id="send_amount" type="text" name="send_amount" class="form-field">
<label for="amount" class="form-label">Amount:</label>
<input id="amount" type="text" name="amount" class="form-field">
</div>
<div>
<input id="utxo_consolidation" type="checkbox" name="utxo_consolidation" class="form-label" checked>
Expand All @@ -61,7 +62,10 @@ <h1>Send Tokens</h1>
<label for="income_update" class="form-field">Update income (requires to be registered in <a
href="https://proofofhumanity.id/">PoH</a>)</label>
</div>
</form>
</form>
<div class="progress">
<div class="progress-circle"></div>
</div>
<div>
<label class="form-label"></label>
<button id="send_tokens_button" class="form-field">Send</button>
Expand Down Expand Up @@ -106,14 +110,16 @@ <h1>Transactions Pool</h1>
});

$(function () {
let pendingTransaction;

$("#send_tokens_button").click(function () {
if (!keyPair) {
alert("The private key must be provided to send tokens")
return
}

const senderAddress = $("#sender_address").val();
const atoms = $("#send_amount").val();
const atoms = $("#amount").val();
const result = atomsToParticles(atoms, 100000000);
if (result.err) {
alert(result.err);
Expand Down Expand Up @@ -151,7 +157,6 @@ <h1>Transactions Pool</h1>
"is_registered": false,
"value": value,
}
console.log(isIncomeUpdateRequested)
const rest = {
"address": senderAddress,
"is_registered": isIncomeUpdateRequested,
Expand All @@ -172,6 +177,7 @@ <h1>Transactions Pool</h1>
success: function (response) {
if (response === "success") {
alert("Send success");
pendingTransaction = transaction
} else {
alert("Send failed: " + response)
}
Expand Down Expand Up @@ -205,6 +211,7 @@ <h1>Transactions Pool</h1>

setInterval(refresh_amount, 100)
setInterval(refresh_transactions, 100)
setInterval(refresh_progress, 100)

function refresh_amount() {
const $walletAmount = $("#wallet_amount");
Expand Down Expand Up @@ -239,6 +246,53 @@ <h1>Transactions Pool</h1>
}
})
}

function refresh_progress() {
const progressBar = document.querySelector('.progress-circle');
if (pendingTransaction === undefined) {
progressBar.style.background = `conic-gradient(white 100%, white 0)`;
progressBar.textContent = "";
} else {
$.ajax({
url: "/transaction/status",
type: "PUT",
contentType: "application/json",
data: JSON.stringify(pendingTransaction),
success: function (response) {
const now = new Date().getTime() * 1000000
let angle = (now - response.current_block_timestamp) / response.validation_timestamp * 100
let color1;
let color2;
switch (response.transaction_status) {
case "sent":
color1 = "lightseagreen";
color2 = "royalblue";
break;
case "validated":
color1 = "seagreen";
color2 = "lightseagreen";
break;
case "confirmed":
color1 = "seagreen";
color2 = "seagreen";
break;
case "rejected":
color1 = "brown";
color2 = "brown";
break;
default:
color1 = "white";
color2 = "white";
}
progressBar.textContent = response.transaction_status[0]
progressBar.style.background = `conic-gradient(${color1} ${angle}%, ${color2} 0)`;
},
error: function (response) {
console.error(response);
}
})
}
}
})

function atomsToParticles(atoms, particlesInOneAtom) {
Expand Down Expand Up @@ -419,4 +473,26 @@ <h1>Transactions Pool</h1>
width: 50%;
margin-left: 90px;
}

.progress {
width: 30px;
height: 30px;
margin-left: 30px;
margin-right: 30px;
border-radius: 50%;
position: absolute;
}

.progress-circle {
width: 100%;
height: 100%;
border-radius: 50%;
background: conic-gradient(white 0, white 0);
animation: progress 5s 1 forwards;
text-align: center;
font-weight: bold;
font-size: 22px;
text-transform: capitalize;
color: lightgrey;
}
</style>

0 comments on commit f5e727b

Please sign in to comment.