From 7d7d84a91ef0370537b4c0b433cf26f9475f0ddf Mon Sep 17 00:00:00 2001 From: Roman Dmytrenko Date: Wed, 29 Nov 2023 20:41:48 +0200 Subject: [PATCH] refactor(ui): move namespaces to use redux rtk fixes: #2447 --- ui/src/app/Layout.tsx | 7 +- ui/src/app/namespaces/Namespaces.tsx | 21 +-- ui/src/app/namespaces/namespacesSlice.ts | 144 ++++++++---------- .../components/namespaces/NamespaceForm.tsx | 15 +- ui/src/data/api.ts | 22 --- ui/src/store.ts | 19 ++- 6 files changed, 102 insertions(+), 126 deletions(-) diff --git a/ui/src/app/Layout.tsx b/ui/src/app/Layout.tsx index 9034678f36..ba7d246606 100644 --- a/ui/src/app/Layout.tsx +++ b/ui/src/app/Layout.tsx @@ -18,8 +18,8 @@ import { useAppDispatch } from '~/data/hooks/store'; import { fetchConfigAsync, fetchInfoAsync } from './meta/metaSlice'; import { currentNamespaceChanged, - fetchNamespacesAsync, - selectCurrentNamespace + selectCurrentNamespace, + useListNamespacesQuery } from './namespaces/namespacesSlice'; function InnerLayout() { @@ -47,8 +47,9 @@ function InnerLayout() { } }, [namespaceKey, currentNamespace, dispatch, navigate, location.pathname]); + useListNamespacesQuery(); + useEffect(() => { - dispatch(fetchNamespacesAsync()); dispatch(fetchInfoAsync()); dispatch(fetchConfigAsync()); }, [dispatch]); diff --git a/ui/src/app/namespaces/Namespaces.tsx b/ui/src/app/namespaces/Namespaces.tsx index 66065a4ce7..bb8de4a232 100644 --- a/ui/src/app/namespaces/Namespaces.tsx +++ b/ui/src/app/namespaces/Namespaces.tsx @@ -1,5 +1,5 @@ import { PlusIcon } from '@heroicons/react/24/outline'; -import { useRef, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { selectReadonly } from '~/app/meta/metaSlice'; import EmptyState from '~/components/EmptyState'; @@ -9,9 +9,11 @@ import NamespaceForm from '~/components/namespaces/NamespaceForm'; import NamespaceTable from '~/components/namespaces/NamespaceTable'; import DeletePanel from '~/components/panels/DeletePanel'; import Slideover from '~/components/Slideover'; -import { useAppDispatch } from '~/data/hooks/store'; import { INamespace } from '~/types/Namespace'; -import { deleteNamespaceAsync, selectNamespaces } from './namespacesSlice'; +import { + useDeleteNamespaceMutation, + useListNamespacesQuery +} from './namespacesSlice'; export default function Namespaces() { const [showNamespaceForm, setShowNamespaceForm] = useState(false); @@ -25,12 +27,13 @@ export default function Namespaces() { const [deletingNamespace, setDeletingNamespace] = useState( null ); + const listNamespaces = useListNamespacesQuery(); - const dispatch = useAppDispatch(); - - const namespaces = useSelector(selectNamespaces); + const namespaces = useMemo(() => { + return listNamespaces.data?.namespaces || []; + }, [listNamespaces]); const readOnly = useSelector(selectReadonly); - + const [deleteNamespace] = useDeleteNamespaceMutation(); const namespaceFormRef = useRef(null); return ( @@ -70,9 +73,7 @@ export default function Namespaces() { panelType="Namespace" setOpen={setShowDeleteNamespaceModal} handleDelete={() => - dispatch( - deleteNamespaceAsync(deletingNamespace?.key ?? '') - ).unwrap() + deleteNamespace(deletingNamespace?.key ?? '').unwrap() } /> diff --git a/ui/src/app/namespaces/namespacesSlice.ts b/ui/src/app/namespaces/namespacesSlice.ts index 03ab4e7bf4..9a57dc216b 100644 --- a/ui/src/app/namespaces/namespacesSlice.ts +++ b/ui/src/app/namespaces/namespacesSlice.ts @@ -1,19 +1,10 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ -import { - createAsyncThunk, - createSelector, - createSlice -} from '@reduxjs/toolkit'; -import { - createNamespace, - deleteNamespace, - listNamespaces, - updateNamespace -} from '~/data/api'; +import { createSelector, createSlice } from '@reduxjs/toolkit'; +import { createApi } from '@reduxjs/toolkit/query/react'; import { RootState } from '~/store'; import { LoadingStatus } from '~/types/Meta'; -import { INamespace, INamespaceBase } from '~/types/Namespace'; - +import { INamespace, INamespaceBase, INamespaceList } from '~/types/Namespace'; +import { baseQuery } from '~/utils/redux-rtk'; const namespaceKey = 'namespace'; interface INamespacesState { @@ -37,38 +28,14 @@ export const namespacesSlice = createSlice({ currentNamespaceChanged: (state, action) => { const namespace = action.payload; state.currentNamespace = namespace.key; - } - }, - extraReducers(builder) { - builder - .addCase(fetchNamespacesAsync.pending, (state, _action) => { - state.status = LoadingStatus.LOADING; - }) - .addCase(fetchNamespacesAsync.fulfilled, (state, action) => { - state.status = LoadingStatus.SUCCEEDED; - state.namespaces = action.payload; - }) - .addCase(fetchNamespacesAsync.rejected, (state, action) => { - state.status = LoadingStatus.FAILED; - state.error = action.error.message; - }) - .addCase(createNamespaceAsync.fulfilled, (state, action) => { - const namespace = action.payload; - state.namespaces[namespace.key] = namespace; - }) - .addCase(updateNamespaceAsync.fulfilled, (state, action) => { - const namespace = action.payload; - state.namespaces[namespace.key] = namespace; - }) - .addCase(deleteNamespaceAsync.fulfilled, (state, action) => { - const key = action.payload; - delete state.namespaces[key]; - // if the current namespace is the one being deleted, set the current namespace to the first one - if (state.currentNamespace === key) { - state.currentNamespace = - state.namespaces[Object.keys(state.namespaces)[0]].key; - } + }, + namespacesChanged: (state, action) => { + const namespaces: { [key: string]: INamespace } = {}; + action.payload.namespaces.forEach((namespace: INamespace) => { + namespaces[namespace.key] = namespace; }); + state.namespaces = namespaces; + } } }); @@ -90,41 +57,58 @@ export const selectCurrentNamespace = createSelector( ({ key: 'default', name: 'Default', description: '' } as INamespace) ); -export const fetchNamespacesAsync = createAsyncThunk( - 'namespaces/fetchNamespaces', - async () => { - const response = await listNamespaces(); - const namespaces: { [key: string]: INamespace } = {}; - response.namespaces.forEach((namespace: INamespace) => { - namespaces[namespace.key] = namespace; - }); - return namespaces; - } -); - -export const createNamespaceAsync = createAsyncThunk( - 'namespaces/createNamespace', - async (namespace: INamespaceBase) => { - const response = await createNamespace(namespace); - return response; - } -); - -export const updateNamespaceAsync = createAsyncThunk( - 'namespaces/updateNamespace', - async (payload: { key: string; namespace: INamespaceBase }) => { - const { key, namespace } = payload; - const response = await updateNamespace(key, namespace); - return response; - } -); - -export const deleteNamespaceAsync = createAsyncThunk( - 'namespaces/deleteNamespace', - async (key: string) => { - await deleteNamespace(key); - return key; - } -); +export const namespaceApi = createApi({ + reducerPath: 'namespacesapi', + baseQuery, + tagTypes: ['Namespace'], + endpoints: (builder) => ({ + // get list of namespaces + listNamespaces: builder.query({ + query: () => '/namespaces', + providesTags: () => [{ type: 'Namespace' }] + }), + // create the namespace + createNamespace: builder.mutation({ + query(values) { + return { + url: '/namespaces', + method: 'POST', + body: values + }; + }, + invalidatesTags: () => [{ type: 'Namespace' }] + }), + // update the namespace + updateNamespace: builder.mutation< + INamespace, + { key: string; values: INamespaceBase } + >({ + query({ key, values }) { + return { + url: `/namespaces/${key}`, + method: 'PUT', + body: values + }; + }, + invalidatesTags: () => [{ type: 'Namespace' }] + }), + // delete the namespace + deleteNamespace: builder.mutation({ + query(namespaceKey) { + return { + url: `/namespaces/${namespaceKey}`, + method: 'DELETE' + }; + }, + invalidatesTags: () => [{ type: 'Namespace' }] + }) + }) +}); +export const { + useListNamespacesQuery, + useCreateNamespaceMutation, + useDeleteNamespaceMutation, + useUpdateNamespaceMutation +} = namespaceApi; export default namespacesSlice.reducer; diff --git a/ui/src/components/namespaces/NamespaceForm.tsx b/ui/src/components/namespaces/NamespaceForm.tsx index 4c441e94df..d0cde28bdd 100644 --- a/ui/src/components/namespaces/NamespaceForm.tsx +++ b/ui/src/components/namespaces/NamespaceForm.tsx @@ -4,15 +4,14 @@ import { Form, Formik } from 'formik'; import { forwardRef } from 'react'; import * as Yup from 'yup'; import { - createNamespaceAsync, - updateNamespaceAsync + useCreateNamespaceMutation, + useUpdateNamespaceMutation } from '~/app/namespaces/namespacesSlice'; import Button from '~/components/forms/buttons/Button'; import Input from '~/components/forms/Input'; import Loading from '~/components/Loading'; import MoreInfo from '~/components/MoreInfo'; import { useError } from '~/data/hooks/error'; -import { useAppDispatch } from '~/data/hooks/store'; import { useSuccess } from '~/data/hooks/success'; import { keyValidation, requiredValidation } from '~/data/validations'; import { INamespace, INamespaceBase } from '~/types/Namespace'; @@ -34,16 +33,14 @@ const NamespaceForm = forwardRef((props: NamespaceFormProps, ref: any) => { const { setError, clearError } = useError(); const { setSuccess } = useSuccess(); - const dispatch = useAppDispatch(); + const [createNamespace] = useCreateNamespaceMutation(); + const [updateNamespace] = useUpdateNamespaceMutation(); const handleSubmit = async (values: INamespaceBase) => { if (isNew) { - return dispatch(createNamespaceAsync(values)).unwrap(); + return createNamespace(values).unwrap(); } - - return dispatch( - updateNamespaceAsync({ key: namespace.key, namespace: values }) - ).unwrap(); + return updateNamespace({ key: namespace.key, values }); }; return ( diff --git a/ui/src/data/api.ts b/ui/src/data/api.ts index c17c32ec8d..f44c50ad05 100644 --- a/ui/src/data/api.ts +++ b/ui/src/data/api.ts @@ -103,28 +103,6 @@ export async function expireAuthSelf() { return put('/self/expire', {}, authURL); } -// -// namespaces -export async function listNamespaces() { - return get('/namespaces'); -} - -export async function getNamespace(key: string) { - return get(`/namespaces/${key}`); -} - -export async function createNamespace(values: any) { - return post('/namespaces', values); -} - -export async function updateNamespace(key: string, values: any) { - return put(`/namespaces/${key}`, values); -} - -export async function deleteNamespace(key: string) { - return del(`/namespaces/${key}`); -} - // // flags export async function listFlags(namespaceKey: string) { diff --git a/ui/src/store.ts b/ui/src/store.ts index 67766e5a1d..bfe587ea5f 100644 --- a/ui/src/store.ts +++ b/ui/src/store.ts @@ -1,14 +1,18 @@ import { configureStore, createListenerMiddleware, - isAnyOf + isAnyOf, + isFulfilled } from '@reduxjs/toolkit'; +import { + namespaceApi, + namespacesSlice +} from '~/app/namespaces/namespacesSlice'; import { flagsSlice } from './app/flags/flagsSlice'; import { rolloutsApi } from './app/flags/rolloutsApi'; import { rulesApi } from './app/flags/rulesApi'; import { metaSlice } from './app/meta/metaSlice'; -import { namespacesSlice } from './app/namespaces/namespacesSlice'; import { preferencesKey, preferencesSlice @@ -46,6 +50,15 @@ listenerMiddleware.startListening({ } }); +listenerMiddleware.startListening({ + matcher: isFulfilled, + effect: (action, api) => { + if (namespaceApi.endpoints.listNamespaces.matchFulfilled(action)) { + api.dispatch(namespacesSlice.actions.namespacesChanged(action.payload)); + } + } +}); + const preferencesState = JSON.parse( localStorage.getItem(preferencesKey) || '{}' ); @@ -72,6 +85,7 @@ export const store = configureStore({ preferences: preferencesSlice.reducer, flags: flagsSlice.reducer, meta: metaSlice.reducer, + [namespaceApi.reducerPath]: namespaceApi.reducer, [segmentsApi.reducerPath]: segmentsApi.reducer, [rulesApi.reducerPath]: rulesApi.reducer, [rolloutsApi.reducerPath]: rolloutsApi.reducer, @@ -81,6 +95,7 @@ export const store = configureStore({ getDefaultMiddleware() .prepend(listenerMiddleware.middleware) .concat( + namespaceApi.middleware, segmentsApi.middleware, rulesApi.middleware, rolloutsApi.middleware,