diff --git a/app/db/crud.py b/app/db/crud.py
@@ -314,7 +314,7 @@ def get_users(
def get_total_usages(
db: Session, admin: Admin, start: datetime, end: datetime
-):
+) -> TrafficUsageSeries:
usages = defaultdict(int)
query = (
@@ -338,20 +338,20 @@ def get_total_usages(
.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
- )
+ timestamp = created_at.replace(tzinfo=timezone.utc).timestamp()
+ usages[timestamp] += int(used_traffic)
- result = TrafficUsageSeries(usages=[])
+ result = TrafficUsageSeries(usages=[], total=0)
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)
- )
+ usage = usages.get(current.timestamp()) or 0
+ result.usages.append((int(current.timestamp()), usage))
+ result.total += usage
current += timedelta(hours=1)
return result
@@ -363,24 +363,27 @@ def get_user_usages(
start: datetime,
end: datetime,
) -> UserUsageSeriesResponse:
+
+ usages = defaultdict(dict)
+
cond = and_(
NodeUserUsage.user_id == db_user.id,
NodeUserUsage.created_at >= start,
NodeUserUsage.created_at <= end,
)
- usages = defaultdict(dict)
-
for v in db.query(NodeUserUsage).filter(cond):
- usages[v.node_id][
- v.created_at.replace(tzinfo=timezone.utc).timestamp()
- ] = v.used_traffic
+ timestamp = v.created_at.replace(tzinfo=timezone.utc).timestamp()
+ usages[v.node_id][timestamp] = v.used_traffic
node_ids = list(usages.keys())
nodes = db.query(Node).where(Node.id.in_(node_ids))
node_id_names = {node.id: node.name for node in nodes}
- result = UserUsageSeriesResponse(username=db_user.username, node_usages=[])
+ result = UserUsageSeriesResponse(
+ username=db_user.username, node_usages=[], total=0
+ )
+
for node_id, rows in usages.items():
node_usages = UserNodeUsageSeries(
node_id=node_id, node_name=node_id_names[node_id], usages=[]
@@ -390,11 +393,12 @@ def get_user_usages(
)
while current <= end:
- node_usages.usages.append(
- (int(current.timestamp()), rows.get(current.timestamp()) or 0)
- )
+ usage = rows.get(current.timestamp()) or 0
+ node_usages.usages.append((int(current.timestamp()), usage))
current += timedelta(hours=1)
+ result.total += usage
result.node_usages.append(node_usages)
+
return result
diff --git a/app/models/system.py b/app/models/system.py
@@ -41,3 +41,4 @@ class NodesStats(BaseModel):
class TrafficUsageSeries(BaseModel):
usages: list[tuple[int, int]]
+ total: int
diff --git a/app/models/user.py b/app/models/user.py
@@ -155,3 +155,4 @@ class UserNodeUsageSeries(BaseModel):
class UserUsageSeriesResponse(BaseModel):
username: str
node_usages: list[UserNodeUsageSeries]
+ total: int
diff --git a/dashboard/src/common/stats-charts/components/select-date-view.tsx b/dashboard/src/common/stats-charts/components/select-date-view.tsx
@@ -1,40 +1,39 @@
+import type { FC, PropsWithChildren } from 'react';
import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
+ RadioGroup,
+ RadioGroupItem,
+ Label
} from "@marzneshin/components";
-import { useTranslation } from 'react-i18next';
+
+const SelectDateViewItem: FC<PropsWithChildren & { interval: string }> = ({ interval, children }) => (
+ <div className="flex gap-2 justify-center items-center relative rounded-full py-0.5 px-1 cursor-pointer">
+ <RadioGroupItem
+ value={interval}
+ id={interval}
+ className="sr-only peer"
+ />
+ <Label
+ htmlFor={interval}
+ className="dark:peer-data-[state=checked]:bg-primary-foreground dark:peer-data-[state=checked]:border-primary peer-data-[state=checked]:border-primary peer-data-[state=checked]:border-1 border-0 peer-data-[state=checked]:text-primary-foreground dark:peer-data-[state=checked]:text-primary peer-data-[state=checked]:bg-primary dark:peer-data-[state=checked]:text-white text-xs rounded-full py-0.5 px-2 transition-colors"
+ >
+ {children}
+ </Label>
+ </div>
+)
export const SelectDateView = (
{ timeRange, setTimeRange }: { timeRange: string, setTimeRange: (s: string) => void }
) => {
- const { t } = useTranslation();
-
return (
- <Select value={timeRange} onValueChange={setTimeRange}>
- <SelectTrigger
- className="w-[160px] rounded-lg sm:ml-auto"
- aria-label="Select a value"
- >
- <SelectValue placeholder="Last 3 months" />
- </SelectTrigger>
- <SelectContent className="rounded-xl">
- <SelectItem value="1d" className="rounded-lg">
- {t('page.users.settings.nodes-usage.last-hours', { count: 24 })}
- </SelectItem>
- <SelectItem value="7d" className="rounded-lg">
- {t('page.users.settings.nodes-usage.last-days', { count: 7 })}
- </SelectItem>
- <SelectItem value="30d" className="rounded-lg">
- {t('page.users.settings.nodes-usage.last-days', { count: 30 })}
- </SelectItem>
- <SelectItem value="90d" className="rounded-lg">
- {t('page.users.settings.nodes-usage.last-months', { count: 3 })}
- </SelectItem>
- </SelectContent>
- </Select>
+ <RadioGroup
+ className="bg-muted border flex flex-row rounded-full"
+ value={timeRange}
+ onValueChange={setTimeRange}
+ >
+ <SelectDateViewItem interval="1d"> 24H</SelectDateViewItem>
+ <SelectDateViewItem interval="7d"> 7D</SelectDateViewItem>
+ <SelectDateViewItem interval="30d"> 30D</SelectDateViewItem>
+ <SelectDateViewItem interval="90d"> 3M</SelectDateViewItem>
+ </RadioGroup>
)
};
-
diff --git a/dashboard/src/components/widgets/section-widget/index.tsx b/dashboard/src/components/widgets/section-widget/index.tsx
@@ -39,10 +39,10 @@ export const SectionWidget: FC<SectionWidgetProps & PropsWithChildren> = ({
}
</CardHeader>
<Separator />
- <CardContent className="p-4">
+ <CardContent className="p-4 flex justify-center">
{content || children}
</CardContent>
- {footer && <CardFooter> {footer} </CardFooter>}
+ {footer && <CardFooter className="flex w-full justify-center"> {footer} </CardFooter>}
</Card>
)
}
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
@@ -5,6 +5,7 @@ export type UsageMetric = number[];
export interface TotalTrafficsResponse {
usages: Array<UsageMetric>;
+ total: number;
}
export interface TotalTrafficsQueryOptions {
@@ -15,6 +16,7 @@ export interface TotalTrafficsQueryOptions {
export type TotalTrafficsQueryKey = [string, string, string, { start?: string, end?: string }]
export const TotalTrafficsDefault = {
+ total: 0,
usages: []
}
diff --git a/dashboard/src/features/total-traffic-widget/index.tsx b/dashboard/src/features/total-traffic-widget/index.tsx
@@ -33,6 +33,7 @@ export const TotalTrafficsWidget: FC = () => {
const { start, end } = useFromNowInterval(timeRange as ChartDateInterval);
const { data, isPending } = useTotalTrafficQuery({ start, end })
const chartData = useTransformData(data.usages);
+ const [totalAmount, totalMetric] = formatByte(data.total);
return (
<Awaiting
@@ -41,10 +42,22 @@ export const TotalTrafficsWidget: FC = () => {
title={
<div className="hstack justify-between w-full">
{t("page.home.total-traffics.title")}
- <SelectDateView timeRange={timeRange} setTimeRange={setTimeRange} />
</div>
}
description={t("page.home.total-traffics.desc")}
+ options={
+ <div className="vstack justify-end w-full">
+ <span className="text-lg leading-none sm:text-2xl w-full">
+ {totalAmount} {totalMetric}
+ </span>
+ <span className="text-sm flex justify-end text-muted-foreground w-full">
+ Total
+ </span>
+ </div>
+ }
+ footer={
+ <SelectDateView timeRange={timeRange} setTimeRange={setTimeRange} />
+ }
>
<ChartContainer
config={chartConfig}
diff --git a/dashboard/src/modules/users/api/queries/user-nodes-usage.query.ts b/dashboard/src/modules/users/api/queries/user-nodes-usage.query.ts
@@ -12,6 +12,7 @@ interface NodeUsage {
export interface UserNodeUsagesResponse {
username: string;
+ total: number;
node_usages: NodeUsage[];
}
@@ -25,7 +26,8 @@ export type UserNodeUsagesQueryKey = [string, string, { start?: string, end?: st
export const UserNodeUsagesDefault = {
username: "",
- node_usages: []
+ node_usages: [],
+ total: 0
}
export async function fetchUserNodeUsages({ queryKey }: { queryKey: UserNodeUsagesQueryKey }): Promise<UserNodeUsagesResponse> {
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
@@ -27,13 +27,27 @@ export const UserNodesUsageWidget: FC<UserNodesUsageWidgetProps> = ({
const { data, isPending } = useUserNodeUsagesQuery({ username: user.username, start, end })
const chartData = useTransformData(data);
const config = useChartConfig(data);
+ const [totalAmount, totalMetric] = formatByte(data.total);
return (
<Awaiting
Component={
<SectionWidget
- title={<div className="hstack justify-between w-full">{t("page.users.settings.nodes-usage.title")} <SelectDateView timeRange={timeRange} setTimeRange={setTimeRange} /></div>}
+ title={<div className="hstack justify-between w-full">{t("page.users.settings.nodes-usage.title")}</div>}
description={t("page.users.settings.nodes-usage.desc")}
+ options={
+ <div className="vstack justify-end w-full">
+ <span className="text-lg leading-none sm:text-2xl w-full">
+ {totalAmount} {totalMetric}
+ </span>
+ <span className="text-sm flex justify-end text-muted-foreground w-full">
+ Total
+ </span>
+ </div>
+ }
+ footer={
+ <SelectDateView timeRange={timeRange} setTimeRange={setTimeRange} />
+ }
>
<ChartContainer
className="aspect-auto h-[320px] w-full"