Dashboard Redesign (#48)

* feat: add user_template setting

* style: header

* style: page padding

* style: header

* feat: header now time

* style: login page

* feat: nav indicator

* style: button inset shadow

* style: footer text size

* feat: header show login_ip

* fix: error toast

* fix: frontend_templates setting

* fix: lint

* feat: pr auto format

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
This commit is contained in:
仓鼠
2024-12-13 23:51:33 +08:00
committed by GitHub
parent b04ef1bb72
commit 8c8d3e3057
132 changed files with 13242 additions and 12878 deletions

View File

@@ -1,53 +1,53 @@
name: Auto Format name: Auto Format
on: on:
pull_request_target: pull_request_target:
types: [opened, synchronize] types: [opened, synchronize]
jobs: jobs:
auto-fix: auto-fix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
ref: ${{ github.head_ref }} ref: ${{ github.head_ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Set up Bun - name: Set up Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: with:
bun-version: "latest" bun-version: "latest"
- name: Install dependencies - name: Install dependencies
run: bun install run: bun install
- name: Run formatter - name: Run formatter
run: bun run format run: bun run format
- name: Check for changes - name: Check for changes
id: check_changes id: check_changes
run: | run: |
git diff --exit-code || echo "has_changes=true" >> $GITHUB_ENV git diff --exit-code || echo "has_changes=true" >> $GITHUB_ENV
- name: Commit and push changes - name: Commit and push changes
if: steps.check_changes.outputs.has_changes == 'true' || env.has_changes == 'true' if: steps.check_changes.outputs.has_changes == 'true' || env.has_changes == 'true'
uses: stefanzweifel/git-auto-commit-action@v5 uses: stefanzweifel/git-auto-commit-action@v5
with: with:
commit_message: "chore: auto-fix linting and formatting issues" commit_message: "chore: auto-fix linting and formatting issues"
commit_options: "--no-verify" commit_options: "--no-verify"
file_pattern: "." file_pattern: "."
- name: Add PR comment - name: Add PR comment
if: steps.check_changes.outputs.has_changes == 'true' || env.has_changes == 'true' if: steps.check_changes.outputs.has_changes == 'true' || env.has_changes == 'true'
uses: actions/github-script@v7 uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GITHUB_TOKEN}}
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({
issue_number: context.issue.number, issue_number: context.issue.number,
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
body: 'Linting and formatting issues were automatically fixed. Please review the changes.' body: 'Linting and formatting issues were automatically fixed. Please review the changes.'
}); });

View File

@@ -1,42 +1,42 @@
name: Build and release static export name: Build and release static export
on: on:
push: push:
tags: tags:
- "v*" - "v*"
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Bun - name: Set up Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: with:
bun-version: "latest" bun-version: "latest"
- name: Install dependencies - name: Install dependencies
run: bun install run: bun install
- name: Build static export - name: Build static export
run: | run: |
bun run build-ignore-error bun run build-ignore-error
- name: Compress dist folder - name: Compress dist folder
run: zip -r dist.zip dist run: zip -r dist.zip dist
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: dist.zip files: dist.zip
- name: Changelog - name: Changelog
run: bun x changelogithub run: bun x changelogithub
env: env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

View File

@@ -1,12 +1,12 @@
{ {
"semi": false, "semi": false,
"singleQuote": false, "singleQuote": false,
"printWidth": 100, "printWidth": 100,
"tabWidth": 4, "tabWidth": 4,
"trailingComma": "all", "trailingComma": "all",
"importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], "importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
"importOrderSeparation": true, "importOrderSeparation": true,
"importOrderSortSpecifiers": true, "importOrderSortSpecifiers": true,
"endOfLine": "auto", "endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss", "@trivago/prettier-plugin-sort-imports"] "plugins": ["prettier-plugin-tailwindcss", "@trivago/prettier-plugin-sort-imports"]
} }

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,20 +1,20 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "default", "style": "default",
"rsc": false, "rsc": false,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "tailwind.config.js", "config": "tailwind.config.js",
"css": "src/index.css", "css": "src/index.css",
"baseColor": "neutral", "baseColor": "neutral",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"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"
} }
} }

View File

@@ -1,29 +1,27 @@
import js from '@eslint/js' import js from "@eslint/js"
import globals from 'globals' import reactHooks from "eslint-plugin-react-hooks"
import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from "eslint-plugin-react-refresh"
import reactRefresh from 'eslint-plugin-react-refresh' import globals from "globals"
import tseslint from 'typescript-eslint' import tseslint from "typescript-eslint"
export default tseslint.config( export default tseslint.config(
{ ignores: ['dist'] }, { ignores: ["dist"] },
{ {
extends: [js.configs.recommended, ...tseslint.configs.recommended], extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'], files: ["**/*.{ts,tsx}"],
languageOptions: { languageOptions: {
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"@typescript-eslint/no-explicit-any": "off",
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
indent: ["error", 4],
},
}, },
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
"indent": ['error', 4],
},
},
) )

View File

@@ -1,16 +1,14 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>哪吒监控 Nezha Monitoring</title>
</head>
<head> <body>
<meta charset="UTF-8" /> <div id="root"></div>
<link rel="shortcut icon" type="image/svg+xml" href="/logo.svg" /> <script type="module" src="/src/main.tsx"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> </body>
<title>哪吒监控 Nezha Monitoring</title> </html>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

13538
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,78 +1,80 @@
{ {
"name": "admin-frontend", "name": "admin-frontend",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"build-ignore-error": "vite build", "build-ignore-error": "vite build",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint --fix .", "lint:fix": "eslint --fix .",
"format": "prettier --write .", "format": "prettier --write .",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.1", "@radix-ui/react-navigation-menu": "^1.2.1",
"@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.2", "@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.5",
"@trivago/prettier-plugin-sort-imports": "^5.2.0", "@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@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.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"i18next": "^24.0.2", "framer-motion": "^11.14.1",
"i18next-browser-languagedetector": "^8.0.0", "i18next": "^24.0.2",
"jotai-zustand": "^0.6.0", "i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.454.0", "jotai-zustand": "^0.6.0",
"next-themes": "^0.3.0", "lucide-react": "^0.454.0",
"prettier-plugin-tailwindcss": "^0.6.9", "luxon": "^3.5.0",
"react": "^18.3.1", "next-themes": "^0.3.0",
"react-dom": "^18.3.1", "prettier-plugin-tailwindcss": "^0.6.9",
"react-hook-form": "^7.53.1", "react": "^18.3.1",
"react-i18next": "^15.1.2", "react-dom": "^18.3.1",
"react-router-dom": "^6.27.0", "react-hook-form": "^7.53.1",
"react-virtuoso": "^4.12.0", "react-i18next": "^15.1.2",
"sonner": "^1.6.1", "react-router-dom": "^6.27.0",
"swr": "^2.2.5", "react-virtuoso": "^4.12.0",
"tailwind-merge": "^2.5.4", "sonner": "^1.6.1",
"tailwindcss-animate": "^1.0.7", "swr": "^2.2.5",
"vaul": "^1.1.1", "tailwind-merge": "^2.5.4",
"zod": "^3.23.8", "tailwindcss-animate": "^1.0.7",
"zustand": "^5.0.1" "vaul": "^1.1.1",
}, "zod": "^3.23.8",
"devDependencies": { "zustand": "^5.0.1"
"@eslint/js": "^9.13.0", },
"@types/node": "^22.8.6", "devDependencies": {
"@types/react": "^18.3.12", "@eslint/js": "^9.13.0",
"@types/react-dom": "^18.3.1", "@types/node": "^22.8.6",
"@vitejs/plugin-react": "^4.3.3", "@types/react": "^18.3.12",
"autoprefixer": "^10.4.20", "@types/react-dom": "^18.3.1",
"eslint": "^9.13.0", "@vitejs/plugin-react": "^4.3.3",
"eslint-plugin-react-hooks": "^5.0.0", "autoprefixer": "^10.4.20",
"eslint-plugin-react-refresh": "^0.4.14", "eslint": "^9.13.0",
"globals": "^15.11.0", "eslint-plugin-react-hooks": "^5.0.0",
"postcss": "^8.4.47", "eslint-plugin-react-refresh": "^0.4.14",
"swagger-typescript-api": "^13.0.22", "globals": "^15.11.0",
"tailwindcss": "^3.4.14", "postcss": "^8.4.47",
"typescript": "~5.6.2", "swagger-typescript-api": "^13.0.22",
"typescript-eslint": "^8.11.0", "tailwindcss": "^3.4.14",
"vite": "^5.4.10" "typescript": "~5.6.2",
} "typescript-eslint": "^8.11.0",
"vite": "^5.4.10"
}
} }

View File

@@ -1,6 +1,6 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} }

View File

@@ -1,14 +1,15 @@
import { ModelAlertRuleForm } from "@/types" import { ModelAlertRuleForm } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const createAlertRule = async (data: ModelAlertRuleForm): Promise<number> => { export const createAlertRule = async (data: ModelAlertRuleForm): Promise<number> => {
return fetcher<number>(FetcherMethod.POST, '/api/v1/alert-rule', data); return fetcher<number>(FetcherMethod.POST, "/api/v1/alert-rule", data)
} }
export const updateAlertRule = async (id: number, data: ModelAlertRuleForm): Promise<void> => { export const updateAlertRule = async (id: number, data: ModelAlertRuleForm): Promise<void> => {
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/alert-rule/${id}`, data); return fetcher<void>(FetcherMethod.PATCH, `/api/v1/alert-rule/${id}`, data)
} }
export const deleteAlertRules = async (id: number[]): Promise<void> => { export const deleteAlertRules = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, '/api/v1/batch-delete/alert-rule', id); return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/alert-rule", id)
} }

View File

@@ -1,17 +1,16 @@
interface CommonResponse<T> { interface CommonResponse<T> {
success: boolean; success: boolean
error: string; error: string
data: T; data: T
} }
function buildUrl(path: string, data?: any): string { function buildUrl(path: string, data?: any): string {
if (!data) if (!data) return path
return path const url = new URL(path)
const url = new URL(path);
for (const key in data) { for (const key in data) {
url.searchParams.append(key, data[key]); url.searchParams.append(key, data[key])
} }
return url.toString(); return url.toString()
} }
export enum FetcherMethod { export enum FetcherMethod {
@@ -22,14 +21,14 @@ export enum FetcherMethod {
DELETE = "DELETE", DELETE = "DELETE",
} }
let lastestRefreshTokenAt = 0; let lastestRefreshTokenAt = 0
export async function fetcher<T>(method: FetcherMethod, path: string, data?: any): Promise<T> { export async function fetcher<T>(method: FetcherMethod, path: string, data?: any): Promise<T> {
let response; let response
if (method === FetcherMethod.GET || method === FetcherMethod.DELETE) { if (method === FetcherMethod.GET || method === FetcherMethod.DELETE) {
response = await fetch(buildUrl(path, data), { response = await fetch(buildUrl(path, data), {
method: "GET", method: "GET",
}); })
} else { } else {
response = await fetch(path, { response = await fetch(path, {
method: method, method: method,
@@ -37,25 +36,28 @@ export async function fetcher<T>(method: FetcherMethod, path: string, data?: any
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: data ? JSON.stringify(data) : null, body: data ? JSON.stringify(data) : null,
}); })
} }
if (!response.ok) { if (!response.ok) {
throw new Error(response.statusText); throw new Error(response.statusText)
} }
const responseData: CommonResponse<T> = await response.json(); const responseData: CommonResponse<T> = await response.json()
if (!responseData.success) { if (!responseData.success) {
throw new Error(responseData.error); throw new Error(responseData.error)
} }
// auto refresh token // auto refresh token
if (document.cookie && (!lastestRefreshTokenAt || Date.now() - lastestRefreshTokenAt > 1000 * 60 * 60)) { if (
lastestRefreshTokenAt = Date.now(); document.cookie &&
(!lastestRefreshTokenAt || Date.now() - lastestRefreshTokenAt > 1000 * 60 * 60)
) {
lastestRefreshTokenAt = Date.now()
fetch("/api/v1/refresh-token") fetch("/api/v1/refresh-token")
} }
return responseData.data; return responseData.data
} }
export async function swrFetcher<T>(input: string | URL | globalThis.Request, init?: RequestInit) { export async function swrFetcher<T>(input: string | URL | globalThis.Request, init?: RequestInit) {
return fetcher<T>(init?.method as FetcherMethod, input.toString(), init?.body); return fetcher<T>(init?.method as FetcherMethod, input.toString(), init?.body)
} }

View File

@@ -1,18 +1,19 @@
import { ModelCronForm } from "@/types" import { ModelCronForm } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const createCron = async (data: ModelCronForm): Promise<number> => { export const createCron = async (data: ModelCronForm): Promise<number> => {
return fetcher<number>(FetcherMethod.POST, '/api/v1/cron', data); return fetcher<number>(FetcherMethod.POST, "/api/v1/cron", data)
} }
export const updateCron = async (id: number, data: ModelCronForm): Promise<void> => { export const updateCron = async (id: number, data: ModelCronForm): Promise<void> => {
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/cron/${id}`, data); return fetcher<void>(FetcherMethod.PATCH, `/api/v1/cron/${id}`, data)
} }
export const deleteCron = async (id: number[]): Promise<void> => { export const deleteCron = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, '/api/v1/batch-delete/cron', id); return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/cron", id)
} }
export const runCron = async (id: number): Promise<void> => { export const runCron = async (id: number): Promise<void> => {
return fetcher<void>(FetcherMethod.GET, `/api/v1/cron/${id}/manual`, null); return fetcher<void>(FetcherMethod.GET, `/api/v1/cron/${id}/manual`, null)
} }

View File

@@ -1,18 +1,19 @@
import { ModelDDNSForm } from "@/types" import { ModelDDNSForm } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const createDDNSProfile = async (data: ModelDDNSForm): Promise<number> => { export const createDDNSProfile = async (data: ModelDDNSForm): Promise<number> => {
return fetcher<number>(FetcherMethod.POST, '/api/v1/ddns', data); return fetcher<number>(FetcherMethod.POST, "/api/v1/ddns", data)
} }
export const updateDDNSProfile = async (id: number, data: ModelDDNSForm): Promise<void> => { export const updateDDNSProfile = async (id: number, data: ModelDDNSForm): Promise<void> => {
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/ddns/${id}`, data); return fetcher<void>(FetcherMethod.PATCH, `/api/v1/ddns/${id}`, data)
} }
export const deleteDDNSProfiles = async (id: number[]): Promise<void> => { export const deleteDDNSProfiles = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, '/api/v1/batch-delete/ddns', id); return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/ddns", id)
} }
export const getDDNSProviders = async (): Promise<string[]> => { export const getDDNSProviders = async (): Promise<string[]> => {
return fetcher<string[]>(FetcherMethod.GET, '/api/v1/ddns/providers', null); return fetcher<string[]>(FetcherMethod.GET, "/api/v1/ddns/providers", null)
} }

View File

@@ -1,6 +1,7 @@
import { ModelCreateFMResponse } from "@/types"; import { ModelCreateFMResponse } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const createFM = async (id: string): Promise<ModelCreateFMResponse> => { export const createFM = async (id: string): Promise<ModelCreateFMResponse> => {
return fetcher<ModelCreateFMResponse>(FetcherMethod.GET, `/api/v1/file?id=${id}`, null); return fetcher<ModelCreateFMResponse>(FetcherMethod.GET, `/api/v1/file?id=${id}`, null)
} }

View File

@@ -1,14 +1,15 @@
import { ModelNATForm } from "@/types" import { ModelNATForm } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const createNAT = async (data: ModelNATForm): Promise<number> => { export const createNAT = async (data: ModelNATForm): Promise<number> => {
return fetcher<number>(FetcherMethod.POST, '/api/v1/nat', data); return fetcher<number>(FetcherMethod.POST, "/api/v1/nat", data)
} }
export const updateNAT = async (id: number, data: ModelNATForm): Promise<void> => { export const updateNAT = async (id: number, data: ModelNATForm): Promise<void> => {
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/nat/${id}`, data); return fetcher<void>(FetcherMethod.PATCH, `/api/v1/nat/${id}`, data)
} }
export const deleteNAT = async (id: number[]): Promise<void> => { export const deleteNAT = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, '/api/v1/batch-delete/nat', id); return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/nat", id)
} }

View File

@@ -1,18 +1,28 @@
import { ModelNotificationGroupForm, ModelNotificationGroupResponseItem } from "@/types" import { ModelNotificationGroupForm, ModelNotificationGroupResponseItem } from "@/types"
import { fetcher, FetcherMethod } from "./api"
export const createNotificationGroup = async (data: ModelNotificationGroupForm): Promise<number> => { import { FetcherMethod, fetcher } from "./api"
return fetcher<number>(FetcherMethod.POST, '/api/v1/notification-group', data);
export const createNotificationGroup = async (
data: ModelNotificationGroupForm,
): Promise<number> => {
return fetcher<number>(FetcherMethod.POST, "/api/v1/notification-group", data)
} }
export const updateNotificationGroup = async (id: number, data: ModelNotificationGroupForm): Promise<void> => { export const updateNotificationGroup = async (
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/notification-group/${id}`, data); id: number,
data: ModelNotificationGroupForm,
): Promise<void> => {
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/notification-group/${id}`, data)
} }
export const deleteNotificationGroups = async (id: number[]): Promise<void> => { export const deleteNotificationGroups = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, `/api/v1/batch-delete/notification-group`, id); return fetcher<void>(FetcherMethod.POST, `/api/v1/batch-delete/notification-group`, id)
} }
export const getNotificationGroups = async (): Promise<ModelNotificationGroupResponseItem[]> => { export const getNotificationGroups = async (): Promise<ModelNotificationGroupResponseItem[]> => {
return fetcher<ModelNotificationGroupResponseItem[]>(FetcherMethod.GET, '/api/v1/notification-group', null); return fetcher<ModelNotificationGroupResponseItem[]>(
FetcherMethod.GET,
"/api/v1/notification-group",
null,
)
} }

View File

@@ -1,18 +1,22 @@
import { ModelNotificationForm, ModelNotification } from "@/types" import { ModelNotification, ModelNotificationForm } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const createNotification = async (data: ModelNotificationForm): Promise<number> => { export const createNotification = async (data: ModelNotificationForm): Promise<number> => {
return fetcher<number>(FetcherMethod.POST, '/api/v1/notification', data); return fetcher<number>(FetcherMethod.POST, "/api/v1/notification", data)
} }
export const updateNotification = async (id: number, data: ModelNotificationForm): Promise<void> => { export const updateNotification = async (
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/notification/${id}`, data); id: number,
data: ModelNotificationForm,
): Promise<void> => {
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/notification/${id}`, data)
} }
export const deleteNotification = async (id: number[]): Promise<void> => { export const deleteNotification = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, '/api/v1/batch-delete/notification', id); return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/notification", id)
} }
export const getNotification = async (): Promise<ModelNotification[]> => { export const getNotification = async (): Promise<ModelNotification[]> => {
return fetcher<ModelNotification[]>(FetcherMethod.GET, '/api/v1/notification', null); return fetcher<ModelNotification[]>(FetcherMethod.GET, "/api/v1/notification", null)
} }

View File

@@ -1,18 +1,19 @@
import { ModelServerGroupForm, ModelServerGroupResponseItem } from "@/types" import { ModelServerGroupForm, ModelServerGroupResponseItem } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const createServerGroup = async (data: ModelServerGroupForm): Promise<number> => { export const createServerGroup = async (data: ModelServerGroupForm): Promise<number> => {
return fetcher<number>(FetcherMethod.POST, '/api/v1/server-group', data); return fetcher<number>(FetcherMethod.POST, "/api/v1/server-group", data)
} }
export const updateServerGroup = async (id: number, data: ModelServerGroupForm): Promise<void> => { export const updateServerGroup = async (id: number, data: ModelServerGroupForm): Promise<void> => {
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/server-group/${id}`, data); return fetcher<void>(FetcherMethod.PATCH, `/api/v1/server-group/${id}`, data)
} }
export const deleteServerGroups = async (id: number[]): Promise<void> => { export const deleteServerGroups = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, `/api/v1/batch-delete/server-group`, id); return fetcher<void>(FetcherMethod.POST, `/api/v1/batch-delete/server-group`, id)
} }
export const getServerGroups = async (): Promise<ModelServerGroupResponseItem[]> => { export const getServerGroups = async (): Promise<ModelServerGroupResponseItem[]> => {
return fetcher<ModelServerGroupResponseItem[]>(FetcherMethod.GET, '/api/v1/server-group', null); return fetcher<ModelServerGroupResponseItem[]>(FetcherMethod.GET, "/api/v1/server-group", null)
} }

View File

@@ -1,18 +1,19 @@
import { ModelServer, ModelServerForm, ModelForceUpdateResponse } from "@/types" import { ModelForceUpdateResponse, ModelServer, ModelServerForm } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const updateServer = async (id: number, data: ModelServerForm): Promise<void> => { export const updateServer = async (id: number, data: ModelServerForm): Promise<void> => {
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/server/${id}`, data); return fetcher<void>(FetcherMethod.PATCH, `/api/v1/server/${id}`, data)
} }
export const deleteServer = async (id: number[]): Promise<void> => { export const deleteServer = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, '/api/v1/batch-delete/server', id); return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/server", id)
} }
export const forceUpdateServer = async (id: number[]): Promise<ModelForceUpdateResponse> => { export const forceUpdateServer = async (id: number[]): Promise<ModelForceUpdateResponse> => {
return fetcher<ModelForceUpdateResponse>(FetcherMethod.POST, '/api/v1/force-update/server', id); return fetcher<ModelForceUpdateResponse>(FetcherMethod.POST, "/api/v1/force-update/server", id)
} }
export const getServers = async (): Promise<ModelServer[]> => { export const getServers = async (): Promise<ModelServer[]> => {
return fetcher<ModelServer[]>(FetcherMethod.GET, '/api/v1/server', null); return fetcher<ModelServer[]>(FetcherMethod.GET, "/api/v1/server", null)
} }

View File

@@ -1,14 +1,15 @@
import { ModelServiceForm } from "@/types" import { ModelServiceForm } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const createService = async (data: ModelServiceForm): Promise<number> => { export const createService = async (data: ModelServiceForm): Promise<number> => {
return fetcher<number>(FetcherMethod.POST, '/api/v1/service', data); return fetcher<number>(FetcherMethod.POST, "/api/v1/service", data)
} }
export const updateService = async (id: number, data: ModelServiceForm): Promise<void> => { export const updateService = async (id: number, data: ModelServiceForm): Promise<void> => {
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/service/${id}`, data); return fetcher<void>(FetcherMethod.PATCH, `/api/v1/service/${id}`, data)
} }
export const deleteService = async (id: number[]): Promise<void> => { export const deleteService = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, '/api/v1/batch-delete/service', id); return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/service", id)
} }

View File

@@ -1,10 +1,11 @@
import { ModelSettingForm, ModelSettingResponse } from "@/types" import { ModelSettingForm, ModelSettingResponse } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const updateSettings = async (data: ModelSettingForm): Promise<void> => { export const updateSettings = async (data: ModelSettingForm): Promise<void> => {
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/setting`, data); return fetcher<void>(FetcherMethod.PATCH, `/api/v1/setting`, data)
} }
export const getSettings = async (): Promise<ModelSettingResponse> => { export const getSettings = async (): Promise<ModelSettingResponse> => {
return fetcher<ModelSettingResponse>(FetcherMethod.GET, '/api/v1/setting', null); return fetcher<ModelSettingResponse>(FetcherMethod.GET, "/api/v1/setting", null)
} }

View File

@@ -1,8 +1,9 @@
import { ModelCreateTerminalResponse } from "@/types"; import { ModelCreateTerminalResponse } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const createTerminal = async (id: number): Promise<ModelCreateTerminalResponse> => { export const createTerminal = async (id: number): Promise<ModelCreateTerminalResponse> => {
return fetcher<ModelCreateTerminalResponse>(FetcherMethod.POST, '/api/v1/terminal', { return fetcher<ModelCreateTerminalResponse>(FetcherMethod.POST, "/api/v1/terminal", {
server_id: id, server_id: id,
}); })
} }

View File

@@ -1,22 +1,23 @@
import { ModelProfile, ModelUserForm, ModelProfileForm } from "@/types" import { ModelProfile, ModelProfileForm, ModelUserForm } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const getProfile = async (): Promise<ModelProfile> => { export const getProfile = async (): Promise<ModelProfile> => {
return fetcher<ModelProfile>(FetcherMethod.GET, '/api/v1/profile', null); return fetcher<ModelProfile>(FetcherMethod.GET, "/api/v1/profile", null)
} }
export const login = async (username: string, password: string): Promise<void> => { export const login = async (username: string, password: string): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, '/api/v1/login', { username, password }); return fetcher<void>(FetcherMethod.POST, "/api/v1/login", { username, password })
} }
export const createUser = async (data: ModelUserForm): Promise<number> => { export const createUser = async (data: ModelUserForm): Promise<number> => {
return fetcher<number>(FetcherMethod.POST, '/api/v1/user', data); return fetcher<number>(FetcherMethod.POST, "/api/v1/user", data)
} }
export const deleteUser = async (id: number[]): Promise<void> => { export const deleteUser = async (id: number[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, '/api/v1/batch-delete/user', id); return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/user", id)
} }
export const updateProfile = async (data: ModelProfileForm): Promise<void> => { export const updateProfile = async (data: ModelProfileForm): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, '/api/v1/profile', data); return fetcher<void>(FetcherMethod.POST, "/api/v1/profile", data)
} }

View File

@@ -1,10 +1,11 @@
import { ModelWAFApiMock } from "@/types" import { ModelWAFApiMock } from "@/types"
import { fetcher, FetcherMethod } from "./api"
import { FetcherMethod, fetcher } from "./api"
export const deleteWAF = async (ip: string[]): Promise<void> => { export const deleteWAF = async (ip: string[]): Promise<void> => {
return fetcher<void>(FetcherMethod.POST, '/api/v1/batch-delete/waf', ip); return fetcher<void>(FetcherMethod.POST, "/api/v1/batch-delete/waf", ip)
} }
export const getWAFList = async (): Promise<ModelWAFApiMock[]> => { export const getWAFList = async (): Promise<ModelWAFApiMock[]> => {
return fetcher<ModelWAFApiMock[]>(FetcherMethod.GET, '/api/v1/waf', null); return fetcher<ModelWAFApiMock[]>(FetcherMethod.GET, "/api/v1/waf", null)
} }

View File

@@ -1,4 +1,3 @@
import { IconButton } from "@/components/xui/icon-button";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -10,22 +9,33 @@ import {
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog"
import { KeyedMutator } from "swr";
import { buttonVariants } from "@/components/ui/button" import { buttonVariants } from "@/components/ui/button"
import { IconButton } from "@/components/xui/icon-button"
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { KeyedMutator } from "swr"
interface ButtonGroupProps<E, U> { interface ButtonGroupProps<E, U> {
className?: string; className?: string
children: React.ReactNode; children: React.ReactNode
delete: { fn: (id: E[]) => Promise<void>, id: E, mutate: KeyedMutator<U> }; delete: { fn: (id: E[]) => Promise<void>; id: E; mutate: KeyedMutator<U> }
} }
export function ActionButtonGroup<E, U>({ className, children, delete: { fn, id, mutate } }: ButtonGroupProps<E, U>) { export function ActionButtonGroup<E, U>({
const { t } = useTranslation(); className,
children,
delete: { fn, id, mutate },
}: ButtonGroupProps<E, U>) {
const { t } = useTranslation()
const handleDelete = async () => { const handleDelete = async () => {
await fn([id]); try {
await mutate(); await fn([id])
} catch (error: any) {
toast(t("Error"), {
description: error.message,
})
}
await mutate()
} }
return ( return (
@@ -44,7 +54,12 @@ export function ActionButtonGroup<E, U>({ className, children, delete: { fn, id,
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel> <AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction className={buttonVariants({ variant: "destructive" })} onClick={handleDelete}>{t("Confirm")}</AlertDialogAction> <AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={handleDelete}
>
{t("Confirm")}
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>

View File

@@ -1,4 +1,6 @@
import { createAlertRule, updateAlertRule } from "@/api/alert-rule"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
@@ -9,14 +11,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { import {
Form, Form,
FormControl, FormControl,
@@ -25,29 +19,35 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { ScrollArea } from "@/components/ui/scroll-area" import { Input } from "@/components/ui/input"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { ModelAlertRule } from "@/types"
import { createAlertRule, updateAlertRule } from "@/api/alert-rule"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { conv } from "@/lib/utils" import { ScrollArea } from "@/components/ui/scroll-area"
import { useState } from "react" import {
import { KeyedMutator } from "swr" Select,
import { asOptionalField } from "@/lib/utils" SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { triggerModes } from "@/types"
import { Textarea } from "./ui/textarea"
import { useNotification } from "@/hooks/useNotfication" import { useNotification } from "@/hooks/useNotfication"
import { Combobox } from "./ui/combobox" import { conv } from "@/lib/utils"
import { asOptionalField } from "@/lib/utils"
import { ModelAlertRule } from "@/types"
import { triggerModes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { KeyedMutator } from "swr"
import { z } from "zod"
import { useTranslation } from "react-i18next"; import { Combobox } from "./ui/combobox"
import { Textarea } from "./ui/textarea"
interface AlertRuleCardProps { interface AlertRuleCardProps {
data?: ModelAlertRule; data?: ModelAlertRule
mutate: KeyedMutator<ModelAlertRule[]>; mutate: KeyedMutator<ModelAlertRule[]>
} }
const ruleSchema = z.object({ const ruleSchema = z.object({
@@ -56,87 +56,91 @@ const ruleSchema = z.object({
max: asOptionalField(z.number()), max: asOptionalField(z.number()),
cycle_start: asOptionalField(z.string()), cycle_start: asOptionalField(z.string()),
cycle_interval: asOptionalField(z.number()), cycle_interval: asOptionalField(z.number()),
cycle_unit: asOptionalField(z.enum(['hour', 'day', 'week', 'month', 'year'])), cycle_unit: asOptionalField(z.enum(["hour", "day", "week", "month", "year"])),
duration: asOptionalField(z.number()), duration: asOptionalField(z.number()),
cover: z.number().int().min(0), cover: z.number().int().min(0),
ignore: asOptionalField(z.record(z.boolean())), ignore: asOptionalField(z.record(z.boolean())),
next_transfer_at: asOptionalField(z.record(z.string())), next_transfer_at: asOptionalField(z.record(z.string())),
last_cycle_status: asOptionalField((z.boolean())), last_cycle_status: asOptionalField(z.boolean()),
}); })
const alertRuleFormSchema = z.object({ const alertRuleFormSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
rules_raw: z.string().refine((val) => { rules_raw: z.string().refine(
try { (val) => {
JSON.parse(val); try {
return true; JSON.parse(val)
} catch (e) { return true
return false; } catch (e) {
} return false
}, { }
message: 'Invalid JSON string', },
}), {
message: "Invalid JSON string",
},
),
rules: z.array(ruleSchema), rules: z.array(ruleSchema),
fail_trigger_tasks: z.array(z.number()), fail_trigger_tasks: z.array(z.number()),
recover_trigger_tasks: z.array(z.number()), recover_trigger_tasks: z.array(z.number()),
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: asOptionalField(z.boolean()),
}); })
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>>({ const form = useForm<z.infer<typeof alertRuleFormSchema>>({
resolver: zodResolver(alertRuleFormSchema), resolver: zodResolver(alertRuleFormSchema),
defaultValues: data ? { defaultValues: data
...data, ? {
rules_raw: JSON.stringify(data.rules), ...data,
} : { rules_raw: JSON.stringify(data.rules),
name: "", }
rules_raw: "", : {
rules: [], name: "",
fail_trigger_tasks: [], rules_raw: "",
recover_trigger_tasks: [], rules: [],
notification_group_id: 0, fail_trigger_tasks: [],
trigger_mode: 0, recover_trigger_tasks: [],
}, notification_group_id: 0,
trigger_mode: 0,
},
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
} },
}) })
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof alertRuleFormSchema>) => { const onSubmit = async (values: z.infer<typeof alertRuleFormSchema>) => {
values.rules = JSON.parse(values.rules_raw); values.rules = JSON.parse(values.rules_raw)
const { rules_raw, ...requiredFields } = values; const { rules_raw, ...requiredFields } = values
data?.id ? await updateAlertRule(data.id, requiredFields) : await createAlertRule(requiredFields); data?.id
setOpen(false); ? await updateAlertRule(data.id, requiredFields)
await mutate(); : await createAlertRule(requiredFields)
form.reset(); setOpen(false)
await mutate()
form.reset()
} }
const { notifierGroup } = useNotification(); const { notifierGroup } = useNotification()
const ngroupList = notifierGroup?.map(ng => ({ const ngroupList = notifierGroup?.map((ng) => ({
value: `${ng.group.id}`, value: `${ng.group.id}`,
label: ng.group.name, label: ng.group.name,
})) || [{ value: "", label: "" }]; })) || [{ value: "", label: "" }]
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
{data {data ? <IconButton variant="outline" icon="edit" /> : <IconButton icon="plus" />}
?
<IconButton variant="outline" icon="edit" />
:
<IconButton icon="plus" />
}
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="sm:max-w-xl">
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3"> <ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1"> <div className="items-center mx-1">
<DialogHeader> <DialogHeader>
<DialogTitle>{data ? t("EditAlertRule") : t("CreateAlertRule")}</DialogTitle> <DialogTitle>
{data ? t("EditAlertRule") : t("CreateAlertRule")}
</DialogTitle>
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
@@ -148,9 +152,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
<FormItem> <FormItem>
<FormLabel>{t("Name")}</FormLabel> <FormLabel>{t("Name")}</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -163,10 +165,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
<FormItem> <FormItem>
<FormLabel>{t("Rules")}</FormLabel> <FormLabel>{t("Rules")}</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea className="resize-y" {...field} />
className="resize-y"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -196,7 +195,10 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("TriggerMode")}</FormLabel> <FormLabel>{t("TriggerMode")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}> <Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@@ -204,7 +206,9 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{Object.entries(triggerModes).map(([k, v]) => ( {Object.entries(triggerModes).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem> <SelectItem key={k} value={k}>
{v}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -217,15 +221,20 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
name="fail_trigger_tasks" name="fail_trigger_tasks"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("TasksToTriggerOnAlert") + t("SeparateWithComma")}</FormLabel> <FormLabel>
{t("TasksToTriggerOnAlert") +
t("SeparateWithComma")}
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="1,2,3" placeholder="1,2,3"
{...field} {...field}
value={conv.arrToStr(field.value ?? [])} value={conv.arrToStr(field.value ?? [])}
onChange={e => { onChange={(e) => {
const arr = conv.strToArr(e.target.value).map(Number); const arr = conv
field.onChange(arr); .strToArr(e.target.value)
.map(Number)
field.onChange(arr)
}} }}
/> />
</FormControl> </FormControl>
@@ -238,15 +247,20 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
name="recover_trigger_tasks" name="recover_trigger_tasks"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("TasksToTriggerAfterRecovery") + t("SeparateWithComma")}</FormLabel> <FormLabel>
{t("TasksToTriggerAfterRecovery") +
t("SeparateWithComma")}
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="1,2,3" placeholder="1,2,3"
{...field} {...field}
value={conv.arrToStr(field.value ?? [])} value={conv.arrToStr(field.value ?? [])}
onChange={e => { onChange={(e) => {
const arr = conv.strToArr(e.target.value).map(Number); const arr = conv
field.onChange(arr); .strToArr(e.target.value)
.map(Number)
field.onChange(arr)
}} }}
/> />
</FormControl> </FormControl>
@@ -278,7 +292,9 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
{t("Close")} {t("Close")}
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" className="my-2">{t("Confirm")}</Button> <Button type="submit" className="my-2">
{t("Confirm")}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>

View File

@@ -1,3 +1,4 @@
import { createCron, updateCron } from "@/api/cron"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
@@ -9,7 +10,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { import {
Form, Form,
FormControl, FormControl,
@@ -18,6 +18,8 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -25,28 +27,26 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { ScrollArea } from "@/components/ui/scroll-area"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { ModelCron } from "@/types"
import { useState } from "react"
import { KeyedMutator } from "swr"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { createCron, updateCron } from "@/api/cron"
import { asOptionalField } from "@/lib/utils"
import { cronTypes, cronCoverageTypes } from "@/types"
import { Textarea } from "./ui/textarea"
import { useServer } from "@/hooks/useServer"
import { useNotification } from "@/hooks/useNotfication" import { useNotification } from "@/hooks/useNotfication"
import { MultiSelect } from "./xui/multi-select" import { useServer } from "@/hooks/useServer"
import { Combobox } from "./ui/combobox" import { asOptionalField } from "@/lib/utils"
import { ModelCron } from "@/types"
import { cronCoverageTypes, cronTypes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { KeyedMutator } from "swr"
import { z } from "zod"
import { useTranslation } from "react-i18next"; import { Combobox } from "./ui/combobox"
import { Textarea } from "./ui/textarea"
import { MultiSelect } from "./xui/multi-select"
interface CronCardProps { interface CronCardProps {
data?: ModelCron; data?: ModelCron
mutate: KeyedMutator<ModelCron[]>; mutate: KeyedMutator<ModelCron[]>
} }
const cronFormSchema = z.object({ const cronFormSchema = z.object({
@@ -58,61 +58,58 @@ const cronFormSchema = z.object({
cover: z.coerce.number().int(), cover: z.coerce.number().int(),
push_successful: asOptionalField(z.boolean()), push_successful: asOptionalField(z.boolean()),
notification_group_id: z.coerce.number().int(), notification_group_id: z.coerce.number().int(),
}); })
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<z.infer<typeof cronFormSchema>>({
resolver: zodResolver(cronFormSchema), resolver: zodResolver(cronFormSchema),
defaultValues: data ? data : { defaultValues: data
name: "", ? data
task_type: 0, : {
scheduler: "", name: "",
servers: [], task_type: 0,
cover: 0, scheduler: "",
notification_group_id: 0, servers: [],
}, cover: 0,
notification_group_id: 0,
},
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
} },
}) })
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof cronFormSchema>) => { const onSubmit = async (values: z.infer<typeof cronFormSchema>) => {
data?.id ? await updateCron(data.id, values) : await createCron(values); data?.id ? await updateCron(data.id, values) : await createCron(values)
setOpen(false); setOpen(false)
await mutate(); await mutate()
form.reset(); form.reset()
} }
const { servers } = useServer(); const { servers } = useServer()
const serverList = servers?.map(s => ({ const serverList = servers?.map((s) => ({
value: `${s.id}`, value: `${s.id}`,
label: s.name, label: s.name,
})) || [{ value: "", label: "" }]; })) || [{ value: "", label: "" }]
const { notifierGroup } = useNotification(); const { notifierGroup } = useNotification()
const ngroupList = notifierGroup?.map(ng => ({ const ngroupList = notifierGroup?.map((ng) => ({
value: `${ng.group.id}`, value: `${ng.group.id}`,
label: ng.group.name, label: ng.group.name,
})) || [{ value: "", label: "" }]; })) || [{ value: "", label: "" }]
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
{data {data ? <IconButton variant="outline" icon="edit" /> : <IconButton icon="plus" />}
?
<IconButton variant="outline" icon="edit" />
:
<IconButton icon="plus" />
}
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="sm:max-w-xl">
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3"> <ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1"> <div className="items-center mx-1">
<DialogHeader> <DialogHeader>
<DialogTitle>{data?t("EditTask"):t("CreateTask")}</DialogTitle> <DialogTitle>{data ? t("EditTask") : t("CreateTask")}</DialogTitle>
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
@@ -124,10 +121,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
<FormItem> <FormItem>
<FormLabel>{t("Name")}</FormLabel> <FormLabel>{t("Name")}</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="My Task" {...field} />
placeholder="My Task"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -139,7 +133,10 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("Type")}</FormLabel> <FormLabel>{t("Type")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}> <Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select task type" /> <SelectValue placeholder="Select task type" />
@@ -147,7 +144,9 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{Object.entries(cronTypes).map(([k, v]) => ( {Object.entries(cronTypes).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem> <SelectItem key={k} value={k}>
{v}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -160,7 +159,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
name="scheduler" name="scheduler"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("CronExpression") }</FormLabel> <FormLabel>{t("CronExpression")}</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="0 0 0 3 * * (At 3 AM)" placeholder="0 0 0 3 * * (At 3 AM)"
@@ -178,10 +177,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
<FormItem> <FormItem>
<FormLabel>{t("Command")}</FormLabel> <FormLabel>{t("Command")}</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea className="resize-y" {...field} />
className="resize-y"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -193,16 +189,23 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("Coverage")}</FormLabel> <FormLabel>{t("Coverage")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}> <Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{Object.entries(cronCoverageTypes).map(([k, v]) => ( {Object.entries(cronCoverageTypes).map(
<SelectItem key={k} value={k}>{v}</SelectItem> ([k, v]) => (
))} <SelectItem key={k} value={k}>
{v}
</SelectItem>
),
)}
</SelectContent> </SelectContent>
</Select> </Select>
<FormMessage /> <FormMessage />
@@ -218,9 +221,9 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
<FormControl> <FormControl>
<MultiSelect <MultiSelect
options={serverList} options={serverList}
onValueChange={e => { onValueChange={(e) => {
const arr = e.map(Number); const arr = e.map(Number)
field.onChange(arr); field.onChange(arr)
}} }}
defaultValue={field.value?.map(String)} defaultValue={field.value?.map(String)}
/> />
@@ -253,7 +256,9 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
{t("Close")} {t("Close")}
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" className="my-2">{t("Confirm")}</Button> <Button type="submit" className="my-2">
{t("Confirm")}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>

View File

@@ -1,4 +1,6 @@
import { createDDNSProfile, updateDDNSProfile } from "@/api/ddns"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
@@ -9,14 +11,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { import {
Form, Form,
FormControl, FormControl,
@@ -25,28 +19,34 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { ScrollArea } from "@/components/ui/scroll-area" import { Input } from "@/components/ui/input"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { ModelDDNSProfile } from "@/types"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { conv } from "@/lib/utils" import { ScrollArea } from "@/components/ui/scroll-area"
import { useState } from "react" import {
import { KeyedMutator } from "swr" Select,
import { asOptionalField } from "@/lib/utils" SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { ddnsTypes, ddnsRequestTypes } from "@/types" import { conv } from "@/lib/utils"
import { createDDNSProfile, updateDDNSProfile } from "@/api/ddns" import { asOptionalField } from "@/lib/utils"
import { ModelDDNSProfile } from "@/types"
import { ddnsRequestTypes, ddnsTypes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { KeyedMutator } from "swr"
import { z } from "zod"
import { Textarea } from "./ui/textarea" import { Textarea } from "./ui/textarea"
import { useTranslation } from "react-i18next";
interface DDNSCardProps { interface DDNSCardProps {
data?: ModelDDNSProfile; data?: ModelDDNSProfile
providers: string[]; providers: string[]
mutate: KeyedMutator<ModelDDNSProfile[]>; mutate: KeyedMutator<ModelDDNSProfile[]>
} }
const ddnsFormSchema = z.object({ const ddnsFormSchema = z.object({
@@ -63,47 +63,44 @@ const ddnsFormSchema = z.object({
webhook_request_type: asOptionalField(z.coerce.number().int().min(1).max(255)), webhook_request_type: asOptionalField(z.coerce.number().int().min(1).max(255)),
webhook_request_body: asOptionalField(z.string()), webhook_request_body: asOptionalField(z.string()),
webhook_headers: asOptionalField(z.string()), webhook_headers: asOptionalField(z.string()),
}); })
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<z.infer<typeof ddnsFormSchema>>({
resolver: zodResolver(ddnsFormSchema), resolver: zodResolver(ddnsFormSchema),
defaultValues: data ? data : { defaultValues: data
max_retries: 3, ? data
name: "", : {
provider: "dummy", max_retries: 3,
domains: [], name: "",
}, provider: "dummy",
domains: [],
},
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
} },
}) })
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof ddnsFormSchema>) => { const onSubmit = async (values: z.infer<typeof ddnsFormSchema>) => {
data?.id ? await updateDDNSProfile(data.id, values) : await createDDNSProfile(values); data?.id ? await updateDDNSProfile(data.id, values) : await createDDNSProfile(values)
setOpen(false); setOpen(false)
await mutate(); await mutate()
form.reset(); form.reset()
} }
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
{data {data ? <IconButton variant="outline" icon="edit" /> : <IconButton icon="plus" />}
?
<IconButton variant="outline" icon="edit" />
:
<IconButton icon="plus" />
}
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="sm:max-w-xl">
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3"> <ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1"> <div className="items-center mx-1">
<DialogHeader> <DialogHeader>
<DialogTitle>{data?t("EditDDNS"):t("CreateDDNS")}</DialogTitle> <DialogTitle>{data ? t("EditDDNS") : t("CreateDDNS")}</DialogTitle>
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
@@ -115,10 +112,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
<FormItem> <FormItem>
<FormLabel>{t("Name")}</FormLabel> <FormLabel>{t("Name")}</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="My DDNS Profile" {...field} />
placeholder="My DDNS Profile"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -130,7 +124,10 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("Provider")}</FormLabel> <FormLabel>{t("Provider")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}> <Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select service type" /> <SelectValue placeholder="Select service type" />
@@ -138,7 +135,9 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{providers.map((v, i) => ( {providers.map((v, i) => (
<SelectItem key={i} value={v}>{v}</SelectItem> <SelectItem key={i} value={v}>
{v}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -151,15 +150,17 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
name="domains" name="domains"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("Domains") + t("SeparateWithComma")}</FormLabel> <FormLabel>
{t("Domains") + t("SeparateWithComma")}
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="www.example.com" placeholder="www.example.com"
{...field} {...field}
value={conv.arrToStr(field.value ?? [])} value={conv.arrToStr(field.value ?? [])}
onChange={e => { onChange={(e) => {
const arr = conv.strToArr(e.target.value); const arr = conv.strToArr(e.target.value)
field.onChange(arr); field.onChange(arr)
}} }}
/> />
</FormControl> </FormControl>
@@ -174,10 +175,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
<FormItem> <FormItem>
<FormLabel>{t("Credential")} 1</FormLabel> <FormLabel>{t("Credential")} 1</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="Token ID" {...field} />
placeholder="Token ID"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -190,10 +188,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
<FormItem> <FormItem>
<FormLabel>{t("Credential")} 2</FormLabel> <FormLabel>{t("Credential")} 2</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="Token Secret" {...field} />
placeholder="Token Secret"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -206,11 +201,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
<FormItem> <FormItem>
<FormLabel>{t("MaximumRetryAttempts")}</FormLabel> <FormLabel>{t("MaximumRetryAttempts")}</FormLabel>
<FormControl> <FormControl>
<Input <Input type="number" placeholder="3" {...field} />
type="number"
placeholder="3"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -238,7 +229,10 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Webhook {t("RequestMethod")}</FormLabel> <FormLabel>Webhook {t("RequestMethod")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}> <Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Webhook Request Method" /> <SelectValue placeholder="Webhook Request Method" />
@@ -246,7 +240,9 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{Object.entries(ddnsTypes).map(([k, v]) => ( {Object.entries(ddnsTypes).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem> <SelectItem key={k} value={k}>
{v}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -260,16 +256,23 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Webhook {t("RequestType")}</FormLabel> <FormLabel>Webhook {t("RequestType")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}> <Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Webhook Request Type" /> <SelectValue placeholder="Webhook Request Type" />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{Object.entries(ddnsRequestTypes).map(([k, v]) => ( {Object.entries(ddnsRequestTypes).map(
<SelectItem key={k} value={k}>{v}</SelectItem> ([k, v]) => (
))} <SelectItem key={k} value={k}>
{v}
</SelectItem>
),
)}
</SelectContent> </SelectContent>
</Select> </Select>
<FormMessage /> <FormMessage />
@@ -321,7 +324,9 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
<Label className="text-sm">{t("Enable")} IPv4</Label> <Label className="text-sm">
{t("Enable")} IPv4
</Label>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -339,7 +344,9 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
<Label className="text-sm">{t("Enable")} IPv6</Label> <Label className="text-sm">
{t("Enable")} IPv6
</Label>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -352,7 +359,9 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
{t("Close")} {t("Close")}
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" className="my-2">{t("Confirm")}</Button> <Button type="submit" className="my-2">
{t("Confirm")}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>

View File

@@ -1,42 +1,15 @@
import { useEffect, useState, useRef, HTMLAttributes } from "react"
import {
Sheet,
SheetTrigger,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription,
} from "./xui/overlayless-sheet"
import { IconButton } from "./xui/icon-button"
import { createFM } from "@/api/fm" import { createFM } from "@/api/fm"
import { ModelCreateFMResponse, FMEntry, FMOpcode, FMIdentifier, FMWorkerData, FMWorkerOpcode } from "@/types"
import { toast } from "sonner"
import { ColumnDef } from "@tanstack/react-table"
import { Folder, File } from "lucide-react"
import { copyToClipboard, fm, formatPath, fmWorker as worker } from "@/lib/utils"
import { import {
AlertDialog, AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent, AlertDialogContent,
AlertDialogDescription, AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
AlertDialogFooter,
AlertDialogCancel,
AlertDialogAction,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Row, flexRender } from "@tanstack/react-table"
import { TableRow, TableCell } from "./ui/table"
import { DataTable } from "./xui/virtulized-data-table"
import { Input } from "@/components/ui/input"
import { Filepath } from "./xui/filepath"
import { useMediaQuery } from "@/hooks/useMediaQuery"
import { import {
Drawer, Drawer,
DrawerContent, DrawerContent,
@@ -44,49 +17,80 @@ import {
DrawerTitle, DrawerTitle,
DrawerTrigger, DrawerTrigger,
} from "@/components/ui/drawer" } from "@/components/ui/drawer"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { useMediaQuery } from "@/hooks/useMediaQuery"
import { copyToClipboard, fm, formatPath, fmWorker as worker } from "@/lib/utils"
import {
FMEntry,
FMIdentifier,
FMOpcode,
FMWorkerData,
FMWorkerOpcode,
ModelCreateFMResponse,
} from "@/types"
import { ColumnDef } from "@tanstack/react-table"
import { Row, flexRender } from "@tanstack/react-table"
import { File, Folder } from "lucide-react"
import { HTMLAttributes, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { useTranslation } from "react-i18next"; import { TableCell, TableRow } from "./ui/table"
import { Filepath } from "./xui/filepath"
import { IconButton } from "./xui/icon-button"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "./xui/overlayless-sheet"
import { DataTable } from "./xui/virtulized-data-table"
interface FMProps { interface FMProps {
wsUrl: string; wsUrl: string
} }
const arraysEqual = (a: Uint8Array, b: Uint8Array) => { const arraysEqual = (a: Uint8Array, b: Uint8Array) => {
if (a.length !== b.length) return false; if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) { for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false; if (a[i] !== b[i]) return false
} }
return true; return true
} }
const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl, ...props }) => { const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl, ...props }) => {
const { t } = useTranslation(); const { t } = useTranslation()
const fmRef = useRef<HTMLDivElement>(null); const fmRef = useRef<HTMLDivElement>(null)
const wsRef = useRef<WebSocket | null>(null); const wsRef = useRef<WebSocket | null>(null)
useEffect(() => { useEffect(() => {
return () => { return () => {
wsRef.current?.close(); wsRef.current?.close()
}; }
}, []); }, [])
const [dOpen, setdOpen] = useState(false); const [dOpen, setdOpen] = useState(false)
const [uOpen, setuOpen] = useState(false); const [uOpen, setuOpen] = useState(false)
const columns: ColumnDef<FMEntry>[] = [ const columns: ColumnDef<FMEntry>[] = [
{ {
id: "type", id: "type",
header: () => <span>{t("Type")}</span>, header: () => <span>{t("Type")}</span>,
accessorFn: row => row.type, accessorFn: (row) => row.type,
cell: ({ row }) => ( cell: ({ row }) => (row.original.type == 0 ? <File size={24} /> : <Folder size={24} />),
row.original.type == 0 ? <File size={24} /> : <Folder size={24} />
),
}, },
{ {
header: () => <span>{t("Name")}</span>, header: () => <span>{t("Name")}</span>,
id: "name", id: "name",
accessorFn: row => row.name, accessorFn: (row) => row.name,
cell: ({ row }) => ( cell: ({ row }) => (
<div className="max-w-48 text-sm whitespace-normal break-words"> <div className="max-w-48 text-sm whitespace-normal break-words">
{row.original.name} {row.original.name}
@@ -99,24 +103,26 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
id: "download", id: "download",
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
<IconButton variant="ghost" icon="download" onClick={ <IconButton
() => { variant="ghost"
if (!dOpen) setdOpen(true); icon="download"
downloadFile(row.original.name); onClick={() => {
} if (!dOpen) setdOpen(true)
} /> downloadFile(row.original.name)
}}
/>
) )
}, },
} },
] ]
const tableRowComponent = (rows: Row<FMEntry>[]) => const tableRowComponent = (rows: Row<FMEntry>[]) =>
function getTableRow(props: HTMLAttributes<HTMLTableRowElement>) { function getTableRow(props: HTMLAttributes<HTMLTableRowElement>) {
// @ts-expect-error data-index is a valid attribute // @ts-expect-error data-index is a valid attribute
const index = props["data-index"]; const index = props["data-index"]
const row = rows[index]; const row = rows[index]
if (!row) return null; if (!row) return null
return ( return (
<TableRow <TableRow
@@ -124,7 +130,7 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
data-state={row.getIsSelected() && "selected"} data-state={row.getIsSelected() && "selected"}
onClick={() => { onClick={() => {
if (row.original.type === 1) { if (row.original.type === 1) {
setPath(`${currentPath}/${row.original.name}`); setPath(`${currentPath}/${row.original.name}`)
} }
}} }}
className={row.original.type === 1 ? "cursor-pointer" : "cursor-default"} className={row.original.type === 1 ? "cursor-pointer" : "cursor-default"}
@@ -136,155 +142,163 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
); )
}; }
const [fmEntires, setFMEntries] = useState<FMEntry[]>([]); const [fmEntires, setFMEntries] = useState<FMEntry[]>([])
const firstChunk = useRef(true); const firstChunk = useRef(true)
const handleReady = useRef(false); const handleReady = useRef(false)
const currentBasename = useRef('temp'); const currentBasename = useRef("temp")
const waitForHandleReady = async () => { const waitForHandleReady = async () => {
while (!handleReady.current) { while (!handleReady.current) {
await new Promise(resolve => setTimeout(resolve, 10)); await new Promise((resolve) => setTimeout(resolve, 10))
} }
}; }
worker.onmessage = async (event: MessageEvent<FMWorkerData>) => { worker.onmessage = async (event: MessageEvent<FMWorkerData>) => {
switch (event.data.type) { switch (event.data.type) {
case FMWorkerOpcode.Error: { case FMWorkerOpcode.Error: {
console.error('Error from worker', event.data.error); console.error("Error from worker", event.data.error)
break; break
} }
case FMWorkerOpcode.Progress: { case FMWorkerOpcode.Progress: {
handleReady.current = true; handleReady.current = true
break; break
} }
case FMWorkerOpcode.Result: { case FMWorkerOpcode.Result: {
handleReady.current = false; handleReady.current = false
if (event.data.blob && event.data.fileName) { if (event.data.blob && event.data.fileName) {
const url = URL.createObjectURL(event.data.blob); const url = URL.createObjectURL(event.data.blob)
const anchor = document.createElement('a'); const anchor = document.createElement("a")
anchor.href = url; anchor.href = url
anchor.download = event.data.fileName; anchor.download = event.data.fileName
anchor.click(); anchor.click()
URL.revokeObjectURL(url); URL.revokeObjectURL(url)
} }
firstChunk.current = true; firstChunk.current = true
if (dOpen) setdOpen(false); if (dOpen) setdOpen(false)
break; break
} }
} }
} }
const [currentPath, setPath] = useState(''); const [currentPath, setPath] = useState("")
useEffect(() => { useEffect(() => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
listFile(); listFile()
} }
}, [wsRef.current, currentPath]) }, [wsRef.current, currentPath])
useEffect(() => { useEffect(() => {
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl)
wsRef.current = ws; wsRef.current = ws
ws.binaryType = 'arraybuffer'; ws.binaryType = "arraybuffer"
ws.onopen = () => { ws.onopen = () => {
listFile(); listFile()
} }
ws.onclose = (e) => { ws.onclose = (e) => {
console.log('WebSocket connection closed:', e); console.log("WebSocket connection closed:", e)
} }
ws.onerror = (e) => { ws.onerror = (e) => {
console.error(e); console.error(e)
toast("Websocket" + " " + t("Error"), { toast("Websocket" + " " + t("Error"), {
description: t("Results.UnExpectedError"), description: t("Results.UnExpectedError"),
}) })
} }
ws.onmessage = async (e) => { ws.onmessage = async (e) => {
try { try {
const buf: ArrayBufferLike = e.data; const buf: ArrayBufferLike = e.data
if (firstChunk.current) { if (firstChunk.current) {
const identifier = new Uint8Array(buf, 0, 4); const identifier = new Uint8Array(buf, 0, 4)
if (arraysEqual(identifier, FMIdentifier.file)) { if (arraysEqual(identifier, FMIdentifier.file)) {
worker.postMessage({ operation: 1, arrayBuffer: buf, fileName: currentBasename.current }); worker.postMessage({
firstChunk.current = false; operation: 1,
arrayBuffer: buf,
fileName: currentBasename.current,
})
firstChunk.current = false
} else if (arraysEqual(identifier, FMIdentifier.fileName)) { } else if (arraysEqual(identifier, FMIdentifier.fileName)) {
const { path, fmList } = await fm.parseFMList(buf); const { path, fmList } = await fm.parseFMList(buf)
setPath(path); setPath(path)
setFMEntries(fmList); setFMEntries(fmList)
} else if (arraysEqual(identifier, FMIdentifier.error)) { } else if (arraysEqual(identifier, FMIdentifier.error)) {
const errBytes = buf.slice(4); const errBytes = buf.slice(4)
const errMsg = new TextDecoder('utf-8').decode(errBytes); const errMsg = new TextDecoder("utf-8").decode(errBytes)
throw new Error(errMsg); throw new Error(errMsg)
} else if (arraysEqual(identifier, FMIdentifier.complete)) { } else if (arraysEqual(identifier, FMIdentifier.complete)) {
// Upload completed // Upload completed
if (uOpen) setuOpen(false); if (uOpen) setuOpen(false)
listFile(); listFile()
} else { } else {
throw new Error(t("Results.UnknownIdentifier")); throw new Error(t("Results.UnknownIdentifier"))
} }
} else { } else {
await waitForHandleReady(); await waitForHandleReady()
worker.postMessage({ operation: 2, arrayBuffer: buf, fileName: currentBasename.current }); worker.postMessage({
operation: 2,
arrayBuffer: buf,
fileName: currentBasename.current,
})
} }
} catch (error) { } catch (error) {
console.error('Error processing received data:', error); console.error("Error processing received data:", error)
toast("FM" + " " + t("Error"), { toast("FM" + " " + t("Error"), {
description: t("Results.UnExpectedError"), description: t("Results.UnExpectedError"),
}) })
if (dOpen) setdOpen(false); if (dOpen) setdOpen(false)
if (uOpen) setuOpen(false); if (uOpen) setuOpen(false)
} }
} }
}, [wsUrl]) }, [wsUrl])
let listFile = () => { const listFile = () => {
const prefix = new Int8Array([FMOpcode.List]); const prefix = new Int8Array([FMOpcode.List])
const pathMsg = new TextEncoder().encode(currentPath); const pathMsg = new TextEncoder().encode(currentPath)
const msg = new Int8Array(prefix.length + pathMsg.length); const msg = new Int8Array(prefix.length + pathMsg.length)
msg.set(prefix); msg.set(prefix)
msg.set(pathMsg, prefix.length); msg.set(pathMsg, prefix.length)
wsRef.current?.send(msg); wsRef.current?.send(msg)
} }
const downloadFile = (basename: string) => { const downloadFile = (basename: string) => {
currentBasename.current = basename; currentBasename.current = basename
const prefix = new Int8Array([FMOpcode.Download]); const prefix = new Int8Array([FMOpcode.Download])
const filePathMessage = new TextEncoder().encode(`${currentPath}/${basename}`); const filePathMessage = new TextEncoder().encode(`${currentPath}/${basename}`)
const msg = new Int8Array(prefix.length + filePathMessage.length); const msg = new Int8Array(prefix.length + filePathMessage.length)
msg.set(prefix); msg.set(prefix)
msg.set(filePathMessage, prefix.length); msg.set(filePathMessage, prefix.length)
wsRef.current?.send(msg); wsRef.current?.send(msg)
} }
const uploadFile = async (file: File) => { const uploadFile = async (file: File) => {
const chunkSize = 1048576; // 1MB chunk const chunkSize = 1048576 // 1MB chunk
let offset = 0; let offset = 0
// Send header // Send header
const header = fm.buildUploadHeader({ path: currentPath, file: file }); const header = fm.buildUploadHeader({ path: currentPath, file: file })
wsRef.current?.send(header); wsRef.current?.send(header)
// Send data chunks // Send data chunks
while (offset < file.size) { while (offset < file.size) {
const chunk = file.slice(offset, offset + chunkSize); const chunk = file.slice(offset, offset + chunkSize)
const arrayBuffer = await fm.readFileAsArrayBuffer(chunk); const arrayBuffer = await fm.readFileAsArrayBuffer(chunk)
if (arrayBuffer) wsRef.current?.send(arrayBuffer); if (arrayBuffer) wsRef.current?.send(arrayBuffer)
offset += chunkSize; offset += chunkSize
} }
} }
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null)
const [gotoPath, setGotoPath] = useState(''); const [gotoPath, setGotoPath] = useState("")
return ( return (
<div ref={fmRef} {...props}> <div ref={fmRef} {...props}>
<div className="flex justify-center items-center gap-4"> <div className="flex justify-center items-center gap-4">
@@ -294,45 +308,72 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
<IconButton variant="ghost" icon="menu" /> <IconButton variant="ghost" icon="menu" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem onClick={listFile}>{t('Refresh')}</DropdownMenuItem> <DropdownMenuItem onClick={listFile}>{t("Refresh")}</DropdownMenuItem>
<DropdownMenuItem onClick={ <DropdownMenuItem
async () => { onClick={async () => {
await copyToClipboard(formatPath(currentPath)); try {
} await copyToClipboard(formatPath(currentPath))
}>{t("CopyPath")}</DropdownMenuItem> } catch (error: any) {
toast("FM" + " " + t("Error"), {
description: error.message,
})
console.log("copy error: ", error)
}
}}
>
{t("CopyPath")}
</DropdownMenuItem>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<DropdownMenuItem>{t('Goto')}</DropdownMenuItem> <DropdownMenuItem>{t("Goto")}</DropdownMenuItem>
</AlertDialogTrigger> </AlertDialogTrigger>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>{t('Goto')}</AlertDialogTitle> <AlertDialogTitle>{t("Goto")}</AlertDialogTitle>
<AlertDialogDescription /> <AlertDialogDescription />
</AlertDialogHeader> </AlertDialogHeader>
<Input className="mb-1" placeholder="Path" value={gotoPath} onChange={(e) => { setGotoPath(e.target.value) }} /> <Input
className="mb-1"
placeholder="Path"
value={gotoPath}
onChange={(e) => {
setGotoPath(e.target.value)
}}
/>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel> <AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction onClick={() => { setPath(gotoPath) }}>{t("Confirm")}</AlertDialogAction> <AlertDialogAction
onClick={() => {
setPath(gotoPath)
}}
>
{t("Confirm")}
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
<h1 className="text-base">{t("FileManager")}</h1> <h1 className="text-base">{t("FileManager")}</h1>
<div className="ml-auto"> <div className="ml-auto">
<input ref={fileInputRef} type="file" className="hidden" onChange={ <input
async (e) => { ref={fileInputRef}
const files = e.target.files; type="file"
className="hidden"
onChange={async (e) => {
const files = e.target.files
if (files && files.length > 0) { if (files && files.length > 0) {
if (!uOpen) setuOpen(true); if (!uOpen) setuOpen(true)
await uploadFile(files[0]); await uploadFile(files[0])
} }
} }}
} /> />
<IconButton icon="upload" variant="ghost" onClick={ <IconButton
() => { icon="upload"
if (fileInputRef.current) fileInputRef.current.click(); variant="ghost"
} onClick={() => {
} /> if (fileInputRef.current) fileInputRef.current.click()
}}
/>
</div> </div>
</div> </div>
<Filepath path={currentPath} setPath={setPath} /> <Filepath path={currentPath} setPath={setPath} />
@@ -354,83 +395,83 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
</AlertDialog> </AlertDialog>
<DataTable columns={columns} data={fmEntires} rowComponent={tableRowComponent} /> <DataTable columns={columns} data={fmEntires} rowComponent={tableRowComponent} />
</div> </div>
); )
} }
export const FMCard = ({ id }: { id?: string }) => { export const FMCard = ({ id }: { id?: string }) => {
const { t } = useTranslation(); const { t } = useTranslation()
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const [fm, setFM] = useState<ModelCreateFMResponse | null>(null); const [fm, setFM] = useState<ModelCreateFMResponse | null>(null)
const [init, setInit] = useState(false); const [init, setInit] = useState(false)
const isDesktop = useMediaQuery("(min-width: 640px)"); const isDesktop = useMediaQuery("(min-width: 640px)")
const fetchFM = async () => { const fetchFM = async () => {
if (id) { if (id) {
try { try {
setInit(false); setInit(false)
const createdFM = await createFM(id); const createdFM = await createFM(id)
setFM(createdFM); setFM(createdFM)
} catch (e) { } catch (e) {
toast(t("Error"), { toast(t("Error"), {
description: t("Results.UnExpectedError"), description: t("Results.UnExpectedError"),
}) })
console.error("fetch error", e); console.error("fetch error", e)
return; return
} }
setInit(true); setInit(true)
} }
} }
return (isDesktop ? return isDesktop ? (
( <Sheet
<Sheet modal={false}
modal={false} open={open}
open={open} onOpenChange={(isOpen) => {
onOpenChange={(isOpen) => { if (isOpen) setOpen(true); }} if (isOpen) setOpen(true)
> }}
<SheetTrigger asChild> >
<IconButton icon="folder-closed" onClick={fetchFM} /> <SheetTrigger asChild>
</SheetTrigger> <IconButton icon="folder-closed" onClick={fetchFM} />
<SheetContent </SheetTrigger>
setOpen={setOpen} <SheetContent setOpen={setOpen} className="min-w-[35%]">
className="min-w-[35%]" <div className="overflow-auto">
> <SheetTitle />
<div className="overflow-auto"> <SheetHeader className="pb-2">
<SheetTitle /> <SheetDescription />
<SheetHeader className="pb-2"> </SheetHeader>
<SheetDescription /> {fm?.session_id && init ? (
</SheetHeader> <FMComponent
{fm?.session_id && init className="p-1 space-y-5"
? wsUrl={`/api/v1/ws/file/${fm.session_id}`}
<FMComponent className="p-1 space-y-5" wsUrl={`/api/v1/ws/file/${fm.session_id}`} /> />
: ) : (
<p>{t("Results.TheServerDoesNotOnline")}</p> <p>{t("Results.TheServerDoesNotOnline")}</p>
} )}
</div> </div>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
) ) : (
: ( <Drawer>
<Drawer> <DrawerTrigger asChild>
<DrawerTrigger asChild> <IconButton icon="folder-closed" onClick={fetchFM} />
<IconButton icon="folder-closed" onClick={fetchFM} /> </DrawerTrigger>
</DrawerTrigger> <DrawerContent className="min-h-[60%] p-4">
<DrawerContent className="min-h-[60%] p-4"> <div className="overflow-auto">
<div className="overflow-auto"> <DrawerTitle />
<DrawerTitle /> <DrawerHeader className="pb-2">
<DrawerHeader className="pb-2"> <SheetDescription />
<SheetDescription /> </DrawerHeader>
</DrawerHeader> {fm?.session_id && init ? (
{fm?.session_id && init <FMComponent
? className="p-1 space-y-5"
<FMComponent className="p-1 space-y-5" wsUrl={`/api/v1/ws/file/${fm.session_id}`} /> wsUrl={`/api/v1/ws/file/${fm.session_id}`}
: />
<p>{t("Results.TheServerDoesNotOnline")}</p> ) : (
} <p>{t("Results.TheServerDoesNotOnline")}</p>
</div> )}
</DrawerContent> </div>
</Drawer> </DrawerContent>
) </Drawer>
) )
} }

View File

@@ -1,15 +1,10 @@
import { import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
Tabs, import { useTranslation } from "react-i18next"
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
import { Link, useLocation } from "react-router-dom" import { Link, useLocation } from "react-router-dom"
import { useTranslation } from "react-i18next";
export const GroupTab = ({ className }: { className?: string }) => { export const GroupTab = ({ className }: { className?: string }) => {
const { t } = useTranslation(); const { t } = useTranslation()
const location = useLocation(); const location = useLocation()
return ( return (
<Tabs defaultValue={location.pathname} className={className}> <Tabs defaultValue={location.pathname} className={className}>

View File

@@ -1,5 +1,3 @@
import { buttonVariants } from "@/components/ui/button";
import { IconButton } from "@/components/xui/icon-button";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -11,34 +9,48 @@ import {
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog"
import { KeyedMutator } from "swr"; import { buttonVariants } from "@/components/ui/button"
import { IconButton } from "@/components/xui/icon-button"
import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
import { KeyedMutator } from "swr"
import { useTranslation } from "react-i18next";
interface ButtonGroupProps<E, U> { interface ButtonGroupProps<E, U> {
className?: string; className?: string
children?: React.ReactNode; children?: React.ReactNode
delete: { fn: (id: E[]) => Promise<void>, id: E[], mutate: KeyedMutator<U> }; delete: { fn: (id: E[]) => Promise<void>; id: E[]; mutate: KeyedMutator<U> }
} }
export function HeaderButtonGroup<E, U>({ className, children, delete: { fn, id, mutate } }: ButtonGroupProps<E, U>) { export function HeaderButtonGroup<E, U>({
className,
children,
delete: { fn, id, mutate },
}: ButtonGroupProps<E, U>) {
const { t } = useTranslation()
const handleDelete = async () => { const handleDelete = async () => {
await fn(id); try {
await mutate(); await fn(id)
} catch (error: any) {
toast(t("Error"), {
description: error.message,
})
}
await mutate()
} }
const { t } = useTranslation();
return ( return (
<div className={className}> <div className={className}>
{id.length < 1 ? ( {id.length < 1 ? (
<> <>
<IconButton variant="destructive" icon="trash" onClick={() => { <IconButton
toast(t("Error"), { variant="destructive"
description: t("Results.NoRowsAreSelected") icon="trash"
}); onClick={() => {
}} /> toast(t("Error"), {
description: t("Results.NoRowsAreSelected"),
})
}}
/>
{children} {children}
</> </>
) : ( ) : (
@@ -56,7 +68,12 @@ export function HeaderButtonGroup<E, U>({ className, children, delete: { fn, id,
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel> <AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction className={buttonVariants({ variant: "destructive" })} onClick={handleDelete}>{t("Confirm")}</AlertDialogAction> <AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={handleDelete}
>
{t("Confirm")}
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>

View File

@@ -1,20 +1,4 @@
import { import { ModeToggle } from "@/components/mode-toggle"
NavigationMenu,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
import { ModeToggle } from "@/components/mode-toggle";
import { Card } from "./ui/card";
import { useMainStore } from "@/hooks/useMainStore";
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
import { NzNavigationMenuLink } from "./xui/navigation-menu";
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger } from "./ui/dropdown-menu";
import { LogOut, Settings, User2 } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useMediaQuery } from "@/hooks/useMediaQuery";
import { import {
Drawer, Drawer,
DrawerClose, DrawerClose,
@@ -25,13 +9,38 @@ import {
DrawerTitle, DrawerTitle,
DrawerTrigger, DrawerTrigger,
} from "@/components/ui/drawer" } from "@/components/ui/drawer"
import { Button } from "./ui/button"; import {
import { IconButton } from "./xui/icon-button"; NavigationMenu,
import { useState } from "react"; NavigationMenuItem,
NavigationMenuLink,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
import { useAuth } from "@/hooks/useAuth"
import { useMainStore } from "@/hooks/useMainStore"
import { useMediaQuery } from "@/hooks/useMediaQuery"
import i18next from "i18next"
import { LogOut, Settings, User2 } from "lucide-react"
import { DateTime } from "luxon"
import { useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { Link, useLocation, useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"; import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"
import { Button } from "./ui/button"
import { Card } from "./ui/card"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "./ui/dropdown-menu"
import { IconButton } from "./xui/icon-button"
import { NzNavigationMenuLink } from "./xui/navigation-menu"
import i18next from "i18next";
const pages = [ const pages = [
{ href: "/dashboard", label: i18next.t("Server") }, { href: "/dashboard", label: i18next.t("Server") },
{ href: "/dashboard/service", label: i18next.t("Service") }, { href: "/dashboard/service", label: i18next.t("Service") },
@@ -43,209 +52,329 @@ const pages = [
] ]
export default function Header() { export default function Header() {
const { t } = useTranslation(); const { t } = useTranslation()
const { logout } = useAuth(); const { logout } = useAuth()
const profile = useMainStore(store => store.profile); const profile = useMainStore((store) => store.profile)
const location = useLocation(); const location = useLocation()
const isDesktop = useMediaQuery("(min-width: 890px)") const isDesktop = useMediaQuery("(min-width: 890px)")
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false)
const navigate = useNavigate(); const navigate = useNavigate()
return ( return isDesktop ? (
isDesktop ? ( <header className="flex pt-8 px-4 overflow-x-auto dark:bg-black/40 bg-muted border-b-[1px]">
<header className="h-16 flex items-center border-b-2 px-4 overflow-x-auto"> <NavigationMenu className="flex flex-col items-start max-w-5xl mx-auto">
<NavigationMenu className="sm:max-w-full"> <section className="w-full flex items-center justify-between">
<NavigationMenuList> <div className="flex justify-between items-center w-full">
<Card className="mr-1"> <NavigationMenuLink
<NavigationMenuLink asChild className={navigationMenuTriggerStyle() + ' !text-foreground'}> asChild
<Link to={profile ? "/dashboard" : '#'}><img className="h-7 mr-1" src='/dashboard/logo.svg' /> {t("nezha")}</Link> className={navigationMenuTriggerStyle() + " !text-foreground"}
</NavigationMenuLink> >
</Card> <Link to={profile ? "/dashboard" : "#"}>
<img className="h-7 mr-1" src="/dashboard/logo.svg" />
{t("nezha")}
</Link>
</NavigationMenuLink>
{ <div className="flex items-center gap-1">
profile && ( <ModeToggle />
{profile && (
<> <>
<NavigationMenuItem> <DropdownMenu
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard"} className={navigationMenuTriggerStyle()}> open={dropdownOpen}
<Link to="/dashboard">{t("Server")}</Link> onOpenChange={setDropdownOpen}
</NzNavigationMenuLink> >
</NavigationMenuItem> <DropdownMenuTrigger asChild>
<NavigationMenuItem> <Avatar className="ml-1 h-8 w-8 cursor-pointer border-foreground border-[1px]">
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/service"} className={navigationMenuTriggerStyle()}> <AvatarImage
<Link to="/dashboard/service">{t("Service")}</Link> src={
</NzNavigationMenuLink> "https://api.dicebear.com/7.x/notionists/svg?seed=" +
</NavigationMenuItem> profile.username
<NavigationMenuItem> }
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/cron"} className={navigationMenuTriggerStyle()}> alt={profile.username}
<Link to="/dashboard/cron">{t('Task')}</Link> />
</NzNavigationMenuLink> <AvatarFallback>{profile.username}</AvatarFallback>
</NavigationMenuItem> </Avatar>
<NavigationMenuItem> </DropdownMenuTrigger>
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/notification" || location.pathname === "/dashboard/alert-rule"} className={navigationMenuTriggerStyle()}> <DropdownMenuContent className="w-32">
<Link to="/dashboard/notification">{t('Notification')}</Link> <DropdownMenuLabel>
</NzNavigationMenuLink> {profile.username}
</NavigationMenuItem> </DropdownMenuLabel>
<NavigationMenuItem> <DropdownMenuSeparator />
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/ddns"} className={navigationMenuTriggerStyle()}> <DropdownMenuGroup>
<Link to="/dashboard/ddns">{t('DDNS')}</Link> <DropdownMenuItem
</NzNavigationMenuLink> onClick={() => {
</NavigationMenuItem> setDropdownOpen(false)
<NavigationMenuItem> navigate("/dashboard/profile")
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/nat"} className={navigationMenuTriggerStyle()}> }}
<Link to="/dashboard/nat">{t('NATT')}</Link> className="cursor-pointer"
</NzNavigationMenuLink> >
</NavigationMenuItem> <div className="flex items-center gap-2 w-full">
<NavigationMenuItem> <User2 />
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/server-group" || location.pathname === "/dashboard/notification-group"} className={navigationMenuTriggerStyle()}> {t("Profile")}
<Link to="/dashboard/server-group">{t('Group')}</Link> </div>
</NzNavigationMenuLink> </DropdownMenuItem>
</NavigationMenuItem> <DropdownMenuItem
onClick={() => {
setDropdownOpen(false)
navigate("/dashboard/settings")
}}
className="cursor-pointer"
>
<div className="flex items-center gap-2 w-full">
<Settings />
{t("Settings")}
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={logout}
className="cursor-pointer"
>
<LogOut />
{t("Logout")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</> </>
) )}
} </div>
</NavigationMenuList>
<div className="ml-auto flex items-center gap-1">
<ModeToggle />
{
profile && <>
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
<Avatar className="ml-1 h-8 w-8 cursor-pointer border-foreground border-[1px]">
<AvatarImage src={'https://api.dicebear.com/7.x/notionists/svg?seed=' + profile.username} alt={profile.username} />
<AvatarFallback>{profile.username}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-32">
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => {
setDropdownOpen(false)
navigate("/dashboard/profile")
}}
className="cursor-pointer"
>
<div className="flex items-center gap-2 w-full">
<User2 />
{t('Profile')}
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => {
setDropdownOpen(false)
navigate("/dashboard/settings")
}}
className="cursor-pointer"
>
<div className="flex items-center gap-2 w-full">
<Settings />
{t('Settings')}
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className="cursor-pointer">
<LogOut />
{t('Logout')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
}
</div> </div>
</NavigationMenu> </section>
</header> <div className="flex mt-4 ml-4">
) <Overview />
: ( </div>
<header className="flex border-b-2 px-4 h-16"> <div className="flex mt-4 list-none">
<div className="flex max-w-max flex-1 items-center justify-center gap-2"> {profile && (
{profile && <>
<Drawer open={open} onOpenChange={setOpen}> <NavigationMenuItem>
<DrawerTrigger aria-label="Toggle Menu" asChild> <NzNavigationMenuLink
<IconButton icon="menu" variant="ghost" /> asChild
</DrawerTrigger> active={location.pathname === "/dashboard"}
<DrawerContent> className={navigationMenuTriggerStyle()}
<DrawerHeader className="text-left"> >
<DrawerTitle>{t('NavigateTo')}</DrawerTitle> <Link to="/dashboard">{t("Server")}</Link>
<DrawerDescription>{t('SelectAPageToNavigateTo')}</DrawerDescription> </NzNavigationMenuLink>
</DrawerHeader> </NavigationMenuItem>
<div className="grid gap-1 px-4"> <NavigationMenuItem>
{pages.slice(0).map((item, index) => ( <NzNavigationMenuLink
<Link asChild
key={index} active={location.pathname === "/dashboard/service"}
to={item.href ? item.href : "#"} className={navigationMenuTriggerStyle()}
className="py-1 text-sm" >
onClick={() => { setOpen(false) }} <Link to="/dashboard/service">{t("Service")}</Link>
> </NzNavigationMenuLink>
{item.label} </NavigationMenuItem>
</Link> <NavigationMenuItem>
))} <NzNavigationMenuLink
</div> asChild
<DrawerFooter> active={location.pathname === "/dashboard/cron"}
<DrawerClose asChild> className={navigationMenuTriggerStyle()}
<Button variant="outline">{t('Close')}</Button> >
</DrawerClose> <Link to="/dashboard/cron">{t("Task")}</Link>
</DrawerFooter> </NzNavigationMenuLink>
</DrawerContent> </NavigationMenuItem>
</Drawer> <NavigationMenuItem>
} <NzNavigationMenuLink
</div> asChild
<Card className="mx-2 my-2 flex justify-center items-center hover:bg-accent transition duration-200"> active={
<Link className="inline-flex w-full items-center px-4 py-2" to={profile ? "/dashboard" : '#'}><img className="h-7 mr-1" src='/dashboard/logo.svg' /> NEZHA</Link> location.pathname === "/dashboard/notification" ||
</Card> location.pathname === "/dashboard/alert-rule"
<div className="ml-auto flex items-center gap-1"> }
<ModeToggle /> className={navigationMenuTriggerStyle()}
{ >
profile && <> <Link to="/dashboard/notification">{t("Notification")}</Link>
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}> </NzNavigationMenuLink>
<DropdownMenuTrigger asChild> </NavigationMenuItem>
<Avatar className="ml-1 h-8 w-8 cursor-pointer border-foreground border-[1px]"> <NavigationMenuItem>
<AvatarImage src={'https://api.dicebear.com/7.x/notionists/svg?seed=' + profile.username} alt={profile.username} /> <NzNavigationMenuLink
<AvatarFallback>{profile.username}</AvatarFallback> asChild
</Avatar> active={location.pathname === "/dashboard/ddns"}
</DropdownMenuTrigger> className={navigationMenuTriggerStyle()}
<DropdownMenuContent className="w-56"> >
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel> <Link to="/dashboard/ddns">{t("DDNS")}</Link>
<DropdownMenuSeparator /> </NzNavigationMenuLink>
<DropdownMenuGroup> </NavigationMenuItem>
<DropdownMenuItem onClick={() => { <NavigationMenuItem>
setDropdownOpen(false) <NzNavigationMenuLink
navigate("/dashboard/profile") asChild
}} active={location.pathname === "/dashboard/nat"}
className="cursor-pointer" className={navigationMenuTriggerStyle()}
> >
<div className="flex items-center gap-2 w-full"> <Link to="/dashboard/nat">{t("NATT")}</Link>
<User2 /> </NzNavigationMenuLink>
{t('Profile')} </NavigationMenuItem>
</div> <NavigationMenuItem>
</DropdownMenuItem> <NzNavigationMenuLink
<DropdownMenuItem onClick={() => { asChild
setDropdownOpen(false) active={
navigate("/dashboard/settings") location.pathname === "/dashboard/server-group" ||
}} location.pathname === "/dashboard/notification-group"
className="cursor-pointer" }
> className={navigationMenuTriggerStyle()}
<div className="flex items-center gap-2 w-full"> >
<Settings /> <Link to="/dashboard/server-group">{t("Group")}</Link>
{t('Settings')} </NzNavigationMenuLink>
</div> </NavigationMenuItem>
</DropdownMenuItem> </>
</DropdownMenuGroup> )}
<DropdownMenuSeparator /> </div>
<DropdownMenuItem onClick={logout} className="cursor-pointer"> </NavigationMenu>
<LogOut /> </header>
{t('Logout')} ) : (
<DropdownMenuShortcut>Q</DropdownMenuShortcut> <header className="flex border-b-2 px-4 h-16">
</DropdownMenuItem> <div className="flex max-w-max flex-1 items-center justify-center gap-2">
</DropdownMenuContent> {profile && (
</DropdownMenu> <Drawer open={open} onOpenChange={setOpen}>
</> <DrawerTrigger aria-label="Toggle Menu" asChild>
} <IconButton icon="menu" variant="ghost" />
</div> </DrawerTrigger>
</header> <DrawerContent>
) <DrawerHeader className="text-left">
<DrawerTitle>{t("NavigateTo")}</DrawerTitle>
<DrawerDescription>
{t("SelectAPageToNavigateTo")}
</DrawerDescription>
</DrawerHeader>
<div className="grid gap-1 px-4">
{pages.slice(0).map((item, index) => (
<Link
key={index}
to={item.href ? item.href : "#"}
className="py-1 text-sm"
onClick={() => {
setOpen(false)
}}
>
{item.label}
</Link>
))}
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">{t("Close")}</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)}
</div>
<Card className="mx-2 my-2 flex justify-center items-center hover:bg-accent transition duration-200">
<Link
className="inline-flex w-full items-center px-4 py-2"
to={profile ? "/dashboard" : "#"}
>
<img className="h-7 mr-1" src="/dashboard/logo.svg" /> NEZHA
</Link>
</Card>
<div className="ml-auto flex items-center gap-1">
<ModeToggle />
{profile && (
<>
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
<Avatar className="ml-1 h-8 w-8 cursor-pointer border-foreground border-[1px]">
<AvatarImage
src={
"https://api.dicebear.com/7.x/notionists/svg?seed=" +
profile.username
}
alt={profile.username}
/>
<AvatarFallback>{profile.username}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() => {
setDropdownOpen(false)
navigate("/dashboard/profile")
}}
className="cursor-pointer"
>
<div className="flex items-center gap-2 w-full">
<User2 />
{t("Profile")}
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setDropdownOpen(false)
navigate("/dashboard/settings")
}}
className="cursor-pointer"
>
<div className="flex items-center gap-2 w-full">
<Settings />
{t("Settings")}
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className="cursor-pointer">
<LogOut />
{t("Logout")}
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
</header>
)
}
// https://github.com/streamich/react-use/blob/master/src/useInterval.ts
const useInterval = (callback: () => void, delay?: number | null) => {
const savedCallback = useRef<() => void>(() => {})
useEffect(() => {
savedCallback.current = callback
})
useEffect(() => {
if (delay !== null) {
const interval = setInterval(() => savedCallback.current(), delay || 0)
return () => clearInterval(interval)
}
return undefined
}, [delay])
}
function Overview() {
const { t } = useTranslation()
const profile = useMainStore((store) => store.profile)
const timeOption = DateTime.TIME_SIMPLE
timeOption.hour12 = true
const [timeString, setTimeString] = useState(
DateTime.now().setLocale("en-US").toLocaleString(timeOption),
)
useInterval(() => {
setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption))
}, 1000)
return (
<section className={"flex flex-col"}>
{profile && (
<div className="flex items-center gap-1.5">
<div className="flex gap-1.5 text-sm font-semibold">
👋 Hi, {profile?.username}
{profile?.login_ip && (
<p className="font-medium opacity-45">from {profile?.login_ip}</p>
)}
</div>
</div>
)}
{!profile && <p className="text-sm font-semibold">{t("LoginFirst")}</p>}
<div className="flex items-center gap-1.5">
<p className="text-[13px] font-medium opacity-50">{t("CurrentTime")}</p>
<p className="opacity-1 text-[13px] font-medium">{timeString}</p>
</div>
</section>
) )
} }

View File

@@ -1,46 +1,45 @@
import { Button, ButtonProps } from "@/components/ui/button"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { Button, ButtonProps } from "@/components/ui/button"
import { forwardRef, useState } from "react"
import useSettings from "@/hooks/useSetting" import useSettings from "@/hooks/useSetting"
import { ModelSettingResponse } from "@/types"
import { Check, Clipboard } from "lucide-react"
import { toast } from "sonner"
import { copyToClipboard } from "@/lib/utils" import { copyToClipboard } from "@/lib/utils"
import { ModelSettingResponse } from "@/types"
import { useTranslation } from "react-i18next"
import i18next from "i18next" import i18next from "i18next"
import { Check, Clipboard } from "lucide-react"
import { forwardRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
enum OSTypes { enum OSTypes {
Linux = 1, Linux = 1,
macOS, macOS,
Windows Windows,
} }
export const InstallCommandsMenu = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => { export const InstallCommandsMenu = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const [copy, setCopy] = useState(false); const [copy, setCopy] = useState(false)
const settings = useSettings(); const settings = useSettings()
const { t } = useTranslation(); const { t } = useTranslation()
const switchState = async (type: number) => { const switchState = async (type: number) => {
if (!copy) { if (!copy) {
try { try {
setCopy(true); setCopy(true)
if (!settings) throw new Error("Settings is not found."); if (!settings) throw new Error("Settings is not found.")
await copyToClipboard(generateCommand(type, settings) || ''); await copyToClipboard(generateCommand(type, settings) || "")
} catch (e: Error | any) { } catch (e: Error | any) {
console.error(e); console.error(e)
toast(t("Error"), { toast(t("Error"), {
description: e.message, description: e.message,
}) })
} finally { } finally {
setTimeout(() => { setTimeout(() => {
setCopy(false); setCopy(false)
}, 2 * 1000); }, 2 * 1000)
} }
} }
} }
@@ -54,32 +53,54 @@ export const InstallCommandsMenu = forwardRef<HTMLButtonElement, ButtonProps>((p
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem className="nezha-copy" onClick={async () => { switchState(OSTypes.Linux) }}>Linux</DropdownMenuItem> <DropdownMenuItem
<DropdownMenuItem className="nezha-copy" onClick={async () => { switchState(OSTypes.macOS) }}>macOS</DropdownMenuItem> className="nezha-copy"
<DropdownMenuItem className="nezha-copy" onClick={async () => { switchState(OSTypes.Windows) }}>Windows</DropdownMenuItem> onClick={async () => {
switchState(OSTypes.Linux)
}}
>
Linux
</DropdownMenuItem>
<DropdownMenuItem
className="nezha-copy"
onClick={async () => {
switchState(OSTypes.macOS)
}}
>
macOS
</DropdownMenuItem>
<DropdownMenuItem
className="nezha-copy"
onClick={async () => {
switchState(OSTypes.Windows)
}}
>
Windows
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); )
}) })
const generateCommand = (type: number, { agent_secret_key, install_host, tls }: ModelSettingResponse) => { const generateCommand = (
type: number,
if (!install_host) { agent_secret_key, install_host, tls }: ModelSettingResponse,
throw new Error(i18next.t("Results.InstallHostRequired")); ) => {
if (!install_host) throw new Error(i18next.t("Results.InstallHostRequired"))
const env = `NZ_SERVER=${install_host} NZ_TLS=${tls || false} NZ_CLIENT_SECRET=${agent_secret_key}`; const env = `NZ_SERVER=${install_host} NZ_TLS=${tls || false} NZ_CLIENT_SECRET=${agent_secret_key}`
const env_win = `$env:NZ_SERVER=\"${install_host}\";$env:NZ_TLS=\"${tls || false}\";$env:NZ_CLIENT_SECRET=\"${agent_secret_key}\";`; const env_win = `$env:NZ_SERVER=\"${install_host}\";$env:NZ_TLS=\"${tls || false}\";$env:NZ_CLIENT_SECRET=\"${agent_secret_key}\";`
switch (type) { switch (type) {
case OSTypes.Linux: case OSTypes.Linux:
case OSTypes.macOS: { case OSTypes.macOS: {
return `curl -L https://raw.githubusercontent.com/nezhahq/scripts/main/agent/install.sh -o agent.sh && chmod +x agent.sh && env ${env} ./agent.sh` return `curl -L https://raw.githubusercontent.com/nezhahq/scripts/main/agent/install.sh -o agent.sh && chmod +x agent.sh && env ${env} ./agent.sh`
} }
case OSTypes.Windows: { case OSTypes.Windows: {
return `${env_win} [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12;set-ExecutionPolicy RemoteSigned;Invoke-WebRequest https://raw.githubusercontent.com/nezhahq/scripts/main/agent/install.ps1 -OutFile C:\install.ps1;powershell.exe C:\install.ps1` return `${env_win} [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12;set-ExecutionPolicy RemoteSigned;Invoke-WebRequest https://raw.githubusercontent.com/nezhahq/scripts/main/agent/install.ps1 -OutFile C:\install.ps1;powershell.exe C:\install.ps1`
} }
default: { default: {
throw new Error(`Unknown OS: ${type}`); throw new Error(`Unknown OS: ${type}`)
} }
} }
} }

View File

@@ -1,5 +1,4 @@
import { Moon, Sun } from "lucide-react" import { Theme, useTheme } from "@/components/theme-provider"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
DropdownMenu, DropdownMenu,
@@ -7,16 +6,15 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { Theme, useTheme } from "@/components/theme-provider" import { Moon, Sun } from "lucide-react"
import { useTranslation } from "react-i18next"
import { useTranslation } from "react-i18next";
export function ModeToggle() { export function ModeToggle() {
const { t } = useTranslation(); const { t } = useTranslation()
const { setTheme } = useTheme() const { setTheme } = useTheme()
const toggleTheme = (theme: Theme) => { const toggleTheme = (theme: Theme) => {
setTheme(theme); setTheme(theme)
} }
return ( return (

View File

@@ -1,3 +1,4 @@
import { createNAT, updateNAT } from "@/api/nat"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
@@ -9,7 +10,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { import {
Form, Form,
FormControl, FormControl,
@@ -18,21 +18,20 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { ModelNAT } from "@/types"
import { useState } from "react"
import { KeyedMutator } from "swr"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { createNAT, updateNAT } from "@/api/nat" import { ModelNAT } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useTranslation } from "react-i18next"; import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { KeyedMutator } from "swr"
import { z } from "zod"
interface NATCardProps { interface NATCardProps {
data?: ModelNAT; data?: ModelNAT
mutate: KeyedMutator<ModelNAT[]>; mutate: KeyedMutator<ModelNAT[]>
} }
const natFormSchema = z.object({ const natFormSchema = z.object({
@@ -40,47 +39,44 @@ const natFormSchema = z.object({
server_id: z.coerce.number().int(), server_id: z.coerce.number().int(),
host: z.string(), host: z.string(),
domain: z.string(), domain: z.string(),
}); })
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<z.infer<typeof natFormSchema>>({
resolver: zodResolver(natFormSchema), resolver: zodResolver(natFormSchema),
defaultValues: data ? data : { defaultValues: data
name: "", ? data
server_id: 0, : {
host: "", name: "",
domain: "", server_id: 0,
}, host: "",
domain: "",
},
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
} },
}) })
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof natFormSchema>) => { const onSubmit = async (values: z.infer<typeof natFormSchema>) => {
data?.id ? await updateNAT(data.id, values) : await createNAT(values); data?.id ? await updateNAT(data.id, values) : await createNAT(values)
setOpen(false); setOpen(false)
await mutate(); await mutate()
form.reset(); form.reset()
} }
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
{data {data ? <IconButton variant="outline" icon="edit" /> : <IconButton icon="plus" />}
?
<IconButton variant="outline" icon="edit" />
:
<IconButton icon="plus" />
}
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="sm:max-w-xl">
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3"> <ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1"> <div className="items-center mx-1">
<DialogHeader> <DialogHeader>
<DialogTitle>{data?t("EditNAT"):t("CreateNAT")}</DialogTitle> <DialogTitle>{data ? t("EditNAT") : t("CreateNAT")}</DialogTitle>
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
@@ -92,10 +88,7 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
<FormItem> <FormItem>
<FormLabel>{t("Name")}</FormLabel> <FormLabel>{t("Name")}</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="My NAT Profile" {...field} />
placeholder="My NAT Profile"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -108,11 +101,7 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
<FormItem> <FormItem>
<FormLabel>{t("Server")} ID</FormLabel> <FormLabel>{t("Server")} ID</FormLabel>
<FormControl> <FormControl>
<Input <Input type="number" placeholder="1" {...field} />
type="number"
placeholder="1"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -156,7 +145,9 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
{t("Close")} {t("Close")}
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" className="my-2">{t("Confirm")}</Button> <Button type="submit" className="my-2">
{t("Confirm")}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>

View File

@@ -1,53 +1,69 @@
import { ButtonProps } from "@/components/ui/button"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { ButtonProps } from "@/components/ui/button" import { copyToClipboard } from "@/lib/utils"
import { forwardRef, useState } from "react" import { forwardRef, useState } from "react"
import { IconButton } from "./xui/icon-button" import { useTranslation } from "react-i18next"
import { toast } from "sonner"; import { toast } from "sonner"
import { useTranslation } from "react-i18next"; import { IconButton } from "./xui/icon-button"
import { copyToClipboard } from "@/lib/utils";
interface NoteMenuProps extends ButtonProps { interface NoteMenuProps extends ButtonProps {
note: { private?: string, public?: string }; note: { private?: string; public?: string }
} }
export const NoteMenu = forwardRef<HTMLButtonElement, NoteMenuProps>((props, ref) => { export const NoteMenu = forwardRef<HTMLButtonElement, NoteMenuProps>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation()
const [copy, setCopy] = useState(false); const [copy, setCopy] = useState(false)
const switchState = async (text?: string) => { const switchState = async (text?: string) => {
if (!text) { if (!text) {
toast("Warning", { toast("Warning", {
description: "You didn't have any note." description: "You didn't have any note.",
}) })
return; return
} }
if (!copy) { if (!copy) {
setCopy(true); setCopy(true)
await copyToClipboard(text); await copyToClipboard(text)
setTimeout(() => { setTimeout(() => {
setCopy(false); setCopy(false)
}, 2 * 1000); }, 2 * 1000)
} }
} }
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<IconButton {...props} ref={ref} variant="outline" size="icon" icon={ <IconButton
copy ? "check" : "clipboard" {...props}
} /> ref={ref}
variant="outline"
size="icon"
icon={copy ? "check" : "clipboard"}
/>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem onClick={() => { switchState(props.note.private) }}>{t("Private")}</DropdownMenuItem> <DropdownMenuItem
<DropdownMenuItem onClick={() => { switchState(props.note.public) }}>{t("Public")}</DropdownMenuItem> onClick={() => {
switchState(props.note.private)
}}
>
{t("Private")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
switchState(props.note.public)
}}
>
{t("Public")}
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); )
}) })

View File

@@ -1,3 +1,4 @@
import { createNotificationGroup, updateNotificationGroup } from "@/api/notification-group"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
@@ -9,7 +10,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { import {
Form, Form,
FormControl, FormControl,
@@ -18,76 +18,76 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { ModelNotificationGroupResponseItem } from "@/types"
import { useState } from "react"
import { KeyedMutator } from "swr"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { createNotificationGroup, updateNotificationGroup } from "@/api/notification-group"
import { MultiSelect } from "@/components/xui/multi-select" import { MultiSelect } from "@/components/xui/multi-select"
import { useNotification } from "@/hooks/useNotfication" import { useNotification } from "@/hooks/useNotfication"
import { ModelNotificationGroupResponseItem } from "@/types"
import { useTranslation } from "react-i18next"; import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { KeyedMutator } from "swr"
import { z } from "zod"
interface NotificationGroupCardProps { interface NotificationGroupCardProps {
data?: ModelNotificationGroupResponseItem; data?: ModelNotificationGroupResponseItem
mutate: KeyedMutator<ModelNotificationGroupResponseItem[]>; mutate: KeyedMutator<ModelNotificationGroupResponseItem[]>
} }
const notificationGroupFormSchema = z.object({ const notificationGroupFormSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
notifications: z.array(z.number()), notifications: z.array(z.number()),
}); })
export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ data, mutate }) => { export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ data, mutate }) => {
const { t } = useTranslation(); const { t } = useTranslation()
const form = useForm<z.infer<typeof notificationGroupFormSchema>>({ const form = useForm<z.infer<typeof notificationGroupFormSchema>>({
resolver: zodResolver(notificationGroupFormSchema), resolver: zodResolver(notificationGroupFormSchema),
defaultValues: data ? { defaultValues: data
name: data.group.name, ? {
notifications: data.notifications, name: data.group.name,
} : { notifications: data.notifications,
name: "", }
notifications: [], : {
}, name: "",
notifications: [],
},
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
} },
}) })
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof notificationGroupFormSchema>) => { const onSubmit = async (values: z.infer<typeof notificationGroupFormSchema>) => {
data?.group.id ? await updateNotificationGroup(data.group.id, values) : await createNotificationGroup(values); data?.group.id
setOpen(false); ? await updateNotificationGroup(data.group.id, values)
await mutate(); : await createNotificationGroup(values)
form.reset(); setOpen(false)
await mutate()
form.reset()
} }
const { notifiers } = useNotification(); const { notifiers } = useNotification()
const notifierList = notifiers?.map(n => ({ const notifierList = notifiers?.map((n) => ({
value: `${n.id}`, value: `${n.id}`,
label: n.name, label: n.name,
})) || [{ value: "", label: "" }]; })) || [{ value: "", label: "" }]
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
{data {data ? <IconButton variant="outline" icon="edit" /> : <IconButton icon="plus" />}
?
<IconButton variant="outline" icon="edit" />
:
<IconButton icon="plus" />
}
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="sm:max-w-xl">
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3"> <ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1"> <div className="items-center mx-1">
<DialogHeader> <DialogHeader>
<DialogTitle>{data ? t("EditNotifierGroup") : t("CreateNotifierGroup")}</DialogTitle> <DialogTitle>
{data ? t("EditNotifierGroup") : t("CreateNotifierGroup")}
</DialogTitle>
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
@@ -99,10 +99,7 @@ export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ da
<FormItem> <FormItem>
<FormLabel>{t("Name")}</FormLabel> <FormLabel>{t("Name")}</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="Group Name" {...field} />
placeholder="Group Name"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -116,9 +113,9 @@ export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ da
<FormLabel>{t("Notification")}</FormLabel> <FormLabel>{t("Notification")}</FormLabel>
<MultiSelect <MultiSelect
options={notifierList} options={notifierList}
onValueChange={e => { onValueChange={(e) => {
const arr = e.map(Number); const arr = e.map(Number)
field.onChange(arr); field.onChange(arr)
}} }}
defaultValue={field.value?.map(String)} defaultValue={field.value?.map(String)}
/> />
@@ -132,7 +129,9 @@ export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ da
{t("Close")} {t("Close")}
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" className="my-2">{t("Confirm")}</Button> <Button type="submit" className="my-2">
{t("Confirm")}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>

View File

@@ -1,16 +1,10 @@
import { import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
Tabs, import { useTranslation } from "react-i18next"
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
import { Link, useLocation } from "react-router-dom" import { Link, useLocation } from "react-router-dom"
import { useTranslation } from "react-i18next";
export const NotificationTab = ({ className }: { className?: string }) => { export const NotificationTab = ({ className }: { className?: string }) => {
const { t } = useTranslation(); const { t } = useTranslation()
const location = useLocation(); const location = useLocation()
return ( return (
<Tabs defaultValue={location.pathname} className={className}> <Tabs defaultValue={location.pathname} className={className}>

View File

@@ -1,4 +1,6 @@
import { createNotification, updateNotification } from "@/api/notification"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
@@ -9,14 +11,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { import {
Form, Form,
FormControl, FormControl,
@@ -25,26 +19,32 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { ScrollArea } from "@/components/ui/scroll-area" import { Input } from "@/components/ui/input"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { ModelNotification } from "@/types"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { useState } from "react" import { ScrollArea } from "@/components/ui/scroll-area"
import { KeyedMutator } from "swr" import {
import { asOptionalField } from "@/lib/utils" Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { nrequestTypes, nrequestMethods } from "@/types" import { asOptionalField } from "@/lib/utils"
import { createNotification, updateNotification } from "@/api/notification" import { ModelNotification } from "@/types"
import { nrequestMethods, nrequestTypes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { KeyedMutator } from "swr"
import { z } from "zod"
import { Textarea } from "./ui/textarea" import { Textarea } from "./ui/textarea"
import { useTranslation } from "react-i18next";
interface NotifierCardProps { interface NotifierCardProps {
data?: ModelNotification; data?: ModelNotification
mutate: KeyedMutator<ModelNotification[]>; mutate: KeyedMutator<ModelNotification[]>
} }
const notificationFormSchema = z.object({ const notificationFormSchema = z.object({
@@ -56,49 +56,48 @@ 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()),
}); })
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>>({ const form = useForm<z.infer<typeof notificationFormSchema>>({
resolver: zodResolver(notificationFormSchema), resolver: zodResolver(notificationFormSchema),
defaultValues: data ? data : { defaultValues: data
name: "", ? data
url: "", : {
request_method: 1, name: "",
request_type: 1, url: "",
request_header: "", request_method: 1,
request_body: "", request_type: 1,
}, request_header: "",
request_body: "",
},
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
} },
}) })
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof notificationFormSchema>) => { const onSubmit = async (values: z.infer<typeof notificationFormSchema>) => {
data?.id ? await updateNotification(data.id, values) : await createNotification(values); data?.id ? await updateNotification(data.id, values) : await createNotification(values)
setOpen(false); setOpen(false)
await mutate(); await mutate()
form.reset(); form.reset()
} }
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
{data {data ? <IconButton variant="outline" icon="edit" /> : <IconButton icon="plus" />}
?
<IconButton variant="outline" icon="edit" />
:
<IconButton icon="plus" />
}
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="sm:max-w-xl">
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3"> <ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1"> <div className="items-center mx-1">
<DialogHeader> <DialogHeader>
<DialogTitle>{data?t("EditNotifier"):t("CreateNotifier")}</DialogTitle> <DialogTitle>
{data ? t("EditNotifier") : t("CreateNotifier")}
</DialogTitle>
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
@@ -110,10 +109,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
<FormItem> <FormItem>
<FormLabel>{t("Name")}</FormLabel> <FormLabel>{t("Name")}</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="My Notifier" {...field} />
placeholder="My Notifier"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -126,9 +122,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
<FormItem> <FormItem>
<FormLabel>URL</FormLabel> <FormLabel>URL</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -140,16 +134,23 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("RequestMethod")}</FormLabel> <FormLabel>{t("RequestMethod")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}> <Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Request Method" /> <SelectValue placeholder="Request Method" />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{Object.entries(nrequestMethods).map(([k, v]) => ( {Object.entries(nrequestMethods).map(
<SelectItem key={k} value={k}>{v}</SelectItem> ([k, v]) => (
))} <SelectItem key={k} value={k}>
{v}
</SelectItem>
),
)}
</SelectContent> </SelectContent>
</Select> </Select>
<FormMessage /> <FormMessage />
@@ -162,7 +163,10 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("Type")}</FormLabel> <FormLabel>{t("Type")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}> <Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Request Type" /> <SelectValue placeholder="Request Type" />
@@ -170,7 +174,9 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{Object.entries(nrequestTypes).map(([k, v]) => ( {Object.entries(nrequestTypes).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem> <SelectItem key={k} value={k}>
{v}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -223,7 +229,9 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
<Label className="text-sm">{t("VerifyTLS")}</Label> <Label className="text-sm">
{t("VerifyTLS")}
</Label>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -241,7 +249,9 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
<Label className="text-sm">{t("DoNotSendTestMessage")}</Label> <Label className="text-sm">
{t("DoNotSendTestMessage")}
</Label>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -254,7 +264,9 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
{t("Close")} {t("Close")}
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" className="my-2">{t("Confirm")}</Button> <Button type="submit" className="my-2">
{t("Confirm")}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>

View File

@@ -1,3 +1,4 @@
import { getProfile, updateProfile } from "@/api/user"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
@@ -9,7 +10,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { import {
Form, Form,
FormControl, FormControl,
@@ -18,54 +18,53 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { getProfile, updateProfile } from "@/api/user"
import { useState } from "react"
import { useMainStore } from "@/hooks/useMainStore" import { useMainStore } from "@/hooks/useMainStore"
import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
import { z } from "zod"
import { useTranslation } from "react-i18next";
const profileFormSchema = z.object({ const profileFormSchema = z.object({
original_password: z.string().min(5).max(72), original_password: z.string().min(5).max(72),
new_password: z.string().min(8).max(72), new_password: z.string().min(8).max(72),
new_username: z.string().min(1).max(32), new_username: z.string().min(1).max(32),
}); })
export const ProfileCard = ({ className }: { className: string }) => { 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<z.infer<typeof profileFormSchema>>({
resolver: zodResolver(profileFormSchema), resolver: zodResolver(profileFormSchema),
defaultValues: { defaultValues: {
original_password: '', original_password: "",
new_password: '', new_password: "",
new_username: profile?.username, new_username: profile?.username,
}, },
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
} },
}) })
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof profileFormSchema>) => { const onSubmit = async (values: z.infer<typeof profileFormSchema>) => {
try { try {
await updateProfile(values); await updateProfile(values)
} catch (e) { } catch (e) {
toast(t("Error"), { toast(t("Error"), {
description: `${e}`, description: `${e}`,
}) })
return; return
} }
const profile = await getProfile(); const profile = await getProfile()
setProfile(profile); setProfile(profile)
setOpen(false); setOpen(false)
form.reset(); form.reset()
} }
return ( return (
@@ -91,10 +90,7 @@ export const ProfileCard = ({ className }: { className: string }) => {
<FormItem> <FormItem>
<FormLabel>{t("NewUsername")}</FormLabel> <FormLabel>{t("NewUsername")}</FormLabel>
<FormControl> <FormControl>
<Input <Input autoComplete="username" {...field} />
autoComplete="username"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -107,10 +103,7 @@ export const ProfileCard = ({ className }: { className: string }) => {
<FormItem> <FormItem>
<FormLabel>{t("OriginalPassword")}</FormLabel> <FormLabel>{t("OriginalPassword")}</FormLabel>
<FormControl> <FormControl>
<Input <Input autoComplete="current-password" {...field} />
autoComplete="current-password"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -123,9 +116,7 @@ export const ProfileCard = ({ className }: { className: string }) => {
<FormItem> <FormItem>
<FormLabel>{t("NewPassword")}</FormLabel> <FormLabel>{t("NewPassword")}</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -138,7 +129,9 @@ export const ProfileCard = ({ className }: { className: string }) => {
{t("Close")} {t("Close")}
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" className="my-2">{t("Confirm")}</Button> <Button type="submit" className="my-2">
{t("Confirm")}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>

View File

@@ -1,3 +1,4 @@
import { createServerGroup, updateServerGroup } from "@/api/server-group"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
@@ -9,7 +10,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { import {
Form, Form,
FormControl, FormControl,
@@ -18,76 +18,76 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { ModelServerGroupResponseItem } from "@/types"
import { useState } from "react"
import { KeyedMutator } from "swr"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { createServerGroup, updateServerGroup } from "@/api/server-group"
import { MultiSelect } from "@/components/xui/multi-select" import { MultiSelect } from "@/components/xui/multi-select"
import { useServer } from "@/hooks/useServer" import { useServer } from "@/hooks/useServer"
import { ModelServerGroupResponseItem } from "@/types"
import { useTranslation } from "react-i18next"; import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { KeyedMutator } from "swr"
import { z } from "zod"
interface ServerGroupCardProps { interface ServerGroupCardProps {
data?: ModelServerGroupResponseItem; data?: ModelServerGroupResponseItem
mutate: KeyedMutator<ModelServerGroupResponseItem[]>; mutate: KeyedMutator<ModelServerGroupResponseItem[]>
} }
const serverGroupFormSchema = z.object({ const serverGroupFormSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
servers: z.array(z.number()), servers: z.array(z.number()),
}); })
export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }) => { export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }) => {
const { t } = useTranslation(); const { t } = useTranslation()
const form = useForm<z.infer<typeof serverGroupFormSchema>>({ const form = useForm<z.infer<typeof serverGroupFormSchema>>({
resolver: zodResolver(serverGroupFormSchema), resolver: zodResolver(serverGroupFormSchema),
defaultValues: data ? { defaultValues: data
name: data.group.name, ? {
servers: data.servers, name: data.group.name,
} : { servers: data.servers,
name: "", }
servers: [], : {
}, name: "",
servers: [],
},
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
} },
}) })
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof serverGroupFormSchema>) => { const onSubmit = async (values: z.infer<typeof serverGroupFormSchema>) => {
data?.group.id ? await updateServerGroup(data.group.id, values) : await createServerGroup(values); data?.group.id
setOpen(false); ? await updateServerGroup(data.group.id, values)
await mutate(); : await createServerGroup(values)
form.reset(); setOpen(false)
await mutate()
form.reset()
} }
const { servers } = useServer(); const { servers } = useServer()
const serverList = servers?.map(s => ({ const serverList = servers?.map((s) => ({
value: `${s.id}`, value: `${s.id}`,
label: s.name, label: s.name,
})) || [{ value: "", label: "" }]; })) || [{ value: "", label: "" }]
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
{data {data ? <IconButton variant="outline" icon="edit" /> : <IconButton icon="plus" />}
?
<IconButton variant="outline" icon="edit" />
:
<IconButton icon="plus" />
}
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="sm:max-w-xl">
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3"> <ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1"> <div className="items-center mx-1">
<DialogHeader> <DialogHeader>
<DialogTitle>{data? t("EditServerGroup"):t("CreateServerGroup")}</DialogTitle> <DialogTitle>
{data ? t("EditServerGroup") : t("CreateServerGroup")}
</DialogTitle>
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
@@ -99,10 +99,7 @@ export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }
<FormItem> <FormItem>
<FormLabel>{t("Name")}</FormLabel> <FormLabel>{t("Name")}</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="Group Name" {...field} />
placeholder="Group Name"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -117,9 +114,9 @@ export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }
<FormControl> <FormControl>
<MultiSelect <MultiSelect
options={serverList} options={serverList}
onValueChange={e => { onValueChange={(e) => {
const arr = e.map(Number); const arr = e.map(Number)
field.onChange(arr); field.onChange(arr)
}} }}
defaultValue={field.value?.map(String)} defaultValue={field.value?.map(String)}
/> />
@@ -134,7 +131,9 @@ export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }
{t("Close")} {t("Close")}
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" className="my-2">{t("Confirm")}</Button> <Button type="submit" className="my-2">
{t("Confirm")}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>

View File

@@ -1,4 +1,6 @@
import { updateServer } from "@/api/server"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
@@ -9,7 +11,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { import {
Form, Form,
FormControl, FormControl,
@@ -18,25 +19,24 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { ScrollArea } from "@/components/ui/scroll-area" import { Input } from "@/components/ui/input"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { ModelServer } from "@/types"
import { updateServer } from "@/api/server"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { conv } from "@/lib/utils" import { ScrollArea } from "@/components/ui/scroll-area"
import { useState } from "react"
import { KeyedMutator } from "swr"
import { asOptionalField } from "@/lib/utils"
import { IconButton } from "@/components/xui/icon-button"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { IconButton } from "@/components/xui/icon-button"
import { conv } from "@/lib/utils"
import { asOptionalField } from "@/lib/utils"
import { ModelServer } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { KeyedMutator } from "swr"
import { z } from "zod"
interface ServerCardProps { interface ServerCardProps {
data: ModelServer; data: ModelServer
mutate: KeyedMutator<ModelServer[]>; mutate: KeyedMutator<ModelServer[]>
} }
const serverFormSchema = z.object({ const serverFormSchema = z.object({
@@ -47,25 +47,25 @@ const serverFormSchema = z.object({
hide_for_guest: asOptionalField(z.boolean()), hide_for_guest: asOptionalField(z.boolean()),
enable_ddns: asOptionalField(z.boolean()), enable_ddns: asOptionalField(z.boolean()),
ddns_profiles: asOptionalField(z.array(z.number())), ddns_profiles: asOptionalField(z.array(z.number())),
}); })
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),
defaultValues: data, defaultValues: data,
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
} },
}) })
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof serverFormSchema>) => { const onSubmit = async (values: z.infer<typeof serverFormSchema>) => {
await updateServer(data.id, values); await updateServer(data.id, values)
setOpen(false); setOpen(false)
await mutate(); await mutate()
form.reset(); form.reset()
} }
return ( return (
@@ -77,7 +77,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3"> <ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1"> <div className="items-center mx-1">
<DialogHeader> <DialogHeader>
<DialogTitle>{t("EditServer") }</DialogTitle> <DialogTitle>{t("EditServer")}</DialogTitle>
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
@@ -89,10 +89,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
<FormItem> <FormItem>
<FormLabel>{t("Name")}</FormLabel> <FormLabel>{t("Name")}</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="My Server" {...field} />
placeholder="My Server"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -105,11 +102,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
<FormItem> <FormItem>
<FormLabel>{t("Weight")}</FormLabel> <FormLabel>{t("Weight")}</FormLabel>
<FormControl> <FormControl>
<Input <Input type="number" placeholder="0" {...field} />
type="number"
placeholder="0"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -120,16 +113,20 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
name="ddns_profiles" name="ddns_profiles"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("DDNSProfiles") + t("SeparateWithComma")}</FormLabel> <FormLabel>
{t("DDNSProfiles") + t("SeparateWithComma")}
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="1,2,3" placeholder="1,2,3"
{...field} {...field}
value={conv.arrToStr(field.value || [])} value={conv.arrToStr(field.value || [])}
onChange={e => { onChange={(e) => {
console.log(field.value) console.log(field.value)
const arr = conv.strToArr(e.target.value).map(Number); const arr = conv
field.onChange(arr); .strToArr(e.target.value)
.map(Number)
field.onChange(arr)
}} }}
/> />
</FormControl> </FormControl>
@@ -148,7 +145,9 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
<Label className="text-sm">{t("Enable") + t("DDNS") }</Label> <Label className="text-sm">
{t("Enable") + t("DDNS")}
</Label>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -166,7 +165,9 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
<Label className="text-sm">{t("HideForGuest")}</Label> <Label className="text-sm">
{t("HideForGuest")}
</Label>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -180,10 +181,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
<FormItem> <FormItem>
<FormLabel>{t("Private") + t("Note")}</FormLabel> <FormLabel>{t("Private") + t("Note")}</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea className="resize-none" {...field} />
className="resize-none"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -196,10 +194,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
<FormItem> <FormItem>
<FormLabel>{t("Public") + t("Note")}</FormLabel> <FormLabel>{t("Public") + t("Note")}</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea className="resize-y" {...field} />
className="resize-y"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -211,7 +206,9 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
{t("Close")} {t("Close")}
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" className="my-2">{t("Submit")}</Button> <Button type="submit" className="my-2">
{t("Submit")}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>

View File

@@ -1,4 +1,6 @@
import { createService, updateService } from "@/api/service"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { import {
Dialog, Dialog,
DialogClose, DialogClose,
@@ -9,14 +11,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { import {
Form, Form,
FormControl, FormControl,
@@ -25,30 +19,36 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { ScrollArea } from "@/components/ui/scroll-area" import { Input } from "@/components/ui/input"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { ModelService } from "@/types"
import { createService, updateService } from "@/api/service"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { conv } from "@/lib/utils" import { ScrollArea } from "@/components/ui/scroll-area"
import { useState } from "react" import {
import { KeyedMutator } from "swr" Select,
import { asOptionalField } from "@/lib/utils" SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { serviceTypes, serviceCoverageTypes } from "@/types"
import { MultiSelect } from "./xui/multi-select"
import { Combobox } from "./ui/combobox"
import { useServer } from "@/hooks/useServer"
import { useNotification } from "@/hooks/useNotfication" import { useNotification } from "@/hooks/useNotfication"
import { useServer } from "@/hooks/useServer"
import { conv } from "@/lib/utils"
import { asOptionalField } from "@/lib/utils"
import { ModelService } from "@/types"
import { serviceCoverageTypes, serviceTypes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { KeyedMutator } from "swr"
import { z } from "zod"
import { useTranslation } from "react-i18next"; import { Combobox } from "./ui/combobox"
import { MultiSelect } from "./xui/multi-select"
interface ServiceCardProps { interface ServiceCardProps {
data?: ModelService; data?: ModelService
mutate: KeyedMutator<ModelService[]>; mutate: KeyedMutator<ModelService[]>
} }
const serviceFormSchema = z.object({ const serviceFormSchema = z.object({
@@ -68,72 +68,73 @@ const serviceFormSchema = z.object({
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),
}); })
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<z.infer<typeof serviceFormSchema>>({
resolver: zodResolver(serviceFormSchema), resolver: zodResolver(serviceFormSchema),
defaultValues: data ? { defaultValues: data
...data, ? {
skip_servers_raw: conv.recordToStrArr(data.skip_servers ? data.skip_servers : {}), ...data,
} : { skip_servers_raw: conv.recordToStrArr(data.skip_servers ? data.skip_servers : {}),
type: 1, }
cover: 0, : {
name: "", type: 1,
target: "", cover: 0,
max_latency: 0.0, name: "",
min_latency: 0.0, target: "",
duration: 30, max_latency: 0.0,
notification_group_id: 0, min_latency: 0.0,
fail_trigger_tasks: [], duration: 30,
recover_trigger_tasks: [], notification_group_id: 0,
skip_servers: {}, fail_trigger_tasks: [],
skip_servers_raw: [], recover_trigger_tasks: [],
}, skip_servers: {},
skip_servers_raw: [],
},
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
} },
}) })
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof serviceFormSchema>) => { const onSubmit = async (values: z.infer<typeof serviceFormSchema>) => {
values.skip_servers = conv.arrToRecord(values.skip_servers_raw); values.skip_servers = conv.arrToRecord(values.skip_servers_raw)
const { skip_servers_raw, ...requiredFields } = values; const { skip_servers_raw, ...requiredFields } = values
data?.id ? await updateService(data.id, requiredFields) : await createService(requiredFields); data?.id
setOpen(false); ? await updateService(data.id, requiredFields)
await mutate(); : await createService(requiredFields)
form.reset(); setOpen(false)
await mutate()
form.reset()
} }
const { servers } = useServer(); const { servers } = useServer()
const serverList = servers?.map(s => ({ const serverList = servers?.map((s) => ({
value: `${s.id}`, value: `${s.id}`,
label: s.name, label: s.name,
})) || [{ value: "", label: "" }]; })) || [{ value: "", label: "" }]
const { notifierGroup } = useNotification(); const { notifierGroup } = useNotification()
const ngroupList = notifierGroup?.map(ng => ({ const ngroupList = notifierGroup?.map((ng) => ({
value: `${ng.group.id}`, value: `${ng.group.id}`,
label: ng.group.name, label: ng.group.name,
})) || [{ value: "", label: "" }]; })) || [{ value: "", label: "" }]
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
{data {data ? <IconButton variant="outline" icon="edit" /> : <IconButton icon="plus" />}
?
<IconButton variant="outline" icon="edit" />
:
<IconButton icon="plus" />
}
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="sm:max-w-xl">
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3"> <ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1"> <div className="items-center mx-1">
<DialogHeader> <DialogHeader>
<DialogTitle>{data?t("EditService"):t("CreateService")}</DialogTitle> <DialogTitle>
{data ? t("EditService") : t("CreateService")}
</DialogTitle>
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
@@ -176,7 +177,10 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("Type")}</FormLabel> <FormLabel>{t("Type")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}> <Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select service type" /> <SelectValue placeholder="Select service type" />
@@ -184,7 +188,9 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{Object.entries(serviceTypes).map(([k, v]) => ( {Object.entries(serviceTypes).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem> <SelectItem key={k} value={k}>
{v}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -203,7 +209,9 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
<Label className="text-sm">{t("ShowInService")}</Label> <Label className="text-sm">
{t("ShowInService")}
</Label>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -217,11 +225,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
<FormItem> <FormItem>
<FormLabel>{t("Interval")} (s)</FormLabel> <FormLabel>{t("Interval")} (s)</FormLabel>
<FormControl> <FormControl>
<Input <Input type="number" placeholder="30" {...field} />
type="number"
placeholder="30"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -233,16 +237,23 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("Coverage")}</FormLabel> <FormLabel>{t("Coverage")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}> <Select
onValueChange={field.onChange}
defaultValue={`${field.value}`}
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{Object.entries(serviceCoverageTypes).map(([k, v]) => ( {Object.entries(serviceCoverageTypes).map(
<SelectItem key={k} value={k}>{v}</SelectItem> ([k, v]) => (
))} <SelectItem key={k} value={k}>
{v}
</SelectItem>
),
)}
</SelectContent> </SelectContent>
</Select> </Select>
<FormMessage /> <FormMessage />
@@ -295,7 +306,9 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
<Label className="text-sm">{t("EnableFailureNotification")}</Label> <Label className="text-sm">
{t("EnableFailureNotification")}
</Label>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -347,7 +360,9 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
<Label className="text-sm">{t("EnableLatencyNotification")}</Label> <Label className="text-sm">
{t("EnableLatencyNotification")}
</Label>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -365,7 +380,9 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
<Label className="text-sm">{t("EnableTriggerTask")}</Label> <Label className="text-sm">
{t("EnableTriggerTask")}
</Label>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -377,15 +394,20 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="fail_trigger_tasks" name="fail_trigger_tasks"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("TasksToTriggerOnAlert") + t("SeparateWithComma")}</FormLabel> <FormLabel>
{t("TasksToTriggerOnAlert") +
t("SeparateWithComma")}
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="1,2,3" placeholder="1,2,3"
{...field} {...field}
value={conv.arrToStr(field.value ?? [])} value={conv.arrToStr(field.value ?? [])}
onChange={e => { onChange={(e) => {
const arr = conv.strToArr(e.target.value).map(Number); const arr = conv
field.onChange(arr); .strToArr(e.target.value)
.map(Number)
field.onChange(arr)
}} }}
/> />
</FormControl> </FormControl>
@@ -398,15 +420,20 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="recover_trigger_tasks" name="recover_trigger_tasks"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("TasksToTriggerAfterRecovery") + t("SeparateWithComma")}</FormLabel> <FormLabel>
{t("TasksToTriggerAfterRecovery") +
t("SeparateWithComma")}
</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="1,2,3" placeholder="1,2,3"
{...field} {...field}
value={conv.arrToStr(field.value ?? [])} value={conv.arrToStr(field.value ?? [])}
onChange={e => { onChange={(e) => {
const arr = conv.strToArr(e.target.value).map(Number); const arr = conv
field.onChange(arr); .strToArr(e.target.value)
.map(Number)
field.onChange(arr)
}} }}
/> />
</FormControl> </FormControl>
@@ -420,7 +447,9 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
{t("Close")} {t("Close")}
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" className="my-2">{t("Submit")}</Button> <Button type="submit" className="my-2">
{t("Submit")}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>

View File

@@ -1,15 +1,10 @@
import { import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
Tabs, import { useTranslation } from "react-i18next"
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
import { Link, useLocation } from "react-router-dom" import { Link, useLocation } from "react-router-dom"
import { useTranslation } from "react-i18next";
export const SettingsTab = ({ className }: { className?: string }) => { export const SettingsTab = ({ className }: { className?: string }) => {
const { t } = useTranslation(); const { t } = useTranslation()
const location = useLocation(); const location = useLocation()
return ( return (
<Tabs defaultValue={location.pathname} className={className}> <Tabs defaultValue={location.pathname} className={className}>

View File

@@ -7,137 +7,144 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog"
import { Terminal } from "@xterm/xterm"; import useTerminal from "@/hooks/useTerminal"
import { AttachAddon } from "@xterm/addon-attach"; import { sleep } from "@/lib/utils"
import { FitAddon } from '@xterm/addon-fit'; import { AttachAddon } from "@xterm/addon-attach"
import { useRef, useEffect, useState, useMemo } from "react"; import { FitAddon } from "@xterm/addon-fit"
import { sleep } from "@/lib/utils"; import { Terminal } from "@xterm/xterm"
import "@xterm/xterm/css/xterm.css"; import "@xterm/xterm/css/xterm.css"
import { useParams } from 'react-router-dom'; import { useEffect, useMemo, useRef, useState } from "react"
import { Button } from "./ui/button"; import { useParams } from "react-router-dom"
import { toast } from "sonner"; import { toast } from "sonner"
import { FMCard } from "./fm";
import useTerminal from "@/hooks/useTerminal"; import { FMCard } from "./fm"
import { IconButton } from "./xui/icon-button"; import { Button } from "./ui/button"
import { IconButton } from "./xui/icon-button"
interface XtermProps { interface XtermProps {
wsUrl: string; wsUrl: string
setClose: React.Dispatch<React.SetStateAction<boolean>>; setClose: React.Dispatch<React.SetStateAction<boolean>>
} }
const XtermComponent: React.FC<XtermProps & JSX.IntrinsicElements["div"]> = ({ wsUrl, setClose, ...props }) => { const XtermComponent: React.FC<XtermProps & JSX.IntrinsicElements["div"]> = ({
const terminalIdRef = useRef<HTMLDivElement>(null); wsUrl,
const terminalRef = useRef<Terminal | null>(null); setClose,
const wsRef = useRef<WebSocket | null>(null); ...props
}) => {
const terminalIdRef = useRef<HTMLDivElement>(null)
const terminalRef = useRef<Terminal | null>(null)
const wsRef = useRef<WebSocket | null>(null)
useEffect(() => { useEffect(() => {
return () => { return () => {
wsRef.current?.close(); wsRef.current?.close()
terminalRef.current?.dispose(); terminalRef.current?.dispose()
}; }
}, []); }, [])
useEffect(() => { useEffect(() => {
terminalRef.current = new Terminal({ terminalRef.current = new Terminal({
cursorBlink: true, cursorBlink: true,
fontSize: 16, fontSize: 16,
}); })
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl)
wsRef.current = ws; wsRef.current = ws
ws.binaryType = "arraybuffer"; ws.binaryType = "arraybuffer"
ws.onopen = () => { ws.onopen = () => {
onResize(); onResize()
} }
ws.onclose = () => { ws.onclose = () => {
terminalRef.current?.dispose(); terminalRef.current?.dispose()
setClose(true); setClose(true)
} }
ws.onerror = (e) => { ws.onerror = (e) => {
console.error(e); console.error(e)
toast("Websocket error", { toast("Websocket error", {
description: "View console for details.", description: "View console for details.",
}) })
} }
}, [wsUrl]); }, [wsUrl])
const fitAddon = useRef(new FitAddon()).current
const fitAddon = useRef(new FitAddon()).current; const sendResize = useRef(false)
const sendResize = useRef(false);
const doResize = () => { const doResize = () => {
if (!terminalIdRef.current) return; if (!terminalIdRef.current) return
fitAddon.fit(); fitAddon.fit()
const dimensions = fitAddon.proposeDimensions(); const dimensions = fitAddon.proposeDimensions()
if (dimensions) { if (dimensions) {
const prefix = new Int8Array([1]); const prefix = new Int8Array([1])
const resizeMessage = new TextEncoder().encode(JSON.stringify({ const resizeMessage = new TextEncoder().encode(
Rows: dimensions.rows, JSON.stringify({
Cols: dimensions.cols, Rows: dimensions.rows,
})); Cols: dimensions.cols,
}),
)
const msg = new Int8Array(prefix.length + resizeMessage.length); const msg = new Int8Array(prefix.length + resizeMessage.length)
msg.set(prefix); msg.set(prefix)
msg.set(resizeMessage, prefix.length); msg.set(resizeMessage, prefix.length)
wsRef.current?.send(msg); wsRef.current?.send(msg)
} }
}; }
const onResize = async () => { const onResize = async () => {
if (sendResize.current) return; if (sendResize.current) return
sendResize.current = true; sendResize.current = true
try { try {
await sleep(1500); await sleep(1500)
doResize(); doResize()
} catch (error) { } catch (error) {
console.error('resize error', error); console.error("resize error", error)
} finally { } finally {
sendResize.current = false; sendResize.current = false
} }
}; }
useEffect(() => { useEffect(() => {
if (!wsRef.current || !terminalIdRef.current || !terminalRef.current) return; if (!wsRef.current || !terminalIdRef.current || !terminalRef.current) return
const attachAddon = new AttachAddon(wsRef.current); const attachAddon = new AttachAddon(wsRef.current)
terminalRef.current.loadAddon(attachAddon); terminalRef.current.loadAddon(attachAddon)
terminalRef.current.loadAddon(fitAddon); terminalRef.current.loadAddon(fitAddon)
terminalRef.current.open(terminalIdRef.current); terminalRef.current.open(terminalIdRef.current)
window.addEventListener('resize', onResize); window.addEventListener("resize", onResize)
return () => { return () => {
window.removeEventListener('resize', onResize); window.removeEventListener("resize", onResize)
if (wsRef.current) { if (wsRef.current) {
wsRef.current.close(); wsRef.current.close()
} }
}; }
}, [wsRef.current, terminalRef.current, terminalIdRef.current]); }, [wsRef.current, terminalRef.current, terminalIdRef.current])
return <div ref={terminalIdRef} {...props} />; return <div ref={terminalIdRef} {...props} />
}; }
export const TerminalPage = () => { export const TerminalPage = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>()
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const terminal = useTerminal(id ? parseInt(id) : undefined); const terminal = useTerminal(id ? parseInt(id) : undefined)
return ( return (
<div className="px-8"> <div className="px-8">
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight"> <h1 className="flex-1 text-3xl font-bold tracking-tight">{`Terminal (${id})`}</h1>
{`Terminal (${id})`}
</h1>
<div className="flex-2 flex ml-auto gap-2"> <div className="flex-2 flex ml-auto gap-2">
<FMCard id={id} /> <FMCard id={id} />
</div> </div>
</div> </div>
{terminal?.session_id {terminal?.session_id ? (
? <XtermComponent
<XtermComponent className="max-h-[60%] mb-5" wsUrl={`/api/v1/ws/terminal/${terminal?.session_id}`} setClose={setOpen} /> className="max-h-[60%] mb-5"
: wsUrl={`/api/v1/ws/terminal/${terminal?.session_id}`}
setClose={setOpen}
/>
) : (
<p>The server does not exist, or have not been connected yet.</p> <p>The server does not exist, or have not been connected yet.</p>
} )}
<AlertDialog open={open} onOpenChange={setOpen}> <AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent className="sm:max-w-lg"> <AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader> <AlertDialogHeader>
@@ -148,9 +155,7 @@ export const TerminalPage = () => {
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogAction asChild> <AlertDialogAction asChild>
<Button onClick={window.close}> <Button onClick={window.close}>Close</Button>
Close
</Button>
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@@ -161,10 +166,8 @@ export const TerminalPage = () => {
export const TerminalButton = ({ id }: { id: number }) => { export const TerminalButton = ({ id }: { id: number }) => {
const handleOpenNewTab = () => { const handleOpenNewTab = () => {
window.open(`/dashboard/terminal/${id}`, '_blank'); window.open(`/dashboard/terminal/${id}`, "_blank")
}; }
return ( return <IconButton variant="outline" icon="terminal" onClick={handleOpenNewTab} />
<IconButton variant="outline" icon="terminal" onClick={handleOpenNewTab} />
)
} }

View File

@@ -27,7 +27,7 @@ export function ThemeProvider({
...props ...props
}: ThemeProviderProps) { }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>( const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme () => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
) )
useEffect(() => { useEffect(() => {
@@ -36,8 +36,7 @@ export function ThemeProvider({
root.classList.remove("light", "dark") root.classList.remove("light", "dark")
if (theme === "system") { if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
.matches
? "dark" ? "dark"
: "light" : "light"
@@ -66,8 +65,7 @@ export function ThemeProvider({
export const useTheme = () => { export const useTheme = () => {
const context = useContext(ThemeProviderContext) const context = useContext(ThemeProviderContext)
if (context === undefined) if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider")
throw new Error("useTheme must be used within a ThemeProvider")
return context return context
} }

View File

@@ -1,8 +1,7 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button" import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import * as React from "react"
const AlertDialog = AlertDialogPrimitive.Root const AlertDialog = AlertDialogPrimitive.Root
@@ -11,129 +10,105 @@ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef< const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>, React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className,
)} )}
{...props} {...props}
ref={ref} ref={ref}
/> />
)) ))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef< const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>, React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay /> <AlertDialogOverlay />
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className,
)} )}
{...props} {...props}
/> />
</AlertDialogPortal> </AlertDialogPortal>
)) ))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({ const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className, <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
) )
AlertDialogHeader.displayName = "AlertDialogHeader" AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({ const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className, <div
...props className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
}: React.HTMLAttributes<HTMLDivElement>) => ( {...props}
<div />
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
) )
AlertDialogFooter.displayName = "AlertDialogFooter" AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef< const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>, React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
ref={ref} ref={ref}
className={cn("text-lg font-semibold", className)} className={cn("text-lg font-semibold", className)}
{...props} {...props}
/> />
)) ))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef< const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>, React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ))
AlertDialogDescription.displayName = AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef< const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>, React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
)) ))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef< const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>, React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel <AlertDialogPrimitive.Cancel
ref={ref} ref={ref}
className={cn( className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
buttonVariants({ variant: "outline" }), {...props}
"mt-2 sm:mt-0", />
className
)}
{...props}
/>
)) ))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export { export {
AlertDialog, AlertDialog,
AlertDialogPortal, AlertDialogPortal,
AlertDialogOverlay, AlertDialogOverlay,
AlertDialogTrigger, AlertDialogTrigger,
AlertDialogContent, AlertDialogContent,
AlertDialogHeader, AlertDialogHeader,
AlertDialogFooter, AlertDialogFooter,
AlertDialogTitle, AlertDialogTitle,
AlertDialogDescription, AlertDialogDescription,
AlertDialogAction, AlertDialogAction,
AlertDialogCancel, AlertDialogCancel,
} }

View File

@@ -1,47 +1,43 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from "react"
const Avatar = React.forwardRef< const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>, React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Root <AvatarPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", {...props}
className />
)}
{...props}
/>
)) ))
Avatar.displayName = AvatarPrimitive.Root.displayName Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef< const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>, React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Image <AvatarPrimitive.Image
ref={ref} ref={ref}
className={cn("aspect-square h-full w-full", className)} className={cn("aspect-square h-full w-full", className)}
{...props} {...props}
/> />
)) ))
AvatarImage.displayName = AvatarPrimitive.Image.displayName AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef< const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>, React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted", "flex h-full w-full items-center justify-center rounded-full bg-muted",
className className,
)} )}
{...props} {...props}
/> />
)) ))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName

View File

@@ -1,36 +1,33 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { type VariantProps, cva } from "class-variance-authority"
import * as React 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",
{ {
variants: { variants: {
variant: { variant: {
default: default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80", "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground", outline: "text-foreground",
}, },
},
defaultVariants: {
variant: "default",
},
}, },
defaultVariants: {
variant: "default",
},
}
) )
export interface BadgeProps export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {} VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return ( return <div className={cn(badgeVariants({ variant }), className)} {...props} />
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
} }
export { Badge, badgeVariants } export { Badge, badgeVariants }

View File

@@ -1,115 +1,100 @@
import * as React from "react" 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 { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef< const Breadcrumb = React.forwardRef<
HTMLElement, HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & { React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode separator?: React.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< const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
HTMLOListElement, ({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<"ol"> <ol
>(({ className, ...props }, ref) => ( ref={ref}
<ol className={cn(
ref={ref} "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className={cn( className,
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", )}
className {...props}
)} />
{...props} ),
/> )
))
BreadcrumbList.displayName = "BreadcrumbList" BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef< const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
HTMLLIElement, ({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<"li"> <li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<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 = React.forwardRef<
HTMLAnchorElement, HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & { React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean asChild?: boolean
} }
>(({ asChild, className, ...props }, ref) => { >(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a" const Comp = asChild ? Slot : "a"
return ( return (
<Comp <Comp
ref={ref} ref={ref}
className={cn("transition-colors hover:text-foreground", className)} className={cn("transition-colors hover:text-foreground", className)}
{...props} {...props}
/> />
) )
}) })
BreadcrumbLink.displayName = "BreadcrumbLink" BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef< const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
HTMLSpanElement, ({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<"span"> <span
>(({ className, ...props }, ref) => ( ref={ref}
<span role="link"
ref={ref} aria-disabled="true"
role="link" aria-current="page"
aria-disabled="true" className={cn("font-normal text-foreground", className)}
aria-current="page" {...props}
className={cn("font-normal text-foreground", className)} />
{...props} ),
/> )
))
BreadcrumbPage.displayName = "BreadcrumbPage" BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({ const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
children, <li
className, role="presentation"
...props aria-hidden="true"
}: React.ComponentProps<"li">) => ( className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
<li {...props}
role="presentation" >
aria-hidden="true" {children ?? <ChevronRight />}
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)} </li>
{...props}
>
{children ?? <ChevronRight />}
</li>
) )
BreadcrumbSeparator.displayName = "BreadcrumbSeparator" BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({ const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
className, <span
...props role="presentation"
}: React.ComponentProps<"span">) => ( aria-hidden="true"
<span className={cn("flex h-9 w-9 items-center justify-center", className)}
role="presentation" {...props}
aria-hidden="true" >
className={cn("flex h-9 w-9 items-center justify-center", className)} <MoreHorizontal className="h-4 w-4" />
{...props} <span className="sr-only">More</span>
> </span>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
) )
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis" BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export { export {
Breadcrumb, Breadcrumb,
BreadcrumbList, BreadcrumbList,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
BreadcrumbEllipsis, BreadcrumbEllipsis,
} }

View File

@@ -1,55 +1,52 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 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",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
"bg-destructive text-destructive-foreground hover:bg-destructive/90", outline:
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
"border border-input bg-background hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
secondary: ghost: "hover:bg-accent hover:text-accent-foreground",
"bg-secondary text-secondary-foreground hover:bg-secondary/80", link: "text-primary underline-offset-4 hover:underline",
ghost: "hover:bg-accent hover:text-accent-foreground", },
link: "text-primary underline-offset-4 hover:underline", size: {
}, default: "h-10 px-4 py-2",
size: { sm: "h-9 rounded-md px-3",
default: "h-10 px-4 py-2", lg: "h-11 rounded-md px-8",
sm: "h-9 rounded-md px-3", icon: "h-10 w-10",
lg: "h-11 rounded-md px-8", },
icon: "h-10 w-10", },
}, defaultVariants: {
variant: "default",
size: "default",
},
}, },
defaultVariants: {
variant: "default",
size: "default",
},
}
) )
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean asChild?: boolean
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.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 (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
{...props} {...props}
/> />
) )
} },
) )
Button.displayName = "Button" Button.displayName = "Button"

View File

@@ -1,79 +1,55 @@
import { cn } from "@/lib/utils"
import * as React from "react" import * as React from "react"
import { cn } from "@/lib/utils" const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
const Card = React.forwardRef< <div
HTMLDivElement, ref={ref}
React.HTMLAttributes<HTMLDivElement> className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
>(({ className, ...props }, ref) => ( {...props}
<div />
ref={ref} ),
className={cn( )
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card" Card.displayName = "Card"
const CardHeader = React.forwardRef< const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<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< const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLHeadingElement> <h3
>(({ className, ...props }, ref) => ( ref={ref}
<h3 className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
ref={ref} {...props}
className={cn( />
"text-2xl font-semibold leading-none tracking-tight", ),
className )
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle" CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef< const CardDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<p <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)) ))
CardDescription.displayName = "CardDescription" CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef< const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> )
))
CardContent.displayName = "CardContent" CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef< const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
HTMLDivElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLDivElement> <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<div )
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter" CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -1,27 +1,26 @@
import * as React from "react" 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 { cn } from "@/lib/utils"
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>, React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"flex peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", "flex peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className className,
)} )}
{...props} {...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
> >
<Check className="h-4 w-4" /> <CheckboxPrimitive.Indicator
</CheckboxPrimitive.Indicator> className={cn("flex items-center justify-center text-current")}
</CheckboxPrimitive.Root> >
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)) ))
Checkbox.displayName = CheckboxPrimitive.Root.displayName Checkbox.displayName = CheckboxPrimitive.Root.displayName

View File

@@ -1,9 +1,5 @@
"use client" "use client"
import * as React from "react"
import { Check, ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Command, Command,
@@ -13,106 +9,97 @@ import {
CommandItem, CommandItem,
CommandList, CommandList,
} from "@/components/ui/command" } from "@/components/ui/command"
import { import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
Popover, import { cn } from "@/lib/utils"
PopoverContent, import { Check, ChevronDown } from "lucide-react"
PopoverTrigger, import * as React from "react"
} from "@/components/ui/popover"
interface ComboboxProps interface ComboboxProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
options: { options: {
label: string, label: string
value: string, value: string
}[]; }[]
placeholder?: string; placeholder?: string
defaultValue?: string; defaultValue?: string
className?: string; className?: string
onValueChange: (value: string) => void; onValueChange: (value: string) => void
} }
export const Combobox = React.forwardRef< export const Combobox = React.forwardRef<HTMLButtonElement, ComboboxProps>(
HTMLButtonElement, ({ options, placeholder, defaultValue, className, onValueChange, ...props }, ref) => {
ComboboxProps const [open, setOpen] = React.useState(false)
>(({ const [value, setValue] = React.useState(defaultValue)
options,
placeholder,
defaultValue,
className,
onValueChange,
...props
}, ref) => {
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState(defaultValue)
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
ref={ref} ref={ref}
{...props} {...props}
role="combobox" role="combobox"
variant="outline" variant="outline"
aria-expanded={open} aria-expanded={open}
className={cn( className={cn("flex w-full justify-between hover:bg-inherit", className)}
"flex w-full justify-between hover:bg-inherit", >
className {value ? (
)} (() => {
> const val = options.find((option) => option.value === value)?.label
{value return val ? (
? (() => {
const val = options.find((option) => option.value === value)?.label
return (
val ? (
<div>{val}</div> <div>{val}</div>
) : ( ) : (
<div className="text-muted-foreground">{placeholder}</div> <div className="text-muted-foreground">{placeholder}</div>
) )
) })()
})() ) : (
: <div className="text-muted-foreground">{placeholder}</div>} <div className="text-muted-foreground">{placeholder}</div>
<ChevronDown className="ml-auto opacity-50" /> )}
</Button> <ChevronDown className="ml-auto opacity-50" />
</PopoverTrigger> </Button>
<PopoverContent className="w-auto p-0" align="start"> </PopoverTrigger>
<Command <PopoverContent className="w-auto p-0" align="start">
filter={(value, search, keywords = []) => { <Command
const extendValue = value + " " + keywords.join(" "); filter={(value, search, keywords = []) => {
if (extendValue.toLowerCase().includes(search.toLowerCase())) { const extendValue = value + " " + keywords.join(" ")
return 1; if (extendValue.toLowerCase().includes(search.toLowerCase())) {
} return 1
return 0; }
}} return 0
> }}
<CommandInput placeholder={placeholder} className="h-9" /> >
<CommandList> <CommandInput placeholder={placeholder} className="h-9" />
<CommandEmpty>No result found.</CommandEmpty> <CommandList>
<CommandGroup> <CommandEmpty>No result found.</CommandEmpty>
{options.map((option) => ( <CommandGroup>
<CommandItem {options.map((option) => (
key={option.value} <CommandItem
value={option.value} key={option.value}
keywords={[option.label]} value={option.value}
onSelect={(currentValue) => { keywords={[option.label]}
setValue(currentValue === value ? "" : currentValue) onSelect={(currentValue) => {
onValueChange(currentValue === value ? "" : currentValue) setValue(currentValue === value ? "" : currentValue)
setOpen(false) onValueChange(
}} currentValue === value ? "" : currentValue,
> )
<Check setOpen(false)
className={cn( }}
"justify-start", >
value === option.value ? "opacity-100" : "opacity-0" <Check
)} className={cn(
/> "justify-start",
<span>{option.label}</span> value === option.value
</CommandItem> ? "opacity-100"
))} : "opacity-0",
</CommandGroup> )}
</CommandList> />
</Command> <span>{option.label}</span>
</PopoverContent> </CommandItem>
</Popover> ))}
) </CommandGroup>
}); </CommandList>
</Command>
</PopoverContent>
</Popover>
)
},
)

View File

@@ -1,151 +1,140 @@
import * as React from "react" import { Dialog, DialogContent } from "@/components/ui/dialog"
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 { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef< const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>, React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive> React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive <CommandPrimitive
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className className,
)} )}
{...props} {...props}
/> />
)) ))
Command.displayName = CommandPrimitive.displayName Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => { const CommandDialog = ({ children, ...props }: DialogProps) => {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg"> <DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children} {children}
</Command> </Command>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) )
} }
const CommandInput = React.forwardRef< const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>, React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> React.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" />
<CommandPrimitive.Input <CommandPrimitive.Input
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
/> />
</div> </div>
)) ))
CommandInput.displayName = CommandPrimitive.Input.displayName CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef< const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>, React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.List <CommandPrimitive.List
ref={ref} ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props} {...props}
/> />
)) ))
CommandList.displayName = CommandPrimitive.List.displayName CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef< const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>, React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => ( >((props, ref) => (
<CommandPrimitive.Empty <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
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 = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>, React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Group <CommandPrimitive.Group
ref={ref} ref={ref}
className={cn( className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className className,
)} )}
{...props} {...props}
/> />
)) ))
CommandGroup.displayName = CommandPrimitive.Group.displayName CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef< const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>, React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Separator <CommandPrimitive.Separator
ref={ref} ref={ref}
className={cn("-mx-1 h-px bg-border", className)} className={cn("-mx-1 h-px bg-border", className)}
{...props} {...props}
/> />
)) ))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef< const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>, React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className className,
)} )}
{...props} {...props}
/> />
)) ))
CommandItem.displayName = CommandPrimitive.Item.displayName CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({ const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
className, return (
...props <span
}: React.HTMLAttributes<HTMLSpanElement>) => { className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
return ( {...props}
<span />
className={cn( )
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
} }
CommandShortcut.displayName = "CommandShortcut" CommandShortcut.displayName = "CommandShortcut"
export { export {
Command, Command,
CommandDialog, CommandDialog,
CommandInput, CommandInput,
CommandList, CommandList,
CommandEmpty, CommandEmpty,
CommandGroup, CommandGroup,
CommandItem, CommandItem,
CommandShortcut, CommandShortcut,
CommandSeparator, CommandSeparator,
} }

View File

@@ -1,8 +1,7 @@
import * as React from "react" 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 { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root const Dialog = DialogPrimitive.Root
@@ -13,108 +12,93 @@ const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className className,
)} )}
{...props} {...props}
/> />
)) ))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DialogPortal> <DialogPortal>
<DialogOverlay /> <DialogOverlay />
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.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-accent data-[state=open]:text-muted-foreground"> <DialogPrimitive.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-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)) ))
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className, <div
...props className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
}: React.HTMLAttributes<HTMLDivElement>) => ( {...props}
<div />
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
) )
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className, <div
...props className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
}: React.HTMLAttributes<HTMLDivElement>) => ( {...props}
<div />
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
) )
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref} ref={ref}
className={cn( className={cn("text-lg font-semibold leading-none tracking-tight", className)}
"text-lg font-semibold leading-none tracking-tight", {...props}
className />
)}
{...props}
/>
)) ))
DialogTitle.displayName = DialogPrimitive.Title.displayName DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Description <DialogPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ))
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = DialogPrimitive.Description.displayName
export { export {
Dialog, Dialog,
DialogPortal, DialogPortal,
DialogOverlay, DialogOverlay,
DialogClose, DialogClose,
DialogTrigger, DialogTrigger,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogFooter, DialogFooter,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
} }

View File

@@ -1,16 +1,12 @@
import { cn } from "@/lib/utils"
import * as React from "react" import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul" import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({ const Drawer = ({
shouldScaleBackground = true, shouldScaleBackground = true,
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => ( }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root <DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
) )
Drawer.displayName = "Drawer" Drawer.displayName = "Drawer"
@@ -21,96 +17,81 @@ const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef< const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>, React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay <DrawerPrimitive.Overlay
ref={ref} ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)} className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props} {...props}
/> />
)) ))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef< const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>, React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DrawerPortal> <DrawerPortal>
<DrawerOverlay /> <DrawerOverlay />
<DrawerPrimitive.Content <DrawerPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background", "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className className,
)} )}
{...props} {...props}
> >
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" /> <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children} {children}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>
)) ))
DrawerContent.displayName = "DrawerContent" DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({ const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className, <div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
) )
DrawerHeader.displayName = "DrawerHeader" DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({ const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className, <div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<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 = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>, React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Title <DrawerPrimitive.Title
ref={ref} ref={ref}
className={cn( className={cn("text-lg font-semibold leading-none tracking-tight", className)}
"text-lg font-semibold leading-none tracking-tight", {...props}
className />
)}
{...props}
/>
)) ))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef< const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>, React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Description <DrawerPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export { export {
Drawer, Drawer,
DrawerPortal, DrawerPortal,
DrawerOverlay, DrawerOverlay,
DrawerTrigger, DrawerTrigger,
DrawerClose, DrawerClose,
DrawerContent, DrawerContent,
DrawerHeader, DrawerHeader,
DrawerFooter, DrawerFooter,
DrawerTitle, DrawerTitle,
DrawerDescription, DrawerDescription,
} }

View File

@@ -1,8 +1,7 @@
import * as React from "react" 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 { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenu = DropdownMenuPrimitive.Root
@@ -17,182 +16,169 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRight className="ml-auto" /> <ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)) ))
DropdownMenuSubTrigger.displayName = DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className,
)} )}
{...props} {...props}
/> />
)) ))
DropdownMenuSubContent.displayName = DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 min-w-[8rem] overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className,
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
)) ))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8", inset && "pl-8",
className className,
)} )}
{...props} {...props}
/> />
)) ))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef< const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => ( >(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)) ))
DropdownMenuCheckboxItem.displayName = DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className,
)} )}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator> <DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" /> <Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator> </DropdownMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
)) ))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef< const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
ref={ref} ref={ref}
className={cn( className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
"px-2 py-1.5 text-sm font-semibold", {...props}
inset && "pl-8", />
className
)}
{...props}
/>
)) ))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef< const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
ref={ref} ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)} className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} {...props}
/> />
)) ))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({ const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
className, return (
...props <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
}: React.HTMLAttributes<HTMLSpanElement>) => { )
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
} }
DropdownMenuShortcut.displayName = "DropdownMenuShortcut" DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export { export {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuRadioItem, DropdownMenuRadioItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuShortcut,
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuPortal, DropdownMenuPortal,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuRadioGroup, DropdownMenuRadioGroup,
} }

View File

@@ -1,176 +1,168 @@
import * as React from "react" import { Label } from "@/components/ui/label"
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 { import {
Controller, Controller,
ControllerProps, ControllerProps,
FieldPath, FieldPath,
FieldValues, FieldValues,
FormProvider, FormProvider,
useFormContext, useFormContext,
} from "react-hook-form" } from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider const Form = FormProvider
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = { > = {
name: TName name: TName
} }
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)
{} as FormFieldContextValue
)
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({ >({
...props ...props
}: ControllerProps<TFieldValues, TName>) => { }: ControllerProps<TFieldValues, TName>) => {
return ( return (
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
) )
} }
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext) const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext() const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error("useFormField should be used within <FormField>")
} }
const { id } = itemContext const { id } = itemContext
return { return {
id, id,
name: fieldContext.name, name: fieldContext.name,
formItemId: `${id}-form-item`, formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState,
} }
} }
type FormItemContextValue = { type FormItemContextValue = {
id: string id: string
} }
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
{} as FormItemContextValue
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
},
) )
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem" FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef< const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField()
return ( return (
<Label <Label
ref={ref} ref={ref}
className={cn(error && "text-destructive", className)} className={cn(error && "text-destructive", className)}
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
) )
}) })
FormLabel.displayName = "FormLabel" FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef< const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>, React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot> React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return ( return (
<Slot <Slot
ref={ref} ref={ref}
id={formItemId} id={formItemId}
aria-describedby={ aria-describedby={
!error !error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
? `${formDescriptionId}` }
: `${formDescriptionId} ${formMessageId}` aria-invalid={!!error}
} {...props}
aria-invalid={!!error} />
{...props} )
/>
)
}) })
FormControl.displayName = "FormControl" FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef< const FormDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField() 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 = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => { >(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children 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 {
useFormField, useFormField,
Form, Form,
FormItem, FormItem,
FormLabel, FormLabel,
FormControl, FormControl,
FormDescription, FormDescription,
FormMessage, FormMessage,
FormField, FormField,
} }

View File

@@ -1,24 +1,22 @@
import { cn } from "@/lib/utils"
import * as React from "react" import * as React from "react"
import { cn } from "@/lib/utils" export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.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"

View File

@@ -1,23 +1,17 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as LabelPrimitive from "@radix-ui/react-label"
import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react"
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 = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<LabelPrimitive.Root <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
)) ))
Label.displayName = LabelPrimitive.Root.displayName Label.displayName = LabelPrimitive.Root.displayName

View File

@@ -1,128 +1,122 @@
import * as React from "react" 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 { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef< const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>, React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root <NavigationMenuPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
"relative z-10 flex max-w-max flex-1 items-center justify-center", {...props}
className >
)} {children}
{...props} <NavigationMenuViewport />
> </NavigationMenuPrimitive.Root>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
)) ))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef< const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>, React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List <NavigationMenuPrimitive.List
ref={ref} ref={ref}
className={cn( className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1", "group flex flex-1 list-none items-center justify-center space-x-1",
className className,
)} )}
{...props} {...props}
/> />
)) ))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva( const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent 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:bg-accent hover:text-accent-foreground focus:bg-accent 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 = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>, React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger <NavigationMenuPrimitive.Trigger
ref={ref} ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)} className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props} {...props}
> >
{children}{" "} {children}{" "}
<ChevronDown <ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180" className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true" aria-hidden="true"
/> />
</NavigationMenuPrimitive.Trigger> </NavigationMenuPrimitive.Trigger>
)) ))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef< const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>, React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content <NavigationMenuPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ", "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className className,
)} )}
{...props} {...props}
/> />
)) ))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef< const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>, React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport> React.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
className={cn( className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]", "origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className className,
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
</div> </div>
)) ))
NavigationMenuViewport.displayName = NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef< const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>, React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator <NavigationMenuPrimitive.Indicator
ref={ref} ref={ref}
className={cn( className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in", "top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className className,
)} )}
{...props} {...props}
> >
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" /> <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator> </NavigationMenuPrimitive.Indicator>
)) ))
NavigationMenuIndicator.displayName = NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName
NavigationMenuPrimitive.Indicator.displayName
export { export {
navigationMenuTriggerStyle, navigationMenuTriggerStyle,
NavigationMenu, NavigationMenu,
NavigationMenuList, NavigationMenuList,
NavigationMenuItem, NavigationMenuItem,
NavigationMenuContent, NavigationMenuContent,
NavigationMenuTrigger, NavigationMenuTrigger,
NavigationMenuLink, NavigationMenuLink,
NavigationMenuIndicator, NavigationMenuIndicator,
NavigationMenuViewport, NavigationMenuViewport,
} }

View File

@@ -1,28 +1,27 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react"
const Popover = PopoverPrimitive.Root const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef< const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> React.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
ref={ref} ref={ref}
align={align} align={align}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className,
)} )}
{...props} {...props}
/> />
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
)) ))
PopoverContent.displayName = PopoverPrimitive.Content.displayName PopoverContent.displayName = PopoverPrimitive.Content.displayName

View File

@@ -1,45 +1,42 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import * as React from "react"
const ScrollArea = React.forwardRef< const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>, React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
ref={ref} ref={ref}
className={cn("relative overflow-hidden", className)} className={cn("relative overflow-hidden", className)}
{...props} {...props}
> >
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]"> <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children} {children}
</ScrollAreaPrimitive.Viewport> </ScrollAreaPrimitive.Viewport>
<ScrollBar /> <ScrollBar />
<ScrollAreaPrimitive.Corner /> <ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root> </ScrollAreaPrimitive.Root>
)) ))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef< const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => ( >(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar <ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref} ref={ref}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"flex touch-none select-none transition-colors", "flex touch-none select-none transition-colors",
orientation === "vertical" && orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
"h-full w-2.5 border-l border-l-transparent p-[1px]", orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
orientation === "horizontal" && className,
"h-2.5 flex-col border-t border-t-transparent p-[1px]", )}
className {...props}
)} >
{...props} <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
> </ScrollAreaPrimitive.ScrollAreaScrollbar>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)) ))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

View File

@@ -1,8 +1,7 @@
import * as React from "react" 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 { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root const Select = SelectPrimitive.Root
@@ -11,148 +10,141 @@ const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef< const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<SelectPrimitive.Icon asChild> <SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" /> <ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
)) ))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef< const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
ref={ref} ref={ref}
className={cn( className={cn("flex cursor-default items-center justify-center py-1", className)}
"flex cursor-default items-center justify-center py-1", {...props}
className >
)} <ChevronUp className="h-4 w-4" />
{...props} </SelectPrimitive.ScrollUpButton>
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
)) ))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef< const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
ref={ref} ref={ref}
className={cn( className={cn("flex cursor-default items-center justify-center py-1", className)}
"flex cursor-default items-center justify-center py-1", {...props}
className >
)} <ChevronDown className="h-4 w-4" />
{...props} </SelectPrimitive.ScrollDownButton>
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
)) ))
SelectScrollDownButton.displayName = SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => ( >(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className className,
)} )}
position={position} position={position}
{...props} {...props}
> >
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)} )}
> >
{children} {children}
</SelectPrimitive.Viewport> </SelectPrimitive.Viewport>
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
)) ))
SelectContent.displayName = SelectPrimitive.Content.displayName SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef< const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>, React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Label <SelectPrimitive.Label
ref={ref} ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props} {...props}
/> />
)) ))
SelectLabel.displayName = SelectPrimitive.Label.displayName SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef< const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>, React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className,
)} )}
{...props} {...props}
> >
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator> <SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator> </SelectPrimitive.ItemIndicator>
</span> </span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
)) ))
SelectItem.displayName = SelectPrimitive.Item.displayName SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef< const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Separator <SelectPrimitive.Separator
ref={ref} ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)} className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} {...props}
/> />
)) ))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export { export {
Select, Select,
SelectGroup, SelectGroup,
SelectValue, SelectValue,
SelectTrigger, SelectTrigger,
SelectContent, SelectContent,
SelectLabel, SelectLabel,
SelectItem, SelectItem,
SelectSeparator, SelectSeparator,
SelectScrollUpButton, SelectScrollUpButton,
SelectScrollDownButton, SelectScrollDownButton,
} }

View File

@@ -1,29 +1,23 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react"
const Separator = React.forwardRef< const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> React.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}
decorative={decorative} decorative={decorative}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"shrink-0 bg-border", "shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className className,
)} )}
{...props} {...props}
/> />
) ))
)
Separator.displayName = SeparatorPrimitive.Root.displayName Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator } export { Separator }

View File

@@ -1,15 +1,7 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function Skeleton({ function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
className, return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
} }
export { Skeleton } export { Skeleton }

View File

@@ -4,26 +4,24 @@ import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner> type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme() const { theme = "system" } = useTheme()
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={theme as ToasterProps["theme"]}
className="toaster group" className="toaster group"
toastOptions={{ toastOptions={{
classNames: { classNames: {
toast: toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", description: "group-[.toast]:text-muted-foreground",
description: "group-[.toast]:text-muted-foreground", actionButton:
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
cancelButton: },
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", }}
}, {...props}
}} />
{...props} )
/>
)
} }
export { Toaster } export { Toaster }

View File

@@ -1,117 +1,94 @@
import { cn } from "@/lib/utils"
import * as React from "react" import * as React from "react"
import { cn } from "@/lib/utils" const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
const Table = React.forwardRef< <div className="relative w-full overflow-auto">
HTMLTableElement, <table
React.HTMLAttributes<HTMLTableElement> ref={ref}
>(({ className, ...props }, ref) => ( className={cn("w-full caption-bottom text-sm", className)}
<div className="relative w-full overflow-auto"> {...props}
<table />
ref={ref} </div>
className={cn("w-full caption-bottom text-sm", className)} ),
{...props} )
/>
</div>
))
Table.displayName = "Table" Table.displayName = "Table"
const TableHeader = React.forwardRef< const TableHeader = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement> React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
)) ))
TableHeader.displayName = "TableHeader" TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef< const TableBody = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement> React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<tbody <tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)) ))
TableBody.displayName = "TableBody" TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef< const TableFooter = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement> React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<tfoot <tfoot
ref={ref} ref={ref}
className={cn( className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", {...props}
className />
)}
{...props}
/>
)) ))
TableFooter.displayName = "TableFooter" TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef< const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
HTMLTableRowElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableRowElement> <tr
>(({ className, ...props }, ref) => ( ref={ref}
<tr className={cn(
ref={ref} "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className={cn( className,
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", )}
className {...props}
)} />
{...props} ),
/> )
))
TableRow.displayName = "TableRow" TableRow.displayName = "TableRow"
const TableHead = React.forwardRef< const TableHead = React.forwardRef<
HTMLTableCellElement, HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement> React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<th <th
ref={ref} ref={ref}
className={cn( className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className className,
)} )}
{...props} {...props}
/> />
)) ))
TableHead.displayName = "TableHead" TableHead.displayName = "TableHead"
const TableCell = React.forwardRef< const TableCell = React.forwardRef<
HTMLTableCellElement, HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement> React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<td <td
ref={ref} ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props} {...props}
/> />
)) ))
TableCell.displayName = "TableCell" TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef< const TableCaption = React.forwardRef<
HTMLTableCaptionElement, HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement> React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<caption <caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
)) ))
TableCaption.displayName = "TableCaption" TableCaption.displayName = "TableCaption"
export { export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -1,52 +1,51 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react"
const Tabs = TabsPrimitive.Root const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef< const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>, React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.List <TabsPrimitive.List
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className className,
)} )}
{...props} {...props}
/> />
)) ))
TabsList.displayName = TabsPrimitive.List.displayName TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef< const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>, React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm", "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className className,
)} )}
{...props} {...props}
/> />
)) ))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef< const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>, React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.Content <TabsPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className className,
)} )}
{...props} {...props}
/> />
)) ))
TabsContent.displayName = TabsPrimitive.Content.displayName TabsContent.displayName = TabsPrimitive.Content.displayName

View File

@@ -1,22 +1,20 @@
import { cn } from "@/lib/utils"
import * as React from "react" import * as React from "react"
import { cn } from "@/lib/utils" const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
({ className, ...props }, ref) => {
const Textarea = React.forwardRef< return (
HTMLTextAreaElement, <textarea
React.ComponentProps<"textarea"> className={cn(
>(({ className, ...props }, ref) => { "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background 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 md:text-sm",
return ( className,
<textarea )}
className={cn( ref={ref}
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background 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 md:text-sm", {...props}
className />
)} )
ref={ref} },
{...props} )
/>
)
})
Textarea.displayName = "Textarea" Textarea.displayName = "Textarea"
export { Textarea } export { Textarea }

View File

@@ -1,3 +1,4 @@
import { createUser } from "@/api/user"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
@@ -9,7 +10,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { import {
Form, Form,
FormControl, FormControl,
@@ -18,29 +18,28 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { ModelUser } from "@/types"
import { useState } from "react"
import { KeyedMutator } from "swr"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { createUser } from "@/api/user" import { ModelUser } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useTranslation } from "react-i18next"; import { useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { KeyedMutator } from "swr"
import { z } from "zod"
interface UserCardProps { interface UserCardProps {
mutate: KeyedMutator<ModelUser[]>; mutate: KeyedMutator<ModelUser[]>
} }
const userFormSchema = z.object({ const userFormSchema = z.object({
username: z.string().min(1), username: z.string().min(1),
password: z.string().min(8).max(72), password: z.string().min(8).max(72),
}); })
export const UserCard: React.FC<UserCardProps> = ({ mutate }) => { export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
const { t } = useTranslation(); const { t } = useTranslation()
const form = useForm<z.infer<typeof userFormSchema>>({ const form = useForm<z.infer<typeof userFormSchema>>({
resolver: zodResolver(userFormSchema), resolver: zodResolver(userFormSchema),
defaultValues: { defaultValues: {
@@ -49,16 +48,16 @@ export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
}, },
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
} },
}) })
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof userFormSchema>) => { const onSubmit = async (values: z.infer<typeof userFormSchema>) => {
await createUser(values); await createUser(values)
setOpen(false); setOpen(false)
await mutate(); await mutate()
form.reset(); form.reset()
} }
return ( return (
@@ -82,9 +81,7 @@ export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
<FormItem> <FormItem>
<FormLabel>{t("Username")}</FormLabel> <FormLabel>{t("Username")}</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -97,9 +94,7 @@ export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
<FormItem> <FormItem>
<FormLabel>{t("Password")}</FormLabel> <FormLabel>{t("Password")}</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -111,7 +106,9 @@ export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
{t("Close")} {t("Close")}
</Button> </Button>
</DialogClose> </DialogClose>
<Button type="submit" className="my-2">{t("Confirm")}</Button> <Button type="submit" className="my-2">
{t("Confirm")}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>

View File

@@ -1,102 +1,115 @@
"use client" "use client"
import * as React from "react"
import { import {
Breadcrumb, Breadcrumb,
BreadcrumbEllipsis, BreadcrumbEllipsis,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbList, BreadcrumbList,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
} from "@/components/ui/breadcrumb" } from "@/components/ui/breadcrumb"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
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"
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: React.Dispatch<React.SetStateAction<string>>
} }
function pathToItems(path: string) { function pathToItems(path: string) {
const segments = path.split('/').filter(Boolean); const segments = path.split("/").filter(Boolean)
const result: { href: string; label: string; }[] = []; const result: { href: string; label: string }[] = []
let currentPath = ''; let currentPath = ""
segments.forEach(segment => { segments.forEach((segment) => {
currentPath += `/${segment}`; currentPath += `/${segment}`
result.push({ href: currentPath, label: segment }); result.push({ href: currentPath, label: segment })
}); })
return result; return result
} }
export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => { export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => {
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const items = pathToItems(formatPath(path)); const items = pathToItems(formatPath(path))
return ( return (
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList>
<BreadcrumbItem> <BreadcrumbItem>
<p className="cursor-pointer hover:text-white transition" onClick={() => { setPath('/') }}>{'/'}</p> <p
</BreadcrumbItem> className="cursor-pointer hover:text-white transition"
<BreadcrumbSeparator /> onClick={() => {
{items.length > ITEMS_TO_DISPLAY ? ( setPath("/")
<> }}
<BreadcrumbItem> >
{ {"/"}
<DropdownMenu open={open} onOpenChange={setOpen}> </p>
<DropdownMenuTrigger </BreadcrumbItem>
className="flex items-center gap-1" <BreadcrumbSeparator />
aria-label="Toggle menu" {items.length > ITEMS_TO_DISPLAY ? (
> <>
<BreadcrumbEllipsis className="h-4 w-4" /> <BreadcrumbItem>
</DropdownMenuTrigger> {
<DropdownMenuContent align="start"> <DropdownMenu open={open} onOpenChange={setOpen}>
{items.slice(0, -ITEMS_TO_DISPLAY).map((item, index) => ( <DropdownMenuTrigger
<DropdownMenuItem key={index}> className="flex items-center gap-1"
<p onClick={() => { setPath(item.href) }}> aria-label="Toggle menu"
{item.label} >
</p> <BreadcrumbEllipsis className="h-4 w-4" />
</DropdownMenuItem> </DropdownMenuTrigger>
))} <DropdownMenuContent align="start">
</DropdownMenuContent> {items.slice(0, -ITEMS_TO_DISPLAY).map((item, index) => (
</DropdownMenu> <DropdownMenuItem key={index}>
} <p
</BreadcrumbItem> onClick={() => {
<BreadcrumbSeparator /> setPath(item.href)
</> }}
) : null} >
{items.slice(-ITEMS_TO_DISPLAY).map((item, index, slicedItems) => ( {item.label}
<React.Fragment key={index}> </p>
<BreadcrumbItem className="overflow-auto"> </DropdownMenuItem>
{item.href ? ( ))}
<> </DropdownMenuContent>
<p </DropdownMenu>
className="max-w-20 truncate md:max-w-none cursor-pointer hover:text-white transition" }
onClick={() => { setPath(item.href) }} </BreadcrumbItem>
> <BreadcrumbSeparator />
{item.label} </>
</p> ) : null}
</> {items.slice(-ITEMS_TO_DISPLAY).map((item, index, slicedItems) => (
) : ( <React.Fragment key={index}>
<BreadcrumbPage className="max-w-20 truncate md:max-w-none"> <BreadcrumbItem className="overflow-auto">
{item.label} {item.href ? (
</BreadcrumbPage> <>
)} <p
</BreadcrumbItem> className="max-w-20 truncate md:max-w-none cursor-pointer hover:text-white transition"
{index !== slicedItems.length - 1 ? <BreadcrumbSeparator /> : null} onClick={() => {
</React.Fragment> setPath(item.href)
))} }}
</BreadcrumbList> >
</Breadcrumb> {item.label}
) </p>
</>
) : (
<BreadcrumbPage className="max-w-20 truncate md:max-w-none">
{item.label}
</BreadcrumbPage>
)}
</BreadcrumbItem>
{index !== slicedItems.length - 1 ? <BreadcrumbSeparator /> : null}
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
)
} }

View File

@@ -1,79 +1,84 @@
import { Button, ButtonProps } from "@/components/ui/button"
import { import {
Plus, Check,
Edit2,
Trash2,
Terminal,
CircleArrowUp, CircleArrowUp,
Clipboard, Clipboard,
Check,
FolderClosed,
Play,
Download, Download,
Upload, Edit2,
FolderClosed,
Menu, Menu,
Play,
Plus,
Terminal,
Trash2,
Upload,
} from "lucide-react" } from "lucide-react"
import { Button, ButtonProps } from "@/components/ui/button" import { forwardRef } from "react"
import { forwardRef } from "react";
export interface IconButtonProps extends ButtonProps { export interface IconButtonProps extends ButtonProps {
icon: icon:
"clipboard" | | "clipboard"
"check" | | "check"
"edit" | | "edit"
"trash" | | "trash"
"plus" | | "plus"
"terminal" | | "terminal"
"update" | | "update"
"folder-closed" | | "folder-closed"
"play" | | "play"
"download" | | "download"
"upload" | | "upload"
"menu"; | "menu"
} }
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => { export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
return ( return (
<Button {...props} ref={ref} size="icon"> <Button
className="rounded-lg shadow-[inset_0_1px_0_rgba(255,255,255,0.2)]"
{...props}
ref={ref}
size="icon"
>
{(() => { {(() => {
switch (props.icon) { switch (props.icon) {
case "clipboard": { case "clipboard": {
return <Clipboard />; return <Clipboard />
} }
case "check": { case "check": {
return <Check />; return <Check />
} }
case "edit": { case "edit": {
return <Edit2 />; return <Edit2 />
} }
case "trash": { case "trash": {
return <Trash2 />; return <Trash2 />
} }
case "plus": { case "plus": {
return <Plus />; return <Plus />
} }
case "terminal": { case "terminal": {
return <Terminal />; return <Terminal />
} }
case "update": { case "update": {
return <CircleArrowUp />; return <CircleArrowUp />
} }
case "folder-closed": { case "folder-closed": {
return <FolderClosed />; return <FolderClosed />
} }
case "play": { case "play": {
return <Play />; return <Play />
} }
case "download": { case "download": {
return <Download />; return <Download />
} }
case "upload": { case "upload": {
return <Upload />; return <Upload />
} }
case "menu": { case "menu": {
return <Menu />; return <Menu />
} }
} }
})()} })()}
</Button> </Button>
); )
}) })

View File

@@ -22,392 +22,367 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
import { Badge } from "@/components/ui/badge"
import * as React from "react"; import { Button } from "@/components/ui/button"
import { cva, type VariantProps } from "class-variance-authority";
import { import {
CheckIcon, Command,
ChevronDown, CommandEmpty,
XIcon, CommandGroup,
WandSparkles, CommandInput,
} from "lucide-react"; CommandItem,
CommandList,
import { cn } from "@/lib/utils"; CommandSeparator,
import { Separator } from "@/components/ui/separator"; } from "@/components/ui/command"
import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"
import { import { cn } from "@/lib/utils"
Popover, import { type VariantProps, cva } from "class-variance-authority"
PopoverContent, import { CheckIcon, ChevronDown, WandSparkles, XIcon } from "lucide-react"
PopoverTrigger, import * as React from "react"
} from "@/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
/** /**
* Variants for the multi-select component to handle different styles. * Variants for the multi-select component to handle different styles.
* Uses class-variance-authority (cva) to define different styles based on "variant" prop. * Uses class-variance-authority (cva) to define different styles based on "variant" prop.
*/ */
const multiSelectVariants = cva( const multiSelectVariants = cva(
"m-1 transition ease-in-out delay-150 hover:-translate-y-1 duration-300", "m-1 transition ease-in-out delay-150 hover:-translate-y-1 duration-300",
{ {
variants: { variants: {
variant: { variant: {
default: default: "border-foreground/10 text-foreground bg-card hover:bg-card/80",
"border-foreground/10 text-foreground bg-card hover:bg-card/80", secondary:
secondary: "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80",
"border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive:
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", inverted: "inverted",
inverted: "inverted", },
}, },
defaultVariants: {
variant: "default",
},
}, },
defaultVariants: { )
variant: "default",
},
}
);
/** /**
* Props for MultiSelect component * Props for MultiSelect component
*/ */
interface MultiSelectProps interface MultiSelectProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof multiSelectVariants> { VariantProps<typeof multiSelectVariants> {
/** /**
* An array of option objects to be displayed in the multi-select component. * An array of option objects to be displayed in the multi-select component.
* Each option object has a label, value, and an optional icon. * Each option object has a label, value, and an optional icon.
*/ */
options: { options: {
/** The text to display for the option. */ /** The text to display for the option. */
label: string; label: string
/** The unique value associated with the option. */ /** The unique value associated with the option. */
value: string; value: string
/** Optional icon component to display alongside the option. */ /** Optional icon component to display alongside the option. */
icon?: React.ComponentType<{ className?: string }>; icon?: React.ComponentType<{ className?: string }>
}[]; }[]
/** /**
* Callback function triggered when the selected values change. * Callback function triggered when the selected values change.
* Receives an array of the new selected values. * Receives an array of the new selected values.
*/ */
onValueChange: (value: string[]) => void; onValueChange: (value: string[]) => void
/** The default selected values when the component mounts. */ /** The default selected values when the component mounts. */
defaultValue?: string[]; defaultValue?: string[]
/** /**
* Placeholder text to be displayed when no values are selected. * Placeholder text to be displayed when no values are selected.
* Optional, defaults to "Select options". * Optional, defaults to "Select options".
*/ */
placeholder?: string; placeholder?: string
/** /**
* Animation duration in seconds for the visual effects (e.g., bouncing badges). * Animation duration in seconds for the visual effects (e.g., bouncing badges).
* Optional, defaults to 0 (no animation). * Optional, defaults to 0 (no animation).
*/ */
animation?: number; animation?: number
/** /**
* Maximum number of items to display. Extra selected items will be summarized. * Maximum number of items to display. Extra selected items will be summarized.
* Optional, defaults to 3. * Optional, defaults to 3.
*/ */
maxCount?: number; maxCount?: number
/** /**
* The modality of the popover. When set to true, interaction with outside elements * The modality of the popover. When set to true, interaction with outside elements
* will be disabled and only popover content will be visible to screen readers. * will be disabled and only popover content will be visible to screen readers.
* Optional, defaults to false. * Optional, defaults to false.
*/ */
modalPopover?: boolean; modalPopover?: boolean
/** /**
* If true, renders the multi-select component as a child of another component. * If true, renders the multi-select component as a child of another component.
* Optional, defaults to false. * Optional, defaults to false.
*/ */
asChild?: boolean; asChild?: boolean
/** /**
* Additional class names to apply custom styles to the multi-select component. * Additional class names to apply custom styles to the multi-select component.
* Optional, can be used to add custom styles. * Optional, can be used to add custom styles.
*/ */
className?: string; className?: string
} }
export const MultiSelect = React.forwardRef< export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
HTMLButtonElement, (
MultiSelectProps {
>( options,
( onValueChange,
{ variant,
options, defaultValue = [],
onValueChange, placeholder = "Select options",
variant, animation = 0,
defaultValue = [], maxCount = 3,
placeholder = "Select options", modalPopover = false,
animation = 0, asChild = false,
maxCount = 3, className,
modalPopover = false, ...props
asChild = false, },
className, ref,
...props
},
ref
) => {
const [selectedValues, setSelectedValues] =
React.useState<string[]>(defaultValue);
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const [isAnimating, setIsAnimating] = React.useState(false);
const handleInputKeyDown = (
event: React.KeyboardEvent<HTMLInputElement>
) => { ) => {
if (event.key === "Enter") { const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue)
setIsPopoverOpen(true); const [isPopoverOpen, setIsPopoverOpen] = React.useState(false)
} else if (event.key === "Backspace" && !event.currentTarget.value) { const [isAnimating, setIsAnimating] = React.useState(false)
const newSelectedValues = [...selectedValues];
newSelectedValues.pop();
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
}
};
const toggleOption = (option: string) => { const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
const newSelectedValues = selectedValues.includes(option) if (event.key === "Enter") {
? selectedValues.filter((value) => value !== option) setIsPopoverOpen(true)
: [...selectedValues, option]; } else if (event.key === "Backspace" && !event.currentTarget.value) {
setSelectedValues(newSelectedValues); const newSelectedValues = [...selectedValues]
onValueChange(newSelectedValues); newSelectedValues.pop()
}; setSelectedValues(newSelectedValues)
onValueChange(newSelectedValues)
}
}
const handleClear = () => { const toggleOption = (option: string) => {
setSelectedValues([]); const newSelectedValues = selectedValues.includes(option)
onValueChange([]); ? selectedValues.filter((value) => value !== option)
}; : [...selectedValues, option]
setSelectedValues(newSelectedValues)
onValueChange(newSelectedValues)
}
const handleTogglePopover = () => { const handleClear = () => {
setIsPopoverOpen((prev) => !prev); setSelectedValues([])
}; onValueChange([])
}
const clearExtraOptions = () => { const handleTogglePopover = () => {
const newSelectedValues = selectedValues.slice(0, maxCount); setIsPopoverOpen((prev) => !prev)
setSelectedValues(newSelectedValues); }
onValueChange(newSelectedValues);
};
const toggleAll = () => { const clearExtraOptions = () => {
if (selectedValues.length === options.length) { const newSelectedValues = selectedValues.slice(0, maxCount)
handleClear(); setSelectedValues(newSelectedValues)
} else { onValueChange(newSelectedValues)
const allValues = options.map((option) => option.value); }
setSelectedValues(allValues);
onValueChange(allValues);
}
};
const stopWheelEventPropagation: React.WheelEventHandler = (e) => { const toggleAll = () => {
e.stopPropagation(); if (selectedValues.length === options.length) {
}; handleClear()
} else {
const allValues = options.map((option) => option.value)
setSelectedValues(allValues)
onValueChange(allValues)
}
}
const stopTouchMoveEventPropagation: React.TouchEventHandler = (e) => { const stopWheelEventPropagation: React.WheelEventHandler = (e) => {
e.stopPropagation(); e.stopPropagation()
}; }
return ( const stopTouchMoveEventPropagation: React.TouchEventHandler = (e) => {
<Popover e.stopPropagation()
open={isPopoverOpen} }
onOpenChange={setIsPopoverOpen}
modal={modalPopover} return (
> <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal={modalPopover}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
ref={ref} ref={ref}
{...props} {...props}
onClick={handleTogglePopover} onClick={handleTogglePopover}
className={cn(
"flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto",
className
)}
>
{selectedValues.length > 0 ? (
<div className="flex justify-between items-center w-full">
<div className="flex flex-wrap items-center">
{selectedValues.slice(0, maxCount).map((value) => {
const option = options.find((o) => o.value === value);
const IconComponent = option?.icon;
return (
<Badge
key={value}
className={cn( className={cn(
isAnimating ? "animate-bounce" : "", "flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto",
multiSelectVariants({ variant }) className,
)} )}
style={{ animationDuration: `${animation}s` }}
>
{IconComponent && (
<IconComponent className="h-4 w-4 mr-2" />
)}
{option?.label}
<XIcon
className="ml-2 h-2 w-2 cursor-pointer"
onClick={(event) => {
event.stopPropagation();
toggleOption(value);
}}
/>
</Badge>
);
})}
{selectedValues.length > maxCount && (
<Badge
className={cn(
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
isAnimating ? "animate-bounce" : "",
multiSelectVariants({ variant })
)}
style={{ animationDuration: `${animation}s` }}
> >
{`+ ${selectedValues.length - maxCount} more`} {selectedValues.length > 0 ? (
<XIcon <div className="flex justify-between items-center w-full">
className="ml-2 h-2 w-2 cursor-pointer" <div className="flex flex-wrap items-center">
onClick={(event) => { {selectedValues.slice(0, maxCount).map((value) => {
event.stopPropagation(); const option = options.find((o) => o.value === value)
clearExtraOptions(); const IconComponent = option?.icon
}} return (
/> <Badge
</Badge> key={value}
)} className={cn(
</div> isAnimating ? "animate-bounce" : "",
<div className="flex items-center justify-between"> multiSelectVariants({ variant }),
<XIcon )}
className="h-4 mx-2 cursor-pointer text-muted-foreground" style={{ animationDuration: `${animation}s` }}
onClick={(event) => { >
event.stopPropagation(); {IconComponent && (
handleClear(); <IconComponent className="h-4 w-4 mr-2" />
}} )}
/> {option?.label}
<Separator <XIcon
orientation="vertical" className="ml-2 h-2 w-2 cursor-pointer"
className="flex min-h-6 h-full" onClick={(event) => {
/> event.stopPropagation()
<ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" /> toggleOption(value)
</div> }}
</div> />
) : ( </Badge>
<div className="flex items-center justify-between w-full mx-auto"> )
<span className="text-sm text-muted-foreground mx-3"> })}
{placeholder} {selectedValues.length > maxCount && (
</span> <Badge
<ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" /> className={cn(
</div> "bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
)} isAnimating ? "animate-bounce" : "",
</Button> multiSelectVariants({ variant }),
</PopoverTrigger> )}
<PopoverContent style={{ animationDuration: `${animation}s` }}
className="w-auto p-0" >
align="start" {`+ ${selectedValues.length - maxCount} more`}
onEscapeKeyDown={() => setIsPopoverOpen(false)} <XIcon
onWheel={stopWheelEventPropagation} className="ml-2 h-2 w-2 cursor-pointer"
onTouchMove={stopTouchMoveEventPropagation} onClick={(event) => {
> event.stopPropagation()
<Command> clearExtraOptions()
<CommandInput }}
placeholder="Search..." />
onKeyDown={handleInputKeyDown} </Badge>
/> )}
<CommandList> </div>
<CommandEmpty>No results found.</CommandEmpty> <div className="flex items-center justify-between">
<CommandGroup> <XIcon
<CommandItem className="h-4 mx-2 cursor-pointer text-muted-foreground"
key="all" onClick={(event) => {
onSelect={toggleAll} event.stopPropagation()
className="cursor-pointer" handleClear()
}}
/>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
/>
<ChevronDown className="h-4 mx-2 cursor-pointer text-muted-foreground" />
</div>
</div>
) : (
<div className="flex items-center justify-between w-full mx-auto">
<span className="text-sm text-muted-foreground mx-3">
{placeholder}
</span>
<ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" />
</div>
)}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0"
align="start"
onEscapeKeyDown={() => setIsPopoverOpen(false)}
onWheel={stopWheelEventPropagation}
onTouchMove={stopTouchMoveEventPropagation}
> >
<div <Command>
className={cn( <CommandInput placeholder="Search..." onKeyDown={handleInputKeyDown} />
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary", <CommandList>
selectedValues.length === options.length <CommandEmpty>No results found.</CommandEmpty>
? "bg-primary text-primary-foreground" <CommandGroup>
: "opacity-50 [&_svg]:invisible" <CommandItem
)} key="all"
> onSelect={toggleAll}
<CheckIcon className="h-4 w-4" /> className="cursor-pointer"
</div> >
<span>(Select All)</span> <div
</CommandItem> className={cn(
{options.map((option) => { "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
const isSelected = selectedValues.includes(option.value); selectedValues.length === options.length
return ( ? "bg-primary text-primary-foreground"
<CommandItem : "opacity-50 [&_svg]:invisible",
key={option.value} )}
onSelect={() => toggleOption(option.value)} >
className="cursor-pointer" <CheckIcon className="h-4 w-4" />
> </div>
<div <span>(Select All)</span>
</CommandItem>
{options.map((option) => {
const isSelected = selectedValues.includes(option.value)
return (
<CommandItem
key={option.value}
onSelect={() => toggleOption(option.value)}
className="cursor-pointer"
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className="h-4 w-4" />
</div>
{option.icon && (
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<span>{option.label}</span>
</CommandItem>
)
})}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<div className="flex items-center justify-between">
{selectedValues.length > 0 && (
<>
<CommandItem
onSelect={handleClear}
className="flex-1 justify-center cursor-pointer"
>
Clear
</CommandItem>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
/>
</>
)}
<CommandItem
onSelect={() => setIsPopoverOpen(false)}
className="flex-1 justify-center cursor-pointer max-w-full"
>
Close
</CommandItem>
</div>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
{animation > 0 && selectedValues.length > 0 && (
<WandSparkles
className={cn( className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary", "cursor-pointer my-2 text-foreground bg-background w-3 h-3",
isSelected isAnimating ? "" : "text-muted-foreground",
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
)} )}
> onClick={() => setIsAnimating(!isAnimating)}
<CheckIcon className="h-4 w-4" /> />
</div> )}
{option.icon && ( </Popover>
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" /> )
)} },
<span>{option.label}</span> )
</CommandItem>
);
})}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<div className="flex items-center justify-between">
{selectedValues.length > 0 && (
<>
<CommandItem
onSelect={handleClear}
className="flex-1 justify-center cursor-pointer"
>
Clear
</CommandItem>
<Separator
orientation="vertical"
className="flex min-h-6 h-full"
/>
</>
)}
<CommandItem
onSelect={() => setIsPopoverOpen(false)}
className="flex-1 justify-center cursor-pointer max-w-full"
>
Close
</CommandItem>
</div>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
{animation > 0 && selectedValues.length > 0 && (
<WandSparkles
className={cn(
"cursor-pointer my-2 text-foreground bg-background w-3 h-3",
isAnimating ? "" : "text-muted-foreground"
)}
onClick={() => setIsAnimating(!isAnimating)}
/>
)}
</Popover>
);
}
);
MultiSelect.displayName = "MultiSelect"; MultiSelect.displayName = "MultiSelect"

View File

@@ -1,10 +1,48 @@
import { NavigationMenuLinkProps, NavigationMenuTriggerProps } from "@radix-ui/react-navigation-menu" import {
import { NavigationMenuLink, NavigationMenuTrigger, navigationMenuTriggerStyle } from "../ui/navigation-menu" NavigationMenuLinkProps,
NavigationMenuTriggerProps,
} from "@radix-ui/react-navigation-menu"
import { motion } from "framer-motion"
export const NzNavigationMenuLink = (props: NavigationMenuLinkProps & React.RefAttributes<HTMLAnchorElement>) => { import {
return <NavigationMenuLink {...props} className={navigationMenuTriggerStyle() + " hover:bg-inherit data-[active]:bg-inherit transition-colors text-foreground/60 data-[active]:text-foreground hover:text-foreground/90"} /> NavigationMenuLink,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "../ui/navigation-menu"
export const NzNavigationMenuLink = (
props: NavigationMenuLinkProps & React.RefAttributes<HTMLAnchorElement>,
) => {
return (
<div className="relative">
<NavigationMenuLink
{...props}
className={
navigationMenuTriggerStyle() +
" hover:bg-inherit data-[active]:bg-inherit transition-colors text-foreground/60 data-[active]:text-foreground hover:text-foreground/90"
}
/>
{props.active && (
<motion.div
layoutId="tab-underline"
className="absolute bottom-0 left-0 right-0 h-[2px] bg-black dark:bg-white"
/>
)}
</div>
)
} }
export const NzNavigationMenuTrigger = (props: Omit<NavigationMenuTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>) => { export const NzNavigationMenuTrigger = (
return <NavigationMenuTrigger {...props} className={navigationMenuTriggerStyle() + " hover:bg-inherit data-[active]:bg-inherit transition-colors text-foreground/60 data-[active]:text-foreground hover:text-foreground/90"} /> props: Omit<NavigationMenuTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> &
React.RefAttributes<HTMLButtonElement>,
) => {
return (
<NavigationMenuTrigger
{...props}
className={
navigationMenuTriggerStyle() +
" hover:bg-inherit data-[active]:bg-inherit transition-colors text-foreground/60 data-[active]:text-foreground hover:text-foreground/90"
}
/>
)
} }

View File

@@ -1,9 +1,8 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { type VariantProps, cva } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react"
const Sheet = SheetPrimitive.Root const Sheet = SheetPrimitive.Root
@@ -14,108 +13,98 @@ const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal const SheetPortal = SheetPrimitive.Portal
const sheetVariants = cva( const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{ {
variants: { variants: {
side: { side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom: bottom: "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
right: },
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", },
}, defaultVariants: {
side: "right",
},
}, },
defaultVariants: {
side: "right",
},
}
) )
interface SheetContentProps interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> { setOpen: React.Dispatch<React.SetStateAction<boolean>> } VariantProps<typeof sheetVariants> {
setOpen: React.Dispatch<React.SetStateAction<boolean>>
}
const SheetContent = React.forwardRef< const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>, React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps SheetContentProps
>(({ side = "right", className, children, setOpen, ...props }, ref) => ( >(({ side = "right", className, children, setOpen, ...props }, ref) => (
<SheetPortal> <SheetPortal>
<SheetPrimitive.Content <SheetPrimitive.Content
ref={ref} ref={ref}
className={cn(sheetVariants({ side }), className)} className={cn(sheetVariants({ side }), className)}
{...props} {...props}
> >
{children} {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"> <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" onClick={() => { setOpen(false) }} /> <X
<span className="sr-only">Close</span> className="h-4 w-4"
</SheetPrimitive.Close> onClick={() => {
</SheetPrimitive.Content> setOpen(false)
</SheetPortal> }}
/>
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)) ))
SheetContent.displayName = SheetPrimitive.Content.displayName SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({ const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className, <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
) )
SheetHeader.displayName = "SheetHeader" SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({ const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
className, <div
...props className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
}: React.HTMLAttributes<HTMLDivElement>) => ( {...props}
<div />
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
) )
SheetFooter.displayName = "SheetFooter" SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef< const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>, React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Title <SheetPrimitive.Title
ref={ref} ref={ref}
className={cn("text-lg font-semibold text-foreground", className)} className={cn("text-lg font-semibold text-foreground", className)}
{...props} {...props}
/> />
)) ))
SheetTitle.displayName = SheetPrimitive.Title.displayName SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef< const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>, React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Description <SheetPrimitive.Description
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ))
SheetDescription.displayName = SheetPrimitive.Description.displayName SheetDescription.displayName = SheetPrimitive.Description.displayName
export { export {
Sheet, Sheet,
SheetPortal, SheetPortal,
SheetTrigger, SheetTrigger,
SheetClose, SheetClose,
SheetContent, SheetContent,
SheetHeader, SheetHeader,
SheetFooter, SheetFooter,
SheetTitle, SheetTitle,
SheetDescription, SheetDescription,
} }

View File

@@ -1,5 +1,9 @@
"use client"; "use client"
import { ScrollArea } from "@/components/ui/scroll-area"
import { TableCell, TableHead, TableRow } from "@/components/ui/table"
import { useMediaQuery } from "@/hooks/useMediaQuery"
import { cn } from "@/lib/utils"
import { import {
ColumnDef, ColumnDef,
Row, Row,
@@ -9,36 +13,26 @@ import {
getCoreRowModel, getCoreRowModel,
getSortedRowModel, getSortedRowModel,
useReactTable, useReactTable,
} from "@tanstack/react-table"; } from "@tanstack/react-table"
import { HTMLAttributes, forwardRef, useEffect, useRef, useState } from "react"
import { TableCell, TableHead, TableRow } from "@/components/ui/table"; import { TableVirtuoso } from "react-virtuoso"
import { HTMLAttributes, forwardRef, useState, useRef, useEffect } from "react";
import { TableVirtuoso } from "react-virtuoso";
import { cn } from "@/lib/utils"
import { ScrollArea } from "@/components/ui/scroll-area";
import { useMediaQuery } from "@/hooks/useMediaQuery";
// 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),
// but here we don't want it, so let's use a new component with only <table> tag // but here we don't want it, so let's use a new component with only <table> tag
const TableComponent = forwardRef< const TableComponent = forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
HTMLTableElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableElement> <table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<table )
ref={ref} TableComponent.displayName = "TableComponent"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
));
TableComponent.displayName = "TableComponent";
const TableRowComponent = <TData,>(rows: Row<TData>[]) => const TableRowComponent = <TData,>(rows: Row<TData>[]) =>
function getTableRow(props: HTMLAttributes<HTMLTableRowElement>) { function getTableRow(props: HTMLAttributes<HTMLTableRowElement>) {
// @ts-expect-error data-index is a valid attribute // @ts-expect-error data-index is a valid attribute
const index = props["data-index"]; const index = props["data-index"]
const row = rows[index]; const row = rows[index]
if (!row) return null; if (!row) return null
return ( return (
<TableRow <TableRow
@@ -53,11 +47,11 @@ const TableRowComponent = <TData,>(rows: Row<TData>[]) =>
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
); )
}; }
function SortingIndicator({ isSorted }: { isSorted: SortDirection | false }) { function SortingIndicator({ isSorted }: { isSorted: SortDirection | false }) {
if (!isSorted) return null; if (!isSorted) return null
return ( return (
<div> <div>
{ {
@@ -67,13 +61,15 @@ function SortingIndicator({ isSorted }: { isSorted: SortDirection | false }) {
}[isSorted] }[isSorted]
} }
</div> </div>
); )
} }
interface DataTableProps<TData, TValue> { interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]; columns: ColumnDef<TData, TValue>[]
data: TData[]; data: TData[]
rowComponent?: (rows: Row<TData>[]) => (props: HTMLAttributes<HTMLTableRowElement>) => JSX.Element | null, rowComponent?: (
rows: Row<TData>[],
) => (props: HTMLAttributes<HTMLTableRowElement>) => JSX.Element | null
} }
export function DataTable<TData, TValue>({ export function DataTable<TData, TValue>({
@@ -81,10 +77,12 @@ export function DataTable<TData, TValue>({
data, data,
rowComponent, rowComponent,
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([{ const [sorting, setSorting] = useState<SortingState>([
id: 'type', {
desc: true, id: "type",
}]); desc: true,
},
])
const table = useReactTable({ const table = useReactTable({
data, data,
columns, columns,
@@ -94,41 +92,43 @@ export function DataTable<TData, TValue>({
onSortingChange: setSorting, onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
}); })
const { rows } = table.getRowModel(); const { rows } = table.getRowModel()
const [heightState, setHeight] = useState(0) const [heightState, setHeight] = useState(0)
const ref = useRef(null); const ref = useRef(null)
const isDesktop = useMediaQuery("(min-width: 640px)"); const isDesktop = useMediaQuery("(min-width: 640px)")
useEffect(() => { useEffect(() => {
const calculateHeight = () => { const calculateHeight = () => {
if (ref.current) { if (ref.current) {
const virtuosoElement = ref.current; const virtuosoElement = ref.current
let topOffset = 0; let topOffset = 0
let currentElement = virtuosoElement as any; let currentElement = virtuosoElement as any
// Calculate the total offset from the top of the document // Calculate the total offset from the top of the document
while (currentElement) { while (currentElement) {
topOffset += currentElement.offsetTop || 0; topOffset += currentElement.offsetTop || 0
currentElement = currentElement.offsetParent as HTMLElement; currentElement = currentElement.offsetParent as HTMLElement
} }
const totalHeight = window.innerHeight; const totalHeight = window.innerHeight
const calculatedHeight = totalHeight - topOffset; const calculatedHeight = totalHeight - topOffset
setHeight(calculatedHeight); setHeight(calculatedHeight)
} }
}; }
calculateHeight(); // Initial calculation calculateHeight() // Initial calculation
if (isDesktop) { if (isDesktop) {
window.addEventListener('resize', calculateHeight); window.addEventListener("resize", calculateHeight)
} }
return () => { if (isDesktop) window.removeEventListener('resize', calculateHeight); } return () => {
}, [isDesktop]); if (isDesktop) window.removeEventListener("resize", calculateHeight)
}
}, [isDesktop])
return ( return (
<div className="rounded-md border" ref={ref} style={{ height: heightState }}> <div className="rounded-md border" ref={ref} style={{ height: heightState }}>
@@ -158,11 +158,12 @@ export function DataTable<TData, TValue>({
{...{ {...{
style: header.column.getCanSort() style: header.column.getCanSort()
? { ? {
cursor: "pointer", cursor: "pointer",
userSelect: "none", userSelect: "none",
} }
: {}, : {},
onClick: header.column.getToggleSortingHandler(), onClick:
header.column.getToggleSortingHandler(),
}} }}
> >
{flexRender( {flexRender(
@@ -175,12 +176,12 @@ export function DataTable<TData, TValue>({
</div> </div>
)} )}
</TableHead> </TableHead>
); )
})} })}
</TableRow> </TableRow>
)) ))
} }
/> />
</div> </div>
); )
} }

View File

@@ -1,18 +1,18 @@
import { useRouteError, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"
import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter } from "@/components/ui/card"
import { Card, CardContent, CardFooter } from "@/components/ui/card"; import { AlertCircle } from "lucide-react"
import { AlertCircle } from "lucide-react"; import { useNavigate, useRouteError } from "react-router-dom"
interface RouterError { interface RouterError {
statusText?: string; statusText?: string
message?: string; message?: string
status?: number; status?: number
} }
export default function ErrorPage() { export default function ErrorPage() {
const error = useRouteError() as RouterError; const error = useRouteError() as RouterError
const navigate = useNavigate(); const navigate = useNavigate()
console.error(error); console.error(error)
return ( return (
<div className="min-h-screen w-full flex items-center justify-center bg-background p-4"> <div className="min-h-screen w-full flex items-center justify-center bg-background p-4">
@@ -32,15 +32,11 @@ export default function ErrorPage() {
</div> </div>
</CardContent> </CardContent>
<CardFooter className="flex justify-center pb-6"> <CardFooter className="flex justify-center pb-6">
<Button <Button variant="default" size="lg" onClick={() => navigate("/dashboard")}>
variant="default"
size="lg"
onClick={() => navigate('/dashboard')}
>
Back to Dashboard Back to Dashboard
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>
); )
} }

View File

@@ -1,51 +1,55 @@
import { createContext, useContext, useEffect, useMemo } from "react"; import { getProfile, login as loginRequest } from "@/api/user"
import { useNavigate } from "react-router-dom"; import { AuthContextProps } from "@/types"
import { useMainStore } from "./useMainStore"; import { createContext, useContext, useEffect, useMemo } from "react"
import { AuthContextProps } from "@/types"; import { useNavigate } from "react-router-dom"
import { getProfile, login as loginRequest } from "@/api/user"; import { toast } from "sonner"
import { toast } from "sonner";
import { useMainStore } from "./useMainStore"
const AuthContext = createContext<AuthContextProps>({ const AuthContext = createContext<AuthContextProps>({
profile: undefined, profile: undefined,
login: () => { }, login: () => {},
logout: () => { }, logout: () => {},
}); })
export const AuthProvider = ({ children }: { export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
children: React.ReactNode; const profile = useMainStore((store) => store.profile)
}) => { const setProfile = useMainStore((store) => store.setProfile)
const profile = useMainStore(store => store.profile)
const setProfile = useMainStore(store => store.setProfile)
useEffect(() => { useEffect(() => {
(async () => { ;(async () => {
try { try {
const user = await getProfile(); const user = await getProfile()
setProfile(user); setProfile(user)
} catch (error) { } catch (error: any) {
setProfile(undefined); setProfile(undefined)
console.log("Error fetching profile", error)
} }
})(); })()
}, []) }, [])
const navigate = useNavigate(); const navigate = useNavigate()
const login = async (username: string, password: string) => { const login = async (username: string, password: string) => {
try { try {
await loginRequest(username, password); await loginRequest(username, password)
const user = await getProfile(); const user = await getProfile()
setProfile(user); setProfile(user)
navigate("/dashboard"); navigate("/dashboard")
} catch (error: any) { } catch (error: any) {
toast(error.message); toast(error.message)
} }
}; }
const logout = () => { const logout = () => {
document.cookie.split(";").forEach(function (c) { document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); }); document.cookie.split(";").forEach(function (c) {
setProfile(undefined); document.cookie = c
navigate("/dashboard/login", { replace: true }); .replace(/^ +/, "")
}; .replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/")
})
setProfile(undefined)
navigate("/dashboard/login", { replace: true })
}
const value = useMemo( const value = useMemo(
() => ({ () => ({
@@ -53,11 +57,11 @@ export const AuthProvider = ({ children }: {
login, login,
logout, logout,
}), }),
[profile] [profile],
); )
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}; }
export const useAuth = () => { export const useAuth = () => {
return useContext(AuthContext); return useContext(AuthContext)
}; }

View File

@@ -1,15 +1,15 @@
import { MainStore } from '@/types' import { MainStore } from "@/types"
import { create } from 'zustand' import { create } from "zustand"
import { persist, createJSONStorage } from 'zustand/middleware' import { createJSONStorage, persist } from "zustand/middleware"
export const useMainStore = create<MainStore, [['zustand/persist', MainStore]]>( export const useMainStore = create<MainStore, [["zustand/persist", MainStore]]>(
persist( persist(
(set, get) => ({ (set, get) => ({
profile: get()?.profile, profile: get()?.profile,
setProfile: profile => set({ profile }), setProfile: (profile) => set({ profile }),
}), }),
{ {
name: 'mainStore', name: "mainStore",
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => localStorage),
}, },
), ),

View File

@@ -1,19 +1,19 @@
import * as React from "react" import * as React from "react"
export function useMediaQuery(query: string) { export function useMediaQuery(query: string) {
const [value, setValue] = React.useState(false) const [value, setValue] = React.useState(false)
React.useEffect(() => { React.useEffect(() => {
function onChange(event: MediaQueryListEvent) { function onChange(event: MediaQueryListEvent) {
setValue(event.matches) setValue(event.matches)
} }
const result = matchMedia(query) const result = matchMedia(query)
result.addEventListener("change", onChange) result.addEventListener("change", onChange)
setValue(result.matches) setValue(result.matches)
return () => result.removeEventListener("change", onChange) return () => result.removeEventListener("change", onChange)
}, [query]) }, [query])
return value return value
} }

View File

@@ -1,56 +1,71 @@
import { createContext, useContext, useEffect, useMemo } from "react"
import { useNotificationStore } from "./useNotificationStore"
import { getNotificationGroups } from "@/api/notification-group"
import { getNotification } from "@/api/notification" import { getNotification } from "@/api/notification"
import { getNotificationGroups } from "@/api/notification-group"
import { NotificationContextProps } from "@/types" import { NotificationContextProps } from "@/types"
import { createContext, useContext, useEffect, useMemo } from "react"
import { useLocation } from "react-router-dom" import { useLocation } from "react-router-dom"
import { toast } from "sonner"
const NotificationContext = createContext<NotificationContextProps>({}); import { useNotificationStore } from "./useNotificationStore"
const NotificationContext = createContext<NotificationContextProps>({})
interface NotificationProviderProps { interface NotificationProviderProps {
children: React.ReactNode; children: React.ReactNode
withNotifier?: boolean; withNotifier?: boolean
withNotifierGroup?: boolean; withNotifierGroup?: boolean
} }
export const NotificationProvider: React.FC<NotificationProviderProps> = ({ children, withNotifier, withNotifierGroup }) => { export const NotificationProvider: React.FC<NotificationProviderProps> = ({
const notifierGroup = useNotificationStore(store => store.notifierGroup); children,
const setNotifierGroup = useNotificationStore(store => store.setNotifierGroup); withNotifier,
withNotifierGroup,
}) => {
const notifierGroup = useNotificationStore((store) => store.notifierGroup)
const setNotifierGroup = useNotificationStore((store) => store.setNotifierGroup)
const notifiers = useNotificationStore(store => store.notifiers); const notifiers = useNotificationStore((store) => store.notifiers)
const setNotifier = useNotificationStore(store => store.setNotifier); const setNotifier = useNotificationStore((store) => store.setNotifier)
const location = useLocation(); const location = useLocation()
useEffect(() => { useEffect(() => {
if (withNotifierGroup) if (withNotifierGroup)
(async () => { (async () => {
try { try {
const ng = await getNotificationGroups(); const ng = await getNotificationGroups()
setNotifierGroup(ng); setNotifierGroup(ng)
} catch (error) { } catch (error: any) {
setNotifierGroup(undefined); toast("NotificationProvider Error", {
description: error.message,
})
setNotifierGroup(undefined)
} }
})(); })()
if (withNotifier) if (withNotifier)
(async () => { (async () => {
try { try {
const n = await getNotification(); const n = await getNotification()
const nData = n.map(({ id, name }) => ({ id, name })); const nData = n.map(({ id, name }) => ({ id, name }))
setNotifier(nData); setNotifier(nData)
} catch (error) { } catch (error: any) {
setNotifier(undefined); toast("NotificationProvider Error", {
description: error.message,
})
setNotifier(undefined)
} }
})(); })()
}, [location.pathname]) }, [location.pathname])
const value: NotificationContextProps = useMemo(() => ({ const value: NotificationContextProps = useMemo(
notifiers: notifiers, () => ({
notifierGroup: notifierGroup, notifiers: notifiers,
}), [notifiers, notifierGroup]); notifierGroup: notifierGroup,
return <NotificationContext.Provider value={value}>{children}</NotificationContext.Provider>; }),
[notifiers, notifierGroup],
)
return <NotificationContext.Provider value={value}>{children}</NotificationContext.Provider>
} }
export const useNotification = () => { export const useNotification = () => {
return useContext(NotificationContext); return useContext(NotificationContext)
}; }

View File

@@ -1,17 +1,20 @@
import { NotificationStore } from '@/types' import { NotificationStore } from "@/types"
import { create } from 'zustand' import { create } from "zustand"
import { persist, createJSONStorage } from 'zustand/middleware' import { createJSONStorage, persist } from "zustand/middleware"
export const useNotificationStore = create<NotificationStore, [['zustand/persist', NotificationStore]]>( export const useNotificationStore = create<
NotificationStore,
[["zustand/persist", NotificationStore]]
>(
persist( persist(
(set, get) => ({ (set, get) => ({
notifiers: get()?.notifiers, notifiers: get()?.notifiers,
notifierGroup: get()?.notifierGroup, notifierGroup: get()?.notifierGroup,
setNotifier: notifiers => set({ notifiers }), setNotifier: (notifiers) => set({ notifiers }),
setNotifierGroup: notifierGroup => set({ notifierGroup }), setNotifierGroup: (notifierGroup) => set({ notifierGroup }),
}), }),
{ {
name: 'notificationStore', name: "notificationStore",
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => localStorage),
}, },
), ),

View File

@@ -1,56 +1,71 @@
import { createContext, useContext, useEffect, useMemo } from "react"
import { useServerStore } from "./useServerStore"
import { getServerGroups } from "@/api/server-group"
import { getServers } from "@/api/server" import { getServers } from "@/api/server"
import { getServerGroups } from "@/api/server-group"
import { ServerContextProps } from "@/types" import { ServerContextProps } from "@/types"
import { createContext, useContext, useEffect, useMemo } from "react"
import { useLocation } from "react-router-dom" import { useLocation } from "react-router-dom"
import { toast } from "sonner"
const ServerContext = createContext<ServerContextProps>({}); import { useServerStore } from "./useServerStore"
const ServerContext = createContext<ServerContextProps>({})
interface ServerProviderProps { interface ServerProviderProps {
children: React.ReactNode; children: React.ReactNode
withServer?: boolean; withServer?: boolean
withServerGroup?: boolean; withServerGroup?: boolean
} }
export const ServerProvider: React.FC<ServerProviderProps> = ({ children, withServer, withServerGroup }) => { export const ServerProvider: React.FC<ServerProviderProps> = ({
const serverGroup = useServerStore(store => store.serverGroup); children,
const setServerGroup = useServerStore(store => store.setServerGroup); withServer,
withServerGroup,
}) => {
const serverGroup = useServerStore((store) => store.serverGroup)
const setServerGroup = useServerStore((store) => store.setServerGroup)
const server = useServerStore(store => store.server); const server = useServerStore((store) => store.server)
const setServer = useServerStore(store => store.setServer); const setServer = useServerStore((store) => store.setServer)
const location = useLocation(); const location = useLocation()
useEffect(() => { useEffect(() => {
if (withServerGroup) if (withServerGroup)
(async () => { (async () => {
try { try {
const sg = await getServerGroups(); const sg = await getServerGroups()
setServerGroup(sg); setServerGroup(sg)
} catch (error) { } catch (error: any) {
setServerGroup(undefined); toast("ServerProvider Error", {
description: error.message,
})
setServerGroup(undefined)
} }
})(); })()
if (withServer) if (withServer)
(async () => { (async () => {
try { try {
const s = await getServers(); const s = await getServers()
const serverData = s.map(({ id, name }) => ({ id, name })); const serverData = s.map(({ id, name }) => ({ id, name }))
setServer(serverData); setServer(serverData)
} catch (error) { } catch (error: any) {
setServer(undefined); toast("ServerProvider Error", {
description: error.message,
})
setServer(undefined)
} }
})(); })()
}, [location.pathname]) }, [location.pathname])
const value: ServerContextProps = useMemo(() => ({ const value: ServerContextProps = useMemo(
servers: server, () => ({
serverGroups: serverGroup, servers: server,
}), [server, serverGroup]); serverGroups: serverGroup,
return <ServerContext.Provider value={value}>{children}</ServerContext.Provider>; }),
[server, serverGroup],
)
return <ServerContext.Provider value={value}>{children}</ServerContext.Provider>
} }
export const useServer = () => { export const useServer = () => {
return useContext(ServerContext); return useContext(ServerContext)
}; }

View File

@@ -1,17 +1,17 @@
import { ServerStore } from '@/types' import { ServerStore } from "@/types"
import { create } from 'zustand' import { create } from "zustand"
import { persist, createJSONStorage } from 'zustand/middleware' import { createJSONStorage, persist } from "zustand/middleware"
export const useServerStore = create<ServerStore, [['zustand/persist', ServerStore]]>( export const useServerStore = create<ServerStore, [["zustand/persist", ServerStore]]>(
persist( persist(
(set, get) => ({ (set, get) => ({
server: get()?.server, server: get()?.server,
serverGroup: get()?.serverGroup, serverGroup: get()?.serverGroup,
setServer: server => set({ server }), setServer: (server) => set({ server }),
setServerGroup: serverGroup => set({ serverGroup }), setServerGroup: (serverGroup) => set({ serverGroup }),
}), }),
{ {
name: 'serverStore', name: "serverStore",
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => localStorage),
}, },
), ),

View File

@@ -1,11 +1,8 @@
import { swrFetcher } from "@/api/api"; import { swrFetcher } from "@/api/api"
import { ModelSettingResponse } from "@/types"; import { ModelSettingResponse } from "@/types"
import useSWR from "swr"; import useSWR from "swr"
export default function useSetting() { export default function useSetting() {
const { data } = useSWR<ModelSettingResponse>( const { data } = useSWR<ModelSettingResponse>("/api/v1/setting", swrFetcher)
"/api/v1/setting", return data
swrFetcher
);
return data;
} }

View File

@@ -1,23 +1,23 @@
import { createTerminal } from "@/api/terminal"; import { createTerminal } from "@/api/terminal"
import { ModelCreateTerminalResponse } from "@/types"; import { ModelCreateTerminalResponse } from "@/types"
import { useState, useEffect } from "react"; import { useEffect, useState } from "react"
export default function useTerminal(serverId?: number) { export default function useTerminal(serverId?: number) {
const [terminal, setTerminal] = useState<ModelCreateTerminalResponse | null>(null); const [terminal, setTerminal] = useState<ModelCreateTerminalResponse | null>(null)
async function fetchTerminal() { async function fetchTerminal() {
try { try {
const response = await createTerminal(serverId!); const response = await createTerminal(serverId!)
setTerminal(response); setTerminal(response)
} catch (error) { } catch (error) {
console.error("Failed to fetch terminal:", error); console.error("Failed to fetch terminal:", error)
} }
} }
useEffect(() => { useEffect(() => {
if (!serverId) return; if (!serverId) return
fetchTerminal(); fetchTerminal()
}, [serverId]); }, [serverId])
return terminal; return terminal
} }

View File

@@ -3,87 +3,86 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--radius: 0.5rem; --radius: 0.5rem;
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 0 0% 3.9%; --foreground: 0 0% 3.9%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 0 0% 3.9%; --card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%; --popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%; --primary: 0 0% 9%;
--primary-foreground: 0 0% 98%; --primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%; --secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%; --secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%; --muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%; --muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%; --accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%; --accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%; --border: 0 0% 89.8%;
--input: 0 0% 89.8%; --input: 0 0% 89.8%;
--ring: 0 0% 3.9%; --ring: 0 0% 3.9%;
--chart-1: 12 76% 61%; --chart-1: 12 76% 61%;
--chart-2: 173 58% 39%; --chart-2: 173 58% 39%;
--chart-3: 197 37% 24%; --chart-3: 197 37% 24%;
--chart-4: 43 74% 66%; --chart-4: 43 74% 66%;
--chart-5: 27 87% 67% --chart-5: 27 87% 67%;
} }
.dark { .dark {
--background: 0 0% 3.9%; --background: 0 0% 9%;
--foreground: 0 0% 98%; --foreground: 0 0% 98%;
--card: 0 0% 3.9%; --card: 0 0% 3.9%;
--card-foreground: 0 0% 98%; --card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%; --popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%; --popover-foreground: 0 0% 98%;
--primary: 0 0% 98%; --primary: 0 0% 98%;
--primary-foreground: 0 0% 9%; --primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%; --secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%; --secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%; --muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%; --muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%; --accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%; --border: 0 0% 14.9%;
--input: 0 0% 14.9%; --input: 0 0% 14.9%;
--ring: 0 0% 83.1%; --ring: 0 0% 83.1%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;
--chart-3: 30 80% 55%; --chart-3: 30 80% 55%;
--chart-4: 280 65% 60%; --chart-4: 280 65% 60%;
--chart-5: 340 75% 55% --chart-5: 340 75% 55%;
} }
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
html, html,
body, body,
#root { #root {
height: 100%; height: 100%;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
@apply w-2.5 h-2.5; @apply h-2.5 w-2.5;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@apply bg-transparent @apply bg-transparent;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
@apply rounded-full bg-border border-[1px] border-transparent border-solid bg-clip-padding; @apply rounded-full border-[1px] border-solid border-transparent bg-border bg-clip-padding;
} }

View File

@@ -1,71 +1,70 @@
let receivedLength = 0; let receivedLength = 0
let expectedLength = 0; let expectedLength = 0
let root: FileSystemDirectoryHandle; let root: FileSystemDirectoryHandle
let draftHandle: FileSystemFileHandle; let draftHandle: FileSystemFileHandle
let accessHandle: FileSystemSyncAccessHandle; let accessHandle: FileSystemSyncAccessHandle
enum Operation { enum Operation {
WriteHeader = 1, WriteHeader = 1,
WriteChunks, WriteChunks,
DeleteFiles, DeleteFiles,
}; }
onmessage = async function (event) { onmessage = async function (event) {
try { try {
const { operation, arrayBuffer, fileName } = event.data; const { operation, arrayBuffer, fileName } = event.data
switch (operation) { switch (operation) {
case Operation.WriteHeader: { case Operation.WriteHeader: {
const dataView = new DataView(arrayBuffer); const dataView = new DataView(arrayBuffer)
expectedLength = Number(dataView.getBigUint64(4, false)); expectedLength = Number(dataView.getBigUint64(4, false))
receivedLength = 0; receivedLength = 0
// Create a new temporary file // Create a new temporary file
root = await navigator.storage.getDirectory(); root = await navigator.storage.getDirectory()
draftHandle = await root.getFileHandle(fileName, { create: true }); draftHandle = await root.getFileHandle(fileName, { create: true })
accessHandle = await draftHandle.createSyncAccessHandle(); accessHandle = await draftHandle.createSyncAccessHandle()
// Inform that file handle is created // Inform that file handle is created
const dataChunk = arrayBuffer.slice(12); const dataChunk = arrayBuffer.slice(12)
receivedLength += dataChunk.byteLength; receivedLength += dataChunk.byteLength
accessHandle.write(dataChunk, { at: 0 }); accessHandle.write(dataChunk, { at: 0 })
const progress = 'got handle'; const progress = "got handle"
postMessage({ type: 1, progress: progress }); postMessage({ type: 1, progress: progress })
break; break
} }
case Operation.WriteChunks: { case Operation.WriteChunks: {
if (!accessHandle) { if (!accessHandle) {
throw new Error('accessHandle is undefined'); throw new Error("accessHandle is undefined")
} }
const dataChunk = arrayBuffer; const dataChunk = arrayBuffer
accessHandle.write(dataChunk, { at: receivedLength }); accessHandle.write(dataChunk, { at: receivedLength })
receivedLength += dataChunk.byteLength; receivedLength += dataChunk.byteLength
if (receivedLength === expectedLength) { if (receivedLength === expectedLength) {
accessHandle.flush(); accessHandle.flush()
accessHandle.close(); accessHandle.close()
const fileBlob = await draftHandle.getFile(); const fileBlob = await draftHandle.getFile()
const blob = new Blob([fileBlob], { type: 'application/octet-stream' }); const blob = new Blob([fileBlob], { type: "application/octet-stream" })
postMessage({ type: 2, blob: blob, fileName: fileName }); postMessage({ type: 2, blob: blob, fileName: fileName })
} }
break; break
} }
case Operation.DeleteFiles: { case Operation.DeleteFiles: {
for await (const [name, handle] of root.entries()) { for await (const [name, handle] of root.entries()) {
if (handle.kind === 'file') { if (handle.kind === "file") {
await root.removeEntry(name); await root.removeEntry(name)
} else if (handle.kind === 'directory') { } else if (handle.kind === "directory") {
await root.removeEntry(name, { recursive: true }); await root.removeEntry(name, { recursive: true })
} }
} }
break; break
} }
} }
} catch (error) { } catch (error) {
if (error instanceof Error) if (error instanceof Error) postMessage({ type: 0, error: error.message })
postMessage({ type: 0, error: error.message });
} }
}; }

View File

@@ -1,10 +1,10 @@
import i18n from "i18next"; import i18n from "i18next"
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next"
import enTranslation from "../locales/en/translation.json"; import enTranslation from "../locales/en/translation.json"
import itTranslation from "../locales/it/translation.json"; import itTranslation from "../locales/it/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"
const resources = { const resources = {
en: { en: {
@@ -19,24 +19,23 @@ const resources = {
"zh-TW": { "zh-TW": {
translation: zhTWTranslation, translation: zhTWTranslation,
}, },
}; }
const getStoredLanguage = () => { const getStoredLanguage = () => {
return localStorage.getItem("language") || "zh-CN"; return localStorage.getItem("language") || "zh-CN"
}; }
i18n.use(initReactI18next) i18n.use(initReactI18next).init({
.init({ resources,
resources, lng: getStoredLanguage(), // 使用localStorage中存储的语言或默认值
lng: getStoredLanguage(), // 使用localStorage中存储的语言或默认值 fallbackLng: "en", // 当前语言的翻译没有找到时,使用的备选语言
fallbackLng: "en", // 当前语言的翻译没有找到时,使用的备选语言 interpolation: {
interpolation: { escapeValue: false, // react已经安全地转义
escapeValue: false, // react已经安全地转义 },
}, })
});
i18n.on("languageChanged", (lng) => { i18n.on("languageChanged", (lng) => {
localStorage.setItem("language", lng); localStorage.setItem("language", lng)
}); })
export default i18n; export default i18n

View File

@@ -1,262 +1,264 @@
import { clsx, type ClassValue } from "clsx" import { FMEntry, FMOpcode, ModelIP } from "@/types"
import { type ClassValue, clsx } from "clsx"
import copy from "copy-to-clipboard"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import { z } from "zod" import { z } from "zod"
import { FMEntry, FMOpcode, ModelIP } from "@/types"
import FMWorker from "./fm?worker" import FMWorker from "./fm?worker"
import copy from "copy-to-clipboard"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
const emptyStringToUndefined = z.literal('').transform(() => undefined); const emptyStringToUndefined = z.literal("").transform(() => undefined)
export function asOptionalField<T extends z.ZodTypeAny>(schema: T) { export function asOptionalField<T extends z.ZodTypeAny>(schema: T) {
return schema.optional().or(emptyStringToUndefined); return schema.optional().or(emptyStringToUndefined)
} }
export const conv = { export const conv = {
recordToStr: (rec: Record<string, boolean>) => { recordToStr: (rec: Record<string, boolean>) => {
const arr: string[] = []; const arr: string[] = []
for (const key in rec) { for (const key in rec) {
arr.push(key); arr.push(key)
} }
return arr.join(','); return arr.join(",")
}, },
strToRecord: (str: string) => { strToRecord: (str: string) => {
const arr = str.split(','); const arr = str.split(",")
return arr.reduce((acc, num) => { return arr.reduce(
acc[num] = true; (acc, num) => {
return acc; acc[num] = true
}, {} as Record<string, boolean>); return acc
}, },
arrToStr: <T>(arr: T[]) => { {} as Record<string, boolean>,
return arr.join(','); )
}, },
strToArr: (str: string) => { arrToStr: <T>(arr: T[]) => {
return str.split(',').filter(Boolean) || []; return arr.join(",")
}, },
recordToArr: <T>(rec: Record<string, T>) => { strToArr: (str: string) => {
const arr: T[] = []; return str.split(",").filter(Boolean) || []
for (const val of Object.values(rec)) { },
arr.push(val); recordToArr: <T>(rec: Record<string, T>) => {
} const arr: T[] = []
return arr; for (const val of Object.values(rec)) {
}, arr.push(val)
recordToStrArr: <T>(rec: Record<string, T>) => { }
const arr: string[] = []; return arr
for (const val of Object.keys(rec)) { },
arr.push(val); recordToStrArr: <T>(rec: Record<string, T>) => {
} const arr: string[] = []
return arr; for (const val of Object.keys(rec)) {
}, arr.push(val)
arrToRecord: (arr: string[]) => { }
const rec: Record<string, boolean> = {}; return arr
for (const val of arr) { },
rec[val] = true; arrToRecord: (arr: string[]) => {
} const rec: Record<string, boolean> = {}
return rec; for (const val of arr) {
} rec[val] = true
}
return rec
},
} }
export const sleep = (ms: number) => { export const sleep = (ms: number) => {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms))
};
export const fm = {
parseFMList: async (buf: ArrayBufferLike) => {
const dataView = new DataView(buf);
let offset = 4; // Identifier: 4 bytes (NZFN), not needed here
const pathLength = dataView.getUint32(offset);
offset += 4; // File Path Length: 4 bytes
const pathBuf = new Uint8Array(buf, offset, pathLength);
const path = new TextDecoder('utf-8').decode(pathBuf);
offset += pathLength; // Path: N bytes
const fmList: FMEntry[] = [];
while (offset < dataView.byteLength) {
const fileType = dataView.getUint8(offset);
offset += 1; // File Type: 1 byte
const nameLength = dataView.getUint8(offset);
offset += 1; // File Name Length: 1 byte
const nameBuf = new Uint8Array(buf, offset, nameLength);
const name = new TextDecoder('utf-8').decode(nameBuf);
offset += nameLength; // File Name: N bytes
fmList.push({
type: fileType,
name: name,
})
}
return { path, fmList };
},
buildUploadHeader: ({ path, file }: { path: string, file: File }) => {
const filePath = `${path}/${file.name}`;
// Build header (opcode + file size + path)
const filePathBytes = new TextEncoder().encode(filePath);
const header = new ArrayBuffer(1 + 8 + filePathBytes.length);
const headerView = new DataView(header);
headerView.setUint8(0, FMOpcode.Upload);
headerView.setBigUint64(1, BigInt(file.size), false);
new Uint8Array(header, 9).set(filePathBytes);
return header;
},
readFileAsArrayBuffer: async (blob: Blob): Promise<string | ArrayBuffer | null> => {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(blob);
});
},
} }
export const fmWorker = new FMWorker(); export const fm = {
parseFMList: async (buf: ArrayBufferLike) => {
const dataView = new DataView(buf)
let offset = 4 // Identifier: 4 bytes (NZFN), not needed here
const pathLength = dataView.getUint32(offset)
offset += 4 // File Path Length: 4 bytes
const pathBuf = new Uint8Array(buf, offset, pathLength)
const path = new TextDecoder("utf-8").decode(pathBuf)
offset += pathLength // Path: N bytes
const fmList: FMEntry[] = []
while (offset < dataView.byteLength) {
const fileType = dataView.getUint8(offset)
offset += 1 // File Type: 1 byte
const nameLength = dataView.getUint8(offset)
offset += 1 // File Name Length: 1 byte
const nameBuf = new Uint8Array(buf, offset, nameLength)
const name = new TextDecoder("utf-8").decode(nameBuf)
offset += nameLength // File Name: N bytes
fmList.push({
type: fileType,
name: name,
})
}
return { path, fmList }
},
buildUploadHeader: ({ path, file }: { path: string; file: File }) => {
const filePath = `${path}/${file.name}`
// Build header (opcode + file size + path)
const filePathBytes = new TextEncoder().encode(filePath)
const header = new ArrayBuffer(1 + 8 + filePathBytes.length)
const headerView = new DataView(header)
headerView.setUint8(0, FMOpcode.Upload)
headerView.setBigUint64(1, BigInt(file.size), false)
new Uint8Array(header, 9).set(filePathBytes)
return header
},
readFileAsArrayBuffer: async (blob: Blob): Promise<string | ArrayBuffer | null> => {
const reader = new FileReader()
return new Promise((resolve, reject) => {
reader.onload = () => resolve(reader.result)
reader.onerror = () => reject(reader.error)
reader.readAsArrayBuffer(blob)
})
},
}
export const fmWorker = new FMWorker()
export function formatPath(path: string) { export function formatPath(path: string) {
return path.replace(/\/{2,}/g, '/'); return path.replace(/\/{2,}/g, "/")
} }
export function joinIP(p?: ModelIP) { export function joinIP(p?: ModelIP) {
if (p) { if (p) {
if (p.ipv4_addr && p.ipv6_addr) { if (p.ipv4_addr && p.ipv6_addr) {
return `${p.ipv4_addr}/${p.ipv6_addr}`; return `${p.ipv4_addr}/${p.ipv6_addr}`
} else if (p.ipv4_addr) { } else if (p.ipv4_addr) {
return p.ipv4_addr; return p.ipv4_addr
}
return p.ipv6_addr
} }
return p.ipv6_addr; return ""
}
return '';
} }
function base64toUint8Array(base64str: string) { function base64toUint8Array(base64str: string) {
const binary = atob(base64str); const binary = atob(base64str)
const len = binary.length; const len = binary.length
const buf = new Uint8Array(len); const buf = new Uint8Array(len)
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
buf[i] = binary.charCodeAt(i); buf[i] = binary.charCodeAt(i)
} }
return buf; return buf
} }
export function ip16Str(base64str: string) { export function ip16Str(base64str: string) {
const buf = base64toUint8Array(base64str); const buf = base64toUint8Array(base64str)
const ip4 = buf.slice(-6); const ip4 = buf.slice(-6)
if (ip4[0] === 255 && ip4[1] === 255) { if (ip4[0] === 255 && ip4[1] === 255) {
return ip4.slice(2).join('.'); return ip4.slice(2).join(".")
} }
return ipv6BinaryToString(buf); return ipv6BinaryToString(buf)
} }
const digits = '0123456789abcdef'; const digits = "0123456789abcdef"
function appendHex(b: string[], x: number): void { function appendHex(b: string[], x: number): void {
if (x >= 0x1000) { if (x >= 0x1000) {
b.push(digits[(x >> 12) & 0xf]); b.push(digits[(x >> 12) & 0xf])
} }
if (x >= 0x100) { if (x >= 0x100) {
b.push(digits[(x >> 8) & 0xf]); b.push(digits[(x >> 8) & 0xf])
} }
if (x >= 0x10) { if (x >= 0x10) {
b.push(digits[(x >> 4) & 0xf]); b.push(digits[(x >> 4) & 0xf])
} }
b.push(digits[x & 0xf]); b.push(digits[x & 0xf])
} }
function ipv6BinaryToString(ip: Uint8Array): string { function ipv6BinaryToString(ip: Uint8Array): string {
let ipBytes: Uint8Array; let ipBytes: Uint8Array
if (ip.length !== 16) { if (ip.length !== 16) {
ipBytes = new Uint8Array(16); ipBytes = new Uint8Array(16)
const len = Math.min(ip.length, 16); const len = Math.min(ip.length, 16)
ipBytes.set(ip.subarray(0, len)); ipBytes.set(ip.subarray(0, len))
} else {
ipBytes = ip;
}
const hextets: number[] = [];
for (let i = 0; i < 16; i += 2) {
hextets.push((ipBytes[i] << 8) | ipBytes[i + 1]);
}
let zeroStart = -1;
let zeroLength = 0;
for (let i = 0; i <= hextets.length;) {
let j = i;
while (j < hextets.length && hextets[j] === 0) {
j++;
}
const length = j - i;
if (length >= 2 && length > zeroLength) {
zeroStart = i;
zeroLength = length;
}
if (j === i) {
i++;
} else { } else {
i = j; ipBytes = ip
}
}
const parts: string[] = [];
for (let i = 0; i < hextets.length; i++) {
if (zeroLength > 0 && i === zeroStart) {
parts.push('');
i += zeroLength - 1;
continue;
} }
if (parts.length > 0) { const hextets: number[] = []
parts.push(':'); for (let i = 0; i < 16; i += 2) {
hextets.push((ipBytes[i] << 8) | ipBytes[i + 1])
} }
const b: string[] = []; let zeroStart = -1
appendHex(b, hextets[i]); let zeroLength = 0
parts.push(b.join(''));
}
let ipv6 = parts.join(''); for (let i = 0; i <= hextets.length; ) {
let j = i
while (j < hextets.length && hextets[j] === 0) {
j++
}
const length = j - i
if (length >= 2 && length > zeroLength) {
zeroStart = i
zeroLength = length
}
if (j === i) {
i++
} else {
i = j
}
}
if (ipv6.startsWith('::')) { const parts: string[] = []
for (let i = 0; i < hextets.length; i++) {
if (zeroLength > 0 && i === zeroStart) {
parts.push("")
i += zeroLength - 1
continue
}
} else if (ipv6.startsWith(':')) { if (parts.length > 0) {
ipv6 = ':' + ipv6; parts.push(":")
} }
if (ipv6.endsWith('::')) {
} else if (ipv6.endsWith(':')) { const b: string[] = []
ipv6 = ipv6 + ':'; appendHex(b, hextets[i])
} parts.push(b.join(""))
if (ipv6 === '') { }
ipv6 = '::';
}
return ipv6; let ipv6 = parts.join("")
if (ipv6.startsWith("::")) {
} else if (ipv6.startsWith(":")) {
ipv6 = ":" + ipv6
}
if (ipv6.endsWith("::")) {
} else if (ipv6.endsWith(":")) {
ipv6 = ipv6 + ":"
}
if (ipv6 === "") {
ipv6 = "::"
}
return ipv6
} }
export async function copyToClipboard(text: string) { export async function copyToClipboard(text: string) {
try { try {
return await navigator.clipboard.writeText(text); return await navigator.clipboard.writeText(text)
} catch (error) { } catch (error) {
console.error('navigator', error); console.error("navigator", error)
} }
try { try {
return copy(text) return copy(text)
} catch (error) { } catch (error) {
console.error('copy', error); console.error("copy", error)
} }
throw new Error('Failed to copy text to clipboard'); throw new Error("Failed to copy text to clipboard")
} }

View File

@@ -1,164 +1,166 @@
{ {
"nezha": "Nezha Monitoring", "nezha": "Nezha Monitoring",
"theme": { "theme": {
"light": "Light", "light": "Light",
"dark": "Dark", "dark": "Dark",
"system": "Follow System" "system": "Follow System"
}, },
"Username": "Username", "Username": "Username",
"Password": "Password", "Password": "Password",
"Results": { "LoginFirst": "Please log in first",
"UsernameMin": "Username must be at least {{number}} characters.", "CurrentTime": "Current time",
"PasswordRequired": "Password cannot be empty.", "Results": {
"ErrorFetchingResource": "Error Fetching Resource : {{error}}", "UsernameMin": "Username must be at least {{number}} characters.",
"SelectAtLeastOneServer": "Please select at least one server.", "PasswordRequired": "Password cannot be empty.",
"UnExpectedError": "UnExpected Error, Please see the console for details.", "ErrorFetchingResource": "Error Fetching Resource : {{error}}",
"ForceUpdate": "Forced upgrade:", "SelectAtLeastOneServer": "Please select at least one server.",
"NoRowsAreSelected": "No rows are selected", "UnExpectedError": "UnExpected Error, Please see the console for details.",
"ThisOperationIsUnrecoverable": "This operation cannot be undone!", "ForceUpdate": "Forced upgrade:",
"TaskTriggeredSuccessfully": "The task triggered successfully", "NoRowsAreSelected": "No rows are selected",
"TheServerDoesNotOnline": "The server does not exist or has not been connected yet", "ThisOperationIsUnrecoverable": "This operation cannot be undone!",
"InstallHostRequired": "The Agent docking address has not been filled in in the settings.", "TaskTriggeredSuccessfully": "The task triggered successfully",
"UnknownIdentifier": "Unknown identifier" "TheServerDoesNotOnline": "The server does not exist or has not been connected yet",
}, "InstallHostRequired": "The Agent docking address has not been filled in in the settings.",
"Login": "Log in", "UnknownIdentifier": "Unknown identifier"
"Server": "Server", },
"Service": "Service", "Login": "Log in",
"Task": "Task", "Server": "Server",
"Notification": "Notification", "Service": "Service",
"DDNS": "Dynamic DNS", "Task": "Task",
"NATT": "NAT Traversal", "Notification": "Notification",
"Group": "Group", "DDNS": "Dynamic DNS",
"Profile": "Profile", "NATT": "NAT Traversal",
"Settings": "System settings", "Group": "Group",
"Logout": "Log out", "Profile": "Profile",
"NavigateTo": "Navigate to", "Settings": "System settings",
"SelectAPageToNavigateTo": "Choose a page to jump to", "Logout": "Log out",
"Close": "Close", "NavigateTo": "Navigate to",
"Error": "Error", "SelectAPageToNavigateTo": "Choose a page to jump to",
"Name": "Name", "Close": "Close",
"Version": "Version", "Error": "Error",
"Unknown": "unknown", "Name": "Name",
"Enable": "Enable", "Version": "Version",
"HideForGuest": "Hidden from visitors", "Unknown": "unknown",
"InstallCommands": "Installation command", "Enable": "Enable",
"Note": "Note", "HideForGuest": "Hidden from visitors",
"Success": "Success", "InstallCommands": "Installation command",
"Done": "Finish", "Note": "Note",
"Offline": "Offline", "Success": "Success",
"Failure": "Fail", "Done": "Finish",
"Loading": "Loading", "Offline": "Offline",
"NoResults": "No results", "Failure": "Fail",
"Actions": "Actions", "Loading": "Loading",
"EditServer": "Edit server", "NoResults": "No results",
"Weight": "Weight (the larger the number, the higher it is displayed)", "Actions": "Actions",
"DDNSProfiles": "DDNS Profile IDs", "EditServer": "Edit server",
"SeparateWithComma": "(Separate with comma)", "Weight": "Weight (the larger the number, the higher it is displayed)",
"Public": "Public", "DDNSProfiles": "DDNS Profile IDs",
"Private": "Private", "SeparateWithComma": "(Separate with comma)",
"Submit": "Submit", "Public": "Public",
"Target": "Target", "Private": "Private",
"Coverage": "Coverage", "Submit": "Submit",
"CoverAll": "Cover all", "Target": "Target",
"IgnoreAll": "Ignore all", "Coverage": "Coverage",
"SpecificServers": "Specific server", "CoverAll": "Cover all",
"Type": "Type", "IgnoreAll": "Ignore all",
"Interval": "Interval", "SpecificServers": "Specific server",
"NotifierGroupID": "Notification group ID", "Type": "Type",
"Trigger": "On Trigger", "Interval": "Interval",
"TasksToTriggerOnAlert": "The task that triggered the alert", "NotifierGroupID": "Notification group ID",
"TasksToTriggerAfterRecovery": "Tasks to be triggered after recovery", "Trigger": "On Trigger",
"Confirm": "Confirm", "TasksToTriggerOnAlert": "The task that triggered the alert",
"ConfirmDeletion": "Confirm deletion?", "TasksToTriggerAfterRecovery": "Tasks to be triggered after recovery",
"Services": "Services", "Confirm": "Confirm",
"ShowInService": "Show in Service", "ConfirmDeletion": "Confirm deletion?",
"Coverages": { "Services": "Services",
"Excludes": "Excludes specific servers", "ShowInService": "Show in Service",
"Only": "Only specific servers", "Coverages": {
"Alarmed": "Executed on the server that triggered the alarm" "Excludes": "Excludes specific servers",
}, "Only": "Only specific servers",
"EnableFailureNotification": "Enable Failure Notification", "Alarmed": "Executed on the server that triggered the alarm"
"MaximumLatency": "Maximum Latency Time (ms)", },
"MinimumLatency": "Minimum delay time (milliseconds)", "EnableFailureNotification": "Enable Failure Notification",
"EnableLatencyNotification": "Enable delayed notifications", "MaximumLatency": "Maximum Latency Time (ms)",
"EnableTriggerTask": "Enable Trigger Task", "MinimumLatency": "Minimum delay time (milliseconds)",
"CronExpression": "Cron expression", "EnableLatencyNotification": "Enable delayed notifications",
"Command": "Order", "EnableTriggerTask": "Enable Trigger Task",
"NotifierGroup": "Notification group", "CronExpression": "Cron expression",
"SendSuccessNotification": "Send success notification", "Command": "Order",
"LastExecution": "Last Execution", "NotifierGroup": "Notification group",
"Result": "Result", "SendSuccessNotification": "Send success notification",
"Scheduled": "Scheduled tasks", "LastExecution": "Last Execution",
"Notifier": "Notifier", "Result": "Result",
"AlertRule": "Alert rules", "Scheduled": "Scheduled tasks",
"VerifyTLS": "Verify TLS", "Notifier": "Notifier",
"TriggerMode": "Trigger mode", "AlertRule": "Alert rules",
"Rules": "Rules", "VerifyTLS": "Verify TLS",
"RequestMethod": "Request method", "TriggerMode": "Trigger mode",
"RequestHeader": "Request header", "Rules": "Rules",
"DoNotSendTestMessage": "Do Not Send Test Message", "RequestMethod": "Request method",
"Always": "Always", "RequestHeader": "Request header",
"Once": "Once", "DoNotSendTestMessage": "Do Not Send Test Message",
"Provider": "Provider", "Always": "Always",
"Domains": "domain name", "Once": "Once",
"MaximumRetryAttempts": "Maximum number of retries", "Provider": "Provider",
"Refresh": "Refresh", "Domains": "domain name",
"CopyPath": "Copy path", "MaximumRetryAttempts": "Maximum number of retries",
"Goto": "Go to", "Refresh": "Refresh",
"UpdateProfile": "Update profile", "CopyPath": "Copy path",
"NewUsername": "New username", "Goto": "Go to",
"OriginalPassword": "Original password", "UpdateProfile": "Update profile",
"NewPassword": "New Password", "NewUsername": "New username",
"EditDDNS": "Edit DDNS", "OriginalPassword": "Original password",
"CreateDDNS": "Create DDNS", "NewPassword": "New Password",
"Credential": "Credential", "EditDDNS": "Edit DDNS",
"RequestType": "Request type", "CreateDDNS": "Create DDNS",
"RequestBody": "Request body", "Credential": "Credential",
"FileManager": "Pseudo File Manager", "RequestType": "Request type",
"Downloading": "Downloading", "RequestBody": "Request body",
"Uploading": "Uploading", "FileManager": "Pseudo File Manager",
"EditNAT": "Edit intranet penetration", "Downloading": "Downloading",
"CreateNAT": "Create intranet penetration", "Uploading": "Uploading",
"LocalService": "Local service", "EditNAT": "Edit intranet penetration",
"BindHostname": "Bind domain name", "CreateNAT": "Create intranet penetration",
"EditServerGroup": "Edit server group", "LocalService": "Local service",
"CreateServerGroup": "Create server group", "BindHostname": "Bind domain name",
"User": "User", "EditServerGroup": "Edit server group",
"WAF": "Web application firewall", "CreateServerGroup": "Create server group",
"SiteName": "Site name", "User": "User",
"DashboardOriginalHost": "Agent docking address [domain name/IP:port]", "WAF": "Web application firewall",
"ConfigTLS": "Use TLS to connect Agent", "SiteName": "Site name",
"LoginFailed": "Login failed", "DashboardOriginalHost": "Agent docking address [domain name/IP:port]",
"BruteForceAttackingToken": "Brute Force Attacking Token", "ConfigTLS": "Use TLS to connect Agent",
"BruteForceAttackingAgentSecret": "Brute Force Attacking Agent Secret", "LoginFailed": "Login failed",
"Language": "Language", "BruteForceAttackingToken": "Brute Force Attacking Token",
"CustomCodes": "Custom Codes (Style and Script)", "BruteForceAttackingAgentSecret": "Brute Force Attacking Agent Secret",
"CustomCodesDashboard": "Custom Codes for Dashboard", "Language": "Language",
"CustomPublicDNSNameserversforDDNS": "Custom Public DNS Nameservers for DDNS", "CustomCodes": "Custom Codes (Style and Script)",
"RealIPHeader": "Real IP request header", "CustomCodesDashboard": "Custom Codes for Dashboard",
"UseDirectConnectingIP": "Use direct connection IP", "CustomPublicDNSNameserversforDDNS": "Custom Public DNS Nameservers for DDNS",
"IPChangeNotification": "IP Change notification", "RealIPHeader": "Real IP request header",
"FullIPNotification": "Show Full IP Address in Notification Messages", "UseDirectConnectingIP": "Use direct connection IP",
"EditService": "Edit service", "IPChangeNotification": "IP Change notification",
"CreateService": "Create service", "FullIPNotification": "Show Full IP Address in Notification Messages",
"EditTask": "Edit task", "EditService": "Edit service",
"CreateTask": "Create task", "CreateService": "Create service",
"CreateNotifier": "Create notification", "EditTask": "Edit task",
"EditNotifier": "Edit notification", "CreateTask": "Create task",
"EditAlertRule": "Edit alarm rules", "CreateNotifier": "Create notification",
"CreateAlertRule": "Create alert rules", "EditNotifier": "Edit notification",
"EditNotifierGroup": "Edit notification group", "EditAlertRule": "Edit alarm rules",
"CreateNotifierGroup": "Create notification group", "CreateAlertRule": "Create alert rules",
"NewUser": "New user", "EditNotifierGroup": "Edit notification group",
"Count": "Count", "CreateNotifierGroup": "Create notification group",
"LastBlockReason": "Last Block Reason", "NewUser": "New user",
"LastBlockTime": "Last ban time", "Count": "Count",
"Theme": "Theme", "LastBlockReason": "Last Block Reason",
"Author": "Author", "LastBlockTime": "Last ban time",
"Repository": "Repository", "Theme": "Theme",
"Community": "Community", "Author": "Author",
"Official": "Official", "Repository": "Repository",
"CommunityThemeWarning": "You are using a community theme", "Community": "Community",
"CommunityThemeDescription": "This theme is provided by the community, use it at your own risk", "Official": "Official",
"Cancel": "Cancel" "CommunityThemeWarning": "You are using a community theme",
"CommunityThemeDescription": "This theme is provided by the community, use it at your own risk",
"Cancel": "Cancel"
} }

View File

@@ -1,164 +1,166 @@
{ {
"nezha": "Monitoraggio Nezha", "nezha": "Monitoraggio Nezha",
"theme": { "theme": {
"light": "Chiaro", "light": "Chiaro",
"dark": "Scuro", "dark": "Scuro",
"system": "Segui il sistema" "system": "Segui il sistema"
}, },
"Username": "Nome utente", "Username": "Nome utente",
"Password": "Password", "Password": "Password",
"Results": { "LoginFirst": "Effettua prima il login",
"UsernameMin": "Il nome utente deve contenere almeno {{number}} caratteri.", "CurrentTime": "Ora attuale",
"PasswordRequired": "La password non può essere vuota.", "Results": {
"ErrorFetchingResource": "Errore nel recupero della risorsa: {{error}}", "UsernameMin": "Il nome utente deve contenere almeno {{number}} caratteri.",
"SelectAtLeastOneServer": "Seleziona almeno un server.", "PasswordRequired": "La password non può essere vuota.",
"UnExpectedError": "Errore imprevisto. Controlla la console per i dettagli.", "ErrorFetchingResource": "Errore nel recupero della risorsa: {{error}}",
"ForceUpdate": "Aggiornamento forzato:", "SelectAtLeastOneServer": "Seleziona almeno un server.",
"NoRowsAreSelected": "Nessuna riga selezionata", "UnExpectedError": "Errore imprevisto. Controlla la console per i dettagli.",
"ThisOperationIsUnrecoverable": "Questa operazione non può essere annullata!", "ForceUpdate": "Aggiornamento forzato:",
"TaskTriggeredSuccessfully": "Attività avviata correttamente", "NoRowsAreSelected": "Nessuna riga selezionata",
"TheServerDoesNotOnline": "Il server non esiste o non è stato ancora connesso", "ThisOperationIsUnrecoverable": "Questa operazione non può essere annullata!",
"InstallHostRequired": "L'indirizzo di aggancio dell'Agent non è stato inserito nelle impostazioni.", "TaskTriggeredSuccessfully": "Attività avviata correttamente",
"UnknownIdentifier": "identificatore sconosciuto" "TheServerDoesNotOnline": "Il server non esiste o non è stato ancora connesso",
}, "InstallHostRequired": "L'indirizzo di aggancio dell'Agent non è stato inserito nelle impostazioni.",
"Login": "Accedi", "UnknownIdentifier": "identificatore sconosciuto"
"Server": "Server", },
"Service": "Servizio", "Login": "Accedi",
"Task": "Compito", "Server": "Server",
"Notification": "Notifica", "Service": "Servizio",
"DDNS": "DNS Dinamico", "Task": "Compito",
"NATT": "Traversata NAT", "Notification": "Notifica",
"Group": "Gruppo", "DDNS": "DNS Dinamico",
"Profile": "Informazioni personali", "NATT": "Traversata NAT",
"Settings": "Impostazioni di sistema", "Group": "Gruppo",
"Logout": "Esci", "Profile": "Informazioni personali",
"NavigateTo": "Vai a", "Settings": "Impostazioni di sistema",
"SelectAPageToNavigateTo": "Scegli una pagina da visitare", "Logout": "Esci",
"Close": "Chiudi", "NavigateTo": "Vai a",
"Error": "Errore", "SelectAPageToNavigateTo": "Scegli una pagina da visitare",
"Name": "Nome", "Close": "Chiudi",
"Version": "Versione", "Error": "Errore",
"Unknown": "Sconosciuto", "Name": "Nome",
"Enable": "Abilita", "Version": "Versione",
"HideForGuest": "Nascosto ai visitatori", "Unknown": "Sconosciuto",
"InstallCommands": "Comando di installazione", "Enable": "Abilita",
"Note": "Osservazione", "HideForGuest": "Nascosto ai visitatori",
"Success": "Successo", "InstallCommands": "Comando di installazione",
"Done": "Fine", "Note": "Osservazione",
"Offline": "Non in linea", "Success": "Successo",
"Failure": "Fallire", "Done": "Fine",
"Loading": "Caricamento", "Offline": "Non in linea",
"NoResults": "Nessun contenuto", "Failure": "Fallire",
"Actions": "Azione", "Loading": "Caricamento",
"EditServer": "Modifica server", "NoResults": "Nessun contenuto",
"Weight": "Peso (più grande è il numero, più alto sarà visualizzato)", "Actions": "Azione",
"DDNSProfiles": "ID profilo DDNS", "EditServer": "Modifica server",
"SeparateWithComma": "(separati da virgole)", "Weight": "Peso (più grande è il numero, più alto sarà visualizzato)",
"Public": "Pubblico", "DDNSProfiles": "ID profilo DDNS",
"Private": "Privato", "SeparateWithComma": "(separati da virgole)",
"Submit": "Invia", "Public": "Pubblico",
"Target": "Bersaglio", "Private": "Privato",
"Coverage": "Copertura", "Submit": "Invia",
"CoverAll": "Copri tutto", "Target": "Bersaglio",
"IgnoreAll": "Ignorare tutto", "Coverage": "Copertura",
"SpecificServers": "Server specifico", "CoverAll": "Copri tutto",
"Type": "Tipo", "IgnoreAll": "Ignorare tutto",
"Interval": "Intervallo", "SpecificServers": "Server specifico",
"NotifierGroupID": "ID del gruppo di notifiche", "Type": "Tipo",
"Trigger": "Grilletto", "Interval": "Intervallo",
"TasksToTriggerOnAlert": "L'attività che ha attivato l'avviso", "NotifierGroupID": "ID del gruppo di notifiche",
"TasksToTriggerAfterRecovery": "Attività da attivare dopo il ripristino", "Trigger": "Grilletto",
"Confirm": "Confermo", "TasksToTriggerOnAlert": "L'attività che ha attivato l'avviso",
"ConfirmDeletion": "Confermi l'eliminazione?", "TasksToTriggerAfterRecovery": "Attività da attivare dopo il ripristino",
"Services": "Servizi", "Confirm": "Confermo",
"ShowInService": "Mostra in servizio", "ConfirmDeletion": "Confermi l'eliminazione?",
"Coverages": { "Services": "Servizi",
"Only": "Solo server specifici", "ShowInService": "Mostra in servizio",
"Excludes": "Escludi server specifici", "Coverages": {
"Alarmed": "Eseguito sul server che ha attivato l'allarme" "Only": "Solo server specifici",
}, "Excludes": "Escludi server specifici",
"EnableFailureNotification": "Abilita la notifica di errore", "Alarmed": "Eseguito sul server che ha attivato l'allarme"
"MaximumLatency": "Tempo di ritardo massimo (millisecondi)", },
"MinimumLatency": "Tempo di ritardo minimo (millisecondi)", "EnableFailureNotification": "Abilita la notifica di errore",
"EnableLatencyNotification": "Abilita le notifiche ritardate", "MaximumLatency": "Tempo di ritardo massimo (millisecondi)",
"EnableTriggerTask": "Abilita attività di attivazione", "MinimumLatency": "Tempo di ritardo minimo (millisecondi)",
"CronExpression": "Espressione cron", "EnableLatencyNotification": "Abilita le notifiche ritardate",
"Command": "Ordine", "EnableTriggerTask": "Abilita attività di attivazione",
"NotifierGroup": "Gruppo di notifica", "CronExpression": "Espressione cron",
"SendSuccessNotification": "Invia notifica di successo", "Command": "Ordine",
"LastExecution": "Ultimo giustiziato", "NotifierGroup": "Gruppo di notifica",
"Result": "Risultato", "SendSuccessNotification": "Invia notifica di successo",
"Scheduled": "Attività pianificate", "LastExecution": "Ultimo giustiziato",
"Notifier": "Notifica", "Result": "Risultato",
"AlertRule": "Regole di allerta", "Scheduled": "Attività pianificate",
"VerifyTLS": "Verifica TLS", "Notifier": "Notifica",
"TriggerMode": "Modalità di attivazione", "AlertRule": "Regole di allerta",
"Rules": "Regola", "VerifyTLS": "Verifica TLS",
"RequestMethod": "Metodo di richiesta", "TriggerMode": "Modalità di attivazione",
"RequestHeader": "Intestazione della richiesta", "Rules": "Regola",
"DoNotSendTestMessage": "Non inviare messaggi di prova", "RequestMethod": "Metodo di richiesta",
"Always": "Sempre", "RequestHeader": "Intestazione della richiesta",
"Once": "Solo una volta", "DoNotSendTestMessage": "Non inviare messaggi di prova",
"Provider": "Fornitore", "Always": "Sempre",
"Domains": "Nome di dominio", "Once": "Solo una volta",
"MaximumRetryAttempts": "Numero massimo di tentativi", "Provider": "Fornitore",
"Refresh": "aggiornare", "Domains": "Nome di dominio",
"CopyPath": "Percorso di copia", "MaximumRetryAttempts": "Numero massimo di tentativi",
"Goto": "Vai a", "Refresh": "aggiornare",
"UpdateProfile": "Aggiorna il profilo", "CopyPath": "Percorso di copia",
"NewUsername": "Nuovo nome utente", "Goto": "Vai a",
"OriginalPassword": "Password originale", "UpdateProfile": "Aggiorna il profilo",
"NewPassword": "Nuova parola d'ordine", "NewUsername": "Nuovo nome utente",
"EditDDNS": "Modifica DDNS", "OriginalPassword": "Password originale",
"CreateDDNS": "Crea DDNS", "NewPassword": "Nuova parola d'ordine",
"Credential": "Credenziale", "EditDDNS": "Modifica DDNS",
"RequestType": "Tipo di richiesta", "CreateDDNS": "Crea DDNS",
"RequestBody": "Richiedi corpo", "Credential": "Credenziale",
"FileManager": "Gestore di File Pseudo", "RequestType": "Tipo di richiesta",
"Downloading": "Download in corso", "RequestBody": "Richiedi corpo",
"Uploading": "Caricamento", "FileManager": "Gestore di File Pseudo",
"EditNAT": "Modifica la penetrazione della intranet", "Downloading": "Download in corso",
"CreateNAT": "Creare penetrazione intranet", "Uploading": "Caricamento",
"LocalService": "servizio locale", "EditNAT": "Modifica la penetrazione della intranet",
"BindHostname": "Associa il nome di dominio", "CreateNAT": "Creare penetrazione intranet",
"EditServerGroup": "Modifica gruppo di server", "LocalService": "servizio locale",
"CreateServerGroup": "Crea gruppo di server", "BindHostname": "Associa il nome di dominio",
"EditService": "Servizi di editing", "EditServerGroup": "Modifica gruppo di server",
"CreateService": "Crea servizio", "CreateServerGroup": "Crea gruppo di server",
"EditTask": "Modifica attività", "EditService": "Servizi di editing",
"CreateTask": "Crea attività", "CreateService": "Crea servizio",
"CreateNotifier": "Crea notifica", "EditTask": "Modifica attività",
"EditNotifier": "Modifica notifica", "CreateTask": "Crea attività",
"EditAlertRule": "Modifica le regole degli allarmi", "CreateNotifier": "Crea notifica",
"CreateAlertRule": "Crea regole di avviso", "EditNotifier": "Modifica notifica",
"EditNotifierGroup": "Modifica gruppo di notifiche", "EditAlertRule": "Modifica le regole degli allarmi",
"CreateNotifierGroup": "Crea gruppo di notifica", "CreateAlertRule": "Crea regole di avviso",
"User": "Utente", "EditNotifierGroup": "Modifica gruppo di notifiche",
"WAF": "Firewall dell'applicazione Web", "CreateNotifierGroup": "Crea gruppo di notifica",
"SiteName": "Nome del sito", "User": "Utente",
"Language": "Lingua", "WAF": "Firewall dell'applicazione Web",
"CustomCodes": "Codice personalizzato (stili e script)", "SiteName": "Nome del sito",
"CustomCodesDashboard": "Codice personalizzato per dashboard", "Language": "Lingua",
"DashboardOriginalHost": "Indirizzo di ancoraggio dell'agente [nome dominio/IP:porta]", "CustomCodes": "Codice personalizzato (stili e script)",
"ConfigTLS": "Usa TLS per connettere Agent", "CustomCodesDashboard": "Codice personalizzato per dashboard",
"CustomPublicDNSNameserversforDDNS": "Server dei nomi DNS pubblici personalizzati per DDNS", "DashboardOriginalHost": "Indirizzo di ancoraggio dell'agente [nome dominio/IP:porta]",
"RealIPHeader": "Intestazione della richiesta IP reale", "ConfigTLS": "Usa TLS per connettere Agent",
"UseDirectConnectingIP": "Utilizzare l'IP di connessione diretta", "CustomPublicDNSNameserversforDDNS": "Server dei nomi DNS pubblici personalizzati per DDNS",
"IPChangeNotification": "Notifica di modifica IP", "RealIPHeader": "Intestazione della richiesta IP reale",
"FullIPNotification": "Mostra l'indirizzo IP completo nei messaggi di notifica", "UseDirectConnectingIP": "Utilizzare l'IP di connessione diretta",
"LoginFailed": "Accesso non riuscito", "IPChangeNotification": "Notifica di modifica IP",
"BruteForceAttackingToken": "Segnalino di attacco di forza bruta", "FullIPNotification": "Mostra l'indirizzo IP completo nei messaggi di notifica",
"BruteForceAttackingAgentSecret": "Segreti proxy dell'attacco di forza bruta", "LoginFailed": "Accesso non riuscito",
"NewUser": "Nuovo utente", "BruteForceAttackingToken": "Segnalino di attacco di forza bruta",
"Count": "Contare", "BruteForceAttackingAgentSecret": "Segreti proxy dell'attacco di forza bruta",
"LastBlockReason": "Motivo dell'ultimo divieto", "NewUser": "Nuovo utente",
"LastBlockTime": "L'ultima volta che è stato vietato", "Count": "Contare",
"Theme": "Tema", "LastBlockReason": "Motivo dell'ultimo divieto",
"Author": "Autore", "LastBlockTime": "L'ultima volta che è stato vietato",
"Repository": "Repository", "Theme": "Tema",
"Community": "Comunità", "Author": "Autore",
"Official": "Ufficiale", "Repository": "Repository",
"CommunityThemeWarning": "Questo tema appartiene alla comunità", "Community": "Comunità",
"CommunityThemeDescription": "Questo tema viene fornito dalla comunità, utilizzalo a tuo rischio e pericolo", "Official": "Ufficiale",
"Cancel": "Annulla" "CommunityThemeWarning": "Questo tema appartiene alla comunità",
"CommunityThemeDescription": "Questo tema viene fornito dalla comunità, utilizzalo a tuo rischio e pericolo",
"Cancel": "Annulla"
} }

View File

@@ -1,164 +1,166 @@
{ {
"nezha": "哪吒监控", "nezha": "哪吒监控",
"theme": { "theme": {
"light": "亮色", "light": "亮色",
"dark": "暗色", "dark": "暗色",
"system": "跟随系统" "system": "跟随系统"
}, },
"Username": "用户名", "Username": "用户名",
"Password": "密码", "Password": "密码",
"Results": { "LoginFirst": "请先登录",
"UsernameMin": "用户名必须至少有 {{number}} 个字符。", "CurrentTime": "当前时间",
"PasswordRequired": "密码不能为空。", "Results": {
"ErrorFetchingResource": "获取资源时出错:{{error}}", "UsernameMin": "用户名必须至少有 {{number}} 个字符。",
"SelectAtLeastOneServer": "请至少选择一台服务器。", "PasswordRequired": "密码不能为空。",
"UnExpectedError": "意外错误,请查看控制台了解详细信息。", "ErrorFetchingResource": "获取资源时出错:{{error}}",
"ForceUpdate": "强制升级:", "SelectAtLeastOneServer": "请至少选择一台服务器。",
"NoRowsAreSelected": "未选择任何行", "UnExpectedError": "意外错误,请查看控制台了解详细信息。",
"ThisOperationIsUnrecoverable": "这个操作将无法恢复!", "ForceUpdate": "强制升级:",
"TaskTriggeredSuccessfully": "任务触发成功", "NoRowsAreSelected": "未选择任何行",
"TheServerDoesNotOnline": "服务器不存在或者还未连接", "ThisOperationIsUnrecoverable": "这个操作将无法恢复!",
"InstallHostRequired": "设置中尚未填写Agent对接地址。", "TaskTriggeredSuccessfully": "任务触发成功",
"UnknownIdentifier": "未知标识符" "TheServerDoesNotOnline": "服务器不存在或者还未连接",
}, "InstallHostRequired": "设置中尚未填写Agent对接地址。",
"Login": "登录", "UnknownIdentifier": "未知标识符"
"Server": "服务器", },
"Service": "服务", "Login": "登录",
"Task": "任务", "Server": "服务器",
"Notification": "通知", "Service": "服务",
"DDNS": "动态域名解析", "Task": "任务",
"NATT": "内网穿透", "Notification": "通知",
"Group": "分组", "DDNS": "动态域名解析",
"Profile": "个人信息", "NATT": "内网穿透",
"Settings": "系统设置", "Group": "分组",
"Logout": "登出", "Profile": "个人信息",
"NavigateTo": "导航至", "Settings": "系统设置",
"SelectAPageToNavigateTo": "选择一个页面跳转", "Logout": "登出",
"Close": "关闭", "NavigateTo": "导航至",
"Error": "错误", "SelectAPageToNavigateTo": "选择一个页面跳转",
"Name": "名称", "Close": "关闭",
"Version": "版本", "Error": "错误",
"Unknown": "未知", "Name": "名称",
"Enable": "启用", "Version": "版本",
"HideForGuest": "对游客隐藏", "Unknown": "未知",
"InstallCommands": "安装命令", "Enable": "启用",
"Note": "备注", "HideForGuest": "对游客隐藏",
"Success": "成功", "InstallCommands": "安装命令",
"Done": "完成", "Note": "备注",
"Offline": "离线", "Success": "成功",
"Failure": "失败", "Done": "完成",
"Loading": "加载中", "Offline": "离线",
"NoResults": "没有内容", "Failure": "失败",
"Actions": "操作", "Loading": "加载中",
"EditServer": "编辑服务器", "NoResults": "没有内容",
"Weight": "权重(数字越大,显示越靠前)", "Actions": "操作",
"DDNSProfiles": "DDNS 配置文件 ID", "EditServer": "编辑服务器",
"SeparateWithComma": "(以英文逗号分隔", "Weight": "权重(数字越大,显示越靠前",
"Public": "公开", "DDNSProfiles": "DDNS 配置文件 ID",
"Private": "私有", "SeparateWithComma": "(以英文逗号分隔)",
"Submit": "提交", "Public": "公开",
"Target": "目标", "Private": "私有",
"Coverage": "覆盖范围", "Submit": "提交",
"CoverAll": "覆盖全部", "Target": "目标",
"IgnoreAll": "忽略全部", "Coverage": "覆盖范围",
"SpecificServers": "特定服务器", "CoverAll": "覆盖全部",
"Type": "类型", "IgnoreAll": "忽略全部",
"Interval": "间隔", "SpecificServers": "特定服务器",
"NotifierGroupID": "通知组ID", "Type": "类型",
"Trigger": "触发", "Interval": "间隔",
"TasksToTriggerOnAlert": "触发警报的任务", "NotifierGroupID": "通知组ID",
"TasksToTriggerAfterRecovery": "恢复后要触发的任务", "Trigger": "触发",
"Confirm": "确认", "TasksToTriggerOnAlert": "触发警报的任务",
"ConfirmDeletion": "确认删除?", "TasksToTriggerAfterRecovery": "恢复后要触发的任务",
"Services": "服务", "Confirm": "确认",
"ShowInService": "服务中显示", "ConfirmDeletion": "确认删除?",
"Coverages": { "Services": "服务",
"Excludes": "排除特定服务器", "ShowInService": "服务中显示",
"Only": "仅特定服务器", "Coverages": {
"Alarmed": "在触发报警的服务器上执行" "Excludes": "排除特定服务器",
}, "Only": "仅特定服务器",
"EnableFailureNotification": "启用失败通知", "Alarmed": "在触发报警的服务器上执行"
"MaximumLatency": "最大延迟时间(毫秒)", },
"MinimumLatency": "最小延迟时间(毫秒)", "EnableFailureNotification": "启用失败通知",
"EnableLatencyNotification": "启用延迟通知", "MaximumLatency": "最大延迟时间(毫秒)",
"EnableTriggerTask": "启用触发任务", "MinimumLatency": "最小延迟时间(毫秒)",
"CronExpression": "Cron表达式", "EnableLatencyNotification": "启用延迟通知",
"Command": "命令", "EnableTriggerTask": "启用触发任务",
"NotifierGroup": "通知组", "CronExpression": "Cron表达式",
"SendSuccessNotification": "发送成功通知", "Command": "命令",
"LastExecution": "最后执行", "NotifierGroup": "通知组",
"Result": "结果", "SendSuccessNotification": "发送成功通知",
"Scheduled": "计划任务", "LastExecution": "最后执行",
"AlertRule": "警报规则", "Result": "结果",
"Notifier": "通知", "Scheduled": "计划任务",
"VerifyTLS": "验证 TLS", "AlertRule": "警报规则",
"TriggerMode": "触发模式", "Notifier": "通知",
"Rules": "规则", "VerifyTLS": "验证 TLS",
"RequestMethod": "请求方式", "TriggerMode": "触发模式",
"RequestHeader": "请求头", "Rules": "规则",
"DoNotSendTestMessage": "不发送测试消息", "RequestMethod": "请求方式",
"Always": "总是", "RequestHeader": "请求头",
"Once": "仅一次", "DoNotSendTestMessage": "不发送测试消息",
"Provider": "提供商", "Always": "总是",
"Domains": "域名", "Once": "仅一次",
"MaximumRetryAttempts": "最大重试次数", "Provider": "提供商",
"Refresh": "刷新", "Domains": "域名",
"CopyPath": "复制路径", "MaximumRetryAttempts": "最大重试次数",
"Goto": "前往", "Refresh": "刷新",
"UpdateProfile": "更新个人资料", "CopyPath": "复制路径",
"NewUsername": "新用户名", "Goto": "前往",
"OriginalPassword": "原始密码", "UpdateProfile": "更新个人资料",
"NewPassword": "新密码", "NewUsername": "新用户名",
"EditDDNS": "编辑DDNS", "OriginalPassword": "原始密码",
"CreateDDNS": "创建DDNS", "NewPassword": "新密码",
"Credential": "凭据", "EditDDNS": "编辑DDNS",
"RequestType": "请求类型", "CreateDDNS": "创建DDNS",
"RequestBody": "请求主体", "Credential": "凭据",
"FileManager": "文件列表", "RequestType": "请求类型",
"Downloading": "下载中", "RequestBody": "请求主体",
"Uploading": "上传中", "FileManager": "文件列表",
"EditNAT": "编辑内网穿透", "Downloading": "下载中",
"CreateNAT": "创建内网穿透", "Uploading": "上传中",
"LocalService": "本地服务", "EditNAT": "编辑内网穿透",
"BindHostname": "绑定域名", "CreateNAT": "创建内网穿透",
"EditServerGroup": "编辑服务器分组", "LocalService": "本地服务",
"CreateServerGroup": "创建服务器分组", "BindHostname": "绑定域名",
"EditService": "编辑服务", "EditServerGroup": "编辑服务器分组",
"CreateService": "创建服务", "CreateServerGroup": "创建服务器分组",
"EditTask": "编辑务", "EditService": "编辑务",
"CreateTask": "创建务", "CreateService": "创建务",
"EditNotifier": "编辑通知", "EditTask": "编辑任务",
"CreateNotifier": "创建通知", "CreateTask": "创建任务",
"EditAlertRule": "编辑报警规则", "EditNotifier": "编辑通知",
"CreateAlertRule": "创建报警规则", "CreateNotifier": "创建通知",
"EditNotifierGroup": "编辑通知分组", "EditAlertRule": "编辑报警规则",
"CreateNotifierGroup": "创建通知分组", "CreateAlertRule": "创建报警规则",
"User": "用户", "EditNotifierGroup": "编辑通知分组",
"WAF": "Web应用防火墙", "CreateNotifierGroup": "创建通知分组",
"SiteName": "站点名称", "User": "用户",
"Language": "语言", "WAF": "Web应用防火墙",
"CustomCodes": "自定义代码(样式和脚本)", "SiteName": "站点名称",
"CustomCodesDashboard": "仪表板的自定义代码", "Language": "语言",
"DashboardOriginalHost": "Agent对接地址【域名/IP:端口】", "CustomCodes": "自定义代码(样式和脚本)",
"ConfigTLS": "Agent 使用 TLS 连接", "CustomCodesDashboard": "仪表板的自定义代码",
"CustomPublicDNSNameserversforDDNS": "DDNS 的自定义公共 DNS 名称服务器", "DashboardOriginalHost": "Agent对接地址【域名/IP:端口】",
"RealIPHeader": "真实IP请求头", "ConfigTLS": "Agent 使用 TLS 连接",
"UseDirectConnectingIP": "使用直连 IP", "CustomPublicDNSNameserversforDDNS": "DDNS 的自定义公共 DNS 名称服务器",
"IPChangeNotification": "IP变更通知", "RealIPHeader": "真实IP请求头",
"FullIPNotification": "在通知消息中显示完整的 IP 地址", "UseDirectConnectingIP": "使用直连 IP",
"LoginFailed": "登录失败", "IPChangeNotification": "IP变更通知",
"BruteForceAttackingToken": "暴力攻击令牌", "FullIPNotification": "在通知消息中显示完整的 IP 地址",
"BruteForceAttackingAgentSecret": "暴力攻击代理秘密", "LoginFailed": "登录失败",
"NewUser": "新用户", "BruteForceAttackingToken": "暴力攻击令牌",
"Count": "计数", "BruteForceAttackingAgentSecret": "暴力攻击代理秘密",
"LastBlockReason": "最后封禁原因", "NewUser": "新用户",
"LastBlockTime": "最后封禁时间", "Count": "计数",
"Theme": "主题", "LastBlockReason": "最后封禁原因",
"Author": "作者", "LastBlockTime": "最后封禁时间",
"Repository": "仓库", "Theme": "主题",
"Community": "社区", "Author": "作者",
"Official": "官方", "Repository": "仓库",
"CommunityThemeWarning": "正在使用社区主题", "Community": "社区",
"CommunityThemeDescription": "社区主题未经官方审计,需自行甄别风险", "Official": "官方",
"Cancel": "取消" "CommunityThemeWarning": "正在使用社区主题",
"CommunityThemeDescription": "社区主题未经官方审计,需自行甄别风险",
"Cancel": "取消"
} }

View File

@@ -1,164 +1,166 @@
{ {
"nezha": "哪吒監控", "nezha": "哪吒監控",
"theme": { "theme": {
"light": "亮色", "light": "亮色",
"dark": "暗色", "dark": "暗色",
"system": "跟隨系統" "system": "跟隨系統"
}, },
"Username": "用戶名", "Username": "用戶名",
"Password": "密碼", "Password": "密碼",
"Results": { "LoginFirst": "請先登錄",
"UsernameMin": "使用者名稱必須至少有 {{number}} 個字元。", "CurrentTime": "當前時間",
"PasswordRequired": "密碼不能為空。", "Results": {
"ErrorFetchingResource": "取得資源時發生錯誤:{{error}}", "UsernameMin": "使用者名稱必須至少有 {{number}} 個字元。",
"SelectAtLeastOneServer": "請至少選擇一台伺服器。", "PasswordRequired": "密碼不能為空。",
"UnExpectedError": "意外錯誤,請查看控制台以了解詳細資訊。", "ErrorFetchingResource": "取得資源時發生錯誤:{{error}}",
"ForceUpdate": "強制升級:", "SelectAtLeastOneServer": "請至少選擇一台伺服器。",
"NoRowsAreSelected": "未選擇任何行", "UnExpectedError": "意外錯誤,請查看控制台以了解詳細資訊。",
"ThisOperationIsUnrecoverable": "這個操作將無法恢復!", "ForceUpdate": "強制升級:",
"TaskTriggeredSuccessfully": "任務觸發成功", "NoRowsAreSelected": "未選擇任何行",
"TheServerDoesNotOnline": "伺服器不存在或尚未連接", "ThisOperationIsUnrecoverable": "這個操作將無法恢復!",
"InstallHostRequired": "設定中尚未填寫Agent對接位址。", "TaskTriggeredSuccessfully": "任務觸發成功",
"UnknownIdentifier": "未知標識符" "TheServerDoesNotOnline": "伺服器不存在或尚未連接",
}, "InstallHostRequired": "設定中尚未填寫Agent對接位址。",
"Login": "登入", "UnknownIdentifier": "未知標識符"
"Server": "伺服器", },
"Service": "服務", "Login": "登入",
"Task": "任務", "Server": "伺服器",
"Notification": "通知", "Service": "服務",
"DDNS": "動態網域解析", "Task": "任務",
"NATT": "內網穿透", "Notification": "通知",
"Group": "分組", "DDNS": "動態網域解析",
"Profile": "個人資訊", "NATT": "內網穿透",
"Settings": "系統設定", "Group": "分組",
"Logout": "登出", "Profile": "個人資訊",
"NavigateTo": "導航至", "Settings": "系統設定",
"SelectAPageToNavigateTo": "選擇一個頁面跳轉", "Logout": "登出",
"Close": "關閉", "NavigateTo": "導航至",
"Error": "錯誤", "SelectAPageToNavigateTo": "選擇一個頁面跳轉",
"Name": "名稱", "Close": "關閉",
"Version": "版本", "Error": "錯誤",
"Unknown": "未知", "Name": "名稱",
"Enable": "啟用", "Version": "版本",
"HideForGuest": "對遊客隱藏", "Unknown": "未知",
"InstallCommands": "安裝命令", "Enable": "啟用",
"Note": "備註", "HideForGuest": "對遊客隱藏",
"Success": "成功", "InstallCommands": "安裝命令",
"Done": "完成", "Note": "備註",
"Offline": "離線", "Success": "成功",
"Failure": "失敗", "Done": "完成",
"NoResults": "沒有內容", "Offline": "離線",
"Loading": "載入中", "Failure": "失敗",
"Actions": "操作", "NoResults": "沒有內容",
"EditServer": "編輯伺服器", "Loading": "載入中",
"Weight": "權重(數字越大,顯示越前)", "Actions": "操作",
"DDNSProfiles": "DDNS 設定檔 ID", "EditServer": "編輯伺服器",
"SeparateWithComma": "(以英文逗號分隔", "Weight": "權重(數字越大,顯示越前",
"Public": "公開", "DDNSProfiles": "DDNS 設定檔 ID",
"Private": "私人", "SeparateWithComma": "(以英文逗號分隔)",
"Submit": "提交", "Public": "公開",
"Target": "目標", "Private": "私人",
"Coverage": "覆蓋範圍", "Submit": "提交",
"CoverAll": "覆蓋全部", "Target": "目標",
"IgnoreAll": "忽略全部", "Coverage": "覆蓋範圍",
"SpecificServers": "特定伺服器", "CoverAll": "覆蓋全部",
"Type": "類型", "IgnoreAll": "忽略全部",
"Interval": "間隔", "SpecificServers": "特定伺服器",
"NotifierGroupID": "通知群組ID", "Type": "類型",
"Trigger": "觸發", "Interval": "間隔",
"TasksToTriggerOnAlert": "觸發警報的任務", "NotifierGroupID": "通知群組ID",
"TasksToTriggerAfterRecovery": "恢復後要觸發的任務", "Trigger": "觸發",
"Confirm": "確認", "TasksToTriggerOnAlert": "觸發警報的任務",
"ConfirmDeletion": "確認刪除?", "TasksToTriggerAfterRecovery": "恢復後要觸發的任務",
"Services": "服務", "Confirm": "確認",
"ShowInService": "服務中顯示", "ConfirmDeletion": "確認刪除?",
"Coverages": { "Services": "服務",
"Only": "僅特定伺服器", "ShowInService": "服務中顯示",
"Excludes": "排除特定伺服器", "Coverages": {
"Alarmed": "在觸發警報的伺服器上執行" "Only": "僅特定伺服器",
}, "Excludes": "排除特定伺服器",
"EnableFailureNotification": "啟用失敗通知", "Alarmed": "在觸發警報的伺服器上執行"
"MaximumLatency": "最大延遲時間(毫秒)", },
"MinimumLatency": "最小延遲時間(毫秒)", "EnableFailureNotification": "啟用失敗通知",
"EnableLatencyNotification": "啟用延遲通知", "MaximumLatency": "最大延遲時間(毫秒)",
"EnableTriggerTask": "啟用觸發任務", "MinimumLatency": "最小延遲時間(毫秒)",
"CronExpression": "Cron表達式", "EnableLatencyNotification": "啟用延遲通知",
"Command": "命令", "EnableTriggerTask": "啟用觸發任務",
"NotifierGroup": "通知群組", "CronExpression": "Cron表達式",
"SendSuccessNotification": "發送成功通知", "Command": "命令",
"LastExecution": "最後執行", "NotifierGroup": "通知群組",
"Result": "結果", "SendSuccessNotification": "發送成功通知",
"Scheduled": "計劃任務", "LastExecution": "最後執行",
"AlertRule": "警報規則", "Result": "結果",
"Notifier": "通知", "Scheduled": "計劃任務",
"VerifyTLS": "驗證 TLS", "AlertRule": "警報規則",
"TriggerMode": "觸發模式", "Notifier": "通知",
"Rules": "規則", "VerifyTLS": "驗證 TLS",
"RequestMethod": "請求方式", "TriggerMode": "觸發模式",
"RequestHeader": "請求頭", "Rules": "規則",
"DoNotSendTestMessage": "不發送測試訊息", "RequestMethod": "請求方式",
"Always": "總是", "RequestHeader": "請求頭",
"Once": "僅一次", "DoNotSendTestMessage": "不發送測試訊息",
"Provider": "提供者", "Always": "總是",
"Domains": "網域", "Once": "僅一次",
"MaximumRetryAttempts": "最大重試次數", "Provider": "提供者",
"Refresh": "刷新", "Domains": "網域",
"CopyPath": "複製路徑", "MaximumRetryAttempts": "最大重試次數",
"Goto": "前往", "Refresh": "刷新",
"UpdateProfile": "更新個人資料", "CopyPath": "複製路徑",
"NewUsername": "新用戶名", "Goto": "前往",
"OriginalPassword": "原始密碼", "UpdateProfile": "更新個人資料",
"NewPassword": "新密碼", "NewUsername": "新用戶名",
"EditDDNS": "編輯DDNS", "OriginalPassword": "原始密碼",
"CreateDDNS": "建立DDNS", "NewPassword": "新密碼",
"Credential": "憑證", "EditDDNS": "編輯DDNS",
"RequestType": "請求類型", "CreateDDNS": "建立DDNS",
"RequestBody": "請求主體", "Credential": "憑證",
"FileManager": "檔案列表", "RequestType": "請求類型",
"Downloading": "下載中", "RequestBody": "請求主體",
"Uploading": "上傳中", "FileManager": "檔案列表",
"EditNAT": "編輯內網穿透", "Downloading": "下載中",
"CreateNAT": "創建內網穿透", "Uploading": "上傳中",
"LocalService": "本地服務", "EditNAT": "編輯內網穿透",
"BindHostname": "綁定域名", "CreateNAT": "創建內網穿透",
"EditServerGroup": "編輯伺服器分組", "LocalService": "本地服務",
"CreateServerGroup": "建立伺服器分組", "BindHostname": "綁定域名",
"EditService": "編輯服務", "EditServerGroup": "編輯伺服器分組",
"CreateService": "創建服務", "CreateServerGroup": "建立伺服器分組",
"EditTask": "編輯務", "EditService": "編輯務",
"CreateTask": "創建務", "CreateService": "創建務",
"CreateNotifier": "建立通知", "EditTask": "編輯任務",
"EditNotifier": "編輯通知", "CreateTask": "創建任務",
"EditAlertRule": "編輯警報規則", "CreateNotifier": "建立通知",
"CreateAlertRule": "建立警報規則", "EditNotifier": "編輯通知",
"EditNotifierGroup": "編輯通知分組", "EditAlertRule": "編輯警報規則",
"CreateNotifierGroup": "建立通知分組", "CreateAlertRule": "建立警報規則",
"User": "使用者", "EditNotifierGroup": "編輯通知分組",
"WAF": "Web應用防火牆", "CreateNotifierGroup": "建立通知分組",
"SiteName": "網站名稱", "User": "使用者",
"Language": "語言", "WAF": "Web應用防火牆",
"CustomCodes": "自訂程式碼(樣式和腳本)", "SiteName": "網站名稱",
"CustomCodesDashboard": "儀表板的自訂程式碼", "Language": "語言",
"DashboardOriginalHost": "Agent對接位址【網域名稱/IP:連接埠】", "CustomCodes": "自訂程式碼(樣式和腳本)",
"ConfigTLS": "Agent 使用 TLS 連線", "CustomCodesDashboard": "儀表板的自訂程式碼",
"CustomPublicDNSNameserversforDDNS": "DDNS 的自訂公共 DNS 名稱伺服器", "DashboardOriginalHost": "Agent對接位址【網域名稱/IP:連接埠】",
"RealIPHeader": "真實IP請求頭", "ConfigTLS": "Agent 使用 TLS 連線",
"UseDirectConnectingIP": "使用直連 IP", "CustomPublicDNSNameserversforDDNS": "DDNS 的自訂公共 DNS 名稱伺服器",
"IPChangeNotification": "IP變更通知", "RealIPHeader": "真實IP請求頭",
"FullIPNotification": "在通知訊息中顯示完整的 IP 位址", "UseDirectConnectingIP": "使用直連 IP",
"LoginFailed": "登入失敗", "IPChangeNotification": "IP變更通知",
"BruteForceAttackingToken": "暴力攻擊令牌", "FullIPNotification": "在通知訊息中顯示完整的 IP 位址",
"BruteForceAttackingAgentSecret": "暴力攻擊代理秘密", "LoginFailed": "登入失敗",
"NewUser": "新用戶", "BruteForceAttackingToken": "暴力攻擊令牌",
"Count": "計數", "BruteForceAttackingAgentSecret": "暴力攻擊代理秘密",
"LastBlockReason": "最後封鎖原因", "NewUser": "新用戶",
"LastBlockTime": "最後封鎖時間", "Count": "計數",
"Theme": "主題", "LastBlockReason": "最後封鎖原因",
"Author": "作者", "LastBlockTime": "最後封鎖時間",
"Repository": "仓库", "Theme": "主題",
"Community": "社群", "Author": "作者",
"Official": "官方", "Repository": "仓库",
"CommunityThemeWarning": "正在使用社區主題", "Community": "社群",
"CommunityThemeDescription": "社群主題未經官方審計,需自行甄別風險", "Official": "官方",
"Cancel": "取消" "CommunityThemeWarning": "正在使用社區主題",
"CommunityThemeDescription": "社群主題未經官方審計,需自行甄別風險",
"Cancel": "取消"
} }

View File

@@ -1,34 +1,30 @@
import { StrictMode } from 'react' import { StrictMode } from "react"
import { createRoot } from 'react-dom/client' import { createRoot } from "react-dom/client"
import { import { RouterProvider, createBrowserRouter } from "react-router-dom"
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import './index.css' import { TerminalPage } from "./components/terminal"
import './lib/i18n'; import ErrorPage from "./error-page"
import { AuthProvider } from "./hooks/useAuth"
import Root from "./routes/root"; import { NotificationProvider } from "./hooks/useNotfication"
import ErrorPage from "./error-page"; import { ServerProvider } from "./hooks/useServer"
import ProtectedRoute from './routes/protect'; import "./index.css"
import LoginPage from './routes/login'; import "./lib/i18n"
import ServerPage from './routes/server'; import AlertRulePage from "./routes/alert-rule"
import ServicePage from './routes/service'; import CronPage from "./routes/cron"
import { AuthProvider } from './hooks/useAuth'; import DDNSPage from "./routes/ddns"
import { TerminalPage } from './components/terminal'; import LoginPage from "./routes/login"
import DDNSPage from './routes/ddns'; import NATPage from "./routes/nat"
import NATPage from './routes/nat'; import NotificationPage from "./routes/notification"
import ServerGroupPage from './routes/server-group'; import NotificationGroupPage from "./routes/notification-group"
import NotificationGroupPage from './routes/notification-group'; import ProfilePage from "./routes/profile"
import { ServerProvider } from './hooks/useServer'; import ProtectedRoute from "./routes/protect"
import { NotificationProvider } from './hooks/useNotfication'; import Root from "./routes/root"
import CronPage from './routes/cron'; import ServerPage from "./routes/server"
import NotificationPage from './routes/notification'; import ServerGroupPage from "./routes/server-group"
import AlertRulePage from './routes/alert-rule'; import ServicePage from "./routes/service"
import SettingsPage from './routes/settings'; import SettingsPage from "./routes/settings"
import UserPage from './routes/user'; import UserPage from "./routes/user"
import WAFPage from './routes/waf'; import WAFPage from "./routes/waf"
import ProfilePage from './routes/profile';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -48,7 +44,11 @@ const router = createBrowserRouter([
}, },
{ {
path: "/dashboard", path: "/dashboard",
element: <ServerProvider withServerGroup><ServerPage /></ServerProvider>, element: (
<ServerProvider withServerGroup>
<ServerPage />
</ServerProvider>
),
}, },
{ {
path: "/dashboard/service", path: "/dashboard/service",
@@ -72,11 +72,19 @@ const router = createBrowserRouter([
}, },
{ {
path: "/dashboard/notification", path: "/dashboard/notification",
element: <NotificationProvider withNotifierGroup><NotificationPage /></NotificationProvider>, element: (
<NotificationProvider withNotifierGroup>
<NotificationPage />
</NotificationProvider>
),
}, },
{ {
path: "/dashboard/alert-rule", path: "/dashboard/alert-rule",
element: <NotificationProvider withNotifierGroup><AlertRulePage /></NotificationProvider>, element: (
<NotificationProvider withNotifierGroup>
<AlertRulePage />
</NotificationProvider>
),
}, },
{ {
path: "/dashboard/ddns", path: "/dashboard/ddns",
@@ -88,11 +96,19 @@ const router = createBrowserRouter([
}, },
{ {
path: "/dashboard/server-group", path: "/dashboard/server-group",
element: <ServerProvider withServer><ServerGroupPage /></ServerProvider>, element: (
<ServerProvider withServer>
<ServerGroupPage />
</ServerProvider>
),
}, },
{ {
path: "/dashboard/notification-group", path: "/dashboard/notification-group",
element: <NotificationProvider withNotifier><NotificationGroupPage /></NotificationProvider>, element: (
<NotificationProvider withNotifier>
<NotificationGroupPage />
</NotificationProvider>
),
}, },
{ {
path: "/dashboard/terminal/:id", path: "/dashboard/terminal/:id",
@@ -100,7 +116,11 @@ const router = createBrowserRouter([
}, },
{ {
path: "/dashboard/profile", path: "/dashboard/profile",
element: <ServerProvider withServer withServerGroup><ProfilePage /></ServerProvider>, element: (
<ServerProvider withServer withServerGroup>
<ProfilePage />
</ServerProvider>
),
}, },
{ {
path: "/dashboard/settings", path: "/dashboard/settings",
@@ -114,10 +134,8 @@ const router = createBrowserRouter([
path: "/dashboard/settings/waf", path: "/dashboard/settings/waf",
element: <WAFPage />, element: <WAFPage />,
}, },
] ],
}, },
]); ])
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById("root")!).render(<RouterProvider router={router} />)
<RouterProvider router={router} />
)

View File

@@ -1,5 +1,10 @@
import { swrFetcher } from "@/api/api"; import { deleteAlertRules } from "@/api/alert-rule"
import { Checkbox } from "@/components/ui/checkbox"; import { swrFetcher } from "@/api/api"
import { ActionButtonGroup } from "@/components/action-button-group"
import { AlertRuleCard } from "@/components/alert-rule"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { NotificationTab } from "@/components/notification-tab"
import { Checkbox } from "@/components/ui/checkbox"
import { import {
Table, Table,
TableBody, TableBody,
@@ -7,35 +12,29 @@ import {
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { ModelAlertRule, triggerModes } from "@/types"
import useSWR from "swr"; import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react"
import { ActionButtonGroup } from "@/components/action-button-group"; import { useTranslation } from "react-i18next"
import { HeaderButtonGroup } from "@/components/header-button-group"; import { toast } from "sonner"
import { toast } from "sonner"; import useSWR from "swr"
import { ModelAlertRule, triggerModes } from "@/types";
import { deleteAlertRules } from "@/api/alert-rule";
import { NotificationTab } from "@/components/notification-tab";
import { AlertRuleCard } from "@/components/alert-rule";
import { useTranslation } from "react-i18next";
export default function AlertRulePage() { export default function AlertRulePage() {
const { t } = useTranslation(); const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelAlertRule[]>( const { data, mutate, error, isLoading } = useSWR<ModelAlertRule[]>(
"/api/v1/alert-rule", "/api/v1/alert-rule",
swrFetcher swrFetcher,
); )
useEffect(() => { useEffect(() => {
if (error) if (error)
toast(t("Error"), { toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }), description: t("Results.ErrorFetchingResource", { error: error.message }),
}); })
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]); }, [error])
const columns: ColumnDef<ModelAlertRule>[] = [ const columns: ColumnDef<ModelAlertRule>[] = [
{ {
@@ -70,8 +69,8 @@ export default function AlertRulePage() {
accessorKey: "name", accessorKey: "name",
accessorFn: (row) => row.name, accessorFn: (row) => row.name,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>; return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>
}, },
}, },
{ {
@@ -87,8 +86,12 @@ export default function AlertRulePage() {
{ {
header: t("Rules"), header: t("Rules"),
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original
return <div className="max-w-48 whitespace-normal break-words">{JSON.stringify(s.rules)}</div>; return (
<div className="max-w-48 whitespace-normal break-words">
{JSON.stringify(s.rules)}
</div>
)
}, },
}, },
{ {
@@ -110,7 +113,7 @@ export default function AlertRulePage() {
id: "actions", id: "actions",
header: t("Actions"), header: t("Actions"),
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original
return ( return (
<ActionButtonGroup <ActionButtonGroup
className="flex gap-2" className="flex gap-2"
@@ -122,25 +125,25 @@ export default function AlertRulePage() {
> >
<AlertRuleCard mutate={mutate} data={s} /> <AlertRuleCard mutate={mutate} data={s} />
</ActionButtonGroup> </ActionButtonGroup>
); )
}, },
}, },
]; ]
const dataCache = useMemo(() => { const dataCache = useMemo(() => {
return data ?? []; return data ?? []
}, [data]); }, [data])
const table = useReactTable({ const table = useReactTable({
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}); })
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows
return ( return (
<div className="px-8"> <div className="px-3">
<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
@@ -164,9 +167,12 @@ export default function AlertRulePage() {
<TableHead key={header.id} className="text-sm"> <TableHead key={header.id} className="text-sm">
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender(header.column.columnDef.header, header.getContext())} : flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead> </TableHead>
); )
})} })}
</TableRow> </TableRow>
))} ))}
@@ -198,5 +204,5 @@ export default function AlertRulePage() {
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
); )
} }

View File

@@ -1,5 +1,9 @@
import { swrFetcher } from "@/api/api"; import { swrFetcher } from "@/api/api"
import { Checkbox } from "@/components/ui/checkbox"; import { deleteCron, runCron } from "@/api/cron"
import { ActionButtonGroup } from "@/components/action-button-group"
import { CronCard } from "@/components/cron"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { Checkbox } from "@/components/ui/checkbox"
import { import {
Table, Table,
TableBody, TableBody,
@@ -7,32 +11,29 @@ import {
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table"
import { ModelCron } from "@/types"; import { IconButton } from "@/components/xui/icon-button"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { ModelCron } from "@/types"
import useSWR from "swr"; import { cronTypes } from "@/types"
import { useEffect, useMemo } from "react"; import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { ActionButtonGroup } from "@/components/action-button-group"; import { useEffect, useMemo } from "react"
import { HeaderButtonGroup } from "@/components/header-button-group"; import { useTranslation } from "react-i18next"
import { toast } from "sonner"; import { toast } from "sonner"
import { deleteCron, runCron } from "@/api/cron"; import useSWR from "swr"
import { CronCard } from "@/components/cron";
import { cronTypes } from "@/types";
import { IconButton } from "@/components/xui/icon-button";
import { useTranslation } from "react-i18next";
export default function CronPage() { export default function CronPage() {
const { t } = useTranslation(); const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelCron[]>("/api/v1/cron", swrFetcher); const { data, mutate, error, isLoading } = useSWR<ModelCron[]>("/api/v1/cron", swrFetcher)
useEffect(() => { useEffect(() => {
if (error) if (error)
toast(t("Error"), { toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }), description: t("Results.ErrorFetchingResource", {
}); error: error.message,
// eslint-disable-next-line react-hooks/exhaustive-deps }),
}, [error]); })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelCron>[] = [ const columns: ColumnDef<ModelCron>[] = [
{ {
@@ -41,7 +42,7 @@ export default function CronPage() {
<Checkbox <Checkbox
checked={ checked={
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
@@ -66,8 +67,8 @@ export default function CronPage() {
header: t("Name"), header: t("Name"),
accessorKey: "name", accessorKey: "name",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>; return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>
}, },
}, },
{ {
@@ -84,8 +85,8 @@ export default function CronPage() {
header: t("Command"), header: t("Command"),
accessorKey: "command", accessorKey: "command",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original
return <div className="max-w-48 whitespace-normal break-words">{s.command}</div>; return <div className="max-w-48 whitespace-normal break-words">{s.command}</div>
}, },
}, },
{ {
@@ -103,24 +104,24 @@ export default function CronPage() {
accessorKey: "cover", accessorKey: "cover",
accessorFn: (row) => row.cover, accessorFn: (row) => row.cover,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original
return ( return (
<div className="max-w-48 whitespace-normal break-words"> <div className="max-w-48 whitespace-normal break-words">
{(() => { {(() => {
switch (s.cover) { switch (s.cover) {
case 0: { case 0: {
return <span>Ignore All</span>; return <span>Ignore All</span>
} }
case 1: { case 1: {
return <span>Cover All</span>; return <span>Cover All</span>
} }
case 2: { case 2: {
return <span>On alert</span>; return <span>On alert</span>
} }
} }
})()} })()}
</div> </div>
); )
}, },
}, },
{ {
@@ -133,8 +134,12 @@ export default function CronPage() {
accessorKey: "lastExecution", accessorKey: "lastExecution",
accessorFn: (row) => row.last_executed_at, accessorFn: (row) => row.last_executed_at,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original
return <div className="max-w-24 whitespace-normal break-words">{s.last_executed_at}</div>; return (
<div className="max-w-24 whitespace-normal break-words">
{s.last_executed_at}
</div>
)
}, },
}, },
{ {
@@ -146,7 +151,7 @@ export default function CronPage() {
id: "actions", id: "actions",
header: t("Actions"), header: t("Actions"),
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original
return ( return (
<ActionButtonGroup <ActionButtonGroup
className="flex gap-2" className="flex gap-2"
@@ -158,43 +163,43 @@ export default function CronPage() {
icon="play" icon="play"
onClick={async () => { onClick={async () => {
try { try {
await runCron(s.id); await runCron(s.id)
} catch (e) { } catch (e) {
console.error(e); console.error(e)
toast(t("Error"), { toast(t("Error"), {
description: t("Results.UnExpectedError"), description: t("Results.UnExpectedError"),
}); })
await mutate(); await mutate()
return; return
} }
toast(t("Success"), { toast(t("Success"), {
description: t("Results.TaskTriggeredSuccessfully"), description: t("Results.TaskTriggeredSuccessfully"),
}); })
await mutate(); await mutate()
}} }}
/> />
<CronCard mutate={mutate} data={s} /> <CronCard mutate={mutate} data={s} />
</> </>
</ActionButtonGroup> </ActionButtonGroup>
); )
}, },
}, },
]; ]
const dataCache = useMemo(() => { const dataCache = useMemo(() => {
return data ?? []; return data ?? []
}, [data]); }, [data])
const table = useReactTable({ const table = useReactTable({
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}); })
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows
return ( return (
<div className="px-8"> <div className="px-3">
<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("Task")}</h1> <h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Task")}</h1>
<HeaderButtonGroup <HeaderButtonGroup
@@ -218,9 +223,12 @@ export default function CronPage() {
<TableHead key={header.id} className="text-sm"> <TableHead key={header.id} className="text-sm">
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender(header.column.columnDef.header, header.getContext())} : flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead> </TableHead>
); )
})} })}
</TableRow> </TableRow>
))} ))}
@@ -252,5 +260,5 @@ export default function CronPage() {
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
); )
} }

Some files were not shown because too many files have changed in this diff Show More