diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx
index a11ed40a90..c549bc62ca 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx
@@ -1,4 +1,4 @@
-import { Typography } from '@mui/material';
+import { styled, Typography } from '@mui/material';
import React, { useState } from 'react';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import Input from 'component/common/Input/Input';
@@ -10,6 +10,11 @@ import useTags from 'hooks/api/getters/useTags/useTags';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
+import { ITag } from 'interfaces/tags';
+
+const StyledInput = styled(Input)(() => ({
+ width: '100%',
+}));
interface IAddTagDialogProps {
open: boolean;
@@ -28,7 +33,7 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
const { classes: styles } = useStyles();
const featureId = useRequiredPathParam('featureId');
const { addTagToFeature, loading } = useFeatureApi();
- const { refetch } = useTags(featureId);
+ const { tags, refetch } = useTags(featureId);
const [errors, setErrors] = useState({ tagError: '' });
const { setToastData } = useToast();
const [tag, setTag] = useState(DEFAULT_TAG);
@@ -39,12 +44,6 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
setTag(DEFAULT_TAG);
};
- const setValue = (field: string, value: string) => {
- const newTag = { ...tag };
- newTag[field] = trim(value);
- setTag(newTag);
- };
-
const onSubmit = async (evt: React.SyntheticEvent) => {
evt.preventDefault();
if (!tag.type) {
@@ -68,6 +67,26 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
}
};
+ const isValueNotEmpty = (name: string) => name.length;
+ const isTagUnique = (tag: ITag) =>
+ !tags.some(
+ ({ type, value }) => type === tag.type && value === tag.value
+ );
+ const isValid = isValueNotEmpty(tag.value) && isTagUnique(tag);
+
+ const onUpdateTag = (key: string, value: string) => {
+ setErrors({ tagError: '' });
+ const updatedTag = { ...tag, [key]: trim(value) };
+
+ if (!isTagUnique(updatedTag)) {
+ setErrors({
+ tagError: 'Tag already exists for this feature toggle.',
+ });
+ }
+
+ setTag(updatedTag);
+ };
+
const formId = 'add-tag-form';
return (
@@ -78,7 +97,7 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
primaryButtonText="Add tag"
title="Add tags to feature toggle"
onClick={onSubmit}
- disabledPrimaryButton={loading}
+ disabledPrimaryButton={loading || !isValid}
onClose={onCancel}
formId={formId}
>
@@ -92,10 +111,10 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
autoFocus
name="type"
value={tag.type}
- onChange={type => setValue('type', type)}
+ onChange={type => onUpdateTag('type', type)}
/>
- {
error={Boolean(errors.tagError)}
errorText={errors.tagError}
onChange={e =>
- setValue('value', e.target.value)
+ onUpdateTag('value', e.target.value)
}
+ required
/>
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx
index 5ecbc84cc4..7729621819 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx
@@ -7,6 +7,8 @@ import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSi
import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags';
const StyledContainer = styled('div')(({ theme }) => ({
+ position: 'sticky',
+ top: theme.spacing(2),
borderRadius: theme.shape.borderRadiusLarge,
backgroundColor: theme.palette.background.paper,
display: 'flex',
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx
index 0b6e3e3f97..8ba0435d6a 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx
@@ -20,25 +20,27 @@ const StyledContainer = styled('div')(({ theme }) => ({
},
}));
-const StyledLabel = styled('label')(({ theme }) => ({
+const StyledLabel = styled('label')(() => ({
display: 'inline-flex',
alignItems: 'center',
cursor: 'pointer',
}));
interface IFeatureOverviewSidePanelEnvironmentSwitchProps {
- env: IFeatureEnvironment;
+ environment: IFeatureEnvironment;
callback?: () => void;
showInfoBox: () => void;
children?: React.ReactNode;
}
export const FeatureOverviewSidePanelEnvironmentSwitch = ({
- env,
+ environment,
callback,
showInfoBox,
children,
}: IFeatureOverviewSidePanelEnvironmentSwitchProps) => {
+ const { name, enabled } = environment;
+
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
@@ -55,11 +57,11 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
const handleToggleEnvironmentOn = async () => {
try {
- await toggleFeatureEnvironmentOn(projectId, featureId, env.name);
+ await toggleFeatureEnvironmentOn(projectId, featureId, name);
setToastData({
type: 'success',
- title: `Available in ${env.name}`,
- text: `${featureId} is now available in ${env.name} based on its defined strategies.`,
+ title: `Available in ${name}`,
+ text: `${featureId} is now available in ${name} based on its defined strategies.`,
});
refetchFeature();
if (callback) {
@@ -79,11 +81,11 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
const handleToggleEnvironmentOff = async () => {
try {
- await toggleFeatureEnvironmentOff(projectId, featureId, env.name);
+ await toggleFeatureEnvironmentOff(projectId, featureId, name);
setToastData({
type: 'success',
- title: `Unavailable in ${env.name}`,
- text: `${featureId} is unavailable in ${env.name} and its strategies will no longer have any effect.`,
+ title: `Unavailable in ${name}`,
+ text: `${featureId} is unavailable in ${name} and its strategies will no longer have any effect.`,
});
refetchFeature();
if (callback) {
@@ -95,12 +97,12 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
};
const toggleEnvironment = async (e: React.ChangeEvent) => {
- if (isChangeRequestConfigured(env.name)) {
+ if (isChangeRequestConfigured(name)) {
e.preventDefault();
- onChangeRequestToggle(featureId, env.name, !env.enabled);
+ onChangeRequestToggle(featureId, name, !enabled);
return;
}
- if (env.enabled) {
+ if (enabled) {
await handleToggleEnvironmentOff();
return;
}
@@ -110,9 +112,9 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
const defaultContent = (
<>
{' '}
- {env.enabled ? 'enabled' : 'disabled'} in
+ {enabled ? 'enabled' : 'disabled'} in
-
+
>
);
@@ -120,11 +122,16 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
{children ?? defaultContent}
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx
index b2531aa6fa..d08fcc7cc3 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches.tsx
@@ -2,7 +2,7 @@ import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDia
import { IFeatureToggle } from 'interfaces/featureToggle';
import { useState } from 'react';
import { FeatureOverviewSidePanelEnvironmentSwitch } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch';
-import { Link, styled } from '@mui/material';
+import { Link, styled, Tooltip } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
const StyledContainer = styled('div')(({ theme }) => ({
@@ -55,22 +55,24 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
const variantsLink = variants.length > 0 && (
<>
{' - '}
-
- {variants.length === 1
- ? '1 variant'
- : `${variants.length} variants`}
-
+
+
+ {variants.length === 1
+ ? '1 variant'
+ : `${variants.length} variants`}
+
+
>
);
return (
{
setEnvironmentName(environment.name);
setShowInfoBox(true);