Compare commits

...

25 Commits

Author SHA1 Message Date
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
85 changed files with 11696 additions and 932 deletions
+1
View File
@@ -35,6 +35,7 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: dist.zip files: dist.zip
generate_release_notes: true
- name: Changelog - name: Changelog
run: bun x changelogithub run: bun x changelogithub
+2
View File
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.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, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"iconLibrary": "lucide",
"aliases": { "aliases": {
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils", "utils": "@/lib/utils",
"ui": "@/components/ui", "ui": "@/components/ui",
"lib": "@/lib", "lib": "@/lib",
"hooks": "@/hooks" "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" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.5", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.5", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.1", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.4", "@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.5", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.1.5", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.13",
"@tanstack/react-table": "^8.20.6", "@tailwindcss/postcss": "^4.1.14",
"@tanstack/react-table": "^8.21.3",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@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-attach": "^0.11.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.4", "cmdk": "^1.1.1",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"framer-motion": "^11.18.2", "date-fns": "^4.1.0",
"i18next": "^24.2.2", "framer-motion": "^12.23.22",
"i18next-browser-languagedetector": "^8.0.2", "i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0",
"jotai-zustand": "^0.6.0", "jotai-zustand": "^0.6.0",
"lucide-react": "^0.454.0", "lucide-react": "^0.545.0",
"luxon": "^3.5.0", "luxon": "^3.7.2",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.14",
"react": "^19.0.0", "react": "^19.2.0",
"react-dom": "^19.0.0", "react-day-picker": "^9.11.1",
"react-hook-form": "^7.54.2", "react-dom": "^19.2.0",
"react-i18next": "^15.4.0", "react-hook-form": "^7.71.1",
"react-router-dom": "^7.1.5", "react-i18next": "^16.0.0",
"react-virtuoso": "^4.12.3", "react-router-dom": "^7.9.4",
"sonner": "^1.7.4", "react-virtuoso": "^4.14.1",
"swr": "^2.3.0", "sonner": "^2.0.7",
"tailwind-merge": "^2.6.0", "swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^3.24.1", "zod": "^4.1.12",
"zustand": "^5.0.3" "zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.37.0",
"@types/node": "^22.13.0", "@types/node": "^24.7.0",
"@types/react": "^18.3.18", "@types/react": "^19.2.2",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^19.2.1",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^5.0.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.21",
"eslint": "^9.19.0", "eslint": "^9.37.0",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.18", "eslint-plugin-react-refresh": "^0.4.23",
"globals": "^15.14.0", "globals": "^16.4.0",
"postcss": "^8.5.1", "postcss": "8.4.24",
"swagger-typescript-api": "^13.0.23", "swagger-typescript-api": "^13.2.15",
"tailwindcss": "^3.4.17", "tailwindcss": "3.4.19",
"typescript": "~5.6.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.22.0", "typescript-eslint": "^8.46.0",
"vite": "^6.0.11" "vite": "^7.1.9"
} }
} }
+1209
View File
File diff suppressed because it is too large Load Diff
-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} {children}
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<IconButton variant="destructive" icon="trash" /> <IconButton variant="destructive" icon="trash" className="text-white" />
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg"> <AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader> <AlertDialogHeader>
@@ -61,7 +61,10 @@ export function ActionButtonGroup<E, U>({
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel> <AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={buttonVariants({ variant: "destructive" })} className={buttonVariants({
variant: "destructive",
className: "text-white",
})}
onClick={handleDelete} onClick={handleDelete}
> >
{t("Confirm")} {t("Confirm")}
@@ -95,7 +98,7 @@ export function BlockButtonGroup<E, U>({
{children} {children}
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<IconButton variant="destructive" icon="ban" /> <IconButton variant="destructive" icon="ban" className="text-white" />
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg"> <AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader> <AlertDialogHeader>
@@ -107,7 +110,10 @@ export function BlockButtonGroup<E, U>({
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel> <AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={buttonVariants({ variant: "destructive" })} className={buttonVariants({
variant: "destructive",
className: "text-white",
})}
onClick={handleBlock} onClick={handleBlock}
> >
{t("Confirm")} {t("Confirm")}
+461 -30
View File
@@ -32,11 +32,10 @@ import {
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { useNotification } from "@/hooks/useNotfication" import { useNotification } from "@/hooks/useNotfication"
import { conv } from "@/lib/utils" import { conv } from "@/lib/utils"
import { asOptionalField } from "@/lib/utils"
import { ModelAlertRule } from "@/types" import { ModelAlertRule } from "@/types"
import { triggerModes } from "@/types" import { triggerModes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react" import { useEffect, useState } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -53,16 +52,16 @@ interface AlertRuleCardProps {
const ruleSchema = z.object({ const ruleSchema = z.object({
type: z.string(), type: z.string(),
min: asOptionalField(z.number()), min: z.number().optional(),
max: asOptionalField(z.number()), max: z.number().optional(),
cycle_start: asOptionalField(z.string()), cycle_start: z.string().optional(),
cycle_interval: asOptionalField(z.number()), cycle_interval: z.number().optional(),
cycle_unit: asOptionalField(z.enum(["hour", "day", "week", "month", "year"])), cycle_unit: z.enum(["hour", "day", "week", "month", "year"]).optional(),
duration: asOptionalField(z.number()), duration: z.number().optional(),
cover: z.number().int().min(0), cover: z.number().int().min(0),
ignore: asOptionalField(z.record(z.boolean())), ignore: z.record(z.string(), z.boolean()).optional(),
next_transfer_at: asOptionalField(z.record(z.string())), next_transfer_at: z.record(z.string(), z.string()).optional(),
last_cycle_status: asOptionalField(z.boolean()), last_cycle_status: z.boolean().optional(),
}) })
const alertRuleFormSchema = z.object({ const alertRuleFormSchema = z.object({
@@ -87,13 +86,16 @@ const alertRuleFormSchema = z.object({
recover_trigger_tasks_raw: z.string(), recover_trigger_tasks_raw: z.string(),
notification_group_id: z.coerce.number().int(), notification_group_id: z.coerce.number().int(),
trigger_mode: z.coerce.number().int().min(0), trigger_mode: z.coerce.number().int().min(0),
enable: asOptionalField(z.boolean()), enable: z.boolean().optional(),
}) })
export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) => { export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) => {
const { t } = useTranslation() 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 defaultValues: data
? { ? {
...data, ...data,
@@ -119,7 +121,28 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
const [open, setOpen] = useState(false) 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.rules = JSON.parse(values.rules_raw)
values.fail_trigger_tasks = conv.strToArr(values.fail_trigger_tasks_raw).map(Number) 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) 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 /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <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 <FormField
control={form.control} control={form.control}
name="name" name="name"
@@ -175,19 +201,424 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
</FormItem> </FormItem>
)} )}
/> />
<FormField {/* 结构化规则编辑器 */}
control={form.control} <FormItem>
name="rules_raw" <FormLabel>{t("Rules")}</FormLabel>
render={({ field }) => ( <div className="space-y-3">
<FormItem> {rulesUI.map((r, idx) => {
<FormLabel>{t("Rules")}</FormLabel> const isCycle =
<FormControl> typeof r.type === "string" &&
<Textarea className="resize-y" {...field} /> r.type.endsWith("_cycle")
</FormControl> const isOffline = r.type === "offline"
<FormMessage /> return (
</FormItem> <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 <FormField
control={form.control} control={form.control}
name="notification_group_id" name="notification_group_id"
@@ -196,7 +627,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
<FormLabel>{t("NotifierGroup")}</FormLabel> <FormLabel>{t("NotifierGroup")}</FormLabel>
<FormControl> <FormControl>
<Combobox <Combobox
placeholder="Search..." placeholder={t("Search")}
options={ngroupList} options={ngroupList}
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value.toString()} defaultValue={field.value.toString()}
+13 -6
View File
@@ -17,13 +17,17 @@ import { IconButton } from "@/components/xui/icon-button"
import { useState } from "react" import { useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
import { Textarea } from "./ui/textarea" import { Textarea } from "./ui/textarea"
interface BatchMoveServerIconProps extends ButtonProps { interface BatchMoveServerIconProps extends ButtonProps {
serverIds: number[] serverIds: number[]
} }
export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ serverIds, ...props }) => { export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({
serverIds,
...props
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [toUserId, setToUserId] = useState<number | undefined>(undefined) const [toUserId, setToUserId] = useState<number | undefined>(undefined)
@@ -32,7 +36,7 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ server
try { try {
await batchMoveServer({ await batchMoveServer({
ids: serverIds, ids: serverIds,
to_user: toUserId! to_user: toUserId!,
}) })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -69,9 +73,7 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ server
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-3 mt-4"> <div className="flex flex-col gap-3 mt-4">
<Label>{t("Servers")}</Label> <Label>{t("Servers")}</Label>
<Textarea disabled> <Textarea disabled>{serverIds.join(", ")}</Textarea>
{serverIds.join(", ")}
</Textarea>
<Label>{t("ToUser")}</Label> <Label>{t("ToUser")}</Label>
<Input <Input
type="number" type="number"
@@ -87,7 +89,12 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ server
{t("Cancel")} {t("Cancel")}
</Button> </Button>
</DialogClose> </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")} {t("Move")}
</Button> </Button>
</DialogFooter> </DialogFooter>
+18 -5
View File
@@ -63,18 +63,31 @@ const cronFormSchema = z.object({
notification_group_id: z.coerce.number().int(), notification_group_id: z.coerce.number().int(),
}) })
type CronFormData = z.infer<typeof cronFormSchema>
export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => { export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof cronFormSchema>>({ const form = useForm<CronFormData>({
resolver: zodResolver(cronFormSchema), resolver: zodResolver(cronFormSchema as any),
defaultValues: data 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, task_type: 0,
name: "",
scheduler: "", scheduler: "",
command: "",
servers: [], servers: [],
cover: 0, cover: 0,
push_successful: false,
notification_group_id: 0, notification_group_id: 0,
}, },
resetOptions: { resetOptions: {
@@ -84,7 +97,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof cronFormSchema>) => { const onSubmit = async (values: CronFormData) => {
try { try {
data?.id ? await updateCron(data.id, values) : await createCron(values) data?.id ? await updateCron(data.id, values) : await createCron(values)
} catch (e) { } catch (e) {
+28 -5
View File
@@ -67,21 +67,44 @@ const ddnsFormSchema = z.object({
webhook_headers: asOptionalField(z.string()), webhook_headers: asOptionalField(z.string()),
}) })
type DDNSFormData = z.infer<typeof ddnsFormSchema>
export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) => { export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof ddnsFormSchema>>({ const form = useForm<DDNSFormData>({
resolver: zodResolver(ddnsFormSchema), resolver: zodResolver(ddnsFormSchema as any),
defaultValues: data defaultValues: data
? { ? {
...data, max_retries: data.max_retries ?? 3,
domains_raw: conv.arrToStr(data.domains), 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, max_retries: 3,
enable_ipv4: false,
enable_ipv6: false,
name: "", name: "",
provider: "dummy", provider: "dummy",
domains: [], domains: [],
domains_raw: "", domains_raw: "",
access_id: "",
access_secret: "",
webhook_url: "",
webhook_method: undefined,
webhook_request_type: undefined,
webhook_request_body: "",
webhook_headers: "",
}, },
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
@@ -90,7 +113,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof ddnsFormSchema>) => { const onSubmit = async (values: DDNSFormData) => {
try { try {
values.domains = conv.strToArr(values.domains_raw) values.domains = conv.strToArr(values.domains_raw)
data?.id ? await updateDDNSProfile(data.id, values) : await createDDNSProfile(values) 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 { ColumnDef } from "@tanstack/react-table"
import { Row, flexRender } from "@tanstack/react-table" import { Row, flexRender } from "@tanstack/react-table"
import { File, Folder } from "lucide-react" 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 { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
+12 -4
View File
@@ -51,6 +51,7 @@ export function HeaderButtonGroup<E, U>({
<IconButton <IconButton
variant="destructive" variant="destructive"
icon="trash" icon="trash"
className="text-white"
onClick={() => { onClick={() => {
toast(t("Error"), { toast(t("Error"), {
description: t("Results.NoRowsAreSelected"), description: t("Results.NoRowsAreSelected"),
@@ -63,7 +64,7 @@ export function HeaderButtonGroup<E, U>({
<> <>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<IconButton variant="destructive" icon="trash" /> <IconButton variant="destructive" icon="trash" className="text-white" />
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg"> <AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader> <AlertDialogHeader>
@@ -75,7 +76,10 @@ export function HeaderButtonGroup<E, U>({
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel> <AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={buttonVariants({ variant: "destructive" })} className={buttonVariants({
variant: "destructive",
className: "text-white",
})}
onClick={handleDelete} onClick={handleDelete}
> >
{t("Confirm")} {t("Confirm")}
@@ -114,6 +118,7 @@ export function HeaderBlockButtonGroup<E, U>({
<IconButton <IconButton
variant="destructive" variant="destructive"
icon="ban" icon="ban"
className="text-white"
onClick={() => { onClick={() => {
toast(t("Error"), { toast(t("Error"), {
description: t("Results.NoRowsAreSelected"), description: t("Results.NoRowsAreSelected"),
@@ -126,7 +131,7 @@ export function HeaderBlockButtonGroup<E, U>({
<> <>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<IconButton variant="destructive" icon="ban" /> <IconButton variant="destructive" icon="ban" className="text-white" />
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg"> <AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader> <AlertDialogHeader>
@@ -138,7 +143,10 @@ export function HeaderBlockButtonGroup<E, U>({
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel> <AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={buttonVariants({ variant: "destructive" })} className={buttonVariants({
variant: "destructive",
className: "text-white",
})}
onClick={handleBlock} onClick={handleBlock}
> >
{t("Confirm")} {t("Confirm")}
+1 -1
View File
@@ -423,7 +423,7 @@ function Overview() {
{!profile && <p className="text-sm font-semibold">{t("LoginFirst")}</p>} {!profile && <p className="text-sm font-semibold">{t("LoginFirst")}</p>}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<p className="text-[13px] font-medium opacity-50">{t("CurrentTime")}</p> <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> </div>
</section> </section>
) )
+116 -61
View File
@@ -10,7 +10,7 @@ import useSettings from "@/hooks/useSetting"
import { copyToClipboard } from "@/lib/utils" import { copyToClipboard } from "@/lib/utils"
import { ModelProfile, ModelSetting } from "@/types" import { ModelProfile, ModelSetting } from "@/types"
import i18next from "i18next" import i18next from "i18next"
import { Check, Clipboard } from "lucide-react" import { Check, Copy, Download } from "lucide-react"
import { forwardRef, useState } from "react" import { forwardRef, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -21,82 +21,137 @@ enum OSTypes {
Windows, Windows,
} }
export const InstallCommandsMenu = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => { type InstallCommandsMenuProps = ButtonProps & {
const [copy, setCopy] = useState(false) uuid?: string
const { data: settings } = useSettings() iconOnly?: boolean
const { profile } = useAuth() 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) => { const { t } = useTranslation()
if (!copy) {
try { const switchState = async (type: number) => {
setCopy(true) if (!copy) {
if (!profile) throw new Error("Profile is not found.") try {
if (!settings?.config) throw new Error("Settings is not found.") setCopy(true)
await copyToClipboard(generateCommand(type, settings!.config, profile) || "") if (!profile) throw new Error("Profile is not found.")
} catch (e: Error | any) { if (!settings?.config) throw new Error("Settings is not found.")
console.error(e) await copyToClipboard(
toast(t("Error"), { generateCommand(type, settings!.config, profile, uuid) || "",
description: e.message, )
}) } catch (e: Error | any) {
} finally { console.error(e)
setTimeout(() => { toast(t("Error"), {
setCopy(false) description: e.message,
}, 2 * 1000) })
} finally {
setTimeout(() => {
setCopy(false)
}, 2 * 1000)
}
} }
} }
}
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button {...props} ref={ref}> {menuItem ? (
{copy ? <Check /> : <Clipboard />} <button
{t("InstallCommands")} type="button"
</Button> className="flex w-full items-center text-sm px-2 py-2 hover:bg-accent hover:text-accent-foreground"
</DropdownMenuTrigger> title={i18next.t("InstallCommands")}
<DropdownMenuContent> >
<DropdownMenuItem {copy ? (
className="nezha-copy" <Check className="h-4 w-4 mr-2" />
onClick={async () => { ) : (
switchState(OSTypes.Linux) <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"
<DropdownMenuItem onClick={async () => {
className="nezha-copy" switchState(OSTypes.Linux)
onClick={async () => { }}
switchState(OSTypes.macOS) >
}} Linux
> </DropdownMenuItem>
macOS <DropdownMenuItem
</DropdownMenuItem> className="nezha-copy"
<DropdownMenuItem onClick={async () => {
className="nezha-copy" switchState(OSTypes.macOS)
onClick={async () => { }}
switchState(OSTypes.Windows) >
}} macOS
> </DropdownMenuItem>
Windows <DropdownMenuItem
</DropdownMenuItem> className="nezha-copy"
</DropdownMenuContent> onClick={async () => {
</DropdownMenu> switchState(OSTypes.Windows)
) }}
}) >
Windows
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
)
const generateCommand = ( const generateCommand = (
type: number, type: number,
{ install_host, tls }: ModelSetting, { install_host, tls }: ModelSetting,
{ agent_secret }: ModelProfile, { agent_secret }: ModelProfile,
uuid?: string,
) => { ) => {
if (!install_host) throw new Error(i18next.t("Results.InstallHostRequired")) if (!install_host) throw new Error(i18next.t("Results.InstallHostRequired"))
if (!agent_secret) throw new Error(i18next.t("Results.AgentSecretRequired")) 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 envParts = [
const env_win = `$env:NZ_SERVER=\"${install_host}\";$env:NZ_TLS=\"${tls || false}\";$env:NZ_CLIENT_SECRET=\"${agent_secret}\";` `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) { switch (type) {
case OSTypes.Linux: case OSTypes.Linux:
+12 -4
View File
@@ -46,12 +46,20 @@ const natFormSchema = z.object({
domain: z.string(), domain: z.string(),
}) })
type NatFormData = z.infer<typeof natFormSchema>
export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => { export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof natFormSchema>>({ const form = useForm<NatFormData>({
resolver: zodResolver(natFormSchema), resolver: zodResolver(natFormSchema as any),
defaultValues: data 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: "", name: "",
enabled: false, enabled: false,
@@ -66,7 +74,7 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof natFormSchema>) => { const onSubmit = async (values: NatFormData) => {
try { try {
data?.id ? await updateNAT(data.id, values) : await createNAT(values) data?.id ? await updateNAT(data.id, values) : await createNAT(values)
} catch (e) { } catch (e) {
+44 -5
View File
@@ -57,14 +57,27 @@ const notificationFormSchema = z.object({
request_body: z.string(), request_body: z.string(),
verify_tls: asOptionalField(z.boolean()), verify_tls: asOptionalField(z.boolean()),
skip_check: asOptionalField(z.boolean()), skip_check: asOptionalField(z.boolean()),
format_metric_units: asOptionalField(z.boolean()),
}) })
export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => { export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof notificationFormSchema>>({ type notificationFormData = z.infer<typeof notificationFormSchema>
resolver: zodResolver(notificationFormSchema),
const form = useForm({
resolver: zodResolver(notificationFormSchema) as any,
defaultValues: data 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 as any).verify_tls ?? false,
skip_check: (data as any).skip_check ?? false,
format_metric_units: (data as any).format_metric_units ?? false,
}
: { : {
name: "", name: "",
url: "", url: "",
@@ -72,6 +85,9 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
request_type: 1, request_type: 1,
request_header: "", request_header: "",
request_body: "", request_body: "",
verify_tls: false,
skip_check: false,
format_metric_units: false,
}, },
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
@@ -80,7 +96,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof notificationFormSchema>) => { const onSubmit = async (values: notificationFormData) => {
try { try {
data?.id ? await updateNotification(data.id, values) : await createNotification(values) data?.id ? await updateNotification(data.id, values) : await createNotification(values)
} catch (e) { } catch (e) {
@@ -110,7 +126,10 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <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 <FormField
control={form.control} control={form.control}
name="name" name="name"
@@ -267,6 +286,26 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="format_metric_units"
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">
{t("FormatMetricUnits")}
</Label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter className="justify-end"> <DialogFooter className="justify-end">
<DialogClose asChild> <DialogClose asChild>
<Button type="button" className="my-2" variant="secondary"> <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 { t } = useTranslation()
const { profile, setProfile } = useMainStore() const { profile, setProfile } = useMainStore()
const form = useForm<z.infer<typeof profileFormSchema>>({ const form = useForm({
resolver: zodResolver(profileFormSchema), resolver: zodResolver(profileFormSchema) as any,
defaultValues: { defaultValues: {
original_password: "", original_password: "",
new_password: "", new_password: "",
@@ -57,7 +57,7 @@ export const ProfileCard = ({ className }: { className: string }) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof profileFormSchema>) => { const onSubmit = async (values: any) => {
try { try {
await updateProfile(values) await updateProfile(values)
} catch (e) { } catch (e) {
+20 -7
View File
@@ -27,6 +27,7 @@ import { IconButton } from "@/components/xui/icon-button"
import { asOptionalField } from "@/lib/utils" import { asOptionalField } from "@/lib/utils"
import { ModelServerTaskResponse } from "@/types" import { ModelServerTaskResponse } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { CogIcon } from "lucide-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
@@ -58,7 +59,7 @@ const agentConfigSchema = z.object({
), ),
), ),
ip_report_period: asOptionalField(z.coerce.number().int().min(30)), 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( nic_allowlist_raw: asOptionalField(
z.string().refine( z.string().refine(
(val) => { (val) => {
@@ -102,9 +103,10 @@ for (let i = 0; i < boolFields.length; i += 2) {
interface ServerConfigCardProps extends ButtonProps { interface ServerConfigCardProps extends ButtonProps {
sid: number sid: number
menuItem?: boolean
} }
export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => { export const ServerConfigCard = ({ sid, menuItem = false, ...props }: ServerConfigCardProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [data, setData] = useState<AgentConfig | undefined>(undefined) const [data, setData] = useState<AgentConfig | undefined>(undefined)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -129,8 +131,8 @@ export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
if (open) fetchData() if (open) fetchData()
}, [open]) }, [open])
const form = useForm<AgentConfig>({ const form = useForm({
resolver: zodResolver(agentConfigSchema), resolver: zodResolver(agentConfigSchema) as any,
defaultValues: { defaultValues: {
...data, ...data,
hard_drive_partition_allowlist_raw: JSON.stringify( hard_drive_partition_allowlist_raw: JSON.stringify(
@@ -155,7 +157,7 @@ export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
} }
}, [data, form]) }, [data, form])
const onSubmit = async (values: AgentConfig) => { const onSubmit = async (values: any) => {
let resp: ModelServerTaskResponse = {} let resp: ModelServerTaskResponse = {}
try { try {
values.nic_allowlist = values.nic_allowlist_raw values.nic_allowlist = values.nic_allowlist_raw
@@ -186,7 +188,18 @@ export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <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> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="sm:max-w-xl">
{loading ? ( {loading ? (
@@ -283,7 +296,7 @@ export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <Checkbox
checked={ checked={
controllerField.value as boolean !!controllerField.value
} }
onCheckedChange={ onCheckedChange={
controllerField.onChange controllerField.onChange
+41 -13
View File
@@ -28,7 +28,7 @@ import { conv } from "@/lib/utils"
import { asOptionalField } from "@/lib/utils" import { asOptionalField } from "@/lib/utils"
import { ModelServer } from "@/types" import { ModelServer } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react" import { useState, useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -70,7 +70,7 @@ const serverFormSchema = z.object({
export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => { export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof serverFormSchema>>({ const form = useForm<z.infer<typeof serverFormSchema>>({
resolver: zodResolver(serverFormSchema), resolver: zodResolver(serverFormSchema) as any,
defaultValues: { defaultValues: {
...data, ...data,
ddns_profiles_raw: data.ddns_profiles ? conv.arrToStr(data.ddns_profiles) : undefined, ddns_profiles_raw: data.ddns_profiles ? conv.arrToStr(data.ddns_profiles) : undefined,
@@ -85,7 +85,20 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false) 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 { try {
values.ddns_profiles = values.ddns_profiles_raw values.ddns_profiles = values.ddns_profiles_raw
? conv.strToArr(values.ddns_profiles_raw).map(Number) ? conv.strToArr(values.ddns_profiles_raw).map(Number)
@@ -118,10 +131,10 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
<DialogTitle>{t("EditServer")}</DialogTitle> <DialogTitle>{t("EditServer")}</DialogTitle>
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...(form as any)}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
<FormField <FormField
control={form.control} control={form.control as any}
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
@@ -134,7 +147,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control as any}
name="display_index" name="display_index"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
@@ -149,7 +162,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
{form.watch("enable_ddns") ? ( {form.watch("enable_ddns") ? (
<> <>
<FormField <FormField
control={form.control} control={form.control as any}
name="ddns_profiles_raw" name="ddns_profiles_raw"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
@@ -164,7 +177,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control as any}
name="override_ddns_domains_raw" name="override_ddns_domains_raw"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
@@ -184,7 +197,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
)} )}
<FormField <FormField
control={form.control} control={form.control as any}
name="enable_ddns" name="enable_ddns"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2"> <FormItem className="flex items-center space-x-2">
@@ -204,7 +217,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control as any}
name="hide_for_guest" name="hide_for_guest"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2"> <FormItem className="flex items-center space-x-2">
@@ -224,7 +237,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control as any}
name="note" name="note"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
@@ -237,11 +250,26 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control as any}
name="public_note" name="public_note"
render={({ field }) => ( render={({ field }) => (
<FormItem> <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> <FormControl>
<Textarea className="resize-y" {...field} /> <Textarea className="resize-y" {...field} />
</FormControl> </FormControl>
+19 -4
View File
@@ -54,6 +54,7 @@ interface ServiceCardProps {
const serviceFormSchema = z.object({ const serviceFormSchema = z.object({
cover: z.coerce.number().int().min(0), cover: z.coerce.number().int().min(0),
display_index: z.coerce.number().int(),
duration: z.coerce.number().int().min(30), duration: z.coerce.number().int().min(30),
enable_show_in_service: asOptionalField(z.boolean()), enable_show_in_service: asOptionalField(z.boolean()),
enable_trigger_task: asOptionalField(z.boolean()), enable_trigger_task: asOptionalField(z.boolean()),
@@ -67,7 +68,7 @@ const serviceFormSchema = z.object({
notify: asOptionalField(z.boolean()), notify: asOptionalField(z.boolean()),
recover_trigger_tasks: z.array(z.number()), recover_trigger_tasks: z.array(z.number()),
recover_trigger_tasks_raw: z.string(), 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()), skip_servers_raw: z.array(z.string()),
target: z.string(), target: z.string(),
type: z.coerce.number().int().min(0), type: z.coerce.number().int().min(0),
@@ -75,8 +76,8 @@ const serviceFormSchema = z.object({
export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => { export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof serviceFormSchema>>({ const form = useForm({
resolver: zodResolver(serviceFormSchema), resolver: zodResolver(serviceFormSchema) as any,
defaultValues: data defaultValues: data
? { ? {
...data, ...data,
@@ -87,6 +88,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
: { : {
type: 1, type: 1,
cover: 0, cover: 0,
display_index: 0,
name: "", name: "",
target: "", target: "",
max_latency: 0.0, max_latency: 0.0,
@@ -107,7 +109,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false) 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.skip_servers = conv.arrToRecord(values.skip_servers_raw)
values.fail_trigger_tasks = conv.strToArr(values.fail_trigger_tasks_raw).map(Number) 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) 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> </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 <FormField
control={form.control} control={form.control}
name="target" 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} />
}
+22 -22
View File
@@ -1,7 +1,7 @@
import { buttonVariants } from "@/components/ui/button" import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 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 const AlertDialog = AlertDialogPrimitive.Root
@@ -9,9 +9,9 @@ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef< const AlertDialogOverlay = forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>, ComponentRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
className={cn( className={cn(
@@ -24,9 +24,9 @@ const AlertDialogOverlay = React.forwardRef<
)) ))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef< const AlertDialogContent = forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>, ComponentRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay /> <AlertDialogOverlay />
@@ -42,12 +42,12 @@ const AlertDialogContent = React.forwardRef<
)) ))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 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} /> <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
) )
AlertDialogHeader.displayName = "AlertDialogHeader" AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const AlertDialogFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} {...props}
@@ -55,9 +55,9 @@ const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDiv
) )
AlertDialogFooter.displayName = "AlertDialogFooter" AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef< const AlertDialogTitle = forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>, ComponentRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
ref={ref} ref={ref}
@@ -65,11 +65,11 @@ const AlertDialogTitle = React.forwardRef<
{...props} {...props}
/> />
)) ))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName AlertDialogTitle.displayName = "AlertDialogTitle"
const AlertDialogDescription = React.forwardRef< const AlertDialogDescription = forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>, ComponentRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
ref={ref} ref={ref}
@@ -79,17 +79,17 @@ const AlertDialogDescription = React.forwardRef<
)) ))
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef< const AlertDialogAction = forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>, ComponentRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} /> <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
)) ))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef< const AlertDialogCancel = forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>, ComponentRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel <AlertDialogPrimitive.Cancel
ref={ref} ref={ref}
+10 -10
View File
@@ -1,10 +1,10 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as AvatarPrimitive from "@radix-ui/react-avatar" import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Avatar = React.forwardRef< const Avatar = forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>, ComponentRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Root <AvatarPrimitive.Root
ref={ref} ref={ref}
@@ -14,9 +14,9 @@ const Avatar = React.forwardRef<
)) ))
Avatar.displayName = AvatarPrimitive.Root.displayName Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef< const AvatarImage = forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>, ComponentRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Image <AvatarPrimitive.Image
ref={ref} ref={ref}
@@ -26,9 +26,9 @@ const AvatarImage = React.forwardRef<
)) ))
AvatarImage.displayName = AvatarPrimitive.Image.displayName AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef< const AvatarFallback = forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>, ComponentRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
ref={ref} ref={ref}
+2 -2
View File
@@ -1,6 +1,6 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react" import { HTMLAttributes } from "react"
const badgeVariants = cva( 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", "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,7 +23,7 @@ const badgeVariants = cva(
) )
export interface BadgeProps export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {} VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
+11 -11
View File
@@ -1,17 +1,17 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react" 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, HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & { ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode separator?: ReactNode
} }
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />) >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb" Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>( const BreadcrumbList = forwardRef<HTMLOListElement, ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<ol <ol
ref={ref} ref={ref}
@@ -25,16 +25,16 @@ const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWi
) )
BreadcrumbList.displayName = "BreadcrumbList" BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>( const BreadcrumbItem = forwardRef<HTMLLIElement, ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} /> <li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
), ),
) )
BreadcrumbItem.displayName = "BreadcrumbItem" BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef< const BreadcrumbLink = forwardRef<
HTMLAnchorElement, HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & { ComponentPropsWithoutRef<"a"> & {
asChild?: boolean asChild?: boolean
} }
>(({ asChild, className, ...props }, ref) => { >(({ asChild, className, ...props }, ref) => {
@@ -50,7 +50,7 @@ const BreadcrumbLink = React.forwardRef<
}) })
BreadcrumbLink.displayName = "BreadcrumbLink" BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>( const BreadcrumbPage = forwardRef<HTMLSpanElement, ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<span <span
ref={ref} ref={ref}
@@ -64,7 +64,7 @@ const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWit
) )
BreadcrumbPage.displayName = "BreadcrumbPage" BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => ( const BreadcrumbSeparator = ({ children, className, ...props }: ComponentProps<"li">) => (
<li <li
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
@@ -76,7 +76,7 @@ const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentP
) )
BreadcrumbSeparator.displayName = "BreadcrumbSeparator" BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => ( const BreadcrumbEllipsis = ({ className, ...props }: ComponentProps<"span">) => (
<span <span
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
+3 -3
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react" import { ButtonHTMLAttributes, forwardRef } from "react"
const buttonVariants = cva( 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", "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,12 @@ const buttonVariants = cva(
) )
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean asChild?: boolean
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button"
return ( 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 { 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) => ( ({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
@@ -12,14 +12,14 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
) )
Card.displayName = "Card" Card.displayName = "Card"
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} /> <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
), ),
) )
CardHeader.displayName = "CardHeader" CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>( const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<h3 <h3
ref={ref} ref={ref}
@@ -30,22 +30,21 @@ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HT
) )
CardTitle.displayName = "CardTitle" CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef< const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLParagraphElement> <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} /> )
))
CardDescription.displayName = "CardDescription" CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
), ),
) )
CardContent.displayName = "CardContent" CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} /> <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 { cn } from "@/lib/utils"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react" import { Check } from "lucide-react"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Checkbox = React.forwardRef< const Checkbox = forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>, ComponentRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
+5 -5
View File
@@ -12,9 +12,9 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Check, ChevronDown } from "lucide-react" 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: { options: {
label: string label: string
value: string value: string
@@ -26,10 +26,10 @@ interface ComboboxProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
onValueChange: (value: string) => void onValueChange: (value: string) => void
} }
export const Combobox = React.forwardRef<HTMLButtonElement, ComboboxProps>( export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(
({ options, placeholder, defaultValue, className, onValueChange, ...props }, ref) => { ({ options, placeholder, defaultValue, className, onValueChange, ...props }, ref) => {
const [open, setOpen] = React.useState(false) const [open, setOpen] = useState(false)
const [value, setValue] = React.useState(defaultValue) const [value, setValue] = useState(defaultValue)
return ( return (
<Popover open={open} onOpenChange={setOpen}> <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 { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk" import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react" import { Search } from "lucide-react"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, HTMLAttributes, forwardRef } from "react"
const Command = React.forwardRef< const Command = forwardRef<
React.ElementRef<typeof CommandPrimitive>, ComponentRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive> ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive <CommandPrimitive
ref={ref} ref={ref}
@@ -32,9 +32,9 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
) )
} }
const CommandInput = React.forwardRef< const CommandInput = forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>, ComponentRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper=""> <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> <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 CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef< const CommandList = forwardRef<
React.ElementRef<typeof CommandPrimitive.List>, ComponentRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.List <CommandPrimitive.List
ref={ref} ref={ref}
@@ -64,18 +64,18 @@ const CommandList = React.forwardRef<
CommandList.displayName = CommandPrimitive.List.displayName CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef< const CommandEmpty = forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>, ComponentRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => ( >((props, ref) => (
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} /> <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
)) ))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef< const CommandGroup = forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>, ComponentRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Group <CommandPrimitive.Group
ref={ref} ref={ref}
@@ -89,9 +89,9 @@ const CommandGroup = React.forwardRef<
CommandGroup.displayName = CommandPrimitive.Group.displayName CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef< const CommandSeparator = forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>, ComponentRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Separator <CommandPrimitive.Separator
ref={ref} ref={ref}
@@ -101,9 +101,9 @@ const CommandSeparator = React.forwardRef<
)) ))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef< const CommandItem = forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>, ComponentRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
@@ -117,7 +117,7 @@ const CommandItem = React.forwardRef<
CommandItem.displayName = CommandPrimitive.Item.displayName CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { const CommandShortcut = ({ className, ...props }: HTMLAttributes<HTMLSpanElement>) => {
return ( return (
<span <span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} 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 { cn } from "@/lib/utils"
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react" import { X } from "lucide-react"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, HTMLAttributes, forwardRef } from "react"
const Dialog = DialogPrimitive.Root const Dialog = DialogPrimitive.Root
@@ -11,9 +11,9 @@ const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef< const DialogOverlay = forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, ComponentRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
@@ -26,9 +26,9 @@ const DialogOverlay = React.forwardRef<
)) ))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef< const DialogContent = forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, ComponentRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DialogPortal> <DialogPortal>
<DialogOverlay /> <DialogOverlay />
@@ -50,7 +50,7 @@ const DialogContent = React.forwardRef<
)) ))
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...props} {...props}
@@ -58,7 +58,7 @@ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
) )
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} {...props}
@@ -66,9 +66,9 @@ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
) )
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef< const DialogTitle = forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, ComponentRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref} ref={ref}
@@ -76,11 +76,11 @@ const DialogTitle = React.forwardRef<
{...props} {...props}
/> />
)) ))
DialogTitle.displayName = DialogPrimitive.Title.displayName DialogTitle.displayName = "DialogTitle"
const DialogDescription = React.forwardRef< const DialogDescription = forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, ComponentRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Description <DialogPrimitive.Description
ref={ref} ref={ref}
@@ -88,7 +88,7 @@ const DialogDescription = React.forwardRef<
{...props} {...props}
/> />
)) ))
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = "DialogDescription"
export { export {
Dialog, Dialog,
+24 -18
View File
@@ -1,11 +1,17 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as React from "react" import {
ComponentProps,
ComponentPropsWithoutRef,
ComponentRef,
HTMLAttributes,
forwardRef,
} from "react"
import { Drawer as DrawerPrimitive } from "vaul" import { Drawer as DrawerPrimitive } from "vaul"
const Drawer = ({ const Drawer = ({
shouldScaleBackground = true, shouldScaleBackground = true,
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => ( }: ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} /> <DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
) )
Drawer.displayName = "Drawer" Drawer.displayName = "Drawer"
@@ -16,9 +22,9 @@ const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef< const DrawerOverlay = forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>, ComponentRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay <DrawerPrimitive.Overlay
ref={ref} ref={ref}
@@ -28,9 +34,9 @@ const DrawerOverlay = React.forwardRef<
)) ))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef< const DrawerContent = forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>, ComponentRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DrawerPortal> <DrawerPortal>
<DrawerOverlay /> <DrawerOverlay />
@@ -49,19 +55,19 @@ const DrawerContent = React.forwardRef<
)) ))
DrawerContent.displayName = "DrawerContent" 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} /> <div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
) )
DrawerHeader.displayName = "DrawerHeader" 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} /> <div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
) )
DrawerFooter.displayName = "DrawerFooter" DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef< const DrawerTitle = forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>, ComponentRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Title <DrawerPrimitive.Title
ref={ref} ref={ref}
@@ -69,11 +75,11 @@ const DrawerTitle = React.forwardRef<
{...props} {...props}
/> />
)) ))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName DrawerTitle.displayName = "DrawerTitle"
const DrawerDescription = React.forwardRef< const DrawerDescription = forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>, ComponentRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Description <DrawerPrimitive.Description
ref={ref} ref={ref}
@@ -81,7 +87,7 @@ const DrawerDescription = React.forwardRef<
{...props} {...props}
/> />
)) ))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName DrawerDescription.displayName = "DrawerDescription"
export { export {
Drawer, Drawer,
+26 -26
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react" import { Check, ChevronRight, Circle } from "lucide-react"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, HTMLAttributes, forwardRef } from "react"
const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenu = DropdownMenuPrimitive.Root
@@ -15,9 +15,9 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, ComponentRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
@@ -36,9 +36,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
)) ))
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, ComponentRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
@@ -51,9 +51,9 @@ const DropdownMenuSubContent = React.forwardRef<
)) ))
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, ComponentRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
@@ -69,9 +69,9 @@ const DropdownMenuContent = React.forwardRef<
)) ))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, ComponentRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
@@ -87,9 +87,9 @@ const DropdownMenuItem = React.forwardRef<
)) ))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef< const DropdownMenuCheckboxItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, ComponentRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => ( >(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
@@ -110,9 +110,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
)) ))
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
@@ -132,9 +132,9 @@ const DropdownMenuRadioItem = React.forwardRef<
)) ))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef< const DropdownMenuLabel = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, ComponentRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
@@ -146,9 +146,9 @@ const DropdownMenuLabel = React.forwardRef<
)) ))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef< const DropdownMenuSeparator = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, ComponentRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
ref={ref} ref={ref}
@@ -158,7 +158,7 @@ const DropdownMenuSeparator = React.forwardRef<
)) ))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { const DropdownMenuShortcut = ({ className, ...props }: HTMLAttributes<HTMLSpanElement>) => {
return ( return (
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} /> <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 { cn } from "@/lib/utils"
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import * as React from "react" import {
ComponentPropsWithoutRef,
ComponentRef,
HTMLAttributes,
createContext,
forwardRef,
useContext,
useId,
} from "react"
import { import {
Controller, Controller,
ControllerProps, ControllerProps,
@@ -21,7 +29,7 @@ type FormFieldContextValue<
name: TName name: TName
} }
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue) const FormFieldContext = createContext<FormFieldContextValue>({} as FormFieldContextValue)
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
@@ -37,8 +45,8 @@ const FormField = <
} }
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext) const itemContext = useContext(FormItemContext)
const { getFieldState, formState } = useFormContext() const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState)
@@ -63,11 +71,11 @@ type FormItemContextValue = {
id: string 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) => { ({ className, ...props }, ref) => {
const id = React.useId() const id = useId()
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
@@ -78,9 +86,9 @@ const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivEl
) )
FormItem.displayName = "FormItem" FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef< const FormLabel = forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, ComponentRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField()
@@ -95,65 +103,62 @@ const FormLabel = React.forwardRef<
}) })
FormLabel.displayName = "FormLabel" FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef< const FormControl = forwardRef<ComponentRef<typeof Slot>, ComponentPropsWithoutRef<typeof Slot>>(
React.ElementRef<typeof Slot>, ({ ...props }, ref) => {
React.ComponentPropsWithoutRef<typeof Slot> const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return ( return (
<Slot <Slot
ref={ref} ref={ref}
id={formItemId} id={formItemId}
aria-describedby={ aria-describedby={
!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}` !error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
} }
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
) )
}) },
)
FormControl.displayName = "FormControl" FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef< const FormDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => {
React.HTMLAttributes<HTMLParagraphElement> const { formDescriptionId } = useFormField()
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return ( return (
<p <p
ref={ref} ref={ref}
id={formDescriptionId} id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
) )
}) },
)
FormDescription.displayName = "FormDescription" FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef< const FormMessage = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
HTMLParagraphElement, ({ className, children, ...props }, ref) => {
React.HTMLAttributes<HTMLParagraphElement> const { error, formMessageId } = useFormField()
>(({ className, children, ...props }, ref) => { const body = error ? String(error?.message) : children
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) { if (!body) {
return null return null
} }
return ( return (
<p <p
ref={ref} ref={ref}
id={formMessageId} id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)} className={cn("text-sm font-medium text-destructive", className)}
{...props} {...props}
> >
{body} {body}
</p> </p>
) )
}) },
)
FormMessage.displayName = "FormMessage" FormMessage.displayName = "FormMessage"
export { export {
+15 -17
View File
@@ -1,23 +1,21 @@
import { cn } from "@/lib/utils" 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>( const Input = forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
({ className, type, ...props }, ref) => { return (
return ( <input
<input type={type}
type={type} className={cn(
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",
"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,
className, )}
)} ref={ref}
ref={ref} {...props}
{...props} />
/> )
) })
},
)
Input.displayName = "Input" Input.displayName = "Input"
export { Input } export { Input }
+4 -4
View File
@@ -1,15 +1,15 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label"
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const labelVariants = cva( const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
) )
const Label = React.forwardRef< const Label = forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, ComponentRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants> ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} /> <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 * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority" import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react" import { ChevronDown } from "lucide-react"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const NavigationMenu = React.forwardRef< const NavigationMenu = forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>, ComponentRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root <NavigationMenuPrimitive.Root
ref={ref} ref={ref}
@@ -19,9 +19,9 @@ const NavigationMenu = React.forwardRef<
)) ))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef< const NavigationMenuList = forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>, ComponentRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List> ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List <NavigationMenuPrimitive.List
ref={ref} 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", "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< const NavigationMenuTrigger = forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>, ComponentRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger> ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger <NavigationMenuPrimitive.Trigger
ref={ref} ref={ref}
@@ -58,9 +58,9 @@ const NavigationMenuTrigger = React.forwardRef<
)) ))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef< const NavigationMenuContent = forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>, ComponentRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content> ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content <NavigationMenuPrimitive.Content
ref={ref} ref={ref}
@@ -75,9 +75,9 @@ NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef< const NavigationMenuViewport = forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>, ComponentRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport> ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}> <div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport <NavigationMenuPrimitive.Viewport
@@ -92,9 +92,9 @@ const NavigationMenuViewport = React.forwardRef<
)) ))
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef< const NavigationMenuIndicator = forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>, ComponentRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator> ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator <NavigationMenuPrimitive.Indicator
ref={ref} ref={ref}
+8 -11
View File
@@ -1,9 +1,9 @@
import { ButtonProps, buttonVariants } from "@/components/ui/button" import { ButtonProps, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" 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 <nav
role="navigation" role="navigation"
aria-label="pagination" aria-label="pagination"
@@ -13,14 +13,14 @@ const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
) )
Pagination.displayName = "Pagination" Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>( const PaginationContent = forwardRef<HTMLUListElement, ComponentProps<"ul">>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} /> <ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
), ),
) )
PaginationContent.displayName = "PaginationContent" 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} />, ({ className, ...props }, ref) => <li ref={ref} className={cn("", className)} {...props} />,
) )
PaginationItem.displayName = "PaginationItem" PaginationItem.displayName = "PaginationItem"
@@ -28,7 +28,7 @@ PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = { type PaginationLinkProps = {
isActive?: boolean isActive?: boolean
} & Pick<ButtonProps, "size"> & } & Pick<ButtonProps, "size"> &
React.ComponentProps<"a"> ComponentProps<"a">
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => ( const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
<a <a
@@ -45,10 +45,7 @@ const PaginationLink = ({ className, isActive, size = "icon", ...props }: Pagina
) )
PaginationLink.displayName = "PaginationLink" PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({ const PaginationPrevious = ({ className, ...props }: ComponentProps<typeof PaginationLink>) => (
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink <PaginationLink
aria-label="Go to previous page" aria-label="Go to previous page"
size="default" size="default"
@@ -61,7 +58,7 @@ const PaginationPrevious = ({
) )
PaginationPrevious.displayName = "PaginationPrevious" PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => ( const PaginationNext = ({ className, ...props }: ComponentProps<typeof PaginationLink>) => (
<PaginationLink <PaginationLink
aria-label="Go to next page" aria-label="Go to next page"
size="default" size="default"
@@ -74,7 +71,7 @@ const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof Pag
) )
PaginationNext.displayName = "PaginationNext" PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => ( const PaginationEllipsis = ({ className, ...props }: ComponentProps<"span">) => (
<span <span
aria-hidden aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)} 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 { cn } from "@/lib/utils"
import * as PopoverPrimitive from "@radix-ui/react-popover" import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Popover = PopoverPrimitive.Root const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef< const PopoverContent = forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, ComponentRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Content <PopoverPrimitive.Content
+7 -7
View File
@@ -1,10 +1,10 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const ScrollArea = React.forwardRef< const ScrollArea = forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>, ComponentRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
ref={ref} ref={ref}
@@ -20,9 +20,9 @@ const ScrollArea = React.forwardRef<
)) ))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef< const ScrollBar = forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, ComponentRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => ( >(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar <ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref} ref={ref}
+22 -22
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as SelectPrimitive from "@radix-ui/react-select" import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react" import { Check, ChevronDown, ChevronUp } from "lucide-react"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Select = SelectPrimitive.Root const Select = SelectPrimitive.Root
@@ -9,9 +9,9 @@ const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef< const SelectTrigger = forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, ComponentRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
@@ -29,9 +29,9 @@ const SelectTrigger = React.forwardRef<
)) ))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef< const SelectScrollUpButton = forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
ref={ref} ref={ref}
@@ -43,9 +43,9 @@ const SelectScrollUpButton = React.forwardRef<
)) ))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef< const SelectScrollDownButton = forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
ref={ref} ref={ref}
@@ -57,9 +57,9 @@ const SelectScrollDownButton = React.forwardRef<
)) ))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef< const SelectContent = forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, ComponentRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => ( >(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
@@ -89,9 +89,9 @@ const SelectContent = React.forwardRef<
)) ))
SelectContent.displayName = SelectPrimitive.Content.displayName SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef< const SelectLabel = forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>, ComponentRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Label <SelectPrimitive.Label
ref={ref} ref={ref}
@@ -101,9 +101,9 @@ const SelectLabel = React.forwardRef<
)) ))
SelectLabel.displayName = SelectPrimitive.Label.displayName SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef< const SelectItem = forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>, ComponentRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
@@ -124,9 +124,9 @@ const SelectItem = React.forwardRef<
)) ))
SelectItem.displayName = SelectPrimitive.Item.displayName SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef< const SelectSeparator = forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, ComponentRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Separator <SelectPrimitive.Separator
ref={ref} ref={ref}
+4 -4
View File
@@ -1,10 +1,10 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Separator = React.forwardRef< const Separator = forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, ComponentRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
ref={ref} 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 { cn } from "@/lib/utils"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Switch = React.forwardRef< const Switch = forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>, ComponentRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SwitchPrimitives.Root <SwitchPrimitives.Root
className={cn( 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", "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 className,
)} )}
{...props} {...props}
ref={ref} ref={ref}
> >
<SwitchPrimitives.Thumb <SwitchPrimitives.Thumb
className={cn( 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" "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>
)) ))
Switch.displayName = SwitchPrimitives.Root.displayName Switch.displayName = SwitchPrimitives.Root.displayName
+52 -54
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils" 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) => ( ({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto"> <div className="relative w-full overflow-auto">
<table <table
@@ -14,35 +14,32 @@ const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableE
) )
Table.displayName = "Table" Table.displayName = "Table"
const TableHeader = React.forwardRef< const TableHeader = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(
HTMLTableSectionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableSectionElement> <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> )
))
TableHeader.displayName = "TableHeader" TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef< const TableBody = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(
HTMLTableSectionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableSectionElement> <tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} /> )
))
TableBody.displayName = "TableBody" TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef< const TableFooter = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(
HTMLTableSectionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableSectionElement> <tfoot
>(({ className, ...props }, ref) => ( ref={ref}
<tfoot className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
ref={ref} {...props}
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} />
{...props} ),
/> )
))
TableFooter.displayName = "TableFooter" TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>( const TableRow = forwardRef<HTMLTableRowElement, HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<tr <tr
ref={ref} ref={ref}
@@ -56,39 +53,40 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
) )
TableRow.displayName = "TableRow" TableRow.displayName = "TableRow"
const TableHead = React.forwardRef< const TableHead = forwardRef<HTMLTableCellElement, ThHTMLAttributes<HTMLTableCellElement>>(
HTMLTableCellElement, ({ className, ...props }, ref) => (
React.ThHTMLAttributes<HTMLTableCellElement> <th
>(({ className, ...props }, ref) => ( ref={ref}
<th className={cn(
ref={ref} "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className={cn( className,
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", )}
className, {...props}
)} />
{...props} ),
/> )
))
TableHead.displayName = "TableHead" TableHead.displayName = "TableHead"
const TableCell = React.forwardRef< const TableCell = forwardRef<HTMLTableCellElement, TdHTMLAttributes<HTMLTableCellElement>>(
HTMLTableCellElement, ({ className, ...props }, ref) => (
React.TdHTMLAttributes<HTMLTableCellElement> <td
>(({ className, ...props }, ref) => ( ref={ref}
<td className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
ref={ref} {...props}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} />
{...props} ),
/> )
))
TableCell.displayName = "TableCell" TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef< const TableCaption = forwardRef<HTMLTableCaptionElement, HTMLAttributes<HTMLTableCaptionElement>>(
HTMLTableCaptionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableCaptionElement> <caption
>(({ className, ...props }, ref) => ( ref={ref}
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} /> className={cn("mt-4 text-sm text-muted-foreground", className)}
)) {...props}
/>
),
)
TableCaption.displayName = "TableCaption" TableCaption.displayName = "TableCaption"
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
+10 -10
View File
@@ -1,12 +1,12 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as TabsPrimitive from "@radix-ui/react-tabs" import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Tabs = TabsPrimitive.Root const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef< const TabsList = forwardRef<
React.ElementRef<typeof TabsPrimitive.List>, ComponentRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.List <TabsPrimitive.List
ref={ref} ref={ref}
@@ -19,9 +19,9 @@ const TabsList = React.forwardRef<
)) ))
TabsList.displayName = TabsPrimitive.List.displayName TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef< const TabsTrigger = forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>, ComponentRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
ref={ref} ref={ref}
@@ -34,9 +34,9 @@ const TabsTrigger = React.forwardRef<
)) ))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef< const TabsContent = forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>, ComponentRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.Content <TabsPrimitive.Content
ref={ref} ref={ref}
+2 -2
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils" 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) => { ({ className, ...props }, ref) => {
return ( return (
<textarea <textarea
+6 -6
View File
@@ -15,13 +15,13 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { formatPath } from "@/lib/utils" import { formatPath } from "@/lib/utils"
import * as React from "react" import { Dispatch, FC, Fragment, SetStateAction, useState } from "react"
const ITEMS_TO_DISPLAY = 3 const ITEMS_TO_DISPLAY = 3
interface FilepathProps { interface FilepathProps {
path: string path: string
setPath: React.Dispatch<React.SetStateAction<string>> setPath: Dispatch<SetStateAction<string>>
} }
function pathToItems(path: string) { function pathToItems(path: string) {
@@ -38,8 +38,8 @@ function pathToItems(path: string) {
return result return result
} }
export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => { export const Filepath: FC<FilepathProps> = ({ path, setPath }) => {
const [open, setOpen] = React.useState(false) const [open, setOpen] = useState(false)
const items = pathToItems(formatPath(path)) const items = pathToItems(formatPath(path))
return ( return (
@@ -87,7 +87,7 @@ export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => {
</> </>
) : null} ) : null}
{items.slice(-ITEMS_TO_DISPLAY).map((item, index, slicedItems) => ( {items.slice(-ITEMS_TO_DISPLAY).map((item, index, slicedItems) => (
<React.Fragment key={index}> <Fragment key={index}>
<BreadcrumbItem className="overflow-auto"> <BreadcrumbItem className="overflow-auto">
{item.href ? ( {item.href ? (
<> <>
@@ -107,7 +107,7 @@ export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => {
)} )}
</BreadcrumbItem> </BreadcrumbItem>
{index !== slicedItems.length - 1 ? <BreadcrumbSeparator /> : null} {index !== slicedItems.length - 1 ? <BreadcrumbSeparator /> : null}
</React.Fragment> </Fragment>
))} ))}
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
+5
View File
@@ -11,6 +11,7 @@ import {
FolderClosed, FolderClosed,
Menu, Menu,
Minus, Minus,
MoreHorizontal,
Play, Play,
Plus, Plus,
Terminal, Terminal,
@@ -39,6 +40,7 @@ export interface IconButtonProps extends ButtonProps {
| "cog" | "cog"
| "minus" | "minus"
| "user-pen" | "user-pen"
| "more"
} }
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => { export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
@@ -102,6 +104,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
case "user-pen": { case "user-pen": {
return <UserPen /> return <UserPen />
} }
case "more": {
return <MoreHorizontal />
}
} }
})()} })()}
</Button> </Button>
+6 -6
View File
@@ -38,7 +38,7 @@ import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
import { CheckIcon, ChevronDown, WandSparkles, XIcon } from "lucide-react" 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. * Variants for the multi-select component to handle different styles.
@@ -129,7 +129,7 @@ interface MultiSelectProps
className?: string className?: string
} }
export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>( export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
( (
{ {
options, options,
@@ -146,11 +146,11 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
}, },
ref, ref,
) => { ) => {
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue) const [selectedValues, setSelectedValues] = useState<string[]>(defaultValue)
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false) const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const [isAnimating, setIsAnimating] = React.useState(false) const [isAnimating, setIsAnimating] = useState(false)
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { const handleInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") { if (event.key === "Enter") {
setIsPopoverOpen(true) setIsPopoverOpen(true)
} else if (event.key === "Backspace" && !event.currentTarget.value) { } else if (event.key === "Backspace" && !event.currentTarget.value) {
+42 -36
View File
@@ -2,7 +2,14 @@ import { cn } from "@/lib/utils"
import * as SheetPrimitive from "@radix-ui/react-dialog" import * as SheetPrimitive from "@radix-ui/react-dialog"
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
import { X } from "lucide-react" import { X } from "lucide-react"
import * as React from "react" import {
ComponentPropsWithoutRef,
ComponentRef,
Dispatch,
HTMLAttributes,
SetStateAction,
forwardRef,
} from "react"
const Sheet = SheetPrimitive.Root const Sheet = SheetPrimitive.Root
@@ -30,42 +37,41 @@ const sheetVariants = cva(
) )
interface SheetContentProps interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, extends ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> { VariantProps<typeof sheetVariants> {
setOpen: React.Dispatch<React.SetStateAction<boolean>> setOpen: Dispatch<SetStateAction<boolean>>
} }
const SheetContent = React.forwardRef< const SheetContent = forwardRef<ComponentRef<typeof SheetPrimitive.Content>, SheetContentProps>(
React.ElementRef<typeof SheetPrimitive.Content>, ({ side = "right", className, children, setOpen, ...props }, ref) => (
SheetContentProps <SheetPortal>
>(({ side = "right", className, children, setOpen, ...props }, ref) => ( <SheetPrimitive.Content
<SheetPortal> ref={ref}
<SheetPrimitive.Content className={cn(sheetVariants({ side }), className)}
ref={ref} {...props}
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">
{children} <X
<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"> className="h-4 w-4"
<X onClick={() => {
className="h-4 w-4" setOpen(false)
onClick={() => { }}
setOpen(false) />
}} <span className="sr-only">Close</span>
/> </SheetPrimitive.Close>
<span className="sr-only">Close</span> </SheetPrimitive.Content>
</SheetPrimitive.Close> </SheetPortal>
</SheetPrimitive.Content> ),
</SheetPortal> )
))
SheetContent.displayName = SheetPrimitive.Content.displayName 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} /> <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
) )
SheetHeader.displayName = "SheetHeader" SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const SheetFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} {...props}
@@ -73,9 +79,9 @@ const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElemen
) )
SheetFooter.displayName = "SheetFooter" SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef< const SheetTitle = forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>, ComponentRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Title <SheetPrimitive.Title
ref={ref} ref={ref}
@@ -83,11 +89,11 @@ const SheetTitle = React.forwardRef<
{...props} {...props}
/> />
)) ))
SheetTitle.displayName = SheetPrimitive.Title.displayName SheetTitle.displayName = "SheetTitle"
const SheetDescription = React.forwardRef< const SheetDescription = forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>, ComponentRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Description <SheetPrimitive.Description
ref={ref} ref={ref}
@@ -95,7 +101,7 @@ const SheetDescription = React.forwardRef<
{...props} {...props}
/> />
)) ))
SheetDescription.displayName = SheetPrimitive.Description.displayName SheetDescription.displayName = "SheetDescription"
export { export {
Sheet, Sheet,
+1 -1
View File
@@ -14,7 +14,7 @@ import {
getSortedRowModel, getSortedRowModel,
useReactTable, useReactTable,
} from "@tanstack/react-table" } 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" import { TableVirtuoso } from "react-virtuoso"
// Original Table is wrapped with a <div> (see https://ui.shadcn.com/docs/components/table#radix-:r24:-content-manual), // 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 { getProfile, login as loginRequest } from "@/api/user"
import { AuthContextProps } from "@/types" import { AuthContextProps } from "@/types"
import { createContext, useContext, useEffect, useMemo } from "react" import { createContext, useContext, useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { toast } from "sonner" import { toast } from "sonner"
@@ -16,6 +17,7 @@ const AuthContext = createContext<AuthContextProps>({
export const AuthProvider = ({ children }: { children: React.ReactNode }) => { export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const profile = useMainStore((store) => store.profile) const profile = useMainStore((store) => store.profile)
const setProfile = useMainStore((store) => store.setProfile) const setProfile = useMainStore((store) => store.setProfile)
const { t } = useTranslation()
useEffect(() => { useEffect(() => {
;(async () => { ;(async () => {
@@ -25,7 +27,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setProfile(user) setProfile(user)
} catch (error: any) { } catch (error: any) {
setProfile(undefined) setProfile(undefined)
console.error("Error fetching profile", error)
} }
})() })()
}, []) }, [])
@@ -40,7 +41,12 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setProfile(user) setProfile(user)
navigate("/dashboard") navigate("/dashboard")
} catch (error: any) { } 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) { export function useMediaQuery(query: string) {
const [value, setValue] = React.useState(false) const [value, setValue] = useState(false)
React.useEffect(() => { useEffect(() => {
function onChange(event: MediaQueryListEvent) { function onChange(event: MediaQueryListEvent) {
setValue(event.matches) setValue(event.matches)
} }
-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 deTranslation from "../locales/de/translation.json"
import enTranslation from "../locales/en/translation.json" import enTranslation from "../locales/en/translation.json"
import esTranslation from "../locales/es/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 itTranslation from "../locales/it/translation.json"
import ruTranslation from "../locales/ru/translation.json" import ruTranslation from "../locales/ru/translation.json"
import taTranslation from "../locales/ta/translation.json" import taTranslation from "../locales/ta/translation.json"
import zhCNTranslation from "../locales/zh-CN/translation.json" import zhCNTranslation from "../locales/zh-CN/translation.json"
import zhTWTranslation from "../locales/zh-TW/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 = { const resources = {
"zh-CN": { "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)
}
+71 -1
View File
@@ -7,6 +7,8 @@
}, },
"Username": "Username", "Username": "Username",
"Password": "Password", "Password": "Password",
"InvalidUsernameOrPassword": "Invalid Username Or Password",
"NetworkError": "Network Error",
"LoginFirst": "Please Log in first", "LoginFirst": "Please Log in first",
"CurrentTime": "Current Time", "CurrentTime": "Current Time",
"Results": { "Results": {
@@ -45,6 +47,8 @@
"Enable": "Enable", "Enable": "Enable",
"HideForGuest": "Hidden from Visitors", "HideForGuest": "Hidden from Visitors",
"InstallCommands": "Installation command", "InstallCommands": "Installation command",
"Terminal": "Terminal",
"Config": "Config",
"Note": "Note", "Note": "Note",
"Success": "Success", "Success": "Success",
"Done": "Finish", "Done": "Finish",
@@ -72,6 +76,10 @@
"Trigger": "On Trigger", "Trigger": "On Trigger",
"TasksToTriggerOnAlert": "Tasks to be triggered on alert", "TasksToTriggerOnAlert": "Tasks to be triggered on alert",
"TasksToTriggerAfterRecovery": "Tasks to be triggered after recovery", "TasksToTriggerAfterRecovery": "Tasks to be triggered after recovery",
"Add": "Add",
"Delete": "Delete",
"AdvancedJSON": "Advanced JSON",
"Save": "Save",
"Confirm": "Confirm", "Confirm": "Confirm",
"ConfirmDeletion": "Confirm Deletion?", "ConfirmDeletion": "Confirm Deletion?",
"Services": "Services", "Services": "Services",
@@ -185,5 +193,67 @@
"EditServerConfig": "Edit Server Config", "EditServerConfig": "Edit Server Config",
"Option": "Option", "Option": "Option",
"Value": "Value", "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)", "OverrideDDNSDomains": "Sobrescribir dominios DDNS (por configuración)",
"EmptyText": "El texto está vacío", "EmptyText": "El texto está vacío",
"EmptyNote": "No tenías ninguna nota.", "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": { "theme": {
"light": "Terang", "light": "Terang",
"dark": "Gelap", "dark": "Gelap",
"system": "Mengikuti sistem" "system": "Ikuti Sistem"
}, },
"Username": "Nama Pengguna", "Username": "Nama Pengguna",
"Password": "Kata Sandi", "Password": "Kata Sandi",
"LoginFirst": "Mohon masuk terlebih dahulu", "LoginFirst": "Harap Masuk terlebih dahulu",
"CurrentTime": "Waktu saat ini", "CurrentTime": "Waktu Saat Ini",
"Results": { "Results": {
"UsernameMin": "Nama Pengguna setidak nya harus {{number}} karakter.", "UsernameMin": "Nama pengguna harus minimal {{number}} karakter.",
"PasswordRequired": "Kata Sandi tidak boleh kosong.", "PasswordRequired": "Kata sandi tidak boleh kosong.",
"ErrorFetchingResource": "Kesalahan mengambil sumber daya: {{error}}", "ErrorFetchingResource": "Galat Mengambil Sumber Daya : {{error}}",
"SelectAtLeastOneServer": "Silahkan pilih setidaknya satu server.", "SelectAtLeastOneServer": "Harap pilih minimal satu server.",
"UnExpectedError": "Kesalahan tidak terduga, Silahhkan lihat konsol untuk detailnya.", "UnExpectedError": "Galat tak terduga, Harap lihat konsol untuk detail.",
"ForceUpdate": "Peningkatan Paksa:", "ForceUpdate": "Peningkatan paksa:",
"NoRowsAreSelected": "Tidak ada baris yang dipilih", "NoRowsAreSelected": "Tidak ada baris yang dipilih",
"ThisOperationIsUnrecoverable": "Operasi tidak dapat dibatalkan!", "ThisOperationIsUnrecoverable": "Operasi ini tidak dapat dibatalkan!",
"TaskTriggeredSuccessfully": "Tugas berhasil dipicu", "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": "Завантаження"
}
+71 -1
View File
@@ -7,6 +7,8 @@
}, },
"Username": "用户名", "Username": "用户名",
"Password": "密码", "Password": "密码",
"InvalidUsernameOrPassword": "用户名或密码错误",
"NetworkError": "网络错误",
"LoginFirst": "请先登录", "LoginFirst": "请先登录",
"CurrentTime": "当前时间", "CurrentTime": "当前时间",
"Results": { "Results": {
@@ -45,6 +47,8 @@
"Enable": "启用", "Enable": "启用",
"HideForGuest": "对游客隐藏", "HideForGuest": "对游客隐藏",
"InstallCommands": "安装命令", "InstallCommands": "安装命令",
"Terminal": "终端",
"Config": "配置",
"Note": "备注", "Note": "备注",
"Success": "成功", "Success": "成功",
"Done": "完成", "Done": "完成",
@@ -72,6 +76,10 @@
"Trigger": "触发", "Trigger": "触发",
"TasksToTriggerOnAlert": "告警时要触发的任务", "TasksToTriggerOnAlert": "告警时要触发的任务",
"TasksToTriggerAfterRecovery": "恢复后要触发的任务", "TasksToTriggerAfterRecovery": "恢复后要触发的任务",
"Add": "添加",
"Delete": "删除",
"AdvancedJSON": "高级 JSON",
"Save": "保存",
"Confirm": "确认", "Confirm": "确认",
"ConfirmDeletion": "确认删除?", "ConfirmDeletion": "确认删除?",
"Services": "服务", "Services": "服务",
@@ -185,5 +193,67 @@
"OverrideDDNSDomains": "强制指定 DDNS 域名(每个配置)", "OverrideDDNSDomains": "强制指定 DDNS 域名(每个配置)",
"Value": "值", "Value": "值",
"Preview": "预览", "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": "編輯伺服器配置", "EditServerConfig": "編輯伺服器配置",
"Option": "選項", "Option": "選項",
"Value": "值", "Value": "值",
"Preview": "預覽" "Preview": "預覽",
"FormatMetricUnits": "格式化資料單位"
} }
+2 -5
View File
@@ -1,7 +1,7 @@
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { RouterProvider, createBrowserRouter } from "react-router-dom" import { RouterProvider, createBrowserRouter } from "react-router-dom"
import { TerminalPage } from "./components/terminal"
import ErrorPage from "./error-page" import ErrorPage from "./error-page"
import { AuthProvider } from "./hooks/useAuth" import { AuthProvider } from "./hooks/useAuth"
import { NotificationProvider } from "./hooks/useNotfication" import { NotificationProvider } from "./hooks/useNotfication"
@@ -110,10 +110,7 @@ const router = createBrowserRouter([
</NotificationProvider> </NotificationProvider>
), ),
}, },
{
path: "/dashboard/terminal/:id",
element: <TerminalPage />,
},
{ {
path: "/dashboard/profile", path: "/dashboard/profile",
element: ( element: (
+1 -1
View File
@@ -144,7 +144,7 @@ export default function AlertRulePage() {
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" /> <NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup <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={{ delete={{
fn: deleteAlertRules, fn: deleteAlertRules,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
+4 -4
View File
@@ -208,11 +208,11 @@ export default function CronPage() {
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
return ( return (
<div className="px-3"> <div className="px-3 max-w-7xl mx-auto">
<div className="flex mt-6 mb-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between w-full gap-3 mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Task")}</h1> <h1 className="text-3xl font-bold tracking-tight">{t("Task")}</h1>
<HeaderButtonGroup <HeaderButtonGroup
className="flex-2 flex ml-auto gap-2" className="flex gap-2 flex-wrap shrink-0"
delete={{ delete={{
fn: deleteCron, fn: deleteCron,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
+1 -1
View File
@@ -149,7 +149,7 @@ export default function DDNSPage() {
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("DDNS")}</h1> <h1 className="flex-1 text-3xl font-bold tracking-tight">{t("DDNS")}</h1>
<HeaderButtonGroup <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={{ delete={{
fn: deleteDDNSProfiles, fn: deleteDDNSProfiles,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
+1 -1
View File
@@ -132,7 +132,7 @@ export default function NATPage() {
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight"> {t("NATT")}</h1> <h1 className="flex-1 text-3xl font-bold tracking-tight"> {t("NATT")}</h1>
<HeaderButtonGroup <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={{ delete={{
fn: deleteNAT, fn: deleteNAT,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
+1 -1
View File
@@ -125,7 +125,7 @@ export default function NotificationGroupPage() {
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" /> <GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup <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={{ delete={{
fn: deleteNotificationGroups, fn: deleteNotificationGroups,
id: selectedRows.map((r) => r.original.group.id), id: selectedRows.map((r) => r.original.group.id),
+1 -1
View File
@@ -140,7 +140,7 @@ export default function NotificationPage() {
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" /> <NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup <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={{ delete={{
fn: deleteNotification, fn: deleteNotification,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
+2 -2
View File
@@ -40,7 +40,7 @@ export default function OnlineUserPage() {
// 计算 offset // 计算 offset
const offset = (page - 1) * pageSize const offset = (page - 1) * pageSize
const { data, mutate, error, isLoading } = useSWR<ModelOnlineUserApi>( const { data, mutate, error, isLoading } = useSWR<ModelOnlineUserApi, Error>(
`/api/v1/online-user?offset=${offset}&limit=${pageSize}`, `/api/v1/online-user?offset=${offset}&limit=${pageSize}`,
swrFetcher, swrFetcher,
) )
@@ -128,7 +128,7 @@ export default function OnlineUserPage() {
return data?.value ?? [] return data?.value ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useReactTable<ModelOnlineUser>({
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
+1 -1
View File
@@ -125,7 +125,7 @@ export default function ServerGroupPage() {
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" /> <GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup <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={{ delete={{
fn: deleteServerGroups, fn: deleteServerGroups,
id: selectedRows.map((r) => r.original.group.id), id: selectedRows.map((r) => r.original.group.id),
+7 -9
View File
@@ -9,7 +9,6 @@ import { NoteMenu } from "@/components/note-menu"
import { ServerCard } from "@/components/server" import { ServerCard } from "@/components/server"
import { ServerConfigCard } from "@/components/server-config" import { ServerConfigCard } from "@/components/server-config"
import { ServerConfigCardBatch } from "@/components/server-config-batch" import { ServerConfigCardBatch } from "@/components/server-config-batch"
import { TerminalButton } from "@/components/terminal"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { import {
Table, Table,
@@ -85,7 +84,7 @@ export default function ServerPage() {
accessorFn: (row) => { accessorFn: (row) => {
return ( return (
serverGroups serverGroups
?.filter((sg) => sg.servers?.includes(row.id)) ?.filter((sg) => sg.servers?.includes(row.id!))
.map((sg) => sg.group.id) || [] .map((sg) => sg.group.id) || []
) )
}, },
@@ -105,7 +104,7 @@ export default function ServerPage() {
{ {
header: t("Version"), header: t("Version"),
accessorKey: "host.version", accessorKey: "host.version",
accessorFn: (row) => row.host.version || t("Unknown"), accessorFn: (row) => row.host?.version || t("Unknown"),
}, },
{ {
header: t("EnableDDNS"), header: t("EnableDDNS"),
@@ -141,12 +140,11 @@ export default function ServerPage() {
return ( return (
<ActionButtonGroup <ActionButtonGroup
className="flex gap-2" 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} /> <ServerCard mutate={mutate} data={s} />
<ServerConfigCard sid={s.id} variant="outline" /> <ServerConfigCard sid={s.id!} variant="outline" />
</> </>
</ActionButtonGroup> </ActionButtonGroup>
) )
@@ -181,7 +179,7 @@ export default function ServerPage() {
<IconButton <IconButton
icon="update" icon="update"
onClick={async () => { onClick={async () => {
const id = selectedRows.map((r) => r.original.id) const id = selectedRows.map((r) => r.original.id) as number[]
if (id.length < 1) { if (id.length < 1) {
toast(t("Error"), { toast(t("Error"), {
description: t("Results.SelectAtLeastOneServer"), description: t("Results.SelectAtLeastOneServer"),
@@ -214,9 +212,9 @@ export default function ServerPage() {
}) })
}} }}
/> />
<BatchMoveServerIcon serverIds={selectedRows.map((r) => r.original.id)} /> <BatchMoveServerIcon serverIds={selectedRows.map((r) => r.original.id) as number[]} />
<ServerConfigCardBatch <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" 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" /> <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" />
+5 -5
View File
@@ -60,7 +60,7 @@ export default function ServicePage() {
{ {
header: "ID", header: "ID",
accessorKey: "id", accessorKey: "id",
accessorFn: (row) => row.id, accessorFn: (row) => `${row.id}(${row.display_index ?? 0})`,
}, },
{ {
header: t("Name"), header: t("Name"),
@@ -174,11 +174,11 @@ export default function ServicePage() {
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
return ( return (
<div className="px-3"> <div className="px-3 max-w-7xl mx-auto">
<div className="flex mt-6 mb-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between w-full gap-3 mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Services")}</h1> <h1 className="text-3xl font-bold tracking-tight">{t("Service")}</h1>
<HeaderButtonGroup <HeaderButtonGroup
className="flex-2 flex ml-auto gap-2" className="flex gap-2 flex-wrap shrink-0"
delete={{ delete={{
fn: deleteService, fn: deleteService,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
+87 -8
View File
@@ -21,6 +21,8 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { Combobox } from "@/components/ui/combobox"
import { useNotification } from "@/hooks/useNotfication"
import { useAuth } from "@/hooks/useAuth" import { useAuth } from "@/hooks/useAuth"
import useSetting from "@/hooks/useSetting" import useSetting from "@/hooks/useSetting"
import { asOptionalField } from "@/lib/utils" import { asOptionalField } from "@/lib/utils"
@@ -50,6 +52,11 @@ const settingFormSchema = z.object({
tls: asOptionalField(z.boolean()), tls: asOptionalField(z.boolean()),
enable_ip_change_notification: asOptionalField(z.boolean()), enable_ip_change_notification: asOptionalField(z.boolean()),
enable_plain_ip_in_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()),
}) })
export default function SettingsPage() { export default function SettingsPage() {
@@ -58,14 +65,20 @@ export default function SettingsPage() {
const { profile } = useAuth() const { profile } = useAuth()
const navigate = useNavigate() 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 const isAdmin = profile?.role === 0
if (!isAdmin) { if (!isAdmin) {
navigate("/dashboard/settings/online-user") navigate("/dashboard/settings/online-user")
} }
const form = useForm<z.infer<typeof settingFormSchema>>({ const form = useForm({
resolver: zodResolver(settingFormSchema), resolver: zodResolver(settingFormSchema) as any,
defaultValues: config defaultValues: config
? { ? {
...config.config, ...config.config,
@@ -92,7 +105,7 @@ export default function SettingsPage() {
} }
}, [config?.config, form]) }, [config?.config, form])
const onSubmit = async (values: z.infer<typeof settingFormSchema>) => { const onSubmit = async (values: any) => {
try { try {
await updateSettings(values) await updateSettings(values)
form.reset() form.reset()
@@ -131,6 +144,71 @@ export default function SettingsPage() {
</FormItem> </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 <FormField
control={form.control} control={form.control}
name="language" name="language"
@@ -448,12 +526,13 @@ export default function SettingsPage() {
name="ip_change_notification_group_id" name="ip_change_notification_group_id"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("NotifierGroupID")}</FormLabel> <FormLabel>{t("NotifierGroup")}</FormLabel>
<FormControl> <FormControl>
<Input <Combobox
placeholder="0" placeholder={t("Search")}
type="number" options={ngroupList}
{...field} onValueChange={field.onChange}
defaultValue={field.value?.toString()}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
+2 -1
View File
@@ -77,7 +77,8 @@ export default function UserPage() {
{ {
header: t("LastLogin"), header: t("LastLogin"),
accessorKey: "updated_at", 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", id: "actions",
+15 -2
View File
@@ -667,8 +667,12 @@ export interface ModelSetting {
/** 用于前端判断生成的安装命令是否启用 TLS */ /** 用于前端判断生成的安装命令是否启用 TLS */
tls?: boolean; tls?: boolean;
user_template?: string; user_template?: string;
/** 前端真实IP */
web_real_ip_header?: string; web_real_ip_header?: string;
custom_logo?: string;
custom_description?: string;
custom_links?: string;
background_image_day?: string;
background_image_night?: string;
} }
export interface ModelSettingForm { export interface ModelSettingForm {
@@ -690,8 +694,17 @@ export interface ModelSettingForm {
site_name?: string; site_name?: string;
tls?: boolean; tls?: boolean;
user_template?: string; user_template?: string;
/** 前端真实IP */
web_real_ip_header?: string; web_real_ip_header?: string;
/** 自定义Logo链接 */
custom_logo?: string;
/** 自定义描述/副标题 */
custom_description?: string;
/** 自定义导航链接 (JSON) */
custom_links?: string;
/** 白天背景图链接 */
background_image_day?: string;
/** 夜间背景图链接 */
background_image_night?: string;
} }
export interface ModelSettingResponse { export interface ModelSettingResponse {
+1 -1
View File
@@ -26,7 +26,7 @@ export const AgentConfigSchema = z.object({
), ),
), ),
ip_report_period: asOptionalField(z.coerce.number().int().min(30)), 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( nic_allowlist_raw: asOptionalField(
z.string().refine( z.string().refine(
(val) => { (val) => {
-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"), "@": 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"
},
},
},
},
})