From 47579e26164277d2df2cf0e34164dabb63b30bec Mon Sep 17 00:00:00 2001
From: Fredrik Strand Oseberg
Date: Fri, 1 Oct 2021 13:49:18 +0200
Subject: [PATCH] Feat/toggle view (#389)
* feat: toggle view
* fix: navigation
* eat: toggle view
* fix: resolve lint
* fix: remove console logs
* fix: reimplement feature validation
---
frontend/src/app.css | 4 +
.../FeatureLog/FeatureLog.styles.ts | 9 +
.../FeatureView2/FeatureLog/FeatureLog.tsx | 19 ++
.../FeatureMetrics/FeatureMetrics.styles.ts | 9 +
.../FeatureMetrics/FeatureMetrics.tsx | 19 ++
.../FeatureOverview/FeatureOverview.styles.ts | 10 +
.../FeatureOverview/FeatureOverview.tsx | 21 +++
.../FeatureOverviewEnvironment.styles.ts | 77 ++++++++
.../FeatureOverviewEnvironment.tsx | 173 ++++++++++++++++++
.../FeatureOverviewStrategyCard.styles.ts | 36 ++++
.../FeatureOverviewStrategyCard.tsx | 42 +++++
.../FeatureOverviewStrategies.styles.ts | 27 +++
.../FeatureOverviewStrategies.tsx | 54 ++++++
.../FeatureOverviewTags.styles.ts | 40 ++++
.../FeatureOverviewTags.tsx | 101 ++++++++++
.../FeatureViewMetaData.tsx | 59 ++++++
.../FeatureViewMetadata.styles.ts | 21 +++
.../FeatureStrategiesConfigure.tsx | 4 +-
.../useFeatureStrategiesEnvironmentList.ts | 7 +-
.../FeatureStrategiesEnvironments.tsx | 37 +++-
.../FeatureStrategyCard.styles.ts | 1 +
.../FeatureVariants/FeatureVariants.styles.ts | 9 +
.../FeatureVariants/FeatureVariants.tsx | 20 ++
.../feature/FeatureView2/FeatureView2.tsx | 92 ++++------
.../FeatureViewMetaData.tsx | 58 ------
.../variant/update-variant-container.jsx | 3 +-
.../__snapshots__/routes-test.jsx.snap | 2 +-
frontend/src/component/menu/routes.js | 2 +-
.../actions/useFeatureApi/useFeatureApi.ts | 93 +++++++++-
.../api/getters/useFeature/useFeature.ts | 2 +-
.../api/getters/useTagTypes/useTagTypes.ts | 36 ++++
.../src/hooks/api/getters/useTags/useTags.ts | 36 ++++
frontend/src/interfaces/tags.ts | 10 +
33 files changed, 1005 insertions(+), 128 deletions(-)
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureLog/FeatureLog.styles.ts
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureLog/FeatureLog.tsx
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureMetrics/FeatureMetrics.styles.ts
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureMetrics/FeatureMetrics.tsx
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverview.styles.ts
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverview.tsx
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewEnvironment.styles.ts
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewStrategyCard/FeatureOverviewStrategyCard.styles.ts
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewStrategyCard/FeatureOverviewStrategyCard.tsx
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewStrategies.styles.ts
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewStrategies.tsx
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewTags/FeatureOverviewTags.styles.ts
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewTags/FeatureOverviewTags.tsx
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureViewMetaData/FeatureViewMetaData.tsx
rename frontend/src/component/feature/FeatureView2/{ => FeatureOverview}/FeatureViewMetaData/FeatureViewMetadata.styles.ts (52%)
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariants.styles.ts
create mode 100644 frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariants.tsx
delete mode 100644 frontend/src/component/feature/FeatureView2/FeatureViewMetaData/FeatureViewMetaData.tsx
create mode 100644 frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts
create mode 100644 frontend/src/hooks/api/getters/useTags/useTags.ts
create mode 100644 frontend/src/interfaces/tags.ts
diff --git a/frontend/src/app.css b/frontend/src/app.css
index a988c73e9f..015916508f 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -13,6 +13,10 @@ body {
font-family: 'Sen', sans-serif;
}
+button {
+ font-family: 'Sen', sans-serif;
+}
+
.MuiButton-root {
border-radius: 3px;
text-transform: none;
diff --git a/frontend/src/component/feature/FeatureView2/FeatureLog/FeatureLog.styles.ts b/frontend/src/component/feature/FeatureView2/FeatureLog/FeatureLog.styles.ts
new file mode 100644
index 0000000000..10dae26fef
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureLog/FeatureLog.styles.ts
@@ -0,0 +1,9 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ borderRadius: '12.5px',
+ backgroundColor: '#fff',
+ padding: '2rem',
+ },
+}));
diff --git a/frontend/src/component/feature/FeatureView2/FeatureLog/FeatureLog.tsx b/frontend/src/component/feature/FeatureView2/FeatureLog/FeatureLog.tsx
new file mode 100644
index 0000000000..4a1453bf4c
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureLog/FeatureLog.tsx
@@ -0,0 +1,19 @@
+import { useParams } from 'react-router';
+import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
+import { useStyles } from './FeatureLog.styles';
+import { IFeatureViewParams } from '../../../../interfaces/params';
+import HistoryComponent from '../../../history/FeatureEventHistory';
+
+const FeatureLog = () => {
+ const styles = useStyles();
+ const { projectId, featureId } = useParams();
+ const { feature } = useFeature(projectId, featureId);
+
+ return (
+
+ ;
+
+ );
+};
+
+export default FeatureLog;
diff --git a/frontend/src/component/feature/FeatureView2/FeatureMetrics/FeatureMetrics.styles.ts b/frontend/src/component/feature/FeatureView2/FeatureMetrics/FeatureMetrics.styles.ts
new file mode 100644
index 0000000000..10dae26fef
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureMetrics/FeatureMetrics.styles.ts
@@ -0,0 +1,9 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ borderRadius: '12.5px',
+ backgroundColor: '#fff',
+ padding: '2rem',
+ },
+}));
diff --git a/frontend/src/component/feature/FeatureView2/FeatureMetrics/FeatureMetrics.tsx b/frontend/src/component/feature/FeatureView2/FeatureMetrics/FeatureMetrics.tsx
new file mode 100644
index 0000000000..00e3d309fd
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureMetrics/FeatureMetrics.tsx
@@ -0,0 +1,19 @@
+import { useParams } from 'react-router';
+import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
+import MetricComponent from '../../view/metric-container';
+import { useStyles } from './FeatureMetrics.styles';
+import { IFeatureViewParams } from '../../../../interfaces/params';
+
+const FeatureMetrics = () => {
+ const styles = useStyles();
+ const { projectId, featureId } = useParams();
+ const { feature } = useFeature(projectId, featureId);
+
+ return (
+
+
+
+ );
+};
+
+export default FeatureMetrics;
diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverview.styles.ts b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverview.styles.ts
new file mode 100644
index 0000000000..1af8d4ceaa
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverview.styles.ts
@@ -0,0 +1,10 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ container: { display: 'flex', width: '100%' },
+ mainContent: {
+ display: 'flex',
+ flexDirection: 'column',
+ width: '100%',
+ },
+}));
diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverview.tsx b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverview.tsx
new file mode 100644
index 0000000000..c0e8ca75d7
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverview.tsx
@@ -0,0 +1,21 @@
+import FeatureViewMetaData from './FeatureViewMetaData/FeatureViewMetaData';
+import FeatureOverviewStrategies from './FeatureOverviewStrategies/FeatureOverviewStrategies';
+import { useStyles } from './FeatureOverview.styles';
+import FeatureOverviewTags from './FeatureOverviewTags/FeatureOverviewTags';
+
+const FeatureOverview = () => {
+ const styles = useStyles();
+ return (
+
+ );
+};
+
+export default FeatureOverview;
diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewEnvironment.styles.ts b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewEnvironment.styles.ts
new file mode 100644
index 0000000000..b8ba159492
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewEnvironment.styles.ts
@@ -0,0 +1,77 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ marginBottom: '2rem',
+ border: `1px solid ${theme.palette.grey[300]}`,
+ borderRadius: '5px',
+ position: 'relative',
+ },
+ header: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ borderBottom: `1px solid ${theme.palette.grey[300]}`,
+ padding: '1rem',
+ },
+ icon: {
+ fill: '#fff',
+ height: '17.5px',
+ width: '17.5px',
+ },
+ strategiesContainer: {
+ padding: '1rem 0',
+ ['& > *']: {
+ margin: '0.5rem 0',
+ },
+ },
+ environmentIdentifier: {
+ position: 'absolute',
+ right: '42.5%',
+ top: '-25px',
+ display: 'flex',
+ background: theme.palette.primary.light,
+ borderRadius: '25px',
+ padding: '0.4rem 1rem',
+ minWidth: '150px',
+ color: '#fff',
+
+ alignItems: 'center',
+ },
+ environmentBadgeParagraph: {
+ fontSize: theme.fontSizes.smallBody,
+ },
+ iconContainer: {
+ padding: '0.25rem',
+ borderRadius: '50%',
+ alignItems: 'center',
+ justifyContent: 'center',
+ display: 'flex',
+ border: '1px solid #fff',
+ marginRight: '0.5rem',
+ },
+ body: {
+ padding: '1rem',
+ },
+ disabledEnvContainer: {
+ backgroundColor: theme.palette.grey[300],
+ color: theme.palette.grey[600],
+ },
+ disabledIconContainer: {
+ border: `1px solid ${theme.palette.grey[500]}`,
+ },
+ iconDisabled: {
+ fill: theme.palette.grey[500],
+ },
+
+ toggleText: {
+ fontSize: theme.fontSizes.smallBody,
+ },
+ toggleLink: {
+ color: theme.palette.primary.main,
+ fontSize: theme.fontSizes.smallBody,
+ },
+ headerDisabledEnv: {
+ border: 'none',
+ },
+}));
diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx
new file mode 100644
index 0000000000..4fb8b4e4b1
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx
@@ -0,0 +1,173 @@
+import { Cloud } from '@material-ui/icons';
+import { IFeatureEnvironment } from '../../../../../../interfaces/featureToggle';
+import { Switch } from '@material-ui/core';
+import { useStyles } from './FeatureOverviewEnvironment.styles';
+import FeatureOverviewStrategyCard from './FeatureOverviewStrategyCard/FeatureOverviewStrategyCard';
+import classNames from 'classnames';
+import ConditionallyRender from '../../../../../common/ConditionallyRender';
+import useFeatureApi from '../../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
+import { useHistory, useParams, Link } from 'react-router-dom';
+import { IFeatureViewParams } from '../../../../../../interfaces/params';
+import useToast from '../../../../../../hooks/useToast';
+
+interface IFeatureOverviewEnvironmentProps {
+ env: IFeatureEnvironment;
+ refetch: () => void;
+}
+
+const FeatureOverviewEnvironment = ({
+ env,
+ refetch,
+}: IFeatureOverviewEnvironmentProps) => {
+ const { featureId, projectId } = useParams();
+ const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
+ useFeatureApi();
+ const styles = useStyles();
+ const { toast, setToastData } = useToast();
+ const history = useHistory();
+
+ console.log(env);
+
+ const handleClick = () => {
+ history.push(
+ `/projects/${projectId}/features2/${featureId}/strategies?environment=${env.name}`
+ );
+ };
+
+ const renderStrategies = () => {
+ const { strategies } = env;
+
+ return strategies.map(strategy => {
+ return (
+
+ );
+ });
+ };
+
+ const handleToggleEnvironmentOn = async () => {
+ try {
+ await toggleFeatureEnvironmentOn(projectId, featureId, env.name);
+ setToastData({
+ type: 'success',
+ show: true,
+ text: 'Successfully turned environment on.',
+ });
+ refetch();
+ } catch (e) {
+ setToastData({
+ show: true,
+ type: 'error',
+ text: e.toString(),
+ });
+ }
+ };
+
+ const handleToggleEnvironmentOff = async () => {
+ try {
+ await toggleFeatureEnvironmentOff(projectId, featureId, env.name);
+ setToastData({
+ type: 'success',
+ show: true,
+ text: 'Successfully turned environment off.',
+ });
+ refetch();
+ } catch (e) {
+ setToastData({
+ show: true,
+ type: 'error',
+ text: e.toString(),
+ });
+ }
+ };
+
+ const toggleEnvironment = (e: React.ChangeEvent) => {
+ if (env.enabled) {
+ handleToggleEnvironmentOff();
+ return;
+ }
+ handleToggleEnvironmentOn();
+ };
+
+ const iconContainerClasses = classNames(styles.iconContainer, {
+ [styles.disabledIconContainer]: !env.enabled,
+ });
+
+ const iconClasses = classNames(styles.icon, {
+ [styles.iconDisabled]: !env.enabled,
+ });
+
+ const headerClasses = classNames(styles.header, {
+ [styles.headerDisabledEnv]: !env.enabled,
+ });
+
+ const environmentIdentifierClasses = classNames(
+ styles.environmentIdentifier,
+ { [styles.disabledEnvContainer]: !env.enabled }
+ );
+
+ return (
+
+
+
+
+
+
+
0}
+ show={
+ <>
+ {' '}
+
+ This environment is{' '}
+ {env.enabled ? 'enabled' : 'disabled'}
+
+ >
+ }
+ elseShow={
+ <>
+
+ No strategies configured for environment.
+
+
+ Configure strategies for {env.name}
+
+ >
+ }
+ />
+
+
+
+
+
+ {renderStrategies()}
+
+
+ }
+ />
+ {toast}
+
+ );
+};
+
+export default FeatureOverviewEnvironment;
diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewStrategyCard/FeatureOverviewStrategyCard.styles.ts b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewStrategyCard/FeatureOverviewStrategyCard.styles.ts
new file mode 100644
index 0000000000..cf610c3638
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewStrategyCard/FeatureOverviewStrategyCard.styles.ts
@@ -0,0 +1,36 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ card: {
+ border: `1px solid ${theme.palette.grey[300]}`,
+ borderRadius: '5px',
+ transition: 'transform 0.3s ease',
+ transitionDelay: '0.1s',
+ position: 'relative',
+ background: 'transparent',
+ width: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ padding: '0.75rem',
+ fontSize: theme.fontSizes.bodySize,
+ },
+ cardHeader: {
+ maxWidth: '200px',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ [theme.breakpoints.down(700)]: {
+ maxWidth: '100px',
+ fontSize: theme.fontSizes.smallBody,
+ },
+ },
+ icon: {
+ marginRight: '0.5rem',
+ fill: theme.palette.primary.main,
+ minWidth: '35px',
+ },
+ rollout: {
+ fontSize: theme.fontSizes.smallBody,
+ marginLeft: '0.5rem',
+ },
+}));
diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewStrategyCard/FeatureOverviewStrategyCard.tsx b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewStrategyCard/FeatureOverviewStrategyCard.tsx
new file mode 100644
index 0000000000..58d927eccc
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewEnvironment/FeatureOverviewStrategyCard/FeatureOverviewStrategyCard.tsx
@@ -0,0 +1,42 @@
+import { useMediaQuery } from '@material-ui/core';
+import { IFeatureStrategy } from '../../../../../../../interfaces/strategy';
+import {
+ getFeatureStrategyIcon,
+ getHumanReadbleStrategyName,
+} from '../../../../../../../utils/strategy-names';
+import ConditionallyRender from '../../../../../../common/ConditionallyRender';
+import { useStyles } from './FeatureOverviewStrategyCard.styles';
+
+interface IFeatureOverviewStrategyCardProps {
+ strategy: IFeatureStrategy;
+ onClick: () => void;
+}
+
+const FeatureOverviewStrategyCard = ({
+ strategy,
+ onClick,
+}: IFeatureOverviewStrategyCardProps) => {
+ const styles = useStyles();
+ const smallScreen = useMediaQuery('(max-width:500px)');
+ const strategyName = getHumanReadbleStrategyName(strategy.name);
+ const Icon = getFeatureStrategyIcon(strategy.name);
+
+ const { parameters } = strategy;
+ return (
+
+ }
+ />
+
+ );
+};
+
+export default FeatureOverviewStrategyCard;
diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewStrategies.styles.ts b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewStrategies.styles.ts
new file mode 100644
index 0000000000..06b52ffb6a
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewStrategies.styles.ts
@@ -0,0 +1,27 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ borderRadius: '12.5px',
+ width: '100%',
+ backgroundColor: '#fff',
+ },
+ headerContainer: {
+ borderBottom: `1px solid ${theme.palette.grey[300]}`,
+ },
+ headerTitle: {
+ fontSize: theme.fontSizes.subHeader,
+ fontWeight: 'normal',
+ margin: 0,
+ },
+ headerInnerContainer: {
+ padding: '1.5rem 2rem',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ actions: { position: 'relative' },
+ bodyContainer: {
+ padding: '3rem 2rem',
+ },
+}));
diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewStrategies.tsx b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewStrategies.tsx
new file mode 100644
index 0000000000..3ea0cfe782
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewStrategies/FeatureOverviewStrategies.tsx
@@ -0,0 +1,54 @@
+import { Add } from '@material-ui/icons';
+import { Link, useParams } from 'react-router-dom';
+import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
+import { IFeatureViewParams } from '../../../../../interfaces/params';
+import ResponsiveButton from '../../../../common/ResponsiveButton/ResponsiveButton';
+import FeatureOverviewEnvironment from './FeatureOverviewEnvironment/FeatureOverviewEnvironment';
+import { useStyles } from './FeatureOverviewStrategies.styles';
+
+const FeatureOverviewStrategies = () => {
+ const styles = useStyles();
+ const { projectId, featureId } = useParams();
+ const { feature, refetch } = useFeature(projectId, featureId);
+
+ if (!feature) return null;
+
+ const { environments } = feature;
+
+ const renderEnvironments = () => {
+ return environments?.map(env => {
+ return (
+
+ );
+ });
+ };
+
+ return (
+
+
+
+
Toggle Strategies
+
+
+ Add new strategy
+
+
+
+
+
+
{renderEnvironments()}
+
+ );
+};
+
+export default FeatureOverviewStrategies;
diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewTags/FeatureOverviewTags.styles.ts b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewTags/FeatureOverviewTags.styles.ts
new file mode 100644
index 0000000000..f5562e2443
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewTags/FeatureOverviewTags.styles.ts
@@ -0,0 +1,40 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ borderRadius: '10px',
+ backgroundColor: '#fff',
+ display: 'flex',
+ flexDirection: 'column',
+
+ maxWidth: '350px',
+ minWidth: '350px',
+ marginRight: '1rem',
+ marginTop: '1rem',
+ },
+ tagheaderContainer: {
+ display: 'flex',
+ alignItems: 'center',
+ padding: '1.5rem',
+ justifyContent: 'space-between',
+ borderBottom: `1px solid ${theme.palette.grey[300]}`,
+ },
+ tagHeader: {
+ display: 'flex',
+ alignItems: 'center',
+ },
+ tag: {
+ height: '40px',
+ width: '40px',
+ fill: theme.palette.primary.main,
+ marginRight: '0.8rem',
+ },
+ tagHeaderText: {
+ fontSize: theme.fontSizes.subHeader,
+ fontWeight: 'normal',
+ margin: 0,
+ },
+ tagContent: {
+ padding: '1.5rem',
+ },
+}));
diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewTags/FeatureOverviewTags.tsx b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewTags/FeatureOverviewTags.tsx
new file mode 100644
index 0000000000..d0b8601933
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewTags/FeatureOverviewTags.tsx
@@ -0,0 +1,101 @@
+import { Chip } from '@material-ui/core';
+import { Label } from '@material-ui/icons';
+import { useParams } from 'react-router-dom';
+import useTags from '../../../../../hooks/api/getters/useTags/useTags';
+import { IFeatureViewParams } from '../../../../../interfaces/params';
+import { useStyles } from './FeatureOverviewTags.styles';
+
+import slackIcon from '../../../../../assets/icons/slack.svg';
+import jiraIcon from '../../../../../assets/icons/jira.svg';
+import webhookIcon from '../../../../../assets/icons/webhooks.svg';
+import { formatAssetPath } from '../../../../../utils/format-path';
+import useTagTypes from '../../../../../hooks/api/getters/useTagTypes/useTagTypes';
+import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
+import AddTagDialogContainer from '../../../add-tag-dialog-container';
+
+const FeatureOverviewTags = () => {
+ const styles = useStyles();
+ const { featureId } = useParams();
+ const { tags, refetch } = useTags(featureId);
+ const { tagTypes } = useTagTypes();
+ const { deleteTag } = useFeatureApi();
+
+ const handleDelete = async (type: string, value: string) => {
+ try {
+ await deleteTag(featureId, type, value);
+ refetch();
+ } catch (e) {
+ // TODO: Handle error
+ console.log(e);
+ }
+ };
+
+ const tagIcon = (typeName: string) => {
+ let tagType = tagTypes.find(type => type.name === typeName);
+
+ const style = { width: '20px', height: '20px', marginRight: '5px' };
+
+ if (tagType && tagType.icon) {
+ switch (tagType.name) {
+ case 'slack':
+ return (
+
+ );
+ case 'jira':
+ return (
+
+ );
+ case 'webhook':
+ return (
+
+ );
+ default:
+ return ;
+ }
+ } else {
+ return {typeName[0].toUpperCase()};
+ }
+ };
+
+ const renderTag = t => (
+ handleDelete(t.type, t.value)}
+ />
+ );
+
+ return (
+
+
+
+
+
Tags
+
+
+
+ {/*
+
+ */}
+
+
+
{tags.map(renderTag)}
+
+ );
+};
+
+export default FeatureOverviewTags;
diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureViewMetaData/FeatureViewMetaData.tsx b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureViewMetaData/FeatureViewMetaData.tsx
new file mode 100644
index 0000000000..f3c3a4239a
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureViewMetaData/FeatureViewMetaData.tsx
@@ -0,0 +1,59 @@
+import { capitalize, IconButton } from '@material-ui/core';
+import classnames from 'classnames';
+import { useParams } from 'react-router-dom';
+import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
+import { getFeatureTypeIcons } from '../../../../../utils/get-feature-type-icons';
+import ConditionallyRender from '../../../../common/ConditionallyRender';
+import { useStyles } from './FeatureViewMetadata.styles';
+
+import { Edit } from '@material-ui/icons';
+import { IFeatureViewParams } from '../../../../../interfaces/params';
+
+const FeatureViewMetaData = () => {
+ const styles = useStyles();
+ const { projectId, featureId } = useParams();
+
+ const { feature } = useFeature(projectId, featureId);
+
+ const { project, description, type } = feature;
+
+ const IconComponent = getFeatureTypeIcons(type);
+
+ return (
+
+
+ {' '}
+
+ {capitalize(type || '')} toggle
+
+
+
+
Project: {project}
+
+ Description:
+
+
+ }
+ elseShow={
+
+ No description.{' '}
+
+
+
+
+ }
+ />
+
+
+ );
+};
+
+export default FeatureViewMetaData;
diff --git a/frontend/src/component/feature/FeatureView2/FeatureViewMetaData/FeatureViewMetadata.styles.ts b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureViewMetaData/FeatureViewMetadata.styles.ts
similarity index 52%
rename from frontend/src/component/feature/FeatureView2/FeatureViewMetaData/FeatureViewMetadata.styles.ts
rename to frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureViewMetaData/FeatureViewMetadata.styles.ts
index 25d5207ca2..bdf7d9e4a8 100644
--- a/frontend/src/component/feature/FeatureView2/FeatureViewMetaData/FeatureViewMetadata.styles.ts
+++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureViewMetaData/FeatureViewMetadata.styles.ts
@@ -15,8 +15,29 @@ export const useStyles = makeStyles(theme => ({
display: 'flex',
alignItems: 'center',
},
+ header: {
+ fontSize: theme.fontSizes.subHeader,
+ fontWeight: 'normal',
+ margin: 0,
+ },
+ body: {
+ margin: '1rem 0',
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ bodyItem: {
+ margin: '0.5rem 0',
+ fontSize: theme.fontSizes.bodySize,
+ },
headerIcon: {
marginRight: '1rem',
+ height: '40px',
+ width: '40px',
fill: theme.palette.primary.main,
},
+ descriptionContainer: {
+ display: 'flex',
+ alignItems: 'center',
+ color: theme.palette.grey[600],
+ },
}));
diff --git a/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesConfigure/FeatureStrategiesConfigure.tsx b/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesConfigure/FeatureStrategiesConfigure.tsx
index 004d07840e..3e06d0b0d4 100644
--- a/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesConfigure/FeatureStrategiesConfigure.tsx
+++ b/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesConfigure/FeatureStrategiesConfigure.tsx
@@ -2,7 +2,7 @@ import { Button, useMediaQuery } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import { useContext, useState } from 'react';
import { getHumanReadbleStrategyName } from '../../../../../../utils/strategy-names';
-import { useParams } from 'react-router-dom';
+import { useHistory, useParams } from 'react-router-dom';
import FeatureStrategiesUIContext from '../../../../../../contexts/FeatureStrategiesUIContext';
import ConditionallyRender from '../../../../../common/ConditionallyRender';
@@ -24,6 +24,7 @@ const FeatureStrategiesConfigure = ({
setToastData,
}: IFeatureStrategiesConfigure) => {
const smallScreen = useMediaQuery('(max-width:900px)');
+ const history = useHistory();
const { projectId, featureId } = useParams();
const [productionGuard, setProductionGuard] = useState(false);
@@ -90,6 +91,7 @@ const FeatureStrategiesConfigure = ({
type: 'success',
text: 'Successfully added strategy.',
});
+ history.replace(history.location.pathname);
} catch (e) {
setToastData({
show: true,
diff --git a/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesEnvironmentList/useFeatureStrategiesEnvironmentList.ts b/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesEnvironmentList/useFeatureStrategiesEnvironmentList.ts
index 13a02fe35d..f200ac77fd 100644
--- a/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesEnvironmentList/useFeatureStrategiesEnvironmentList.ts
+++ b/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesEnvironmentList/useFeatureStrategiesEnvironmentList.ts
@@ -1,5 +1,5 @@
import { useContext, useEffect, useRef, useState } from 'react';
-import { useParams } from 'react-router-dom';
+import { useHistory, useParams } from 'react-router-dom';
import FeatureStrategiesUIContext from '../../../../../../contexts/FeatureStrategiesUIContext';
import useFeatureStrategyApi from '../../../../../../hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
import useToast from '../../../../../../hooks/useToast';
@@ -11,7 +11,7 @@ const useFeatureStrategiesEnvironmentList = (
strategies: IFeatureStrategy[]
) => {
const { projectId, featureId } = useParams();
-
+ const history = useHistory();
const { deleteStrategyFromFeature, updateStrategyOnFeature } =
useFeatureStrategyApi();
@@ -71,7 +71,7 @@ const useFeatureStrategiesEnvironmentList = (
strategy.parameters = updateStrategyPayload.parameters;
strategy.constraints = updateStrategyPayload.constraints;
-
+ history.replace(history.location.pathname);
setFeatureCache(feature);
} catch (e) {
setToastData({
@@ -109,6 +109,7 @@ const useFeatureStrategiesEnvironmentList = (
type: 'success',
text: `Successfully deleted strategy from ${featureId}`,
});
+ history.replace(history.location.pathname);
} catch (e) {
setToastData({
show: true,
diff --git a/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesEnvironments.tsx b/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesEnvironments.tsx
index 4cb213451a..4d0f1f587a 100644
--- a/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesEnvironments.tsx
+++ b/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesEnvironments/FeatureStrategiesEnvironments.tsx
@@ -1,4 +1,4 @@
-import { useParams } from 'react-router-dom';
+import { useHistory, useParams } from 'react-router-dom';
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
import { useStyles } from './FeatureStrategiesEnvironments.styles';
import { Tabs, Tab, Button, useMediaQuery } from '@material-ui/core';
@@ -21,10 +21,12 @@ import ResponsiveButton from '../../../../common/ResponsiveButton/ResponsiveButt
import { Add } from '@material-ui/icons';
import AccessContext from '../../../../../contexts/AccessContext';
import { UPDATE_FEATURE } from '../../../../AccessProvider/permissions';
+import useQueryParams from '../../../../../hooks/useQueryParams';
const FeatureStrategiesEnvironments = () => {
const smallScreen = useMediaQuery('(max-width:700px)');
const { hasAccess } = useContext(AccessContext);
+ const history = useHistory();
const startingTabId = 0;
const { projectId, featureId } = useParams();
@@ -32,6 +34,10 @@ const FeatureStrategiesEnvironments = () => {
const [showRefreshPrompt, setShowRefreshPrompt] = useState(false);
const styles = useStyles();
+ const query = useQueryParams();
+ const addStrategy = query.get('addStrategy');
+ const environmentTab = query.get('environment');
+
const { a11yProps, activeTabIdx, setActiveTab } = useTabs(startingTabId);
const {
setActiveEnvironment,
@@ -49,6 +55,28 @@ const FeatureStrategiesEnvironments = () => {
refreshInterval: 5000,
});
+ useEffect(() => {
+ if (addStrategy) {
+ setExpandedSidebar(true);
+ }
+
+ if (environmentTab) {
+ const env = feature.environments.find(
+ env => env.name === environmentTab
+ );
+ const index = feature.environments.findIndex(
+ env => env.name === environmentTab
+ );
+ if (index < 0 || !env) return;
+ setActiveEnvironment(env);
+ setActiveTab(index);
+ return;
+ }
+
+ setActiveEnvironment(feature?.environments[activeTabIdx]);
+ /*eslint-disable-next-line */
+ }, [feature]);
+
useEffect(() => {
if (!feature) return;
if (featureCache === null || !featureCache.createdAt) {
@@ -70,12 +98,6 @@ const FeatureStrategiesEnvironments = () => {
/*eslint-disable-next-line */
}, [feature]);
- useEffect(() => {
- if (!feature?.environments?.length > 0) return;
- setActiveEnvironment(feature?.environments[activeTabIdx]);
- /* eslint-disable-next-line */
- }, [feature]);
-
const renderTabs = () => {
return featureCache?.environments?.map((env, index) => {
return (
@@ -363,6 +385,7 @@ const FeatureStrategiesEnvironments = () => {
setActiveEnvironment(
featureCache?.environments[tabId]
);
+ history.replace(history.location.pathname);
}}
indicatorColor="primary"
textColor="primary"
diff --git a/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesList/FeatureStrategyCard/FeatureStrategyCard.styles.ts b/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesList/FeatureStrategyCard/FeatureStrategyCard.styles.ts
index f61fd80e31..4871f88cd8 100644
--- a/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesList/FeatureStrategyCard/FeatureStrategyCard.styles.ts
+++ b/frontend/src/component/feature/FeatureView2/FeatureStrategies/FeatureStrategiesList/FeatureStrategyCard/FeatureStrategyCard.styles.ts
@@ -9,6 +9,7 @@ export const useStyles = makeStyles(theme => ({
margin: '0.5rem 0',
display: 'flex',
position: 'relative',
+ cursor: 'pointer',
width: '100%',
'&:active': {
backgroundColor: theme.palette.primary.main,
diff --git a/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariants.styles.ts b/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariants.styles.ts
new file mode 100644
index 0000000000..10dae26fef
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariants.styles.ts
@@ -0,0 +1,9 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ container: {
+ borderRadius: '12.5px',
+ backgroundColor: '#fff',
+ padding: '2rem',
+ },
+}));
diff --git a/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariants.tsx b/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariants.tsx
new file mode 100644
index 0000000000..df223addc7
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView2/FeatureVariants/FeatureVariants.tsx
@@ -0,0 +1,20 @@
+import { useStyles } from './FeatureVariants.styles';
+import { useHistory, useParams } from 'react-router';
+import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
+import { IFeatureViewParams } from '../../../../interfaces/params';
+import EditVariants from '../../variant/update-variant-container';
+
+const FeatureVariants = () => {
+ const styles = useStyles();
+ const { projectId, featureId } = useParams();
+ const { feature } = useFeature(projectId, featureId);
+ const history = useHistory();
+
+ return (
+
+
+
+ );
+};
+
+export default FeatureVariants;
diff --git a/frontend/src/component/feature/FeatureView2/FeatureView2.tsx b/frontend/src/component/feature/FeatureView2/FeatureView2.tsx
index 4f7427d75e..c069384399 100644
--- a/frontend/src/component/feature/FeatureView2/FeatureView2.tsx
+++ b/frontend/src/component/feature/FeatureView2/FeatureView2.tsx
@@ -1,64 +1,46 @@
import { Tabs, Tab } from '@material-ui/core';
-import { useEffect } from 'react';
-import { useHistory, useParams } from 'react-router-dom';
+import { Route, useHistory, useParams } from 'react-router-dom';
import useFeature from '../../../hooks/api/getters/useFeature/useFeature';
import useTabs from '../../../hooks/useTabs';
import { IFeatureViewParams } from '../../../interfaces/params';
-import TabPanel from '../../common/TabNav/TabPanel';
+import FeatureLog from './FeatureLog/FeatureLog';
+import FeatureMetrics from './FeatureMetrics/FeatureMetrics';
+import FeatureOverview from './FeatureOverview/FeatureOverview';
import FeatureStrategies from './FeatureStrategies/FeatureStrategies';
+import FeatureVariants from './FeatureVariants/FeatureVariants';
import { useStyles } from './FeatureView2.styles';
-import FeatureViewEnvironment from './FeatureViewEnvironment/FeatureViewEnvironment';
-import FeatureViewMetaData from './FeatureViewMetaData/FeatureViewMetaData';
const FeatureView2 = () => {
- const { projectId, featureId, activeTab } = useParams();
+ const { projectId, featureId } = useParams();
const { feature } = useFeature(projectId, featureId);
- const { a11yProps, activeTabIdx, setActiveTab } = useTabs(0);
+ const { a11yProps } = useTabs(0);
const styles = useStyles();
const history = useHistory();
const basePath = `/projects/${projectId}/features2/${featureId}`;
- useEffect(() => {
- const tabIdx = tabData.findIndex(tab => tab.name === activeTab);
- setActiveTab(tabIdx);
- /* eslint-disable-next-line */
- }, []);
-
- const renderOverview = () => {
- return (
-
-
-
- {feature?.environments?.map(env => {
- return (
-
- );
- })}
-
-
- );
- };
-
const tabData = [
{
title: 'Overview',
- component: renderOverview(),
path: `${basePath}/overview`,
name: 'overview',
},
{
title: 'Strategies',
- component: ,
path: `${basePath}/strategies`,
name: 'strategies',
},
+ {
+ title: 'Metrics',
+ path: `${basePath}/metrics`,
+ name: 'Metrics',
+ },
+ {
+ title: 'Event log',
+ path: `${basePath}/logs`,
+ name: 'Event log',
+ },
+ { title: 'Variants', path: `${basePath}/variants`, name: 'Variants' },
];
const renderTabs = () => {
@@ -67,9 +49,9 @@ const FeatureView2 = () => {
{
- setActiveTab(index);
history.push(tab.path);
}}
className={styles.tabButton}
@@ -78,16 +60,6 @@ const FeatureView2 = () => {
});
};
- const renderTabContent = () => {
- return tabData.map((tab, index) => {
- return (
-
- {tab.component}
-
- );
- });
- };
-
return (
<>
@@ -97,10 +69,7 @@ const FeatureView2 = () => {
{
- setActiveTab(tabId);
- }}
+ value={history.location.pathname}
indicatorColor="primary"
textColor="primary"
className={styles.tabNavigation}
@@ -109,7 +78,26 @@ const FeatureView2 = () => {
- {renderTabContent()}
+
+
+
+
+
>
);
};
diff --git a/frontend/src/component/feature/FeatureView2/FeatureViewMetaData/FeatureViewMetaData.tsx b/frontend/src/component/feature/FeatureView2/FeatureViewMetaData/FeatureViewMetaData.tsx
deleted file mode 100644
index db8ed445e0..0000000000
--- a/frontend/src/component/feature/FeatureView2/FeatureViewMetaData/FeatureViewMetaData.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { capitalize, IconButton } from '@material-ui/core';
-import classnames from 'classnames';
-import { useParams } from 'react-router-dom';
-import { useCommonStyles } from '../../../../common.styles';
-import useFeature from '../../../../hooks/api/getters/useFeature/useFeature';
-import { getFeatureTypeIcons } from '../../../../utils/get-feature-type-icons';
-import ConditionallyRender from '../../../common/ConditionallyRender';
-import { useStyles } from './FeatureViewMetadata.styles';
-
-import { Edit } from '@material-ui/icons';
-
-const FeatureViewMetaData = () => {
- const styles = useStyles();
- const commonStyles = useCommonStyles();
- const { projectId, featureId } = useParams();
-
- const { feature } = useFeature(projectId, featureId);
-
- const { project, description, type } = feature;
-
- const IconComponent = getFeatureTypeIcons(type);
-
- return (
-
-
- {' '}
- {capitalize(type || '')} toggle
-
- Project: {project}
-
- Description: {description}{' '}
-
-
-
-
- }
- elseShow={
-
- No description.{' '}
-
-
-
-
- }
- />
-
- );
-};
-
-export default FeatureViewMetaData;
diff --git a/frontend/src/component/feature/variant/update-variant-container.jsx b/frontend/src/component/feature/variant/update-variant-container.jsx
index 2ccd749a3f..2116cca026 100644
--- a/frontend/src/component/feature/variant/update-variant-container.jsx
+++ b/frontend/src/component/feature/variant/update-variant-container.jsx
@@ -6,6 +6,7 @@ import { updateWeight } from '../../common/util';
const mapStateToProps = (state, ownProps) => ({
variants: ownProps.featureToggle.variants || [],
+ features: state.features.toJS(),
stickinessOptions: [
'default',
...state.context.filter(c => c.stickiness).map(c => c.name),
@@ -20,7 +21,7 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
if (currentVariants.length > 0) {
stickiness = currentVariants[0].stickiness || 'default';
} else {
- stickiness = 'default'
+ stickiness = 'default';
}
variant.stickiness = stickiness;
diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap
index eceef9ce63..ea996a5066 100644
--- a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap
+++ b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap
@@ -190,7 +190,7 @@ Array [
"layout": "main",
"menu": Object {},
"parent": "/projects",
- "path": "/projects/:projectId/features2/:featureId/:activeTab",
+ "path": "/projects/:projectId/features2/:featureId",
"title": "FeatureView2",
"type": "protected",
},
diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js
index 8b09a0dd97..3d9fbbcb87 100644
--- a/frontend/src/component/menu/routes.js
+++ b/frontend/src/component/menu/routes.js
@@ -222,7 +222,7 @@ export const routes = [
menu: {},
},
{
- path: '/projects/:projectId/features2/:featureId/:activeTab',
+ path: '/projects/:projectId/features2/:featureId',
parent: '/projects',
title: 'FeatureView2',
component: FeatureView2,
diff --git a/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts b/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts
index 3d5f61b1da..f76c95ca3e 100644
--- a/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts
+++ b/frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts
@@ -1,3 +1,4 @@
+import { ITag } from '../../../../interfaces/tags';
import useAPI from '../useApi/useApi';
const useFeatureApi = () => {
@@ -5,12 +6,54 @@ const useFeatureApi = () => {
propagateErrors: true,
});
+ const toggleFeatureEnvironmentOn = async (
+ projectId: string,
+ featureId: string,
+ environmentId: string
+ ) => {
+ const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/on`;
+ const req = createRequest(
+ path,
+ { method: 'POST' },
+ 'toggleFeatureEnvironmentOn'
+ );
+
+ try {
+ const res = await makeRequest(req.caller, req.id);
+
+ return res;
+ } catch (e) {
+ throw e;
+ }
+ };
+
+ const toggleFeatureEnvironmentOff = async (
+ projectId: string,
+ featureId: string,
+ environmentId: string
+ ) => {
+ const path = `api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/off`;
+ const req = createRequest(
+ path,
+ { method: 'POST' },
+ 'toggleFeatureEnvironmentOff'
+ );
+
+ try {
+ const res = await makeRequest(req.caller, req.id);
+
+ return res;
+ } catch (e) {
+ throw e;
+ }
+ };
+
const changeFeatureProject = async (
projectId: string,
- featureName: string,
+ featureId: string,
newProjectId: string
) => {
- const path = `api/admin/projects/${projectId}/features/${featureName}/changeProject`;
+ const path = `api/admin/projects/${projectId}/features/${featureId}/changeProject`;
const req = createRequest(path, {
method: 'POST',
body: JSON.stringify({ newProjectId }),
@@ -25,7 +68,51 @@ const useFeatureApi = () => {
}
};
- return { changeFeatureProject, errors };
+ const addTag = async (featureId: string, tag: ITag) => {
+ // TODO: Change this path to the new API when moved.
+ const path = `api/admin/features/${featureId}/tags`;
+ const req = createRequest(path, {
+ method: 'POST',
+ body: JSON.stringify({ ...tag }),
+ });
+
+ try {
+ const res = await makeRequest(req.caller, req.id);
+
+ return res;
+ } catch (e) {
+ throw e;
+ }
+ };
+
+ const deleteTag = async (
+ featureId: string,
+ type: string,
+ value: string
+ ) => {
+ // TODO: Change this path to the new API when moved.
+ const path = `api/admin/features/${featureId}/tags/${type}/${value}`;
+ const req = createRequest(path, {
+ method: 'DELETE',
+ });
+
+ try {
+ const res = await makeRequest(req.caller, req.id);
+
+ return res;
+ } catch (e) {
+ throw e;
+ }
+ };
+
+ return {
+ changeFeatureProject,
+ errors,
+ toggleFeatureEnvironmentOn,
+ toggleFeatureEnvironmentOff,
+ addTag,
+ deleteTag,
+ };
};
export default useFeatureApi;
diff --git a/frontend/src/hooks/api/getters/useFeature/useFeature.ts b/frontend/src/hooks/api/getters/useFeature/useFeature.ts
index e65e812b8c..de83e101f8 100644
--- a/frontend/src/hooks/api/getters/useFeature/useFeature.ts
+++ b/frontend/src/hooks/api/getters/useFeature/useFeature.ts
@@ -15,7 +15,7 @@ interface IUseFeatureOptions {
const useFeature = (
projectId: string,
id: string,
- options: IUseFeatureOptions
+ options: IUseFeatureOptions = {}
) => {
const fetcher = () => {
const path = formatApiPath(
diff --git a/frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts b/frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts
new file mode 100644
index 0000000000..4dd0380807
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts
@@ -0,0 +1,36 @@
+import useSWR, { mutate } from 'swr';
+import { useState, useEffect } from 'react';
+import { formatApiPath } from '../../../../utils/format-path';
+import { ITagType } from '../../../../interfaces/tags';
+
+const useTagTypes = () => {
+ const fetcher = async () => {
+ const path = formatApiPath(`api/admin/tag-types`);
+ const res = await fetch(path, {
+ method: 'GET',
+ });
+ return res.json();
+ };
+
+ const KEY = `api/admin/tag-types`;
+
+ const { data, error } = useSWR(KEY, fetcher);
+ const [loading, setLoading] = useState(!error && !data);
+
+ const refetch = () => {
+ mutate(KEY);
+ };
+
+ useEffect(() => {
+ setLoading(!error && !data);
+ }, [data, error]);
+
+ return {
+ tagTypes: (data?.tagTypes as ITagType[]) || [],
+ error,
+ loading,
+ refetch,
+ };
+};
+
+export default useTagTypes;
diff --git a/frontend/src/hooks/api/getters/useTags/useTags.ts b/frontend/src/hooks/api/getters/useTags/useTags.ts
new file mode 100644
index 0000000000..5e31443b3b
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useTags/useTags.ts
@@ -0,0 +1,36 @@
+import useSWR, { mutate } from 'swr';
+import { useState, useEffect } from 'react';
+import { formatApiPath } from '../../../../utils/format-path';
+import { ITag } from '../../../../interfaces/tags';
+
+const useTags = (featureId: string) => {
+ const fetcher = async () => {
+ const path = formatApiPath(`api/admin/features/${featureId}/tags`);
+ const res = await fetch(path, {
+ method: 'GET',
+ });
+ return res.json();
+ };
+
+ const KEY = `api/admin/features/${featureId}/tags`;
+
+ const { data, error } = useSWR(KEY, fetcher);
+ const [loading, setLoading] = useState(!error && !data);
+
+ const refetch = () => {
+ mutate(KEY);
+ };
+
+ useEffect(() => {
+ setLoading(!error && !data);
+ }, [data, error]);
+
+ return {
+ tags: (data?.tags as ITag[]) || [],
+ error,
+ loading,
+ refetch,
+ };
+};
+
+export default useTags;
diff --git a/frontend/src/interfaces/tags.ts b/frontend/src/interfaces/tags.ts
new file mode 100644
index 0000000000..ee8ea2982c
--- /dev/null
+++ b/frontend/src/interfaces/tags.ts
@@ -0,0 +1,10 @@
+export interface ITag {
+ value: string;
+ type: string;
+}
+
+export interface ITagType {
+ name: string;
+ description: string;
+ icon: string;
+}