diff --git a/frontend/common/services/useHealthProvider.ts b/frontend/common/services/useHealthProvider.ts index e0df2f34f240..9160bcb5420d 100644 --- a/frontend/common/services/useHealthProvider.ts +++ b/frontend/common/services/useHealthProvider.ts @@ -22,7 +22,7 @@ export const healthProviderService = service invalidatesTags: [{ id: 'LIST', type: 'HealthProviders' }], query: (query: Req['deleteHealthProvider']) => ({ method: 'DELETE', - url: `projects/${query.projectId}/feature-health/providers/${query.providerId}/`, + url: `projects/${query.projectId}/feature-health/providers/${query.name}/`, }), }, ), diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index 6e9d7d85cfc2..ab22c7ac00ee 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -117,7 +117,7 @@ export type Req = { getHealthEvents: { projectId: number | string } getHealthProviders: { projectId: number } createHealthProvider: { projectId: number; name: string } - deleteHealthProvider: { projectId: number; providerId: number } + deleteHealthProvider: { projectId: number; name: string } updateTag: { projectId: string; tag: Tag } deleteTag: { id: number diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index dfedc50e2a32..ea8c3d03c246 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -344,6 +344,8 @@ export type APIKey = { name: string } +export type TagType = 'STALE' | 'UNHEALTHY' | 'NONE' + export type Tag = { id: number color: string @@ -352,7 +354,7 @@ export type Tag = { label: string is_system_tag: boolean is_permanent: boolean - type: 'STALE' | 'UNHEALTHY' | 'NONE' + type: TagType } export type MultivariateFeatureStateValue = { @@ -637,13 +639,15 @@ export type SAMLAttributeMapping = { idp_attribute_name: string } +export type HealthEventType = 'HEALTHY' | 'UNHEALTHY' + export type HealthEvent = { created_at: string environment: number feature: number provider_name: string reason: string - type: 'HEALTHY' | 'UNHEALTHY' + type: HealthEventType } export type HealthProvider = { diff --git a/frontend/web/components/EditHealthProvider.tsx b/frontend/web/components/EditHealthProvider.tsx index 4cd3d3a21a76..513a3e11b278 100644 --- a/frontend/web/components/EditHealthProvider.tsx +++ b/frontend/web/components/EditHealthProvider.tsx @@ -218,8 +218,7 @@ const EditHealthProvider: FC = ({ onClick={(e) => { e.stopPropagation() e.preventDefault() - // TODO: API Needs to expose provider id - // deleteProvider({ projectId, providerId: provider.id }) + deleteProvider({ name, projectId }) }} className='btn btn-with-icon' > diff --git a/frontend/web/components/FeatureRow.js b/frontend/web/components/FeatureRow.js index e7824bf67420..53fb6f4da3e7 100644 --- a/frontend/web/components/FeatureRow.js +++ b/frontend/web/components/FeatureRow.js @@ -17,8 +17,6 @@ import SegmentOverridesIcon from './SegmentOverridesIcon' import IdentityOverridesIcon from './IdentityOverridesIcon' import StaleFlagWarning from './StaleFlagWarning' import UnhealthyFlagWarning from './UnhealthyFlagWarning' -import { getTags } from 'common/services/useTag' -import { getStore } from 'common/store' export const width = [200, 70, 55, 70, 450] @@ -43,14 +41,6 @@ class TheComponent extends Component { this.state = { unhealthyTagId: undefined, } - - getTags(getStore(), { - projectId: `${this.props.projectId}`, - }).then((res) => { - this.setState({ - unhealthyTagId: res.data?.find((tag) => tag?.type === 'UNHEALTHY')?.id, - }) - }) } confirmToggle = () => { @@ -83,7 +73,7 @@ class TheComponent extends Component { componentDidMount() { const { environmentFlags, projectFlag } = this.props - const { feature, tab } = Utils.fromParam() + const { feature } = Utils.fromParam() const { id } = projectFlag if (`${id}` === feature) { this.editFeature(projectFlag, environmentFlags[id]) @@ -113,9 +103,6 @@ class TheComponent extends Component { return } API.trackEvent(Constants.events.VIEW_FEATURE) - const hideTags = this.state.unhealthyTagId - ? [this.state.unhealthyTagId] - : [] history.replaceState( {}, null, @@ -137,7 +124,7 @@ class TheComponent extends Component { , = ({ return null return ( -
- {/* TODO: Provider info and link to issue will be provided by reason via the API */} - {latestHealthEvent.reason} - {latestHealthEvent.reason && ( - - )} -
+ + {/* TODO: Provider info and link to issue will be provided by reason via the API */} + {latestHealthEvent.reason} + {latestHealthEvent.reason && ( + + )} + + } + > + This feature is tagged as unhealthy in one or more environments. + ) } diff --git a/frontend/web/components/modals/CreateFlag.js b/frontend/web/components/modals/CreateFlag.js index 6f9d25294d61..b3ea53f7c28e 100644 --- a/frontend/web/components/modals/CreateFlag.js +++ b/frontend/web/components/modals/CreateFlag.js @@ -72,7 +72,7 @@ const CreateFlag = class extends Component { multivariate_options: [], } const { allowEditDescription } = this.props - const hideTags = this.props.hideTags || [] + const hideTagsByType = this.props.hideTagsByType || [] if (this.props.projectFlag) { this.userOverridesPage(1) } @@ -107,7 +107,7 @@ const CreateFlag = class extends Component { name, period: 30, selectedIdentity: null, - tags: tags?.filter((tag) => !hideTags.includes(tag)) || [], + tags: tags?.filter((tag) => hideTagsByType.includes(tag.type)) || [], } } @@ -604,6 +604,7 @@ const CreateFlag = class extends Component { tooltip={Constants.strings.TAGS_DESCRIPTION} component={ -type EnvSelectOptionProps = OptionProps & { +type CustomOptionProps = ComponentProps & { hasWarning?: boolean } -const EnvSelectOption = ({ hasWarning, ...rest }: EnvSelectOptionProps) => { +type CustomSingleValueProps = ComponentProps & { + hasWarning?: boolean +} + +const TooltipWrapper = ({ + showWarning, + title, +}: { + title: React.ReactElement + showWarning: boolean +}) => { + return showWarning ? ( + + One or more environments have unhealthy features + + ) : ( + title + ) +} + +const CustomOption = ({ hasWarning, ...rest }: CustomOptionProps) => { + const showWarning = + Utils.getFlagsmithHasFeature('feature_health') && hasWarning return (
{rest.children}
- {Utils.getFlagsmithHasFeature('feature_health') && hasWarning && ( + {showWarning && ( @@ -63,6 +83,25 @@ const EnvSelectOption = ({ hasWarning, ...rest }: EnvSelectOptionProps) => { ) } +const CustomSingleValue = ({ hasWarning, ...rest }: CustomSingleValueProps) => { + const showWarning = + Utils.getFlagsmithHasFeature('feature_health') && hasWarning + return ( + +
+
{rest.children}
+
+ {showWarning && ( +
+ +
+ )} +
+
+
+ ) +} + const HomeAside: FC = ({ environmentId, history, @@ -89,19 +128,20 @@ const HomeAside: FC = ({ //eslint-disable-next-line }, []) - const unhealthyEnvironments = useMemo(() => { - return healthEvents - ?.filter((event) => event.type === 'UNHEALTHY') - .map( - (event) => - ( - ProjectStore.getEnvironmentById( - event.environment, - ) as Environment | null - )?.api_key, - ) - }, [healthEvents]) + const unhealthyEnvironments = healthEvents + ?.filter((event) => event?.type === 'UNHEALTHY' && !!event?.environment) + .map( + (event) => + ( + ProjectStore.getEnvironmentById( + event.environment, + ) as Environment | null + )?.api_key, + ) + const hasUnhealthyEnvironments = + Utils.getFlagsmithHasFeature('feature_health') && + !!unhealthyEnvironments?.length const environment: Environment | null = environmentId === 'create' ? null @@ -158,7 +198,10 @@ const HomeAside: FC = ({ styles={{ container: (base: any) => ({ ...base, - border: 'none', + border: hasUnhealthyEnvironments + ? '1px solid #D35400' + : 'none', + borderRadius: 6, padding: 0, }), }} @@ -166,16 +209,28 @@ const HomeAside: FC = ({ value={environmentId} projectId={projectId} components={{ - Menu: ({ ...props }: any) => { - return ( - - {props.children} - {createEnvironmentButton} - - ) - }, - Option: (props: OptionProps) => ( - ( + } + /> + ), + Menu: ({ ...props }: any) => ( + + {props.children} + {createEnvironmentButton} + + ), + Option: (props) => ( + + ), + SingleValue: (props) => ( + = ({ }) => { const [filter, setFilter] = useState('') const { data } = useGetTagsQuery({ projectId }) + + const isFeatureHealthEnabled = Utils.getFlagsmithHasFeature('feature_health') + const flagGatedTags = useMemo(() => { + if (!isFeatureHealthEnabled) + return data?.filter((tag) => tag.type !== 'UNHEALTHY') + + return data + }, [data, isFeatureHealthEnabled]) + const filteredTags = useMemo(() => { return filter - ? data?.filter((v) => v.label.toLowerCase().includes(filter)) - : data - }, [data, filter]) + ? flagGatedTags?.filter((v) => v.label.toLowerCase().includes(filter)) + : flagGatedTags?.filter((tag) => tag) + }, [flagGatedTags, filter]) const length = (value?.length || 0) + (showArchived ? 1 : 0) return (
diff --git a/frontend/web/components/tags/AddEditTags.tsx b/frontend/web/components/tags/AddEditTags.tsx index fbbd3dbf9e7c..24868d3a8e97 100644 --- a/frontend/web/components/tags/AddEditTags.tsx +++ b/frontend/web/components/tags/AddEditTags.tsx @@ -10,7 +10,7 @@ import { useDeleteTagMutation, useGetTagsQuery, } from 'common/services/useTag' -import { Tag as TTag } from 'common/types/responses' +import { TagType, Tag as TTag } from 'common/types/responses' import Tag from './Tag' import CreateEditTag from './CreateEditTag' import Input from 'components/base/forms/Input' @@ -20,20 +20,28 @@ import TagUsage from 'components/TagUsage' type AddEditTagsType = { value?: number[] + hideTagsByType?: TagType[] readOnly?: boolean onChange: (value: number[]) => void projectId: string } const AddEditTags: FC = ({ + hideTagsByType = [], onChange, projectId, readOnly, value, }) => { - const { data: projectTags, isLoading: tagsLoading } = useGetTagsQuery({ + const { data, isLoading: tagsLoading } = useGetTagsQuery({ projectId, }) + const projectTags = useMemo(() => { + return data?.filter( + (projectTag) => !hideTagsByType.includes(projectTag.type), + ) + }, [data, hideTagsByType]) + const [filter, setFilter] = useState('') const [isOpen, setIsOpen] = useState(false) const [tag, setTag] = useState() @@ -103,6 +111,7 @@ const AddEditTags: FC = ({ tag.label.toLowerCase().includes(filter), ) } + return projectTags || [] }, [filter, projectTags]) diff --git a/frontend/web/components/tags/Tag.tsx b/frontend/web/components/tags/Tag.tsx index 595d4b63441a..365b313ea94a 100644 --- a/frontend/web/components/tags/Tag.tsx +++ b/frontend/web/components/tags/Tag.tsx @@ -66,6 +66,14 @@ const Tag: FC = ({ ) } + // Hide unhealthy tags if feature is disabled + if ( + !Utils.getFlagsmithHasFeature('feature_health') && + tag.type === 'UNHEALTHY' + ) { + return null + } + return (
{ diff --git a/frontend/web/components/tags/TagContent.tsx b/frontend/web/components/tags/TagContent.tsx index c8668e8e0c77..dd3e6170d21c 100644 --- a/frontend/web/components/tags/TagContent.tsx +++ b/frontend/web/components/tags/TagContent.tsx @@ -64,7 +64,6 @@ const getTooltip = (tag: TTag | undefined) => { const disabled = Utils.tagDisabled(tag) const truncated = Format.truncateText(tag.label, 12) const isTruncated = truncated !== tag.label ? tag.label : null - const isFeatureHealthEnabled = Utils.getFlagsmithHasFeature('feature_health') let tooltip = null switch (tag.type) { case 'STALE': { @@ -75,12 +74,6 @@ const getTooltip = (tag: TTag | undefined) => { }A feature is marked as stale if no changes have been made to it in any environment within ${stale_flags_limit_days} days. This is automatically applied and will be re-evaluated if you remove this tag unless you apply a permanent tag to the feature.` break } - case 'UNHEALTHY': { - tooltip = isFeatureHealthEnabled - ? 'This feature is tagged as unhealthy in one or more environments.' - : '' - break - } default: break }