diff --git a/.github/workflows/dashboard-ci.yml b/.github/workflows/dashboard-ci.yml
@@ -61,8 +61,8 @@ jobs:
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
- version: 8
- run_install: false
+ version: 9
+ run_install: true
- name: Get pnpm store directory
shell: bash
run: |
diff --git a/app/db/__init__.py b/app/db/__init__.py
@@ -11,7 +11,7 @@ from .crud import (
get_admins,
get_jwt_secret_key,
get_notification_reminder,
- assure_node_inbounds,
+ ensure_node_inbounds,
get_system_usage,
get_tls_certificate,
get_user,
diff --git a/app/db/crud.py b/app/db/crud.py
@@ -21,6 +21,7 @@ from app.db.models import (
Inbound,
System,
User,
+ Backend,
)
from app.models.admin import AdminCreate, AdminPartialModify
from app.models.node import (
@@ -55,7 +56,24 @@ def add_default_hosts(db: Session, inbounds: List[Inbound]):
db.commit()
-def assure_node_inbounds(db: Session, inbounds: List[Inbound], node_id: int):
+def ensure_node_backends(db: Session, backends, node_id: int):
+ old_backends = db.query(Backend).where(Backend.node_id == node_id)
+ for backend in old_backends:
+ db.delete(backend)
+ backends = [
+ Backend(
+ name=backend.name,
+ backend_type=backend.type,
+ version=backend.version,
+ node_id=node_id,
+ )
+ for backend in backends
+ ]
+ db.add_all(backends)
+ db.flush()
+
+
+def ensure_node_inbounds(db: Session, inbounds: List[Inbound], node_id: int):
current_tags = [
i[0]
for i in db.execute(
@@ -67,10 +85,10 @@ def assure_node_inbounds(db: Session, inbounds: List[Inbound], node_id: int):
for tag in current_tags:
if tag not in updated_tags:
tag_deletions.add(tag)
- db_inbounds = db.query(Inbound).where(
+ removals = db.query(Inbound).where(
and_(Inbound.node_id == node_id, Inbound.tag.in_(tag_deletions))
)
- for i in db_inbounds:
+ for i in removals:
db.delete(i)
for inb in inbounds:
diff --git a/app/db/migrations/versions/35774c98f945_add_node_backends.py b/app/db/migrations/versions/35774c98f945_add_node_backends.py
@@ -0,0 +1,43 @@
+"""add node backends
+
+Revision ID: 35774c98f945
+Revises: c4fb5f6423a8
+Create Date: 2024-08-01 08:29:56.436001
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "35774c98f945"
+down_revision = "c4fb5f6423a8"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table(
+ "backends",
+ sa.Column("id", sa.Integer(), nullable=False),
+ sa.Column("name", sa.String(length=64), nullable=False),
+ sa.Column("node_id", sa.Integer(), nullable=True),
+ sa.Column("backend_type", sa.String(length=32), nullable=False),
+ sa.Column("version", sa.String(length=32), nullable=True),
+ sa.Column("running", sa.Boolean(), nullable=False),
+ sa.ForeignKeyConstraint(
+ ["node_id"],
+ ["nodes.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_index(op.f("ix_backends_node_id"), "backends", ["node_id"])
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f("ix_backends_node_id"), table_name="backends")
+ op.drop_table("backends")
+ # ### end Alembic commands ###
diff --git a/app/db/models.py b/app/db/models.py
@@ -274,6 +274,18 @@ class User(Base):
return self.admin.username
+class Backend(Base):
+ __tablename__ = "backends"
+
+ id = Column(Integer, primary_key=True)
+ name = Column(String(64), nullable=False)
+ node_id = Column(Integer, ForeignKey("nodes.id"), index=True)
+ node = relationship("Node", back_populates="backends")
+ backend_type = Column(String(32), nullable=False)
+ version = Column(String(32))
+ running = Column(Boolean, default=True, nullable=False)
+
+
class Inbound(Base):
__tablename__ = "inbounds"
__table_args__ = (UniqueConstraint("node_id", "tag"),)
@@ -375,6 +387,9 @@ class Node(Base):
inbounds = relationship(
"Inbound", back_populates="node", cascade="all, delete"
)
+ backends = relationship(
+ "Backend", back_populates="node", cascade="all, delete"
+ )
status = Column(
Enum(NodeStatus), nullable=False, default=NodeStatus.unhealthy
)
diff --git a/app/marznode/base.py b/app/marznode/base.py
@@ -11,8 +11,16 @@ class MarzNodeBase(ABC):
async def fetch_users_stats(self):
"""get user stats from the node"""
- async def is_alive(self) -> bool:
- """
- checks if node is up and running and connected
- :return: True if the node is responsive, otherwise False
- """
+ async def get_logs(self, name: str, include_buffer: bool):
+ pass
+
+ async def restart_backend(
+ self, name: str, config: str, config_format: int
+ ):
+ pass
+
+ async def get_backend_config(self, name: str) -> tuple[str, str]:
+ pass
+
+ async def get_backend_stats(self, name: str):
+ pass
diff --git a/app/marznode/database.py b/app/marznode/database.py
@@ -15,9 +15,13 @@ class MarzNodeDB:
users[rel[0]]["inbounds"].append(rel[3].tag)
return list(users.values())
- def store_inbounds(self, inbounds):
+ def store_backends(self, backends):
+ inbounds = [
+ inbound for backend in backends for inbound in backend.inbounds
+ ]
with GetDB() as db:
- crud.assure_node_inbounds(db, inbounds, self.id)
+ crud.ensure_node_backends(db, backends, self.id)
+ crud.ensure_node_inbounds(db, inbounds, self.id)
def set_status(self, status: NodeStatus, message: str | None = None):
with GetDB() as db:
diff --git a/app/marznode/grpcio.py b/app/marznode/grpcio.py
@@ -14,8 +14,11 @@ from .marznode_pb2 import (
Empty,
User,
Inbound,
- XrayLogsRequest,
- XrayConfig,
+ BackendLogsRequest,
+ RestartBackendRequest,
+ BackendConfig,
+ Backend,
+ BackendStats,
)
from .marznode_pb2_grpc import MarzServiceStub
from ..models.node import NodeStatus
@@ -125,26 +128,37 @@ class MarzNodeGRPCIO(MarzNodeBase, MarzNodeDB):
response = await self._stub.FetchUsersStats(Empty())
return response.users_stats
- async def _fetch_inbounds(self) -> list:
- response = await self._stub.FetchInbounds(Empty())
- return response.inbounds
+ async def _fetch_backends(self) -> list:
+ response = await self._stub.FetchBackends(Empty())
+ return response.backends
async def _sync(self):
- inbounds = await self._fetch_inbounds()
- self.store_inbounds(inbounds)
+ backends = await self._fetch_backends()
+ self.store_backends(backends)
users = self.list_users()
await self._repopulate_users(users)
self.synced = True
- async def get_logs(self, include_buffer=True):
- async for response in self._stub.StreamXrayLogs(
- XrayLogsRequest(include_buffer=include_buffer)
+ async def get_logs(self, name: str = "xray", include_buffer=True):
+ async for response in self._stub.StreamBackendLogs(
+ BackendLogsRequest(
+ backend_name=name, include_buffer=include_buffer
+ )
):
yield response.line
- async def restart_xray(self, config: str):
+ async def restart_backend(
+ self, name: str, config: str, config_format: int
+ ):
try:
- await self._stub.RestartXray(XrayConfig(configuration=config))
+ await self._stub.RestartBackend(
+ RestartBackendRequest(
+ backend_name=name,
+ config=BackendConfig(
+ configuration=config, config_format=config_format
+ ),
+ )
+ )
await self._sync()
except RpcError:
self.synced = False
@@ -153,6 +167,12 @@ class MarzNodeGRPCIO(MarzNodeBase, MarzNodeDB):
else:
self.set_status(NodeStatus.healthy)
- async def get_xray_config(self):
- response = await self._stub.FetchXrayConfig(Empty())
- return response.configuration
+ async def get_backend_config(self, name: str = "xray"):
+ response = await self._stub.FetchBackendConfig(Backend(name=name))
+ return response.configuration, response.config_format
+
+ async def get_backend_stats(self, name: str):
+ response: BackendStats = await self._stub.GetBackendStats(
+ Backend(name=name)
+ )
+ return response
diff --git a/app/marznode/grpclib.py b/app/marznode/grpclib.py
@@ -17,8 +17,11 @@ from .marznode_pb2 import (
Empty,
User,
Inbound,
- XrayLogsRequest,
- XrayConfig,
+ BackendConfig,
+ BackendLogsRequest,
+ Backend,
+ RestartBackendRequest,
+ BackendStats,
)
from ..models.node import NodeStatus
@@ -133,29 +136,40 @@ class MarzNodeGRPCLIB(MarzNodeBase, MarzNodeDB):
response = await self._stub.FetchUsersStats(Empty())
return response.users_stats
- async def _fetch_inbounds(self) -> list:
- response = await self._stub.FetchInbounds(Empty())
- return response.inbounds
+ async def _fetch_backends(self) -> list:
+ response = await self._stub.FetchBackends(Empty())
+ return response.backends
async def _sync(self):
- inbounds = await self._fetch_inbounds()
- self.store_inbounds(inbounds)
+ backends = await self._fetch_backends()
+ self.store_backends(backends)
users = self.list_users()
await self._repopulate_users(users)
self.synced = True
- async def get_logs(self, include_buffer=True):
- async with self._stub.StreamXrayLogs.open() as stm:
+ async def get_logs(self, name: str = "xray", include_buffer=True):
+ async with self._stub.StreamBackendLogs.open() as stm:
await stm.send_message(
- XrayLogsRequest(include_buffer=include_buffer)
+ BackendLogsRequest(
+ backend_name=name, include_buffer=include_buffer
+ )
)
while True:
response = await stm.recv_message()
yield response.line
- async def restart_xray(self, config: str):
+ async def restart_backend(
+ self, name: str, config: str, config_format: int
+ ):
try:
- await self._stub.RestartXray(XrayConfig(configuration=config))
+ await self._stub.RestartBackend(
+ RestartBackendRequest(
+ backend_name=name,
+ config=BackendConfig(
+ configuration=config, config_format=config_format
+ ),
+ )
+ )
await self._sync()
except:
self.synced = False
@@ -164,6 +178,14 @@ class MarzNodeGRPCLIB(MarzNodeBase, MarzNodeDB):
else:
self.set_status(NodeStatus.healthy)
- async def get_xray_config(self):
- response = await self._stub.FetchXrayConfig(Empty())
- return response.configuration
+ async def get_backend_config(self, name: str):
+ response: BackendConfig = await self._stub.FetchBackendConfig(
+ Backend(name=name)
+ )
+ return response.configuration, response.config_format
+
+ async def get_backend_stats(self, name: str):
+ response: BackendStats = await self._stub.GetBackendStats(
+ Backend(name=name)
+ )
+ return response
diff --git a/app/marznode/marznode.proto b/app/marznode/marznode.proto
@@ -4,15 +4,28 @@ package marznode;
message Empty {}
+enum ConfigFormat {
+ PLAIN = 0;
+ JSON = 1;
+ YAML = 2;
+}
+
+message Backend {
+ string name = 1;
+ optional string type = 2;
+ optional string version = 3;
+ repeated Inbound inbounds = 4;
+}
+
+message BackendsResponse {
+ repeated Backend backends = 1;
+}
+
message Inbound {
string tag = 1;
optional string config = 2;
}
-message InboundsResponse {
- repeated Inbound inbounds = 1;
-}
-
message User {
uint32 id = 1;
string username = 2;
@@ -40,20 +53,32 @@ message LogLine {
string line = 1;
}
-message XrayConfig {
+message BackendConfig {
string configuration = 1;
+ ConfigFormat config_format = 2;
+}
+
+message BackendLogsRequest {
+ string backend_name = 1;
+ bool include_buffer = 2;
+}
+
+message RestartBackendRequest {
+ string backend_name = 1;
+ optional BackendConfig config = 2;
}
-message XrayLogsRequest {
- bool include_buffer = 1;
+message BackendStats {
+ bool running = 1;
}
service MarzService {
rpc SyncUsers(stream UserData) returns (Empty);
rpc RepopulateUsers(UsersData) returns (Empty);
- rpc FetchInbounds(Empty) returns (InboundsResponse);
+ rpc FetchBackends(Empty) returns (BackendsResponse);
rpc FetchUsersStats(Empty) returns (UsersStats);
- rpc FetchXrayConfig(Empty) returns (XrayConfig);
- rpc RestartXray(XrayConfig) returns (InboundsResponse);
- rpc StreamXrayLogs(XrayLogsRequest) returns(stream LogLine);
+ rpc FetchBackendConfig(Backend) returns (BackendConfig);
+ rpc RestartBackend(RestartBackendRequest) returns (Empty);
+ rpc StreamBackendLogs(BackendLogsRequest) returns (stream LogLine);
+ rpc GetBackendStats(Backend) returns (BackendStats);
}
diff --git a/app/marznode/marznode_grpc.py b/app/marznode/marznode_grpc.py
@@ -23,7 +23,7 @@ class MarzServiceBase(abc.ABC):
pass
@abc.abstractmethod
- async def FetchInbounds(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.Empty, app.marznode.marznode_pb2.InboundsResponse]') -> None:
+ async def FetchBackends(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.Empty, app.marznode.marznode_pb2.BackendsResponse]') -> None:
pass
@abc.abstractmethod
@@ -31,15 +31,19 @@ class MarzServiceBase(abc.ABC):
pass
@abc.abstractmethod
- async def FetchXrayConfig(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.Empty, app.marznode.marznode_pb2.XrayConfig]') -> None:
+ async def FetchBackendConfig(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.Backend, app.marznode.marznode_pb2.BackendConfig]') -> None:
pass
@abc.abstractmethod
- async def RestartXray(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.XrayConfig, app.marznode.marznode_pb2.InboundsResponse]') -> None:
+ async def RestartBackend(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.RestartBackendRequest, app.marznode.marznode_pb2.Empty]') -> None:
pass
@abc.abstractmethod
- async def StreamXrayLogs(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.XrayLogsRequest, app.marznode.marznode_pb2.LogLine]') -> None:
+ async def StreamBackendLogs(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.BackendLogsRequest, app.marznode.marznode_pb2.LogLine]') -> None:
+ pass
+
+ @abc.abstractmethod
+ async def GetBackendStats(self, stream: 'grpclib.server.Stream[app.marznode.marznode_pb2.Backend, app.marznode.marznode_pb2.BackendStats]') -> None:
pass
def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
@@ -56,11 +60,11 @@ class MarzServiceBase(abc.ABC):
app.marznode.marznode_pb2.UsersData,
app.marznode.marznode_pb2.Empty,
),
- '/marznode.MarzService/FetchInbounds': grpclib.const.Handler(
- self.FetchInbounds,
+ '/marznode.MarzService/FetchBackends': grpclib.const.Handler(
+ self.FetchBackends,
grpclib.const.Cardinality.UNARY_UNARY,
app.marznode.marznode_pb2.Empty,
- app.marznode.marznode_pb2.InboundsResponse,
+ app.marznode.marznode_pb2.BackendsResponse,
),
'/marznode.MarzService/FetchUsersStats': grpclib.const.Handler(
self.FetchUsersStats,
@@ -68,24 +72,30 @@ class MarzServiceBase(abc.ABC):
app.marznode.marznode_pb2.Empty,
app.marznode.marznode_pb2.UsersStats,
),
- '/marznode.MarzService/FetchXrayConfig': grpclib.const.Handler(
- self.FetchXrayConfig,
+ '/marznode.MarzService/FetchBackendConfig': grpclib.const.Handler(
+ self.FetchBackendConfig,
grpclib.const.Cardinality.UNARY_UNARY,
- app.marznode.marznode_pb2.Empty,
- app.marznode.marznode_pb2.XrayConfig,
+ app.marznode.marznode_pb2.Backend,
+ app.marznode.marznode_pb2.BackendConfig,
),
- '/marznode.MarzService/RestartXray': grpclib.const.Handler(
- self.RestartXray,
+ '/marznode.MarzService/RestartBackend': grpclib.const.Handler(
+ self.RestartBackend,
grpclib.const.Cardinality.UNARY_UNARY,
- app.marznode.marznode_pb2.XrayConfig,
- app.marznode.marznode_pb2.InboundsResponse,
+ app.marznode.marznode_pb2.RestartBackendRequest,
+ app.marznode.marznode_pb2.Empty,
),
- '/marznode.MarzService/StreamXrayLogs': grpclib.const.Handler(
- self.StreamXrayLogs,
+ '/marznode.MarzService/StreamBackendLogs': grpclib.const.Handler(
+ self.StreamBackendLogs,
grpclib.const.Cardinality.UNARY_STREAM,
- app.marznode.marznode_pb2.XrayLogsRequest,
+ app.marznode.marznode_pb2.BackendLogsRequest,
app.marznode.marznode_pb2.LogLine,
),
+ '/marznode.MarzService/GetBackendStats': grpclib.const.Handler(
+ self.GetBackendStats,
+ grpclib.const.Cardinality.UNARY_UNARY,
+ app.marznode.marznode_pb2.Backend,
+ app.marznode.marznode_pb2.BackendStats,
+ ),
}
@@ -104,11 +114,11 @@ class MarzServiceStub:
app.marznode.marznode_pb2.UsersData,
app.marznode.marznode_pb2.Empty,
)
- self.FetchInbounds = grpclib.client.UnaryUnaryMethod(
+ self.FetchBackends = grpclib.client.UnaryUnaryMethod(
channel,
- '/marznode.MarzService/FetchInbounds',
+ '/marznode.MarzService/FetchBackends',
app.marznode.marznode_pb2.Empty,
- app.marznode.marznode_pb2.InboundsResponse,
+ app.marznode.marznode_pb2.BackendsResponse,
)
self.FetchUsersStats = grpclib.client.UnaryUnaryMethod(
channel,
@@ -116,21 +126,27 @@ class MarzServiceStub:
app.marznode.marznode_pb2.Empty,
app.marznode.marznode_pb2.UsersStats,
)
- self.FetchXrayConfig = grpclib.client.UnaryUnaryMethod(
+ self.FetchBackendConfig = grpclib.client.UnaryUnaryMethod(
channel,
- '/marznode.MarzService/FetchXrayConfig',
- app.marznode.marznode_pb2.Empty,
- app.marznode.marznode_pb2.XrayConfig,
+ '/marznode.MarzService/FetchBackendConfig',
+ app.marznode.marznode_pb2.Backend,
+ app.marznode.marznode_pb2.BackendConfig,
)
- self.RestartXray = grpclib.client.UnaryUnaryMethod(
+ self.RestartBackend = grpclib.client.UnaryUnaryMethod(
channel,
- '/marznode.MarzService/RestartXray',
- app.marznode.marznode_pb2.XrayConfig,
- app.marznode.marznode_pb2.InboundsResponse,
+ '/marznode.MarzService/RestartBackend',
+ app.marznode.marznode_pb2.RestartBackendRequest,
+ app.marznode.marznode_pb2.Empty,
)
- self.StreamXrayLogs = grpclib.client.UnaryStreamMethod(
+ self.StreamBackendLogs = grpclib.client.UnaryStreamMethod(
channel,
- '/marznode.MarzService/StreamXrayLogs',
- app.marznode.marznode_pb2.XrayLogsRequest,
+ '/marznode.MarzService/StreamBackendLogs',
+ app.marznode.marznode_pb2.BackendLogsRequest,
app.marznode.marznode_pb2.LogLine,
)
+ self.GetBackendStats = grpclib.client.UnaryUnaryMethod(
+ channel,
+ '/marznode.MarzService/GetBackendStats',
+ app.marznode.marznode_pb2.Backend,
+ app.marznode.marznode_pb2.BackendStats,
+ )
diff --git a/app/marznode/marznode_pb2.py b/app/marznode/marznode_pb2.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: app/marznode/marznode.proto
-# Protobuf Python Version: 4.25.1
+# Protobuf Python Version: 5.26.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
@@ -14,35 +14,43 @@ _sym_db = _symbol_database.Default()
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x61pp/marznode/marznode.proto\x12\x08marznode\"\x07\n\x05\x45mpty\"6\n\x07Inbound\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x13\n\x06\x63onfig\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_config\"7\n\x10InboundsResponse\x12#\n\x08inbounds\x18\x01 \x03(\x0b\x32\x11.marznode.Inbound\"1\n\x04User\x12\n\n\x02id\x18\x01 \x01(\r\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\t\"M\n\x08UserData\x12\x1c\n\x04user\x18\x01 \x01(\x0b\x32\x0e.marznode.User\x12#\n\x08inbounds\x18\x02 \x03(\x0b\x32\x11.marznode.Inbound\"3\n\tUsersData\x12&\n\nusers_data\x18\x01 \x03(\x0b\x32\x12.marznode.UserData\"j\n\nUsersStats\x12\x33\n\x0busers_stats\x18\x01 \x03(\x0b\x32\x1e.marznode.UsersStats.UserStats\x1a\'\n\tUserStats\x12\x0b\n\x03uid\x18\x01 \x01(\r\x12\r\n\x05usage\x18\x02 \x01(\x04\"\x17\n\x07LogLine\x12\x0c\n\x04line\x18\x01 \x01(\t\"#\n\nXrayConfig\x12\x15\n\rconfiguration\x18\x01 \x01(\t\")\n\x0fXrayLogsRequest\x12\x16\n\x0einclude_buffer\x18\x01 \x01(\x08\x32\xaf\x03\n\x0bMarzService\x12\x32\n\tSyncUsers\x12\x12.marznode.UserData\x1a\x0f.marznode.Empty(\x01\x12\x37\n\x0fRepopulateUsers\x12\x13.marznode.UsersData\x1a\x0f.marznode.Empty\x12<\n\rFetchInbounds\x12\x0f.marznode.Empty\x1a\x1a.marznode.InboundsResponse\x12\x38\n\x0f\x46\x65tchUsersStats\x12\x0f.marznode.Empty\x1a\x14.marznode.UsersStats\x12\x38\n\x0f\x46\x65tchXrayConfig\x12\x0f.marznode.Empty\x1a\x14.marznode.XrayConfig\x12?\n\x0bRestartXray\x12\x14.marznode.XrayConfig\x1a\x1a.marznode.InboundsResponse\x12@\n\x0eStreamXrayLogs\x12\x19.marznode.XrayLogsRequest\x1a\x11.marznode.LogLine0\x01\x62\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x61pp/marznode/marznode.proto\x12\x08marznode\"\x07\n\x05\x45mpty\"z\n\x07\x42\x61\x63kend\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\x04type\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x07version\x18\x03 \x01(\tH\x01\x88\x01\x01\x12#\n\x08inbounds\x18\x04 \x03(\x0b\x32\x11.marznode.InboundB\x07\n\x05_typeB\n\n\x08_version\"7\n\x10\x42\x61\x63kendsResponse\x12#\n\x08\x62\x61\x63kends\x18\x01 \x03(\x0b\x32\x11.marznode.Backend\"6\n\x07Inbound\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12\x13\n\x06\x63onfig\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_config\"1\n\x04User\x12\n\n\x02id\x18\x01 \x01(\r\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\t\"M\n\x08UserData\x12\x1c\n\x04user\x18\x01 \x01(\x0b\x32\x0e.marznode.User\x12#\n\x08inbounds\x18\x02 \x03(\x0b\x32\x11.marznode.Inbound\"3\n\tUsersData\x12&\n\nusers_data\x18\x01 \x03(\x0b\x32\x12.marznode.UserData\"j\n\nUsersStats\x12\x33\n\x0busers_stats\x18\x01 \x03(\x0b\x32\x1e.marznode.UsersStats.UserStats\x1a\'\n\tUserStats\x12\x0b\n\x03uid\x18\x01 \x01(\r\x12\r\n\x05usage\x18\x02 \x01(\x04\"\x17\n\x07LogLine\x12\x0c\n\x04line\x18\x01 \x01(\t\"U\n\rBackendConfig\x12\x15\n\rconfiguration\x18\x01 \x01(\t\x12-\n\rconfig_format\x18\x02 \x01(\x0e\x32\x16.marznode.ConfigFormat\"B\n\x12\x42\x61\x63kendLogsRequest\x12\x14\n\x0c\x62\x61\x63kend_name\x18\x01 \x01(\t\x12\x16\n\x0einclude_buffer\x18\x02 \x01(\x08\"f\n\x15RestartBackendRequest\x12\x14\n\x0c\x62\x61\x63kend_name\x18\x01 \x01(\t\x12,\n\x06\x63onfig\x18\x02 \x01(\x0b\x32\x17.marznode.BackendConfigH\x00\x88\x01\x01\x42\t\n\x07_config\"\x1f\n\x0c\x42\x61\x63kendStats\x12\x0f\n\x07running\x18\x01 \x01(\x08*-\n\x0c\x43onfigFormat\x12\t\n\x05PLAIN\x10\x00\x12\x08\n\x04JSON\x10\x01\x12\x08\n\x04YAML\x10\x02\x32\xfe\x03\n\x0bMarzService\x12\x32\n\tSyncUsers\x12\x12.marznode.UserData\x1a\x0f.marznode.Empty(\x01\x12\x37\n\x0fRepopulateUsers\x12\x13.marznode.UsersData\x1a\x0f.marznode.Empty\x12<\n\rFetchBackends\x12\x0f.marznode.Empty\x1a\x1a.marznode.BackendsResponse\x12\x38\n\x0f\x46\x65tchUsersStats\x12\x0f.marznode.Empty\x1a\x14.marznode.UsersStats\x12@\n\x12\x46\x65tchBackendConfig\x12\x11.marznode.Backend\x1a\x17.marznode.BackendConfig\x12\x42\n\x0eRestartBackend\x12\x1f.marznode.RestartBackendRequest\x1a\x0f.marznode.Empty\x12\x46\n\x11StreamBackendLogs\x12\x1c.marznode.BackendLogsRequest\x1a\x11.marznode.LogLine0\x01\x12<\n\x0fGetBackendStats\x12\x11.marznode.Backend\x1a\x16.marznode.BackendStatsb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'app.marznode.marznode_pb2', _globals)
-if _descriptor._USE_C_DESCRIPTORS == False:
- DESCRIPTOR._options = None
+if not _descriptor._USE_C_DESCRIPTORS:
+ DESCRIPTOR._loaded_options = None
+ _globals['_CONFIGFORMAT']._serialized_start=895
+ _globals['_CONFIGFORMAT']._serialized_end=940
_globals['_EMPTY']._serialized_start=41
_globals['_EMPTY']._serialized_end=48
- _globals['_INBOUND']._serialized_start=50
- _globals['_INBOUND']._serialized_end=104
- _globals['_INBOUNDSRESPONSE']._serialized_start=106
- _globals['_INBOUNDSRESPONSE']._serialized_end=161
- _globals['_USER']._serialized_start=163
- _globals['_USER']._serialized_end=212
- _globals['_USERDATA']._serialized_start=214
- _globals['_USERDATA']._serialized_end=291
- _globals['_USERSDATA']._serialized_start=293
- _globals['_USERSDATA']._serialized_end=344
- _globals['_USERSSTATS']._serialized_start=346
- _globals['_USERSSTATS']._serialized_end=452
- _globals['_USERSSTATS_USERSTATS']._serialized_start=413
- _globals['_USERSSTATS_USERSTATS']._serialized_end=452
- _globals['_LOGLINE']._serialized_start=454
- _globals['_LOGLINE']._serialized_end=477
- _globals['_XRAYCONFIG']._serialized_start=479
- _globals['_XRAYCONFIG']._serialized_end=514
- _globals['_XRAYLOGSREQUEST']._serialized_start=516
- _globals['_XRAYLOGSREQUEST']._serialized_end=557
- _globals['_MARZSERVICE']._serialized_start=560
- _globals['_MARZSERVICE']._serialized_end=991
+ _globals['_BACKEND']._serialized_start=50
+ _globals['_BACKEND']._serialized_end=172
+ _globals['_BACKENDSRESPONSE']._serialized_start=174
+ _globals['_BACKENDSRESPONSE']._serialized_end=229
+ _globals['_INBOUND']._serialized_start=231
+ _globals['_INBOUND']._serialized_end=285
+ _globals['_USER']._serialized_start=287
+ _globals['_USER']._serialized_end=336
+ _globals['_USERDATA']._serialized_start=338
+ _globals['_USERDATA']._serialized_end=415
+ _globals['_USERSDATA']._serialized_start=417
+ _globals['_USERSDATA']._serialized_end=468
+ _globals['_USERSSTATS']._serialized_start=470
+ _globals['_USERSSTATS']._serialized_end=576
+ _globals['_USERSSTATS_USERSTATS']._serialized_start=537
+ _globals['_USERSSTATS_USERSTATS']._serialized_end=576
+ _globals['_LOGLINE']._serialized_start=578
+ _globals['_LOGLINE']._serialized_end=601
+ _globals['_BACKENDCONFIG']._serialized_start=603
+ _globals['_BACKENDCONFIG']._serialized_end=688
+ _globals['_BACKENDLOGSREQUEST']._serialized_start=690
+ _globals['_BACKENDLOGSREQUEST']._serialized_end=756
+ _globals['_RESTARTBACKENDREQUEST']._serialized_start=758
+ _globals['_RESTARTBACKENDREQUEST']._serialized_end=860
+ _globals['_BACKENDSTATS']._serialized_start=862
+ _globals['_BACKENDSTATS']._serialized_end=893
+ _globals['_MARZSERVICE']._serialized_start=943
+ _globals['_MARZSERVICE']._serialized_end=1453
# @@protoc_insertion_point(module_scope)
diff --git a/app/marznode/marznode_pb2.pyi b/app/marznode/marznode_pb2.pyi
@@ -1,14 +1,42 @@
from google.protobuf.internal import containers as _containers
+from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
+class ConfigFormat(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = ()
+ PLAIN: _ClassVar[ConfigFormat]
+ JSON: _ClassVar[ConfigFormat]
+ YAML: _ClassVar[ConfigFormat]
+PLAIN: ConfigFormat
+JSON: ConfigFormat
+YAML: ConfigFormat
+
class Empty(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
+class Backend(_message.Message):
+ __slots__ = ("name", "type", "version", "inbounds")
+ NAME_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ VERSION_FIELD_NUMBER: _ClassVar[int]
+ INBOUNDS_FIELD_NUMBER: _ClassVar[int]
+ name: str
+ type: str
+ version: str
+ inbounds: _containers.RepeatedCompositeFieldContainer[Inbound]
+ def __init__(self, name: _Optional[str] = ..., type: _Optional[str] = ..., version: _Optional[str] = ..., inbounds: _Optional[_Iterable[_Union[Inbound, _Mapping]]] = ...) -> None: ...
+
+class BackendsResponse(_message.Message):
+ __slots__ = ("backends",)
+ BACKENDS_FIELD_NUMBER: _ClassVar[int]
+ backends: _containers.RepeatedCompositeFieldContainer[Backend]
+ def __init__(self, backends: _Optional[_Iterable[_Union[Backend, _Mapping]]] = ...) -> None: ...
+
class Inbound(_message.Message):
__slots__ = ("tag", "config")
TAG_FIELD_NUMBER: _ClassVar[int]
@@ -17,12 +45,6 @@ class Inbound(_message.Message):
config: str
def __init__(self, tag: _Optional[str] = ..., config: _Optional[str] = ...) -> None: ...
-class InboundsResponse(_message.Message):
- __slots__ = ("inbounds",)
- INBOUNDS_FIELD_NUMBER: _ClassVar[int]
- inbounds: _containers.RepeatedCompositeFieldContainer[Inbound]
- def __init__(self, inbounds: _Optional[_Iterable[_Union[Inbound, _Mapping]]] = ...) -> None: ...
-
class User(_message.Message):
__slots__ = ("id", "username", "key")
ID_FIELD_NUMBER: _ClassVar[int]
@@ -66,14 +88,32 @@ class LogLine(_message.Message):
line: str
def __init__(self, line: _Optional[str] = ...) -> None: ...
-class XrayConfig(_message.Message):
- __slots__ = ("configuration",)
+class BackendConfig(_message.Message):
+ __slots__ = ("configuration", "config_format")
CONFIGURATION_FIELD_NUMBER: _ClassVar[int]
+ CONFIG_FORMAT_FIELD_NUMBER: _ClassVar[int]
configuration: str
- def __init__(self, configuration: _Optional[str] = ...) -> None: ...
+ config_format: ConfigFormat
+ def __init__(self, configuration: _Optional[str] = ..., config_format: _Optional[_Union[ConfigFormat, str]] = ...) -> None: ...
-class XrayLogsRequest(_message.Message):
- __slots__ = ("include_buffer",)
+class BackendLogsRequest(_message.Message):
+ __slots__ = ("backend_name", "include_buffer")
+ BACKEND_NAME_FIELD_NUMBER: _ClassVar[int]
INCLUDE_BUFFER_FIELD_NUMBER: _ClassVar[int]
+ backend_name: str
include_buffer: bool
- def __init__(self, include_buffer: bool = ...) -> None: ...
+ def __init__(self, backend_name: _Optional[str] = ..., include_buffer: bool = ...) -> None: ...
+
+class RestartBackendRequest(_message.Message):
+ __slots__ = ("backend_name", "config")
+ BACKEND_NAME_FIELD_NUMBER: _ClassVar[int]
+ CONFIG_FIELD_NUMBER: _ClassVar[int]
+ backend_name: str
+ config: BackendConfig
+ def __init__(self, backend_name: _Optional[str] = ..., config: _Optional[_Union[BackendConfig, _Mapping]] = ...) -> None: ...
+
+class BackendStats(_message.Message):
+ __slots__ = ("running",)
+ RUNNING_FIELD_NUMBER: _ClassVar[int]
+ running: bool
+ def __init__(self, running: bool = ...) -> None: ...
diff --git a/app/marznode/marznode_pb2_grpc.py b/app/marznode/marznode_pb2_grpc.py
@@ -1,9 +1,34 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
+import warnings
from app.marznode import marznode_pb2 as app_dot_marznode_dot_marznode__pb2
+GRPC_GENERATED_VERSION = '1.65.4'
+GRPC_VERSION = grpc.__version__
+EXPECTED_ERROR_RELEASE = '1.66.0'
+SCHEDULED_RELEASE_DATE = 'August 6, 2024'
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ warnings.warn(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in app/marznode/marznode_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},'
+ + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.',
+ RuntimeWarning
+ )
+
class MarzServiceStub(object):
"""Missing associated documentation comment in .proto file."""
@@ -18,37 +43,42 @@ class MarzServiceStub(object):
'/marznode.MarzService/SyncUsers',
request_serializer=app_dot_marznode_dot_marznode__pb2.UserData.SerializeToString,
response_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString,
- )
+ _registered_method=True)
self.RepopulateUsers = channel.unary_unary(
'/marznode.MarzService/RepopulateUsers',
request_serializer=app_dot_marznode_dot_marznode__pb2.UsersData.SerializeToString,
response_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString,
- )
- self.FetchInbounds = channel.unary_unary(
- '/marznode.MarzService/FetchInbounds',
+ _registered_method=True)
+ self.FetchBackends = channel.unary_unary(
+ '/marznode.MarzService/FetchBackends',
request_serializer=app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString,
- response_deserializer=app_dot_marznode_dot_marznode__pb2.InboundsResponse.FromString,
- )
+ response_deserializer=app_dot_marznode_dot_marznode__pb2.BackendsResponse.FromString,
+ _registered_method=True)
self.FetchUsersStats = channel.unary_unary(
'/marznode.MarzService/FetchUsersStats',
request_serializer=app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString,
response_deserializer=app_dot_marznode_dot_marznode__pb2.UsersStats.FromString,
- )
- self.FetchXrayConfig = channel.unary_unary(
- '/marznode.MarzService/FetchXrayConfig',
- request_serializer=app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString,
- response_deserializer=app_dot_marznode_dot_marznode__pb2.XrayConfig.FromString,
- )
- self.RestartXray = channel.unary_unary(
- '/marznode.MarzService/RestartXray',
- request_serializer=app_dot_marznode_dot_marznode__pb2.XrayConfig.SerializeToString,
- response_deserializer=app_dot_marznode_dot_marznode__pb2.InboundsResponse.FromString,
- )
- self.StreamXrayLogs = channel.unary_stream(
- '/marznode.MarzService/StreamXrayLogs',
- request_serializer=app_dot_marznode_dot_marznode__pb2.XrayLogsRequest.SerializeToString,
+ _registered_method=True)
+ self.FetchBackendConfig = channel.unary_unary(
+ '/marznode.MarzService/FetchBackendConfig',
+ request_serializer=app_dot_marznode_dot_marznode__pb2.Backend.SerializeToString,
+ response_deserializer=app_dot_marznode_dot_marznode__pb2.BackendConfig.FromString,
+ _registered_method=True)
+ self.RestartBackend = channel.unary_unary(
+ '/marznode.MarzService/RestartBackend',
+ request_serializer=app_dot_marznode_dot_marznode__pb2.RestartBackendRequest.SerializeToString,
+ response_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString,
+ _registered_method=True)
+ self.StreamBackendLogs = channel.unary_stream(
+ '/marznode.MarzService/StreamBackendLogs',
+ request_serializer=app_dot_marznode_dot_marznode__pb2.BackendLogsRequest.SerializeToString,
response_deserializer=app_dot_marznode_dot_marznode__pb2.LogLine.FromString,
- )
+ _registered_method=True)
+ self.GetBackendStats = channel.unary_unary(
+ '/marznode.MarzService/GetBackendStats',
+ request_serializer=app_dot_marznode_dot_marznode__pb2.Backend.SerializeToString,
+ response_deserializer=app_dot_marznode_dot_marznode__pb2.BackendStats.FromString,
+ _registered_method=True)
class MarzServiceServicer(object):
@@ -66,7 +96,7 @@ class MarzServiceServicer(object):
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
- def FetchInbounds(self, request, context):
+ def FetchBackends(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
@@ -78,19 +108,25 @@ class MarzServiceServicer(object):
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
- def FetchXrayConfig(self, request, context):
+ def FetchBackendConfig(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
- def RestartXray(self, request, context):
+ def RestartBackend(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
- def StreamXrayLogs(self, request, context):
+ def StreamBackendLogs(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetBackendStats(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
@@ -109,35 +145,41 @@ def add_MarzServiceServicer_to_server(servicer, server):
request_deserializer=app_dot_marznode_dot_marznode__pb2.UsersData.FromString,
response_serializer=app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString,
),
- 'FetchInbounds': grpc.unary_unary_rpc_method_handler(
- servicer.FetchInbounds,
+ 'FetchBackends': grpc.unary_unary_rpc_method_handler(
+ servicer.FetchBackends,
request_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString,
- response_serializer=app_dot_marznode_dot_marznode__pb2.InboundsResponse.SerializeToString,
+ response_serializer=app_dot_marznode_dot_marznode__pb2.BackendsResponse.SerializeToString,
),
'FetchUsersStats': grpc.unary_unary_rpc_method_handler(
servicer.FetchUsersStats,
request_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString,
response_serializer=app_dot_marznode_dot_marznode__pb2.UsersStats.SerializeToString,
),
- 'FetchXrayConfig': grpc.unary_unary_rpc_method_handler(
- servicer.FetchXrayConfig,
- request_deserializer=app_dot_marznode_dot_marznode__pb2.Empty.FromString,
- response_serializer=app_dot_marznode_dot_marznode__pb2.XrayConfig.SerializeToString,
+ 'FetchBackendConfig': grpc.unary_unary_rpc_method_handler(
+ servicer.FetchBackendConfig,
+ request_deserializer=app_dot_marznode_dot_marznode__pb2.Backend.FromString,
+ response_serializer=app_dot_marznode_dot_marznode__pb2.BackendConfig.SerializeToString,
),
- 'RestartXray': grpc.unary_unary_rpc_method_handler(
- servicer.RestartXray,
- request_deserializer=app_dot_marznode_dot_marznode__pb2.XrayConfig.FromString,
- response_serializer=app_dot_marznode_dot_marznode__pb2.InboundsResponse.SerializeToString,
+ 'RestartBackend': grpc.unary_unary_rpc_method_handler(
+ servicer.RestartBackend,
+ request_deserializer=app_dot_marznode_dot_marznode__pb2.RestartBackendRequest.FromString,
+ response_serializer=app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString,
),
- 'StreamXrayLogs': grpc.unary_stream_rpc_method_handler(
- servicer.StreamXrayLogs,
- request_deserializer=app_dot_marznode_dot_marznode__pb2.XrayLogsRequest.FromString,
+ 'StreamBackendLogs': grpc.unary_stream_rpc_method_handler(
+ servicer.StreamBackendLogs,
+ request_deserializer=app_dot_marznode_dot_marznode__pb2.BackendLogsRequest.FromString,
response_serializer=app_dot_marznode_dot_marznode__pb2.LogLine.SerializeToString,
),
+ 'GetBackendStats': grpc.unary_unary_rpc_method_handler(
+ servicer.GetBackendStats,
+ request_deserializer=app_dot_marznode_dot_marznode__pb2.Backend.FromString,
+ response_serializer=app_dot_marznode_dot_marznode__pb2.BackendStats.SerializeToString,
+ ),
}
generic_handler = grpc.method_handlers_generic_handler(
'marznode.MarzService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
+ server.add_registered_method_handlers('marznode.MarzService', rpc_method_handlers)
# This class is part of an EXPERIMENTAL API.
@@ -155,11 +197,21 @@ class MarzService(object):
wait_for_ready=None,
timeout=None,
metadata=None):
- return grpc.experimental.stream_unary(request_iterator, target, '/marznode.MarzService/SyncUsers',
+ return grpc.experimental.stream_unary(
+ request_iterator,
+ target,
+ '/marznode.MarzService/SyncUsers',
app_dot_marznode_dot_marznode__pb2.UserData.SerializeToString,
app_dot_marznode_dot_marznode__pb2.Empty.FromString,
- options, channel_credentials,
- insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
@staticmethod
def RepopulateUsers(request,
@@ -172,14 +224,24 @@ class MarzService(object):
wait_for_ready=None,
timeout=None,
metadata=None):
- return grpc.experimental.unary_unary(request, target, '/marznode.MarzService/RepopulateUsers',
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/marznode.MarzService/RepopulateUsers',
app_dot_marznode_dot_marznode__pb2.UsersData.SerializeToString,
app_dot_marznode_dot_marznode__pb2.Empty.FromString,
- options, channel_credentials,
- insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
@staticmethod
- def FetchInbounds(request,
+ def FetchBackends(request,
target,
options=(),
channel_credentials=None,
@@ -189,11 +251,21 @@ class MarzService(object):
wait_for_ready=None,
timeout=None,
metadata=None):
- return grpc.experimental.unary_unary(request, target, '/marznode.MarzService/FetchInbounds',
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/marznode.MarzService/FetchBackends',
app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString,
- app_dot_marznode_dot_marznode__pb2.InboundsResponse.FromString,
- options, channel_credentials,
- insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+ app_dot_marznode_dot_marznode__pb2.BackendsResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
@staticmethod
def FetchUsersStats(request,
@@ -206,14 +278,24 @@ class MarzService(object):
wait_for_ready=None,
timeout=None,
metadata=None):
- return grpc.experimental.unary_unary(request, target, '/marznode.MarzService/FetchUsersStats',
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/marznode.MarzService/FetchUsersStats',
app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString,
app_dot_marznode_dot_marznode__pb2.UsersStats.FromString,
- options, channel_credentials,
- insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
@staticmethod
- def FetchXrayConfig(request,
+ def FetchBackendConfig(request,
target,
options=(),
channel_credentials=None,
@@ -223,14 +305,24 @@ class MarzService(object):
wait_for_ready=None,
timeout=None,
metadata=None):
- return grpc.experimental.unary_unary(request, target, '/marznode.MarzService/FetchXrayConfig',
- app_dot_marznode_dot_marznode__pb2.Empty.SerializeToString,
- app_dot_marznode_dot_marznode__pb2.XrayConfig.FromString,
- options, channel_credentials,
- insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/marznode.MarzService/FetchBackendConfig',
+ app_dot_marznode_dot_marznode__pb2.Backend.SerializeToString,
+ app_dot_marznode_dot_marznode__pb2.BackendConfig.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
@staticmethod
- def RestartXray(request,
+ def RestartBackend(request,
target,
options=(),
channel_credentials=None,
@@ -240,14 +332,24 @@ class MarzService(object):
wait_for_ready=None,
timeout=None,
metadata=None):
- return grpc.experimental.unary_unary(request, target, '/marznode.MarzService/RestartXray',
- app_dot_marznode_dot_marznode__pb2.XrayConfig.SerializeToString,
- app_dot_marznode_dot_marznode__pb2.InboundsResponse.FromString,
- options, channel_credentials,
- insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/marznode.MarzService/RestartBackend',
+ app_dot_marznode_dot_marznode__pb2.RestartBackendRequest.SerializeToString,
+ app_dot_marznode_dot_marznode__pb2.Empty.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
@staticmethod
- def StreamXrayLogs(request,
+ def StreamBackendLogs(request,
target,
options=(),
channel_credentials=None,
@@ -257,8 +359,45 @@ class MarzService(object):
wait_for_ready=None,
timeout=None,
metadata=None):
- return grpc.experimental.unary_stream(request, target, '/marznode.MarzService/StreamXrayLogs',
- app_dot_marznode_dot_marznode__pb2.XrayLogsRequest.SerializeToString,
+ return grpc.experimental.unary_stream(
+ request,
+ target,
+ '/marznode.MarzService/StreamBackendLogs',
+ app_dot_marznode_dot_marznode__pb2.BackendLogsRequest.SerializeToString,
app_dot_marznode_dot_marznode__pb2.LogLine.FromString,
- options, channel_credentials,
- insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetBackendStats(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/marznode.MarzService/GetBackendStats',
+ app_dot_marznode_dot_marznode__pb2.Backend.SerializeToString,
+ app_dot_marznode_dot_marznode__pb2.BackendStats.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
diff --git a/app/models/node.py b/app/models/node.py
@@ -3,6 +3,28 @@ from enum import Enum
from pydantic import ConfigDict, BaseModel, Field
+class BackendConfigFormat(Enum):
+ PLAIN = 0
+ JSON = 1
+ YAML = 2
+
+
+class BackendConfig(BaseModel):
+ config: str
+ format: BackendConfigFormat
+
+
+class BackendStats(BaseModel):
+ running: bool
+
+
+class Backend(BaseModel):
+ name: str
+ backend_type: str
+ version: str | None
+ running: bool
+
+
class NodeStatus(str, Enum):
healthy = "healthy"
unhealthy = "unhealthy"
@@ -70,6 +92,7 @@ class NodeResponse(Node):
message: str | None = None
model_config = ConfigDict(from_attributes=True)
inbound_ids: list[int] | None = None
+ backends: list[Backend]
class NodeUsageResponse(BaseModel):
diff --git a/app/routes/node.py b/app/routes/node.py
@@ -1,4 +1,3 @@
-import json
import logging
from typing import Annotated
@@ -26,6 +25,8 @@ from app.models.node import (
NodeSettings,
NodeStatus,
NodesUsageResponse,
+ BackendConfig,
+ BackendStats,
)
logger = logging.getLogger(__name__)
@@ -98,9 +99,13 @@ def get_node(node_id: int, db: DBDep, admin: SudoAdminDep):
return db_node
-@router.websocket("/{node_id}/logs")
+@router.websocket("/{node_id}/{backend}/logs")
async def node_logs(
- node_id: int, websocket: WebSocket, db: DBDep, include_buffer: bool = True
+ node_id: int,
+ backend: str,
+ websocket: WebSocket,
+ db: DBDep,
+ include_buffer: bool = True,
):
token = websocket.query_params.get("token") or websocket.headers.get(
"Authorization", ""
@@ -113,13 +118,10 @@ async def node_logs(
if not marznode.nodes.get(node_id):
return await websocket.close(reason="Node not found", code=4404)
- # if not await marznode.nodes[node_id].is_healthy:
- # return await websocket.close(reason="Node is not connected", code=4400)
-
await websocket.accept()
try:
async for line in marznode.nodes[node_id].get_logs(
- include_buffer=include_buffer
+ name=backend, include_buffer=include_buffer
):
try:
await websocket.send_text(line)
@@ -170,28 +172,52 @@ async def reconnect_node(node_id: int, db: DBDep, admin: SudoAdminDep):
return {}
-@router.get("/{node_id}/xray_config")
-async def get_node_xray_config(node_id: int, db: DBDep, admin: SudoAdminDep):
+@router.get("/{node_id}/{backend}/stats", response_model=BackendStats)
+async def get_backend_stats(
+ node_id: int, backend: str, db: DBDep, admin: SudoAdminDep
+):
if not (node := marznode.nodes.get(node_id)):
raise HTTPException(status_code=404, detail="Node not found")
try:
- config = await node.get_xray_config()
- except:
+ stats = await node.get_backend_stats(backend)
+ except Exception:
+ raise HTTPException(502)
+ else:
+ return BackendStats(running=stats.running)
+
+
+@router.get("/{node_id}/{backend}/config", response_model=BackendConfig)
+async def get_node_xray_config(
+ node_id: int, backend: str, db: DBDep, admin: SudoAdminDep
+):
+ if not (node := marznode.nodes.get(node_id)):
+ raise HTTPException(status_code=404, detail="Node not found")
+
+ try:
+ config, config_format = await node.get_backend_config(name=backend)
+ except Exception:
raise HTTPException(status_code=502, detail="Node isn't responsive")
else:
- return json.loads(config)
+ return {"config": config, "format": config_format}
-@router.put("/{node_id}/xray_config")
+@router.put("/{node_id}/{backend}/config")
async def alter_node_xray_config(
- node_id: int, db: DBDep, admin: SudoAdminDep, body: Annotated[dict, Body()]
+ node_id: int,
+ backend: str,
+ db: DBDep,
+ admin: SudoAdminDep,
+ config: Annotated[BackendConfig, Body()],
):
if not (node := marznode.nodes.get(node_id)):
raise HTTPException(status_code=404, detail="Node not found")
- xray_config = json.dumps(body)
try:
- await node.restart_xray(xray_config)
+ await node.restart_backend(
+ name=backend,
+ config=config.config,
+ config_format=config.format.value,
+ )
except:
raise HTTPException(status_code=502, detail="Node isn't responsive")
return {}
diff --git a/dashboard/public/locales/en.json b/dashboard/public/locales/en.json
@@ -72,6 +72,8 @@
"normal": "Normal",
"sudo": "Sudo",
"owner": "Owner",
+ "running": "Running",
+ "down": "Down",
"support-us": {
"title": "Support Us",
"desc": "Marzneshin is free and open-source software (FOSS), join us and show your support by donating.",
diff --git a/dashboard/src/components/settings-dialog/index.tsx b/dashboard/src/components/settings-dialog/index.tsx
@@ -1,32 +1,41 @@
-import { FC, PropsWithChildren } from "react";
+import { type FC, type PropsWithChildren, useEffect } from "react";
import {
- ScrollArea,
- Sheet,
- SheetContent,
- SheetHeader,
- SheetTitle,
+ ScrollArea,
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
} from "@marzneshin/components";
import { useTranslation } from "react-i18next";
export interface SettingsDialogProps {
- onOpenChange: (state: boolean) => void;
- open: boolean;
+ onOpenChange: (state: boolean) => void;
+ open: boolean;
+ onClose?: () => void;
}
export const SettingsDialog: FC<SettingsDialogProps & PropsWithChildren> = ({
- open, onOpenChange, children
+ open,
+ onOpenChange,
+ children,
+ onClose = () => null,
}) => {
- const { t } = useTranslation();
- return (
- <Sheet open={open} onOpenChange={onOpenChange}>
- <SheetContent className="sm:min-w-full md:min-w-[700px] space-y-5">
- <SheetHeader >
- <SheetTitle>{t("settings")}</SheetTitle>
- </SheetHeader>
- <ScrollArea className="flex flex-col gap-4 h-full">
- {children}
- </ScrollArea>
- </SheetContent>
- </Sheet>
- )
-}
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ if (!open) onClose();
+ }, [open, onClose]);
+
+ return (
+ <Sheet open={open} onOpenChange={onOpenChange}>
+ <SheetContent className="sm:min-w-full md:min-w-[700px] space-y-5">
+ <SheetHeader>
+ <SheetTitle>{t("settings")}</SheetTitle>
+ </SheetHeader>
+ <ScrollArea className="flex flex-col gap-4 h-full">
+ {children}
+ </ScrollArea>
+ </SheetContent>
+ </Sheet>
+ );
+};
diff --git a/dashboard/src/features/admins/components/dialogs/settings/index.tsx b/dashboard/src/features/admins/components/dialogs/settings/index.tsx
@@ -1,73 +1,69 @@
import {
- // Button,
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger,
- Awaiting,
- SettingsInfoSkeleton,
- SettingsDialogProps,
- SettingsDialog,
+ // Button,
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+ Awaiting,
+ SettingsInfoSkeleton,
+ SettingsDialogProps,
+ SettingsDialog,
} from "@marzneshin/components";
-import {
- type AdminType
-} from "@marzneshin/features/admins";
-import { type FC, useEffect } from "react";
+import { type AdminType } from "@marzneshin/features/admins";
+import { type FC } from "react";
import { AdminInfoTable } from "./admin-info";
import { useTranslation } from "react-i18next";
interface AdminsSettingsDialogProps extends SettingsDialogProps {
- entity: AdminType | null;
- onClose: () => void;
- isPending: boolean;
+ entity: AdminType | null;
+ onClose: () => void;
+ isPending: boolean;
}
export const AdminsSettingsDialog: FC<AdminsSettingsDialogProps> = ({
- onOpenChange,
- open,
- entity,
- onClose,
- isPending,
+ onOpenChange,
+ open,
+ entity,
+ onClose,
+ isPending,
}) => {
- const { t } = useTranslation();
-
- useEffect(() => {
- if (!open) onClose();
- }, [open, onClose]);
+ const { t } = useTranslation();
- return (
- <SettingsDialog open={open} onOpenChange={onOpenChange}>
- <Awaiting
- Component={entity ? (
- <Tabs defaultValue="info" className="w-full h-full">
- <TabsList className="w-full bg-accent">
- <TabsTrigger className="w-full" value="info">
- {t("admin_info")}
- </TabsTrigger>
- <TabsTrigger className="w-full" value="services">
- {t("services")}
- </TabsTrigger>
- <TabsTrigger className="w-full" value="subscription">
- {t("users")}
- </TabsTrigger>
- </TabsList>
- <TabsContent
- value="info"
- className="flex flex-col gap-2 w-full h-full"
- >
- <AdminInfoTable admin={entity} />
- </TabsContent>
- <TabsContent value="services">
- {/* <AdminServicesTable admin={entity} /> */}
- </TabsContent>
- <TabsContent value="users">
- Users
- </TabsContent>
- </Tabs>
- ) : (<div>Not Found</div>)}
- Skeleton={<SettingsInfoSkeleton />}
- isFetching={isPending}
- />
- </SettingsDialog>
- );
+ return (
+ <SettingsDialog open={open} onClose={onClose} onOpenChange={onOpenChange}>
+ <Awaiting
+ Component={
+ entity ? (
+ <Tabs defaultValue="info" className="w-full h-full">
+ <TabsList className="w-full bg-accent">
+ <TabsTrigger className="w-full" value="info">
+ {t("admin_info")}
+ </TabsTrigger>
+ <TabsTrigger className="w-full" value="services">
+ {t("services")}
+ </TabsTrigger>
+ <TabsTrigger className="w-full" value="subscription">
+ {t("users")}
+ </TabsTrigger>
+ </TabsList>
+ <TabsContent
+ value="info"
+ className="flex flex-col gap-2 w-full h-full"
+ >
+ <AdminInfoTable admin={entity} />
+ </TabsContent>
+ <TabsContent value="services">
+ {/* <AdminServicesTable admin={entity} /> */}
+ </TabsContent>
+ <TabsContent value="users">Users</TabsContent>
+ </Tabs>
+ ) : (
+ <div>Not Found</div>
+ )
+ }
+ Skeleton={<SettingsInfoSkeleton />}
+ isFetching={isPending}
+ />
+ </SettingsDialog>
+ );
};
diff --git a/dashboard/src/features/hosts/dialogs/settings/index.tsx b/dashboard/src/features/hosts/dialogs/settings/index.tsx
@@ -1,41 +1,42 @@
-import { type FC, useEffect } from "react";
+import { type FC } from "react";
import {
- SettingsDialogProps,
- SettingsDialog,
- Table,
- TableBody,
- TableRowWithCell,
-} from "@marzneshin/components"
-import { useTranslation } from "react-i18next"
-import { HostType } from "../.."
+ SettingsDialogProps,
+ SettingsDialog,
+ Table,
+ TableBody,
+ TableRowWithCell,
+} from "@marzneshin/components";
+import { useTranslation } from "react-i18next";
+import { HostType } from "../..";
interface HostsSettingsDialogProps extends SettingsDialogProps {
- onOpenChange: (state: boolean) => void
- onClose: () => void
- open: boolean
- entity: HostType
+ onOpenChange: (state: boolean) => void;
+ onClose: () => void;
+ open: boolean;
+ entity: HostType;
}
-export const HostSettingsDialog: FC<HostsSettingsDialogProps> = ({ onOpenChange, open, entity, onClose }) => {
- const { t } = useTranslation();
+export const HostSettingsDialog: FC<HostsSettingsDialogProps> = ({
+ onOpenChange,
+ open,
+ entity,
+ onClose,
+}) => {
+ const { t } = useTranslation();
- useEffect(() => {
- if (!open) onClose();
- }, [open, onClose]);
-
- return (
- <SettingsDialog open={open} onOpenChange={onOpenChange}>
- <Table>
- <TableBody>
- <TableRowWithCell label={t('remark')} value={entity.remark} />
- <TableRowWithCell label={t('address')} value={entity.address} />
- <TableRowWithCell label={t('port')} value={entity.port} />
- <TableRowWithCell label={t('host')} value={entity.host} />
- <TableRowWithCell label={t('path')} value={entity.path} />
- <TableRowWithCell label={t('sni')} value={entity.sni} />
- <TableRowWithCell label={t('alpn')} value={entity.alpn} />
- </TableBody>
- </Table>
- </SettingsDialog>
- )
-}
+ return (
+ <SettingsDialog open={open} onClose={onClose} onOpenChange={onOpenChange}>
+ <Table>
+ <TableBody>
+ <TableRowWithCell label={t("remark")} value={entity.remark} />
+ <TableRowWithCell label={t("address")} value={entity.address} />
+ <TableRowWithCell label={t("port")} value={entity.port} />
+ <TableRowWithCell label={t("host")} value={entity.host} />
+ <TableRowWithCell label={t("path")} value={entity.path} />
+ <TableRowWithCell label={t("sni")} value={entity.sni} />
+ <TableRowWithCell label={t("alpn")} value={entity.alpn} />
+ </TableBody>
+ </Table>
+ </SettingsDialog>
+ );
+};
diff --git a/dashboard/src/features/nodes/dialogs/settings/index.tsx b/dashboard/src/features/nodes/dialogs/settings/index.tsx
@@ -1,65 +1,63 @@
import {
- Card,
- CardContent,
- SettingsDialogProps,
- SettingsDialog,
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger,
+ type SettingsDialogProps,
+ SettingsDialog,
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
} from "@marzneshin/components";
-import { type FC, useEffect } from "react";
+import type { FC } from "react";
import { useTranslation } from "react-i18next";
-import type { NodeType } from "../..";
-import { LogContainer } from "../../log";
+import type { NodeBackendType, NodeType } from "../..";
import { NodesDetailTable } from "../../tables/detail-table";
-import { NodeConfigEditor } from "./node-config-editor";
+import { NodeBackendSetting } from "./node-backend-setting";
interface NodesSettingsDialogProps extends SettingsDialogProps {
- entity: NodeType;
- onClose: () => void;
+ entity: NodeType;
+ onClose: () => void;
}
export const NodesSettingsDialog: FC<NodesSettingsDialogProps> = ({
- onOpenChange,
- open,
- entity,
- onClose,
+ onOpenChange,
+ open,
+ entity,
+ onClose,
}) => {
- const { t } = useTranslation();
+ const { t } = useTranslation();
- useEffect(() => {
- if (!open) onClose();
- }, [open, onClose]);
-
- return (
- <SettingsDialog open={open} onOpenChange={onOpenChange}>
- <div className="my-4">
- <h1 className="font-medium font-header">
- {t("page.nodes.settings.detail")}
- </h1>
- <NodesDetailTable node={entity} />
- </div>
- <Tabs className="my-3 max-w-full h-full" defaultValue="logs">
- <TabsList className="w-full">
- <TabsTrigger className="w-full" value="logs">
- {t("logs")}
- </TabsTrigger>
- <TabsTrigger className="w-full" value="config">
- {t("config")}
- </TabsTrigger>
- </TabsList>
- <TabsContent value="logs" className="h-full w-full">
- <LogContainer node={entity} />
- </TabsContent>
- <TabsContent value="config" className="h-full z-51">
- <Card>
- <CardContent className="p-2 h-1/2 flex flex-col gap-2">
- <NodeConfigEditor entity={entity} />
- </CardContent>
- </Card>
- </TabsContent>
- </Tabs>
- </SettingsDialog>
- );
+ return (
+ <SettingsDialog open={open} onClose={onClose} onOpenChange={onOpenChange}>
+ <div className="my-4">
+ <h1 className="font-medium font-header">
+ {t("page.nodes.settings.detail")}
+ </h1>
+ <NodesDetailTable node={entity} />
+ </div>
+ <Tabs
+ className="my-3 w-full h-full"
+ defaultValue={entity.backends[0].name}
+ >
+ <TabsList className="w-full">
+ {...entity.backends.map((backend: NodeBackendType) => (
+ <TabsTrigger
+ className="capitalize w-full"
+ value={backend.name}
+ key={backend.name}
+ >
+ {backend.name}
+ </TabsTrigger>
+ ))}
+ </TabsList>
+ {...entity.backends.map((backend: NodeBackendType) => (
+ <TabsContent
+ className="w-full"
+ value={backend.name}
+ key={backend.name}
+ >
+ <NodeBackendSetting node={entity} backend={backend.name} />
+ </TabsContent>
+ ))}
+ </Tabs>
+ </SettingsDialog>
+ );
};
diff --git a/dashboard/src/features/nodes/dialogs/settings/node-backend-setting.tsx b/dashboard/src/features/nodes/dialogs/settings/node-backend-setting.tsx
@@ -0,0 +1,66 @@
+import {
+ Badge,
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "@marzneshin/components";
+import { NodeConfigEditor } from "./node-config-editor";
+import { LogContainer } from "../../log";
+import { useTranslation } from "react-i18next";
+import { type NodeType, useBackendStatsQuery } from "../..";
+import type { FC } from "react";
+
+export const NodeBackendSetting: FC<{ node: NodeType; backend: string }> = ({
+ node,
+ backend,
+}) => {
+ const { t } = useTranslation();
+ const nodeBackend = node.backends.find((item) => item.name === backend);
+ const { data: backendStats } = useBackendStatsQuery(node, backend);
+ return (
+ <Card>
+ <CardHeader className="flex flex-row items-center gap-2 w-full">
+ <CardTitle className="capitalize">{backend}</CardTitle>
+ {nodeBackend?.version && (
+ <Badge className="size-fit">v{nodeBackend.version}</Badge>
+ )}
+ {backendStats === null ? (
+ <Badge variant="disabled" className="size-fit">
+ {t("unkown")}
+ </Badge>
+ ) : backendStats.running ? (
+ <Badge variant="positive" className="size-fit">
+ {t("running")}
+ </Badge>
+ ) : (
+ <Badge variant="destructive" className="size-fit">
+ {t("down")}
+ </Badge>
+ )}
+ </CardHeader>
+ <CardContent className="p-2 w-full h-1/2 flex flex-col gap-2">
+ <Tabs className="my-3 w-full h-full" defaultValue="logs">
+ <TabsList className="w-full">
+ <TabsTrigger className="w-full" value="logs">
+ {t("logs")}
+ </TabsTrigger>
+ <TabsTrigger className="w-full" value="config">
+ {t("config")}
+ </TabsTrigger>
+ </TabsList>
+ <TabsContent value="logs" className="h-full w-full">
+ <LogContainer node={node} backend={backend} />
+ </TabsContent>
+ <TabsContent value="config" className="h-full z-51">
+ <NodeConfigEditor entity={node} backend={backend} />
+ </TabsContent>
+ </Tabs>
+ </CardContent>
+ </Card>
+ );
+};
diff --git a/dashboard/src/features/nodes/dialogs/settings/node-config-editor.tsx b/dashboard/src/features/nodes/dialogs/settings/node-config-editor.tsx
@@ -1,41 +1,44 @@
-
-import Editor from '@monaco-editor/react';
+import Editor from "@monaco-editor/react";
import { useTheme } from "@marzneshin/features/theme-switch";
import { useNodesSettings } from "./use-nodes-settings";
-import { Button } from '@marzneshin/components';
-import { useTranslation } from 'react-i18next';
-import { NodeType } from '../..';
-
-export const NodeConfigEditor = ({ entity }: { entity: NodeType }) => {
- const { t } = useTranslation()
- const { theme } = useTheme()
- const {
- payloadValidity,
- data,
- handleConfigSave,
- handleConfigChange,
- handleEditorValidation
- } = useNodesSettings(entity)
+import { Button } from "@marzneshin/components";
+import { useTranslation } from "react-i18next";
+import type { NodeType } from "../..";
- return (<>
- <Editor
- height="50vh"
- className="rounded-sm border"
- defaultLanguage="json"
- theme={theme === "dark" ? "vs-dark" : "github"}
- defaultValue={JSON.stringify(data, null, '\t')}
- onChange={handleConfigChange}
- onValidate={handleEditorValidation}
+export const NodeConfigEditor = ({
+ entity,
+ backend,
+}: { entity: NodeType; backend: string }) => {
+ const { t } = useTranslation();
+ const { theme } = useTheme();
+ const {
+ payloadValidity,
+ language,
+ config,
+ handleConfigSave,
+ handleConfigChange,
+ handleEditorValidation,
+ } = useNodesSettings(entity, backend);
- />
- <Button
- className="w-full"
- variant={!payloadValidity ? "destructive" : "default"}
- onClick={handleConfigSave}
- disabled={!payloadValidity}
- >
- {t('save')}
- </Button>
+ return (
+ <>
+ <Editor
+ height="50vh"
+ className="rounded-sm border"
+ defaultLanguage={language}
+ theme={theme === "dark" ? "vs-dark" : "github"}
+ defaultValue={config}
+ onChange={handleConfigChange}
+ onValidate={handleEditorValidation}
+ />
+ <Button
+ className="w-full"
+ variant={!payloadValidity ? "destructive" : "default"}
+ onClick={handleConfigSave}
+ disabled={!payloadValidity}
+ >
+ {t("save")}
+ </Button>
</>
- )
-}
+ );
+};
diff --git a/dashboard/src/features/nodes/dialogs/settings/use-nodes-settings.ts b/dashboard/src/features/nodes/dialogs/settings/use-nodes-settings.ts
@@ -1,30 +1,61 @@
import { useState } from "react";
-import { NodeType, useNodesSettingsMutation, useNodesSettingsQuery } from "../..";
+import YAML from "yaml";
+import {
+ type NodeType,
+ NodeBackendSettingConfigFormat,
+ useNodesSettingsMutation,
+ useNodesSettingsQuery,
+} from "../..";
-export const useNodesSettings = (entity: NodeType) => {
- const { data } = useNodesSettingsQuery(entity);
- const [config, setConfig] = useState<any>(data)
- const [payloadValidity, setPayloadValidity] = useState<boolean>(true)
- const mutate = useNodesSettingsMutation();
+const parseConfig = (data: {
+ config: string;
+ format: NodeBackendSettingConfigFormat;
+}) => {
+ return {
+ [NodeBackendSettingConfigFormat.PLAIN]: () => data.config,
+ [NodeBackendSettingConfigFormat.JSON]: () =>
+ JSON.stringify(JSON.parse(data.config), null, "\t"),
+ [NodeBackendSettingConfigFormat.YAML]: () =>
+ YAML.stringify(YAML.parse(data.config), null, "\t"),
+ }[data.format]();
+};
- const handleEditorValidation = (markers: any[]) => {
- setPayloadValidity(!markers.length)
- }
+export const useNodesSettings = (entity: NodeType, backend: string) => {
+ const { data } = useNodesSettingsQuery(entity, backend);
+ const [config, setConfig] = useState<string>(data.config);
+ const [payloadValidity, setPayloadValidity] = useState<boolean>(true);
+ const mutate = useNodesSettingsMutation();
+ const language = {
+ [NodeBackendSettingConfigFormat.PLAIN]: "text",
+ [NodeBackendSettingConfigFormat.JSON]: "json",
+ [NodeBackendSettingConfigFormat.YAML]: "yaml",
+ }[data.format];
- const handleConfigSave = () => {
- mutate.mutate({ node: entity, config })
- }
+ const handleEditorValidation = (markers: any[]) => {
+ setPayloadValidity(!markers.length);
+ };
- const handleConfigChange = (newConfig: string | undefined) => {
- if (newConfig) {
- try {
- const parsedConfig = JSON.parse(newConfig);
- setConfig(parsedConfig);
- } catch (error) {
- setPayloadValidity(false)
- }
- }
- };
+ const handleConfigSave = () => {
+ mutate.mutate({ node: entity, backend, config, format: data.format });
+ };
+
+ const handleConfigChange = (newConfig: string | undefined) => {
+ if (newConfig) {
+ try {
+ setConfig(parseConfig({ config: newConfig, format: data.format }));
+ setPayloadValidity(true);
+ } catch (error) {
+ setPayloadValidity(false);
+ }
+ }
+ };
- return { payloadValidity, data, handleConfigSave, handleConfigChange, handleEditorValidation }
-}
+ return {
+ payloadValidity,
+ language,
+ config: parseConfig(data),
+ handleConfigSave,
+ handleConfigChange,
+ handleEditorValidation,
+ };
+};
diff --git a/dashboard/src/features/nodes/log/container.tsx b/dashboard/src/features/nodes/log/container.tsx
@@ -1,57 +1,55 @@
import {
- Badge,
- Card,
- CardContent,
- CardHeader,
- ScrollArea,
- Separator,
+ Badge,
+ Card,
+ CardContent,
+ CardHeader,
+ ScrollArea,
+ Separator,
} from "@marzneshin/components";
import type { NodeType } from "..";
import { useNodesLog } from "..";
-import { cn } from "@marzneshin/utils";
import type { FC } from "react";
import { useTranslation } from "react-i18next";
interface LogContainerProps {
- node: NodeType;
+ node: NodeType;
+ backend: string;
}
-export const LogContainer: FC<LogContainerProps> = ({ node }) => {
- const { logs, status, logsDiv } = useNodesLog(node);
- const { t } = useTranslation();
+export const LogContainer: FC<LogContainerProps> = ({ node, backend }) => {
+ const { logs, status, logsDiv } = useNodesLog(node, backend);
+ const { t } = useTranslation();
- return (
- <Card className="h-full w-full">
- <CardContent className="flex flex-col p-2 h-full w-full">
- <CardHeader className="flex flex-row justify-between items-center p-2">
- <p className="capitalize font-header">
- {t("page.nodes.settings.log-container.title")}
- </p>
- <div className="flex flex-row gap-2 justify-center items-center">
- <Badge
- variant={status === "closed" ? "destructive" : "default"}
- className={cn("flex justify-center items-center w-full h-6 capitalize hover:text-primary-foreground", { "bg-success text-success-foreground": (status !== "closed") })}
- >
- {status}
- </Badge>
- </div>
- </CardHeader>
- <ScrollArea
- className="flex p-2 h-full max-h-[calc(100vh-20rem)] rounded-md bg-secondary text-primary w-full overflow-auto"
- ref={logsDiv}
- >
- {logs.map((log: string, index: number) => (
- <div key={log} className="w-full">
- <p className="font-mono text-[0.7rem] text-primary my-1 break-all rounded-sm p-[1px] hover:bg-gray-300 dark:hover:bg-gray-900">
- {log}
- </p>
- {index !== logs.length - 1 && (
- <Separator className="bg-white dark:bg-gray-700" />
- )}
- </div>
- ))}
- </ScrollArea>
- </CardContent>
- </Card>
- );
+ return (
+ <Card className="h-full w-full">
+ <CardContent className="flex flex-col p-2 h-full w-full">
+ <CardHeader className="flex flex-row justify-between items-center p-2">
+ <p className="capitalize font-header">{`${backend} ${t("logs")}`}</p>
+ <div className="flex flex-row gap-2 justify-center items-center">
+ <Badge
+ className="capitalize"
+ variant={status === "closed" ? "destructive" : "positive"}
+ >
+ {status}
+ </Badge>
+ </div>
+ </CardHeader>
+ <ScrollArea
+ className="flex p-2 h-full max-h-[calc(100vh-20rem)] rounded-md bg-secondary text-primary w-full overflow-auto"
+ ref={logsDiv}
+ >
+ {logs.map((log: string, index: number) => (
+ <div key={log} className="w-full">
+ <p className="font-mono text-[0.7rem] text-primary my-1 break-all rounded-sm p-[1px] hover:bg-gray-300 dark:hover:bg-gray-900">
+ {log}
+ </p>
+ {index !== logs.length - 1 && (
+ <Separator className="bg-white dark:bg-gray-700" />
+ )}
+ </div>
+ ))}
+ </ScrollArea>
+ </CardContent>
+ </Card>
+ );
};
diff --git a/dashboard/src/features/nodes/services/backend-stats.query.ts b/dashboard/src/features/nodes/services/backend-stats.query.ts
@@ -0,0 +1,35 @@
+import type { NodeType } from "@marzneshin/features/nodes";
+import { useQuery } from "@tanstack/react-query";
+import { fetch } from "@marzneshin/utils";
+
+type NodeBackendStatsQueryKey = [string, number, string, string];
+
+interface NodeBackendStatsQuery {
+ queryKey: NodeBackendStatsQueryKey;
+}
+
+export async function fetchNodeBackendStats({
+ queryKey,
+}: NodeBackendStatsQuery): Promise<{
+ running: boolean;
+} | null> {
+ return fetch(`/nodes/${Number(queryKey[1])}/${queryKey[2]}/stats`)
+ .then((stats) => stats)
+ .catch(() => null);
+}
+
+export const useBackendStatsQuery = (node: NodeType, backend: string) => {
+ const queryKey: NodeBackendStatsQueryKey = [
+ "nodes",
+ node.id,
+ backend,
+ "stats",
+ ];
+
+ return useQuery({
+ queryKey,
+ queryFn: fetchNodeBackendStats,
+ refetchInterval: 1000 * 5,
+ initialData: null,
+ });
+};
diff --git a/dashboard/src/features/nodes/services/index.ts b/dashboard/src/features/nodes/services/index.ts
@@ -1,8 +1,9 @@
-export * from './nodes.query'
-export * from './node.query'
-export * from './logs.socket'
-export * from './settings.query'
-export * from './settings.mutate'
-export * from './update-node.mutate'
-export * from './create-node.mutate'
-export * from './delete-node.mutate'
+export * from "./nodes.query";
+export * from "./node.query";
+export * from "./logs.socket";
+export * from "./settings.query";
+export * from "./backend-stats.query";
+export * from "./settings.mutate";
+export * from "./update-node.mutate";
+export * from "./create-node.mutate";
+export * from "./delete-node.mutate";
diff --git a/dashboard/src/features/nodes/services/logs.socket.ts b/dashboard/src/features/nodes/services/logs.socket.ts
@@ -4,75 +4,77 @@ import { useAuth } from "@marzneshin/features/auth";
import { joinPaths } from "@tanstack/react-router";
import { useCallback, useEffect, useRef, useState } from "react";
-export const MAX_NUMBER_OF_LOGS = 500
+export const MAX_NUMBER_OF_LOGS = 500;
export const getStatus = (status: string) => {
- return {
- [ReadyState.CONNECTING]: 'connecting',
- [ReadyState.OPEN]: 'connected',
- [ReadyState.CLOSING]: 'closed',
- [ReadyState.CLOSED]: 'closed',
- [ReadyState.UNINSTANTIATED]: 'closed',
- }[status];
+ return {
+ [ReadyState.CONNECTING]: "connecting",
+ [ReadyState.OPEN]: "connected",
+ [ReadyState.CLOSING]: "closed",
+ [ReadyState.CLOSED]: "closed",
+ [ReadyState.UNINSTANTIATED]: "closed",
+ }[status];
};
-export const getWebsocketUrl = (nodeId: number) => {
- try {
- const baseURL = new URL(
- import.meta.env.VITE_BASE_API.startsWith('/')
- ? window.location.origin + import.meta.env.VITE_BASE_API
- : import.meta.env.VITE_BASE_API
- );
-
- return (
- (baseURL.protocol === 'https:' ? 'wss://' : 'ws://') +
- joinPaths([baseURL.host + baseURL.pathname, `/nodes/${nodeId}/logs`]) +
- '?interval=1&token=' +
- useAuth.getState().getAuthToken()
- );
- } catch (e) {
- console.error('Unable to generate websocket url');
- console.error(e);
- return null;
- }
+export const getWebsocketUrl = (nodeId: number, backend: string) => {
+ try {
+ const baseURL = new URL(
+ import.meta.env.VITE_BASE_API.startsWith("/")
+ ? window.location.origin + import.meta.env.VITE_BASE_API
+ : import.meta.env.VITE_BASE_API,
+ );
+ const protocol = baseURL.protocol === "https:" ? "wss://" : "ws://";
+ return `${protocol}${joinPaths([baseURL.host + baseURL.pathname, `/nodes/${nodeId}/${backend}/logs`])}?interval=1&token=${useAuth.getState().getAuthToken()}`;
+ } catch (e) {
+ console.error("Unable to generate websocket url");
+ console.error(e);
+ return null;
+ }
};
+export const useNodesLog = (node: NodeType, backend: string) => {
+ const [logs, setLogs] = useState<string[]>([]);
+ const logsDiv = useRef<HTMLDivElement | null>(null);
+ const scrollShouldStayOnEnd = useRef(true);
-export const useNodesLog = (node: NodeType) => {
- const [logs, setLogs] = useState<string[]>([]);
- const logsDiv = useRef<HTMLDivElement | null>(null);
- const scrollShouldStayOnEnd = useRef(true);
+ const updateLogs = useCallback(
+ (callback: (prevLogs: string[]) => string[]) => {
+ setLogs((prevLogs) => {
+ const newLogs = callback(prevLogs);
+ return newLogs.length > MAX_NUMBER_OF_LOGS
+ ? newLogs.slice(-MAX_NUMBER_OF_LOGS)
+ : newLogs;
+ });
+ },
+ [],
+ );
- const updateLogs = useCallback((callback: (prevLogs: string[]) => string[]) => {
- setLogs(prevLogs => {
- const newLogs = callback(prevLogs);
- return newLogs.length > MAX_NUMBER_OF_LOGS ? newLogs.slice(-MAX_NUMBER_OF_LOGS) : newLogs;
- });
- }, []);
+ const { readyState } = useWebSocket(
+ node?.id ? getWebsocketUrl(node.id, backend) : "",
+ {
+ onMessage: (e: any) => {
+ updateLogs((prevLogs) => [...prevLogs, e.data]);
+ },
+ shouldReconnect: () => true,
+ reconnectAttempts: 10,
+ reconnectInterval: 1000,
+ },
+ );
- const { readyState } = useWebSocket(node?.id ? getWebsocketUrl(node.id) : '', {
- onMessage: (e: any) => {
- updateLogs(prevLogs => [...prevLogs, e.data]);
- },
- shouldReconnect: () => true,
- reconnectAttempts: 10,
- reconnectInterval: 1000,
- });
-
- useEffect(() => {
- if (logsDiv.current && scrollShouldStayOnEnd.current) {
- logsDiv.current.scrollTop = logsDiv.current.scrollHeight;
- }
- }, [logs]);
+ useEffect(() => {
+ if (logsDiv.current && scrollShouldStayOnEnd.current) {
+ logsDiv.current.scrollTop = logsDiv.current.scrollHeight;
+ }
+ }, [logs]);
- useEffect(() => {
- return () => {
- // Cleanup
- setLogs([]);
- };
- }, []);
+ useEffect(() => {
+ return () => {
+ // Cleanup
+ setLogs([]);
+ };
+ }, []);
- const status = getStatus(readyState.toString());
+ const status = getStatus(readyState.toString());
- return { status, readyState, logs, logsDiv };
+ return { status, readyState, logs, logsDiv };
};
diff --git a/dashboard/src/features/nodes/services/nodes.query.ts b/dashboard/src/features/nodes/services/nodes.query.ts
@@ -1,13 +1,17 @@
-import { NodeType } from "@marzneshin/features/nodes";
+import type { NodeType } from "@marzneshin/features/nodes";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@marzneshin/utils";
-import {
+import type {
EntityQueryKeyType,
- UseEntityQueryProps
+ UseEntityQueryProps,
} from "@marzneshin/features/entity-table";
-export async function fetchNodes({ queryKey }: EntityQueryKeyType): Promise<{ entity: NodeType[], pageCount: number }> {
- return fetch(`/nodes?page=${queryKey[1]}&size=${queryKey[2]}&name=${queryKey[3]}`).then((result) => {
+export async function fetchNodes({
+ queryKey,
+}: EntityQueryKeyType): Promise<{ entity: NodeType[]; pageCount: number }> {
+ return fetch(
+ `/nodes?page=${queryKey[1]}&size=${queryKey[2]}&name=${queryKey[3]}`,
+ ).then((result) => {
return {
entity: result.items,
pageCount: result.pages,
@@ -17,11 +21,10 @@ export async function fetchNodes({ queryKey }: EntityQueryKeyType): Promise<{ en
export const NodesQueryFetchKey = "nodes";
-
export const useNodesQuery = ({ page, size, search }: UseEntityQueryProps) => {
return useQuery({
queryKey: [NodesQueryFetchKey, page, size, search ? search : ""],
queryFn: fetchNodes,
initialData: { entity: [], pageCount: 0 },
- })
-}
+ });
+};
diff --git a/dashboard/src/features/nodes/services/settings.mutate.ts b/dashboard/src/features/nodes/services/settings.mutate.ts
@@ -1,40 +1,48 @@
-import { NodeType } from "@marzneshin/features/nodes";
+import type { NodeType } from "@marzneshin/features/nodes";
import { useMutation } from "@tanstack/react-query";
import { fetch, queryClient } from "@marzneshin/utils";
import { toast } from "sonner";
import i18n from "@marzneshin/features/i18n";
-import { NodesSettingsQueryFetchKey } from "./settings.query";
+import {
+ type NodeBackendSettingConfigFormat,
+ NodesSettingsQueryFetchKey,
+} from "./settings.query";
interface UpdateNodesSettings {
- node: NodeType
- config: string
+ node: NodeType;
+ backend: string;
+ config: string;
+ format: NodeBackendSettingConfigFormat;
}
-export async function fetchUpdateNodesSettings({ node, config }: UpdateNodesSettings): Promise<NodeType> {
- return fetch(`/nodes/${node.id}/xray_config`, { method: 'put', body: config }).then((node) => {
+export async function fetchUpdateNodesSettings({
+ node,
+ backend,
+ config,
+ format,
+}: UpdateNodesSettings): Promise<NodeType> {
+ return fetch(`/nodes/${node.id}/${backend}/config`, {
+ method: "put",
+ body: { config, format },
+ }).then((node) => {
return node;
});
}
const handleError = (error: Error, value: UpdateNodesSettings) => {
- toast.error(
- i18n.t('events.update.error', { name: value.node.name }),
- {
- description: error.message
- })
-}
+ toast.error(i18n.t("events.update.error", { name: value.node.name }), {
+ description: error.message,
+ });
+};
const handleSuccess = (value: NodeType) => {
- toast.success(
- i18n.t('events.update.success.title', { name: value.name }),
- {
- description: i18n.t('events.update.success.desc')
- })
- queryClient.invalidateQueries({ queryKey: [NodesSettingsQueryFetchKey] })
-}
-
+ toast.success(i18n.t("events.update.success.title", { name: value.name }), {
+ description: i18n.t("events.update.success.desc"),
+ });
+ queryClient.invalidateQueries({ queryKey: [NodesSettingsQueryFetchKey] });
+};
-const NodesSettingsUpdateFetchKey = ["nodes", "xray_config"];
+const NodesSettingsUpdateFetchKey = ["nodes", "config"];
export const useNodesSettingsMutation = () => {
return useMutation({
@@ -42,5 +50,5 @@ export const useNodesSettingsMutation = () => {
mutationFn: fetchUpdateNodesSettings,
onError: handleError,
onSuccess: handleSuccess,
- })
-}
+ });
+};
diff --git a/dashboard/src/features/nodes/services/settings.query.ts b/dashboard/src/features/nodes/services/settings.query.ts
@@ -1,29 +1,45 @@
-import { NodeType } from "@marzneshin/features/nodes";
+import type { NodeType } from "@marzneshin/features/nodes";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@marzneshin/utils";
-type NodesSettingsQueryKey = [string, number | null, string]
+type NodesSettingsQueryKey = [string, number, string, string];
-export async function fetchNodesSettings({ queryKey }: { queryKey: NodesSettingsQueryKey }): Promise<NodeType[]> {
- return fetch(`/nodes/${Number(queryKey[1])}/xray_config`).then((config) => {
- return config;
- });
+export enum NodeBackendSettingConfigFormat {
+ PLAIN = 0,
+ JSON = 1,
+ YAML = 2,
}
-export const NodesSettingsQueryFetchKey = "nodes";
+interface NodesSettingsQuery {
+ queryKey: NodesSettingsQueryKey;
+}
-export const useNodesSettingsQuery = (node: NodeType | null) => {
- const nodeId: number | null = node?.id ?? null
- const queryKey: NodesSettingsQueryKey =
- [NodesSettingsQueryFetchKey, nodeId, "xray_config"];
+export async function fetchNodesSettings({
+ queryKey,
+}: NodesSettingsQuery): Promise<{
+ config: string;
+ format: NodeBackendSettingConfigFormat;
+}> {
+ return fetch(`/nodes/${Number(queryKey[1])}/${queryKey[2]}/config`).then(
+ (config) => {
+ return config;
+ },
+ );
+}
+
+export const NodesSettingsQueryFetchKey = "nodes";
- const queryFn = nodeId !== null
- ? ({ queryKey }: { queryKey: [string, number | null, string] }) => fetchNodesSettings({ queryKey })
- : () => Promise.resolve<NodeType[]>([]);
+export const useNodesSettingsQuery = (node: NodeType, backend: string) => {
+ const queryKey: NodesSettingsQueryKey = [
+ NodesSettingsQueryFetchKey,
+ node.id,
+ backend,
+ "config",
+ ];
return useQuery({
queryKey,
- queryFn,
- initialData: [],
+ queryFn: fetchNodesSettings,
+ initialData: { config: "", format: NodeBackendSettingConfigFormat.PLAIN },
});
};
diff --git a/dashboard/src/features/nodes/types/index.ts b/dashboard/src/features/nodes/types/index.ts
@@ -3,43 +3,53 @@ import { PowerOff, Zap, ZapOff } from "lucide-react";
import { z } from "zod";
export const NodesStatus = {
- healthy: {
- label: "healthy",
- icon: Zap,
- },
- unhealthy: {
- label: "unhealthy",
- icon: ZapOff,
- },
- disabled: {
- label: "disabled",
- icon: PowerOff,
- },
- none: {
- label: "none",
- icon: null,
- },
+ healthy: {
+ label: "healthy",
+ icon: Zap,
+ },
+ unhealthy: {
+ label: "unhealthy",
+ icon: ZapOff,
+ },
+ disabled: {
+ label: "disabled",
+ icon: PowerOff,
+ },
+ none: {
+ label: "none",
+ icon: null,
+ },
} as Record<string, StatusType>;
export const NodeSchema = z.object({
- name: z.string().min(1),
- address: z.string().min(1),
- port: z
- .number()
- .min(1)
- .or(z.string().transform((v) => Number.parseFloat(v))),
- id: z.number().nullable().optional(),
- status: z.enum([
- NodesStatus.healthy.label,
- NodesStatus.unhealthy.label,
- "none",
- NodesStatus.disabled.label,
- ]),
- usage_coefficient: z
- .number()
- .default(1.0)
- .or(z.string().transform((v) => Number.parseFloat(v))),
- connection_backend: z.enum(["grpclib", "grpcio"]).default("grpclib"),
+ name: z.string().min(1),
+ address: z.string().min(1),
+ port: z
+ .number()
+ .min(1)
+ .or(z.string().transform((v) => Number.parseFloat(v))),
+ id: z.number().nullable().optional(),
+ status: z.enum([
+ NodesStatus.healthy.label,
+ NodesStatus.unhealthy.label,
+ "none",
+ NodesStatus.disabled.label,
+ ]),
+ usage_coefficient: z
+ .number()
+ .default(1.0)
+ .or(z.string().transform((v) => Number.parseFloat(v))),
+ connection_backend: z.enum(["grpclib", "grpcio"]).default("grpclib"),
});
-export type NodeType = z.infer<typeof NodeSchema> & { id: number };
+export type NodeBackendType = {
+ name: string;
+ backend_type: string;
+ version: string;
+ running: boolean;
+};
+
+export type NodeType = z.infer<typeof NodeSchema> & {
+ id: number;
+ backends: NodeBackendType[];
+};
diff --git a/dashboard/src/features/services/components/dialogs/settings.dialog.tsx b/dashboard/src/features/services/components/dialogs/settings.dialog.tsx
@@ -1,47 +1,50 @@
import {
- SettingsDialogProps,
- SettingsDialog,
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger,
-} from "@marzneshin/components"
-import { type FC, useEffect } from "react"
-import { useTranslation } from "react-i18next"
+ SettingsDialogProps,
+ SettingsDialog,
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "@marzneshin/components";
+import { type FC } from "react";
+import { useTranslation } from "react-i18next";
import {
- ServicesUsersTable,
- ServiceInboundsTable,
- ServiceType
+ ServicesUsersTable,
+ ServiceInboundsTable,
+ ServiceType,
} from "@marzneshin/features/services";
interface ServicecSettingsDialogProps extends SettingsDialogProps {
- entity: ServiceType
- onClose: () => void;
+ entity: ServiceType;
+ onClose: () => void;
}
export const ServiceSettingsDialog: FC<ServicecSettingsDialogProps> = ({
- onOpenChange, open, entity, onClose
+ onOpenChange,
+ open,
+ entity,
+ onClose,
}) => {
- const { t } = useTranslation();
+ const { t } = useTranslation();
- useEffect(() => {
- if (!open) onClose();
- }, [open, onClose]);
-
- return (
- <SettingsDialog open={open} onOpenChange={onOpenChange}>
- <Tabs className="my-3 w-full h-full" defaultValue="inbounds">
- <TabsList className="w-full">
- <TabsTrigger className="w-full" value="inbounds">{t('inbounds')}</TabsTrigger>
- <TabsTrigger className="w-full" value="users">{t('users')}</TabsTrigger>
- </TabsList>
- <TabsContent value="inbounds" className="h-full">
- <ServiceInboundsTable service={entity} />
- </TabsContent>
- <TabsContent value="users" className="h-full">
- <ServicesUsersTable service={entity} />
- </TabsContent>
- </Tabs>
- </SettingsDialog>
- )
-}
+ return (
+ <SettingsDialog open={open} onClose={onClose} onOpenChange={onOpenChange}>
+ <Tabs className="my-3 w-full h-full" defaultValue="inbounds">
+ <TabsList className="w-full">
+ <TabsTrigger className="w-full" value="inbounds">
+ {t("inbounds")}
+ </TabsTrigger>
+ <TabsTrigger className="w-full" value="users">
+ {t("users")}
+ </TabsTrigger>
+ </TabsList>
+ <TabsContent value="inbounds" className="h-full">
+ <ServiceInboundsTable service={entity} />
+ </TabsContent>
+ <TabsContent value="users" className="h-full">
+ <ServicesUsersTable service={entity} />
+ </TabsContent>
+ </Tabs>
+ </SettingsDialog>
+ );
+};
diff --git a/dashboard/src/features/users/components/dialogs/settings/index.tsx b/dashboard/src/features/users/components/dialogs/settings/index.tsx
@@ -1,91 +1,94 @@
import {
- Button,
- ScrollArea,
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger,
- Awaiting,
- SettingsInfoSkeleton,
- SettingsDialogProps,
- SettingsDialog,
+ Button,
+ ScrollArea,
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+ Awaiting,
+ SettingsInfoSkeleton,
+ SettingsDialogProps,
+ SettingsDialog,
} from "@marzneshin/components";
import {
- UserServicesTable,
- useUserUsageResetCmd,
- type UserType
+ UserServicesTable,
+ useUserUsageResetCmd,
+ type UserType,
} from "@marzneshin/features/users";
-import { type FC, useEffect } from "react";
+import { type FC } from "react";
import { useTranslation } from "react-i18next";
import {
- UserStatusEnableButton,
- SubscriptionActions,
- QRCodeSection,
- UserInfoTable
+ UserStatusEnableButton,
+ SubscriptionActions,
+ QRCodeSection,
+ UserInfoTable,
} from "./user-info";
interface UsersSettingsDialogProps extends SettingsDialogProps {
- entity: UserType | null;
- onClose: () => void;
- isPending: boolean;
+ entity: UserType | null;
+ onClose: () => void;
+ isPending: boolean;
}
export const UsersSettingsDialog: FC<UsersSettingsDialogProps> = ({
- onOpenChange,
- open,
- entity,
- onClose,
- isPending,
+ onOpenChange,
+ open,
+ entity,
+ onClose,
+ isPending,
}) => {
- const { t } = useTranslation();
- const { mutate: resetUsage } = useUserUsageResetCmd();
+ const { t } = useTranslation();
+ const { mutate: resetUsage } = useUserUsageResetCmd();
- useEffect(() => {
- if (!open) onClose();
- }, [open, onClose]);
-
- return (
- <SettingsDialog open={open} onOpenChange={onOpenChange}>
- <Awaiting
- Component={entity ? (
- <ScrollArea className="flex flex-col gap-4 h-full">
- <Tabs defaultValue="info" className="w-full h-full">
- <TabsList className="w-full bg-accent">
- <TabsTrigger className="w-full" value="info">
- {t("user_info")}
- </TabsTrigger>
- <TabsTrigger className="w-full" value="services">
- {t("services")}
- </TabsTrigger>
- <TabsTrigger className="w-full" value="subscription">
- {t("subscription")}
- </TabsTrigger>
- </TabsList>
- <TabsContent
- value="info"
- className="flex flex-col gap-2 w-full h-full"
- >
- <UserInfoTable user={entity} />
- <div className="hstack justify-center items-center gap-2">
- <Button className="w-1/2" onClick={() => resetUsage(entity)}>
- {t("page.users.reset_usage")}
- </Button>
- <UserStatusEnableButton user={entity} />
- </div>
- </TabsContent>
- <TabsContent value="services">
- <UserServicesTable user={entity} />
- </TabsContent>
- <TabsContent value="subscription">
- <QRCodeSection entity={entity} />
- <SubscriptionActions entity={entity} />
- </TabsContent>
- </Tabs>
- </ScrollArea>
- ) : (<div>Not Found</div>)}
- Skeleton={<SettingsInfoSkeleton />}
- isFetching={isPending}
- />
- </SettingsDialog>
- );
+ return (
+ <SettingsDialog open={open} onClose={onClose} onOpenChange={onOpenChange}>
+ <Awaiting
+ Component={
+ entity ? (
+ <ScrollArea className="flex flex-col gap-4 h-full">
+ <Tabs defaultValue="info" className="w-full h-full">
+ <TabsList className="w-full bg-accent">
+ <TabsTrigger className="w-full" value="info">
+ {t("user_info")}
+ </TabsTrigger>
+ <TabsTrigger className="w-full" value="services">
+ {t("services")}
+ </TabsTrigger>
+ <TabsTrigger className="w-full" value="subscription">
+ {t("subscription")}
+ </TabsTrigger>
+ </TabsList>
+ <TabsContent
+ value="info"
+ className="flex flex-col gap-2 w-full h-full"
+ >
+ <UserInfoTable user={entity} />
+ <div className="hstack justify-center items-center gap-2">
+ <Button
+ className="w-1/2"
+ onClick={() => resetUsage(entity)}
+ >
+ {t("page.users.reset_usage")}
+ </Button>
+ <UserStatusEnableButton user={entity} />
+ </div>
+ </TabsContent>
+ <TabsContent value="services">
+ <UserServicesTable user={entity} />
+ </TabsContent>
+ <TabsContent value="subscription">
+ <QRCodeSection entity={entity} />
+ <SubscriptionActions entity={entity} />
+ </TabsContent>
+ </Tabs>
+ </ScrollArea>
+ ) : (
+ <div>Not Found</div>
+ )
+ }
+ Skeleton={<SettingsInfoSkeleton />}
+ isFetching={isPending}
+ />
+ </SettingsDialog>
+ );
};
diff --git a/package.json b/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "yaml": "^2.5.0"
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
@@ -0,0 +1,24 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ yaml:
+ specifier: ^2.5.0
+ version: 2.5.0
+
+packages:
+
+ yaml@2.5.0:
+ resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==}
+ engines: {node: '>= 14'}
+ hasBin: true
+
+snapshots:
+
+ yaml@2.5.0: {}
diff --git a/requirements.txt b/requirements.txt
@@ -21,7 +21,7 @@ websockets==12.0
PyYAML==6.0.1
aiohttp==3.9.4
xxhash==3.4.1
-grpcio==1.60.0
+grpcio==1.65.2
psycopg==3.1.18
v2share==0.1.0b12
PyJWT~=2.8.0
\ No newline at end of file