Compare commits

..

32 Commits

Author SHA1 Message Date
Bot 2da8565e47 fix: stabilize admin frontend and resolve TS build errors 2026-05-01 13:55:01 +08:00
Bot 9720bc258f feat: refactor notification UI and remove background image 2026-04-28 00:04:16 +08:00
Bot 1e4fae5306 fix: resolve domain.ts type errors and SWR fetching bugs; fix vps config unmarshal issue 2026-04-26 22:07:27 +08:00
Bot 825bcb08f4 feat: add Sync WHOIS button and API integration 2026-04-16 23:08:56 +08:00
Bot cd3b8fdf91 fix: relax URL validation to allow Bot Tokens and SMTP hosts 2026-04-16 21:56:56 +08:00
Bot 48ccde053b feat: specialized UI labels for Telegram notification type 2026-04-16 21:43:50 +08:00
Bot d9ec7c362c feat: add billing UI, SMTP support and expiry settings 2026-04-16 21:36:55 +08:00
Bot 53cb369e4a feat: add inputs for custom branding and background images in Settings 2026-04-16 17:18:49 +08:00
Bot 0f8e9e25fe fix: bypass react-hook-form type mismatches with any casts in ServerCard 2026-04-16 16:47:33 +08:00
Bot 06762437fa fix: correct nzcfg.html URL to /dashboard prefix for proper backend routing 2026-04-16 16:39:39 +08:00
Bot b81d0fe9fc Integrate interactive visual configuration generator (nzcfg) into Server public note form 2026-04-16 16:21:47 +08:00
Bot 8733070cf1 Fix ts configuration and missing fragment in server route after terminal removal 2026-04-16 16:21:40 +08:00
Bot a4dc173fcc Remove SSH terminal functionality for security reasons 2026-04-16 16:21:35 +08:00
Buriburizaemon e848a34fe7 Merge upstream/main 2026-04-16 11:52:58 +08:00
Weblate (bot) f4696421ec Translated using Weblate (Indonesian) (#156)
Currently translated at 100.0% (245 of 245 strings)


Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/id/
Translation: Nezha/Admin frontend

Co-authored-by: Arif Budiman <arifpedia@gmail.com>
2026-04-05 09:58:43 +08:00
naiba 341a6fa666 fix: use Combobox for notification group selection in settings page
Settings page used a plain number input for ip_change_notification_group_id,
requiring users to manually enter a group ID. Replace it with a searchable
Combobox component (consistent with alert-rule/service/cron pages) and wrap
the settings route with NotificationProvider to load notification group data.

Closes nezhahq/nezha#1174

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
2026-03-01 01:11:37 +00:00
Weblate (bot) 4f6e6d1a21 Translated using Weblate (Ukrainian) (#155)
Currently translated at 49.7% (122 of 245 strings)


Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/uk/
Translation: Nezha/Admin frontend

Co-authored-by: Руслан Пузич <visp80@gmail.com>
2026-02-22 21:45:21 +08:00
Weblate (bot) 0bdb63cb20 Translated using Weblate (Galician) (#153)
Currently translated at 7.7% (19 of 245 strings)

Added translation using Weblate (Galician)

Translated using Weblate (Spanish)

Currently translated at 100.0% (244 of 244 strings)



Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/es/
Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/gl/
Translation: Nezha/Admin frontend

Co-authored-by: nlimeres <dynosaurioprogramador@gmail.com>
2026-02-19 14:34:50 +08:00
奶爸 d04c4a1784 fix: public_note validation rejects non-object JSON values, preventing second edit (#154)
Co-authored-by: naiba <naiba@users.noreply.github.com>
2026-02-19 14:30:10 +08:00
UUBulb 2fe19adb96 fix: downgrade tailwindcss to v3 (#151)
* fix: downgrade tailwindcss to v3

* chore: auto-fix linting and formatting issues
2026-02-14 18:06:12 +08:00
naiba 84ba33dac3 fix: server edit dialog submit button unresponsive after first save (#1132)
Upgrade react-hook-form 7.63→7.71 to fix form.reset() race condition
that silently broke zod validation on subsequent submissions.

Reset form and public-note state (publicNoteObj/Raw/Mode/Errors)
from latest data when the edit dialog opens, preventing stale state
from a previous edit session.
2026-02-14 12:11:47 +08:00
胡说丷刂 ff231da753 service 增加权重 (#150)
* 服务增加权重

* 避免显示成 undefined

---------

Co-authored-by: huYang <306061454@qq.com>
2026-02-14 10:10:24 +08:00
UUBulb 78c63bce33 fix: variable not renamed (#148) 2026-01-10 20:22:13 +08:00
UUBulb b4221213a0 feat(notification): add option to convert metric units in request body (#147)
* feat(notification): add option to convert metric units in request body

* rename fields

* add prettierignore

* chore: auto-fix linting and formatting issues
2026-01-10 17:27:13 +08:00
naiba f5fd7c390c fix: immutable release 2026-01-01 11:58:11 +08:00
Weblate (bot) 878cef08ad Translated using Weblate (Ukrainian) (#145)
Currently translated at 34.8% (85 of 244 strings)

Added translation using Weblate (Ukrainian)



Translate-URL: https://hosted.weblate.org/projects/nezha/admin-frontend/uk/
Translation: Nezha/Admin frontend

Co-authored-by: Руслан Пузич <visp80@gmail.com>
2026-01-01 10:32:17 +08:00
Weblate (bot) 6e7fca8c7d Added translation using Weblate (Japanese) (#142)
Co-authored-by: Zilong Liu <2821624044@qq.com>
2025-10-26 21:32:10 +08:00
naiba 1a7e2ad37a chore: fix import 2025-10-09 09:48:07 +08:00
Chillln e783692ac9 feat: enhance public notes functionality with flexible options (#141)
* feat: enhance public notes functionality with flexible options

- Make public notes optional to prevent default values causing frontend issues
- Add dual editing modes: raw text editing and custom fields (avoid hardcoded schema)
- Set raw text mode as default, populate input on edit, submit raw text content always
- Add toggle switch: submit raw text when enabled, submit empty & hide controls when disabled
- New records default to disabled public notes; auto-expand on edit based on content

* chore: auto-fix linting and formatting issues

* feat: Add public annotation data structure and utility functions

Implemented Zod validation patterns, default values, parsing functions, and utility functions for public notes, and updated related internationalization text.

* chore: auto-fix linting and formatting issues

* refactor(server): Replace i18n implementation

Replace direct use of i18n.t with react-i18next's useTranslation hook to improve internationalization support.

* refactor(public-note): Optimize data model and validation logic

Removed the pruneEmpty function and simplified the date processing logic, making billingDataMod and planDataMod optional fields. Also optimized the validation logic to handle optional fields.

* chore: auto-fix linting and formatting issues

* fix zod validation & don't write empty values when parsing

* use raw mode if object contains unknown fields

* rename some features

* chore: Update dependency package versions

Upgrade multiple npm dependencies to their latest versions, including react, tailwindcss, and eslint. Ignore lock files.

* fix(server): Fix default value when bill amount is undefined

Changed undefined values ​​for bill amount to the default value "0" to avoid potential null value errors.

---------

Co-authored-by: Guccen <171530509+Chillln@users.noreply.github.com>
Co-authored-by: uubulb <uub@suwako.de>
2025-10-09 09:35:13 +08:00
naiba ec6511bcb8 💩 f*ck react 2025-10-02 17:15:20 +08:00
naiba cb749c6d16 fix: Cannot read properties of undefined (reading 'displayName') 2025-10-02 15:26:31 +08:00
Chillln bb288c554f Fix and update (#139)
* refactor(ui): 统一组件引用类型为ComponentRef

更新所有UI组件中的forwardRef类型,从ElementRef改为ComponentRef以保持一致性
迁移postcss配置至mjs格式并更新依赖版本

* refactor: 优化表单类型定义和验证逻辑

移除自定义的 asOptionalField 工具函数,直接使用 Zod 的 optional() 方法,并明确定义表单数据类型。

* style: 更新UI主题配置和样式变量

将主题风格从default切换为new-york,并重构CSS变量使用OKLCH色彩空间。同时添加tailwindcss-animate插件支持。

* style: 统一页面头部按钮组样式

优化多个页面头部按钮组的布局样式,增加响应式设计和flex-wrap支持

* fix(server): 修复对话框交互问题并优化SWR配置

修复对话框关闭逻辑并阻止外部交互,同时禁用SWR的自动重新验证功能以提升性能。

* feat: 添加日历组件及账单相关国际化

实现基于 react-day-picker 的日历组件,并添加账单管理相关的多语言支持

* style(components): 统一按钮样式并格式化代码

为删除和禁用按钮添加text-white类名,同时调整ServerCard组件中的代码缩进格式。

* perf(build): 优化Vite打包配置与代码分割策略

调整Vite构建配置,改进第三方依赖的分组逻辑并添加UUID支持到安装命令组件

* fix: 修正页面标题翻译不一致问题

将CronPage和ServicePage的标题从"Server"分别改为"Task"和"Service",并优化NotificationGroupPage的按钮组布局。

* fix(auth): 改进登录错误处理和国际化支持

优化登录错误提示,添加多语言支持并移除控制台错误日志。同时修复头部组件透明度样式问题。

* feat: 添加服务器操作下拉菜单

为服务器卡片添加统一的下拉菜单操作入口,整合终端、配置和安装命令功能。

* feat[alert-rule]: 优化告警规则组件性能

重构告警规则组件代码结构,提升渲染效率并减少内存占用。

* docs(i18n): 新增翻译字段

为界面添加"Add"、"Delete"、"AdvancedJSON"和"Save"等关键操作的翻译字段,支持中英文双语显示。

* perf(vite): 优化分包策略以提升构建性能

重构 manualChunks 逻辑,按功能类别分组依赖项,并增加大型库的独立分包规则。

* style: 统一危险操作按钮的文字颜色

在所有确认操作的弹窗按钮中添加白色文字样式,保持视觉一致性。

* fix(components): 调整下拉菜单对齐方式

根据菜单项状态动态设置下拉菜单的对齐方向和起始位置。

* fix(types): 修复在线用户API分页类型

添加ModelOnlineUserApi接口类型,包含分页信息,并移除index.ts中重复的类型定义。

* chore: auto-fix linting and formatting issues

* feat(locales): 添加无过期相关翻译项

为英文和中文翻译文件添加"NoExpiry"、"SetNoExpiry"等无过期相关字段的翻译。

fix(components): 移除重复的图标按钮选项

从IconButton组件中删除重复的"more"图标选项。

* feat(ServerCard): 优化日期选择器并添加下拉提示

为日期选择器添加下拉布局和年份范围限制,并在公共笔记区域增加下拉项生效提示文本。

* chore: auto-fix linting and formatting issues

* style: 优化多个组件的UI交互细节

统一按钮悬停样式并简化国际化文本调用,移除冗余的单位显示和空值判断逻辑。

* refactor(ServerCard): 移除网络路由相关代码

删除 ServerCard 组件中与 plan.networkRoute 相关的字段验证和错误显示逻辑。

* chore: auto-fix linting and formatting issues

* feat(ui): 添加Switch组件并改进服务器表单交互

- 新增Radix UI Switch组件依赖及实现
- 将IPv4/IPv6输入改为开关控件,优化用户体验
- 添加"按量付费"选项和新的翻译字段
- 改进网络路由和备注输入的占位提示
- 修复暗黑模式下的按钮背景色

* style(components): 为禁止按钮添加白色文本样式

* chore: auto-fix linting and formatting issues

* fix(ServerCard): 修复日期选择器样式和滚动问题

调整日期选择器的宽度和高度限制,添加滚动容器以解决内容溢出问题

* refactor(server-config): 简化复选框checked属性的布尔转换

使用!!操作符简化controllerField.value的布尔值转换,使代码更简洁

* feat(国际化): 添加告警规则和搜索框的国际化支持

为告警规则组件添加多语言支持,包括服务器监控选项、忽略提示和示例文本。同时将搜索框的占位文本替换为国际化字段。

* chore: auto-fix linting and formatting issues

* fix(switch): 修正 Switch 组件 ref 类型定义错误

---------

Co-authored-by: Guccen <171530509+Chillln@users.noreply.github.com>
2025-10-02 14:23:11 +08:00
94 changed files with 13298 additions and 1877 deletions
+1
View File
@@ -35,6 +35,7 @@ jobs:
uses: softprops/action-gh-release@v2
with:
files: dist.zip
generate_release_notes: true
- name: Changelog
run: bun x changelogithub
+2
View File
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
bun.lock
pnpm-lock.yaml
+1
View File
@@ -0,0 +1 @@
src/main.tsx
+3 -1
View File
@@ -10,11 +10,13 @@
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
},
"registries": {}
}
+7957
View File
File diff suppressed because it is too large Load Diff
+53 -50
View File
@@ -13,69 +13,72 @@
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-alert-dialog": "^1.1.5",
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-dropdown-menu": "^2.1.5",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-navigation-menu": "^1.2.4",
"@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.5",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.2",
"@tanstack/react-table": "^8.20.6",
"@radix-ui/react-tabs": "^1.1.13",
"@tailwindcss/postcss": "^4.1.14",
"@tanstack/react-table": "^8.21.3",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/luxon": "^3.4.2",
"@types/luxon": "^3.7.1",
"@xterm/addon-attach": "^0.11.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"cmdk": "^1.1.1",
"copy-to-clipboard": "^3.3.3",
"framer-motion": "^11.18.2",
"i18next": "^24.2.2",
"i18next-browser-languagedetector": "^8.0.2",
"date-fns": "^4.1.0",
"framer-motion": "^12.23.22",
"i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0",
"jotai-zustand": "^0.6.0",
"lucide-react": "^0.454.0",
"luxon": "^3.5.0",
"lucide-react": "^0.545.0",
"luxon": "^3.7.2",
"next-themes": "^0.4.6",
"prettier-plugin-tailwindcss": "^0.6.11",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-i18next": "^15.4.0",
"react-router-dom": "^7.1.5",
"react-virtuoso": "^4.12.3",
"sonner": "^1.7.4",
"swr": "^2.3.0",
"tailwind-merge": "^2.6.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"react": "^19.2.0",
"react-day-picker": "^9.11.1",
"react-dom": "^19.2.0",
"react-hook-form": "^7.71.1",
"react-i18next": "^16.0.0",
"react-router-dom": "^7.9.4",
"react-virtuoso": "^4.14.1",
"sonner": "^2.0.7",
"swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^3.24.1",
"zustand": "^5.0.3"
"zod": "^4.1.12",
"zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@types/node": "^22.13.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.19.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
"postcss": "^8.5.1",
"swagger-typescript-api": "^13.0.23",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.3",
"typescript-eslint": "^8.22.0",
"vite": "^6.0.11"
"@eslint/js": "^9.37.0",
"@types/node": "^24.7.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react": "^5.0.4",
"autoprefixer": "^10.4.21",
"eslint": "^9.37.0",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.23",
"globals": "^16.4.0",
"postcss": "8.4.24",
"swagger-typescript-api": "^13.2.15",
"tailwindcss": "3.4.19",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.0",
"vite": "^7.1.9"
}
}
+1348
View File
File diff suppressed because it is too large Load Diff
+31 -12
View File
@@ -1,39 +1,58 @@
import { fetcher, FetcherMethod, swrFetcher } from './api' // 导入正确的 fetcher 函数和方法枚举
import type { Domain, BillingDataMod} from '@/types/api'
// 导入正确的 fetcher 函数和方法枚举
import type { BillingDataMod, Domain } from "@/types/domain"
import { FetcherMethod, fetcher } from "./api"
// --- GET 请求 (用于 SWR) ---
// 获取域名列表的函数,专门为 useSWR 设计
// swrFetcher 内部会调用 fetcher,这一部分是正确的
export const useDomainList = () => {
return swrFetcher<Domain[]>('/api/v1/domains')
export const useDomainList = (url: string) => {
return fetcher<Domain[]>(FetcherMethod.GET, url)
}
// --- POST, PUT, DELETE 请求 (使用 fetcher) ---
// 添加一个新的域名
export const addDomain = (domain: string) => {
return fetcher<Domain>(FetcherMethod.POST, '/api/v1/domains', { domain })
return fetcher<Domain>(FetcherMethod.POST, "/api/v1/domains", { domain })
}
// 触发域名验证
export const verifyDomain = (id: number) => {
return fetcher<{ success: boolean; message: string }>(FetcherMethod.POST, `/api/v1/domains/${id}/verify`)
return fetcher<{ success: boolean; message: string }>(
FetcherMethod.POST,
`/api/v1/domains/${id}/verify`,
)
}
// 更新域名的配置信息
export const updateDomainConfig = (id: number, billingData: BillingDataMod) => {
return fetcher<Domain>(FetcherMethod.PUT, `/api/v1/domains/${id}`, { billing_data: billingData })
return fetcher<Domain>(FetcherMethod.PUT, `/api/v1/domains/${id}`, {
billing_data: billingData,
})
}
// 删除一个域名
export const deleteDomain = (id: number) => {
// DELETE 请求通常没有响应体,所以 T 可以是 any 或 unknown
return fetcher<any>(FetcherMethod.DELETE, `/api/v1/domains/${id}`)
// DELETE 请求通常没有响应体,所以 T 可以是 any 或 unknown
return fetcher<any>(FetcherMethod.DELETE, `/api/v1/domains/${id}`)
}
// 更新一个域名(包括公开状态和配置信息)
export const updateDomain = (id: number, data: { is_public: boolean, billing_data: BillingDataMod }) => {
export const updateDomain = (
id: number,
data: { is_public: boolean; billing_data: BillingDataMod },
) => {
return fetcher<Domain>(FetcherMethod.PUT, `/api/v1/domains/${id}`, data)
}
}
// 同步 Whois 信息
export const syncDomainWHOIS = (id: number) => {
return fetcher<Domain>(FetcherMethod.POST, `/api/v1/domains/${id}/sync`)
}
// 同步所有域名
export const syncAllDomains = () => {
return fetcher<any>(FetcherMethod.POST, "/api/v1/domains/sync-all")
}
-9
View File
@@ -1,9 +0,0 @@
import { ModelCreateTerminalResponse } from "@/types"
import { FetcherMethod, fetcher } from "./api"
export const createTerminal = async (id: number): Promise<ModelCreateTerminalResponse> => {
return fetcher<ModelCreateTerminalResponse>(FetcherMethod.POST, "/api/v1/terminal", {
server_id: id,
})
}
+10 -4
View File
@@ -49,7 +49,7 @@ export function ActionButtonGroup<E, U>({
{children}
<AlertDialog>
<AlertDialogTrigger asChild>
<IconButton variant="destructive" icon="trash" />
<IconButton variant="destructive" icon="trash" className="text-white" />
</AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader>
@@ -61,7 +61,10 @@ export function ActionButtonGroup<E, U>({
<AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
className={buttonVariants({
variant: "destructive",
className: "text-white",
})}
onClick={handleDelete}
>
{t("Confirm")}
@@ -95,7 +98,7 @@ export function BlockButtonGroup<E, U>({
{children}
<AlertDialog>
<AlertDialogTrigger asChild>
<IconButton variant="destructive" icon="ban" />
<IconButton variant="destructive" icon="ban" className="text-white" />
</AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader>
@@ -107,7 +110,10 @@ export function BlockButtonGroup<E, U>({
<AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
className={buttonVariants({
variant: "destructive",
className: "text-white",
})}
onClick={handleBlock}
>
{t("Confirm")}
+461 -30
View File
@@ -32,11 +32,10 @@ import {
import { IconButton } from "@/components/xui/icon-button"
import { useNotification } from "@/hooks/useNotfication"
import { conv } from "@/lib/utils"
import { asOptionalField } from "@/lib/utils"
import { ModelAlertRule } from "@/types"
import { triggerModes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -53,16 +52,16 @@ interface AlertRuleCardProps {
const ruleSchema = z.object({
type: z.string(),
min: asOptionalField(z.number()),
max: asOptionalField(z.number()),
cycle_start: asOptionalField(z.string()),
cycle_interval: asOptionalField(z.number()),
cycle_unit: asOptionalField(z.enum(["hour", "day", "week", "month", "year"])),
duration: asOptionalField(z.number()),
min: z.number().optional(),
max: z.number().optional(),
cycle_start: z.string().optional(),
cycle_interval: z.number().optional(),
cycle_unit: z.enum(["hour", "day", "week", "month", "year"]).optional(),
duration: z.number().optional(),
cover: z.number().int().min(0),
ignore: asOptionalField(z.record(z.boolean())),
next_transfer_at: asOptionalField(z.record(z.string())),
last_cycle_status: asOptionalField(z.boolean()),
ignore: z.record(z.string(), z.boolean()).optional(),
next_transfer_at: z.record(z.string(), z.string()).optional(),
last_cycle_status: z.boolean().optional(),
})
const alertRuleFormSchema = z.object({
@@ -87,13 +86,16 @@ const alertRuleFormSchema = z.object({
recover_trigger_tasks_raw: z.string(),
notification_group_id: z.coerce.number().int(),
trigger_mode: z.coerce.number().int().min(0),
enable: asOptionalField(z.boolean()),
enable: z.boolean().optional(),
})
export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) => {
const { t } = useTranslation()
const form = useForm<z.infer<typeof alertRuleFormSchema>>({
resolver: zodResolver(alertRuleFormSchema),
type AlertRuleFormData = z.infer<typeof alertRuleFormSchema>
const form = useForm({
resolver: zodResolver(alertRuleFormSchema) as any,
defaultValues: data
? {
...data,
@@ -119,7 +121,28 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof alertRuleFormSchema>) => {
// 结构化规则编辑状态:从已有数据或 rules_raw 初始化
const initialRules = (() => {
try {
if (data?.rules) return data.rules as any[]
const raw = form.getValues("rules_raw")
return raw ? JSON.parse(raw) : []
} catch {
return []
}
})()
const [rulesUI, setRulesUI] = useState<any[]>(initialRules)
// 同步到 rules_raw(提交仍走 JSON 字符串)
useEffect(() => {
try {
form.setValue("rules_raw", JSON.stringify(rulesUI), { shouldDirty: true })
} catch {
// ignore
}
}, [rulesUI])
const onSubmit = async (values: AlertRuleFormData) => {
values.rules = JSON.parse(values.rules_raw)
values.fail_trigger_tasks = conv.strToArr(values.fail_trigger_tasks_raw).map(Number)
values.recover_trigger_tasks = conv.strToArr(values.recover_trigger_tasks_raw).map(Number)
@@ -161,7 +184,10 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
<form
onSubmit={form.handleSubmit(onSubmit as any)}
className="space-y-2 my-2"
>
<FormField
control={form.control}
name="name"
@@ -175,19 +201,424 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
</FormItem>
)}
/>
<FormField
control={form.control}
name="rules_raw"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Rules")}</FormLabel>
<FormControl>
<Textarea className="resize-y" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 结构化规则编辑器 */}
<FormItem>
<FormLabel>{t("Rules")}</FormLabel>
<div className="space-y-3">
{rulesUI.map((r, idx) => {
const isCycle =
typeof r.type === "string" &&
r.type.endsWith("_cycle")
const isOffline = r.type === "offline"
return (
<div
key={idx}
className="rounded-md border p-3 space-y-2"
>
{/* 类型选择 */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<div>
<Label className="text-sm">
{t("Type")}
</Label>
<Select
onValueChange={(val) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
type: val,
}
// 切换类型时,若不是周期型,清理周期字段
if (!val.endsWith("_cycle")) {
delete next[idx].cycle_start
delete next[idx]
.cycle_interval
delete next[idx].cycle_unit
}
setRulesUI(next)
}}
defaultValue={r.type || ""}
>
<SelectTrigger>
<SelectValue
placeholder={t("Select")}
/>
</SelectTrigger>
<SelectContent>
{/* 资源类 */}
<SelectItem value="cpu">
cpu
</SelectItem>
<SelectItem value="gpu">
gpu
</SelectItem>
<SelectItem value="memory">
memory
</SelectItem>
<SelectItem value="swap">
swap
</SelectItem>
<SelectItem value="disk">
disk
</SelectItem>
{/* 网络类 */}
<SelectItem value="net_in_speed">
net_in_speed
</SelectItem>
<SelectItem value="net_out_speed">
net_out_speed
</SelectItem>
<SelectItem value="net_all_speed">
net_all_speed
</SelectItem>
<SelectItem value="transfer_in">
transfer_in
</SelectItem>
<SelectItem value="transfer_out">
transfer_out
</SelectItem>
<SelectItem value="transfer_all">
transfer_all
</SelectItem>
{/* 系统类 */}
<SelectItem value="offline">
offline
</SelectItem>
<SelectItem value="load1">
load1
</SelectItem>
<SelectItem value="load5">
load5
</SelectItem>
<SelectItem value="load15">
load15
</SelectItem>
<SelectItem value="process_count">
process_count
</SelectItem>
{/* 连接数 */}
<SelectItem value="tcp_conn_count">
tcp_conn_count
</SelectItem>
<SelectItem value="udp_conn_count">
udp_conn_count
</SelectItem>
{/* 温度 */}
<SelectItem value="temperature_max">
temperature_max
</SelectItem>
{/* 特殊:周期流量 */}
<SelectItem value="transfer_in_cycle">
transfer_in_cycle
</SelectItem>
<SelectItem value="transfer_out_cycle">
transfer_out_cycle
</SelectItem>
<SelectItem value="transfer_all_cycle">
transfer_all_cycle
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-sm">
duration
</Label>
<Input
type="number"
value={r.duration ?? ""}
onChange={(e) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
duration: e.target.value
? Number(e.target.value)
: undefined,
}
setRulesUI(next)
}}
placeholder="10"
/>
</div>
</div>
{/* 阈值:offline 不需要 min/max */}
{!isOffline && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<div>
<Label className="text-sm">
min
</Label>
<Input
type="number"
value={r.min ?? ""}
onChange={(e) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
min: e.target.value
? Number(
e.target
.value,
)
: undefined,
}
setRulesUI(next)
}}
placeholder="0"
/>
</div>
<div>
<Label className="text-sm">
max
</Label>
<Input
type="number"
value={r.max ?? ""}
onChange={(e) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
max: e.target.value
? Number(
e.target
.value,
)
: undefined,
}
setRulesUI(next)
}}
placeholder="100"
/>
</div>
</div>
)}
{/* 覆盖/忽略 */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<div>
<Label className="text-sm">cover</Label>
<Select
onValueChange={(val) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
cover: Number(val),
}
setRulesUI(next)
}}
defaultValue={(
r.cover ?? 0
).toString()}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">
0
{t(
"AlertRules.CoverAllServers",
)}
</SelectItem>
<SelectItem value="1">
1
{t(
"AlertRules.IgnoreAllSelectSpecific",
)}
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-sm">
{t("AlertRules.IgnoreHint", {
server: t("Server"),
})}
</Label>
{/* 简化:以 JSON 对象输入 */}
<Textarea
className="resize-y"
value={(() => {
try {
return r.ignore
? JSON.stringify(
r.ignore,
)
: ""
} catch {
return ""
}
})()}
onChange={(e) => {
const next = [...rulesUI]
try {
const obj = e.target.value
? JSON.parse(
e.target.value,
)
: undefined
next[idx] = {
...next[idx],
ignore: obj,
}
} catch {
// 保持原值,避免无效 JSON 覆盖
}
setRulesUI(next)
}}
placeholder={t(
"AlertRules.IgnoreExample",
)}
/>
</div>
</div>
{/* 周期型字段 */}
{isCycle && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="sm:col-span-2">
<Label className="text-sm">
cycle_start (RFC3339)
</Label>
<Input
value={r.cycle_start ?? ""}
onChange={(e) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
cycle_start:
e.target.value ||
undefined,
}
setRulesUI(next)
}}
placeholder="2022-01-01T00:00:00+08:00"
/>
</div>
<div>
<Label className="text-sm">
cycle_interval
</Label>
<Input
type="number"
value={r.cycle_interval ?? ""}
onChange={(e) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
cycle_interval: e.target
.value
? Number(
e.target
.value,
)
: undefined,
}
setRulesUI(next)
}}
placeholder="1"
/>
</div>
<div>
<Label className="text-sm">
cycle_unit
</Label>
<Select
onValueChange={(val) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
cycle_unit: val,
}
setRulesUI(next)
}}
defaultValue={
r.cycle_unit || "month"
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hour">
hour
</SelectItem>
<SelectItem value="day">
day
</SelectItem>
<SelectItem value="week">
week
</SelectItem>
<SelectItem value="month">
month
</SelectItem>
<SelectItem value="year">
year
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
<div className="flex justify-between">
<Button
type="button"
variant="secondary"
onClick={() => {
const next = [...rulesUI]
next.splice(idx, 1)
setRulesUI(next)
}}
>
{t("Delete")}
</Button>
{/* 占位以对齐 */}
<span />
</div>
</div>
)
})}
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={() => {
setRulesUI([
...rulesUI,
{ type: "", cover: 0, duration: 10 },
])
}}
>
{t("Add")}
</Button>
</div>
</div>
{/* 高级:直接编辑 JSON(与结构化编辑器同步) */}
<FormLabel className="mt-3">{t("AdvancedJSON")}</FormLabel>
<FormControl>
<Textarea
className="resize-y"
value={form.watch("rules_raw")}
onChange={(e) => {
// 同步到结构化编辑器
form.setValue("rules_raw", e.target.value, {
shouldDirty: true,
})
try {
const arr = JSON.parse(e.target.value)
if (Array.isArray(arr)) setRulesUI(arr)
} catch {
// ignore invalid
}
}}
/>
</FormControl>
</FormItem>
<FormField
control={form.control}
name="notification_group_id"
@@ -196,7 +627,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
<FormLabel>{t("NotifierGroup")}</FormLabel>
<FormControl>
<Combobox
placeholder="Search..."
placeholder={t("Search")}
options={ngroupList}
onValueChange={field.onChange}
defaultValue={field.value.toString()}
+13 -6
View File
@@ -17,13 +17,17 @@ import { IconButton } from "@/components/xui/icon-button"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { Textarea } from "./ui/textarea"
interface BatchMoveServerIconProps extends ButtonProps {
serverIds: number[]
}
export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ serverIds, ...props }) => {
export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({
serverIds,
...props
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [toUserId, setToUserId] = useState<number | undefined>(undefined)
@@ -32,7 +36,7 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ server
try {
await batchMoveServer({
ids: serverIds,
to_user: toUserId!
to_user: toUserId!,
})
} catch (e) {
console.error(e)
@@ -69,9 +73,7 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ server
</DialogHeader>
<div className="flex flex-col gap-3 mt-4">
<Label>{t("Servers")}</Label>
<Textarea disabled>
{serverIds.join(", ")}
</Textarea>
<Textarea disabled>{serverIds.join(", ")}</Textarea>
<Label>{t("ToUser")}</Label>
<Input
type="number"
@@ -87,7 +89,12 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ server
{t("Cancel")}
</Button>
</DialogClose>
<Button disabled={!toUserId || toUserId == 0} type="submit" className="my-2" onClick={onSubmit}>
<Button
disabled={!toUserId || toUserId == 0}
type="submit"
className="my-2"
onClick={onSubmit}
>
{t("Move")}
</Button>
</DialogFooter>
+18 -5
View File
@@ -63,18 +63,31 @@ const cronFormSchema = z.object({
notification_group_id: z.coerce.number().int(),
})
type CronFormData = z.infer<typeof cronFormSchema>
export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
const { t } = useTranslation()
const form = useForm<z.infer<typeof cronFormSchema>>({
resolver: zodResolver(cronFormSchema),
const form = useForm<CronFormData>({
resolver: zodResolver(cronFormSchema as any),
defaultValues: data
? data
? {
task_type: data.task_type ?? 0,
name: data.name ?? "",
scheduler: data.scheduler ?? "",
command: (data as any).command ?? "",
servers: data.servers ?? [],
cover: data.cover ?? 0,
push_successful: (data as any).push_successful ?? false,
notification_group_id: data.notification_group_id ?? 0,
}
: {
name: "",
task_type: 0,
name: "",
scheduler: "",
command: "",
servers: [],
cover: 0,
push_successful: false,
notification_group_id: 0,
},
resetOptions: {
@@ -84,7 +97,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof cronFormSchema>) => {
const onSubmit = async (values: CronFormData) => {
try {
data?.id ? await updateCron(data.id, values) : await createCron(values)
} catch (e) {
+28 -5
View File
@@ -67,21 +67,44 @@ const ddnsFormSchema = z.object({
webhook_headers: asOptionalField(z.string()),
})
type DDNSFormData = z.infer<typeof ddnsFormSchema>
export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) => {
const { t } = useTranslation()
const form = useForm<z.infer<typeof ddnsFormSchema>>({
resolver: zodResolver(ddnsFormSchema),
const form = useForm<DDNSFormData>({
resolver: zodResolver(ddnsFormSchema as any),
defaultValues: data
? {
...data,
domains_raw: conv.arrToStr(data.domains),
max_retries: data.max_retries ?? 3,
enable_ipv4: (data as any).enable_ipv4 ?? false,
enable_ipv6: (data as any).enable_ipv6 ?? false,
name: data.name ?? "",
provider: data.provider ?? "dummy",
domains: data.domains ?? [],
domains_raw: conv.arrToStr(data.domains ?? []),
access_id: (data as any).access_id ?? "",
access_secret: (data as any).access_secret ?? "",
webhook_url: (data as any).webhook_url ?? "",
webhook_method: (data as any).webhook_method,
webhook_request_type: (data as any).webhook_request_type,
webhook_request_body: (data as any).webhook_request_body ?? "",
webhook_headers: (data as any).webhook_headers ?? "",
}
: {
max_retries: 3,
enable_ipv4: false,
enable_ipv6: false,
name: "",
provider: "dummy",
domains: [],
domains_raw: "",
access_id: "",
access_secret: "",
webhook_url: "",
webhook_method: undefined,
webhook_request_type: undefined,
webhook_request_body: "",
webhook_headers: "",
},
resetOptions: {
keepDefaultValues: false,
@@ -90,7 +113,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof ddnsFormSchema>) => {
const onSubmit = async (values: DDNSFormData) => {
try {
values.domains = conv.strToArr(values.domains_raw)
data?.id ? await updateDDNSProfile(data.id, values) : await createDDNSProfile(values)
+1 -1
View File
@@ -37,7 +37,7 @@ import {
import { ColumnDef } from "@tanstack/react-table"
import { Row, flexRender } from "@tanstack/react-table"
import { File, Folder } from "lucide-react"
import { HTMLAttributes, useEffect, useRef, useState } from "react"
import { HTMLAttributes, JSX, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
+12 -4
View File
@@ -51,6 +51,7 @@ export function HeaderButtonGroup<E, U>({
<IconButton
variant="destructive"
icon="trash"
className="text-white"
onClick={() => {
toast(t("Error"), {
description: t("Results.NoRowsAreSelected"),
@@ -63,7 +64,7 @@ export function HeaderButtonGroup<E, U>({
<>
<AlertDialog>
<AlertDialogTrigger asChild>
<IconButton variant="destructive" icon="trash" />
<IconButton variant="destructive" icon="trash" className="text-white" />
</AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader>
@@ -75,7 +76,10 @@ export function HeaderButtonGroup<E, U>({
<AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
className={buttonVariants({
variant: "destructive",
className: "text-white",
})}
onClick={handleDelete}
>
{t("Confirm")}
@@ -114,6 +118,7 @@ export function HeaderBlockButtonGroup<E, U>({
<IconButton
variant="destructive"
icon="ban"
className="text-white"
onClick={() => {
toast(t("Error"), {
description: t("Results.NoRowsAreSelected"),
@@ -126,7 +131,7 @@ export function HeaderBlockButtonGroup<E, U>({
<>
<AlertDialog>
<AlertDialogTrigger asChild>
<IconButton variant="destructive" icon="ban" />
<IconButton variant="destructive" icon="ban" className="text-white" />
</AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader>
@@ -138,7 +143,10 @@ export function HeaderBlockButtonGroup<E, U>({
<AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
className={buttonVariants({
variant: "destructive",
className: "text-white",
})}
onClick={handleBlock}
>
{t("Confirm")}
+16 -18
View File
@@ -1,4 +1,6 @@
import { ModeToggle } from "@/components/mode-toggle"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
Drawer,
DrawerClose,
@@ -9,12 +11,24 @@ import {
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuLink,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
import { IconButton } from "@/components/xui/icon-button"
import { NzNavigationMenuLink } from "@/components/xui/navigation-menu"
import { useAuth } from "@/hooks/useAuth"
import { useMainStore } from "@/hooks/useMainStore"
import { useMediaQuery } from "@/hooks/useMediaQuery"
@@ -26,21 +40,6 @@ import { useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { Link, useLocation, useNavigate } from "react-router-dom"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { IconButton } from "@/components/xui/icon-button"
import { NzNavigationMenuLink } from "@/components/xui/navigation-menu"
// =======================================================
// vvvvvvvvvvv 1. 在这里为移动端菜单添加新页面 vvvvvvvvvvv
const pages = [
@@ -252,7 +251,7 @@ export default function Header() {
</NavigationMenuItem>
{/* ^^^^^^^^^^^ 2. 在这里为桌面端菜单添加新链接 ^^^^^^^^^^^ */}
{/* ======================================================= */}
<NavigationMenuItem>
<NzNavigationMenuLink
asChild
@@ -423,9 +422,8 @@ function Overview() {
{!profile && <p className="text-sm font-semibold">{t("LoginFirst")}</p>}
<div className="flex items-center gap-1.5">
<p className="text-[13px] font-medium opacity-50">{t("CurrentTime")}</p>
<p className="opacity-1 text-[13px] font-medium">{timeString}</p>
<p className="opacity-100 text-[13px] font-medium">{timeString}</p>
</div>
</section>
)
}
+116 -61
View File
@@ -10,7 +10,7 @@ import useSettings from "@/hooks/useSetting"
import { copyToClipboard } from "@/lib/utils"
import { ModelProfile, ModelSetting } from "@/types"
import i18next from "i18next"
import { Check, Clipboard } from "lucide-react"
import { Check, Copy, Download } from "lucide-react"
import { forwardRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -21,82 +21,137 @@ enum OSTypes {
Windows,
}
export const InstallCommandsMenu = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const [copy, setCopy] = useState(false)
const { data: settings } = useSettings()
const { profile } = useAuth()
type InstallCommandsMenuProps = ButtonProps & {
uuid?: string
iconOnly?: boolean
menuItem?: boolean
}
const { t } = useTranslation()
export const InstallCommandsMenu = forwardRef<HTMLButtonElement, InstallCommandsMenuProps>(
({ uuid, iconOnly = false, menuItem = false, ...props }, ref) => {
const [copy, setCopy] = useState(false)
const { data: settings } = useSettings()
const { profile } = useAuth()
const switchState = async (type: number) => {
if (!copy) {
try {
setCopy(true)
if (!profile) throw new Error("Profile is not found.")
if (!settings?.config) throw new Error("Settings is not found.")
await copyToClipboard(generateCommand(type, settings!.config, profile) || "")
} catch (e: Error | any) {
console.error(e)
toast(t("Error"), {
description: e.message,
})
} finally {
setTimeout(() => {
setCopy(false)
}, 2 * 1000)
const { t } = useTranslation()
const switchState = async (type: number) => {
if (!copy) {
try {
setCopy(true)
if (!profile) throw new Error("Profile is not found.")
if (!settings?.config) throw new Error("Settings is not found.")
await copyToClipboard(
generateCommand(type, settings!.config, profile, uuid) || "",
)
} catch (e: Error | any) {
console.error(e)
toast(t("Error"), {
description: e.message,
})
} finally {
setTimeout(() => {
setCopy(false)
}, 2 * 1000)
}
}
}
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button {...props} ref={ref}>
{copy ? <Check /> : <Clipboard />}
{t("InstallCommands")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="nezha-copy"
onClick={async () => {
switchState(OSTypes.Linux)
}}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
{menuItem ? (
<button
type="button"
className="flex w-full items-center text-sm px-2 py-2 hover:bg-accent hover:text-accent-foreground"
title={i18next.t("InstallCommands")}
>
{copy ? (
<Check className="h-4 w-4 mr-2" />
) : (
<Copy className="h-4 w-4 mr-2" />
)}
<span>{i18next.t("InstallCommands")}</span>
</button>
) : iconOnly ? (
<Button
ref={ref}
title={i18next.t("InstallCommands")}
size="icon"
{...props}
>
{copy ? (
<Check className="h-4 w-4" />
) : (
<Download className="h-4 w-4" />
)}
</Button>
) : (
<Button ref={ref} title={i18next.t("InstallCommands")} {...props}>
{copy ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
<span className="ml-2">{i18next.t("InstallCommands")}</span>
</Button>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
side={menuItem ? "right" : undefined}
align={menuItem ? "start" : undefined}
>
Linux
</DropdownMenuItem>
<DropdownMenuItem
className="nezha-copy"
onClick={async () => {
switchState(OSTypes.macOS)
}}
>
macOS
</DropdownMenuItem>
<DropdownMenuItem
className="nezha-copy"
onClick={async () => {
switchState(OSTypes.Windows)
}}
>
Windows
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
})
<DropdownMenuItem
className="nezha-copy"
onClick={async () => {
switchState(OSTypes.Linux)
}}
>
Linux
</DropdownMenuItem>
<DropdownMenuItem
className="nezha-copy"
onClick={async () => {
switchState(OSTypes.macOS)
}}
>
macOS
</DropdownMenuItem>
<DropdownMenuItem
className="nezha-copy"
onClick={async () => {
switchState(OSTypes.Windows)
}}
>
Windows
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
)
const generateCommand = (
type: number,
{ install_host, tls }: ModelSetting,
{ agent_secret }: ModelProfile,
uuid?: string,
) => {
if (!install_host) throw new Error(i18next.t("Results.InstallHostRequired"))
if (!agent_secret) throw new Error(i18next.t("Results.AgentSecretRequired"))
const env = `NZ_SERVER=${install_host} NZ_TLS=${tls || false} NZ_CLIENT_SECRET=${agent_secret}`
const env_win = `$env:NZ_SERVER=\"${install_host}\";$env:NZ_TLS=\"${tls || false}\";$env:NZ_CLIENT_SECRET=\"${agent_secret}\";`
const envParts = [
`NZ_SERVER=${install_host}`,
`NZ_TLS=${tls || false}`,
`NZ_CLIENT_SECRET=${agent_secret}`,
]
if (uuid) envParts.push(`NZ_UUID=${uuid}`)
const env = envParts.join(" ")
const envWinParts = [
`$env:NZ_SERVER=\"${install_host}\";`,
`$env:NZ_TLS=\"${tls || false}\";`,
`$env:NZ_CLIENT_SECRET=\"${agent_secret}\";`,
]
if (uuid) envWinParts.push(`$env:NZ_UUID=\"${uuid}\";`)
const env_win = envWinParts.join("")
switch (type) {
case OSTypes.Linux:
+12 -4
View File
@@ -46,12 +46,20 @@ const natFormSchema = z.object({
domain: z.string(),
})
type NatFormData = z.infer<typeof natFormSchema>
export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
const { t } = useTranslation()
const form = useForm<z.infer<typeof natFormSchema>>({
resolver: zodResolver(natFormSchema),
const form = useForm<NatFormData>({
resolver: zodResolver(natFormSchema as any),
defaultValues: data
? data
? {
name: data.name ?? "",
enabled: (data as any).enabled ?? false,
server_id: data.server_id ?? 0,
host: data.host ?? "",
domain: data.domain ?? "",
}
: {
name: "",
enabled: false,
@@ -66,7 +74,7 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof natFormSchema>) => {
const onSubmit = async (values: NatFormData) => {
try {
data?.id ? await updateNAT(data.id, values) : await createNAT(values)
} catch (e) {
+232 -123
View File
@@ -30,7 +30,6 @@ import {
SelectValue,
} from "@/components/ui/select"
import { IconButton } from "@/components/xui/icon-button"
import { asOptionalField } from "@/lib/utils"
import { ModelNotification } from "@/types"
import { nrequestMethods, nrequestTypes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
@@ -50,21 +49,36 @@ interface NotifierCardProps {
const notificationFormSchema = z.object({
name: z.string().min(1),
url: z.string().url(),
url: z.string().min(1),
request_method: z.coerce.number().int().min(1).max(255),
request_type: z.coerce.number().int().min(1).max(255),
request_header: z.string(),
request_body: z.string(),
verify_tls: asOptionalField(z.boolean()),
skip_check: asOptionalField(z.boolean()),
verify_tls: z.boolean().default(false),
skip_check: z.boolean().default(false),
format_metric_units: z.boolean().default(false),
type: z.coerce.number().int().default(1),
})
export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
const { t } = useTranslation()
const form = useForm<z.infer<typeof notificationFormSchema>>({
resolver: zodResolver(notificationFormSchema),
type notificationFormData = z.infer<typeof notificationFormSchema>
const form = useForm({
resolver: zodResolver(notificationFormSchema) as any,
defaultValues: data
? data
? {
name: data.name ?? "",
url: data.url ?? "",
request_method: data.request_method ?? 1,
request_type: data.request_type ?? 1,
request_header: data.request_header ?? "",
request_body: data.request_body ?? "",
verify_tls: data.verify_tls ?? false,
skip_check: data.skip_check ?? false,
format_metric_units: data.format_metric_units ?? false,
type: data.type ?? 1,
}
: {
name: "",
url: "",
@@ -72,6 +86,10 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
request_type: 1,
request_header: "",
request_body: "",
verify_tls: false,
skip_check: false,
format_metric_units: false,
type: 1,
},
resetOptions: {
keepDefaultValues: false,
@@ -80,7 +98,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof notificationFormSchema>) => {
const onSubmit = async (values: notificationFormData) => {
try {
data?.id ? await updateNotification(data.id, values) : await createNotification(values)
} catch (e) {
@@ -110,7 +128,10 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
<DialogDescription />
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
<form
onSubmit={form.handleSubmit(onSubmit as any)}
className="space-y-2 my-2"
>
<FormField
control={form.control}
name="name"
@@ -124,85 +145,141 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Notification Type</FormLabel>
<Select
onValueChange={field.onChange}
value={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="1">Webhook</SelectItem>
<SelectItem value="2">SMTP (Email)</SelectItem>
<SelectItem value="3">Telegram</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>URL</FormLabel>
<FormLabel>
{form.watch("type") == 2
? "SMTP Server (host:port)"
: form.watch("type") == 3
? "Bot Token"
: "URL"}
</FormLabel>
<FormControl>
<Input {...field} />
<Input
{...field}
placeholder={
form.watch("type") == 3
? "123456:ABC-DEF"
: ""
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="request_method"
render={({ field }) => (
<FormItem>
<FormLabel>{t("RequestMethod")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Request Method" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(nrequestMethods).map(
([k, v]) => (
<SelectItem key={k} value={k}>
{v}
</SelectItem>
),
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="request_type"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Type")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Request Type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(nrequestTypes).map(([k, v]) => (
<SelectItem key={k} value={k}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{form.watch("type") != 2 && form.watch("type") != 3 && (
<>
<FormField
control={form.control}
name="request_method"
render={({ field }) => (
<FormItem>
<FormLabel>{t("RequestMethod")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Request Method" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(nrequestMethods).map(
([k, v]) => (
<SelectItem key={k} value={k}>
{v}
</SelectItem>
),
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="request_type"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Type")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Request Type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(nrequestTypes).map(
([k, v]) => (
<SelectItem key={k} value={k}>
{v}
</SelectItem>
),
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</>
)}
<FormField
control={form.control}
name="request_header"
render={({ field }) => (
<FormItem>
<FormLabel>{t("RequestHeader")}</FormLabel>
<FormLabel>
{form.watch("type") == 2
? "SMTP User:Pass"
: form.watch("type") == 3
? "Chat ID"
: t("RequestHeader")}
</FormLabel>
<FormControl>
<Textarea
className="resize-y"
placeholder='{"User-Agent":"Nezha-Agent"}'
placeholder={
form.watch("type") == 2
? "user:pass"
: form.watch("type") == 3
? "123456789"
: '{"User-Agent":"Nezha-Agent"}'
}
{...field}
/>
</FormControl>
@@ -210,63 +287,95 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="request_body"
render={({ field }) => (
<FormItem>
<FormLabel>{t("RequestBody")}</FormLabel>
<FormControl>
<Textarea
className="resize-y h-[240px]"
placeholder='{&#13;&#10; "content":"#NEZHA#",&#13;&#10; "ServerName":"#SERVER.NAME#",&#13;&#10; "ServerIP":"#SERVER.IP#",&#13;&#10; "ServerIPV4":"#SERVER.IPV4#",&#13;&#10; "ServerIPV6":"#SERVER.IPV6#",&#13;&#10; "CPU":"#SERVER.CPU#",&#13;&#10; "MEM":"#SERVER.MEM#",&#13;&#10; "SWAP":"#SERVER.SWAP#",&#13;&#10; "DISK":"#SERVER.DISK#",&#13;&#10; "NetInSpeed":"#SERVER.NETINSPEED#",&#13;&#10; "NetOutSpeed":"#SERVER.NETOUTSPEED#",&#13;&#10; "TransferIn":"#SERVER.TRANSFERIN#",&#13;&#10; "TranferOut":"#SERVER.TRANSFEROUT#",&#13;&#10; "Load1":"#SERVER.LOAD1#",&#13;&#10; "Load5":"#SERVER.LOAD5#",&#13;&#10; "Load15":"#SERVER.LOAD15#",&#13;&#10; "TCP_CONN_COUNT":"#SERVER.TCPCONNCOUNT",&#13;&#10; "UDP_CONN_COUNT":"#SERVER.UDPCONNCOUNT"&#13;&#10;}'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="verify_tls"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
{form.watch("type") != 3 && (
<FormField
control={form.control}
name="request_body"
render={({ field }) => (
<FormItem>
<FormLabel>
{form.watch("type") == 2
? "Recipient Email"
: t("RequestBody")}
</FormLabel>
<FormControl>
<Textarea
className={
form.watch("type") == 2
? "resize-y"
: "resize-y h-[240px]"
}
placeholder={
form.watch("type") == 2
? "target@example.com"
: "..."
}
{...field}
/>
<Label className="text-sm">
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="pt-4 border-t space-y-3">
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
{t("AdvancedSettings")}
</Label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-2">
<FormField
control={form.control}
name="verify_tls"
render={({ field }) => (
<FormItem className="flex items-center space-x-2 space-y-0 py-1">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="text-sm font-normal cursor-pointer">
{t("VerifyTLS")}
</Label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="skip_check"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="skip_check"
render={({ field }) => (
<FormItem className="flex items-center space-x-2 space-y-0 py-1">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="text-sm font-normal cursor-pointer">
{t("DoNotSendTestMessage")}
</Label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</FormLabel>
</FormItem>
)}
/>
<FormField
control={form.control}
name="format_metric_units"
render={({ field }) => (
<FormItem className="flex items-center space-x-2 space-y-0 py-1">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="text-sm font-normal cursor-pointer">
{t("FormatMetricUnits")}
</FormLabel>
</FormItem>
)}
/>
</div>
</div>
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
+3 -3
View File
@@ -42,8 +42,8 @@ export const ProfileCard = ({ className }: { className: string }) => {
const { t } = useTranslation()
const { profile, setProfile } = useMainStore()
const form = useForm<z.infer<typeof profileFormSchema>>({
resolver: zodResolver(profileFormSchema),
const form = useForm({
resolver: zodResolver(profileFormSchema) as any,
defaultValues: {
original_password: "",
new_password: "",
@@ -57,7 +57,7 @@ export const ProfileCard = ({ className }: { className: string }) => {
const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof profileFormSchema>) => {
const onSubmit = async (values: any) => {
try {
await updateProfile(values)
} catch (e) {
+30 -13
View File
@@ -27,6 +27,7 @@ import { IconButton } from "@/components/xui/icon-button"
import { asOptionalField } from "@/lib/utils"
import { ModelServerTaskResponse } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { CogIcon } from "lucide-react"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
@@ -58,7 +59,7 @@ const agentConfigSchema = z.object({
),
),
ip_report_period: asOptionalField(z.coerce.number().int().min(30)),
nic_allowlist: asOptionalField(z.record(z.boolean())),
nic_allowlist: asOptionalField(z.record(z.string(), z.boolean())),
nic_allowlist_raw: asOptionalField(
z.string().refine(
(val) => {
@@ -102,9 +103,10 @@ for (let i = 0; i < boolFields.length; i += 2) {
interface ServerConfigCardProps extends ButtonProps {
sid: number
menuItem?: boolean
}
export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
export const ServerConfigCard = ({ sid, menuItem = false, ...props }: ServerConfigCardProps) => {
const { t } = useTranslation()
const [data, setData] = useState<AgentConfig | undefined>(undefined)
const [loading, setLoading] = useState(true)
@@ -129,8 +131,8 @@ export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
if (open) fetchData()
}, [open])
const form = useForm<AgentConfig>({
resolver: zodResolver(agentConfigSchema),
const form = useForm({
resolver: zodResolver(agentConfigSchema) as any,
defaultValues: {
...data,
hard_drive_partition_allowlist_raw: JSON.stringify(
@@ -155,16 +157,20 @@ export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
}
}, [data, form])
const onSubmit = async (values: AgentConfig) => {
const onSubmit = async (values: any) => {
let resp: ModelServerTaskResponse = {}
try {
values.nic_allowlist = values.nic_allowlist_raw
? JSON.parse(values.nic_allowlist_raw)
const submitValues = { ...values }
submitValues.nic_allowlist = submitValues.nic_allowlist_raw
? JSON.parse(submitValues.nic_allowlist_raw)
: undefined
values.hard_drive_partition_allowlist = values.hard_drive_partition_allowlist_raw
? JSON.parse(values.hard_drive_partition_allowlist_raw)
: undefined
resp = await setServerConfig({ config: JSON.stringify(values), servers: [sid] })
submitValues.hard_drive_partition_allowlist =
submitValues.hard_drive_partition_allowlist_raw
? JSON.parse(submitValues.hard_drive_partition_allowlist_raw)
: undefined
delete submitValues.nic_allowlist_raw
delete submitValues.hard_drive_partition_allowlist_raw
resp = await setServerConfig({ config: JSON.stringify(submitValues), servers: [sid] })
} catch (e) {
console.error(e)
toast(t("Error"), {
@@ -186,7 +192,18 @@ export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<IconButton {...props} icon="cog" />
{menuItem ? (
<button
type="button"
className="flex w-full items-center text-sm px-2 py-2 hover:bg-accent hover:text-accent-foreground"
onClick={() => setOpen(true)}
>
<CogIcon className="h-4 w-4 mr-2" />
<span>{t("Config")}</span>
</button>
) : (
<IconButton {...props} icon="cog" />
)}
</DialogTrigger>
<DialogContent className="sm:max-w-xl">
{loading ? (
@@ -283,7 +300,7 @@ export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
<div className="flex items-center gap-2">
<Checkbox
checked={
controllerField.value as boolean
!!controllerField.value
}
onCheckedChange={
controllerField.onChange
+105 -13
View File
@@ -28,7 +28,7 @@ import { conv } from "@/lib/utils"
import { asOptionalField } from "@/lib/utils"
import { ModelServer } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@@ -65,12 +65,19 @@ const serverFormSchema = z.object({
},
),
),
billing_data: z
.object({
registrar: asOptionalField(z.string()),
endDate: asOptionalField(z.string()),
notes: asOptionalField(z.string()),
})
.optional(),
})
export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
const { t } = useTranslation()
const form = useForm<z.infer<typeof serverFormSchema>>({
resolver: zodResolver(serverFormSchema),
resolver: zodResolver(serverFormSchema) as any,
defaultValues: {
...data,
ddns_profiles_raw: data.ddns_profiles ? conv.arrToStr(data.ddns_profiles) : undefined,
@@ -85,7 +92,20 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof serverFormSchema>) => {
useEffect(() => {
const handleMessage = (e: MessageEvent) => {
if (e.data?.type === "NZCFG_JSON") {
if (e.data.target === "public_note") {
form.setValue("public_note", e.data.payload)
toast(t("Success"), { description: "配置已通过可视化构建器自动填入" })
}
}
}
window.addEventListener("message", handleMessage)
return () => window.removeEventListener("message", handleMessage)
}, [form, t])
const onSubmit = async (values: any) => {
try {
values.ddns_profiles = values.ddns_profiles_raw
? conv.strToArr(values.ddns_profiles_raw).map(Number)
@@ -118,10 +138,10 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
<DialogTitle>{t("EditServer")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
<Form {...(form as any)}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
<FormField
control={form.control}
control={form.control as any}
name="name"
render={({ field }) => (
<FormItem>
@@ -134,7 +154,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
)}
/>
<FormField
control={form.control}
control={form.control as any}
name="display_index"
render={({ field }) => (
<FormItem>
@@ -149,7 +169,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
{form.watch("enable_ddns") ? (
<>
<FormField
control={form.control}
control={form.control as any}
name="ddns_profiles_raw"
render={({ field }) => (
<FormItem>
@@ -164,7 +184,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
)}
/>
<FormField
control={form.control}
control={form.control as any}
name="override_ddns_domains_raw"
render={({ field }) => (
<FormItem>
@@ -184,7 +204,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
)}
<FormField
control={form.control}
control={form.control as any}
name="enable_ddns"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
@@ -204,7 +224,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
)}
/>
<FormField
control={form.control}
control={form.control as any}
name="hide_for_guest"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
@@ -224,7 +244,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
)}
/>
<FormField
control={form.control}
control={form.control as any}
name="note"
render={({ field }) => (
<FormItem>
@@ -236,12 +256,84 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
</FormItem>
)}
/>
<div className="p-3 border rounded-md border-dashed space-y-2">
<Label className="text-xs text-muted-foreground uppercase font-bold">
Billing & Expiry
</Label>
<FormField
control={form.control as any}
name="billing_data.registrar"
render={({ field }) => (
<FormItem>
<FormLabel>Registrar</FormLabel>
<FormControl>
<Input
placeholder="AWS / Azure /阿里云"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control as any}
name="billing_data.endDate"
render={({ field }) => (
<FormItem>
<FormLabel>Expiry Date</FormLabel>
<FormControl>
<Input
type="date"
{...field}
value={field.value?.split("T")[0] || ""}
onChange={(e) =>
field.onChange(
e.target.value
? new Date(
e.target.value,
).toISOString()
: "",
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
control={form.control as any}
name="public_note"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Public") + t("Note")}</FormLabel>
<FormLabel className="flex justify-between items-center w-full">
<span>{t("Public") + t("Note")}</span>
<a
href="/dashboard/nzcfg.html"
target="_blank"
className="text-blue-500 hover:text-blue-700 text-xs flex items-center gap-1"
onClick={(e) => {
e.preventDefault()
const popup = window.open(
"/dashboard/nzcfg.html",
"nzcfg",
"width=1000,height=800",
)
if (popup) {
const timer = setInterval(() => {
if (popup.closed) {
clearInterval(timer)
}
}, 500)
}
}}
>
{" "}
<i className="fa-solid fa-up-right-from-square"></i>
</a>
</FormLabel>
<FormControl>
<Textarea className="resize-y" {...field} />
</FormControl>
+19 -4
View File
@@ -54,6 +54,7 @@ interface ServiceCardProps {
const serviceFormSchema = z.object({
cover: z.coerce.number().int().min(0),
display_index: z.coerce.number().int(),
duration: z.coerce.number().int().min(30),
enable_show_in_service: asOptionalField(z.boolean()),
enable_trigger_task: asOptionalField(z.boolean()),
@@ -67,7 +68,7 @@ const serviceFormSchema = z.object({
notify: asOptionalField(z.boolean()),
recover_trigger_tasks: z.array(z.number()),
recover_trigger_tasks_raw: z.string(),
skip_servers: z.record(z.boolean()),
skip_servers: z.record(z.string(), z.boolean()),
skip_servers_raw: z.array(z.string()),
target: z.string(),
type: z.coerce.number().int().min(0),
@@ -75,8 +76,8 @@ const serviceFormSchema = z.object({
export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
const { t } = useTranslation()
const form = useForm<z.infer<typeof serviceFormSchema>>({
resolver: zodResolver(serviceFormSchema),
const form = useForm({
resolver: zodResolver(serviceFormSchema) as any,
defaultValues: data
? {
...data,
@@ -87,6 +88,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
: {
type: 1,
cover: 0,
display_index: 0,
name: "",
target: "",
max_latency: 0.0,
@@ -107,7 +109,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof serviceFormSchema>) => {
const onSubmit = async (values: any) => {
values.skip_servers = conv.arrToRecord(values.skip_servers_raw)
values.fail_trigger_tasks = conv.strToArr(values.fail_trigger_tasks_raw).map(Number)
values.recover_trigger_tasks = conv.strToArr(values.recover_trigger_tasks_raw).map(Number)
@@ -172,6 +174,19 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="display_index"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Weight")}</FormLabel>
<FormControl>
<Input type="number" placeholder="0" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="target"
-190
View File
@@ -1,190 +0,0 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import useTerminal from "@/hooks/useTerminal"
import { sleep } from "@/lib/utils"
import { AttachAddon } from "@xterm/addon-attach"
import { FitAddon } from "@xterm/addon-fit"
import { Terminal } from "@xterm/xterm"
import "@xterm/xterm/css/xterm.css"
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"
import { useParams } from "react-router-dom"
import { toast } from "sonner"
import { FMCard } from "./fm"
import { Button } from "./ui/button"
import { IconButton } from "./xui/icon-button"
interface XtermProps {
wsUrl: string
setClose: React.Dispatch<React.SetStateAction<boolean>>
}
const XtermComponent = forwardRef<HTMLDivElement, XtermProps & JSX.IntrinsicElements["div"]>(
({ wsUrl, setClose, ...props }, ref) => {
const terminalIdRef = useRef<HTMLDivElement>(null)
const terminalRef = useRef<Terminal | null>(null)
const wsRef = useRef<WebSocket | null>(null)
useImperativeHandle(ref, () => {
return {
...terminalIdRef.current!,
async requestFullscreen() {
await terminalIdRef.current?.requestFullscreen()
},
}
}, [])
useEffect(() => {
return () => {
wsRef.current?.close()
terminalRef.current?.dispose()
}
}, [])
useEffect(() => {
terminalRef.current = new Terminal({
cursorBlink: true,
fontSize: 16,
})
const url = new URL(wsUrl, window.location.origin)
url.protocol = url.protocol.replace("http", "ws")
const ws = new WebSocket(url)
wsRef.current = ws
ws.binaryType = "arraybuffer"
ws.onopen = () => {
onResize()
}
ws.onclose = () => {
terminalRef.current?.dispose()
setClose(true)
}
ws.onerror = (e) => {
console.error(e)
toast("Websocket error", {
description: "View console for details.",
})
}
}, [wsUrl])
const fitAddon = useRef(new FitAddon()).current
const sendResize = useRef(false)
const doResize = () => {
if (!terminalIdRef.current) return
fitAddon.fit()
const dimensions = fitAddon.proposeDimensions()
if (dimensions) {
const prefix = new Int8Array([1])
const resizeMessage = new TextEncoder().encode(
JSON.stringify({
Rows: dimensions.rows,
Cols: dimensions.cols,
}),
)
const msg = new Int8Array(prefix.length + resizeMessage.length)
msg.set(prefix)
msg.set(resizeMessage, prefix.length)
wsRef.current?.send(msg)
}
}
const onResize = async () => {
if (sendResize.current) return
sendResize.current = true
try {
await sleep(1500)
doResize()
} catch (error) {
console.error("resize error", error)
} finally {
sendResize.current = false
}
}
useEffect(() => {
if (!wsRef.current || !terminalIdRef.current || !terminalRef.current) return
const attachAddon = new AttachAddon(wsRef.current)
terminalRef.current.loadAddon(attachAddon)
terminalRef.current.loadAddon(fitAddon)
terminalRef.current.open(terminalIdRef.current)
window.addEventListener("resize", onResize)
return () => {
window.removeEventListener("resize", onResize)
if (wsRef.current) {
wsRef.current.close()
}
}
}, [wsRef.current, terminalRef.current, terminalIdRef.current])
return <div ref={terminalIdRef} {...props} />
},
)
export const TerminalPage = () => {
const { id } = useParams<{ id: string }>()
const [open, setOpen] = useState(false)
const terminal = useTerminal(id ? parseInt(id) : undefined)
const terminalIdRef = useRef<HTMLDivElement>(null)
return (
<div className="px-8">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{`Terminal (${id})`}</h1>
<div className="flex-2 flex ml-auto gap-2">
<IconButton
icon="expand"
onClick={async () => {
await terminalIdRef.current?.requestFullscreen()
}}
/>
<FMCard id={id} />
</div>
</div>
{terminal?.session_id ? (
<XtermComponent
ref={terminalIdRef}
className="max-h-[60%] mb-5 overflow-auto"
wsUrl={`/api/v1/ws/terminal/${terminal?.session_id}`}
setClose={setOpen}
/>
) : (
<p>The server does not exist, or have not been connected yet.</p>
)}
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader>
<AlertDialogTitle>Session completed</AlertDialogTitle>
<AlertDialogDescription>
You may close this window now.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction asChild>
<Button onClick={window.close}>Close</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}
export const TerminalButton = ({ id }: { id: number }) => {
const handleOpenNewTab = () => {
window.open(`/dashboard/terminal/${id}`, "_blank")
}
return <IconButton variant="outline" icon="terminal" onClick={handleOpenNewTab} />
}
+15 -8
View File
@@ -1,4 +1,5 @@
import { createContext, useContext, useEffect, useState } from "react"
import { DateTime } from "luxon"
export type Theme = "dark" | "light" | "system"
@@ -30,22 +31,28 @@ export function ThemeProvider({
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
)
const [hour, setHour] = useState(() => DateTime.now().hour)
useEffect(() => {
const timer = setInterval(() => {
setHour(DateTime.now().hour)
}, 60000)
return () => clearInterval(timer)
}, [])
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
let effectiveTheme = theme
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
const isNight = hour >= 18 || hour < 6
effectiveTheme = isNight ? "dark" : "light"
}
root.classList.add(theme)
}, [theme])
root.classList.add(effectiveTheme)
}, [theme, hour])
const value = {
theme,
+22 -22
View File
@@ -1,7 +1,7 @@
import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, HTMLAttributes, forwardRef } from "react"
const AlertDialog = AlertDialogPrimitive.Root
@@ -9,9 +9,9 @@ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
const AlertDialogOverlay = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Overlay>,
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
@@ -24,9 +24,9 @@ const AlertDialogOverlay = React.forwardRef<
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
const AlertDialogContent = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Content>,
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
@@ -42,12 +42,12 @@ const AlertDialogContent = React.forwardRef<
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
const AlertDialogHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
const AlertDialogFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
@@ -55,9 +55,9 @@ const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDiv
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
const AlertDialogTitle = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Title>,
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
@@ -65,11 +65,11 @@ const AlertDialogTitle = React.forwardRef<
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
AlertDialogTitle.displayName = "AlertDialogTitle"
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
const AlertDialogDescription = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Description>,
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
@@ -79,17 +79,17 @@ const AlertDialogDescription = React.forwardRef<
))
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
const AlertDialogAction = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Action>,
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
const AlertDialogCancel = forwardRef<
ComponentRef<typeof AlertDialogPrimitive.Cancel>,
ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
+10 -10
View File
@@ -1,10 +1,10 @@
import { cn } from "@/lib/utils"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
const Avatar = forwardRef<
ComponentRef<typeof AvatarPrimitive.Root>,
ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
@@ -14,9 +14,9 @@ const Avatar = React.forwardRef<
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
const AvatarImage = forwardRef<
ComponentRef<typeof AvatarPrimitive.Image>,
ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
@@ -26,9 +26,9 @@ const AvatarImage = React.forwardRef<
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
const AvatarFallback = forwardRef<
ComponentRef<typeof AvatarPrimitive.Fallback>,
ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
+2 -3
View File
@@ -1,6 +1,6 @@
import { cn } from "@/lib/utils"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import { HTMLAttributes } from "react"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
@@ -23,8 +23,7 @@ const badgeVariants = cva(
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
+11 -11
View File
@@ -1,17 +1,17 @@
import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import * as React from "react"
import { ComponentProps, ComponentPropsWithoutRef, ReactNode, forwardRef } from "react"
const Breadcrumb = React.forwardRef<
const Breadcrumb = forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
ComponentPropsWithoutRef<"nav"> & {
separator?: ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
const BreadcrumbList = forwardRef<HTMLOListElement, ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
@@ -25,16 +25,16 @@ const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWi
)
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
const BreadcrumbItem = forwardRef<HTMLLIElement, ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
),
)
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
const BreadcrumbLink = forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
@@ -50,7 +50,7 @@ const BreadcrumbLink = React.forwardRef<
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
const BreadcrumbPage = forwardRef<HTMLSpanElement, ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
@@ -64,7 +64,7 @@ const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWit
)
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
const BreadcrumbSeparator = ({ children, className, ...props }: ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
@@ -76,7 +76,7 @@ const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentP
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
const BreadcrumbEllipsis = ({ className, ...props }: ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
+3 -4
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import { ButtonHTMLAttributes, forwardRef } from "react"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
@@ -31,12 +31,11 @@ const buttonVariants = cva(
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
+184
View File
@@ -0,0 +1,184 @@
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
import { ComponentProps, useEffect, useRef } from "react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: ComponentProps<typeof DayPicker> & {
buttonVariant?: ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className,
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav,
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_previous,
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_next,
),
month_caption: cn(
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
defaultClassNames.month_caption,
),
dropdowns: cn(
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns,
),
dropdown_root: cn(
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
defaultClassNames.dropdown_root,
),
dropdown: cn("bg-popover absolute inset-0 opacity-0", defaultClassNames.dropdown),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label,
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
defaultClassNames.weekday,
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-[--cell-size] select-none",
defaultClassNames.week_number_header,
),
week_number: cn(
"text-muted-foreground select-none text-[0.8rem]",
defaultClassNames.week_number,
),
day: cn(
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
defaultClassNames.day,
),
range_start: cn("bg-accent rounded-l-md", defaultClassNames.range_start),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today,
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside,
),
disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return <ChevronLeftIcon className={cn("size-4", className)} {...props} />
}
if (orientation === "right") {
return <ChevronRightIcon className={cn("size-4", className)} {...props} />
}
return <ChevronDownIcon className={cn("size-4", className)} {...props} />
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-[--cell-size] items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = useRef<HTMLButtonElement>(null)
useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className,
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }
+11 -12
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils"
import * as React from "react"
import { HTMLAttributes, forwardRef } from "react"
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
@@ -12,14 +12,14 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
)
Card.displayName = "Card"
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
),
)
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
@@ -30,22 +30,21 @@ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HT
)
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
))
const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
),
)
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
),
)
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
+4 -4
View File
@@ -1,11 +1,11 @@
import { cn } from "@/lib/utils"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
const Checkbox = forwardRef<
ComponentRef<typeof CheckboxPrimitive.Root>,
ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
+5 -5
View File
@@ -12,9 +12,9 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { cn } from "@/lib/utils"
import { Check, ChevronDown } from "lucide-react"
import * as React from "react"
import { ButtonHTMLAttributes, forwardRef, useState } from "react"
interface ComboboxProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
interface ComboboxProps extends ButtonHTMLAttributes<HTMLButtonElement> {
options: {
label: string
value: string
@@ -26,10 +26,10 @@ interface ComboboxProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
onValueChange: (value: string) => void
}
export const Combobox = React.forwardRef<HTMLButtonElement, ComboboxProps>(
export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(
({ options, placeholder, defaultValue, className, onValueChange, ...props }, ref) => {
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState(defaultValue)
const [open, setOpen] = useState(false)
const [value, setValue] = useState(defaultValue)
return (
<Popover open={open} onOpenChange={setOpen}>
+23 -23
View File
@@ -3,11 +3,11 @@ import { cn } from "@/lib/utils"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, HTMLAttributes, forwardRef } from "react"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
const Command = forwardRef<
ComponentRef<typeof CommandPrimitive>,
ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
@@ -32,9 +32,9 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
const CommandInput = forwardRef<
ComponentRef<typeof CommandPrimitive.Input>,
ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
@@ -51,9 +51,9 @@ const CommandInput = React.forwardRef<
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
const CommandList = forwardRef<
ComponentRef<typeof CommandPrimitive.List>,
ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
@@ -64,18 +64,18 @@ const CommandList = React.forwardRef<
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
const CommandEmpty = forwardRef<
ComponentRef<typeof CommandPrimitive.Empty>,
ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
const CommandGroup = forwardRef<
ComponentRef<typeof CommandPrimitive.Group>,
ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
@@ -89,9 +89,9 @@ const CommandGroup = React.forwardRef<
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
const CommandSeparator = forwardRef<
ComponentRef<typeof CommandPrimitive.Separator>,
ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
@@ -101,9 +101,9 @@ const CommandSeparator = React.forwardRef<
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
const CommandItem = forwardRef<
ComponentRef<typeof CommandPrimitive.Item>,
ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
@@ -117,7 +117,7 @@ const CommandItem = React.forwardRef<
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
const CommandShortcut = ({ className, ...props }: HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
+17 -17
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, HTMLAttributes, forwardRef } from "react"
const Dialog = DialogPrimitive.Root
@@ -11,9 +11,9 @@ const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
const DialogOverlay = forwardRef<
ComponentRef<typeof DialogPrimitive.Overlay>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
@@ -26,9 +26,9 @@ const DialogOverlay = React.forwardRef<
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
const DialogContent = forwardRef<
ComponentRef<typeof DialogPrimitive.Content>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
@@ -50,7 +50,7 @@ const DialogContent = React.forwardRef<
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
const DialogHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...props}
@@ -58,7 +58,7 @@ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
const DialogFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
@@ -66,9 +66,9 @@ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
const DialogTitle = forwardRef<
ComponentRef<typeof DialogPrimitive.Title>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
@@ -76,11 +76,11 @@ const DialogTitle = React.forwardRef<
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
DialogTitle.displayName = "DialogTitle"
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
const DialogDescription = forwardRef<
ComponentRef<typeof DialogPrimitive.Description>,
ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
@@ -88,7 +88,7 @@ const DialogDescription = React.forwardRef<
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
DialogDescription.displayName = "DialogDescription"
export {
Dialog,
+24 -18
View File
@@ -1,11 +1,17 @@
import { cn } from "@/lib/utils"
import * as React from "react"
import {
ComponentProps,
ComponentPropsWithoutRef,
ComponentRef,
HTMLAttributes,
forwardRef,
} from "react"
import { Drawer as DrawerPrimitive } from "vaul"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
}: ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
)
Drawer.displayName = "Drawer"
@@ -16,9 +22,9 @@ const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
const DrawerOverlay = forwardRef<
ComponentRef<typeof DrawerPrimitive.Overlay>,
ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
@@ -28,9 +34,9 @@ const DrawerOverlay = React.forwardRef<
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
const DrawerContent = forwardRef<
ComponentRef<typeof DrawerPrimitive.Content>,
ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
@@ -49,19 +55,19 @@ const DrawerContent = React.forwardRef<
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
const DrawerHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
const DrawerFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
const DrawerTitle = forwardRef<
ComponentRef<typeof DrawerPrimitive.Title>,
ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
@@ -69,11 +75,11 @@ const DrawerTitle = React.forwardRef<
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
DrawerTitle.displayName = "DrawerTitle"
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
const DrawerDescription = forwardRef<
ComponentRef<typeof DrawerPrimitive.Description>,
ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
@@ -81,7 +87,7 @@ const DrawerDescription = React.forwardRef<
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
DrawerDescription.displayName = "DrawerDescription"
export {
Drawer,
+26 -26
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, HTMLAttributes, forwardRef } from "react"
const DropdownMenu = DropdownMenuPrimitive.Root
@@ -15,9 +15,9 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
const DropdownMenuSubTrigger = forwardRef<
ComponentRef<typeof DropdownMenuPrimitive.SubTrigger>,
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
@@ -36,9 +36,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
))
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
const DropdownMenuSubContent = forwardRef<
ComponentRef<typeof DropdownMenuPrimitive.SubContent>,
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
@@ -51,9 +51,9 @@ const DropdownMenuSubContent = React.forwardRef<
))
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
const DropdownMenuContent = forwardRef<
ComponentRef<typeof DropdownMenuPrimitive.Content>,
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
@@ -69,9 +69,9 @@ const DropdownMenuContent = React.forwardRef<
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
const DropdownMenuItem = forwardRef<
ComponentRef<typeof DropdownMenuPrimitive.Item>,
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
@@ -87,9 +87,9 @@ const DropdownMenuItem = React.forwardRef<
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
const DropdownMenuCheckboxItem = forwardRef<
ComponentRef<typeof DropdownMenuPrimitive.CheckboxItem>,
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
@@ -110,9 +110,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
))
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
const DropdownMenuRadioItem = forwardRef<
ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
@@ -132,9 +132,9 @@ const DropdownMenuRadioItem = React.forwardRef<
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
const DropdownMenuLabel = forwardRef<
ComponentRef<typeof DropdownMenuPrimitive.Label>,
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
@@ -146,9 +146,9 @@ const DropdownMenuLabel = React.forwardRef<
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
const DropdownMenuSeparator = forwardRef<
ComponentRef<typeof DropdownMenuPrimitive.Separator>,
ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
@@ -158,7 +158,7 @@ const DropdownMenuSeparator = React.forwardRef<
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
const DropdownMenuShortcut = ({ className, ...props }: HTMLAttributes<HTMLSpanElement>) => {
return (
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
)
+66 -61
View File
@@ -2,7 +2,15 @@ import { Label } from "@/components/ui/label"
import { cn } from "@/lib/utils"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import * as React from "react"
import {
ComponentPropsWithoutRef,
ComponentRef,
HTMLAttributes,
createContext,
forwardRef,
useContext,
useId,
} from "react"
import {
Controller,
ControllerProps,
@@ -21,7 +29,7 @@ type FormFieldContextValue<
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)
const FormFieldContext = createContext<FormFieldContextValue>({} as FormFieldContextValue)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
@@ -37,8 +45,8 @@ const FormField = <
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const fieldContext = useContext(FormFieldContext)
const itemContext = useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
@@ -63,11 +71,11 @@ type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
const FormItemContext = createContext<FormItemContextValue>({} as FormItemContextValue)
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
const FormItem = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId()
const id = useId()
return (
<FormItemContext.Provider value={{ id }}>
@@ -78,9 +86,9 @@ const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivEl
)
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
const FormLabel = forwardRef<
ComponentRef<typeof LabelPrimitive.Root>,
ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
@@ -95,65 +103,62 @@ const FormLabel = React.forwardRef<
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
const FormControl = forwardRef<ComponentRef<typeof Slot>, ComponentPropsWithoutRef<typeof Slot>>(
({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
},
)
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
const FormDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
})
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
},
)
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
const FormMessage = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
},
)
FormMessage.displayName = "FormMessage"
export {
+15 -17
View File
@@ -1,23 +1,21 @@
import { cn } from "@/lib/utils"
import * as React from "react"
import { InputHTMLAttributes, forwardRef } from "react"
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
)
},
)
const Input = forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
)
})
Input.displayName = "Input"
export { Input }
+4 -4
View File
@@ -1,15 +1,15 @@
import { cn } from "@/lib/utils"
import * as LabelPrimitive from "@radix-ui/react-label"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
const Label = forwardRef<
ComponentRef<typeof LabelPrimitive.Root>,
ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
+19 -19
View File
@@ -2,11 +2,11 @@ import { cn } from "@/lib/utils"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
const NavigationMenu = forwardRef<
ComponentRef<typeof NavigationMenuPrimitive.Root>,
ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
@@ -19,9 +19,9 @@ const NavigationMenu = React.forwardRef<
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
const NavigationMenuList = forwardRef<
ComponentRef<typeof NavigationMenuPrimitive.List>,
ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
@@ -40,9 +40,9 @@ const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors hover:text-accent-foreground focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
const NavigationMenuTrigger = forwardRef<
ComponentRef<typeof NavigationMenuPrimitive.Trigger>,
ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
@@ -58,9 +58,9 @@ const NavigationMenuTrigger = React.forwardRef<
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
const NavigationMenuContent = forwardRef<
ComponentRef<typeof NavigationMenuPrimitive.Content>,
ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
@@ -75,9 +75,9 @@ NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
const NavigationMenuViewport = forwardRef<
ComponentRef<typeof NavigationMenuPrimitive.Viewport>,
ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
@@ -92,9 +92,9 @@ const NavigationMenuViewport = React.forwardRef<
))
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
const NavigationMenuIndicator = forwardRef<
ComponentRef<typeof NavigationMenuPrimitive.Indicator>,
ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
+8 -11
View File
@@ -1,9 +1,9 @@
import { ButtonProps, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import * as React from "react"
import { ComponentProps, forwardRef } from "react"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
const Pagination = ({ className, ...props }: ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
@@ -13,14 +13,14 @@ const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
)
Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
const PaginationContent = forwardRef<HTMLUListElement, ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
),
)
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(
const PaginationItem = forwardRef<HTMLLIElement, ComponentProps<"li">>(
({ className, ...props }, ref) => <li ref={ref} className={cn("", className)} {...props} />,
)
PaginationItem.displayName = "PaginationItem"
@@ -28,7 +28,7 @@ PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = {
isActive?: boolean
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
ComponentProps<"a">
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
<a
@@ -45,10 +45,7 @@ const PaginationLink = ({ className, isActive, size = "icon", ...props }: Pagina
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
const PaginationPrevious = ({ className, ...props }: ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
@@ -61,7 +58,7 @@ const PaginationPrevious = ({
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
const PaginationNext = ({ className, ...props }: ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
@@ -74,7 +71,7 @@ const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof Pag
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
const PaginationEllipsis = ({ className, ...props }: ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
+4 -4
View File
@@ -1,14 +1,14 @@
import { cn } from "@/lib/utils"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
const PopoverContent = forwardRef<
ComponentRef<typeof PopoverPrimitive.Content>,
ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
+7 -7
View File
@@ -1,10 +1,10 @@
import { cn } from "@/lib/utils"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
const ScrollArea = forwardRef<
ComponentRef<typeof ScrollAreaPrimitive.Root>,
ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
@@ -20,9 +20,9 @@ const ScrollArea = React.forwardRef<
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
const ScrollBar = forwardRef<
ComponentRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
+22 -22
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Select = SelectPrimitive.Root
@@ -9,9 +9,9 @@ const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
const SelectTrigger = forwardRef<
ComponentRef<typeof SelectPrimitive.Trigger>,
ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
@@ -29,9 +29,9 @@ const SelectTrigger = React.forwardRef<
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
const SelectScrollUpButton = forwardRef<
ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
@@ -43,9 +43,9 @@ const SelectScrollUpButton = React.forwardRef<
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
const SelectScrollDownButton = forwardRef<
ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
@@ -57,9 +57,9 @@ const SelectScrollDownButton = React.forwardRef<
))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
const SelectContent = forwardRef<
ComponentRef<typeof SelectPrimitive.Content>,
ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
@@ -89,9 +89,9 @@ const SelectContent = React.forwardRef<
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
const SelectLabel = forwardRef<
ComponentRef<typeof SelectPrimitive.Label>,
ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
@@ -101,9 +101,9 @@ const SelectLabel = React.forwardRef<
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
const SelectItem = forwardRef<
ComponentRef<typeof SelectPrimitive.Item>,
ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
@@ -124,9 +124,9 @@ const SelectItem = React.forwardRef<
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
const SelectSeparator = forwardRef<
ComponentRef<typeof SelectPrimitive.Separator>,
ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
+4 -4
View File
@@ -1,10 +1,10 @@
import { cn } from "@/lib/utils"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
const Separator = forwardRef<
ComponentRef<typeof SeparatorPrimitive.Root>,
ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
+19 -20
View File
@@ -1,26 +1,25 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
const Switch = forwardRef<
ComponentRef<typeof SwitchPrimitives.Root>,
ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
+52 -54
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils"
import * as React from "react"
import { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes, forwardRef } from "react"
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
const Table = forwardRef<HTMLTableElement, HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
@@ -14,35 +14,32 @@ const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableE
)
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
const TableHeader = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
),
)
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
))
const TableBody = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
),
)
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
{...props}
/>
))
const TableFooter = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
{...props}
/>
),
)
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
const TableRow = forwardRef<HTMLTableRowElement, HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
@@ -56,39 +53,40 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
)
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
))
const TableHead = forwardRef<HTMLTableCellElement, ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
),
)
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
const TableCell = forwardRef<HTMLTableCellElement, TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
),
)
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
))
const TableCaption = forwardRef<HTMLTableCaptionElement, HTMLAttributes<HTMLTableCaptionElement>>(
({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
),
)
TableCaption.displayName = "TableCaption"
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
+10 -10
View File
@@ -1,12 +1,12 @@
import { cn } from "@/lib/utils"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react"
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
const TabsList = forwardRef<
ComponentRef<typeof TabsPrimitive.List>,
ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
@@ -19,9 +19,9 @@ const TabsList = React.forwardRef<
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
const TabsTrigger = forwardRef<
ComponentRef<typeof TabsPrimitive.Trigger>,
ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
@@ -34,9 +34,9 @@ const TabsTrigger = React.forwardRef<
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
const TabsContent = forwardRef<
ComponentRef<typeof TabsPrimitive.Content>,
ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
+2 -2
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils"
import * as React from "react"
import { ComponentProps, forwardRef } from "react"
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
const Textarea = forwardRef<HTMLTextAreaElement, ComponentProps<"textarea">>(
({ className, ...props }, ref) => {
return (
<textarea
+6 -6
View File
@@ -15,13 +15,13 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { formatPath } from "@/lib/utils"
import * as React from "react"
import { Dispatch, FC, Fragment, SetStateAction, useState } from "react"
const ITEMS_TO_DISPLAY = 3
interface FilepathProps {
path: string
setPath: React.Dispatch<React.SetStateAction<string>>
setPath: Dispatch<SetStateAction<string>>
}
function pathToItems(path: string) {
@@ -38,8 +38,8 @@ function pathToItems(path: string) {
return result
}
export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => {
const [open, setOpen] = React.useState(false)
export const Filepath: FC<FilepathProps> = ({ path, setPath }) => {
const [open, setOpen] = useState(false)
const items = pathToItems(formatPath(path))
return (
@@ -87,7 +87,7 @@ export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => {
</>
) : null}
{items.slice(-ITEMS_TO_DISPLAY).map((item, index, slicedItems) => (
<React.Fragment key={index}>
<Fragment key={index}>
<BreadcrumbItem className="overflow-auto">
{item.href ? (
<>
@@ -107,7 +107,7 @@ export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => {
)}
</BreadcrumbItem>
{index !== slicedItems.length - 1 ? <BreadcrumbSeparator /> : null}
</React.Fragment>
</Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
+5
View File
@@ -11,6 +11,7 @@ import {
FolderClosed,
Menu,
Minus,
MoreHorizontal,
Play,
Plus,
Terminal,
@@ -39,6 +40,7 @@ export interface IconButtonProps extends ButtonProps {
| "cog"
| "minus"
| "user-pen"
| "more"
}
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
@@ -102,6 +104,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
case "user-pen": {
return <UserPen />
}
case "more": {
return <MoreHorizontal />
}
}
})()}
</Button>
+8 -7
View File
@@ -38,7 +38,7 @@ import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils"
import { type VariantProps, cva } from "class-variance-authority"
import { CheckIcon, ChevronDown, WandSparkles, XIcon } from "lucide-react"
import * as React from "react"
import { KeyboardEvent, forwardRef, useState } from "react"
/**
* Variants for the multi-select component to handle different styles.
@@ -67,7 +67,8 @@ const multiSelectVariants = cva(
* Props for MultiSelect component
*/
interface MultiSelectProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof multiSelectVariants> {
/**
* An array of option objects to be displayed in the multi-select component.
@@ -129,7 +130,7 @@ interface MultiSelectProps
className?: string
}
export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
(
{
options,
@@ -146,11 +147,11 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
},
ref,
) => {
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue)
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false)
const [isAnimating, setIsAnimating] = React.useState(false)
const [selectedValues, setSelectedValues] = useState<string[]>(defaultValue)
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const [isAnimating, setIsAnimating] = useState(false)
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
const handleInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
setIsPopoverOpen(true)
} else if (event.key === "Backspace" && !event.currentTarget.value) {
+43 -36
View File
@@ -2,7 +2,14 @@ import { cn } from "@/lib/utils"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { type VariantProps, cva } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react"
import {
ComponentPropsWithoutRef,
ComponentRef,
Dispatch,
HTMLAttributes,
SetStateAction,
forwardRef,
} from "react"
const Sheet = SheetPrimitive.Root
@@ -30,42 +37,42 @@ const sheetVariants = cva(
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
extends
ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {
setOpen: React.Dispatch<React.SetStateAction<boolean>>
setOpen: Dispatch<SetStateAction<boolean>>
}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, setOpen, ...props }, ref) => (
<SheetPortal>
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X
className="h-4 w-4"
onClick={() => {
setOpen(false)
}}
/>
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
const SheetContent = forwardRef<ComponentRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({ side = "right", className, children, setOpen, ...props }, ref) => (
<SheetPortal>
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X
className="h-4 w-4"
onClick={() => {
setOpen(false)
}}
/>
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
),
)
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
const SheetHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
const SheetFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
@@ -73,9 +80,9 @@ const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElemen
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
const SheetTitle = forwardRef<
ComponentRef<typeof SheetPrimitive.Title>,
ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
@@ -83,11 +90,11 @@ const SheetTitle = React.forwardRef<
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
SheetTitle.displayName = "SheetTitle"
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
const SheetDescription = forwardRef<
ComponentRef<typeof SheetPrimitive.Description>,
ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
@@ -95,7 +102,7 @@ const SheetDescription = React.forwardRef<
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
SheetDescription.displayName = "SheetDescription"
export {
Sheet,
+1 -1
View File
@@ -14,7 +14,7 @@ import {
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import { HTMLAttributes, forwardRef, useEffect, useRef, useState } from "react"
import { HTMLAttributes, JSX, forwardRef, useEffect, useRef, useState } from "react"
import { TableVirtuoso } from "react-virtuoso"
// Original Table is wrapped with a <div> (see https://ui.shadcn.com/docs/components/table#radix-:r24:-content-manual),
+8 -2
View File
@@ -1,6 +1,7 @@
import { getProfile, login as loginRequest } from "@/api/user"
import { AuthContextProps } from "@/types"
import { createContext, useContext, useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { toast } from "sonner"
@@ -16,6 +17,7 @@ const AuthContext = createContext<AuthContextProps>({
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const profile = useMainStore((store) => store.profile)
const setProfile = useMainStore((store) => store.setProfile)
const { t } = useTranslation()
useEffect(() => {
;(async () => {
@@ -25,7 +27,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setProfile(user)
} catch (error: any) {
setProfile(undefined)
console.error("Error fetching profile", error)
}
})()
}, [])
@@ -40,7 +41,12 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setProfile(user)
navigate("/dashboard")
} catch (error: any) {
toast(error.message)
const msg = error?.message
if (msg === "ApiErrorUnauthorized" || msg === "Unauthorized") {
toast(t("InvalidUsernameOrPassword"))
} else {
toast(msg || t("NetworkError"))
}
}
}
+3 -3
View File
@@ -1,9 +1,9 @@
import * as React from "react"
import { useEffect, useState } from "react"
export function useMediaQuery(query: string) {
const [value, setValue] = React.useState(false)
const [value, setValue] = useState(false)
React.useEffect(() => {
useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches)
}
+1 -1
View File
@@ -45,7 +45,7 @@ export const NotificationProvider: React.FC<NotificationProviderProps> = ({
(async () => {
try {
const n = (await getNotification()) || []
const nData = n.map(({ id, name }) => ({ id, name }))
const nData = n.map(({ id, name }) => ({ id: id!, name }))
setNotifier(nData)
} catch (error: any) {
toast("NotificationProvider Error", {
+1 -1
View File
@@ -45,7 +45,7 @@ export const ServerProvider: React.FC<ServerProviderProps> = ({
(async () => {
try {
const s = (await getServers()) || []
const serverData = s.map(({ id, name }) => ({ id, name }))
const serverData = s.map(({ id, name }) => ({ id: id!, name }))
setServer(serverData)
} catch (error: any) {
toast("ServerProvider Error", {
-23
View File
@@ -1,23 +0,0 @@
import { createTerminal } from "@/api/terminal"
import { ModelCreateTerminalResponse } from "@/types"
import { useEffect, useState } from "react"
export default function useTerminal(serverId?: number) {
const [terminal, setTerminal] = useState<ModelCreateTerminalResponse | null>(null)
async function fetchTerminal() {
try {
const response = await createTerminal(serverId!)
setTerminal(response)
} catch (error) {
console.error("Failed to fetch terminal:", error)
}
}
useEffect(() => {
if (!serverId) return
fetchTerminal()
}, [serverId])
return terminal
}
+2 -2
View File
@@ -4,13 +4,13 @@ import { initReactI18next } from "react-i18next"
import deTranslation from "../locales/de/translation.json"
import enTranslation from "../locales/en/translation.json"
import esTranslation from "../locales/es/translation.json"
import frTranslation from "../locales/fr/translation.json"
import idTranslation from "../locales/id/translation.json"
import itTranslation from "../locales/it/translation.json"
import ruTranslation from "../locales/ru/translation.json"
import taTranslation from "../locales/ta/translation.json"
import zhCNTranslation from "../locales/zh-CN/translation.json"
import zhTWTranslation from "../locales/zh-TW/translation.json"
import frTranslation from "../locales/fr/translation.json"
import idTranslation from "../locales/id/translation.json"
const resources = {
"zh-CN": {
+182
View File
@@ -0,0 +1,182 @@
import { z } from "zod"
import i18n from "./i18n"
/**
* Zod schema for PublicNote
* Conventions:
* - All fields are strings and may be empty
* - IPv4/IPv6/autoRenewal must be "0" or "1"
* - cycle is one of Day/Week/Month/Year
* - Date fields can be empty, ISO-like, or the special value "0000-00-00T23:59:59+08:00"
*/
export const PublicNoteSchema = z.object({
billingDataMod: z
.object({
startDate: z.string().optional(),
endDate: z.string().optional(),
autoRenewal: z.string().optional(),
cycle: z.string().optional(),
amount: z.string().optional(),
})
.optional(),
planDataMod: z
.object({
bandwidth: z.string().optional(),
trafficVol: z.string().optional(),
trafficType: z.string().optional(),
IPv4: z.string().optional(),
IPv6: z.string().optional(),
networkRoute: z.string().optional(),
extra: z.string().optional(),
})
.optional(),
})
export type PublicNote = z.infer<typeof PublicNoteSchema>
export const defaultPublicNote: PublicNote = {}
export const isValidISOLike = (v: string) => {
if (!v) return true
if (v === "0000-00-00T23:59:59+08:00") return true
const d = new Date(v)
return !isNaN(d.getTime())
}
export const normalizeISO = (v?: string) => {
if (!v) return undefined
if (v === "0000-00-00T23:59:59+08:00") return v
const date = new Date(v)
return isNaN(date.getTime()) ? v : date.toISOString()
}
/**
* Parse a string into PublicNote; return the default object if not valid JSON or validation fails.
*/
export const parsePublicNote = (s?: string): PublicNote => {
if (!s) return defaultPublicNote
try {
const obj = JSON.parse(s)
const parsed = PublicNoteSchema.safeParse(obj)
if (parsed.success) {
return parsed.data
}
return defaultPublicNote
} catch {
return defaultPublicNote
}
}
export const validatePublicNote = (pn: PublicNote) => {
const errors: Partial<Record<string, string>> = {}
// Structural and enum validations
if (pn.billingDataMod?.autoRenewal && !/^(0|1)$/.test(pn.billingDataMod.autoRenewal)) {
errors["billing.autoRenewal"] = i18n.t("Validation.MustBe0Or1")
}
if (pn.billingDataMod?.cycle && !/^(Day|Week|Month|Year)$/i.test(pn.billingDataMod.cycle)) {
errors["billing.cycle"] = i18n.t("Validation.MustBeDayWeekMonthYear")
}
if (pn.planDataMod?.trafficType && !/^(1|2)$/.test(pn.planDataMod.trafficType)) {
errors["plan.trafficType"] = i18n.t("Validation.MustBe1Or2")
}
if (pn.planDataMod?.IPv4 !== undefined && !/^(0|1)$/.test(pn.planDataMod.IPv4)) {
errors["plan.IPv4"] = i18n.t("Validation.MustBe0Or1")
}
if (pn.planDataMod?.IPv6 !== undefined && !/^(0|1)$/.test(pn.planDataMod.IPv6)) {
errors["plan.IPv6"] = i18n.t("Validation.MustBe0Or1")
}
// Date validity checks
if (pn.billingDataMod?.startDate && !isValidISOLike(pn.billingDataMod.startDate)) {
errors["billing.startDate"] = i18n.t("Validation.InvalidDate")
}
if (pn.billingDataMod?.endDate && !isValidISOLike(pn.billingDataMod.endDate)) {
errors["billing.endDate"] = i18n.t("Validation.InvalidDate")
}
return { errors, valid: Object.keys(errors).length === 0 }
}
/**
* Detect default mode from string: JSON matching schema -> "structured"; otherwise "raw".
*/
export const detectPublicNoteMode = (s?: string): "structured" | "raw" => {
if (!s) return "raw"
try {
const obj = JSON.parse(s)
const parsed = PublicNoteSchema.strict().safeParse(obj)
return parsed.success ? "structured" : "raw"
} catch {
return "raw"
}
}
/**
* Immutable patch by path, for use in component wrappers around setPublicNoteObj.
* Example path: "billingDataMod.startDate"
*/
export const applyPublicNotePatch = (
obj: PublicNote,
path: string,
value: string | undefined,
): PublicNote => {
const keys = path.split(".")
const draft: any = structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj))
let cur: any = draft
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i]
cur[k] = { ...(cur[k] ?? {}) }
cur = cur[k]
}
cur[keys[keys.length - 1]] = value
return draft
}
/**
* Update a date field while preserving time parts: if the previous value is a valid date,
* keep hours/minutes/seconds. Path example: "billingDataMod.startDate" | "billingDataMod.endDate"
*/
export const applyPublicNoteDate = (obj: PublicNote, path: string, date: Date): PublicNote => {
const keys = path.split(".")
const draft: any = structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj))
// Read previous value to preserve time components
let curRead: any = draft
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i]
curRead = (curRead as any)[k]
if (!curRead) break
}
const leafKey = keys[keys.length - 1]
const prevVal: string | undefined = curRead ? curRead[leafKey] : undefined
const d = new Date(date)
if (prevVal) {
const pd = new Date(prevVal)
if (!isNaN(pd.getTime())) {
d.setHours(pd.getHours(), pd.getMinutes(), pd.getSeconds(), 0)
}
}
// Write back
let curWrite: any = draft
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i]
curWrite[k] = { ...(curWrite[k] ?? {}) }
curWrite = curWrite[k]
}
curWrite[leafKey] = d.toISOString()
return draft
}
/**
* Toggle the special "no expiry" value for endDate.
*/
export const toggleEndNoExpiry = (obj: PublicNote): PublicNote => {
const NO_EXPIRY = "0000-00-00T23:59:59+08:00"
const current = obj.billingDataMod?.endDate
const next = current === NO_EXPIRY ? "" : NO_EXPIRY
return applyPublicNotePatch(obj, "billingDataMod.endDate", next)
}
+73 -1
View File
@@ -7,6 +7,8 @@
},
"Username": "Username",
"Password": "Password",
"InvalidUsernameOrPassword": "Invalid Username Or Password",
"NetworkError": "Network Error",
"LoginFirst": "Please Log in first",
"CurrentTime": "Current Time",
"Results": {
@@ -45,6 +47,8 @@
"Enable": "Enable",
"HideForGuest": "Hidden from Visitors",
"InstallCommands": "Installation command",
"Terminal": "Terminal",
"Config": "Config",
"Note": "Note",
"Success": "Success",
"Done": "Finish",
@@ -72,6 +76,10 @@
"Trigger": "On Trigger",
"TasksToTriggerOnAlert": "Tasks to be triggered on alert",
"TasksToTriggerAfterRecovery": "Tasks to be triggered after recovery",
"Add": "Add",
"Delete": "Delete",
"AdvancedJSON": "Advanced JSON",
"Save": "Save",
"Confirm": "Confirm",
"ConfirmDeletion": "Confirm Deletion?",
"Services": "Services",
@@ -143,6 +151,8 @@
"AgentRealIPHeader": "Agent real IP request header",
"UseDirectConnectingIP": "Use direct connection IP",
"IPChangeNotification": "IP Change Notification",
"IPChangeNotificationGroupID": "IP Change Notification Group ID",
"ExpiryNotificationGroupID": "Expiry Notification Group ID",
"FullIPNotification": "Show Full IP Address in Notification Messages",
"EditService": "Edit Service",
"CreateService": "Create Service",
@@ -185,5 +195,67 @@
"EditServerConfig": "Edit Server Config",
"Option": "Option",
"Value": "Value",
"Preview": "Preview"
"Preview": "Preview",
"PublicNote": {
"Label": "Public Note",
"Billing": "Billing",
"Plan": "Plan",
"StartDate": "Start Date",
"EndDate": "End Date",
"AutoRenewal": "Auto Renewal",
"Cycle": "Cycle",
"Amount": "Amount",
"Bandwidth": "Bandwidth",
"TrafficVolume": "Traffic Volume",
"TrafficType": "Traffic Type",
"IPv4": "IPv4",
"IPv6": "IPv6",
"NetworkRoute": "Network Route",
"Extra": "Extra",
"Enabled": "Enabled",
"Disabled": "Disabled",
"Inbound": "Inbound",
"Both": "Both",
"Day": "Day",
"Week": "Week",
"Month": "Month",
"Year": "Year",
"NoExpiry": "No Expiry",
"SetNoExpiry": "Set No Expiry",
"CancelNoExpiry": "Cancel No Expiry",
"Free": "Free",
"PayAsYouGo": "Pay as you go",
"CommaSeparated": "Separate multiple items with commas",
"Has": "Has",
"None": "None",
"CustomFields": "Custom Fields",
"ClearDate": "Clear Date",
"Clear": "Clear",
"RawText": "Raw Text"
},
"Validation": {
"InvalidDate": "Invalid date",
"MustBe0Or1": "Must be 0 or 1",
"MustBeDayWeekMonthYear": "Must be Day/Week/Month/Year",
"MustBe1Or2": "Must be 1 or 2",
"DigitsOnly": "Digits only",
"InvalidForm": "Invalid form",
"InvalidJSON": "Invalid JSON"
},
"AlertRules": {
"CoverAllServers": "Monitor all servers",
"IgnoreAllSelectSpecific": "Ignore all, select specific servers",
"IgnoreHint": "{{server}} ID: true/false",
"IgnoreExample": "e.g., {\"1\": true, \"2\": false}"
},
"Search": "Search...",
"Format": "Format",
"Formatted": "Formatted",
"Copy": "Copy",
"Paste": "Paste",
"CopiedToClipboard": "Copied to clipboard",
"ClipboardWriteFailed": "Clipboard write failed",
"PastedFromClipboard": "Pasted from clipboard",
"ClipboardReadFailed": "Clipboard read failed",
"FormatMetricUnits": "Format Metric Units"
}
+70 -1
View File
@@ -185,5 +185,74 @@
"OverrideDDNSDomains": "Sobrescribir dominios DDNS (por configuración)",
"EmptyText": "El texto está vacío",
"EmptyNote": "No tenías ninguna nota.",
"EditServerConfig": "Editar configuración del servidor"
"EditServerConfig": "Editar configuración del servidor",
"InvalidUsernameOrPassword": "Usuario o Contraseña inválidos",
"NetworkError": "Error de Red",
"Terminal": "Terminal",
"Config": "Configuración",
"Add": "Añadir",
"Delete": "Eliminar",
"AdvancedJSON": "JSON Avanzado",
"Save": "Guardar",
"PublicNote": {
"Label": "Nota pública",
"Billing": "Facturación",
"Plan": "Plan",
"StartDate": "Fecha de inicio",
"EndDate": "Fecha de finalización",
"AutoRenewal": "Renovación automática",
"Cycle": "Ciclo",
"Amount": "Cantidad",
"Bandwidth": "Ancho de banda",
"TrafficVolume": "Volumen de tráfico",
"TrafficType": "Tipo de tráfico",
"IPv4": "IPv4",
"IPv6": "IPv6",
"NetworkRoute": "Ruta de red",
"Extra": "Extra",
"Enabled": "Activado",
"Disabled": "Desactivado",
"Inbound": "Entrante",
"Both": "Ambos",
"Day": "Dia",
"Week": "Semana",
"Month": "Mes",
"Year": "Año",
"NoExpiry": "No expira",
"SetNoExpiry": "Fijar que no expire",
"CancelNoExpiry": "Cancelar que no expire",
"Free": "Gratis",
"PayAsYouGo": "Paga sobre la marcha",
"CommaSeparated": "Separa los múltiples items con comas",
"Has": "Tiene",
"None": "Ninguno",
"CustomFields": "Campos personalizados",
"ClearDate": "Borrar fecha",
"Clear": "Borrar",
"RawText": "Texto sin formato"
},
"Validation": {
"InvalidDate": "Fecha no válida",
"MustBe0Or1": "Tiene que ser 0 o 1",
"MustBeDayWeekMonthYear": "Tiene que ser Día/Semana/Mes/Año",
"MustBe1Or2": "Tiene que ser 1 o 2",
"DigitsOnly": "Solo dígitos",
"InvalidForm": "Forma no válida",
"InvalidJSON": "JSON no válido"
},
"AlertRules": {
"CoverAllServers": "Monitorizar todos los servidores",
"IgnoreAllSelectSpecific": "Ignorar todos, seleccionar servidores específicos",
"IgnoreHint": "{{server}} ID: verdadero/falso",
"IgnoreExample": "p.e., {\"1\": verdadero, \"2\": falso}"
},
"Search": "Buscar...",
"Format": "Formato",
"Formatted": "Formateado",
"Copy": "Copiar",
"Paste": "Pegar",
"CopiedToClipboard": "Copiar al portapapeles",
"ClipboardWriteFailed": "Error al escribir en el portapapeles",
"PastedFromClipboard": "Pegado del portapapeles",
"ClipboardReadFailed": "Falló la lectura del portapapeles"
}
+25
View File
@@ -0,0 +1,25 @@
{
"nezha": "Monitoreo Nezha",
"theme": {
"light": "Claro",
"dark": "Escuro",
"system": "Seguir o sistema"
},
"Username": "Nome de usuario",
"Password": "Contrasinal",
"InvalidUsernameOrPassword": "Nome de usuario ou contrasinal non válidos",
"NetworkError": "Error de rede",
"LoginFirst": "Por favor, inicie sesión primeiro",
"CurrentTime": "Hora actual",
"Results": {
"UsernameMin": "O nome de usuario debe ter polo menos {{number}} caracteres.",
"PasswordRequired": "O contrasinal non pode estar baleiro.",
"ErrorFetchingResource": "Erro ao obter o recurso: {{error}}",
"SelectAtLeastOneServer": "Por favor, selecciona polo menos un servidor.",
"UnExpectedError": "Erro inesperado. Consulta a consola para obter máis detalles.",
"ForceUpdate": "Actualización forzada:",
"NoRowsAreSelected": "Non hai filas seleccionadas",
"ThisOperationIsUnrecoverable": "A operación non se pode desfacer!",
"TaskTriggeredSuccessfully": "A tarefa desencadeouse correctamente"
}
}
+248 -13
View File
@@ -1,24 +1,259 @@
{
"nezha": "Monitor Nezha",
"nezha": "Pemantauan Nezha",
"theme": {
"light": "Terang",
"dark": "Gelap",
"system": "Mengikuti sistem"
"system": "Ikuti Sistem"
},
"Username": "Nama Pengguna",
"Password": "Kata Sandi",
"LoginFirst": "Mohon masuk terlebih dahulu",
"CurrentTime": "Waktu saat ini",
"LoginFirst": "Harap Masuk terlebih dahulu",
"CurrentTime": "Waktu Saat Ini",
"Results": {
"UsernameMin": "Nama Pengguna setidak nya harus {{number}} karakter.",
"PasswordRequired": "Kata Sandi tidak boleh kosong.",
"ErrorFetchingResource": "Kesalahan mengambil sumber daya: {{error}}",
"SelectAtLeastOneServer": "Silahkan pilih setidaknya satu server.",
"UnExpectedError": "Kesalahan tidak terduga, Silahhkan lihat konsol untuk detailnya.",
"ForceUpdate": "Peningkatan Paksa:",
"UsernameMin": "Nama pengguna harus minimal {{number}} karakter.",
"PasswordRequired": "Kata sandi tidak boleh kosong.",
"ErrorFetchingResource": "Galat Mengambil Sumber Daya : {{error}}",
"SelectAtLeastOneServer": "Harap pilih minimal satu server.",
"UnExpectedError": "Galat tak terduga, Harap lihat konsol untuk detail.",
"ForceUpdate": "Peningkatan paksa:",
"NoRowsAreSelected": "Tidak ada baris yang dipilih",
"ThisOperationIsUnrecoverable": "Operasi tidak dapat dibatalkan!",
"ThisOperationIsUnrecoverable": "Operasi ini tidak dapat dibatalkan!",
"TaskTriggeredSuccessfully": "Tugas berhasil dipicu",
"TheServerDoesNotOnline": "Server tidak ada atau masih belum terhubung"
}
"TheServerDoesNotOnline": "Server tidak ada atau belum terhubung",
"InstallHostRequired": "Alamat docking Agen belum diisi dalam pengaturan.",
"UnknownIdentifier": "Pengidentifikasi tidak diketahui"
},
"InvalidUsernameOrPassword": "Nama Pengguna atau Kata Sandi Tidak Valid",
"NetworkError": "Galat Jaringan",
"Login": "Masuk",
"Server": "Server",
"Service": "Layanan",
"Task": "Tugas",
"Notification": "Notifikasi",
"DDNS": "DNS Dinamis",
"NATT": "Penembusan NAT",
"Group": "Grup",
"Profile": "Profil",
"Settings": "Pengaturan sistem",
"BackToHome": "Kembali ke Beranda",
"Logout": "Keluar",
"NavigateTo": "Navigasi ke",
"SelectAPageToNavigateTo": "Pilih halaman untuk dilompati",
"Close": "Tutup",
"Error": "Galat",
"Name": "Nama",
"Version": "Versi",
"Unknown": "tidak diketahui",
"Enable": "Aktifkan",
"HideForGuest": "Tersembunyi dari Pengunjung",
"InstallCommands": "Perintah instalasi",
"Terminal": "Terminal",
"Config": "Konfigurasi",
"Note": "Catatan",
"Success": "Berhasil",
"Done": "Selesai",
"Offline": "Luring",
"Failure": "Gagal",
"Loading": "Memuat",
"NoResults": "Tidak ada hasil",
"Actions": "Tindakan",
"EditServer": "Edit Server",
"Weight": "Bobot (semakin besar angkanya, semakin tinggi ditampilkan)",
"DDNSProfiles": "ID Profil DDNS",
"SeparateWithComma": "(Pisahkan dengan koma)",
"Public": "Publik",
"Private": "Pribadi",
"Submit": "Kirim",
"Target": "Target",
"Coverage": "Cakupan",
"CoverAll": "Tutup Semua",
"IgnoreAll": "Abaikan Semua",
"OnAlert": "Server yang Terpicu Alarm",
"SpecificServers": "Server tertentu",
"Type": "Jenis",
"Interval": "Interval",
"NotifierGroupID": "ID Grup Notifikasi",
"Trigger": "Saat Dipicu",
"TasksToTriggerOnAlert": "Tugas yang dipicu saat peringatan",
"TasksToTriggerAfterRecovery": "Tugas yang dipicu setelah pemulihan",
"Add": "Tambah",
"Delete": "Hapus",
"AdvancedJSON": "JSON Lanjutan",
"Save": "Simpan",
"Confirm": "Konfirmasi",
"ConfirmDeletion": "Konfirmasi Penghapusan?",
"Services": "Layanan",
"ShowInService": "Tampilkan di Layanan",
"Coverages": {
"Excludes": "Kecualikan Server Tertentu",
"Only": "Hanya Server Tertentu",
"Alarmed": "Dijalankan di server yang memicu alarm"
},
"EnableFailureNotification": "Aktifkan Notifikasi Kegagalan",
"MaximumLatency": "Tunda Maksimum (ms)",
"MinimumLatency": "Tunda Minimum (milidetik)",
"EnableLatencyNotification": "Aktifkan Notifikasi Latensi",
"EnableTriggerTask": "Aktifkan Tugas Pemicu",
"CronExpression": "Ekspresi Cron",
"Command": "Perintah",
"NotifierGroup": "Grup Notifikasi",
"SendSuccessNotification": "Kirim Notifikasi Berhasil",
"LastExecution": "Eksekusi Terakhir",
"Result": "Hasil",
"Scheduled": "Tugas Terjadwal",
"Notifier": "Pemberi Notifikasi",
"AlertRule": "Aturan Peringatan",
"VerifyTLS": "Verifikasi TLS",
"TriggerMode": "Mode Pemicu",
"Rules": "Aturan",
"RequestMethod": "Metode Permintaan",
"RequestHeader": "Header Permintaan",
"DoNotSendTestMessage": "Jangan Kirim Pesan Uji",
"Always": "Selalu",
"Once": "Sekali",
"Provider": "Penyedia",
"Domains": "Domain",
"MaximumRetryAttempts": "Waktu maksimum untuk upaya ulang",
"Refresh": "Segarkan",
"CopyPath": "Salin jalur",
"Goto": "Pergi ke",
"UpdateProfile": "Perbarui Profil",
"NewUsername": "Nama Pengguna Baru",
"OriginalPassword": "Kata Sandi Asli",
"NewPassword": "Kata Sandi Baru",
"EditDDNS": "Edit DDNS",
"CreateDDNS": "Buat DDNS",
"Credential": "Kredensial",
"RequestType": "Jenis Permintaan",
"RequestBody": "Isi Permintaan",
"FileManager": "Manajer Berkas Pseudo",
"Downloading": "Mengunduh",
"Uploading": "Mengunggah",
"EditNAT": "Edit Konfigurasi NAT",
"CreateNAT": "Buat Konfigurasi NAT",
"LocalService": "Layanan Lokal",
"BindHostname": "Ikat Nama Domain",
"EditServerGroup": "Edit Grup Server",
"CreateServerGroup": "Buat Grup Server",
"User": "Pengguna",
"WAF": "Firewall Aplikasi Web",
"SiteName": "Nama Situs",
"DashboardOriginalHost": "Alamat koneksi Agen [nama domain/IP:port]",
"ConfigTLS": "Gunakan TLS untuk menghubungkan Agen",
"LoginFailed": "Gagal Masuk",
"BruteForceAttackingToken": "Token Serangan Brute Force",
"BruteForceAttackingAgentSecret": "Rahasia Agen Serangan Brute Force",
"Language": "Bahasa",
"CustomCodes": "Kode Kustom (Gaya dan Skrip)",
"CustomCodesDashboard": "Kode Kustom untuk Dasbor",
"CustomPublicDNSNameserversforDDNS": "DNS Publik Kustom untuk DDNS",
"WebRealIPHeader": "Header permintaan IP asli Frontend",
"AgentRealIPHeader": "Header permintaan IP asli Agen",
"UseDirectConnectingIP": "Gunakan IP koneksi langsung",
"IPChangeNotification": "Notifikasi Perubahan IP",
"FullIPNotification": "Tampilkan Alamat IP Lengkap dalam Pesan Notifikasi",
"EditService": "Edit Layanan",
"CreateService": "Buat Layanan",
"EditTask": "Edit Tugas",
"CreateTask": "Buat Tugas",
"CreateNotifier": "Buat Pemberi Notifikasi",
"EditNotifier": "Edit Pemberi Notifikasi",
"EditAlertRule": "Edit Aturan Peringatan",
"CreateAlertRule": "Buat Aturan Peringatan",
"EditNotifierGroup": "Edit Grup Pemberi Notifikasi",
"CreateNotifierGroup": "Buat Grup Pemberi Notifikasi",
"NewUser": "Pengguna Baru",
"Count": "Jumlah",
"LastBlockReason": "Alasan Blokir Terakhir",
"LastBlockTime": "Waktu larangan terakhir",
"Theme": "Tema",
"Author": "Penulis",
"Repository": "Repositori",
"Community": "Komunitas",
"Official": "Resmi",
"CommunityThemeWarning": "Anda menggunakan tema komunitas",
"CommunityThemeDescription": "Tema ini disediakan oleh komunitas, gunakan dengan risiko Anda sendiri",
"Cancel": "Batal",
"EnableDDNS": "Aktifkan DDNS",
"PushSuccessful": "Push jika Berhasil",
"GrpcAuthFailed": "Autentikasi gRPC gagal",
"APITokenInvalid": "Token API tidak valid",
"UserInvalid": "Pengguna tidak valid",
"BlockByUser": "Diblokir oleh admin",
"BlockIdentifier": "Pengidentifikasi blokir",
"UserId": "ID Pengguna",
"ConnectedAt": "Terhubung pada",
"OnlineUser": "Pengguna Daring",
"Total": "Total",
"ConfirmBlock": "Konfirmasi Blokir",
"RejectPassword": "Tolak Masuk dengan Kata Sandi",
"EmptyText": "Teks kosong",
"EmptyNote": "Anda tidak memiliki catatan.",
"OverrideDDNSDomains": "Timpa Domain DDNS (per konfigurasi)",
"EditServerConfig": "Edit Konfigurasi Server",
"Option": "Opsi",
"Value": "Nilai",
"Preview": "Pratinjau",
"PublicNote": {
"Label": "Catatan Publik",
"Billing": "Penagihan",
"Plan": "Paket",
"StartDate": "Tanggal Mulai",
"EndDate": "Tanggal Berakhir",
"AutoRenewal": "Perpanjangan Otomatis",
"Cycle": "Siklus",
"Amount": "Jumlah",
"Bandwidth": "Bandwidth",
"TrafficVolume": "Volume Lalu Lintas",
"TrafficType": "Jenis Lalu Lintas",
"IPv4": "IPv4",
"IPv6": "IPv6",
"NetworkRoute": "Rute Jaringan",
"Extra": "Ekstra",
"Enabled": "Diaktifkan",
"Disabled": "Dinonaktifkan",
"Inbound": "Masuk",
"Both": "Keduanya",
"Day": "Hari",
"Week": "Minggu",
"Month": "Bulan",
"Year": "Tahun",
"NoExpiry": "Tanpa Kedaluwarsa",
"SetNoExpiry": "Atur Tanpa Kedaluwarsa",
"CancelNoExpiry": "Batalkan Tanpa Kedaluwarsa",
"Free": "Gratis",
"PayAsYouGo": "Bayar sesuai penggunaan",
"CommaSeparated": "Pisahkan beberapa item dengan koma",
"Has": "Memiliki",
"None": "Tidak ada",
"CustomFields": "Bidang Kustom",
"ClearDate": "Hapus Tanggal",
"Clear": "Bersihkan",
"RawText": "Teks Mentah"
},
"Validation": {
"InvalidDate": "Tanggal tidak valid",
"MustBe0Or1": "Harus 0 atau 1",
"MustBeDayWeekMonthYear": "Harus Hari/Minggu/Bulan/Tahun",
"MustBe1Or2": "Harus 1 atau 2",
"DigitsOnly": "Hanya digit",
"InvalidForm": "Formulir tidak valid",
"InvalidJSON": "JSON tidak valid"
},
"AlertRules": {
"CoverAllServers": "Pantau semua server",
"IgnoreAllSelectSpecific": "Abaikan semua, pilih server tertentu",
"IgnoreHint": "{{server}} ID: true/false",
"IgnoreExample": "misalnya, {\"1\": true, \"2\": false}"
},
"Search": "Cari...",
"Format": "Format",
"Formatted": "Terformat",
"Copy": "Salin",
"Paste": "Tempel",
"CopiedToClipboard": "Disalin ke papan klip",
"ClipboardWriteFailed": "Gagal menulis ke papan klip",
"PastedFromClipboard": "Ditempel dari papan klip",
"ClipboardReadFailed": "Gagal membaca papan klip",
"FormatMetricUnits": "Format Satuan Metrik"
}
+1
View File
@@ -0,0 +1 @@
{}
+131
View File
@@ -0,0 +1,131 @@
{
"nezha": "Nezha Моніторинг",
"theme": {
"light": "Світло",
"dark": "Темний",
"system": "Така як система"
},
"Username": "Нікнейм",
"Password": "Пароль",
"InvalidUsernameOrPassword": "Невірне Ім'я або Пароль",
"NetworkError": "Помилка мережі",
"LoginFirst": "Будь ласка, спочатку увійдіть до системи",
"CurrentTime": "Поточний час",
"Results": {
"UsernameMin": "Ім'я користувача має містити не менше {{number}} символів.",
"PasswordRequired": "Пароль не може бути порожнім.",
"ErrorFetchingResource": "Помилка при отриманні ресурсу: {{error}}",
"SelectAtLeastOneServer": "Виберіть хоча б один сервер.",
"UnExpectedError": "Непередбачена помилка. Подробиці у консолі.",
"ForceUpdate": "Примусове оновлення:",
"NoRowsAreSelected": "Не вибрані рядки",
"ThisOperationIsUnrecoverable": "Цю операцію не можна скасувати!",
"TaskTriggeredSuccessfully": "Завдання успішно запущено",
"TheServerDoesNotOnline": "Сервер не існує або ще не підключено",
"InstallHostRequired": "В установках не вказано адресу підключення агента.",
"UnknownIdentifier": "Невідомий ідентифікатор"
},
"Login": "Увійти",
"Server": "Сервер",
"Service": "Сервіс",
"Task": "Задача",
"Notification": "Повідомлення",
"DDNS": "Динамічний DNS",
"NATT": "Обхід NAT",
"Group": "Група",
"Profile": "Профіль",
"Settings": "Системні налаштування",
"BackToHome": "Повернутися на головну",
"Logout": "Вийти",
"NavigateTo": "Перейти до",
"SelectAPageToNavigateTo": "Виберіть сторінку, щоб перейти",
"Close": "Закрити",
"Error": "Помилка",
"Name": "Назва",
"Version": "Версія",
"Unknown": "невідомо",
"Enable": "Увімкнути",
"HideForGuest": "Приховано для гостей",
"InstallCommands": "Команда установки",
"Terminal": "Термінал",
"Config": "Конфігурація",
"Note": "Примітка",
"Success": "Успіх",
"Done": "Готово",
"Offline": "Оффлайн",
"Failure": "Невдача",
"Loading": "Загрузка",
"NoResults": "Немає результатів",
"Actions": "Дії",
"EditServer": "Редагувати сервер",
"Weight": "Вага (чим більше число, тим вище воно відображається)",
"DDNSProfiles": "ID профілей DDNS",
"SeparateWithComma": "(Розділіть комою)",
"Public": "Публічний",
"Private": "Приватний",
"Submit": "Відправити",
"Target": "Ціль",
"Coverage": "Покриття",
"CoverAll": "Охопити все",
"IgnoreAll": "Ігнорувати все",
"OnAlert": "Сервери з тривогами",
"SpecificServers": "Конкретний сервер",
"Type": "Тип",
"Interval": "Інтервал",
"NotifierGroupID": "ID групи повідомлень",
"Trigger": "При спрацьовуванні",
"TasksToTriggerOnAlert": "Завдання, що запускаються при тривозі",
"TasksToTriggerAfterRecovery": "Завдання після відновлення",
"Add": "Додати",
"Delete": "Видалити",
"AdvancedJSON": "Розширений JSON",
"Save": "Зберегти",
"Confirm": "Підтвердити",
"ConfirmDeletion": "Підтвердити видалення?",
"Services": "Сервіси",
"ShowInService": "Показувати у Сервісі",
"Coverages": {
"Excludes": "Виключити певні сервери",
"Only": "Тільки певні сервери",
"Alarmed": "Виконано на сервері, який спричинив тривогу"
},
"EnableFailureNotification": "Увімкнути сповіщення про помилки",
"MaximumLatency": "Максимальна затримка (мс)",
"MinimumLatency": "Мінімальна затримка (мс)",
"EnableLatencyNotification": "Увімкнути сповіщення про затримку",
"EnableTriggerTask": "Увімкнути завдання-тригер",
"CronExpression": "Cron-вираз",
"Command": "Команда",
"NotifierGroup": "Група сповіщень",
"SendSuccessNotification": "Надіслати сповіщення про успіх",
"LastExecution": "Останнє виконання",
"Result": "Результат",
"Scheduled": "Заплановані завдання",
"Notifier": "Повідомити",
"AlertRule": "Правила сповіщень",
"VerifyTLS": "Перевірка TLS",
"TriggerMode": "Режим спрацювання",
"Rules": "Правила",
"RequestMethod": "Метод запиту",
"RequestHeader": "Заголовок запиту",
"DoNotSendTestMessage": "Не надсилати тестове повідомлення",
"Always": "Зажди",
"Once": "Один раз",
"Provider": "Провайдер",
"Domains": "Домени",
"MaximumRetryAttempts": "Максимальний час для повторних спроб",
"Refresh": "Оновити",
"CopyPath": "Копіювати шлях",
"Goto": "Перейти",
"UpdateProfile": "Оновити профіль",
"NewUsername": "Нове ім'я користувача",
"OriginalPassword": "Старий пароль",
"NewPassword": "Новий пароль",
"EditDDNS": "Редагувати DDNS",
"CreateDDNS": "Створити DDNS",
"Credential": "Облікові дані",
"RequestType": "Тип запиту",
"RequestBody": "Тіло запиту",
"FileManager": "Псевдо Менеджер файлів",
"Downloading": "Завантаження"
}
+74 -2
View File
@@ -7,6 +7,8 @@
},
"Username": "用户名",
"Password": "密码",
"InvalidUsernameOrPassword": "用户名或密码错误",
"NetworkError": "网络错误",
"LoginFirst": "请先登录",
"CurrentTime": "当前时间",
"Results": {
@@ -45,6 +47,8 @@
"Enable": "启用",
"HideForGuest": "对游客隐藏",
"InstallCommands": "安装命令",
"Terminal": "终端",
"Config": "配置",
"Note": "备注",
"Success": "成功",
"Done": "完成",
@@ -72,6 +76,10 @@
"Trigger": "触发",
"TasksToTriggerOnAlert": "告警时要触发的任务",
"TasksToTriggerAfterRecovery": "恢复后要触发的任务",
"Add": "添加",
"Delete": "删除",
"AdvancedJSON": "高级 JSON",
"Save": "保存",
"Confirm": "确认",
"ConfirmDeletion": "确认删除?",
"Services": "服务",
@@ -149,7 +157,9 @@
"WebRealIPHeader": "前端真实IP请求头",
"AgentRealIPHeader": "Agent真实IP请求头",
"UseDirectConnectingIP": "使用直连 IP",
"IPChangeNotification": "IP变更通知",
"IPChangeNotification": "IP 变更通知",
"IPChangeNotificationGroupID": "IP 变更通知组 ID",
"ExpiryNotificationGroupID": "到期通知组 ID",
"FullIPNotification": "在通知消息中显示完整的 IP 地址",
"LoginFailed": "登录失败",
"BruteForceAttackingToken": "暴力攻击令牌",
@@ -185,5 +195,67 @@
"OverrideDDNSDomains": "强制指定 DDNS 域名(每个配置)",
"Value": "值",
"Preview": "预览",
"Option": "选项"
"Option": "选项",
"PublicNote": {
"Label": "公开备注",
"Billing": "账单信息",
"Plan": "套餐配置",
"StartDate": "开始时间",
"EndDate": "结束时间",
"AutoRenewal": "自动续费",
"Cycle": "周期",
"Amount": "金额",
"Bandwidth": "带宽",
"TrafficVolume": "流量配额",
"TrafficType": "流量类型",
"IPv4": "IPv4",
"IPv6": "IPv6",
"NetworkRoute": "网络路由",
"Extra": "额外备注",
"Enabled": "启用",
"Disabled": "禁用",
"Inbound": "入站",
"Both": "双向",
"Day": "天",
"Week": "周",
"Month": "月",
"Year": "年",
"NoExpiry": "不过期",
"SetNoExpiry": "设置为不过期",
"CancelNoExpiry": "取消不过期",
"Free": "免费",
"PayAsYouGo": "按量付费",
"CommaSeparated": "以英文逗号分隔多个",
"Has": "有",
"None": "无",
"CustomFields": "自定义字段",
"ClearDate": "清除日期",
"Clear": "清除",
"RawText": "原始文本"
},
"Validation": {
"InvalidDate": "无效的日期格式",
"MustBe0Or1": "只能为 0 或 1",
"MustBeDayWeekMonthYear": "必须为 Day/Week/Month/Year",
"MustBe1Or2": "只能为 1 或 2",
"DigitsOnly": "仅允许数字",
"InvalidForm": "表单校验失败",
"InvalidJSON": "无效的 JSON"
},
"AlertRules": {
"CoverAllServers": "监控所有服务器",
"IgnoreAllSelectSpecific": "忽略所有,选择特定服务器",
"IgnoreHint": "{{server}}ID: true/false",
"IgnoreExample": "例如:{\"1\": true, \"2\": false}"
},
"Search": "搜索...",
"Format": "格式化",
"Formatted": "已格式化",
"Copy": "复制",
"Paste": "粘贴",
"CopiedToClipboard": "已复制到剪贴板",
"ClipboardWriteFailed": "无法写入剪贴板",
"PastedFromClipboard": "已从剪贴板粘贴",
"ClipboardReadFailed": "无法读取剪贴板",
"FormatMetricUnits": "格式化数据单位"
}
+2 -1
View File
@@ -185,5 +185,6 @@
"EditServerConfig": "編輯伺服器配置",
"Option": "選項",
"Value": "值",
"Preview": "預覽"
"Preview": "預覽",
"FormatMetricUnits": "格式化資料單位"
}
+2 -5
View File
@@ -1,7 +1,7 @@
import { createRoot } from "react-dom/client"
import { RouterProvider, createBrowserRouter } from "react-router-dom"
import { TerminalPage } from "./components/terminal"
import ErrorPage from "./error-page"
import { AuthProvider } from "./hooks/useAuth"
import { NotificationProvider } from "./hooks/useNotfication"
@@ -110,10 +110,7 @@ const router = createBrowserRouter([
</NotificationProvider>
),
},
{
path: "/dashboard/terminal/:id",
element: <TerminalPage />,
},
{
path: "/dashboard/profile",
element: (
+3 -3
View File
@@ -116,7 +116,7 @@ export default function AlertRulePage() {
className="flex gap-2"
delete={{
fn: deleteAlertRules,
id: s.id,
id: s.id!,
mutate: mutate,
}}
>
@@ -144,10 +144,10 @@ export default function AlertRulePage() {
<div className="flex mt-6 mb-4">
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup
className="flex-2 flex gap-2 ml-auto"
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
delete={{
fn: deleteAlertRules,
id: selectedRows.map((r) => r.original.id),
id: selectedRows.map((r) => r.original.id!),
mutate: mutate,
}}
>
+7 -7
View File
@@ -164,7 +164,7 @@ export default function CronPage() {
return (
<ActionButtonGroup
className="flex gap-2"
delete={{ fn: deleteCron, id: s.id, mutate: mutate }}
delete={{ fn: deleteCron, id: s.id!, mutate: mutate }}
>
<>
<IconButton
@@ -172,7 +172,7 @@ export default function CronPage() {
icon="play"
onClick={async () => {
try {
await runCron(s.id)
await runCron(s.id!)
} catch (e) {
console.error(e)
toast(t("Error"), {
@@ -208,14 +208,14 @@ export default function CronPage() {
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-3">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Task")}</h1>
<div className="px-3 max-w-7xl mx-auto">
<div className="flex flex-col sm:flex-row sm:items-center justify-between w-full gap-3 mt-6 mb-4">
<h1 className="text-3xl font-bold tracking-tight">{t("Task")}</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
className="flex gap-2 flex-wrap shrink-0"
delete={{
fn: deleteCron,
id: selectedRows.map((r) => r.original.id),
id: selectedRows.map((r) => r.original.id!),
mutate: mutate,
}}
>
+3 -3
View File
@@ -121,7 +121,7 @@ export default function DDNSPage() {
className="flex gap-2"
delete={{
fn: deleteDDNSProfiles,
id: s.id,
id: s.id!,
mutate: mutate,
}}
>
@@ -149,10 +149,10 @@ export default function DDNSPage() {
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("DDNS")}</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
delete={{
fn: deleteDDNSProfiles,
id: selectedRows.map((r) => r.original.id),
id: selectedRows.map((r) => r.original.id!),
mutate: mutate,
}}
>
+453 -246
View File
@@ -1,272 +1,479 @@
// src/routes/domain.tsx (最终 Bug 修复版)
import { useState, useEffect } from 'react'
import { PlusCircle, RefreshCw, MoreVertical, Trash2, Edit, CheckCircle } from 'lucide-react'
import {
addDomain,
deleteDomain,
syncAllDomains,
syncDomainWHOIS,
updateDomain,
useDomainList,
verifyDomain,
} from "@/api/domain"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
// 导入 shadcn/ui 组件
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { toast } from 'sonner'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Textarea } from "@/components/ui/textarea"
// 导入 API 类型和函数
import type { Domain, BillingDataMod } from '@/types/api'
import { useDomainList, addDomain, verifyDomain, deleteDomain, updateDomain } from '@/api/domain'
import useSWR from 'swr'
import type { BillingDataMod, Domain } from "@/types/domain"
import {
CheckCircle,
Edit,
MoreVertical,
PlusCircle,
RefreshCcw,
RefreshCw,
Trash2,
} from "lucide-react"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import useSWR from "swr"
export default function DomainPage() {
// --- React State Hooks ---
const [domains, setDomains] = useState<Domain[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
const [newDomainName, setNewDomainName] = useState('')
const [verificationToken, setVerificationToken] = useState('')
const [isVerificationInfoModalOpen, setIsVerificationInfoModalOpen] = useState(false)
// --- React State Hooks ---
const [domains, setDomains] = useState<Domain[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [currentDomain, setCurrentDomain] = useState<Domain | null>(null)
const [editFormData, setEditFormData] = useState<Partial<BillingDataMod>>({})
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
const [newDomainName, setNewDomainName] = useState("")
// --- 数据获取 (使用 SWR) ---
const { data: domainData, error, mutate } = useSWR('/api/v1/domains', useDomainList, { revalidateOnFocus: false })
const [verificationToken, setVerificationToken] = useState("")
const [isVerificationInfoModalOpen, setIsVerificationInfoModalOpen] = useState(false)
useEffect(() => {
if (domainData) {
setDomains(domainData)
setIsLoading(false)
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [currentDomain, setCurrentDomain] = useState<Domain | null>(null)
const [editFormData, setEditFormData] = useState<Partial<BillingDataMod>>({})
// --- 数据获取 (使用 SWR) ---
const {
data: domainData,
error,
mutate,
isValidating,
} = useSWR("/api/v1/domains", useDomainList, { revalidateOnFocus: false })
useEffect(() => {
if (domainData) {
setDomains(domainData)
setIsLoading(false)
}
if (error) {
toast.error("无法加载域名列表,请检查后端服务是否正常。")
setIsLoading(false)
}
}, [domainData, error])
const handleRefreshAll = async () => {
try {
await syncAllDomains()
toast.success("刷新成功", { description: "已触发所有域名的状态同步。" })
mutate()
} catch (err) {
toast.error("刷新失败", { description: (err as Error).message })
}
}
if (error) {
toast.error('无法加载域名列表,请检查后端服务是否正常。')
setIsLoading(false)
const handleAddDomain = async () => {
if (!newDomainName) {
toast.error("请输入域名")
return
}
try {
const response = await addDomain(newDomainName)
setVerificationToken(response.VerifyToken)
setIsAddModalOpen(false)
setIsVerificationInfoModalOpen(true)
setNewDomainName("")
mutate()
} catch (err) {
toast.error("添加失败", { description: (err as Error).message })
}
}
}, [domainData, error])
const handleAddDomain = async () => {
if (!newDomainName) {
toast.error('请输入域名')
return
const handleVerify = async (domainId: number) => {
try {
const response = await verifyDomain(domainId)
if (response.success) {
toast.success("验证成功", { description: response.message })
} else {
toast.warning("验证失败", { description: response.message })
}
setTimeout(() => mutate(), 2000)
} catch (err) {
toast.error("操作失败", { description: (err as Error).message })
}
}
try {
const response = await addDomain(newDomainName)
setVerificationToken(response.VerifyToken)
setIsAddModalOpen(false)
setIsVerificationInfoModalOpen(true)
setNewDomainName('')
mutate()
} catch (err) {
toast.error('添加失败', { description: (err as Error).message })
const handleSyncWhois = async (domainId: number) => {
const loadingToast = toast.loading("正在同步 Whois 信息...")
try {
await syncDomainWHOIS(domainId)
toast.success("同步成功", { id: loadingToast, description: "域名 Whois 信息已更新。" })
mutate()
} catch (err) {
toast.error("同步失败", { id: loadingToast, description: (err as Error).message })
}
}
}
const handleVerify = async (domainId: number) => {
try {
const response = await verifyDomain(domainId)
if (response.success) {
toast.success('验证成功', { description: response.message })
} else {
toast.warning('验证失败', { description: response.message })
}
setTimeout(() => mutate(), 2000)
} catch (err) {
toast.error('操作失败', { description: (err as Error).message })
const handleDelete = async (domainId: number, domainName: string) => {
if (window.confirm(`确定要删除域名 ${domainName} 吗?`)) {
try {
await deleteDomain(domainId)
toast.success("删除成功", { description: `域名 ${domainName} 已被删除。` })
mutate()
} catch (err) {
toast.error("删除失败", { description: (err as Error).message })
}
}
}
}
const handleDelete = async (domainId: number, domainName: string) => {
if (window.confirm(`确定要删除域名 ${domainName} 吗?`)) {
try {
await deleteDomain(domainId)
toast.success('删除成功', { description: `域名 ${domainName} 已被删除。` })
mutate()
} catch (err) {
toast.error('删除失败', { description: (err as Error).message })
}
const handlePublicToggle = async (domain: Domain) => {
try {
await updateDomain(domain.ID, {
is_public: !domain.IsPublic,
billing_data: domain.BillingData as BillingDataMod,
})
toast.success(`域名 ${domain.Domain} 的可见状态已更新`)
mutate()
} catch (err) {
toast.error("更新失败", { description: (err as Error).message })
}
}
}
const handlePublicToggle = async (domain: Domain) => {
try {
await updateDomain(domain.ID, {
is_public: !domain.IsPublic,
billing_data: domain.BillingData as BillingDataMod,
})
toast.success(`域名 ${domain.Domain} 的可见状态已更新`)
mutate()
} catch (err) {
toast.error('更新失败', { description: (err as Error).message })
const handleEditClick = (domain: Domain) => {
setCurrentDomain(domain)
setEditFormData(domain.BillingData || {})
setIsEditModalOpen(true)
}
}
const handleEditClick = (domain: Domain) => {
setCurrentDomain(domain)
setEditFormData(domain.BillingData || {})
setIsEditModalOpen(true)
}
const handleEditFormChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setEditFormData({
...editFormData,
[e.target.name]: e.target.value,
})
}
const handleUpdateDomain = async () => {
if (!currentDomain) return
try {
const dataToSend = { ...editFormData };
if (dataToSend.registeredDate) {
dataToSend.registeredDate = new Date(dataToSend.registeredDate).toISOString();
}
if (dataToSend.endDate) {
dataToSend.endDate = new Date(dataToSend.endDate).toISOString();
}
await updateDomain(currentDomain.ID, {
is_public: currentDomain.IsPublic,
billing_data: dataToSend as BillingDataMod
})
toast.success('更新成功', { description: `域名 ${currentDomain.Domain} 的配置已保存。` })
setIsEditModalOpen(false)
mutate()
} catch (err) {
toast.error('更新失败', { description: (err as Error).message })
const handleEditFormChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setEditFormData({
...editFormData,
[e.target.name]: e.target.value,
})
}
}
const getStatusVariant = (status: string): 'default' | 'secondary' | 'destructive' | 'outline' => {
switch (status) {
case 'verified': return 'default'
case 'pending': return 'secondary'
case 'expired': return 'destructive'
default: return 'outline'
const handleUpdateDomain = async () => {
if (!currentDomain) return
try {
const dataToSend = { ...editFormData }
if (dataToSend.registeredDate) {
dataToSend.registeredDate = new Date(dataToSend.registeredDate).toISOString()
}
if (dataToSend.endDate) {
dataToSend.endDate = new Date(dataToSend.endDate).toISOString()
}
await updateDomain(currentDomain.ID, {
is_public: currentDomain.IsPublic,
billing_data: dataToSend as BillingDataMod,
})
toast.success("更新成功", {
description: `域名 ${currentDomain.Domain} 的配置已保存。`,
})
setIsEditModalOpen(false)
mutate()
} catch (err) {
toast.error("更新失败", { description: (err as Error).message })
}
}
}
// --- JSX 渲染 (保持不变) ---
return (
<>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="icon" onClick={() => mutate()} disabled={isLoading}>
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
</Button>
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
<DialogTrigger asChild><Button><PlusCircle className="mr-2 h-4 w-4" /></Button></DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription> "example.com"</DialogDescription>
</DialogHeader>
<div className="py-4">
<Input value={newDomainName} onChange={(e) => setNewDomainName(e.target.value)} placeholder="your-domain.com" onKeyUp={(e) => e.key === 'Enter' && handleAddDomain()} />
</div>
<DialogFooter>
<Button variant="secondary" onClick={() => setIsAddModalOpen(false)}></Button>
<Button onClick={handleAddDomain}></Button>
</DialogFooter>
</DialogContent>
const getStatusVariant = (
status: string,
): "default" | "secondary" | "destructive" | "outline" => {
switch (status) {
case "verified":
return "default"
case "pending":
return "secondary"
case "expired":
return "destructive"
default:
return "outline"
}
}
// --- JSX 渲染 (保持不变) ---
return (
<>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={handleRefreshAll}
disabled={isValidating}
>
<RefreshCw className={`h-4 w-4 ${isValidating ? "animate-spin" : ""}`} />
</Button>
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
<DialogTrigger asChild>
<Button>
<PlusCircle className="mr-2 h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
"example.com"
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Input
value={newDomainName}
onChange={(e) => setNewDomainName(e.target.value)}
placeholder="your-domain.com"
onKeyUp={(e) => e.key === "Enter" && handleAddDomain()}
/>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => setIsAddModalOpen(false)}
>
</Button>
<Button onClick={handleAddDomain}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-10 text-muted-foreground">...</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{domains.map((domain) => (
<TableRow key={domain.ID}>
<TableCell className="font-medium">
{domain.Domain}
</TableCell>
<TableCell>
<Badge variant={getStatusVariant(domain.Status)}>
{domain.Status}
</Badge>
</TableCell>
<TableCell>{domain.expires_in_days ?? "N/A"}</TableCell>
<TableCell>
<Switch
checked={domain.IsPublic}
onCheckedChange={() => handlePublicToggle(domain)}
/>
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{domain.Status === "pending" && (
<DropdownMenuItem
onClick={() => handleVerify(domain.ID)}
>
<CheckCircle className="mr-2 h-4 w-4" />{" "}
</DropdownMenuItem>
)}
{domain.Status === "verified" && (
<DropdownMenuItem
onClick={() =>
handleSyncWhois(domain.ID)
}
>
<RefreshCcw className="mr-2 h-4 w-4" />{" "}
Whois
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => handleEditClick(domain)}
>
<Edit className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem
className="text-red-600"
onClick={() =>
handleDelete(domain.ID, domain.Domain)
}
>
<Trash2 className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
{/* 验证信息弹窗 */}
<Dialog
open={isVerificationInfoModalOpen}
onOpenChange={setIsVerificationInfoModalOpen}
>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
DNS TXT
</DialogDescription>
</DialogHeader>
<div className="py-4 space-y-2">
<p> DNS </p>
<div className="p-2 bg-muted rounded-md text-sm">
<p>
<span className="font-semibold">:</span> TXT
</p>
<p>
<span className="font-semibold">/:</span> @
</p>
<p className="font-semibold">:</p>
<p className="font-mono bg-background p-2 rounded">
{verificationToken}
</p>
</div>
<p className="text-xs text-muted-foreground">
DNS
</p>
</div>
<DialogFooter>
<Button onClick={() => setIsVerificationInfoModalOpen(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
{isLoading ? ( <div className="text-center py-10 text-muted-foreground">...</div> ) : (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{domains.map((domain) => (
<TableRow key={domain.ID}>
<TableCell className="font-medium">{domain.Domain}</TableCell>
<TableCell><Badge variant={getStatusVariant(domain.Status)}>{domain.Status}</Badge></TableCell>
<TableCell>{domain.expires_in_days ?? 'N/A'}</TableCell>
<TableCell>
<Switch
checked={domain.IsPublic}
onCheckedChange={() => handlePublicToggle(domain)}
/>
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild><Button variant="ghost" size="icon"><MoreVertical className="h-4 w-4" /></Button></DropdownMenuTrigger>
<DropdownMenuContent>
{domain.Status === 'pending' && (<DropdownMenuItem onClick={() => handleVerify(domain.ID)}><CheckCircle className="mr-2 h-4 w-4" /> </DropdownMenuItem>)}
<DropdownMenuItem onClick={() => handleEditClick(domain)}><Edit className="mr-2 h-4 w-4" /> </DropdownMenuItem>
<DropdownMenuItem className="text-red-600" onClick={() => handleDelete(domain.ID, domain.Domain)}><Trash2 className="mr-2 h-4 w-4" /> </DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
{/* 验证信息弹窗 */}
<Dialog open={isVerificationInfoModalOpen} onOpenChange={setIsVerificationInfoModalOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription> DNS TXT </DialogDescription>
</DialogHeader>
<div className="py-4 space-y-2">
<p> DNS </p>
<div className="p-2 bg-muted rounded-md text-sm">
<p><span className="font-semibold">:</span> TXT</p>
<p><span className="font-semibold">/:</span> @</p>
<p className="font-semibold">:</p>
<p className="font-mono bg-background p-2 rounded">{verificationToken}</p>
</div>
<p className="text-xs text-muted-foreground">DNS </p>
</div>
<DialogFooter><Button onClick={() => setIsVerificationInfoModalOpen(false)}></Button></DialogFooter>
</DialogContent>
</Dialog>
{/* 编辑弹窗 */}
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription> <span className="font-mono">{currentDomain?.Domain}</span> </DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4"><Label htmlFor="registrar" className="text-right"></Label><Input id="registrar" name="registrar" value={editFormData.registrar || ''} onChange={handleEditFormChange} className="col-span-3" /></div>
<div className="grid grid-cols-4 items-center gap-4"><Label htmlFor="registeredDate" className="text-right"></Label><Input id="registeredDate" name="registeredDate" type="date" value={editFormData.registeredDate?.split('T')[0] || ''} onChange={handleEditFormChange} className="col-span-3" /></div>
<div className="grid grid-cols-4 items-center gap-4"><Label htmlFor="endDate" className="text-right"></Label><Input id="endDate" name="endDate" type="date" value={editFormData.endDate?.split('T')[0] || ''} onChange={handleEditFormChange} className="col-span-3" /></div>
<div className="grid grid-cols-4 items-center gap-4"><Label htmlFor="renewalPrice" className="text-right"></Label><Input id="renewalPrice" name="renewalPrice" value={editFormData.renewalPrice || ''} onChange={handleEditFormChange} className="col-span-3" /></div>
<div className="grid grid-cols-4 items-center gap-4"><Label htmlFor="notes" className="text-right"></Label><Textarea id="notes" name="notes" value={editFormData.notes || ''} onChange={handleEditFormChange} className="col-span-3" /></div>
</div>
<DialogFooter>
<Button variant="secondary" onClick={() => setIsEditModalOpen(false)}></Button>
<Button onClick={handleUpdateDomain}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}
{/* 编辑弹窗 */}
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
<span className="font-mono">{currentDomain?.Domain}</span>{" "}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="registrar" className="text-right">
</Label>
<Input
id="registrar"
name="registrar"
value={editFormData.registrar || ""}
onChange={handleEditFormChange}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="registeredDate" className="text-right">
</Label>
<Input
id="registeredDate"
name="registeredDate"
type="date"
value={editFormData.registeredDate?.split("T")[0] || ""}
onChange={handleEditFormChange}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="endDate" className="text-right">
</Label>
<Input
id="endDate"
name="endDate"
type="date"
value={editFormData.endDate?.split("T")[0] || ""}
onChange={handleEditFormChange}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="renewalPrice" className="text-right">
</Label>
<Input
id="renewalPrice"
name="renewalPrice"
value={editFormData.renewalPrice || ""}
onChange={handleEditFormChange}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="notes" className="text-right">
</Label>
<Textarea
id="notes"
name="notes"
value={editFormData.notes || ""}
onChange={handleEditFormChange}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button variant="secondary" onClick={() => setIsEditModalOpen(false)}>
</Button>
<Button onClick={handleUpdateDomain}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}
+3 -3
View File
@@ -106,7 +106,7 @@ export default function NATPage() {
return (
<ActionButtonGroup
className="flex gap-2"
delete={{ fn: deleteNAT, id: s.id, mutate: mutate }}
delete={{ fn: deleteNAT, id: s.id!, mutate: mutate }}
>
<NATCard mutate={mutate} data={s} />
</ActionButtonGroup>
@@ -132,10 +132,10 @@ export default function NATPage() {
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight"> {t("NATT")}</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
delete={{
fn: deleteNAT,
id: selectedRows.map((r) => r.original.id),
id: selectedRows.map((r) => r.original.id!),
mutate: mutate,
}}
>
+3 -3
View File
@@ -97,7 +97,7 @@ export default function NotificationGroupPage() {
className="flex gap-2"
delete={{
fn: deleteNotificationGroups,
id: s.group.id,
id: s.group.id!,
mutate: mutate,
}}
>
@@ -125,10 +125,10 @@ export default function NotificationGroupPage() {
<div className="flex mt-6 mb-4">
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup
className="flex-2 flex gap-2 ml-auto"
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
delete={{
fn: deleteNotificationGroups,
id: selectedRows.map((r) => r.original.group.id),
id: selectedRows.map((r) => r.original.group.id!),
mutate: mutate,
}}
>
+4 -4
View File
@@ -83,7 +83,7 @@ export default function NotificationPage() {
accessorFn: (row) => {
return (
notifierGroup
?.filter((ng) => ng.notifications?.includes(row.id))
?.filter((ng) => ng.notifications?.includes(row.id!))
.map((ng) => ng.group.id) || []
)
},
@@ -112,7 +112,7 @@ export default function NotificationPage() {
className="flex gap-2"
delete={{
fn: deleteNotification,
id: s.id,
id: s.id!,
mutate: mutate,
}}
>
@@ -140,10 +140,10 @@ export default function NotificationPage() {
<div className="flex mt-6 mb-4">
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup
className="flex-2 flex gap-2 ml-auto"
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
delete={{
fn: deleteNotification,
id: selectedRows.map((r) => r.original.id),
id: selectedRows.map((r) => r.original.id!),
mutate: mutate,
}}
>
+7 -7
View File
@@ -22,7 +22,7 @@ import {
TableRow,
} from "@/components/ui/table"
import { useAuth } from "@/hooks/useAuth"
import { ModelOnlineUser, ModelOnlineUserApi } from "@/types"
import { GithubComNezhahqNezhaModelPaginatedResponseArrayModelOnlineUserModelOnlineUser, ModelOnlineUser } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
@@ -40,7 +40,7 @@ export default function OnlineUserPage() {
// 计算 offset
const offset = (page - 1) * pageSize
const { data, mutate, error, isLoading } = useSWR<ModelOnlineUserApi>(
const { data, mutate, error, isLoading } = useSWR<GithubComNezhahqNezhaModelPaginatedResponseArrayModelOnlineUserModelOnlineUser, Error>(
`/api/v1/online-user?offset=${offset}&limit=${pageSize}`,
swrFetcher,
)
@@ -94,7 +94,7 @@ export default function OnlineUserPage() {
accessorFn: (row) => row.connected_at,
cell: ({ row }) => {
const s = row.original
const date = new Date(s.connected_at)
const date = new Date(s.connected_at!)
return <span>{date.toISOString()}</span>
},
},
@@ -125,10 +125,10 @@ export default function OnlineUserPage() {
}
const dataCache = useMemo(() => {
return data?.value ?? []
return data?.data?.value ?? []
}, [data])
const table = useReactTable({
const table = useReactTable<ModelOnlineUser>({
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
@@ -137,9 +137,9 @@ export default function OnlineUserPage() {
const selectedRows = table.getSelectedRowModel().rows
const renderPagination = () => {
if (!data?.pagination) return null
if (!data?.data?.pagination) return null
const { total } = data.pagination
const { total = 0 } = data.data.pagination
const totalPages = Math.ceil(total / pageSize)
const handlePageChange = (newPage: number) => {
+4 -2
View File
@@ -36,8 +36,10 @@ export default function Root() {
return (
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<section className="text-sm mx-auto h-full flex flex-col justify-between">
<div>
<section
className="text-sm mx-auto h-full flex flex-col justify-between relative z-10 bg-background"
>
<div className="flex-1">
<Header />
<div className="max-w-5xl mx-auto">
<Outlet />
+3 -3
View File
@@ -97,7 +97,7 @@ export default function ServerGroupPage() {
className="flex gap-2"
delete={{
fn: deleteServerGroups,
id: s.group.id,
id: s.group.id!,
mutate: mutate,
}}
>
@@ -125,10 +125,10 @@ export default function ServerGroupPage() {
<div className="flex mt-6 mb-4">
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
delete={{
fn: deleteServerGroups,
id: selectedRows.map((r) => r.original.group.id),
id: selectedRows.map((r) => r.original.group.id!),
mutate: mutate,
}}
>
+9 -9
View File
@@ -9,7 +9,6 @@ import { NoteMenu } from "@/components/note-menu"
import { ServerCard } from "@/components/server"
import { ServerConfigCard } from "@/components/server-config"
import { ServerConfigCardBatch } from "@/components/server-config-batch"
import { TerminalButton } from "@/components/terminal"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
@@ -85,7 +84,7 @@ export default function ServerPage() {
accessorFn: (row) => {
return (
serverGroups
?.filter((sg) => sg.servers?.includes(row.id))
?.filter((sg) => sg.servers?.includes(row.id!))
.map((sg) => sg.group.id) || []
)
},
@@ -105,7 +104,7 @@ export default function ServerPage() {
{
header: t("Version"),
accessorKey: "host.version",
accessorFn: (row) => row.host.version || t("Unknown"),
accessorFn: (row) => row.host?.version || t("Unknown"),
},
{
header: t("EnableDDNS"),
@@ -141,12 +140,11 @@ export default function ServerPage() {
return (
<ActionButtonGroup
className="flex gap-2"
delete={{ fn: deleteServer, id: s.id, mutate: mutate }}
delete={{ fn: deleteServer, id: s.id!, mutate: mutate }}
>
<>
<TerminalButton id={s.id} />
<ServerCard mutate={mutate} data={s} />
<ServerConfigCard sid={s.id} variant="outline" />
<ServerConfigCard sid={s.id!} variant="outline" />
</>
</ActionButtonGroup>
)
@@ -181,7 +179,7 @@ export default function ServerPage() {
<IconButton
icon="update"
onClick={async () => {
const id = selectedRows.map((r) => r.original.id)
const id = selectedRows.map((r) => r.original.id) as number[]
if (id.length < 1) {
toast(t("Error"), {
description: t("Results.SelectAtLeastOneServer"),
@@ -214,9 +212,11 @@ export default function ServerPage() {
})
}}
/>
<BatchMoveServerIcon serverIds={selectedRows.map((r) => r.original.id)} />
<BatchMoveServerIcon
serverIds={selectedRows.map((r) => r.original.id) as number[]}
/>
<ServerConfigCardBatch
sid={selectedRows.map((r) => r.original.id)}
sid={selectedRows.map((r) => r.original.id) as number[]}
className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-yellow-600 text-white hover:bg-yellow-500 dark:hover:bg-yellow-700 rounded-lg"
/>
<InstallCommandsMenu className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-blue-700 text-white hover:bg-blue-600 dark:hover:bg-blue-800 rounded-lg" />
+7 -7
View File
@@ -60,7 +60,7 @@ export default function ServicePage() {
{
header: "ID",
accessorKey: "id",
accessorFn: (row) => row.id,
accessorFn: (row) => `${row.id}(${row.display_index ?? 0})`,
},
{
header: t("Name"),
@@ -152,7 +152,7 @@ export default function ServicePage() {
return (
<ActionButtonGroup
className="flex gap-2"
delete={{ fn: deleteService, id: s.id, mutate: mutate }}
delete={{ fn: deleteService, id: s.id!, mutate: mutate }}
>
<ServiceCard mutate={mutate} data={s} />
</ActionButtonGroup>
@@ -174,14 +174,14 @@ export default function ServicePage() {
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-3">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Services")}</h1>
<div className="px-3 max-w-7xl mx-auto">
<div className="flex flex-col sm:flex-row sm:items-center justify-between w-full gap-3 mt-6 mb-4">
<h1 className="text-3xl font-bold tracking-tight">{t("Service")}</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
className="flex gap-2 flex-wrap shrink-0"
delete={{
fn: deleteService,
id: selectedRows.map((r) => r.original.id),
id: selectedRows.map((r) => r.original.id!),
mutate: mutate,
}}
>
+157 -8
View File
@@ -3,6 +3,7 @@ import { SettingsTab } from "@/components/settings-tab"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import { Combobox } from "@/components/ui/combobox"
import {
Form,
FormControl,
@@ -22,6 +23,7 @@ import {
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { useAuth } from "@/hooks/useAuth"
import { useNotification } from "@/hooks/useNotfication"
import useSetting from "@/hooks/useSetting"
import { asOptionalField } from "@/lib/utils"
import { nezhaLang, settingCoverageTypes } from "@/types"
@@ -50,6 +52,13 @@ const settingFormSchema = z.object({
tls: asOptionalField(z.boolean()),
enable_ip_change_notification: asOptionalField(z.boolean()),
enable_plain_ip_in_notification: asOptionalField(z.boolean()),
custom_logo: asOptionalField(z.string()),
custom_description: asOptionalField(z.string()),
custom_links: asOptionalField(z.string()),
background_image_day: asOptionalField(z.string()),
background_image_night: asOptionalField(z.string()),
telegram_bot_token: asOptionalField(z.string()),
telegram_admin_chat_id: asOptionalField(z.string()),
})
export default function SettingsPage() {
@@ -58,14 +67,20 @@ export default function SettingsPage() {
const { profile } = useAuth()
const navigate = useNavigate()
const { notifierGroup } = useNotification()
const ngroupList = notifierGroup?.map((ng) => ({
value: `${ng.group.id}`,
label: ng.group.name,
})) || [{ value: "", label: "" }]
const isAdmin = profile?.role === 0
if (!isAdmin) {
navigate("/dashboard/settings/online-user")
}
const form = useForm<z.infer<typeof settingFormSchema>>({
resolver: zodResolver(settingFormSchema),
const form = useForm({
resolver: zodResolver(settingFormSchema) as any,
defaultValues: config
? {
...config.config,
@@ -92,7 +107,7 @@ export default function SettingsPage() {
}
}, [config?.config, form])
const onSubmit = async (values: z.infer<typeof settingFormSchema>) => {
const onSubmit = async (values: any) => {
try {
await updateSettings(values)
form.reset()
@@ -118,6 +133,36 @@ export default function SettingsPage() {
<div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
<FormField
control={form.control}
name="ip_change_notification_group_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("IPChangeNotificationGroupID")}</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expiry_notification_group_id"
render={({ field }) => (
<FormItem>
<FormLabel>Expiry Notification Group ID</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Enter Group ID"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="site_name"
@@ -131,6 +176,109 @@ export default function SettingsPage() {
</FormItem>
)}
/>
<FormField
control={form.control}
name="custom_logo"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Logo URL</FormLabel>
<FormControl>
<Input
placeholder="https://example.com/logo.png"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="custom_description"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Description / Subtitle</FormLabel>
<FormControl>
<Input placeholder="My monitoring dashboard" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="custom_links"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Links (JSON Array)</FormLabel>
<FormControl>
<Input
placeholder='[{"link":"https://loohui.com/","name":"Blog","blank":false}]'
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="background_image_day"
render={({ field }) => (
<FormItem>
<FormLabel>Background Image (Day)</FormLabel>
<FormControl>
<Input
placeholder="https://example.com/day.jpg"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="background_image_night"
render={({ field }) => (
<FormItem>
<FormLabel>Background Image (Night)</FormLabel>
<FormControl>
<Input
placeholder="https://example.com/night.jpg"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="telegram_bot_token"
render={({ field }) => (
<FormItem>
<FormLabel>Telegram Bot Token</FormLabel>
<FormControl>
<Input placeholder="123456789:ABCDEF..." {...field} value={field.value as string || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="telegram_admin_chat_id"
render={({ field }) => (
<FormItem>
<FormLabel>Telegram Admin Chat ID</FormLabel>
<FormControl>
<Input placeholder="12345678" {...field} value={field.value as string || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="language"
@@ -448,12 +596,13 @@ export default function SettingsPage() {
name="ip_change_notification_group_id"
render={({ field }) => (
<FormItem>
<FormLabel>{t("NotifierGroupID")}</FormLabel>
<FormLabel>{t("NotifierGroup")}</FormLabel>
<FormControl>
<Input
placeholder="0"
type="number"
{...field}
<Combobox
placeholder={t("Search")}
options={ngroupList}
onValueChange={field.onChange}
defaultValue={field.value?.toString()}
/>
</FormControl>
<FormMessage />
+4 -3
View File
@@ -77,7 +77,8 @@ export default function UserPage() {
{
header: t("LastLogin"),
accessorKey: "updated_at",
accessorFn: (row) => row.updated_at ? new Date(row.updated_at).toLocaleString() : t("Never"),
accessorFn: (row) =>
row.updated_at ? new Date(row.updated_at).toLocaleString() : t("Never"),
},
{
id: "actions",
@@ -89,7 +90,7 @@ export default function UserPage() {
className="flex gap-2"
delete={{
fn: deleteUser,
id: s.id,
id: s.id!,
mutate: mutate,
}}
>
@@ -120,7 +121,7 @@ export default function UserPage() {
className="flex-2 flex gap-2 ml-auto"
delete={{
fn: deleteUser,
id: selectedRows.map((r) => r.original.id),
id: selectedRows.map((r) => r.original.id!),
mutate: mutate,
}}
>
+3 -3
View File
@@ -98,13 +98,13 @@ export default function WAFPage() {
header: t("LastBlockReason"),
accessorKey: "lastBlockReason",
accessorFn: (row) => row.block_reason,
cell: ({ row }) => <span>{wafBlockReasons[row.original.block_reason] || ""}</span>,
cell: ({ row }) => <span>{wafBlockReasons[row.original.block_reason!] || ""}</span>,
},
{
header: t("BlockIdentifier"),
accessorKey: "BlockIdentifier",
accessorFn: (row) => {
return wafBlockIdentifiers[row.block_identifier] || row.block_identifier
return wafBlockIdentifiers[row.block_identifier!] || row.block_identifier
},
},
{
@@ -158,7 +158,7 @@ export default function WAFPage() {
const renderPagination = () => {
if (!data?.pagination) return null
const { total } = data.pagination
const { total = 0 } = data.pagination
const totalPages = Math.ceil(total / pageSize)
const handlePageChange = (newPage: number) => {
+543 -504
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
export interface BillingDataMod {
registrar?: string;
registeredDate?: string;
endDate?: string;
renewalPrice?: string;
autoRenewal?: string;
notes?: string;
cycle?: string;
amount?: string;
}
export interface Domain {
ID: number;
Domain: string;
Status: string;
VerifyToken: string;
IsPublic: boolean;
BillingData: BillingDataMod | null;
expires_in_days?: number;
}
+1 -1
View File
@@ -26,7 +26,7 @@ export const AgentConfigSchema = z.object({
),
),
ip_report_period: asOptionalField(z.coerce.number().int().min(30)),
nic_allowlist: asOptionalField(z.record(z.boolean())),
nic_allowlist: asOptionalField(z.record(z.string(), z.boolean())),
nic_allowlist_raw: asOptionalField(
z.string().refine(
(val) => {
+25
View File
@@ -0,0 +1,25 @@
package main
import (
"encoding/json"
"fmt"
"strings"
)
type Config struct {
IPReportPeriod int `json:"ip_report_period"`
}
func main() {
jsonData := `{"ip_report_period":30, "unknown_field": 123}`
var c Config
dec := json.NewDecoder(strings.NewReader(jsonData))
dec.DisallowUnknownFields()
err := dec.Decode(&c)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Success")
}
}
-1
View File
@@ -1 +0,0 @@
{"root":["./src/error-page.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/alert-rule.ts","./src/api/api.ts","./src/api/cron.ts","./src/api/ddns.ts","./src/api/domain.ts","./src/api/fm.ts","./src/api/nat.ts","./src/api/notification-group.ts","./src/api/notification.ts","./src/api/oauth2.ts","./src/api/online-user.ts","./src/api/server-group.ts","./src/api/server.ts","./src/api/service.ts","./src/api/settings.ts","./src/api/terminal.ts","./src/api/user.ts","./src/api/waf.ts","./src/components/action-button-group.tsx","./src/components/alert-rule.tsx","./src/components/batch-move-server-icon.tsx","./src/components/copy-button.tsx","./src/components/cron.tsx","./src/components/ddns.tsx","./src/components/fm.tsx","./src/components/group-tab.tsx","./src/components/header-button-group.tsx","./src/components/header.tsx","./src/components/install-commands.tsx","./src/components/mode-toggle.tsx","./src/components/nat.tsx","./src/components/note-menu.tsx","./src/components/notification-group.tsx","./src/components/notification-tab.tsx","./src/components/notifier.tsx","./src/components/profile.tsx","./src/components/server-config-batch.tsx","./src/components/server-config.tsx","./src/components/server-group.tsx","./src/components/server.tsx","./src/components/service.tsx","./src/components/settings-tab.tsx","./src/components/terminal.tsx","./src/components/theme-provider.tsx","./src/components/user.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/icon.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/xui/filepath.tsx","./src/components/xui/icon-button.tsx","./src/components/xui/multi-select.tsx","./src/components/xui/navigation-menu.tsx","./src/components/xui/overlayless-sheet.tsx","./src/components/xui/pusher.tsx","./src/components/xui/virtulized-data-table.tsx","./src/hooks/useAuth.tsx","./src/hooks/useMainStore.ts","./src/hooks/useMediaQuery.tsx","./src/hooks/useNotfication.tsx","./src/hooks/useNotificationStore.ts","./src/hooks/useServer.tsx","./src/hooks/useServerStore.ts","./src/hooks/useSetting.tsx","./src/hooks/useTerminal.ts","./src/lib/fm.ts","./src/lib/i18n.ts","./src/lib/inject.ts","./src/lib/utils.ts","./src/routes/alert-rule.tsx","./src/routes/cron.tsx","./src/routes/ddns.tsx","./src/routes/domain.tsx","./src/routes/login.tsx","./src/routes/nat.tsx","./src/routes/notification-group.tsx","./src/routes/notification.tsx","./src/routes/online-user.tsx","./src/routes/profile.tsx","./src/routes/protect.tsx","./src/routes/root.tsx","./src/routes/server-group.tsx","./src/routes/server.tsx","./src/routes/service.tsx","./src/routes/settings.tsx","./src/routes/user.tsx","./src/routes/waf.tsx","./src/types/alert-rule.ts","./src/types/api.ts","./src/types/authContext.ts","./src/types/cron.ts","./src/types/ddns.ts","./src/types/fm.ts","./src/types/index.ts","./src/types/mainStore.ts","./src/types/notification.ts","./src/types/notificationContext.ts","./src/types/notificationStore.ts","./src/types/server.ts","./src/types/serverContext.ts","./src/types/serverStore.ts","./src/types/service.ts","./src/types/settings.ts"],"errors":true,"version":"5.6.3"}
+92 -1
View File
@@ -23,4 +23,95 @@ export default defineConfig({
"@": path.resolve(__dirname, "./src"),
},
},
})
build: {
cssCodeSplit: true,
sourcemap: false,
chunkSizeWarningLimit: 1500,
rollupOptions: {
output: {
manualChunks(id: string) {
if (!id.includes("node_modules")) return
// 提取顶级包名,兼容 scoped packages(如 @radix-ui/react-dialog
const match = id.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/)
const pkg = match ? match[1] : null
if (!pkg) return "vendor"
// 1. 核心框架:React 及其紧密依赖(必须合并,避免运行时错误)
if (
pkg === "react" ||
pkg === "react-dom" ||
pkg === "scheduler" ||
pkg === "react-router" ||
pkg === "react-router-dom" ||
pkg === "history"
) {
return "react"
}
// 2. UI 相关:Radix UI + shadcn 工具链
if (
pkg.startsWith("@radix-ui/") ||
pkg === "class-variance-authority" ||
pkg === "clsx" ||
pkg === "tailwind-merge"
) {
return "ui"
}
// 3. 表单与校验
if (
pkg === "react-hook-form" ||
pkg.startsWith("@hookform/") || // 匹配 @hookform/resolvers, @hookform/devtools 等
pkg === "zod"
) {
return "form"
}
// 4. 国际化
if (pkg === "i18next" || pkg === "react-i18next") {
return "i18n"
}
// 5. 数据获取
if (pkg === "swr") {
return "data"
}
// 6. 工具类库(高频、轻量、通用)—— 合并减少请求数
const utilityLibs = [
"lodash-es",
"date-fns",
"dayjs",
"axios",
"nanoid",
"uuid",
"immer",
"lodash",
]
if (utilityLibs.includes(pkg)) {
return "utils"
}
// 7. 大型独立库(如图表、富文本等)单独分包,按需加载
const largeLibs = [
"chart.js",
"recharts",
"echarts",
"quill",
"draft-js",
"monaco-editor",
"@monaco-editor/react",
]
if (largeLibs.includes(pkg)) {
return `lib-${pkg.replace(/@/g, "").replace(/\//g, "-")}`
}
// 8. 其他第三方库:按顶级包名分组,但限制数量(避免太多小 chunk)
// 如果你项目依赖很多,可考虑合并为 "vendor-others"
return "vendor"
},
},
},
},
})