Skip to content

Commit

Permalink
feat: stale flags (FE) (#3606)
Browse files Browse the repository at this point in the history
Co-authored-by: Matthew Elwell <matthew.elwell@flagsmith.com>
  • Loading branch information
kyle-ssg and matthewelwell committed Apr 18, 2024
1 parent 9a4dbae commit 424b754
Show file tree
Hide file tree
Showing 15 changed files with 289 additions and 90 deletions.
3 changes: 3 additions & 0 deletions frontend/common/stores/project-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ const store = Object.assign({}, BaseStore, {
getMaxSegmentsAllowed: () => {
return store.model && store.model.max_segments_allowed
},
getStaleFlagsLimit: () => {
return store.model && store.model.stale_flags_limit_days
},
getTotalFeatures: () => {
return store.model && store.model.total_features
},
Expand Down
4 changes: 4 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export type Project = {
max_features_allowed?: number | null
max_segment_overrides_allowed?: number | null
total_features?: number
stale_flags_limit_days?: number
total_segments?: number
environments: Environment[]
}
Expand Down Expand Up @@ -251,6 +252,9 @@ export type Tag = {
description: string
project: number
label: string
is_system_tag: boolean
is_permanent: boolean
type: 'STALE' | 'NONE'
}

export type MultivariateFeatureStateValue = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getStore } from 'common/store'
import { ProjectFlag, Tag } from 'common/types/responses'
import { tagService } from 'common/services/useTag'

export const hasProtectedTag = (
export const getProtectedTags = (
projectFlag: ProjectFlag,
projectId: string,
) => {
Expand All @@ -11,15 +11,12 @@ export const hasProtectedTag = (
tagService.endpoints.getTags.select({ projectId: `${projectId}` })(
store.getState(),
).data || []
return !!projectFlag.tags?.find((id) => {
const tag = tags.find((tag) => tag.id === id)
if (tag) {
const label = tag.label.toLowerCase().replace(/[ _]/g, '')
return (
label === 'protected' ||
label === 'donotdelete' ||
label === 'permanent'
)
}
})
return projectFlag.tags
?.filter((id) => {
const tag = tags.find((tag) => tag.id === id)
return tag?.is_permanent
})
.map((id) => {
return tags.find((tag) => tag.id === id)
})
}
4 changes: 4 additions & 0 deletions frontend/common/utils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,10 @@ const Utils = Object.assign({}, require('./base/_utils'), {
valid = isScaleupOrGreater
break
}
case 'STALE_FLAGS': {
valid = isEnterprise
break
}
default:
valid = true
break
Expand Down
25 changes: 22 additions & 3 deletions frontend/web/components/FeatureAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import Constants from 'common/constants'
import Permission from 'common/providers/Permission'
import Button from './base/forms/Button'
import Icon from './Icon'
import { Tag } from 'common/types/responses'
import color from 'color'
import { getTagColor } from './tags/Tag'

interface FeatureActionProps {
projectId: string
featureIndex: number
readOnly: boolean
isProtected: boolean
protectedTags: Tag[] | undefined
hideAudit: boolean
hideHistory: boolean
hideRemove: boolean
Expand Down Expand Up @@ -44,12 +47,12 @@ export const FeatureAction: FC<FeatureActionProps> = ({
hideHistory,
hideRemove,
isCompact,
isProtected,
onCopyName,
onRemove,
onShowAudit,
onShowHistory,
projectId,
protectedTags,
readOnly,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(false)
Expand Down Expand Up @@ -90,6 +93,7 @@ export const FeatureAction: FC<FeatureActionProps> = ({
listRef.current.style.left = `${listPosition.left}px`
}, [isOpen])

const isProtected = !!protectedTags?.length
return (
<div className='feature-action'>
<div ref={btnRef}>
Expand Down Expand Up @@ -169,7 +173,22 @@ export const FeatureAction: FC<FeatureActionProps> = ({
}
>
{isProtected &&
'<span>This feature has been tagged as <bold>protected</bold>, <bold>permanent</bold>, <bold>do not delete</bold>, or <bold>read only</bold>. Please remove the tag before attempting to delete this flag.</span>'}
`<span>This feature has been tagged with the permanent tag${
protectedTags?.length > 1 ? 's' : ''
} ${protectedTags
?.map((tag) => {
const tagColor = getTagColor(tag)
return `<strong class='chip chip--xs d-inline-block ms-1' style='background:${color(
tagColor,
).fade(0.92)};border-color:${color(tagColor).darken(
0.1,
)};color:${color(tagColor).darken(0.1)};'>
${tag.label}
</strong>`
})
.join('')}. Please remove the tag${
protectedTags?.length > 1 ? 's' : ''
} before attempting to delete this flag.</span>`}
</Tooltip>,
)
}
Expand Down
6 changes: 3 additions & 3 deletions frontend/web/components/FeatureRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ConfirmRemoveFeature from './modals/ConfirmRemoveFeature'
import CreateFlagModal from './modals/CreateFlag'
import ProjectStore from 'common/stores/project-store'
import Constants from 'common/constants'
import { hasProtectedTag } from 'common/utils/hasProtectedTag'
import { getProtectedTags } from 'common/utils/getProtectedTags'
import Icon from './Icon'
import FeatureValue from './FeatureValue'
import FeatureAction from './FeatureAction'
Expand Down Expand Up @@ -132,7 +132,7 @@ class TheComponent extends Component {
const { created_date, description, id, name } = this.props.projectFlag
const readOnly =
this.props.readOnly || Utils.getFlagsmithHasFeature('read_only_mode')
const isProtected = hasProtectedTag(projectFlag, projectId)
const protectedTags = getProtectedTags(projectFlag, projectId)
const environment = ProjectStore.getEnvironment(environmentId)
const changeRequestsEnabled = Utils.changeRequestsEnabled(
environment && environment.minimum_change_request_approvals,
Expand Down Expand Up @@ -368,7 +368,7 @@ class TheComponent extends Component {
projectId={projectId}
featureIndex={this.props.index}
readOnly={readOnly}
isProtected={isProtected}
protectedTags={protectedTags}
isCompact={isCompact}
hideAudit={
AccountStore.getOrganisationRole() !== 'ADMIN' ||
Expand Down
11 changes: 9 additions & 2 deletions frontend/web/components/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ export type TooltipProps = {
children: string
place?: _TooltipProps['place']
plainText?: boolean
titleClassName?: string
}

const Tooltip: FC<TooltipProps> = ({ children, place, plainText, title }) => {
const Tooltip: FC<TooltipProps> = ({
children,
place,
plainText,
title,
titleClassName,
}) => {
const id = Utils.GUID()

if (!children) {
Expand All @@ -19,7 +26,7 @@ const Tooltip: FC<TooltipProps> = ({ children, place, plainText, title }) => {
return (
<>
{title && (
<span data-for={id} data-tip>
<span className={titleClassName} data-for={id} data-tip>
{title}
</span>
)}
Expand Down
70 changes: 62 additions & 8 deletions frontend/web/components/pages/ProjectSettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ImportPage from 'components/import-export/ImportPage'
import FeatureExport from 'components/import-export/FeatureExport'
import ProjectUsage from 'components/ProjectUsage'
import ProjectStore from 'common/stores/project-store'
import Tooltip from 'components/Tooltip'

const ProjectSettingsPage = class extends Component {
static displayName = 'ProjectSettingsPage'
Expand Down Expand Up @@ -154,7 +155,8 @@ const ProjectSettingsPage = class extends Component {
}

render() {
const { name } = this.state
const { name, stale_flags_limit_days } = this.state
const hasStaleFlagsPermission = Utils.getPlansPermission('STALE_FLAGS')

return (
<div className='app-container container'>
Expand All @@ -163,6 +165,15 @@ const ProjectSettingsPage = class extends Component {
onSave={this.onSave}
>
{({ deleteProject, editProject, isLoading, isSaving, project }) => {
if (
!this.state.stale_flags_limit_days &&
project?.stale_flags_limit_days
) {
this.state.stale_flags_limit_days = project.stale_flags_limit_days
}
if (!this.state.name && project?.name) {
this.state.name = project.name
}
if (
!this.state.populatedProjectState &&
project?.feature_name_regex
Expand All @@ -181,11 +192,16 @@ const ProjectSettingsPage = class extends Component {
e.preventDefault()
!isSaving &&
name &&
editProject(Object.assign({}, project, { name }))
editProject(
Object.assign({}, project, { name, stale_flags_limit_days }),
)
}

const featureRegexEnabled =
typeof this.state.feature_name_regex === 'string'

const hasVersioning =
Utils.getFlagsmithHasFeature('feature_versioning')
return (
<div>
<PageTitle title={'Project Settings'} />
Expand All @@ -200,14 +216,13 @@ const ProjectSettingsPage = class extends Component {
/>
<label>Project Name</label>
<FormGroup>
<form onSubmit={saveProject}>
<Row className='align-items-start col-md-8'>
<form className='col-md-6' onSubmit={saveProject}>
<Row className='align-items-start'>
<Flex className='ml-0'>
<Input
ref={(e) => (this.input = e)}
defaultValue={project.name}
value={this.state.name}
inputClassName='input--wide'
inputClassName='full-width'
name='proj-name'
onChange={(e) =>
this.setState({
Expand All @@ -220,15 +235,54 @@ const ProjectSettingsPage = class extends Component {
placeholder='My Project Name'
/>
</Flex>
</Row>
{!!hasVersioning && (
<Tooltip
title={
<div>
<label className='mt-4'>
Days before a feature is marked as stale{' '}
<Icon name='info-outlined' />
</label>
<div style={{ width: 80 }} className='ml-0'>
<Input
disabled={!hasStaleFlagsPermission}
ref={(e) => (this.input = e)}
value={
this.state.stale_flags_limit_days
}
onChange={(e) =>
this.setState({
stale_flags_limit_days: parseInt(
Utils.safeParseEventValue(e),
),
})
}
isValid={!!stale_flags_limit_days}
type='number'
placeholder='Number of Days'
/>
</div>
</div>
}
>
{`${
!hasStaleFlagsPermission
? 'This feature is available with our enterprise plan. '
: ''
}If no changes have been made to a feature in any environment within this threshold the feature will be tagged as stale. You will need to enable feature versioning in your environments for stale features to be detected.`}
</Tooltip>
)}
<div className='text-right'>
<Button
type='submit'
id='save-proj-btn'
disabled={isSaving || !name}
className='ml-3'
>
{isSaving ? 'Updating' : 'Update Name'}
{isSaving ? 'Updating' : 'Update'}
</Button>
</Row>
</div>
</form>
</FormGroup>
</div>
Expand Down
8 changes: 7 additions & 1 deletion frontend/web/components/tables/TableTagFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import TableFilterItem from './TableFilterItem'
import Constants from 'common/constants'
import { TagStrategy } from 'common/types/responses'
import { AsyncStorage } from 'polyfill-react-native'
import TagContent from 'components/tags/TagContent'

type TableFilterType = {
projectId: string
Expand Down Expand Up @@ -167,7 +168,12 @@ const TableTagFilter: FC<TableFilterType> = ({
className='px-2 py-2 mr-1'
tag={tag}
/>
<div className='ml-2'>{tag.label}</div>
<div
style={{ width: 150 }}
className='ml-2 text-nowrap text-overflow'
>
<TagContent tag={tag} />
</div>
</Row>
}
key={tag.id}
Expand Down
41 changes: 24 additions & 17 deletions frontend/web/components/tags/AddEditTags.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, useMemo, useState } from 'react'
import React, { FC, useEffect, useMemo, useState } from 'react'
import { filter as loFilter } from 'lodash'
import { useHasPermission } from 'common/providers/Permission'
import Utils from 'common/utils/utils'
Expand Down Expand Up @@ -46,6 +46,11 @@ const AddEditTags: FC<AddEditTagsType> = ({
permission: 'ADMIN',
})

useEffect(() => {
if (!isOpen) {
setTab('SELECT')
}
}, [isOpen])
const selectTag = (tag: TTag) => {
const _value = value || []
const isSelected = _value?.includes(tag.id)
Expand Down Expand Up @@ -200,22 +205,24 @@ const AddEditTags: FC<AddEditTagsType> = ({
tag={tag}
/>
</Flex>
{!readOnly && !!projectAdminPermission && (
<>
<div
onClick={() => editTag(tag)}
className='clickable'
>
<Icon name='setting' fill='#9DA4AE' />
</div>
<div
onClick={() => confirmDeleteTag(tag)}
className='ml-3 clickable'
>
<Icon name='trash-2' fill='#9DA4AE' />
</div>
</>
)}
{!readOnly &&
!!projectAdminPermission &&
!tag.is_system_tag && (
<>
<div
onClick={() => editTag(tag)}
className='clickable'
>
<Icon name='setting' fill='#9DA4AE' />
</div>
<div
onClick={() => confirmDeleteTag(tag)}
className='ml-3 clickable'
>
<Icon name='trash-2' fill='#9DA4AE' />
</div>
</>
)}
</Row>
</div>
))}
Expand Down

0 comments on commit 424b754

Please sign in to comment.