Skip to content

Commit bfd7f90

Browse files
committed
feat(ui): add no data ui for templates, users, hosts, and groups
1 parent 0e20920 commit bfd7f90

File tree

10 files changed

+273
-97
lines changed

10 files changed

+273
-97
lines changed

dashboard/public/statics/locales/en.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,10 @@
652652
"disableSuccess": "Template «{{name}}» has been disabled successfully",
653653
"enableFailed": "Failed to enable admin «{{name}}»",
654654
"disableFailed": "Failed to disable admin «{{name}}»",
655-
"resetUsages": "Reset Usages"
655+
"resetUsages": "Reset Usages",
656+
"noTemplates": "No templates configured",
657+
"noTemplatesDescription": "Get started by creating your first user template.",
658+
"noSearchResults": "No templates match your search criteria. Try adjusting your search terms."
656659
},
657660
"core.configuration": "Configuration",
658661
"core.generalErrorMessage": "Something went wrong, please check the configuration",
@@ -1228,6 +1231,9 @@
12281231
"usersTable.dataUsage": "data usage",
12291232
"usersTable.noUser": "There is no user Created to the system",
12301233
"usersTable.noUserMatched": "It seems there is no user matched with what you are looking for",
1234+
"users.noUsers": "No users configured",
1235+
"users.noUsersDescription": "Get started by creating your first user account.",
1236+
"users.noSearchResults": "No users match your search criteria. Try adjusting your search terms.",
12311237
"usersTable.status": "Status",
12321238
"usersTable.total": "Total",
12331239
"autoRefresh": {
@@ -1274,6 +1280,9 @@
12741280
"disableFailed": "Failed to disable host «{{name}}»",
12751281
"duplicateSuccess": "Host «{{name}}» has been duplicated successfully",
12761282
"duplicateFailed": "Failed to duplicate host «{{name}}»",
1283+
"noHosts": "No hosts configured",
1284+
"noHostsDescription": "Get started by creating your first host configuration.",
1285+
"noSearchResults": "No hosts match your search criteria. Try adjusting your search terms.",
12771286
"xudp_proxy_443": "XUDP Proxy 443",
12781287
"reject": "Reject",
12791288
"allow": "Allow",
@@ -1299,7 +1308,10 @@
12991308
"enableFailed": "Failed to enable group «{{name}}»",
13001309
"disableSuccess": "Group «{{name}}» has been disabled successfully",
13011310
"disableFailed": "Failed to disable group «{{name}}»",
1302-
"deleteConfirmation": "Remove Group"
1311+
"deleteConfirmation": "Remove Group",
1312+
"noGroups": "No groups configured",
1313+
"noGroupsDescription": "Get started by creating your first group.",
1314+
"noSearchResults": "No groups match your search criteria. Try adjusting your search terms."
13031315
},
13041316
"name": "Name",
13051317
"inboundTags": "Inbound Tags",

dashboard/public/statics/locales/fa.json

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,10 @@
541541
"disableSuccess": "قالب «{{name}}» با موفقیت غیرفعال شد",
542542
"enableFailed": "فعال‌سازی مدیر «{{name}}» ناموفق بود",
543543
"disableFailed": "غیرفعال‌سازی مدیر «{{name}}» ناموفق بود",
544-
"resetUsages": "ریست مصرف"
544+
"resetUsages": "ریست مصرف",
545+
"noTemplates": "هیچ قالبی پیکربندی نشده",
546+
"noTemplatesDescription": "با ایجاد اولین قالب کاربر شروع کنید.",
547+
"noSearchResults": "هیچ قالبی با معیارهای جستجوی شما مطابقت ندارد. لطفاً عبارات جستجوی خود را تغییر دهید."
545548
},
546549
"core.configuration": "پیکربندی",
547550
"core.generalErrorMessage": "مشکلی پیش آمده، لطفا پیکربندی را بررسی کنید",
@@ -1070,6 +1073,9 @@
10701073
"usersTable.dataUsage": "مصرف داده",
10711074
"usersTable.noUser": "کاربری افزوده نشده است",
10721075
"usersTable.noUserMatched": "به‌نظر میرسه کاربری که جستجو کردید، وجود ندارد",
1076+
"users.noUsers": "هیچ کاربری پیکربندی نشده",
1077+
"users.noUsersDescription": "با ایجاد اولین حساب کاربری شروع کنید.",
1078+
"users.noSearchResults": "هیچ کاربری با معیارهای جستجوی شما مطابقت ندارد. لطفاً عبارات جستجوی خود را تغییر دهید.",
10731079
"usersTable.status": "وضعیت",
10741080
"usersTable.total": "مجموع",
10751081
"autoRefresh": {
@@ -1128,7 +1134,10 @@
11281134
"allow": "اجازه دادن",
11291135
"skip": "رد شدن",
11301136
"priorityUpdated": "هاست‌ها با موفقیت به روز شدند",
1131-
"priorityUpdateError": "به روزرسانی هاست‌ها با شکست مواجه شد"
1137+
"priorityUpdateError": "به روزرسانی هاست‌ها با شکست مواجه شد",
1138+
"noHosts": "هیچ هاستی پیکربندی نشده",
1139+
"noHostsDescription": "با ایجاد اولین پیکربندی هاست شروع کنید.",
1140+
"noSearchResults": "هیچ هاستی با معیارهای جستجوی شما مطابقت ندارد. لطفاً عبارات جستجوی خود را تغییر دهید."
11321141
},
11331142
"usersTable.sortByExpire": "مرتب‌سازی بر اساس زمان انقضا",
11341143
"group": {
@@ -1143,7 +1152,10 @@
11431152
"deleteSuccess": "گروه «{{name}}» با موفقیت حذف شد",
11441153
"deleteFailed": "حذف گروه «{{name}}» با خطا مواجه شد",
11451154
"deleteConfirm": "آیا مطمئن هستید که می‌خواهید گروه «{{name}}» را حذف کنید؟",
1146-
"deleteConfirmation": "حذف گروه"
1155+
"deleteConfirmation": "حذف گروه",
1156+
"noGroups": "هیچ گروهی پیکربندی نشده",
1157+
"noGroupsDescription": "با ایجاد اولین گروه شروع کنید.",
1158+
"noSearchResults": "هیچ گروهی با معیارهای جستجوی شما مطابقت ندارد. لطفاً عبارات جستجوی خود را تغییر دهید."
11471159
},
11481160
"name": "نام",
11491161
"nodeModal": {

dashboard/public/statics/locales/ru.json

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,10 @@
638638
"disableSuccess": "Шаблон «{{name}}» был успешно отключён",
639639
"enableFailed": "Не удалось включить администратора «{{name}}»",
640640
"disableFailed": "Не удалось отключить администратора «{{name}}»",
641-
"resetUsages": "Сброс использования"
641+
"resetUsages": "Сброс использования",
642+
"noTemplates": "Шаблоны не настроены",
643+
"noTemplatesDescription": "Начните с создания первого шаблона пользователя.",
644+
"noSearchResults": "Нет шаблонов, соответствующих вашим критериям поиска. Попробуйте изменить условия поиска."
642645
},
643646
"core.configuration": "Конфигурация",
644647
"core.generalErrorMessage": "Что-то пошло не так, пожалуйста, проверьте конфигурацию",
@@ -859,6 +862,9 @@
859862
"usersTable.dataUsage": "Расход трафика",
860863
"usersTable.noUser": "В системе нет созданных пользователей",
861864
"usersTable.noUserMatched": "Похоже, нет пользователя, соответствующего вашему запросу",
865+
"users.noUsers": "Пользователи не настроены",
866+
"users.noUsersDescription": "Начните с создания первой учетной записи пользователя.",
867+
"users.noSearchResults": "Нет пользователей, соответствующих вашим критериям поиска. Попробуйте изменить условия поиска.",
862868
"usersTable.status": "Статус",
863869
"usersTable.total": "Всего",
864870
"autoRefresh": {
@@ -1186,7 +1192,10 @@
11861192
"allow": "Разрешить",
11871193
"skip": "Пропустить",
11881194
"priorityUpdated": "Хосты успешно обновлены",
1189-
"priorityUpdateError": "Не удалось обновить хосты"
1195+
"priorityUpdateError": "Не удалось обновить хосты",
1196+
"noHosts": "Хосты не настроены",
1197+
"noHostsDescription": "Начните с создания первой конфигурации хоста.",
1198+
"noSearchResults": "Нет хостов, соответствующих вашим критериям поиска. Попробуйте изменить условия поиска."
11901199
},
11911200
"group": {
11921201
"createSuccess": "Группа «{{name}}» успешно создана",
@@ -1200,7 +1209,10 @@
12001209
"enableFailed": "Не удалось включить группу «{{name}}»",
12011210
"disableSuccess": "Группа «{{name}}» успешно отключена",
12021211
"disableFailed": "Не удалось отключить группу «{{name}}»",
1203-
"deleteConfirmation": "Удалить группу"
1212+
"deleteConfirmation": "Удалить группу",
1213+
"noGroups": "Группы не настроены",
1214+
"noGroupsDescription": "Начните с создания первой группы.",
1215+
"noSearchResults": "Нет групп, соответствующих вашим критериям поиска. Попробуйте изменить условия поиска."
12041216
},
12051217
"name": "Имя",
12061218
"nodeModal": {

dashboard/public/statics/locales/zh.json

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,10 @@
651651
"disableSuccess": "模板「{{name}}」已成功禁用",
652652
"enableFailed": "启用管理员「{{name}}」失败",
653653
"disableFailed": "禁用管理员「{{name}}」失败",
654-
"resetUsages": "重置使用量"
654+
"resetUsages": "重置使用量",
655+
"noTemplates": "未配置模板",
656+
"noTemplatesDescription": "开始创建您的第一个用户模板。",
657+
"noSearchResults": "没有模板匹配您的搜索条件。请尝试调整搜索词。"
655658
},
656659
"core.configuration": "配置",
657660
"core.generalErrorMessage": "配置有误, 请检查",
@@ -1174,6 +1177,9 @@
11741177
"usersTable.dataUsage": "流量统计",
11751178
"usersTable.noUser": "还没有添加任何用户",
11761179
"usersTable.noUserMatched": "没有找到您搜索的用户",
1180+
"users.noUsers": "未配置用户",
1181+
"users.noUsersDescription": "开始创建您的第一个用户账户。",
1182+
"users.noSearchResults": "没有用户匹配您的搜索条件。请尝试调整搜索词。",
11771183
"usersTable.status": "状态",
11781184
"usersTable.sortByExpire": "按过期时间排序",
11791185
"usersTable.total": "总共",
@@ -1243,7 +1249,10 @@
12431249
"duplicateSuccess": "主机「{{name}}」已成功复制",
12441250
"duplicateFailed": "复制主机「{{name}}」失败",
12451251
"priorityUpdated": "主机已成功更新",
1246-
"priorityUpdateError": "更新主机失败"
1252+
"priorityUpdateError": "更新主机失败",
1253+
"noHosts": "未配置主机",
1254+
"noHostsDescription": "开始创建您的第一个主机配置。",
1255+
"noSearchResults": "没有主机匹配您的搜索条件。请尝试调整搜索词。"
12471256
},
12481257
"pasarguard": "PasarGuard",
12491258
"group": {
@@ -1258,7 +1267,10 @@
12581267
"enableFailed": "启用群组「{{name}}」失败",
12591268
"disableSuccess": "群组「{{name}}」已成功禁用",
12601269
"disableFailed": "禁用群组「{{name}}」失败",
1261-
"deleteConfirmation": "移除群组"
1270+
"deleteConfirmation": "移除群组",
1271+
"noGroups": "未配置群组",
1272+
"noGroupsDescription": "开始创建您的第一个群组。",
1273+
"noSearchResults": "没有群组匹配您的搜索条件。请尝试调整搜索词。"
12621274
},
12631275
"name": "名称",
12641276
"nodeModal": {

dashboard/src/components/bulk/bulk-flow.tsx

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ShadowsocksMethods,
1515
} from '@/service/api'
1616
import { Button } from '@/components/ui/button'
17+
import { LoaderButton } from '@/components/ui/loader-button'
1718
import { Input } from '@/components/ui/input'
1819
import { Badge } from '@/components/ui/badge'
1920
import { Card, CardContent } from '@/components/ui/card'
@@ -157,10 +158,8 @@ export default function BulkFlow({ operationType }: BulkFlowProps) {
157158
case 'expire':
158159
return expireSeconds !== undefined
159160
case 'groups':
160-
if (groupsOperation === 'remove') {
161-
return selectedHasGroups.length > 0 || selectedUsers.length > 0 || selectedAdmins.length > 0
162-
}
163-
return selectedUsers.length > 0 || selectedAdmins.length > 0 || selectedHasGroups.length > 0
161+
// Allow proceeding even if no targets selected - will apply to all users
162+
return true
164163
default:
165164
return false
166165
}
@@ -172,10 +171,13 @@ export default function BulkFlow({ operationType }: BulkFlowProps) {
172171
}
173172

174173
const handleApply = () => {
175-
const totalTargets = selectedUsers.length + selectedAdmins.length + selectedGroups.length + selectedHasGroups.length
176-
if (operationType === 'groups' && groupsOperation === 'remove' && totalTargets === 0) {
177-
toast.error(t('error'), { description: t('bulk.noTargetsSelected') })
178-
return
174+
// For groups remove operation, require at least hasGroups, users, or admins to be selected
175+
if (operationType === 'groups' && groupsOperation === 'remove') {
176+
const totalTargets = selectedUsers.length + selectedAdmins.length + selectedHasGroups.length
177+
if (totalTargets === 0) {
178+
toast.error(t('error'), { description: t('bulk.noTargetsSelected') })
179+
return
180+
}
179181
}
180182
setShowConfirmDialog(true)
181183
}
@@ -275,7 +277,9 @@ export default function BulkFlow({ operationType }: BulkFlowProps) {
275277
)
276278
}
277279

278-
const totalTargets = selectedUsers.length + selectedAdmins.length + selectedGroups.length + (operationType === 'groups' ? selectedHasGroups.length : 0)
280+
// For groups operation, groups are the operation target, not user targets
281+
// So isApplyToAll should only check users, admins, and hasGroups
282+
const totalTargets = selectedUsers.length + selectedAdmins.length + (operationType === 'groups' ? selectedHasGroups.length : selectedGroups.length)
279283
const isApplyToAll = totalTargets === 0
280284

281285
const formatTime = (seconds: number) => {
@@ -841,10 +845,19 @@ export default function BulkFlow({ operationType }: BulkFlowProps) {
841845
<ChevronRight className={cn('h-4 w-4', isRTL ? 'mr-1.5 rotate-180' : 'ml-1.5')} />
842846
</Button>
843847
) : (
844-
<Button onClick={handleApply} disabled={!canProceedToNext()} size="sm" className="w-full sm:w-auto">
845-
<CheckCircle className={cn('h-4 w-4', isRTL ? 'ml-1.5' : 'mr-1.5')} />
846-
<span>{t('bulk.applyOperation', { defaultValue: 'Apply Operation' })}</span>
847-
</Button>
848+
<LoaderButton
849+
onClick={handleApply}
850+
disabled={!canProceedToNext()}
851+
isLoading={proxyMutation.isPending || dataMutation.isPending || expireMutation.isPending || addGroupsMutation.isPending || removeGroupsMutation.isPending}
852+
loadingText={t('applying', { defaultValue: 'Applying...' })}
853+
size="sm"
854+
className="w-full sm:w-auto"
855+
>
856+
<div className="flex items-center gap-1.5">
857+
<CheckCircle className={cn('h-4 w-4', isRTL ? 'ml-1.5' : 'mr-1.5')} />
858+
<span>{t('bulk.applyOperation', { defaultValue: 'Apply Operation' })}</span>
859+
</div>
860+
</LoaderButton>
848861
)}
849862
</div>
850863

dashboard/src/components/groups/groups-list.tsx

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'
1111
import { queryClient } from '@/utils/query-client'
1212
import useDirDetection from '@/hooks/use-dir-detection'
1313
import { Skeleton } from '@/components/ui/skeleton'
14-
import { Card } from '@/components/ui/card'
14+
import { Card, CardContent } from '@/components/ui/card'
1515
import { Input } from '@/components/ui/input'
1616
import { Button } from '@/components/ui/button'
1717
import { RefreshCw, Search, X } from 'lucide-react'
@@ -93,6 +93,9 @@ export default function GroupsList({ isDialogOpen, onOpenChange }: GroupsListPro
9393
await refetch()
9494
}
9595

96+
const isEmpty = !isLoading && (!filteredGroups || filteredGroups.length === 0) && !searchQuery.trim()
97+
const isSearchEmpty = !isLoading && (!filteredGroups || filteredGroups.length === 0) && searchQuery.trim() !== ''
98+
9699
return (
97100
<div className="w-full flex-1 space-y-4">
98101
{/* Search Input */}
@@ -117,24 +120,46 @@ export default function GroupsList({ isDialogOpen, onOpenChange }: GroupsListPro
117120
<RefreshCw className={cn('h-4 w-4', isFetching && 'animate-spin')} />
118121
</Button>
119122
</div>
120-
<ScrollArea className="h-[calc(100vh-8rem)]">
121-
<div dir={dir} className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
122-
{isLoading
123-
? [...Array(6)].map((_, i) => (
124-
<Card key={i} className="px-4 py-5">
125-
<div className="flex items-center gap-2 sm:gap-3">
126-
<Skeleton className="h-8 w-8 shrink-0 rounded-full" />
127-
<div className="min-w-0 flex-1 space-y-2">
128-
<Skeleton className="h-5 w-24 sm:w-32" />
129-
<Skeleton className="h-4 w-20 sm:w-24" />
123+
{isEmpty && (
124+
<Card className="mb-12">
125+
<CardContent className="p-8 text-center">
126+
<div className="space-y-4">
127+
<h3 className="text-lg font-semibold">{t('group.noGroups')}</h3>
128+
<p className="mx-auto max-w-2xl text-muted-foreground">{t('group.noGroupsDescription')}</p>
129+
</div>
130+
</CardContent>
131+
</Card>
132+
)}
133+
{isSearchEmpty && (
134+
<Card className="mb-12">
135+
<CardContent className="p-8 text-center">
136+
<div className="space-y-4">
137+
<h3 className="text-lg font-semibold">{t('noResults')}</h3>
138+
<p className="mx-auto max-w-2xl text-muted-foreground">{t('group.noSearchResults')}</p>
139+
</div>
140+
</CardContent>
141+
</Card>
142+
)}
143+
{!isEmpty && !isSearchEmpty && (
144+
<ScrollArea className="h-[calc(100vh-8rem)]">
145+
<div dir={dir} className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
146+
{isLoading
147+
? [...Array(6)].map((_, i) => (
148+
<Card key={i} className="px-4 py-5">
149+
<div className="flex items-center gap-2 sm:gap-3">
150+
<Skeleton className="h-8 w-8 shrink-0 rounded-full" />
151+
<div className="min-w-0 flex-1 space-y-2">
152+
<Skeleton className="h-5 w-24 sm:w-32" />
153+
<Skeleton className="h-4 w-20 sm:w-24" />
154+
</div>
155+
<Skeleton className="h-8 w-8 shrink-0" />
130156
</div>
131-
<Skeleton className="h-8 w-8 shrink-0" />
132-
</div>
133-
</Card>
134-
))
135-
: filteredGroups?.map(group => <Group key={group.id} group={group} onEdit={handleEdit} onToggleStatus={handleToggleStatus} />)}
136-
</div>
137-
</ScrollArea>
157+
</Card>
158+
))
159+
: filteredGroups?.map(group => <Group key={group.id} group={group} onEdit={handleEdit} onToggleStatus={handleToggleStatus} />)}
160+
</div>
161+
</ScrollArea>
162+
)}
138163

139164
<GroupModal
140165
isDialogOpen={isDialogOpen}

0 commit comments

Comments
 (0)