mirror of
https://github.com/remnawave/frontend.git
synced 2026-05-13 04:09:03 +00:00
init
Co-authored-by: Ivan <84693047+exact01@users.noreply.github.com>
This commit is contained in:
commit
dc8c653bc7
213 changed files with 19287 additions and 0 deletions
133
.gitignore
vendored
Normal file
133
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
.DS_Store
|
||||
.vercel
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
legacy-peer-deps=true
|
||||
36
.prettierrc.mjs
Normal file
36
.prettierrc.mjs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/** @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
.stylelintignore
Normal file
1
.stylelintignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
dist
|
||||
28
.stylelintrc.json
Normal file
28
.stylelintrc.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"extends": ["stylelint-config-standard-scss"],
|
||||
"rules": {
|
||||
"custom-property-pattern": null,
|
||||
"selector-class-pattern": null,
|
||||
"scss/no-duplicate-mixins": null,
|
||||
"declaration-empty-line-before": null,
|
||||
"declaration-block-no-redundant-longhand-properties": null,
|
||||
"alpha-value-notation": null,
|
||||
"custom-property-empty-line-before": null,
|
||||
"property-no-vendor-prefix": null,
|
||||
"color-function-notation": null,
|
||||
"length-zero-no-unit": null,
|
||||
"selector-not-notation": null,
|
||||
"no-descending-specificity": null,
|
||||
"comment-empty-line-before": null,
|
||||
"scss/at-mixin-pattern": null,
|
||||
"scss/at-rule-no-unknown": null,
|
||||
"value-keyword-case": null,
|
||||
"media-feature-range-notation": null,
|
||||
"selector-pseudo-class-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignorePseudoClasses": ["global"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["vunguyentuan.vscode-postcss"]
|
||||
}
|
||||
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"cssVariables.lookupFiles": [
|
||||
"**/*.css",
|
||||
"**/*.scss",
|
||||
"**/*.sass",
|
||||
"**/*.less",
|
||||
"node_modules/@mantine/core/styles.css"
|
||||
]
|
||||
}
|
||||
3
.yarnrc.yml
Normal file
3
.yarnrc.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.1.1.cjs
|
||||
2
client.d.ts
vendored
Normal file
2
client.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
declare const __DOMAIN_BACKEND__: string;
|
||||
declare const __NODE_ENV__: string;
|
||||
7
eslint.config.js
Normal file
7
eslint.config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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'] },
|
||||
);
|
||||
26
index.html
Normal file
26
index.html
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
|
||||
/>
|
||||
<meta
|
||||
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>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
14199
package-lock.json
generated
Normal file
14199
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
110
package.json
Normal file
110
package.json
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"name": "@remnawave/frontend",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@emotion/react": "^11.13.5",
|
||||
"@mantine/carousel": "^7.12.2",
|
||||
"@mantine/charts": "^7.12.2",
|
||||
"@mantine/code-highlight": "^7.12.2",
|
||||
"@mantine/core": "^7.12.2",
|
||||
"@mantine/dates": "^7.12.2",
|
||||
"@mantine/dropzone": "^7.12.2",
|
||||
"@mantine/form": "^7.12.2",
|
||||
"@mantine/hooks": "^7.12.2",
|
||||
"@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",
|
||||
"axios": "^1.7.7",
|
||||
"byte-size": "^9.0.0",
|
||||
"bytes": "^3.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"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",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"xbytes": "^1.9.1",
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
||||
"@types/byte-size": "^8.1.2",
|
||||
"@types/bytes": "^3.1.4",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@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",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"eslint-config-mantine": "4.0.2",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.0",
|
||||
"eslint-plugin-react": "^7.35.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"postcss": "^8.4.45",
|
||||
"postcss-preset-mantine": "1.17.0",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
19
postcss.config.cjs
Normal file
19
postcss.config.cjs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {
|
||||
autoRem: true,
|
||||
},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '30em',
|
||||
'mantine-breakpoint-sm': '40em',
|
||||
'mantine-breakpoint-md': '48em',
|
||||
'mantine-breakpoint-lg': '64em',
|
||||
'mantine-breakpoint-xl': '80em',
|
||||
'mantine-breakpoint-2xl': '96em',
|
||||
'mantine-breakpoint-3xl': '120em',
|
||||
'mantine-breakpoint-4xl': '160em',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
37
src/app.tsx
Normal file
37
src/app.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
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 { 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';
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<AuthProvider>
|
||||
<MantineProvider theme={theme} defaultColorScheme="dark">
|
||||
<Notifications position="top-right" />
|
||||
<NavigationProgress />
|
||||
<ModalsProvider>
|
||||
<Router />
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</AuthProvider>
|
||||
</HelmetProvider>
|
||||
);
|
||||
}
|
||||
12
src/app/layouts/auth/index.tsx
Normal file
12
src/app/layouts/auth/index.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Outlet } from 'react-router-dom';
|
||||
import { Box, Center } from '@mantine/core';
|
||||
|
||||
export function AuthLayout() {
|
||||
return (
|
||||
<Center mih="100vh" p="md">
|
||||
<Box maw="25rem">
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
40
src/app/layouts/dashboard/header/header.module.css
Normal file
40
src/app/layouts/dashboard/header/header.module.css
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
.root {
|
||||
z-index: 198;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (min-width: $mantine-breakpoint-2xl) {
|
||||
padding-top: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
@media (min-width: $mantine-breakpoint-3xl) {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: $mantine-breakpoint-4xl) {
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.rightContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-inline-end: 1rem;
|
||||
width: 2.25rem;
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
|
||||
@media (min-width: $mantine-breakpoint-lg) {
|
||||
margin-inline-end: 1.25rem;
|
||||
}
|
||||
|
||||
@media (min-width: $mantine-breakpoint-xl) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
19
src/app/layouts/dashboard/header/index.tsx
Normal file
19
src/app/layouts/dashboard/header/index.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { StickyHeader } from '@shared/ui/sticky-header';
|
||||
import { Group } from '@mantine/core';
|
||||
import { HeaderButtons } from '../../../../features/dashboard/header-buttons';
|
||||
import { SidebarButton } from './sidebar-button';
|
||||
import classes from './header.module.css';
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<StickyHeader className={classes.root}>
|
||||
<div className={classes.rightContent}>
|
||||
<SidebarButton />
|
||||
</div>
|
||||
|
||||
<Group>
|
||||
<HeaderButtons />
|
||||
</Group>
|
||||
</StickyHeader>
|
||||
);
|
||||
}
|
||||
33
src/app/layouts/dashboard/header/sidebar-button.tsx
Normal file
33
src/app/layouts/dashboard/header/sidebar-button.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { useEffect } from 'react';
|
||||
import { HamburgerButton } from '@shared/ui/hamburger-button';
|
||||
import { Logo } from '@shared/ui/logo';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Drawer } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { Sidebar } from '../sidebar';
|
||||
|
||||
export function SidebarButton() {
|
||||
const location = useLocation();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
useEffect(() => {
|
||||
close();
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer.Root opened={opened} onClose={close} size="270px">
|
||||
<Drawer.Overlay />
|
||||
<Drawer.Content>
|
||||
<Drawer.Header px="1.725rem" mb="md">
|
||||
<Logo w="3rem" />
|
||||
</Drawer.Header>
|
||||
<Drawer.Body>
|
||||
<Sidebar />
|
||||
</Drawer.Body>
|
||||
</Drawer.Content>
|
||||
</Drawer.Root>
|
||||
<HamburgerButton onClick={open} display={{ xl: 'none' }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
3
src/app/layouts/dashboard/index.ts
Normal file
3
src/app/layouts/dashboard/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './root';
|
||||
export * from './sidebar';
|
||||
export * from './header';
|
||||
28
src/app/layouts/dashboard/root/index.tsx
Normal file
28
src/app/layouts/dashboard/root/index.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Logo } from '@shared/ui/logo';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Paper, ScrollArea } from '@mantine/core';
|
||||
import { Header } from '../header';
|
||||
import { Sidebar } from '../sidebar';
|
||||
import classes from './root.module.css';
|
||||
|
||||
export function DashboardLayout() {
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Paper className={classes.sidebarWrapper} withBorder>
|
||||
<div className={classes.logoWrapper}>
|
||||
<Logo w="3rem" />
|
||||
</div>
|
||||
<ScrollArea flex="1" px="md">
|
||||
<Sidebar />
|
||||
</ScrollArea>
|
||||
</Paper>
|
||||
<div className={classes.content}>
|
||||
<Header />
|
||||
|
||||
<main className={classes.main}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
src/app/layouts/dashboard/root/root.module.css
Normal file
80
src/app/layouts/dashboard/root/root.module.css
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
:root {
|
||||
--sidebar-width-xl: 270px;
|
||||
--sidebar-width-2xl: 288px;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: $mantine-breakpoint-xl) {
|
||||
margin-inline-start: var(--sidebar-width-xl);
|
||||
width: calc(100% - var(--sidebar-width-xl));
|
||||
}
|
||||
|
||||
@media (min-width: $mantine-breakpoint-2xl) {
|
||||
margin-inline-start: var(--sidebar-width-2xl);
|
||||
width: calc(100% - var(--sidebar-width-2xl));
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem 1rem 1.5rem;
|
||||
|
||||
@media (min-width: $mantine-breakpoint-md) {
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
@media (min-width: $mantine-breakpoint-lg) {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: $mantine-breakpoint-3xl) {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: $mantine-breakpoint-4xl) {
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
padding-bottom: 2.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarWrapper {
|
||||
position: fixed;
|
||||
display: none;
|
||||
bottom: 0;
|
||||
z-index: 50;
|
||||
height: 100%;
|
||||
padding-bottom: var(--mantine-spacing-md);
|
||||
|
||||
@media (min-width: $mantine-breakpoint-xl) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--sidebar-width-xl);
|
||||
}
|
||||
|
||||
@media (min-width: $mantine-breakpoint-2xl) {
|
||||
width: var(--sidebar-width-2xl);
|
||||
}
|
||||
}
|
||||
|
||||
.logoWrapper {
|
||||
padding: var(--mantine-spacing-md) var(--mantine-spacing-xl);
|
||||
margin-bottom: var(--mantine-spacing-md);
|
||||
}
|
||||
57
src/app/layouts/dashboard/sidebar/index.tsx
Normal file
57
src/app/layouts/dashboard/sidebar/index.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { NavLink as RouterLink, useLocation } from 'react-router-dom';
|
||||
import { NavLink, Stack, Title } from '@mantine/core';
|
||||
import { menu } from './menu-sections';
|
||||
import classes from './sidebar.module.css';
|
||||
|
||||
export function Sidebar() {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
{menu.map((item) => (
|
||||
<div key={item.header}>
|
||||
<Title order={6} className={classes.sectionTitle}>
|
||||
{item.header}
|
||||
</Title>
|
||||
|
||||
{item.section.map((subItem) =>
|
||||
subItem.dropdownItems ? (
|
||||
<NavLink
|
||||
variant="subtle"
|
||||
key={subItem.name}
|
||||
label={subItem.name}
|
||||
childrenOffset={0}
|
||||
className={classes.sectionLink}
|
||||
active={pathname.includes(subItem.href)}
|
||||
leftSection={subItem.icon && <subItem.icon />}
|
||||
>
|
||||
{subItem.dropdownItems?.map((dropdownItem) => (
|
||||
<NavLink
|
||||
variant="subtle"
|
||||
component={RouterLink}
|
||||
to={dropdownItem.href}
|
||||
key={dropdownItem.name}
|
||||
label={dropdownItem.name}
|
||||
active={pathname.includes(dropdownItem.href)}
|
||||
className={classes.sectionDropdownItemLink}
|
||||
leftSection={<span className="dot" />}
|
||||
/>
|
||||
))}
|
||||
</NavLink>
|
||||
) : (
|
||||
<NavLink
|
||||
variant="subtle"
|
||||
component={RouterLink}
|
||||
to={subItem.href}
|
||||
key={subItem.name}
|
||||
label={subItem.name}
|
||||
className={classes.sectionLink}
|
||||
leftSection={subItem.icon && <subItem.icon />}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
1
src/app/layouts/dashboard/sidebar/interfaces/index.ts
Normal file
1
src/app/layouts/dashboard/sidebar/interfaces/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './menu-item.interface';
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { ElementType } from 'react';
|
||||
|
||||
export interface MenuItem {
|
||||
header: string;
|
||||
section: {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: ElementType;
|
||||
dropdownItems?: {
|
||||
name: string;
|
||||
href: string;
|
||||
badge?: string;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
96
src/app/layouts/dashboard/sidebar/menu-sections.ts
Normal file
96
src/app/layouts/dashboard/sidebar/menu-sections.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { ElementType } from 'react';
|
||||
import { ROUTES } from '@shared/constants';
|
||||
import { PiStarDuotone, PiUsersDuotone } from 'react-icons/pi';
|
||||
import { MenuItem } from './interfaces';
|
||||
|
||||
export const menu: MenuItem[] = [
|
||||
{
|
||||
header: 'Overview',
|
||||
section: [
|
||||
{
|
||||
name: 'Home',
|
||||
href: ROUTES.DASHBOARD.HOME,
|
||||
icon: PiStarDuotone,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: 'Users',
|
||||
section: [{ name: 'Users', href: ROUTES.DASHBOARD.USERS, icon: PiUsersDuotone }],
|
||||
},
|
||||
|
||||
// {
|
||||
// header: 'Apps',
|
||||
// section: [
|
||||
// {
|
||||
// name: 'Kanban',
|
||||
// href: paths.dashboard.apps.kanban,
|
||||
// icon: PiKanbanDuotone,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
|
||||
// {
|
||||
// header: 'Management',
|
||||
// section: [
|
||||
// {
|
||||
// name: 'Customers',
|
||||
// icon: PiUsersDuotone,
|
||||
// href: paths.dashboard.management.customers.root,
|
||||
// dropdownItems: [
|
||||
// {
|
||||
// name: 'List',
|
||||
// href: paths.dashboard.management.customers.list,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
|
||||
// {
|
||||
// header: 'Widgets',
|
||||
// section: [
|
||||
// {
|
||||
// name: 'Charts',
|
||||
// href: paths.dashboard.widgets.charts,
|
||||
// icon: PiChartLineUpDuotone,
|
||||
// },
|
||||
// {
|
||||
// name: 'Metrics',
|
||||
// href: paths.dashboard.widgets.metrics,
|
||||
// icon: PiSquaresFourDuotone,
|
||||
// },
|
||||
// {
|
||||
// name: 'Tables',
|
||||
// href: paths.dashboard.widgets.tables,
|
||||
// icon: PiTableDuotone,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
|
||||
// {
|
||||
// header: 'Authentication',
|
||||
// section: [
|
||||
// {
|
||||
// name: 'Register',
|
||||
// href: paths.auth.register,
|
||||
// icon: PiUserPlusDuotone,
|
||||
// },
|
||||
// {
|
||||
// name: 'Login',
|
||||
// href: paths.auth.login,
|
||||
// icon: PiShieldCheckDuotone,
|
||||
// },
|
||||
// {
|
||||
// name: 'Forgot Password',
|
||||
// href: paths.auth.forgotPassword,
|
||||
// icon: PiLockKeyDuotone,
|
||||
// },
|
||||
// {
|
||||
// name: 'OTP',
|
||||
// href: paths.auth.otp,
|
||||
// icon: PiChatCenteredDotsDuotone,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
];
|
||||
53
src/app/layouts/dashboard/sidebar/sidebar.module.css
Normal file
53
src/app/layouts/dashboard/sidebar/sidebar.module.css
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
.sectionTitle {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--mantine-color-dimmed);
|
||||
margin-bottom: var(--mantine-spacing-sm);
|
||||
padding-left: var(--mantine-spacing-md);
|
||||
}
|
||||
|
||||
.sectionLink {
|
||||
border-radius: var(--mantine-radius-md);
|
||||
|
||||
& svg {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sectionDropdownItemLink {
|
||||
border-radius: var(--mantine-radius-md);
|
||||
|
||||
& [data-position='left'] {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
& :global(.dot) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& :global(.dot)::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
background-color: var(--mantine-color-dimmed);
|
||||
height: 0.3rem;
|
||||
width: 0.3rem;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
&[data-active='true'] :global(.dot)::before {
|
||||
background-color: var(--mantine-primary-color-filled);
|
||||
height: 0.4rem;
|
||||
width: 0.4rem;
|
||||
}
|
||||
}
|
||||
1
src/app/router/index.ts
Normal file
1
src/app/router/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './router';
|
||||
36
src/app/router/router.tsx
Normal file
36
src/app/router/router.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import {
|
||||
createBrowserRouter,
|
||||
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';
|
||||
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route element={<AuthGuard />}>
|
||||
<Route path="/" element={<Navigate to={ROUTES.DASHBOARD.ROOT} replace />} />
|
||||
<Route path={ROUTES.AUTH.ROOT} element={<AuthLayout />}>
|
||||
<Route index element={<Navigate to={ROUTES.AUTH.LOGIN} replace />} />
|
||||
<Route path={ROUTES.AUTH.LOGIN} element={<LoginPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path={ROUTES.DASHBOARD.ROOT} element={<DashboardLayout />}>
|
||||
<Route index element={<Navigate to={ROUTES.DASHBOARD.HOME} replace />} />
|
||||
<Route path={ROUTES.DASHBOARD.HOME} element={<HomePageConnectior />} />
|
||||
<Route path={ROUTES.DASHBOARD.USERS} element={<UsersPageConnector />} />
|
||||
</Route>
|
||||
</Route>
|
||||
)
|
||||
);
|
||||
|
||||
export function Router() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
5
src/config.ts
Normal file
5
src/config.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export const app = {
|
||||
name: 'Remnawave Dashboard',
|
||||
redirectQueryParamName: 'r',
|
||||
accessTokenStoreKey: 'access_token',
|
||||
};
|
||||
60
src/entitites/auth/auth-store/auth-store.ts
Normal file
60
src/entitites/auth/auth-store/auth-store.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { LoginCommand } 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 { removeToken, setToken } from '../session-store/use-session-store';
|
||||
import type { IActions, IState } from './interfaces';
|
||||
|
||||
const initialState: IState = {
|
||||
isLoading: false,
|
||||
loginResponse: null,
|
||||
};
|
||||
|
||||
const useAuthStore = create<IState & IActions>()(
|
||||
devtools(
|
||||
(set, getState) => ({
|
||||
...initialState,
|
||||
actions: {
|
||||
login: async (data: LoginCommand.Request): Promise<void> => {
|
||||
try {
|
||||
set({ isLoading: true });
|
||||
const response = await instance.post<LoginCommand.Response>(LoginCommand.url, data);
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
const { accessToken } = dataResponse;
|
||||
setToken({ token: accessToken });
|
||||
set({ loginResponse: dataResponse });
|
||||
|
||||
notifications.show({
|
||||
title: 'Welcome back!',
|
||||
message: 'You have successfully logged in',
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
notifications.show({ message: e.message, color: 'red' });
|
||||
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
resetState: async () => {
|
||||
removeToken();
|
||||
set({ ...initialState });
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'loginPageStore',
|
||||
anonymousActionType: 'loginPageStore',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const useAuthStoreIsLoading = () => useAuthStore((store) => store.isLoading);
|
||||
export const useLoginResponse = () => useAuthStore((state) => state.loginResponse);
|
||||
export const useLoginPageStoreActions = () => useAuthStore((store) => store.actions);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { LoginCommand } from '@remnawave/backend-contract';
|
||||
|
||||
export interface IActions {
|
||||
actions: {
|
||||
login: (data: LoginCommand.Request) => Promise<void>;
|
||||
resetState: () => Promise<void>;
|
||||
};
|
||||
}
|
||||
2
src/entitites/auth/auth-store/interfaces/index.ts
Normal file
2
src/entitites/auth/auth-store/interfaces/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './action.interface.js';
|
||||
export * from './state.interface.js';
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { LoginCommand } from '@remnawave/backend-contract'
|
||||
|
||||
export interface IState {
|
||||
isLoading: boolean
|
||||
loginResponse: LoginCommand.Response['response'] | null
|
||||
}
|
||||
1
src/entitites/auth/index.ts
Normal file
1
src/entitites/auth/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './session-store';
|
||||
2
src/entitites/auth/session-store/index.ts
Normal file
2
src/entitites/auth/session-store/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './interfaces';
|
||||
export * from './use-session-store';
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import type { ISetTokenAction } from './set-token-action.interface.ts'
|
||||
|
||||
export interface IActions {
|
||||
actions: {
|
||||
setToken: (dto: ISetTokenAction) => void
|
||||
removeToken: () => void
|
||||
}
|
||||
}
|
||||
3
src/entitites/auth/session-store/interfaces/index.ts
Normal file
3
src/entitites/auth/session-store/interfaces/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './action.interface';
|
||||
export * from './set-token-action.interface';
|
||||
export * from './state.interface';
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export interface ISetTokenAction {
|
||||
token: string
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export interface IState {
|
||||
token: string
|
||||
}
|
||||
42
src/entitites/auth/session-store/use-session-store.ts
Normal file
42
src/entitites/auth/session-store/use-session-store.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { create } from 'zustand';
|
||||
import { createJSONStorage, devtools, persist } from 'zustand/middleware';
|
||||
import { setAuthorizationToken } from '@/shared/api';
|
||||
import { IActions, ISetTokenAction, IState } from './interfaces';
|
||||
|
||||
export const useSessionStore = create<IState & IActions>()(
|
||||
persist(
|
||||
devtools(
|
||||
(set) => ({
|
||||
token: '',
|
||||
actions: {
|
||||
setToken: (dto: ISetTokenAction) => {
|
||||
set({ token: dto.token });
|
||||
},
|
||||
removeToken: () => {
|
||||
set({ token: '' });
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ name: 'sessionStore', anonymousActionType: 'sessionStore' }
|
||||
),
|
||||
{
|
||||
name: 'sessionStore',
|
||||
partialize: (state) => ({
|
||||
token: state.token,
|
||||
}),
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
useSessionStore.subscribe((state) => {
|
||||
setAuthorizationToken(state.token);
|
||||
});
|
||||
|
||||
setAuthorizationToken(useSessionStore.getState().token);
|
||||
|
||||
export const useToken = () => useSessionStore((state) => state.token);
|
||||
export const useSessionStoreActions = () => useSessionStore((state) => state.actions);
|
||||
|
||||
export const setToken = (dto: ISetTokenAction) => useSessionStore.setState({ ...dto });
|
||||
export const removeToken = () => useSessionStore.getState().actions.removeToken();
|
||||
136
src/entitites/dashboard/dashboard-store/dashboard-store.ts
Normal file
136
src/entitites/dashboard/dashboard-store/dashboard-store.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
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';
|
||||
|
||||
const initialState: IState = {
|
||||
isLoading: false,
|
||||
systemInfo: null,
|
||||
users: null,
|
||||
usersParams: {
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
orderBy: 'createdAt',
|
||||
orderDir: 'desc',
|
||||
},
|
||||
totalUsers: 0,
|
||||
inbounds: null,
|
||||
isInboundsLoading: false,
|
||||
};
|
||||
|
||||
export const useDashboardStore = create<IState & IActions>()(
|
||||
devtools(
|
||||
(set, getState) => ({
|
||||
...initialState,
|
||||
actions: {
|
||||
getSystemInfo: async (): Promise<boolean> => {
|
||||
try {
|
||||
set({ isLoading: true });
|
||||
const response = await instance.get<GetStatsCommand.Response>(GetStatsCommand.url);
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
|
||||
set({ systemInfo: dataResponse });
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
getUsers: async (params?: Partial<IUsersParams>): Promise<boolean> => {
|
||||
try {
|
||||
set({ isLoading: true });
|
||||
const currentParams = getState().usersParams;
|
||||
const newParams = { ...currentParams, ...params };
|
||||
|
||||
const response = await instance.get<GetAllUsersCommand.Response>(
|
||||
GetAllUsersCommand.url,
|
||||
{
|
||||
params: {
|
||||
limit: newParams.limit,
|
||||
offset: newParams.offset,
|
||||
orderBy: newParams.orderBy,
|
||||
orderDir: newParams.orderDir,
|
||||
search: newParams.search,
|
||||
searchBy: newParams.searchBy,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
|
||||
set({
|
||||
users: dataResponse.users,
|
||||
totalUsers: dataResponse.total,
|
||||
usersParams: newParams,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
getInbounds: async (): Promise<boolean> => {
|
||||
try {
|
||||
set({ isInboundsLoading: true });
|
||||
const response = await instance.get<GetInboundsCommand.Response>(
|
||||
GetInboundsCommand.url
|
||||
);
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
|
||||
set({ inbounds: dataResponse });
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
set({ isInboundsLoading: false });
|
||||
}
|
||||
},
|
||||
resetState: async () => {
|
||||
set({ ...initialState });
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: '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);
|
||||
|
||||
// Inbounds
|
||||
export const useDSInbounds = () => useDashboardStore((state) => state.inbounds);
|
||||
export const useDSInboundsLoading = () => useDashboardStore((state) => state.isInboundsLoading);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { IUsersParams } from './users-params.interface';
|
||||
|
||||
export interface IActions {
|
||||
actions: {
|
||||
getSystemInfo: () => Promise<boolean>;
|
||||
getUsers: (params?: Partial<IUsersParams>) => Promise<boolean>;
|
||||
getInbounds: () => Promise<boolean>;
|
||||
resetState: () => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './action.interface';
|
||||
export * from './state.interface';
|
||||
export * from './users-params.interface';
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import {
|
||||
GetAllUsersCommand,
|
||||
GetInboundsCommand,
|
||||
GetStatsCommand,
|
||||
} from '@remnawave/backend-contract';
|
||||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { GetAllUsersCommand } from '@remnawave/backend-contract';
|
||||
|
||||
export interface IUsersParams {
|
||||
limit: number;
|
||||
offset: number;
|
||||
orderBy?: GetAllUsersCommand.SortableField;
|
||||
orderDir?: 'asc' | 'desc';
|
||||
search?: string;
|
||||
searchBy?: GetAllUsersCommand.SearchableField;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import {
|
||||
GetUserByUuidCommand,
|
||||
RevokeUserSubscriptionCommand,
|
||||
UpdateUserCommand,
|
||||
} from '@remnawave/backend-contract';
|
||||
|
||||
export interface IActions {
|
||||
actions: {
|
||||
getUser: () => Promise<boolean>;
|
||||
updateUser: (body: UpdateUserCommand.Request) => Promise<boolean>;
|
||||
changeModalState: (state: boolean) => void;
|
||||
setUserUuid: (userUuid: string) => Promise<void>;
|
||||
resetState: () => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './action.interface';
|
||||
export * from './state.interface';
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import {
|
||||
GetAllUsersCommand,
|
||||
GetInboundsCommand,
|
||||
GetStatsCommand,
|
||||
GetUserByUuidCommand,
|
||||
} from '@remnawave/backend-contract';
|
||||
|
||||
export interface IState {
|
||||
isLoading: boolean;
|
||||
isModalOpen: boolean;
|
||||
userUuid: string | null;
|
||||
user: GetUserByUuidCommand.Response['response'] | null;
|
||||
}
|
||||
96
src/entitites/dashboard/user-modal-store/user-modal-store.ts
Normal file
96
src/entitites/dashboard/user-modal-store/user-modal-store.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { GetUserByUuidCommand, UpdateUserCommand } 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,
|
||||
userUuid: null,
|
||||
user: null,
|
||||
};
|
||||
|
||||
export const useUserModalStore = create<IState & IActions>()(
|
||||
devtools(
|
||||
(set, getState) => ({
|
||||
...initialState,
|
||||
actions: {
|
||||
getUser: async (): Promise<boolean> => {
|
||||
try {
|
||||
set({ isLoading: true });
|
||||
|
||||
const userUuid = getState().userUuid;
|
||||
|
||||
if (!userUuid) {
|
||||
throw new Error('User UUID is required');
|
||||
}
|
||||
|
||||
const response = await instance.get<GetUserByUuidCommand.Response>(
|
||||
GetUserByUuidCommand.url(userUuid)
|
||||
);
|
||||
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
|
||||
set({ user: dataResponse });
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
updateUser: async (body: UpdateUserCommand.Request): Promise<boolean> => {
|
||||
try {
|
||||
set({ isLoading: true });
|
||||
const response = await instance.patch<UpdateUserCommand.Response>(
|
||||
UpdateUserCommand.url,
|
||||
body
|
||||
);
|
||||
const {
|
||||
data: { response: dataResponse },
|
||||
} = response;
|
||||
|
||||
set({ user: dataResponse });
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof AxiosError) {
|
||||
throw e;
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
changeModalState: (modalState: boolean) => {
|
||||
set((state) => ({ isModalOpen: modalState }));
|
||||
if (!modalState) {
|
||||
getState().actions.resetState();
|
||||
}
|
||||
},
|
||||
setUserUuid: async (userUuid: string): Promise<void> => {
|
||||
set((state) => ({ userUuid }));
|
||||
},
|
||||
resetState: async (): Promise<void> => {
|
||||
set({ ...initialState });
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'userModalStore',
|
||||
anonymousActionType: 'userModalStore',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const useUserModalStoreIsModalOpen = () => useUserModalStore((state) => state.isModalOpen);
|
||||
export const useUserModalStoreActions = () => useUserModalStore((store) => store.actions);
|
||||
export const useUserModalStoreUser = () => useUserModalStore((state) => state.user);
|
||||
2
src/entitites/dashboard/users/models/index.ts
Normal file
2
src/entitites/dashboard/users/models/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './interfaces';
|
||||
export * from './tabs.entity';
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export interface IGetTabData {
|
||||
totalUsers?: number;
|
||||
activeUsers?: number;
|
||||
disabledUsers?: number;
|
||||
expiredUsers?: number;
|
||||
limitedUsers?: number;
|
||||
}
|
||||
2
src/entitites/dashboard/users/models/interfaces/index.ts
Normal file
2
src/entitites/dashboard/users/models/interfaces/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './get-tab-data.interface';
|
||||
export * from './user.type';
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { GetAllUsersCommand } from '@remnawave/backend-contract';
|
||||
|
||||
export type User = GetAllUsersCommand.Response['response']['users'][0];
|
||||
41
src/entitites/dashboard/users/models/tabs.entity.ts
Normal file
41
src/entitites/dashboard/users/models/tabs.entity.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { IGetTabData, User } from '@/entitites/dashboard/users/models/interfaces';
|
||||
import { DataTable } from '@/shared/ui/stuff/data-table';
|
||||
import { UseDataTableArgs } from '@/shared/ui/stuff/data-table/use-data-table';
|
||||
|
||||
export function getTabDataUsers(data: IGetTabData) {
|
||||
return DataTable.useDataTable<User>({
|
||||
tabsConfig: {
|
||||
tabs: [
|
||||
{
|
||||
value: '*',
|
||||
label: 'All',
|
||||
counter: data.totalUsers || 0,
|
||||
},
|
||||
{
|
||||
value: 'ACTIVE',
|
||||
label: 'Active',
|
||||
color: 'teal',
|
||||
counter: data.activeUsers || 0,
|
||||
},
|
||||
{
|
||||
value: 'DISABLED',
|
||||
label: 'Disabled',
|
||||
color: 'gray',
|
||||
counter: data.disabledUsers || 0,
|
||||
},
|
||||
{
|
||||
value: 'EXPIRED',
|
||||
label: 'Expired',
|
||||
color: 'red',
|
||||
counter: data.expiredUsers || 0,
|
||||
},
|
||||
{
|
||||
value: 'LIMITED',
|
||||
label: 'Limited',
|
||||
color: 'orange',
|
||||
counter: data.limitedUsers || 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
1
src/entitites/dashboard/users/ui/index.ts
Normal file
1
src/entitites/dashboard/users/ui/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './table-columns';
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { RESET_PERIODS } from '@remnawave/backend-contract';
|
||||
import { Box, Group, Indicator, Progress, Text } from '@mantine/core';
|
||||
import { IProps } from '@/entitites/dashboard/users/ui/table-columns/username/interface';
|
||||
import { prettyBytesUtil } from '@/shared/utils/bytes';
|
||||
|
||||
export function DataUsageColumnEntity(props: IProps) {
|
||||
const { user } = props;
|
||||
|
||||
const usedTrafficPercentage = (user.usedTrafficBytes / user.trafficLimitBytes) * 100;
|
||||
const limitBytes = prettyBytesUtil(user.trafficLimitBytes, true);
|
||||
const totalUsedTraffic = prettyBytesUtil(user.usedTrafficBytes, true);
|
||||
const lifetimeUsedTraffic = prettyBytesUtil(user.totalUsedBytes, true);
|
||||
|
||||
const strategy = {
|
||||
[RESET_PERIODS.YEAR]: 'Yearly',
|
||||
[RESET_PERIODS.MONTH]: 'Montly',
|
||||
[RESET_PERIODS.WEEK]: 'Weekly',
|
||||
[RESET_PERIODS.DAY]: 'Daily',
|
||||
[RESET_PERIODS.NO_RESET]: '∞',
|
||||
}[user.trafficLimitStrategy];
|
||||
|
||||
return (
|
||||
<Box w={300}>
|
||||
<Progress
|
||||
radius="xs"
|
||||
size="md"
|
||||
value={usedTrafficPercentage}
|
||||
color={usedTrafficPercentage > 100 ? 'yellow' : 'cyan'}
|
||||
striped
|
||||
animated
|
||||
/>
|
||||
<Group gap="xs" justify="space-between" mt={2}>
|
||||
<Text size="xs" c="dimmed">
|
||||
{totalUsedTraffic} / {limitBytes} {strategy}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Σ {lifetimeUsedTraffic}
|
||||
</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './interface';
|
||||
export * from './data-usage.column';
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { User } from '@/entitites/dashboard/users/models';
|
||||
|
||||
export interface IProps {
|
||||
user: User;
|
||||
}
|
||||
3
src/entitites/dashboard/users/ui/table-columns/index.ts
Normal file
3
src/entitites/dashboard/users/ui/table-columns/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './data-usage';
|
||||
export * from './status';
|
||||
export * from './username';
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './interface';
|
||||
export * from './short-uuid.column';
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { User } from '@/entitites/dashboard/users/models';
|
||||
|
||||
export interface IProps {
|
||||
user: User;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
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';
|
||||
|
||||
export function ShortUuidColumnEntity(props: IProps) {
|
||||
const { user } = props;
|
||||
const shortDisplay = user.shortUuid.slice(0, 5);
|
||||
|
||||
return (
|
||||
<CopyButton
|
||||
value={user.shortUuid} // копируется полное значение
|
||||
>
|
||||
{({ copied, copy }) => (
|
||||
<LinkChip inline onClick={copy} checked={copied} icon={<LuCopy />}>
|
||||
{shortDisplay}...
|
||||
</LinkChip>
|
||||
)}
|
||||
</CopyButton>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './interface';
|
||||
export * from './status.column';
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { User } from '@/entitites/dashboard/users/models';
|
||||
|
||||
export interface IProps {
|
||||
user: User;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { Group, Text } from '@mantine/core';
|
||||
import { IProps } from '@/entitites/dashboard/users/ui/table-columns/username/interface';
|
||||
import { getExpirationTextUtil } from '@/shared/utils/time-utils';
|
||||
import { UserStatusBadge } from '@/widgets/dashboard/users/user-status-badge';
|
||||
|
||||
export function StatusColumnEntity(props: IProps) {
|
||||
const { user } = props;
|
||||
|
||||
const expirationText = getExpirationTextUtil(user.expireAt);
|
||||
|
||||
return (
|
||||
<Group gap="xs">
|
||||
<UserStatusBadge status={user.status} />
|
||||
<Text size="xs" c="dimmed">
|
||||
{expirationText}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './interface';
|
||||
export * from './username.column';
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { User } from '@/entitites/dashboard/users/models';
|
||||
|
||||
export interface IProps {
|
||||
user: User;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { Box, Group, Indicator, Text } from '@mantine/core';
|
||||
import { IProps } from '@/entitites/dashboard/users/ui/table-columns/username/interface';
|
||||
import { getConnectionStatusColorUtil, getTimeAgoUtil } from '@/shared/utils/time-utils';
|
||||
|
||||
export function UsernameColumnEntity(props: IProps) {
|
||||
const { user } = props;
|
||||
|
||||
const color = getConnectionStatusColorUtil(user.onlineAt);
|
||||
const timeAgo = getTimeAgoUtil(user.onlineAt);
|
||||
|
||||
return (
|
||||
<Group wrap="nowrap" gap="md" align="center">
|
||||
<Indicator inline processing color={color} size={12} />
|
||||
<Box w="100%">
|
||||
<Text size="sm" fw={500} truncate="end">
|
||||
{user.username}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{timeAgo}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
1
src/features/auth/index.ts
Normal file
1
src/features/auth/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './login-form.feature';
|
||||
65
src/features/auth/login-form.feature.tsx
Normal file
65
src/features/auth/login-form.feature.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { LoginCommand } from '@remnawave/backend-contract';
|
||||
import { useAuth } from '@shared/hooks/use-auth';
|
||||
import { PiSignInDuotone, PiSignOutDuotone } from 'react-icons/pi';
|
||||
import { Button, Container, Paper, PasswordInput, TextInput } from '@mantine/core';
|
||||
import { useForm, zodResolver } from '@mantine/form';
|
||||
import {
|
||||
useAuthStoreIsLoading,
|
||||
useLoginPageStoreActions,
|
||||
} from '../../entitites/auth/auth-store/auth-store';
|
||||
import { handleFormErrors } from '../../shared/utils/form';
|
||||
|
||||
export const LoginForm = () => {
|
||||
const { setIsAuthenticated } = useAuth();
|
||||
const { login } = useLoginPageStoreActions();
|
||||
const isLoading = useAuthStoreIsLoading();
|
||||
|
||||
const form = useForm({
|
||||
mode: 'uncontrolled',
|
||||
validate: zodResolver(LoginCommand.RequestSchema),
|
||||
initialValues: { username: '', password: '' },
|
||||
});
|
||||
|
||||
const handleSubmit = form.onSubmit(async (variables) => {
|
||||
try {
|
||||
await login({ username: variables.username, password: variables.password });
|
||||
setIsAuthenticated(true);
|
||||
} catch (error) {
|
||||
handleFormErrors(form, error);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Container size={600} my={40}>
|
||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||
<TextInput
|
||||
name="username"
|
||||
label="Username"
|
||||
placeholder="username"
|
||||
required
|
||||
{...form.getInputProps('username')}
|
||||
/>
|
||||
<PasswordInput
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="Your password"
|
||||
required
|
||||
mt="md"
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
<Button
|
||||
fullWidth
|
||||
mt="xl"
|
||||
variant="outline"
|
||||
type="submit"
|
||||
leftSection={<PiSignInDuotone size="1rem" />}
|
||||
loading={isLoading}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</Paper>
|
||||
</Container>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { PiSignOutDuotone } from 'react-icons/pi';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Menu } from '@mantine/core';
|
||||
import { removeToken, useSessionStoreActions } from '../../../entitites/auth';
|
||||
import { ROUTES } from '../../../shared/constants';
|
||||
import { useAuth } from '../../../shared/hooks/use-auth';
|
||||
|
||||
export const HeaderButtons = () => {
|
||||
const { setIsAuthenticated } = useAuth();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const handleLogout = () => {
|
||||
setIsAuthenticated(false);
|
||||
removeToken();
|
||||
navigate(ROUTES.AUTH.LOGIN);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="md"
|
||||
leftSection={<PiSignOutDuotone size="1rem" />}
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
1
src/features/dashboard/header-buttons/index.ts
Normal file
1
src/features/dashboard/header-buttons/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './header-buttons.feature';
|
||||
10
src/global.css
Normal file
10
src/global.css
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
@layer mantine, mantine-datatable;
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100vh;
|
||||
}
|
||||
4
src/main.tsx
Normal file
4
src/main.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import ReactDOM from 'react-dom/client';
|
||||
import { App } from './app';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|
||||
36
src/pages/auth/login/login.page.tsx
Normal file
36
src/pages/auth/login/login.page.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { Page } from '@shared/ui/page';
|
||||
import { UnderlineShape } from '@shared/ui/underline-shape';
|
||||
import { Box, Group, Stack, Text, Title } from '@mantine/core';
|
||||
import { Logo } from '@/shared/ui/logo';
|
||||
import { LoginForm } from '../../../features/auth';
|
||||
|
||||
export const LoginPage = () => {
|
||||
return (
|
||||
<Page title="Login">
|
||||
<Stack gap="xl" align="center">
|
||||
<Group justify="center" align="center">
|
||||
<Logo size="2.5rem" c="red" />
|
||||
<Title order={1}>
|
||||
<Text fz="inherit" fw="inherit" component="span" pos="relative">
|
||||
Remnawave
|
||||
<UnderlineShape
|
||||
c="red"
|
||||
left="0"
|
||||
pos="absolute"
|
||||
h="0.625rem"
|
||||
bottom="-1rem"
|
||||
w="7rem"
|
||||
/>
|
||||
</Text>
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Box w={500} maw={800}>
|
||||
<LoginForm />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
7
src/pages/dashboard/home/components/home.module.css
Normal file
7
src/pages/dashboard/home/components/home.module.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.root {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
115
src/pages/dashboard/home/components/home.page.tsx
Normal file
115
src/pages/dashboard/home/components/home.page.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
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 {
|
||||
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';
|
||||
|
||||
export const HomePage = (props: IProps) => {
|
||||
const { systemInfo, breadcrumbs } = props;
|
||||
|
||||
if (!systemInfo) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
const { users, memory } = systemInfo;
|
||||
|
||||
const simpleMetrics = [
|
||||
{
|
||||
icon: PiDevicesDuotone,
|
||||
title: 'Online users',
|
||||
value: formatInt(users.onlineLastMinute ?? 0),
|
||||
color: 'teal',
|
||||
},
|
||||
{
|
||||
icon: PiUsersDuotone,
|
||||
title: 'Total users',
|
||||
value: formatInt(users.totalUsers ?? 0),
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
icon: PiChartBarDuotone,
|
||||
title: 'Total traffic',
|
||||
value: prettyBytes(Number(users.totalTrafficBytes) ?? 0),
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
icon: PiMemoryDuotone,
|
||||
title: 'Available RAM',
|
||||
value:
|
||||
prettyBytes(Number(memory.available) ?? 0) + ' / ' + prettyBytes(Number(memory.total) ?? 0),
|
||||
color: 'cyan',
|
||||
},
|
||||
];
|
||||
|
||||
const usersMetrics = [
|
||||
{
|
||||
icon: PiPulseDuotone,
|
||||
title: 'Active users',
|
||||
value: users.statusCounts.ACTIVE,
|
||||
color: 'teal',
|
||||
},
|
||||
{
|
||||
icon: PiClockUserDuotone,
|
||||
title: 'Expired users',
|
||||
value: users.statusCounts.EXPIRED,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
icon: PiClockCountdownDuotone,
|
||||
title: 'Limited users',
|
||||
value: users.statusCounts.LIMITED,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
icon: PiProhibitDuotone,
|
||||
title: 'Disabled users',
|
||||
value: users.statusCounts.DISABLED,
|
||||
color: 'gray',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Page title="Home">
|
||||
<PageHeader title="Short stats" breadcrumbs={breadcrumbs} />
|
||||
|
||||
<Stack gap="sm" mb="xl">
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, xl: 4 }}>
|
||||
{simpleMetrics.map((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} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
2
src/pages/dashboard/home/components/index.ts
Normal file
2
src/pages/dashboard/home/components/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './home.page';
|
||||
export * from './interfaces';
|
||||
1
src/pages/dashboard/home/components/interfaces/index.ts
Normal file
1
src/pages/dashboard/home/components/interfaces/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './props.interface';
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { GetStatsCommand } from '@remnawave/backend-contract';
|
||||
import { IBreadcrumb } from '@/shared/interfaces';
|
||||
|
||||
export interface IProps {
|
||||
systemInfo: GetStatsCommand.Response['response'];
|
||||
breadcrumbs: IBreadcrumb[];
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { IBreadcrumb } from '@/shared/interfaces';
|
||||
|
||||
export const BREADCRUMBS: IBreadcrumb[] = [{ label: 'Dashboard', href: '/' }, { label: 'Home' }];
|
||||
1
src/pages/dashboard/home/connectores/constant/index.ts
Normal file
1
src/pages/dashboard/home/connectores/constant/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './constants';
|
||||
27
src/pages/dashboard/home/connectores/home.page.connector.tsx
Normal file
27
src/pages/dashboard/home/connectores/home.page.connector.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useEffect } from 'react';
|
||||
import {
|
||||
useDashboardStoreActions,
|
||||
useDashboardStoreSystemInfo,
|
||||
} from '@/entitites/dashboard/dashboard-store/dashboard-store';
|
||||
import { HomePage } from '@/pages/dashboard/home/components';
|
||||
import { LoadingScreen } from '@/shared/ui/loading-screen';
|
||||
import { BREADCRUMBS } from './constant';
|
||||
|
||||
export const HomePageConnectior = () => {
|
||||
const actions = useDashboardStoreActions();
|
||||
const systemInfo = useDashboardStoreSystemInfo();
|
||||
|
||||
useEffect(() => {
|
||||
const intervalTime = setInterval(() => {
|
||||
actions.getSystemInfo();
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(intervalTime);
|
||||
}, [actions]);
|
||||
|
||||
if (!systemInfo) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
return <HomePage systemInfo={systemInfo} breadcrumbs={BREADCRUMBS} />;
|
||||
};
|
||||
1
src/pages/dashboard/home/connectores/index.ts
Normal file
1
src/pages/dashboard/home/connectores/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './home.page.connector';
|
||||
|
|
@ -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';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './props.interface';
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
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 {
|
||||
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;
|
||||
}
|
||||
53
src/pages/dashboard/users/ui/components/users.page.tsx
Normal file
53
src/pages/dashboard/users/ui/components/users.page.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
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';
|
||||
|
||||
export default function UsersPageComponent(props: IProps) {
|
||||
const {
|
||||
users,
|
||||
tabs,
|
||||
search,
|
||||
setSearch,
|
||||
searchBy,
|
||||
setSearchBy,
|
||||
columns,
|
||||
handleSortStatusChange,
|
||||
handlePageChange,
|
||||
handleRecordsPerPageChange,
|
||||
handleUpdate,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Page title="Users">
|
||||
<PageHeader title="Users" breadcrumbs={BREADCRUMBS} />
|
||||
|
||||
<Grid>
|
||||
<Grid.Col span={12}>
|
||||
<UsersMetrics />
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={12}>
|
||||
<UserTableWidget
|
||||
tabs={tabs}
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
searchBy={searchBy}
|
||||
setSearchBy={setSearchBy}
|
||||
columns={columns}
|
||||
handleSortStatusChange={handleSortStatusChange}
|
||||
handlePageChange={handlePageChange}
|
||||
handleRecordsPerPageChange={handleRecordsPerPageChange}
|
||||
handleUpdate={handleUpdate}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<ViewUserModal />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
209
src/pages/dashboard/users/ui/connectors/users.page.connector.tsx
Normal file
209
src/pages/dashboard/users/ui/connectors/users.page.connector.tsx
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
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 {
|
||||
useDashboardStoreActions,
|
||||
useDashboardStoreIsLoading,
|
||||
useDashboardStoreParams,
|
||||
useDashboardStoreSystemInfo,
|
||||
useDashboardStoreTotalUsers,
|
||||
useDashboardStoreUsers,
|
||||
} from '@/entitites/dashboard/dashboard-store/dashboard-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';
|
||||
|
||||
export function UsersPageConnector() {
|
||||
const users = useDashboardStoreUsers();
|
||||
const isLoading = useDashboardStoreIsLoading();
|
||||
const totalUsers = useDashboardStoreTotalUsers();
|
||||
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 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,
|
||||
});
|
||||
|
||||
const [debouncedFilters] = useDebouncedValue(dataTab.filters.filters, 300);
|
||||
|
||||
useEffect(() => {
|
||||
actions.getSystemInfo();
|
||||
|
||||
const filterEntry = Object.entries(debouncedFilters).find(([_, filter]) => filter?.value);
|
||||
|
||||
const searchParams = filterEntry
|
||||
? {
|
||||
search: String(filterEntry[1].value),
|
||||
searchBy: filterEntry[0] as GetAllUsersCommand.SearchableField,
|
||||
}
|
||||
: {
|
||||
search: dataTab.tabs.value === '*' ? '' : dataTab.tabs.value,
|
||||
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]);
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
const offset = (page - 1) * params.limit;
|
||||
actions.getUsers({ offset });
|
||||
};
|
||||
|
||||
const handleRecordsPerPageChange = (limit: number) => {
|
||||
actions.getUsers({ limit, offset: 0 });
|
||||
};
|
||||
|
||||
const handleSortStatusChange = (status: {
|
||||
columnAccessor: string;
|
||||
direction: 'asc' | 'desc';
|
||||
}) => {
|
||||
actions.getUsers({
|
||||
orderBy: status.columnAccessor as GetAllUsersCommand.SortableField,
|
||||
orderDir: status.direction,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdate = () => {
|
||||
actions.getUsers({ offset: 0 });
|
||||
};
|
||||
|
||||
// User Modal
|
||||
const isModalOpen = useUserModalStoreIsModalOpen();
|
||||
const userModalActions = useUserModalStoreActions();
|
||||
|
||||
const handleOpenModal = async (userUuid: string) => {
|
||||
await userModalActions.setUserUuid(userUuid);
|
||||
console.log('userUuid', userUuid);
|
||||
userModalActions.changeModalState(true);
|
||||
// !TODO: Ваня помоги
|
||||
};
|
||||
|
||||
const columns = useMemo<DataTableColumn<User>[]>(
|
||||
() => [
|
||||
{
|
||||
accessor: 'shortUuid' as const,
|
||||
title: 'Sub-link',
|
||||
width: 50,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filter: (
|
||||
<TextInput
|
||||
placeholder="Search by subscription link"
|
||||
value={dataTab.filters.filters.shortUuid?.value as string}
|
||||
onChange={(e) =>
|
||||
dataTab.filters.change({
|
||||
name: 'shortUuid',
|
||||
label: 'Sub-link',
|
||||
value: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (user) => <ShortUuidColumnEntity user={user} />,
|
||||
},
|
||||
{
|
||||
accessor: 'username' as const,
|
||||
title: 'Username',
|
||||
width: 100,
|
||||
sortable: true,
|
||||
|
||||
filter: (
|
||||
<TextInput
|
||||
placeholder="Search by username"
|
||||
value={dataTab.filters.filters.username?.value as string}
|
||||
onChange={(e) =>
|
||||
dataTab.filters.change({
|
||||
name: 'username',
|
||||
label: 'Username',
|
||||
value: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (user) => <UsernameColumnEntity user={user} />,
|
||||
},
|
||||
{
|
||||
accessor: 'expireAt' as const,
|
||||
title: 'Status',
|
||||
width: 100,
|
||||
sortable: true,
|
||||
filter: (
|
||||
<MultiSelect
|
||||
w="10rem"
|
||||
data={['ACTIVE', 'DISABLED', 'EXPIRED', 'LIMITED']}
|
||||
value={dataTab.filters.filters.status?.value as string[]}
|
||||
onChange={(value) =>
|
||||
dataTab.filters.change({
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
render: (user) => <StatusColumnEntity user={user} />,
|
||||
},
|
||||
{
|
||||
accessor: 'usedTrafficBytes' as const,
|
||||
title: 'Data usage',
|
||||
width: 150,
|
||||
sortable: true,
|
||||
render: (user) => <DataUsageColumnEntity user={user} />,
|
||||
},
|
||||
{
|
||||
accessor: 'actions',
|
||||
title: 'Actions',
|
||||
textAlign: 'right',
|
||||
width: 100,
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
24
src/shared/api/axios.ts
Normal file
24
src/shared/api/axios.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import axios from 'axios';
|
||||
|
||||
let authorizationToken = '';
|
||||
|
||||
export const instance = axios.create({
|
||||
baseURL: __DOMAIN_BACKEND__,
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
config.headers.set('Authorization', `Bearer ${authorizationToken}`);
|
||||
return config;
|
||||
});
|
||||
|
||||
export const setAuthorizationToken = (token: string) => {
|
||||
authorizationToken = token;
|
||||
};
|
||||
|
||||
instance.interceptors.response.use((response) => {
|
||||
return response;
|
||||
});
|
||||
1
src/shared/api/index.ts
Normal file
1
src/shared/api/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './axios';
|
||||
1
src/shared/constants/index.ts
Normal file
1
src/shared/constants/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './routes';
|
||||
11
src/shared/constants/routes.ts
Normal file
11
src/shared/constants/routes.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export const ROUTES = {
|
||||
AUTH: {
|
||||
ROOT: '/auth',
|
||||
LOGIN: '/auth/login',
|
||||
},
|
||||
DASHBOARD: {
|
||||
ROOT: '/dashboard',
|
||||
HOME: '/dashboard/home',
|
||||
USERS: '/dashboard/users',
|
||||
},
|
||||
} as const;
|
||||
32
src/shared/hocs/guards/auth-guard.tsx
Normal file
32
src/shared/hocs/guards/auth-guard.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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();
|
||||
|
||||
if (!isInitialized) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
if (location.pathname.includes(ROUTES.AUTH.ROOT)) {
|
||||
return <Outlet />;
|
||||
}
|
||||
return <Navigate to={ROUTES.AUTH.LOGIN} replace />;
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
if (location.pathname.includes(ROUTES.DASHBOARD.ROOT)) {
|
||||
return <Outlet />;
|
||||
}
|
||||
return <Navigate to={ROUTES.DASHBOARD.ROOT} replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
18
src/shared/hooks/api/auth/auth.ts
Normal file
18
src/shared/hooks/api/auth/auth.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// import { LoginCommand } from '@remnawave/backend-contract';
|
||||
// import { setClientAccessToken } from '@shared/api/axios';
|
||||
// import { createPostMutationHook } from '@shared/api/axios-proxy';
|
||||
// import { notifications } from '@mantine/notifications';
|
||||
|
||||
// export const useLogin = createPostMutationHook({
|
||||
// endpoint: LoginCommand.url,
|
||||
// bodySchema: LoginCommand.RequestSchema,
|
||||
// responseSchema: LoginCommand.ResponseSchema,
|
||||
// rMutationParams: {
|
||||
// onSuccess: (data) => {
|
||||
// setClientAccessToken(data.response.accessToken);
|
||||
// },
|
||||
// onError: (error) => {
|
||||
// notifications.show({ message: error.message, color: 'red' });
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
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'] },
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue