mirror of
https://github.com/remnawave/frontend.git
synced 2026-05-13 04:09:03 +00:00
🍏
Co-authored-by: Ivan <84693047+exact01@users.noreply.github.com>
This commit is contained in:
parent
b341bb273a
commit
dea45b7121
95 changed files with 12327 additions and 11009 deletions
90
.eslintrc.js
Normal file
90
.eslintrc.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// import mantine from 'eslint-config-mantine';
|
||||
// import tseslint from 'typescript-eslint';
|
||||
|
||||
// export default tseslint.config(
|
||||
// ...mantine,
|
||||
// { ignores: ['**/*.{mjs,cjs,js,d.ts,d.mts}', './.storybook/main.ts'] },
|
||||
// );
|
||||
|
||||
export default [
|
||||
{
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'airbnb-base',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
'prettier'
|
||||
],
|
||||
ignorePatterns: [
|
||||
'dist',
|
||||
'.eslintrc.cjs',
|
||||
'plop',
|
||||
'plop/**',
|
||||
'plopfile.js',
|
||||
'.stylelintrc.js'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh', 'import'],
|
||||
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx']
|
||||
},
|
||||
'import/resolver': {
|
||||
node: true,
|
||||
typescript: {
|
||||
project: '.'
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
indent: ['error', 4, { SwitchCase: 1 }],
|
||||
'max-classes-per-file': 'off',
|
||||
'import/no-extraneous-dependencies': ['off'],
|
||||
'import/no-unresolved': 'error',
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/extensions': 'off',
|
||||
'no-bitwise': 'off',
|
||||
'no-plusplus': 'off',
|
||||
'no-restricted-syntax': ['off', 'ForInStatement'],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
'newlines-between': 'never'
|
||||
}
|
||||
],
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
'no-shadow': ['off'],
|
||||
'arrow-body-style': ['off'],
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'array-bracket-spacing': ['error', 'never'],
|
||||
'no-underscore-dangle': [
|
||||
'off',
|
||||
{
|
||||
allow: ['_'],
|
||||
allowAfterThis: true,
|
||||
allowAfterSuper: true,
|
||||
allowAfterThisConstructor: true,
|
||||
enforceInMethodNames: false
|
||||
}
|
||||
],
|
||||
semi: ['error', 'never'],
|
||||
'comma-dangle': ['off'],
|
||||
'brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
||||
'object-curly-newline': ['error', { multiline: true, consistent: true }],
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'no-empty-pattern': 'warn',
|
||||
'@typescript-eslint/ban-types': [
|
||||
'error',
|
||||
{
|
||||
types: {
|
||||
'{}': false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
25
.prettierrc
Normal file
25
.prettierrc
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"singleAttributePerLine": false,
|
||||
"tabWidth": 4,
|
||||
"printWidth": 100,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"plugins": [
|
||||
"@ianvs/prettier-plugin-sort-imports"
|
||||
],
|
||||
"importOrderCaseSensitive": false,
|
||||
"importOrder": [
|
||||
"^react$",
|
||||
"",
|
||||
"^@mantine/(.*)$",
|
||||
"^@mantinex/(.*)$",
|
||||
"<THIRD_PARTY_MODULES>",
|
||||
"^(@)(/.*)$",
|
||||
"^../(?!.*.css$).*$",
|
||||
"^./(?!.*.css$).*$",
|
||||
"\\.css$",
|
||||
"",
|
||||
"^[.]"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */
|
||||
const config = {
|
||||
printWidth: 100,
|
||||
singleQuote: true,
|
||||
tabWidth: 4,
|
||||
trailingComma: 'es5',
|
||||
plugins: ['@ianvs/prettier-plugin-sort-imports'],
|
||||
importOrder: [
|
||||
'.*styles.css$',
|
||||
'',
|
||||
'dayjs',
|
||||
'^react$',
|
||||
'^next$',
|
||||
'^next/.*$',
|
||||
'<BUILTIN_MODULES>',
|
||||
'<THIRD_PARTY_MODULES>',
|
||||
'^@mantine/(.*)$',
|
||||
'^@mantinex/(.*)$',
|
||||
'^@mantine-tests/(.*)$',
|
||||
'^@docs/(.*)$',
|
||||
'^@/.*$',
|
||||
'^../(?!.*.css$).*$',
|
||||
'^./(?!.*.css$).*$',
|
||||
'\\.css$',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: '*.mdx',
|
||||
options: {
|
||||
printWidth: 70,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.1.1.cjs
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import mantine from 'eslint-config-mantine';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
...mantine,
|
||||
{ ignores: ['**/*.{mjs,cjs,js,d.ts,d.mts}', './.storybook/main.ts'] },
|
||||
);
|
||||
|
|
@ -13,10 +13,6 @@
|
|||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://raw.githubusercontent.com/nedois/mantine-dashboard/main/screenshoots/screen-1.jpeg"
|
||||
/>
|
||||
<title>Remnawave Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
15691
package-lock.json
generated
15691
package-lock.json
generated
File diff suppressed because it is too large
Load diff
69
package.json
69
package.json
|
|
@ -4,24 +4,19 @@
|
|||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"start:dev": "vite",
|
||||
"start:build": "NODE_ENV=production tsc && vite build",
|
||||
"cb": "vite build",
|
||||
"start:preview": "vite preview --port 3333",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "npm run lint:eslint && npm run lint:stylelint",
|
||||
"lint:eslint": "eslint . --ext .ts,.tsx --cache",
|
||||
"lint:stylelint": "stylelint '**/*.css' --cache",
|
||||
"prettier": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
|
||||
"vitest": "vitest run",
|
||||
"vitest:watch": "vitest",
|
||||
"test": "npm run typecheck && npm run prettier && npm run lint && npm run vitest && npm run build",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build"
|
||||
"prettier:write": "prettier --write \"**/*.{ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@emotion/react": "^11.13.5",
|
||||
"@hello-pangea/dnd": "^17.0.0",
|
||||
"@mantine/carousel": "^7.12.2",
|
||||
"@mantine/charts": "^7.12.2",
|
||||
"@mantine/code-highlight": "^7.12.2",
|
||||
|
|
@ -33,40 +28,21 @@
|
|||
"@mantine/modals": "^7.12.2",
|
||||
"@mantine/notifications": "^7.12.2",
|
||||
"@mantine/nprogress": "^7.12.2",
|
||||
"@mantine/spotlight": "^7.12.2",
|
||||
"@mantine/tiptap": "^7.12.2",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@mdx-js/rollup": "^3.0.1",
|
||||
"@paralleldrive/cuid2": "github:paralleldrive/cuid2",
|
||||
"@remnawave/backend-contract": "^0.0.12",
|
||||
"@tabler/icons-react": "^3.14.0",
|
||||
"@tanstack/react-query": "^5.54.1",
|
||||
"@tanstack/react-query-devtools": "^5.54.1",
|
||||
"@tiptap/extension-link": "^2.6.6",
|
||||
"@tiptap/react": "^2.6.6",
|
||||
"@tiptap/starter-kit": "^2.6.6",
|
||||
"@tsmx/human-readable": "^2.0.3",
|
||||
"@remnawave/backend-contract": "^0.0.17",
|
||||
"axios": "^1.7.7",
|
||||
"byte-size": "^9.0.0",
|
||||
"bytes": "^3.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"color-hash": "^2.0.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dinero.js": "^2.0.0-alpha.14",
|
||||
"dotenv": "^16.4.5",
|
||||
"embla-carousel-react": "^8.2.1",
|
||||
"framer-motion": "^11.5.2",
|
||||
"libphonenumber-js": "^1.11.7",
|
||||
"mantine-datatable": "^7.12.4",
|
||||
"msw": "^2.4.2",
|
||||
"nanoid": "^5.0.7",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-imask": "^7.6.1",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"recharts": "^2.12.7",
|
||||
"recharts": "^2.13.3",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"xbytes": "^1.9.1",
|
||||
"zod": "^3.23.8",
|
||||
|
|
@ -77,12 +53,15 @@
|
|||
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
||||
"@types/byte-size": "^8.1.2",
|
||||
"@types/bytes": "^3.1.4",
|
||||
"@types/color-hash": "^2.0.0",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.4.0",
|
||||
"@typescript-eslint/parser": "^8.4.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
|
|
@ -98,13 +77,27 @@
|
|||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.3.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"storybook": "^8.2.9",
|
||||
"storybook-dark-mode": "^4.0.2",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"stylelint": "^16.9.0",
|
||||
"stylelint-config-standard-scss": "^13.1.0",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.3",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"vitest": "^2.0.5"
|
||||
"vite": "6.0.1",
|
||||
"vite-plugin-javascript-obfuscator": "^3.1.0",
|
||||
"vite-plugin-preload": "^0.4.0",
|
||||
"vite-plugin-webfont-dl": "^3.9.4",
|
||||
"vite-tsconfig-paths": "^5.0.1"
|
||||
},
|
||||
"commitlint": {
|
||||
"extends": [
|
||||
"@commitlint/config-conventional"
|
||||
]
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "4.9.5"
|
||||
},
|
||||
"overrides": {
|
||||
"node-plop": {
|
||||
"inquirer": "9.3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/app.tsx
62
src/app.tsx
|
|
@ -1,37 +1,35 @@
|
|||
import '@mantine/carousel/styles.layer.css';
|
||||
import '@mantine/charts/styles.layer.css';
|
||||
import '@mantine/code-highlight/styles.layer.css';
|
||||
import '@mantine/core/styles.layer.css';
|
||||
import '@mantine/dates/styles.layer.css';
|
||||
import '@mantine/dropzone/styles.layer.css';
|
||||
import '@mantine/notifications/styles.layer.css';
|
||||
import '@mantine/nprogress/styles.layer.css';
|
||||
import '@mantine/spotlight/styles.layer.css';
|
||||
import '@mantine/tiptap/styles.layer.css';
|
||||
import 'mantine-datatable/styles.layer.css';
|
||||
import './global.css';
|
||||
import '@mantine/carousel/styles.layer.css'
|
||||
import '@mantine/charts/styles.layer.css'
|
||||
import '@mantine/code-highlight/styles.layer.css'
|
||||
import '@mantine/core/styles.layer.css'
|
||||
import '@mantine/dates/styles.layer.css'
|
||||
import '@mantine/dropzone/styles.layer.css'
|
||||
import '@mantine/notifications/styles.layer.css'
|
||||
import '@mantine/nprogress/styles.layer.css'
|
||||
import 'mantine-datatable/styles.layer.css'
|
||||
import './global.css'
|
||||
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import { NavigationProgress } from '@mantine/nprogress';
|
||||
import { Router } from '@/app/router/router';
|
||||
import { AuthProvider } from '@/shared/providers/auth-provider';
|
||||
import { theme } from '@/shared/theme';
|
||||
import { MantineProvider } from '@mantine/core'
|
||||
import { ModalsProvider } from '@mantine/modals'
|
||||
import { Notifications } from '@mantine/notifications'
|
||||
import { NavigationProgress } from '@mantine/nprogress'
|
||||
import { Router } from '@/app/router/router'
|
||||
import { AuthProvider } from '@/shared/providers/auth-provider'
|
||||
import { theme } from '@/shared/theme'
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<AuthProvider>
|
||||
<MantineProvider theme={theme} defaultColorScheme="dark">
|
||||
<Notifications position="top-right" />
|
||||
<NavigationProgress />
|
||||
<ModalsProvider>
|
||||
<Router />
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</AuthProvider>
|
||||
</HelmetProvider>
|
||||
);
|
||||
<AuthProvider>
|
||||
<MantineProvider
|
||||
theme={theme}
|
||||
defaultColorScheme="dark"
|
||||
>
|
||||
<Notifications position="top-right" />
|
||||
<NavigationProgress />
|
||||
<ModalsProvider>
|
||||
<Router />
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ElementType } from 'react';
|
||||
import { ROUTES } from '@shared/constants';
|
||||
import { PiStarDuotone, PiUsersDuotone } from 'react-icons/pi';
|
||||
import { PiScrewdriverDuotone, PiStarDuotone, PiUsersDuotone } from 'react-icons/pi';
|
||||
import { MenuItem } from './interfaces';
|
||||
|
||||
export const menu: MenuItem[] = [
|
||||
|
|
@ -15,8 +15,11 @@ export const menu: MenuItem[] = [
|
|||
],
|
||||
},
|
||||
{
|
||||
header: 'Users',
|
||||
section: [{ name: 'Users', href: ROUTES.DASHBOARD.USERS, icon: PiUsersDuotone }],
|
||||
header: 'Management',
|
||||
section: [
|
||||
{ name: 'Users', href: ROUTES.DASHBOARD.USERS, icon: PiUsersDuotone },
|
||||
{ name: 'Hosts', href: ROUTES.DASHBOARD.HOSTS, icon: PiScrewdriverDuotone },
|
||||
],
|
||||
},
|
||||
|
||||
// {
|
||||
|
|
|
|||
|
|
@ -3,15 +3,16 @@ import {
|
|||
createRoutesFromElements,
|
||||
Navigate,
|
||||
Route,
|
||||
RouterProvider,
|
||||
} from 'react-router-dom';
|
||||
import { AuthLayout } from '@/app/layouts/auth';
|
||||
import { DashboardLayout } from '@/app/layouts/dashboard';
|
||||
import { LoginPage } from '@/pages/auth/login/login.page';
|
||||
import { HomePageConnectior } from '@/pages/dashboard/home/connectores/home.page.connector';
|
||||
import { UsersPageConnector } from '@/pages/dashboard/users/ui/connectors/users.page.connector';
|
||||
import { AuthGuard } from '@/shared/hocs/guards/auth-guard';
|
||||
import { ROUTES } from '../../shared/constants';
|
||||
RouterProvider
|
||||
} from 'react-router-dom'
|
||||
import { AuthLayout } from '@/app/layouts/auth'
|
||||
import { DashboardLayout } from '@/app/layouts/dashboard'
|
||||
import { LoginPage } from '@/pages/auth/login/login.page'
|
||||
import { HomePageConnectior } from '@/pages/dashboard/home/connectores/home.page.connector'
|
||||
import { HostsPageConnector } from '@/pages/dashboard/hosts/ui/connectors/hosts.page.connector'
|
||||
import { UsersPageConnector } from '@/pages/dashboard/users/ui/connectors/users.page.connector'
|
||||
import { AuthGuard } from '@/shared/hocs/guards/auth-guard'
|
||||
import { ROUTES } from '../../shared/constants'
|
||||
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
|
|
@ -26,11 +27,12 @@ const router = createBrowserRouter(
|
|||
<Route index element={<Navigate to={ROUTES.DASHBOARD.HOME} replace />} />
|
||||
<Route path={ROUTES.DASHBOARD.HOME} element={<HomePageConnectior />} />
|
||||
<Route path={ROUTES.DASHBOARD.USERS} element={<UsersPageConnector />} />
|
||||
<Route path={ROUTES.DASHBOARD.HOSTS} element={<HostsPageConnector />} />
|
||||
</Route>
|
||||
</Route>
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
export function Router() {
|
||||
return <RouterProvider router={router} />;
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
import { notifications } from '@mantine/notifications'
|
||||
import {
|
||||
GetAllUsersCommand,
|
||||
GetInboundsCommand,
|
||||
GetStatsCommand,
|
||||
} from '@remnawave/backend-contract';
|
||||
import { instance } from '@shared/api';
|
||||
import { AxiosError } from 'axios';
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IActions, IState, IUsersParams } from './interfaces';
|
||||
GetStatsCommand
|
||||
} from '@remnawave/backend-contract'
|
||||
import { instance } from '@shared/api'
|
||||
import { AxiosError } from 'axios'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { IInboundsHashMap } from '@/entitites/dashboard/dashboard-store/interfaces/inbounds-hash-map.interface'
|
||||
import { IActions, IState, IUsersParams } from './interfaces'
|
||||
|
||||
const initialState: IState = {
|
||||
isLoading: false,
|
||||
isUsersLoading: false,
|
||||
systemInfo: null,
|
||||
users: null,
|
||||
usersParams: {
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
orderBy: 'createdAt',
|
||||
orderDir: 'desc',
|
||||
orderDir: 'desc'
|
||||
},
|
||||
totalUsers: 0,
|
||||
inbounds: null,
|
||||
isInboundsLoading: false,
|
||||
};
|
||||
inboundsHashMap: null
|
||||
}
|
||||
|
||||
export const useDashboardStore = create<IState & IActions>()(
|
||||
devtools(
|
||||
|
|
@ -32,31 +35,31 @@ export const useDashboardStore = create<IState & IActions>()(
|
|||
actions: {
|
||||
getSystemInfo: async (): Promise<boolean> => {
|
||||
try {
|
||||
set({ isLoading: true });
|
||||
set({ isLoading: true })
|
||||
const response = await instance.get<GetStatsCommand.Response>(
|
||||
GetStatsCommand.url
|
||||
);
|
||||
)
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
data: { response: dataResponse }
|
||||
} = response
|
||||
|
||||
set({ systemInfo: dataResponse });
|
||||
set({ systemInfo: dataResponse })
|
||||
|
||||
return true;
|
||||
return true
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
throw e
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
set({ isLoading: false })
|
||||
}
|
||||
},
|
||||
getUsers: async (params?: Partial<IUsersParams>): Promise<boolean> => {
|
||||
try {
|
||||
set({ isLoading: true });
|
||||
const currentParams = getState().usersParams;
|
||||
const newParams = { ...currentParams, ...params };
|
||||
set({ isUsersLoading: true })
|
||||
const currentParams = getState().usersParams
|
||||
const newParams = { ...currentParams, ...params }
|
||||
|
||||
const response = await instance.get<GetAllUsersCommand.Response>(
|
||||
GetAllUsersCommand.url,
|
||||
|
|
@ -67,72 +70,87 @@ export const useDashboardStore = create<IState & IActions>()(
|
|||
orderBy: newParams.orderBy,
|
||||
orderDir: newParams.orderDir,
|
||||
search: newParams.search,
|
||||
searchBy: newParams.searchBy,
|
||||
},
|
||||
searchBy: newParams.searchBy
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
data: { response: dataResponse }
|
||||
} = response
|
||||
|
||||
set({
|
||||
users: dataResponse.users,
|
||||
totalUsers: dataResponse.total,
|
||||
usersParams: newParams,
|
||||
});
|
||||
usersParams: newParams
|
||||
})
|
||||
|
||||
return true;
|
||||
return true
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
throw e
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
set({ isUsersLoading: false })
|
||||
}
|
||||
},
|
||||
getInbounds: async (): Promise<boolean> => {
|
||||
try {
|
||||
set({ isInboundsLoading: true });
|
||||
set({ isInboundsLoading: true })
|
||||
const response = await instance.get<GetInboundsCommand.Response>(
|
||||
GetInboundsCommand.url
|
||||
);
|
||||
)
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
data: { response: dataResponse }
|
||||
} = response
|
||||
|
||||
set({ inbounds: dataResponse });
|
||||
set({ inbounds: dataResponse })
|
||||
|
||||
return true;
|
||||
const inboundsHashMap = new Map<string, IInboundsHashMap>(
|
||||
dataResponse.map((inbound) => [
|
||||
inbound.uuid,
|
||||
{
|
||||
tag: inbound.tag,
|
||||
type: inbound.type
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
set({ inboundsHashMap })
|
||||
|
||||
return true
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
throw e
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
} finally {
|
||||
set({ isInboundsLoading: false });
|
||||
set({ isInboundsLoading: false })
|
||||
}
|
||||
},
|
||||
resetState: async () => {
|
||||
set({ ...initialState });
|
||||
},
|
||||
},
|
||||
set({ ...initialState })
|
||||
}
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'dashboardStore',
|
||||
anonymousActionType: 'dashboardStore',
|
||||
anonymousActionType: 'dashboardStore'
|
||||
}
|
||||
)
|
||||
);
|
||||
)
|
||||
|
||||
export const useDashboardStoreIsLoading = () => useDashboardStore((store) => store.isLoading);
|
||||
export const useDashboardStoreSystemInfo = () => useDashboardStore((state) => state.systemInfo);
|
||||
export const useDashboardStoreActions = () => useDashboardStore((store) => store.actions);
|
||||
export const useDashboardStoreUsers = () => useDashboardStore((state) => state.users);
|
||||
export const useDashboardStoreTotalUsers = () => useDashboardStore((state) => state.totalUsers);
|
||||
export const useDashboardStoreParams = () => useDashboardStore((state) => state.usersParams);
|
||||
export const useDashboardStoreIsLoading = () => useDashboardStore((store) => store.isLoading)
|
||||
export const useDashboardStoreUsersLoading = () =>
|
||||
useDashboardStore((store) => store.isUsersLoading)
|
||||
export const useDashboardStoreSystemInfo = () => useDashboardStore((state) => state.systemInfo)
|
||||
export const useDashboardStoreActions = () => useDashboardStore((store) => store.actions)
|
||||
export const useDashboardStoreUsers = () => useDashboardStore((state) => state.users)
|
||||
export const useDashboardStoreTotalUsers = () => useDashboardStore((state) => state.totalUsers)
|
||||
export const useDashboardStoreParams = () => useDashboardStore((state) => state.usersParams)
|
||||
|
||||
// Inbounds
|
||||
export const useDSInbounds = () => useDashboardStore((state) => state.inbounds);
|
||||
export const useDSInboundsLoading = () => useDashboardStore((state) => state.isInboundsLoading);
|
||||
export const useDSInbounds = () => useDashboardStore((state) => state.inbounds)
|
||||
export const useDSInboundsLoading = () => useDashboardStore((state) => state.isInboundsLoading)
|
||||
export const useDSInboundsHashMap = () => useDashboardStore((state) => state.inboundsHashMap)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
export interface IInboundsHashMap {
|
||||
tag: string;
|
||||
type: string;
|
||||
}
|
||||
|
|
@ -1,16 +1,19 @@
|
|||
import {
|
||||
GetAllUsersCommand,
|
||||
GetInboundsCommand,
|
||||
GetStatsCommand,
|
||||
} from '@remnawave/backend-contract';
|
||||
import { IUsersParams } from '../interfaces';
|
||||
GetStatsCommand
|
||||
} from '@remnawave/backend-contract'
|
||||
import { IInboundsHashMap } from '@/entitites/dashboard/dashboard-store/interfaces/inbounds-hash-map.interface'
|
||||
import { IUsersParams } from '../interfaces'
|
||||
|
||||
export interface IState {
|
||||
isLoading: boolean;
|
||||
isInboundsLoading: boolean;
|
||||
systemInfo: GetStatsCommand.Response['response'] | null;
|
||||
users: GetAllUsersCommand.Response['response']['users'] | null;
|
||||
usersParams: IUsersParams;
|
||||
totalUsers: number;
|
||||
inbounds: GetInboundsCommand.Response['response'] | null;
|
||||
isLoading: boolean
|
||||
isUsersLoading: boolean
|
||||
isInboundsLoading: boolean
|
||||
systemInfo: GetStatsCommand.Response['response'] | null
|
||||
users: GetAllUsersCommand.Response['response']['users'] | null
|
||||
usersParams: IUsersParams
|
||||
totalUsers: number
|
||||
inbounds: GetInboundsCommand.Response['response'] | null
|
||||
inboundsHashMap: Map<string, IInboundsHashMap> | null
|
||||
}
|
||||
|
|
|
|||
90
src/entitites/dashboard/hosts/hosts-store/hosts-store.ts
Normal file
90
src/entitites/dashboard/hosts/hosts-store/hosts-store.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { GetAllHostsCommand, ReorderHostCommand } from '@remnawave/backend-contract';
|
||||
import { instance } from '@shared/api';
|
||||
import { AxiosError } from 'axios';
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { IActions, IState } from './interfaces';
|
||||
|
||||
const initialState: IState = {
|
||||
isHostsLoading: false,
|
||||
hosts: null,
|
||||
};
|
||||
|
||||
export const useDashboardStore = create<IState & IActions>()(
|
||||
devtools(
|
||||
(set, getState) => ({
|
||||
...initialState,
|
||||
actions: {
|
||||
getHosts: async (): Promise<boolean> => {
|
||||
try {
|
||||
set({ isHostsLoading: true });
|
||||
|
||||
const response = await instance.get<GetAllHostsCommand.Response>(
|
||||
GetAllHostsCommand.url
|
||||
);
|
||||
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
|
||||
set({
|
||||
hosts: dataResponse,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
set({ isHostsLoading: false });
|
||||
}
|
||||
},
|
||||
reorderHosts: async (
|
||||
hosts: ReorderHostCommand.Request['hosts']
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
set({ isHostsLoading: true });
|
||||
|
||||
const response = await instance.post<ReorderHostCommand.Response>(
|
||||
ReorderHostCommand.url,
|
||||
{
|
||||
hosts,
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
|
||||
if (dataResponse.isUpdated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
set({ isHostsLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
resetState: async () => {
|
||||
set({ ...initialState });
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'hostsStore',
|
||||
anonymousActionType: 'hostsStore',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const useHostsStoreIsLoading = () => useDashboardStore((store) => store.isHostsLoading);
|
||||
export const useHostsStoreHosts = () => useDashboardStore((state) => state.hosts);
|
||||
export const useHostsStoreActions = () => useDashboardStore((store) => store.actions);
|
||||
2
src/entitites/dashboard/hosts/hosts-store/index.ts
Normal file
2
src/entitites/dashboard/hosts/hosts-store/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './hosts-store';
|
||||
export * from './interfaces';
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { ReorderHostCommand } from '@remnawave/backend-contract';
|
||||
|
||||
export interface IActions {
|
||||
actions: {
|
||||
getHosts: () => Promise<boolean>;
|
||||
reorderHosts: (hosts: ReorderHostCommand.Request['hosts']) => Promise<boolean>;
|
||||
resetState: () => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './action.interface';
|
||||
export * from './state.interface';
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { GetAllHostsCommand } from '@remnawave/backend-contract';
|
||||
|
||||
export interface IState {
|
||||
isHostsLoading: boolean;
|
||||
hosts: GetAllHostsCommand.Response['response'] | null;
|
||||
}
|
||||
1
src/entitites/dashboard/hosts/index.ts
Normal file
1
src/entitites/dashboard/hosts/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './hosts-store';
|
||||
1
src/entitites/dashboard/index.ts
Normal file
1
src/entitites/dashboard/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './hosts';
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { CreateUserCommand } from '@remnawave/backend-contract';
|
||||
|
||||
export interface IActions {
|
||||
actions: {
|
||||
createUser: (body: CreateUserCommand.Request) => Promise<boolean>;
|
||||
changeModalState: (state: boolean) => void;
|
||||
resetState: () => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './action.interface';
|
||||
export * from './state.interface';
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export interface IState {
|
||||
isLoading: boolean;
|
||||
isModalOpen: boolean;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { CreateUserCommand } from '@remnawave/backend-contract';
|
||||
import { instance } from '@shared/api';
|
||||
import { AxiosError } from 'axios';
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { IActions, IState } from './interfaces';
|
||||
|
||||
const initialState: IState = {
|
||||
isLoading: false,
|
||||
isModalOpen: false,
|
||||
};
|
||||
|
||||
export const useUserCreationModalStore = create<IState & IActions>()(
|
||||
devtools(
|
||||
(set, getState) => ({
|
||||
...initialState,
|
||||
actions: {
|
||||
createUser: async (body: CreateUserCommand.Request): Promise<boolean> => {
|
||||
try {
|
||||
set({ isLoading: true });
|
||||
await instance.post<CreateUserCommand.Response>(
|
||||
CreateUserCommand.url,
|
||||
body
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
changeModalState: async (modalState: boolean) => {
|
||||
set((state) => ({ isModalOpen: modalState }));
|
||||
if (!modalState) {
|
||||
await getState().actions.resetState();
|
||||
}
|
||||
},
|
||||
resetState: async (): Promise<void> => {
|
||||
set({ ...initialState });
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'userCreationModalStore',
|
||||
anonymousActionType: 'userCreationModalStore',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const useUserCreationModalStoreIsModalOpen = () =>
|
||||
useUserCreationModalStore((state) => state.isModalOpen);
|
||||
export const useUserCreationModalStoreActions = () =>
|
||||
useUserCreationModalStore((store) => store.actions);
|
||||
|
|
@ -8,6 +8,10 @@ export interface IActions {
|
|||
actions: {
|
||||
getUser: () => Promise<boolean>;
|
||||
updateUser: (body: UpdateUserCommand.Request) => Promise<boolean>;
|
||||
disableUser: () => Promise<boolean>;
|
||||
enableUser: () => Promise<boolean>;
|
||||
deleteUser: () => Promise<boolean>;
|
||||
reveokeSubscription: () => Promise<boolean>;
|
||||
changeModalState: (state: boolean) => void;
|
||||
setUserUuid: (userUuid: string) => Promise<void>;
|
||||
resetState: () => Promise<void>;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
import { GetUserByUuidCommand, UpdateUserCommand } from '@remnawave/backend-contract';
|
||||
import {
|
||||
DeleteUserCommand,
|
||||
DisableUserCommand,
|
||||
EnableUserCommand,
|
||||
GetUserByUuidCommand,
|
||||
RevokeUserSubscriptionCommand,
|
||||
UpdateUserCommand,
|
||||
USERS_STATUS,
|
||||
} from '@remnawave/backend-contract';
|
||||
import { instance } from '@shared/api';
|
||||
import { AxiosError } from 'axios';
|
||||
import { create } from 'zustand';
|
||||
|
|
@ -70,6 +78,108 @@ export const useUserModalStore = create<IState & IActions>()(
|
|||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
disableUser: async (): Promise<boolean> => {
|
||||
try {
|
||||
const userUuid = getState().userUuid;
|
||||
|
||||
if (!userUuid) {
|
||||
throw new Error('User UUID is required');
|
||||
}
|
||||
|
||||
const response = await instance.patch<DisableUserCommand.Response>(
|
||||
DisableUserCommand.url(userUuid)
|
||||
);
|
||||
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
|
||||
set({ user: dataResponse });
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
enableUser: async (): Promise<boolean> => {
|
||||
try {
|
||||
const userUuid = getState().userUuid;
|
||||
|
||||
if (!userUuid) {
|
||||
throw new Error('User UUID is required');
|
||||
}
|
||||
|
||||
const response = await instance.patch<EnableUserCommand.Response>(
|
||||
EnableUserCommand.url(userUuid)
|
||||
);
|
||||
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
|
||||
set({ user: dataResponse });
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
deleteUser: async (): Promise<boolean> => {
|
||||
try {
|
||||
const userUuid = getState().userUuid;
|
||||
|
||||
if (!userUuid) {
|
||||
throw new Error('User UUID is required');
|
||||
}
|
||||
|
||||
await instance.delete<DeleteUserCommand.Response>(
|
||||
DeleteUserCommand.url(userUuid)
|
||||
);
|
||||
|
||||
getState().actions.resetState();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
reveokeSubscription: async (): Promise<boolean> => {
|
||||
try {
|
||||
const userUuid = getState().userUuid;
|
||||
|
||||
if (!userUuid) {
|
||||
throw new Error('User UUID is required');
|
||||
}
|
||||
|
||||
const response =
|
||||
await instance.patch<RevokeUserSubscriptionCommand.Response>(
|
||||
RevokeUserSubscriptionCommand.url(userUuid)
|
||||
);
|
||||
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
|
||||
set({ user: dataResponse });
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
changeModalState: (modalState: boolean) => {
|
||||
set((state) => ({ isModalOpen: modalState }));
|
||||
if (!modalState) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
export * from './data-usage';
|
||||
export * from './status';
|
||||
export * from './username';
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import { RESET_PERIODS } from '@remnawave/backend-contract';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { LuCopy } from 'react-icons/lu';
|
||||
import { Box, Button, Chip, CopyButton, Group, Indicator, Progress, Text } from '@mantine/core';
|
||||
import { IProps } from '@/entitites/dashboard/users/ui/table-columns/username/interface';
|
||||
import { LinkChip } from '@/shared/ui/stuff/link-chip';
|
||||
import { CopyButton } from '@mantine/core'
|
||||
import { LuCopy } from 'react-icons/lu'
|
||||
import { IProps } from '@/entitites/dashboard/users/ui/table-columns/username/interface'
|
||||
import { LinkChip } from '@/shared/ui/stuff/link-chip'
|
||||
|
||||
export function ShortUuidColumnEntity(props: IProps) {
|
||||
const { user } = props;
|
||||
const shortDisplay = user.shortUuid.slice(0, 5);
|
||||
const { user } = props
|
||||
|
||||
const shortDisplay = user.shortUuid.slice(0, 5)
|
||||
|
||||
return (
|
||||
<CopyButton
|
||||
|
|
@ -19,5 +18,5 @@ export function ShortUuidColumnEntity(props: IProps) {
|
|||
</LinkChip>
|
||||
)}
|
||||
</CopyButton>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const LoginForm = () => {
|
|||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Container size={600} my={40}>
|
||||
<Container size={'100%'} my={40}>
|
||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||
<TextInput
|
||||
name="username"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,58 @@
|
|||
import { useState } from 'react';
|
||||
import { PiTrashDuotone } from 'react-icons/pi';
|
||||
import { ActionIcon, Tooltip } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
useUserModalStore,
|
||||
useUserModalStoreUser,
|
||||
} from '@/entitites/dashboard/user-modal-store/user-modal-store';
|
||||
import { IProps } from './interfaces';
|
||||
|
||||
export function DeleteUserFeature(props: IProps) {
|
||||
const { actions } = useUserModalStore();
|
||||
const user = useUserModalStoreUser();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const handleDeleteUser = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await actions.deleteUser();
|
||||
|
||||
notifications.show({
|
||||
title: 'User deleted',
|
||||
message: 'User has been deleted successfully',
|
||||
color: 'green',
|
||||
});
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to delete user',
|
||||
color: 'red',
|
||||
});
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="subtle"
|
||||
color="red"
|
||||
leftSection={<PiTrashDuotone size="1rem" />}
|
||||
<Tooltip
|
||||
label="Delete user"
|
||||
arrowSize={2}
|
||||
transitionProps={{ transition: 'scale-x', duration: 300 }}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<ActionIcon
|
||||
variant="outline"
|
||||
size="xl"
|
||||
radius="xs"
|
||||
color="red"
|
||||
onClick={handleDeleteUser}
|
||||
loading={isLoading}
|
||||
>
|
||||
<PiTrashDuotone size="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,34 @@
|
|||
import { PiClockCounterClockwiseDuotone } from 'react-icons/pi';
|
||||
import { Button } from '@mantine/core';
|
||||
import { ActionIcon, Tooltip } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IProps } from './interfaces';
|
||||
|
||||
export function ResetUsageUserFeature(props: IProps) {
|
||||
const handleResetUsage = async () => {
|
||||
notifications.show({
|
||||
title: 'Reset usage',
|
||||
message: 'Reset usage not yet implemented',
|
||||
color: 'yellow',
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: Implement reset usage
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="subtle"
|
||||
leftSection={<PiClockCounterClockwiseDuotone size="1rem" />}
|
||||
<Tooltip
|
||||
label="Reset usage"
|
||||
arrowSize={2}
|
||||
transitionProps={{ transition: 'scale-x', duration: 300 }}
|
||||
>
|
||||
Reset Usage
|
||||
</Button>
|
||||
<ActionIcon
|
||||
variant="outline"
|
||||
size="xl"
|
||||
radius="xs"
|
||||
color="blue"
|
||||
onClick={handleResetUsage}
|
||||
>
|
||||
<PiClockCounterClockwiseDuotone size="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,57 @@
|
|||
import { Button } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { PiKeyDuotone } from 'react-icons/pi';
|
||||
import { ActionIcon, Button, Tooltip } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
useUserModalStoreActions,
|
||||
useUserModalStoreUser,
|
||||
} from '@/entitites/dashboard/user-modal-store/user-modal-store';
|
||||
import { IProps } from './interfaces';
|
||||
|
||||
export function RevokeSubscriptionUserFeature(props: IProps) {
|
||||
const actions = useUserModalStoreActions();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const user = useUserModalStoreUser();
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const handleRevokeSubscription = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await actions.reveokeSubscription();
|
||||
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Subscription revoked',
|
||||
color: 'green',
|
||||
});
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to revoke subscription',
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button type="button" variant="subtle" color="red">
|
||||
Revoke Subscription
|
||||
</Button>
|
||||
<Tooltip
|
||||
label="Revoke subscription"
|
||||
arrowSize={2}
|
||||
transitionProps={{ transition: 'scale-x', duration: 300 }}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="outline"
|
||||
size="xl"
|
||||
radius="xs"
|
||||
color="green"
|
||||
loading={isLoading}
|
||||
onClick={handleRevokeSubscription}
|
||||
>
|
||||
<PiKeyDuotone size="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from './toggle-user-status-button.feature';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './props.interface';
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { GetInboundsCommand } from '@remnawave/backend-contract';
|
||||
|
||||
export interface IProps {}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { useState } from 'react';
|
||||
import { USERS_STATUS } from '@remnawave/backend-contract';
|
||||
import { PiCellSignalFullDuotone, PiCellSignalSlashDuotone, PiTrashDuotone } from 'react-icons/pi';
|
||||
import { Button } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
useUserModalStoreActions,
|
||||
useUserModalStoreUser,
|
||||
} from '@/entitites/dashboard/user-modal-store/user-modal-store';
|
||||
import { IProps } from './interfaces';
|
||||
|
||||
export function ToggleUserStatusButtonFeature(props: IProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const user = useUserModalStoreUser();
|
||||
const actions = useUserModalStoreActions();
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
let buttonLabel = '';
|
||||
let color = 'blue';
|
||||
let icon = <PiTrashDuotone size="1rem" />;
|
||||
|
||||
if (user.status === USERS_STATUS.DISABLED) {
|
||||
color = 'green';
|
||||
buttonLabel = 'Enable';
|
||||
icon = <PiCellSignalFullDuotone size="1rem" />;
|
||||
} else {
|
||||
color = 'red';
|
||||
buttonLabel = 'Disable';
|
||||
icon = <PiCellSignalSlashDuotone size="1rem" />;
|
||||
}
|
||||
|
||||
const handleToggleUserStatus = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (user.status !== USERS_STATUS.DISABLED) {
|
||||
await actions.disableUser();
|
||||
} else {
|
||||
await actions.enableUser();
|
||||
}
|
||||
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'User status updated',
|
||||
color: 'green',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to toggle user status',
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
size="md"
|
||||
variant="outline"
|
||||
color={color}
|
||||
leftSection={icon}
|
||||
onClick={handleToggleUserStatus}
|
||||
loading={isLoading}
|
||||
>
|
||||
{buttonLabel} user
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import ReactDOM from 'react-dom/client';
|
||||
import { App } from './app';
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { App } from './app'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|
||||
const root = ReactDOM.createRoot(document.getElementById('root')!)
|
||||
root.render(<App />)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const LoginPage = () => {
|
|||
</Title>
|
||||
</Group>
|
||||
|
||||
<Box w={500} maw={800}>
|
||||
<Box w={{ base: 440, sm: 500, md: 500 }} maw={800}>
|
||||
<LoginForm />
|
||||
</Box>
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -1,117 +1,119 @@
|
|||
import { Page } from '@shared/ui/page';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { BsFillClipboard2DataFill } from 'react-icons/bs';
|
||||
import {
|
||||
FaBan,
|
||||
FaCheckCircle,
|
||||
FaExclamationCircle,
|
||||
FaRegDotCircle,
|
||||
FaTimesCircle,
|
||||
FaUsers,
|
||||
} from 'react-icons/fa';
|
||||
import { LuPieChart } from 'react-icons/lu';
|
||||
import { SimpleGrid, Stack, Text } from '@mantine/core'
|
||||
import { Page } from '@shared/ui/page'
|
||||
import {
|
||||
PiChartBarDuotone,
|
||||
PiChartPieSliceDuotone,
|
||||
PiClockCountdownDuotone,
|
||||
PiClockUserDuotone,
|
||||
PiDevicesDuotone,
|
||||
PiMemoryDuotone,
|
||||
PiProhibitDuotone,
|
||||
PiPulseDuotone,
|
||||
PiTimerDuotone,
|
||||
PiUsersDuotone,
|
||||
} from 'react-icons/pi';
|
||||
import { SimpleGrid, Stack, Text } from '@mantine/core';
|
||||
import { LoadingScreen, PageHeader } from '@/shared/ui';
|
||||
import { formatInt } from '@/shared/utils';
|
||||
import { MetricWithIcon } from '@/widgets/dashboard/home/metric-with-icons';
|
||||
import { IProps } from './interfaces';
|
||||
PiUsersDuotone
|
||||
} from 'react-icons/pi'
|
||||
import { LoadingScreen, PageHeader } from '@/shared/ui'
|
||||
import { formatInt } from '@/shared/utils'
|
||||
import { prettyBytesUtil } from '@/shared/utils/bytes'
|
||||
import { MetricWithIcon } from '@/widgets/dashboard/home/metric-with-icons'
|
||||
import { IProps } from './interfaces'
|
||||
|
||||
export const HomePage = (props: IProps) => {
|
||||
const { systemInfo, breadcrumbs } = props;
|
||||
const { systemInfo, breadcrumbs } = props
|
||||
|
||||
if (!systemInfo) {
|
||||
return <LoadingScreen />;
|
||||
return <LoadingScreen />
|
||||
}
|
||||
|
||||
const { users, memory } = systemInfo;
|
||||
const { users, memory } = systemInfo
|
||||
|
||||
console.log(prettyBytesUtil(memory.total))
|
||||
|
||||
const totalRamGB = prettyBytesUtil(memory.total) ?? 0
|
||||
const availableRamGB = prettyBytesUtil(memory.available) ?? 0
|
||||
|
||||
const simpleMetrics = [
|
||||
{
|
||||
icon: PiDevicesDuotone,
|
||||
title: 'Online users',
|
||||
value: formatInt(users.onlineLastMinute ?? 0),
|
||||
color: 'teal',
|
||||
value: formatInt(users.onlineLastMinute) ?? 0,
|
||||
color: 'teal'
|
||||
},
|
||||
{
|
||||
icon: PiUsersDuotone,
|
||||
title: 'Total users',
|
||||
value: formatInt(users.totalUsers ?? 0),
|
||||
color: 'blue',
|
||||
value: formatInt(users.totalUsers) ?? 0,
|
||||
color: 'blue'
|
||||
},
|
||||
{
|
||||
icon: PiChartBarDuotone,
|
||||
title: 'Total traffic',
|
||||
value: prettyBytes(Number(users.totalTrafficBytes) ?? 0),
|
||||
color: 'green',
|
||||
value: prettyBytesUtil(Number(users.totalTrafficBytes)) ?? 0,
|
||||
color: 'green'
|
||||
},
|
||||
{
|
||||
icon: PiMemoryDuotone,
|
||||
title: 'Available RAM',
|
||||
value:
|
||||
prettyBytes(Number(memory.available) ?? 0) +
|
||||
' / ' +
|
||||
prettyBytes(Number(memory.total) ?? 0),
|
||||
color: 'cyan',
|
||||
},
|
||||
];
|
||||
value: `${availableRamGB} / ${totalRamGB}`,
|
||||
color: 'cyan'
|
||||
}
|
||||
]
|
||||
|
||||
const usersMetrics = [
|
||||
{
|
||||
icon: PiPulseDuotone,
|
||||
title: 'Active users',
|
||||
value: users.statusCounts.ACTIVE,
|
||||
color: 'teal',
|
||||
color: 'teal'
|
||||
},
|
||||
{
|
||||
icon: PiClockUserDuotone,
|
||||
title: 'Expired users',
|
||||
value: users.statusCounts.EXPIRED,
|
||||
color: 'red',
|
||||
color: 'red'
|
||||
},
|
||||
{
|
||||
icon: PiClockCountdownDuotone,
|
||||
title: 'Limited users',
|
||||
value: users.statusCounts.LIMITED,
|
||||
color: 'orange',
|
||||
color: 'orange'
|
||||
},
|
||||
{
|
||||
icon: PiProhibitDuotone,
|
||||
title: 'Disabled users',
|
||||
value: users.statusCounts.DISABLED,
|
||||
color: 'gray',
|
||||
},
|
||||
];
|
||||
color: 'gray'
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Page title="Home">
|
||||
<PageHeader title="Short stats" breadcrumbs={breadcrumbs} />
|
||||
<PageHeader
|
||||
title="Short stats"
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
|
||||
<Stack gap="sm" mb="xl">
|
||||
<Stack
|
||||
gap="sm"
|
||||
mb="xl"
|
||||
>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, xl: 4 }}>
|
||||
{simpleMetrics.map((metric) => (
|
||||
<MetricWithIcon key={metric.title} {...metric} />
|
||||
<MetricWithIcon
|
||||
key={metric.title}
|
||||
{...metric}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Text>Users</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, xl: 4 }}>
|
||||
{usersMetrics.map((metric) => (
|
||||
<MetricWithIcon key={metric.title} {...metric} />
|
||||
<MetricWithIcon
|
||||
key={metric.title}
|
||||
{...metric}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import { ROUTES } from '@/shared/constants';
|
||||
import { IBreadcrumb } from '@/shared/interfaces';
|
||||
|
||||
export const BREADCRUMBS: IBreadcrumb[] = [
|
||||
{ label: 'Dashboard', href: ROUTES.DASHBOARD.HOME },
|
||||
{ label: 'Users', href: ROUTES.DASHBOARD.USERS },
|
||||
];
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './constants';
|
||||
47
src/pages/dashboard/hosts/ui/components/hosts.page.tsx
Normal file
47
src/pages/dashboard/hosts/ui/components/hosts.page.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { useEffect } from 'react'
|
||||
|
||||
import { Grid } from '@mantine/core'
|
||||
import { useHostsStoreActions, useHostsStoreHosts } from '@entitites/dashboard'
|
||||
import {
|
||||
useDashboardStoreActions,
|
||||
useDSInbounds
|
||||
} from '@entitites/dashboard/dashboard-store/dashboard-store'
|
||||
import { Page, PageHeader } from '@/shared/ui'
|
||||
import { HostsTableWidget } from '@/widgets/dashboard/hosts/hosts-table'
|
||||
import { BREADCRUMBS } from './constants'
|
||||
import { IProps } from './interfaces'
|
||||
|
||||
export default function HostsPageComponent(props: IProps) {
|
||||
const hosts = useHostsStoreHosts()
|
||||
const actions = useHostsStoreActions()
|
||||
const dsActions = useDashboardStoreActions()
|
||||
const inbounds = useDSInbounds()
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
await actions.getHosts()
|
||||
await dsActions.getInbounds()
|
||||
})()
|
||||
}, [])
|
||||
|
||||
if (!hosts || !inbounds) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Page title="Hosts">
|
||||
<PageHeader
|
||||
title="Hosts"
|
||||
breadcrumbs={BREADCRUMBS}
|
||||
/>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col span={12}>
|
||||
<HostsTableWidget
|
||||
hosts={hosts}
|
||||
inbounds={inbounds}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './props.interface';
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { GetAllUsersCommand } from '@remnawave/backend-contract';
|
||||
import { DataTableColumn, DataTableSortStatus } from 'mantine-datatable';
|
||||
import { User } from '@/entitites/dashboard/users/models';
|
||||
import { DataTableReturn } from '@/pages/dashboard/users/ui/connectors/interfaces';
|
||||
|
||||
export interface IProps {}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { GetAllUsersCommand } from '@remnawave/backend-contract';
|
||||
import { DataTableColumn } from 'mantine-datatable';
|
||||
import { MultiSelect, TextInput } from '@mantine/core';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import {
|
||||
useDashboardStoreActions,
|
||||
useDashboardStoreIsLoading,
|
||||
useDashboardStoreParams,
|
||||
useDashboardStoreSystemInfo,
|
||||
useDashboardStoreTotalUsers,
|
||||
useDashboardStoreUsers,
|
||||
} from '@/entitites/dashboard/dashboard-store/dashboard-store';
|
||||
import {
|
||||
useHostsStoreHosts,
|
||||
useHostsStoreIsLoading,
|
||||
} from '@/entitites/dashboard/hosts/hosts-store/hosts-store';
|
||||
import {
|
||||
useUserCreationModalStoreActions,
|
||||
useUserCreationModalStoreIsModalOpen,
|
||||
} from '@/entitites/dashboard/user-creation-modal-store/user-creation-modal-store';
|
||||
import {
|
||||
useUserModalStoreActions,
|
||||
useUserModalStoreIsModalOpen,
|
||||
} from '@/entitites/dashboard/user-modal-store/user-modal-store';
|
||||
import { getTabDataUsers, User } from '@/entitites/dashboard/users/models';
|
||||
import { DataUsageColumnEntity } from '@/entitites/dashboard/users/ui';
|
||||
import { ShortUuidColumnEntity } from '@/entitites/dashboard/users/ui/table-columns/short-uuid';
|
||||
import { StatusColumnEntity } from '@/entitites/dashboard/users/ui/table-columns/status/status.column';
|
||||
import { UsernameColumnEntity } from '@/entitites/dashboard/users/ui/table-columns/username/username.column';
|
||||
import HostsPageComponent from '@/pages/dashboard/hosts/ui/components/hosts.page';
|
||||
import UsersPageComponent from '@/pages/dashboard/users/ui/components/users.page';
|
||||
import { DataTable } from '@/shared/ui/stuff/data-table';
|
||||
|
||||
export function HostsPageConnector() {
|
||||
const hosts = useHostsStoreHosts();
|
||||
const isHostsLoading = useHostsStoreIsLoading();
|
||||
|
||||
return <HostsPageComponent />;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { DataTableSortStatus } from 'mantine-datatable';
|
||||
import { DataTableFilter } from '@/shared/ui/stuff/data-table/data-table-filters';
|
||||
import { DataTableTabsProps } from '@/shared/ui/stuff/data-table/data-table-tabs';
|
||||
|
||||
export type DataTableReturn<SortableFields> = {
|
||||
readonly tabs: {
|
||||
readonly value: string | undefined;
|
||||
readonly change: (value: string) => void;
|
||||
readonly tabs: DataTableTabsProps['tabs'];
|
||||
};
|
||||
readonly filters: {
|
||||
readonly filters: Record<string, DataTableFilter>;
|
||||
readonly clear: () => void;
|
||||
readonly change: (filter: Omit<DataTableFilter, 'onRemove'>) => void;
|
||||
readonly remove: (name: string) => void;
|
||||
readonly query: Record<string, unknown>;
|
||||
};
|
||||
readonly sort: {
|
||||
readonly change: (status: DataTableSortStatus<SortableFields>) => void;
|
||||
readonly column: keyof SortableFields;
|
||||
readonly direction: 'asc' | 'desc';
|
||||
readonly status: DataTableSortStatus<SortableFields>;
|
||||
readonly query: `${string}:${'asc' | 'desc'}`;
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './data-table-return.type';
|
||||
|
|
@ -1,19 +1,12 @@
|
|||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { GetAllUsersCommand } from '@remnawave/backend-contract';
|
||||
import { DataTableColumn, DataTableSortStatus } from 'mantine-datatable';
|
||||
import { User } from '@/entitites/dashboard/users/models';
|
||||
import { DataTableReturn } from '@/pages/dashboard/users/ui/connectors/interfaces';
|
||||
import { User } from '@entitites/dashboard/users/models'
|
||||
import { DataTableReturn } from '@pages/dashboard/users/ui/connectors/interfaces'
|
||||
import { DataTableColumn } from 'mantine-datatable'
|
||||
|
||||
export interface IProps {
|
||||
users: User[];
|
||||
tabs: DataTableReturn<User>;
|
||||
setSearch: Dispatch<SetStateAction<string>>;
|
||||
search: string;
|
||||
setSearchBy: Dispatch<SetStateAction<GetAllUsersCommand.SearchableField>>;
|
||||
searchBy: string;
|
||||
columns: DataTableColumn<User>[];
|
||||
handleSortStatusChange: (status: { columnAccessor: string; direction: 'asc' | 'desc' }) => void;
|
||||
handlePageChange: (page: number) => void;
|
||||
handleRecordsPerPageChange: (recordsPerPage: number) => void;
|
||||
handleUpdate: () => void;
|
||||
tabs: DataTableReturn<User>
|
||||
columns: DataTableColumn<User>[]
|
||||
handleSortStatusChange: (status: { columnAccessor: string; direction: 'asc' | 'desc' }) => void
|
||||
handlePageChange: (page: number) => void
|
||||
handleRecordsPerPageChange: (recordsPerPage: number) => void
|
||||
handleUpdate: () => void
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,22 @@
|
|||
import { Grid } from '@mantine/core';
|
||||
import { BREADCRUMBS } from '@/pages/dashboard/users/ui/components/constants';
|
||||
import { Page } from '@/shared/ui/page';
|
||||
import { PageHeader } from '@/shared/ui/page-header';
|
||||
import { UsersMetrics } from '@/widgets/dashboard/users/users-metrics';
|
||||
import { UserTableWidget } from '@/widgets/dashboard/users/users-table';
|
||||
import { ViewUserModal } from '@/widgets/dashboard/users/view-user-modal';
|
||||
import { IProps } from './interfaces';
|
||||
import { Grid } from '@mantine/core'
|
||||
import { BREADCRUMBS } from '@/pages/dashboard/users/ui/components/constants'
|
||||
import { Page } from '@/shared/ui/page'
|
||||
import { PageHeader } from '@/shared/ui/page-header'
|
||||
import { CreateUserModalWidget } from '@/widgets/dashboard/users/create-user-modal'
|
||||
import { UsersMetrics } from '@/widgets/dashboard/users/users-metrics'
|
||||
import { UserTableWidget } from '@/widgets/dashboard/users/users-table'
|
||||
import { ViewUserModal } from '@/widgets/dashboard/users/view-user-modal'
|
||||
import { IProps } from './interfaces'
|
||||
|
||||
export default function UsersPageComponent(props: IProps) {
|
||||
const {
|
||||
users,
|
||||
tabs,
|
||||
search,
|
||||
setSearch,
|
||||
searchBy,
|
||||
setSearchBy,
|
||||
columns,
|
||||
handleSortStatusChange,
|
||||
handlePageChange,
|
||||
handleRecordsPerPageChange,
|
||||
handleUpdate,
|
||||
} = props;
|
||||
handleUpdate
|
||||
} = props
|
||||
|
||||
return (
|
||||
<Page title="Users">
|
||||
|
|
@ -34,10 +30,6 @@ export default function UsersPageComponent(props: IProps) {
|
|||
<Grid.Col span={12}>
|
||||
<UserTableWidget
|
||||
tabs={tabs}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
searchBy={searchBy}
|
||||
setSearchBy={setSearchBy}
|
||||
columns={columns}
|
||||
handleSortStatusChange={handleSortStatusChange}
|
||||
handlePageChange={handlePageChange}
|
||||
|
|
@ -48,6 +40,7 @@ export default function UsersPageComponent(props: IProps) {
|
|||
</Grid>
|
||||
|
||||
<ViewUserModal />
|
||||
<CreateUserModalWidget />
|
||||
</Page>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,113 +1,96 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { GetAllUsersCommand } from '@remnawave/backend-contract';
|
||||
import { DataTableColumn } from 'mantine-datatable';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { Box, Group, Indicator, MultiSelect, Select, Text, TextInput } from '@mantine/core';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { MultiSelect, TextInput } from '@mantine/core'
|
||||
import { useDebouncedValue } from '@mantine/hooks'
|
||||
import {
|
||||
useDashboardStoreActions,
|
||||
useDashboardStoreIsLoading,
|
||||
useDashboardStoreParams,
|
||||
useDashboardStoreSystemInfo,
|
||||
useDashboardStoreTotalUsers,
|
||||
useDashboardStoreUsers,
|
||||
} from '@/entitites/dashboard/dashboard-store/dashboard-store';
|
||||
useDashboardStoreSystemInfo
|
||||
} from '@entitites/dashboard/dashboard-store/dashboard-store'
|
||||
import { useUserCreationModalStoreIsModalOpen } from '@entitites/dashboard/user-creation-modal-store/user-creation-modal-store'
|
||||
import {
|
||||
useUserModalStoreActions,
|
||||
useUserModalStoreIsModalOpen,
|
||||
} from '@/entitites/dashboard/user-modal-store/user-modal-store';
|
||||
import { getTabDataUsers, User } from '@/entitites/dashboard/users/models';
|
||||
import { DataUsageColumnEntity } from '@/entitites/dashboard/users/ui';
|
||||
import { ShortUuidColumnEntity } from '@/entitites/dashboard/users/ui/table-columns/short-uuid';
|
||||
import { StatusColumnEntity } from '@/entitites/dashboard/users/ui/table-columns/status/status.column';
|
||||
import { UsernameColumnEntity } from '@/entitites/dashboard/users/ui/table-columns/username/username.column';
|
||||
import UsersPage from '@/pages/dashboard/users/ui/components/users.page';
|
||||
import UsersPageComponent from '@/pages/dashboard/users/ui/components/users.page';
|
||||
import { AddButton } from '@/shared/ui/stuff/add-button';
|
||||
import { DataTable } from '@/shared/ui/stuff/data-table';
|
||||
useUserModalStoreIsModalOpen
|
||||
} from '@entitites/dashboard/user-modal-store/user-modal-store'
|
||||
import { getTabDataUsers, User } from '@entitites/dashboard/users/models'
|
||||
import { DataUsageColumnEntity } from '@entitites/dashboard/users/ui'
|
||||
import { ShortUuidColumnEntity } from '@entitites/dashboard/users/ui/table-columns/short-uuid'
|
||||
import { StatusColumnEntity } from '@entitites/dashboard/users/ui/table-columns/status'
|
||||
import { UsernameColumnEntity } from '@entitites/dashboard/users/ui/table-columns/username'
|
||||
import { GetAllUsersCommand } from '@remnawave/backend-contract'
|
||||
import { DataTable } from '@shared/ui/stuff/data-table'
|
||||
import { DataTableColumn } from 'mantine-datatable'
|
||||
import UsersPageComponent from '../components/users.page'
|
||||
|
||||
export function UsersPageConnector() {
|
||||
const users = useDashboardStoreUsers();
|
||||
const isLoading = useDashboardStoreIsLoading();
|
||||
const totalUsers = useDashboardStoreTotalUsers();
|
||||
const params = useDashboardStoreParams();
|
||||
const actions = useDashboardStoreActions();
|
||||
const systemInfo = useDashboardStoreSystemInfo();
|
||||
const params = useDashboardStoreParams()
|
||||
const actions = useDashboardStoreActions()
|
||||
const systemInfo = useDashboardStoreSystemInfo()
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [debouncedSearch] = useDebouncedValue(search, 300);
|
||||
const [searchBy, setSearchBy] = useState<GetAllUsersCommand.SearchableField>('username');
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const isModalOpen = useUserModalStoreIsModalOpen()
|
||||
const userModalActions = useUserModalStoreActions()
|
||||
|
||||
// create user modal
|
||||
const isCreateUserModalOpen = useUserCreationModalStoreIsModalOpen()
|
||||
|
||||
const dataTab = getTabDataUsers({
|
||||
totalUsers: systemInfo?.users.totalUsers,
|
||||
activeUsers: systemInfo?.users.statusCounts.ACTIVE,
|
||||
disabledUsers: systemInfo?.users.statusCounts.DISABLED,
|
||||
expiredUsers: systemInfo?.users.statusCounts.EXPIRED,
|
||||
limitedUsers: systemInfo?.users.statusCounts.LIMITED,
|
||||
});
|
||||
limitedUsers: systemInfo?.users.statusCounts.LIMITED
|
||||
})
|
||||
|
||||
const [debouncedFilters] = useDebouncedValue(dataTab.filters.filters, 300);
|
||||
const [debouncedFilters] = useDebouncedValue(dataTab.filters.filters, 300)
|
||||
|
||||
useEffect(() => {
|
||||
actions.getSystemInfo();
|
||||
if (isModalOpen || isCreateUserModalOpen) return
|
||||
actions.getSystemInfo()
|
||||
|
||||
const filterEntry = Object.entries(debouncedFilters).find(([_, filter]) => filter?.value);
|
||||
const filterEntry = Object.entries(debouncedFilters).find(([_, filter]) => filter?.value)
|
||||
|
||||
const searchParams = filterEntry
|
||||
? {
|
||||
search: String(filterEntry[1].value),
|
||||
searchBy: filterEntry[0] as GetAllUsersCommand.SearchableField,
|
||||
searchBy: filterEntry[0] as GetAllUsersCommand.SearchableField
|
||||
}
|
||||
: {
|
||||
search: dataTab.tabs.value === '*' ? '' : dataTab.tabs.value,
|
||||
searchBy:
|
||||
dataTab.tabs.value === '*' ? ('username' as const) : ('status' as const),
|
||||
};
|
||||
searchBy: dataTab.tabs.value === '*' ? ('username' as const) : ('status' as const)
|
||||
}
|
||||
|
||||
actions.getUsers(searchParams);
|
||||
}, [dataTab.tabs.value, debouncedFilters]);
|
||||
|
||||
// useEffect(() => {
|
||||
// actions.getSystemInfo();
|
||||
// actions.getUsers({
|
||||
// search: dataTab.tabs.value === '*' ? debouncedSearch : dataTab.tabs.value,
|
||||
// searchBy: dataTab.tabs.value === '*' ? searchBy : 'status',
|
||||
// });
|
||||
// }, [debouncedSearch, searchBy, dataTab.tabs.value]);
|
||||
actions.getUsers(searchParams)
|
||||
}, [dataTab.tabs.value, debouncedFilters, isModalOpen, isCreateUserModalOpen])
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
const offset = (page - 1) * params.limit;
|
||||
actions.getUsers({ offset });
|
||||
};
|
||||
const offset = (page - 1) * params.limit
|
||||
actions.getUsers({ offset })
|
||||
}
|
||||
|
||||
const handleRecordsPerPageChange = (limit: number) => {
|
||||
actions.getUsers({ limit, offset: 0 });
|
||||
};
|
||||
actions.getUsers({ limit, offset: 0 })
|
||||
}
|
||||
|
||||
const handleSortStatusChange = (status: {
|
||||
columnAccessor: string;
|
||||
direction: 'asc' | 'desc';
|
||||
columnAccessor: string
|
||||
direction: 'asc' | 'desc'
|
||||
}) => {
|
||||
actions.getUsers({
|
||||
orderBy: status.columnAccessor as GetAllUsersCommand.SortableField,
|
||||
orderDir: status.direction,
|
||||
});
|
||||
};
|
||||
orderDir: status.direction
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdate = () => {
|
||||
actions.getUsers({ offset: 0 });
|
||||
};
|
||||
|
||||
// User Modal
|
||||
const isModalOpen = useUserModalStoreIsModalOpen();
|
||||
const userModalActions = useUserModalStoreActions();
|
||||
actions.getUsers({ offset: 0 })
|
||||
}
|
||||
|
||||
const handleOpenModal = async (userUuid: string) => {
|
||||
await userModalActions.setUserUuid(userUuid);
|
||||
console.log('userUuid', userUuid);
|
||||
userModalActions.changeModalState(true);
|
||||
// !TODO: Ваня помоги
|
||||
};
|
||||
await userModalActions.setUserUuid(userUuid)
|
||||
userModalActions.changeModalState(true)
|
||||
}
|
||||
|
||||
const columns = useMemo<DataTableColumn<User>[]>(
|
||||
() => [
|
||||
|
|
@ -125,12 +108,12 @@ export function UsersPageConnector() {
|
|||
dataTab.filters.change({
|
||||
name: 'shortUuid',
|
||||
label: 'Sub-link',
|
||||
value: e.currentTarget.value,
|
||||
value: e.currentTarget.value
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (user) => <ShortUuidColumnEntity user={user} />,
|
||||
render: (user) => <ShortUuidColumnEntity user={user} />
|
||||
},
|
||||
{
|
||||
accessor: 'username' as const,
|
||||
|
|
@ -146,12 +129,12 @@ export function UsersPageConnector() {
|
|||
dataTab.filters.change({
|
||||
name: 'username',
|
||||
label: 'Username',
|
||||
value: e.currentTarget.value,
|
||||
value: e.currentTarget.value
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (user) => <UsernameColumnEntity user={user} />,
|
||||
render: (user) => <UsernameColumnEntity user={user} />
|
||||
},
|
||||
{
|
||||
accessor: 'expireAt' as const,
|
||||
|
|
@ -167,44 +150,39 @@ export function UsersPageConnector() {
|
|||
dataTab.filters.change({
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
value,
|
||||
value
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (user) => <StatusColumnEntity user={user} />,
|
||||
render: (user) => <StatusColumnEntity user={user} />
|
||||
},
|
||||
{
|
||||
accessor: 'usedTrafficBytes' as const,
|
||||
title: 'Data usage',
|
||||
width: 150,
|
||||
sortable: true,
|
||||
render: (user) => <DataUsageColumnEntity user={user} />,
|
||||
render: (user) => <DataUsageColumnEntity user={user} />
|
||||
},
|
||||
{
|
||||
accessor: 'actions',
|
||||
title: 'Actions',
|
||||
textAlign: 'right',
|
||||
width: 100,
|
||||
render: (user) => <DataTable.Actions onView={() => handleOpenModal(user.uuid)} />,
|
||||
},
|
||||
render: (user) => <DataTable.Actions onView={() => handleOpenModal(user.uuid)} />
|
||||
}
|
||||
],
|
||||
[]
|
||||
);
|
||||
)
|
||||
|
||||
return (
|
||||
<UsersPageComponent
|
||||
tabs={dataTab}
|
||||
users={users || []}
|
||||
setSearch={setSearch}
|
||||
setSearchBy={setSearchBy}
|
||||
search={search}
|
||||
searchBy={searchBy}
|
||||
columns={columns}
|
||||
handleSortStatusChange={handleSortStatusChange}
|
||||
handlePageChange={handlePageChange}
|
||||
handleRecordsPerPageChange={handleRecordsPerPageChange}
|
||||
handleUpdate={handleUpdate}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { RESET_PERIODS } from '@remnawave/backend-contract';
|
||||
|
||||
export const resetDataStrategy = [
|
||||
{ value: 'NO_RESET', label: 'Never reset' },
|
||||
{ value: 'DAILY', label: 'Reset daily' },
|
||||
{ value: 'WEEKLY', label: 'Reset weekly' },
|
||||
{ value: 'MONTHLY', label: 'Reset monthly' },
|
||||
{ value: 'YEARLY', label: 'Reset yearly' },
|
||||
{ value: RESET_PERIODS.NO_RESET, label: 'Never reset' },
|
||||
{ value: RESET_PERIODS.DAY, label: 'Reset daily' },
|
||||
{ value: RESET_PERIODS.WEEK, label: 'Reset weekly' },
|
||||
{ value: RESET_PERIODS.MONTH, label: 'Reset monthly' },
|
||||
{ value: RESET_PERIODS.YEAR, label: 'Reset yearly' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -7,5 +7,6 @@ export const ROUTES = {
|
|||
ROOT: '/dashboard',
|
||||
HOME: '/dashboard/home',
|
||||
USERS: '/dashboard/users',
|
||||
HOSTS: '/dashboard/hosts',
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { ROUTES } from '@shared/constants/routes';
|
||||
import { useAuth } from '@shared/hooks';
|
||||
import { LoadingScreen } from '@shared/ui/loading-screen';
|
||||
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
||||
|
||||
export function AuthGuard() {
|
||||
console.log('123131');
|
||||
const location = useLocation();
|
||||
|
||||
const { isAuthenticated, isInitialized } = useAuth();
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { GetAllUsersCommand } from '@remnawave/backend-contract';
|
||||
import { createGetQueryHook } from '@shared/api/axios-proxy';
|
||||
|
||||
export const useGetSystemInfo = createGetQueryHook({
|
||||
endpoint: GetAllUsersCommand.url,
|
||||
responseSchema: GetAllUsersCommand.ResponseSchema,
|
||||
rQueryParams: { queryKey: ['system-info'] },
|
||||
});
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './get-system-info.query';
|
||||
|
|
@ -32,6 +32,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||
await actions.getSystemInfo();
|
||||
setIsAuthenticated(true);
|
||||
} catch (error) {
|
||||
setIsAuthenticated(false);
|
||||
} finally {
|
||||
setIsInitialized(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
export * from './auth-provider';
|
||||
export * from './mdx-provider';
|
||||
export * from './auth-provider'
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { ComponentProps } from 'react';
|
||||
import { MDXProvider as MDXDefaultProvider } from '@mdx-js/react';
|
||||
|
||||
type MDXDefaultProviderProps = ComponentProps<typeof MDXDefaultProvider>;
|
||||
|
||||
type MDXProviderProps = Omit<MDXDefaultProviderProps, 'components'>;
|
||||
|
||||
const components: MDXDefaultProviderProps['components'] = {
|
||||
em: (props) => <em style={{ color: 'red' }} {...props} />,
|
||||
};
|
||||
|
||||
export function MDXProvider(props: MDXProviderProps) {
|
||||
return <MDXDefaultProvider components={components} {...props} />;
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { forwardRef, ReactNode, useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { Box, BoxProps } from '@mantine/core';
|
||||
import { nprogress } from '@mantine/nprogress';
|
||||
import { app } from '@/config';
|
||||
|
|
@ -18,10 +17,8 @@ export const Page = forwardRef<HTMLDivElement, PageProps>(
|
|||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{`${title} | ${app.name}`}</title>
|
||||
{meta}
|
||||
</Helmet>
|
||||
<title>{`${title} | ${app.name}`}</title>
|
||||
{meta}
|
||||
|
||||
<Box ref={ref} {...other}>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Card, CardProps } from '@mantine/core';
|
||||
import { Card, CardProps } from '@mantine/core'
|
||||
|
||||
type DataTableContainerProps = CardProps;
|
||||
type DataTableContainerProps = CardProps
|
||||
|
||||
export function DataTableContainer({ children, ...props }: DataTableContainerProps) {
|
||||
return <Card {...props}>{children}</Card>;
|
||||
return <Card {...props}>{children}</Card>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import { Badge, BadgeProps } from '@mantine/core';
|
||||
import { Customer } from '@/api/entities/customers';
|
||||
import { match } from '@/utilities/match';
|
||||
|
||||
interface CustomerStatusBadgeProps extends Omit<BadgeProps, 'children' | 'color'> {
|
||||
status: Customer['status'];
|
||||
}
|
||||
|
||||
export function CustomerStatusBadge({
|
||||
status,
|
||||
variant = 'outline',
|
||||
...props
|
||||
}: CustomerStatusBadgeProps) {
|
||||
const color = match(
|
||||
[status === 'active', 'teal'],
|
||||
[status === 'banned', 'orange'],
|
||||
[status === 'archived', 'red'],
|
||||
[true, 'gray']
|
||||
);
|
||||
|
||||
return (
|
||||
<Badge color={color} variant={variant} {...props}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './customer-status-badge';
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { PiCommand as CommandIcon, PiMagnifyingGlassBold as SearchIcon } from 'react-icons/pi';
|
||||
import {
|
||||
Button,
|
||||
ElementProps,
|
||||
TextInput,
|
||||
UnstyledButton,
|
||||
UnstyledButtonProps,
|
||||
} from '@mantine/core';
|
||||
import { spotlight } from '@mantine/spotlight';
|
||||
import classes from './spotlight-search-bar-button.module.css';
|
||||
|
||||
interface SpotlightSearchBarButtonProps
|
||||
extends Omit<UnstyledButtonProps, 'children'>,
|
||||
ElementProps<'div', keyof UnstyledButtonProps> {
|
||||
placeholder?: string;
|
||||
spotlight: ReactNode;
|
||||
}
|
||||
|
||||
export function SpotlightSearchBarButton({
|
||||
placeholder,
|
||||
spotlight: spotlightComponent,
|
||||
...props
|
||||
}: SpotlightSearchBarButtonProps) {
|
||||
return (
|
||||
<>
|
||||
<UnstyledButton
|
||||
component="div"
|
||||
className={classes.input}
|
||||
onClick={spotlight.open}
|
||||
{...props}
|
||||
>
|
||||
<TextInput
|
||||
placeholder={placeholder}
|
||||
leftSection={<SearchIcon />}
|
||||
rightSection={
|
||||
<Button
|
||||
component="span"
|
||||
size="compact-xs"
|
||||
leftSection={<CommandIcon size="1rem" />}
|
||||
>
|
||||
K
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</UnstyledButton>
|
||||
|
||||
<Button
|
||||
c="inherit"
|
||||
variant="transparent"
|
||||
className={classes.button}
|
||||
onClick={spotlight.open}
|
||||
leftSection={<SearchIcon size="1.2rem" />}
|
||||
rightSection={
|
||||
<Button
|
||||
component="span"
|
||||
variant="filled"
|
||||
size="compact-md"
|
||||
leftSection={<CommandIcon size="1rem" />}
|
||||
>
|
||||
K
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{spotlightComponent}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
.input {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $mantine-breakpoint-xl) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
& input {
|
||||
padding-inline-end: 4rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
& [data-position='right'] {
|
||||
pointer-events: none;
|
||||
--section-size: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
|
||||
@media (min-width: $mantine-breakpoint-xl) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& [data-position] {
|
||||
pointer-events: none;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,19 @@
|
|||
import { fromBytes } from '@tsmx/human-readable';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import xbytes from 'xbytes';
|
||||
import xbytes from 'xbytes'
|
||||
|
||||
export function bytesToGbUtil(bytesInput: number | undefined | string): number | undefined {
|
||||
if (!bytesInput) return undefined;
|
||||
if (typeof bytesInput === 'undefined') return undefined
|
||||
|
||||
if (typeof bytesInput === 'string') {
|
||||
bytesInput = Number(bytesInput);
|
||||
bytesInput = Number(bytesInput)
|
||||
}
|
||||
|
||||
const res = xbytes.parseBytes(bytesInput, {
|
||||
sticky: true,
|
||||
prefixIndex: 3,
|
||||
fixed: 0,
|
||||
iec: true,
|
||||
space: false,
|
||||
});
|
||||
space: false
|
||||
})
|
||||
|
||||
return Number(res.size.replace('GiB', ''));
|
||||
return Number(res.size.replace('GiB', ''))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import xbytes from 'xbytes';
|
||||
|
||||
export function gbToBytesUtil(gbInput: number | undefined): number | undefined {
|
||||
if (!gbInput) return undefined;
|
||||
if (typeof gbInput === 'undefined') return undefined;
|
||||
if (typeof gbInput === 'string') {
|
||||
gbInput = Number(gbInput);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import xbytes from 'xbytes';
|
||||
import xbytes from 'xbytes'
|
||||
|
||||
export function prettyBytesUtil(
|
||||
bytesInput: number | undefined | string,
|
||||
returnZero: boolean = false
|
||||
): string | undefined {
|
||||
if (!bytesInput) {
|
||||
return returnZero ? '0' : undefined;
|
||||
return returnZero ? '0' : undefined
|
||||
}
|
||||
if (typeof bytesInput === 'string') {
|
||||
bytesInput = Number(bytesInput);
|
||||
bytesInput = Number(bytesInput)
|
||||
}
|
||||
|
||||
const res = xbytes.parseBytes(bytesInput, { sticky: true, prefixIndex: 3, iec: true });
|
||||
console.log(res);
|
||||
const res = xbytes.parseBytes(bytesInput, { sticky: true, prefixIndex: 3, iec: true })
|
||||
|
||||
return String(res.size);
|
||||
return String(res.size)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
export * from './boolean';
|
||||
export * from './date';
|
||||
export * from './factory';
|
||||
export * from './form';
|
||||
export * from './is';
|
||||
export * from './match';
|
||||
export * from './money';
|
||||
export * from './number';
|
||||
export * from './phone-number';
|
||||
export * from './pipe';
|
||||
export * from './text';
|
||||
export * from './uid';
|
||||
export * from './boolean'
|
||||
export * from './date'
|
||||
export * from './factory'
|
||||
export * from './form'
|
||||
export * from './is'
|
||||
export * from './match'
|
||||
export * from './number'
|
||||
export * from './pipe'
|
||||
export * from './text'
|
||||
export * from './uid'
|
||||
|
|
|
|||
|
|
@ -1,110 +0,0 @@
|
|||
import { USD } from '@dinero.js/currencies';
|
||||
import {
|
||||
add,
|
||||
dinero,
|
||||
equal,
|
||||
isNegative,
|
||||
isPositive,
|
||||
isZero,
|
||||
multiply,
|
||||
subtract,
|
||||
toDecimal,
|
||||
toSnapshot,
|
||||
transformScale,
|
||||
up,
|
||||
type Dinero,
|
||||
} from 'dinero.js';
|
||||
import { formatCurrency } from './number';
|
||||
|
||||
const CURRENCY = USD;
|
||||
|
||||
export class Money {
|
||||
private readonly dinero: Dinero<number>;
|
||||
|
||||
amountInCents: number;
|
||||
|
||||
amount: number;
|
||||
|
||||
constructor(amountInCents: number | string) {
|
||||
this.dinero = dinero({ amount: Number(amountInCents), currency: CURRENCY });
|
||||
this.amount = Number(toDecimal(this.dinero));
|
||||
this.amountInCents = Number(amountInCents);
|
||||
}
|
||||
|
||||
add(m: Money) {
|
||||
const { amount } = toSnapshot(add(this.dinero, m.dinero));
|
||||
return new Money(amount);
|
||||
}
|
||||
|
||||
subtract(m: Money) {
|
||||
const { amount } = toSnapshot(subtract(this.dinero, m.dinero));
|
||||
return new Money(amount);
|
||||
}
|
||||
|
||||
multiply(multiplier: number, scale = 0) {
|
||||
const { amount } = toSnapshot(
|
||||
transformScale(
|
||||
multiply(this.dinero, { amount: multiplier, scale }),
|
||||
CURRENCY.exponent,
|
||||
up
|
||||
)
|
||||
);
|
||||
return new Money(amount);
|
||||
}
|
||||
|
||||
isGreaterThan(m: Money) {
|
||||
return this.amountInCents > m.amountInCents;
|
||||
}
|
||||
|
||||
isGreaterThanOrEqual(m: Money) {
|
||||
return this.amountInCents >= m.amountInCents;
|
||||
}
|
||||
|
||||
isLessThan(m: Money) {
|
||||
return this.amountInCents < m.amountInCents;
|
||||
}
|
||||
|
||||
isLessThanOrEqual(m: Money) {
|
||||
return this.amountInCents <= m.amountInCents;
|
||||
}
|
||||
|
||||
percentage(percentage: number) {
|
||||
if (percentage < 0 || percentage > 100) {
|
||||
throw new Error('Percentage must be between 0 and 100');
|
||||
}
|
||||
|
||||
const normalize = Math.ceil(percentage * 100);
|
||||
return this.multiply(normalize, 4);
|
||||
}
|
||||
|
||||
isZero() {
|
||||
return isZero(this.dinero);
|
||||
}
|
||||
|
||||
isNegative() {
|
||||
return isNegative(this.dinero);
|
||||
}
|
||||
|
||||
isPositive() {
|
||||
return isPositive(this.dinero);
|
||||
}
|
||||
|
||||
equal(m: Money) {
|
||||
return equal(this.dinero, m.dinero);
|
||||
}
|
||||
|
||||
format() {
|
||||
return toDecimal(this.dinero, ({ value, currency }) =>
|
||||
formatCurrency(value, ` ${currency.code}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface Options {
|
||||
minorUnit?: boolean;
|
||||
}
|
||||
|
||||
export function money(amount: number | string, options: Options = { minorUnit: true }): Money {
|
||||
const arg = options.minorUnit ? amount : Number(amount) * 100;
|
||||
return new Money(arg);
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import parsePhoneNumber, { isValidPhoneNumber as baseIsValidPhoneNumber } from 'libphonenumber-js';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const isValidPhoneNumber = (value: string) => baseIsValidPhoneNumber(value);
|
||||
|
||||
export const phoneNumberSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.refine(isValidPhoneNumber, 'Invalid phone number');
|
||||
|
||||
export function formatPhoneNumber(phoneNumber: string) {
|
||||
return parsePhoneNumber(phoneNumber)?.formatInternational() ?? phoneNumber;
|
||||
}
|
||||
23
src/widgets/dashboard/hosts/host-card/HostCard.module.css
Normal file
23
src/widgets/dashboard/hosts/host-card/HostCard.module.css
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--mantine-radius-md);
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
padding: var(--mantine-spacing-sm) var(--mantine-spacing-xl);
|
||||
padding-left: calc(var(--mantine-spacing-xl) - var(--mantine-spacing-md));
|
||||
margin-bottom: var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
.itemDragging {
|
||||
border: 1px solid light-dark(var(--mantine-color-blue-5), var(--mantine-color-teal-5));
|
||||
box-shadow: var(--mantine-shadow-xl);
|
||||
}
|
||||
|
||||
.dragHandle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--mantine-color-dark-1);
|
||||
padding-right: var(--mantine-spacing-md);
|
||||
}
|
||||
82
src/widgets/dashboard/hosts/host-card/host-card.widget.tsx
Normal file
82
src/widgets/dashboard/hosts/host-card/host-card.widget.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { Badge, Group, Text } from '@mantine/core'
|
||||
import { useDSInboundsHashMap } from '@entitites/dashboard/dashboard-store/dashboard-store'
|
||||
import { Draggable } from '@hello-pangea/dnd'
|
||||
import cx from 'clsx'
|
||||
import ColorHash from 'color-hash'
|
||||
import { PiDotsSixVertical } from 'react-icons/pi'
|
||||
import { IProps } from './interfaces'
|
||||
import classes from './HostCard.module.css'
|
||||
|
||||
export function HostCardWidget(props: IProps) {
|
||||
const { item, index } = props
|
||||
const inbounds = useDSInboundsHashMap()
|
||||
|
||||
if (!inbounds) {
|
||||
return null
|
||||
}
|
||||
|
||||
const inbound = inbounds.get(item.inboundUuid)
|
||||
|
||||
if (!inbound) {
|
||||
return null
|
||||
}
|
||||
|
||||
const ch = new ColorHash()
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={item.uuid}
|
||||
index={index}
|
||||
draggableId={item.uuid}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={cx(classes.item, {
|
||||
[classes.itemDragging]: snapshot.isDragging
|
||||
})}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
>
|
||||
<div
|
||||
{...provided.dragHandleProps}
|
||||
className={classes.dragHandle}
|
||||
>
|
||||
<PiDotsSixVertical
|
||||
size="2rem"
|
||||
color="white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Group>
|
||||
<Badge
|
||||
miw={'15ch'}
|
||||
size="lg"
|
||||
autoContrast
|
||||
variant="light"
|
||||
radius="md"
|
||||
color={ch.hex(item.inboundUuid)}
|
||||
>
|
||||
{inbound.tag}
|
||||
</Badge>
|
||||
<Text
|
||||
fw={400}
|
||||
miw={'30ch'}
|
||||
c={'white'}
|
||||
>
|
||||
{item.remark}
|
||||
</Text>
|
||||
<Text
|
||||
c="dimmed"
|
||||
size="sm"
|
||||
>
|
||||
{item.address}
|
||||
{item.port ? `:${item.port}` : ''}
|
||||
</Text>
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
1
src/widgets/dashboard/hosts/host-card/index.ts
Normal file
1
src/widgets/dashboard/hosts/host-card/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './host-card.widget';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './props.interface';
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { GetAllHostsCommand } from '@remnawave/backend-contract';
|
||||
|
||||
export interface IProps {
|
||||
item: GetAllHostsCommand.Response['response'][number];
|
||||
index: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import { useEffect } from 'react'
|
||||
|
||||
import { Badge, Button, Group, Text } from '@mantine/core'
|
||||
import { useListState } from '@mantine/hooks'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import { PiArrowsClockwise, PiDotsSixVertical, PiHandDuotone, PiPlus } from 'react-icons/pi'
|
||||
import { useHostsStoreActions } from '@/entitites/dashboard'
|
||||
import { DataTable } from '@/shared/ui/stuff/data-table'
|
||||
import { HostCardWidget } from '@/widgets/dashboard/hosts/host-card'
|
||||
import { IProps } from './interfaces'
|
||||
|
||||
export function HostsTableWidget(props: IProps) {
|
||||
const { hosts, inbounds } = props
|
||||
|
||||
const actions = useHostsStoreActions()
|
||||
const [state, handlers] = useListState(hosts)
|
||||
|
||||
const handleDragEnd = async (result: DropResult) => {
|
||||
const { source, destination } = result
|
||||
handlers.reorder({ from: source.index, to: destination?.index || 0 })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
console.log('State updated:', state)
|
||||
const updatedHosts = hosts.map((host) => ({
|
||||
uuid: host.uuid,
|
||||
viewPosition: state.findIndex((stateItem) => stateItem.uuid === host.uuid)
|
||||
}))
|
||||
console.log(updatedHosts)
|
||||
const res = await actions.reorderHosts(updatedHosts)
|
||||
console.log(res)
|
||||
})()
|
||||
}, [state])
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable.Container mb="xl">
|
||||
<DataTable.Title
|
||||
title="Users"
|
||||
description="List of all users"
|
||||
actions={
|
||||
<>
|
||||
<Group>
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
leftSection={<PiArrowsClockwise size="1rem" />}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
leftSection={<PiPlus size="1rem" />}
|
||||
>
|
||||
Create new user
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</DataTable.Container>
|
||||
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<Droppable
|
||||
droppableId="dnd-list"
|
||||
direction="vertical"
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{state.map((item, index) => (
|
||||
<HostCardWidget
|
||||
item={item}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</>
|
||||
)
|
||||
}
|
||||
1
src/widgets/dashboard/hosts/hosts-table/index.ts
Normal file
1
src/widgets/dashboard/hosts/hosts-table/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './hosts-table.widget';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './props.interface';
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Dispatch, SetStateAction } from 'react';
|
||||
import {
|
||||
GetAllHostsCommand,
|
||||
GetAllUsersCommand,
|
||||
GetInboundsCommand,
|
||||
} from '@remnawave/backend-contract';
|
||||
import { DataTableColumn, DataTableSortStatus } from 'mantine-datatable';
|
||||
import { User } from '@/entitites/dashboard/users/models';
|
||||
import { DataTableReturn } from '@/pages/dashboard/users/ui/connectors/interfaces';
|
||||
|
||||
export interface IProps {
|
||||
hosts: GetAllHostsCommand.Response['response'];
|
||||
inbounds: GetInboundsCommand.Response['response'];
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Group,
|
||||
Modal,
|
||||
NumberInput,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput
|
||||
} from '@mantine/core'
|
||||
import { DateTimePicker } from '@mantine/dates'
|
||||
import { useForm, zodResolver } from '@mantine/form'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import { CreateUserCommand, USERS_STATUS } from '@remnawave/backend-contract'
|
||||
import { LoaderModalShared } from '@shared/ui/loader-modal'
|
||||
import {
|
||||
PiCalendarDuotone,
|
||||
PiClockDuotone,
|
||||
PiFloppyDiskDuotone,
|
||||
PiUserDuotone
|
||||
} from 'react-icons/pi'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
useDashboardStoreActions,
|
||||
useDSInbounds
|
||||
} from '@/entitites/dashboard/dashboard-store/dashboard-store'
|
||||
import {
|
||||
useUserCreationModalStoreActions,
|
||||
useUserCreationModalStoreIsModalOpen
|
||||
} from '@/entitites/dashboard/user-creation-modal-store/user-creation-modal-store'
|
||||
import { resetDataStrategy } from '@/shared/constants'
|
||||
import { handleFormErrors } from '@/shared/utils'
|
||||
import { gbToBytesUtil } from '@/shared/utils/bytes'
|
||||
import { InboundCheckboxCardWidget } from '../inbound-checkbox-card'
|
||||
|
||||
export const CreateUserModalWidget = () => {
|
||||
const isModalOpen = useUserCreationModalStoreIsModalOpen()
|
||||
const actions = useUserCreationModalStoreActions()
|
||||
const inbounds = useDSInbounds()
|
||||
const actionsDS = useDashboardStoreActions()
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isDataSubmitting, setIsDataSubmitting] = useState(false)
|
||||
|
||||
const form = useForm<CreateUserCommand.Request>({
|
||||
name: 'create-user-form',
|
||||
mode: 'uncontrolled',
|
||||
validate: zodResolver(CreateUserCommand.RequestSchema),
|
||||
initialValues: {
|
||||
status: USERS_STATUS.ACTIVE,
|
||||
username: '',
|
||||
trafficLimitStrategy: 'NO_RESET',
|
||||
expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
||||
trafficLimitBytes: 0,
|
||||
activeUserInbounds: []
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout | undefined
|
||||
if (isModalOpen) {
|
||||
;(async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await actionsDS.getInbounds()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
timeout = setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 300)
|
||||
}
|
||||
})()
|
||||
if (!isModalOpen) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isModalOpen])
|
||||
|
||||
const handleSubmit = form.onSubmit(async (values) => {
|
||||
try {
|
||||
setIsDataSubmitting(true)
|
||||
const createData = {
|
||||
username: values.username,
|
||||
trafficLimitStrategy: values.trafficLimitStrategy,
|
||||
trafficLimitBytes: gbToBytesUtil(values.trafficLimitBytes),
|
||||
expireAt: values.expireAt,
|
||||
activeUserInbounds: values.activeUserInbounds,
|
||||
status: values.status
|
||||
}
|
||||
|
||||
const res = await actions.createUser(createData)
|
||||
|
||||
if (res) {
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'User created successfully',
|
||||
color: 'green'
|
||||
})
|
||||
handleCloseModal()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error('Zod validation error:', error.errors)
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
console.error('Error message:', error.message)
|
||||
console.error('Error stack:', error.stack)
|
||||
}
|
||||
|
||||
handleFormErrors(form, error)
|
||||
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: error instanceof Error ? error.message : 'Failed to update user',
|
||||
color: 'red'
|
||||
})
|
||||
} finally {
|
||||
setIsDataSubmitting(false)
|
||||
}
|
||||
})
|
||||
|
||||
const handleCloseModal = () => {
|
||||
actions.changeModalState(false)
|
||||
form.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal opened={isModalOpen} onClose={handleCloseModal} title="Create user" centered>
|
||||
{isLoading ? (
|
||||
<LoaderModalShared text="Loading user creation..." h="500" />
|
||||
) : (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Group align="flex-start" grow={false}>
|
||||
<Stack gap="md" w={400}>
|
||||
<TextInput
|
||||
label="Username"
|
||||
description="Username cannot be changed later"
|
||||
key={form.key('username')}
|
||||
{...form.getInputProps('username')}
|
||||
radius="xs"
|
||||
leftSection={<PiUserDuotone size="1rem" />}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
leftSection={
|
||||
<>
|
||||
<Text
|
||||
ta="center"
|
||||
size="0.75rem"
|
||||
w={26}
|
||||
display="flex"
|
||||
style={{ justifyContent: 'center' }}
|
||||
>
|
||||
GB
|
||||
</Text>
|
||||
<Divider orientation="vertical" />
|
||||
</>
|
||||
}
|
||||
radius="xs"
|
||||
label="Data Limit"
|
||||
description="Enter data limit in GB, 0 for unlimited"
|
||||
allowDecimal={false}
|
||||
defaultValue={0}
|
||||
decimalScale={0}
|
||||
key={form.key('trafficLimitBytes')}
|
||||
{...form.getInputProps('trafficLimitBytes')}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Traffic reset strategy"
|
||||
description="How often the user's traffic should be reset"
|
||||
placeholder="Pick value"
|
||||
radius="xs"
|
||||
allowDeselect={false}
|
||||
defaultValue={form.values.trafficLimitStrategy}
|
||||
data={resetDataStrategy}
|
||||
leftSection={<PiClockDuotone size="1rem" />}
|
||||
key={form.key('trafficLimitStrategy')}
|
||||
{...form.getInputProps('trafficLimitStrategy')}
|
||||
/>
|
||||
|
||||
<DateTimePicker
|
||||
label="Expiry Date"
|
||||
valueFormat="MMMM D, YYYY - HH:mm"
|
||||
key={form.key('expireAt')}
|
||||
{...form.getInputProps('expireAt')}
|
||||
leftSection={<PiCalendarDuotone size="1rem" />}
|
||||
/>
|
||||
|
||||
<Checkbox.Group
|
||||
key={form.key('activeUserInbounds')}
|
||||
{...form.getInputProps('activeUserInbounds')}
|
||||
label="Inbounds"
|
||||
description="Select available inbounds for this user"
|
||||
>
|
||||
<SimpleGrid
|
||||
pt="md"
|
||||
cols={{
|
||||
base: 1,
|
||||
sm: 1,
|
||||
md: 2
|
||||
}}
|
||||
>
|
||||
{inbounds?.map((inbound) => (
|
||||
<>
|
||||
<InboundCheckboxCardWidget
|
||||
key={inbound.uuid}
|
||||
inbound={inbound}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Checkbox.Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
<Group justify="right" mt="xl">
|
||||
<Button
|
||||
type="submit"
|
||||
color="teal"
|
||||
leftSection={<PiFloppyDiskDuotone size="1rem" />}
|
||||
variant="outline"
|
||||
size="md"
|
||||
loading={isDataSubmitting}
|
||||
>
|
||||
Create user
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
1
src/widgets/dashboard/users/create-user-modal/index.ts
Normal file
1
src/widgets/dashboard/users/create-user-modal/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './create-user-modal.widget';
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { CreateUserCommand } from '@remnawave/backend-contract';
|
||||
|
||||
export interface IFormValues extends CreateUserCommand.Request {}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './form-values.interface';
|
||||
|
|
@ -1,28 +1,28 @@
|
|||
.root {
|
||||
position: relative;
|
||||
padding: var(--mantine-spacing-md);
|
||||
transition: border-color 150ms ease;
|
||||
max-width: 50%;
|
||||
position: relative;
|
||||
padding: var(--mantine-spacing-xs);
|
||||
transition: border-color 150ms ease;
|
||||
|
||||
&[data-checked] {
|
||||
border-color: var(--mantine-primary-color-filled);
|
||||
}
|
||||
|
||||
@mixin hover {
|
||||
@mixin light {
|
||||
background-color: var(--mantine-color-gray-0);
|
||||
&[data-checked] {
|
||||
border-color: var(--mantine-primary-color-filled);
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
@mixin hover {
|
||||
@mixin light {
|
||||
background-color: var(--mantine-color-gray-0);
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-family: var(--mantine-font-family-monospace);
|
||||
font-weight: bold;
|
||||
font-size: var(--mantine-font-size-md);
|
||||
line-height: 1.3;
|
||||
color: var(--mantine-color-bright);
|
||||
font-family: var(--mantine-font-family-monospace);
|
||||
font-weight: bold;
|
||||
font-size: var(--mantine-font-size-md);
|
||||
line-height: 1.3;
|
||||
color: var(--mantine-color-bright);
|
||||
max-width: 13ch;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ export function InboundCheckboxCardWidget(props: IProps) {
|
|||
const { inbound } = props;
|
||||
return (
|
||||
<Checkbox.Card className={classes.root} radius="md" value={inbound.uuid} key={inbound.uuid}>
|
||||
<Group wrap="nowrap" align="flex-start">
|
||||
<Group align="flex-start" gap="xs">
|
||||
<Checkbox.Indicator />
|
||||
<div>
|
||||
<Text className={classes.label}>{inbound.tag}</Text>
|
||||
<Badge variant="outline" size="sm" color="gray">
|
||||
<Text size="sm" className={classes.label} truncate="end">
|
||||
{inbound.tag}
|
||||
</Text>
|
||||
<Badge variant="outline" size="xs" color="gray">
|
||||
{inbound.type}
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { GetAllUsersCommand } from '@remnawave/backend-contract';
|
||||
import { DataTableColumn, DataTableSortStatus } from 'mantine-datatable';
|
||||
import { User } from '@/entitites/dashboard/users/models';
|
||||
import { DataTableReturn } from '@/pages/dashboard/users/ui/connectors/interfaces';
|
||||
import { User } from '@entitites/dashboard/users/models'
|
||||
import { DataTableReturn } from '@pages/dashboard/users/ui/connectors/interfaces'
|
||||
import { DataTableColumn } from 'mantine-datatable'
|
||||
|
||||
export interface IProps {
|
||||
tabs: DataTableReturn<User>;
|
||||
setSearch: Dispatch<SetStateAction<string>>;
|
||||
search: string;
|
||||
setSearchBy: Dispatch<SetStateAction<GetAllUsersCommand.SearchableField>>;
|
||||
searchBy: string;
|
||||
columns: DataTableColumn<User>[];
|
||||
handleSortStatusChange: (status: { columnAccessor: string; direction: 'asc' | 'desc' }) => void;
|
||||
handlePageChange: (page: number) => void;
|
||||
handleRecordsPerPageChange: (recordsPerPage: number) => void;
|
||||
handleUpdate: () => void;
|
||||
tabs: DataTableReturn<User>
|
||||
columns: DataTableColumn<User>[]
|
||||
handleSortStatusChange: (status: { columnAccessor: string; direction: 'asc' | 'desc' }) => void
|
||||
handlePageChange: (page: number) => void
|
||||
handleRecordsPerPageChange: (recordsPerPage: number) => void
|
||||
handleUpdate: () => void
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,69 +1,58 @@
|
|||
import { ChangeEvent, useState } from 'react';
|
||||
import { GetAllUsersCommand } from '@remnawave/backend-contract';
|
||||
import { LuRefreshCcw } from 'react-icons/lu';
|
||||
import { PiArrowCircleDownDuotone, PiDownload } from 'react-icons/pi';
|
||||
import { Box, Button, Group, Select, TextInput } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { ChangeEvent, useState } from 'react'
|
||||
|
||||
import { Button, Group } from '@mantine/core'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import {
|
||||
useDashboardStoreIsLoading,
|
||||
useDashboardStoreParams,
|
||||
useDashboardStoreTotalUsers,
|
||||
useDashboardStoreUsers,
|
||||
} from '@/entitites/dashboard/dashboard-store/dashboard-store';
|
||||
import {
|
||||
useUserModalStoreActions,
|
||||
useUserModalStoreIsModalOpen,
|
||||
} from '@/entitites/dashboard/user-modal-store/user-modal-store';
|
||||
import { AddButton } from '@/shared/ui/stuff/add-button';
|
||||
import { DataTable } from '@/shared/ui/stuff/data-table';
|
||||
import { IProps } from './interfaces';
|
||||
useDashboardStoreUsersLoading
|
||||
} from '@entitites/dashboard/dashboard-store/dashboard-store'
|
||||
import { useUserCreationModalStoreActions } from '@entitites/dashboard/user-creation-modal-store/user-creation-modal-store'
|
||||
import { GetAllUsersCommand } from '@remnawave/backend-contract'
|
||||
import { PiArrowsClockwise, PiPlus } from 'react-icons/pi'
|
||||
import { DataTable } from '@/shared/ui/stuff/data-table'
|
||||
import { IProps } from './interfaces'
|
||||
|
||||
export function UserTableWidget(props: IProps) {
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
searchBy,
|
||||
setSearchBy,
|
||||
tabs: tabsProps,
|
||||
columns,
|
||||
handleSortStatusChange,
|
||||
handlePageChange,
|
||||
handleRecordsPerPageChange,
|
||||
handleUpdate,
|
||||
} = props;
|
||||
handleUpdate
|
||||
} = props
|
||||
|
||||
const { tabs, filters } = tabsProps;
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const { tabs, filters } = tabsProps
|
||||
|
||||
const params = useDashboardStoreParams();
|
||||
const isLoading = useDashboardStoreIsLoading();
|
||||
const users = useDashboardStoreUsers();
|
||||
const totalUsers = useDashboardStoreTotalUsers();
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
|
||||
const handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearch(e.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleSelectSearch = (value: string | null) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
setSearchBy(value as GetAllUsersCommand.SearchableField);
|
||||
};
|
||||
const params = useDashboardStoreParams()
|
||||
const isLoading = useDashboardStoreIsLoading()
|
||||
const isUsersLoading = useDashboardStoreUsersLoading()
|
||||
const users = useDashboardStoreUsers()
|
||||
const totalUsers = useDashboardStoreTotalUsers()
|
||||
const userCreationModalActions = useUserCreationModalStoreActions()
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
setIsRefreshing(true)
|
||||
|
||||
handleUpdate();
|
||||
handleUpdate()
|
||||
setTimeout(() => {
|
||||
notifications.show({
|
||||
title: 'Success',
|
||||
message: 'Users fetched successfully',
|
||||
});
|
||||
message: 'Users fetched successfully'
|
||||
})
|
||||
|
||||
setIsRefreshing(false);
|
||||
}, 1000);
|
||||
};
|
||||
setIsRefreshing(false)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleOpenCreateUserModal = async () => {
|
||||
userCreationModalActions.changeModalState(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable.Container>
|
||||
|
|
@ -76,16 +65,21 @@ export function UserTableWidget(props: IProps) {
|
|||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
leftSection={<LuRefreshCcw size="1rem" />}
|
||||
leftSection={<PiArrowsClockwise size="1rem" />}
|
||||
onClick={handleRefresh}
|
||||
loading={isRefreshing}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
|
||||
<AddButton variant="default" size="xs">
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
leftSection={<PiPlus size="1rem" />}
|
||||
onClick={handleOpenCreateUserModal}
|
||||
>
|
||||
Create new user
|
||||
</AddButton>
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
}
|
||||
|
|
@ -94,15 +88,6 @@ export function UserTableWidget(props: IProps) {
|
|||
<DataTable.Filters filters={filters.filters} onClear={filters.clear} />
|
||||
<DataTable.Tabs tabs={tabs.tabs} onChange={tabs.change} />
|
||||
|
||||
{/* <Group mb="md">
|
||||
<TextInput placeholder="Search..." value={search} onChange={handleSearch} />
|
||||
<Select
|
||||
data={GetAllUsersCommand.SearchableFields}
|
||||
value={searchBy}
|
||||
onChange={handleSelectSearch}
|
||||
/>
|
||||
</Group> */}
|
||||
|
||||
<DataTable.Content>
|
||||
<DataTable.Table
|
||||
withTableBorder
|
||||
|
|
@ -121,17 +106,17 @@ export function UserTableWidget(props: IProps) {
|
|||
page={params.offset / params.limit + 1}
|
||||
onPageChange={handlePageChange}
|
||||
records={users || []}
|
||||
fetching={isLoading}
|
||||
fetching={isUsersLoading}
|
||||
recordsPerPage={params.limit}
|
||||
totalRecords={totalUsers}
|
||||
sortStatus={{
|
||||
columnAccessor: params.orderBy || 'createdAt',
|
||||
direction: params.orderDir || 'desc',
|
||||
direction: params.orderDir || 'desc'
|
||||
}}
|
||||
onSortStatusChange={handleSortStatusChange}
|
||||
columns={columns}
|
||||
/>
|
||||
</DataTable.Content>
|
||||
</DataTable.Container>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,6 @@ import { UpdateUserCommand } from '@remnawave/backend-contract';
|
|||
|
||||
export interface IFormValues extends UpdateUserCommand.Request {
|
||||
username: string;
|
||||
shortUuid: string;
|
||||
trojanPassword: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { UpdateUserCommand } from '@remnawave/backend-contract';
|
||||
import { PiUserDuotone } from 'react-icons/pi';
|
||||
import { LoaderModalShared } from '@shared/ui/loader-modal';
|
||||
import {
|
||||
PiCalendarDuotone,
|
||||
PiClockDuotone,
|
||||
PiFloppyDiskDuotone,
|
||||
PiLinkDuotone,
|
||||
PiUserDuotone,
|
||||
} from 'react-icons/pi';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
|
|
@ -10,7 +18,9 @@ import {
|
|||
Group,
|
||||
Modal,
|
||||
NumberInput,
|
||||
Progress,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
|
|
@ -30,10 +40,11 @@ import {
|
|||
import { DeleteUserFeature } from '@/features/ui/dashboard/users/delete-user';
|
||||
import { ResetUsageUserFeature } from '@/features/ui/dashboard/users/reset-usage-user';
|
||||
import { RevokeSubscriptionUserFeature } from '@/features/ui/dashboard/users/revoke-subscription-user';
|
||||
import { ToggleUserStatusButtonFeature } from '@/features/ui/dashboard/users/toggle-user-status-button';
|
||||
import { resetDataStrategy } from '@/shared/constants';
|
||||
import { handleFormErrors } from '@/shared/utils';
|
||||
import { bytesToGbUtil, gbToBytesUtil } from '@/shared/utils/bytes';
|
||||
import { LoaderModalShared } from '../../../../shared/ui/loader-modal';
|
||||
import { bytesToGbUtil, gbToBytesUtil, prettyBytesUtil } from '@/shared/utils/bytes';
|
||||
import { UserStatusBadge } from '@/widgets/dashboard/users/user-status-badge';
|
||||
import { InboundCheckboxCardWidget } from '../inbound-checkbox-card';
|
||||
import { IFormValues } from './interfaces';
|
||||
|
||||
|
|
@ -48,6 +59,7 @@ export const ViewUserModal = () => {
|
|||
const [isDataSubmitting, setIsDataSubmitting] = useState(false);
|
||||
|
||||
const form = useForm<IFormValues>({
|
||||
name: 'edit-user-form',
|
||||
mode: 'uncontrolled',
|
||||
validate: zodResolver(UpdateUserCommand.RequestSchema),
|
||||
});
|
||||
|
|
@ -77,6 +89,7 @@ export const ViewUserModal = () => {
|
|||
useEffect(() => {
|
||||
if (user && inbounds) {
|
||||
const activeInboundUuids = user.activeUserInbounds.map((inbound) => inbound.uuid);
|
||||
|
||||
form.setValues({
|
||||
uuid: user.uuid,
|
||||
username: user.username,
|
||||
|
|
@ -84,10 +97,19 @@ export const ViewUserModal = () => {
|
|||
trafficLimitStrategy: user.trafficLimitStrategy,
|
||||
expireAt: user.expireAt ? new Date(user.expireAt) : new Date(),
|
||||
activeUserInbounds: activeInboundUuids,
|
||||
shortUuid: user.shortUuid,
|
||||
trojanPassword: user.trojanPassword,
|
||||
});
|
||||
}
|
||||
}, [user, inbounds]);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const usedTrafficPercentage = (user.usedTrafficBytes / user.trafficLimitBytes) * 100;
|
||||
const totalUsedTraffic = prettyBytesUtil(user.usedTrafficBytes, true);
|
||||
|
||||
const handleSubmit = form.onSubmit(async (values) => {
|
||||
try {
|
||||
setIsDataSubmitting(true);
|
||||
|
|
@ -107,8 +129,6 @@ export const ViewUserModal = () => {
|
|||
color: 'green',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Full error:', error);
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error('Zod validation error:', error.errors);
|
||||
}
|
||||
|
|
@ -119,6 +139,7 @@ export const ViewUserModal = () => {
|
|||
}
|
||||
|
||||
handleFormErrors(form, error);
|
||||
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: error instanceof Error ? error.message : 'Failed to update user',
|
||||
|
|
@ -126,7 +147,6 @@ export const ViewUserModal = () => {
|
|||
});
|
||||
} finally {
|
||||
setIsDataSubmitting(false);
|
||||
actions.changeModalState(false);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -135,32 +155,54 @@ export const ViewUserModal = () => {
|
|||
opened={isModalOpen}
|
||||
onClose={() => actions.changeModalState(false)}
|
||||
title="Edit user"
|
||||
size="lg"
|
||||
size="900px"
|
||||
centered
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderModalShared text="Loading user data..." h="400" />
|
||||
) : (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Username"
|
||||
description="Username cannot be changed"
|
||||
{...form.getInputProps('username')}
|
||||
disabled
|
||||
radius="xs"
|
||||
leftSection={<PiUserDuotone size="1rem" />}
|
||||
/>
|
||||
<Group align="flex-start" grow={false}>
|
||||
{/* Left Section - User Settings */}
|
||||
<Stack gap="md" w={400}>
|
||||
<Group gap="xs" justify="space-between" w="100%">
|
||||
<Text fw={500}>User details</Text>
|
||||
<UserStatusBadge status={user.status} />
|
||||
</Group>
|
||||
|
||||
<TextInput
|
||||
label="Username"
|
||||
description="Username cannot be changed"
|
||||
key={form.key('username')}
|
||||
{...form.getInputProps('username')}
|
||||
disabled
|
||||
radius="xs"
|
||||
leftSection={<PiUserDuotone size="1rem" />}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Subscription short uuid"
|
||||
key={form.key('shortUuid')}
|
||||
{...form.getInputProps('shortUuid')}
|
||||
disabled
|
||||
radius="xs"
|
||||
leftSection={<PiLinkDuotone size="1rem" />}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Divider orientation="vertical" />
|
||||
|
||||
{/* Right Section - Connection Details */}
|
||||
<Stack gap="md" w={400}>
|
||||
<Text fw={500}>Connection Details</Text>
|
||||
|
||||
<Group grow align="flex-start">
|
||||
<NumberInput
|
||||
{...form.getInputProps('trafficLimitBytes')}
|
||||
leftSection={
|
||||
<>
|
||||
<Text
|
||||
ta="center"
|
||||
size="1rem"
|
||||
w={28}
|
||||
size="0.75rem"
|
||||
w={26}
|
||||
display="flex"
|
||||
style={{ justifyContent: 'center' }}
|
||||
>
|
||||
|
|
@ -169,55 +211,100 @@ export const ViewUserModal = () => {
|
|||
<Divider orientation="vertical" />
|
||||
</>
|
||||
}
|
||||
radius="xs"
|
||||
label="Data Limit"
|
||||
description="Enter data limit in GB, 0 for unlimited"
|
||||
allowDecimal={false}
|
||||
decimalScale={0}
|
||||
key={form.key('trafficLimitBytes')}
|
||||
{...form.getInputProps('trafficLimitBytes')}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Progress
|
||||
radius="xs"
|
||||
size="xl"
|
||||
value={usedTrafficPercentage}
|
||||
color={usedTrafficPercentage > 100 ? 'yellow' : 'cyan'}
|
||||
striped
|
||||
/>
|
||||
<Group gap="xs" justify="center" mt={2}>
|
||||
<Text size="sm" c="white" fw={500}>
|
||||
{totalUsedTraffic === '0' ? '' : totalUsedTraffic}
|
||||
</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Select
|
||||
label="Traffic reset strategy"
|
||||
description="How often the user's traffic should be reset"
|
||||
placeholder="Pick value"
|
||||
radius="xs"
|
||||
allowDeselect={false}
|
||||
defaultValue={form.values.trafficLimitStrategy}
|
||||
data={resetDataStrategy}
|
||||
leftSection={<PiClockDuotone size="1rem" />}
|
||||
key={form.key('trafficLimitStrategy')}
|
||||
{...form.getInputProps('trafficLimitStrategy')}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<DateTimePicker
|
||||
label="Expiry Date"
|
||||
valueFormat="MMMM D, YYYY - HH:mm"
|
||||
{...form.getInputProps('expireAt')}
|
||||
clearable
|
||||
/>
|
||||
<DateTimePicker
|
||||
label="Expiry Date"
|
||||
valueFormat="MMMM D, YYYY - HH:mm"
|
||||
key={form.key('expireAt')}
|
||||
{...form.getInputProps('expireAt')}
|
||||
leftSection={<PiCalendarDuotone size="1rem" />}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<Stack gap="xs">
|
||||
<Checkbox.Group
|
||||
{...form.getInputProps('activeUserInbounds')}
|
||||
label="Inbounds"
|
||||
description="Select available inbounds for this user"
|
||||
<Checkbox.Group
|
||||
key={form.key('activeUserInbounds')}
|
||||
{...form.getInputProps('activeUserInbounds')}
|
||||
label="Inbounds"
|
||||
description="Select available inbounds for this user"
|
||||
>
|
||||
<SimpleGrid
|
||||
pt="md"
|
||||
cols={{
|
||||
base: 1,
|
||||
sm: 1,
|
||||
md: 2,
|
||||
}}
|
||||
>
|
||||
<Stack pt="md" gap="xs">
|
||||
{inbounds?.map((inbound) => (
|
||||
<InboundCheckboxCardWidget inbound={inbound} />
|
||||
))}
|
||||
</Stack>
|
||||
</Checkbox.Group>
|
||||
</Stack>
|
||||
</Box>
|
||||
{inbounds?.map((inbound) => (
|
||||
<>
|
||||
<InboundCheckboxCardWidget
|
||||
key={inbound.uuid}
|
||||
inbound={inbound}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Checkbox.Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
<Group justify="space-between" mt="xl">
|
||||
<Group>
|
||||
<Group justify="space-between" mt="xl">
|
||||
<Group>
|
||||
<ActionIcon.Group>
|
||||
<DeleteUserFeature />
|
||||
<ResetUsageUserFeature />
|
||||
<RevokeSubscriptionUserFeature />
|
||||
</Group>
|
||||
|
||||
<Button type="submit" color="blue" loading={isDataSubmitting}>
|
||||
</ActionIcon.Group>
|
||||
</Group>
|
||||
<Group>
|
||||
<ToggleUserStatusButtonFeature />
|
||||
<Button
|
||||
type="submit"
|
||||
color="blue"
|
||||
leftSection={<PiFloppyDiskDuotone size="1rem" />}
|
||||
variant="outline"
|
||||
size="md"
|
||||
loading={isDataSubmitting}
|
||||
>
|
||||
Edit user
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
|
|
|
|||
4842
stats.html
Normal file
4842
stats.html
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,29 +1,53 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"node",
|
||||
],
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"removeComments": true,
|
||||
"target": "ES2020",
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"skipLibCheck": true,
|
||||
/* ------------------------------------------------------------ */
|
||||
/* Bundler mode */
|
||||
/* ------------------------------------------------------------ */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* ------------------------------------------------------------ */
|
||||
/* "useDefineForClassFields": true,
|
||||
/* "esModuleInterop": false,
|
||||
/* "allowSyntheticDefaultImports": true,
|
||||
"allowJs": false,
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false, // TURN THIS
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
/* ------------------------------------------------------------ */
|
||||
/* Paths */
|
||||
/* ------------------------------------------------------------ */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@entitites/*": [
|
||||
"./src/entitites/*"
|
||||
],
|
||||
"@features/*": [
|
||||
"./src/features/*"
|
||||
],
|
||||
"@pages/*": [
|
||||
"./src/pages/*"
|
||||
],
|
||||
"@widgets/*": [
|
||||
"./src/widgets/*"
|
||||
],
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
|
|
@ -38,5 +62,10 @@
|
|||
"include": [
|
||||
"src",
|
||||
"client.d.ts",
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
tsconfig.node.json
Normal file
13
tsconfig.node.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { fileURLToPath, URL } from 'node:url';
|
||||
import mdx from '@mdx-js/rollup';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import * as dotenv from 'dotenv';
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
dotenv.config({ path: `${__dirname}/.env` });
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tsconfigPaths(), mdx()],
|
||||
define: {
|
||||
__DOMAIN_BACKEND__: JSON.stringify(process.env.DOMAIN_BACKEND).trim(),
|
||||
__NODE_ENV__: JSON.stringify(process.env.NODE_ENV).trim(),
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3333,
|
||||
cors: false,
|
||||
strictPort: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@public': fileURLToPath(new URL('./public', import.meta.url)),
|
||||
'@shared': fileURLToPath(new URL('./src/shared', import.meta.url)),
|
||||
},
|
||||
},
|
||||
});
|
||||
56
vite.config.ts
Normal file
56
vite.config.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* eslint-disable indent */
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import * as dotenv from 'dotenv'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
import { defineConfig, PluginOption } from 'vite'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
|
||||
dotenv.config({ path: `${__dirname}/.env` })
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), visualizer() as PluginOption, tsconfigPaths()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
react: ['react', 'react-dom', 'react-router-dom', 'zustand'],
|
||||
axios: ['axios'],
|
||||
zod: ['zod'],
|
||||
mantine: [
|
||||
'@mantine/core',
|
||||
'@mantine/hooks',
|
||||
'@mantine/dates',
|
||||
'@mantine/nprogress',
|
||||
'@mantine/notifications',
|
||||
'@mantine/modals'
|
||||
],
|
||||
recharts: ['recharts'],
|
||||
dnd: ['@hello-pangea/dnd']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
define: {
|
||||
__DOMAIN_BACKEND__: JSON.stringify(process.env.DOMAIN_BACKEND).trim(),
|
||||
__NODE_ENV__: JSON.stringify(process.env.NODE_ENV).trim()
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3333,
|
||||
cors: false,
|
||||
strictPort: true
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@entitites': fileURLToPath(new URL('./src/entitites', import.meta.url)),
|
||||
'@features': fileURLToPath(new URL('./src/features', import.meta.url)),
|
||||
'@pages': fileURLToPath(new URL('./src/pages', import.meta.url)),
|
||||
'@widgets': fileURLToPath(new URL('./src/widgets', import.meta.url)),
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@public': fileURLToPath(new URL('./public', import.meta.url)),
|
||||
'@shared': fileURLToPath(new URL('./src/shared', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue