diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 5936834..0000000 --- a/.prettierignore +++ /dev/null @@ -1,8 +0,0 @@ -# npm -**/node_modules/* -package.json -package-lock.json -yarn.lock - -# build -dist/* \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index d9b4102..0000000 --- a/.prettierrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "printWidth": 120, - "semi": true, - "singleQuote": true, - "quoteProps": "as-needed", - "jsxSingleQuote": false, - "trailingComma": "es5", - "bracketSpacing": true, - "jsxBracketSameLine": false, - "requirePragma": false, - "insertPragma": false, - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "css", - "endOfLine": "lf" -} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..0579168 --- /dev/null +++ b/biome.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true, + "defaultBranch": "main" + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noUnusedTemplateLiteral": "warn", + "noNonNullAssertion": "warn" + }, + "suspicious": { + "noExplicitAny": "warn", + "noArrayIndexKey": "warn" + }, + "a11y": { + "useKeyWithClickEvents": "off", + "noSvgWithoutTitle": "off", + "noLabelWithoutControl": "off" + }, + "complexity": { + "noForEach": "off" + }, + "security": { + "noDangerouslySetInnerHtml": "warn" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index a830662..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,30 +0,0 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import reactHooks from 'eslint-plugin-react-hooks'; -import reactRefresh from 'eslint-plugin-react-refresh'; -import tseslint from 'typescript-eslint'; -import { fixupPluginRules } from '@eslint/compat'; - -export default tseslint.config( - { ignores: ['dist'] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - 'react-hooks': fixupPluginRules(reactHooks), - 'react-refresh': reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], - 'react-hooks/exhaustive-deps': 'error', - '@typescript-eslint/ban-ts-comment': 'off', - 'no-useless-escape': 'warn', - '@typescript-eslint/no-unused-expressions': 'warn', - }, - } -); diff --git a/package.json b/package.json index c3467a3..f217c4b 100644 --- a/package.json +++ b/package.json @@ -1,100 +1,101 @@ { - "name": "meilisearch-ui", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "start": "vite", - "build": "vite build", - "build:safe": "tsc && vite build", - "preview": "vite preview", - "lint": "eslint" - }, - "repository": "https://github.com/riccox/meilisearch-ui", - "homepage": "https://github.com/riccox/meilisearch-ui#readme", - "author": "Ricco Xie (https://riccox.com)", - "dependencies": { - "@arco-design/web-react": "^2.64.0", - "@arco-themes/react-meilisearch": "^0.0.1", - "@douyinfe/semi-ui": "^2.67.0", - "@emotion/react": "^11.13.3", - "@hookform/resolvers": "^3.9.0", - "@mantine/core": "^7.13.2", - "@mantine/form": "^7.13.2", - "@mantine/hooks": "^7.13.2", - "@mantine/modals": "^7.13.2", - "@monaco-editor/react": "^4.6.0", - "@nextui-org/react": "^2.4.8", - "@radix-ui/react-slot": "^1.1.0", - "@semi-bot/semi-theme-meilisearch": "^0.0.2", - "@tabler/icons-react": "^3.19.0", - "@tanstack/react-query": "^5.59.8", - "@tanstack/react-query-devtools": "^5.59.8", - "@tanstack/react-router": "^1.64.0", - "ahooks": "^3.8.1", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.1", - "dayjs": "^1.11.13", - "echarts": "^5.5.1", - "echarts-for-react": "^3.0.2", - "filesize": "^10.1.6", - "framer-motion": "^11.11.7", - "fuse.js": "^7.0.0", - "i18next": "^23.15.2", - "i18next-browser-languagedetector": "^8.0.0", - "i18next-resources-to-backend": "^1.2.1", - "immer": "^10.1.1", - "lodash": "^4.17.21", - "lucide-react": "^0.451.0", - "meilisearch": "0.44.1", - "qs": "^6.13.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-error-boundary": "^4.0.13", - "react-hook-form": "^7.53.0", - "react-i18next": "^15.0.2", - "react-json-view": "^1.21.3", - "sonner": "^1.5.0", - "tailwind-merge": "^2.5.3", - "tailwind-variants": "^0.2.1", - "tailwindcss-animate": "^1.0.7", - "use-immer": "^0.10.0", - "vaul": "^1.0.0", - "zod": "^3.23.8", - "zustand": "5.0.0-rc.2" - }, - "devDependencies": { - "@eslint/compat": "^1.2.0", - "@eslint/js": "^9.12.0", - "@iconify-json/lucide": "^1.2.8", - "@tanstack/router-devtools": "^1.64.0", - "@tanstack/router-plugin": "^1.64.0", - "@types/lodash": "^4.17.10", - "@types/node": "^22.7.5", - "@types/qs": "^6.9.16", - "@types/react": "^18.3.11", - "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^8.8.1", - "@typescript-eslint/parser": "^8.8.1", - "@unocss/preset-icons": "^0.63.4", - "@vitejs/plugin-react-swc": "^3.7.1", - "autoprefixer": "^10.4.20", - "eslint": "9.12.0", - "eslint-config-react-app": "^7.0.1", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.12", - "globals": "^15.11.0", - "postcss": "^8.4.47", - "prettier": "^3.3.3", - "sass": "^1.79.4", - "tailwindcss": "^3.4.13", - "typescript": "5.6.3", - "typescript-eslint": "^8.8.1", - "unocss": "^0.63.4", - "vite": "^5.4.8", - "vite-tsconfig-paths": "^5.0.1" - }, - "version": "0.11.0", - "main": "index.js", - "license": "Apache-2.0" + "name": "meilisearch-ui", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "start": "vite", + "build": "vite build", + "build:safe": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint" + }, + "repository": "https://github.com/riccox/meilisearch-ui", + "homepage": "https://github.com/riccox/meilisearch-ui#readme", + "author": "Ricco Xie (https://riccox.com)", + "dependencies": { + "@arco-design/web-react": "^2.64.0", + "@arco-themes/react-meilisearch": "^0.0.1", + "@douyinfe/semi-ui": "^2.67.0", + "@emotion/react": "^11.13.3", + "@hookform/resolvers": "^3.9.0", + "@mantine/core": "^7.13.2", + "@mantine/form": "^7.13.2", + "@mantine/hooks": "^7.13.2", + "@mantine/modals": "^7.13.2", + "@monaco-editor/react": "^4.6.0", + "@nextui-org/react": "^2.4.8", + "@radix-ui/react-slot": "^1.1.0", + "@semi-bot/semi-theme-meilisearch": "^0.0.2", + "@tabler/icons-react": "^3.19.0", + "@tanstack/react-query": "^5.59.8", + "@tanstack/react-query-devtools": "^5.59.8", + "@tanstack/react-router": "^1.64.0", + "ahooks": "^3.8.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.13", + "echarts": "^5.5.1", + "echarts-for-react": "^3.0.2", + "filesize": "^10.1.6", + "framer-motion": "^11.11.7", + "fuse.js": "^7.0.0", + "i18next": "^23.15.2", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-resources-to-backend": "^1.2.1", + "immer": "^10.1.1", + "lodash": "^4.17.21", + "lucide-react": "^0.451.0", + "meilisearch": "0.44.1", + "qs": "^6.13.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-error-boundary": "^4.0.13", + "react-hook-form": "^7.53.0", + "react-i18next": "^15.0.2", + "react-json-view": "^1.21.3", + "sonner": "^1.5.0", + "tailwind-merge": "^2.5.3", + "tailwind-variants": "^0.2.1", + "tailwindcss-animate": "^1.0.7", + "use-immer": "^0.10.0", + "vaul": "^1.0.0", + "zod": "^3.23.8", + "zustand": "5.0.0-rc.2" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@eslint/compat": "^1.2.0", + "@eslint/js": "^9.12.0", + "@iconify-json/lucide": "^1.2.8", + "@tanstack/router-devtools": "^1.64.0", + "@tanstack/router-plugin": "^1.64.0", + "@types/lodash": "^4.17.10", + "@types/node": "^22.7.5", + "@types/qs": "^6.9.16", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.8.1", + "@typescript-eslint/parser": "^8.8.1", + "@unocss/preset-icons": "^0.63.4", + "@vitejs/plugin-react-swc": "^3.7.1", + "autoprefixer": "^10.4.20", + "eslint": "9.12.0", + "eslint-config-react-app": "^7.0.1", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.12", + "globals": "^15.11.0", + "postcss": "^8.4.47", + "prettier": "^3.3.3", + "sass": "^1.79.4", + "tailwindcss": "^3.4.13", + "typescript": "5.6.3", + "typescript-eslint": "^8.8.1", + "unocss": "^0.63.4", + "vite": "^5.4.8", + "vite-tsconfig-paths": "^5.0.1" + }, + "version": "0.11.0", + "main": "index.js", + "license": "Apache-2.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6cd67c..5d327dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -153,6 +153,9 @@ importers: specifier: 5.0.0-rc.2 version: 5.0.0-rc.2(@types/react@18.3.11)(immer@10.1.1)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)) devDependencies: + '@biomejs/biome': + specifier: 1.9.4 + version: 1.9.4 '@eslint/compat': specifier: ^1.2.0 version: 1.2.0(eslint@9.12.0(jiti@2.3.3)) @@ -1071,6 +1074,63 @@ packages: resolution: {integrity: sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==} engines: {node: '>=6.9.0'} + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@dnd-kit/accessibility@3.1.0': resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} peerDependencies: @@ -7130,6 +7190,41 @@ snapshots: '@babel/helper-validator-identifier': 7.25.7 to-fast-properties: 2.0.0 + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + '@dnd-kit/accessibility@3.1.0(react@18.3.1)': dependencies: react: 18.3.1 diff --git a/postcss.config.cjs b/postcss.config.cjs index fef1b22..e873f1a 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,6 +1,6 @@ module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/src/components/404.tsx b/src/components/404.tsx index 947b69d..c89b103 100644 --- a/src/components/404.tsx +++ b/src/components/404.tsx @@ -1,22 +1,26 @@ -import { Button } from '@mantine/core'; -import { useRouter } from '@tanstack/react-router'; -import { Footer } from '@/components/Footer'; -import { Logo } from '@/components/Logo'; -import { useTranslation } from 'react-i18next'; +import { Footer } from "@/components/Footer"; +import { Logo } from "@/components/Logo"; +import { Button } from "@mantine/core"; +import { useRouter } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; export function NotFound() { - const { history } = useRouter(); - const { t } = useTranslation(); - return ( -
- -

404 Page Not Found

-
- -
-
-
- ); + const { history } = useRouter(); + const { t } = useTranslation(); + return ( +
+ +

404 Page Not Found

+
+ +
+
+
+ ); } diff --git a/src/components/Breadcrumb.tsx b/src/components/Breadcrumb.tsx index 32a658a..dfa29f8 100644 --- a/src/components/Breadcrumb.tsx +++ b/src/components/Breadcrumb.tsx @@ -1,53 +1,90 @@ -import { useMatchRoute } from '@tanstack/react-router'; -import { useTranslation } from 'react-i18next'; -import { Breadcrumbs, BreadcrumbItem } from '@nextui-org/react'; -import { useAppStore } from '@/store'; -import { isSingletonMode } from '@/utils/conn'; +import { useAppStore } from "@/store"; +import { isSingletonMode } from "@/utils/conn"; +import { BreadcrumbItem, Breadcrumbs } from "@nextui-org/react"; +import { useMatchRoute } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; export const DashBreadcrumb = () => { - const matchRoute = useMatchRoute(); - const { t } = useTranslation(); + const matchRoute = useMatchRoute(); + const { t } = useTranslation(); - const insRoute = matchRoute({ to: '/ins/$insID', fuzzy: true }) as unknown as { insID: string }; - const insKeysRoute = matchRoute({ to: '/ins/$insID/keys', fuzzy: true }) as unknown as { insID: string }; - const insTasksRoute = matchRoute({ to: '/ins/$insID/tasks', fuzzy: true }) as unknown as { insID: string }; - const indexRoute = matchRoute({ to: '/ins/$insID/index/$indexUID', fuzzy: true }) as unknown as { indexUID: string }; - const indexDocsRoute = matchRoute({ to: '/ins/$insID/index/$indexUID/documents', fuzzy: true }); - const indexDocsUploadRoute = matchRoute({ to: '/ins/$insID/index/$indexUID/documents/upload', fuzzy: true }); - const indexSettingRoute = matchRoute({ to: '/ins/$insID/index/$indexUID/setting', fuzzy: true }); + const insRoute = matchRoute({ + to: "/ins/$insID", + fuzzy: true, + }) as unknown as { insID: string }; + const insKeysRoute = matchRoute({ + to: "/ins/$insID/keys", + fuzzy: true, + }) as unknown as { insID: string }; + const insTasksRoute = matchRoute({ + to: "/ins/$insID/tasks", + fuzzy: true, + }) as unknown as { insID: string }; + const indexRoute = matchRoute({ + to: "/ins/$insID/index/$indexUID", + fuzzy: true, + }) as unknown as { indexUID: string }; + const indexDocsRoute = matchRoute({ + to: "/ins/$insID/index/$indexUID/documents", + fuzzy: true, + }); + const indexDocsUploadRoute = matchRoute({ + to: "/ins/$insID/index/$indexUID/documents/upload", + fuzzy: true, + }); + const indexSettingRoute = matchRoute({ + to: "/ins/$insID/index/$indexUID/setting", + fuzzy: true, + }); - const currentInstance = useAppStore((state) => state.instances.find((i) => i.id === parseInt(insRoute.insID))); + const currentInstance = useAppStore((state) => + state.instances.find((i) => i.id === Number.parseInt(insRoute.insID)), + ); - return ( - - {!isSingletonMode() && 🏠} - {insRoute && ( - - {isSingletonMode() ? '🏠' : `#${insRoute.insID} ${t('common:instance')} ${currentInstance?.name}`} - - )} - {insKeysRoute && {`${t('common:keys')}`}} - {insTasksRoute && {`${t('common:tasks')}`}} - {indexRoute && ( - {`${t('common:indexes')}: ${indexRoute.indexUID}`} - )} - {indexDocsRoute && ( - {`${t('documents')}`} - )} - {indexDocsUploadRoute && ( - {`${t('upload:title')}`} - )} - {indexSettingRoute && ( - {`${t('settings')}`} - )} - - ); + return ( + + {!isSingletonMode() && ( + + 🏠 + + )} + {insRoute && ( + + {isSingletonMode() + ? "🏠" + : `#${insRoute.insID} ${t("common:instance")} ${currentInstance?.name}`} + + )} + {insKeysRoute && ( + {`${t("common:keys")}`} + )} + {insTasksRoute && ( + {`${t("common:tasks")}`} + )} + {indexRoute && ( + {`${t("common:indexes")}: ${indexRoute.indexUID}`} + )} + {indexDocsRoute && ( + {`${t("documents")}`} + )} + {indexDocsUploadRoute && ( + {`${t("upload:title")}`} + )} + {indexSettingRoute && ( + {`${t("settings")}`} + )} + + ); }; diff --git a/src/components/Copyable.tsx b/src/components/Copyable.tsx index 95367b7..a2e77a7 100644 --- a/src/components/Copyable.tsx +++ b/src/components/Copyable.tsx @@ -1,28 +1,28 @@ -'use client'; -import { FC } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Typography } from '@arco-design/web-react'; -import { toast } from 'sonner'; -import { cn } from '@/lib/cn'; +"use client"; +import { cn } from "@/lib/cn"; +import { Typography } from "@arco-design/web-react"; +import type { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; interface Props { - children: string; - className?: string; + children: string; + className?: string; } -export const Copyable: FC = ({ className = '', children }) => { - const { t } = useTranslation(); +export const Copyable: FC = ({ className = "", children }) => { + const { t } = useTranslation(); - return ( - { - toast.success(t('common:copied')); - }, - text: children, - }} - > - {children} - - ); + return ( + { + toast.success(t("common:copied")); + }, + text: children, + }} + > + {children} + + ); }; diff --git a/src/components/DashboardSettingsButton.tsx b/src/components/DashboardSettingsButton.tsx index ad5a138..61ba94b 100644 --- a/src/components/DashboardSettingsButton.tsx +++ b/src/components/DashboardSettingsButton.tsx @@ -1,127 +1,150 @@ -import { useCallback, useRef } from 'react'; -import { Instance, useAppStore } from '@/store'; -import { useTranslation } from 'react-i18next'; -import { IconFileExport, IconFileImport, IconSettings, IconTrash } from '@tabler/icons-react'; -import { Menu, ActionIcon } from '@mantine/core'; -import { Button } from '@douyinfe/semi-ui'; -import { Modal } from '@arco-design/web-react'; -import { cn } from '@/lib/cn'; -import type { FC } from 'react'; +import { cn } from "@/lib/cn"; +import { type Instance, useAppStore } from "@/store"; +import { Modal } from "@arco-design/web-react"; +import { Menu } from "@mantine/core"; +import { + IconFileExport, + IconFileImport, + IconSettings, + IconTrash, +} from "@tabler/icons-react"; +import { useCallback, useRef } from "react"; +import type { FC } from "react"; +import { useTranslation } from "react-i18next"; interface Props { - className?: string; + className?: string; } -export const DashboardSettingsButton: FC = ({ className = '' }) => { - - const { t } = useTranslation('dashboard'); - const instances = useAppStore((state) => state.instances); - const addInstance = useAppStore((state) => state.addInstance); - const removeAllInstances = useAppStore((state) => state.removeAllInstances); - const importInstancesFileInputRef = useRef(null); - - const onClickExportInstances = () => { - if (instances.length > 0) { - const jsonString = JSON.stringify(instances, null, 2); - const blob = new Blob([jsonString], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `export-meilisearch-ui.json`; - - document.body.appendChild(a); - a.click(); - - document.body.removeChild(a); - URL.revokeObjectURL(url); - } - } - - const onClickImportInstances = () => { - if (importInstancesFileInputRef.current) { - importInstancesFileInputRef.current.click(); - } - } - - const handleImportInstancesFileUpload = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = (e) => { - try { - const content = e.target?.result; - if (content) { - const parsed = JSON.parse(content as string); - - if (isValidInstanceArray(parsed)) { - parsed.forEach(ins => { - const _ins = { ...ins, id: 0 }; - addInstance(ins); - }); - } - } - } catch (err: any) { - } - finally { - event.target.value = ""; - } - }; - - reader.readAsText(file); - } - - const isValidInstanceArray = (data: any): data is Instance[] => { - if (!Array.isArray(data)) return false; - - return data.every( - (item) => - typeof item.id === "number" && - typeof item.name === "string" && - typeof item.host === "string" && - (item.apiKey === undefined || typeof item.apiKey === "string") && - (item.updatedTime === undefined || !isNaN(new Date(item.updatedTime).getTime())) - ); - } - - const onClickRemoveAllInstances = useCallback(() => { - Modal.confirm({ - title: t('settings.remove.title'), - centered: true, - content: t('settings.remove.tip'), - onOk: async () => { - return removeAllInstances(); - }, - okText: t('confirm'), - cancelText: t('cancel'), - }); - }, [removeAllInstances, t] - ) - - return ( -
- - - - - - - - } disabled={!(instances.length > 0)} onClick={onClickExportInstances}> - {t('settings.export')} - - } onClick={onClickImportInstances}> - {t('settings.import')} - - - - - {t('settings.danger_zone')} - } color="red" disabled={!(instances.length > 0)} onClick={onClickRemoveAllInstances}> - {t('settings.remove.title')} - - - -
- ); -} +export const DashboardSettingsButton: FC = ({ className = "" }) => { + const { t } = useTranslation("dashboard"); + const instances = useAppStore((state) => state.instances); + const addInstance = useAppStore((state) => state.addInstance); + const removeAllInstances = useAppStore((state) => state.removeAllInstances); + const importInstancesFileInputRef = useRef(null); + + const onClickExportInstances = () => { + if (instances.length > 0) { + const jsonString = JSON.stringify(instances, null, 2); + const blob = new Blob([jsonString], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "export-meilisearch-ui.json"; + + document.body.appendChild(a); + a.click(); + + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + }; + + const onClickImportInstances = () => { + if (importInstancesFileInputRef.current) { + importInstancesFileInputRef.current.click(); + } + }; + + const handleImportInstancesFileUpload = ( + event: React.ChangeEvent, + ) => { + const file = event.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const content = e.target?.result; + if (content) { + const parsed = JSON.parse(content as string); + + if (isValidInstanceArray(parsed)) { + parsed.forEach((ins) => { + const _ins = { ...ins, id: 0 }; + addInstance(ins); + }); + } + } + } catch (err: any) { + } finally { + event.target.value = ""; + } + }; + + reader.readAsText(file); + }; + + const isValidInstanceArray = (data: any): data is Instance[] => { + if (!Array.isArray(data)) return false; + + return data.every( + (item) => + typeof item.id === "number" && + typeof item.name === "string" && + typeof item.host === "string" && + (item.apiKey === undefined || typeof item.apiKey === "string") && + (item.updatedTime === undefined || + !Number.isNaN(new Date(item.updatedTime).getTime())), + ); + }; + + const onClickRemoveAllInstances = useCallback(() => { + Modal.confirm({ + title: t("settings.remove.title"), + alignCenter: true, + content: t("settings.remove.tip"), + onOk: async () => { + return removeAllInstances(); + }, + okText: t("confirm"), + cancelText: t("cancel"), + }); + }, [removeAllInstances, t]); + + return ( +
+ + + + + + + + } + disabled={!(instances.length > 0)} + onClick={onClickExportInstances} + > + {t("settings.export")} + + } + onClick={onClickImportInstances} + > + {t("settings.import")} + + + + + {t("settings.danger_zone")} + } + color="red" + disabled={!(instances.length > 0)} + onClick={onClickRemoveAllInstances} + > + {t("settings.remove.title")} + + + +
+ ); +}; diff --git a/src/components/Document/AttrTags.tsx b/src/components/Document/AttrTags.tsx index ff7b352..90a8de4 100644 --- a/src/components/Document/AttrTags.tsx +++ b/src/components/Document/AttrTags.tsx @@ -1,35 +1,38 @@ -'use client'; -import { Tooltip } from '@arco-design/web-react'; -import { useTranslation } from 'react-i18next'; -import { Tag } from '@douyinfe/semi-ui'; -import { Settings } from 'meilisearch'; +"use client"; +import { Tooltip } from "@arco-design/web-react"; +import { Tag } from "@douyinfe/semi-ui"; +import type { Settings } from "meilisearch"; +import { useTranslation } from "react-i18next"; -export const AttrTags = ({ attr, indexSettings }: { attr: string; indexSettings: Settings }) => { - const { t } = useTranslation('index'); +export const AttrTags = ({ + attr, + indexSettings, +}: { attr: string; indexSettings: Settings }) => { + const { t } = useTranslation("index"); - return ( - <> - {indexSettings?.filterableAttributes?.includes(attr) && ( - - - FL - - - )} - {indexSettings?.searchableAttributes?.includes(attr) && ( - - - SC - - - )} - {indexSettings?.sortableAttributes?.includes(attr) && ( - - - ST - - - )} - - ); + return ( + <> + {indexSettings?.filterableAttributes?.includes(attr) && ( + + + FL + + + )} + {indexSettings?.searchableAttributes?.includes(attr) && ( + + + SC + + + )} + {indexSettings?.sortableAttributes?.includes(attr) && ( + + + ST + + + )} + + ); }; diff --git a/src/components/Document/GridItem.tsx b/src/components/Document/GridItem.tsx index bf8e913..65bde52 100644 --- a/src/components/Document/GridItem.tsx +++ b/src/components/Document/GridItem.tsx @@ -1,44 +1,60 @@ -import { Button } from '@arco-design/web-react'; -import { BaseDocItemProps, ValueDisplay } from './list'; -import { useTranslation } from 'react-i18next'; -import { Descriptions } from '@douyinfe/semi-ui'; -import { AttrTags } from './AttrTags'; -import { Settings } from 'meilisearch'; +import { Button } from "@arco-design/web-react"; +import { Descriptions } from "@douyinfe/semi-ui"; +import type { Settings } from "meilisearch"; +import { useTranslation } from "react-i18next"; +import { AttrTags } from "./AttrTags"; +import { type BaseDocItemProps, ValueDisplay } from "./list"; export const GridItem = ({ - doc, - onClickDocumentDel, - onClickDocumentUpdate, - indexSettings, + doc, + onClickDocumentDel, + onClickDocumentUpdate, + indexSettings, }: BaseDocItemProps & { indexSettings?: Settings }) => { - const { t } = useTranslation('document'); + const { t } = useTranslation("document"); - return ( -
- ({ - key: ( -
- {indexSettings && } -

{k}

-
- ), - value: , - }))} - /> -
- - -
-
- ); + return ( +
+ ({ + key: ( +
+ {indexSettings && ( + + )} +

{k}

+
+ ), + value: , + }))} + /> +
+ + +
+
+ ); }; diff --git a/src/components/Document/JSONItem.tsx b/src/components/Document/JSONItem.tsx index 1003dbd..9b89487 100644 --- a/src/components/Document/JSONItem.tsx +++ b/src/components/Document/JSONItem.tsx @@ -1,29 +1,51 @@ -import ReactJson from 'react-json-view'; -import { Button } from '@arco-design/web-react'; -import { BaseDocItemProps } from './list'; -import { useTranslation } from 'react-i18next'; +import { Button } from "@arco-design/web-react"; +import { useTranslation } from "react-i18next"; +import ReactJson from "react-json-view"; +import type { BaseDocItemProps } from "./list"; -export const JSONItem = ({ doc, onClickDocumentDel, onClickDocumentUpdate }: BaseDocItemProps) => { - const { t } = useTranslation('document'); +export const JSONItem = ({ + doc, + onClickDocumentDel, + onClickDocumentUpdate, +}: BaseDocItemProps) => { + const { t } = useTranslation("document"); - return ( -
- -
- - -
-
- ); + return ( +
+ +
+ + +
+
+ ); }; diff --git a/src/components/Document/list.tsx b/src/components/Document/list.tsx index 70a2e5a..dfcc37e 100644 --- a/src/components/Document/list.tsx +++ b/src/components/Document/list.tsx @@ -1,311 +1,352 @@ -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { showTaskErrorNotification, showTaskSubmitNotification, stringifyJsonPretty } from '@/utils/text'; -import { toast } from '@/utils/toast'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import { useCallback, useMemo, useState } from 'react'; -import MonacoEditor from '@monaco-editor/react'; -import { useTranslation } from 'react-i18next'; -import { JSONItem } from './JSONItem'; -import { Table, TableProps, Modal } from '@arco-design/web-react'; -import { GridItem } from './GridItem'; -import { Button } from '@arco-design/web-react'; -import { Image } from '@douyinfe/semi-ui'; -import _ from 'lodash'; -import { Copyable } from '../Copyable'; -import { getTimeText, isValidDateTime, isValidImgUrl } from '@/utils/text'; -import { Index } from 'meilisearch'; -import { useCurrentInstance } from '@/hooks/useCurrentInstance'; -import { AttrTags } from './AttrTags'; +import { useCurrentInstance } from "@/hooks/useCurrentInstance"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { + showTaskErrorNotification, + showTaskSubmitNotification, + stringifyJsonPretty, +} from "@/utils/text"; +import { getTimeText, isValidDateTime, isValidImgUrl } from "@/utils/text"; +import { toast } from "@/utils/toast"; +import { Modal, Table, type TableProps } from "@arco-design/web-react"; +import { Button } from "@arco-design/web-react"; +import { Image } from "@douyinfe/semi-ui"; +import MonacoEditor from "@monaco-editor/react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import _ from "lodash"; +import type { Index } from "meilisearch"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Copyable } from "../Copyable"; +import { AttrTags } from "./AttrTags"; +import { GridItem } from "./GridItem"; +import { JSONItem } from "./JSONItem"; -export type Doc = { indexId: string; content: Record; primaryKey: string }; +export type Doc = { + indexId: string; + content: Record; + primaryKey: string; +}; export type BaseDocItemProps = { - doc: Doc; - onClickDocumentUpdate: (doc: Doc) => void; - onClickDocumentDel: (doc: Doc) => void; + doc: Doc; + onClickDocumentUpdate: (doc: Doc) => void; + onClickDocumentDel: (doc: Doc) => void; }; -export type ListType = 'json' | 'table' | 'grid'; +export type ListType = "json" | "table" | "grid"; export const ValueDisplay = ({ - name, - value, - dateParser = true, + name, + value, + dateParser = true, }: { - name: string; - value: unknown; - dateParser?: boolean; + name: string; + value: unknown; + dateParser?: boolean; }) => { - let str = _.toString(value).trim(); + let str = _.toString(value).trim(); - if (_.isObjectLike(value)) { - str = stringifyJsonPretty(value as object); - } + if (_.isObjectLike(value)) { + str = stringifyJsonPretty(value as object); + } - return ( -
{ - Modal.info({ - title: name, - content: ( -
- {str} - {isValidImgUrl(str) && } -
- ), - }); - }} - > - {dateParser && /^.*(date|time).*$/gim.test(name) && isValidDateTime(str) ? ( - getTimeText(isValidDateTime(str) as Date) - ) : isValidImgUrl(str) ? ( - - ) : ( - _.truncate(str, { length: 20 }) - )} -
- ); + return ( +
{ + Modal.info({ + title: name, + content: ( +
+ + {str} + + {isValidImgUrl(str) && } +
+ ), + }); + }} + > + {dateParser && + /^.*(date|time).*$/gim.test(name) && + isValidDateTime(str) ? ( + getTimeText(isValidDateTime(str) as Date) + ) : isValidImgUrl(str) ? ( + + ) : ( + _.truncate(str, { length: 20 }) + )} +
+ ); }; interface Props { - currentIndex: Index; - type?: ListType; - docs?: Doc[]; - refetchDocs: () => void; + currentIndex: Index; + type?: ListType; + docs?: Doc[]; + refetchDocs: () => void; } -export const DocumentList = ({ docs = [], type = 'json', currentIndex }: Props) => { - const { t } = useTranslation('document'); - const client = useMeiliClient(); - const [editingDocument, setEditingDocument] = useState(); - const [editingDocModalVisible, setEditingDocModalVisible] = useState(false); - const currentInstance = useCurrentInstance(); +export const DocumentList = ({ + docs = [], + type = "json", + currentIndex, +}: Props) => { + const { t } = useTranslation("document"); + const client = useMeiliClient(); + const [editingDocument, setEditingDocument] = useState(); + const [editingDocModalVisible, setEditingDocModalVisible] = + useState(false); + const currentInstance = useCurrentInstance(); - const indexSettingsQuery = useQuery({ - queryKey: ['indexSettings', currentInstance.host, currentIndex.uid], - queryFn: async () => { - return await currentIndex.getSettings(); - }, - }); + const indexSettingsQuery = useQuery({ + queryKey: ["indexSettings", currentInstance.host, currentIndex.uid], + queryFn: async () => { + return await currentIndex.getSettings(); + }, + }); - const indexSettings = useMemo(() => { - return indexSettingsQuery.data; - }, [indexSettingsQuery.data]); + const indexSettings = useMemo(() => { + return indexSettingsQuery.data; + }, [indexSettingsQuery.data]); - const editDocumentMutation = useMutation({ - mutationFn: async ({ docs }: { docs: object[] }) => { - return await currentIndex.updateDocuments(docs); - }, - onSuccess: (t) => { - showTaskSubmitNotification(t); - }, - onError: (error) => { - console.error(error); - showTaskErrorNotification(error); - }, - }); + const editDocumentMutation = useMutation({ + mutationFn: async ({ docs }: { docs: object[] }) => { + return await currentIndex.updateDocuments(docs); + }, + onSuccess: (t) => { + showTaskSubmitNotification(t); + }, + onError: (error) => { + console.error(error); + showTaskErrorNotification(error); + }, + }); - const removeDocumentsMutation = useMutation({ - mutationFn: async ({ indexId, docId }: { indexId: string; docId: string[] | number[] }) => { - return await client.index(indexId).deleteDocuments(docId); - }, - onSuccess: (t) => { - showTaskSubmitNotification(t); - }, - onError: (error: Error) => { - console.error(error); - showTaskErrorNotification(error); - }, - }); + const removeDocumentsMutation = useMutation({ + mutationFn: async ({ + indexId, + docId, + }: { + indexId: string; + docId: string[] | number[]; + }) => { + return await client.index(indexId).deleteDocuments(docId); + }, + onSuccess: (t) => { + showTaskSubmitNotification(t); + }, + onError: (error: Error) => { + console.error(error); + showTaskErrorNotification(error); + }, + }); - const onClickDocumentDel = useCallback( - (doc: Doc) => { - const pk = doc.primaryKey; - console.debug('onClickDocumentDel', 'pk', pk); - if (pk) { - Modal.confirm({ - title: t('delete_document'), - content: ( -

- ), - okText: t('confirm'), - cancelText: t('cancel'), - onOk: () => { - // @ts-ignore - removeDocumentsMutation.mutate({ indexId: doc.indexId, docId: [doc.content[pk]] }); - }, - }); - } else { - toast.error(t('delete.require_primaryKey', { indexId: doc.indexId })); - } - }, - [removeDocumentsMutation, t] - ); + const onClickDocumentDel = useCallback( + (doc: Doc) => { + const pk = doc.primaryKey; + console.debug("onClickDocumentDel", "pk", pk); + if (pk) { + Modal.confirm({ + title: t("delete_document"), + content: ( +

+ ), + okText: t("confirm"), + cancelText: t("cancel"), + onOk: () => { + removeDocumentsMutation.mutate({ + indexId: doc.indexId, + // @ts-ignore + docId: [doc.content[pk]], + }); + }, + }); + } else { + toast.error(t("delete.require_primaryKey", { indexId: doc.indexId })); + } + }, + [removeDocumentsMutation, t], + ); - const onEditDocumentJsonEditorUpdate = useCallback( - (value: string = '[]') => setEditingDocument((prev) => ({ ...prev!, content: JSON.parse(value) })), - [] - ); + const onEditDocumentJsonEditorUpdate = useCallback( + (value = "[]") => + setEditingDocument((prev) => ({ ...prev!, content: JSON.parse(value) })), + [], + ); - const onClickDocumentUpdate = useCallback((doc: Doc) => { - const pk = doc.primaryKey; - console.debug('onClickDocumentUpdate', 'pk', pk); - if (pk) { - setEditingDocument(doc); - setEditingDocModalVisible(true); - } - }, []); + const onClickDocumentUpdate = useCallback((doc: Doc) => { + const pk = doc.primaryKey; + console.debug("onClickDocumentUpdate", "pk", pk); + if (pk) { + setEditingDocument(doc); + setEditingDocModalVisible(true); + } + }, []); - return useMemo( - () => ( - <> - { - console.debug('submit doc update', editingDocument); - if (editingDocument) { - editDocumentMutation - .mutateAsync({ - docs: [editingDocument.content], - }) - .then(() => { - setEditingDocModalVisible(false); - }); - } - }} - onCancel={() => setEditingDocModalVisible(false)} - > -

- -
- - {type === 'table' ? ( -
- { - return keys.concat(Object.keys(obj.content)); - }, - [docs[0].primaryKey] - ) - ), - ].map((i) => ({ - title: ( -
-

{i}

- {indexSettings && } -
- ), - dataIndex: i, - width: '15rem', - ellipsis: true, - render(_col, item) { - return ; - }, - })) as TableProps['columns'])!.concat([ - { - title: t('common:actions'), - fixed: 'right', - align: 'center', - width: '8rem', - render: (_col, _record, index) => ( -
- - -
- ), - }, - ])} - data={docs.map((d) => ({ ...d.content }))} - stripe - hover - virtualized - pagination={false} - size="small" - scroll={{ x: true }} - /> - - ) : type === 'grid' ? ( -
- {docs.map((d, i) => { - return ( - - ); - })} -
- ) : ( - <> - {docs.map((d, i) => { - return ( - - ); - })} - - )} - - ), - [ - docs, - editDocumentMutation, - editingDocModalVisible, - editingDocument, - indexSettings, - onClickDocumentDel, - onClickDocumentUpdate, - onEditDocumentJsonEditorUpdate, - t, - type, - ] - ); + return useMemo( + () => ( + <> + { + console.debug("submit doc update", editingDocument); + if (editingDocument) { + editDocumentMutation + .mutateAsync({ + docs: [editingDocument.content], + }) + .then(() => { + setEditingDocModalVisible(false); + }); + } + }} + onCancel={() => setEditingDocModalVisible(false)} + > +
+ +
+
+ {type === "table" ? ( +
+
{ + return keys.concat(Object.keys(obj.content)); + }, + [docs[0].primaryKey], + ), + ), + ].map((i) => ({ + title: ( +
+

{i}

+ {indexSettings && ( + + )} +
+ ), + dataIndex: i, + width: "15rem", + ellipsis: true, + render(_col, item) { + return ( + + ); + }, + })) as TableProps["columns"] + )?.concat([ + { + title: t("common:actions"), + fixed: "right", + align: "center", + width: "8rem", + render: (_col, _record, index) => ( +
+ + +
+ ), + }, + ])} + data={docs.map((d) => ({ ...d.content }))} + stripe + hover + virtualized + pagination={false} + size="small" + scroll={{ x: true }} + /> + + ) : type === "grid" ? ( +
+ {docs.map((d, i) => { + return ( + + ); + })} +
+ ) : ( + <> + {docs.map((d, i) => { + return ( + + ); + })} + + )} + + ), + [ + docs, + editDocumentMutation, + editingDocModalVisible, + editingDocument, + indexSettings, + onClickDocumentDel, + onClickDocumentUpdate, + onEditDocumentJsonEditorUpdate, + t, + type, + ], + ); }; diff --git a/src/components/Document/searchForm.tsx b/src/components/Document/searchForm.tsx index e4dca55..689e4ee 100644 --- a/src/components/Document/searchForm.tsx +++ b/src/components/Document/searchForm.tsx @@ -1,145 +1,173 @@ -import { useMemo } from 'react'; -import { NumberInput, TextInput } from '@mantine/core'; -import { IconAlignBoxLeftMiddle, IconArrowsSort, IconFilter, IconSearch } from '@tabler/icons-react'; -import clsx from 'clsx'; -import { UseFormReturnType } from '@mantine/form'; -import { useTranslation } from 'react-i18next'; -import { Button } from '@nextui-org/react'; -import { Switch, Tooltip } from '@douyinfe/semi-ui'; +import { Switch, Tooltip } from "@douyinfe/semi-ui"; +import { NumberInput, TextInput } from "@mantine/core"; +import type { UseFormReturnType } from "@mantine/form"; +import { Button } from "@nextui-org/react"; +import { + IconAlignBoxLeftMiddle, + IconArrowsSort, + IconFilter, + IconSearch, +} from "@tabler/icons-react"; +import clsx from "clsx"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; type Props = { - isFetching: boolean; - searchForm: UseFormReturnType<{ - q: string; - offset: number; - limit: number; - filter: string; - sort: string; - indexId?: string; - showRankingScore: boolean; - }>; - searchFormError: string | null; - onFormSubmit: () => void; - onAutoRefreshChange: (value: boolean) => void; - submitBtnText: string; - indexIdEnable?: boolean; + isFetching: boolean; + searchForm: UseFormReturnType<{ + q: string; + offset: number; + limit: number; + filter: string; + sort: string; + indexId?: string; + showRankingScore: boolean; + }>; + searchFormError: string | null; + onFormSubmit: () => void; + onAutoRefreshChange: (value: boolean) => void; + submitBtnText: string; + indexIdEnable?: boolean; }; export const SearchForm = ({ - searchForm, - searchFormError, - onFormSubmit, - submitBtnText, - indexIdEnable = false, - isFetching, - onAutoRefreshChange, + searchForm, + searchFormError, + onFormSubmit, + submitBtnText, + indexIdEnable = false, + isFetching, + onAutoRefreshChange, }: Props) => { - const { t } = useTranslation('document'); + const { t } = useTranslation("document"); - return useMemo( - () => ( -
-
-
- - - - - - -
-
-

{searchFormError}

-
-
- {indexIdEnable && ( - } - radius="md" - placeholder={t('search.form.indexId.placeholder')} - required - {...searchForm.getInputProps('indexId')} - /> - )} - } - autoFocus - radius="md" - placeholder={t('search.form.q.placeholder')} - {...searchForm.getInputProps('q')} - /> -
- } - radius="md" - {...searchForm.getInputProps('filter')} - /> - - } - radius="md" - {...searchForm.getInputProps('sort')} - /> - -
-
- - + return useMemo( + () => ( + +
+
+ + + + + + +
+
+

{searchFormError}

+
+
+ {indexIdEnable && ( + } + radius="md" + placeholder={t("search.form.indexId.placeholder")} + required + {...searchForm.getInputProps("indexId")} + /> + )} + } + autoFocus + radius="md" + placeholder={t("search.form.q.placeholder")} + {...searchForm.getInputProps("q")} + /> +
+ } + radius="md" + {...searchForm.getInputProps("filter")} + /> + + } + radius="md" + {...searchForm.getInputProps("sort")} + /> + +
+
+ + - {/* right btn group */} -
- -
- {' '} - -
-
- -
- - onAutoRefreshChange(v)}> -
-
- {/* submit btn */} - -
-
- - ), - [onFormSubmit, searchFormError, indexIdEnable, t, searchForm, isFetching, submitBtnText, onAutoRefreshChange] - ); + {/* right btn group */} +
+ +
+
+ {t("search.form.showRankingScore.label")} +
+ +
+
+ +
+
+ {t("search.form.autoRefresh.label")} +
+ onAutoRefreshChange(v)} + /> +
+
+ {/* submit btn */} + +
+
+ + ), + [ + onFormSubmit, + searchFormError, + indexIdEnable, + t, + searchForm, + isFetching, + submitBtnText, + onAutoRefreshChange, + ], + ); }; diff --git a/src/components/EmptyArea/index.tsx b/src/components/EmptyArea/index.tsx index eb7f2ad..e71d8d3 100644 --- a/src/components/EmptyArea/index.tsx +++ b/src/components/EmptyArea/index.tsx @@ -1,17 +1,21 @@ -import type { FC } from 'react'; -import undraw_web_search from '@/assets/undraw_web_search.svg'; -import { useTranslation } from 'react-i18next'; +import undraw_web_search from "@/assets/undraw_web_search.svg"; +import type { FC } from "react"; +import { useTranslation } from "react-i18next"; interface Props { - text?: string; + text?: string; } export const EmptyArea: FC = ({ text }) => { - const { t } = useTranslation(); - return ( -
- {'undraw_web_search'} -

{text ?? t('common:empty')}

-
- ); + const { t } = useTranslation(); + return ( +
+ {"undraw_web_search"} +

{text ?? t("common:empty")}

+
+ ); }; diff --git a/src/components/ErrorBoundary/Fallback/AppFallback.tsx b/src/components/ErrorBoundary/Fallback/AppFallback.tsx index 4b44d72..5cc0d3f 100644 --- a/src/components/ErrorBoundary/Fallback/AppFallback.tsx +++ b/src/components/ErrorBoundary/Fallback/AppFallback.tsx @@ -1,30 +1,30 @@ -import type { FC } from 'react'; +import type { FC } from "react"; -import type { ErrorFallbackProps } from './ErrorFallbackProps'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from "react-i18next"; +import type { ErrorFallbackProps } from "./ErrorFallbackProps"; const handleReload = () => { - window.location.assign(window.location.origin); + window.location.assign(window.location.origin); }; export const AppFallback: FC = ({ error }) => { - const { t } = useTranslation('sys'); - return ( -
-
-
-
-

🚨

-

{t('warning')}

-
-
-

{error.message}

-

- {t('reload')} -

-
-
-
-
- ); + const { t } = useTranslation("sys"); + return ( +
+
+
+
+

🚨

+

{t("warning")}

+
+
+

{error.message}

+

+ {t("reload")} +

+
+
+
+
+ ); }; diff --git a/src/components/ErrorBoundary/Fallback/ErrorFallbackProps.ts b/src/components/ErrorBoundary/Fallback/ErrorFallbackProps.ts index 7aeec86..64e06f8 100644 --- a/src/components/ErrorBoundary/Fallback/ErrorFallbackProps.ts +++ b/src/components/ErrorBoundary/Fallback/ErrorFallbackProps.ts @@ -1,4 +1,4 @@ export type ErrorFallbackProps = { - error: Error; - resetErrorBoundary: () => void; + error: Error; + resetErrorBoundary: () => void; }; diff --git a/src/components/ErrorBoundary/Fallback/index.ts b/src/components/ErrorBoundary/Fallback/index.ts index ed034e8..108bf7b 100644 --- a/src/components/ErrorBoundary/Fallback/index.ts +++ b/src/components/ErrorBoundary/Fallback/index.ts @@ -1 +1 @@ -export { AppFallback } from './AppFallback'; +export { AppFallback } from "./AppFallback"; diff --git a/src/components/ErrorBoundary/index.tsx b/src/components/ErrorBoundary/index.tsx index 2ebe484..5ba9293 100644 --- a/src/components/ErrorBoundary/index.tsx +++ b/src/components/ErrorBoundary/index.tsx @@ -1,19 +1,26 @@ -import type { FC, ReactNode } from 'react'; -import { ErrorBoundary } from 'react-error-boundary'; +import type { FC, ReactNode } from "react"; +import { ErrorBoundary } from "react-error-boundary"; -import { AppFallback } from './Fallback'; -import type { ErrorFallbackProps } from './Fallback/ErrorFallbackProps'; +import { AppFallback } from "./Fallback"; +import type { ErrorFallbackProps } from "./Fallback/ErrorFallbackProps"; type ErrorBoundaryProps = { - children: ReactNode; - onReset?: () => void; - FallbackComponent?: FC; + children: ReactNode; + onReset?: () => void; + FallbackComponent?: FC; }; -export const ReactErrorBoundary: FC = ({ children, onReset, FallbackComponent }) => { - return ( - - {children} - - ); +export const ReactErrorBoundary: FC = ({ + children, + onReset, + FallbackComponent, +}) => { + return ( + + {children} + + ); }; diff --git a/src/components/Footer/Copyright.tsx b/src/components/Footer/Copyright.tsx index b850fff..f477128 100644 --- a/src/components/Footer/Copyright.tsx +++ b/src/components/Footer/Copyright.tsx @@ -1,14 +1,19 @@ -import type { FC } from 'react'; -import { useTranslation } from 'react-i18next'; +import type { FC } from "react"; +import { useTranslation } from "react-i18next"; export const Copyright: FC = () => { - const { t } = useTranslation('footer'); - return ( -
- {t('powered_by') + ' '} - - Ricco Xie - -
- ); + const { t } = useTranslation("footer"); + return ( +
+ {`${t("powered_by")} `} + + Ricco Xie + +
+ ); }; diff --git a/src/components/Footer/Singleton.tsx b/src/components/Footer/Singleton.tsx index f48dca7..b891f2f 100644 --- a/src/components/Footer/Singleton.tsx +++ b/src/components/Footer/Singleton.tsx @@ -1,13 +1,12 @@ -import { isSingletonMode } from '@/utils/conn'; -import { Tag } from '@douyinfe/semi-ui'; -import type { FC } from 'react'; -import { useTranslation } from 'react-i18next'; +import { isSingletonMode } from "@/utils/conn"; +import { Tag } from "@douyinfe/semi-ui"; +import type { FC } from "react"; +import { useTranslation } from "react-i18next"; export const Singleton: FC = () => { - const { t } = useTranslation('common'); - if (!isSingletonMode()) { - return <>; - } else { - return {t('singleton_mode')}; - } + const { t } = useTranslation("common"); + if (!isSingletonMode()) { + return <>; + } + return {t("singleton_mode")}; }; diff --git a/src/components/Footer/Version.tsx b/src/components/Footer/Version.tsx index 5282377..cd5b7f9 100644 --- a/src/components/Footer/Version.tsx +++ b/src/components/Footer/Version.tsx @@ -1,27 +1,30 @@ -import { FC } from 'react'; -import { useTranslation } from 'react-i18next'; -import packageJson from '../../../package.json'; -import { Tooltip } from '@douyinfe/semi-ui'; +import { Tooltip } from "@douyinfe/semi-ui"; +import type { FC } from "react"; +import { useTranslation } from "react-i18next"; +import packageJson from "../../../package.json"; const appVersion = packageJson.version; -const meilisearchJSVersion = packageJson.dependencies.meilisearch.replace(/[^\d.]/g, ''); +const meilisearchJSVersion = packageJson.dependencies.meilisearch.replace( + /[^\d.]/g, + "", +); declare const __GIT_HASH__: string; export const Version: FC = () => { - const { t } = useTranslation(); - const gitHash = __GIT_HASH__ || 'unknown'; + const { t } = useTranslation(); + const gitHash = __GIT_HASH__ || "unknown"; - return ( - - - {t('common:version')}: {appVersion} - {`[${gitHash.slice(0, 7)}]`} - - - - {t('footer:meilisearchJSInUse')}: {meilisearchJSVersion} - - - - ); + return ( + + + {t("common:version")}: {appVersion} + {`[${gitHash.slice(0, 7)}]`} + + + + {t("footer:meilisearchJSInUse")}: {meilisearchJSVersion} + + + + ); }; diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx index 3695356..6a64bd0 100644 --- a/src/components/Footer/index.tsx +++ b/src/components/Footer/index.tsx @@ -1,24 +1,34 @@ -import type { FC } from 'react'; -import { Copyright } from '@/components/Footer/Copyright'; -import { LangSelector } from '../lang'; -import { Version } from './Version'; -import { Singleton } from './Singleton'; -import { cn } from '@/lib/cn'; +import { Copyright } from "@/components/Footer/Copyright"; +import { cn } from "@/lib/cn"; +import type { FC } from "react"; +import { LangSelector } from "../lang"; +import { Singleton } from "./Singleton"; +import { Version } from "./Version"; interface Props { - className?: string; + className?: string; } -export const Footer: FC = ({ className = '' }) => { - return ( -
- - - - Github - - - -
- ); +export const Footer: FC = ({ className = "" }) => { + return ( +
+ + + + Github + + + +
+ ); }; diff --git a/src/components/IndexList.tsx b/src/components/IndexList.tsx index 2f0a091..08f715c 100644 --- a/src/components/IndexList.tsx +++ b/src/components/IndexList.tsx @@ -1,196 +1,227 @@ -'use client'; -import { useCurrentInstance } from '@/hooks/useCurrentInstance'; -import { useInstanceStats } from '@/hooks/useInstanceStats'; -import { Button, Pagination, Tag, Tooltip } from '@douyinfe/semi-ui'; -import { Card, CardBody, CardHeader } from '@nextui-org/react'; -import { IconAlertTriangle } from '@tabler/icons-react'; -import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import { Link, useNavigate } from '@tanstack/react-router'; -import _ from 'lodash'; -import MeiliSearch, { Index } from 'meilisearch'; -import { FC, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useImmer } from 'use-immer'; -import { CreateIndexButton } from './createIndex'; -import { cn } from '@/lib/cn'; -import Fuse from 'fuse.js'; -import { Input } from '@arco-design/web-react'; -import { EmptyArea } from './EmptyArea'; -import { TimeAgo } from './timeago'; +"use client"; +import { useCurrentInstance } from "@/hooks/useCurrentInstance"; +import { useInstanceStats } from "@/hooks/useInstanceStats"; +import { cn } from "@/lib/cn"; +import { Input } from "@arco-design/web-react"; +import { Button, Pagination, Tag, Tooltip } from "@douyinfe/semi-ui"; +import { Card, CardBody, CardHeader } from "@nextui-org/react"; +import { IconAlertTriangle } from "@tabler/icons-react"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { Link, useNavigate } from "@tanstack/react-router"; +import Fuse from "fuse.js"; +import _ from "lodash"; +import type MeiliSearch from "meilisearch"; +import type { Index } from "meilisearch"; +import { type FC, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useImmer } from "use-immer"; +import { EmptyArea } from "./EmptyArea"; +import { CreateIndexButton } from "./createIndex"; +import { TimeAgo } from "./timeago"; interface Props { - className?: string; - client: MeiliSearch; + className?: string; + client: MeiliSearch; } const fuse = new Fuse([], { - keys: ['uid', 'primaryKey'], - includeMatches: false, - includeScore: true, - shouldSort: true, + keys: ["uid", "primaryKey"], + includeMatches: false, + includeScore: true, + shouldSort: true, }); -export const IndexList: FC = ({ className = '', client }) => { - const navigate = useNavigate(); - const { t } = useTranslation('index'); - const currentInstance = useCurrentInstance(); - const host = currentInstance?.host; - const stats = useInstanceStats(client); +export const IndexList: FC = ({ className = "", client }) => { + const navigate = useNavigate(); + const { t } = useTranslation("index"); + const currentInstance = useCurrentInstance(); + const host = currentInstance?.host; + const stats = useInstanceStats(client); - const [state, updateState] = useImmer({ - offset: 0, - limit: 24, - query: '', - }); + const [state, updateState] = useImmer({ + offset: 0, + limit: 24, + query: "", + }); - const query = useQuery({ - queryKey: ['indexList', host, state], - queryFn: async () => { - const res = await client.getIndexes(_.pick(state, ['offset', 'limit'])); - fuse.setCollection(res.results || []); - return res; - }, - placeholderData: keepPreviousData, - }); + const query = useQuery({ + queryKey: ["indexList", host, state], + queryFn: async () => { + const res = await client.getIndexes(_.pick(state, ["offset", "limit"])); + fuse.setCollection(res.results || []); + return res; + }, + placeholderData: keepPreviousData, + }); - const filteredData = useMemo(() => { - // empty string cause fuse.search return empty array. - if (state.query && state.query.trim().length > 0) { - return fuse.search(state.query).map((d) => d.item) || []; - } - return query.data?.results || []; - }, [query.data?.results, state.query]); + const filteredData = useMemo(() => { + // empty string cause fuse.search return empty array. + if (state.query && state.query.trim().length > 0) { + return fuse.search(state.query).map((d) => d.item) || []; + } + return query.data?.results || []; + }, [query.data?.results, state.query]); - const listData = useMemo(() => { - return filteredData.map((index) => { - const uid = index.uid; - const indexStats = stats?.indexes[index.uid]; + const listData = useMemo(() => { + return filteredData.map((index) => { + const uid = index.uid; + const indexStats = stats?.indexes[index.uid]; - return { - uid, - numberOfDocuments: indexStats?.numberOfDocuments || 0, - href: `/ins/${currentInstance.id}/index/${uid}`, - isIndexing: indexStats?.isIndexing, - createdAt: index.createdAt, - updatedAt: index.updatedAt, - primaryKey: index.primaryKey, - }; - }); - }, [currentInstance.id, filteredData, stats?.indexes]); + return { + uid, + numberOfDocuments: indexStats?.numberOfDocuments || 0, + href: `/ins/${currentInstance.id}/index/${uid}`, + isIndexing: indexStats?.isIndexing, + createdAt: index.createdAt, + updatedAt: index.updatedAt, + primaryKey: index.primaryKey, + }; + }); + }, [currentInstance.id, filteredData, stats?.indexes]); - const pagination = useMemo(() => { - return { - currentPage: state.offset / state.limit + 1, - totalPage: _.ceil((query.data?.total || 0) / state.limit), - }; - }, [query.data?.total, state.limit, state.offset]); + const pagination = useMemo(() => { + return { + currentPage: state.offset / state.limit + 1, + totalPage: _.ceil((query.data?.total || 0) / state.limit), + }; + }, [query.data?.total, state.limit, state.offset]); - return useMemo( - () => ( -
-
-
{t('common:indexes')}
- -
- - updateState((d) => { - d.query = v; - }) - } - /> -
-
- query.refetch()} /> -
- {listData && listData.length > 0 ? ( -
- {listData?.map((item) => { - return ( - - -
{item.uid}
-
- -
- {item.createdAt && ( -

- {t('common:created_at')} -

- )} - {item.updatedAt && ( -

- {t('common:updated_at')} -

- )} -
-
-
- - {t('count')}: {item.numberOfDocuments ?? 0} - - {item.isIndexing && ( - - - -
{t('indexing')}...
-
-
- )} -
-
- -
} - size="small" - onClick={() => navigate({ to: `index/${item.uid}/setting`, from: '/ins/$insID' })} - /> - - -
} - size="small" - onClick={() => - navigate({ to: `tasks`, search: { indexUids: [item.uid] }, from: '/ins/$insID' }) - } - /> - -
-
- - - ); - })} - - ) : ( - - )} -
- { - updateState((d) => { - d.offset = (c - 1) * state.limit; - }); - query.refetch(); - }} - /> -
- - ), - [className, listData, navigate, pagination.currentPage, query, state.limit, t, updateState] - ); + return useMemo( + () => ( +
+
+
+ {t("common:indexes")} +
+ +
+ + updateState((d) => { + d.query = v; + }) + } + /> +
+
+ query.refetch()} /> +
+ {listData && listData.length > 0 ? ( +
+ {listData?.map((item) => { + return ( + + +
{item.uid}
+
+ +
+ {item.createdAt && ( +

+ {t("common:created_at")}{" "} + +

+ )} + {item.updatedAt && ( +

+ {t("common:updated_at")}{" "} + +

+ )} +
+
+
+ + {t("count")}: {item.numberOfDocuments ?? 0} + + {item.isIndexing && ( + + + +
{t("indexing")}...
+
+
+ )} +
+
+ +
+
+
+
+ ); + })} +
+ ) : ( + + )} +
+ { + updateState((d) => { + d.offset = (c - 1) * state.limit; + }); + query.refetch(); + }} + /> +
+
+ ), + [ + className, + listData, + navigate, + pagination.currentPage, + query, + state.limit, + t, + updateState, + ], + ); }; diff --git a/src/components/InsHeader.tsx b/src/components/InsHeader.tsx index 5913f11..2924c96 100644 --- a/src/components/InsHeader.tsx +++ b/src/components/InsHeader.tsx @@ -1,44 +1,44 @@ -import { IconBook2, IconBrandGithub, IconBug } from '@tabler/icons-react'; -import { DashBreadcrumb } from './Breadcrumb'; -import { Logo } from './Logo'; -import { LangSelector } from './lang'; -import { useTranslation } from 'react-i18next'; -import { Link } from '@arco-design/web-react'; +import { Link } from "@arco-design/web-react"; +import { IconBook2, IconBrandGithub, IconBug } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { DashBreadcrumb } from "./Breadcrumb"; +import { Logo } from "./Logo"; +import { LangSelector } from "./lang"; export const Header = () => { - const { t } = useTranslation('header'); + const { t } = useTranslation("header"); - return ( -
- - -
- } - > - {t('meilisearch_docs')} - - } - > - {t('issues')} - - } - > - {t('open_source')} - - -
-
- ); + return ( +
+ + +
+ } + > + {t("meilisearch_docs")} + + } + > + {t("issues")} + + } + > + {t("open_source")} + + +
+
+ ); }; diff --git a/src/components/Logo/index.tsx b/src/components/Logo/index.tsx index 5b211d5..d84aa03 100644 --- a/src/components/Logo/index.tsx +++ b/src/components/Logo/index.tsx @@ -1,19 +1,19 @@ -import { cn } from '@/lib/cn'; -import type { FC } from 'react'; -import meiliLOGO from '@/assets/meili-logo.svg'; +import meiliLOGO from "@/assets/meili-logo.svg"; +import { cn } from "@/lib/cn"; +import type { FC } from "react"; export const Logo: FC<{ - className?: string; - href?: string; + className?: string; + href?: string; }> = ({ className, href }) => { - return ( - - Meili logo - - ); + return ( + + Meili logo + + ); }; diff --git a/src/components/Settings/dangerZone.tsx b/src/components/Settings/dangerZone.tsx index e8fee94..ca992e8 100644 --- a/src/components/Settings/dangerZone.tsx +++ b/src/components/Settings/dangerZone.tsx @@ -1,113 +1,119 @@ -import { FC, useMemo } from 'react'; -import { useNavigate } from '@tanstack/react-router'; -import { useMutation } from '@tanstack/react-query'; -import { hiddenRequestLoader, showRequestLoader } from '@/utils/loader'; -import { showTaskSubmitNotification } from '@/utils/text'; -import { useCurrentInstance } from '@/hooks/useCurrentInstance'; -import { useTranslation } from 'react-i18next'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { useCurrentIndex } from '@/hooks/useCurrentIndex'; -import { Modal } from '@douyinfe/semi-ui'; -import { Button } from '@nextui-org/react'; +import { useCurrentIndex } from "@/hooks/useCurrentIndex"; +import { useCurrentInstance } from "@/hooks/useCurrentInstance"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { hiddenRequestLoader, showRequestLoader } from "@/utils/loader"; +import { showTaskSubmitNotification } from "@/utils/text"; +import { Modal } from "@douyinfe/semi-ui"; +import { Button } from "@nextui-org/react"; +import { useMutation } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; +import { type FC, useMemo } from "react"; +import { useTranslation } from "react-i18next"; export const DangerZone: FC<{ - afterMutation: () => void; + afterMutation: () => void; }> = ({ afterMutation }) => { - const { t } = useTranslation('index'); - const navigate = useNavigate(); - const currentInstance = useCurrentInstance(); - const client = useMeiliClient(); - const currentIndex = useCurrentIndex(client); + const { t } = useTranslation("index"); + const navigate = useNavigate(); + const currentInstance = useCurrentInstance(); + const client = useMeiliClient(); + const currentIndex = useCurrentIndex(client); - const delIndexMutation = useMutation({ - mutationFn: async () => { - showRequestLoader(); - return await currentIndex.index.delete(); - }, - onSuccess: (t) => { - showTaskSubmitNotification(t); - afterMutation(); - navigate({ to: `/ins/${currentInstance.id}` }); - }, - onSettled: () => { - hiddenRequestLoader(); - }, - }); + const delIndexMutation = useMutation({ + mutationFn: async () => { + showRequestLoader(); + return await currentIndex.index.delete(); + }, + onSuccess: (t) => { + showTaskSubmitNotification(t); + afterMutation(); + navigate({ to: `/ins/${currentInstance.id}` }); + }, + onSettled: () => { + hiddenRequestLoader(); + }, + }); - const delIndexAllDocumentsMutation = useMutation({ - mutationFn: async () => { - showRequestLoader(); - return await currentIndex.index.deleteAllDocuments(); - }, - onSuccess: (t) => { - showTaskSubmitNotification(t); - afterMutation(); - }, - onSettled: () => { - hiddenRequestLoader(); - }, - }); + const delIndexAllDocumentsMutation = useMutation({ + mutationFn: async () => { + showRequestLoader(); + return await currentIndex.index.deleteAllDocuments(); + }, + onSuccess: (t) => { + showTaskSubmitNotification(t); + afterMutation(); + }, + onSettled: () => { + hiddenRequestLoader(); + }, + }); - return useMemo( - () => ( -
-

{t('setting.index.danger_zone')}

-
-
- - -
-
-
- ), - [t, currentIndex.index.uid, delIndexMutation, delIndexAllDocumentsMutation] - ); + return useMemo( + () => ( +
+

+ {t("setting.index.danger_zone")} +

+
+
+ + +
+
+
+ ), + [t, currentIndex.index.uid, delIndexMutation, delIndexAllDocumentsMutation], + ); }; diff --git a/src/components/Settings/index.ts b/src/components/Settings/index.ts index a10613d..808461f 100644 --- a/src/components/Settings/index.ts +++ b/src/components/Settings/index.ts @@ -1,11 +1,11 @@ -import { Index } from 'meilisearch'; +import type { Index } from "meilisearch"; export type IndexSettingComponentProps = { - className?: string; - host?: string; - client: Index; + className?: string; + host?: string; + client: Index; }; export type IndexSettingConfigComponentProps = IndexSettingComponentProps & { - toggleLoading: (bool: boolean) => void; + toggleLoading: (bool: boolean) => void; }; diff --git a/src/components/createIndex.tsx b/src/components/createIndex.tsx index 92c9115..4b53b30 100644 --- a/src/components/createIndex.tsx +++ b/src/components/createIndex.tsx @@ -1,104 +1,113 @@ -import { useCallback, useState } from 'react'; -import { useForm } from '@mantine/form'; -import _ from 'lodash'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { showTaskSubmitNotification } from '@/utils/text'; -import { toast } from '@/utils/toast'; -import { useTranslation } from 'react-i18next'; -import { Input, Modal, Tooltip } from '@douyinfe/semi-ui'; -import { Button } from '@nextui-org/react'; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { showTaskSubmitNotification } from "@/utils/text"; +import { toast } from "@/utils/toast"; +import { Input, Modal, Tooltip } from "@douyinfe/semi-ui"; +import { useForm } from "@mantine/form"; +import { Button } from "@nextui-org/react"; +import _ from "lodash"; +import type { EnqueuedTask } from "meilisearch"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; type Props = { - afterMutation: () => void; + afterMutation: () => void; }; export const CreateIndexButton = ({ afterMutation }: Props) => { - const { t } = useTranslation('instance'); - const client = useMeiliClient(); - const form = useForm({ - initialValues: { - uid: '', - primaryKey: undefined, - }, - validate: { - uid: (value: string) => (/[\da-zA-Z-_]+/.test(value) ? null : t('create_index.form.uid.validation_error')), - }, - }); + const { t } = useTranslation("instance"); + const client = useMeiliClient(); + const form = useForm({ + initialValues: { + uid: "", + primaryKey: undefined, + }, + validate: { + uid: (value: string) => + /[\da-zA-Z-_]+/.test(value) + ? null + : t("create_index.form.uid.validation_error"), + }, + }); - const [visible, setVisible] = useState(false); + const [visible, setVisible] = useState(false); - const showDialog = () => { - setVisible(true); - }; - const closeDialog = () => { - setVisible(false); - form.reset(); - }; + const showDialog = () => { + setVisible(true); + }; + const closeDialog = () => { + setVisible(false); + form.reset(); + }; - const onCreateSubmit = useCallback( - async (values: typeof form.values) => { - let task; - try { - task = await client.createIndex(values.uid, { primaryKey: values.primaryKey }); - console.info(task); - if (!_.isEmpty(task)) { - showTaskSubmitNotification(task); - afterMutation(); - } - } catch (e) { - console.warn(e); - toast.error(t('toast.fail', { msg: e as string })); - } - }, - [afterMutation, client, form, t] - ); + const onCreateSubmit = async (values: typeof form.values) => { + let task: EnqueuedTask; + try { + task = await client.createIndex(values.uid, { + primaryKey: values.primaryKey, + }); + console.info(task); + if (!_.isEmpty(task)) { + showTaskSubmitNotification(task); + afterMutation(); + } + } catch (e) { + console.warn(e); + toast.error(t("toast.fail", { msg: e as string })); + } + }; - return ( - <> - - { - closeDialog(); - }} - onOk={async () => { - await onCreateSubmit(form.values); - closeDialog(); - }} - onCancel={closeDialog} - > -
- - - - - - - -
- - ); + return ( + <> + + { + closeDialog(); + }} + onOk={async () => { + await onCreateSubmit(form.values); + closeDialog(); + }} + onCancel={closeDialog} + > +
+ + + + + + + +
+ + ); }; diff --git a/src/components/dump.tsx b/src/components/dump.tsx index c92aa16..df4806d 100644 --- a/src/components/dump.tsx +++ b/src/components/dump.tsx @@ -1,33 +1,35 @@ -import { useCurrentInstance } from '@/hooks/useCurrentInstance'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { Button } from '@nextui-org/react'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { showTaskSubmitNotification } from '@/utils/text'; -import { Modal } from '@douyinfe/semi-ui'; +import { useCurrentInstance } from "@/hooks/useCurrentInstance"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { showTaskSubmitNotification } from "@/utils/text"; +import { Modal } from "@douyinfe/semi-ui"; +import { Button } from "@nextui-org/react"; +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; export const DumpButton = () => { - const { t } = useTranslation('instance'); - const currentInstance = useCurrentInstance(); - const client = useMeiliClient(); + const { t } = useTranslation("instance"); + const currentInstance = useCurrentInstance(); + const client = useMeiliClient(); - const onClickDump = useCallback(() => { - Modal.confirm({ - title: t('instance:dump.dialog.title'), - centered: true, - content:

{t('instance:dump.dialog.tip', { name: currentInstance.name })}

, - onOk: async () => { - showTaskSubmitNotification(await client.createDump()); - }, - okText: t('confirm'), - cancelText: t('cancel'), - }); - }, [client, currentInstance.name, t]); + const onClickDump = useCallback(() => { + Modal.confirm({ + title: t("instance:dump.dialog.title"), + centered: true, + content: ( +

{t("instance:dump.dialog.tip", { name: currentInstance.name })}

+ ), + onOk: async () => { + showTaskSubmitNotification(await client.createDump()); + }, + okText: t("confirm"), + cancelText: t("cancel"), + }); + }, [client, currentInstance.name, t]); - return ( - - ); + return ( + + ); }; diff --git a/src/components/indexConfigEditor.tsx b/src/components/indexConfigEditor.tsx index 58774f6..88be4ce 100644 --- a/src/components/indexConfigEditor.tsx +++ b/src/components/indexConfigEditor.tsx @@ -1,167 +1,198 @@ -import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import { hiddenRequestLoader, showRequestLoader } from '@/utils/loader'; -import { showTaskSubmitNotification } from '@/utils/text'; -import { Settings } from 'meilisearch'; -import MonacoEditor from '@monaco-editor/react'; -import { useTranslation } from 'react-i18next'; -import { useCurrentIndex } from '@/hooks/useCurrentIndex'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { cn } from '@/lib/cn'; -import { Button } from '@nextui-org/react'; +import { useCurrentIndex } from "@/hooks/useCurrentIndex"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { cn } from "@/lib/cn"; +import { hiddenRequestLoader, showRequestLoader } from "@/utils/loader"; +import { showTaskSubmitNotification } from "@/utils/text"; +import MonacoEditor from "@monaco-editor/react"; +import { Button } from "@nextui-org/react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import type { Settings } from "meilisearch"; +import { + type FC, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; export const IndexConfigEditor: FC<{ - className?: string; + className?: string; }> = ({ className }) => { - const { t } = useTranslation('index'); - const client = useMeiliClient(); - const currentIndex = useCurrentIndex(client); + const { t } = useTranslation("index"); + const client = useMeiliClient(); + const currentIndex = useCurrentIndex(client); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const editorRef = useRef(null); - const [isSettingsEditing, setIsSettingsEditing] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const editorRef = useRef(null); + const [isSettingsEditing, setIsSettingsEditing] = useState(false); - const onClickEditSettings = useCallback(() => { - setIsSettingsEditing(true); - }, []); + const onClickEditSettings = useCallback(() => { + setIsSettingsEditing(true); + }, []); - const [indexSettingDisplayData, setIndexSettingDisplayData] = useState({}); - const [indexSettingInputData, setIndexSettingInputData] = useState(indexSettingDisplayData); + const [indexSettingDisplayData, setIndexSettingDisplayData] = + useState({}); + const [indexSettingInputData, setIndexSettingInputData] = useState( + indexSettingDisplayData, + ); - const resetSettings = useCallback( - (data: Settings = {}) => { - setIsSettingsEditing(false); - setIndexSettingDisplayData(data); - if (!isSettingsEditing) { - setIndexSettingInputData(data); - editorRef.current?.setValue(JSON.stringify(data, null, 2)); - } - }, - [isSettingsEditing] - ); + const resetSettings = useCallback( + (data: Settings = {}) => { + setIsSettingsEditing(false); + setIndexSettingDisplayData(data); + if (!isSettingsEditing) { + setIndexSettingInputData(data); + editorRef.current?.setValue(JSON.stringify(data, null, 2)); + } + }, + [isSettingsEditing], + ); - const querySettings = useQuery({ - queryKey: ['settings', client.config.host, currentIndex.index.uid], - queryFn: async () => { - showRequestLoader(); - return await currentIndex.index.getSettings(); - }, - }); + const querySettings = useQuery({ + queryKey: ["settings", client.config.host, currentIndex.index.uid], + queryFn: async () => { + showRequestLoader(); + return await currentIndex.index.getSettings(); + }, + }); - useEffect(() => { - if (querySettings.isSuccess) { - // change display data when not editing - !isSettingsEditing && resetSettings(querySettings.data); - } - if (!querySettings.isFetching) { - hiddenRequestLoader(); - } - }, [isSettingsEditing, querySettings.data, querySettings.isFetching, querySettings.isSuccess, resetSettings]); + useEffect(() => { + if (querySettings.isSuccess) { + // change display data when not editing + !isSettingsEditing && resetSettings(querySettings.data); + } + if (!querySettings.isFetching) { + hiddenRequestLoader(); + } + }, [ + isSettingsEditing, + querySettings.data, + querySettings.isFetching, + querySettings.isSuccess, + resetSettings, + ]); - const onSettingJsonEditorUpdate = useCallback( - (value?: string) => value && setIndexSettingInputData(JSON.parse(value) as Settings), - [setIndexSettingInputData] - ); + const onSettingJsonEditorUpdate = useCallback( + (value?: string) => + value && setIndexSettingInputData(JSON.parse(value) as Settings), + [], + ); - const settingsMutation = useMutation({ - mutationFn: async (variables: Settings) => { - showRequestLoader(); - return await currentIndex.index.updateSettings(variables); - }, + const settingsMutation = useMutation({ + mutationFn: async (variables: Settings) => { + showRequestLoader(); + return await currentIndex.index.updateSettings(variables); + }, - onSuccess: (t) => { - showTaskSubmitNotification(t); - setTimeout(() => querySettings.refetch(), 450); - }, - onSettled: () => { - hiddenRequestLoader(); - }, - }); + onSuccess: (t) => { + showTaskSubmitNotification(t); + setTimeout(() => querySettings.refetch(), 450); + }, + onSettled: () => { + hiddenRequestLoader(); + }, + }); - const onSaveSettings = useCallback(() => { - setIsSettingsEditing(false); - indexSettingInputData && settingsMutation.mutate(indexSettingInputData); - }, [indexSettingInputData, settingsMutation]); + const onSaveSettings = useCallback(() => { + setIsSettingsEditing(false); + indexSettingInputData && settingsMutation.mutate(indexSettingInputData); + }, [indexSettingInputData, settingsMutation]); - const isLoading = useMemo(() => { - return querySettings.isLoading || querySettings.isFetching || settingsMutation.isPending; - }, [querySettings.isFetching, querySettings.isLoading, settingsMutation.isPending]); + const isLoading = useMemo(() => { + return ( + querySettings.isLoading || + querySettings.isFetching || + settingsMutation.isPending + ); + }, [ + querySettings.isFetching, + querySettings.isLoading, + settingsMutation.isPending, + ]); - return useMemo( - () => ( -
-
-
-

JSON {t('setting.index.config.label')}

- -
-
-
- {!isSettingsEditing && ( - - )} - {isSettingsEditing && ( - - )} - {isSettingsEditing && ( - - )} -
- { - editorRef.current = ed; - }} - > -
- ), - [ - className, - t, - isSettingsEditing, - isLoading, - indexSettingDisplayData, - onSettingJsonEditorUpdate, - onClickEditSettings, - onSaveSettings, - resetSettings, - ] - ); + return useMemo( + () => ( +
+
+
+

JSON {t("setting.index.config.label")}

+ +
+ +
+ {!isSettingsEditing && ( + + )} + {isSettingsEditing && ( + + )} + {isSettingsEditing && ( + + )} +
+ { + editorRef.current = ed; + }} + /> +
+ ), + [ + className, + t, + isSettingsEditing, + isLoading, + indexSettingDisplayData, + onSettingJsonEditorUpdate, + onClickEditSettings, + onSaveSettings, + resetSettings, + ], + ); }; diff --git a/src/components/indexPrimaryKey.tsx b/src/components/indexPrimaryKey.tsx index a8aff67..e2a4969 100644 --- a/src/components/indexPrimaryKey.tsx +++ b/src/components/indexPrimaryKey.tsx @@ -1,93 +1,102 @@ -import { useCallback, useState } from 'react'; -import { useForm } from '@mantine/form'; -import _ from 'lodash'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { showTaskSubmitNotification } from '@/utils/text'; -import { toast } from '@/utils/toast'; -import { useTranslation } from 'react-i18next'; -import { Input, Modal, Tooltip } from '@douyinfe/semi-ui'; -import { Copyable } from './Copyable'; -import { useCurrentIndex } from '@/hooks/useCurrentIndex'; +import { useCurrentIndex } from "@/hooks/useCurrentIndex"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { showTaskSubmitNotification } from "@/utils/text"; +import { toast } from "@/utils/toast"; +import { Input, Modal, Tooltip } from "@douyinfe/semi-ui"; +import { useForm } from "@mantine/form"; +import _ from "lodash"; +import type { EnqueuedTask } from "meilisearch"; +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Copyable } from "./Copyable"; type Props = { - afterMutation: () => void; + afterMutation: () => void; }; export const IndexPrimaryKey = ({ afterMutation }: Props) => { - const { t } = useTranslation('index'); - const client = useMeiliClient(); - const currentIndex = useCurrentIndex(client); - const form = useForm({ - initialValues: { - primaryKey: currentIndex.index.primaryKey, - }, - }); + const { t } = useTranslation("index"); + const client = useMeiliClient(); + const currentIndex = useCurrentIndex(client); + const form = useForm({ + initialValues: { + primaryKey: currentIndex.index.primaryKey, + }, + }); - const [visible, setVisible] = useState(false); + const [visible, setVisible] = useState(false); - const showDialog = () => { - setVisible(true); - }; - const closeDialog = () => { - setVisible(false); - form.reset(); - }; + const showDialog = () => { + setVisible(true); + }; + const closeDialog = () => { + setVisible(false); + form.reset(); + }; - const onSubmit = useCallback( - async (values: typeof form.values) => { - let task; - try { - task = await currentIndex.index.update({ primaryKey: values.primaryKey }); - console.info(task); - if (!_.isEmpty(task)) { - showTaskSubmitNotification(task); - afterMutation(); - } - } catch (e) { - console.warn(e); - toast.error(t('toast.fail', { msg: e as string })); - } - }, - [afterMutation, currentIndex.index, form, t] - ); + const onSubmit = useCallback( + async (values: typeof form.values) => { + let task: EnqueuedTask; + try { + task = await currentIndex.index.update({ + primaryKey: values.primaryKey, + }); + console.info(task); + if (!_.isEmpty(task)) { + showTaskSubmitNotification(task); + afterMutation(); + } + } catch (e) { + console.warn(e); + toast.error(t("toast.fail", { msg: e as string })); + } + }, + [afterMutation, currentIndex.index, t], + ); - return ( - <> -
- {currentIndex.index.primaryKey ? ( - {currentIndex.index.primaryKey as string} - ) : ( - t('common:none') - )} -
{ - showDialog(); - }} - >
-
- { - closeDialog(); - }} - onOk={async () => { - await onSubmit(form.values); - closeDialog(); - }} - onCancel={closeDialog} - > -
- - - - -
- - ); + return ( + <> +
+ {currentIndex.index.primaryKey ? ( + {currentIndex.index.primaryKey as string} + ) : ( + t("common:none") + )} +
{ + showDialog(); + }} + /> +
+ { + closeDialog(); + }} + onOk={async () => { + await onSubmit(form.values); + closeDialog(); + }} + onCancel={closeDialog} + > +
+ + + + +
+ + ); }; diff --git a/src/components/instanceFormModal.tsx b/src/components/instanceFormModal.tsx index c5b0fce..d497d94 100644 --- a/src/components/instanceFormModal.tsx +++ b/src/components/instanceFormModal.tsx @@ -1,177 +1,211 @@ -'use client'; -import { FC, ReactNode, useCallback, useState } from 'react'; -import { Drawer } from 'vaul'; -import { cn } from '@/lib/cn'; -import { Footer } from './Footer'; -import { useTranslation } from 'react-i18next'; -import { Controller, useForm } from 'react-hook-form'; -import { defaultInstance, Instance, useAppStore } from '@/store'; -import { useCurrentInstance } from '@/hooks/useCurrentInstance'; -import { Input, Button, Tooltip } from '@nextui-org/react'; -import _ from 'lodash'; -import { testConnection } from '@/utils/conn'; +"use client"; +import { useCurrentInstance } from "@/hooks/useCurrentInstance"; +import { cn } from "@/lib/cn"; +import { type Instance, defaultInstance, useAppStore } from "@/store"; +import { testConnection } from "@/utils/conn"; +import { Button, Input, Tooltip } from "@nextui-org/react"; +import _ from "lodash"; +import { type FC, type ReactNode, useCallback, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { Drawer } from "vaul"; +import { Footer } from "./Footer"; interface Props { - className?: string; - children: ReactNode; - ins?: Instance; - type?: 'create' | 'edit'; + className?: string; + children: ReactNode; + ins?: Instance; + type?: "create" | "edit"; } -type InstanceFormValue = Pick; +type InstanceFormValue = Pick; -export const InsFormModal: FC = ({ className = '', children, type = 'create', ins }) => { - const [visible, setVisible] = useState(false); - const { t } = useTranslation('instance'); - const currentInstance = useCurrentInstance(); - const [instanceFormType] = useState<'create' | 'edit'>(type); - const [isSubmitInstanceLoading, setIsSubmitInstanceLoading] = useState(false); - const [instanceEditing] = useState(ins); - const addInstance = useAppStore((state) => state.addInstance); - const editInstance = useAppStore((state) => state.editInstance); - const instances = useAppStore((state) => state.instances); +export const InsFormModal: FC = ({ + className = "", + children, + type = "create", + ins, +}) => { + const [visible, setVisible] = useState(false); + const { t } = useTranslation("instance"); + const currentInstance = useCurrentInstance(); + const [instanceFormType] = useState<"create" | "edit">(type); + const [isSubmitInstanceLoading, setIsSubmitInstanceLoading] = useState(false); + const [instanceEditing] = useState(ins); + const addInstance = useAppStore((state) => state.addInstance); + const editInstance = useAppStore((state) => state.editInstance); + const instances = useAppStore((state) => state.instances); - const form = useForm({ - defaultValues: { - ...defaultInstance, - host: currentInstance?.host ?? defaultInstance.host, - apiKey: currentInstance?.apiKey ?? defaultInstance.apiKey, - }, - }); + const form = useForm({ + defaultValues: { + ...defaultInstance, + host: currentInstance?.host ?? defaultInstance.host, + apiKey: currentInstance?.apiKey ?? defaultInstance.apiKey, + }, + }); - const onSubmitInstance = useCallback( - async (values: InstanceFormValue) => { - // button loading - setIsSubmitInstanceLoading(true); - // normalize host string - const cfg = { - ...values, - host: `${/^(https?:\/\/)/.test(values.host) ? '' : 'http://'}${values.host}`, - }; - // remove empty apikey - cfg.apiKey = _.isEmpty(cfg.apiKey) ? undefined : cfg.apiKey; - // do connection check - testConnection({ ...cfg }) - .finally(() => { - setIsSubmitInstanceLoading(false); - }) - .then(() => { - switch (instanceFormType) { - case 'create': - addInstance({ ...cfg }); - break; - case 'edit': - editInstance(instanceEditing!.id, { ...cfg }); - break; - } - // close modal - setVisible(false); - form.reset(); - }); - }, - [instanceFormType, form, addInstance, editInstance, instanceEditing] - ); + const onSubmitInstance = useCallback( + async (values: InstanceFormValue) => { + // button loading + setIsSubmitInstanceLoading(true); + // normalize host string + const cfg = { + ...values, + host: `${/^(https?:\/\/)/.test(values.host) ? "" : "http://"}${values.host}`, + }; + // remove empty apikey + cfg.apiKey = _.isEmpty(cfg.apiKey) ? undefined : cfg.apiKey; + // do connection check + testConnection({ ...cfg }) + .finally(() => { + setIsSubmitInstanceLoading(false); + }) + .then(() => { + switch (instanceFormType) { + case "create": + addInstance({ ...cfg }); + break; + case "edit": + editInstance(instanceEditing!.id, { ...cfg }); + break; + } + // close modal + setVisible(false); + form.reset(); + }); + }, + [instanceFormType, form, addInstance, editInstance, instanceEditing], + ); - return ( - { - form.reset(ins); - setVisible(open); - }} - > - {children} - - - -
-
-

{t(`form.title.${instanceFormType}`)}

- { - let otherNames: string[] = []; - switch (instanceFormType) { - case 'create': - otherNames = instances.map((i) => i.name); - break; - case 'edit': - otherNames = instances.map((i) => i.name).filter((n) => n !== instanceEditing!.name); - break; - } - if (otherNames.includes(value)) { - form.setError('name', { message: 'Name should be different from others' }); - return false; - } else { - return true; - } - }, - }} - render={({ field, fieldState }) => ( - - )} - /> - { - if (/^(https?:\/\/)?([a-zA-Z0-9-]+((\.[a-zA-Z0-9-]+)+)?(:\d+)?)(\/[^\s]*)?$/.test(value)) { - return true; - } else { - form.setError('host', { message: 'Host not valid' }); - return false; - } - }, - }} - render={({ field, fieldState }) => ( - <> - - - - - )} - /> - ( - - - - )} - /> - -
- - - - - ); + return ( + { + form.reset(ins); + setVisible(open); + }} + > + + {children} + + + + +
+
+

+ {t(`form.title.${instanceFormType}`)} +

+ { + let otherNames: string[] = []; + switch (instanceFormType) { + case "create": + otherNames = instances.map((i) => i.name); + break; + case "edit": + otherNames = instances + .map((i) => i.name) + .filter((n) => n !== instanceEditing?.name); + break; + } + if (otherNames.includes(value)) { + form.setError("name", { + message: "Name should be different from others", + }); + return false; + } + return true; + }, + }} + render={({ field, fieldState }) => ( + + )} + /> + { + if ( + /^(https?:\/\/)?([a-zA-Z0-9-]+((\.[a-zA-Z0-9-]+)+)?(:\d+)?)(\/[^\s]*)?$/.test( + value, + ) + ) { + return true; + } + form.setError("host", { message: "Host not valid" }); + return false; + }, + }} + render={({ field, fieldState }) => ( + <> + + + + + )} + /> + ( + + + + )} + /> + +
+ + + + + ); }; diff --git a/src/components/keyForm.tsx b/src/components/keyForm.tsx index 51ca8c9..a3d64e4 100644 --- a/src/components/keyForm.tsx +++ b/src/components/keyForm.tsx @@ -1,295 +1,315 @@ -'use client'; -import { FC, useCallback, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Controller, useForm } from 'react-hook-form'; -import { Button, Tooltip } from '@nextui-org/react'; -import { Input, Select, DatePicker } from '@douyinfe/semi-ui'; -import _ from 'lodash'; -import { KeyCreation } from 'meilisearch'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { useIndexes } from '@/hooks/useIndexes'; -import { getTimeText } from '@/utils/text'; -import dayjs from 'dayjs'; -import { cn } from '@/lib/cn'; +"use client"; +import { useIndexes } from "@/hooks/useIndexes"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { cn } from "@/lib/cn"; +import { getTimeText } from "@/utils/text"; +import { DatePicker, Input, Select } from "@douyinfe/semi-ui"; +import { Button, Tooltip } from "@nextui-org/react"; +import dayjs from "dayjs"; +import _ from "lodash"; +import type { KeyCreation } from "meilisearch"; +import { type FC, useCallback, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; interface Props { - className?: string; - data?: Required; - type?: 'create' | 'edit'; - afterSubmit: () => void; + className?: string; + data?: Required; + type?: "create" | "edit"; + afterSubmit: () => void; } -type KeyFormValue = KeyCreation & Partial>; +type KeyFormValue = KeyCreation & + Partial>; -export const KeyForm: FC = ({ className = '', type = 'create', data, afterSubmit }) => { - const { t } = useTranslation('key'); - const client = useMeiliClient(); - const [formType] = useState<'create' | 'edit'>(type); - const [isSubmitLoading, setIsSubmitLoading] = useState(false); - const [editing] = useState(data); - // list as many as possible - const [indexes] = useIndexes(client, { limit: 10000 }); +export const KeyForm: FC = ({ + className = "", + type = "create", + data, + afterSubmit, +}) => { + const { t } = useTranslation("key"); + const client = useMeiliClient(); + const [formType] = useState<"create" | "edit">(type); + const [isSubmitLoading, setIsSubmitLoading] = useState(false); + const [editing] = useState(data); + // list as many as possible + const [indexes] = useIndexes(client, { limit: 10000 }); - const form = useForm({ - defaultValues: type === 'edit' ? editing : {}, - }); + const form = useForm({ + defaultValues: type === "edit" ? editing : {}, + }); - const onSubmit = useCallback( - async (values: KeyFormValue) => { - // loading - setIsSubmitLoading(true); - switch (formType) { - case 'create': - await client.createKey({ - ...values, - indexes: _.isEmpty(values.indexes) ? ['*'] : values.indexes, - actions: _.isEmpty(values.actions) ? ['*'] : values.actions, - // ignore type error for legacy version compatible - // @ts-ignore - expiresAt: _.isEmpty(values.expiresAt) - ? null - : getTimeText(values.expiresAt, { format: 'YYYY-MM-DD HH:mm:ss+00:00' }), - }); - break; - case 'edit': - await client.updateKey(editing!.uid, { ...values }); - break; - } - setIsSubmitLoading(false); - afterSubmit(); - }, - [formType, afterSubmit, client, editing] - ); + const onSubmit = useCallback( + async (values: KeyFormValue) => { + // loading + setIsSubmitLoading(true); + switch (formType) { + case "create": + await client.createKey({ + ...values, + indexes: _.isEmpty(values.indexes) ? ["*"] : values.indexes, + actions: _.isEmpty(values.actions) ? ["*"] : values.actions, + // ignore type error for legacy version compatible + // @ts-ignore + expiresAt: _.isEmpty(values.expiresAt) + ? null + : getTimeText(values.expiresAt, { + format: "YYYY-MM-DD HH:mm:ss+00:00", + }), + }); + break; + case "edit": + await client.updateKey(editing!.uid, { ...values }); + break; + } + setIsSubmitLoading(false); + afterSubmit(); + }, + [formType, afterSubmit, client, editing], + ); - return ( -
-

{t(`form.title.${formType}`)}

+ return ( + +

+ {t(`form.title.${formType}`)} +

- ( - - )} - /> - ( - - )} - /> - ( - - )} - /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> - - ( - - )} - /> - - - ( - - )} - /> - + + ( + + )} + /> + + + ( + + )} + /> + - - { - if (value && dayjs(value).isValid()) { - return true; - } else { - form.setError('expiresAt', { message: t('form.expireAt.invalid') }); - return false; - } - }, - }} - render={({ field }) => ( - + )} + /> + + + + ); }; diff --git a/src/components/lang.tsx b/src/components/lang.tsx index adf55a0..6352754 100644 --- a/src/components/lang.tsx +++ b/src/components/lang.tsx @@ -1,35 +1,41 @@ -'use client'; -import { FC, useEffect } from 'react'; -import { locale2DayjsLocale, SUPPORTED_LANGUAGE, SUPPORTED_LANGUAGE_LOCALIZED } from '../utils/i18n'; -import _ from 'lodash'; -import { useTranslation } from 'react-i18next'; -import { cn } from '@/lib/cn'; -import dayjs from 'dayjs'; -import 'dayjs/locale/zh-cn'; +"use client"; +import { cn } from "@/lib/cn"; +import dayjs from "dayjs"; +import _ from "lodash"; +import { type FC, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { + type SUPPORTED_LANGUAGE, + SUPPORTED_LANGUAGE_LOCALIZED, + locale2DayjsLocale, +} from "../utils/i18n"; +import "dayjs/locale/zh-cn"; interface Props { - className?: string; + className?: string; } -export const LangSelector: FC = ({ className = '' }) => { - const { i18n } = useTranslation(); - useEffect(() => { - dayjs.locale(locale2DayjsLocale(i18n.resolvedLanguage as SUPPORTED_LANGUAGE)); - }, [i18n.resolvedLanguage]); - return ( - - ); +export const LangSelector: FC = ({ className = "" }) => { + const { i18n } = useTranslation(); + useEffect(() => { + dayjs.locale( + locale2DayjsLocale(i18n.resolvedLanguage as SUPPORTED_LANGUAGE), + ); + }, [i18n.resolvedLanguage]); + return ( + + ); }; diff --git a/src/components/lazy.tsx b/src/components/lazy.tsx index 9a34e37..9603e3f 100644 --- a/src/components/lazy.tsx +++ b/src/components/lazy.tsx @@ -1,22 +1,24 @@ -'use client'; -import { FC, ReactNode, Suspense } from 'react'; -import clsx from 'clsx'; -import { Loader } from './loader'; +"use client"; +import clsx from "clsx"; +import { type FC, type ReactNode, Suspense } from "react"; +import { Loader } from "./loader"; interface Props { - className?: string; - children: ReactNode; + className?: string; + children: ReactNode; } -export const Lazy: FC = ({ className = '', children }) => { - return ( - - -
- } - > - {children} - - ); +export const Lazy: FC = ({ className = "", children }) => { + return ( + + +
+ } + > + {children} + + ); }; diff --git a/src/components/loader.tsx b/src/components/loader.tsx index 969f6e1..981a31f 100644 --- a/src/components/loader.tsx +++ b/src/components/loader.tsx @@ -1,38 +1,43 @@ -import type { FC } from 'react'; -import cls from '../style/loader.module.css'; -import { cn } from '@/lib/cn'; +import { cn } from "@/lib/cn"; +import type { FC } from "react"; +import cls from "../style/loader.module.css"; type Props = { - className?: string; + className?: string; }; export const Loader: FC = ({ className }) => { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
- ); + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); }; export const LoaderPage: FC< - Props & { - loaderCls?: string; - } + Props & { + loaderCls?: string; + } > = ({ className, loaderCls }) => { - return ( -
- -
- ); + return ( +
+ +
+ ); }; diff --git a/src/components/timeago.tsx b/src/components/timeago.tsx index 6b0059c..dec488e 100644 --- a/src/components/timeago.tsx +++ b/src/components/timeago.tsx @@ -1,26 +1,30 @@ -'use client'; -import { cn } from '@/lib/cn'; -import { getTimeAgo, getTimeText } from '@/utils/text'; -import dayjs from 'dayjs'; -import { FC, useState } from 'react'; +"use client"; +import { cn } from "@/lib/cn"; +import { getTimeAgo, getTimeText } from "@/utils/text"; +import dayjs from "dayjs"; +import { type FC, useState } from "react"; interface Props { - className?: string; - date?: Date; + className?: string; + date?: Date; } -export const TimeAgo: FC = ({ className = '', date }) => { - const [isHover, setIsHover] = useState(false); - return ( -
{ - setIsHover(true); - }} - onMouseLeave={() => { - setIsHover(false); - }} - > - {date && dayjs(date).isValid() ? (isHover ? getTimeText(date) : getTimeAgo(date)) : '-'} -
- ); +export const TimeAgo: FC = ({ className = "", date }) => { + const [isHover, setIsHover] = useState(false); + return ( +
{ + setIsHover(true); + }} + onMouseLeave={() => { + setIsHover(false); + }} + > + {date && dayjs(date).isValid() + ? isHover + ? getTimeText(date) + : getTimeAgo(date) + : "-"} +
+ ); }; diff --git a/src/components/title.tsx b/src/components/title.tsx index 890537b..0d72643 100644 --- a/src/components/title.tsx +++ b/src/components/title.tsx @@ -1,17 +1,23 @@ -'use client'; -import { cn } from '@/lib/cn'; -import _ from 'lodash'; -import { FC } from 'react'; +"use client"; +import { cn } from "@/lib/cn"; +import _ from "lodash"; +import type { FC } from "react"; interface Props { - className?: string; - children: string; + className?: string; + children: string; } -export const TitleWithUnderline: FC = ({ className = '', children }) => { - return ( -
- {_.truncate(children, { length: 20 })} -
-
- ); +export const TitleWithUnderline: FC = ({ className = "", children }) => { + return ( +
+ {_.truncate(children, { length: 20 })} +
+
+ ); }; diff --git a/src/hooks/useCurrentIndex.ts b/src/hooks/useCurrentIndex.ts index 08a12c8..d4d3e09 100644 --- a/src/hooks/useCurrentIndex.ts +++ b/src/hooks/useCurrentIndex.ts @@ -1,33 +1,39 @@ -import { MeiliSearch } from 'meilisearch'; -import { useEffect, useMemo } from 'react'; -import { useSuspenseQuery } from '@tanstack/react-query'; -import { useParams, useRouter } from '@tanstack/react-router'; -import _ from 'lodash'; -import { toast } from '@/utils/toast'; -import { useTranslation } from 'react-i18next'; +import { toast } from "@/utils/toast"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { useParams, useRouter } from "@tanstack/react-router"; +import _ from "lodash"; +import type { MeiliSearch } from "meilisearch"; +import { useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; export const useCurrentIndex = (client: MeiliSearch) => { - const { history } = useRouter(); - const { t, i18n } = useTranslation('index'); - const { indexUID, insID } = useParams({ strict: false }) as { insID: string; indexUID: string }; + const { history } = useRouter(); + const { t, i18n } = useTranslation("index"); + const { indexUID, insID } = useParams({ strict: false }) as { + insID: string; + indexUID: string; + }; - const query = useSuspenseQuery({ - queryKey: ['index', insID, indexUID], - queryFn: async () => { - console.debug('getting current index', client.config); - return await client.getIndex(indexUID); - }, - }); + const query = useSuspenseQuery({ + queryKey: ["index", insID, indexUID], + queryFn: async () => { + console.debug("getting current index", client.config); + return await client.getIndex(indexUID); + }, + }); - const ready = useMemo(() => query.isFetched && !_.isEmpty(query.data), [query.data, query.isFetched]); + const ready = useMemo( + () => query.isFetched && !_.isEmpty(query.data), + [query.data, query.isFetched], + ); - useEffect(() => { - if (query.isFetched && _.isEmpty(query.data)) { - console.debug('useCurrentIndex', 'lost index', query.data); - toast.error(`${t('not_found')} 🤥`); - history.back(); - } - }, [history, query.data, query.isFetched, i18n.resolvedLanguage, t]); + useEffect(() => { + if (query.isFetched && _.isEmpty(query.data)) { + console.debug("useCurrentIndex", "lost index", query.data); + toast.error(`${t("not_found")} 🤥`); + history.back(); + } + }, [history, query.data, query.isFetched, i18n.resolvedLanguage, t]); - return { index: query.data, ready, query }; + return { index: query.data, ready, query }; }; diff --git a/src/hooks/useCurrentInstance.ts b/src/hooks/useCurrentInstance.ts index 2926831..4e8c540 100644 --- a/src/hooks/useCurrentInstance.ts +++ b/src/hooks/useCurrentInstance.ts @@ -1,37 +1,38 @@ -import { Instance, useAppStore } from '@/store'; -import _ from 'lodash'; -import { toast } from '../utils/toast'; -import { useParams } from '@tanstack/react-router'; -import { useTranslation } from 'react-i18next'; -import { getSingletonCfg, isSingletonMode } from '@/utils/conn'; +import { type Instance, useAppStore } from "@/store"; +import { getSingletonCfg, isSingletonMode } from "@/utils/conn"; +import { useParams } from "@tanstack/react-router"; +import _ from "lodash"; +import { useTranslation } from "react-i18next"; +import { toast } from "../utils/toast"; export const useCurrentInstance = () => { - const { t } = useTranslation('instance'); - const { insID } = useParams({ strict: false }) as { insID: string }; - const currentInstance = useAppStore((state) => state.instances.find((i) => i.id === parseInt(insID || '1'))); - const setWarningPageData = useAppStore((state) => state.setWarningPageData); + const { t } = useTranslation("instance"); + const { insID } = useParams({ strict: false }) as { insID: string }; + let currentInstance = useAppStore((state) => + state.instances.find((i) => i.id === Number.parseInt(insID || "1")), + ); + const setWarningPageData = useAppStore((state) => state.setWarningPageData); - if (!isSingletonMode()) { - if (currentInstance && _.isEmpty(currentInstance)) { - toast.error(`${t('not_found')} 🤥`); - console.debug('useCurrentInstance', 'Instance lost'); - // do not use useNavigate, because maybe in first render - window.location.assign(import.meta.env.BASE_URL ?? '/'); - } - return currentInstance as Instance; - } else { - const currentInstance = getSingletonCfg(); - if (!currentInstance) { - toast.error(`${t('not_found')} 🤥`); - console.debug('useCurrentInstance', 'Singleton Instance lost'); - setWarningPageData({ prompt: t('instance:singleton_cfg_not_found') }); - // do not use useNavigate, because maybe in first render - if (import.meta.env.BASE_URL !== '/') { - window.location.assign((import.meta.env.BASE_URL || '') + '/warning'); - } else { - window.location.assign('/warning'); - } - } - return currentInstance as Instance; - } + if (!isSingletonMode()) { + if (currentInstance && _.isEmpty(currentInstance)) { + toast.error(`${t("not_found")} 🤥`); + console.debug("useCurrentInstance", "Instance lost"); + // do not use useNavigate, because maybe in first render + window.location.assign(import.meta.env.BASE_URL ?? "/"); + } + return currentInstance as Instance; + } + currentInstance = getSingletonCfg() as Instance; + if (!currentInstance) { + toast.error(`${t("not_found")} 🤥`); + console.debug("useCurrentInstance", "Singleton Instance lost"); + setWarningPageData({ prompt: t("instance:singleton_cfg_not_found") }); + // do not use useNavigate, because maybe in first render + if (import.meta.env.BASE_URL !== "/") { + window.location.assign(`${import.meta.env.BASE_URL || ""}/warning`); + } else { + window.location.assign("/warning"); + } + } + return currentInstance as Instance; }; diff --git a/src/hooks/useIndexes.ts b/src/hooks/useIndexes.ts index e5a2192..99f548c 100644 --- a/src/hooks/useIndexes.ts +++ b/src/hooks/useIndexes.ts @@ -1,27 +1,30 @@ -import { Index, MeiliSearch } from 'meilisearch'; -import { useEffect, useState } from 'react'; -import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import { IndexesQuery } from 'meilisearch/src/types'; -import { useCurrentInstance } from './useCurrentInstance'; +import { type UseQueryResult, useQuery } from "@tanstack/react-query"; +import type { Index, MeiliSearch } from "meilisearch"; +import type { IndexesQuery } from "meilisearch/src/types"; +import { useEffect, useState } from "react"; +import { useCurrentInstance } from "./useCurrentInstance"; -export const useIndexes = (client: MeiliSearch, params?: IndexesQuery): [Index[], UseQueryResult] => { - const currentInstance = useCurrentInstance(); - const host = currentInstance?.host; +export const useIndexes = ( + client: MeiliSearch, + params?: IndexesQuery, +): [Index[], UseQueryResult] => { + const currentInstance = useCurrentInstance(); + const host = currentInstance?.host; - const [indexes, setIndexes] = useState([]); + const [indexes, setIndexes] = useState([]); - const query = useQuery({ - queryKey: ['indexes', host], - queryFn: async () => { - return (await client.getIndexes(params)).results; - }, - }); + const query = useQuery({ + queryKey: ["indexes", host], + queryFn: async () => { + return (await client.getIndexes(params)).results; + }, + }); - useEffect(() => { - if (query.isSuccess) { - setIndexes(query.data); - } - }, [query.data, query.isSuccess]); + useEffect(() => { + if (query.isSuccess) { + setIndexes(query.data); + } + }, [query.data, query.isSuccess]); - return [indexes, query]; + return [indexes, query]; }; diff --git a/src/hooks/useInstanceHealth.ts b/src/hooks/useInstanceHealth.ts index 7dbadf2..1c26c6a 100644 --- a/src/hooks/useInstanceHealth.ts +++ b/src/hooks/useInstanceHealth.ts @@ -1,26 +1,26 @@ -import { MeiliSearch } from 'meilisearch'; -import { useEffect, useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { useCurrentInstance } from './useCurrentInstance'; +import { useQuery } from "@tanstack/react-query"; +import type { MeiliSearch } from "meilisearch"; +import { useEffect, useState } from "react"; +import { useCurrentInstance } from "./useCurrentInstance"; export const useInstanceHealth = (client: MeiliSearch) => { - const currentInstance = useCurrentInstance(); + const currentInstance = useCurrentInstance(); - const [health, setHealth] = useState(true); + const [health, setHealth] = useState(true); - const queryHealth = useQuery({ - queryKey: ['health', currentInstance?.host], - queryFn: async () => { - return (await client.health()).status === 'available'; - }, - refetchInterval: 30000, - }); + const queryHealth = useQuery({ + queryKey: ["health", currentInstance?.host], + queryFn: async () => { + return (await client.health()).status === "available"; + }, + refetchInterval: 30000, + }); - useEffect(() => { - if (queryHealth.isSuccess) { - setHealth(queryHealth.data); - } - }, [queryHealth.data, queryHealth.isSuccess]); + useEffect(() => { + if (queryHealth.isSuccess) { + setHealth(queryHealth.data); + } + }, [queryHealth.data, queryHealth.isSuccess]); - return health; + return health; }; diff --git a/src/hooks/useInstanceStats.ts b/src/hooks/useInstanceStats.ts index 9886f2e..92fae4f 100644 --- a/src/hooks/useInstanceStats.ts +++ b/src/hooks/useInstanceStats.ts @@ -1,28 +1,31 @@ -import { MeiliSearch, Stats, Version } from 'meilisearch'; -import { useEffect, useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { useCurrentInstance } from './useCurrentInstance'; +import { useQuery } from "@tanstack/react-query"; +import type { MeiliSearch, Stats, Version } from "meilisearch"; +import { useEffect, useState } from "react"; +import { useCurrentInstance } from "./useCurrentInstance"; export const useInstanceStats = (client: MeiliSearch) => { - const currentInstance = useCurrentInstance(); - const host = currentInstance?.host; - const [stats, setStats] = useState(); + const currentInstance = useCurrentInstance(); + const host = currentInstance?.host; + const [stats, setStats] = useState(); - const query = useQuery({ - queryKey: ['stats', host], - queryFn: async () => { - return { ...(await client.getStats()), version: await client.getVersion() }; - }, - }); + const query = useQuery({ + queryKey: ["stats", host], + queryFn: async () => { + return { + ...(await client.getStats()), + version: await client.getVersion(), + }; + }, + }); - useEffect(() => { - if (query.isSuccess) { - setStats(query.data); - } - if (query.isError) { - console.warn('get meilisearch stats error', query.error); - } - }, [query.data, query.error, query.isError, query.isSuccess]); + useEffect(() => { + if (query.isSuccess) { + setStats(query.data); + } + if (query.isError) { + console.warn("get meilisearch stats error", query.error); + } + }, [query.data, query.error, query.isError, query.isSuccess]); - return stats; + return stats; }; diff --git a/src/hooks/useMeiliClient.ts b/src/hooks/useMeiliClient.ts index e7a6cf4..ed4eb78 100644 --- a/src/hooks/useMeiliClient.ts +++ b/src/hooks/useMeiliClient.ts @@ -1,68 +1,70 @@ -import { MeiliSearch } from 'meilisearch'; -import { useCallback, useEffect, useState } from 'react'; -import _ from 'lodash'; -import { toast } from '../utils/toast'; -import { useCurrentInstance } from './useCurrentInstance'; -import { useTranslation } from 'react-i18next'; -import { isSingletonMode } from '@/utils/conn'; -import { useAppStore } from '@/store'; +import { useAppStore } from "@/store"; +import { isSingletonMode } from "@/utils/conn"; +import _ from "lodash"; +import { MeiliSearch } from "meilisearch"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "../utils/toast"; +import { useCurrentInstance } from "./useCurrentInstance"; export const useMeiliClient = () => { - const { t } = useTranslation('instance'); - const currentInstance = useCurrentInstance(); + const { t } = useTranslation("instance"); + const currentInstance = useCurrentInstance(); - const [client, setClient] = useState( - new MeiliSearch({ - ...currentInstance, - }) - ); + const [client, setClient] = useState( + new MeiliSearch({ + ...currentInstance, + }), + ); - const setWarningPageData = useAppStore((state) => state.setWarningPageData); + const setWarningPageData = useAppStore((state) => state.setWarningPageData); - const connect = useCallback(async () => { - if (_.isEmpty(currentInstance?.host)) { - toast.error(t('connection_failed')); - console.debug('useMeilisearchClient', 'connection config lost'); - if (!isSingletonMode()) { - // do not use useNavigate, because maybe in first render - window.location.assign(import.meta.env.BASE_URL ?? '/'); - } else { - setWarningPageData({ prompt: t('instance:singleton_cfg_not_found') }); - // do not use useNavigate, because maybe in first render - if (import.meta.env.BASE_URL !== '/') { - window.location.assign((import.meta.env.BASE_URL || '') + '/warning'); - } else { - window.location.assign('/warning'); - } - } - return; - } - const conn = new MeiliSearch({ ...currentInstance }); - try { - await conn.getStats(); - setClient(conn); - } catch (err) { - console.warn('useMeilisearchClient', 'test conn error', err); - toast.error(t('connection_failed')); - if (!isSingletonMode()) { - // do not use useNavigate, because maybe in first render - window.location.assign(import.meta.env.BASE_URL ?? '/'); - } else { - setWarningPageData({ prompt: t('instance:singleton_cfg_not_found') }); - // do not use useNavigate, because maybe in first render - if (import.meta.env.BASE_URL !== '/') { - window.location.assign((import.meta.env.BASE_URL || '') + '/warning'); - } else { - window.location.assign('/warning'); - } - } - } - }, [currentInstance, setWarningPageData, t]); + const connect = useCallback(async () => { + if (_.isEmpty(currentInstance?.host)) { + toast.error(t("connection_failed")); + console.debug("useMeilisearchClient", "connection config lost"); + if (!isSingletonMode()) { + // do not use useNavigate, because maybe in first render + window.location.assign(import.meta.env.BASE_URL ?? "/"); + } else { + setWarningPageData({ prompt: t("instance:singleton_cfg_not_found") }); + // do not use useNavigate, because maybe in first render + if (import.meta.env.BASE_URL !== "/") { + window.location.assign(`${import.meta.env.BASE_URL || ""}/warning`); + } else { + window.location.assign("/warning"); + } + } + return; + } + const conn = new MeiliSearch({ ...currentInstance }); + try { + await conn.getStats(); + setClient(conn); + } catch (err) { + console.warn("useMeilisearchClient", "test conn error", err); + toast.error(t("connection_failed")); + if (!isSingletonMode()) { + // do not use useNavigate, because maybe in first render + window.location.assign(import.meta.env.BASE_URL ?? "/"); + } else { + setWarningPageData({ prompt: t("instance:singleton_cfg_not_found") }); + // do not use useNavigate, because maybe in first render + if (import.meta.env.BASE_URL !== "/") { + window.location.assign(`${import.meta.env.BASE_URL || ""}/warning`); + } else { + window.location.assign("/warning"); + } + } + } + }, [currentInstance, setWarningPageData, t]); - useEffect(() => { - console.debug('useMeilisearchClient', 'rebuilt meili client'); - connect().then(); - }, [connect, currentInstance]); + // need to use currentInstance as deps, because it should be emitted when instance changed + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + console.debug("useMeilisearchClient", "rebuilt meili client"); + connect().then(); + }, [connect, currentInstance]); - return client; + return client; }; diff --git a/src/hooks/useRoutePreCheck.ts b/src/hooks/useRoutePreCheck.ts index 757063e..029eaf1 100644 --- a/src/hooks/useRoutePreCheck.ts +++ b/src/hooks/useRoutePreCheck.ts @@ -1,32 +1,32 @@ -import { useAppStore, WarningPageData } from '@/store'; -import { useNavigate, UseNavigateResult } from '@tanstack/react-router'; -import { useCallback } from 'react'; +import { type WarningPageData, useAppStore } from "@/store"; +import { type UseNavigateResult, useNavigate } from "@tanstack/react-router"; +import { useCallback } from "react"; export type NavigateFuncParams = Parameters>[0]; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type NavigateFunc = (params: NavigateFuncParams, opt?: any) => void; export const useNavigatePreCheck = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pre: (params: NavigateFuncParams, opt?: any) => null | WarningPageData + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pre: (params: NavigateFuncParams, opt?: any) => null | WarningPageData, ): NavigateFunc => { - const navigate = useNavigate(); - const setWarningPageData = useAppStore((state) => state.setWarningPageData); + const navigate = useNavigate(); + const setWarningPageData = useAppStore((state) => state.setWarningPageData); - const ret: NavigateFunc = useCallback( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (params: NavigateFuncParams, opt?: any) => { - console.debug('useNavigatePreCheck', params); - const preFuncRes = pre(params, opt); - if (preFuncRes !== null) { - setWarningPageData(preFuncRes); - navigate({ to: '/warning' }); - } else { - navigate(params ?? {}); - } - }, - [navigate, pre, setWarningPageData] - ); + const ret: NavigateFunc = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (params: NavigateFuncParams, opt?: any) => { + console.debug("useNavigatePreCheck", params); + const preFuncRes = pre(params, opt); + if (preFuncRes !== null) { + setWarningPageData(preFuncRes); + navigate({ to: "/warning" }); + } else { + navigate(params ?? {}); + } + }, + [navigate, pre, setWarningPageData], + ); - return ret; + return ret; }; diff --git a/src/lib/cn.ts b/src/lib/cn.ts index d084cca..ac680b3 100644 --- a/src/lib/cn.ts +++ b/src/lib/cn.ts @@ -1,6 +1,6 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } diff --git a/src/lib/semi.js b/src/lib/semi.js index 6d2eac2..f99aeee 100644 --- a/src/lib/semi.js +++ b/src/lib/semi.js @@ -3,13 +3,13 @@ * * PNPM 定制Semi DSM 替换 Plugin */ -import FS from 'fs'; -import Path from 'path'; +import FS from "node:fs"; +import Path from "node:path"; -import { pathToFileURL } from 'url'; +import { pathToFileURL } from "node:url"; -import pkg from 'sass'; -import { platform } from 'os'; +import { platform } from "node:os"; +import pkg from "sass"; const { compileString, Logger } = pkg; /** @@ -29,16 +29,18 @@ const { compileString, Logger } = pkg; */ export default function SemiPlugin({ theme, options = {} }) { return { - name: 'semi-theme', - enforce: 'post', + name: "semi-theme", + enforce: "post", load(id) { const filePath = normalizePath(id); if (options.include) { options.include = normalizePath(options.include); } // https://github.com/DouyinFE/semi-design/blob/main/packages/semi-webpack/src/semi-webpack-plugin.ts#L83 - if (/@douyinfe\/semi-(ui|icons|foundation)\/lib\/.+\.css$/.test(filePath)) { - const scssFilePath = filePath.replace(/\.css$/, '.scss'); + if ( + /@douyinfe\/semi-(ui|icons|foundation)\/lib\/.+\.css$/.test(filePath) + ) { + const scssFilePath = filePath.replace(/\.css$/, ".scss"); // 目前只有 name // https://github.com/DouyinFE/semi-design/blob/04d17a72846dfb5452801a556b6e01f9b0e8eb9d/packages/semi-webpack/src/semi-webpack-plugin.ts#L23 @@ -55,21 +57,25 @@ export default function SemiPlugin({ theme, options = {} }) { importers: [ { findFileUrl(url) { - if (url.startsWith('~')) { - const key = '/node_modules/'; + if (url.startsWith("~")) { + const key = "/node_modules/"; return new URL( url.substring(1), pathToFileURL( scssFilePath.substring( 0, - (url.startsWith('~@semi-bot') ? scssFilePath.indexOf(key) : scssFilePath.lastIndexOf(key)) + - key.length + (url.startsWith("~@semi-bot") + ? scssFilePath.indexOf(key) + : scssFilePath.lastIndexOf(key)) + key.length ) ) ); } - const filePath = Path.resolve(Path.dirname(scssFilePath), url); + const filePath = Path.resolve( + Path.dirname(scssFilePath), + url + ); if (FS.existsSync(filePath)) { return pathToFileURL(filePath); @@ -89,9 +95,9 @@ export default function SemiPlugin({ theme, options = {} }) { // copy from https://github.com/DouyinFE/semi-design/blob/main/packages/semi-webpack/src/semi-theme-loader.ts function loader(source, options) { - let fileStr = source.toString('utf8'); + let fileStr = source.toString("utf8"); - const theme = options.name || '@douyinfe/semi-theme-default'; + const theme = options.name || "@douyinfe/semi-theme-default"; // always inject const scssVarStr = `@import "~${theme}/scss/index.scss";\n`; // inject once @@ -102,10 +108,10 @@ function loader(source, options) { try { require.resolve(`${theme}/scss/animation.scss`); } catch (e) { - animationStr = ''; // fallback to empty string + animationStr = ""; // fallback to empty string } - const shouldInject = fileStr.includes('semi-base'); + const shouldInject = fileStr.includes("semi-base"); let componentVariables; @@ -114,7 +120,7 @@ function loader(source, options) { } catch (e) {} if (options.include || options.variables || componentVariables) { - let localImport = ''; + let localImport = ""; if (componentVariables) { localImport += `\n@import "~${theme}/scss/local.scss";`; } @@ -125,34 +131,37 @@ function loader(source, options) { localImport += `\n${options.variables}`; } try { - const regex = /(@import '.\/variables.scss';?|@import ".\/variables.scss";?)/g; + const regex = + /(@import '.\/variables.scss';?|@import ".\/variables.scss";?)/g; const fileSplit = fileStr.split(regex).filter((item) => Boolean(item)); if (fileSplit.length > 1) { fileSplit.splice(fileSplit.length - 1, 0, localImport); - fileStr = fileSplit.join(''); + fileStr = fileSplit.join(""); } } catch (error) {} } // inject prefix - const prefixCls = options.prefixCls || 'semi'; + const prefixCls = options.prefixCls || "semi"; const prefixClsStr = `$prefix: '${prefixCls}';\n`; if (shouldInject) { return `${animationStr}${cssVarStr}${scssVarStr}${prefixClsStr}${fileStr}`; - } else { - return `${scssVarStr}${prefixClsStr}${fileStr}`; } + return `${scssVarStr}${prefixClsStr}${fileStr}`; } // copy from https://github.com/DouyinFE/semi-design/blob/main/packages/semi-webpack/src/semi-webpack-plugin.ts#L136 function convertMapToString(map) { - return Object.keys(map).reduce(function (prev, curr) { - return prev + `${curr}: ${map[curr]};\n`; - }, ''); + return Object.keys(map).reduce( + (prev, curr) => `${prev}${curr}: ${map[curr]};\n`, + "" + ); } function normalizePath(id) { - return Path.posix.normalize(platform() === 'win32' ? id.replace(/\\/g, '/') : id); + return Path.posix.normalize( + platform() === "win32" ? id.replace(/\\/g, "/") : id + ); } diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 3949560..3b2b8ea 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -1,43 +1,43 @@ { - "empty": "Empty", - "or": "or", - "unknown": "Unknown", - "uploading": "Uploading", - "search": "Search", - "save": "Save", - "submit": "Submit", - "forever": "Forever", - "actions": "Actions", - "detail": "Detail", - "create": "Create", - "delete": "Delete", - "update": "Update", - "confirm": "Confirm", - "cancel": "Cancel", - "edit": "Edit", - "system": "System", - "instance": "Instance", - "documents": "Documents", - "settings": "Settings", - "tasks": "Tasks", - "indexes": "Indexes", - "keys": "Keys", - "back": "Back", - "copy": "Copy", - "copied": "Copied", - "name": "Name", - "description": "Description", - "created_at": "Created At", - "updated_at": "Updated At", - "expired_at": "Expired At", - "learn_more": "Learn More", - "type": "Type", - "status": "Status", - "dashboard": "Dashboard", - "version": "Version", - "none": "None", - "singleton_mode": "Singleton Mode", - "toast": { - "fail": "Failed, {{msg}}" - } + "empty": "Empty", + "or": "or", + "unknown": "Unknown", + "uploading": "Uploading", + "search": "Search", + "save": "Save", + "submit": "Submit", + "forever": "Forever", + "actions": "Actions", + "detail": "Detail", + "create": "Create", + "delete": "Delete", + "update": "Update", + "confirm": "Confirm", + "cancel": "Cancel", + "edit": "Edit", + "system": "System", + "instance": "Instance", + "documents": "Documents", + "settings": "Settings", + "tasks": "Tasks", + "indexes": "Indexes", + "keys": "Keys", + "back": "Back", + "copy": "Copy", + "copied": "Copied", + "name": "Name", + "description": "Description", + "created_at": "Created At", + "updated_at": "Updated At", + "expired_at": "Expired At", + "learn_more": "Learn More", + "type": "Type", + "status": "Status", + "dashboard": "Dashboard", + "version": "Version", + "none": "None", + "singleton_mode": "Singleton Mode", + "toast": { + "fail": "Failed, {{msg}}" + } } diff --git a/src/locales/en/dashboard.json b/src/locales/en/dashboard.json index be32ff1..e68badf 100644 --- a/src/locales/en/dashboard.json +++ b/src/locales/en/dashboard.json @@ -1,19 +1,19 @@ { - "slogan": "A Beautiful Meilisearch UI", - "instance": { - "updated_at": "Updated at", - "remove": { - "title": "Remove this instance", - "tip": "Are you sure you want to remove this instance" - } - }, - "settings": { - "export": "Export instances", - "import": "Import instances", - "danger_zone": "Danger zone", - "remove": { - "title": "Remove all instances", - "tip": "Are you sure you want to remove all instances?" - } - } -} \ No newline at end of file + "slogan": "A Beautiful Meilisearch UI", + "instance": { + "updated_at": "Updated at", + "remove": { + "title": "Remove this instance", + "tip": "Are you sure you want to remove this instance" + } + }, + "settings": { + "export": "Export instances", + "import": "Import instances", + "danger_zone": "Danger zone", + "remove": { + "title": "Remove all instances", + "tip": "Are you sure you want to remove all instances?" + } + } +} diff --git a/src/locales/en/document.json b/src/locales/en/document.json index 985ff98..d21d5ad 100644 --- a/src/locales/en/document.json +++ b/src/locales/en/document.json @@ -1,52 +1,52 @@ { - "upload_documents": "Upload documents", - "delete_document": "Delete document", - "delete": { - "tip": "Are you sure you want to delete this document from index {{indexId}} with primaryKey {{primaryKey}}?", - "require_primaryKey": "Document deletion require the valid primaryKey " - }, - "edit_document": "Edit document", - "search": { - "form": { - "limit": { - "label": "Limit", - "validation_error": "limit search value allow (<500) in this ui console for performance" - }, - "indexId": { - "placeholder": "input target index id" - }, - "q": { - "placeholder": "type some search query..." - }, - "filter": { - "label": "Filter" - }, - "offset": { - "label": "Offset" - }, - "sort": { - "label": "Sort", - "tip": "use half-width comma(',') to separate multi sort expression and order" - }, - "autoRefresh": { - "label": "Auto Refresh", - "tip": "After turning it on, the search results will be automatically refreshed (every 7 seconds, when filter conditions change, when the network environment changes). This option will increase QPS. Please pay attention to paid instances!" - }, - "showRankingScore": { - "label": "Show Ranking Score", - "tip": "This option is only support showRankingScore parameters instance (> v1.3.0) effectively, the _rankingScore field will appear in the returned search results when enabled, please refer to the official document for the meaning of this parameter" - } - }, - "results": { - "label": "Results", - "download": "Download results", - "type": { - "json": "JSON", - "table": "Table", - "grid": "Grid" - }, - "total_hits": "total {{estimatedTotalHits}} hits", - "processing_time": "in {{processingTimeMs}} ms" - } - } + "upload_documents": "Upload documents", + "delete_document": "Delete document", + "delete": { + "tip": "Are you sure you want to delete this document from index {{indexId}} with primaryKey {{primaryKey}}?", + "require_primaryKey": "Document deletion require the valid primaryKey " + }, + "edit_document": "Edit document", + "search": { + "form": { + "limit": { + "label": "Limit", + "validation_error": "limit search value allow (<500) in this ui console for performance" + }, + "indexId": { + "placeholder": "input target index id" + }, + "q": { + "placeholder": "type some search query..." + }, + "filter": { + "label": "Filter" + }, + "offset": { + "label": "Offset" + }, + "sort": { + "label": "Sort", + "tip": "use half-width comma(',') to separate multi sort expression and order" + }, + "autoRefresh": { + "label": "Auto Refresh", + "tip": "After turning it on, the search results will be automatically refreshed (every 7 seconds, when filter conditions change, when the network environment changes). This option will increase QPS. Please pay attention to paid instances!" + }, + "showRankingScore": { + "label": "Show Ranking Score", + "tip": "This option is only support showRankingScore parameters instance (> v1.3.0) effectively, the _rankingScore field will appear in the returned search results when enabled, please refer to the official document for the meaning of this parameter" + } + }, + "results": { + "label": "Results", + "download": "Download results", + "type": { + "json": "JSON", + "table": "Table", + "grid": "Grid" + }, + "total_hits": "total {{estimatedTotalHits}} hits", + "processing_time": "in {{processingTimeMs}} ms" + } + } } diff --git a/src/locales/en/footer.json b/src/locales/en/footer.json index bd0a5fb..a35eeec 100644 --- a/src/locales/en/footer.json +++ b/src/locales/en/footer.json @@ -1,5 +1,5 @@ { - "powered_by": "Powered by", - "meilisearchJSInUse": "Meilisearch JS SDK version in use", - "meilisearchVersionTip": "Please pay attention to the compatibility of this SDK version with the Meilisearch instance version you are using for a good experience" + "powered_by": "Powered by", + "meilisearchJSInUse": "Meilisearch JS SDK version in use", + "meilisearchVersionTip": "Please pay attention to the compatibility of this SDK version with the Meilisearch instance version you are using for a good experience" } diff --git a/src/locales/en/header.json b/src/locales/en/header.json index c77966d..4d49caa 100644 --- a/src/locales/en/header.json +++ b/src/locales/en/header.json @@ -1,7 +1,7 @@ { - "home": "Home", - "support": "Support", - "meilisearch_docs": "Meilisearch Docs", - "issues": "Issues", - "open_source": "Open Source" + "home": "Home", + "support": "Support", + "meilisearch_docs": "Meilisearch Docs", + "issues": "Issues", + "open_source": "Open Source" } diff --git a/src/locales/en/index.json b/src/locales/en/index.json index 4ded02f..047cb33 100644 --- a/src/locales/en/index.json +++ b/src/locales/en/index.json @@ -1,40 +1,40 @@ { - "fieldDistribution": { - "label": "Field Distribution", - "tip": "Only the first 10 fields are shown, sorted by distribution", - "subtitle": "Click me to go for official docs about “Field Distribution”" - }, - "count": "Count", - "indexing": "Indexing", - "indexing_tip": "This index is indexing documents, setting & search results may be incorrect now!", - "all_documents_delete": { - "dialog": { - "content": "You are deleting all documents of index {{uid}}.\nThis action is so important that you are required to confirm it.\nPlease click one of these buttons to proceed.", - "title": "Please confirm your action" - }, - "label": "Delete All Documents" - }, - "index_delete": { - "dialog": { - "content": "You are deleting index {{uid}}.\nThis action is so important that you are required to confirm it.\nPlease click one of these buttons to proceed.", - "title": "Please confirm your action" - }, - "label": "Delete This Index" - }, - "primaryKey": "Primary Key", - "not_found": "Index Not Found", - "setting": { - "index": { - "config": { - "label": "Index Configuration" - }, - "danger_zone": "Danger Zone" - }, - "filterableAttributes": "Filterable Attributes", - "searchableAttributes": "Searchable Attributes", - "sortableAttributes": "Sortable Attributes" - }, - "search": { - "tip": "Only the data presented on this page is searched here because Meilisearch does not provide a search interface for index lists!" - } + "fieldDistribution": { + "label": "Field Distribution", + "tip": "Only the first 10 fields are shown, sorted by distribution", + "subtitle": "Click me to go for official docs about “Field Distribution”" + }, + "count": "Count", + "indexing": "Indexing", + "indexing_tip": "This index is indexing documents, setting & search results may be incorrect now!", + "all_documents_delete": { + "dialog": { + "content": "You are deleting all documents of index {{uid}}.\nThis action is so important that you are required to confirm it.\nPlease click one of these buttons to proceed.", + "title": "Please confirm your action" + }, + "label": "Delete All Documents" + }, + "index_delete": { + "dialog": { + "content": "You are deleting index {{uid}}.\nThis action is so important that you are required to confirm it.\nPlease click one of these buttons to proceed.", + "title": "Please confirm your action" + }, + "label": "Delete This Index" + }, + "primaryKey": "Primary Key", + "not_found": "Index Not Found", + "setting": { + "index": { + "config": { + "label": "Index Configuration" + }, + "danger_zone": "Danger Zone" + }, + "filterableAttributes": "Filterable Attributes", + "searchableAttributes": "Searchable Attributes", + "sortableAttributes": "Sortable Attributes" + }, + "search": { + "tip": "Only the data presented on this page is searched here because Meilisearch does not provide a search interface for index lists!" + } } diff --git a/src/locales/en/instance.json b/src/locales/en/instance.json index c04ea5f..1a3e67f 100644 --- a/src/locales/en/instance.json +++ b/src/locales/en/instance.json @@ -1,55 +1,55 @@ { - "host": "Host", - "status": { - "label": "Status", - "available": "Available" - }, - "version": { - "label": "Meilisearch Version" - }, - "db_size": "Instance DB Size", - "meili_version": "Meili Version", - "connection_failed": "Connection fail, go check your config! 🤥", - "create_index": { - "form": { - "primaryKey": { - "label": "Primary Key", - "placeholder": "Primary key of the requested index", - "tip": "[DOCS] It uniquely identifies each document in an index, ensuring that it is impossible to have two exactly identical documents present in the same index." - }, - "uid": { - "placeholder": "uid of the requested index", - "tip": "[DOCS] Once defined, the uid cannot be changed, and you cannot create another index with the same uid.", - "validation_error": "The uid is the unique identifier of an index. It is set when creating the index and must be an integer or string containing only alphanumeric characters a-z A-Z 0-9, hyphens - and underscores _." - } - }, - "label": "Create Index" - }, - "dump": { - "dialog": { - "tip": "Are you sure you want to start a new dump for instance {{name}}?", - "title": "Create a new dump" - } - }, - "no_master_key_error": "Meilisearch is running without a master key.\nTo access this API endpoint, you must have set a master key at launch.", - "singleton_cfg_not_found": "Singleton mode detected! Connection fail!\nRunning with invalid instance config, you must have set correct instance env config before launch.", - "not_found": "Instance not found", - "form": { - "title": { - "create": "Add a new instance", - "edit": "Edit instance" - }, - "name": { - "label": "Name", - "placeholder": "name your instance, should be different from others" - }, - "host": { - "label": "Host", - "tip": "Remember enable CORS in your instance server for this ui domain first" - }, - "api_key": { - "label": "API Key", - "tip": "Don't care! Your instance config will only be store in your local browser" - } - } + "host": "Host", + "status": { + "label": "Status", + "available": "Available" + }, + "version": { + "label": "Meilisearch Version" + }, + "db_size": "Instance DB Size", + "meili_version": "Meili Version", + "connection_failed": "Connection fail, go check your config! 🤥", + "create_index": { + "form": { + "primaryKey": { + "label": "Primary Key", + "placeholder": "Primary key of the requested index", + "tip": "[DOCS] It uniquely identifies each document in an index, ensuring that it is impossible to have two exactly identical documents present in the same index." + }, + "uid": { + "placeholder": "uid of the requested index", + "tip": "[DOCS] Once defined, the uid cannot be changed, and you cannot create another index with the same uid.", + "validation_error": "The uid is the unique identifier of an index. It is set when creating the index and must be an integer or string containing only alphanumeric characters a-z A-Z 0-9, hyphens - and underscores _." + } + }, + "label": "Create Index" + }, + "dump": { + "dialog": { + "tip": "Are you sure you want to start a new dump for instance {{name}}?", + "title": "Create a new dump" + } + }, + "no_master_key_error": "Meilisearch is running without a master key.\nTo access this API endpoint, you must have set a master key at launch.", + "singleton_cfg_not_found": "Singleton mode detected! Connection fail!\nRunning with invalid instance config, you must have set correct instance env config before launch.", + "not_found": "Instance not found", + "form": { + "title": { + "create": "Add a new instance", + "edit": "Edit instance" + }, + "name": { + "label": "Name", + "placeholder": "name your instance, should be different from others" + }, + "host": { + "label": "Host", + "tip": "Remember enable CORS in your instance server for this ui domain first" + }, + "api_key": { + "label": "API Key", + "tip": "Don't care! Your instance config will only be store in your local browser" + } + } } diff --git a/src/locales/en/key.json b/src/locales/en/key.json index 9566789..9a30f02 100644 --- a/src/locales/en/key.json +++ b/src/locales/en/key.json @@ -1,47 +1,47 @@ { - "props": { - "key": "Key", - "indexes": "Indexes", - "actions": "Actions" - }, - "form": { - "title": { - "create": "Create Key", - "edit": "Update Key" - }, - "uid": { - "placeholder": "leave empty to generate a random uid by meilisearch" - }, - "name": { - "placeholder": "name your key" - }, - "description": { - "placeholder": "add optional description for your key" - }, - "indexes": { - "placeholder": "select permitted indexes", - "tip": "Leave this option empty means all indexes permitted" - }, - "actions": { - "placeholder": "select permitted actions", - "tip": "Leave this option empty means all actions permitted" - }, - "expiresAt": { - "invalid": "Invalid Expire Time", - "placeholder": "!!UTC!! time and format must be YYYY-MM-DD HH:mm:ss", - "tip": "Leave this option empty means this key never expires" - } - }, - "delete": { - "title": "Delete this key", - "tip": "Are you sure you want to delete this key?" - }, - "create": { - "title": "Add new key", - "fail": "Creation fail, go check tasks! 🤥", - "submit": "Create this key" - }, - "search": { - "placeholder": "Search keys" - } + "props": { + "key": "Key", + "indexes": "Indexes", + "actions": "Actions" + }, + "form": { + "title": { + "create": "Create Key", + "edit": "Update Key" + }, + "uid": { + "placeholder": "leave empty to generate a random uid by meilisearch" + }, + "name": { + "placeholder": "name your key" + }, + "description": { + "placeholder": "add optional description for your key" + }, + "indexes": { + "placeholder": "select permitted indexes", + "tip": "Leave this option empty means all indexes permitted" + }, + "actions": { + "placeholder": "select permitted actions", + "tip": "Leave this option empty means all actions permitted" + }, + "expiresAt": { + "invalid": "Invalid Expire Time", + "placeholder": "!!UTC!! time and format must be YYYY-MM-DD HH:mm:ss", + "tip": "Leave this option empty means this key never expires" + } + }, + "delete": { + "title": "Delete this key", + "tip": "Are you sure you want to delete this key?" + }, + "create": { + "title": "Add new key", + "fail": "Creation fail, go check tasks! 🤥", + "submit": "Create this key" + }, + "search": { + "placeholder": "Search keys" + } } diff --git a/src/locales/en/sys.json b/src/locales/en/sys.json index 88910ad..b07df3d 100644 --- a/src/locales/en/sys.json +++ b/src/locales/en/sys.json @@ -1,4 +1,4 @@ { - "warning": "Warning", - "reload": "Reload" + "warning": "Warning", + "reload": "Reload" } diff --git a/src/locales/en/task.json b/src/locales/en/task.json index 7478d72..f937561 100644 --- a/src/locales/en/task.json +++ b/src/locales/en/task.json @@ -1,46 +1,46 @@ { - "duration": "Duration", - "enqueued_at": "Enqueued At", - "started_at": "Started At", - "finished_at": "Finished At", - "search": { - "placeholder": "Search tasks" - }, - "filter": { - "index": { - "placeholder": "Filter Index UID" - }, - "enqueuedAt": { - "label": "Enqueued At" - }, - "type": { - "placeholder": "Filter Task Type" - }, - "status": { - "placeholder": "Filter Task Status" - } - }, - "type": { - "documentAdditionOrUpdate": "Document Addition Or Update", - "documentDeletion": "Document Deletion", - "dumpCreation": "Dump Creation", - "indexCreation": "Index Creation", - "indexDeletion": "Index Deletion", - "indexSwap": "Index Swap", - "indexUpdate": "Index Update", - "settingsUpdate": "Settings Update", - "snapshotCreation": "Snapshot Creation", - "taskCancelation": "Task Cancelation", - "taskDeletion": "Task Deletion" - }, - "status": { - "succeeded": "✅ Succeeded", - "processing": "⚡ Processing", - "failed": "❌ Failed", - "enqueued": "🔀 Enqueued", - "canceled": "🚫 Canceled" - }, - "detail": { - "title": "Task Detail" - } + "duration": "Duration", + "enqueued_at": "Enqueued At", + "started_at": "Started At", + "finished_at": "Finished At", + "search": { + "placeholder": "Search tasks" + }, + "filter": { + "index": { + "placeholder": "Filter Index UID" + }, + "enqueuedAt": { + "label": "Enqueued At" + }, + "type": { + "placeholder": "Filter Task Type" + }, + "status": { + "placeholder": "Filter Task Status" + } + }, + "type": { + "documentAdditionOrUpdate": "Document Addition Or Update", + "documentDeletion": "Document Deletion", + "dumpCreation": "Dump Creation", + "indexCreation": "Index Creation", + "indexDeletion": "Index Deletion", + "indexSwap": "Index Swap", + "indexUpdate": "Index Update", + "settingsUpdate": "Settings Update", + "snapshotCreation": "Snapshot Creation", + "taskCancelation": "Task Cancelation", + "taskDeletion": "Task Deletion" + }, + "status": { + "succeeded": "✅ Succeeded", + "processing": "⚡ Processing", + "failed": "❌ Failed", + "enqueued": "🔀 Enqueued", + "canceled": "🚫 Canceled" + }, + "detail": { + "title": "Task Detail" + } } diff --git a/src/locales/en/upload.json b/src/locales/en/upload.json index 84dc6f4..c386184 100644 --- a/src/locales/en/upload.json +++ b/src/locales/en/upload.json @@ -1,13 +1,13 @@ { - "clipboard_json_pasted": "Clipboard JSON pasted", - "title": "Upload documents into Index", - "input_by_editor": "Input by editor", - "manually_type_in": "Manually type in", - "click_to_paste_clipboard_content_if_it_is_valid_json": "Click to paste clipboard content (if it is valid JSON)", - "import_json_file": "Import json file", - "for_large_documents": "For large documents", - "release_to_upload_documents": "Release to Upload Documents", - "drag_and_drop_a_file_here": "Drag & Drop A {{- type}} File Here", - "browse_file": "Browse File", - "documents_json_array_requirement": "Added documents should be JSON Array whose length > 0" + "clipboard_json_pasted": "Clipboard JSON pasted", + "title": "Upload documents into Index", + "input_by_editor": "Input by editor", + "manually_type_in": "Manually type in", + "click_to_paste_clipboard_content_if_it_is_valid_json": "Click to paste clipboard content (if it is valid JSON)", + "import_json_file": "Import json file", + "for_large_documents": "For large documents", + "release_to_upload_documents": "Release to Upload Documents", + "drag_and_drop_a_file_here": "Drag & Drop A {{- type}} File Here", + "browse_file": "Browse File", + "documents_json_array_requirement": "Added documents should be JSON Array whose length > 0" } diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json index 7e47303..27d5191 100644 --- a/src/locales/zh/common.json +++ b/src/locales/zh/common.json @@ -1,43 +1,43 @@ { - "empty": "空空如也", - "or": "或", - "unknown": "未知", - "uploading": "上传中", - "search": "搜索", - "save": "保存", - "submit": "提交", - "forever": "永久", - "actions": "操作", - "detail": "详情", - "create": "创建", - "delete": "删除", - "update": "更新", - "confirm": "确认", - "cancel": "取消", - "edit": "编辑", - "system": "系统", - "instance": "实例", - "documents": "文档", - "settings": "设置", - "tasks": "任务", - "indexes": "索引", - "keys": "密钥", - "back": "回退", - "copy": "复制", - "copied": "已复制", - "name": "名称", - "description": "描述", - "created_at": "创建于", - "updated_at": "更新于", - "expired_at": "过期于", - "learn_more": "更多", - "type": "类型", - "status": "状态", - "dashboard": "仪表盘", - "version": "版本", - "none": "暂无", - "singleton_mode": "单实例模式", - "toast": { - "fail": "失败, {{msg}}" - } + "empty": "空空如也", + "or": "或", + "unknown": "未知", + "uploading": "上传中", + "search": "搜索", + "save": "保存", + "submit": "提交", + "forever": "永久", + "actions": "操作", + "detail": "详情", + "create": "创建", + "delete": "删除", + "update": "更新", + "confirm": "确认", + "cancel": "取消", + "edit": "编辑", + "system": "系统", + "instance": "实例", + "documents": "文档", + "settings": "设置", + "tasks": "任务", + "indexes": "索引", + "keys": "密钥", + "back": "回退", + "copy": "复制", + "copied": "已复制", + "name": "名称", + "description": "描述", + "created_at": "创建于", + "updated_at": "更新于", + "expired_at": "过期于", + "learn_more": "更多", + "type": "类型", + "status": "状态", + "dashboard": "仪表盘", + "version": "版本", + "none": "暂无", + "singleton_mode": "单实例模式", + "toast": { + "fail": "失败, {{msg}}" + } } diff --git a/src/locales/zh/dashboard.json b/src/locales/zh/dashboard.json index 430bb13..c2bcad0 100644 --- a/src/locales/zh/dashboard.json +++ b/src/locales/zh/dashboard.json @@ -1,37 +1,37 @@ { - "slogan": "美丽的 MeiliSearch 管理面板", - "instance": { - "updated_at": "更新于", - "form": { - "title": { - "create": "新增实例", - "edit": "编辑实例" - }, - "name": { - "label": "名称", - "placeholder": "给你的实例一个名字,应该要不同于其它实例" - }, - "host": { - "label": "主机", - "tip": "请记得首先在实例所在的服务器上为管理面板地址设置跨域" - }, - "api_key": { - "label": "API 密钥", - "tip": "不用担心!你的数据只会储存在浏览器本地" - } - }, - "remove": { - "title": "移除这个实例", - "tip": "你真的希望移除这个实例" - } - }, - "settings": { - "export": "导出实例", - "import": "导入实例", - "danger_zone": "危险区", - "remove": { - "title": "移除所有实例", - "tip": "您确实要删除所有实例吗?" - } - } + "slogan": "美丽的 MeiliSearch 管理面板", + "instance": { + "updated_at": "更新于", + "form": { + "title": { + "create": "新增实例", + "edit": "编辑实例" + }, + "name": { + "label": "名称", + "placeholder": "给你的实例一个名字,应该要不同于其它实例" + }, + "host": { + "label": "主机", + "tip": "请记得首先在实例所在的服务器上为管理面板地址设置跨域" + }, + "api_key": { + "label": "API 密钥", + "tip": "不用担心!你的数据只会储存在浏览器本地" + } + }, + "remove": { + "title": "移除这个实例", + "tip": "你真的希望移除这个实例" + } + }, + "settings": { + "export": "导出实例", + "import": "导入实例", + "danger_zone": "危险区", + "remove": { + "title": "移除所有实例", + "tip": "您确实要删除所有实例吗?" + } + } } diff --git a/src/locales/zh/document.json b/src/locales/zh/document.json index 3ef0932..556b524 100644 --- a/src/locales/zh/document.json +++ b/src/locales/zh/document.json @@ -1,52 +1,52 @@ { - "upload_documents": "上传文档", - "delete_document": "删除文档", - "delete": { - "tip": "是否确实要删除索引 {{indexId}} 中这个主键为 {{primaryKey}} 的文档?", - "require_primaryKey": "文档删除需要索引 {{indexId}} 中存在有效的primaryKey" - }, - "edit_document": "编辑文档", - "search": { - "form": { - "limit": { - "label": "限制结果数", - "validation_error": "为了此面板的性能,限制此搜索结果数(<500)" - }, - "indexId": { - "placeholder": "输入目标索引id" - }, - "q": { - "placeholder": "输入搜索词..." - }, - "filter": { - "label": "过滤器" - }, - "offset": { - "label": "偏移" - }, - "sort": { - "label": "排序", - "tip": "使用半角逗号(',')分隔多个排序表达式" - }, - "autoRefresh": { - "label": "自动刷新", - "tip": "开启后将(每7s、筛选条件变化时、网络环境变化时)自动刷新搜索结果,此选项会提高QPS,付费实例请留意!" - }, - "showRankingScore": { - "label": "显示排名分数", - "tip": "此选项仅在支持 showRankingScore 参数的实例(> v1.3.0)上有效,开启后将在返回的搜索结果中出现 _rankingScore 字段,请参考官方文档了解此参数的含义" - } - }, - "results": { - "label": "搜索结果", - "download": "下载搜索结果", - "type": { - "json": "JSON", - "table": "表格", - "grid": "网格" - }, - "total_hits": "总计返回 {{estimatedTotalHits}} 条结果", - "processing_time": "用时 {{processingTimeMs}} ms" - } - } + "upload_documents": "上传文档", + "delete_document": "删除文档", + "delete": { + "tip": "是否确实要删除索引 {{indexId}} 中这个主键为 {{primaryKey}} 的文档?", + "require_primaryKey": "文档删除需要索引 {{indexId}} 中存在有效的primaryKey" + }, + "edit_document": "编辑文档", + "search": { + "form": { + "limit": { + "label": "限制结果数", + "validation_error": "为了此面板的性能,限制此搜索结果数(<500)" + }, + "indexId": { + "placeholder": "输入目标索引id" + }, + "q": { + "placeholder": "输入搜索词..." + }, + "filter": { + "label": "过滤器" + }, + "offset": { + "label": "偏移" + }, + "sort": { + "label": "排序", + "tip": "使用半角逗号(',')分隔多个排序表达式" + }, + "autoRefresh": { + "label": "自动刷新", + "tip": "开启后将(每7s、筛选条件变化时、网络环境变化时)自动刷新搜索结果,此选项会提高QPS,付费实例请留意!" + }, + "showRankingScore": { + "label": "显示排名分数", + "tip": "此选项仅在支持 showRankingScore 参数的实例(> v1.3.0)上有效,开启后将在返回的搜索结果中出现 _rankingScore 字段,请参考官方文档了解此参数的含义" + } + }, + "results": { + "label": "搜索结果", + "download": "下载搜索结果", + "type": { + "json": "JSON", + "table": "表格", + "grid": "网格" + }, + "total_hits": "总计返回 {{estimatedTotalHits}} 条结果", + "processing_time": "用时 {{processingTimeMs}} ms" + } + } } diff --git a/src/locales/zh/footer.json b/src/locales/zh/footer.json index 411aa3b..dbe5c8e 100644 --- a/src/locales/zh/footer.json +++ b/src/locales/zh/footer.json @@ -1,5 +1,5 @@ { - "powered_by": "技术支持", - "meilisearchJSInUse": "正在使用的 Meilisearch JS SDK 版本", - "meilisearchVersionTip": "请注意关注该SDK版本与你正在使用的 Meilisearch 实例版本的适配性以获得良好的使用体验" + "powered_by": "技术支持", + "meilisearchJSInUse": "正在使用的 Meilisearch JS SDK 版本", + "meilisearchVersionTip": "请注意关注该SDK版本与你正在使用的 Meilisearch 实例版本的适配性以获得良好的使用体验" } diff --git a/src/locales/zh/header.json b/src/locales/zh/header.json index 1e7e8cb..aa716d8 100644 --- a/src/locales/zh/header.json +++ b/src/locales/zh/header.json @@ -1,7 +1,7 @@ { - "home": "主页", - "support": "支持", - "meilisearch_docs": "Meilisearch 文档", - "issues": "报告问题", - "open_source": "开源" + "home": "主页", + "support": "支持", + "meilisearch_docs": "Meilisearch 文档", + "issues": "报告问题", + "open_source": "开源" } diff --git a/src/locales/zh/index.json b/src/locales/zh/index.json index 2b54bf1..929eefd 100644 --- a/src/locales/zh/index.json +++ b/src/locales/zh/index.json @@ -1,40 +1,40 @@ { - "fieldDistribution": { - "label": "字段分布", - "tip": "仅展示前十个字段,按分布排序", - "subtitle": "点我转到有关 Field Distribution 的官方文档" - }, - "count": "文档计数", - "indexing": "索引中", - "indexing_tip": "此索引正在为文档编制索引,设置与搜索结果现在可能显示不正确!", - "index_delete": { - "dialog": { - "content": "您正在删除索引{{uid}}。\n此操作非常重要,需要您确认。\n请点击以下按钮之一继续。", - "title": "请确认您的操作" - }, - "label": "删除此索引" - }, - "all_documents_delete": { - "dialog": { - "content": "您正在删除索引{{uid}}的所有文档。\n此操作非常重要,需要您确认。\n请点击以下按钮之一继续。", - "title": "请确认您的操作" - }, - "label": "删除所有文档" - }, - "primaryKey": "主键", - "not_found": "未找到索引", - "setting": { - "index": { - "config": { - "label": "索引配置" - }, - "danger_zone": "危险区" - }, - "filterableAttributes": "可筛选属性", - "searchableAttributes": "可搜索属性", - "sortableAttributes": "可排序属性" - }, - "search": { - "tip": "此处仅搜索本页所展示的数据,这是因为 Meilisearch 并未提供对索引列表的搜索接口!" - } + "fieldDistribution": { + "label": "字段分布", + "tip": "仅展示前十个字段,按分布排序", + "subtitle": "点我转到有关 Field Distribution 的官方文档" + }, + "count": "文档计数", + "indexing": "索引中", + "indexing_tip": "此索引正在为文档编制索引,设置与搜索结果现在可能显示不正确!", + "index_delete": { + "dialog": { + "content": "您正在删除索引{{uid}}。\n此操作非常重要,需要您确认。\n请点击以下按钮之一继续。", + "title": "请确认您的操作" + }, + "label": "删除此索引" + }, + "all_documents_delete": { + "dialog": { + "content": "您正在删除索引{{uid}}的所有文档。\n此操作非常重要,需要您确认。\n请点击以下按钮之一继续。", + "title": "请确认您的操作" + }, + "label": "删除所有文档" + }, + "primaryKey": "主键", + "not_found": "未找到索引", + "setting": { + "index": { + "config": { + "label": "索引配置" + }, + "danger_zone": "危险区" + }, + "filterableAttributes": "可筛选属性", + "searchableAttributes": "可搜索属性", + "sortableAttributes": "可排序属性" + }, + "search": { + "tip": "此处仅搜索本页所展示的数据,这是因为 Meilisearch 并未提供对索引列表的搜索接口!" + } } diff --git a/src/locales/zh/instance.json b/src/locales/zh/instance.json index 7ba7418..344a29a 100644 --- a/src/locales/zh/instance.json +++ b/src/locales/zh/instance.json @@ -1,55 +1,55 @@ { - "host": "主机", - "status": { - "label": "状态", - "available": "可用" - }, - "version": { - "label": "Meilisearch 版本" - }, - "db_size": "实例数据库大小", - "meili_version": "Meili 版本", - "connection_failed": "连接失败, 去检查你的配置! 🤥", - "create_index": { - "form": { - "primaryKey": { - "label": "主键", - "placeholder": "请求创建的索引的主键", - "tip": "[文档]它唯一地标识索引中的每个文档,确保不可能在同一索引中存在两个完全相同的文档。" - }, - "uid": { - "placeholder": "请求创建的索引的uid", - "tip": "[文档]定义后,就不能更改uid,也不能使用相同的uid创建另一个索引。", - "validation_error": "uid是索引的唯一标识符。它是在创建索引时设置的,必须是一个整数或仅包含字母数字字符、连字符-和下划线_的字符串。" - } - }, - "label": "创建索引" - }, - "dump": { - "dialog": { - "tip": "你确定给实例 {{name}} 开启一次新的 dump ?", - "title": "创建一份 Dump" - } - }, - "no_master_key_error": "Meilisearch 正在没有设置主密钥的情况下运行。\n若要访问此API端点,您必须在启动时设置主密钥。", - "singleton_cfg_not_found": "检测到单例模式连接失败!\n正在没有有效实例配置的情况下运行,您必须在启动之前设置正确的实例环境配置", - "not_found": "未找到实例", - "form": { - "title": { - "create": "新增实例", - "edit": "编辑实例" - }, - "name": { - "label": "名称", - "placeholder": "给你的实例一个名字,应该要不同于其它实例" - }, - "host": { - "label": "主机", - "tip": "请记得首先在实例所在的服务器上为管理面板地址设置跨域" - }, - "api_key": { - "label": "API 密钥", - "tip": "不用担心!你的数据只会储存在浏览器本地" - } - } + "host": "主机", + "status": { + "label": "状态", + "available": "可用" + }, + "version": { + "label": "Meilisearch 版本" + }, + "db_size": "实例数据库大小", + "meili_version": "Meili 版本", + "connection_failed": "连接失败, 去检查你的配置! 🤥", + "create_index": { + "form": { + "primaryKey": { + "label": "主键", + "placeholder": "请求创建的索引的主键", + "tip": "[文档]它唯一地标识索引中的每个文档,确保不可能在同一索引中存在两个完全相同的文档。" + }, + "uid": { + "placeholder": "请求创建的索引的uid", + "tip": "[文档]定义后,就不能更改uid,也不能使用相同的uid创建另一个索引。", + "validation_error": "uid是索引的唯一标识符。它是在创建索引时设置的,必须是一个整数或仅包含字母数字字符、连字符-和下划线_的字符串。" + } + }, + "label": "创建索引" + }, + "dump": { + "dialog": { + "tip": "你确定给实例 {{name}} 开启一次新的 dump ?", + "title": "创建一份 Dump" + } + }, + "no_master_key_error": "Meilisearch 正在没有设置主密钥的情况下运行。\n若要访问此API端点,您必须在启动时设置主密钥。", + "singleton_cfg_not_found": "检测到单例模式连接失败!\n正在没有有效实例配置的情况下运行,您必须在启动之前设置正确的实例环境配置", + "not_found": "未找到实例", + "form": { + "title": { + "create": "新增实例", + "edit": "编辑实例" + }, + "name": { + "label": "名称", + "placeholder": "给你的实例一个名字,应该要不同于其它实例" + }, + "host": { + "label": "主机", + "tip": "请记得首先在实例所在的服务器上为管理面板地址设置跨域" + }, + "api_key": { + "label": "API 密钥", + "tip": "不用担心!你的数据只会储存在浏览器本地" + } + } } diff --git a/src/locales/zh/key.json b/src/locales/zh/key.json index cf7410d..25b1bb7 100644 --- a/src/locales/zh/key.json +++ b/src/locales/zh/key.json @@ -1,47 +1,47 @@ { - "props": { - "key": "密钥", - "indexes": "索引", - "actions": "行为" - }, - "form": { - "title": { - "create": "新增密钥", - "edit": "更新密钥" - }, - "uid": { - "placeholder": "留空以通过 Meilisearch 生成随机 uid" - }, - "name": { - "placeholder": "命名你的密钥" - }, - "description": { - "placeholder": "添加可选的密钥描述" - }, - "indexes": { - "placeholder": "选择允许的索引", - "tip": "保留此选项为空表示允许所有索引" - }, - "actions": { - "placeholder": "选择允许的行为", - "tip": "保留此选项为空表示允许所有行为" - }, - "expiresAt": { - "invalid": "无效的过期时间", - "placeholder": "!!UTC!!时间和格式必须为 YYYY-MM-DD HH:mm:ss", - "tip": "保留此选项为空意味着此密钥永远不会过期" - } - }, - "delete": { - "title": "删除此密钥", - "tip": "确实要删除此项吗?" - }, - "create": { - "title": "添加新密钥", - "fail": "创建失败,请检查任务! 🤥", - "submit": "创建此密钥" - }, - "search": { - "placeholder": "搜索密钥信息" - } + "props": { + "key": "密钥", + "indexes": "索引", + "actions": "行为" + }, + "form": { + "title": { + "create": "新增密钥", + "edit": "更新密钥" + }, + "uid": { + "placeholder": "留空以通过 Meilisearch 生成随机 uid" + }, + "name": { + "placeholder": "命名你的密钥" + }, + "description": { + "placeholder": "添加可选的密钥描述" + }, + "indexes": { + "placeholder": "选择允许的索引", + "tip": "保留此选项为空表示允许所有索引" + }, + "actions": { + "placeholder": "选择允许的行为", + "tip": "保留此选项为空表示允许所有行为" + }, + "expiresAt": { + "invalid": "无效的过期时间", + "placeholder": "!!UTC!!时间和格式必须为 YYYY-MM-DD HH:mm:ss", + "tip": "保留此选项为空意味着此密钥永远不会过期" + } + }, + "delete": { + "title": "删除此密钥", + "tip": "确实要删除此项吗?" + }, + "create": { + "title": "添加新密钥", + "fail": "创建失败,请检查任务! 🤥", + "submit": "创建此密钥" + }, + "search": { + "placeholder": "搜索密钥信息" + } } diff --git a/src/locales/zh/sys.json b/src/locales/zh/sys.json index fbd0e6a..7b32d72 100644 --- a/src/locales/zh/sys.json +++ b/src/locales/zh/sys.json @@ -1,4 +1,4 @@ { - "warning": "警告", - "reload": "重新加载" + "warning": "警告", + "reload": "重新加载" } diff --git a/src/locales/zh/task.json b/src/locales/zh/task.json index 7f2af10..0610e6d 100644 --- a/src/locales/zh/task.json +++ b/src/locales/zh/task.json @@ -1,46 +1,46 @@ { - "duration": "用时", - "enqueued_at": "入列于", - "started_at": "开始于", - "finished_at": "结束于", - "search": { - "placeholder": "搜索任务" - }, - "filter": { - "index": { - "placeholder": "过滤索引UID" - }, - "enqueuedAt": { - "label": "入列时间" - }, - "type": { - "placeholder": "过滤任务类型" - }, - "status": { - "placeholder": "过滤任务状态" - } - }, - "type": { - "documentAdditionOrUpdate": "文档新增或更新", - "documentDeletion": "文档移除", - "dumpCreation": "转储创建", - "indexCreation": "索引创建", - "indexDeletion": "索引移除", - "indexSwap": "索引互换", - "indexUpdate": "索引更新", - "settingsUpdate": "设置更新", - "snapshotCreation": "快照创建", - "taskCancelation": "任务取消", - "taskDeletion": "任务移除" - }, - "status": { - "succeeded": "✅ 已成功", - "processing": "⚡ 处理中", - "failed": "❌ 失败了", - "enqueued": "🔀 已入列", - "canceled": "🚫 已取消" - }, - "detail": { - "title": "任务详情" - } + "duration": "用时", + "enqueued_at": "入列于", + "started_at": "开始于", + "finished_at": "结束于", + "search": { + "placeholder": "搜索任务" + }, + "filter": { + "index": { + "placeholder": "过滤索引UID" + }, + "enqueuedAt": { + "label": "入列时间" + }, + "type": { + "placeholder": "过滤任务类型" + }, + "status": { + "placeholder": "过滤任务状态" + } + }, + "type": { + "documentAdditionOrUpdate": "文档新增或更新", + "documentDeletion": "文档移除", + "dumpCreation": "转储创建", + "indexCreation": "索引创建", + "indexDeletion": "索引移除", + "indexSwap": "索引互换", + "indexUpdate": "索引更新", + "settingsUpdate": "设置更新", + "snapshotCreation": "快照创建", + "taskCancelation": "任务取消", + "taskDeletion": "任务移除" + }, + "status": { + "succeeded": "✅ 已成功", + "processing": "⚡ 处理中", + "failed": "❌ 失败了", + "enqueued": "🔀 已入列", + "canceled": "🚫 已取消" + }, + "detail": { + "title": "任务详情" + } } diff --git a/src/locales/zh/upload.json b/src/locales/zh/upload.json index f6d4a72..17b7306 100644 --- a/src/locales/zh/upload.json +++ b/src/locales/zh/upload.json @@ -1,13 +1,13 @@ { - "clipboard_json_pasted": "剪切板 JSON 已粘贴", - "title": "上传文档至索引", - "input_by_editor": "通过编辑器输入", - "manually_type_in": "手动输入", - "click_to_paste_clipboard_content_if_it_is_valid_json": "单击以粘贴剪贴板内容(如果是有效的JSON)", - "import_json_file": "导入 JSON 文件", - "for_large_documents": "适用大型文档", - "release_to_upload_documents": "释放以上载文档", - "drag_and_drop_a_file_here": "将 {{- type}} 文件拖放到此处", - "browse_file": "浏览文件", - "documents_json_array_requirement": "添加的文档应该是长度 > 0的JSON数组" + "clipboard_json_pasted": "剪切板 JSON 已粘贴", + "title": "上传文档至索引", + "input_by_editor": "通过编辑器输入", + "manually_type_in": "手动输入", + "click_to_paste_clipboard_content_if_it_is_valid_json": "单击以粘贴剪贴板内容(如果是有效的JSON)", + "import_json_file": "导入 JSON 文件", + "for_large_documents": "适用大型文档", + "release_to_upload_documents": "释放以上载文档", + "drag_and_drop_a_file_here": "将 {{- type}} 文件拖放到此处", + "browse_file": "浏览文件", + "documents_json_array_requirement": "添加的文档应该是长度 > 0的JSON数组" } diff --git a/src/main.tsx b/src/main.tsx index 38484c3..439f417 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,75 +1,75 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import 'virtual:uno.css'; -import './style/global.css'; -import '@arco-themes/react-meilisearch/css/arco.css'; -import './utils/i18n'; -import { AppProvider } from '@/providers'; -import { RouterProvider, createRouter } from '@tanstack/react-router'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import "virtual:uno.css"; +import "./style/global.css"; +import "@arco-themes/react-meilisearch/css/arco.css"; +import "./utils/i18n"; +import { AppProvider } from "@/providers"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { RouterProvider, createRouter } from "@tanstack/react-router"; +import { NotFound } from "./components/404"; +import { Logo } from "./components/Logo"; +import { Loader } from "./components/loader"; // Import the generated route tree -import { routeTree } from './routeTree.gen'; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { NotFound } from './components/404'; -import { Loader } from './components/loader'; -import { Logo } from './components/Logo'; +import { routeTree } from "./routeTree.gen"; const queryClient = new QueryClient({ - defaultOptions: { - queries: { - throwOnError: true, - refetchIntervalInBackground: false, - refetchOnReconnect: 'always', - refetchOnMount: 'always', - refetchOnWindowFocus: 'always', - refetchInterval: 30000, - }, - }, + defaultOptions: { + queries: { + throwOnError: true, + refetchIntervalInBackground: false, + refetchOnReconnect: "always", + refetchOnMount: "always", + refetchOnWindowFocus: "always", + refetchInterval: 30000, + }, + }, }); // Create a new router instance const router = createRouter({ - routeTree, - // why not use import.meta.env.BASE_PATH? ref: https://cn.vite.dev/guide/env-and-mode.html#env-variables - basepath: import.meta.env.BASE_URL || '/', - context: { - queryClient, - }, - defaultPreload: 'intent', - defaultPendingComponent: () => ( -
-
- -
- -
-
- ), - // Since we're using React Query, we don't want loader calls to ever be stale - // This will ensure that the loader is always called when the route is preloaded or visited - defaultPreloadStaleTime: 0, - defaultNotFoundComponent: NotFound, + routeTree, + // why not use import.meta.env.BASE_PATH? ref: https://cn.vite.dev/guide/env-and-mode.html#env-variables + basepath: import.meta.env.BASE_URL || "/", + context: { + queryClient, + }, + defaultPreload: "intent", + defaultPendingComponent: () => ( +
+
+ +
+ +
+
+ ), + // Since we're using React Query, we don't want loader calls to ever be stale + // This will ensure that the loader is always called when the route is preloaded or visited + defaultPreloadStaleTime: 0, + defaultNotFoundComponent: NotFound, }); // Register the router instance for type safety -declare module '@tanstack/react-router' { - interface Register { - router: typeof router; - } +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } } // Render the app -const rootElement = document.getElementById('root')!; +const rootElement = document.getElementById("root")!; if (!rootElement.innerHTML) { - const root = ReactDOM.createRoot(rootElement); - root.render( - - - - - - - - - ); + const root = ReactDOM.createRoot(rootElement); + root.render( + + + + + + + + , + ); } diff --git a/src/providers/error-boundary.tsx b/src/providers/error-boundary.tsx index d09b4f5..3faa6fe 100644 --- a/src/providers/error-boundary.tsx +++ b/src/providers/error-boundary.tsx @@ -1,12 +1,16 @@ -import type { FC, ReactNode } from 'react'; +import type { FC, ReactNode } from "react"; -import { ReactErrorBoundary } from '@/components/ErrorBoundary'; -import { AppFallback } from '@/components/ErrorBoundary/Fallback'; +import { ReactErrorBoundary } from "@/components/ErrorBoundary"; +import { AppFallback } from "@/components/ErrorBoundary/Fallback"; type Props = { - children: ReactNode; + children: ReactNode; }; export const ErrorBoundaryProvider: FC = ({ children }) => { - return {children}; + return ( + + {children} + + ); }; diff --git a/src/providers/index.tsx b/src/providers/index.tsx index 0fa5fc3..c7753c7 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -1,19 +1,19 @@ -import type { FC, ReactNode } from 'react'; +import type { FC, ReactNode } from "react"; -import { ErrorBoundaryProvider } from './error-boundary'; -import { UIProvider } from '@/providers/ui'; -import { ToastProvider } from './toast'; +import { UIProvider } from "@/providers/ui"; +import { ErrorBoundaryProvider } from "./error-boundary"; +import { ToastProvider } from "./toast"; type Props = { - children: ReactNode; + children: ReactNode; }; export const AppProvider: FC = ({ children }) => { - return ( - - - {children} - - - ); + return ( + + + {children} + + + ); }; diff --git a/src/providers/toast.tsx b/src/providers/toast.tsx index 8023781..9257d73 100644 --- a/src/providers/toast.tsx +++ b/src/providers/toast.tsx @@ -1,13 +1,13 @@ -import { FC, ReactNode } from 'react'; -import { Toaster } from 'sonner'; +import type { FC, ReactNode } from "react"; +import { Toaster } from "sonner"; export const ToastProvider: FC<{ - children: ReactNode; + children: ReactNode; }> = ({ children }) => { - return ( - <> - {children} - - - ); + return ( + <> + {children} + + + ); }; diff --git a/src/providers/ui.tsx b/src/providers/ui.tsx index 5e22bb8..1c42870 100644 --- a/src/providers/ui.tsx +++ b/src/providers/ui.tsx @@ -1,49 +1,64 @@ -import type { FC, ReactNode } from 'react'; -import { MantineColorsTuple, MantineProvider } from '@mantine/core'; -import theme from '@/style/theme.json'; -import _ from 'lodash'; -import { ModalsProvider } from '@mantine/modals'; -import '@mantine/core/styles.css'; -import { NextUIProvider } from '@nextui-org/react'; -import { LocaleProvider } from '@douyinfe/semi-ui'; -import { lang2ArcoLocale, lang2SemiLocale, SUPPORTED_LANGUAGE } from '@/utils/i18n'; -import { useTranslation } from 'react-i18next'; -import { ConfigProvider } from '@arco-design/web-react'; +import theme from "@/style/theme.json"; +import { type MantineColorsTuple, MantineProvider } from "@mantine/core"; +import { ModalsProvider } from "@mantine/modals"; +import _ from "lodash"; +import type { FC, ReactNode } from "react"; +import "@mantine/core/styles.css"; +import { + type SUPPORTED_LANGUAGE, + lang2ArcoLocale, + lang2SemiLocale, +} from "@/utils/i18n"; +import { ConfigProvider } from "@arco-design/web-react"; +import { LocaleProvider } from "@douyinfe/semi-ui"; +import { NextUIProvider } from "@nextui-org/react"; +import { useTranslation } from "react-i18next"; type Props = { - children: ReactNode; + children: ReactNode; }; export const UIProvider: FC = ({ children }) => { - const { i18n } = useTranslation(); - return ( - - - { - result || (result = {}); - result[key] = Object.values(value) as unknown as MantineColorsTuple; - }), - primaryColor: 'primary', - }} - > - - {children} - - - - - ); + const { i18n } = useTranslation(); + return ( + + + { + // biome-ignore lint/suspicious/noAssignInExpressions: + // biome-ignore lint/style/noParameterAssign: + result || (result = {}); + result[key] = Object.values( + value, + ) as unknown as MantineColorsTuple; + }, + ), + primaryColor: "primary", + }} + > + + {children} + + + + + ); }; diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 7055ee0..b1fb8d6 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -8,374 +8,374 @@ // This file is auto-generated by TanStack Router -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute } from "@tanstack/react-router"; // Import Routes -import { Route as rootRoute } from './routes/__root' -import { Route as WarningImport } from './routes/warning' -import { Route as IndexImport } from './routes/index' -import { Route as InsInsIDLayoutImport } from './routes/ins/$insID/_layout' -import { Route as InsInsIDLayoutIndexImport } from './routes/ins/$insID/_layout/index' -import { Route as InsInsIDLayoutTasksImport } from './routes/ins/$insID/_layout/tasks' -import { Route as InsInsIDLayoutKeysImport } from './routes/ins/$insID/_layout/keys' -import { Route as InsInsIDLayoutIndexIndexUIDLayoutImport } from './routes/ins/$insID/_layout/index/$indexUID/_layout' -import { Route as InsInsIDLayoutIndexIndexUIDLayoutIndexImport } from './routes/ins/$insID/_layout/index/$indexUID/_layout/index' -import { Route as InsInsIDLayoutIndexIndexUIDLayoutSettingImport } from './routes/ins/$insID/_layout/index/$indexUID/_layout/setting' -import { Route as InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexImport } from './routes/ins/$insID/_layout/index/$indexUID/_layout/documents/index' -import { Route as InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadImport } from './routes/ins/$insID/_layout/index/$indexUID/_layout/documents/upload' +import { Route as rootRoute } from "./routes/__root"; +import { Route as IndexImport } from "./routes/index"; +import { Route as InsInsIDLayoutImport } from "./routes/ins/$insID/_layout"; +import { Route as InsInsIDLayoutIndexImport } from "./routes/ins/$insID/_layout/index"; +import { Route as InsInsIDLayoutIndexIndexUIDLayoutImport } from "./routes/ins/$insID/_layout/index/$indexUID/_layout"; +import { Route as InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexImport } from "./routes/ins/$insID/_layout/index/$indexUID/_layout/documents/index"; +import { Route as InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadImport } from "./routes/ins/$insID/_layout/index/$indexUID/_layout/documents/upload"; +import { Route as InsInsIDLayoutIndexIndexUIDLayoutIndexImport } from "./routes/ins/$insID/_layout/index/$indexUID/_layout/index"; +import { Route as InsInsIDLayoutIndexIndexUIDLayoutSettingImport } from "./routes/ins/$insID/_layout/index/$indexUID/_layout/setting"; +import { Route as InsInsIDLayoutKeysImport } from "./routes/ins/$insID/_layout/keys"; +import { Route as InsInsIDLayoutTasksImport } from "./routes/ins/$insID/_layout/tasks"; +import { Route as WarningImport } from "./routes/warning"; // Create Virtual Routes -const InsInsIDImport = createFileRoute('/ins/$insID')() +const InsInsIDImport = createFileRoute("/ins/$insID")(); const InsInsIDLayoutIndexIndexUIDImport = createFileRoute( - '/ins/$insID/_layout/index/$indexUID', -)() + "/ins/$insID/_layout/index/$indexUID", +)(); // Create/Update Routes const WarningRoute = WarningImport.update({ - path: '/warning', - getParentRoute: () => rootRoute, -} as any) + path: "/warning", + getParentRoute: () => rootRoute, +} as any); const IndexRoute = IndexImport.update({ - path: '/', - getParentRoute: () => rootRoute, -} as any) + path: "/", + getParentRoute: () => rootRoute, +} as any); const InsInsIDRoute = InsInsIDImport.update({ - path: '/ins/$insID', - getParentRoute: () => rootRoute, -} as any) + path: "/ins/$insID", + getParentRoute: () => rootRoute, +} as any); const InsInsIDLayoutRoute = InsInsIDLayoutImport.update({ - id: '/_layout', - getParentRoute: () => InsInsIDRoute, -} as any) + id: "/_layout", + getParentRoute: () => InsInsIDRoute, +} as any); const InsInsIDLayoutIndexRoute = InsInsIDLayoutIndexImport.update({ - path: '/', - getParentRoute: () => InsInsIDLayoutRoute, -} as any) + path: "/", + getParentRoute: () => InsInsIDLayoutRoute, +} as any); const InsInsIDLayoutTasksRoute = InsInsIDLayoutTasksImport.update({ - path: '/tasks', - getParentRoute: () => InsInsIDLayoutRoute, -} as any) + path: "/tasks", + getParentRoute: () => InsInsIDLayoutRoute, +} as any); const InsInsIDLayoutKeysRoute = InsInsIDLayoutKeysImport.update({ - path: '/keys', - getParentRoute: () => InsInsIDLayoutRoute, -} as any) + path: "/keys", + getParentRoute: () => InsInsIDLayoutRoute, +} as any); const InsInsIDLayoutIndexIndexUIDRoute = - InsInsIDLayoutIndexIndexUIDImport.update({ - path: '/index/$indexUID', - getParentRoute: () => InsInsIDLayoutRoute, - } as any) + InsInsIDLayoutIndexIndexUIDImport.update({ + path: "/index/$indexUID", + getParentRoute: () => InsInsIDLayoutRoute, + } as any); const InsInsIDLayoutIndexIndexUIDLayoutRoute = - InsInsIDLayoutIndexIndexUIDLayoutImport.update({ - id: '/_layout', - getParentRoute: () => InsInsIDLayoutIndexIndexUIDRoute, - } as any) + InsInsIDLayoutIndexIndexUIDLayoutImport.update({ + id: "/_layout", + getParentRoute: () => InsInsIDLayoutIndexIndexUIDRoute, + } as any); const InsInsIDLayoutIndexIndexUIDLayoutIndexRoute = - InsInsIDLayoutIndexIndexUIDLayoutIndexImport.update({ - path: '/', - getParentRoute: () => InsInsIDLayoutIndexIndexUIDLayoutRoute, - } as any) + InsInsIDLayoutIndexIndexUIDLayoutIndexImport.update({ + path: "/", + getParentRoute: () => InsInsIDLayoutIndexIndexUIDLayoutRoute, + } as any); const InsInsIDLayoutIndexIndexUIDLayoutSettingRoute = - InsInsIDLayoutIndexIndexUIDLayoutSettingImport.update({ - path: '/setting', - getParentRoute: () => InsInsIDLayoutIndexIndexUIDLayoutRoute, - } as any) + InsInsIDLayoutIndexIndexUIDLayoutSettingImport.update({ + path: "/setting", + getParentRoute: () => InsInsIDLayoutIndexIndexUIDLayoutRoute, + } as any); const InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute = - InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexImport.update({ - path: '/documents/', - getParentRoute: () => InsInsIDLayoutIndexIndexUIDLayoutRoute, - } as any) + InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexImport.update({ + path: "/documents/", + getParentRoute: () => InsInsIDLayoutIndexIndexUIDLayoutRoute, + } as any); const InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute = - InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadImport.update({ - path: '/documents/upload', - getParentRoute: () => InsInsIDLayoutIndexIndexUIDLayoutRoute, - } as any) + InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadImport.update({ + path: "/documents/upload", + getParentRoute: () => InsInsIDLayoutIndexIndexUIDLayoutRoute, + } as any); // Populate the FileRoutesByPath interface -declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexImport - parentRoute: typeof rootRoute - } - '/warning': { - id: '/warning' - path: '/warning' - fullPath: '/warning' - preLoaderRoute: typeof WarningImport - parentRoute: typeof rootRoute - } - '/ins/$insID': { - id: '/ins/$insID' - path: '/ins/$insID' - fullPath: '/ins/$insID' - preLoaderRoute: typeof InsInsIDImport - parentRoute: typeof rootRoute - } - '/ins/$insID/_layout': { - id: '/ins/$insID/_layout' - path: '/ins/$insID' - fullPath: '/ins/$insID' - preLoaderRoute: typeof InsInsIDLayoutImport - parentRoute: typeof InsInsIDRoute - } - '/ins/$insID/_layout/keys': { - id: '/ins/$insID/_layout/keys' - path: '/keys' - fullPath: '/ins/$insID/keys' - preLoaderRoute: typeof InsInsIDLayoutKeysImport - parentRoute: typeof InsInsIDLayoutImport - } - '/ins/$insID/_layout/tasks': { - id: '/ins/$insID/_layout/tasks' - path: '/tasks' - fullPath: '/ins/$insID/tasks' - preLoaderRoute: typeof InsInsIDLayoutTasksImport - parentRoute: typeof InsInsIDLayoutImport - } - '/ins/$insID/_layout/': { - id: '/ins/$insID/_layout/' - path: '/' - fullPath: '/ins/$insID/' - preLoaderRoute: typeof InsInsIDLayoutIndexImport - parentRoute: typeof InsInsIDLayoutImport - } - '/ins/$insID/_layout/index/$indexUID': { - id: '/ins/$insID/_layout/index/$indexUID' - path: '/index/$indexUID' - fullPath: '/ins/$insID/index/$indexUID' - preLoaderRoute: typeof InsInsIDLayoutIndexIndexUIDImport - parentRoute: typeof InsInsIDLayoutImport - } - '/ins/$insID/_layout/index/$indexUID/_layout': { - id: '/ins/$insID/_layout/index/$indexUID/_layout' - path: '/index/$indexUID' - fullPath: '/ins/$insID/index/$indexUID' - preLoaderRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutImport - parentRoute: typeof InsInsIDLayoutIndexIndexUIDRoute - } - '/ins/$insID/_layout/index/$indexUID/_layout/setting': { - id: '/ins/$insID/_layout/index/$indexUID/_layout/setting' - path: '/setting' - fullPath: '/ins/$insID/index/$indexUID/setting' - preLoaderRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutSettingImport - parentRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutImport - } - '/ins/$insID/_layout/index/$indexUID/_layout/': { - id: '/ins/$insID/_layout/index/$indexUID/_layout/' - path: '/' - fullPath: '/ins/$insID/index/$indexUID/' - preLoaderRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutIndexImport - parentRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutImport - } - '/ins/$insID/_layout/index/$indexUID/_layout/documents/upload': { - id: '/ins/$insID/_layout/index/$indexUID/_layout/documents/upload' - path: '/documents/upload' - fullPath: '/ins/$insID/index/$indexUID/documents/upload' - preLoaderRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadImport - parentRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutImport - } - '/ins/$insID/_layout/index/$indexUID/_layout/documents/': { - id: '/ins/$insID/_layout/index/$indexUID/_layout/documents/' - path: '/documents' - fullPath: '/ins/$insID/index/$indexUID/documents' - preLoaderRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexImport - parentRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutImport - } - } +declare module "@tanstack/react-router" { + interface FileRoutesByPath { + "/": { + id: "/"; + path: "/"; + fullPath: "/"; + preLoaderRoute: typeof IndexImport; + parentRoute: typeof rootRoute; + }; + "/warning": { + id: "/warning"; + path: "/warning"; + fullPath: "/warning"; + preLoaderRoute: typeof WarningImport; + parentRoute: typeof rootRoute; + }; + "/ins/$insID": { + id: "/ins/$insID"; + path: "/ins/$insID"; + fullPath: "/ins/$insID"; + preLoaderRoute: typeof InsInsIDImport; + parentRoute: typeof rootRoute; + }; + "/ins/$insID/_layout": { + id: "/ins/$insID/_layout"; + path: "/ins/$insID"; + fullPath: "/ins/$insID"; + preLoaderRoute: typeof InsInsIDLayoutImport; + parentRoute: typeof InsInsIDRoute; + }; + "/ins/$insID/_layout/keys": { + id: "/ins/$insID/_layout/keys"; + path: "/keys"; + fullPath: "/ins/$insID/keys"; + preLoaderRoute: typeof InsInsIDLayoutKeysImport; + parentRoute: typeof InsInsIDLayoutImport; + }; + "/ins/$insID/_layout/tasks": { + id: "/ins/$insID/_layout/tasks"; + path: "/tasks"; + fullPath: "/ins/$insID/tasks"; + preLoaderRoute: typeof InsInsIDLayoutTasksImport; + parentRoute: typeof InsInsIDLayoutImport; + }; + "/ins/$insID/_layout/": { + id: "/ins/$insID/_layout/"; + path: "/"; + fullPath: "/ins/$insID/"; + preLoaderRoute: typeof InsInsIDLayoutIndexImport; + parentRoute: typeof InsInsIDLayoutImport; + }; + "/ins/$insID/_layout/index/$indexUID": { + id: "/ins/$insID/_layout/index/$indexUID"; + path: "/index/$indexUID"; + fullPath: "/ins/$insID/index/$indexUID"; + preLoaderRoute: typeof InsInsIDLayoutIndexIndexUIDImport; + parentRoute: typeof InsInsIDLayoutImport; + }; + "/ins/$insID/_layout/index/$indexUID/_layout": { + id: "/ins/$insID/_layout/index/$indexUID/_layout"; + path: "/index/$indexUID"; + fullPath: "/ins/$insID/index/$indexUID"; + preLoaderRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutImport; + parentRoute: typeof InsInsIDLayoutIndexIndexUIDRoute; + }; + "/ins/$insID/_layout/index/$indexUID/_layout/setting": { + id: "/ins/$insID/_layout/index/$indexUID/_layout/setting"; + path: "/setting"; + fullPath: "/ins/$insID/index/$indexUID/setting"; + preLoaderRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutSettingImport; + parentRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutImport; + }; + "/ins/$insID/_layout/index/$indexUID/_layout/": { + id: "/ins/$insID/_layout/index/$indexUID/_layout/"; + path: "/"; + fullPath: "/ins/$insID/index/$indexUID/"; + preLoaderRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutIndexImport; + parentRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutImport; + }; + "/ins/$insID/_layout/index/$indexUID/_layout/documents/upload": { + id: "/ins/$insID/_layout/index/$indexUID/_layout/documents/upload"; + path: "/documents/upload"; + fullPath: "/ins/$insID/index/$indexUID/documents/upload"; + preLoaderRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadImport; + parentRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutImport; + }; + "/ins/$insID/_layout/index/$indexUID/_layout/documents/": { + id: "/ins/$insID/_layout/index/$indexUID/_layout/documents/"; + path: "/documents"; + fullPath: "/ins/$insID/index/$indexUID/documents"; + preLoaderRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexImport; + parentRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutImport; + }; + } } // Create and export the route tree interface InsInsIDLayoutIndexIndexUIDLayoutRouteChildren { - InsInsIDLayoutIndexIndexUIDLayoutSettingRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutSettingRoute - InsInsIDLayoutIndexIndexUIDLayoutIndexRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutIndexRoute - InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute - InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute + InsInsIDLayoutIndexIndexUIDLayoutSettingRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutSettingRoute; + InsInsIDLayoutIndexIndexUIDLayoutIndexRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutIndexRoute; + InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute; + InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute; } const InsInsIDLayoutIndexIndexUIDLayoutRouteChildren: InsInsIDLayoutIndexIndexUIDLayoutRouteChildren = - { - InsInsIDLayoutIndexIndexUIDLayoutSettingRoute: - InsInsIDLayoutIndexIndexUIDLayoutSettingRoute, - InsInsIDLayoutIndexIndexUIDLayoutIndexRoute: - InsInsIDLayoutIndexIndexUIDLayoutIndexRoute, - InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute: - InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute, - InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute: - InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute, - } + { + InsInsIDLayoutIndexIndexUIDLayoutSettingRoute: + InsInsIDLayoutIndexIndexUIDLayoutSettingRoute, + InsInsIDLayoutIndexIndexUIDLayoutIndexRoute: + InsInsIDLayoutIndexIndexUIDLayoutIndexRoute, + InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute: + InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute, + InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute: + InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute, + }; const InsInsIDLayoutIndexIndexUIDLayoutRouteWithChildren = - InsInsIDLayoutIndexIndexUIDLayoutRoute._addFileChildren( - InsInsIDLayoutIndexIndexUIDLayoutRouteChildren, - ) + InsInsIDLayoutIndexIndexUIDLayoutRoute._addFileChildren( + InsInsIDLayoutIndexIndexUIDLayoutRouteChildren, + ); interface InsInsIDLayoutIndexIndexUIDRouteChildren { - InsInsIDLayoutIndexIndexUIDLayoutRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutRouteWithChildren + InsInsIDLayoutIndexIndexUIDLayoutRoute: typeof InsInsIDLayoutIndexIndexUIDLayoutRouteWithChildren; } const InsInsIDLayoutIndexIndexUIDRouteChildren: InsInsIDLayoutIndexIndexUIDRouteChildren = - { - InsInsIDLayoutIndexIndexUIDLayoutRoute: - InsInsIDLayoutIndexIndexUIDLayoutRouteWithChildren, - } + { + InsInsIDLayoutIndexIndexUIDLayoutRoute: + InsInsIDLayoutIndexIndexUIDLayoutRouteWithChildren, + }; const InsInsIDLayoutIndexIndexUIDRouteWithChildren = - InsInsIDLayoutIndexIndexUIDRoute._addFileChildren( - InsInsIDLayoutIndexIndexUIDRouteChildren, - ) + InsInsIDLayoutIndexIndexUIDRoute._addFileChildren( + InsInsIDLayoutIndexIndexUIDRouteChildren, + ); interface InsInsIDLayoutRouteChildren { - InsInsIDLayoutKeysRoute: typeof InsInsIDLayoutKeysRoute - InsInsIDLayoutTasksRoute: typeof InsInsIDLayoutTasksRoute - InsInsIDLayoutIndexRoute: typeof InsInsIDLayoutIndexRoute - InsInsIDLayoutIndexIndexUIDRoute: typeof InsInsIDLayoutIndexIndexUIDRouteWithChildren + InsInsIDLayoutKeysRoute: typeof InsInsIDLayoutKeysRoute; + InsInsIDLayoutTasksRoute: typeof InsInsIDLayoutTasksRoute; + InsInsIDLayoutIndexRoute: typeof InsInsIDLayoutIndexRoute; + InsInsIDLayoutIndexIndexUIDRoute: typeof InsInsIDLayoutIndexIndexUIDRouteWithChildren; } const InsInsIDLayoutRouteChildren: InsInsIDLayoutRouteChildren = { - InsInsIDLayoutKeysRoute: InsInsIDLayoutKeysRoute, - InsInsIDLayoutTasksRoute: InsInsIDLayoutTasksRoute, - InsInsIDLayoutIndexRoute: InsInsIDLayoutIndexRoute, - InsInsIDLayoutIndexIndexUIDRoute: - InsInsIDLayoutIndexIndexUIDRouteWithChildren, -} + InsInsIDLayoutKeysRoute: InsInsIDLayoutKeysRoute, + InsInsIDLayoutTasksRoute: InsInsIDLayoutTasksRoute, + InsInsIDLayoutIndexRoute: InsInsIDLayoutIndexRoute, + InsInsIDLayoutIndexIndexUIDRoute: + InsInsIDLayoutIndexIndexUIDRouteWithChildren, +}; const InsInsIDLayoutRouteWithChildren = InsInsIDLayoutRoute._addFileChildren( - InsInsIDLayoutRouteChildren, -) + InsInsIDLayoutRouteChildren, +); interface InsInsIDRouteChildren { - InsInsIDLayoutRoute: typeof InsInsIDLayoutRouteWithChildren + InsInsIDLayoutRoute: typeof InsInsIDLayoutRouteWithChildren; } const InsInsIDRouteChildren: InsInsIDRouteChildren = { - InsInsIDLayoutRoute: InsInsIDLayoutRouteWithChildren, -} + InsInsIDLayoutRoute: InsInsIDLayoutRouteWithChildren, +}; const InsInsIDRouteWithChildren = InsInsIDRoute._addFileChildren( - InsInsIDRouteChildren, -) + InsInsIDRouteChildren, +); export interface FileRoutesByFullPath { - '/': typeof IndexRoute - '/warning': typeof WarningRoute - '/ins/$insID': typeof InsInsIDLayoutRouteWithChildren - '/ins/$insID/keys': typeof InsInsIDLayoutKeysRoute - '/ins/$insID/tasks': typeof InsInsIDLayoutTasksRoute - '/ins/$insID/': typeof InsInsIDLayoutIndexRoute - '/ins/$insID/index/$indexUID': typeof InsInsIDLayoutIndexIndexUIDLayoutRouteWithChildren - '/ins/$insID/index/$indexUID/setting': typeof InsInsIDLayoutIndexIndexUIDLayoutSettingRoute - '/ins/$insID/index/$indexUID/': typeof InsInsIDLayoutIndexIndexUIDLayoutIndexRoute - '/ins/$insID/index/$indexUID/documents/upload': typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute - '/ins/$insID/index/$indexUID/documents': typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute + "/": typeof IndexRoute; + "/warning": typeof WarningRoute; + "/ins/$insID": typeof InsInsIDLayoutRouteWithChildren; + "/ins/$insID/keys": typeof InsInsIDLayoutKeysRoute; + "/ins/$insID/tasks": typeof InsInsIDLayoutTasksRoute; + "/ins/$insID/": typeof InsInsIDLayoutIndexRoute; + "/ins/$insID/index/$indexUID": typeof InsInsIDLayoutIndexIndexUIDLayoutRouteWithChildren; + "/ins/$insID/index/$indexUID/setting": typeof InsInsIDLayoutIndexIndexUIDLayoutSettingRoute; + "/ins/$insID/index/$indexUID/": typeof InsInsIDLayoutIndexIndexUIDLayoutIndexRoute; + "/ins/$insID/index/$indexUID/documents/upload": typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute; + "/ins/$insID/index/$indexUID/documents": typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute; } export interface FileRoutesByTo { - '/': typeof IndexRoute - '/warning': typeof WarningRoute - '/ins/$insID': typeof InsInsIDLayoutIndexRoute - '/ins/$insID/keys': typeof InsInsIDLayoutKeysRoute - '/ins/$insID/tasks': typeof InsInsIDLayoutTasksRoute - '/ins/$insID/index/$indexUID': typeof InsInsIDLayoutIndexIndexUIDLayoutIndexRoute - '/ins/$insID/index/$indexUID/setting': typeof InsInsIDLayoutIndexIndexUIDLayoutSettingRoute - '/ins/$insID/index/$indexUID/documents/upload': typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute - '/ins/$insID/index/$indexUID/documents': typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute + "/": typeof IndexRoute; + "/warning": typeof WarningRoute; + "/ins/$insID": typeof InsInsIDLayoutIndexRoute; + "/ins/$insID/keys": typeof InsInsIDLayoutKeysRoute; + "/ins/$insID/tasks": typeof InsInsIDLayoutTasksRoute; + "/ins/$insID/index/$indexUID": typeof InsInsIDLayoutIndexIndexUIDLayoutIndexRoute; + "/ins/$insID/index/$indexUID/setting": typeof InsInsIDLayoutIndexIndexUIDLayoutSettingRoute; + "/ins/$insID/index/$indexUID/documents/upload": typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute; + "/ins/$insID/index/$indexUID/documents": typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute; } export interface FileRoutesById { - __root__: typeof rootRoute - '/': typeof IndexRoute - '/warning': typeof WarningRoute - '/ins/$insID': typeof InsInsIDRouteWithChildren - '/ins/$insID/_layout': typeof InsInsIDLayoutRouteWithChildren - '/ins/$insID/_layout/keys': typeof InsInsIDLayoutKeysRoute - '/ins/$insID/_layout/tasks': typeof InsInsIDLayoutTasksRoute - '/ins/$insID/_layout/': typeof InsInsIDLayoutIndexRoute - '/ins/$insID/_layout/index/$indexUID': typeof InsInsIDLayoutIndexIndexUIDRouteWithChildren - '/ins/$insID/_layout/index/$indexUID/_layout': typeof InsInsIDLayoutIndexIndexUIDLayoutRouteWithChildren - '/ins/$insID/_layout/index/$indexUID/_layout/setting': typeof InsInsIDLayoutIndexIndexUIDLayoutSettingRoute - '/ins/$insID/_layout/index/$indexUID/_layout/': typeof InsInsIDLayoutIndexIndexUIDLayoutIndexRoute - '/ins/$insID/_layout/index/$indexUID/_layout/documents/upload': typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute - '/ins/$insID/_layout/index/$indexUID/_layout/documents/': typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute + __root__: typeof rootRoute; + "/": typeof IndexRoute; + "/warning": typeof WarningRoute; + "/ins/$insID": typeof InsInsIDRouteWithChildren; + "/ins/$insID/_layout": typeof InsInsIDLayoutRouteWithChildren; + "/ins/$insID/_layout/keys": typeof InsInsIDLayoutKeysRoute; + "/ins/$insID/_layout/tasks": typeof InsInsIDLayoutTasksRoute; + "/ins/$insID/_layout/": typeof InsInsIDLayoutIndexRoute; + "/ins/$insID/_layout/index/$indexUID": typeof InsInsIDLayoutIndexIndexUIDRouteWithChildren; + "/ins/$insID/_layout/index/$indexUID/_layout": typeof InsInsIDLayoutIndexIndexUIDLayoutRouteWithChildren; + "/ins/$insID/_layout/index/$indexUID/_layout/setting": typeof InsInsIDLayoutIndexIndexUIDLayoutSettingRoute; + "/ins/$insID/_layout/index/$indexUID/_layout/": typeof InsInsIDLayoutIndexIndexUIDLayoutIndexRoute; + "/ins/$insID/_layout/index/$indexUID/_layout/documents/upload": typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsUploadRoute; + "/ins/$insID/_layout/index/$indexUID/_layout/documents/": typeof InsInsIDLayoutIndexIndexUIDLayoutDocumentsIndexRoute; } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: - | '/' - | '/warning' - | '/ins/$insID' - | '/ins/$insID/keys' - | '/ins/$insID/tasks' - | '/ins/$insID/' - | '/ins/$insID/index/$indexUID' - | '/ins/$insID/index/$indexUID/setting' - | '/ins/$insID/index/$indexUID/' - | '/ins/$insID/index/$indexUID/documents/upload' - | '/ins/$insID/index/$indexUID/documents' - fileRoutesByTo: FileRoutesByTo - to: - | '/' - | '/warning' - | '/ins/$insID' - | '/ins/$insID/keys' - | '/ins/$insID/tasks' - | '/ins/$insID/index/$indexUID' - | '/ins/$insID/index/$indexUID/setting' - | '/ins/$insID/index/$indexUID/documents/upload' - | '/ins/$insID/index/$indexUID/documents' - id: - | '__root__' - | '/' - | '/warning' - | '/ins/$insID' - | '/ins/$insID/_layout' - | '/ins/$insID/_layout/keys' - | '/ins/$insID/_layout/tasks' - | '/ins/$insID/_layout/' - | '/ins/$insID/_layout/index/$indexUID' - | '/ins/$insID/_layout/index/$indexUID/_layout' - | '/ins/$insID/_layout/index/$indexUID/_layout/setting' - | '/ins/$insID/_layout/index/$indexUID/_layout/' - | '/ins/$insID/_layout/index/$indexUID/_layout/documents/upload' - | '/ins/$insID/_layout/index/$indexUID/_layout/documents/' - fileRoutesById: FileRoutesById + fileRoutesByFullPath: FileRoutesByFullPath; + fullPaths: + | "/" + | "/warning" + | "/ins/$insID" + | "/ins/$insID/keys" + | "/ins/$insID/tasks" + | "/ins/$insID/" + | "/ins/$insID/index/$indexUID" + | "/ins/$insID/index/$indexUID/setting" + | "/ins/$insID/index/$indexUID/" + | "/ins/$insID/index/$indexUID/documents/upload" + | "/ins/$insID/index/$indexUID/documents"; + fileRoutesByTo: FileRoutesByTo; + to: + | "/" + | "/warning" + | "/ins/$insID" + | "/ins/$insID/keys" + | "/ins/$insID/tasks" + | "/ins/$insID/index/$indexUID" + | "/ins/$insID/index/$indexUID/setting" + | "/ins/$insID/index/$indexUID/documents/upload" + | "/ins/$insID/index/$indexUID/documents"; + id: + | "__root__" + | "/" + | "/warning" + | "/ins/$insID" + | "/ins/$insID/_layout" + | "/ins/$insID/_layout/keys" + | "/ins/$insID/_layout/tasks" + | "/ins/$insID/_layout/" + | "/ins/$insID/_layout/index/$indexUID" + | "/ins/$insID/_layout/index/$indexUID/_layout" + | "/ins/$insID/_layout/index/$indexUID/_layout/setting" + | "/ins/$insID/_layout/index/$indexUID/_layout/" + | "/ins/$insID/_layout/index/$indexUID/_layout/documents/upload" + | "/ins/$insID/_layout/index/$indexUID/_layout/documents/"; + fileRoutesById: FileRoutesById; } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - WarningRoute: typeof WarningRoute - InsInsIDRoute: typeof InsInsIDRouteWithChildren + IndexRoute: typeof IndexRoute; + WarningRoute: typeof WarningRoute; + InsInsIDRoute: typeof InsInsIDRouteWithChildren; } const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, - WarningRoute: WarningRoute, - InsInsIDRoute: InsInsIDRouteWithChildren, -} + IndexRoute: IndexRoute, + WarningRoute: WarningRoute, + InsInsIDRoute: InsInsIDRouteWithChildren, +}; export const routeTree = rootRoute - ._addFileChildren(rootRouteChildren) - ._addFileTypes() + ._addFileChildren(rootRouteChildren) + ._addFileTypes(); /* prettier-ignore-end */ diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 6d1d656..cb95003 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,22 +1,22 @@ -import { createRootRoute, Outlet } from '@tanstack/react-router'; -import React from 'react'; +import { Outlet, createRootRoute } from "@tanstack/react-router"; +import React from "react"; const TanStackRouterDevtools = - process.env.NODE_ENV === 'development' - ? React.lazy(() => - // Lazy load in development - import('@tanstack/router-devtools').then((res) => ({ - default: res.TanStackRouterDevtools, - })) - ) - : () => null; // Render nothing in production; + process.env.NODE_ENV === "development" + ? React.lazy(() => + // Lazy load in development + import("@tanstack/router-devtools").then((res) => ({ + default: res.TanStackRouterDevtools, + })), + ) + : () => null; // Render nothing in production; export const Route = createRootRoute({ - component: () => ( - <> - - - - ), - wrapInSuspense: true, + component: () => ( + <> + + + + ), + wrapInSuspense: true, }); diff --git a/src/routes/index.tsx b/src/routes/index.tsx index edcc602..2448f4d 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,198 +1,230 @@ -import { Logo } from '@/components/Logo'; -import { useCallback, useMemo } from 'react'; -import { Instance, useAppStore } from '@/store'; -import { ActionIcon, Tooltip } from '@mantine/core'; -import { Footer } from '@/components/Footer'; -import { IconBooks, IconCirclePlus, IconCircleX, IconKey, IconListCheck, IconPencilMinus } from '@tabler/icons-react'; -import { isSingletonMode, testConnection, validateKeysRouteAvailable } from '@/utils/conn'; -import { modals } from '@mantine/modals'; -import { useNavigatePreCheck } from '@/hooks/useRoutePreCheck'; -import { useTranslation } from 'react-i18next'; -import { createFileRoute, redirect } from '@tanstack/react-router'; -import { Button } from '@douyinfe/semi-ui'; -import { InsFormModal } from '@/components/instanceFormModal'; -import { TimeAgo } from '@/components/timeago'; -import { DashboardSettingsButton } from '@/components/DashboardSettingsButton'; +import { DashboardSettingsButton } from "@/components/DashboardSettingsButton"; +import { Footer } from "@/components/Footer"; +import { Logo } from "@/components/Logo"; +import { InsFormModal } from "@/components/instanceFormModal"; +import { TimeAgo } from "@/components/timeago"; +import { useNavigatePreCheck } from "@/hooks/useRoutePreCheck"; +import { type Instance, useAppStore } from "@/store"; +import { + isSingletonMode, + testConnection, + validateKeysRouteAvailable, +} from "@/utils/conn"; +import { Button } from "@douyinfe/semi-ui"; +import { ActionIcon, Tooltip } from "@mantine/core"; +import { modals } from "@mantine/modals"; +import { + IconBooks, + IconCirclePlus, + IconCircleX, + IconKey, + IconListCheck, + IconPencilMinus, +} from "@tabler/icons-react"; +import { createFileRoute, redirect } from "@tanstack/react-router"; +import { useCallback, useMemo } from "react"; +import { useTranslation } from "react-i18next"; -const instanceCardClassName = `col-span-1 h-28 rounded-lg`; +const instanceCardClassName = "col-span-1 h-28 rounded-lg"; function Dashboard() { - const { t } = useTranslation('dashboard'); + const { t } = useTranslation("dashboard"); - const navigate = useNavigatePreCheck((params, opt) => { - console.debug('dashboard', 'navigate', params.to, opt?.currentInstance); - if (typeof params.to === 'string' && /\/keys$/.test(params.to)) { - // check before keys page (no masterKey will cause error) - return validateKeysRouteAvailable(opt?.currentInstance?.apiKey); - } - return null; - }); + const navigate = useNavigatePreCheck((params, opt) => { + console.debug("dashboard", "navigate", params.to, opt?.currentInstance); + if (typeof params.to === "string" && /\/keys$/.test(params.to)) { + // check before keys page (no masterKey will cause error) + return validateKeysRouteAvailable(opt?.currentInstance?.apiKey); + } + return null; + }); - const removeInstance = useAppStore((state) => state.removeInstance); - const instances = useAppStore((state) => state.instances); + const removeInstance = useAppStore((state) => state.removeInstance); + const instances = useAppStore((state) => state.instances); - const onClickInstance = useCallback( - (ins: Instance, to?: string) => { - // do connection test before next step - testConnection({ ...ins }).then(() => { - if (to) { - navigate({ to }, { currentInstance: ins }); - } - }); - }, - [navigate] - ); + const onClickInstance = useCallback( + (ins: Instance, to?: string) => { + // do connection test before next step + testConnection({ ...ins }).then(() => { + if (to) { + navigate({ to }, { currentInstance: ins }); + } + }); + }, + [navigate], + ); - const onClickRemoveInstance = useCallback( - (ins: Instance) => { - const modalId = 'removeInsModal'; - modals.open({ - modalId, - title: t('instance.remove.title'), - centered: true, - children: ( -
-

- {t('instance.remove.tip')} ({ins.name})? -

-
- - -
-
- ), - }); - }, - [removeInstance, t] - ); + const onClickRemoveInstance = useCallback( + (ins: Instance) => { + const modalId = "removeInsModal"; + modals.open({ + modalId, + title: t("instance.remove.title"), + centered: true, + children: ( +
+

+ {t("instance.remove.tip")} ({ins.name})? +

+
+ + +
+
+ ), + }); + }, + [removeInstance, t], + ); - const instancesList = useMemo(() => { - return instances.map((instance, index) => { - return ( -
{ + return instances.map((instance, index) => { + return ( +
-
-
-

onClickInstance(instance, `/ins/${instance.id}`)} - > - {instance.name} -

-

onClickInstance(instance, `/ins/${instance.id}`)} - > - #{instance.id} -

-
-
- - - - - - - - onClickRemoveInstance(instance)}> - - -
-
-
-

- {t('instance.updated_at')} -

- - onClickInstance(instance, `/ins/${instance.id}`)} - > - - - - - onClickInstance(instance, `/ins/${instance.id}/tasks`)} - > - - - - - onClickInstance(instance, `/ins/${instance.id}/keys`)} - > - - - -
-
- ); - }); - }, [instances, onClickInstance, onClickRemoveInstance, t]); + > +
+
+

onClickInstance(instance, `/ins/${instance.id}`)} + > + {instance.name} +

+

onClickInstance(instance, `/ins/${instance.id}`)} + > + #{instance.id} +

+
+
+ + + + + + + + onClickRemoveInstance(instance)} + > + + +
+
+
+

+ {t("instance.updated_at")} +

+ + onClickInstance(instance, `/ins/${instance.id}`)} + > + + + + + + onClickInstance(instance, `/ins/${instance.id}/tasks`) + } + > + + + + + + onClickInstance(instance, `/ins/${instance.id}/keys`) + } + > + + + +
+
+ ); + }); + }, [instances, onClickInstance, onClickRemoveInstance, t]); - return ( -
-
- -

{t('slogan')}

- -
- {instancesList} - -
+
+ +

+ {t("slogan")} +

+ +
+ {instancesList} + +
- -
-
-
-
-
-
- ); + > + +
+ +
+
+
+
+ ); } -export const Route = createFileRoute('/')({ - component: Dashboard, - beforeLoad: async () => { - if (isSingletonMode()) { - throw redirect({ - to: '/ins/$insID', - params: { - insID: '0', - }, - }); - } - }, +export const Route = createFileRoute("/")({ + component: Dashboard, + beforeLoad: async () => { + if (isSingletonMode()) { + throw redirect({ + to: "/ins/$insID", + params: { + insID: "0", + }, + }); + } + }, }); diff --git a/src/routes/ins/$insID/_layout.tsx b/src/routes/ins/$insID/_layout.tsx index 9918c5f..59f4569 100644 --- a/src/routes/ins/$insID/_layout.tsx +++ b/src/routes/ins/$insID/_layout.tsx @@ -1,17 +1,17 @@ -import { Outlet, createFileRoute } from '@tanstack/react-router'; -import { Header } from '@/components/InsHeader'; -import { LoaderPage } from '@/components/loader'; +import { Header } from "@/components/InsHeader"; +import { LoaderPage } from "@/components/loader"; +import { Outlet, createFileRoute } from "@tanstack/react-router"; function InsLayout() { - return ( -
-
- -
- ); + return ( +
+
+ +
+ ); } -export const Route = createFileRoute('/ins/$insID/_layout')({ - component: InsLayout, - pendingComponent: LoaderPage, +export const Route = createFileRoute("/ins/$insID/_layout")({ + component: InsLayout, + pendingComponent: LoaderPage, }); diff --git a/src/routes/ins/$insID/_layout/index.tsx b/src/routes/ins/$insID/_layout/index.tsx index 4106d10..bdfc61f 100644 --- a/src/routes/ins/$insID/_layout/index.tsx +++ b/src/routes/ins/$insID/_layout/index.tsx @@ -1,102 +1,125 @@ -import { Link, createFileRoute } from '@tanstack/react-router'; -import { useTranslation } from 'react-i18next'; -import { Descriptions, Skeleton, Tag } from '@douyinfe/semi-ui'; -import { useCurrentInstance } from '@/hooks/useCurrentInstance'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { useInstanceStats } from '@/hooks/useInstanceStats'; -import { useMemo } from 'react'; -import { Copyable } from '@/components/Copyable'; -import { useInstanceHealth } from '@/hooks/useInstanceHealth'; -import { IndexList } from '@/components/IndexList'; -import { TitleWithUnderline } from '@/components/title'; -import { Tooltip } from '@arco-design/web-react'; -import { InsFormModal } from '@/components/instanceFormModal'; -import { Button } from '@nextui-org/react'; -import { DumpButton } from '@/components/dump'; -import { LoaderPage } from '@/components/loader'; -import { isSingletonMode } from '@/utils/conn'; -import { Footer } from '@/components/Footer'; -import { TimeAgo } from '@/components/timeago'; -import { filesize } from 'filesize'; +import { Copyable } from "@/components/Copyable"; +import { Footer } from "@/components/Footer"; +import { IndexList } from "@/components/IndexList"; +import { DumpButton } from "@/components/dump"; +import { InsFormModal } from "@/components/instanceFormModal"; +import { LoaderPage } from "@/components/loader"; +import { TimeAgo } from "@/components/timeago"; +import { TitleWithUnderline } from "@/components/title"; +import { useCurrentInstance } from "@/hooks/useCurrentInstance"; +import { useInstanceHealth } from "@/hooks/useInstanceHealth"; +import { useInstanceStats } from "@/hooks/useInstanceStats"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { isSingletonMode } from "@/utils/conn"; +import { Tooltip } from "@arco-design/web-react"; +import { Descriptions, Skeleton, Tag } from "@douyinfe/semi-ui"; +import { Button } from "@nextui-org/react"; +import { Link, createFileRoute } from "@tanstack/react-router"; +import { filesize } from "filesize"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; function InsDash() { - const { t } = useTranslation('instance'); - const currentInstance = useCurrentInstance(); - const client = useMeiliClient(); - const stats = useInstanceStats(client); - const isHealth = useInstanceHealth(client); + const { t } = useTranslation("instance"); + const currentInstance = useCurrentInstance(); + const client = useMeiliClient(); + const stats = useInstanceStats(client); + const isHealth = useInstanceHealth(client); - const insDescriptionsData = useMemo(() => { - return [ - { - key: t('host'), - value: {currentInstance.host}, - }, - { - key: t('common:updated_at'), - value: , - }, - { - key: t('db_size'), - value: ( - } active loading={!stats?.databaseSize}> - {filesize(stats?.databaseSize ?? 0)} - - ), - }, - { - key: t('status.label'), - value: isHealth ? {t('status.available')} : {t('unknown')}, - }, - { - key: t('version.label'), - value: ( - } active loading={!stats?.version}> - {stats?.version.pkgVersion} - - ), - }, - ]; - }, [currentInstance.host, currentInstance.updatedTime, isHealth, stats?.databaseSize, stats?.version, t]); + const insDescriptionsData = useMemo(() => { + return [ + { + key: t("host"), + value: {currentInstance.host}, + }, + { + key: t("common:updated_at"), + value: , + }, + { + key: t("db_size"), + value: ( + } + active + loading={!stats?.databaseSize} + > + {filesize(stats?.databaseSize ?? 0)} + + ), + }, + { + key: t("status.label"), + value: isHealth ? ( + {t("status.available")} + ) : ( + {t("unknown")} + ), + }, + { + key: t("version.label"), + value: ( + } + active + loading={!stats?.version} + > + {stats?.version.pkgVersion} + + ), + }, + ]; + }, [ + currentInstance.host, + currentInstance.updatedTime, + isHealth, + stats?.databaseSize, + stats?.version, + t, + ]); - return ( -
-
- {!isSingletonMode() && ( -
- {`#${currentInstance.id} ${currentInstance.name}`} - - -
-
-
-
- )} -
- -
- - - - - - - -
-
- -
-
-
- ); + return ( +
+
+ {!isSingletonMode() && ( +
+ {`#${currentInstance.id} ${currentInstance.name}`} + + +
+ + +
+ )} +
+ +
+ + + + + + + +
+
+ +
+
+
+ ); } -export const Route = createFileRoute('/ins/$insID/_layout/')({ - component: InsDash, - pendingComponent: LoaderPage, +export const Route = createFileRoute("/ins/$insID/_layout/")({ + component: InsDash, + pendingComponent: LoaderPage, }); diff --git a/src/routes/ins/$insID/_layout/index/$indexUID/_layout.tsx b/src/routes/ins/$insID/_layout/index/$indexUID/_layout.tsx index 45dddba..dc44332 100644 --- a/src/routes/ins/$insID/_layout/index/$indexUID/_layout.tsx +++ b/src/routes/ins/$insID/_layout/index/$indexUID/_layout.tsx @@ -1,114 +1,130 @@ -import { Link, Outlet, createFileRoute } from '@tanstack/react-router'; -import { useTranslation } from 'react-i18next'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { ReactNode, useMemo } from 'react'; -import { Copyable } from '@/components/Copyable'; -import { useCurrentIndex } from '@/hooks/useCurrentIndex'; -import { LoaderPage } from '@/components/loader'; -import { TitleWithUnderline } from '@/components/title'; -import { Button } from '@nextui-org/react'; -import { IndexPrimaryKey } from '@/components/indexPrimaryKey'; -import { TimeAgo } from '@/components/timeago'; -import { useInstanceStats } from '@/hooks/useInstanceStats'; -import { Skeleton } from '@douyinfe/semi-ui'; -import { filesize } from 'filesize'; +import { Copyable } from "@/components/Copyable"; +import { IndexPrimaryKey } from "@/components/indexPrimaryKey"; +import { LoaderPage } from "@/components/loader"; +import { TimeAgo } from "@/components/timeago"; +import { TitleWithUnderline } from "@/components/title"; +import { useCurrentIndex } from "@/hooks/useCurrentIndex"; +import { useInstanceStats } from "@/hooks/useInstanceStats"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { Skeleton } from "@douyinfe/semi-ui"; +import { Button } from "@nextui-org/react"; +import { Link, Outlet, createFileRoute } from "@tanstack/react-router"; +import { filesize } from "filesize"; +import { type ReactNode, useMemo } from "react"; +import { useTranslation } from "react-i18next"; const InfoRow = ({ value, label }: { label: string; value: ReactNode }) => { - return ( -
-

{label}

- {value} -
- ); + return ( +
+

{label}

+ {value} +
+ ); }; function IndexDash() { - const { t } = useTranslation('index'); - const client = useMeiliClient(); - const currentIndex = useCurrentIndex(client); - const stats = useInstanceStats(client); + const { t } = useTranslation("index"); + const client = useMeiliClient(); + const currentIndex = useCurrentIndex(client); + const stats = useInstanceStats(client); - console.debug('index dash page building', currentIndex); + console.debug("index dash page building", currentIndex); - return useMemo(() => { - return ( -
-
- {`${currentIndex.index?.uid}`} - {currentIndex.index?.uid || ''}} /> - } /> - {currentIndex.index && ( - { - window.location.reload(); - }} - /> - } - /> - )} - } active loading={!stats?.databaseSize}> - {filesize(stats?.databaseSize ?? 0)} - - } - /> -
- - - - - - - - - - - - - - - -
-
-
- -
-
- ); - }, [currentIndex.index, stats?.databaseSize, t]); + return useMemo(() => { + return ( +
+
+ {`${currentIndex.index?.uid}`} + {currentIndex.index?.uid || ""}} + /> + } + /> + {currentIndex.index && ( + { + window.location.reload(); + }} + /> + } + /> + )} + } + active + loading={!stats?.databaseSize} + > + {filesize(stats?.databaseSize ?? 0)} + + } + /> +
+ + + + + + + + + + + + + + + +
+
+
+ +
+
+ ); + }, [currentIndex.index, stats?.databaseSize, t]); } -export const Route = createFileRoute('/ins/$insID/_layout/index/$indexUID/_layout')({ - component: IndexDash, - pendingComponent: LoaderPage, +export const Route = createFileRoute( + "/ins/$insID/_layout/index/$indexUID/_layout", +)({ + component: IndexDash, + pendingComponent: LoaderPage, }); diff --git a/src/routes/ins/$insID/_layout/index/$indexUID/_layout/documents/index.tsx b/src/routes/ins/$insID/_layout/index/$indexUID/_layout/documents/index.tsx index b882d49..37e8026 100644 --- a/src/routes/ins/$insID/_layout/index/$indexUID/_layout/documents/index.tsx +++ b/src/routes/ins/$insID/_layout/index/$indexUID/_layout/documents/index.tsx @@ -1,240 +1,282 @@ -import { LoaderPage } from '@/components/loader'; -import { useCurrentIndex } from '@/hooks/useCurrentIndex'; -import { createFileRoute, useNavigate } from '@tanstack/react-router'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { DocumentList, ListType } from '@/components/Document/list'; -import { useForm } from '@mantine/form'; -import { useQuery } from '@tanstack/react-query'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { useCurrentInstance } from '@/hooks/useCurrentInstance'; -import { useTranslation } from 'react-i18next'; -import useDebounce from 'ahooks/lib/useDebounce'; -import { Loader } from '@/components/loader'; -import { SearchForm } from '@/components/Document/searchForm'; -import { Button, Radio, RadioGroup } from '@douyinfe/semi-ui'; -import { exportToJSON } from '@/utils/file'; -import { z } from 'zod'; -import { EmptyArea } from '@/components/EmptyArea'; -import _ from 'lodash'; -import { cn } from '@/lib/cn'; +import { DocumentList, type ListType } from "@/components/Document/list"; +import { SearchForm } from "@/components/Document/searchForm"; +import { EmptyArea } from "@/components/EmptyArea"; +import { LoaderPage } from "@/components/loader"; +import { Loader } from "@/components/loader"; +import { useCurrentIndex } from "@/hooks/useCurrentIndex"; +import { useCurrentInstance } from "@/hooks/useCurrentInstance"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { cn } from "@/lib/cn"; +import { exportToJSON } from "@/utils/file"; +import { Button, Radio, RadioGroup } from "@douyinfe/semi-ui"; +import { useForm } from "@mantine/form"; +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import useDebounce from "ahooks/lib/useDebounce"; +import _ from "lodash"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; const emptySearchResult = { - hits: [], - estimatedTotalHits: 0, - processingTimeMs: 0, + hits: [], + estimatedTotalHits: 0, + processingTimeMs: 0, }; const searchSchema = z - .object({ - q: z.string().optional().default(''), - limit: z.number().positive().optional().default(20), - offset: z.number().nonnegative().optional().default(0), - filter: z.string().optional().default(''), - sort: z.string().optional().default(''), - listType: z.enum(['json', 'table', 'grid']).optional().default('json'), - showRankingScore: z.coerce.boolean().optional().default(false), - }) - .optional(); + .object({ + q: z.string(), + limit: z.number().positive(), + offset: z.number().nonnegative(), + filter: z.string(), + sort: z.string(), + listType: z.enum(["json", "table", "grid"]), + showRankingScore: z.coerce.boolean(), + }) + .partial(); export const Page = () => { - const navigate = useNavigate({ from: Route.fullPath }); - const searchParams = Route.useSearch(); - const { t } = useTranslation('document'); - const [listType, setListType] = useState(searchParams?.listType || 'json'); - const [searchAutoRefresh, setSearchAutoRefresh] = useState(false); - const [searchFormError, setSearchFormError] = useState(null); - const client = useMeiliClient(); - const currentIndex = useCurrentIndex(client); - const currentInstance = useCurrentInstance(); - const host = currentInstance?.host; - - const indexClient = useMemo(() => { - return currentIndex ? client.index(currentIndex.index.uid) : undefined; - }, [client, currentIndex]); - - const searchForm = useForm({ - initialValues: { - ..._.omit(searchParams, ['listType']), - }, - validate: { - limit: (value: number) => (value < 500 ? null : t('search.form.limit.validation_error')), - }, - }); - - useEffect(() => { - // update search params when form values changed - navigate({ - search: () => ({ - ...searchForm.values, - listType, - }), - }); - }, [navigate, searchForm.values, listType]); - - const indexPrimaryKeyQuery = useQuery({ - queryKey: ['indexPrimaryKey', host, indexClient?.uid], - queryFn: async () => { - return (await indexClient?.getRawInfo())?.primaryKey; - }, - - enabled: !!currentIndex, - }); - - // use debounced values as dependencies for the search refresh - const debouncedSearchFormValue = useDebounce(searchForm.values, { wait: 450 }); - - const searchDocumentsQuery = useQuery({ - queryKey: ['searchDocuments', host, indexClient?.uid], - refetchInterval: searchAutoRefresh ? 7000 : false, - refetchOnMount: searchAutoRefresh, - refetchOnWindowFocus: searchAutoRefresh, - refetchOnReconnect: searchAutoRefresh, - queryFn: async () => { - const { - q, - limit, - offset, - filter, - sort = '', - showRankingScore, - } = { ...searchForm.values, ...(debouncedSearchFormValue as typeof searchForm.values) }; - // prevent app error from request param invalid - if (searchForm.validate().hasErrors) return emptySearchResult; - - // search sorting expression - const sortExpressions: string[] = - (sort.match(/(([\w\.]+)|(_geoPoint\([\d\.,\s]+\))){1}\:((asc)|(desc))/g) as string[]) || []; - - console.debug('search sorting expression', sort, sortExpressions); - - try { - const data = await indexClient!.search(q, { - limit, - offset, - filter, - sort: sortExpressions.map((v) => v.trim()), - showRankingScore, - }); - // clear error message if results are running normally - setSearchFormError(null); - return data || emptySearchResult; - } catch (err) { - const msg = (err as Error).message; - setSearchFormError(null); - if (msg.match(/filter/i)) { - searchForm.setFieldError('filter', msg); - } else if (msg.match(/sort/i)) { - searchForm.setFieldError('sort', msg); - } else { - setSearchFormError(msg); - } - return emptySearchResult; - } - }, - - enabled: !!currentIndex, - }); - - const onSearchSubmit = useCallback(async () => { - await searchDocumentsQuery.refetch(); - }, [searchDocumentsQuery]); - - // use this to refresh search when typing, DO NOT use useQuery dependencies (will cause unknown rerender error). - useEffect(() => { - searchAutoRefresh && searchDocumentsQuery.refetch(); - // prevent infinite recursion rerender. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedSearchFormValue]); - - return useMemo( - () => ( -
- {/* Search bar */} -
-
- setSearchAutoRefresh(v)} - isFetching={searchDocumentsQuery.isFetching} - searchForm={searchForm} - searchFormError={searchFormError} - onFormSubmit={searchForm.onSubmit(onSearchSubmit)} - submitBtnText={t('common:search')} - /> -
-
-
-
-

{t('search.results.label')}

- setListType(e.target.value)} - > - JSON - {t('search.results.type.table')} - {t('search.results.type.grid')} - -
- -

- {t('search.results.total_hits', { estimatedTotalHits: searchDocumentsQuery.data?.estimatedTotalHits })} -

-

- {t('search.results.processing_time', { processingTimeMs: searchDocumentsQuery.data?.processingTimeMs })} -

-
-
- {/* Doc List */} -
- {searchDocumentsQuery.isFetching ? ( -
- -
- ) : (searchDocumentsQuery.data?.hits.length || 0) > 0 ? ( - ({ - indexId: currentIndex.index.uid, - content: i, - primaryKey: indexPrimaryKeyQuery.data!, - }))} - refetchDocs={searchDocumentsQuery.refetch} - /> - ) : ( -
- -
- )} -
-
- ), - [ - searchDocumentsQuery.isFetching, - searchDocumentsQuery.data?.estimatedTotalHits, - searchDocumentsQuery.data?.processingTimeMs, - searchDocumentsQuery.data?.hits, - searchDocumentsQuery.refetch, - searchForm, - searchFormError, - onSearchSubmit, - t, - listType, - currentIndex, - indexPrimaryKeyQuery.data, - ] - ); + const navigate = useNavigate({ from: Route.fullPath }); + const searchParams = _.defaults(Route.useSearch(), { + listType: "json", + showRankingScore: false, + limit: 20, + }) as Required>; + const { t } = useTranslation("document"); + const [listType, setListType] = useState( + searchParams?.listType || "json", + ); + const [searchAutoRefresh, setSearchAutoRefresh] = useState(false); + const [searchFormError, setSearchFormError] = useState(null); + const client = useMeiliClient(); + const currentIndex = useCurrentIndex(client); + const currentInstance = useCurrentInstance(); + const host = currentInstance?.host; + + const indexClient = useMemo(() => { + return currentIndex ? client.index(currentIndex.index.uid) : undefined; + }, [client, currentIndex]); + + const searchForm = useForm({ + initialValues: { + ..._.omit(searchParams, ["listType"]), + }, + validate: { + limit: (value: number) => { + return value < 500 ? null : t("search.form.limit.validation_error"); + }, + }, + }); + + useEffect(() => { + // update search params when form values changed + navigate({ + search: () => ({ + ...searchForm.values, + listType, + }), + }); + }, [navigate, searchForm.values, listType]); + + const indexPrimaryKeyQuery = useQuery({ + queryKey: ["indexPrimaryKey", host, indexClient?.uid], + queryFn: async () => { + return (await indexClient?.getRawInfo())?.primaryKey; + }, + + enabled: !!currentIndex, + }); + + // use debounced values as dependencies for the search refresh + const debouncedSearchFormValue = useDebounce(searchForm.values, { + wait: 450, + }); + + const searchDocumentsQuery = useQuery({ + queryKey: ["searchDocuments", host, indexClient?.uid], + refetchInterval: searchAutoRefresh ? 7000 : false, + refetchOnMount: searchAutoRefresh, + refetchOnWindowFocus: searchAutoRefresh, + refetchOnReconnect: searchAutoRefresh, + queryFn: async () => { + const { + q, + limit, + offset, + filter, + sort = "", + showRankingScore, + } = { + ...searchForm.values, + ...(debouncedSearchFormValue as typeof searchForm.values), + }; + // prevent app error from request param invalid + if (searchForm.validate().hasErrors) return emptySearchResult; + + // search sorting expression + const sortExpressions: string[] = + (sort.match( + /(([\w\.]+)|(_geoPoint\([\d\.,\s]+\))){1}\:((asc)|(desc))/g, + ) as string[]) || []; + + console.debug("search sorting expression", sort, sortExpressions); + + try { + const data = await indexClient!.search(q, { + limit, + offset, + filter, + sort: sortExpressions.map((v) => v.trim()), + showRankingScore, + }); + // clear error message if results are running normally + setSearchFormError(null); + return data || emptySearchResult; + } catch (err) { + const msg = (err as Error).message; + setSearchFormError(null); + if (msg.match(/filter/i)) { + searchForm.setFieldError("filter", msg); + } else if (msg.match(/sort/i)) { + searchForm.setFieldError("sort", msg); + } else { + setSearchFormError(msg); + } + return emptySearchResult; + } + }, + + enabled: !!currentIndex, + }); + + const onSearchSubmit = useCallback(async () => { + await searchDocumentsQuery.refetch(); + }, [searchDocumentsQuery]); + + // use this to refresh search when typing, DO NOT use useQuery dependencies (will cause unknown rerender error). + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + searchAutoRefresh && searchDocumentsQuery.refetch(); + // prevent infinite recursion rerender. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedSearchFormValue]); + + return useMemo( + () => ( +
+ {/* Search bar */} +
+
+ setSearchAutoRefresh(v)} + isFetching={searchDocumentsQuery.isFetching} + searchForm={searchForm} + searchFormError={searchFormError} + onFormSubmit={searchForm.onSubmit(onSearchSubmit)} + submitBtnText={t("common:search")} + /> +
+
+
+
+

+ {t("search.results.label")} +

+ setListType(e.target.value)} + > + JSON + {t("search.results.type.table")} + {t("search.results.type.grid")} + +
+ +

+ {t("search.results.total_hits", { + estimatedTotalHits: + searchDocumentsQuery.data?.estimatedTotalHits, + })} +

+

+ {t("search.results.processing_time", { + processingTimeMs: searchDocumentsQuery.data?.processingTimeMs, + })} +

+
+
+ {/* Doc List */} +
+ {searchDocumentsQuery.isFetching ? ( +
+ +
+ ) : (searchDocumentsQuery.data?.hits.length || 0) > 0 ? ( + ({ + indexId: currentIndex.index.uid, + content: i, + primaryKey: indexPrimaryKeyQuery.data!, + }))} + refetchDocs={searchDocumentsQuery.refetch} + /> + ) : ( +
+ +
+ )} +
+
+ ), + [ + searchDocumentsQuery.isFetching, + searchDocumentsQuery.data?.estimatedTotalHits, + searchDocumentsQuery.data?.processingTimeMs, + searchDocumentsQuery.data?.hits, + searchDocumentsQuery.refetch, + searchForm, + searchFormError, + onSearchSubmit, + t, + listType, + currentIndex, + indexPrimaryKeyQuery.data, + ], + ); }; -export const Route = createFileRoute('/ins/$insID/_layout/index/$indexUID/_layout/documents/')({ - component: Page, - pendingComponent: LoaderPage, - validateSearch: searchSchema, +export const Route = createFileRoute( + "/ins/$insID/_layout/index/$indexUID/_layout/documents/", +)({ + component: Page, + pendingComponent: LoaderPage, + validateSearch: searchSchema, }); diff --git a/src/routes/ins/$insID/_layout/index/$indexUID/_layout/documents/upload.tsx b/src/routes/ins/$insID/_layout/index/$indexUID/_layout/documents/upload.tsx index 45d5ec0..8da0d45 100644 --- a/src/routes/ins/$insID/_layout/index/$indexUID/_layout/documents/upload.tsx +++ b/src/routes/ins/$insID/_layout/index/$indexUID/_layout/documents/upload.tsx @@ -1,270 +1,316 @@ -import { DragEventHandler, useCallback, useMemo, useRef, useState } from 'react'; -import { useCurrentInstance } from '@/hooks/useCurrentInstance'; -import { toast } from '@/utils/toast'; -import { useForm } from '@mantine/form'; -import { useMutation } from '@tanstack/react-query'; -import { EnqueuedTask } from 'meilisearch'; -import _ from 'lodash'; -import { showTaskErrorNotification, showTaskSubmitNotification } from '@/utils/text'; -import MonacoEditor from '@monaco-editor/react'; -import { IconCopy } from '@tabler/icons-react'; -import { hiddenRequestLoader, showRequestLoader } from '@/utils/loader'; -import { useTranslation } from 'react-i18next'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { useCurrentIndex } from '@/hooks/useCurrentIndex'; -import { createFileRoute } from '@tanstack/react-router'; -import { LoaderPage } from '@/components/loader'; -import { Tag } from '@douyinfe/semi-ui'; -import { Tooltip } from '@arco-design/web-react'; -import { Button } from '@nextui-org/react'; -import { cn } from '@/lib/cn'; +import { LoaderPage } from "@/components/loader"; +import { useCurrentIndex } from "@/hooks/useCurrentIndex"; +import { useCurrentInstance } from "@/hooks/useCurrentInstance"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { cn } from "@/lib/cn"; +import { hiddenRequestLoader, showRequestLoader } from "@/utils/loader"; +import { + showTaskErrorNotification, + showTaskSubmitNotification, +} from "@/utils/text"; +import { toast } from "@/utils/toast"; +import { Tooltip } from "@arco-design/web-react"; +import { Tag } from "@douyinfe/semi-ui"; +import { useForm } from "@mantine/form"; +import MonacoEditor from "@monaco-editor/react"; +import { Button } from "@nextui-org/react"; +import { IconCopy } from "@tabler/icons-react"; +import { useMutation } from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; +import _ from "lodash"; +import type { EnqueuedTask } from "meilisearch"; +import { + type DragEventHandler, + useCallback, + useMemo, + useRef, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; const UploadDoc = () => { - const { t } = useTranslation('upload'); + const { t } = useTranslation("upload"); - const [dragAreaState, setDragAreaState] = useState<'leave' | 'over' | 'uploading'>('leave'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const editorRef = useRef(null); - const currentInstance = useCurrentInstance(); - const client = useMeiliClient(); - const currentIndex = useCurrentIndex(client); + const [dragAreaState, setDragAreaState] = useState< + "leave" | "over" | "uploading" + >("leave"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const editorRef = useRef(null); + const currentInstance = useCurrentInstance(); + const client = useMeiliClient(); + const currentIndex = useCurrentIndex(client); - const host = currentInstance?.host; - const apiKey = currentInstance?.apiKey; + const host = currentInstance?.host; + const apiKey = currentInstance?.apiKey; - const addDocumentsForm = useForm<{ - documents: object[] | File; - }>({ - initialValues: { - documents: [], - }, - validate: { - documents: (value: object[] | File) => { - if (value instanceof File ? value.size > 0 : value.length > 0) { - return null; - } else { - const msg = t('documents_json_array_requirement'); - toast.error(msg); - return msg; - } - }, - }, - }); + const addDocumentsForm = useForm<{ + documents: object[] | File; + }>({ + initialValues: { + documents: [], + }, + validate: { + documents: (value: object[] | File) => { + if (value instanceof File ? value.size > 0 : value.length > 0) { + return null; + } + const msg = t("documents_json_array_requirement"); + toast.error(msg); + return msg; + }, + }, + }); - const addDocumentsMutation = useMutation({ - mutationFn: async (variables: object[] | File) => { - const url = new URL(`/indexes/${currentIndex.index.uid}/documents`, host); - const response = await fetch(url, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - Authorization: apiKey ? `Bearer ${apiKey}` : '', - }, - body: variables instanceof File ? (variables as File) : JSON.stringify(variables), - }); - const task = (await response.json()) as EnqueuedTask; - console.debug('addDocumentsMutation', 'response', task); - return task; - }, + const addDocumentsMutation = useMutation({ + mutationFn: async (variables: object[] | File) => { + const url = new URL(`/indexes/${currentIndex.index.uid}/documents`, host); + const response = await fetch(url, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: apiKey ? `Bearer ${apiKey}` : "", + }, + body: + variables instanceof File + ? (variables as File) + : JSON.stringify(variables), + }); + const task = (await response.json()) as EnqueuedTask; + console.debug("addDocumentsMutation", "response", task); + return task; + }, - onSuccess: (t) => { - showTaskSubmitNotification(t); - addDocumentsForm.reset(); - }, - onError: (error) => { - console.error(error); - showTaskErrorNotification(error); - }, - onMutate() { - if (dragAreaState !== 'uploading') { - setDragAreaState('uploading'); - showRequestLoader(); - } - }, - onSettled() { - if (dragAreaState === 'uploading') { - setDragAreaState('leave'); - } - hiddenRequestLoader(); - }, - }); + onSuccess: (t) => { + showTaskSubmitNotification(t); + addDocumentsForm.reset(); + }, + onError: (error) => { + console.error(error); + showTaskErrorNotification(error); + }, + onMutate() { + if (dragAreaState !== "uploading") { + setDragAreaState("uploading"); + showRequestLoader(); + } + }, + onSettled() { + if (dragAreaState === "uploading") { + setDragAreaState("leave"); + } + hiddenRequestLoader(); + }, + }); - // only read at first render time, so we don't need to use "useCallback" - const pasteClipboardJSON = useCallback(async () => { - // read clipboard and set default val if clipboard value match rules - const [firstContent] = await navigator.clipboard.read(); - if (firstContent.types.includes('text/plain')) { - const json = JSON.parse(await (await firstContent.getType('text/plain')).text()); - const arr = _.isArray(json) ? json : _.isObject(json) ? [json] : []; - console.debug('onAddDocumentsByEditorClick paste clipboard', arr, json); - if (arr.length > 0) { - if (editorRef) { - editorRef.current.setValue(JSON.stringify(arr, null, 2)); - toast.success(t('clipboard_json_pasted')); - } - } - } - }, [t]); + // only read at first render time, so we don't need to use "useCallback" + const pasteClipboardJSON = useCallback(async () => { + // read clipboard and set default val if clipboard value match rules + const [firstContent] = await navigator.clipboard.read(); + if (firstContent.types.includes("text/plain")) { + const json = JSON.parse( + await (await firstContent.getType("text/plain")).text(), + ); + const arr = _.isArray(json) ? json : _.isObject(json) ? [json] : []; + console.debug("onAddDocumentsByEditorClick paste clipboard", arr, json); + if (arr.length > 0) { + if (editorRef) { + editorRef.current.setValue(JSON.stringify(arr, null, 2)); + toast.success(t("clipboard_json_pasted")); + } + } + } + }, [t]); - const onAddDocumentsSubmit = useCallback( - async (val: typeof addDocumentsForm.values) => { - await addDocumentsMutation.mutate(val.documents); - }, - [addDocumentsForm, addDocumentsMutation] - ); + const onAddDocumentsSubmit = useCallback( + async (val: typeof addDocumentsForm.values) => { + await addDocumentsMutation.mutate(val.documents); + }, + [addDocumentsMutation], + ); - const onImportAreaClick = useCallback(async () => { - if (dragAreaState === 'uploading') return; - const fileElem = document.getElementById('documents-json-file-selector'); - if (fileElem) { - fileElem.click(); - await fileElem.addEventListener( - 'change', - async (ev) => { - // @ts-ignore - const jsonFile = ev.target!.files[0] as File; - console.debug('onImportAreaClick', 'file-change', jsonFile); - await onAddDocumentsSubmit({ documents: jsonFile }); - }, - false - ); - } - }, [dragAreaState, onAddDocumentsSubmit]); + const onImportAreaClick = useCallback(async () => { + if (dragAreaState === "uploading") return; + const fileElem = document.getElementById("documents-json-file-selector"); + if (fileElem) { + fileElem.click(); + await fileElem.addEventListener( + "change", + async (ev) => { + // @ts-ignore + const jsonFile = ev.target?.files[0] as File; + console.debug("onImportAreaClick", "file-change", jsonFile); + await onAddDocumentsSubmit({ documents: jsonFile }); + }, + false, + ); + } + }, [dragAreaState, onAddDocumentsSubmit]); - const onImportAreaDrop: DragEventHandler = useCallback( - async (ev) => { - ev.preventDefault(); - if (dragAreaState === 'uploading') return; - const jsonFile = ev.dataTransfer.files[0] as File; - console.debug('onImportAreaDrop', 'drop', jsonFile); - await onAddDocumentsSubmit({ documents: jsonFile }); - }, - [dragAreaState, onAddDocumentsSubmit] - ); + const onImportAreaDrop: DragEventHandler = useCallback( + async (ev) => { + ev.preventDefault(); + if (dragAreaState === "uploading") return; + const jsonFile = ev.dataTransfer.files[0] as File; + console.debug("onImportAreaDrop", "drop", jsonFile); + await onAddDocumentsSubmit({ documents: jsonFile }); + }, + [dragAreaState, onAddDocumentsSubmit], + ); - const onAddDocumentsJsonEditorUpdate = useCallback( - (value: string = '[]') => addDocumentsForm.setFieldValue('documents', JSON.parse(value)), - [addDocumentsForm] - ); + const onAddDocumentsJsonEditorUpdate = useCallback( + (value = "[]") => + addDocumentsForm.setFieldValue("documents", JSON.parse(value)), + [addDocumentsForm], + ); - return useMemo( - () => ( -
( +
-
-
-
- {t('input_by_editor')} - {t('manually_type_in')} - - pasteClipboardJSON()} /> - -
-
-
- { - console.debug('editorDidMount', editor, editor.getValue(), editor.getModel()); - editorRef.current = editor; - }} - > -
- - -
-
-
- {t('import_json_file')} - {t('for_large_documents')} -
-
onImportAreaClick()} - onDrop={onImportAreaDrop} - onDragOver={(ev) => { - ev.preventDefault(); - setDragAreaState('over'); - }} - onDragLeave={(ev) => { - ev.preventDefault(); - setDragAreaState('leave'); - }} - > -
- {dragAreaState === 'over' ? ( - <>{t('release_to_upload_documents')} - ) : dragAreaState === 'uploading' ? ( - <>{t('uploading')}... - ) : ( - JSON' }), - }} - > - )} -
- {t('or')} - - -
-
-
-
- ), - [ - addDocumentsForm, - dragAreaState, - onAddDocumentsJsonEditorUpdate, - onAddDocumentsSubmit, - onImportAreaClick, - onImportAreaDrop, - pasteClipboardJSON, - t, - ] - ); + > +
+
+
+ {t("input_by_editor")} + {t("manually_type_in")} + + pasteClipboardJSON()} + /> + +
+
+
+ { + console.debug( + "editorDidMount", + editor, + editor.getValue(), + editor.getModel(), + ); + editorRef.current = editor; + }} + /> +
+ + +
+
+
+ {t("import_json_file")} + {t("for_large_documents")} +
+
onImportAreaClick()} + onDrop={onImportAreaDrop} + onDragOver={(ev) => { + ev.preventDefault(); + setDragAreaState("over"); + }} + onDragLeave={(ev) => { + ev.preventDefault(); + setDragAreaState("leave"); + }} + > +
+ {dragAreaState === "over" ? ( + <>{t("release_to_upload_documents")} + ) : dragAreaState === "uploading" ? ( + <>{t("uploading")}... + ) : ( + JSON', + }), + }} + /> + )} +
+ {t("or")} + + +
+
+
+
+ ), + [ + addDocumentsForm, + dragAreaState, + onAddDocumentsJsonEditorUpdate, + onAddDocumentsSubmit, + onImportAreaClick, + onImportAreaDrop, + pasteClipboardJSON, + t, + ], + ); }; -export const Route = createFileRoute('/ins/$insID/_layout/index/$indexUID/_layout/documents/upload')({ - component: UploadDoc, - pendingComponent: LoaderPage, +export const Route = createFileRoute( + "/ins/$insID/_layout/index/$indexUID/_layout/documents/upload", +)({ + component: UploadDoc, + pendingComponent: LoaderPage, }); diff --git a/src/routes/ins/$insID/_layout/index/$indexUID/_layout/index.tsx b/src/routes/ins/$insID/_layout/index/$indexUID/_layout/index.tsx index 7e905ab..6a34a54 100644 --- a/src/routes/ins/$insID/_layout/index/$indexUID/_layout/index.tsx +++ b/src/routes/ins/$insID/_layout/index/$indexUID/_layout/index.tsx @@ -1,97 +1,100 @@ -import { useCurrentIndex } from '@/hooks/useCurrentIndex'; -import { useInstanceStats } from '@/hooks/useInstanceStats'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { createFileRoute } from '@tanstack/react-router'; -import { FieldDistribution } from 'meilisearch'; -import { useTranslation } from 'react-i18next'; -import ReactECharts from 'echarts-for-react'; -import { Statistic } from '@arco-design/web-react'; -import { LoaderPage } from '@/components/loader'; +import { LoaderPage } from "@/components/loader"; +import { useCurrentIndex } from "@/hooks/useCurrentIndex"; +import { useInstanceStats } from "@/hooks/useInstanceStats"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { Statistic } from "@arco-design/web-react"; +import { createFileRoute } from "@tanstack/react-router"; +import ReactECharts from "echarts-for-react"; +import type { FieldDistribution } from "meilisearch"; +import { useTranslation } from "react-i18next"; const Page = () => { - const { t } = useTranslation('index'); - const client = useMeiliClient(); - const currentIndex = useCurrentIndex(client); - const stats = useInstanceStats(client); + const { t } = useTranslation("index"); + const client = useMeiliClient(); + const currentIndex = useCurrentIndex(client); + const stats = useInstanceStats(client); - const fieldDistribution: FieldDistribution = stats?.indexes[currentIndex.index.uid].fieldDistribution ?? {}; + const fieldDistribution: FieldDistribution = + stats?.indexes[currentIndex.index.uid].fieldDistribution ?? {}; - return ( -
-
-
- {t('count')}
} - value={stats?.indexes[currentIndex.index.uid].numberOfDocuments} - countUp - loading={!stats?.indexes[currentIndex.index.uid]} - styleValue={{ color: 'green' }} - groupSeparator - /> -
- ({ name: k, value: v })) - .sort((a, b) => b.value - a.value) - .slice(0, 10), - }, - ], - }} - notMerge={true} - lazyUpdate={true} - className="!h-full" - /> - -
- ); + return ( +
+
+
+ {t("count")}
+ } + value={stats?.indexes[currentIndex.index.uid].numberOfDocuments} + countUp + loading={!stats?.indexes[currentIndex.index.uid]} + styleValue={{ color: "green" }} + groupSeparator + /> +
+ + `${params.name}(${params.value})`, + }, + labelLine: { + show: false, + }, + }, + }, + data: Object.entries(fieldDistribution) + .map(([k, v]) => ({ name: k, value: v })) + .sort((a, b) => b.value - a.value) + .slice(0, 10), + }, + ], + }} + notMerge={true} + lazyUpdate={true} + className="!h-full" + /> + +
+ ); }; -export const Route = createFileRoute('/ins/$insID/_layout/index/$indexUID/_layout/')({ - component: Page, - pendingComponent: LoaderPage, +export const Route = createFileRoute( + "/ins/$insID/_layout/index/$indexUID/_layout/", +)({ + component: Page, + pendingComponent: LoaderPage, }); diff --git a/src/routes/ins/$insID/_layout/index/$indexUID/_layout/setting.tsx b/src/routes/ins/$insID/_layout/index/$indexUID/_layout/setting.tsx index dce91bb..090932a 100644 --- a/src/routes/ins/$insID/_layout/index/$indexUID/_layout/setting.tsx +++ b/src/routes/ins/$insID/_layout/index/$indexUID/_layout/setting.tsx @@ -1,22 +1,24 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { IndexConfigEditor } from '@/components/indexConfigEditor'; -import { Loader } from '@/components/loader'; -import { DangerZone } from '@/components/Settings/dangerZone'; +import { DangerZone } from "@/components/Settings/dangerZone"; +import { IndexConfigEditor } from "@/components/indexConfigEditor"; +import { Loader } from "@/components/loader"; +import { createFileRoute } from "@tanstack/react-router"; const Page = () => { - return ( -
-
-
- - window.location.reload()} /> -
-
-
- ); + return ( +
+
+
+ + window.location.reload()} /> +
+
+
+ ); }; -export const Route = createFileRoute('/ins/$insID/_layout/index/$indexUID/_layout/setting')({ - component: Page, - pendingComponent: Loader, +export const Route = createFileRoute( + "/ins/$insID/_layout/index/$indexUID/_layout/setting", +)({ + component: Page, + pendingComponent: Loader, }); diff --git a/src/routes/ins/$insID/_layout/keys.tsx b/src/routes/ins/$insID/_layout/keys.tsx index ae2d688..35eaf45 100644 --- a/src/routes/ins/$insID/_layout/keys.tsx +++ b/src/routes/ins/$insID/_layout/keys.tsx @@ -1,248 +1,290 @@ -import { KeyForm } from '@/components/keyForm'; -import { LoaderPage } from '@/components/loader'; -import { TimeAgo } from '@/components/timeago'; -import { useCurrentInstance } from '@/hooks/useCurrentInstance'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { hiddenRequestLoader, showRequestLoader } from '@/utils/loader'; -import { getTimeText } from '@/utils/text'; -import { Modal, TagGroup, Table, Descriptions, Pagination } from '@douyinfe/semi-ui'; -import { ColumnProps } from '@douyinfe/semi-ui/lib/es/table'; -import { ActionIcon, CopyButton } from '@mantine/core'; -import { Button, Tooltip } from '@nextui-org/react'; -import { IconCheck, IconCopy } from '@tabler/icons-react'; -import { useQuery } from '@tanstack/react-query'; -import { createFileRoute } from '@tanstack/react-router'; -import { Key } from 'meilisearch'; -import { useCallback, useEffect, useReducer } from 'react'; -import { useTranslation } from 'react-i18next'; +import { KeyForm } from "@/components/keyForm"; +import { LoaderPage } from "@/components/loader"; +import { TimeAgo } from "@/components/timeago"; +import { useCurrentInstance } from "@/hooks/useCurrentInstance"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { hiddenRequestLoader, showRequestLoader } from "@/utils/loader"; +import { getTimeText } from "@/utils/text"; +import { + Descriptions, + Modal, + Pagination, + Table, + TagGroup, +} from "@douyinfe/semi-ui"; +import type { ColumnProps } from "@douyinfe/semi-ui/lib/es/table"; +import { ActionIcon, CopyButton } from "@mantine/core"; +import { Button, Tooltip } from "@nextui-org/react"; +import { IconCheck, IconCopy } from "@tabler/icons-react"; +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; +import type { Key } from "meilisearch"; +import { useCallback, useEffect, useReducer } from "react"; +import { useTranslation } from "react-i18next"; -type KeyFormState = { visible: boolean; editing?: Key; type: 'create' | 'edit' }; +type KeyFormState = { + visible: boolean; + editing?: Key; + type: "create" | "edit"; +}; type PaginationState = { pageSize: number; currentPage: number }; const Page = () => { - const { t } = useTranslation('key'); - const client = useMeiliClient(); - const currentInstance = useCurrentInstance(); + const { t } = useTranslation("key"); + const client = useMeiliClient(); + const currentInstance = useCurrentInstance(); - const host = currentInstance?.host; + const host = currentInstance?.host; - const [formState, updateFormState] = useReducer( - (prev: KeyFormState, next: Partial) => { - return { ...prev, ...next }; - }, - { visible: false, type: 'create' } as KeyFormState - ); + const [formState, updateFormState] = useReducer( + (prev: KeyFormState, next: Partial) => { + return { ...prev, ...next }; + }, + { visible: false, type: "create" } as KeyFormState, + ); - const [pagination, updatePagination] = useReducer( - (prev: PaginationState, next: Partial) => { - return { ...prev, ...next }; - }, - { pageSize: 20, currentPage: 1 } as PaginationState - ); + const [pagination, updatePagination] = useReducer( + (prev: PaginationState, next: Partial) => { + return { ...prev, ...next }; + }, + { pageSize: 20, currentPage: 1 } as PaginationState, + ); - const keysQuery = useQuery({ - queryKey: ['keys', host, pagination], - queryFn: async () => { - showRequestLoader(); - console.debug(client.config, pagination); - return await client.getKeys({ - limit: pagination.pageSize, - offset: (pagination.currentPage - 1) * pagination.pageSize, - }); - }, - }); + const keysQuery = useQuery({ + queryKey: ["keys", host, pagination], + queryFn: async () => { + showRequestLoader(); + console.debug(client.config, pagination); + return await client.getKeys({ + limit: pagination.pageSize, + offset: (pagination.currentPage - 1) * pagination.pageSize, + }); + }, + }); - useEffect(() => { - if (keysQuery.isError) { - console.warn('get meilisearch keys error', keysQuery.error); - } - if (!keysQuery.isFetching) { - hiddenRequestLoader(); - } - }, [keysQuery.error, keysQuery.isError, keysQuery.isFetching]); + useEffect(() => { + if (keysQuery.isError) { + console.warn("get meilisearch keys error", keysQuery.error); + } + if (!keysQuery.isFetching) { + hiddenRequestLoader(); + } + }, [keysQuery.error, keysQuery.isError, keysQuery.isFetching]); - const refreshKeys = useCallback(() => { - // wait for create key task complete - setTimeout(() => { - keysQuery.refetch().then(); - }, 1000); - }, [keysQuery]); + const refreshKeys = useCallback(() => { + // wait for create key task complete + setTimeout(() => { + keysQuery.refetch().then(); + }, 1000); + }, [keysQuery]); - const onClickDelKey = useCallback( - (key: Key) => { - Modal.confirm({ - title: t('delete.title'), - centered: true, - content:

{t('delete.tip')}

, - onOk: async () => { - client.deleteKey(key.uid).finally(() => { - refreshKeys(); - }); - }, - okText: t('confirm'), - cancelText: t('cancel'), - }); - }, - [client, refreshKeys, t] - ); + const onClickDelKey = useCallback( + (key: Key) => { + Modal.confirm({ + title: t("delete.title"), + centered: true, + content:

{t("delete.tip")}

, + onOk: async () => { + client.deleteKey(key.uid).finally(() => { + refreshKeys(); + }); + }, + okText: t("confirm"), + cancelText: t("cancel"), + }); + }, + [client, refreshKeys, t], + ); - const columns: ColumnProps[] = [ - { - title: t('name'), - dataIndex: 'name', - width: 200, - }, - { - title: t('props.key'), - dataIndex: 'key', - width: 120, - render: (text) => { - return ( -
- {/* desensitization */} -

{text.replace(/^(.{8})(?:\S+)(.{6})$/, '$1****$2')}

- - {({ copied, copy }) => ( - - - {copied ? : } - - - )} - -
- ); - }, - }, - { - title: t('props.indexes'), - dataIndex: 'indexes', - width: 170, - render: (_, item) => { - return ({ children: i }))} showPopover />; - }, - }, - { - title: t('props.actions'), - dataIndex: 'actions', - width: 180, - render: (_, item) => { - return ({ children: i }))} showPopover />; - }, - }, - { - title: t('expired_at'), - dataIndex: 'expiresAt', - width: 200, - render: (_, item) => { - return getTimeText(item.expiresAt, { defaultText: t('forever') }); - }, - }, - { - title: t('actions'), - dataIndex: 'operate', - render: (_, record) => ( -
- - - -
- ), - }, - ]; + const columns: ColumnProps[] = [ + { + title: t("name"), + dataIndex: "name", + width: 200, + }, + { + title: t("props.key"), + dataIndex: "key", + width: 120, + render: (text) => { + return ( +
+ {/* desensitization */} +

{text.replace(/^(.{8})(?:\S+)(.{6})$/, "$1****$2")}

+ + {({ copied, copy }) => ( + + + {copied ? : } + + + )} + +
+ ); + }, + }, + { + title: t("props.indexes"), + dataIndex: "indexes", + width: 170, + render: (_, item) => { + return ( + ({ children: i }))} + showPopover + /> + ); + }, + }, + { + title: t("props.actions"), + dataIndex: "actions", + width: 180, + render: (_, item) => { + return ( + ({ children: i }))} + showPopover + /> + ); + }, + }, + { + title: t("expired_at"), + dataIndex: "expiresAt", + width: 200, + render: (_, item) => { + return getTimeText(item.expiresAt, { defaultText: t("forever") }); + }, + }, + { + title: t("actions"), + dataIndex: "operate", + render: (_, record) => ( +
+ + + +
+ ), + }, + ]; - return ( -
-
-
- - { - updatePagination({ currentPage: curr }); - keysQuery.refetch(); - }} - /> -
+ return ( +
+
+
+ + { + updatePagination({ currentPage: curr }); + keysQuery.refetch(); + }} + /> +
-
+
- { - updateFormState({ visible: false }); - }} - closable={false} - footer={false} - > - { - refreshKeys(); - updateFormState({ visible: false }); - }} - data={formState.editing ? { ...formState.editing, name: formState.editing.name ?? '' } : undefined} - type={formState.type} - /> - - - - ); + { + updateFormState({ visible: false }); + }} + closable={false} + footer={false} + > + { + refreshKeys(); + updateFormState({ visible: false }); + }} + data={ + formState.editing + ? { ...formState.editing, name: formState.editing.name ?? "" } + : undefined + } + type={formState.type} + /> + + + + ); }; -export const Route = createFileRoute('/ins/$insID/_layout/keys')({ - component: Page, - pendingComponent: LoaderPage, +export const Route = createFileRoute("/ins/$insID/_layout/keys")({ + component: Page, + pendingComponent: LoaderPage, }); diff --git a/src/routes/ins/$insID/_layout/tasks.tsx b/src/routes/ins/$insID/_layout/tasks.tsx index ad54c32..0180483 100644 --- a/src/routes/ins/$insID/_layout/tasks.tsx +++ b/src/routes/ins/$insID/_layout/tasks.tsx @@ -1,250 +1,276 @@ -import { LoaderPage } from '@/components/loader'; -import { TimeAgo } from '@/components/timeago'; -import { useCurrentInstance } from '@/hooks/useCurrentInstance'; -import { useMeiliClient } from '@/hooks/useMeiliClient'; -import { hiddenRequestLoader, showRequestLoader } from '@/utils/loader'; -import { getDuration } from '@/utils/text'; -import { Modal, Select, Table, TagInput } from '@douyinfe/semi-ui'; -import { ColumnProps } from '@douyinfe/semi-ui/lib/es/table'; -import { Button } from '@nextui-org/react'; -import { useInfiniteQuery } from '@tanstack/react-query'; -import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'; -import _ from 'lodash'; -import { TasksQuery, Task, TaskTypes, TaskStatus } from 'meilisearch'; -import { useEffect, useMemo, useReducer } from 'react'; -import { useTranslation } from 'react-i18next'; -import ReactJson from 'react-json-view'; -import { z } from 'zod'; +import { LoaderPage } from "@/components/loader"; +import { TimeAgo } from "@/components/timeago"; +import { useCurrentInstance } from "@/hooks/useCurrentInstance"; +import { useMeiliClient } from "@/hooks/useMeiliClient"; +import { hiddenRequestLoader, showRequestLoader } from "@/utils/loader"; +import { getDuration } from "@/utils/text"; +import { Modal, Select, Table, TagInput } from "@douyinfe/semi-ui"; +import type { ColumnProps } from "@douyinfe/semi-ui/lib/es/table"; +import { Button } from "@nextui-org/react"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { Link, createFileRoute, useNavigate } from "@tanstack/react-router"; +import _ from "lodash"; +import type { Task, TaskStatus, TaskTypes, TasksQuery } from "meilisearch"; +import { useEffect, useMemo, useReducer } from "react"; +import { useTranslation } from "react-i18next"; +import ReactJson from "react-json-view"; +import { z } from "zod"; const searchSchema = z - .object({ - indexUids: z.string().array().optional(), - limit: z.number().positive().optional().default(20), - statuses: z.string().array().optional(), - types: z.string().array().optional(), - }) - .optional(); + .object({ + indexUids: z.string().array().optional(), + limit: z.number().positive().optional().default(20), + statuses: z.string().array().optional(), + types: z.string().array().optional(), + }) + .partial(); -type State = Pick & Required>; +type State = Pick & + Required>; const Page = () => { - const navigate = useNavigate({ from: Route.fullPath }); - const searchParams = Route.useSearch(); - const { t } = useTranslation('task'); - const client = useMeiliClient(); - const currentInstance = useCurrentInstance(); + const navigate = useNavigate({ from: Route.fullPath }); + const searchParams = Route.useSearch(); + const { t } = useTranslation("task"); + const client = useMeiliClient(); + const currentInstance = useCurrentInstance(); - const host = currentInstance?.host; + const host = currentInstance?.host; - const [state, updateState] = useReducer( - (prev: State, next: Partial) => { - return { ...prev, ...next }; - }, - { ...searchParams } as State - ); + const [state, updateState] = useReducer( + (prev: State, next: Partial) => { + return { ...prev, ...next }; + }, + { ...searchParams } as State, + ); - useEffect(() => { - // update search params when state changed - navigate({ - search: () => ({ - ...state, - }), - }); - }, [navigate, state]); + useEffect(() => { + // update search params when state changed + navigate({ + search: () => ({ + ...state, + }), + }); + }, [navigate, state]); - // @ts-expect-error - const query = useInfiniteQuery({ - queryKey: ['tasks', host, state], - queryFn: async ({ pageParam }: { pageParam: { limit: number; from?: number } }) => { - showRequestLoader(); - console.debug('getTasks', client.config, state); - return await client.getTasks({ - ..._.omitBy(state, _.isEmpty), - from: pageParam.from, - limit: pageParam.limit, - }); - }, - initialPageParam: { - limit: state.limit, - }, - getNextPageParam: (lastPage) => { - return { - limit: lastPage.limit, - from: lastPage.next, - }; - }, - }); + // @ts-expect-error + const query = useInfiniteQuery({ + queryKey: ["tasks", host, state], + queryFn: async ({ + pageParam, + }: { pageParam: { limit: number; from?: number } }) => { + showRequestLoader(); + console.debug("getTasks", client.config, state); + return await client.getTasks({ + ..._.omitBy(state, _.isEmpty), + from: pageParam.from, + limit: pageParam.limit, + }); + }, + initialPageParam: { + limit: state.limit, + }, + getNextPageParam: (lastPage) => { + return { + limit: lastPage.limit, + from: lastPage.next, + }; + }, + }); - useEffect(() => { - if (query.isError) { - console.warn('get meilisearch tasks error', query.error); - } - if (!query.isFetching) { - hiddenRequestLoader(); - } - }, [query.error, query.isError, query.isFetching]); + useEffect(() => { + if (query.isError) { + console.warn("get meilisearch tasks error", query.error); + } + if (!query.isFetching) { + hiddenRequestLoader(); + } + }, [query.error, query.isError, query.isFetching]); - const list = useMemo(() => { - return query.data?.pages.map((page) => page.results).reduce((acc, cur) => acc.concat(cur), []) || []; - }, [query.data?.pages]); + const list = useMemo(() => { + return ( + query.data?.pages + .map((page) => page.results) + .reduce((acc, cur) => acc.concat(cur), []) || [] + ); + }, [query.data?.pages]); - const columns: ColumnProps[] = useMemo( - () => [ - { - title: 'UID', - dataIndex: 'uid', - width: 100, - }, - { - title: t('indexes'), - dataIndex: 'indexUid', - render: (val) => (val ? {val} : '-'), - }, - { - title: t('common:type'), - dataIndex: 'type', - render: (_) => t(`type.${_}`), - }, - { - title: t('common:status'), - dataIndex: 'status', - width: 120, - render: (_) => t(`status.${_}`), - }, - { - title: t('duration'), - dataIndex: 'duration', - width: 200, - render: (_, item) => { - if (!item.duration) { - return '-'; - } + const columns: ColumnProps[] = useMemo( + () => [ + { + title: "UID", + dataIndex: "uid", + width: 100, + }, + { + title: t("indexes"), + dataIndex: "indexUid", + render: (val) => + val ? ( + {val} + ) : ( + "-" + ), + }, + { + title: t("common:type"), + dataIndex: "type", + render: (_) => t(`type.${_}`), + }, + { + title: t("common:status"), + dataIndex: "status", + width: 120, + render: (_) => t(`status.${_}`), + }, + { + title: t("duration"), + dataIndex: "duration", + width: 200, + render: (_, item) => { + if (!item.duration) { + return "-"; + } - return `${getDuration(item.duration, 'millisecond')}ms`; - }, - }, - { - title: t('enqueued_at'), - dataIndex: 'enqueuedAt', - width: 220, - render: (_, item) => { - return ; - }, - }, - { - title: t('started_at'), - dataIndex: 'startedAt', - width: 220, - render: (_, item) => { - return ; - }, - }, - { - title: t('finished_at'), - dataIndex: 'finishedAt', - width: 220, - render: (_, item) => { - return ; - }, - }, - { - title: t('actions'), - fixed: 'right', - width: 150, - render: (_, record) => ( -
- -
- ), - }, - ], - [currentInstance.id, t] - ); + return `${getDuration(item.duration, "millisecond")}ms`; + }, + }, + { + title: t("enqueued_at"), + dataIndex: "enqueuedAt", + width: 220, + render: (_, item) => { + return ; + }, + }, + { + title: t("started_at"), + dataIndex: "startedAt", + width: 220, + render: (_, item) => { + return ; + }, + }, + { + title: t("finished_at"), + dataIndex: "finishedAt", + width: 220, + render: (_, item) => { + return ; + }, + }, + { + title: t("actions"), + fixed: "right", + width: 150, + render: (_, record) => ( +
+ +
+ ), + }, + ], + [currentInstance.id, t], + ); - return useMemo( - () => ( -
-
-
- { - updateState({ indexUids: value }); - }} - /> - ).map(([k, v]) => ({ - value: k, - label: v, - }))} - multiple - value={state.statuses} - onChange={(value) => { - updateState({ statuses: (value as TaskStatus[]) || undefined }); - }} - /> -
+ return useMemo( + () => ( +
+
+
+ { + updateState({ indexUids: value }); + }} + /> + , + ).map(([k, v]) => ({ + value: k, + label: v, + }))} + multiple + value={state.statuses} + onChange={(value) => { + updateState({ statuses: (value as TaskStatus[]) || undefined }); + }} + /> +
-
{ - const { scrollTop, clientHeight, scrollHeight } = e.target; - if (Math.abs(scrollHeight - (scrollTop + clientHeight)) <= 1) { - query.fetchNextPage(); - } - }} - > -
- - - - ), - [t, state.indexUids, state.types, state.statuses, columns, list, query] - ); +
{ + // @ts-expect-error + const { scrollTop, clientHeight, scrollHeight } = e.target; + if (Math.abs(scrollHeight - (scrollTop + clientHeight)) <= 1) { + query.fetchNextPage(); + } + }} + > +
+ + + + ), + [t, state.indexUids, state.types, state.statuses, columns, list, query], + ); }; -export const Route = createFileRoute('/ins/$insID/_layout/tasks')({ - component: Page, - pendingComponent: LoaderPage, - validateSearch: searchSchema, +export const Route = createFileRoute("/ins/$insID/_layout/tasks")({ + component: Page, + pendingComponent: LoaderPage, + validateSearch: searchSchema, }); diff --git a/src/routes/warning.tsx b/src/routes/warning.tsx index e2b06c5..52e9a74 100644 --- a/src/routes/warning.tsx +++ b/src/routes/warning.tsx @@ -1,38 +1,51 @@ -import { Button } from '@mantine/core'; -import { createFileRoute, useRouter } from '@tanstack/react-router'; -import { Footer } from '@/components/Footer'; -import { Logo } from '@/components/Logo'; -import { useAppStore } from '@/store'; -import { useTranslation } from 'react-i18next'; +import { Footer } from "@/components/Footer"; +import { Logo } from "@/components/Logo"; +import { useAppStore } from "@/store"; +import { Button } from "@mantine/core"; +import { createFileRoute, useRouter } from "@tanstack/react-router"; +import { useTranslation } from "react-i18next"; function Warning() { - const { history } = useRouter(); - const { t } = useTranslation('sys'); - const warningPageData = useAppStore((state) => state.warningPageData); - return ( -
-
- -

{t('warning')}

-
- {warningPageData?.prompt && ( -

- {warningPageData.prompt} -

- )} -
- - -
-
-
- ); + const { history } = useRouter(); + const { t } = useTranslation("sys"); + const warningPageData = useAppStore((state) => state.warningPageData); + return ( +
+
+ +

+ {t("warning")} +

+
+ {warningPageData?.prompt && ( +

+ {warningPageData.prompt} +

+ )} +
+ + +
+
+
+ ); } -export const Route = createFileRoute('/warning')({ - component: Warning, +export const Route = createFileRoute("/warning")({ + component: Warning, }); diff --git a/src/store/index.ts b/src/store/index.ts index 0237ef9..baaf352 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,84 +1,89 @@ -import { create } from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; -import { produce } from 'immer'; -import _ from 'lodash'; +import { produce } from "immer"; +import _ from "lodash"; +import { create } from "zustand"; +import { devtools, persist } from "zustand/middleware"; export interface WarningPageData { - prompt: string; + prompt: string; } export interface Instance { - id: number; - name: string; - host: string; - apiKey?: string; - updatedTime?: Date; + id: number; + name: string; + host: string; + apiKey?: string; + updatedTime?: Date; } -export const defaultInstance: Omit = { - name: 'default', - host: 'http://127.0.0.1:7700', - apiKey: undefined, +export const defaultInstance: Omit = { + name: "default", + host: "http://127.0.0.1:7700", + apiKey: undefined, }; interface State { - warningPageData?: WarningPageData; - instances: Instance[]; - setWarningPageData: (data?: WarningPageData) => void; - addInstance: (cfg: Omit) => void; - editInstance: (id: number, cfg: Omit) => void; - removeInstance: (id: number) => void; - removeAllInstances: () => void; + warningPageData?: WarningPageData; + instances: Instance[]; + setWarningPageData: (data?: WarningPageData) => void; + addInstance: (cfg: Omit) => void; + editInstance: (id: number, cfg: Omit) => void; + removeInstance: (id: number) => void; + removeAllInstances: () => void; } export const useAppStore = create()( - devtools( - persist( - (set, get) => ({ - instances: [], - setWarningPageData: (data?: WarningPageData) => - set( - produce((state: State) => { - state.warningPageData = data; - }) - ), - addInstance: (cfg) => - set( - produce((state: State) => { - state.instances.push({ - ...cfg, - updatedTime: new Date(), - // calculate next ins id - // start from 1, id 0 is reserved for singleton mode - id: (_.maxBy(get().instances, 'id')?.id || 0) + 1, - }); - }) - ), - editInstance: (id, cfg) => - set( - produce((state: State) => { - const index = state.instances.findIndex((i) => i.id === id); - if (index !== -1) state.instances[index] = { ...cfg, id, updatedTime: new Date() }; - }) - ), - removeInstance: (id) => - set( - produce((state: State) => { - const index = state.instances.findIndex((i) => i.id === id); - if (index !== -1) state.instances.splice(index, 1); - }) - ), - removeAllInstances: () => - set( - produce((state: State) => { - state.instances = []; - }) - ) - }), - { - name: 'meilisearch-ui-store', - version: 5, - } - ) - ) + devtools( + persist( + (set, get) => ({ + instances: [], + setWarningPageData: (data?: WarningPageData) => + set( + produce((state: State) => { + state.warningPageData = data; + }), + ), + addInstance: (cfg) => + set( + produce((state: State) => { + state.instances.push({ + ...cfg, + updatedTime: new Date(), + // calculate next ins id + // start from 1, id 0 is reserved for singleton mode + id: (_.maxBy(get().instances, "id")?.id || 0) + 1, + }); + }), + ), + editInstance: (id, cfg) => + set( + produce((state: State) => { + const index = state.instances.findIndex((i) => i.id === id); + if (index !== -1) + state.instances[index] = { + ...cfg, + id, + updatedTime: new Date(), + }; + }), + ), + removeInstance: (id) => + set( + produce((state: State) => { + const index = state.instances.findIndex((i) => i.id === id); + if (index !== -1) state.instances.splice(index, 1); + }), + ), + removeAllInstances: () => + set( + produce((state: State) => { + state.instances = []; + }), + ), + }), + { + name: "meilisearch-ui-store", + version: 5, + }, + ), + ), ); diff --git a/src/style/global.css b/src/style/global.css index cc41590..bf56d68 100644 --- a/src/style/global.css +++ b/src/style/global.css @@ -3,104 +3,105 @@ @tailwind utilities; @layer base { - :root { - font-size: 17px; - font-synthesis: none; - @apply font-mono selection:bg-primary-200 selection:bg-opacity-50; - - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; - - scroll-behavior: smooth; - } - - /* make body fill full page */ - html { - @apply w-screen h-screen; - } - - body { - @apply h-full w-full m-0 p-0 border-0 align-baseline !important; - @apply transition-colors duration-300 ease-in-out; - min-width: 320px; - min-height: 100vh; - } + :root { + font-size: 17px; + font-synthesis: none; + @apply font-mono selection:bg-primary-200 selection:bg-opacity-50; + + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + + scroll-behavior: smooth; + } + + /* make body fill full page */ + html { + @apply w-screen h-screen; + } + + body { + @apply h-full w-full m-0 p-0 border-0 align-baseline !important; + @apply transition-colors duration-300 ease-in-out; + min-width: 320px; + min-height: 100vh; + } } @layer components { - .full-page { - @apply flex flex-col w-screen h-screen overflow-hidden !important; - } - - .fill { - @apply w-full h-full; - } - - .fullscreen { - @apply w-screen h-screen; - } - - a { - @apply hover:underline hover:cursor-pointer; - font-weight: 500; - text-decoration: inherit; - } - - .logo { - will-change: filter; - } - - .logo:hover { - filter: drop-shadow(0 0 2rem #646cffaa); - } + .full-page { + @apply flex flex-col w-screen h-screen overflow-hidden !important; + } + + .fill { + @apply w-full h-full; + } + + .fullscreen { + @apply w-screen h-screen; + } + + a { + @apply hover:underline hover:cursor-pointer; + font-weight: 500; + text-decoration: inherit; + } + + .logo { + will-change: filter; + } + + .logo:hover { + filter: drop-shadow(0 0 2rem #646cffaa); + } } @layer utilities { - .remove-scroll-bar::-webkit-scrollbar { - /* Hide scrollbar for Chrome, Safari and Opera */ - display: none !important; - } - - .remove-scroll-bar::-webkit-scrollbar { - -ms-overflow-style: none !important; /* IE and Edge */ - scrollbar-width: none !important; /* Firefox */ - } - - .bg-night { - @apply bg-secondary-800; - } - - .bg-mount { - @apply bg-secondary-800; - @apply bg-fixed bg-cover; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1600 900'%3E%3Cpolygon fill='%23ff5caa' points='957 450 539 900 1396 900'/%3E%3Cpolygon fill='%23ff4e62' points='957 450 872.9 900 1396 900'/%3E%3Cpolygon fill='%23f55cb8' points='-60 900 398 662 816 900'/%3E%3Cpolygon fill='%23f9377d' points='337 900 398 662 816 900'/%3E%3Cpolygon fill='%23e75fc5' points='1203 546 1552 900 876 900'/%3E%3Cpolygon fill='%23e82c9a' points='1203 546 1552 900 1162 900'/%3E%3Cpolygon fill='%23d763d1' points='641 695 886 900 367 900'/%3E%3Cpolygon fill='%23ca34b6' points='587 900 641 695 886 900'/%3E%3Cpolygon fill='%23c468dd' points='1710 900 1401 632 1096 900'/%3E%3Cpolygon fill='%239d45d0' points='1710 900 1401 632 1365 900'/%3E%3Cpolygon fill='%23ad6de7' points='1210 900 971 687 725 900'/%3E%3Cpolygon fill='%234f55e3' points='943 900 1210 900 971 687'/%3E%3C/svg%3E"); - } - - /* rainbow rotate animated ring border */ - .rainbow-ring-rotate { - @apply z-0 overflow-hidden relative; - @apply bg-gradient-to-br from-primary-400 via-primary-500 to-primary-600 !important; - } - - @keyframes rainbow-ring-rotate { - 100% { - transform: rotate(1turn); - } - } - - .rainbow-ring-rotate::before { - content: ''; - width: 200%; - height: 200%; - @apply absolute -z-20 -left-1/2 -top-1/2 bg-primary-100 bg-no-repeat; - background-image: linear-gradient(#3f5efb, #fc466b), linear-gradient(#833ab4, #fd1d1d, #fcb045); - animation: rainbow-ring-rotate 4s linear infinite; - } - - .rainbow-ring-rotate::after { - content: ''; - @apply -z-10 absolute inset-1 bg-white rounded; - } + .remove-scroll-bar::-webkit-scrollbar { + /* Hide scrollbar for Chrome, Safari and Opera */ + display: none !important; + } + + .remove-scroll-bar::-webkit-scrollbar { + -ms-overflow-style: none !important; /* IE and Edge */ + scrollbar-width: none !important; /* Firefox */ + } + + .bg-night { + @apply bg-secondary-800; + } + + .bg-mount { + @apply bg-secondary-800; + @apply bg-fixed bg-cover; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1600 900'%3E%3Cpolygon fill='%23ff5caa' points='957 450 539 900 1396 900'/%3E%3Cpolygon fill='%23ff4e62' points='957 450 872.9 900 1396 900'/%3E%3Cpolygon fill='%23f55cb8' points='-60 900 398 662 816 900'/%3E%3Cpolygon fill='%23f9377d' points='337 900 398 662 816 900'/%3E%3Cpolygon fill='%23e75fc5' points='1203 546 1552 900 876 900'/%3E%3Cpolygon fill='%23e82c9a' points='1203 546 1552 900 1162 900'/%3E%3Cpolygon fill='%23d763d1' points='641 695 886 900 367 900'/%3E%3Cpolygon fill='%23ca34b6' points='587 900 641 695 886 900'/%3E%3Cpolygon fill='%23c468dd' points='1710 900 1401 632 1096 900'/%3E%3Cpolygon fill='%239d45d0' points='1710 900 1401 632 1365 900'/%3E%3Cpolygon fill='%23ad6de7' points='1210 900 971 687 725 900'/%3E%3Cpolygon fill='%234f55e3' points='943 900 1210 900 971 687'/%3E%3C/svg%3E"); + } + + /* rainbow rotate animated ring border */ + .rainbow-ring-rotate { + @apply z-0 overflow-hidden relative; + @apply bg-gradient-to-br from-primary-400 via-primary-500 to-primary-600 !important; + } + + @keyframes rainbow-ring-rotate { + 100% { + transform: rotate(1turn); + } + } + + .rainbow-ring-rotate::before { + content: ""; + width: 200%; + height: 200%; + @apply absolute -z-20 -left-1/2 -top-1/2 bg-primary-100 bg-no-repeat; + background-image: linear-gradient(#3f5efb, #fc466b), + linear-gradient(#833ab4, #fd1d1d, #fcb045); + animation: rainbow-ring-rotate 4s linear infinite; + } + + .rainbow-ring-rotate::after { + content: ""; + @apply -z-10 absolute inset-1 bg-white rounded; + } } diff --git a/src/style/loader.module.css b/src/style/loader.module.css index 3e7f9b2..2442049 100644 --- a/src/style/loader.module.css +++ b/src/style/loader.module.css @@ -1,16 +1,15 @@ /* HTML:
*/ .loader { - width: 2.6em; - aspect-ratio: 1; - border-radius: 50%; - background: - radial-gradient(farthest-side, currentColor 94%, #0000) top/8px 8px no-repeat, - conic-gradient(#0000 30%, currentColor); - -webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 8px), #000 0); - animation: l13 1s infinite linear; + width: 2.6em; + aspect-ratio: 1; + border-radius: 50%; + background: radial-gradient(farthest-side, currentColor 94%, #0000) top / 8px + 8px no-repeat, conic-gradient(#0000 30%, currentColor); + -webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 8px), #000 0); + animation: l13 1s infinite linear; } @keyframes l13 { - 100% { - transform: rotate(1turn); - } + 100% { + transform: rotate(1turn); + } } diff --git a/src/style/theme.json b/src/style/theme.json index 4866b5b..557e35c 100644 --- a/src/style/theme.json +++ b/src/style/theme.json @@ -1,30 +1,30 @@ { - "colors": { - "primary": { - "50": "#ffe2f1", - "100": "#ffb3d0", - "200": "#fd82af", - "300": "#fb518e", - "400": "#f8206e", - "500": "#df0755", - "600": "#ae0142", - "700": "#7e002f", - "800": "#4d001c", - "900": "#20000a", - "DEFAULT": "#f9377d" - }, - "secondary": { - "50": "#f2e4ff", - "100": "#d4b3ff", - "200": "#b881fd", - "300": "#9b4efc", - "400": "#7e1ffb", - "500": "#6608e2", - "600": "#4f04b0", - "700": "#38027e", - "800": "#22004d", - "900": "#0c001d", - "DEFAULT": "#21004b" - } - } + "colors": { + "primary": { + "50": "#ffe2f1", + "100": "#ffb3d0", + "200": "#fd82af", + "300": "#fb518e", + "400": "#f8206e", + "500": "#df0755", + "600": "#ae0142", + "700": "#7e002f", + "800": "#4d001c", + "900": "#20000a", + "DEFAULT": "#f9377d" + }, + "secondary": { + "50": "#f2e4ff", + "100": "#d4b3ff", + "200": "#b881fd", + "300": "#9b4efc", + "400": "#7e1ffb", + "500": "#6608e2", + "600": "#4f04b0", + "700": "#38027e", + "800": "#22004d", + "900": "#0c001d", + "DEFAULT": "#21004b" + } + } } diff --git a/src/utils/array.ts b/src/utils/array.ts index 93d6585..4085faf 100644 --- a/src/utils/array.ts +++ b/src/utils/array.ts @@ -1,17 +1,21 @@ -export const arrayMove = (arr: T[], old_index: number, new_index: number): T[] => { - while (old_index < 0) { - old_index += arr.length; - } - while (new_index < 0) { - new_index += arr.length; - } - if (new_index >= arr.length) { - let k = new_index - arr.length + 1; - while (k--) { - // @ts-ignore - arr.push(undefined); - } - } - arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); - return arr; +export const arrayMove = ( + arr: T[], + old_index: number, + new_index: number, +): T[] => { + while (old_index < 0) { + old_index += arr.length; + } + while (new_index < 0) { + new_index += arr.length; + } + if (new_index >= arr.length) { + let k = new_index - arr.length + 1; + while (k--) { + // @ts-ignore + arr.push(undefined); + } + } + arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); + return arr; }; diff --git a/src/utils/conn.ts b/src/utils/conn.ts index 375300a..972bf5c 100644 --- a/src/utils/conn.ts +++ b/src/utils/conn.ts @@ -1,67 +1,70 @@ -import { hiddenConnectionTestLoader, showConnectionTestLoader } from '@/utils/loader'; -import { Config, MeiliSearch } from 'meilisearch'; -import _ from 'lodash'; -import { Instance, WarningPageData } from '@/store'; -import { toast } from './toast'; -import i18n from './i18n'; +import type { Instance, WarningPageData } from "@/store"; +import { + hiddenConnectionTestLoader, + showConnectionTestLoader, +} from "@/utils/loader"; +import _ from "lodash"; +import { type Config, MeiliSearch, type Stats } from "meilisearch"; +import i18n from "./i18n"; +import { toast } from "./toast"; const t = i18n.t; export const testConnection = async (cfg: Config) => { - showConnectionTestLoader(); - const client = new MeiliSearch({ ...cfg }); - let stats; - try { - stats = await client.getStats(); - console.debug('[meilisearch connection test]', stats); - } catch (e) { - console.warn('[meilisearch connection test error]', e); - toast.error(t('instance:connection_failed')); - // stop loading when error. - hiddenConnectionTestLoader(); - throw e; - } - // stop loading - hiddenConnectionTestLoader(); - if (_.isEmpty(stats)) { - const msg = t('instance:connection_failed'); - toast.error(msg); - console.error(msg, stats); - throw new Error('msg'); - } + showConnectionTestLoader(); + const client = new MeiliSearch({ ...cfg }); + let stats: Stats; + try { + stats = await client.getStats(); + console.debug("[meilisearch connection test]", stats); + } catch (e) { + console.warn("[meilisearch connection test error]", e); + toast.error(t("instance:connection_failed")); + // stop loading when error. + hiddenConnectionTestLoader(); + throw e; + } + // stop loading + hiddenConnectionTestLoader(); + if (_.isEmpty(stats)) { + const msg = t("instance:connection_failed"); + toast.error(msg); + console.error(msg, stats); + throw new Error("msg"); + } }; /** * check before keys page (no masterKey will cause error) */ -export const validateKeysRouteAvailable = (apiKey?: string): null | WarningPageData => { - if (_.isEmpty(apiKey)) { - return { - prompt: t('instance:no_master_key_error'), - }; - } else { - return null; - } +export const validateKeysRouteAvailable = ( + apiKey?: string, +): null | WarningPageData => { + if (_.isEmpty(apiKey)) { + return { + prompt: t("instance:no_master_key_error"), + }; + } + return null; }; /** * check is singleton mode */ export const isSingletonMode = (): boolean => { - return String(import.meta.env.VITE_SINGLETON_MODE) === 'true'; + return String(import.meta.env.VITE_SINGLETON_MODE) === "true"; }; /** * get singleton mode config */ export const getSingletonCfg = (): false | Instance => { - if (isSingletonMode()) { - return { - id: 0, - name: '', - host: import.meta.env.VITE_SINGLETON_HOST, - apiKey: import.meta.env.VITE_SINGLETON_API_KEY, - }; - } else { - return false; - } + if (isSingletonMode()) { + return { + id: 0, + name: "", + host: import.meta.env.VITE_SINGLETON_HOST, + apiKey: import.meta.env.VITE_SINGLETON_API_KEY, + }; + } + return false; }; diff --git a/src/utils/file.ts b/src/utils/file.ts index 62cff3f..e1d82c9 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,26 +1,30 @@ -'use client'; +"use client"; -export const downloadFile = ({ data, fileName, fileType }: { data: BlobPart; fileName: string; fileType: string }) => { - // Create a blob with the data we want to download as a file - const blob = new Blob([data], { type: fileType }); - // Create an anchor element and dispatch a click event on it - // to trigger a download - const a = document.createElement('a'); - a.download = fileName; - a.href = window.URL.createObjectURL(blob); - const clickEvt = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true, - }); - a.dispatchEvent(clickEvt); - a.remove(); +export const downloadFile = ({ + data, + fileName, + fileType, +}: { data: BlobPart; fileName: string; fileType: string }) => { + // Create a blob with the data we want to download as a file + const blob = new Blob([data], { type: fileType }); + // Create an anchor element and dispatch a click event on it + // to trigger a download + const a = document.createElement("a"); + a.download = fileName; + a.href = window.URL.createObjectURL(blob); + const clickEvt = new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + }); + a.dispatchEvent(clickEvt); + a.remove(); }; export const exportToJSON = (obj: object | object[], fileName: string) => { - downloadFile({ - data: JSON.stringify(obj), - fileName: `${fileName}.json`, - fileType: 'text/json', - }); + downloadFile({ + data: JSON.stringify(obj), + fileName: `${fileName}.json`, + fileType: "text/json", + }); }; diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts index a43cc7b..7c832c2 100644 --- a/src/utils/i18n.ts +++ b/src/utils/i18n.ts @@ -1,89 +1,94 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; -import LanguageDetector from 'i18next-browser-languagedetector'; -import resourcesToBackend from 'i18next-resources-to-backend'; +import i18n from "i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; +import resourcesToBackend from "i18next-resources-to-backend"; +import { initReactI18next } from "react-i18next"; -export const SUPPORTED_LANGUAGES = ['en', 'zh'] as const; +export const SUPPORTED_LANGUAGES = ["en", "zh"] as const; export type SUPPORTED_LANGUAGE = (typeof SUPPORTED_LANGUAGES)[number]; export enum SUPPORTED_LANGUAGE_LOCALIZED { - en = 'EN', - zh = '中文', + en = "EN", + zh = "中文", } export const NAMESPACES = [ - 'common', - 'dashboard', - 'task', - 'key', - 'upload', - 'document', - 'index', - 'instance', - 'header', - 'sys', + "common", + "dashboard", + "task", + "key", + "upload", + "document", + "index", + "instance", + "header", + "sys", ] as const; i18n - // .use(Backend) - .use(resourcesToBackend((language: string, namespace: string) => import(`../locales/${language}/${namespace}.json`))) - .use(LanguageDetector) - .use(initReactI18next) // passes i18n down to react-i18next - .init({ - debug: true, - fallbackLng: SUPPORTED_LANGUAGES[0], - ns: NAMESPACES, - fallbackNS: NAMESPACES[0], - interpolation: { - escapeValue: false, // react already safes from xss - }, - detection: { - // order and from where user language should be detected - order: ['querystring', 'navigator', 'htmlTag'], + // .use(Backend) + .use( + resourcesToBackend( + (language: string, namespace: string) => + import(`../locales/${language}/${namespace}.json`), + ), + ) + .use(LanguageDetector) + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + debug: true, + fallbackLng: SUPPORTED_LANGUAGES[0], + ns: NAMESPACES, + fallbackNS: NAMESPACES[0], + interpolation: { + escapeValue: false, // react already safes from xss + }, + detection: { + // order and from where user language should be detected + order: ["querystring", "navigator", "htmlTag"], - // keys or params to lookup language from - lookupQuerystring: 'lang', + // keys or params to lookup language from + lookupQuerystring: "lang", - // cache user language on - caches: ['localStorage', 'cookie'], + // cache user language on + caches: ["localStorage", "cookie"], - // optional htmlTag with lang attribute, the default is: - htmlTag: document.documentElement, + // optional htmlTag with lang attribute, the default is: + htmlTag: document.documentElement, - // only look up languages rather than locales. default english - convertDetectedLanguage: (lng) => /^(\w+)/.exec(lng)![0] || 'en', - }, - }); + // only look up languages rather than locales. default english + convertDetectedLanguage: (lng) => /^(\w+)/.exec(lng)![0] || "en", + }, + }); export default i18n; -import semi_zh_CN from '@douyinfe/semi-ui/lib/es/locale/source/zh_CN'; -import semi_en_US from '@douyinfe/semi-ui/lib/es/locale/source/en_US'; +import semi_en_US from "@douyinfe/semi-ui/lib/es/locale/source/en_US"; +import semi_zh_CN from "@douyinfe/semi-ui/lib/es/locale/source/zh_CN"; export const lang2SemiLocale = (lang?: SUPPORTED_LANGUAGE) => { - switch (lang) { - case 'zh': - return semi_zh_CN; - default: - return semi_en_US; - } + switch (lang) { + case "zh": + return semi_zh_CN; + default: + return semi_en_US; + } }; -import arco_zh_CN from '@arco-design/web-react/es/locale/zh-CN'; -import arco_en_US from '@arco-design/web-react/es/locale/en-US'; +import arco_en_US from "@arco-design/web-react/es/locale/en-US"; +import arco_zh_CN from "@arco-design/web-react/es/locale/zh-CN"; export const lang2ArcoLocale = (lang?: SUPPORTED_LANGUAGE) => { - switch (lang) { - case 'zh': - return arco_zh_CN; - default: - return arco_en_US; - } + switch (lang) { + case "zh": + return arco_zh_CN; + default: + return arco_en_US; + } }; export const locale2DayjsLocale = (lang?: SUPPORTED_LANGUAGE) => { - switch (lang) { - case 'zh': - return 'zh-CN'; - default: - return 'en'; - } + switch (lang) { + case "zh": + return "zh-CN"; + default: + return "en"; + } }; diff --git a/src/utils/loader.ts b/src/utils/loader.ts index 1c4c43c..e87d86c 100644 --- a/src/utils/loader.ts +++ b/src/utils/loader.ts @@ -1,52 +1,52 @@ -import { toast } from './toast'; +import { toast } from "./toast"; -const RequestLoaderID = 'request-loader'; +const RequestLoaderID = "request-loader"; let RequestLoaderTimeoutId: NodeJS.Timeout[] = []; export const showRequestLoader = () => { - console.log('show loader'); + console.log("show loader"); - const tid = setTimeout(() => { - // pull current tid from queue - const arr = [...RequestLoaderTimeoutId]; - arr.splice( - arr.findIndex((e) => e === tid), - 1 - ); - RequestLoaderTimeoutId = arr; - toast.loading( - new Promise(() => { - // longest 3s - setTimeout(() => toast.remove(RequestLoaderID), 5000); - }), - { - label: 'Request loading...', - success: 'Request completed', - error: 'Request failed', - id: RequestLoaderID, - } - ); - // just show loader for slow request(>=2s) - }, 2000); - RequestLoaderTimeoutId.push(tid); + const tid = setTimeout(() => { + // pull current tid from queue + const arr = [...RequestLoaderTimeoutId]; + arr.splice( + arr.findIndex((e) => e === tid), + 1, + ); + RequestLoaderTimeoutId = arr; + toast.loading( + new Promise(() => { + // longest 3s + setTimeout(() => toast.remove(RequestLoaderID), 5000); + }), + { + label: "Request loading...", + success: "Request completed", + error: "Request failed", + id: RequestLoaderID, + }, + ); + // just show loader for slow request(>=2s) + }, 2000); + RequestLoaderTimeoutId.push(tid); }; export const hiddenRequestLoader = () => { - // clearTimeout(RequestLoaderTimeoutId[0]); - // RequestLoaderTimeoutId.splice(0, 1); - RequestLoaderTimeoutId.forEach((i) => clearTimeout(i)); - RequestLoaderTimeoutId = []; - toast.remove(RequestLoaderID); + // clearTimeout(RequestLoaderTimeoutId[0]); + // RequestLoaderTimeoutId.splice(0, 1); + RequestLoaderTimeoutId.forEach((i) => clearTimeout(i)); + RequestLoaderTimeoutId = []; + toast.remove(RequestLoaderID); }; -const ConnectionTestLoaderID = 'conn-test-loader'; +const ConnectionTestLoaderID = "conn-test-loader"; export const showConnectionTestLoader = () => { - toast.loading(new Promise(() => {}), { - label: 'Connection testing...', - id: ConnectionTestLoaderID, - duration: Infinity, - }); + toast.loading(new Promise(() => {}), { + label: "Connection testing...", + id: ConnectionTestLoaderID, + duration: Number.POSITIVE_INFINITY, + }); }; export const hiddenConnectionTestLoader = () => { - toast.remove(ConnectionTestLoaderID); + toast.remove(ConnectionTestLoaderID); }; diff --git a/src/utils/text.ts b/src/utils/text.ts index 6fab8ad..c9e4722 100644 --- a/src/utils/text.ts +++ b/src/utils/text.ts @@ -1,104 +1,109 @@ -import { EnqueuedTask } from 'meilisearch'; -import dayjs from 'dayjs'; -import { toast } from './toast'; -import relativeTime from 'dayjs/plugin/relativeTime'; -import duration from 'dayjs/plugin/duration'; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import relativeTime from "dayjs/plugin/relativeTime"; +import type { EnqueuedTask } from "meilisearch"; +import { toast } from "./toast"; dayjs.extend(relativeTime); dayjs.extend(duration); export const getTaskSubmitMessage = (task: EnqueuedTask): string => { - return `Task submit ${task.status}, task uid ${task.taskUid} 🚀`; + return `Task submit ${task.status}, task uid ${task.taskUid} 🚀`; }; -const enum TaskStatus { - TASK_SUCCEEDED = 'succeeded', - TASK_PROCESSING = 'processing', - TASK_FAILED = 'failed', - TASK_ENQUEUED = 'enqueued', - TASK_CANCEL = 'canceled', +enum TaskStatus { + TASK_SUCCEEDED = "succeeded", + TASK_PROCESSING = "processing", + TASK_FAILED = "failed", + TASK_ENQUEUED = "enqueued", + TASK_CANCEL = "canceled", } const TaskStatusToast: Record< - TaskStatus, - (typeof toast)['success'] | (typeof toast)['info'] | (typeof toast)['error'] + TaskStatus, + (typeof toast)["success"] | (typeof toast)["info"] | (typeof toast)["error"] > = { - [TaskStatus.TASK_SUCCEEDED]: toast.success, - [TaskStatus.TASK_ENQUEUED]: toast.info, - [TaskStatus.TASK_FAILED]: toast.error, - [TaskStatus.TASK_PROCESSING]: toast.info, - [TaskStatus.TASK_CANCEL]: toast.info, + [TaskStatus.TASK_SUCCEEDED]: toast.success, + [TaskStatus.TASK_ENQUEUED]: toast.info, + [TaskStatus.TASK_FAILED]: toast.error, + [TaskStatus.TASK_PROCESSING]: toast.info, + [TaskStatus.TASK_CANCEL]: toast.info, }; export const TaskThemes: Record = { - [TaskStatus.TASK_SUCCEEDED]: 'success', - [TaskStatus.TASK_ENQUEUED]: 'warn', - [TaskStatus.TASK_FAILED]: 'danger', - [TaskStatus.TASK_PROCESSING]: 'secondary', - [TaskStatus.TASK_CANCEL]: 'info', + [TaskStatus.TASK_SUCCEEDED]: "success", + [TaskStatus.TASK_ENQUEUED]: "warn", + [TaskStatus.TASK_FAILED]: "danger", + [TaskStatus.TASK_PROCESSING]: "secondary", + [TaskStatus.TASK_CANCEL]: "info", }; export const showTaskSubmitNotification = (task: EnqueuedTask): void => { - TaskStatusToast[task.status](getTaskSubmitMessage(task)); + TaskStatusToast[task.status](getTaskSubmitMessage(task)); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const showTaskErrorNotification = (err: any): void => { - TaskStatusToast.failed(`Task Fail: ${err.toString()}`); + TaskStatusToast.failed(`Task Fail: ${err.toString()}`); }; export const getTimeText = ( - date: dayjs.ConfigType, - { - defaultText = '-', - format = 'YYYY-MM-DD HH:mm:ss.SSS', - }: { - format?: string; - defaultText?: string; - } = {} + date: dayjs.ConfigType, + { + defaultText = "-", + format = "YYYY-MM-DD HH:mm:ss.SSS", + }: { + format?: string; + defaultText?: string; + } = {}, ): string => { - if (!date && defaultText) { - return defaultText; - } - return dayjs(date).format(format); + if (!date && defaultText) { + return defaultText; + } + return dayjs(date).format(format); }; export const getTimeAgo = (date: dayjs.ConfigType): string => { - return dayjs(date).fromNow(); + return dayjs(date).fromNow(); }; -export const getDuration = (date: string, unit: duration.DurationUnitType): string => { - return dayjs.duration(date).get(unit).toPrecision(5).toString(); +export const getDuration = ( + date: string, + unit: duration.DurationUnitType, +): string => { + return dayjs.duration(date).get(unit).toPrecision(5).toString(); }; export const stringifyJsonPretty = (json?: string | object | null) => { - return JSON.stringify(json, undefined, 2); + return JSON.stringify(json, undefined, 2); }; export function isValidDateTime(str: string): Date | false { - if (dayjs(str).isValid()) { - if (/^\d+$/g.test(str) && str.length < 13) { - // unix timestamp - return dayjs.unix(parseInt(str)).toDate(); - } - return dayjs(str).toDate(); - } else { - return false; - } + if (dayjs(str).isValid()) { + if (/^\d+$/g.test(str) && str.length < 13) { + // unix timestamp + return dayjs.unix(Number.parseInt(str)).toDate(); + } + return dayjs(str).toDate(); + } + return false; } export function isValidHttpUrl(str: string): boolean { - try { - const url = new URL(str); - return url.protocol === 'http:' || url.protocol === 'https:'; - } catch { - return false; - } + try { + const url = new URL(str); + return url.protocol === "http:" || url.protocol === "https:"; + } catch { + return false; + } } export function isValidImgUrl(str: string): boolean { - try { - const url = new URL(str); - return (url.protocol === 'http:' || url.protocol === 'https:') && /\.(jpg|jpeg|png|gif|webp)$/i.test(url.pathname); - } catch { - return false; - } + try { + const url = new URL(str); + return ( + (url.protocol === "http:" || url.protocol === "https:") && + /\.(jpg|jpeg|png|gif|webp)$/i.test(url.pathname) + ); + } catch { + return false; + } } diff --git a/src/utils/toast.ts b/src/utils/toast.ts index 248e228..d59db75 100644 --- a/src/utils/toast.ts +++ b/src/utils/toast.ts @@ -1,43 +1,51 @@ -import React from 'react'; -import { toast as sonner } from 'sonner'; +import type React from "react"; +import { toast as sonner } from "sonner"; // eslint-disable-next-line @typescript-eslint/no-namespace export namespace Toast { - export type ID = string | number; - export type Content = string | React.ReactNode; - export interface Params { - icon?: React.ReactNode; - id?: ID; - duration?: number; - } - export type LoadingParams = Params & { - label?: Content; - success?: Content | ((data: T) => Content); - error?: Content | ((error: unknown) => Content); - }; + export type ID = string | number; + export type Content = string | React.ReactNode; + export interface Params { + icon?: React.ReactNode; + id?: ID; + duration?: number; + } + export type LoadingParams = Params & { + label?: Content; + success?: Content | ((data: T) => Content); + error?: Content | ((error: unknown) => Content); + }; } const base = (content: Toast.Content, params?: Toast.Params): Toast.ID => { - return sonner(content, params); + return sonner(content, params); }; const success = (content: Toast.Content, params?: Toast.Params): Toast.ID => { - return sonner.success(content, params); + return sonner.success(content, params); }; const error = (content: Toast.Content, params?: Toast.Params): Toast.ID => { - return sonner.error(content, params); + return sonner.error(content, params); }; const info = (content: Toast.Content, params?: Toast.Params): Toast.ID => { - return sonner.message(content, params); + return sonner.message(content, params); }; -const loading = (promise: Promise, params: Toast.LoadingParams): Toast.ID => { - return sonner.promise(promise, { loading: params.label, success: 'Success', error: 'Error', ...params }); +const loading = ( + promise: Promise, + params: Toast.LoadingParams, +): Toast.ID => { + return sonner.promise(promise, { + loading: params.label, + success: "Success", + error: "Error", + ...params, + }); }; const remove = (id?: Toast.ID) => sonner.dismiss(id); export const toast = Object.assign(base, { - success, - error, - info, - remove, - loading, + success, + error, + info, + remove, + loading, }); diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 0fe0e60..5bc4494 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -1,44 +1,44 @@ -const theme = require('./src/style/theme.json'); -const { nextui } = require('@nextui-org/react'); +const theme = require("./src/style/theme.json"); +const { nextui } = require("@nextui-org/react"); /** @type {import('tailwindcss').Config} */ module.exports = { - darkMode: ['class'], - content: [ - './pages/**/*.{ts,tsx}', - './components/**/*.{ts,tsx}', - './app/**/*.{ts,tsx}', - './src/**/*.{ts,tsx,css}', - './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}', - ], - prefix: '', - theme: { - container: { - center: true, - }, - extend: { - colors: { ...theme.colors }, - screens: { - tablet: '640px', - laptop: '1024px', - desktop: '1280px', - }, - }, - }, - plugins: [ - nextui({ - layout: { - radius: { - s: '4px', // rounded-s - m: '6px', // rounded-m - l: '8px', // rounded-l - }, - }, - themes: { - light: { - colors: { ...theme.colors }, - }, - }, - }), - ], + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx,css}", + "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}", + ], + prefix: "", + theme: { + container: { + center: true, + }, + extend: { + colors: { ...theme.colors }, + screens: { + tablet: "640px", + laptop: "1024px", + desktop: "1280px", + }, + }, + }, + plugins: [ + nextui({ + layout: { + radius: { + s: "4px", // rounded-s + m: "6px", // rounded-m + l: "8px", // rounded-l + }, + }, + themes: { + light: { + colors: { ...theme.colors }, + }, + }, + }), + ], }; diff --git a/tsconfig.json b/tsconfig.json index 3c9f42f..a73bf58 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,30 +1,30 @@ { - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Node", - "resolveJsonModule": true, - "isolatedModules": false, - "noEmit": true, - "jsx": "react-jsx", - "baseUrl": "./", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src"], - "exclude": ["node_modules"], - "references": [ - { - "path": "./tsconfig.node.json" - } - ] + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": false, + "noEmit": true, + "jsx": "react-jsx", + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules"], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] } diff --git a/tsconfig.node.json b/tsconfig.node.json index b8afcc8..d3bf4b8 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,11 +1,9 @@ { - "compilerOptions": { - "composite": true, - "module": "ESNext", - "moduleResolution": "Node", - "allowSyntheticDefaultImports": true - }, - "include": [ - "vite.config.ts" - ] + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] } diff --git a/uno.config.ts b/uno.config.ts index 8b503b1..eabd85f 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -1,14 +1,13 @@ +import presetIcons from "@unocss/preset-icons"; // uno.config.ts -import { defineConfig, presetUno } from 'unocss'; -import presetIcons from '@unocss/preset-icons'; -import transformerAttributifyJsx from '@unocss/transformer-attributify-jsx'; -import theme from './src/style/theme.json'; +import { defineConfig, presetUno } from "unocss"; +import theme from "./src/style/theme.json"; export default defineConfig({ - presets: [presetUno(), presetIcons({})], - theme: { - colors: { - ...theme.colors, - }, - }, + presets: [presetUno(), presetIcons({})], + theme: { + colors: { + ...theme.colors, + }, + }, }); diff --git a/vercel.json b/vercel.json index 1c2d0ee..8e78be1 100644 --- a/vercel.json +++ b/vercel.json @@ -1,8 +1,8 @@ { - "rewrites": [ - { - "source": "/(.*)", - "destination": "/" - } - ] -} \ No newline at end of file + "rewrites": [ + { + "source": "/(.*)", + "destination": "/" + } + ] +} diff --git a/vite.config.ts b/vite.config.ts index 4ef64c1..41c8ba5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,77 +1,79 @@ +import { execSync } from "node:child_process"; +import path from "node:path"; +import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; +import react from "@vitejs/plugin-react-swc"; +import UnoCSS from "unocss/vite"; // vite.config.ts -import { defineConfig, loadEnv, Plugin } from 'vite'; -import path from 'path'; -import react from '@vitejs/plugin-react-swc'; -import { TanStackRouterVite } from '@tanstack/router-plugin/vite'; -import tsconfigPaths from 'vite-tsconfig-paths'; -import SemiPlugin from './src/lib/semi'; -import UnoCSS from 'unocss/vite'; -import { execSync } from 'child_process'; +import { type Plugin, defineConfig, loadEnv } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import SemiPlugin from "./src/lib/semi"; // Plugin to get Git hash function gitHashPlugin(): Plugin { - return { - name: 'git-hash-plugin', - config: () => { - const hash = execSync('git rev-parse HEAD').toString().trim(); - return { - define: { - __GIT_HASH__: JSON.stringify(hash), - }, - }; - }, - }; + return { + name: "git-hash-plugin", + config: () => { + const hash = execSync("git rev-parse HEAD").toString().trim(); + return { + define: { + __GIT_HASH__: JSON.stringify(hash), + }, + }; + }, + }; } export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), ''); - console.debug('print current base path', env.BASE_PATH); - return { - base: env.BASE_PATH || '/', - plugins: [ - tsconfigPaths({ root: './' }), - react(), - UnoCSS(), - TanStackRouterVite(), - SemiPlugin({ - theme: '@semi-bot/semi-theme-meilisearch', - }), - gitHashPlugin(), - ], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - server: { - host: true, - port: 24900, - }, - preview: { - host: true, - port: 24900, - strictPort: true, - }, - css: { - modules: { - localsConvention: 'camelCaseOnly', - }, - }, - build: { - rollupOptions: { - output: { - manualChunks(id) { - // node_modules is mostly the main reason for the large chunk problem, - // With this you're telling Vite to treat the used modules separately. - // To understand better what it does, - // try to compare the logs from the build command with and without this change. - if (id.includes('node_modules')) { - const importStrArr = id.toString().split('node_modules/'); - return importStrArr[importStrArr.length - 1].split('/')[0].toString(); - } - }, - }, - }, - }, - }; + const env = loadEnv(mode, process.cwd(), ""); + console.debug("print current base path", env.BASE_PATH); + return { + base: env.BASE_PATH || "/", + plugins: [ + tsconfigPaths({ root: "./" }), + react(), + UnoCSS(), + TanStackRouterVite(), + SemiPlugin({ + theme: "@semi-bot/semi-theme-meilisearch", + }), + gitHashPlugin(), + ], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + server: { + host: true, + port: 24900, + }, + preview: { + host: true, + port: 24900, + strictPort: true, + }, + css: { + modules: { + localsConvention: "camelCaseOnly", + }, + }, + build: { + rollupOptions: { + output: { + manualChunks(id) { + // node_modules is mostly the main reason for the large chunk problem, + // With this you're telling Vite to treat the used modules separately. + // To understand better what it does, + // try to compare the logs from the build command with and without this change. + if (id.includes("node_modules")) { + const importStrArr = id.toString().split("node_modules/"); + return importStrArr[importStrArr.length - 1] + .split("/")[0] + .toString(); + } + }, + }, + }, + }, + }; });