mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-23 16:59:08 +00:00
feat: added server workflow
This commit is contained in:
1
client/.npmrc
Normal file
1
client/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
public-hoist-pattern[]=*@heroui/*
|
@@ -5,6 +5,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>client</title>
|
||||
<link href="/src/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
3987
client/package-lock.json
generated
3987
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,14 +13,19 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.5",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"axios": "^1.12.2",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"framer-motion": "^12.23.24",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.64.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.9.3"
|
||||
"react-router": "^7.9.3",
|
||||
"tailwindcss": "^4.1.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
|
@@ -0,0 +1,33 @@
|
||||
import {addToast, type ToastProps} from "@heroui/toast";
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
appNotificator,
|
||||
type Notice,
|
||||
type NoticeType,
|
||||
} from '../../../utils/notification/app-notificator';
|
||||
|
||||
const colorMap = new Map<NoticeType, string>([
|
||||
['success', 'Success'],
|
||||
['error', 'Danger'],
|
||||
['warn', 'Warning'],
|
||||
]);
|
||||
|
||||
const paramsMappers = (notice: Notice): Partial<ToastProps> => {
|
||||
const { type, message } = notice;
|
||||
const color = colorMap.get(type);
|
||||
return {
|
||||
description: message,
|
||||
color: color?.toLowerCase() as ToastProps['color'],
|
||||
};
|
||||
};
|
||||
|
||||
export const ApplyNotificator = () => {
|
||||
useEffect(() => {
|
||||
appNotificator.applyProvider({
|
||||
paramsMappers,
|
||||
show: (params: Partial<ToastProps>) => addToast(params),
|
||||
});
|
||||
}, []);
|
||||
return <></>;
|
||||
};
|
1
client/src/common/components/apply-notificator/index.ts
Normal file
1
client/src/common/components/apply-notificator/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './apply-notificator'
|
@@ -1,7 +1,7 @@
|
||||
import { useForm, type SubmitHandler } from 'react-hook-form';
|
||||
import { createServerAction } from '../../duck';
|
||||
import { useAppDispatch } from '../../../../common/hooks';
|
||||
import type { CreateServerForm } from './types';
|
||||
import type { CreateServerForm } from '../../types';
|
||||
|
||||
|
||||
|
||||
|
@@ -0,0 +1,94 @@
|
||||
import { useEffect, type FC } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
} from '@heroui/react';
|
||||
import type { CreateServerForm } from '../../types';
|
||||
import { getServer } from '../../duck/api';
|
||||
import { useAppDispatch } from '../../../../common/hooks';
|
||||
import { updateServer } from '../../duck';
|
||||
|
||||
export interface ServerEditProps {
|
||||
serverId: string;
|
||||
isOpen: boolean;
|
||||
onOpenChange: () => void;
|
||||
}
|
||||
|
||||
export const ServerEdit: FC<ServerEditProps> = (props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { serverId, isOpen, onOpenChange } = props;
|
||||
const { register, handleSubmit, reset } = useForm<CreateServerForm>();
|
||||
|
||||
useEffect(() => {
|
||||
getServer(serverId).then((response) => {
|
||||
const { data } = response;
|
||||
reset({
|
||||
...data,
|
||||
grpc_port: String(data.grpc_port),
|
||||
});
|
||||
});
|
||||
}, [serverId]);
|
||||
|
||||
const onSubmit = (values: CreateServerForm) => {
|
||||
const data = {
|
||||
...values,
|
||||
grpc_port: parseInt(values.grpc_port),
|
||||
};
|
||||
|
||||
dispatch(
|
||||
updateServer({
|
||||
id: serverId,
|
||||
server: data,
|
||||
}),
|
||||
).then(() => {
|
||||
|
||||
onOpenChange();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
Modal Title
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="form-group">
|
||||
<label>Name:</label>
|
||||
<input {...register('name', { required: true })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Hostname:</label>
|
||||
<input {...register('hostname', { required: true })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>gRPC Port:</label>
|
||||
<input
|
||||
type="number"
|
||||
{...register('grpc_port', { required: true })}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@@ -0,0 +1,60 @@
|
||||
import type { FC } from 'react';
|
||||
import { useDisclosure } from '@heroui/react';
|
||||
import { deleteServerAction, type ServerDTO } from '../../duck';
|
||||
import { testServer } from '../../duck/api';
|
||||
import { appNotificator } from '../../../../utils/notification/app-notificator';
|
||||
import { useAppDispatch } from '../../../../common/hooks';
|
||||
import { ServerEdit } from './server-edit';
|
||||
|
||||
export interface ServerViewProps {
|
||||
server: ServerDTO;
|
||||
}
|
||||
|
||||
export const ServerView: FC<ServerViewProps> = ({ server }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleTestServer = () => {
|
||||
testServer(server.id).then((result) => {
|
||||
const { connected } = result.data;
|
||||
appNotificator.add({
|
||||
message: connected ? 'Connection OK' : 'Connection failed',
|
||||
type: connected ? 'success' : 'error',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteServer = () => {
|
||||
if (confirm('Delete server?')) {
|
||||
dispatch(deleteServerAction(server.id));
|
||||
}
|
||||
};
|
||||
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td>{server.name}</td>
|
||||
<td>{server.hostname}</td>
|
||||
<td>{server.grpc_port}</td>
|
||||
<td>{server.status}</td>
|
||||
<td>
|
||||
<button className="btn btn-success" onClick={handleTestServer}>
|
||||
Test
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={onOpen}>
|
||||
Edit
|
||||
</button>
|
||||
<button className="btn btn-danger" onClick={handleDeleteServer}>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<ServerEdit
|
||||
serverId={server.id}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@@ -1,5 +1,6 @@
|
||||
import type { FC } from 'react';
|
||||
import type { ServerDTO } from '../../duck';
|
||||
import { ServerView } from './server-view';
|
||||
|
||||
export interface ServersListProps {
|
||||
servers: ServerDTO[];
|
||||
@@ -10,41 +11,20 @@ export const ServersList: FC<ServersListProps> = (props) => {
|
||||
|
||||
return (
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Hostname</th>
|
||||
<th>Port</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{servers.map((s) => (
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{s.name}</td>
|
||||
<td>{s.hostname}</td>
|
||||
<td>{s.grpc_port}</td>
|
||||
<td>{s.status}</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
// onclick="testServer('${s.id}')"
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
// onclick="editServer('${s.id}')"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
// onclick="deleteServer('${s.id}')"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
<th>Name</th>
|
||||
<th>Hostname</th>
|
||||
<th>Port</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{servers.map((server) => (
|
||||
<ServerView key={server.id} server={server} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { serversSlice } from './slice';
|
||||
import { createServer, getServers } from './api';
|
||||
import { createServer, deleteServer, getServers, patchServer } from './api';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { getServersState } from './selectors';
|
||||
import type { RootState } from '../../../store';
|
||||
import type { CreateServerDTO } from './dto';
|
||||
import { appNotificator } from '../../../utils/notification/app-notificator';
|
||||
|
||||
const PREFFIX = 'servers'
|
||||
const PREFFIX = 'servers';
|
||||
|
||||
export const fetchServers = createAsyncThunk(
|
||||
`${PREFFIX}/fetchAll`,
|
||||
@@ -20,7 +20,6 @@ export const fetchServers = createAsyncThunk(
|
||||
dispatch(serversSlice.actions.setLoading(true));
|
||||
const response = await getServers().then(({ data }) => data);
|
||||
dispatch(serversSlice.actions.setServers(response));
|
||||
|
||||
} catch (e) {
|
||||
const message =
|
||||
e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`;
|
||||
@@ -34,14 +33,67 @@ export const fetchServers = createAsyncThunk(
|
||||
export const createServerAction = createAsyncThunk(
|
||||
`${PREFFIX}/createServer`,
|
||||
async (params: CreateServerDTO, { dispatch }) => {
|
||||
try{
|
||||
await createServer(params)
|
||||
dispatch(fetchServers())
|
||||
} catch(e){
|
||||
try {
|
||||
await createServer(params);
|
||||
dispatch(fetchServers());
|
||||
} catch (e) {
|
||||
appNotificator.add({
|
||||
message: e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/createServer`,
|
||||
type: 'error'
|
||||
})
|
||||
message:
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: `Unknown error in ${PREFFIX}/createServer`,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
export const deleteServerAction = createAsyncThunk(
|
||||
`${PREFFIX}/deleteServer`,
|
||||
async (id: string, { dispatch }) => {
|
||||
try {
|
||||
await deleteServer(id);
|
||||
appNotificator.add({
|
||||
message: 'Server deleted',
|
||||
type: 'success',
|
||||
});
|
||||
dispatch(fetchServers());
|
||||
} catch (e) {
|
||||
appNotificator.add({
|
||||
type: 'error',
|
||||
message:
|
||||
e instanceof Error
|
||||
? `Delete error: ${e.message}`
|
||||
: `Unknown error in ${PREFFIX}/deleteServer`,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const updateServer = createAsyncThunk(
|
||||
`${PREFFIX}/updateServer`,
|
||||
async (
|
||||
params: {
|
||||
id: string;
|
||||
server: CreateServerDTO;
|
||||
},
|
||||
{ dispatch },
|
||||
) => {
|
||||
try {
|
||||
await patchServer(params.id, params.server);
|
||||
dispatch(fetchServers());
|
||||
appNotificator.add({
|
||||
message: 'Server updated',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
appNotificator.add({
|
||||
type: 'error',
|
||||
message:
|
||||
e instanceof Error
|
||||
? `Error updating: ${e.message}`
|
||||
: `Unknown error in ${PREFFIX}/deleteServer`,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@@ -1,13 +1,28 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { api } from '../../../api/api';
|
||||
import type { ServerDTO, CreateServerDTO } from './dto';
|
||||
import type { ServerDTO, CreateServerDTO, TestServerDTO } from './dto';
|
||||
|
||||
export const getServers = () =>
|
||||
api.get<never, AxiosResponse<ServerDTO[]>>('/servers');
|
||||
|
||||
export const createServer = (params: CreateServerDTO ) =>
|
||||
export const createServer = (params: CreateServerDTO) =>
|
||||
api.post<AxiosResponse>('servers', params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
export const testServer = (id: string) =>
|
||||
api.post<TestServerDTO>(`/servers/${id}/test`);
|
||||
|
||||
export const deleteServer = (id: string) => api.delete(`/servers/${id}`);
|
||||
|
||||
export const getServer = (id: string) =>
|
||||
api.get<string, AxiosResponse<ServerDTO>>(`/servers/${id}`);
|
||||
|
||||
export const patchServer = (id: string, server: CreateServerDTO) =>
|
||||
api.put(`/servers/${id}`, server, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
export interface ServerDTO {
|
||||
id: number | string
|
||||
id: string
|
||||
name: string
|
||||
hostname: string
|
||||
grpc_port: number
|
||||
@@ -11,3 +11,8 @@ export interface CreateServerDTO {
|
||||
hostname: string;
|
||||
grpc_port: number;
|
||||
}
|
||||
|
||||
export interface TestServerDTO {
|
||||
connected: boolean,
|
||||
endpoint: string
|
||||
}
|
||||
|
1
client/src/features/servers/types/index.ts
Normal file
1
client/src/features/servers/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './form'
|
2
client/src/hero.ts
Normal file
2
client/src/hero.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import { heroui } from "@heroui/react";
|
||||
export default heroui();
|
@@ -0,0 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@plugin './hero.ts';
|
||||
|
||||
@source '../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
@custom-variant dark (&:is(.dark *));
|
@@ -3,13 +3,20 @@ import { createRoot } from 'react-dom/client';
|
||||
import { RouterProvider } from 'react-router/dom';
|
||||
import { store } from './store/store';
|
||||
import { Provider } from 'react-redux';
|
||||
import { HeroUIProvider } from '@heroui/react';
|
||||
import {ToastProvider} from "@heroui/toast";
|
||||
import { router } from './router';
|
||||
import './index.css';
|
||||
import { ApplyNotificator } from './common/components/apply-notificator';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
<RouterProvider router={router} />
|
||||
<HeroUIProvider>
|
||||
<RouterProvider router={router} />
|
||||
<ToastProvider/>
|
||||
<ApplyNotificator/>
|
||||
</HeroUIProvider>
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
@@ -7,26 +7,13 @@ export const Home = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="container">
|
||||
<h1>Xray Admin Panel - Test Interface</h1>
|
||||
<h1 className="text-3xl font-bold underline">Xray Admin Panel - Test Interface</h1>
|
||||
|
||||
{/* <!-- Toast notifications container --> */}
|
||||
<div className="toast-container" id="toastContainer"></div>
|
||||
|
||||
<NavMenu items={navItems} />
|
||||
|
||||
{/* <!-- Dashboard --> */}
|
||||
|
||||
<Outlet />
|
||||
|
||||
{/* <!-- Servers --> */}
|
||||
|
||||
{/* <!-- Templates --> */}
|
||||
|
||||
{/* <!-- Certificates --> */}
|
||||
|
||||
{/* <!-- Server Inbounds --> */}
|
||||
|
||||
{/* <!-- Users --> */}
|
||||
</div>
|
||||
|
||||
{/* <!-- Modal dialogs --> */}
|
||||
|
@@ -9,8 +9,15 @@ export interface Notificator {
|
||||
remove: (id: number) => void;
|
||||
getAll: () => Notice[];
|
||||
}
|
||||
|
||||
export interface INotificatorViewProvider<T = Record<string, unknown>> {
|
||||
paramsMappers: (notice: Notice) => T;
|
||||
show: (params: T) => void;
|
||||
}
|
||||
|
||||
class AppNotificator implements Notificator {
|
||||
public list: Map<number, Notice> = new Map();
|
||||
private viewProvider: INotificatorViewProvider | null = null;
|
||||
|
||||
add = (notice: Notice) => {
|
||||
const id = Date.now();
|
||||
@@ -20,6 +27,12 @@ class AppNotificator implements Notificator {
|
||||
return id;
|
||||
};
|
||||
|
||||
applyProvider = <T extends Record<string, unknown>>(
|
||||
provider: INotificatorViewProvider<T>,
|
||||
) => {
|
||||
(this.viewProvider as INotificatorViewProvider<T>) = provider;
|
||||
};
|
||||
|
||||
remove = (id: number) => {
|
||||
this.list.delete(id);
|
||||
};
|
||||
@@ -29,11 +42,16 @@ class AppNotificator implements Notificator {
|
||||
};
|
||||
|
||||
show = (notice: Notice, id?: number) => {
|
||||
// TODO
|
||||
alert(JSON.stringify(notice));
|
||||
if (this.viewProvider) {
|
||||
const { paramsMappers, show } = this.viewProvider;
|
||||
const params = paramsMappers(notice);
|
||||
show(params);
|
||||
} else {
|
||||
alert(JSON.stringify(notice));
|
||||
}
|
||||
if (id) {
|
||||
setTimeout(() => {
|
||||
this.remove(id)
|
||||
this.remove(id);
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
14
client/tailwind.config.js
Normal file
14
client/tailwind.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// tailwind.config.js
|
||||
const {heroui} = require("@heroui/react");
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: [heroui()],
|
||||
};
|
@@ -24,5 +24,5 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src", "hero.ts"]
|
||||
}
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
react(),
|
||||
],
|
||||
})
|
||||
|
1832
package-lock.json
generated
Normal file
1832
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.72.2"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user