{
- expect(baseRoutes).toHaveLength(38);
+ expect(baseRoutes).toHaveLength(39);
expect(baseRoutes).toMatchSnapshot();
});
diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js
index 63175e8289..7f507c98dc 100644
--- a/frontend/src/component/menu/routes.js
+++ b/frontend/src/component/menu/routes.js
@@ -43,6 +43,7 @@ import RedirectArchive from '../feature/RedirectArchive/RedirectArchive';
import EnvironmentList from '../environments/EnvironmentList/EnvironmentList';
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
import FeatureView2 from '../feature/FeatureView2/FeatureView2';
+import FeatureStrategies from '../feature/FeatureView2/FeatureStrategies/FeatureStrategies';
export const routes = [
// Features
@@ -242,6 +243,16 @@ export const routes = [
layout: 'main',
menu: {},
},
+ {
+ path: '/projects/:projectId/features2/:featureId/strategies',
+ parent: '/projects',
+ title: 'FeatureView2',
+ component: FeatureStrategies,
+ type: 'protected',
+ layout: 'main',
+ flags: E,
+ menu: {},
+ },
{
path: '/projects/:projectId/features2/:featureId',
parent: '/projects',
diff --git a/frontend/src/constants/environmentTypes.ts b/frontend/src/constants/environmentTypes.ts
new file mode 100644
index 0000000000..e0a3cb9e60
--- /dev/null
+++ b/frontend/src/constants/environmentTypes.ts
@@ -0,0 +1 @@
+export const PRODUCTION = 'production';
diff --git a/frontend/src/contexts/FeatureStrategiesUIContext.ts b/frontend/src/contexts/FeatureStrategiesUIContext.ts
new file mode 100644
index 0000000000..f0395e3204
--- /dev/null
+++ b/frontend/src/contexts/FeatureStrategiesUIContext.ts
@@ -0,0 +1,21 @@
+import React from 'react';
+import { IFeatureEnvironment } from '../interfaces/featureToggle';
+import { IStrategyPayload } from '../interfaces/strategy';
+
+interface IFeatureStrategiesUIContext {
+ configureNewStrategy: IStrategyPayload | null;
+ setConfigureNewStrategy: React.Dispatch<
+ React.SetStateAction
+ >;
+ setActiveEnvironment: React.Dispatch<
+ React.SetStateAction
+ >;
+ activeEnvironment: IFeatureEnvironment | null;
+ expandedSidebar: boolean;
+ setExpandedSidebar: React.Dispatch>;
+}
+
+const FeatureStrategiesUIContext =
+ React.createContext(null);
+
+export default FeatureStrategiesUIContext;
diff --git a/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts b/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts
new file mode 100644
index 0000000000..75e972b45c
--- /dev/null
+++ b/frontend/src/hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi.ts
@@ -0,0 +1,85 @@
+import { IStrategyPayload } from '../../../../interfaces/strategy';
+import useAPI from '../useApi/useApi';
+
+const useFeatureStrategyApi = () => {
+ const { makeRequest, createRequest, errors, loading } = useAPI({
+ propagateErrors: true,
+ });
+
+ const addStrategyToFeature = async (
+ projectId: string,
+ featureId: string,
+ environmentId: string,
+ payload: IStrategyPayload
+ ) => {
+ const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies`;
+ const req = createRequest(
+ path,
+ { method: 'POST', body: JSON.stringify(payload) },
+ 'addStrategyToFeature'
+ );
+
+ try {
+ const res = await makeRequest(req.caller, req.id);
+
+ return res;
+ } catch (e) {
+ throw e;
+ }
+ };
+
+ const deleteStrategyFromFeature = async (
+ projectId: string,
+ featureId: string,
+ environmentId: string,
+ strategyId: string
+ ) => {
+ const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies/${strategyId}`;
+ const req = createRequest(
+ path,
+ { method: 'DELETE' },
+ 'deleteStrategyFromFeature'
+ );
+
+ try {
+ const res = await makeRequest(req.caller, req.id);
+
+ return res;
+ } catch (e) {
+ throw e;
+ }
+ };
+
+ const updateStrategyOnFeature = async (
+ projectId: string,
+ featureId: string,
+ environmentId: string,
+ strategyId: string,
+ payload: IStrategyPayload
+ ) => {
+ const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies/${strategyId}`;
+ const req = createRequest(
+ path,
+ { method: 'PUT', body: JSON.stringify(payload) },
+ 'updateStrategyOnFeature'
+ );
+
+ try {
+ const res = await makeRequest(req.caller, req.id);
+
+ return res;
+ } catch (e) {
+ throw e;
+ }
+ };
+
+ return {
+ addStrategyToFeature,
+ updateStrategyOnFeature,
+ deleteStrategyFromFeature,
+ loading,
+ errors,
+ };
+};
+
+export default useFeatureStrategyApi;
diff --git a/frontend/src/hooks/api/getters/useFeature/useFeature.ts b/frontend/src/hooks/api/getters/useFeature/useFeature.ts
index 59bde50a74..e65e812b8c 100644
--- a/frontend/src/hooks/api/getters/useFeature/useFeature.ts
+++ b/frontend/src/hooks/api/getters/useFeature/useFeature.ts
@@ -5,7 +5,18 @@ import { formatApiPath } from '../../../../utils/format-path';
import { IFeatureToggle } from '../../../../interfaces/featureToggle';
import { defaultFeature } from './defaultFeature';
-const useFeature = (projectId: string, id: string) => {
+interface IUseFeatureOptions {
+ refreshInterval?: number;
+ revalidateOnFocus?: boolean;
+ revalidateOnReconnect?: boolean;
+ revalidateIfStale?: boolean;
+}
+
+const useFeature = (
+ projectId: string,
+ id: string,
+ options: IUseFeatureOptions
+) => {
const fetcher = () => {
const path = formatApiPath(
`api/admin/projects/${projectId}/features/${id}`
@@ -15,31 +26,28 @@ const useFeature = (projectId: string, id: string) => {
}).then(res => res.json());
};
- const KEY = `api/admin/projects/${projectId}/features/${id}`;
+ const FEATURE_CACHE_KEY = `api/admin/projects/${projectId}/features/${id}`;
+
+ const { data, error } = useSWR(FEATURE_CACHE_KEY, fetcher, {
+ ...options,
+ });
- const { data, error } = useSWR(KEY, fetcher);
const [loading, setLoading] = useState(!error && !data);
const refetch = () => {
- mutate(KEY);
+ mutate(FEATURE_CACHE_KEY);
};
useEffect(() => {
setLoading(!error && !data);
}, [data, error]);
- let feature = defaultFeature;
- if (data) {
- if (data.environments) {
- feature = data;
- }
- }
-
return {
- feature,
+ feature: data || defaultFeature,
error,
loading,
refetch,
+ FEATURE_CACHE_KEY,
};
};
diff --git a/frontend/src/hooks/api/getters/useFeatureStrategy/useFeatureStrategy.ts b/frontend/src/hooks/api/getters/useFeatureStrategy/useFeatureStrategy.ts
new file mode 100644
index 0000000000..60a870ec00
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useFeatureStrategy/useFeatureStrategy.ts
@@ -0,0 +1,67 @@
+import useSWR, { mutate } from 'swr';
+import { useState, useEffect } from 'react';
+
+import { formatApiPath } from '../../../../utils/format-path';
+import { IFeatureStrategy } from '../../../../interfaces/strategy';
+
+interface IUseFeatureOptions {
+ refreshInterval?: number;
+ revalidateOnFocus?: boolean;
+ revalidateOnReconnect?: boolean;
+ revalidateIfStale?: boolean;
+ revalidateOnMount?: boolean;
+}
+
+const useFeatureStrategy = (
+ projectId: string,
+ featureId: string,
+ environmentId: string,
+ strategyId: string,
+ options: IUseFeatureOptions
+) => {
+ const fetcher = () => {
+ const path = formatApiPath(
+ `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies/${strategyId}`
+ );
+ return fetch(path, {
+ method: 'GET',
+ }).then(res => res.json());
+ };
+
+ const FEATURE_STRATEGY_CACHE_KEY = strategyId;
+
+ const { data, error } = useSWR(
+ FEATURE_STRATEGY_CACHE_KEY,
+ fetcher,
+ {
+ ...options,
+ }
+ );
+
+ const [loading, setLoading] = useState(!error && !data);
+
+ const refetch = () => {
+ mutate(FEATURE_STRATEGY_CACHE_KEY);
+ };
+
+ useEffect(() => {
+ setLoading(!error && !data);
+ }, [data, error]);
+
+ return {
+ strategy:
+ data ||
+ ({
+ constraints: [],
+ parameters: {},
+ id: '',
+ name: '',
+ } as IFeatureStrategy),
+ error,
+ loading,
+ refetch,
+ FEATURE_STRATEGY_CACHE_KEY,
+ };
+};
+
+export default useFeatureStrategy;
diff --git a/frontend/src/hooks/api/getters/useStrategies/useStrategies.ts b/frontend/src/hooks/api/getters/useStrategies/useStrategies.ts
new file mode 100644
index 0000000000..9638c58409
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useStrategies/useStrategies.ts
@@ -0,0 +1,40 @@
+import useSWR, { mutate } from 'swr';
+import { useEffect, useState } from 'react';
+import { formatApiPath } from '../../../../utils/format-path';
+import { IStrategy } from '../../../../interfaces/strategy';
+
+export const STRATEGIES_CACHE_KEY = 'api/admin/strategies';
+
+const useStrategies = () => {
+ const fetcher = () => {
+ const path = formatApiPath(`api/admin/strategies`);
+
+ return fetch(path, {
+ method: 'GET',
+ credentials: 'include',
+ }).then(res => res.json());
+ };
+
+ const { data, error } = useSWR<{ strategies: IStrategy[] }>(
+ STRATEGIES_CACHE_KEY,
+ fetcher
+ );
+ const [loading, setLoading] = useState(!error && !data);
+
+ const refetch = () => {
+ mutate(STRATEGIES_CACHE_KEY);
+ };
+
+ useEffect(() => {
+ setLoading(!error && !data);
+ }, [data, error]);
+
+ return {
+ strategies: data?.strategies || [],
+ error,
+ loading,
+ refetch,
+ };
+};
+
+export default useStrategies;
diff --git a/frontend/src/hooks/api/getters/useUnleashContext/useUnleashContext.ts b/frontend/src/hooks/api/getters/useUnleashContext/useUnleashContext.ts
new file mode 100644
index 0000000000..35a99b661e
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useUnleashContext/useUnleashContext.ts
@@ -0,0 +1,40 @@
+import useSWR, { mutate } from 'swr';
+import { useState, useEffect } from 'react';
+import { formatApiPath } from '../../../../utils/format-path';
+
+const useUnleashContext = (revalidate = true) => {
+ const fetcher = () => {
+ const path = formatApiPath(`api/admin/context`);
+ return fetch(path, {
+ method: 'GET',
+ }).then(res => res.json());
+ };
+
+ const CONTEXT_CACHE_KEY = 'api/admin/context';
+
+ const { data, error } = useSWR(CONTEXT_CACHE_KEY, fetcher, {
+ revalidateOnFocus: revalidate,
+ revalidateOnReconnect: revalidate,
+ revalidateIfStale: revalidate,
+ });
+
+ const [loading, setLoading] = useState(!error && !data);
+
+ const refetch = () => {
+ mutate(CONTEXT_CACHE_KEY);
+ };
+
+ useEffect(() => {
+ setLoading(!error && !data);
+ }, [data, error]);
+
+ return {
+ context: data || [],
+ error,
+ loading,
+ refetch,
+ CONTEXT_CACHE_KEY,
+ };
+};
+
+export default useUnleashContext;
diff --git a/frontend/src/hooks/useTabs.ts b/frontend/src/hooks/useTabs.ts
new file mode 100644
index 0000000000..4a00da83bb
--- /dev/null
+++ b/frontend/src/hooks/useTabs.ts
@@ -0,0 +1,14 @@
+import { useState } from 'react';
+
+const useTabs = (startingIndex: number = 0) => {
+ const [activeTab, setActiveTab] = useState(startingIndex);
+
+ const a11yProps = (index: number) => ({
+ id: `tab-${index}`,
+ 'aria-controls': `tabpanel-${index}`,
+ });
+
+ return { activeTab, setActiveTab, a11yProps };
+};
+
+export default useTabs;
diff --git a/frontend/src/hooks/useToast.tsx b/frontend/src/hooks/useToast.tsx
index 7c9534f3ed..3c388fc79d 100644
--- a/frontend/src/hooks/useToast.tsx
+++ b/frontend/src/hooks/useToast.tsx
@@ -1,15 +1,21 @@
import { useState } from 'react';
import Toast from '../component/common/Toast/Toast';
+export interface IToast {
+ show: boolean;
+ type: string;
+ text: string;
+}
+
const useToast = () => {
- const [toastData, setToastData] = useState({
+ const [toastData, setToastData] = useState({
show: false,
type: 'success',
text: '',
});
const hideToast = () => {
- setToastData(prev => ({ ...prev, show: false }));
+ setToastData((prev: IToast) => ({ ...prev, show: false }));
};
const toast = (
nameMapping[strategyName];
+export const getHumanReadbleStrategy = strategyName =>
+ nameMapping[strategyName];
export const getHumanReadbleStrategyName = strategyName => {
const humanReadableStrategy = nameMapping[strategyName];
@@ -44,3 +55,18 @@ export const getHumanReadbleStrategyName = strategyName => {
}
return strategyName;
};
+
+export const getFeatureStrategyIcon = strategyName => {
+ switch (strategyName) {
+ case 'remoteAddress':
+ return LanguageIcon;
+ case 'flexibleRollout':
+ return DonutLarge;
+ case 'userWithId':
+ return PeopleIcon;
+ case 'applicationHostname':
+ return LocationOnIcon;
+ default:
+ return MapIcon;
+ }
+};
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index fa9451b337..4e1c7292a9 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -3495,9 +3495,11 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001181:
- version "1.0.30001207"
- resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001207.tgz"
- integrity sha512-UPQZdmAsyp2qfCTiMU/zqGSWOYaY9F9LL61V8f+8MrubsaDGpaHD9HRV/EWZGULZn0Hxu48SKzI5DgFwTvHuYw==
+ version "1.0.30001260"
+ resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001260.tgz"
+ integrity sha512-Fhjc/k8725ItmrvW5QomzxLeojewxvqiYCKeFcfFEhut28IVLdpHU19dneOmltZQIE5HNbawj1HYD+1f2bM1Dg==
+ dependencies:
+ nanocolors "^0.1.0"
capture-exit@^2.0.0:
version "2.0.0"
@@ -8214,6 +8216,11 @@ nan@^2.12.1:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
+nanocolors@^0.1.0:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/nanocolors/-/nanocolors-0.1.12.tgz#8577482c58cbd7b5bb1681db4cf48f11a87fd5f6"
+ integrity sha512-2nMHqg1x5PU+unxX7PGY7AuYxl2qDx7PSrTRjizr8sxdd3l/3hBuWWaki62qmtYm2U5i4Z5E7GbjlyDFhs9/EQ==
+
nanoid@^3.1.22:
version "3.1.22"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz"