Skip to content

Commit

Permalink
refactor(ui): move Rollouts to use Redux storage (#2465)
Browse files Browse the repository at this point in the history
* refactor(ui): move Rollouts to use Redux storage

Refs: #2434

* fix the issue reported by eslint

* it should be finally fixed

---------

Co-authored-by: Mark Phelps <[email protected]>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 29, 2023
1 parent f6f41a3 commit 035de76
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 156 deletions.
80 changes: 41 additions & 39 deletions ui/src/app/flags/rollouts/Rollouts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ import {
verticalListSortingStrategy
} from '@dnd-kit/sortable';
import { PlusIcon, StarIcon } from '@heroicons/react/24/outline';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import {
useDeleteRolloutMutation,
useListRolloutsQuery,
useOrderRolloutsMutation
} from '~/app/flags/rolloutsApi';
import { selectReadonly } from '~/app/meta/metaSlice';
import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice';
import { useListSegmentsQuery } from '~/app/segments/segmentsApi';
Expand All @@ -27,11 +32,10 @@ import RolloutForm from '~/components/rollouts/forms/RolloutForm';
import Rollout from '~/components/rollouts/Rollout';
import SortableRollout from '~/components/rollouts/SortableRollout';
import Slideover from '~/components/Slideover';
import { deleteRollout, listRollouts, orderRollouts } from '~/data/api';
import { useError } from '~/data/hooks/error';
import { useSuccess } from '~/data/hooks/success';
import { IFlag } from '~/types/Flag';
import { IRollout, IRolloutList } from '~/types/Rollout';
import { IRollout } from '~/types/Rollout';
import { SegmentOperatorType } from '~/types/Segment';

type RolloutsProps = {
Expand All @@ -41,11 +45,8 @@ type RolloutsProps = {
export default function Rollouts(props: RolloutsProps) {
const { flag } = props;

const [rollouts, setRollouts] = useState<IRollout[]>([]);

const [activeRollout, setActiveRollout] = useState<IRollout | null>(null);

const [rolloutsVersion, setRolloutsVersion] = useState(0);
const [showRolloutForm, setShowRolloutForm] = useState<boolean>(false);

const [showEditRolloutForm, setShowEditRolloutForm] =
Expand All @@ -63,18 +64,27 @@ export default function Rollouts(props: RolloutsProps) {

const namespace = useSelector(selectCurrentNamespace);
const readOnly = useSelector(selectReadonly);
const segments = useListSegmentsQuery(namespace.key)?.data?.segments || [];
const segmentsList = useListSegmentsQuery(namespace.key);
const segments = useMemo(
() => segmentsList.data?.segments || [],
[segmentsList]
);

const loadData = useCallback(async () => {
// TODO: move to redux
const rolloutList = (await listRollouts(
namespace.key,
flag.key
)) as IRolloutList;
const [deleteRollout] = useDeleteRolloutMutation();

const rolloutsList = useListRolloutsQuery({
namespaceKey: namespace.key,
flagKey: flag.key
});
const rolloutsRules = useMemo(
() => rolloutsList.data?.rules || [],
[rolloutsList]
);

const rollouts = useMemo(() => {
// Combine both segmentKey and segmentKeys for legacy purposes.
// TODO(yquansah): Should be removed once there are no more references to `segmentKey`.
const rolloutRules = rolloutList.rules.map((rollout) => {
return rolloutsRules.map((rollout) => {
if (rollout.segment) {
let segmentKeys: string[] = [];
if (
Expand All @@ -101,13 +111,7 @@ export default function Rollouts(props: RolloutsProps) {
...rollout
};
});

setRollouts(rolloutRules);
}, [namespace.key, flag.key]);

const incrementRolloutsVersion = () => {
setRolloutsVersion(rolloutsVersion + 1);
};
}, [rolloutsRules]);

const sensors = useSensors(
useSensor(PointerSensor),
Expand All @@ -116,14 +120,15 @@ export default function Rollouts(props: RolloutsProps) {
})
);

const [orderRollouts] = useOrderRolloutsMutation();

const reorderRollouts = (rollouts: IRollout[]) => {
orderRollouts(
namespace.key,
flag.key,
rollouts.map((rollout) => rollout.id)
)
orderRollouts({
namespaceKey: namespace.key,
flagKey: flag.key,
rolloutIds: rollouts.map((rollout) => rollout.id)
})
.then(() => {
incrementRolloutsVersion();
clearError();
setSuccess('Successfully reordered rollouts');
})
Expand All @@ -150,7 +155,6 @@ export default function Rollouts(props: RolloutsProps) {
})(rollouts);

reorderRollouts(reordered);
setRollouts(reordered);
}

setActiveRollout(null);
Expand All @@ -166,10 +170,6 @@ export default function Rollouts(props: RolloutsProps) {
}
};

useEffect(() => {
loadData();
}, [loadData, rolloutsVersion]);

return (
<>
{/* rollout delete modal */}
Expand All @@ -187,11 +187,15 @@ export default function Rollouts(props: RolloutsProps) {
}
panelType="Rollout"
setOpen={setShowDeleteRolloutModal}
handleDelete={
() =>
deleteRollout(namespace.key, flag.key, deletingRollout?.id ?? '') // TODO: Determine impact of blank ID param
handleDelete={() =>
deleteRollout({
namespaceKey: namespace.key,
flagKey: flag.key,
rolloutId: deletingRollout?.id ?? ''
// TODO: Determine impact of blank ID param
}).unwrap()
}
onSuccess={incrementRolloutsVersion}
onSuccess={() => {}}
/>
</Modal>

Expand All @@ -207,7 +211,6 @@ export default function Rollouts(props: RolloutsProps) {
segments={segments}
setOpen={setShowRolloutForm}
onSuccess={() => {
incrementRolloutsVersion();
setShowRolloutForm(false);
}}
/>
Expand All @@ -227,7 +230,6 @@ export default function Rollouts(props: RolloutsProps) {
setOpen={setShowEditRolloutForm}
onSuccess={() => {
setShowEditRolloutForm(false);
incrementRolloutsVersion();
}}
/>
</Slideover>
Expand Down Expand Up @@ -297,7 +299,7 @@ export default function Rollouts(props: RolloutsProps) {
flag={flag}
rollout={rollout}
segments={segments}
onSuccess={incrementRolloutsVersion}
onSuccess={() => {}}
onEdit={() => {
setEditingRollout(rollout);
setShowEditRolloutForm(true);
Expand Down
141 changes: 141 additions & 0 deletions ui/src/app/flags/rolloutsApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { IRollout, IRolloutBase, IRolloutList } from '~/types/Rollout';

import { baseQuery } from '~/utils/redux-rtk';

export const rolloutsApi = createApi({
reducerPath: 'rollouts',
baseQuery,
tagTypes: ['Rollout'],
endpoints: (builder) => ({
// get list of rollouts
listRollouts: builder.query<
IRolloutList,
{ namespaceKey: string; flagKey: string }
>({
query: ({ namespaceKey, flagKey }) =>
`/namespaces/${namespaceKey}/flags/${flagKey}/rollouts`,
providesTags: (_result, _error, { namespaceKey, flagKey }) => [
{ type: 'Rollout', id: namespaceKey + '/' + flagKey }
]
}),
// delete the rollout
deleteRollout: builder.mutation<
void,
{ namespaceKey: string; flagKey: string; rolloutId: string }
>({
query({ namespaceKey, flagKey, rolloutId }) {
return {
url: `/namespaces/${namespaceKey}/flags/${flagKey}/rollouts/${rolloutId}`,
method: 'DELETE'
};
},
invalidatesTags: (_result, _error, { namespaceKey, flagKey }) => [
{ type: 'Rollout', id: namespaceKey + '/' + flagKey }
]
}),
// create the rollout
createRollout: builder.mutation<
void,
{
namespaceKey: string;
flagKey: string;
values: IRolloutBase;
}
>({
query({ namespaceKey, flagKey, values }) {
return {
url: `/namespaces/${namespaceKey}/flags/${flagKey}/rollouts`,
method: 'POST',
body: values
};
},
invalidatesTags: (_result, _error, { namespaceKey, flagKey }) => [
{ type: 'Rollout', id: namespaceKey + '/' + flagKey }
]
}),
// update the rollout
updateRollout: builder.mutation<
void,
{
namespaceKey: string;
flagKey: string;
rolloutId: string;
values: IRolloutBase;
}
>({
query({ namespaceKey, flagKey, rolloutId, values }) {
return {
url: `/namespaces/${namespaceKey}/flags/${flagKey}/rollouts/${rolloutId}`,
method: 'PUT',
body: values
};
},
invalidatesTags: (_result, _error, { namespaceKey, flagKey }) => [
{ type: 'Rollout', id: namespaceKey + '/' + flagKey }
]
}),
// reorder the rollouts
orderRollouts: builder.mutation<
IRollout,
{
namespaceKey: string;
flagKey: string;
rolloutIds: string[];
}
>({
query({ namespaceKey, flagKey, rolloutIds }) {
return {
url: `/namespaces/${namespaceKey}/flags/${flagKey}/rollouts/order`,
method: 'PUT',
body: { rolloutIds: rolloutIds }
};
},
async onQueryStarted(
{ namespaceKey, flagKey, rolloutIds },
{ dispatch, queryFulfilled }
) {
// this is manual optimistic cache update of the listRollouts
// to set a desire order of rules of the listRollouts while server is updating the state.
// If we don't do this we will have very strange UI state with rules in old order
// until the result will be get from the server. It's very visible on slow connections.
const patchResult = dispatch(
rolloutsApi.util.updateQueryData(
'listRollouts',
{ namespaceKey, flagKey },
(draft: IRolloutList) => {
const rules = draft.rules;
const resortedRules = rules.sort((a, b) => {
const ida = rolloutIds.indexOf(a.id);
const idb = rolloutIds.indexOf(b.id);
if (ida < idb) {
return -1;
} else if (ida > idb) {
return 1;
}
return 0;
});
return Object.assign(draft, { rules: resortedRules });
}
)
);
try {
await queryFulfilled;
} catch {
patchResult.undo();
}
},
invalidatesTags: (_result, _error, { namespaceKey, flagKey }) => [
{ type: 'Rollout', id: namespaceKey + '/' + flagKey }
]
})
})
});

export const {
useListRolloutsQuery,
useCreateRolloutMutation,
useUpdateRolloutMutation,
useDeleteRolloutMutation,
useOrderRolloutsMutation
} = rolloutsApi;
44 changes: 27 additions & 17 deletions ui/src/components/rollouts/forms/EditRolloutForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { XMarkIcon } from '@heroicons/react/24/outline';
import { FieldArray, Form, Formik } from 'formik';
import { useSelector } from 'react-redux';
import { twMerge } from 'tailwind-merge';
import { useUpdateRolloutMutation } from '~/app/flags/rolloutsApi';
import { selectReadonly } from '~/app/meta/metaSlice';
import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice';
import Button from '~/components/forms/buttons/Button';
Expand All @@ -11,7 +12,6 @@ import SegmentsPicker from '~/components/forms/SegmentsPicker';
import Select from '~/components/forms/Select';
import Loading from '~/components/Loading';
import MoreInfo from '~/components/MoreInfo';
import { updateRollout } from '~/data/api';
import { useError } from '~/data/hooks/error';
import { useSuccess } from '~/data/hooks/success';
import { IRollout, RolloutType } from '~/types/Rollout';
Expand Down Expand Up @@ -65,34 +65,44 @@ export default function EditRolloutForm(props: EditRolloutFormProps) {
: SegmentOperatorType.OR;

const readOnly = useSelector(selectReadonly);

const [updateRollout] = useUpdateRolloutMutation();
const handleSegmentSubmit = (values: RolloutFormValues) => {
let rolloutSegment = rollout;
rolloutSegment.threshold = undefined;

return updateRollout(namespace.key, flagKey, rollout.id, {
...rolloutSegment,
description: values.description,
segment: {
segmentKeys: values.segmentKeys?.map((s) => s.key),
segmentOperator: values.operator,
value: values.value === 'true'
return updateRollout({
namespaceKey: namespace.key,
flagKey,
rolloutId: rollout.id,
values: {
...rolloutSegment,
description: values.description,
segment: {
segmentKeys: values.segmentKeys?.map((s) => s.key),
segmentOperator: values.operator,
value: values.value === 'true'
}
}
});
}).unwrap();
};

const handleThresholdSubmit = (values: RolloutFormValues) => {
let rolloutThreshold = rollout;
rolloutThreshold.segment = undefined;

return updateRollout(namespace.key, flagKey, rollout.id, {
...rolloutThreshold,
description: values.description,
threshold: {
percentage: values.percentage || 0,
value: values.value === 'true'
return updateRollout({
namespaceKey: namespace.key,
flagKey,
rolloutId: rollout.id,
values: {
...rolloutThreshold,
description: values.description,
threshold: {
percentage: values.percentage || 0,
value: values.value === 'true'
}
}
});
}).unwrap();
};

const initialValue =
Expand Down
Loading

0 comments on commit 035de76

Please sign in to comment.