Skip to content

Commit

Permalink
feat: Adds unhealthy events reason (#5157)
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagoapolo authored and khvn26 committed Feb 26, 2025
1 parent 96ae05d commit 5521b45
Show file tree
Hide file tree
Showing 12 changed files with 345 additions and 55 deletions.
4 changes: 4 additions & 0 deletions frontend/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,12 @@ const Constants = {
},
"event_type": "FLAG_UPDATED"
}`,
featureHealth: {
unhealthyColor: '#D35400',
},
featurePanelTabs: {
ANALYTICS: 'analytics',
FEATURE_HEALTH: 'feature-health',
HISTORY: 'history',
IDENTITY_OVERRIDES: 'identity-overrides',
LINKS: 'links',
Expand Down
17 changes: 16 additions & 1 deletion frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,12 +639,27 @@ export type SAMLAttributeMapping = {

export type HealthEventType = 'HEALTHY' | 'UNHEALTHY'

export type FeatureHealthEventReasonTextBlock = {
text: string
title?: string
}

export type FeatureHealthEventReasonUrlBlock = {
url: string
title?: string
}

export type HealthEventReason = {
text_blocks: FeatureHealthEventReasonTextBlock[]
url_blocks: FeatureHealthEventReasonUrlBlock[]
}

export type HealthEvent = {
created_at: string
environment: number
feature: number
provider_name: string
reason: string
reason: HealthEventReason | null
type: HealthEventType
}

Expand Down
12 changes: 11 additions & 1 deletion frontend/web/components/CondensedFeatureRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import FeatureValue from './FeatureValue'
import SegmentOverridesIcon from './SegmentOverridesIcon'
import IdentityOverridesIcon from './IdentityOverridesIcon'
import Constants from 'common/constants'
import Utils from 'common/utils/utils'

export interface CondensedFeatureRowProps {
disableControls?: boolean
Expand All @@ -27,6 +28,7 @@ export interface CondensedFeatureRowProps {
isCompact?: boolean
fadeEnabled?: boolean
fadeValue?: boolean
hasUnhealthyEvents?: boolean
index: number
}

Expand All @@ -37,6 +39,7 @@ const CondensedFeatureRow: React.FC<CondensedFeatureRowProps> = ({
environmentFlags,
fadeEnabled,
fadeValue,
hasUnhealthyEvents,
index,
isCompact,
onChange,
Expand All @@ -53,7 +56,14 @@ const CondensedFeatureRow: React.FC<CondensedFeatureRowProps> = ({
<Flex
onClick={() => {
if (disableControls) return
!readOnly && editFeature(projectFlag, environmentFlags?.[id])
!readOnly &&
editFeature(
projectFlag,
environmentFlags?.[id],
hasUnhealthyEvents
? Constants.featurePanelTabs.FEATURE_HEALTH
: undefined,
)
}}
style={{ ...style }}
className={classNames('flex-row', { 'fs-small': isCompact }, className)}
Expand Down
49 changes: 37 additions & 12 deletions frontend/web/components/EditHealthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useGetHealthProvidersQuery,
} from 'common/services/useHealthProvider'
import { components } from 'react-select'
import InfoMessage from './InfoMessage'

type EditHealthProviderType = {
projectId: number
Expand All @@ -32,7 +33,6 @@ const CreateHealthProviderForm = ({ projectId }: { projectId: number }) => {
const [createProvider, { error, isError, isLoading, isSuccess }] =
useCreateHealthProviderMutation()

// TODO: Replace from list of provider options from API
const providers = [{ name: 'Sample' }, { name: 'Grafana' }]

const providerOptions = providers.map((provider) => ({
Expand Down Expand Up @@ -76,17 +76,17 @@ const CreateHealthProviderForm = ({ projectId }: { projectId: number }) => {
options={providerOptions}
/>
</Flex>
<div className='text-right'>
<Button
type='submit'
id='save-proj-btn'
disabled={isLoading || !selected}
className='ml-3'
>
{isLoading ? 'Creating' : 'Create'}
</Button>
</div>
</Row>
<div className='text-right mt-4'>
<Button
type='submit'
id='save-proj-btn'
disabled={isLoading || !selected}
className='ml-3'
>
{isLoading ? 'Creating' : 'Create'}
</Button>
</div>
</form>
)
}
Expand Down Expand Up @@ -137,13 +137,38 @@ const EditHealthProvider: FC<EditHealthProviderType> = ({
unhealthy state in different environments.{' '}
<Button
theme='text'
href='' // TODO: Add docs
href='https://docs.flagsmith.com/advanced-use/feature-health'
target='_blank'
className='fw-normal'
>
Learn about Feature Health.
</Button>
</p>
<InfoMessage>
<div>
<strong>
Follow the documentation to configure alerting using the supported
providers.
</strong>
</div>
<div>
<span>
Sample provider:{' '}
<a href='https://docs.flagsmith.com/advanced-use/feature-health#sample-provider'>
https://docs.flagsmith.com/advanced-use/feature-health#sample-provider
</a>
</span>
</div>
<div>
<span>
Grafana provider:{' '}
<a href='https://docs.flagsmith.com/integrations/apm/grafana/#in-grafana-1'>
{' '}
https://docs.flagsmith.com/integrations/apm/grafana/#in-grafana-1
</a>
</span>
</div>
</InfoMessage>

<label>Provider Name</label>
<CreateHealthProviderForm projectId={projectId} />
Expand Down
57 changes: 53 additions & 4 deletions frontend/web/components/FeatureRow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, useEffect } from 'react'
import React, { FC, useEffect, useMemo } from 'react'
import TagValues from './tags/TagValues'
import ConfirmToggleFeature from './modals/ConfirmToggleFeature'
import ConfirmRemoveFeature from './modals/ConfirmRemoveFeature'
Expand Down Expand Up @@ -30,6 +30,7 @@ import Switch from './Switch'
import AccountStore from 'common/stores/account-store'
import CondensedFeatureRow from './CondensedFeatureRow'
import { RouterChildContext } from 'react-router'
import { useGetHealthEventsQuery } from 'common/services/useHealthEvents'

interface FeatureRowProps {
disableControls?: boolean
Expand Down Expand Up @@ -76,6 +77,11 @@ const FeatureRow: FC<FeatureRowProps> = ({
}) => {
const protectedTags = useProtectedTags(projectFlag, projectId)

const { data: healthEvents } = useGetHealthEventsQuery(
{ projectId: String(projectFlag.project) },
{ skip: !projectFlag?.project },
)

useEffect(() => {
const { feature } = Utils.fromParam()
const { id } = projectFlag
Expand All @@ -89,6 +95,15 @@ const FeatureRow: FC<FeatureRowProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [environmentFlags, projectFlag])

const featureUnhealthyEvents = useMemo(
() =>
healthEvents?.filter(
(event) =>
event.type === 'UNHEALTHY' && event.feature === projectFlag.id,
),
[healthEvents, projectFlag],
)

const copyFeature = () => {
Utils.copyToClipboard(projectFlag.name)
}
Expand Down Expand Up @@ -168,6 +183,9 @@ const FeatureRow: FC<FeatureRowProps> = ({
</Row>,
<CreateFlagModal
hideTagsByType={['UNHEALTHY']}
hasUnhealthyEvents={
isFeatureHealthEnabled && featureUnhealthyEvents?.length
}
history={history}
environmentId={environmentId}
projectId={projectId}
Expand All @@ -187,6 +205,14 @@ const FeatureRow: FC<FeatureRowProps> = ({
)
}

const openFeatureHealthTab = (id: number) => {
editFeature(
projectFlag,
environmentFlags?.[id],
Constants.featurePanelTabs.FEATURE_HEALTH,
)
}

const isReadOnly = readOnly || Utils.getFlagsmithHasFeature('read_only_mode')
const isFeatureHealthEnabled = Utils.getFlagsmithHasFeature('feature_health')

Expand All @@ -208,6 +234,9 @@ const FeatureRow: FC<FeatureRowProps> = ({
environmentFlags={environmentFlags}
permission={permission}
editFeature={editFeature}
hasUnhealthyEvents={
isFeatureHealthEnabled && featureUnhealthyEvents?.length
}
onChange={onChange}
style={style}
className={className}
Expand Down Expand Up @@ -302,19 +331,39 @@ const FeatureRow: FC<FeatureRowProps> = ({
}
</Tooltip>
)}
<TagValues projectId={`${projectId}`} value={projectFlag.tags}>
<TagValues
projectId={`${projectId}`}
value={projectFlag.tags}
onClick={(tag) => {
if (tag?.type === 'UNHEALTHY') {
openFeatureHealthTab(id)
}
}}
>
{projectFlag.is_archived && (
<Tag className='chip--xs' tag={Constants.archivedTag} />
)}
</TagValues>
{!!isCompact && <StaleFlagWarning projectFlag={projectFlag} />}
{isFeatureHealthEnabled && !!isCompact && (
<UnhealthyFlagWarning projectFlag={projectFlag} />
<UnhealthyFlagWarning
featureUnhealthyEvents={featureUnhealthyEvents}
onClick={(e) => {
e?.stopPropagation()
openFeatureHealthTab(id)
}}
/>
)}
</Row>
{!isCompact && <StaleFlagWarning projectFlag={projectFlag} />}
{isFeatureHealthEnabled && !isCompact && (
<UnhealthyFlagWarning projectFlag={projectFlag} />
<UnhealthyFlagWarning
featureUnhealthyEvents={featureUnhealthyEvents}
onClick={(e) => {
e?.stopPropagation()
openFeatureHealthTab(id)
}}
/>
)}
{description && !isCompact && (
<div
Expand Down
45 changes: 16 additions & 29 deletions frontend/web/components/UnhealthyFlagWarning.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,38 @@
import { FC } from 'react'
import Constants from 'common/constants'
import { ProjectFlag } from 'common/types/responses'
import { HealthEvent } from 'common/types/responses'
import { IonIcon } from '@ionic/react'
import { warning } from 'ionicons/icons'
import { useGetTagsQuery } from 'common/services/useTag'
import { useGetHealthEventsQuery } from 'common/services/useHealthEvents'

type UnhealthyFlagWarningType = {
projectFlag: ProjectFlag
featureUnhealthyEvents?: HealthEvent[]
onClick?: (e?: React.MouseEvent) => void
}

const UnhealthyFlagWarning: FC<UnhealthyFlagWarningType> = ({
projectFlag,
featureUnhealthyEvents,
onClick,
}) => {
const { data: tags } = useGetTagsQuery(
{ projectId: String(projectFlag.project) },
{ refetchOnFocus: false, skip: !projectFlag?.project },
)
const { data: healthEvents } = useGetHealthEventsQuery(
{ projectId: String(projectFlag.project) },
{ refetchOnFocus: false, skip: !projectFlag?.project },
)
const unhealthyTagId = tags?.find((tag) => tag.type === 'UNHEALTHY')?.id
const latestHealthEvent = healthEvents?.find(
(event) => event.feature === projectFlag.id,
)

if (
!unhealthyTagId ||
!projectFlag?.tags?.includes(unhealthyTagId) ||
latestHealthEvent?.type !== 'UNHEALTHY'
)
return null
if (!featureUnhealthyEvents?.length) return null

return (
<Tooltip
title={
<div className='fs-caption' style={{ color: Constants.tagColors[16] }}>
{/* TODO: Provider info and link to issue will be provided by reason via the API */}
{latestHealthEvent.reason}
{latestHealthEvent.reason && (
<div
className='fs-caption'
style={{ color: Constants.featureHealth.unhealthyColor }}
onClick={onClick}
>
<div>
This feature has {featureUnhealthyEvents?.length} active alert
{featureUnhealthyEvents?.length > 1 ? 's' : ''}. Check them in the
'Feature Health' tab.
<IonIcon
style={{ marginBottom: -2 }}
className='ms-1'
icon={warning}
/>
)}
</div>
</div>
}
>
Expand Down
31 changes: 31 additions & 0 deletions frontend/web/components/modals/CreateFlag.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ import FeatureHistory from 'components/FeatureHistory'
import WarningMessage from 'components/WarningMessage'
import { getPermission } from 'common/services/usePermission'
import { getChangeRequests } from 'common/services/useChangeRequest'
import FeatureHealthTabContent from './FeatureHealthTabContent'
import { IonIcon } from '@ionic/react'
import { warning } from 'ionicons/icons'

const CreateFlag = class extends Component {
static displayName = 'CreateFlag'
Expand Down Expand Up @@ -1904,6 +1907,34 @@ const CreateFlag = class extends Component {
</InfoMessage>
</TabItem>
)}
{this.props.hasUnhealthyEvents && (
<TabItem
data-test='feature_health'
tabLabelString='Feature Health'
tabLabel={
<Row
className={`inline-block justify-content-center ${
true ? 'pr-1' : ''
}`}
>
Feature Health{' '}
<IonIcon
icon={warning}
style={{
color:
Constants.featureHealth
.unhealthyColor,
marginBottom: -2,
}}
/>
</Row>
}
>
<FeatureHealthTabContent
projectId={projectFlag.project}
/>
</TabItem>
)}
{hasIntegrationWithGithub &&
projectFlag?.id && (
<TabItem
Expand Down
Loading

0 comments on commit 5521b45

Please sign in to comment.