mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-24 09:19:09 +00:00
feat: added redux & ducks
This commit is contained in:
113
client/package-lock.json
generated
113
client/package-lock.json
generated
@@ -8,10 +8,12 @@
|
|||||||
"name": "client",
|
"name": "client",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.9.0",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-router": "^7.9.3"
|
"react-router": "^7.9.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1052,6 +1054,32 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^10.0.3",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.38",
|
"version": "1.0.0-beta.38",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
|
||||||
@@ -1367,6 +1395,18 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -1440,7 +1480,7 @@
|
|||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==",
|
"integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -1456,6 +1496,12 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.45.0",
|
"version": "8.45.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz",
|
||||||
@@ -2126,7 +2172,7 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
@@ -2848,6 +2894,16 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
|
||||||
|
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@@ -3496,6 +3552,29 @@
|
|||||||
"react": "^19.2.0"
|
"react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
@@ -3528,6 +3607,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@@ -3979,6 +4079,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.8",
|
"version": "7.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.8.tgz",
|
||||||
|
|||||||
@@ -13,10 +13,12 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^2.9.0",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-router": "^7.9.3"
|
"react-router": "^7.9.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { api } from './api';
|
|
||||||
|
|
||||||
export interface Certificate {}
|
|
||||||
|
|
||||||
export const getCertificates = api.get<never, Certificate[]>('/certificates');
|
|
||||||
@@ -1,4 +1 @@
|
|||||||
export * from './certificates';
|
export * from './api'
|
||||||
export * from './servers';
|
|
||||||
export * from './templates';
|
|
||||||
export * from './users';
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { api } from './api';
|
|
||||||
|
|
||||||
export interface Server {}
|
|
||||||
|
|
||||||
export const getServers = api.get<never, Server[]>('/servers');
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { api } from './api';
|
|
||||||
|
|
||||||
export interface Template {}
|
|
||||||
|
|
||||||
export const getTemplates = api.get<never, Template[]>('/templates');
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { api } from './api';
|
|
||||||
|
|
||||||
export interface User {}
|
|
||||||
|
|
||||||
export const getUsers = api.get<never, User[]>('/users');
|
|
||||||
2
client/src/common/hooks/index.ts
Normal file
2
client/src/common/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './use-app-dispatch'
|
||||||
|
export * from './use-app-selector'
|
||||||
4
client/src/common/hooks/use-app-dispatch.ts
Normal file
4
client/src/common/hooks/use-app-dispatch.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import type { AppDispatch } from '../../store'
|
||||||
|
|
||||||
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||||
4
client/src/common/hooks/use-app-selector.ts
Normal file
4
client/src/common/hooks/use-app-selector.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import type { RootState } from '../../store'
|
||||||
|
|
||||||
|
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||||
30
client/src/features/certificates/duck/actions.ts
Normal file
30
client/src/features/certificates/duck/actions.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { certificateSlice } from './slice';
|
||||||
|
import { getCertificates } from './api';
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { getCertificatesState } from './selectors';
|
||||||
|
import type { RootState } from '../../../store';
|
||||||
|
|
||||||
|
const PREFFIX = 'certificates'
|
||||||
|
|
||||||
|
export const fetchCertificates = createAsyncThunk(
|
||||||
|
`${PREFFIX}/fetchAll`,
|
||||||
|
async (_, { dispatch, getState }) => {
|
||||||
|
const { loading } = getCertificatesState(getState() as RootState);
|
||||||
|
try {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(certificateSlice.actions.setLoading(true));
|
||||||
|
const response = await getCertificates().then(({ data }) => data);
|
||||||
|
dispatch(certificateSlice.actions.setUsers(response));
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
const message =
|
||||||
|
e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`;
|
||||||
|
dispatch(certificateSlice.actions.setError(message));
|
||||||
|
} finally {
|
||||||
|
dispatch(certificateSlice.actions.setLoading(false));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
6
client/src/features/certificates/duck/api.ts
Normal file
6
client/src/features/certificates/duck/api.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { AxiosResponse } from 'axios';
|
||||||
|
import { api } from '../../../api/api';
|
||||||
|
import type { CertificateDTO } from './dto';
|
||||||
|
|
||||||
|
export const getCertificates = () =>
|
||||||
|
api.get<never, AxiosResponse<CertificateDTO[]>>('/certificates');
|
||||||
1
client/src/features/certificates/duck/dto.ts
Normal file
1
client/src/features/certificates/duck/dto.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export interface CertificateDTO {}
|
||||||
4
client/src/features/certificates/duck/index.ts
Normal file
4
client/src/features/certificates/duck/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './actions'
|
||||||
|
export * from './dto'
|
||||||
|
export * from './slice'
|
||||||
|
export * from './selectors'
|
||||||
3
client/src/features/certificates/duck/selectors.ts
Normal file
3
client/src/features/certificates/duck/selectors.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { RootState } from '../../../store';
|
||||||
|
|
||||||
|
export const getCertificatesState = (state: RootState) => state.certificates;
|
||||||
32
client/src/features/certificates/duck/slice.ts
Normal file
32
client/src/features/certificates/duck/slice.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import type { CertificateDTO } from './dto';
|
||||||
|
|
||||||
|
export interface CertificatesState {
|
||||||
|
loading: boolean;
|
||||||
|
certificates: CertificateDTO[]
|
||||||
|
error: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: CertificatesState = {
|
||||||
|
loading: false,
|
||||||
|
certificates: [],
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const certificateSlice = createSlice({
|
||||||
|
name: 'certificates',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.loading = action.payload
|
||||||
|
},
|
||||||
|
setUsers: (state, action: PayloadAction<CertificateDTO[]>) => {
|
||||||
|
state.certificates = action.payload
|
||||||
|
},
|
||||||
|
setError: (state, action: PayloadAction<string>) => {
|
||||||
|
state.error = action.payload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
1
client/src/features/certificates/index.ts
Normal file
1
client/src/features/certificates/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './duck'
|
||||||
4
client/src/features/index.ts
Normal file
4
client/src/features/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './servers'
|
||||||
|
export * from './templates'
|
||||||
|
export * from './users'
|
||||||
|
export * from './certificates'
|
||||||
30
client/src/features/servers/duck/actions.ts
Normal file
30
client/src/features/servers/duck/actions.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { serversSlice } from './slice';
|
||||||
|
import { getServers } from './api';
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { getServersState } from './selectors';
|
||||||
|
import type { RootState } from '../../../store';
|
||||||
|
|
||||||
|
const PREFFIX = 'servers'
|
||||||
|
|
||||||
|
export const fetchServers = createAsyncThunk(
|
||||||
|
`${PREFFIX}/fetchAll`,
|
||||||
|
async (_, { dispatch, getState }) => {
|
||||||
|
const { loading } = getServersState(getState() as RootState);
|
||||||
|
try {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`;
|
||||||
|
dispatch(serversSlice.actions.setError(message));
|
||||||
|
} finally {
|
||||||
|
dispatch(serversSlice.actions.setLoading(false));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
5
client/src/features/servers/duck/api.ts
Normal file
5
client/src/features/servers/duck/api.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { AxiosResponse } from 'axios';
|
||||||
|
import { api } from '../../../api/api';
|
||||||
|
import type { ServerDTO } from './dto';
|
||||||
|
|
||||||
|
export const getServers = () => api.get<never, AxiosResponse<ServerDTO[]>>('/servers');
|
||||||
7
client/src/features/servers/duck/dto.ts
Normal file
7
client/src/features/servers/duck/dto.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface ServerDTO {
|
||||||
|
id: number | string
|
||||||
|
name: string
|
||||||
|
hostname: string
|
||||||
|
grpc_port: number
|
||||||
|
status: string
|
||||||
|
}
|
||||||
4
client/src/features/servers/duck/index.ts
Normal file
4
client/src/features/servers/duck/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './actions'
|
||||||
|
export * from './dto'
|
||||||
|
export * from './slice'
|
||||||
|
export * from './selectors'
|
||||||
3
client/src/features/servers/duck/selectors.ts
Normal file
3
client/src/features/servers/duck/selectors.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { RootState } from '../../../store';
|
||||||
|
|
||||||
|
export const getServersState = (state: RootState) => state.servers;
|
||||||
32
client/src/features/servers/duck/slice.ts
Normal file
32
client/src/features/servers/duck/slice.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import type { ServerDTO } from './dto';
|
||||||
|
|
||||||
|
export interface ServersState {
|
||||||
|
loading: boolean;
|
||||||
|
servers: ServerDTO[];
|
||||||
|
error: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ServersState = {
|
||||||
|
loading: false,
|
||||||
|
servers: [],
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serversSlice = createSlice({
|
||||||
|
name: 'servers',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.loading = action.payload
|
||||||
|
},
|
||||||
|
setServers: (state, action: PayloadAction<ServerDTO[]>) => {
|
||||||
|
state.servers = action.payload
|
||||||
|
},
|
||||||
|
setError: (state, action: PayloadAction<string>) => {
|
||||||
|
state.error = action.payload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
1
client/src/features/servers/index.ts
Normal file
1
client/src/features/servers/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './duck'
|
||||||
29
client/src/features/templates/duck/actions.ts
Normal file
29
client/src/features/templates/duck/actions.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { templatesSlice } from './slice';
|
||||||
|
import { getTemplates } from './api';
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { getTemplatesState } from './selectors';
|
||||||
|
import type { RootState } from '../../../store';
|
||||||
|
|
||||||
|
const PREFFIX = 'templates';
|
||||||
|
|
||||||
|
export const fetchTemplates = createAsyncThunk(
|
||||||
|
`${PREFFIX}/fetchAll`,
|
||||||
|
async (_, { dispatch, getState }) => {
|
||||||
|
const { loading } = getTemplatesState(getState() as RootState);
|
||||||
|
try {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(templatesSlice.actions.setLoading(true));
|
||||||
|
const response = await getTemplates().then(({ data }) => data);
|
||||||
|
dispatch(templatesSlice.actions.setTemplates(response));
|
||||||
|
} catch (e) {
|
||||||
|
const message =
|
||||||
|
e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`;
|
||||||
|
dispatch(templatesSlice.actions.setError(message));
|
||||||
|
} finally {
|
||||||
|
dispatch(templatesSlice.actions.setLoading(false));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
5
client/src/features/templates/duck/api.ts
Normal file
5
client/src/features/templates/duck/api.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { AxiosResponse } from 'axios';
|
||||||
|
import { api } from '../../../api/api';
|
||||||
|
import type { TemplateDTO } from './dto';
|
||||||
|
|
||||||
|
export const getTemplates = () => api.get<never, AxiosResponse<TemplateDTO[]>>('/templates');
|
||||||
1
client/src/features/templates/duck/dto.ts
Normal file
1
client/src/features/templates/duck/dto.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export interface TemplateDTO {}
|
||||||
4
client/src/features/templates/duck/index.ts
Normal file
4
client/src/features/templates/duck/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './actions'
|
||||||
|
export * from './dto'
|
||||||
|
export * from './slice'
|
||||||
|
export * from './selectors'
|
||||||
3
client/src/features/templates/duck/selectors.ts
Normal file
3
client/src/features/templates/duck/selectors.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { RootState } from '../../../store';
|
||||||
|
|
||||||
|
export const getTemplatesState = (state: RootState) => state.templates;
|
||||||
32
client/src/features/templates/duck/slice.ts
Normal file
32
client/src/features/templates/duck/slice.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import type { TemplateDTO } from './dto';
|
||||||
|
|
||||||
|
export interface TemplateState {
|
||||||
|
loading: boolean;
|
||||||
|
templates: TemplateDTO[];
|
||||||
|
error: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: TemplateState = {
|
||||||
|
loading: false,
|
||||||
|
templates: [],
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const templatesSlice = createSlice({
|
||||||
|
name: 'templates',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.loading = action.payload
|
||||||
|
},
|
||||||
|
setTemplates: (state, action: PayloadAction<TemplateDTO[]>) => {
|
||||||
|
state.templates = action.payload
|
||||||
|
},
|
||||||
|
setError: (state, action: PayloadAction<string>) => {
|
||||||
|
state.error = action.payload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
1
client/src/features/templates/index.ts
Normal file
1
client/src/features/templates/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './duck'
|
||||||
30
client/src/features/users/duck/actions.ts
Normal file
30
client/src/features/users/duck/actions.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { usersSlice } from './slice';
|
||||||
|
import { getUsers } from './api';
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { getUsersState } from './selectors';
|
||||||
|
import type { RootState } from '../../../store';
|
||||||
|
|
||||||
|
const PREFFIX = 'users'
|
||||||
|
|
||||||
|
export const fetchUsers = createAsyncThunk(
|
||||||
|
`${PREFFIX}/fetchAll`,
|
||||||
|
async (_, { dispatch, getState }) => {
|
||||||
|
const { loading } = getUsersState(getState() as RootState);
|
||||||
|
try {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(usersSlice.actions.setLoading(true));
|
||||||
|
const response = await getUsers().then(({ data }) => data);
|
||||||
|
dispatch(usersSlice.actions.setUsers(response));
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
const message =
|
||||||
|
e instanceof Error ? e.message : `Unknown error in ${PREFFIX}/fetchAll`;
|
||||||
|
dispatch(usersSlice.actions.setError(message));
|
||||||
|
} finally {
|
||||||
|
dispatch(usersSlice.actions.setLoading(false));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
5
client/src/features/users/duck/api.ts
Normal file
5
client/src/features/users/duck/api.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { AxiosResponse } from 'axios';
|
||||||
|
import { api } from '../../../api/api';
|
||||||
|
import type { UserDTO } from './dto';
|
||||||
|
|
||||||
|
export const getUsers = () => api.get<never, AxiosResponse<UserDTO[]>>('/users');
|
||||||
8
client/src/features/users/duck/dto.ts
Normal file
8
client/src/features/users/duck/dto.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface User {}
|
||||||
|
|
||||||
|
export interface UserDTO {
|
||||||
|
page: number
|
||||||
|
per_page: number
|
||||||
|
total: number
|
||||||
|
users: User[]
|
||||||
|
}
|
||||||
4
client/src/features/users/duck/index.ts
Normal file
4
client/src/features/users/duck/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './actions'
|
||||||
|
export * from './dto'
|
||||||
|
export * from './slice'
|
||||||
|
export * from './selectors'
|
||||||
3
client/src/features/users/duck/selectors.ts
Normal file
3
client/src/features/users/duck/selectors.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { RootState } from '../../../store';
|
||||||
|
|
||||||
|
export const getUsersState = (state: RootState) => state.users;
|
||||||
32
client/src/features/users/duck/slice.ts
Normal file
32
client/src/features/users/duck/slice.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import type { UserDTO, User } from './dto';
|
||||||
|
|
||||||
|
export interface UsersState {
|
||||||
|
loading: boolean;
|
||||||
|
users: User[]
|
||||||
|
error: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: UsersState = {
|
||||||
|
loading: false,
|
||||||
|
users: [],
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usersSlice = createSlice({
|
||||||
|
name: 'certificate',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.loading = action.payload
|
||||||
|
},
|
||||||
|
setUsers: (state, action: PayloadAction<UserDTO[]>) => {
|
||||||
|
state.users = action.payload.users
|
||||||
|
},
|
||||||
|
setError: (state, action: PayloadAction<string>) => {
|
||||||
|
state.error = action.payload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
1
client/src/features/users/index.ts
Normal file
1
client/src/features/users/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './duck'
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { RouterProvider } from 'react-router/dom';
|
import { RouterProvider } from 'react-router/dom';
|
||||||
|
import { store } from './store/store';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<RouterProvider router={router} />
|
<Provider store={store}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</Provider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,55 +1,32 @@
|
|||||||
import type { RouteObject } from 'react-router';
|
import type { RouteObject } from 'react-router';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../common/hooks';
|
||||||
import {
|
import {
|
||||||
getServers,
|
fetchServers,
|
||||||
getTemplates,
|
getServersState,
|
||||||
getCertificates,
|
fetchTemplates,
|
||||||
getUsers,
|
getTemplatesState,
|
||||||
type Server,
|
fetchUsers,
|
||||||
type Template,
|
getUsersState,
|
||||||
type Certificate,
|
getCertificatesState,
|
||||||
type User,
|
fetchCertificates,
|
||||||
} from '../../api';
|
} from '../../features';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export const loadDashboard = async () => {
|
|
||||||
try {
|
|
||||||
const [servers, templates, certificates, users] = await Promise.all([
|
|
||||||
getServers.then((data) => data),
|
|
||||||
getTemplates.then((data) => data),
|
|
||||||
getCertificates.then((data) => data),
|
|
||||||
getUsers.then((data) => data),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [servers, templates, certificates, users];
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
alert('loading error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Dashboard = () => {
|
export const Dashboard = () => {
|
||||||
const [servers, setServers] = useState<Server[] | undefined>(undefined);
|
const dispatch = useAppDispatch();
|
||||||
const [templates, setTemplates] = useState<Template[] | undefined>(undefined);
|
const { loading: serverLoading, servers } = useAppSelector(getServersState);
|
||||||
const [certificates, setCertificates] = useState<Certificate[] | undefined>(
|
const { loading: usersLoading, users } = useAppSelector(getUsersState);
|
||||||
undefined,
|
const { loading: certificatesLoading, certificates } =
|
||||||
);
|
useAppSelector(getCertificatesState);
|
||||||
const [users, setUsers] = useState<User[] | undefined>(undefined);
|
const { loading: templatesLoading, templates } =
|
||||||
|
useAppSelector(getTemplatesState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDashboard()
|
dispatch(fetchServers());
|
||||||
.then((res) => {
|
dispatch(fetchTemplates());
|
||||||
if (res) {
|
dispatch(fetchUsers());
|
||||||
const [servers, templates, certificates, users] = res;
|
dispatch(fetchCertificates());
|
||||||
setServers(servers);
|
}, [dispatch]);
|
||||||
setTemplates(templates);
|
|
||||||
setCertificates(certificates);
|
|
||||||
setUsers(users);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="dashboard" className="tab-content active">
|
<div id="dashboard" className="tab-content active">
|
||||||
@@ -58,24 +35,30 @@ export const Dashboard = () => {
|
|||||||
<p>
|
<p>
|
||||||
Servers:{' '}
|
Servers:{' '}
|
||||||
<span id="serverCount">
|
<span id="serverCount">
|
||||||
{servers ? servers.length || 0 : 'Loading...'}
|
{serverLoading === true && 'Loading...'}
|
||||||
|
{servers && String(servers.length)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Templates:{' '}
|
Templates:{' '}
|
||||||
<span id="templateCount">
|
<span id="templateCount">
|
||||||
{templates ? templates.length || 0 : 'Loading...'}
|
{templatesLoading && 'Loading...'}
|
||||||
|
{templates && String(templates.length)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Certificates:{' '}
|
Certificates:{' '}
|
||||||
<span id="certCount">
|
<span id="certCount">
|
||||||
{certificates ? certificates.length || 0 : 'Loading...'}
|
{certificatesLoading && 'Loading...'}
|
||||||
|
{certificates && String(certificates.length)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Users:{' '}
|
Users:{' '}
|
||||||
<span id="userCount">{users ? users.length || 0 : 'Loading...'}</span>
|
<span id="userCount">
|
||||||
|
{usersLoading && 'Loading...'}
|
||||||
|
{users && String(users.length)}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
client/src/store/index.ts
Normal file
1
client/src/store/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './store'
|
||||||
16
client/src/store/store.ts
Normal file
16
client/src/store/store.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { configureStore, combineReducers } from '@reduxjs/toolkit'
|
||||||
|
import {serversSlice, templatesSlice, certificateSlice, usersSlice} from '../features'
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: combineReducers({
|
||||||
|
servers: serversSlice.reducer,
|
||||||
|
templates: templatesSlice.reducer,
|
||||||
|
users: usersSlice.reducer,
|
||||||
|
certificates: certificateSlice.reducer
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||||
|
export type RootState = ReturnType<typeof store.getState>
|
||||||
|
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
||||||
|
export type AppDispatch = typeof store.dispatch
|
||||||
Reference in New Issue
Block a user