diff --git a/client/package-lock.json b/client/package-lock.json index 2d7faf9..ac075f5 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13,6 +13,7 @@ "clsx": "^2.1.1", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-hook-form": "^7.64.0", "react-redux": "^9.2.0", "react-router": "^7.9.3" }, @@ -3552,6 +3553,22 @@ "react": "^19.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.64.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.64.0.tgz", + "integrity": "sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", diff --git a/client/package.json b/client/package.json index bcd6b46..2f81f92 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "clsx": "^2.1.1", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-hook-form": "^7.64.0", "react-redux": "^9.2.0", "react-router": "^7.9.3" }, diff --git a/client/src/features/servers/components/add-server/add-server.tsx b/client/src/features/servers/components/add-server/add-server.tsx new file mode 100644 index 0000000..3040b10 --- /dev/null +++ b/client/src/features/servers/components/add-server/add-server.tsx @@ -0,0 +1,44 @@ +import { useForm, type SubmitHandler } from 'react-hook-form'; +import { createServerAction } from '../../duck'; +import { useAppDispatch } from '../../../../common/hooks'; +import type { CreateServerForm } from './types'; + + + +export const AddServer = () => { + const dispatch = useAppDispatch(); + const { register, handleSubmit, reset } = useForm(); + + const onSubmit: SubmitHandler = (values) => { + const data = { + ...values, + grpc_port: parseInt(values.grpc_port) + } + dispatch(createServerAction(data)).then(() => { + reset(); + }); + }; + + return ( +
+

Add Server

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ ); +}; diff --git a/client/src/features/servers/components/add-server/index.ts b/client/src/features/servers/components/add-server/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/client/src/features/servers/components/add-server/types.ts b/client/src/features/servers/components/add-server/types.ts new file mode 100644 index 0000000..612b427 --- /dev/null +++ b/client/src/features/servers/components/add-server/types.ts @@ -0,0 +1,5 @@ +export interface CreateServerForm { + name: string; + hostname: string; + grpc_port: string; +} \ No newline at end of file diff --git a/client/src/features/servers/components/servers-list/index.ts b/client/src/features/servers/components/servers-list/index.ts new file mode 100644 index 0000000..7399bcd --- /dev/null +++ b/client/src/features/servers/components/servers-list/index.ts @@ -0,0 +1 @@ +export * from './servers-list' \ No newline at end of file diff --git a/client/src/features/servers/components/servers-list/servers-list.tsx b/client/src/features/servers/components/servers-list/servers-list.tsx new file mode 100644 index 0000000..15daec6 --- /dev/null +++ b/client/src/features/servers/components/servers-list/servers-list.tsx @@ -0,0 +1,50 @@ +import type { FC } from 'react'; +import type { ServerDTO } from '../../duck'; + +export interface ServersListProps { + servers: ServerDTO[]; +} + +export const ServersList: FC = (props) => { + const { servers } = props; + + return ( + + + + + + + + + {servers.map((s) => ( + + + + + + + + ))} +
NameHostnamePortStatusActions
{s.name}{s.hostname}{s.grpc_port}{s.status} + + + +
+ ); +}; diff --git a/client/src/features/servers/duck/actions.ts b/client/src/features/servers/duck/actions.ts index 84ceaf6..9ce7cff 100644 --- a/client/src/features/servers/duck/actions.ts +++ b/client/src/features/servers/duck/actions.ts @@ -1,8 +1,10 @@ import { serversSlice } from './slice'; -import { getServers } from './api'; +import { createServer, getServers } 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' @@ -28,3 +30,18 @@ export const fetchServers = createAsyncThunk( } }, ); + +export const createServerAction = createAsyncThunk( + `${PREFFIX}/createServer`, + async (params: CreateServerDTO, { dispatch }) => { + try{ + await createServer(params) + dispatch(fetchServers()) + } catch(e){ + appNotificator.add({ + message: e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/createServer`, + type: 'error' + }) + } + } +) \ No newline at end of file diff --git a/client/src/features/servers/duck/api.ts b/client/src/features/servers/duck/api.ts index 21eb4b1..d9651c3 100644 --- a/client/src/features/servers/duck/api.ts +++ b/client/src/features/servers/duck/api.ts @@ -1,5 +1,13 @@ import type { AxiosResponse } from 'axios'; import { api } from '../../../api/api'; -import type { ServerDTO } from './dto'; +import type { ServerDTO, CreateServerDTO } from './dto'; -export const getServers = () => api.get>('/servers'); +export const getServers = () => + api.get>('/servers'); + +export const createServer = (params: CreateServerDTO ) => + api.post('servers', params, { + headers: { + 'Content-Type': 'application/json', + }, + }); diff --git a/client/src/features/servers/duck/dto.ts b/client/src/features/servers/duck/dto.ts index 5045ad6..89ca88e 100644 --- a/client/src/features/servers/duck/dto.ts +++ b/client/src/features/servers/duck/dto.ts @@ -4,4 +4,10 @@ export interface ServerDTO { hostname: string grpc_port: number status: string -} \ No newline at end of file +} + +export interface CreateServerDTO { + name: string; + hostname: string; + grpc_port: number; +} diff --git a/client/src/pages/servers/servers.tsx b/client/src/pages/servers/servers.tsx index 4a9da10..1d4b746 100644 --- a/client/src/pages/servers/servers.tsx +++ b/client/src/pages/servers/servers.tsx @@ -1,33 +1,32 @@ +import { useEffect } from 'react'; import type { RouteObject } from 'react-router'; +import { AddServer } from '../../features/servers/components/add-server/add-server'; +import { fetchServers, getServersState } from '../../features'; +import { useAppDispatch, useAppSelector } from '../../common/hooks'; +import clsx from 'clsx'; +import { ServersList } from '../../features/servers/components/servers-list'; export const Servers = () => { + const dispatch = useAppDispatch(); + const { loading, servers } = useAppSelector(getServersState); + + useEffect(() => { + dispatch(fetchServers()); + }, [dispatch]); + return (
-
-

Add Server

-
-
- - -
-
- - -
-
- - -
- -
-
+

Servers List

-
- Loading... +
+ {loading && 'Loading...'} + {servers.length ? ( + + ) : ( +

No servers found

+ )}
diff --git a/client/src/utils/notification/app-notificator.ts b/client/src/utils/notification/app-notificator.ts new file mode 100644 index 0000000..2e296ec --- /dev/null +++ b/client/src/utils/notification/app-notificator.ts @@ -0,0 +1,42 @@ +export type NoticeType = 'success' | 'error' | 'warn'; +export interface Notice { + message: string; + type: NoticeType; +} +export interface Notificator { + list: Map; + add: (notice: Notice) => number; + remove: (id: number) => void; + getAll: () => Notice[]; +} +class AppNotificator implements Notificator { + public list: Map = new Map(); + + add = (notice: Notice) => { + const id = Date.now(); + this.list.set(id, notice); + this.show(notice, id); + // TODO show on UI + return id; + }; + + remove = (id: number) => { + this.list.delete(id); + }; + + getAll = () => { + return Array.from(this.list.values()); + }; + + show = (notice: Notice, id?: number) => { + // TODO + alert(JSON.stringify(notice)); + if (id) { + setTimeout(() => { + this.remove(id) + }, 300); + } + }; +} + +export const appNotificator = new AppNotificator();