diff --git a/client/package-lock.json b/client/package-lock.json index 4e2ceba..2d7faf9 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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", diff --git a/client/package.json b/client/package.json index 9eef341..bcd6b46 100644 --- a/client/package.json +++ b/client/package.json @@ -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": { diff --git a/client/src/api/certificates.ts b/client/src/api/certificates.ts deleted file mode 100644 index 7cab77b..0000000 --- a/client/src/api/certificates.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { api } from './api'; - -export interface Certificate {} - -export const getCertificates = api.get('/certificates'); diff --git a/client/src/api/index.ts b/client/src/api/index.ts index f3e7afc..201fc0f 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -1,4 +1 @@ -export * from './certificates'; -export * from './servers'; -export * from './templates'; -export * from './users'; +export * from './api' \ No newline at end of file diff --git a/client/src/api/servers.ts b/client/src/api/servers.ts deleted file mode 100644 index 4535e16..0000000 --- a/client/src/api/servers.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { api } from './api'; - -export interface Server {} - -export const getServers = api.get('/servers'); diff --git a/client/src/api/templates.ts b/client/src/api/templates.ts deleted file mode 100644 index 3351246..0000000 --- a/client/src/api/templates.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { api } from './api'; - -export interface Template {} - -export const getTemplates = api.get('/templates'); diff --git a/client/src/api/users.ts b/client/src/api/users.ts deleted file mode 100644 index 491438b..0000000 --- a/client/src/api/users.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { api } from './api'; - -export interface User {} - -export const getUsers = api.get('/users'); diff --git a/client/src/common/hooks/index.ts b/client/src/common/hooks/index.ts new file mode 100644 index 0000000..ae0fbbe --- /dev/null +++ b/client/src/common/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './use-app-dispatch' +export * from './use-app-selector' \ No newline at end of file diff --git a/client/src/common/hooks/use-app-dispatch.ts b/client/src/common/hooks/use-app-dispatch.ts new file mode 100644 index 0000000..08522b1 --- /dev/null +++ b/client/src/common/hooks/use-app-dispatch.ts @@ -0,0 +1,4 @@ +import { useDispatch } from 'react-redux' +import type { AppDispatch } from '../../store' + +export const useAppDispatch = useDispatch.withTypes() \ No newline at end of file diff --git a/client/src/common/hooks/use-app-selector.ts b/client/src/common/hooks/use-app-selector.ts new file mode 100644 index 0000000..cdb5cae --- /dev/null +++ b/client/src/common/hooks/use-app-selector.ts @@ -0,0 +1,4 @@ +import { useSelector } from 'react-redux' +import type { RootState } from '../../store' + +export const useAppSelector = useSelector.withTypes() \ No newline at end of file diff --git a/client/src/features/certificates/duck/actions.ts b/client/src/features/certificates/duck/actions.ts new file mode 100644 index 0000000..1e278d3 --- /dev/null +++ b/client/src/features/certificates/duck/actions.ts @@ -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)); + } + }, +); diff --git a/client/src/features/certificates/duck/api.ts b/client/src/features/certificates/duck/api.ts new file mode 100644 index 0000000..cd53463 --- /dev/null +++ b/client/src/features/certificates/duck/api.ts @@ -0,0 +1,6 @@ +import type { AxiosResponse } from 'axios'; +import { api } from '../../../api/api'; +import type { CertificateDTO } from './dto'; + +export const getCertificates = () => + api.get>('/certificates'); diff --git a/client/src/features/certificates/duck/dto.ts b/client/src/features/certificates/duck/dto.ts new file mode 100644 index 0000000..50430d6 --- /dev/null +++ b/client/src/features/certificates/duck/dto.ts @@ -0,0 +1 @@ +export interface CertificateDTO {} \ No newline at end of file diff --git a/client/src/features/certificates/duck/index.ts b/client/src/features/certificates/duck/index.ts new file mode 100644 index 0000000..c3e3d04 --- /dev/null +++ b/client/src/features/certificates/duck/index.ts @@ -0,0 +1,4 @@ +export * from './actions' +export * from './dto' +export * from './slice' +export * from './selectors' \ No newline at end of file diff --git a/client/src/features/certificates/duck/selectors.ts b/client/src/features/certificates/duck/selectors.ts new file mode 100644 index 0000000..4ceb166 --- /dev/null +++ b/client/src/features/certificates/duck/selectors.ts @@ -0,0 +1,3 @@ +import type { RootState } from '../../../store'; + +export const getCertificatesState = (state: RootState) => state.certificates; diff --git a/client/src/features/certificates/duck/slice.ts b/client/src/features/certificates/duck/slice.ts new file mode 100644 index 0000000..df47f0d --- /dev/null +++ b/client/src/features/certificates/duck/slice.ts @@ -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) => { + state.loading = action.payload + }, + setUsers: (state, action: PayloadAction) => { + state.certificates = action.payload + }, + setError: (state, action: PayloadAction) => { + state.error = action.payload + } + }, +}); + diff --git a/client/src/features/certificates/index.ts b/client/src/features/certificates/index.ts new file mode 100644 index 0000000..edc3f9b --- /dev/null +++ b/client/src/features/certificates/index.ts @@ -0,0 +1 @@ +export * from './duck' \ No newline at end of file diff --git a/client/src/features/index.ts b/client/src/features/index.ts new file mode 100644 index 0000000..5d6921b --- /dev/null +++ b/client/src/features/index.ts @@ -0,0 +1,4 @@ +export * from './servers' +export * from './templates' +export * from './users' +export * from './certificates' \ No newline at end of file diff --git a/client/src/features/servers/duck/actions.ts b/client/src/features/servers/duck/actions.ts new file mode 100644 index 0000000..84ceaf6 --- /dev/null +++ b/client/src/features/servers/duck/actions.ts @@ -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)); + } + }, +); diff --git a/client/src/features/servers/duck/api.ts b/client/src/features/servers/duck/api.ts new file mode 100644 index 0000000..21eb4b1 --- /dev/null +++ b/client/src/features/servers/duck/api.ts @@ -0,0 +1,5 @@ +import type { AxiosResponse } from 'axios'; +import { api } from '../../../api/api'; +import type { ServerDTO } from './dto'; + +export const getServers = () => api.get>('/servers'); diff --git a/client/src/features/servers/duck/dto.ts b/client/src/features/servers/duck/dto.ts new file mode 100644 index 0000000..5045ad6 --- /dev/null +++ b/client/src/features/servers/duck/dto.ts @@ -0,0 +1,7 @@ +export interface ServerDTO { + id: number | string + name: string + hostname: string + grpc_port: number + status: string +} \ No newline at end of file diff --git a/client/src/features/servers/duck/index.ts b/client/src/features/servers/duck/index.ts new file mode 100644 index 0000000..c3e3d04 --- /dev/null +++ b/client/src/features/servers/duck/index.ts @@ -0,0 +1,4 @@ +export * from './actions' +export * from './dto' +export * from './slice' +export * from './selectors' \ No newline at end of file diff --git a/client/src/features/servers/duck/selectors.ts b/client/src/features/servers/duck/selectors.ts new file mode 100644 index 0000000..e90df68 --- /dev/null +++ b/client/src/features/servers/duck/selectors.ts @@ -0,0 +1,3 @@ +import type { RootState } from '../../../store'; + +export const getServersState = (state: RootState) => state.servers; diff --git a/client/src/features/servers/duck/slice.ts b/client/src/features/servers/duck/slice.ts new file mode 100644 index 0000000..2fa10ef --- /dev/null +++ b/client/src/features/servers/duck/slice.ts @@ -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) => { + state.loading = action.payload + }, + setServers: (state, action: PayloadAction) => { + state.servers = action.payload + }, + setError: (state, action: PayloadAction) => { + state.error = action.payload + } + }, +}); + diff --git a/client/src/features/servers/index.ts b/client/src/features/servers/index.ts new file mode 100644 index 0000000..edc3f9b --- /dev/null +++ b/client/src/features/servers/index.ts @@ -0,0 +1 @@ +export * from './duck' \ No newline at end of file diff --git a/client/src/features/templates/duck/actions.ts b/client/src/features/templates/duck/actions.ts new file mode 100644 index 0000000..77c8799 --- /dev/null +++ b/client/src/features/templates/duck/actions.ts @@ -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)); + } + }, +); diff --git a/client/src/features/templates/duck/api.ts b/client/src/features/templates/duck/api.ts new file mode 100644 index 0000000..f30a79e --- /dev/null +++ b/client/src/features/templates/duck/api.ts @@ -0,0 +1,5 @@ +import type { AxiosResponse } from 'axios'; +import { api } from '../../../api/api'; +import type { TemplateDTO } from './dto'; + +export const getTemplates = () => api.get>('/templates'); \ No newline at end of file diff --git a/client/src/features/templates/duck/dto.ts b/client/src/features/templates/duck/dto.ts new file mode 100644 index 0000000..ecb4dc2 --- /dev/null +++ b/client/src/features/templates/duck/dto.ts @@ -0,0 +1 @@ +export interface TemplateDTO {} \ No newline at end of file diff --git a/client/src/features/templates/duck/index.ts b/client/src/features/templates/duck/index.ts new file mode 100644 index 0000000..c3e3d04 --- /dev/null +++ b/client/src/features/templates/duck/index.ts @@ -0,0 +1,4 @@ +export * from './actions' +export * from './dto' +export * from './slice' +export * from './selectors' \ No newline at end of file diff --git a/client/src/features/templates/duck/selectors.ts b/client/src/features/templates/duck/selectors.ts new file mode 100644 index 0000000..5905a3f --- /dev/null +++ b/client/src/features/templates/duck/selectors.ts @@ -0,0 +1,3 @@ +import type { RootState } from '../../../store'; + +export const getTemplatesState = (state: RootState) => state.templates; diff --git a/client/src/features/templates/duck/slice.ts b/client/src/features/templates/duck/slice.ts new file mode 100644 index 0000000..54c5ece --- /dev/null +++ b/client/src/features/templates/duck/slice.ts @@ -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) => { + state.loading = action.payload + }, + setTemplates: (state, action: PayloadAction) => { + state.templates = action.payload + }, + setError: (state, action: PayloadAction) => { + state.error = action.payload + } + }, +}); + diff --git a/client/src/features/templates/index.ts b/client/src/features/templates/index.ts new file mode 100644 index 0000000..edc3f9b --- /dev/null +++ b/client/src/features/templates/index.ts @@ -0,0 +1 @@ +export * from './duck' \ No newline at end of file diff --git a/client/src/features/users/duck/actions.ts b/client/src/features/users/duck/actions.ts new file mode 100644 index 0000000..24bf007 --- /dev/null +++ b/client/src/features/users/duck/actions.ts @@ -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)); + } + }, +); diff --git a/client/src/features/users/duck/api.ts b/client/src/features/users/duck/api.ts new file mode 100644 index 0000000..9478e7f --- /dev/null +++ b/client/src/features/users/duck/api.ts @@ -0,0 +1,5 @@ +import type { AxiosResponse } from 'axios'; +import { api } from '../../../api/api'; +import type { UserDTO } from './dto'; + +export const getUsers = () => api.get>('/users'); diff --git a/client/src/features/users/duck/dto.ts b/client/src/features/users/duck/dto.ts new file mode 100644 index 0000000..d5e18db --- /dev/null +++ b/client/src/features/users/duck/dto.ts @@ -0,0 +1,8 @@ +export interface User {} + +export interface UserDTO { + page: number + per_page: number + total: number + users: User[] +} diff --git a/client/src/features/users/duck/index.ts b/client/src/features/users/duck/index.ts new file mode 100644 index 0000000..c3e3d04 --- /dev/null +++ b/client/src/features/users/duck/index.ts @@ -0,0 +1,4 @@ +export * from './actions' +export * from './dto' +export * from './slice' +export * from './selectors' \ No newline at end of file diff --git a/client/src/features/users/duck/selectors.ts b/client/src/features/users/duck/selectors.ts new file mode 100644 index 0000000..49d9c28 --- /dev/null +++ b/client/src/features/users/duck/selectors.ts @@ -0,0 +1,3 @@ +import type { RootState } from '../../../store'; + +export const getUsersState = (state: RootState) => state.users; diff --git a/client/src/features/users/duck/slice.ts b/client/src/features/users/duck/slice.ts new file mode 100644 index 0000000..b3cbdbe --- /dev/null +++ b/client/src/features/users/duck/slice.ts @@ -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) => { + state.loading = action.payload + }, + setUsers: (state, action: PayloadAction) => { + state.users = action.payload.users + }, + setError: (state, action: PayloadAction) => { + state.error = action.payload + } + }, +}); + diff --git a/client/src/features/users/index.ts b/client/src/features/users/index.ts new file mode 100644 index 0000000..edc3f9b --- /dev/null +++ b/client/src/features/users/index.ts @@ -0,0 +1 @@ +export * from './duck' \ No newline at end of file diff --git a/client/src/main.tsx b/client/src/main.tsx index 4904132..01b5386 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -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( - + + + , ); diff --git a/client/src/pages/dashboard/dashboard.tsx b/client/src/pages/dashboard/dashboard.tsx index 7358ea0..5585cbf 100644 --- a/client/src/pages/dashboard/dashboard.tsx +++ b/client/src/pages/dashboard/dashboard.tsx @@ -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(undefined); - const [templates, setTemplates] = useState(undefined); - const [certificates, setCertificates] = useState( - undefined, - ); - const [users, setUsers] = useState(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 (
@@ -58,24 +35,30 @@ export const Dashboard = () => {

Servers:{' '} - {servers ? servers.length || 0 : 'Loading...'} + {serverLoading === true && 'Loading...'} + {servers && String(servers.length)}

Templates:{' '} - {templates ? templates.length || 0 : 'Loading...'} + {templatesLoading && 'Loading...'} + {templates && String(templates.length)}

Certificates:{' '} - {certificates ? certificates.length || 0 : 'Loading...'} + {certificatesLoading && 'Loading...'} + {certificates && String(certificates.length)}

Users:{' '} - {users ? users.length || 0 : 'Loading...'} + + {usersLoading && 'Loading...'} + {users && String(users.length)} +

diff --git a/client/src/store/index.ts b/client/src/store/index.ts new file mode 100644 index 0000000..514bb54 --- /dev/null +++ b/client/src/store/index.ts @@ -0,0 +1 @@ +export * from './store' \ No newline at end of file diff --git a/client/src/store/store.ts b/client/src/store/store.ts new file mode 100644 index 0000000..6b0a6c3 --- /dev/null +++ b/client/src/store/store.ts @@ -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 +// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} +export type AppDispatch = typeof store.dispatch \ No newline at end of file