mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-02-04 12:40:08 +00:00
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:
@@ -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: {
|
plugins: {
|
||||||
'react-hooks': reactHooks,
|
"react-hooks": reactHooks,
|
||||||
'react-refresh': reactRefresh,
|
"react-refresh": reactRefresh,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
'react-refresh/only-export-components': [
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
'warn',
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
{ allowConstantExport: true },
|
indent: ["error", 4],
|
||||||
],
|
|
||||||
"indent": ['error', 4],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
10
index.html
10
index.html
@@ -1,16 +1,14 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="shortcut icon" type="image/svg+xml" href="/logo.svg" />
|
<link rel="shortcut icon" type="image/svg+xml" href="/logo.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>哪吒监控 Nezha Monitoring</title>
|
<title>哪吒监控 Nezha Monitoring</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -37,10 +37,12 @@
|
|||||||
"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",
|
||||||
|
"framer-motion": "^11.14.1",
|
||||||
"i18next": "^24.0.2",
|
"i18next": "^24.0.2",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"jotai-zustand": "^0.6.0",
|
"jotai-zustand": "^0.6.0",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
|
"luxon": "^3.5.0",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,42 +56,47 @@ 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(
|
||||||
|
(val) => {
|
||||||
try {
|
try {
|
||||||
JSON.parse(val);
|
JSON.parse(val)
|
||||||
return true;
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
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,
|
...data,
|
||||||
rules_raw: JSON.stringify(data.rules),
|
rules_raw: JSON.stringify(data.rules),
|
||||||
} : {
|
}
|
||||||
|
: {
|
||||||
name: "",
|
name: "",
|
||||||
rules_raw: "",
|
rules_raw: "",
|
||||||
rules: [],
|
rules: [],
|
||||||
@@ -102,41 +107,40 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
|||||||
},
|
},
|
||||||
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>
|
||||||
|
|||||||
@@ -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,13 +58,15 @@ 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
|
||||||
|
? data
|
||||||
|
: {
|
||||||
name: "",
|
name: "",
|
||||||
task_type: 0,
|
task_type: 0,
|
||||||
scheduler: "",
|
scheduler: "",
|
||||||
@@ -74,45 +76,40 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
|
|||||||
},
|
},
|
||||||
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>
|
||||||
|
|||||||
@@ -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,13 +63,15 @@ 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
|
||||||
|
? data
|
||||||
|
: {
|
||||||
max_retries: 3,
|
max_retries: 3,
|
||||||
name: "",
|
name: "",
|
||||||
provider: "dummy",
|
provider: "dummy",
|
||||||
@@ -77,33 +79,28 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
|
|||||||
},
|
},
|
||||||
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>
|
||||||
|
|||||||
@@ -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))
|
||||||
|
} catch (error: any) {
|
||||||
|
toast("FM" + " " + t("Error"), {
|
||||||
|
description: error.message,
|
||||||
|
})
|
||||||
|
console.log("copy error: ", error)
|
||||||
}
|
}
|
||||||
}>{t("CopyPath")}</DropdownMenuItem>
|
}}
|
||||||
|
>
|
||||||
|
{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,64 +395,63 @@ 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) => { if (isOpen) setOpen(true); }}
|
onOpenChange={(isOpen) => {
|
||||||
|
if (isOpen) setOpen(true)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<IconButton icon="folder-closed" onClick={fetchFM} />
|
<IconButton icon="folder-closed" onClick={fetchFM} />
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent
|
<SheetContent setOpen={setOpen} className="min-w-[35%]">
|
||||||
setOpen={setOpen}
|
|
||||||
className="min-w-[35%]"
|
|
||||||
>
|
|
||||||
<div className="overflow-auto">
|
<div className="overflow-auto">
|
||||||
<SheetTitle />
|
<SheetTitle />
|
||||||
<SheetHeader className="pb-2">
|
<SheetHeader className="pb-2">
|
||||||
<SheetDescription />
|
<SheetDescription />
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
{fm?.session_id && init
|
{fm?.session_id && init ? (
|
||||||
?
|
<FMComponent
|
||||||
<FMComponent className="p-1 space-y-5" wsUrl={`/api/v1/ws/file/${fm.session_id}`} />
|
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} />
|
||||||
@@ -422,15 +462,16 @@ export const FMCard = ({ id }: { id?: string }) => {
|
|||||||
<DrawerHeader className="pb-2">
|
<DrawerHeader className="pb-2">
|
||||||
<SheetDescription />
|
<SheetDescription />
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
{fm?.session_id && init
|
{fm?.session_id && init ? (
|
||||||
?
|
<FMComponent
|
||||||
<FMComponent className="p-1 space-y-5" wsUrl={`/api/v1/ws/file/${fm.session_id}`} />
|
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>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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
|
||||||
|
variant="destructive"
|
||||||
|
icon="trash"
|
||||||
|
onClick={() => {
|
||||||
toast(t("Error"), {
|
toast(t("Error"), {
|
||||||
description: t("Results.NoRowsAreSelected")
|
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>
|
||||||
|
|||||||
@@ -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,87 +52,61 @@ 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"}
|
||||||
|
>
|
||||||
|
<Link to={profile ? "/dashboard" : "#"}>
|
||||||
|
<img className="h-7 mr-1" src="/dashboard/logo.svg" />
|
||||||
|
{t("nezha")}
|
||||||
|
</Link>
|
||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
</Card>
|
|
||||||
|
|
||||||
{
|
<div className="flex items-center gap-1">
|
||||||
profile && (
|
|
||||||
<>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard"} className={navigationMenuTriggerStyle()}>
|
|
||||||
<Link to="/dashboard">{t("Server")}</Link>
|
|
||||||
</NzNavigationMenuLink>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/service"} className={navigationMenuTriggerStyle()}>
|
|
||||||
<Link to="/dashboard/service">{t("Service")}</Link>
|
|
||||||
</NzNavigationMenuLink>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/cron"} className={navigationMenuTriggerStyle()}>
|
|
||||||
<Link to="/dashboard/cron">{t('Task')}</Link>
|
|
||||||
</NzNavigationMenuLink>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/notification" || location.pathname === "/dashboard/alert-rule"} className={navigationMenuTriggerStyle()}>
|
|
||||||
<Link to="/dashboard/notification">{t('Notification')}</Link>
|
|
||||||
</NzNavigationMenuLink>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/ddns"} className={navigationMenuTriggerStyle()}>
|
|
||||||
<Link to="/dashboard/ddns">{t('DDNS')}</Link>
|
|
||||||
</NzNavigationMenuLink>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/nat"} className={navigationMenuTriggerStyle()}>
|
|
||||||
<Link to="/dashboard/nat">{t('NATT')}</Link>
|
|
||||||
</NzNavigationMenuLink>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/server-group" || location.pathname === "/dashboard/notification-group"} className={navigationMenuTriggerStyle()}>
|
|
||||||
<Link to="/dashboard/server-group">{t('Group')}</Link>
|
|
||||||
</NzNavigationMenuLink>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</NavigationMenuList>
|
|
||||||
<div className="ml-auto flex items-center gap-1">
|
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
{
|
{profile && (
|
||||||
profile && <>
|
<>
|
||||||
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
<DropdownMenu
|
||||||
|
open={dropdownOpen}
|
||||||
|
onOpenChange={setDropdownOpen}
|
||||||
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Avatar className="ml-1 h-8 w-8 cursor-pointer border-foreground border-[1px]">
|
<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} />
|
<AvatarImage
|
||||||
|
src={
|
||||||
|
"https://api.dicebear.com/7.x/notionists/svg?seed=" +
|
||||||
|
profile.username
|
||||||
|
}
|
||||||
|
alt={profile.username}
|
||||||
|
/>
|
||||||
<AvatarFallback>{profile.username}</AvatarFallback>
|
<AvatarFallback>{profile.username}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-32">
|
<DropdownMenuContent className="w-32">
|
||||||
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
{profile.username}
|
||||||
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem onClick={() => {
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
setDropdownOpen(false)
|
setDropdownOpen(false)
|
||||||
navigate("/dashboard/profile")
|
navigate("/dashboard/profile")
|
||||||
}}
|
}}
|
||||||
@@ -131,10 +114,11 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
<User2 />
|
<User2 />
|
||||||
{t('Profile')}
|
{t("Profile")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => {
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
setDropdownOpen(false)
|
setDropdownOpen(false)
|
||||||
navigate("/dashboard/settings")
|
navigate("/dashboard/settings")
|
||||||
}}
|
}}
|
||||||
@@ -142,35 +126,119 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
<Settings />
|
<Settings />
|
||||||
{t('Settings')}
|
{t("Settings")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={logout} className="cursor-pointer">
|
<DropdownMenuItem
|
||||||
|
onClick={logout}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
<LogOut />
|
<LogOut />
|
||||||
{t('Logout')}
|
{t("Logout")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div className="flex mt-4 ml-4">
|
||||||
|
<Overview />
|
||||||
|
</div>
|
||||||
|
<div className="flex mt-4 list-none">
|
||||||
|
{profile && (
|
||||||
|
<>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NzNavigationMenuLink
|
||||||
|
asChild
|
||||||
|
active={location.pathname === "/dashboard"}
|
||||||
|
className={navigationMenuTriggerStyle()}
|
||||||
|
>
|
||||||
|
<Link to="/dashboard">{t("Server")}</Link>
|
||||||
|
</NzNavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NzNavigationMenuLink
|
||||||
|
asChild
|
||||||
|
active={location.pathname === "/dashboard/service"}
|
||||||
|
className={navigationMenuTriggerStyle()}
|
||||||
|
>
|
||||||
|
<Link to="/dashboard/service">{t("Service")}</Link>
|
||||||
|
</NzNavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NzNavigationMenuLink
|
||||||
|
asChild
|
||||||
|
active={location.pathname === "/dashboard/cron"}
|
||||||
|
className={navigationMenuTriggerStyle()}
|
||||||
|
>
|
||||||
|
<Link to="/dashboard/cron">{t("Task")}</Link>
|
||||||
|
</NzNavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NzNavigationMenuLink
|
||||||
|
asChild
|
||||||
|
active={
|
||||||
|
location.pathname === "/dashboard/notification" ||
|
||||||
|
location.pathname === "/dashboard/alert-rule"
|
||||||
}
|
}
|
||||||
|
className={navigationMenuTriggerStyle()}
|
||||||
|
>
|
||||||
|
<Link to="/dashboard/notification">{t("Notification")}</Link>
|
||||||
|
</NzNavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NzNavigationMenuLink
|
||||||
|
asChild
|
||||||
|
active={location.pathname === "/dashboard/ddns"}
|
||||||
|
className={navigationMenuTriggerStyle()}
|
||||||
|
>
|
||||||
|
<Link to="/dashboard/ddns">{t("DDNS")}</Link>
|
||||||
|
</NzNavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NzNavigationMenuLink
|
||||||
|
asChild
|
||||||
|
active={location.pathname === "/dashboard/nat"}
|
||||||
|
className={navigationMenuTriggerStyle()}
|
||||||
|
>
|
||||||
|
<Link to="/dashboard/nat">{t("NATT")}</Link>
|
||||||
|
</NzNavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NzNavigationMenuLink
|
||||||
|
asChild
|
||||||
|
active={
|
||||||
|
location.pathname === "/dashboard/server-group" ||
|
||||||
|
location.pathname === "/dashboard/notification-group"
|
||||||
|
}
|
||||||
|
className={navigationMenuTriggerStyle()}
|
||||||
|
>
|
||||||
|
<Link to="/dashboard/server-group">{t("Group")}</Link>
|
||||||
|
</NzNavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</NavigationMenu>
|
</NavigationMenu>
|
||||||
</header>
|
</header>
|
||||||
)
|
) : (
|
||||||
: (
|
|
||||||
<header className="flex border-b-2 px-4 h-16">
|
<header className="flex border-b-2 px-4 h-16">
|
||||||
<div className="flex max-w-max flex-1 items-center justify-center gap-2">
|
<div className="flex max-w-max flex-1 items-center justify-center gap-2">
|
||||||
{profile &&
|
{profile && (
|
||||||
<Drawer open={open} onOpenChange={setOpen}>
|
<Drawer open={open} onOpenChange={setOpen}>
|
||||||
<DrawerTrigger aria-label="Toggle Menu" asChild>
|
<DrawerTrigger aria-label="Toggle Menu" asChild>
|
||||||
<IconButton icon="menu" variant="ghost" />
|
<IconButton icon="menu" variant="ghost" />
|
||||||
</DrawerTrigger>
|
</DrawerTrigger>
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<DrawerHeader className="text-left">
|
<DrawerHeader className="text-left">
|
||||||
<DrawerTitle>{t('NavigateTo')}</DrawerTitle>
|
<DrawerTitle>{t("NavigateTo")}</DrawerTitle>
|
||||||
<DrawerDescription>{t('SelectAPageToNavigateTo')}</DrawerDescription>
|
<DrawerDescription>
|
||||||
|
{t("SelectAPageToNavigateTo")}
|
||||||
|
</DrawerDescription>
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<div className="grid gap-1 px-4">
|
<div className="grid gap-1 px-4">
|
||||||
{pages.slice(0).map((item, index) => (
|
{pages.slice(0).map((item, index) => (
|
||||||
@@ -178,7 +246,9 @@ export default function Header() {
|
|||||||
key={index}
|
key={index}
|
||||||
to={item.href ? item.href : "#"}
|
to={item.href ? item.href : "#"}
|
||||||
className="py-1 text-sm"
|
className="py-1 text-sm"
|
||||||
onClick={() => { setOpen(false) }}
|
onClick={() => {
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -186,24 +256,35 @@ export default function Header() {
|
|||||||
</div>
|
</div>
|
||||||
<DrawerFooter>
|
<DrawerFooter>
|
||||||
<DrawerClose asChild>
|
<DrawerClose asChild>
|
||||||
<Button variant="outline">{t('Close')}</Button>
|
<Button variant="outline">{t("Close")}</Button>
|
||||||
</DrawerClose>
|
</DrawerClose>
|
||||||
</DrawerFooter>
|
</DrawerFooter>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Card className="mx-2 my-2 flex justify-center items-center hover:bg-accent transition duration-200">
|
<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>
|
<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>
|
</Card>
|
||||||
<div className="ml-auto flex items-center gap-1">
|
<div className="ml-auto flex items-center gap-1">
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
{
|
{profile && (
|
||||||
profile && <>
|
<>
|
||||||
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Avatar className="ml-1 h-8 w-8 cursor-pointer border-foreground border-[1px]">
|
<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} />
|
<AvatarImage
|
||||||
|
src={
|
||||||
|
"https://api.dicebear.com/7.x/notionists/svg?seed=" +
|
||||||
|
profile.username
|
||||||
|
}
|
||||||
|
alt={profile.username}
|
||||||
|
/>
|
||||||
<AvatarFallback>{profile.username}</AvatarFallback>
|
<AvatarFallback>{profile.username}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -211,7 +292,8 @@ export default function Header() {
|
|||||||
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel>
|
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem onClick={() => {
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
setDropdownOpen(false)
|
setDropdownOpen(false)
|
||||||
navigate("/dashboard/profile")
|
navigate("/dashboard/profile")
|
||||||
}}
|
}}
|
||||||
@@ -219,10 +301,11 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
<User2 />
|
<User2 />
|
||||||
{t('Profile')}
|
{t("Profile")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => {
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
setDropdownOpen(false)
|
setDropdownOpen(false)
|
||||||
navigate("/dashboard/settings")
|
navigate("/dashboard/settings")
|
||||||
}}
|
}}
|
||||||
@@ -230,22 +313,68 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
<Settings />
|
<Settings />
|
||||||
{t('Settings')}
|
{t("Settings")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={logout} className="cursor-pointer">
|
<DropdownMenuItem onClick={logout} className="cursor-pointer">
|
||||||
<LogOut />
|
<LogOut />
|
||||||
{t('Logout')}
|
{t("Logout")}
|
||||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,21 +53,43 @@ 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,
|
||||||
|
{ agent_secret_key, install_host, tls }: ModelSettingResponse,
|
||||||
|
) => {
|
||||||
|
if (!install_host) throw new Error(i18next.t("Results.InstallHostRequired"))
|
||||||
|
|
||||||
if (!install_host)
|
const env = `NZ_SERVER=${install_host} NZ_TLS=${tls || false} NZ_CLIENT_SECRET=${agent_secret_key}`
|
||||||
throw new Error(i18next.t("Results.InstallHostRequired"));
|
const env_win = `$env:NZ_SERVER=\"${install_host}\";$env:NZ_TLS=\"${tls || false}\";$env: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}\";`;
|
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case OSTypes.Linux:
|
case OSTypes.Linux:
|
||||||
@@ -79,7 +100,7 @@ const generateCommand = (type: number, { agent_secret_key, install_host, tls }:
|
|||||||
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}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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,13 +39,15 @@ 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
|
||||||
|
? data
|
||||||
|
: {
|
||||||
name: "",
|
name: "",
|
||||||
server_id: 0,
|
server_id: 0,
|
||||||
host: "",
|
host: "",
|
||||||
@@ -54,33 +55,28 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
|
|||||||
},
|
},
|
||||||
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
name: data.group.name,
|
||||||
notifications: data.notifications,
|
notifications: data.notifications,
|
||||||
} : {
|
}
|
||||||
|
: {
|
||||||
name: "",
|
name: "",
|
||||||
notifications: [],
|
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>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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,13 +56,15 @@ 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
|
||||||
|
? data
|
||||||
|
: {
|
||||||
name: "",
|
name: "",
|
||||||
url: "",
|
url: "",
|
||||||
request_method: 1,
|
request_method: 1,
|
||||||
@@ -72,33 +74,30 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
|||||||
},
|
},
|
||||||
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
name: data.group.name,
|
||||||
servers: data.servers,
|
servers: data.servers,
|
||||||
} : {
|
}
|
||||||
|
: {
|
||||||
name: "",
|
name: "",
|
||||||
servers: [],
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,16 +68,18 @@ 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,
|
...data,
|
||||||
skip_servers_raw: conv.recordToStrArr(data.skip_servers ? data.skip_servers : {}),
|
skip_servers_raw: conv.recordToStrArr(data.skip_servers ? data.skip_servers : {}),
|
||||||
} : {
|
}
|
||||||
|
: {
|
||||||
type: 1,
|
type: 1,
|
||||||
cover: 0,
|
cover: 0,
|
||||||
name: "",
|
name: "",
|
||||||
@@ -93,47 +95,46 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
|||||||
},
|
},
|
||||||
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>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
JSON.stringify({
|
||||||
Rows: dimensions.rows,
|
Rows: dimensions.rows,
|
||||||
Cols: dimensions.cols,
|
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} />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
|
|||||||
<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}
|
||||||
@@ -35,7 +34,7 @@ const AlertDialogContent = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -43,29 +42,14 @@ const AlertDialogContent = React.forwardRef<
|
|||||||
))
|
))
|
||||||
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,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -93,18 +77,13 @@ const AlertDialogDescription = React.forwardRef<
|
|||||||
{...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
|
||||||
|
|
||||||
@@ -114,11 +93,7 @@ const AlertDialogCancel = React.forwardRef<
|
|||||||
>(({ 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" }),
|
|
||||||
"mt-2 sm:mt-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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>,
|
||||||
@@ -9,10 +8,7 @@ const Avatar = React.forwardRef<
|
|||||||
>(({ 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",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -38,7 +34,7 @@ const AvatarFallback = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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",
|
||||||
@@ -20,7 +19,7 @@ const badgeVariants = cva(
|
|||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
@@ -28,9 +27,7 @@ export interface BadgeProps
|
|||||||
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 }
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
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,
|
||||||
@@ -12,31 +11,25 @@ const Breadcrumb = React.forwardRef<
|
|||||||
>(({ ...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">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ol
|
<ol
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||||
className
|
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<
|
||||||
@@ -57,10 +50,8 @@ const BreadcrumbLink = React.forwardRef<
|
|||||||
})
|
})
|
||||||
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">
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<span
|
<span
|
||||||
ref={ref}
|
ref={ref}
|
||||||
role="link"
|
role="link"
|
||||||
@@ -69,14 +60,11 @@ const BreadcrumbPage = React.forwardRef<
|
|||||||
className={cn("font-normal text-foreground", className)}
|
className={cn("font-normal text-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
),
|
||||||
|
)
|
||||||
BreadcrumbPage.displayName = "BreadcrumbPage"
|
BreadcrumbPage.displayName = "BreadcrumbPage"
|
||||||
|
|
||||||
const BreadcrumbSeparator = ({
|
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"li">) => (
|
|
||||||
<li
|
<li
|
||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -88,10 +76,7 @@ const BreadcrumbSeparator = ({
|
|||||||
)
|
)
|
||||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
||||||
|
|
||||||
const BreadcrumbEllipsis = ({
|
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) => (
|
|
||||||
<span
|
<span
|
||||||
role="presentation"
|
role="presentation"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
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:
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
@@ -30,7 +27,7 @@ const buttonVariants = cva(
|
|||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
@@ -49,7 +46,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
Button.displayName = "Button"
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
|||||||
@@ -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<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
|
||||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...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>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<h3
|
<h3
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
|
||||||
"text-2xl font-semibold leading-none tracking-tight",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...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>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
))
|
),
|
||||||
|
)
|
||||||
CardContent.displayName = "CardContent"
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<
|
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 }
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
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>,
|
||||||
@@ -12,7 +11,7 @@ const Checkbox = React.forwardRef<
|
|||||||
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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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,36 +9,25 @@ 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
|
|
||||||
>(({
|
|
||||||
options,
|
|
||||||
placeholder,
|
|
||||||
defaultValue,
|
|
||||||
className,
|
|
||||||
onValueChange,
|
|
||||||
...props
|
|
||||||
}, ref) => {
|
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
const [value, setValue] = React.useState(defaultValue)
|
const [value, setValue] = React.useState(defaultValue)
|
||||||
|
|
||||||
@@ -55,34 +40,31 @@ export const Combobox = React.forwardRef<
|
|||||||
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
|
{value ? (
|
||||||
? (() => {
|
(() => {
|
||||||
const val = options.find((option) => option.value === value)?.label
|
const val = options.find((option) => option.value === value)?.label
|
||||||
return (
|
return val ? (
|
||||||
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" />
|
<ChevronDown className="ml-auto opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
<Command
|
<Command
|
||||||
filter={(value, search, keywords = []) => {
|
filter={(value, search, keywords = []) => {
|
||||||
const extendValue = value + " " + keywords.join(" ");
|
const extendValue = value + " " + keywords.join(" ")
|
||||||
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
|
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
|
||||||
return 1;
|
return 1
|
||||||
}
|
}
|
||||||
return 0;
|
return 0
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CommandInput placeholder={placeholder} className="h-9" />
|
<CommandInput placeholder={placeholder} className="h-9" />
|
||||||
@@ -96,14 +78,18 @@ export const Combobox = React.forwardRef<
|
|||||||
keywords={[option.label]}
|
keywords={[option.label]}
|
||||||
onSelect={(currentValue) => {
|
onSelect={(currentValue) => {
|
||||||
setValue(currentValue === value ? "" : currentValue)
|
setValue(currentValue === value ? "" : currentValue)
|
||||||
onValueChange(currentValue === value ? "" : currentValue)
|
onValueChange(
|
||||||
|
currentValue === value ? "" : currentValue,
|
||||||
|
)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
"justify-start",
|
"justify-start",
|
||||||
value === option.value ? "opacity-100" : "opacity-0"
|
value === option.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
@@ -115,4 +101,5 @@ export const Combobox = React.forwardRef<
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
});
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
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>,
|
||||||
@@ -14,7 +13,7 @@ const Command = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -43,7 +42,7 @@ const CommandInput = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -69,11 +68,7 @@ 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
|
||||||
@@ -86,7 +81,7 @@ const CommandGroup = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -114,7 +109,7 @@ const CommandItem = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -122,16 +117,10 @@ const CommandItem = React.forwardRef<
|
|||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||||
|
|
||||||
const CommandShortcut = ({
|
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ const DialogOverlay = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -37,7 +36,7 @@ const DialogContent = React.forwardRef<
|
|||||||
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}
|
||||||
>
|
>
|
||||||
@@ -51,29 +50,17 @@ const DialogContent = React.forwardRef<
|
|||||||
))
|
))
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
DialogHeader.displayName = "DialogHeader"
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
const DialogFooter = ({
|
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -85,10 +72,7 @@ const DialogTitle = React.forwardRef<
|
|||||||
>(({ 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",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
@@ -42,7 +38,7 @@ const DrawerContent = React.forwardRef<
|
|||||||
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}
|
||||||
>
|
>
|
||||||
@@ -53,25 +49,13 @@ const DrawerContent = React.forwardRef<
|
|||||||
))
|
))
|
||||||
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"
|
||||||
|
|
||||||
@@ -81,10 +65,7 @@ const DrawerTitle = React.forwardRef<
|
|||||||
>(({ 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",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||||||
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}
|
||||||
>
|
>
|
||||||
@@ -35,8 +34,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||||||
<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>,
|
||||||
@@ -46,13 +44,12 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||||||
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>,
|
||||||
@@ -63,8 +60,8 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -83,7 +80,7 @@ const DropdownMenuItem = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -98,7 +95,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||||||
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}
|
||||||
@@ -111,8 +108,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||||||
{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>,
|
||||||
@@ -122,7 +118,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
|||||||
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}
|
||||||
>
|
>
|
||||||
@@ -144,11 +140,7 @@ const DropdownMenuLabel = React.forwardRef<
|
|||||||
>(({ 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",
|
|
||||||
inset && "pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -166,15 +158,9 @@ const DropdownMenuSeparator = React.forwardRef<
|
|||||||
))
|
))
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
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,
|
||||||
@@ -10,25 +12,20 @@ import {
|
|||||||
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>) => {
|
||||||
@@ -66,14 +63,10 @@ type FormItemContextValue = {
|
|||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
|
||||||
{} as FormItemContextValue
|
|
||||||
)
|
|
||||||
|
|
||||||
const FormItem = React.forwardRef<
|
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
HTMLDivElement,
|
({ className, ...props }, ref) => {
|
||||||
React.HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
const id = React.useId()
|
const id = React.useId()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -81,7 +74,8 @@ const FormItem = React.forwardRef<
|
|||||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||||
</FormItemContext.Provider>
|
</FormItemContext.Provider>
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
FormItem.displayName = "FormItem"
|
FormItem.displayName = "FormItem"
|
||||||
|
|
||||||
const FormLabel = React.forwardRef<
|
const FormLabel = React.forwardRef<
|
||||||
@@ -112,9 +106,7 @@ const FormControl = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
id={formItemId}
|
id={formItemId}
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
!error
|
!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
|
||||||
? `${formDescriptionId}`
|
|
||||||
: `${formDescriptionId} ${formMessageId}`
|
|
||||||
}
|
}
|
||||||
aria-invalid={!!error}
|
aria-invalid={!!error}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
|
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) => {
|
||||||
@@ -12,13 +10,13 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
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>,
|
||||||
@@ -11,10 +10,7 @@ const NavigationMenu = React.forwardRef<
|
|||||||
>(({ 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",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -31,7 +27,7 @@ const NavigationMenuList = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -41,7 +37,7 @@ 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<
|
||||||
@@ -70,7 +66,7 @@ const NavigationMenuContent = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -87,15 +83,14 @@ const NavigationMenuViewport = React.forwardRef<
|
|||||||
<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>,
|
||||||
@@ -105,15 +100,14 @@ const NavigationMenuIndicator = React.forwardRef<
|
|||||||
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,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ const PopoverContent = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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>,
|
||||||
@@ -30,11 +29,9 @@ const ScrollBar = React.forwardRef<
|
|||||||
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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ const SelectTrigger = React.forwardRef<
|
|||||||
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}
|
||||||
>
|
>
|
||||||
@@ -36,10 +35,7 @@ const SelectScrollUpButton = React.forwardRef<
|
|||||||
>(({ 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",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronUp className="h-4 w-4" />
|
<ChevronUp className="h-4 w-4" />
|
||||||
@@ -53,17 +49,13 @@ const SelectScrollDownButton = React.forwardRef<
|
|||||||
>(({ 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",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</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>,
|
||||||
@@ -76,7 +68,7 @@ const SelectContent = React.forwardRef<
|
|||||||
"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}
|
||||||
@@ -86,7 +78,7 @@ const SelectContent = React.forwardRef<
|
|||||||
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}
|
||||||
@@ -117,7 +109,7 @@ const SelectItem = React.forwardRef<
|
|||||||
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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
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}
|
||||||
@@ -18,12 +13,11 @@ const Separator = React.forwardRef<
|
|||||||
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 }
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -12,13 +12,11 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
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:
|
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
|
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<
|
|
||||||
HTMLTableElement,
|
|
||||||
React.HTMLAttributes<HTMLTableElement>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<div className="relative w-full overflow-auto">
|
<div className="relative w-full overflow-auto">
|
||||||
<table
|
<table
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -13,7 +10,8 @@ const Table = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
),
|
||||||
|
)
|
||||||
Table.displayName = "Table"
|
Table.displayName = "Table"
|
||||||
|
|
||||||
const TableHeader = React.forwardRef<
|
const TableHeader = React.forwardRef<
|
||||||
@@ -28,11 +26,7 @@ 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"
|
||||||
|
|
||||||
@@ -42,28 +36,24 @@ const TableFooter = React.forwardRef<
|
|||||||
>(({ 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",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...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>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<tr
|
<tr
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
),
|
||||||
|
)
|
||||||
TableRow.displayName = "TableRow"
|
TableRow.displayName = "TableRow"
|
||||||
|
|
||||||
const TableHead = React.forwardRef<
|
const TableHead = React.forwardRef<
|
||||||
@@ -74,7 +64,7 @@ const TableHead = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -97,21 +87,8 @@ 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,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -13,7 +12,7 @@ const TabsList = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -28,7 +27,7 @@ const TabsTrigger = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
@@ -43,7 +42,7 @@ const TabsContent = React.forwardRef<
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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<
|
|
||||||
HTMLTextAreaElement,
|
|
||||||
React.ComponentProps<"textarea">
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
"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",
|
"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",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
Textarea.displayName = "Textarea"
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbEllipsis,
|
BreadcrumbEllipsis,
|
||||||
@@ -16,37 +15,45 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { formatPath } from "@/lib/utils"
|
import { formatPath } from "@/lib/utils"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
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
|
||||||
|
className="cursor-pointer hover:text-white transition"
|
||||||
|
onClick={() => {
|
||||||
|
setPath("/")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{"/"}
|
||||||
|
</p>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator />
|
<BreadcrumbSeparator />
|
||||||
{items.length > ITEMS_TO_DISPLAY ? (
|
{items.length > ITEMS_TO_DISPLAY ? (
|
||||||
@@ -63,7 +70,11 @@ export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => {
|
|||||||
<DropdownMenuContent align="start">
|
<DropdownMenuContent align="start">
|
||||||
{items.slice(0, -ITEMS_TO_DISPLAY).map((item, index) => (
|
{items.slice(0, -ITEMS_TO_DISPLAY).map((item, index) => (
|
||||||
<DropdownMenuItem key={index}>
|
<DropdownMenuItem key={index}>
|
||||||
<p onClick={() => { setPath(item.href) }}>
|
<p
|
||||||
|
onClick={() => {
|
||||||
|
setPath(item.href)
|
||||||
|
}}
|
||||||
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</p>
|
</p>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -82,7 +93,9 @@ export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => {
|
|||||||
<>
|
<>
|
||||||
<p
|
<p
|
||||||
className="max-w-20 truncate md:max-w-none cursor-pointer hover:text-white transition"
|
className="max-w-20 truncate md:max-w-none cursor-pointer hover:text-white transition"
|
||||||
onClick={() => { setPath(item.href) }}
|
onClick={() => {
|
||||||
|
setPath(item.href)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,25 +22,8 @@
|
|||||||
* 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 {
|
|
||||||
CheckIcon,
|
|
||||||
ChevronDown,
|
|
||||||
XIcon,
|
|
||||||
WandSparkles,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -49,7 +32,13 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command"
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { type VariantProps, cva } from "class-variance-authority"
|
||||||
|
import { CheckIcon, ChevronDown, WandSparkles, XIcon } from "lucide-react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variants for the multi-select component to handle different styles.
|
* Variants for the multi-select component to handle different styles.
|
||||||
@@ -60,8 +49,7 @@ const multiSelectVariants = cva(
|
|||||||
{
|
{
|
||||||
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:
|
||||||
@@ -72,8 +60,8 @@ const multiSelectVariants = cva(
|
|||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for MultiSelect component
|
* Props for MultiSelect component
|
||||||
@@ -87,64 +75,61 @@ interface MultiSelectProps
|
|||||||
*/
|
*/
|
||||||
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,
|
options,
|
||||||
@@ -159,73 +144,66 @@ export const MultiSelect = React.forwardRef<
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const [selectedValues, setSelectedValues] =
|
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue)
|
||||||
React.useState<string[]>(defaultValue);
|
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false)
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
|
const [isAnimating, setIsAnimating] = React.useState(false)
|
||||||
const [isAnimating, setIsAnimating] = React.useState(false);
|
|
||||||
|
|
||||||
const handleInputKeyDown = (
|
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
event: React.KeyboardEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
setIsPopoverOpen(true);
|
setIsPopoverOpen(true)
|
||||||
} else if (event.key === "Backspace" && !event.currentTarget.value) {
|
} else if (event.key === "Backspace" && !event.currentTarget.value) {
|
||||||
const newSelectedValues = [...selectedValues];
|
const newSelectedValues = [...selectedValues]
|
||||||
newSelectedValues.pop();
|
newSelectedValues.pop()
|
||||||
setSelectedValues(newSelectedValues);
|
setSelectedValues(newSelectedValues)
|
||||||
onValueChange(newSelectedValues);
|
onValueChange(newSelectedValues)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const toggleOption = (option: string) => {
|
const toggleOption = (option: string) => {
|
||||||
const newSelectedValues = selectedValues.includes(option)
|
const newSelectedValues = selectedValues.includes(option)
|
||||||
? selectedValues.filter((value) => value !== option)
|
? selectedValues.filter((value) => value !== option)
|
||||||
: [...selectedValues, option];
|
: [...selectedValues, option]
|
||||||
setSelectedValues(newSelectedValues);
|
setSelectedValues(newSelectedValues)
|
||||||
onValueChange(newSelectedValues);
|
onValueChange(newSelectedValues)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
setSelectedValues([]);
|
setSelectedValues([])
|
||||||
onValueChange([]);
|
onValueChange([])
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleTogglePopover = () => {
|
const handleTogglePopover = () => {
|
||||||
setIsPopoverOpen((prev) => !prev);
|
setIsPopoverOpen((prev) => !prev)
|
||||||
};
|
}
|
||||||
|
|
||||||
const clearExtraOptions = () => {
|
const clearExtraOptions = () => {
|
||||||
const newSelectedValues = selectedValues.slice(0, maxCount);
|
const newSelectedValues = selectedValues.slice(0, maxCount)
|
||||||
setSelectedValues(newSelectedValues);
|
setSelectedValues(newSelectedValues)
|
||||||
onValueChange(newSelectedValues);
|
onValueChange(newSelectedValues)
|
||||||
};
|
}
|
||||||
|
|
||||||
const toggleAll = () => {
|
const toggleAll = () => {
|
||||||
if (selectedValues.length === options.length) {
|
if (selectedValues.length === options.length) {
|
||||||
handleClear();
|
handleClear()
|
||||||
} else {
|
} else {
|
||||||
const allValues = options.map((option) => option.value);
|
const allValues = options.map((option) => option.value)
|
||||||
setSelectedValues(allValues);
|
setSelectedValues(allValues)
|
||||||
onValueChange(allValues);
|
onValueChange(allValues)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const stopWheelEventPropagation: React.WheelEventHandler = (e) => {
|
const stopWheelEventPropagation: React.WheelEventHandler = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
};
|
}
|
||||||
|
|
||||||
const stopTouchMoveEventPropagation: React.TouchEventHandler = (e) => {
|
const stopTouchMoveEventPropagation: React.TouchEventHandler = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal={modalPopover}>
|
||||||
open={isPopoverOpen}
|
|
||||||
onOpenChange={setIsPopoverOpen}
|
|
||||||
modal={modalPopover}
|
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -233,21 +211,21 @@ export const MultiSelect = React.forwardRef<
|
|||||||
onClick={handleTogglePopover}
|
onClick={handleTogglePopover}
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{selectedValues.length > 0 ? (
|
{selectedValues.length > 0 ? (
|
||||||
<div className="flex justify-between items-center w-full">
|
<div className="flex justify-between items-center w-full">
|
||||||
<div className="flex flex-wrap items-center">
|
<div className="flex flex-wrap items-center">
|
||||||
{selectedValues.slice(0, maxCount).map((value) => {
|
{selectedValues.slice(0, maxCount).map((value) => {
|
||||||
const option = options.find((o) => o.value === value);
|
const option = options.find((o) => o.value === value)
|
||||||
const IconComponent = option?.icon;
|
const IconComponent = option?.icon
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
key={value}
|
key={value}
|
||||||
className={cn(
|
className={cn(
|
||||||
isAnimating ? "animate-bounce" : "",
|
isAnimating ? "animate-bounce" : "",
|
||||||
multiSelectVariants({ variant })
|
multiSelectVariants({ variant }),
|
||||||
)}
|
)}
|
||||||
style={{ animationDuration: `${animation}s` }}
|
style={{ animationDuration: `${animation}s` }}
|
||||||
>
|
>
|
||||||
@@ -258,19 +236,19 @@ export const MultiSelect = React.forwardRef<
|
|||||||
<XIcon
|
<XIcon
|
||||||
className="ml-2 h-2 w-2 cursor-pointer"
|
className="ml-2 h-2 w-2 cursor-pointer"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation()
|
||||||
toggleOption(value);
|
toggleOption(value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
{selectedValues.length > maxCount && (
|
{selectedValues.length > maxCount && (
|
||||||
<Badge
|
<Badge
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
|
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
|
||||||
isAnimating ? "animate-bounce" : "",
|
isAnimating ? "animate-bounce" : "",
|
||||||
multiSelectVariants({ variant })
|
multiSelectVariants({ variant }),
|
||||||
)}
|
)}
|
||||||
style={{ animationDuration: `${animation}s` }}
|
style={{ animationDuration: `${animation}s` }}
|
||||||
>
|
>
|
||||||
@@ -278,8 +256,8 @@ export const MultiSelect = React.forwardRef<
|
|||||||
<XIcon
|
<XIcon
|
||||||
className="ml-2 h-2 w-2 cursor-pointer"
|
className="ml-2 h-2 w-2 cursor-pointer"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation()
|
||||||
clearExtraOptions();
|
clearExtraOptions()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -289,8 +267,8 @@ export const MultiSelect = React.forwardRef<
|
|||||||
<XIcon
|
<XIcon
|
||||||
className="h-4 mx-2 cursor-pointer text-muted-foreground"
|
className="h-4 mx-2 cursor-pointer text-muted-foreground"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation()
|
||||||
handleClear();
|
handleClear()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Separator
|
<Separator
|
||||||
@@ -318,10 +296,7 @@ export const MultiSelect = React.forwardRef<
|
|||||||
onTouchMove={stopTouchMoveEventPropagation}
|
onTouchMove={stopTouchMoveEventPropagation}
|
||||||
>
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput placeholder="Search..." onKeyDown={handleInputKeyDown} />
|
||||||
placeholder="Search..."
|
|
||||||
onKeyDown={handleInputKeyDown}
|
|
||||||
/>
|
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
@@ -335,7 +310,7 @@ export const MultiSelect = React.forwardRef<
|
|||||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||||
selectedValues.length === options.length
|
selectedValues.length === options.length
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary text-primary-foreground"
|
||||||
: "opacity-50 [&_svg]:invisible"
|
: "opacity-50 [&_svg]:invisible",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckIcon className="h-4 w-4" />
|
||||||
@@ -343,7 +318,7 @@ export const MultiSelect = React.forwardRef<
|
|||||||
<span>(Select All)</span>
|
<span>(Select All)</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const isSelected = selectedValues.includes(option.value);
|
const isSelected = selectedValues.includes(option.value)
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
@@ -355,7 +330,7 @@ export const MultiSelect = React.forwardRef<
|
|||||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||||
isSelected
|
isSelected
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary text-primary-foreground"
|
||||||
: "opacity-50 [&_svg]:invisible"
|
: "opacity-50 [&_svg]:invisible",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckIcon className="h-4 w-4" />
|
||||||
@@ -365,7 +340,7 @@ export const MultiSelect = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
@@ -400,14 +375,14 @@ export const MultiSelect = React.forwardRef<
|
|||||||
<WandSparkles
|
<WandSparkles
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer my-2 text-foreground bg-background w-3 h-3",
|
"cursor-pointer my-2 text-foreground bg-background w-3 h-3",
|
||||||
isAnimating ? "" : "text-muted-foreground"
|
isAnimating ? "" : "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
onClick={() => setIsAnimating(!isAnimating)}
|
onClick={() => setIsAnimating(!isAnimating)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
)
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
MultiSelect.displayName = "MultiSelect";
|
MultiSelect.displayName = "MultiSelect"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -19,22 +18,22 @@ const sheetVariants = cva(
|
|||||||
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:
|
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",
|
||||||
"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: {
|
defaultVariants: {
|
||||||
side: "right",
|
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>,
|
||||||
@@ -48,7 +47,12 @@ const SheetContent = React.forwardRef<
|
|||||||
>
|
>
|
||||||
{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
|
||||||
|
className="h-4 w-4"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
@@ -56,29 +60,14 @@ const SheetContent = React.forwardRef<
|
|||||||
))
|
))
|
||||||
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,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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',
|
{
|
||||||
|
id: "type",
|
||||||
desc: true,
|
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 }}>
|
||||||
@@ -162,7 +162,8 @@ export function DataTable<TData, TValue>({
|
|||||||
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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
notifiers: notifiers,
|
||||||
notifierGroup: notifierGroup,
|
notifierGroup: notifierGroup,
|
||||||
}), [notifiers, 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)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
servers: server,
|
||||||
serverGroups: serverGroup,
|
serverGroups: serverGroup,
|
||||||
}), [server, 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)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,11 +28,11 @@
|
|||||||
--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%;
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
--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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,14 +76,13 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
238
src/lib/utils.ts
238
src/lib/utils.ts
@@ -1,92 +1,96 @@
|
|||||||
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
|
||||||
|
},
|
||||||
|
{} as Record<string, boolean>,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
arrToStr: <T>(arr: T[]) => {
|
arrToStr: <T>(arr: T[]) => {
|
||||||
return arr.join(',');
|
return arr.join(",")
|
||||||
},
|
},
|
||||||
strToArr: (str: string) => {
|
strToArr: (str: string) => {
|
||||||
return str.split(',').filter(Boolean) || [];
|
return str.split(",").filter(Boolean) || []
|
||||||
},
|
},
|
||||||
recordToArr: <T>(rec: Record<string, T>) => {
|
recordToArr: <T>(rec: Record<string, T>) => {
|
||||||
const arr: T[] = [];
|
const arr: T[] = []
|
||||||
for (const val of Object.values(rec)) {
|
for (const val of Object.values(rec)) {
|
||||||
arr.push(val);
|
arr.push(val)
|
||||||
}
|
}
|
||||||
return arr;
|
return arr
|
||||||
},
|
},
|
||||||
recordToStrArr: <T>(rec: Record<string, T>) => {
|
recordToStrArr: <T>(rec: Record<string, T>) => {
|
||||||
const arr: string[] = [];
|
const arr: string[] = []
|
||||||
for (const val of Object.keys(rec)) {
|
for (const val of Object.keys(rec)) {
|
||||||
arr.push(val);
|
arr.push(val)
|
||||||
}
|
}
|
||||||
return arr;
|
return arr
|
||||||
},
|
},
|
||||||
arrToRecord: (arr: string[]) => {
|
arrToRecord: (arr: string[]) => {
|
||||||
const rec: Record<string, boolean> = {};
|
const rec: Record<string, boolean> = {}
|
||||||
for (const val of arr) {
|
for (const val of arr) {
|
||||||
rec[val] = true;
|
rec[val] = true
|
||||||
}
|
|
||||||
return rec;
|
|
||||||
}
|
}
|
||||||
|
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 = {
|
export const fm = {
|
||||||
parseFMList: async (buf: ArrayBufferLike) => {
|
parseFMList: async (buf: ArrayBufferLike) => {
|
||||||
const dataView = new DataView(buf);
|
const dataView = new DataView(buf)
|
||||||
let offset = 4; // Identifier: 4 bytes (NZFN), not needed here
|
let offset = 4 // Identifier: 4 bytes (NZFN), not needed here
|
||||||
|
|
||||||
const pathLength = dataView.getUint32(offset);
|
const pathLength = dataView.getUint32(offset)
|
||||||
offset += 4; // File Path Length: 4 bytes
|
offset += 4 // File Path Length: 4 bytes
|
||||||
|
|
||||||
const pathBuf = new Uint8Array(buf, offset, pathLength);
|
const pathBuf = new Uint8Array(buf, offset, pathLength)
|
||||||
const path = new TextDecoder('utf-8').decode(pathBuf);
|
const path = new TextDecoder("utf-8").decode(pathBuf)
|
||||||
offset += pathLength; // Path: N bytes
|
offset += pathLength // Path: N bytes
|
||||||
|
|
||||||
const fmList: FMEntry[] = [];
|
const fmList: FMEntry[] = []
|
||||||
while (offset < dataView.byteLength) {
|
while (offset < dataView.byteLength) {
|
||||||
const fileType = dataView.getUint8(offset);
|
const fileType = dataView.getUint8(offset)
|
||||||
offset += 1; // File Type: 1 byte
|
offset += 1 // File Type: 1 byte
|
||||||
|
|
||||||
const nameLength = dataView.getUint8(offset);
|
const nameLength = dataView.getUint8(offset)
|
||||||
offset += 1; // File Name Length: 1 byte
|
offset += 1 // File Name Length: 1 byte
|
||||||
|
|
||||||
const nameBuf = new Uint8Array(buf, offset, nameLength);
|
const nameBuf = new Uint8Array(buf, offset, nameLength)
|
||||||
const name = new TextDecoder('utf-8').decode(nameBuf);
|
const name = new TextDecoder("utf-8").decode(nameBuf)
|
||||||
offset += nameLength; // File Name: N bytes
|
offset += nameLength // File Name: N bytes
|
||||||
|
|
||||||
fmList.push({
|
fmList.push({
|
||||||
type: fileType,
|
type: fileType,
|
||||||
@@ -94,169 +98,167 @@ export const fm = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return { path, fmList };
|
return { path, fmList }
|
||||||
},
|
},
|
||||||
|
|
||||||
buildUploadHeader: ({ path, file }: { path: string, file: File }) => {
|
buildUploadHeader: ({ path, file }: { path: string; file: File }) => {
|
||||||
const filePath = `${path}/${file.name}`;
|
const filePath = `${path}/${file.name}`
|
||||||
|
|
||||||
// Build header (opcode + file size + path)
|
// Build header (opcode + file size + path)
|
||||||
const filePathBytes = new TextEncoder().encode(filePath);
|
const filePathBytes = new TextEncoder().encode(filePath)
|
||||||
const header = new ArrayBuffer(1 + 8 + filePathBytes.length);
|
const header = new ArrayBuffer(1 + 8 + filePathBytes.length)
|
||||||
const headerView = new DataView(header);
|
const headerView = new DataView(header)
|
||||||
|
|
||||||
headerView.setUint8(0, FMOpcode.Upload);
|
headerView.setUint8(0, FMOpcode.Upload)
|
||||||
headerView.setBigUint64(1, BigInt(file.size), false);
|
headerView.setBigUint64(1, BigInt(file.size), false)
|
||||||
|
|
||||||
new Uint8Array(header, 9).set(filePathBytes);
|
new Uint8Array(header, 9).set(filePathBytes)
|
||||||
return header;
|
return header
|
||||||
},
|
},
|
||||||
|
|
||||||
readFileAsArrayBuffer: async (blob: Blob): Promise<string | ArrayBuffer | null> => {
|
readFileAsArrayBuffer: async (blob: Blob): Promise<string | ArrayBuffer | null> => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader()
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
reader.onload = () => resolve(reader.result);
|
reader.onload = () => resolve(reader.result)
|
||||||
reader.onerror = () => reject(reader.error);
|
reader.onerror = () => reject(reader.error)
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.readAsArrayBuffer(blob)
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fmWorker = new FMWorker();
|
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 {
|
} else {
|
||||||
ipBytes = ip;
|
ipBytes = ip
|
||||||
}
|
}
|
||||||
|
|
||||||
const hextets: number[] = [];
|
const hextets: number[] = []
|
||||||
for (let i = 0; i < 16; i += 2) {
|
for (let i = 0; i < 16; i += 2) {
|
||||||
hextets.push((ipBytes[i] << 8) | ipBytes[i + 1]);
|
hextets.push((ipBytes[i] << 8) | ipBytes[i + 1])
|
||||||
}
|
}
|
||||||
|
|
||||||
let zeroStart = -1;
|
let zeroStart = -1
|
||||||
let zeroLength = 0;
|
let zeroLength = 0
|
||||||
|
|
||||||
for (let i = 0; i <= hextets.length;) {
|
for (let i = 0; i <= hextets.length; ) {
|
||||||
let j = i;
|
let j = i
|
||||||
while (j < hextets.length && hextets[j] === 0) {
|
while (j < hextets.length && hextets[j] === 0) {
|
||||||
j++;
|
j++
|
||||||
}
|
}
|
||||||
const length = j - i;
|
const length = j - i
|
||||||
if (length >= 2 && length > zeroLength) {
|
if (length >= 2 && length > zeroLength) {
|
||||||
zeroStart = i;
|
zeroStart = i
|
||||||
zeroLength = length;
|
zeroLength = length
|
||||||
}
|
}
|
||||||
if (j === i) {
|
if (j === i) {
|
||||||
i++;
|
i++
|
||||||
} else {
|
} else {
|
||||||
i = j;
|
i = j
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts: string[] = [];
|
const parts: string[] = []
|
||||||
for (let i = 0; i < hextets.length; i++) {
|
for (let i = 0; i < hextets.length; i++) {
|
||||||
if (zeroLength > 0 && i === zeroStart) {
|
if (zeroLength > 0 && i === zeroStart) {
|
||||||
parts.push('');
|
parts.push("")
|
||||||
i += zeroLength - 1;
|
i += zeroLength - 1
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
parts.push(':');
|
parts.push(":")
|
||||||
}
|
}
|
||||||
|
|
||||||
const b: string[] = [];
|
const b: string[] = []
|
||||||
appendHex(b, hextets[i]);
|
appendHex(b, hextets[i])
|
||||||
parts.push(b.join(''));
|
parts.push(b.join(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
let ipv6 = parts.join('');
|
let ipv6 = parts.join("")
|
||||||
|
|
||||||
if (ipv6.startsWith('::')) {
|
if (ipv6.startsWith("::")) {
|
||||||
|
} else if (ipv6.startsWith(":")) {
|
||||||
} else if (ipv6.startsWith(':')) {
|
ipv6 = ":" + ipv6
|
||||||
ipv6 = ':' + ipv6;
|
|
||||||
}
|
}
|
||||||
if (ipv6.endsWith('::')) {
|
if (ipv6.endsWith("::")) {
|
||||||
|
} else if (ipv6.endsWith(":")) {
|
||||||
} else if (ipv6.endsWith(':')) {
|
ipv6 = ipv6 + ":"
|
||||||
ipv6 = ipv6 + ':';
|
|
||||||
}
|
}
|
||||||
if (ipv6 === '') {
|
if (ipv6 === "") {
|
||||||
ipv6 = '::';
|
ipv6 = "::"
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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")
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
},
|
},
|
||||||
"Username": "Username",
|
"Username": "Username",
|
||||||
"Password": "Password",
|
"Password": "Password",
|
||||||
|
"LoginFirst": "Please log in first",
|
||||||
|
"CurrentTime": "Current time",
|
||||||
"Results": {
|
"Results": {
|
||||||
"UsernameMin": "Username must be at least {{number}} characters.",
|
"UsernameMin": "Username must be at least {{number}} characters.",
|
||||||
"PasswordRequired": "Password cannot be empty.",
|
"PasswordRequired": "Password cannot be empty.",
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
},
|
},
|
||||||
"Username": "Nome utente",
|
"Username": "Nome utente",
|
||||||
"Password": "Password",
|
"Password": "Password",
|
||||||
|
"LoginFirst": "Effettua prima il login",
|
||||||
|
"CurrentTime": "Ora attuale",
|
||||||
"Results": {
|
"Results": {
|
||||||
"UsernameMin": "Il nome utente deve contenere almeno {{number}} caratteri.",
|
"UsernameMin": "Il nome utente deve contenere almeno {{number}} caratteri.",
|
||||||
"PasswordRequired": "La password non può essere vuota.",
|
"PasswordRequired": "La password non può essere vuota.",
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
},
|
},
|
||||||
"Username": "用户名",
|
"Username": "用户名",
|
||||||
"Password": "密码",
|
"Password": "密码",
|
||||||
|
"LoginFirst": "请先登录",
|
||||||
|
"CurrentTime": "当前时间",
|
||||||
"Results": {
|
"Results": {
|
||||||
"UsernameMin": "用户名必须至少有 {{number}} 个字符。",
|
"UsernameMin": "用户名必须至少有 {{number}} 个字符。",
|
||||||
"PasswordRequired": "密码不能为空。",
|
"PasswordRequired": "密码不能为空。",
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
},
|
},
|
||||||
"Username": "用戶名",
|
"Username": "用戶名",
|
||||||
"Password": "密碼",
|
"Password": "密碼",
|
||||||
|
"LoginFirst": "請先登錄",
|
||||||
|
"CurrentTime": "當前時間",
|
||||||
"Results": {
|
"Results": {
|
||||||
"UsernameMin": "使用者名稱必須至少有 {{number}} 個字元。",
|
"UsernameMin": "使用者名稱必須至少有 {{number}} 個字元。",
|
||||||
"PasswordRequired": "密碼不能為空。",
|
"PasswordRequired": "密碼不能為空。",
|
||||||
|
|||||||
100
src/main.tsx
100
src/main.tsx
@@ -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} />
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [error]);
|
}, [error])
|
||||||
|
|
||||||
const columns: ColumnDef<ModelCron>[] = [
|
const columns: ColumnDef<ModelCron>[] = [
|
||||||
{
|
{
|
||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { swrFetcher } from "@/api/api";
|
import { swrFetcher } from "@/api/api"
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { deleteDDNSProfiles, getDDNSProviders } from "@/api/ddns"
|
||||||
import { DDNSCard } from "@/components/ddns";
|
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||||
|
import { DDNSCard } from "@/components/ddns"
|
||||||
|
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -8,38 +11,39 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table"
|
||||||
import { ModelDDNSProfile } from "@/types";
|
import { ModelDDNSProfile } from "@/types"
|
||||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||||
import useSWR from "swr";
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useTranslation } from "react-i18next"
|
||||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
import { toast } from "sonner"
|
||||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
import useSWR from "swr"
|
||||||
import { toast } from "sonner";
|
|
||||||
import { deleteDDNSProfiles, getDDNSProviders } from "@/api/ddns";
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export default function DDNSPage() {
|
export default function DDNSPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const { data, mutate, error, isLoading } = useSWR<ModelDDNSProfile[]>("/api/v1/ddns", swrFetcher);
|
const { data, mutate, error, isLoading } = useSWR<ModelDDNSProfile[]>(
|
||||||
const [providers, setProviders] = useState<string[]>([]);
|
"/api/v1/ddns",
|
||||||
|
swrFetcher,
|
||||||
|
)
|
||||||
|
const [providers, setProviders] = useState<string[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchProviders = async () => {
|
const fetchProviders = async () => {
|
||||||
const fetchedProviders = await getDDNSProviders();
|
const fetchedProviders = await getDDNSProviders()
|
||||||
setProviders(fetchedProviders);
|
setProviders(fetchedProviders)
|
||||||
};
|
}
|
||||||
fetchProviders();
|
fetchProviders()
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
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<ModelDDNSProfile>[] = [
|
const columns: ColumnDef<ModelDDNSProfile>[] = [
|
||||||
{
|
{
|
||||||
@@ -74,8 +78,8 @@ export default function DDNSPage() {
|
|||||||
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-24 whitespace-normal break-words">{s.name}</div>;
|
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -94,12 +98,12 @@ export default function DDNSPage() {
|
|||||||
accessorFn: (row) => row.provider,
|
accessorFn: (row) => row.provider,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: t('Domains'),
|
header: t("Domains"),
|
||||||
accessorKey: "domains",
|
accessorKey: "domains",
|
||||||
accessorFn: (row) => row.domains,
|
accessorFn: (row) => row.domains,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original
|
||||||
return <div className="max-w-24 whitespace-normal break-words">{s.domains}</div>;
|
return <div className="max-w-24 whitespace-normal break-words">{s.domains}</div>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -111,33 +115,37 @@ export default function DDNSPage() {
|
|||||||
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"
|
||||||
delete={{ fn: deleteDDNSProfiles, id: s.id, mutate: mutate }}
|
delete={{
|
||||||
|
fn: deleteDDNSProfiles,
|
||||||
|
id: s.id,
|
||||||
|
mutate: mutate,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DDNSCard mutate={mutate} data={s} providers={providers} />
|
<DDNSCard mutate={mutate} data={s} providers={providers} />
|
||||||
</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("DDNS")}</h1>
|
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("DDNS")}</h1>
|
||||||
<HeaderButtonGroup
|
<HeaderButtonGroup
|
||||||
@@ -161,9 +169,12 @@ export default function DDNSPage() {
|
|||||||
<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>
|
||||||
))}
|
))}
|
||||||
@@ -195,5 +206,5 @@ export default function DDNSPage() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import { z } from "zod"
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -13,9 +9,11 @@ import {
|
|||||||
} from "@/components/ui/form"
|
} from "@/components/ui/form"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { useAuth } from "@/hooks/useAuth"
|
import { useAuth } from "@/hooks/useAuth"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import i18next from "i18next"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import i18next from "i18next";
|
import { z } from "zod"
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
username: z.string().min(2, {
|
username: z.string().min(2, {
|
||||||
@@ -23,10 +21,9 @@ const formSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
password: z.string().min(1, {
|
password: z.string().min(1, {
|
||||||
message: i18next.t("Results.PasswordRequired"),
|
message: i18next.t("Results.PasswordRequired"),
|
||||||
})
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
const { login } = useAuth()
|
const { login } = useAuth()
|
||||||
|
|
||||||
@@ -42,10 +39,10 @@ function Login() {
|
|||||||
login(values.username, values.password)
|
login(values.username, values.password)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-8 max-w-xl m-auto">
|
<div className="mt-28 max-w-sm m-auto">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -68,7 +65,12 @@ function Login() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t("Password")}</FormLabel>
|
<FormLabel>{t("Password")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="admin" autoComplete="current-password" {...field} />
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="admin"
|
||||||
|
autoComplete="current-password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -81,4 +83,4 @@ function Login() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Login;
|
export default Login
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { swrFetcher } from "@/api/api";
|
import { swrFetcher } from "@/api/api"
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { deleteNAT } from "@/api/nat"
|
||||||
import { NATCard } from "@/components/nat";
|
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||||
|
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||||
|
import { NATCard } from "@/components/nat"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -8,29 +11,27 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table"
|
||||||
import { ModelNAT } from "@/types";
|
import { ModelNAT } from "@/types"
|
||||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||||
import useSWR from "swr";
|
import { useEffect, useMemo } from "react"
|
||||||
import { useEffect, useMemo } from "react";
|
import { useTranslation } from "react-i18next"
|
||||||
import { ActionButtonGroup } from "@/components/action-button-group";
|
import { toast } from "sonner"
|
||||||
import { HeaderButtonGroup } from "@/components/header-button-group";
|
import useSWR from "swr"
|
||||||
import { toast } from "sonner";
|
|
||||||
import { deleteNAT } from "@/api/nat";
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export default function NATPage() {
|
export default function NATPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>("/api/v1/nat", swrFetcher);
|
const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>("/api/v1/nat", 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<ModelNAT>[] = [
|
const columns: ColumnDef<ModelNAT>[] = [
|
||||||
{
|
{
|
||||||
@@ -65,12 +66,12 @@ export default function NATPage() {
|
|||||||
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>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: t("Server")+" ID",
|
header: t("Server") + " ID",
|
||||||
accessorKey: "serverID",
|
accessorKey: "serverID",
|
||||||
accessorFn: (row) => row.server_id,
|
accessorFn: (row) => row.server_id,
|
||||||
},
|
},
|
||||||
@@ -79,8 +80,8 @@ export default function NATPage() {
|
|||||||
accessorKey: "host",
|
accessorKey: "host",
|
||||||
accessorFn: (row) => row.host,
|
accessorFn: (row) => row.host,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original
|
||||||
return <div className="max-w-32 whitespace-normal break-words">{s.host}</div>;
|
return <div className="max-w-32 whitespace-normal break-words">{s.host}</div>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -88,15 +89,15 @@ export default function NATPage() {
|
|||||||
accessorKey: "domain",
|
accessorKey: "domain",
|
||||||
accessorFn: (row) => row.domain,
|
accessorFn: (row) => row.domain,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original
|
||||||
return <div className="max-w-32 whitespace-normal break-words">{s.domain}</div>;
|
return <div className="max-w-32 whitespace-normal break-words">{s.domain}</div>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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"
|
||||||
@@ -104,25 +105,25 @@ export default function NATPage() {
|
|||||||
>
|
>
|
||||||
<NATCard mutate={mutate} data={s} />
|
<NATCard 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("NATT")}</h1>
|
<h1 className="flex-1 text-3xl font-bold tracking-tight"> {t("NATT")}</h1>
|
||||||
<HeaderButtonGroup
|
<HeaderButtonGroup
|
||||||
@@ -146,9 +147,12 @@ export default function NATPage() {
|
|||||||
<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>
|
||||||
))}
|
))}
|
||||||
@@ -180,5 +184,5 @@ export default function NATPage() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { swrFetcher } from "@/api/api";
|
import { swrFetcher } from "@/api/api"
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { deleteNotificationGroups } from "@/api/notification-group"
|
||||||
|
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||||
|
import { GroupTab } from "@/components/group-tab"
|
||||||
|
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||||
|
import { NotificationGroupCard } from "@/components/notification-group"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -7,34 +12,30 @@ 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 { ModelNotificationGroupResponseItem } 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 { ModelNotificationGroupResponseItem } from "@/types";
|
|
||||||
import { deleteNotificationGroups } from "@/api/notification-group";
|
|
||||||
import { GroupTab } from "@/components/group-tab";
|
|
||||||
import { NotificationGroupCard } from "@/components/notification-group";
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export default function NotificationGroupPage() {
|
export default function NotificationGroupPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const { data, mutate, error, isLoading } = useSWR<ModelNotificationGroupResponseItem[]>(
|
const { data, mutate, error, isLoading } = useSWR<ModelNotificationGroupResponseItem[]>(
|
||||||
"/api/v1/notification-group",
|
"/api/v1/notification-group",
|
||||||
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<ModelNotificationGroupResponseItem>[] = [
|
const columns: ColumnDef<ModelNotificationGroupResponseItem>[] = [
|
||||||
{
|
{
|
||||||
@@ -69,12 +70,12 @@ export default function NotificationGroupPage() {
|
|||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
accessorFn: (row) => row.group.name,
|
accessorFn: (row) => row.group.name,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original
|
||||||
return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>;
|
return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: t("Notifier")+"(ID)",
|
header: t("Notifier") + "(ID)",
|
||||||
accessorKey: "notifications",
|
accessorKey: "notifications",
|
||||||
accessorFn: (row) => row.notifications,
|
accessorFn: (row) => row.notifications,
|
||||||
},
|
},
|
||||||
@@ -82,7 +83,7 @@ export default function NotificationGroupPage() {
|
|||||||
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"
|
||||||
@@ -94,25 +95,25 @@ export default function NotificationGroupPage() {
|
|||||||
>
|
>
|
||||||
<NotificationGroupCard mutate={mutate} data={s} />
|
<NotificationGroupCard 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">
|
||||||
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||||
<HeaderButtonGroup
|
<HeaderButtonGroup
|
||||||
@@ -136,9 +137,12 @@ export default function NotificationGroupPage() {
|
|||||||
<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>
|
||||||
))}
|
))}
|
||||||
@@ -170,5 +174,5 @@ export default function NotificationGroupPage() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { swrFetcher } from "@/api/api";
|
import { swrFetcher } from "@/api/api"
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { deleteNotification } from "@/api/notification"
|
||||||
|
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||||
|
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||||
|
import { NotificationTab } from "@/components/notification-tab"
|
||||||
|
import { NotifierCard } from "@/components/notifier"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -7,37 +12,32 @@ 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 { useNotification } from "@/hooks/useNotfication"
|
||||||
import useSWR from "swr";
|
import { ModelNotification } 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 { ModelNotification } from "@/types";
|
import useSWR from "swr"
|
||||||
import { deleteNotification } from "@/api/notification";
|
|
||||||
import { NotificationTab } from "@/components/notification-tab";
|
|
||||||
import { NotifierCard } from "@/components/notifier";
|
|
||||||
import { useNotification } from "@/hooks/useNotfication";
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
|
|
||||||
export default function NotificationPage() {
|
export default function NotificationPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation()
|
||||||
const { data, mutate, error, isLoading } = useSWR<ModelNotification[]>(
|
const { data, mutate, error, isLoading } = useSWR<ModelNotification[]>(
|
||||||
"/api/v1/notification",
|
"/api/v1/notification",
|
||||||
swrFetcher
|
swrFetcher,
|
||||||
);
|
)
|
||||||
const { notifierGroup } = useNotification();
|
const { notifierGroup } = useNotification()
|
||||||
|
|
||||||
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<ModelNotification>[] = [
|
const columns: ColumnDef<ModelNotification>[] = [
|
||||||
{
|
{
|
||||||
@@ -72,8 +72,8 @@ export default function NotificationPage() {
|
|||||||
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>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -84,7 +84,7 @@ export default function NotificationPage() {
|
|||||||
notifierGroup
|
notifierGroup
|
||||||
?.filter((ng) => ng.notifications?.includes(row.id))
|
?.filter((ng) => ng.notifications?.includes(row.id))
|
||||||
.map((ng) => ng.group.id) || []
|
.map((ng) => ng.group.id) || []
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -92,8 +92,8 @@ export default function NotificationPage() {
|
|||||||
accessorKey: "url",
|
accessorKey: "url",
|
||||||
accessorFn: (row) => row.url,
|
accessorFn: (row) => row.url,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original
|
||||||
return <div className="max-w-64 whitespace-normal break-words">{s.url}</div>;
|
return <div className="max-w-64 whitespace-normal break-words">{s.url}</div>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -105,7 +105,7 @@ export default function NotificationPage() {
|
|||||||
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"
|
||||||
@@ -117,25 +117,25 @@ export default function NotificationPage() {
|
|||||||
>
|
>
|
||||||
<NotifierCard mutate={mutate} data={s} />
|
<NotifierCard 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
|
||||||
@@ -159,9 +159,12 @@ export default function NotificationPage() {
|
|||||||
<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>
|
||||||
))}
|
))}
|
||||||
@@ -193,5 +196,5 @@ export default function NotificationPage() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,45 @@
|
|||||||
|
import { ProfileCard } from "@/components/profile"
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { useMainStore } from "@/hooks/useMainStore"
|
import { useMainStore } from "@/hooks/useMainStore"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { useMediaQuery } from "@/hooks/useMediaQuery"
|
||||||
import {
|
import { useServer } from "@/hooks/useServer"
|
||||||
Card,
|
import { Boxes, Server } from "lucide-react"
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card"
|
|
||||||
import { useMediaQuery } from "@/hooks/useMediaQuery";
|
|
||||||
import { Server, Boxes } from "lucide-react";
|
|
||||||
import { useServer } from "@/hooks/useServer";
|
|
||||||
import { ProfileCard } from "@/components/profile";
|
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const { profile } = useMainStore();
|
const { profile } = useMainStore()
|
||||||
const { servers, serverGroups } = useServer();
|
const { servers, serverGroups } = useServer()
|
||||||
const isDesktop = useMediaQuery("(min-width: 890px)")
|
const isDesktop = useMediaQuery("(min-width: 890px)")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
profile && (
|
profile && (
|
||||||
<div className={`flex p-8 gap-4 ${isDesktop ? 'ml-6' : 'flex-col'}`}>
|
<div className={`flex p-8 gap-4 ${isDesktop ? "ml-6" : "flex-col"}`}>
|
||||||
<div className={`flex ${isDesktop ? 'flex-col mr-6' : 'gap-4 w-full items-center'}`}>
|
<div
|
||||||
<Avatar className={`${isDesktop ? 'h-[300px] w-[300px]' : 'h-[150px] w-[150px]'} border-foreground border-[1px]`}>
|
className={`flex ${isDesktop ? "flex-col mr-6" : "gap-4 w-full items-center"}`}
|
||||||
<AvatarImage src={'https://api.dicebear.com/7.x/notionists/svg?seed=' + profile.username} alt={profile.username} />
|
>
|
||||||
|
<Avatar
|
||||||
|
className={`${isDesktop ? "h-[300px] w-[300px]" : "h-[150px] w-[150px]"} border-foreground border-[1px]`}
|
||||||
|
>
|
||||||
|
<AvatarImage
|
||||||
|
src={
|
||||||
|
"https://api.dicebear.com/7.x/notionists/svg?seed=" +
|
||||||
|
profile.username
|
||||||
|
}
|
||||||
|
alt={profile.username}
|
||||||
|
/>
|
||||||
<AvatarFallback>{profile.username}</AvatarFallback>
|
<AvatarFallback>{profile.username}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<p className="justify-center text-3xl font-semibold">{profile.username}</p>
|
<p className="justify-center text-3xl font-semibold">{profile.username}</p>
|
||||||
<p className="text-gray-400">IP: {profile.login_ip || 'Unknown'}</p>
|
<p className="text-gray-400">IP: {profile.login_ip || "Unknown"}</p>
|
||||||
</div>
|
</div>
|
||||||
{isDesktop &&
|
{isDesktop && (
|
||||||
<ProfileCard className="flex mt-4 justify-center items-center max-w-[300px] rounded-lg" />
|
<ProfileCard className="flex mt-4 justify-center items-center max-w-[300px] rounded-lg" />
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isDesktop &&
|
{!isDesktop && (
|
||||||
<ProfileCard className="flex justify-center items-center max-w-full rounded-lg" />
|
<ProfileCard className="flex justify-center items-center max-w-full rounded-lg" />
|
||||||
}
|
)}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
@@ -61,5 +66,5 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { Navigate } from "react-router-dom";
|
import { useAuth } from "@/hooks/useAuth"
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { Navigate } from "react-router-dom"
|
||||||
|
|
||||||
export const ProtectedRoute = ({ children }: {
|
export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||||
children: React.ReactNode;
|
const { profile } = useAuth()
|
||||||
}) => {
|
|
||||||
const { profile } = useAuth();
|
|
||||||
|
|
||||||
if (!profile && window.location.pathname !== "/dashboard/login") {
|
if (!profile && window.location.pathname !== "/dashboard/login") {
|
||||||
return <><Navigate to="/dashboard/login" />{children}</>;
|
return (
|
||||||
|
<>
|
||||||
|
<Navigate to="/dashboard/login" />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
return children
|
||||||
};
|
}
|
||||||
|
|
||||||
export default ProtectedRoute;
|
export default ProtectedRoute
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user