Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: stale flags (FE) #3606

Merged
merged 23 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cb4a289
Add FE logic for creating is_permanent tags
matthewelwell Dec 7, 2023
27d682d
WIP: add more control over tag content
matthewelwell Jan 5, 2024
b19726a
Fix styles for create, add icons and tooltips for permanent / system …
kyle-ssg Jan 11, 2024
e2d3cee
Merge branch 'feat/stale-flags-fe' into feat/stale-flags
kyle-ssg Mar 13, 2024
0b9759f
Standardise tag tooltips, add stale tag
kyle-ssg Mar 13, 2024
58f246f
Standardise tag tooltips, add stale tag
kyle-ssg Mar 13, 2024
28ab430
Standardise tag tooltips, add stale tag
kyle-ssg Mar 13, 2024
2853d57
Add tooltip for permanent tags, reset create edit tag modal on close
kyle-ssg Mar 13, 2024
62bdb8d
fix tooltip gap
kyle-ssg Mar 13, 2024
350000b
remove unused type
kyle-ssg Mar 13, 2024
d3f6ff1
Update frontend/web/components/tags/TagContent.tsx
kyle-ssg Mar 13, 2024
15bb8e2
Add stale setting in project settings
kyle-ssg Mar 13, 2024
58dbf56
Merge remote-tracking branch 'origin/feat/stale-flags' into feat/stal…
kyle-ssg Mar 13, 2024
3648805
remove stale_flags_limit_days check
kyle-ssg Mar 13, 2024
4197a45
Merge branch 'main' into feat/stale-flags
kyle-ssg Mar 13, 2024
9e6534b
add protected tag message
kyle-ssg Mar 19, 2024
382410b
Merge branch 'main' into feat/stale-flags
kyle-ssg Apr 8, 2024
f1793ed
Adjust tooltip
kyle-ssg Apr 8, 2024
0ab2f55
Update plan permissions
kyle-ssg Apr 11, 2024
ccc5126
Merge branch 'main' into feat/stale-flags
kyle-ssg Apr 11, 2024
173cd0d
Fix update environment check
kyle-ssg Apr 11, 2024
b854677
Add versioning notes
kyle-ssg Apr 12, 2024
577668f
Fix stale tag tooltip
kyle-ssg Apr 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -91,6 +91,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 @@ -250,6 +251,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
Loading