diff --git a/ui/src/app/flags/rollouts/Rollouts.tsx b/ui/src/app/flags/rollouts/Rollouts.tsx
index 222212058b..62b3087525 100644
--- a/ui/src/app/flags/rollouts/Rollouts.tsx
+++ b/ui/src/app/flags/rollouts/Rollouts.tsx
@@ -18,6 +18,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { selectReadonly } from '~/app/meta/metaSlice';
import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice';
+import { useListSegmentsQuery } from '~/app/segments/segmentsApi';
import Button from '~/components/forms/buttons/Button';
import Modal from '~/components/Modal';
import DeletePanel from '~/components/panels/DeletePanel';
@@ -26,17 +27,12 @@ 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,
- listSegments,
- orderRollouts
-} from '~/data/api';
+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 { ISegment, ISegmentList, SegmentOperatorType } from '~/types/Segment';
+import { SegmentOperatorType } from '~/types/Segment';
type RolloutsProps = {
flag: IFlag;
@@ -45,7 +41,6 @@ type RolloutsProps = {
export default function Rollouts(props: RolloutsProps) {
const { flag } = props;
- const [segments, setSegments] = useState([]);
const [rollouts, setRollouts] = useState([]);
const [activeRollout, setActiveRollout] = useState(null);
@@ -68,13 +63,10 @@ export default function Rollouts(props: RolloutsProps) {
const namespace = useSelector(selectCurrentNamespace);
const readOnly = useSelector(selectReadonly);
+ const segments = useListSegmentsQuery(namespace.key)?.data?.segments || [];
const loadData = useCallback(async () => {
// TODO: move to redux
- const segmentList = (await listSegments(namespace.key)) as ISegmentList;
- const { segments } = segmentList;
- setSegments(segments);
-
const rolloutList = (await listRollouts(
namespace.key,
flag.key
@@ -284,7 +276,7 @@ export default function Rollouts(props: RolloutsProps) {
{rollouts && rollouts.length > 0 && (
diff --git a/ui/src/app/flags/rules/Rules.tsx b/ui/src/app/flags/rules/Rules.tsx
index ba76fce268..11ebf10f8a 100644
--- a/ui/src/app/flags/rules/Rules.tsx
+++ b/ui/src/app/flags/rules/Rules.tsx
@@ -14,12 +14,13 @@ import {
verticalListSortingStrategy
} from '@dnd-kit/sortable';
import { PlusIcon } from '@heroicons/react/24/outline';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useNavigate, useOutletContext } from 'react-router-dom';
import { FlagProps } from '~/app/flags/FlagProps';
import { selectReadonly } from '~/app/meta/metaSlice';
import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice';
+import { useListSegmentsQuery } from '~/app/segments/segmentsApi';
import EmptyState from '~/components/EmptyState';
import Button from '~/components/forms/buttons/Button';
import Modal from '~/components/Modal';
@@ -28,20 +29,19 @@ import RuleForm from '~/components/rules/forms/RuleForm';
import Rule from '~/components/rules/Rule';
import SortableRule from '~/components/rules/SortableRule';
import Slideover from '~/components/Slideover';
-import { deleteRule, listRules, listSegments, orderRules } from '~/data/api';
+import { deleteRule, listRules, orderRules } from '~/data/api';
import { useError } from '~/data/hooks/error';
import { useSuccess } from '~/data/hooks/success';
import { IDistribution } from '~/types/Distribution';
import { IEvaluatable } from '~/types/Evaluatable';
import { FlagType } from '~/types/Flag';
import { IRule, IRuleList } from '~/types/Rule';
-import { ISegment, ISegmentList, SegmentOperatorType } from '~/types/Segment';
+import { ISegment, SegmentOperatorType } from '~/types/Segment';
import { IVariant } from '~/types/Variant';
export default function Rules() {
const { flag } = useOutletContext();
- const [segments, setSegments] = useState([]);
const [rules, setRules] = useState([]);
const [activeRule, setActiveRule] = useState(null);
@@ -60,13 +60,14 @@ export default function Rules() {
const namespace = useSelector(selectCurrentNamespace);
const readOnly = useSelector(selectReadonly);
+ const listSegments = useListSegmentsQuery(namespace.key);
+ const segments = useMemo(
+ () => listSegments.data?.segments || [],
+ [listSegments]
+ );
const loadData = useCallback(async () => {
// TODO: move to redux
- const segmentList = (await listSegments(namespace.key)) as ISegmentList;
- const { segments } = segmentList;
- setSegments(segments);
-
const ruleList = (await listRules(namespace.key, flag.key)) as IRuleList;
const rules = ruleList.rules.flatMap((rule: IRule) => {
@@ -133,7 +134,7 @@ export default function Rules() {
});
setRules(rules);
- }, [namespace.key, flag]);
+ }, [namespace.key, flag, segments]);
const incrementRulesVersion = () => {
setRulesVersion(rulesVersion + 1);
@@ -283,7 +284,7 @@ export default function Rules() {
{
@@ -75,9 +74,6 @@ export default function Segment() {
let { segmentKey } = useParams();
const { inTimezone } = useTimezone();
- const [segment, setSegment] = useState(null);
- const [segmentVersion, setSegmentVersion] = useState(0);
-
const [showConstraintForm, setShowConstraintForm] = useState(false);
const [editingConstraint, setEditingConstraint] =
useState(null);
@@ -99,26 +95,27 @@ export default function Segment() {
const namespace = useSelector(selectCurrentNamespace);
const readOnly = useSelector(selectReadonly);
- const incrementSegmentVersion = () => {
- setSegmentVersion(segmentVersion + 1);
- };
-
- useEffect(() => {
- if (!segmentKey) return;
-
- getSegment(namespace.key, segmentKey)
- .then((segment: ISegment) => {
- setSegment(segment);
- clearError();
- })
- .catch((err) => {
- setError(err);
- });
- }, [segmentVersion, namespace.key, segmentKey, clearError, setError]);
-
+ const {
+ data: segment,
+ error,
+ isLoading,
+ isError
+ } = useGetSegmentQuery({
+ namespaceKey: namespace.key,
+ segmentKey: segmentKey || ''
+ });
+ const [deleteSegment] = useDeleteSegmentMutation();
+ const [deleteSegmentConstraint] = useDeleteConstraintMutation();
+ const [copySegment] = useCopySegmentMutation();
const constraintFormRef = useRef(null);
- if (!segment) return ;
+ if (isError) {
+ setError(error);
+ }
+
+ if (isLoading || !segment) {
+ return ;
+ }
return (
<>
@@ -134,7 +131,7 @@ export default function Segment() {
constraint={editingConstraint || undefined}
setOpen={setShowConstraintForm}
onSuccess={() => {
- incrementSegmentVersion();
+ clearError();
setShowConstraintForm(false);
}}
/>
@@ -157,17 +154,15 @@ export default function Segment() {
}
panelType="Constraint"
setOpen={setShowDeleteConstraintModal}
- handleDelete={
- () =>
- deleteConstraint(
- namespace.key,
- segment.key,
- deletingConstraint?.id ?? ''
- ) // TODO: Determine impact of blank ID param
+ handleDelete={() =>
+ deleteSegmentConstraint({
+ namespaceKey: namespace.key,
+ segmentKey: segment.key,
+ constraintId: deletingConstraint?.id ?? ''
+ // TODO: Determine impact of blank ID param
+ }).unwrap()
}
- onSuccess={() => {
- incrementSegmentVersion();
- }}
+ onSuccess={() => {}}
/>
@@ -183,7 +178,12 @@ export default function Segment() {
}
panelType="Segment"
setOpen={setShowDeleteSegmentModal}
- handleDelete={() => deleteSegment(namespace.key, segment.key)}
+ handleDelete={() =>
+ deleteSegment({
+ namespaceKey: namespace.key,
+ segmentKey: segment.key
+ }).unwrap()
+ }
onSuccess={() => {
navigate(`/namespaces/${namespace.key}/segments`);
}}
@@ -203,10 +203,10 @@ export default function Segment() {
panelType="Segment"
setOpen={setShowCopySegmentModal}
handleCopy={(namespaceKey: string) =>
- copySegment(
- { namespaceKey: namespace.key, key: segment.key },
- { namespaceKey: namespaceKey, key: segment.key }
- )
+ copySegment({
+ from: { namespaceKey: namespace.key, segmentKey: segment.key },
+ to: { namespaceKey: namespaceKey, segmentKey: segment.key }
+ }).unwrap()
}
onSuccess={() => {
clearError();
@@ -279,10 +279,7 @@ export default function Segment() {
-
+
diff --git a/ui/src/app/segments/Segments.tsx b/ui/src/app/segments/Segments.tsx
index c9ffff2afc..09420a57f3 100644
--- a/ui/src/app/segments/Segments.tsx
+++ b/ui/src/app/segments/Segments.tsx
@@ -2,24 +2,21 @@ import { PlusIcon } from '@heroicons/react/24/outline';
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
-import useSWR from 'swr';
import { selectReadonly } from '~/app/meta/metaSlice';
import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice';
+import { useListSegmentsQuery } from '~/app/segments/segmentsApi';
import EmptyState from '~/components/EmptyState';
import Button from '~/components/forms/buttons/Button';
import SegmentTable from '~/components/segments/SegmentTable';
import { useError } from '~/data/hooks/error';
-import { ISegmentList } from '~/types/Segment';
export default function Segments() {
const namespace = useSelector(selectCurrentNamespace);
const path = `/namespaces/${namespace.key}/segments`;
- const { data, error } = useSWR(path);
-
- const segments = data?.segments;
-
+ const { data, error } = useListSegmentsQuery(namespace.key);
+ const segments = data?.segments || [];
const navigate = useNavigate();
const { setError, clearError } = useError();
diff --git a/ui/src/app/segments/segmentsApi.ts b/ui/src/app/segments/segmentsApi.ts
new file mode 100644
index 0000000000..b0d1d2bff6
--- /dev/null
+++ b/ui/src/app/segments/segmentsApi.ts
@@ -0,0 +1,200 @@
+import { createApi } from '@reduxjs/toolkit/query/react';
+import { IConstraintBase } from '~/types/Constraint';
+import { ISegment, ISegmentBase, ISegmentList } from '~/types/Segment';
+import { baseQuery } from '~/utils/redux-rtk';
+
+export const segmentsApi = createApi({
+ reducerPath: 'segments',
+ baseQuery,
+ tagTypes: ['Segment'],
+ endpoints: (builder) => ({
+ // get list of segments in this namespace
+ listSegments: builder.query({
+ query: (namespaceKey) => `/namespaces/${namespaceKey}/segments`,
+ providesTags: (result, _error, namespaceKey) =>
+ result
+ ? [
+ ...result.segments.map(({ key }) => ({
+ type: 'Segment' as const,
+ id: namespaceKey + '/' + key
+ })),
+ { type: 'Segment', id: namespaceKey }
+ ]
+ : [{ type: 'Segment', id: namespaceKey }]
+ }),
+ // get segment in this namespace
+ getSegment: builder.query<
+ ISegment,
+ { namespaceKey: string; segmentKey: string }
+ >({
+ query: ({ namespaceKey, segmentKey }) =>
+ `/namespaces/${namespaceKey}/segments/${segmentKey}`,
+ providesTags: (_result, _error, { namespaceKey, segmentKey }) => [
+ { type: 'Segment', id: namespaceKey + '/' + segmentKey }
+ ]
+ }),
+ // create a new segment in the namespace
+ createSegment: builder.mutation<
+ ISegment,
+ { namespaceKey: string; values: ISegmentBase }
+ >({
+ query({ namespaceKey, values }) {
+ return {
+ url: `/namespaces/${namespaceKey}/segments`,
+ method: 'POST',
+ body: values
+ };
+ },
+ invalidatesTags: (_result, _error, { namespaceKey, values }) => [
+ { type: 'Segment', id: namespaceKey },
+ { type: 'Segment', id: namespaceKey + '/' + values.key }
+ ]
+ }),
+ // delete the segment from the namespace
+ deleteSegment: builder.mutation<
+ void,
+ { namespaceKey: string; segmentKey: string }
+ >({
+ query({ namespaceKey, segmentKey }) {
+ return {
+ url: `/namespaces/${namespaceKey}/segments/${segmentKey}`,
+ method: 'DELETE'
+ };
+ },
+ invalidatesTags: (_result, _error, { namespaceKey }) => [
+ { type: 'Segment', id: namespaceKey }
+ ]
+ }),
+ // update the segment in the namespace
+ updateSegment: builder.mutation<
+ ISegment,
+ { namespaceKey: string; segmentKey: string; values: ISegmentBase }
+ >({
+ query({ namespaceKey, segmentKey, values }) {
+ return {
+ url: `/namespaces/${namespaceKey}/segments/${segmentKey}`,
+ method: 'PUT',
+ body: values
+ };
+ },
+ invalidatesTags: (_result, _error, { namespaceKey, segmentKey }) => [
+ { type: 'Segment', id: namespaceKey },
+ { type: 'Segment', id: namespaceKey + '/' + segmentKey }
+ ]
+ }),
+ // copy the segment from one namespace to another one
+ copySegment: builder.mutation<
+ void,
+ {
+ from: { namespaceKey: string; segmentKey: string };
+ to: { namespaceKey: string; segmentKey: string };
+ }
+ >({
+ queryFn: async ({ from, to }, _api, _extraOptions, baseQuery) => {
+ let resp = await baseQuery({
+ url: `/namespaces/${from.namespaceKey}/segments/${from.segmentKey}`,
+ method: 'get'
+ });
+ if (resp.error) {
+ return { error: resp.error };
+ }
+ let data = resp.data as ISegment;
+
+ if (to.segmentKey) {
+ data.key = to.segmentKey;
+ }
+ // first create the segment
+ resp = await baseQuery({
+ url: `/namespaces/${to.namespaceKey}/segments`,
+ method: 'POST',
+ body: data
+ });
+ if (resp.error) {
+ return { error: resp.error };
+ }
+ // then copy the constraints
+ const constraints = data.constraints || [];
+ for (let constraint of constraints) {
+ resp = await baseQuery({
+ url: `/namespaces/${to.namespaceKey}/segments/${to.segmentKey}/constraints`,
+ method: 'POST',
+ body: constraint
+ });
+ if (resp.error) {
+ return { error: resp.error };
+ }
+ }
+
+ return { data: undefined };
+ },
+ invalidatesTags: (_result, _error, { to }) => [
+ { type: 'Segment', id: to.namespaceKey }
+ ]
+ }),
+
+ // create the segment constraint in the namespace
+ createConstraint: builder.mutation<
+ void,
+ { namespaceKey: string; segmentKey: string; values: IConstraintBase }
+ >({
+ query({ namespaceKey, segmentKey, values }) {
+ return {
+ url: `/namespaces/${namespaceKey}/segments/${segmentKey}/constraints`,
+ method: 'POST',
+ body: values
+ };
+ },
+ invalidatesTags: (_result, _error, { namespaceKey, segmentKey }) => [
+ { type: 'Segment', id: namespaceKey + '/' + segmentKey }
+ ]
+ }),
+ // update the segment constraint in the namespace
+ updateConstraint: builder.mutation<
+ void,
+ {
+ namespaceKey: string;
+ segmentKey: string;
+ constraintId: string;
+ values: IConstraintBase;
+ }
+ >({
+ query({ namespaceKey, segmentKey, constraintId, values }) {
+ return {
+ url: `/namespaces/${namespaceKey}/segments/${segmentKey}/constraints/${constraintId}`,
+ method: 'PUT',
+ body: values
+ };
+ },
+ invalidatesTags: (_result, _error, { namespaceKey, segmentKey }) => [
+ { type: 'Segment', id: namespaceKey + '/' + segmentKey }
+ ]
+ }),
+ // delete the segment constraint in the namespace
+ deleteConstraint: builder.mutation<
+ void,
+ { namespaceKey: string; segmentKey: string; constraintId: string }
+ >({
+ query({ namespaceKey, segmentKey, constraintId }) {
+ return {
+ url: `/namespaces/${namespaceKey}/segments/${segmentKey}/constraints/${constraintId}`,
+ method: 'DELETE'
+ };
+ },
+ invalidatesTags: (_result, _error, { namespaceKey, segmentKey }) => [
+ { type: 'Segment', id: namespaceKey + '/' + segmentKey }
+ ]
+ })
+ })
+});
+
+export const {
+ useListSegmentsQuery,
+ useGetSegmentQuery,
+ useCreateSegmentMutation,
+ useDeleteSegmentMutation,
+ useUpdateSegmentMutation,
+ useCopySegmentMutation,
+ useCreateConstraintMutation,
+ useUpdateConstraintMutation,
+ useDeleteConstraintMutation
+} = segmentsApi;
diff --git a/ui/src/components/segments/ConstraintForm.tsx b/ui/src/components/segments/ConstraintForm.tsx
index 11bc73f275..59d6e04a0f 100644
--- a/ui/src/components/segments/ConstraintForm.tsx
+++ b/ui/src/components/segments/ConstraintForm.tsx
@@ -9,12 +9,15 @@ import { Link } from 'react-router-dom';
import * as Yup from 'yup';
import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice';
import { selectTimezone } from '~/app/preferences/preferencesSlice';
+import {
+ useCreateConstraintMutation,
+ useUpdateConstraintMutation
+} from '~/app/segments/segmentsApi';
import Button from '~/components/forms/buttons/Button';
import Input from '~/components/forms/Input';
import Select from '~/components/forms/Select';
import Loading from '~/components/Loading';
import MoreInfo from '~/components/MoreInfo';
-import { createConstraint, updateConstraint } from '~/data/api';
import { useError } from '~/data/hooks/error';
import { useSuccess } from '~/data/hooks/success';
import {
@@ -266,11 +269,23 @@ const ConstraintForm = forwardRef((props: ConstraintFormProps, ref: any) => {
description: constraint?.description || ''
};
+ const [createConstraint] = useCreateConstraintMutation();
+ const [updateConstraint] = useUpdateConstraintMutation();
+
const handleSubmit = (values: IConstraintBase) => {
if (isNew) {
- return createConstraint(namespace.key, segmentKey, values);
+ return createConstraint({
+ namespaceKey: namespace.key,
+ segmentKey,
+ values
+ }).unwrap();
}
- return updateConstraint(namespace.key, segmentKey, constraint?.id, values);
+ return updateConstraint({
+ namespaceKey: namespace.key,
+ segmentKey,
+ constraintId: constraint?.id,
+ values
+ }).unwrap();
};
const getValuePlaceholder = (type: ConstraintType, operator: string) => {
diff --git a/ui/src/components/segments/SegmentForm.tsx b/ui/src/components/segments/SegmentForm.tsx
index 6eefe5f8cf..ce7d21bcd9 100644
--- a/ui/src/components/segments/SegmentForm.tsx
+++ b/ui/src/components/segments/SegmentForm.tsx
@@ -6,10 +6,13 @@ import { useNavigate } from 'react-router-dom';
import * as Yup from 'yup';
import { selectReadonly } from '~/app/meta/metaSlice';
import { selectCurrentNamespace } from '~/app/namespaces/namespacesSlice';
+import {
+ useCreateSegmentMutation,
+ useUpdateSegmentMutation
+} from '~/app/segments/segmentsApi';
import Button from '~/components/forms/buttons/Button';
import Input from '~/components/forms/Input';
import Loading from '~/components/Loading';
-import { createSegment, updateSegment } from '~/data/api';
import { useError } from '~/data/hooks/error';
import { useSuccess } from '~/data/hooks/success';
import { keyValidation, requiredValidation } from '~/data/validations';
@@ -47,11 +50,18 @@ export default function SegmentForm(props: SegmentFormProps) {
const namespace = useSelector(selectCurrentNamespace);
const readOnly = useSelector(selectReadonly);
+ const [createSegment] = useCreateSegmentMutation();
+ const [updateSegment] = useUpdateSegmentMutation();
+
const handleSubmit = (values: ISegmentBase) => {
if (isNew) {
- return createSegment(namespace.key, values);
+ return createSegment({ namespaceKey: namespace.key, values }).unwrap();
}
- return updateSegment(namespace.key, segment?.key, values);
+ return updateSegment({
+ namespaceKey: namespace.key,
+ segmentKey: segment?.key,
+ values
+ }).unwrap();
};
const initialValues: ISegmentBase = {
diff --git a/ui/src/data/api.ts b/ui/src/data/api.ts
index aef1818945..9c1f12e0e1 100644
--- a/ui/src/data/api.ts
+++ b/ui/src/data/api.ts
@@ -1,14 +1,12 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import { IAuthTokenBase } from '~/types/auth/Token';
-import { IConstraintBase } from '~/types/Constraint';
import { IDistributionBase } from '~/types/Distribution';
import { FlagType, IFlagBase } from '~/types/Flag';
import { IRolloutBase } from '~/types/Rollout';
import { IRuleBase } from '~/types/Rule';
-import { ISegmentBase } from '~/types/Segment';
import { IVariantBase } from '~/types/Variant';
-const apiURL = '/api/v1';
+export const apiURL = '/api/v1';
const authURL = '/auth/v1';
const evaluateURL = '/evaluate/v1';
const metaURL = '/meta';
@@ -26,34 +24,46 @@ export class APIError extends Error {
//
// base methods
-function setCsrf(req: any) {
+function headerCsrf(): Record {
const csrfToken = window.localStorage.getItem(csrfTokenHeaderKey);
if (csrfToken !== null) {
- req.headers[csrfTokenHeaderKey] = csrfToken;
+ return { 'x-csrf-token': csrfToken };
}
+ return {};
+}
+// TODO: find a better name for this
+export function checkResponse(response: Response) {
+ if (!response.ok) {
+ if (response.status === 401) {
+ window.localStorage.removeItem(csrfTokenHeaderKey);
+ window.localStorage.removeItem(sessionKey);
+ window.location.reload();
+ }
+ }
+}
- return req;
+export function defaultHeaders(): Record {
+ const headers = {
+ ...headerCsrf(),
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ 'Cache-Control': 'no-store'
+ };
+
+ return headers;
}
export async function request(method: string, uri: string, body?: any) {
- const req = setCsrf({
+ const req = {
method,
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- 'Cache-Control': 'no-store'
- },
+ headers: defaultHeaders(),
body: JSON.stringify(body)
- });
+ };
const res = await fetch(uri, req);
- if (!res.ok) {
- if (res.status === 401) {
- window.localStorage.removeItem(csrfTokenHeaderKey);
- window.localStorage.removeItem(sessionKey);
- window.location.reload();
- }
+ if (!res.ok) {
+ checkResponse(res);
const contentType = res.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
@@ -64,7 +74,6 @@ export async function request(method: string, uri: string, body?: any) {
let err = await res.json();
throw new APIError(err.message, res.status);
}
-
return res.json();
}
@@ -216,15 +225,16 @@ export async function orderRollouts(
flagKey: string,
rolloutIds: string[]
) {
- const req = setCsrf({
+ const req = {
method: 'PUT',
headers: {
+ ...headerCsrf(),
'Content-Type': 'application/json'
},
body: JSON.stringify({
rolloutIds: rolloutIds
})
- });
+ };
const res = await fetch(
`${apiURL}/namespaces/${namespaceKey}/flags/${flagKey}/rollouts/order`,
@@ -276,15 +286,16 @@ export async function orderRules(
flagKey: string,
ruleIds: string[]
) {
- const req = setCsrf({
+ const req = {
method: 'PUT',
headers: {
+ ...headerCsrf(),
'Content-Type': 'application/json'
},
body: JSON.stringify({
ruleIds
})
- });
+ };
const res = await fetch(
`${apiURL}/namespaces/${namespaceKey}/flags/${flagKey}/rules/order`,
@@ -354,90 +365,6 @@ export async function deleteVariant(
);
}
-//
-// segments
-export async function listSegments(namespaceKey: string) {
- return get(`/namespaces/${namespaceKey}/segments`);
-}
-
-export async function getSegment(namespaceKey: string, key: string) {
- return get(`/namespaces/${namespaceKey}/segments/${key}`);
-}
-
-export async function createSegment(
- namespaceKey: string,
- values: ISegmentBase
-) {
- return post(`/namespaces/${namespaceKey}/segments`, values);
-}
-
-export async function updateSegment(
- namespaceKey: string,
- key: string,
- values: ISegmentBase
-) {
- return put(`/namespaces/${namespaceKey}/segments/${key}`, values);
-}
-
-export async function deleteSegment(namespaceKey: string, key: string) {
- return del(`/namespaces/${namespaceKey}/segments/${key}`);
-}
-
-export async function copySegment(
- from: { namespaceKey: string; key: string },
- to: { namespaceKey: string; key?: string }
-) {
- let segment = await get(
- `/namespaces/${from.namespaceKey}/segments/${from.key}`
- );
- if (to.key) {
- segment.key = to.key;
- }
-
- // first create the segment
- await post(`/namespaces/${to.namespaceKey}/segments`, segment);
-
- // then copy the constraints
- for (let constraint of segment.constraints) {
- await createConstraint(to.namespaceKey, segment.key, constraint);
- }
-}
-
-//
-// constraints
-export async function createConstraint(
- namespaceKey: string,
- segmentKey: string,
- values: IConstraintBase
-) {
- return post(
- `/namespaces/${namespaceKey}/segments/${segmentKey}/constraints`,
- values
- );
-}
-
-export async function updateConstraint(
- namespaceKey: string,
- segmentKey: string,
- constraintId: string,
- values: IConstraintBase
-) {
- return put(
- `/namespaces/${namespaceKey}/segments/${segmentKey}/constraints/${constraintId}`,
- values
- );
-}
-
-export async function deleteConstraint(
- namespaceKey: string,
- segmentKey: string,
- constraintId: string
-) {
- return del(
- `/namespaces/${namespaceKey}/segments/${segmentKey}/constraints/${constraintId}`
- );
-}
-
//
// evaluate
export async function evaluate(
diff --git a/ui/src/store.ts b/ui/src/store.ts
index c69f4650ba..909cb4a728 100644
--- a/ui/src/store.ts
+++ b/ui/src/store.ts
@@ -11,6 +11,7 @@ import {
preferencesKey,
preferencesSlice
} from './app/preferences/preferencesSlice';
+import { segmentsApi } from './app/segments/segmentsApi';
import { LoadingStatus } from './types/Meta';
const listenerMiddleware = createListenerMiddleware();
@@ -67,10 +68,13 @@ export const store = configureStore({
namespaces: namespacesSlice.reducer,
preferences: preferencesSlice.reducer,
flags: flagsSlice.reducer,
- meta: metaSlice.reducer
+ meta: metaSlice.reducer,
+ [segmentsApi.reducerPath]: segmentsApi.reducer
},
middleware: (getDefaultMiddleware) =>
- getDefaultMiddleware().prepend(listenerMiddleware.middleware)
+ getDefaultMiddleware()
+ .prepend(listenerMiddleware.middleware)
+ .concat(segmentsApi.middleware)
});
// Infer the `RootState` and `AppDispatch` types from the store itself
diff --git a/ui/src/utils/helpers.ts b/ui/src/utils/helpers.ts
index 74b843383e..0ac59a8aba 100644
--- a/ui/src/utils/helpers.ts
+++ b/ui/src/utils/helpers.ts
@@ -58,9 +58,23 @@ function isErrorWithMessage(error: unknown): error is ErrorWithMessage {
);
}
+function isFetchBaseQueryError(error: unknown): error is ErrorWithMessage {
+ return (
+ typeof error === 'object' &&
+ error !== null &&
+ 'data' in error &&
+ error.data !== null &&
+ typeof (error.data as Record).message === 'string'
+ );
+}
+
function toErrorWithMessage(maybeError: unknown): ErrorWithMessage {
if (isErrorWithMessage(maybeError)) return maybeError;
-
+ // handle Redux FetchBaseQueryError
+ if (isFetchBaseQueryError(maybeError)) {
+ // @ts-ignore
+ return maybeError.data;
+ }
try {
return new Error(JSON.stringify(maybeError));
} catch {
diff --git a/ui/src/utils/redux-rtk.ts b/ui/src/utils/redux-rtk.ts
new file mode 100644
index 0000000000..d02e3c8501
--- /dev/null
+++ b/ui/src/utils/redux-rtk.ts
@@ -0,0 +1,23 @@
+import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
+import { apiURL, checkResponse, defaultHeaders } from '~/data/api';
+
+type CustomFetchFn = (
+ url: RequestInfo,
+ options: RequestInit | undefined
+) => Promise;
+
+const customFetchFn: CustomFetchFn = async (url, options) => {
+ const headers = defaultHeaders();
+
+ const response = await fetch(url, {
+ ...options,
+ headers
+ });
+ checkResponse(response);
+ return response;
+};
+
+export const baseQuery = fetchBaseQuery({
+ baseUrl: apiURL,
+ fetchFn: customFetchFn
+});