diff --git a/app/db/crud.py b/app/db/crud.py index 963a3bd..c35acf2 100644 --- a/app/db/crud.py +++ b/app/db/crud.py @@ -6,7 +6,7 @@ from enum import Enum from types import NoneType from typing import List, Optional, Tuple, Union -from sqlalchemy import and_, delete, update, select +from sqlalchemy import and_, delete, update, select, func from sqlalchemy.orm import Session from app.db.models import ( @@ -33,6 +33,7 @@ from app.models.node import ( ) from app.models.proxy import InboundHost as InboundHostModify from app.models.service import Service as ServiceModify, ServiceCreate +from app.models.system import TrafficUsageSeries from app.models.user import ( ReminderType, UserCreate, @@ -311,13 +312,57 @@ def get_users( return query.all() +def get_total_usages( + db: Session, admin: Admin, start: datetime, end: datetime +): + usages = defaultdict(int) + + query = ( + db.query( + NodeUserUsage.created_at, func.sum(NodeUserUsage.used_traffic) + ) + .group_by(NodeUserUsage.created_at) + .filter( + and_( + NodeUserUsage.created_at >= start, + NodeUserUsage.created_at <= end, + ) + ) + ) + + if not admin.is_sudo: + query = ( + query.filter( + Admin.id == admin.id, + ) + .join(User, NodeUserUsage.user_id == User.id) + .join(Admin, User.admin_id == Admin.id) + ) + for created_at, used_traffic in query.all(): + usages[created_at.replace(tzinfo=timezone.utc).timestamp()] += int( + used_traffic + ) + + result = TrafficUsageSeries(usages=[]) + current = start.astimezone(timezone.utc).replace( + minute=0, second=0, microsecond=0 + ) + + while current <= end.replace(tzinfo=timezone.utc): + result.usages.append( + (int(current.timestamp()), usages.get(current.timestamp()) or 0) + ) + current += timedelta(hours=1) + + return result + + def get_user_usages( db: Session, db_user: User, start: datetime, end: datetime, ) -> UserUsageSeriesResponse: - start = start.replace(minute=0, second=0, microsecond=0) cond = and_( NodeUserUsage.user_id == db_user.id, NodeUserUsage.created_at >= start, diff --git a/app/models/system.py b/app/models/system.py index 68cf653..e137bc1 100644 --- a/app/models/system.py +++ b/app/models/system.py @@ -27,7 +27,6 @@ class UsersStats(BaseModel): expired: int limited: int online: int - recent_subscription_updates: list[str] class AdminsStats(BaseModel): @@ -38,3 +37,7 @@ class NodesStats(BaseModel): total: int healthy: int unhealthy: int + + +class TrafficUsageSeries(BaseModel): + usages: list[tuple[int, int]] diff --git a/app/models/user.py b/app/models/user.py index 60a8048..7f48f2c 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -146,11 +146,6 @@ class UserResponse(User): model_config = ConfigDict(from_attributes=True) -class UserUsage(BaseModel): - used_traffic: int - usage_date: datetime - - class UserNodeUsageSeries(BaseModel): node_id: int | None = None node_name: str diff --git a/app/routes/system.py b/app/routes/system.py index 81d2cb1..134efbd 100644 --- a/app/routes/system.py +++ b/app/routes/system.py @@ -1,12 +1,23 @@ from fastapi import APIRouter from app.db import crud -from app.db.models import Admin as DBAdmin, User, Settings +from app.db.models import Admin as DBAdmin, Settings from app.db.models import Node -from app.dependencies import DBDep, AdminDep, SudoAdminDep +from app.dependencies import ( + DBDep, + AdminDep, + SudoAdminDep, + EndDateDep, + StartDateDep, +) from app.models.node import NodeStatus from app.models.settings import SubscriptionSettings, TelegramSettings -from app.models.system import UsersStats, NodesStats, AdminsStats +from app.models.system import ( + UsersStats, + NodesStats, + AdminsStats, + TrafficUsageSeries, +) from app.models.user import UserExpireStrategy router = APIRouter(tags=["System"], prefix="/system") @@ -60,6 +71,13 @@ def get_nodes_stats(db: DBDep, admin: SudoAdminDep): ) +@router.get("/stats/traffic", response_model=TrafficUsageSeries) +def get_total_traffic_stats( + db: DBDep, admin: AdminDep, start_date: StartDateDep, end_date: EndDateDep +): + return crud.get_total_usages(db, admin, start_date, end_date) + + @router.get("/stats/users", response_model=UsersStats) def get_users_stats(db: DBDep, admin: AdminDep): return UsersStats( @@ -87,11 +105,4 @@ def get_users_stats(db: DBDep, admin: AdminDep): online=crud.get_users_count( db, admin=admin if not admin.is_sudo else None, online=True ), - recent_subscription_updates=list( - i[0] - for i in db.query(User.username) - .filter(User.sub_updated_at != None) - .order_by(User.sub_updated_at) - .limit(5) - ), ) diff --git a/dashboard/public/locales/en.json b/dashboard/public/locales/en.json index b9e344a..b365d3b 100644 --- a/dashboard/public/locales/en.json +++ b/dashboard/public/locales/en.json @@ -81,6 +81,15 @@ "donate": "Donate" }, "page": { + "home": { + "users-stats": { + "desc": "Overview of user's statuses" + }, + "total-traffics": { + "title": "Total System Traffic", + "desc": "Traffics combined from all nodes" + } + }, "settings": { "subscription-settings": { "template-on-acceptance": "Template on Acceptance", diff --git a/dashboard/src/common/stats-charts/components/index.ts b/dashboard/src/common/stats-charts/components/index.ts new file mode 100644 index 0000000..fd69b24 --- /dev/null +++ b/dashboard/src/common/stats-charts/components/index.ts @@ -0,0 +1 @@ +export * from "./select-date-view"; diff --git a/dashboard/src/modules/users/components/widgets/user-nodes-usage-widget/select-date-view.tsx b/dashboard/src/common/stats-charts/components/select-date-view.tsx similarity index 100% rename from dashboard/src/modules/users/components/widgets/user-nodes-usage-widget/select-date-view.tsx rename to dashboard/src/common/stats-charts/components/select-date-view.tsx diff --git a/dashboard/src/common/stats-charts/hooks/index.ts b/dashboard/src/common/stats-charts/hooks/index.ts new file mode 100644 index 0000000..2aac369 --- /dev/null +++ b/dashboard/src/common/stats-charts/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-from-now-interval" diff --git a/dashboard/src/common/stats-charts/hooks/use-from-now-interval.ts b/dashboard/src/common/stats-charts/hooks/use-from-now-interval.ts new file mode 100644 index 0000000..3338dd0 --- /dev/null +++ b/dashboard/src/common/stats-charts/hooks/use-from-now-interval.ts @@ -0,0 +1,13 @@ +import { ChartDateInterval } from "../types"; + +export function useFromNowInterval(interval: ChartDateInterval) { + const start = new Date(); + const end = new Date(); + start.setDate(end.getDate() - parseInt(interval.replace("d", ""))); + end.setMinutes(0, 0, 0); + start.setMinutes(0, 0, 0); + return { + start: start.toISOString(), + end: end.toISOString() + } +} diff --git a/dashboard/src/common/stats-charts/index.ts b/dashboard/src/common/stats-charts/index.ts new file mode 100644 index 0000000..ea10b00 --- /dev/null +++ b/dashboard/src/common/stats-charts/index.ts @@ -0,0 +1,4 @@ +export * from "./components" +export * from "./hooks" +export * from "./utils" +export * from "./types" diff --git a/dashboard/src/common/stats-charts/types.ts b/dashboard/src/common/stats-charts/types.ts new file mode 100644 index 0000000..481900c --- /dev/null +++ b/dashboard/src/common/stats-charts/types.ts @@ -0,0 +1,2 @@ + +export type ChartDateInterval = '90d' | '30d' | '7d' | '1d'; diff --git a/dashboard/src/common/stats-charts/utils/date-x-axies.ts b/dashboard/src/common/stats-charts/utils/date-x-axies.ts new file mode 100644 index 0000000..b530992 --- /dev/null +++ b/dashboard/src/common/stats-charts/utils/date-x-axies.ts @@ -0,0 +1,28 @@ +import { ChartDateInterval } from "../types"; + +export const dateXAxisTicks = (value: string, timeRange: ChartDateInterval) => { + const date = new Date(value); + const format = { + "90d": { + day: "numeric", + month: "short", + }, + "30d": { + day: "numeric", + }, + "7d": { + day: "numeric", + hour: "numeric", + }, + "1d": { + minute: "numeric", + hour: "numeric", + } + }[timeRange as ChartDateInterval]; + + if (timeRange === "1d") { + return date.toLocaleTimeString("en-US", format as Intl.DateTimeFormatOptions); + } + + return date.toLocaleDateString("en-US", format as Intl.DateTimeFormatOptions); +} diff --git a/dashboard/src/common/stats-charts/utils/index.ts b/dashboard/src/common/stats-charts/utils/index.ts new file mode 100644 index 0000000..2617f36 --- /dev/null +++ b/dashboard/src/common/stats-charts/utils/index.ts @@ -0,0 +1 @@ +export * from "./date-x-axies" diff --git a/dashboard/src/components/page/index.tsx b/dashboard/src/components/page/index.tsx index 48eae7e..5bba518 100644 --- a/dashboard/src/components/page/index.tsx +++ b/dashboard/src/components/page/index.tsx @@ -1,13 +1,41 @@ -import { FC, PropsWithChildren } from 'react' -import { ScrollArea } from '@marzneshin/components' +import { FC, PropsWithChildren, HTMLAttributes } from 'react' +import { + Card, + CardContent, + ScrollArea, + CardFooter, + CardHeader, + CardTitle, +} from '@marzneshin/components'; +import { cn } from '@marzneshin/utils'; -interface PageProps extends PropsWithChildren { } +interface PageProps { + title: JSX.Element | string; + content?: JSX.Element | string; + footer?: JSX.Element | string; +} -export const Page: FC = ({ children }) => { +export const Page: FC> = ({ + footer, + content, + children, + title, + className +}) => { return ( - +
- {children} + + + + {title} + + + + {content || children} + + {footer && {footer} } +
); diff --git a/dashboard/src/components/widgets/section-widget/index.tsx b/dashboard/src/components/widgets/section-widget/index.tsx index cbb185f..6fd8092 100644 --- a/dashboard/src/components/widgets/section-widget/index.tsx +++ b/dashboard/src/components/widgets/section-widget/index.tsx @@ -9,19 +9,20 @@ import { } from '@marzneshin/components'; import type { FC, PropsWithChildren } from 'react'; -export interface SectionWidgetProps extends PropsWithChildren { - title: JSX.Element | string; - description: JSX.Element | string; - content?: JSX.Element | string; - footer?: JSX.Element | string; - options?: JSX.Element | string; +export interface SectionWidgetProps { + title: any; + description: any; + content?: any; + footer?: any; + options?: any; + className?: string; } -export const SectionWidget: FC = ({ - options, footer, content, children, title, description +export const SectionWidget: FC = ({ + options, footer, content, children, title, description, className }) => { return ( - +
diff --git a/dashboard/src/features/entity-table/entity-table.tsx b/dashboard/src/features/entity-table/entity-table.tsx index 1336f69..2c47cfc 100644 --- a/dashboard/src/features/entity-table/entity-table.tsx +++ b/dashboard/src/features/entity-table/entity-table.tsx @@ -83,7 +83,7 @@ export function EntityTable({ return ( -
+
diff --git a/dashboard/src/features/total-traffic-widget/api/index.ts b/dashboard/src/features/total-traffic-widget/api/index.ts new file mode 100644 index 0000000..418cbe5 --- /dev/null +++ b/dashboard/src/features/total-traffic-widget/api/index.ts @@ -0,0 +1 @@ +export * from "./total-traffic.query"; diff --git a/dashboard/src/features/total-traffic-widget/api/total-traffic.query.ts b/dashboard/src/features/total-traffic-widget/api/total-traffic.query.ts new file mode 100644 index 0000000..f8fd0bd --- /dev/null +++ b/dashboard/src/features/total-traffic-widget/api/total-traffic.query.ts @@ -0,0 +1,38 @@ +import { useQuery } from "@tanstack/react-query"; +import { fetch } from "@marzneshin/utils"; + +export type UsageMetric = number[]; + +export interface TotalTrafficsResponse { + usages: Array; +} + +export interface TotalTrafficsQueryOptions { + start: string; + end: string +} + +export type TotalTrafficsQueryKey = [string, string, string, { start?: string, end?: string }] + +export const TotalTrafficsDefault = { + usages: [] +} + +export async function fetchTotalTraffics({ queryKey }: { queryKey: TotalTrafficsQueryKey }): Promise { + return await fetch(`/system/stats/traffic`, { + query: { + start: queryKey[3].start, + end: queryKey[3].end + } + }).then((result) => { + return result; + }); +} +export const useTotalTrafficQuery = ({ start, end }: TotalTrafficsQueryOptions) => { + return useQuery({ + queryKey: ["system", "stats", "traffic", { start, end }], + queryFn: fetchTotalTraffics, + refetchInterval: 1000 * 60 * 5, // 60min refresh + initialData: TotalTrafficsDefault + }) +} diff --git a/dashboard/src/features/total-traffic-widget/components/index.ts b/dashboard/src/features/total-traffic-widget/components/index.ts new file mode 100644 index 0000000..d889ad7 --- /dev/null +++ b/dashboard/src/features/total-traffic-widget/components/index.ts @@ -0,0 +1 @@ +export * from "./skeleton" diff --git a/dashboard/src/features/total-traffic-widget/components/skeleton.tsx b/dashboard/src/features/total-traffic-widget/components/skeleton.tsx new file mode 100644 index 0000000..e2505cb --- /dev/null +++ b/dashboard/src/features/total-traffic-widget/components/skeleton.tsx @@ -0,0 +1,18 @@ +import { + Skeleton, +} from "@marzneshin/components"; +export function UsageGraphSkeleton() { + return ( +
+
+ +
+
+ + + + +
+
+ ) +} diff --git a/dashboard/src/features/total-traffic-widget/hooks/index.ts b/dashboard/src/features/total-traffic-widget/hooks/index.ts new file mode 100644 index 0000000..d8f6e9a --- /dev/null +++ b/dashboard/src/features/total-traffic-widget/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-transform-data" diff --git a/dashboard/src/features/total-traffic-widget/hooks/use-transform-data.ts b/dashboard/src/features/total-traffic-widget/hooks/use-transform-data.ts new file mode 100644 index 0000000..59c4afb --- /dev/null +++ b/dashboard/src/features/total-traffic-widget/hooks/use-transform-data.ts @@ -0,0 +1,17 @@ +import { UsageMetric } from "../api"; + +export type ChartDataEntry = { + datetime: string; + [key: string]: number | string; +}; + +export type ChartData = ChartDataEntry[]; + +export function useTransformData(usages: Array): ChartData { + return usages.map(usageData => { + const [timestamp, usage] = usageData; + const date = new Date(timestamp * 1000); + const formattedDate = date.toISOString(); + return { datetime: formattedDate, traffic: usage }; + }); +} diff --git a/dashboard/src/features/total-traffic-widget/index.tsx b/dashboard/src/features/total-traffic-widget/index.tsx new file mode 100644 index 0000000..c9ed7d1 --- /dev/null +++ b/dashboard/src/features/total-traffic-widget/index.tsx @@ -0,0 +1,107 @@ +import { useState, FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + SectionWidget, + ChartContainer, + Awaiting, + ChartTooltip, + ChartTooltipContent, + ChartConfig, +} from "@marzneshin/components"; +import { XAxis, BarChart, YAxis, CartesianGrid, Bar } from "recharts" +import { useTransformData } from "./hooks"; +import { format as formatByte } from '@chbphone55/pretty-bytes'; +import { useTotalTrafficQuery } from "./api"; +import { UsageGraphSkeleton } from "./components"; +import { + ChartDateInterval, + SelectDateView, + dateXAxisTicks, + useFromNowInterval +} from "@marzneshin/common/stats-charts"; + +const chartConfig = { + traffic: { + label: "Traffic", + color: "hsl(var(--chart-1))", + }, +} satisfies ChartConfig + +export const TotalTrafficsWidget: FC = () => { + const { t } = useTranslation(); + const [timeRange, setTimeRange] = useState("1d") + const { start, end } = useFromNowInterval(timeRange as ChartDateInterval); + const { data, isPending } = useTotalTrafficQuery({ start, end }) + const chartData = useTransformData(data.usages); + + return ( + + {t("page.home.total-traffics.title")} + +
+ } + description={t("page.home.total-traffics.desc")} + > + + + + { + const [amount, metric] = formatByte(value) + return `${amount} ${metric}` + }} + /> + dateXAxisTicks(value, timeRange as ChartDateInterval)} + /> + { + return new Date(value).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric" + }) + }} + valueFormatter={(value) => { + const [amount, metric] = formatByte(value as number) + return `${amount} ${metric}` + }} + /> + } + /> + + + + + } + Skeleton={} + isFetching={isPending} + /> + ); +}; diff --git a/dashboard/src/features/total-traffic-widget/types.ts b/dashboard/src/features/total-traffic-widget/types.ts new file mode 100644 index 0000000..a601179 --- /dev/null +++ b/dashboard/src/features/total-traffic-widget/types.ts @@ -0,0 +1,10 @@ +import { UserType } from "@marzneshin/modules/users"; + +export interface NodesUsage { + datetime: Date; + nodes: Record; +} + +export interface UserNodesUsageWidgetProps { + user: UserType; +} diff --git a/dashboard/src/globals.css b/dashboard/src/globals.css index 8b06ae8..15ba2f6 100644 --- a/dashboard/src/globals.css +++ b/dashboard/src/globals.css @@ -113,3 +113,7 @@ @apply bg-background text-foreground; } } + +.recharts-responsive-container { + height: initial !important; +} diff --git a/dashboard/src/modules/users/components/widgets/user-nodes-usage-widget/filter-data-by-view.ts b/dashboard/src/modules/users/components/widgets/user-nodes-usage-widget/filter-data-by-view.ts deleted file mode 100644 index e69de29..0000000 diff --git a/dashboard/src/modules/users/components/widgets/user-nodes-usage-widget/index.tsx b/dashboard/src/modules/users/components/widgets/user-nodes-usage-widget/index.tsx index e19bb7c..a6be3a4 100644 --- a/dashboard/src/modules/users/components/widgets/user-nodes-usage-widget/index.tsx +++ b/dashboard/src/modules/users/components/widgets/user-nodes-usage-widget/index.tsx @@ -11,9 +11,9 @@ import { } from "@marzneshin/components"; import { Area, AreaChart, CartesianGrid, YAxis, XAxis } from "recharts" import { useState } from "react"; -import { SelectDateView } from "./select-date-view"; -import { ChartDateInterval, UserNodesUsageWidgetProps } from "./types"; -import { useChartConfig, useTransformData, useFromNowInterval } from "./hooks"; +import { UserNodesUsageWidgetProps } from "./types"; +import { dateXAxisTicks, useFromNowInterval, SelectDateView, ChartDateInterval } from "@marzneshin/common/stats-charts"; +import { useChartConfig, useTransformData } from "./hooks"; import { format as formatByte } from '@chbphone55/pretty-bytes'; import { useUserNodeUsagesQuery } from "@marzneshin/modules/users"; import { UsageGraphSkeleton } from "./skeleton" @@ -35,7 +35,6 @@ export const UserNodesUsageWidget: FC = ({ title={
{t("page.users.settings.nodes-usage.title")}
} description={t("page.users.settings.nodes-usage.desc")} > - @@ -58,35 +57,12 @@ export const UserNodesUsageWidget: FC = ({ return `${amount} ${metric}` }} /> - { - const date = new Date(value); - const format = { - "90d": { - day: "numeric", - month: "short", - }, "30d": { - day: "numeric", - }, "7d": { - day: "numeric", - hour: "numeric", - }, - "1d": { - minute: "numeric", - hour: "numeric", - } - }[timeRange as ChartDateInterval]; - if (timeRange === "1d") { - return date.toLocaleTimeString("en-US", format as Intl.DateTimeFormatOptions); - } - - return date.toLocaleDateString("en-US", format as Intl.DateTimeFormatOptions); - }} + tickFormatter={(value) => dateXAxisTicks(value, timeRange as ChartDateInterval)} /> = ({ - active, online, expired, on_hold, limited, total, -}) => { +export const UsersStatsWidget: FC = ({ total, limited, active, expired, on_hold, online }) => { const { t } = useTranslation(); - const data = [ - { symbol: "Active", amount: active, color: "fill-blue-500" }, - { symbol: "Online", amount: online, color: "fill-emerald-500" }, - { symbol: "Expired", amount: expired, color: "fill-gray-600" }, - { symbol: "On Hold", amount: on_hold, color: "fill-purple-500" }, - { symbol: "Limited", amount: limited, color: "fill-red-500" }, - ]; + const stats = [ + { type: "limited", total: limited, fill: "var(--color-limited)" }, + { type: "active", total: active, fill: "var(--color-active)" }, + { type: "expired", total: expired, fill: "var(--color-expired)" }, + { type: "onHold", total: on_hold, fill: "var(--color-onHold)" }, + { type: "online", total: online, fill: "var(--color-online)" }, + ] return ( - {t('users')} } + description={t('page.home.users-stats.desc')} + className="h-full" > -
- - -
-
+ + + } + /> + + + } /> + + + ); }; diff --git a/dashboard/src/modules/users/components/widgets/users-stats-widget/legend.tsx b/dashboard/src/modules/users/components/widgets/users-stats-widget/legend.tsx deleted file mode 100644 index 9d20633..0000000 --- a/dashboard/src/modules/users/components/widgets/users-stats-widget/legend.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { FC } from 'react' -import { scaleOrdinal } from '@visx/scale'; -import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'; - -const legendGlyphSize = 15; - -const ordinalColorScale = scaleOrdinal({ - domain: ["Active", "Online", "Expired", "On Hold", "Limited"], - range: ['#3b82f6', '#10b981', '#4b5563', '#A855F7', '#ef4444'], -}); - -export const UsersStatsLegend: FC = () => { - return ( - - {(labels) => ( -
- {labels.map((label, i) => ( - - - - - - {label.text} - - - ))} -
- )} -
- ) -} diff --git a/dashboard/src/routes/_dashboard/admins.lazy.tsx b/dashboard/src/routes/_dashboard/admins.lazy.tsx index 9f334e5..d1ea02c 100644 --- a/dashboard/src/routes/_dashboard/admins.lazy.tsx +++ b/dashboard/src/routes/_dashboard/admins.lazy.tsx @@ -1,8 +1,4 @@ import { - Card, - CardContent, - CardHeader, - CardTitle, Page, Loading, } from '@marzneshin/components'; @@ -15,20 +11,11 @@ import { useTranslation } from 'react-i18next'; export const AdminsPage: FC = () => { const { t } = useTranslation(); return ( - - - - - {t('admins')} - - - - - }> - - - - + + + }> + + ) }; diff --git a/dashboard/src/routes/_dashboard/hosts.lazy.tsx b/dashboard/src/routes/_dashboard/hosts.lazy.tsx index fd49fb8..2fbe8f0 100644 --- a/dashboard/src/routes/_dashboard/hosts.lazy.tsx +++ b/dashboard/src/routes/_dashboard/hosts.lazy.tsx @@ -1,9 +1,4 @@ import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, Page, Loading, } from '@marzneshin/components' @@ -16,22 +11,11 @@ import { Suspense } from 'react'; export const HostsPage = () => { const { t } = useTranslation() return ( - - - - - {t('hosts')} - - - - - }> - - - - - - + + + }> + + ) } diff --git a/dashboard/src/routes/_dashboard/index.lazy.tsx b/dashboard/src/routes/_dashboard/index.lazy.tsx index b5fdcc9..1df6526 100644 --- a/dashboard/src/routes/_dashboard/index.lazy.tsx +++ b/dashboard/src/routes/_dashboard/index.lazy.tsx @@ -1,31 +1,29 @@ import { createFileRoute } from '@tanstack/react-router' import { - Card, - CardContent, - CardHeader, - CardTitle, Page, } from '@marzneshin/components'; import { UsersStatsWidget, useUsersStatsQuery } from '@marzneshin/modules/users'; +import { TotalTrafficsWidget } from '@marzneshin/features/total-traffic-widget'; import { useTranslation } from 'react-i18next'; import { FC } from 'react'; export const DashboardPage: FC = () => { const { t } = useTranslation(); - const { data: stats } = useUsersStatsQuery() + const { data } = useUsersStatsQuery() return ( - - - - - {t('home')} - - - - - - + +
+
+ +
+ + {data.total !== 0 && ( +
+ +
+ )} +
) }; diff --git a/dashboard/src/routes/_dashboard/nodes.lazy.tsx b/dashboard/src/routes/_dashboard/nodes.lazy.tsx index 12ede6a..154ed37 100644 --- a/dashboard/src/routes/_dashboard/nodes.lazy.tsx +++ b/dashboard/src/routes/_dashboard/nodes.lazy.tsx @@ -1,10 +1,5 @@ import { AlertCard, - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, Page, Loading, } from '@marzneshin/components'; @@ -17,32 +12,26 @@ import { SudoRoute } from "@marzneshin/features/sudo-routes"; export const NodesPage: FC = () => { const { t } = useTranslation(); return ( - - - - - {t('nodes')} - - - - - }> - - - - - - {t('page.nodes.certificate-alert.desc')} - {t('page.nodes.certificate-alert.click')} - - } - /> - - - + + {t('page.nodes.certificate-alert.desc')} + {t('page.nodes.certificate-alert.click')} + + } + /> + } + > + + }> + + + ) }; diff --git a/dashboard/src/routes/_dashboard/services.lazy.tsx b/dashboard/src/routes/_dashboard/services.lazy.tsx index 7d6c788..dbc2afc 100644 --- a/dashboard/src/routes/_dashboard/services.lazy.tsx +++ b/dashboard/src/routes/_dashboard/services.lazy.tsx @@ -1,8 +1,4 @@ import { - Card, - CardContent, - CardHeader, - CardTitle, Page, Loading, } from "@marzneshin/components"; @@ -15,18 +11,11 @@ import { SudoRoute } from "@marzneshin/features/sudo-routes"; export const ServicesPage: FC = () => { const { t } = useTranslation(); return ( - - - - {t("services")} - - - - }> - - - - + + + }> + + ); }; diff --git a/dashboard/src/routes/_dashboard/settings.lazy.tsx b/dashboard/src/routes/_dashboard/settings.lazy.tsx index 3eae13a..6996961 100644 --- a/dashboard/src/routes/_dashboard/settings.lazy.tsx +++ b/dashboard/src/routes/_dashboard/settings.lazy.tsx @@ -1,5 +1,5 @@ -import { Card, CardContent, CardHeader, CardTitle, Page, VStack } from '@marzneshin/components' +import { Page, VStack } from '@marzneshin/components' import { CertificateWidget } from '@marzneshin/features/settings'; import { SubscriptionSettingsWidget } from '@marzneshin/features/subscription-settings'; import { createFileRoute } from '@tanstack/react-router'; @@ -9,21 +9,12 @@ import { SudoRoute } from "@marzneshin/features/sudo-routes"; export const Settings = () => { const { t } = useTranslation() return ( - - - - - {t('settings')} - - - - {/* */} - - - - - - + + {/* */} + + + + ) } diff --git a/dashboard/src/routes/_dashboard/users.lazy.tsx b/dashboard/src/routes/_dashboard/users.lazy.tsx index 80df956..d4cf2ed 100644 --- a/dashboard/src/routes/_dashboard/users.lazy.tsx +++ b/dashboard/src/routes/_dashboard/users.lazy.tsx @@ -1,9 +1,4 @@ import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, Page, Loading, } from '@marzneshin/components'; @@ -14,24 +9,18 @@ import { useTranslation } from 'react-i18next'; export const UsersPage: FC = () => { const { t } = useTranslation(); + return ( - - - - - {t('users')} - - - - - }> - - - - - - - + + } + > + + }> + + ) };