Skip to content

Commit 88c4294

Browse files
kyle-ssgclaude
andauthored
feat(Hackathon): Feature lifecycle (#6684)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f644651 commit 88c4294

29 files changed

Lines changed: 1853 additions & 24 deletions

frontend/common/services/useCodeReferences.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const codeReferencesService = service
2121

2222
export const {
2323
useGetFeatureCodeReferencesQuery,
24+
useLazyGetFeatureCodeReferencesQuery,
2425
// END OF EXPORTS
2526
} = codeReferencesService
2627

frontend/common/services/useGithubIntegration.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ export const githubIntegrationService = service
66
.enhanceEndpoints({ addTagTypes: ['GithubIntegration'] })
77
.injectEndpoints({
88
endpoints: (builder) => ({
9+
createCleanupIssue: builder.mutation<
10+
Res['createCleanupIssue'],
11+
Req['createCleanupIssue']
12+
>({
13+
query: (query: Req['createCleanupIssue']) => ({
14+
body: query.body,
15+
method: 'POST',
16+
url: `organisations/${query.organisation_id}/github/create-cleanup-issue/`,
17+
}),
18+
}),
919
createGithubIntegration: builder.mutation<
1020
Res['githubIntegrations'],
1121
Req['createGithubIntegration']
@@ -107,9 +117,24 @@ export async function updateGithubIntegration(
107117
),
108118
)
109119
}
120+
export async function createCleanupIssue(
121+
store: any,
122+
data: Req['createCleanupIssue'],
123+
options?: Parameters<
124+
typeof githubIntegrationService.endpoints.createCleanupIssue.initiate
125+
>[1],
126+
) {
127+
return store.dispatch(
128+
githubIntegrationService.endpoints.createCleanupIssue.initiate(
129+
data,
130+
options,
131+
),
132+
)
133+
}
110134
// END OF FUNCTION_EXPORTS
111135

112136
export const {
137+
useCreateCleanupIssueMutation,
113138
useCreateGithubIntegrationMutation,
114139
useDeleteGithubIntegrationMutation,
115140
useGetGithubIntegrationQuery,

frontend/common/types/requests.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,5 +865,11 @@ export type Req = {
865865
environmentFlagId: number
866866
body: UpdateFeatureStateBody
867867
}
868+
createCleanupIssue: {
869+
organisation_id: number
870+
body: {
871+
feature_id: number
872+
}
873+
}
868874
// END OF TYPES
869875
}

frontend/common/types/responses.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,5 +1100,9 @@ export type Res = {
11001100
}
11011101
}
11021102
featureState: FeatureState
1103+
createCleanupIssue: {
1104+
feature_external_resource_id: number
1105+
html_url: string
1106+
}
11031107
// END OF TYPES
11041108
}

frontend/web/components/Icon.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export type IconName =
6666
| 'shield'
6767
| 'star'
6868
| 'link'
69+
| 'lock'
6970
| 'sun'
7071
| 'timer'
7172
| 'trash-2'
@@ -128,11 +129,28 @@ const Icon: FC<IconType> = ({ fill, fill2, height, name, width, ...rest }) => {
128129
</svg>
129130
)
130131
}
132+
case 'lock': {
133+
return (
134+
<svg
135+
xmlns='http://www.w3.org/2000/svg'
136+
width={width || '24'}
137+
height={height || width || '24'}
138+
viewBox='0 0 512 512'
139+
fill='none'
140+
{...rest}
141+
>
142+
<path
143+
d='M368 192h-16v-80a96 96 0 10-192 0v80h-16a64.07 64.07 0 00-64 64v176a64.07 64.07 0 0064 64h224a64.07 64.07 0 0064-64V256a64.07 64.07 0 00-64-64zm-48 0H192v-80a64 64 0 11128 0z'
144+
fill={fill}
145+
/>
146+
</svg>
147+
)
148+
}
131149
case 'shield': {
132150
return (
133151
<svg
134-
width='24'
135-
height='24'
152+
width={width || '24'}
153+
height={height || '24'}
136154
viewBox='0 0 24 24'
137155
fill='none'
138156
xmlns='http://www.w3.org/2000/svg'

frontend/web/components/PermissionControl.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const SingleValue = (props: any) => {
2828
<Icon width={18} name='checkmark' fill='#27AB95' />
2929
)}
3030
{props.data.value === PermissionRoleType.GRANTED_FOR_TAGS && (
31-
<Icon width={18} name='shield' fill='#ff9f43' />
31+
<Icon width={18} name='shield' height={18} fill='#ff9f43' />
3232
)}
3333
{props.children}
3434
</div>

frontend/web/components/feature-summary/FeatureRow.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -377,14 +377,14 @@ const FeatureRow: FC<FeatureRowProps> = (props) => {
377377
e.stopPropagation()
378378
}}
379379
>
380-
<Switch
381-
disabled={!permission || isReadOnly || isLoading}
382-
data-test={`feature-switch-${index}${
383-
displayEnabled ? '-on' : '-off'
384-
}`}
385-
checked={displayEnabled}
386-
onChange={onChange}
387-
/>
380+
<Switch
381+
disabled={!permission || isReadOnly || isLoading}
382+
data-test={`feature-switch-${index}${
383+
displayEnabled ? '-on' : '-off'
384+
}`}
385+
checked={displayEnabled}
386+
onChange={onChange}
387+
/>
388388
</div>
389389
<FeatureAction {...featureActionProps} disableE2E={true} />
390390
</div>
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import React, { FC } from 'react'
2+
import classNames from 'classnames'
3+
import { ProjectFlag, VCSProvider } from 'common/types/responses'
4+
import FeatureName from './FeatureName'
5+
import FeatureDescription from './FeatureDescription'
6+
import TagValues from 'components/tags/TagValues'
7+
import VCSProviderTag from 'components/tags/VCSProviderTag'
8+
import Utils from 'common/utils/utils'
9+
10+
export interface ProjectFeatureRowProps {
11+
projectFlag: ProjectFlag
12+
index: number
13+
isSelected?: boolean
14+
onSelect?: (projectFlag: ProjectFlag) => void
15+
className?: string
16+
actions?: React.ReactNode
17+
}
18+
19+
const ProjectFeatureRow: FC<ProjectFeatureRowProps> = ({
20+
actions,
21+
className,
22+
index,
23+
isSelected,
24+
onSelect,
25+
projectFlag,
26+
}) => {
27+
const { description } = projectFlag
28+
29+
const isCodeReferencesEnabled = Utils.getFlagsmithHasFeature(
30+
'git_code_references',
31+
)
32+
const hasScannedCodeReferences =
33+
isCodeReferencesEnabled && projectFlag?.code_references_counts?.length > 0
34+
const codeReferencesCounts = isCodeReferencesEnabled
35+
? projectFlag?.code_references_counts?.reduce(
36+
(acc, curr) => acc + curr.count,
37+
0,
38+
) || 0
39+
: 0
40+
41+
return (
42+
<>
43+
{/* Desktop */}
44+
<div
45+
className={classNames(
46+
'd-none d-lg-flex align-items-lg-center flex-lg-row list-item py-0 list-item-xs fs-small',
47+
className,
48+
)}
49+
data-test={`cleanup-feature-item-${index}`}
50+
>
51+
{onSelect && (
52+
<div
53+
className='table-column px-2 align-items-center'
54+
onClick={(e) => {
55+
e.stopPropagation()
56+
}}
57+
>
58+
<input
59+
type='checkbox'
60+
checked={!!isSelected}
61+
onChange={() => onSelect(projectFlag)}
62+
/>
63+
</div>
64+
)}
65+
<div className='table-column ps-2 px-0 align-items-center flex-1'>
66+
<div className='mx-0 flex-1 flex-column'>
67+
<div className='d-flex align-items-center'>
68+
<FeatureName name={projectFlag.name} />
69+
{isCodeReferencesEnabled && hasScannedCodeReferences && (
70+
<Tooltip
71+
title={
72+
<VCSProviderTag
73+
count={codeReferencesCounts}
74+
isWarning={codeReferencesCounts === 0}
75+
vcsProvider={VCSProvider.GITHUB}
76+
/>
77+
}
78+
place='top'
79+
>
80+
{`Scanned ${codeReferencesCounts} times in ${projectFlag?.code_references_counts?.length} repositories`}
81+
</Tooltip>
82+
)}
83+
<TagValues
84+
projectId={`${projectFlag.project}`}
85+
value={projectFlag.tags}
86+
/>
87+
</div>
88+
<FeatureDescription description={description} />
89+
</div>
90+
</div>
91+
{actions && (
92+
<div className='table-column px-2 align-items-center'>{actions}</div>
93+
)}
94+
</div>
95+
96+
{/* Mobile */}
97+
<div
98+
className={classNames(
99+
'd-flex flex-column justify-content-center px-2 list-item py-1 d-lg-none',
100+
)}
101+
>
102+
<div className='d-flex gap-2 align-items-center'>
103+
{onSelect && (
104+
<div
105+
onClick={(e) => {
106+
e.stopPropagation()
107+
}}
108+
>
109+
<input
110+
type='checkbox'
111+
checked={!!isSelected}
112+
onChange={() => onSelect(projectFlag)}
113+
/>
114+
</div>
115+
)}
116+
<div className='flex-1 align-items-center flex-wrap'>
117+
<FeatureName name={projectFlag.name} />
118+
{isCodeReferencesEnabled && hasScannedCodeReferences && (
119+
<Tooltip
120+
title={
121+
<VCSProviderTag
122+
count={codeReferencesCounts}
123+
isWarning={codeReferencesCounts === 0}
124+
vcsProvider={VCSProvider.GITHUB}
125+
/>
126+
}
127+
place='top'
128+
>
129+
{`Scanned ${codeReferencesCounts} times in ${projectFlag?.code_references_counts?.length} repositories`}
130+
</Tooltip>
131+
)}
132+
<TagValues
133+
projectId={`${projectFlag.project}`}
134+
value={projectFlag.tags}
135+
/>
136+
</div>
137+
{actions}
138+
</div>
139+
</div>
140+
</>
141+
)
142+
}
143+
144+
export default ProjectFeatureRow

frontend/web/components/navigation/navbars/ProjectNavbar.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ const ProjectNavbar: FC<ProjectNavType> = ({ environmentId, projectId }) => {
5050
>
5151
Segments
5252
</NavSubLink>
53+
{Utils.getFlagsmithHasFeature('feature_lifecycle') && (
54+
<NavSubLink
55+
icon={<Icon name='refresh' />}
56+
id='lifecycle-link'
57+
to={`/project/${projectId}/lifecycle`}
58+
isActive={(_, location) =>
59+
location.pathname.startsWith(`/project/${projectId}/lifecycle`)
60+
}
61+
>
62+
Feature Lifecycle
63+
</NavSubLink>
64+
)}
5365
<Permission level='project' permission='VIEW_AUDIT_LOG' id={projectId}>
5466
{({ permission }) =>
5567
permission && (

0 commit comments

Comments
 (0)