Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(BIDS-2841) Displaying amounts in the currency that the user selected #2770

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
16 changes: 8 additions & 8 deletions db/bigtable_eth1.go
Original file line number Diff line number Diff line change
Expand Up @@ -1988,7 +1988,7 @@ func (bigtable *Bigtable) GetIndexedEth1Transaction(txHash []byte) (*types.Eth1T
}
}

func (bigtable *Bigtable) GetAddressTransactionsTableData(address []byte, pageToken string) (*types.DataTableResponse, error) {
func (bigtable *Bigtable) GetAddressTransactionsTableData(address []byte, pageToken string, currency string) (*types.DataTableResponse, error) {

tmr := time.AfterFunc(REPORT_TIMEOUT, func() {
logger.WithFields(logrus.Fields{
Expand Down Expand Up @@ -2033,7 +2033,7 @@ func (bigtable *Bigtable) GetAddressTransactionsTableData(address []byte, pageTo
utils.FormatAddressWithLimitsInAddressPageTable(address, t.From, fromName, false, digitLimitInAddressPagesTable, nameLimitInAddressPagesTable, true),
utils.FormatInOutSelf(address, t.From, t.To),
utils.FormatAddressWithLimitsInAddressPageTable(address, t.To, toName, false, digitLimitInAddressPagesTable, nameLimitInAddressPagesTable, true),
utils.FormatAmount(new(big.Int).SetBytes(t.Value), utils.Config.Frontend.ElCurrency, 6),
Eisei24 marked this conversation as resolved.
Show resolved Hide resolved
utils.FormatAmount(new(big.Int).SetBytes(t.Value), currency, 6),
}
}

Expand Down Expand Up @@ -2103,7 +2103,7 @@ func (bigtable *Bigtable) GetEth1BlocksForAddress(prefix string, limit int64) ([
return data, indexes[len(indexes)-1], nil
}

func (bigtable *Bigtable) GetAddressBlocksMinedTableData(address string, pageToken string) (*types.DataTableResponse, error) {
func (bigtable *Bigtable) GetAddressBlocksMinedTableData(address string, pageToken string, currency string) (*types.DataTableResponse, error) {

tmr := time.AfterFunc(REPORT_TIMEOUT, func() {
logger.WithFields(logrus.Fields{
Expand All @@ -2130,7 +2130,7 @@ func (bigtable *Bigtable) GetAddressBlocksMinedTableData(address string, pageTok
utils.FormatBlockNumber(b.Number),
utils.FormatTimestamp(b.Time.AsTime().Unix()),
utils.FormatBlockUsage(b.GasUsed, b.GasLimit),
utils.FormatAmount(reward, utils.Config.Frontend.ElCurrency, 6),
utils.FormatAmount(reward, currency, 6),
}
}

Expand Down Expand Up @@ -2200,7 +2200,7 @@ func (bigtable *Bigtable) GetEth1UnclesForAddress(prefix string, limit int64) ([
return data, indexes[len(indexes)-1], nil
}

func (bigtable *Bigtable) GetAddressUnclesMinedTableData(address string, pageToken string) (*types.DataTableResponse, error) {
func (bigtable *Bigtable) GetAddressUnclesMinedTableData(address string, pageToken string, currency string) (*types.DataTableResponse, error) {

tmr := time.AfterFunc(REPORT_TIMEOUT, func() {
logger.WithFields(logrus.Fields{
Expand All @@ -2225,7 +2225,7 @@ func (bigtable *Bigtable) GetAddressUnclesMinedTableData(address string, pageTok
utils.FormatBlockNumber(u.Number),
utils.FormatTimestamp(u.Time.AsTime().Unix()),
utils.FormatDifficulty(new(big.Int).SetBytes(u.Difficulty)),
utils.FormatAmount(new(big.Int).SetBytes(u.Reward), utils.Config.Frontend.ElCurrency, 6),
utils.FormatAmount(new(big.Int).SetBytes(u.Reward), currency, 6),
}
}

Expand Down Expand Up @@ -2411,7 +2411,7 @@ func (bigtable *Bigtable) GetEth1ItxForAddress(prefix string, limit int64) ([]*t
return data, indexes[len(indexes)-1], nil
}

func (bigtable *Bigtable) GetAddressInternalTableData(address []byte, pageToken string) (*types.DataTableResponse, error) {
func (bigtable *Bigtable) GetAddressInternalTableData(address []byte, pageToken string, currency string) (*types.DataTableResponse, error) {

tmr := time.AfterFunc(REPORT_TIMEOUT, func() {
logger.WithFields(logrus.Fields{
Expand Down Expand Up @@ -2454,7 +2454,7 @@ func (bigtable *Bigtable) GetAddressInternalTableData(address []byte, pageToken
utils.FormatAddressWithLimitsInAddressPageTable(address, t.From, fromName, false, digitLimitInAddressPagesTable, nameLimitInAddressPagesTable, true),
utils.FormatInOutSelf(address, t.From, t.To),
utils.FormatAddressWithLimitsInAddressPageTable(address, t.To, toName, false, digitLimitInAddressPagesTable, nameLimitInAddressPagesTable, true),
utils.FormatAmount(new(big.Int).SetBytes(t.Value), utils.Config.Frontend.ElCurrency, 6),
utils.FormatAmount(new(big.Int).SetBytes(t.Value), currency, 6),
t.Type,
}
}
Expand Down
19 changes: 9 additions & 10 deletions handlers/eth1Account.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func Eth1Address(w http.ResponseWriter, r *http.Request) {
})
g.Go(func() error {
var err error
txns, err = db.BigtableClient.GetAddressTransactionsTableData(addressBytes, "")
txns, err = db.BigtableClient.GetAddressTransactionsTableData(addressBytes, "", currency)
if err != nil {
return fmt.Errorf("GetAddressTransactionsTableData: %w", err)
}
Expand All @@ -99,7 +99,7 @@ func Eth1Address(w http.ResponseWriter, r *http.Request) {
})
g.Go(func() error {
var err error
internal, err = db.BigtableClient.GetAddressInternalTableData(addressBytes, "")
internal, err = db.BigtableClient.GetAddressInternalTableData(addressBytes, "", currency)
if err != nil {
return fmt.Errorf("GetAddressInternalTableData: %w", err)
}
Expand Down Expand Up @@ -131,15 +131,15 @@ func Eth1Address(w http.ResponseWriter, r *http.Request) {
})
g.Go(func() error {
var err error
blocksMined, err = db.BigtableClient.GetAddressBlocksMinedTableData(address, "")
blocksMined, err = db.BigtableClient.GetAddressBlocksMinedTableData(address, "", currency)
if err != nil {
return fmt.Errorf("GetAddressBlocksMinedTableData: %w", err)
}
return nil
})
g.Go(func() error {
var err error
unclesMined, err = db.BigtableClient.GetAddressUnclesMinedTableData(address, "")
unclesMined, err = db.BigtableClient.GetAddressUnclesMinedTableData(address, "", currency)
if err != nil {
return fmt.Errorf("GetAddressUnclesMinedTableData: %w", err)
}
Expand Down Expand Up @@ -282,7 +282,7 @@ func Eth1AddressTransactions(w http.ResponseWriter, r *http.Request) {

pageToken := q.Get("pageToken")

data, err := db.BigtableClient.GetAddressTransactionsTableData(addressBytes, pageToken)
data, err := db.BigtableClient.GetAddressTransactionsTableData(addressBytes, pageToken, GetCurrency(r))
if err != nil {
utils.LogError(err, "error getting eth1 tx table data", 0, errFields)
}
Expand Down Expand Up @@ -310,7 +310,7 @@ func Eth1AddressBlocksMined(w http.ResponseWriter, r *http.Request) {

pageToken := q.Get("pageToken")

data, err := db.BigtableClient.GetAddressBlocksMinedTableData(address, pageToken)
data, err := db.BigtableClient.GetAddressBlocksMinedTableData(address, pageToken, GetCurrency(r))
if err != nil {
utils.LogError(err, "error getting eth1 blocks mined table data", 0, errFields)
}
Expand All @@ -337,7 +337,7 @@ func Eth1AddressUnclesMined(w http.ResponseWriter, r *http.Request) {

pageToken := q.Get("pageToken")

data, err := db.BigtableClient.GetAddressUnclesMinedTableData(address, pageToken)
data, err := db.BigtableClient.GetAddressUnclesMinedTableData(address, pageToken, GetCurrency(r))
if err != nil {
utils.LogError(err, "error getting eth1 uncles mined data", 0, errFields)
}
Expand All @@ -353,7 +353,6 @@ func Eth1AddressUnclesMined(w http.ResponseWriter, r *http.Request) {
func Eth1AddressWithdrawals(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

currency := GetCurrency(r)
q := r.URL.Query()
address, err := lowerAddressFromRequest(w, r)
if err != nil {
Expand All @@ -363,7 +362,7 @@ func Eth1AddressWithdrawals(w http.ResponseWriter, r *http.Request) {
errFields := map[string]interface{}{
"route": r.URL.String()}

data, err := db.GetAddressWithdrawalTableData(common.HexToAddress(address).Bytes(), q.Get("pageToken"), currency)
data, err := db.GetAddressWithdrawalTableData(common.HexToAddress(address).Bytes(), q.Get("pageToken"), GetCurrency(r))
if err != nil {
utils.LogError(err, "error getting address withdrawals data", 0, errFields)
http.Error(w, "Internal server error", http.StatusInternalServerError)
Expand Down Expand Up @@ -419,7 +418,7 @@ func Eth1AddressInternalTransactions(w http.ResponseWriter, r *http.Request) {
"route": r.URL.String()}

pageToken := q.Get("pageToken")
data, err := db.BigtableClient.GetAddressInternalTableData(addressBytes, pageToken)
data, err := db.BigtableClient.GetAddressInternalTableData(addressBytes, pageToken, GetCurrency(r))
if err != nil {
utils.LogError(err, "error getting eth1 internal tx table data", 0, errFields)
}
Expand Down
8 changes: 4 additions & 4 deletions handlers/eth1Blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func Eth1BlocksData(w http.ResponseWriter, r *http.Request) {
length = 100
}

data, err := getEth1BlocksTableData(draw, start, length, recordsTotal)
data, err := getEth1BlocksTableData(draw, start, length, recordsTotal, GetCurrency(r))
if err != nil {
utils.LogError(err, "error getting eth1 block table data", 0)
}
Expand Down Expand Up @@ -128,7 +128,7 @@ func Eth1BlocksHighest(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fmt.Sprintf("%d", services.LatestEth1BlockNumber())))
}

func getEth1BlocksTableData(draw, start, length, recordsTotal uint64) (*types.DataTableResponse, error) {
func getEth1BlocksTableData(draw, start, length, recordsTotal uint64, currency string) (*types.DataTableResponse, error) {
if recordsTotal == 0 {
recordsTotal = services.LatestEth1BlockNumber() + 1 // +1 to include block 0
}
Expand Down Expand Up @@ -234,8 +234,8 @@ func getEth1BlocksTableData(draw, start, length, recordsTotal uint64) (*types.Da
template.HTML(fmt.Sprintf(`%v<BR /><span data-toggle="tooltip" data-placement="top" title="Gas Used %%" style="font-size: .63rem; color: grey;">%.2f%%</span>&nbsp;<span data-toggle="tooltip" data-placement="top" title="%% of Gas Target" style="font-size: .63rem; color: grey;">(%+.2f%%)</span>`, utils.FormatAddCommas(b.GetGasUsed()), float64(int64(float64(b.GetGasUsed())/float64(b.GetGasLimit())*10000.0))/100.0, float64(int64(((float64(b.GetGasUsed())-gasHalf)/gasHalf)*10000.0))/100.0)), // Gas Used
utils.FormatAddCommas(b.GetGasLimit()), // Gas Limit
utils.FormatAmountFormatted(baseFee, "GWei", 5, 4, true, true, true), // Base Fee
utils.FormatAmountFormatted(new(big.Int).Add(utils.Eth1BlockReward(blockNumber, b.GetDifficulty()), new(big.Int).Add(txReward, new(big.Int).SetBytes(b.GetUncleReward()))), utils.Config.Frontend.ElCurrency, 5, 4, true, true, true), // Reward
fmt.Sprintf(`%v<BR /><span data-toggle="tooltip" data-placement="top" title="%% of Transactions Fees" style="font-size: .63rem; color: grey;">%.2f%%</span>`, utils.FormatAmountFormatted(burned, utils.Config.Frontend.ElCurrency, 5, 4, true, true, false), float64(int64(burnedPercentage*10000.0))/100.0), // Burned Fees
utils.FormatAmountFormatted(new(big.Int).Add(utils.Eth1BlockReward(blockNumber, b.GetDifficulty()), new(big.Int).Add(txReward, new(big.Int).SetBytes(b.GetUncleReward()))), currency, 5, 4, true, true, true), // Reward
fmt.Sprintf(`%v<BR /><span data-toggle="tooltip" data-placement="top" title="%% of Transactions Fees" style="font-size: .63rem; color: grey;">%.2f%%</span>`, utils.FormatAmountFormatted(burned, currency, 5, 4, true, true, false), float64(int64(burnedPercentage*10000.0))/100.0), // Burned Fees
}
}

Expand Down
10 changes: 5 additions & 5 deletions handlers/eth1Transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func Eth1Transactions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")

data := InitPageData(w, r, "blockchain", "/eth1transactions", "Transactions", templateFiles)
data.Data = getTransactionDataStartingWithPageToken("")
data.Data = getTransactionDataStartingWithPageToken("", GetCurrency(r))

if handleTemplateError(w, r, "eth1Transactions.go", "Eth1Transactions", "", eth1TransactionsTemplate.ExecuteTemplate(w, "layout", data)) != nil {
return // an error has occurred and was processed
Expand All @@ -38,14 +38,14 @@ func Eth1Transactions(w http.ResponseWriter, r *http.Request) {
func Eth1TransactionsData(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

err := json.NewEncoder(w).Encode(getTransactionDataStartingWithPageToken(r.URL.Query().Get("pageToken")))
err := json.NewEncoder(w).Encode(getTransactionDataStartingWithPageToken(r.URL.Query().Get("pageToken"), GetCurrency(r)))
if err != nil {
logger.Errorf("error enconding json response for %v route: %v", r.URL.String(), err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}

func getTransactionDataStartingWithPageToken(pageToken string) *types.DataTableResponse {
func getTransactionDataStartingWithPageToken(pageToken string, currency string) *types.DataTableResponse {
pageTokenId := uint64(0)
{
if len(pageToken) > 0 {
Expand Down Expand Up @@ -118,8 +118,8 @@ func getTransactionDataStartingWithPageToken(pageToken string) *types.DataTableR
utils.FormatTimestamp(b.GetTime().AsTime().Unix()),
utils.FormatAddressWithLimits(v.GetFrom(), names[string(v.GetFrom())], false, "address", visibleDigitsForHash+5, 18, true),
toText,
utils.FormatAmountFormatted(new(big.Int).SetBytes(v.GetValue()), utils.Config.Frontend.ElCurrency, 8, 4, true, true, false),
utils.FormatAmountFormatted(db.CalculateTxFeeFromTransaction(v, new(big.Int).SetBytes(b.GetBaseFee())), utils.Config.Frontend.ElCurrency, 8, 4, true, true, false),
utils.FormatAmountFormatted(new(big.Int).SetBytes(v.GetValue()), currency, 8, 4, true, true, false),
utils.FormatAmountFormatted(db.CalculateTxFeeFromTransaction(v, new(big.Int).SetBytes(b.GetBaseFee())), currency, 8, 4, true, true, false),
})
return nil
})
Expand Down
14 changes: 7 additions & 7 deletions handlers/mempoolView.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

func MempoolView(w http.ResponseWriter, r *http.Request) {
mempool := services.LatestMempoolTransactions()
formatedData := formatToTable(mempool)
formatedData := formatToTable(mempool, GetCurrency(r))
templateFiles := append(layoutTemplateFiles, "mempoolview.html")
var mempoolViewTemplate = templates.GetTemplate(templateFiles...)

Expand All @@ -38,33 +38,33 @@ func _isContractCreation(tx *common.Address) string {

// This Function formats each Transaction into Html string.
// This makes all calculations faster, reducing browser's rendering time.
func formatToTable(content *types.RawMempoolResponse) *types.DataTableResponse {
func formatToTable(content *types.RawMempoolResponse, currency string) *types.DataTableResponse {
dataTable := &types.DataTableResponse{}

for _, txs := range content.Pending {
for _, tx := range txs {
dataTable.Data = append(dataTable.Data, toTableDataRow(tx))
dataTable.Data = append(dataTable.Data, toTableDataRow(tx, currency))
}
}
for _, txs := range content.BaseFee {
for _, tx := range txs {
dataTable.Data = append(dataTable.Data, toTableDataRow(tx))
dataTable.Data = append(dataTable.Data, toTableDataRow(tx, currency))
}
}
for _, txs := range content.Queued {
for _, tx := range txs {
dataTable.Data = append(dataTable.Data, toTableDataRow(tx))
dataTable.Data = append(dataTable.Data, toTableDataRow(tx, currency))
}
}
return dataTable
}

func toTableDataRow(tx *types.RawMempoolTransaction) []interface{} {
func toTableDataRow(tx *types.RawMempoolTransaction, currency string) []interface{} {
return []any{
utils.FormatAddressWithLimits(tx.Hash.Bytes(), "", false, "tx", 15, 18, true),
utils.FormatAddressAll(tx.From.Bytes(), "", false, "address", int(12), int(12), true),
_isContractCreation(tx.To),
utils.FormatAmount((*big.Int)(tx.Value), utils.Config.Frontend.ElCurrency, 5),
utils.FormatAmount((*big.Int)(tx.Value), currency, 5),
utils.FormatAddCommasFormatted(float64(tx.Gas.ToInt().Int64()), 0),
utils.FormatAmountFormatted(tx.GasPrice.ToInt(), "GWei", 5, 0, true, true, false),
tx.Nonce.ToInt(),
Expand Down
7 changes: 4 additions & 3 deletions handlers/slot.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ func BlockTransactionsData(w http.ResponseWriter, r *http.Request) {
return
}

currency := GetCurrency(r)
data := make([]*transactionsData, len(transactions.Txs))
for i, v := range transactions.Txs {
methodFormatted := `<span class="badge badge-light">Transfer</span>`
Expand All @@ -688,9 +689,9 @@ func BlockTransactionsData(w http.ResponseWriter, r *http.Request) {
Method: methodFormatted,
FromFormatted: v.FromFormatted,
ToFormatted: v.ToFormatted,
Value: utils.FormatAmountFormatted(v.Value, utils.Config.Frontend.ElCurrency, 5, 0, true, true, false),
Fee: utils.FormatAmountFormatted(v.Fee, utils.Config.Frontend.ElCurrency, 5, 0, true, true, false),
GasPrice: utils.FormatAmountFormatted(v.GasPrice, "GWei", 5, 0, true, true, false),
Eisei24 marked this conversation as resolved.
Show resolved Hide resolved
Value: utils.FormatAmountFormatted(v.Value, currency, 5, 0, true, true, false),
Fee: utils.FormatAmountFormatted(v.Fee, currency, 5, 0, true, true, false),
GasPrice: utils.FormatAmountFormatted(v.GasPrice, currency, 5, 0, true, true, false),
}
}

Expand Down
26 changes: 18 additions & 8 deletions price/price.go
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Just a reminder that changes to this file have to be reverted after the review is done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I made theses changes clean and didn't remove them because I think that it is good to allow testers to select "USD" in the drop-down when the explorer runs locally. The price of ETH is then arbitrary ($2000).
What do you think?

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
"golang.org/x/sync/errgroup"
)

const ETHGWeiCommaShift = 9
const GWeiWeiCommaShift = 9
const ETHWeiCommaShift = ETHGWeiCommaShift + GWeiWeiCommaShift
thib-wien marked this conversation as resolved.
Show resolved Hide resolved
const ETHinGWei int64 = 1000000000
const GWeiInWei int64 = 1000000000
const ETHinWei = ETHinGWei * GWeiInWei
Eisei24 marked this conversation as resolved.
Show resolved Hide resolved

var logger = logrus.New().WithField("module", "price")

var availableCurrencies = []string{}
Expand Down Expand Up @@ -63,9 +70,11 @@
switch chainId {
case 1, 100:
default:
usd := "USD"
setPrice(clCurrency, usd, 2300)
setPrice(elCurrency, elCurrency, 1)
setPrice(clCurrency, clCurrency, 1)
availableCurrencies = []string{clCurrency, elCurrency}
availableCurrencies = []string{clCurrency, elCurrency, usd}
logger.Warnf("chainId not supported for fetching prices: %v", chainId)
runOnce.Do(func() { runOnceWg.Done() })
return
Expand Down Expand Up @@ -149,16 +158,17 @@
}
feeds[pair] = feed
}

go func() {
for {
updatePrices()
time.Sleep(time.Minute)
}
}()
/*
go func() {
for {
updatePrices()
time.Sleep(time.Minute)
}
}()
*/
}

func updatePrices() {

Check failure on line 171 in price/price.go

View workflow job for this annotation

GitHub Actions / Run CI (ubuntu-latest, 1.20.x)

func updatePrices is unused (U1000)
g := &errgroup.Group{}
for pair, feed := range feeds {
pair := pair
Expand Down Expand Up @@ -194,7 +204,7 @@
runOnce.Do(func() { runOnceWg.Done() })
}

func calcPricePairs(currency string) error {

Check failure on line 207 in price/price.go

View workflow job for this annotation

GitHub Actions / Run CI (ubuntu-latest, 1.20.x)

func calcPricePairs is unused (U1000)
pricesMu.Lock()
defer pricesMu.Unlock()
pricesCopy := prices
Expand Down Expand Up @@ -240,7 +250,7 @@
return price
}

func getPriceFromFeed(feed *chainlink_feed.Feed) (float64, error) {

Check failure on line 253 in price/price.go

View workflow job for this annotation

GitHub Actions / Run CI (ubuntu-latest, 1.20.x)

func getPriceFromFeed is unused (U1000)
decimals := decimal.NewFromInt(1e8) // 8 decimal places for the Chainlink feeds
res, err := feed.LatestRoundData(&bind.CallOpts{})
if err != nil {
Expand Down
2 changes: 0 additions & 2 deletions templates/layout.html
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Just a reminder that the changes to this file have to be reverted after the review is done.

Copy link
Contributor Author

@thib-wien thib-wien Jan 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we can keep this change to see the currency drop-down when the explorer runs locally. The price of ETH is then arbitrary ($2000).
I am not sure why this was deactivated for local tests.
What do you think?

Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@
</div>
</div>
<div class="info-banner-right">
{{ if .Mainnet }}
<div class="dropdown">
<a class="btn btn-transparent btn-sm dropdown-toggle currency-dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<div id="currencyFlagDropdown">
Expand All @@ -178,7 +177,6 @@
{{ end }}
</div>
</div>
{{ end }}
{{ if .User.Authenticated }}
<div class="dropdown">
<a class="btn btn-transparent btn-sm dropdown-toggle" id="userDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Expand Down
Loading
Loading