Skip to content

Commit

Permalink
refactor(ui): move namespaces to use redux rtk
Browse files Browse the repository at this point in the history
  • Loading branch information
erka committed Nov 30, 2023
1 parent 10bfcbc commit 8941d24
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 125 deletions.
7 changes: 4 additions & 3 deletions ui/src/app/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -47,8 +47,9 @@ function InnerLayout() {
}
}, [namespaceKey, currentNamespace, dispatch, navigate, location.pathname]);

useListNamespacesQuery();

useEffect(() => {
dispatch(fetchNamespacesAsync());
dispatch(fetchInfoAsync());
dispatch(fetchConfigAsync());
}, [dispatch]);
Expand Down
21 changes: 11 additions & 10 deletions ui/src/app/namespaces/Namespaces.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<boolean>(false);
Expand All @@ -25,12 +27,13 @@ export default function Namespaces() {
const [deletingNamespace, setDeletingNamespace] = useState<INamespace | null>(
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 (
Expand Down Expand Up @@ -70,9 +73,7 @@ export default function Namespaces() {
panelType="Namespace"
setOpen={setShowDeleteNamespaceModal}
handleDelete={() =>
dispatch(
deleteNamespaceAsync(deletingNamespace?.key ?? '')
).unwrap()
deleteNamespace(deletingNamespace?.key ?? '').unwrap()
}
/>
</Modal>
Expand Down
145 changes: 65 additions & 80 deletions ui/src/app/namespaces/namespacesSlice.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -37,38 +28,15 @@ 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;
state.status = LoadingStatus.SUCCEEDED;
}
}
});

Expand All @@ -90,41 +58,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: 'namespaces-api',
baseQuery,
tagTypes: ['Namespace'],
endpoints: (builder) => ({
// get list of namespaces
listNamespaces: builder.query<INamespaceList, void>({
query: () => '/namespaces',
providesTags: () => [{ type: 'Namespace' }]
}),
// create the namespace
createNamespace: builder.mutation<INamespace, INamespaceBase>({
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<void, string>({
query(namespaceKey) {
return {
url: `/namespaces/${namespaceKey}`,
method: 'DELETE'
};
},
invalidatesTags: () => [{ type: 'Namespace' }]
})
})
});

export const {
useListNamespacesQuery,
useCreateNamespaceMutation,
useDeleteNamespaceMutation,
useUpdateNamespaceMutation
} = namespaceApi;
export default namespacesSlice.reducer;
15 changes: 6 additions & 9 deletions ui/src/components/namespaces/NamespaceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 }).unwrap();
};

return (
Expand Down
22 changes: 0 additions & 22 deletions ui/src/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
20 changes: 19 additions & 1 deletion ui/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import {
isAnyOf
} 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
Expand Down Expand Up @@ -45,6 +48,19 @@ listenerMiddleware.startListening({
);
}
});
/*
* It could be anti-pattern but it feels like the right thing to do.
* The namespacesSlice holds the namespaces globally and doesn't refetch them
* from server as often as it could be with `useListNamespacesQuery`.
* Each time the the app is loaded or namespaces changed by user this
* listener will propagate the latest namespaces to the namespacesSlice.
*/
listenerMiddleware.startListening({
matcher: namespaceApi.endpoints.listNamespaces.matchFulfilled,
effect: (action, api) => {
api.dispatch(namespacesSlice.actions.namespacesChanged(action.payload));
}
});

const preferencesState = JSON.parse(
localStorage.getItem(preferencesKey) || '{}'
Expand Down Expand Up @@ -72,6 +88,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,
Expand All @@ -81,6 +98,7 @@ export const store = configureStore({
getDefaultMiddleware()
.prepend(listenerMiddleware.middleware)
.concat(
namespaceApi.middleware,
segmentsApi.middleware,
rulesApi.middleware,
rolloutsApi.middleware,
Expand Down

0 comments on commit 8941d24

Please sign in to comment.