mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-23 16:59:08 +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",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"axios": "^1.12.2",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1052,6 +1054,32 @@
|
||||
"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": {
|
||||
"version": "1.0.0-beta.38",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
|
||||
@@ -1367,6 +1395,18 @@
|
||||
"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": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -1440,7 +1480,7 @@
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@@ -1456,6 +1496,12 @@
|
||||
"@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": {
|
||||
"version": "8.45.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz",
|
||||
@@ -2126,7 +2172,7 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
@@ -2848,6 +2894,16 @@
|
||||
"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": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
@@ -3496,6 +3552,29 @@
|
||||
"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": {
|
||||
"version": "0.17.0",
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -3979,6 +4079,15 @@
|
||||
"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": {
|
||||
"version": "7.1.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.8.tgz",
|
||||
|
@@ -13,10 +13,12 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"axios": "^1.12.2",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.9.3"
|
||||
},
|
||||
"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 './servers';
|
||||
export * from './templates';
|
||||
export * from './users';
|
||||
export * from './api'
|
@@ -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 { createRoot } from 'react-dom/client';
|
||||
import { RouterProvider } from 'react-router/dom';
|
||||
import { store } from './store/store';
|
||||
import { Provider } from 'react-redux';
|
||||
import { router } from './router';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
<Provider store={store}>
|
||||
<RouterProvider router={router} />
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
@@ -1,55 +1,32 @@
|
||||
import type { RouteObject } from 'react-router';
|
||||
import { useEffect } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../common/hooks';
|
||||
import {
|
||||
getServers,
|
||||
getTemplates,
|
||||
getCertificates,
|
||||
getUsers,
|
||||
type Server,
|
||||
type Template,
|
||||
type Certificate,
|
||||
type User,
|
||||
} from '../../api';
|
||||
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');
|
||||
}
|
||||
};
|
||||
fetchServers,
|
||||
getServersState,
|
||||
fetchTemplates,
|
||||
getTemplatesState,
|
||||
fetchUsers,
|
||||
getUsersState,
|
||||
getCertificatesState,
|
||||
fetchCertificates,
|
||||
} from '../../features';
|
||||
|
||||
export const Dashboard = () => {
|
||||
const [servers, setServers] = useState<Server[] | undefined>(undefined);
|
||||
const [templates, setTemplates] = useState<Template[] | undefined>(undefined);
|
||||
const [certificates, setCertificates] = useState<Certificate[] | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [users, setUsers] = useState<User[] | undefined>(undefined);
|
||||
const dispatch = useAppDispatch();
|
||||
const { loading: serverLoading, servers } = useAppSelector(getServersState);
|
||||
const { loading: usersLoading, users } = useAppSelector(getUsersState);
|
||||
const { loading: certificatesLoading, certificates } =
|
||||
useAppSelector(getCertificatesState);
|
||||
const { loading: templatesLoading, templates } =
|
||||
useAppSelector(getTemplatesState);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboard()
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
const [servers, templates, certificates, users] = res;
|
||||
setServers(servers);
|
||||
setTemplates(templates);
|
||||
setCertificates(certificates);
|
||||
setUsers(users);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
}, []);
|
||||
dispatch(fetchServers());
|
||||
dispatch(fetchTemplates());
|
||||
dispatch(fetchUsers());
|
||||
dispatch(fetchCertificates());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div id="dashboard" className="tab-content active">
|
||||
@@ -58,24 +35,30 @@ export const Dashboard = () => {
|
||||
<p>
|
||||
Servers:{' '}
|
||||
<span id="serverCount">
|
||||
{servers ? servers.length || 0 : 'Loading...'}
|
||||
{serverLoading === true && 'Loading...'}
|
||||
{servers && String(servers.length)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Templates:{' '}
|
||||
<span id="templateCount">
|
||||
{templates ? templates.length || 0 : 'Loading...'}
|
||||
{templatesLoading && 'Loading...'}
|
||||
{templates && String(templates.length)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Certificates:{' '}
|
||||
<span id="certCount">
|
||||
{certificates ? certificates.length || 0 : 'Loading...'}
|
||||
{certificatesLoading && 'Loading...'}
|
||||
{certificates && String(certificates.length)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Users:{' '}
|
||||
<span id="userCount">{users ? users.length || 0 : 'Loading...'}</span>
|
||||
<span id="userCount">
|
||||
{usersLoading && 'Loading...'}
|
||||
{users && String(users.length)}
|
||||
</span>
|
||||
</p>
|
||||
</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