mirror of
https://github.com/lnbits/lnbits.git
synced 2025-06-10 17:01:36 +02:00
[feat] ui support for high number of wallets and payments (#3174)
This commit is contained in:
parent
375b95c004
commit
beee24bd92
@ -123,12 +123,8 @@ async def get_payments_paginated(
|
|||||||
values["wallet_id"] = wallet_id
|
values["wallet_id"] = wallet_id
|
||||||
clause.append("wallet_id = :wallet_id")
|
clause.append("wallet_id = :wallet_id")
|
||||||
elif user_id:
|
elif user_id:
|
||||||
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
|
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
||||||
"no-wallets-for-user"
|
clause.append(only_user_wallets)
|
||||||
]
|
|
||||||
# wallet ids are safe to use in sql queries
|
|
||||||
wallet_ids_str = [f"'{w}'" for w in wallet_ids]
|
|
||||||
clause.append(f""" wallet_id IN ({", ".join(wallet_ids_str)}) """)
|
|
||||||
|
|
||||||
if complete and pending:
|
if complete and pending:
|
||||||
clause.append(
|
clause.append(
|
||||||
@ -366,12 +362,19 @@ async def get_payments_history(
|
|||||||
async def get_payment_count_stats(
|
async def get_payment_count_stats(
|
||||||
field: PaymentCountField,
|
field: PaymentCountField,
|
||||||
filters: Optional[Filters[PaymentFilters]] = None,
|
filters: Optional[Filters[PaymentFilters]] = None,
|
||||||
|
user_id: Optional[str] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> list[PaymentCountStat]:
|
) -> list[PaymentCountStat]:
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
clause = filters.where()
|
extra_stmts = []
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
||||||
|
extra_stmts.append(only_user_wallets)
|
||||||
|
|
||||||
|
clause = filters.where(extra_stmts)
|
||||||
data = await (conn or db).fetchall(
|
data = await (conn or db).fetchall(
|
||||||
query=f"""
|
query=f"""
|
||||||
SELECT {field} as field, count(*) as total
|
SELECT {field} as field, count(*) as total
|
||||||
@ -389,18 +392,26 @@ async def get_payment_count_stats(
|
|||||||
|
|
||||||
async def get_daily_stats(
|
async def get_daily_stats(
|
||||||
filters: Optional[Filters[PaymentFilters]] = None,
|
filters: Optional[Filters[PaymentFilters]] = None,
|
||||||
|
user_id: Optional[str] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> Tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
) -> Tuple[list[PaymentDailyStats], list[PaymentDailyStats]]:
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
|
|
||||||
in_clause = filters.where(
|
in_where_stmts = ["(apipayments.status = 'success' AND apipayments.amount > 0)"]
|
||||||
["(apipayments.status = 'success' AND apipayments.amount > 0)"]
|
out_where_stmts = [
|
||||||
)
|
"(apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)"
|
||||||
out_clause = filters.where(
|
]
|
||||||
["(apipayments.status IN ('success', 'pending') AND apipayments.amount < 0)"]
|
|
||||||
)
|
if user_id:
|
||||||
|
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
||||||
|
in_where_stmts.append(only_user_wallets)
|
||||||
|
out_where_stmts.append(only_user_wallets)
|
||||||
|
|
||||||
|
in_clause = filters.where(in_where_stmts)
|
||||||
|
out_clause = filters.where(out_where_stmts)
|
||||||
|
|
||||||
date_trunc = db.datetime_grouping("day")
|
date_trunc = db.datetime_grouping("day")
|
||||||
query = """
|
query = """
|
||||||
SELECT {date_trunc} date,
|
SELECT {date_trunc} date,
|
||||||
@ -431,6 +442,7 @@ async def get_daily_stats(
|
|||||||
|
|
||||||
async def get_wallets_stats(
|
async def get_wallets_stats(
|
||||||
filters: Optional[Filters[PaymentFilters]] = None,
|
filters: Optional[Filters[PaymentFilters]] = None,
|
||||||
|
user_id: Optional[str] = None,
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> list[PaymentWalletStats]:
|
) -> list[PaymentWalletStats]:
|
||||||
|
|
||||||
@ -449,6 +461,10 @@ async def get_wallets_stats(
|
|||||||
)
|
)
|
||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
|
if user_id:
|
||||||
|
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
||||||
|
where_stmts.append(only_user_wallets)
|
||||||
|
|
||||||
clauses = filters.where(where_stmts)
|
clauses = filters.where(where_stmts)
|
||||||
|
|
||||||
data = await (conn or db).fetchall(
|
data = await (conn or db).fetchall(
|
||||||
@ -524,3 +540,14 @@ async def mark_webhook_sent(payment_hash: str, status: str) -> None:
|
|||||||
""",
|
""",
|
||||||
{"status": status, "hash": payment_hash},
|
{"status": status, "hash": payment_hash},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _only_user_wallets_statement(
|
||||||
|
user_id: str, conn: Optional[Connection] = None
|
||||||
|
) -> str:
|
||||||
|
wallet_ids = await get_wallets_ids(user_id=user_id, conn=conn) or [
|
||||||
|
"no-wallets-for-user"
|
||||||
|
]
|
||||||
|
# wallet ids are safe to use in sql queries
|
||||||
|
wallet_ids_str = [f"'{w}'" for w in wallet_ids]
|
||||||
|
return f""" wallet_id IN ({", ".join(wallet_ids_str)}) """
|
||||||
|
@ -4,7 +4,8 @@ from typing import Optional
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.db import Connection
|
from lnbits.core.models.wallets import WalletsFilters
|
||||||
|
from lnbits.db import Connection, Filters, Page
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from ..models import Wallet
|
from ..models import Wallet
|
||||||
@ -135,6 +136,29 @@ async def get_wallets(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_wallets_paginated(
|
||||||
|
user_id: str,
|
||||||
|
deleted: Optional[bool] = None,
|
||||||
|
filters: Optional[Filters[WalletsFilters]] = None,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
) -> Page[Wallet]:
|
||||||
|
if deleted is None:
|
||||||
|
deleted = False
|
||||||
|
|
||||||
|
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
|
||||||
|
return await (conn or db).fetch_page(
|
||||||
|
"""
|
||||||
|
SELECT *, COALESCE((
|
||||||
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
|
), 0) AS balance_msat FROM wallets
|
||||||
|
""",
|
||||||
|
where=where,
|
||||||
|
values={"user": user_id, "deleted": deleted},
|
||||||
|
filters=filters,
|
||||||
|
model=Wallet,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets_ids(
|
async def get_wallets_ids(
|
||||||
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
user_id: str, deleted: Optional[bool] = None, conn: Optional[Connection] = None
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
|
@ -27,6 +27,9 @@ class UserExtra(BaseModel):
|
|||||||
# - "google | github | ...": the user was created using an SSO provider
|
# - "google | github | ...": the user was created using an SSO provider
|
||||||
provider: str | None = "lnbits" # auth provider
|
provider: str | None = "lnbits" # auth provider
|
||||||
|
|
||||||
|
# how many wallets are shown in the user interface
|
||||||
|
visible_wallet_count: int | None = 10
|
||||||
|
|
||||||
|
|
||||||
class EndpointAccess(BaseModel):
|
class EndpointAccess(BaseModel):
|
||||||
path: str
|
path: str
|
||||||
|
@ -9,6 +9,7 @@ from enum import Enum
|
|||||||
from ecdsa import SECP256k1, SigningKey
|
from ecdsa import SECP256k1, SigningKey
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from lnbits.db import FilterModel
|
||||||
from lnbits.helpers import url_for
|
from lnbits.helpers import url_for
|
||||||
from lnbits.lnurl import encode as lnurl_encode
|
from lnbits.lnurl import encode as lnurl_encode
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@ -25,6 +26,7 @@ class BaseWallet(BaseModel):
|
|||||||
class WalletExtra(BaseModel):
|
class WalletExtra(BaseModel):
|
||||||
icon: str = "flash_on"
|
icon: str = "flash_on"
|
||||||
color: str = "primary"
|
color: str = "primary"
|
||||||
|
pinned: bool = False
|
||||||
|
|
||||||
|
|
||||||
class Wallet(BaseModel):
|
class Wallet(BaseModel):
|
||||||
@ -83,3 +85,13 @@ class KeyType(Enum):
|
|||||||
class WalletTypeInfo:
|
class WalletTypeInfo:
|
||||||
key_type: KeyType
|
key_type: KeyType
|
||||||
wallet: Wallet
|
wallet: Wallet
|
||||||
|
|
||||||
|
|
||||||
|
class WalletsFilters(FilterModel):
|
||||||
|
__search_fields__ = ["id", "name", "currency"]
|
||||||
|
|
||||||
|
__sort_fields__ = ["id", "name", "currency", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
id: str | None
|
||||||
|
name: str | None
|
||||||
|
currency: str | None
|
||||||
|
@ -399,8 +399,9 @@ async def check_transaction_status(
|
|||||||
|
|
||||||
async def get_payments_daily_stats(
|
async def get_payments_daily_stats(
|
||||||
filters: Filters[PaymentFilters],
|
filters: Filters[PaymentFilters],
|
||||||
|
user_id: Optional[str] = None,
|
||||||
) -> list[PaymentDailyStats]:
|
) -> list[PaymentDailyStats]:
|
||||||
data_in, data_out = await get_daily_stats(filters)
|
data_in, data_out = await get_daily_stats(filters, user_id=user_id)
|
||||||
balance_total: float = 0
|
balance_total: float = 0
|
||||||
|
|
||||||
_none = PaymentDailyStats(date=datetime.now(timezone.utc))
|
_none = PaymentDailyStats(date=datetime.now(timezone.utc))
|
||||||
|
@ -335,6 +335,22 @@
|
|||||||
</q-btn-dropdown>
|
</q-btn-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row q-mb-md">
|
||||||
|
<div class="col-4">
|
||||||
|
<span v-text="$t('visible_wallet_count')"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<q-input
|
||||||
|
v-model="user.extra.visible_wallet_count"
|
||||||
|
:label="$t('visible_wallet_count')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="number"
|
||||||
|
class="q-mb-md"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row q-mb-md">
|
<div class="row q-mb-md">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<span v-text="$t('color_scheme')"></span>
|
<span v-text="$t('color_scheme')"></span>
|
||||||
|
@ -183,7 +183,7 @@
|
|||||||
<span v-text="$t('existing_account_question')"></span>
|
<span v-text="$t('existing_account_question')"></span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="text-secondary cursor-pointer"
|
class="text-secondary cursor-pointer q-ml-sm"
|
||||||
@click="showLogin('username-password')"
|
@click="showLogin('username-password')"
|
||||||
v-text="$t('login')"
|
v-text="$t('login')"
|
||||||
></span>
|
></span>
|
||||||
|
@ -242,6 +242,23 @@
|
|||||||
{{ SITE_TITLE }} Wallet:
|
{{ SITE_TITLE }} Wallet:
|
||||||
<strong><em v-text="g.wallet.name"></em></strong>
|
<strong><em v-text="g.wallet.name"></em></strong>
|
||||||
</div>
|
</div>
|
||||||
|
<q-space></q-space>
|
||||||
|
<div class="float-right">
|
||||||
|
<q-btn
|
||||||
|
@click="updateWallet({ pinned: !g.wallet.extra.pinned })"
|
||||||
|
round
|
||||||
|
class="float-right"
|
||||||
|
:color="g.wallet.extra.pinned ? 'primary' : 'grey-5'"
|
||||||
|
text-color="black"
|
||||||
|
size="sm"
|
||||||
|
icon="push_pin"
|
||||||
|
style="transform: rotate(30deg)"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
><span v-text="$t('pin_wallet')"></span
|
||||||
|
></q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
|
165
lnbits/core/templates/core/wallets.html
Normal file
165
lnbits/core/templates/core/wallets.html
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
||||||
|
<!---->
|
||||||
|
{% from "macros.jinja" import window_vars with context %}
|
||||||
|
<!---->
|
||||||
|
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
||||||
|
|
||||||
|
<div class="row q-col-gutter-md q-mb-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-card>
|
||||||
|
<div class="q-pa-sm q-pl-lg">
|
||||||
|
<div class="row items-center justify-between q-gutter-xs">
|
||||||
|
<div class="col">
|
||||||
|
<q-btn
|
||||||
|
@click="showAddWalletDialog.show = true"
|
||||||
|
:label="$t('add_wallet')"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<div class="float-left">
|
||||||
|
<q-input
|
||||||
|
:label="$t('search_wallets')"
|
||||||
|
dense
|
||||||
|
class="float-right q-pr-xl"
|
||||||
|
v-model="walletsTable.search"
|
||||||
|
>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search"> </q-icon>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon
|
||||||
|
v-if="walletsTable.search !== ''"
|
||||||
|
name="close"
|
||||||
|
@click="walletsTable.search = ''"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<q-table
|
||||||
|
grid
|
||||||
|
grid-header
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
:rows="wallets"
|
||||||
|
:columns="walletsTable.columns"
|
||||||
|
v-model:pagination="walletsTable.pagination"
|
||||||
|
:loading="walletsTable.loading"
|
||||||
|
@request="getUserWallets"
|
||||||
|
row-key="id"
|
||||||
|
:filter="filter"
|
||||||
|
hide-header
|
||||||
|
>
|
||||||
|
<template v-slot:item="props">
|
||||||
|
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
|
||||||
|
<q-card
|
||||||
|
class="q-ma-sm cursor-pointer wallet-list-card"
|
||||||
|
style="text-decoration: none"
|
||||||
|
@click="goToWallet(props.row.id)"
|
||||||
|
>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center">
|
||||||
|
<q-avatar
|
||||||
|
size="lg"
|
||||||
|
:text-color="$q.dark.isActive ? 'black' : 'grey-3'"
|
||||||
|
:color="props.row.extra.color"
|
||||||
|
:icon="props.row.extra.icon"
|
||||||
|
>
|
||||||
|
</q-avatar>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="text-h6 q-pl-md ellipsis"
|
||||||
|
class="text-bold"
|
||||||
|
v-text="props.row.name"
|
||||||
|
></div>
|
||||||
|
<q-space> </q-space>
|
||||||
|
<q-btn
|
||||||
|
v-if="props.row.extra.pinned"
|
||||||
|
round
|
||||||
|
color="primary"
|
||||||
|
text-color="black"
|
||||||
|
size="xs"
|
||||||
|
icon="push_pin"
|
||||||
|
class="float-right"
|
||||||
|
style="transform: rotate(30deg)"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row items-center q-pt-sm">
|
||||||
|
<h6 class="q-my-none ellipsis full-width">
|
||||||
|
<strong
|
||||||
|
v-text="formatBalance(props.row.balance_msat / 1000)"
|
||||||
|
></strong>
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-card-section class="text-left">
|
||||||
|
<small>
|
||||||
|
<strong>
|
||||||
|
<span v-text="$t('currency')"></span>
|
||||||
|
</strong>
|
||||||
|
<span v-text="props.row.currency || 'sat'"></span>
|
||||||
|
</small>
|
||||||
|
<br />
|
||||||
|
<small>
|
||||||
|
<strong>
|
||||||
|
<span v-text="$t('id')"></span>
|
||||||
|
:
|
||||||
|
</strong>
|
||||||
|
<span v-text="props.row.id"></span>
|
||||||
|
</small>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-dialog
|
||||||
|
v-model="showAddWalletDialog.show"
|
||||||
|
persistent
|
||||||
|
@hide="showAddWalletDialog = {show: false}"
|
||||||
|
>
|
||||||
|
<q-card style="min-width: 350px">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">
|
||||||
|
<span v-text="$t('wallet_name')"></span>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section class="q-pt-none">
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
v-model="showAddWalletDialog.name"
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="submitAddWallet()"
|
||||||
|
></q-input>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right" class="text-primary">
|
||||||
|
<q-btn flat :label="$t('cancel')" v-close-popup></q-btn>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
:label="$t('add_wallet')"
|
||||||
|
v-close-popup
|
||||||
|
@click="submitAddWallet()"
|
||||||
|
></q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -216,6 +216,25 @@ async def account(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@generic_router.get(
|
||||||
|
"/wallets",
|
||||||
|
response_class=HTMLResponse,
|
||||||
|
description="show wallets page",
|
||||||
|
)
|
||||||
|
async def wallets(
|
||||||
|
request: Request,
|
||||||
|
user: User = Depends(check_user_exists),
|
||||||
|
):
|
||||||
|
return template_renderer().TemplateResponse(
|
||||||
|
request,
|
||||||
|
"core/wallets.html",
|
||||||
|
{
|
||||||
|
"user": user.json(),
|
||||||
|
"ajax": _is_ajax_request(request),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/service-worker.js")
|
@generic_router.get("/service-worker.js")
|
||||||
async def service_worker(request: Request):
|
async def service_worker(request: Request):
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
@ -402,7 +421,7 @@ async def audit_index(request: Request, user: User = Depends(check_admin)):
|
|||||||
|
|
||||||
|
|
||||||
@generic_router.get("/payments", response_class=HTMLResponse)
|
@generic_router.get("/payments", response_class=HTMLResponse)
|
||||||
async def payments_index(request: Request, user: User = Depends(check_admin)):
|
async def payments_index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
"payments/index.html",
|
"payments/index.html",
|
||||||
{
|
{
|
||||||
|
@ -44,7 +44,6 @@ from lnbits.core.services.payments import (
|
|||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
check_admin,
|
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
parse_filters,
|
parse_filters,
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
@ -116,30 +115,44 @@ async def api_payments_history(
|
|||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
"/stats/count",
|
"/stats/count",
|
||||||
name="Get payments history for all users",
|
name="Get payments history for all users",
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
response_model=List[PaymentCountStat],
|
response_model=List[PaymentCountStat],
|
||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments_counting_stats(
|
async def api_payments_counting_stats(
|
||||||
count_by: PaymentCountField = Query("tag"),
|
count_by: PaymentCountField = Query("tag"),
|
||||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||||
|
user: User = Depends(check_user_exists),
|
||||||
):
|
):
|
||||||
|
|
||||||
return await get_payment_count_stats(count_by, filters)
|
if user.admin:
|
||||||
|
# admin user can see payments from all wallets
|
||||||
|
for_user_id = None
|
||||||
|
else:
|
||||||
|
# regular user can only see payments from their wallets
|
||||||
|
for_user_id = user.id
|
||||||
|
|
||||||
|
return await get_payment_count_stats(count_by, filters=filters, user_id=for_user_id)
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
"/stats/wallets",
|
"/stats/wallets",
|
||||||
name="Get payments history for all users",
|
name="Get payments history for all users",
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
response_model=List[PaymentWalletStats],
|
response_model=List[PaymentWalletStats],
|
||||||
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
openapi_extra=generate_filter_params_openapi(PaymentFilters),
|
||||||
)
|
)
|
||||||
async def api_payments_wallets_stats(
|
async def api_payments_wallets_stats(
|
||||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||||
|
user: User = Depends(check_user_exists),
|
||||||
):
|
):
|
||||||
|
|
||||||
return await get_wallets_stats(filters)
|
if user.admin:
|
||||||
|
# admin user can see payments from all wallets
|
||||||
|
for_user_id = None
|
||||||
|
else:
|
||||||
|
# regular user can only see payments from their wallets
|
||||||
|
for_user_id = user.id
|
||||||
|
|
||||||
|
return await get_wallets_stats(filters, user_id=for_user_id)
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
@ -153,22 +166,13 @@ async def api_payments_daily_stats(
|
|||||||
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
filters: Filters[PaymentFilters] = Depends(parse_filters(PaymentFilters)),
|
||||||
):
|
):
|
||||||
|
|
||||||
if not user.admin:
|
if user.admin:
|
||||||
exc = HTTPException(
|
# admin user can see payments from all wallets
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
for_user_id = None
|
||||||
detail="Missing wallet id.",
|
else:
|
||||||
)
|
# regular user can only see payments from their wallets
|
||||||
wallet_filter = next(
|
for_user_id = user.id
|
||||||
(f for f in filters.filters if f.field == "wallet_id"), None
|
return await get_payments_daily_stats(filters, user_id=for_user_id)
|
||||||
)
|
|
||||||
if not wallet_filter:
|
|
||||||
raise exc
|
|
||||||
wallet_id = list((wallet_filter.values or {}).values())
|
|
||||||
if len(wallet_id) == 0:
|
|
||||||
raise exc
|
|
||||||
if not user.get_wallet(wallet_id[0]):
|
|
||||||
raise exc
|
|
||||||
return await get_payments_daily_stats(filters)
|
|
||||||
|
|
||||||
|
|
||||||
@payment_router.get(
|
@payment_router.get(
|
||||||
|
@ -9,13 +9,18 @@ from fastapi import (
|
|||||||
HTTPException,
|
HTTPException,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from lnbits.core.crud.wallets import get_wallets_paginated
|
||||||
from lnbits.core.models import CreateWallet, KeyType, User, Wallet
|
from lnbits.core.models import CreateWallet, KeyType, User, Wallet
|
||||||
|
from lnbits.core.models.wallets import WalletsFilters
|
||||||
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
|
parse_filters,
|
||||||
require_admin_key,
|
require_admin_key,
|
||||||
require_invoice_key,
|
require_invoice_key,
|
||||||
)
|
)
|
||||||
|
from lnbits.helpers import generate_filter_params_openapi
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_wallet,
|
create_wallet,
|
||||||
@ -38,6 +43,26 @@ async def api_wallet(key_info: WalletTypeInfo = Depends(require_invoice_key)):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@wallet_router.get(
|
||||||
|
"/paginated",
|
||||||
|
name="Wallet List",
|
||||||
|
summary="get paginated list of user wallets",
|
||||||
|
response_description="list of user wallets",
|
||||||
|
response_model=Page[Wallet],
|
||||||
|
openapi_extra=generate_filter_params_openapi(WalletsFilters),
|
||||||
|
)
|
||||||
|
async def api_wallets_paginated(
|
||||||
|
user: User = Depends(check_user_exists),
|
||||||
|
filters: Filters = Depends(parse_filters(WalletsFilters)),
|
||||||
|
):
|
||||||
|
page = await get_wallets_paginated(
|
||||||
|
user_id=user.id,
|
||||||
|
filters=filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
return page
|
||||||
|
|
||||||
|
|
||||||
@wallet_router.put("/{new_name}")
|
@wallet_router.put("/{new_name}")
|
||||||
async def api_update_wallet_name(
|
async def api_update_wallet_name(
|
||||||
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||||
@ -74,6 +99,7 @@ async def api_update_wallet(
|
|||||||
icon: Optional[str] = Body(None),
|
icon: Optional[str] = Body(None),
|
||||||
color: Optional[str] = Body(None),
|
color: Optional[str] = Body(None),
|
||||||
currency: Optional[str] = Body(None),
|
currency: Optional[str] = Body(None),
|
||||||
|
pinned: Optional[bool] = Body(None),
|
||||||
key_info: WalletTypeInfo = Depends(require_admin_key),
|
key_info: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> Wallet:
|
) -> Wallet:
|
||||||
wallet = await get_wallet(key_info.wallet.id)
|
wallet = await get_wallet(key_info.wallet.id)
|
||||||
@ -82,6 +108,7 @@ async def api_update_wallet(
|
|||||||
wallet.name = name or wallet.name
|
wallet.name = name or wallet.name
|
||||||
wallet.extra.icon = icon or wallet.extra.icon
|
wallet.extra.icon = icon or wallet.extra.icon
|
||||||
wallet.extra.color = color or wallet.extra.color
|
wallet.extra.color = color or wallet.extra.color
|
||||||
|
wallet.extra.pinned = pinned if pinned is not None else wallet.extra.pinned
|
||||||
wallet.currency = currency if currency is not None else wallet.currency
|
wallet.currency = currency if currency is not None else wallet.currency
|
||||||
await update_wallet(wallet)
|
await update_wallet(wallet)
|
||||||
return wallet
|
return wallet
|
||||||
|
2
lnbits/static/bundle-components.min.js
vendored
2
lnbits/static/bundle-components.min.js
vendored
File diff suppressed because one or more lines are too long
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
@ -49,8 +49,11 @@ window.localisation.en = {
|
|||||||
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
|
'This QR code contains your wallet URL with full access. You can scan it from your phone to open your wallet from there.',
|
||||||
access_wallet_on_mobile: 'Mobile Access',
|
access_wallet_on_mobile: 'Mobile Access',
|
||||||
wallet: 'Wallet: ',
|
wallet: 'Wallet: ',
|
||||||
|
wallet_name: 'Wallet name',
|
||||||
wallets: 'Wallets',
|
wallets: 'Wallets',
|
||||||
add_wallet: 'Add a new wallet',
|
add_wallet: 'Add wallet',
|
||||||
|
add_new_wallet: 'Add a new wallet',
|
||||||
|
pin_wallet: 'Pin wallet',
|
||||||
delete_wallet: 'Delete wallet',
|
delete_wallet: 'Delete wallet',
|
||||||
delete_wallet_desc:
|
delete_wallet_desc:
|
||||||
'This whole wallet will be deleted, the funds will be UNRECOVERABLE.',
|
'This whole wallet will be deleted, the funds will be UNRECOVERABLE.',
|
||||||
@ -125,6 +128,7 @@ window.localisation.en = {
|
|||||||
no_extensions: "You don't have any extensions installed :(",
|
no_extensions: "You don't have any extensions installed :(",
|
||||||
created: 'Created',
|
created: 'Created',
|
||||||
search_extensions: 'Search extensions',
|
search_extensions: 'Search extensions',
|
||||||
|
search_wallets: 'Search wallets',
|
||||||
extension_sources: 'Extension Sources',
|
extension_sources: 'Extension Sources',
|
||||||
ext_sources_hint: 'Repositories from where the extensions can be downloaded',
|
ext_sources_hint: 'Repositories from where the extensions can be downloaded',
|
||||||
ext_sources_label:
|
ext_sources_label:
|
||||||
@ -264,6 +268,7 @@ window.localisation.en = {
|
|||||||
notification_source_label:
|
notification_source_label:
|
||||||
'Source URL (only use the official LNbits status source, and sources you can trust)',
|
'Source URL (only use the official LNbits status source, and sources you can trust)',
|
||||||
more: 'more',
|
more: 'more',
|
||||||
|
more_count: '{count} more',
|
||||||
less: 'less',
|
less: 'less',
|
||||||
releases: 'Releases',
|
releases: 'Releases',
|
||||||
watchdog: 'Watchdog',
|
watchdog: 'Watchdog',
|
||||||
@ -330,6 +335,7 @@ window.localisation.en = {
|
|||||||
username: 'Username',
|
username: 'Username',
|
||||||
pubkey: 'Public Key',
|
pubkey: 'Public Key',
|
||||||
user_id: 'User ID',
|
user_id: 'User ID',
|
||||||
|
id: 'ID',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
first_name: 'First Name',
|
first_name: 'First Name',
|
||||||
last_name: 'Last Name',
|
last_name: 'Last Name',
|
||||||
@ -356,6 +362,7 @@ window.localisation.en = {
|
|||||||
gradient_background: 'Gradient Background',
|
gradient_background: 'Gradient Background',
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
color_scheme: 'Color Scheme',
|
color_scheme: 'Color Scheme',
|
||||||
|
visible_wallet_count: 'Visible Wallet Count',
|
||||||
admin_settings: 'Admin Settings',
|
admin_settings: 'Admin Settings',
|
||||||
extension_cost: 'This release requires a payment of minimum {cost} sats.',
|
extension_cost: 'This release requires a payment of minimum {cost} sats.',
|
||||||
extension_paid_sats: 'You have already paid {paid_sats} sats.',
|
extension_paid_sats: 'You have already paid {paid_sats} sats.',
|
||||||
|
@ -197,7 +197,8 @@ window.LNbits = {
|
|||||||
email: data.email,
|
email: data.email,
|
||||||
extensions: data.extensions,
|
extensions: data.extensions,
|
||||||
wallets: data.wallets,
|
wallets: data.wallets,
|
||||||
super_user: data.super_user
|
super_user: data.super_user,
|
||||||
|
extra: data.extra ?? {}
|
||||||
}
|
}
|
||||||
const mapWallet = this.wallet
|
const mapWallet = this.wallet
|
||||||
obj.wallets = obj.wallets
|
obj.wallets = obj.wallets
|
||||||
@ -205,6 +206,9 @@ window.LNbits = {
|
|||||||
return mapWallet(obj)
|
return mapWallet(obj)
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
if (a.extra.pinned !== b.extra.pinned) {
|
||||||
|
return a.extra.pinned ? -1 : 1
|
||||||
|
}
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
})
|
})
|
||||||
obj.walletOptions = obj.wallets.map(obj => {
|
obj.walletOptions = obj.wallets.map(obj => {
|
||||||
@ -213,6 +217,10 @@ window.LNbits = {
|
|||||||
value: obj.id
|
value: obj.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
obj.hiddenWalletsCount = Math.max(
|
||||||
|
0,
|
||||||
|
data.wallets.length - data.extra.visible_wallet_count
|
||||||
|
)
|
||||||
return obj
|
return obj
|
||||||
},
|
},
|
||||||
wallet(data) {
|
wallet(data) {
|
||||||
@ -508,6 +516,9 @@ window.windowMixin = {
|
|||||||
}
|
}
|
||||||
this.$q.localStorage.set('lnbits.walletFlip', this.walletFlip)
|
this.$q.localStorage.set('lnbits.walletFlip', this.walletFlip)
|
||||||
},
|
},
|
||||||
|
goToWallets() {
|
||||||
|
window.location = '/wallets'
|
||||||
|
},
|
||||||
submitAddWallet() {
|
submitAddWallet() {
|
||||||
if (
|
if (
|
||||||
this.showAddWalletDialog.name &&
|
this.showAddWalletDialog.name &&
|
||||||
|
@ -103,14 +103,7 @@ window.app.component('lnbits-extension-list', {
|
|||||||
window.app.component('lnbits-manage', {
|
window.app.component('lnbits-manage', {
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
template: '#lnbits-manage',
|
template: '#lnbits-manage',
|
||||||
props: [
|
props: ['showAdmin', 'showNode', 'showExtensions', 'showUsers', 'showAudit'],
|
||||||
'showAdmin',
|
|
||||||
'showNode',
|
|
||||||
'showExtensions',
|
|
||||||
'showUsers',
|
|
||||||
'showAudit',
|
|
||||||
'showPayments'
|
|
||||||
],
|
|
||||||
methods: {
|
methods: {
|
||||||
isActive(path) {
|
isActive(path) {
|
||||||
return window.location.pathname === path
|
return window.location.pathname === path
|
||||||
|
@ -193,6 +193,15 @@ const routes = [
|
|||||||
scripts: ['/static/js/account.js']
|
scripts: ['/static/js/account.js']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/wallets',
|
||||||
|
name: 'Wallets',
|
||||||
|
component: DynamicComponent,
|
||||||
|
props: {
|
||||||
|
fetchUrl: '/wallets',
|
||||||
|
scripts: ['/static/js/wallets.js']
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/node',
|
path: '/node',
|
||||||
name: 'Node',
|
name: 'Node',
|
||||||
|
@ -662,7 +662,7 @@ window.WalletPageLogic = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
message: 'Wallet and user updated.',
|
message: 'Wallet updated.',
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
timeout: 3500
|
timeout: 3500
|
||||||
})
|
})
|
||||||
|
89
lnbits/static/js/wallets.js
Normal file
89
lnbits/static/js/wallets.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
window.WalletsPageLogic = {
|
||||||
|
mixins: [window.windowMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
user: null,
|
||||||
|
tab: 'wallets',
|
||||||
|
wallets: [],
|
||||||
|
showAddWalletDialog: {show: false},
|
||||||
|
walletsTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Name',
|
||||||
|
field: 'name',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'currency',
|
||||||
|
align: 'center',
|
||||||
|
label: 'Currency',
|
||||||
|
field: 'currency',
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'updated_at',
|
||||||
|
align: 'right',
|
||||||
|
label: 'Last Updated',
|
||||||
|
field: 'updated_at',
|
||||||
|
sortable: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
sortBy: 'updated_at',
|
||||||
|
rowsPerPage: 12,
|
||||||
|
page: 1,
|
||||||
|
descending: true,
|
||||||
|
rowsNumber: 10
|
||||||
|
},
|
||||||
|
search: '',
|
||||||
|
hideEmpty: true,
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'walletsTable.search': {
|
||||||
|
handler() {
|
||||||
|
const props = {}
|
||||||
|
if (this.walletsTable.search) {
|
||||||
|
props['search'] = this.walletsTable.search
|
||||||
|
}
|
||||||
|
this.getUserWallets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getUserWallets(props) {
|
||||||
|
try {
|
||||||
|
this.walletsTable.loading = true
|
||||||
|
const params = LNbits.utils.prepareFilterQuery(this.walletsTable, props)
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/api/v1/wallet/paginated?${params}`,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
this.wallets = data.data
|
||||||
|
this.walletsTable.pagination.rowsNumber = data.total
|
||||||
|
} catch (e) {
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
} finally {
|
||||||
|
this.walletsTable.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
goToWallet(walletId) {
|
||||||
|
window.location = `/wallet?wal=${walletId}`
|
||||||
|
},
|
||||||
|
formattedFiatAmount(amount, currency) {
|
||||||
|
return LNbits.utils.formatCurrency(Number(amount).toFixed(2), currency)
|
||||||
|
},
|
||||||
|
formattedSatAmount(amount) {
|
||||||
|
return LNbits.utils.formatMsat(amount) + ' sat'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
await this.getUserWallets()
|
||||||
|
}
|
||||||
|
}
|
@ -179,12 +179,12 @@
|
|||||||
>
|
>
|
||||||
<q-scroll-area style="height: 100%">
|
<q-scroll-area style="height: 100%">
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section>
|
<q-item-section class="cursor-pointer" @click="goToWallets()">
|
||||||
<q-item-label
|
<q-item-label
|
||||||
:style="$q.dark.isActive ? 'color:rgba(255, 255, 255, 0.64)' : ''"
|
:style="$q.dark.isActive ? 'color:rgba(255, 255, 255, 0.64)' : ''"
|
||||||
class="q-item__label q-item__label--header q-pa-none"
|
class="q-item__label q-item__label--header q-pa-none"
|
||||||
header
|
header
|
||||||
v-text="$t('wallets')"
|
v-text="$t('wallets') + ' (' + g.user.wallets.length + ')'"
|
||||||
></q-item-label>
|
></q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
@ -211,7 +211,6 @@
|
|||||||
:show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'"
|
:show-admin="'{{LNBITS_ADMIN_UI}}' == 'True'"
|
||||||
:show-users="'{{LNBITS_ADMIN_UI}}' == 'True'"
|
:show-users="'{{LNBITS_ADMIN_UI}}' == 'True'"
|
||||||
:show-audit="'{{LNBITS_AUDIT_ENABLED}}' == 'True'"
|
:show-audit="'{{LNBITS_AUDIT_ENABLED}}' == 'True'"
|
||||||
:show-payments="'{{LNBITS_ADMIN_UI}}' == 'True'"
|
|
||||||
:show-node="'{{LNBITS_NODE_UI}}' == 'True'"
|
:show-node="'{{LNBITS_NODE_UI}}' == 'True'"
|
||||||
:show-extensions="'{{LNBITS_EXTENSIONS_DEACTIVATE_ALL}}' == 'False'"
|
:show-extensions="'{{LNBITS_EXTENSIONS_DEACTIVATE_ALL}}' == 'False'"
|
||||||
></lnbits-manage>
|
></lnbits-manage>
|
||||||
@ -248,7 +247,7 @@
|
|||||||
@click="showAddWalletDialog.show = true"
|
@click="showAddWalletDialog.show = true"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('add_wallet')"></span
|
><span v-text="$t('add_new_wallet')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-dialog
|
<q-dialog
|
||||||
@ -258,7 +257,9 @@
|
|||||||
>
|
>
|
||||||
<q-card style="min-width: 350px">
|
<q-card style="min-width: 350px">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h6">Wallet name</div>
|
<div class="text-h6">
|
||||||
|
<span v-text="$t('wallet_name')"></span>
|
||||||
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section class="q-pt-none">
|
<q-card-section class="q-pt-none">
|
||||||
@ -271,10 +272,14 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-actions align="right" class="text-primary">
|
<q-card-actions align="right" class="text-primary">
|
||||||
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
label="Add wallet"
|
:label="$t('cancel')"
|
||||||
|
v-close-popup
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
:label="$t('add_wallet')"
|
||||||
v-close-popup
|
v-close-popup
|
||||||
@click="submitAddWallet()"
|
@click="submitAddWallet()"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
@ -285,7 +290,7 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
<q-card
|
<q-card
|
||||||
v-for="wallet in g.user.wallets"
|
v-for="wallet in g.user.wallets.slice(0, g.user.extra.visible_wallet_count || 10)"
|
||||||
:key="wallet.id"
|
:key="wallet.id"
|
||||||
clickable
|
clickable
|
||||||
@click="selectWallet(wallet)"
|
@click="selectWallet(wallet)"
|
||||||
@ -331,6 +336,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
<q-card
|
||||||
|
v-if="g.user.hiddenWalletsCount"
|
||||||
|
class="wallet-list-card"
|
||||||
|
>
|
||||||
|
<q-card-section
|
||||||
|
class="flex flex-center column full-height text-center"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
color="primary"
|
||||||
|
icon="more_horiz"
|
||||||
|
@click="goToWallets()"
|
||||||
|
>
|
||||||
|
<q-tooltip
|
||||||
|
><span
|
||||||
|
v-text="$t('more_count', {count: g.user.hiddenWalletsCount})"
|
||||||
|
></span
|
||||||
|
></q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</q-scroll-area>
|
</q-scroll-area>
|
||||||
|
|
||||||
|
@ -5,7 +5,10 @@
|
|||||||
class="lnbits-drawer__q-list"
|
class="lnbits-drawer__q-list"
|
||||||
>
|
>
|
||||||
<q-item
|
<q-item
|
||||||
v-for="walletRec in g.user.wallets"
|
v-for="walletRec in g.user.wallets.slice(
|
||||||
|
0,
|
||||||
|
g.user.extra.visible_wallet_count || 10
|
||||||
|
)"
|
||||||
:key="walletRec.id"
|
:key="walletRec.id"
|
||||||
clickable
|
clickable
|
||||||
:active="g.wallet && g.wallet.id === walletRec.id"
|
:active="g.wallet && g.wallet.id === walletRec.id"
|
||||||
@ -43,6 +46,22 @@
|
|||||||
<q-item-section side v-show="g.wallet && g.wallet.id === walletRec.id">
|
<q-item-section side v-show="g.wallet && g.wallet.id === walletRec.id">
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="g.user.hiddenWalletsCount > 0"
|
||||||
|
clickable
|
||||||
|
@click="goToWallets()"
|
||||||
|
>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-icon name="more_horiz" color="grey-5" size="md"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
lines="1"
|
||||||
|
class="text-caption"
|
||||||
|
v-text="$t('more_count', {count: g.user.hiddenWalletsCount})"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
<q-item clickable @click="showForm = !showForm">
|
<q-item clickable @click="showForm = !showForm">
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-icon
|
<q-icon
|
||||||
@ -55,7 +74,7 @@
|
|||||||
<q-item-label
|
<q-item-label
|
||||||
lines="1"
|
lines="1"
|
||||||
class="text-caption"
|
class="text-caption"
|
||||||
v-text="$t('add_wallet')"
|
v-text="$t('add_new_wallet')"
|
||||||
></q-item-label>
|
></q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@ -178,7 +197,8 @@
|
|||||||
<q-item-label lines="1" v-text="$t('api_watch')"></q-item-label>
|
<q-item-label lines="1" v-text="$t('api_watch')"></q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-item v-if="showPayments" to="/payments">
|
</div>
|
||||||
|
<q-item to="/payments">
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="query_stats"
|
name="query_stats"
|
||||||
@ -190,7 +210,6 @@
|
|||||||
<q-item-label lines="1" v-text="$t('payments')"></q-item-label>
|
<q-item-label lines="1" v-text="$t('payments')"></q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
|
||||||
<q-item v-if="showExtensions" to="/extensions">
|
<q-item v-if="showExtensions" to="/extensions">
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
<q-icon
|
<q-icon
|
||||||
@ -1185,7 +1204,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
:disable="walletName == ''"
|
:disable="walletName == ''"
|
||||||
type="submit"
|
type="submit"
|
||||||
:label="$t('add_wallet')"
|
:label="$t('add_new_wallet')"
|
||||||
class="full-width q-mb-sm"
|
class="full-width q-mb-sm"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
Loading…
x
Reference in New Issue
Block a user