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: Flag group owners #3112

Merged
merged 3 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions frontend/common/services/useProjectFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export const projectFlagService = service
.enhanceEndpoints({ addTagTypes: ['ProjectFlag'] })
.injectEndpoints({
endpoints: (builder) => ({
getProjectFlag: builder.query<Res['projectFlag'], Req['getProjectFlag']>({
providesTags: (res) => [{ id: res?.id, type: 'ProjectFlag' }],
query: (query: Req['getProjectFlag']) => ({
url: `projects/${query.project}/features/${query.id}/`,
}),
}),
getProjectFlags: builder.query<
Res['projectFlags'],
Req['getProjectFlags']
Expand Down Expand Up @@ -62,9 +68,21 @@ export async function getProjectFlags(
projectFlagService.endpoints.getProjectFlags.initiate(data, options),
)
}
export async function getProjectFlag(
store: any,
data: Req['getProjectFlag'],
options?: Parameters<
typeof projectFlagService.endpoints.getProjectFlag.initiate
>[1],
) {
return store.dispatch(
projectFlagService.endpoints.getProjectFlag.initiate(data, options),
)
}
// END OF FUNCTION_EXPORTS

export const {
useGetProjectFlagQuery,
useGetProjectFlagsQuery,
// END OF EXPORTS
} = projectFlagService
Expand Down
1 change: 1 addition & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export type Req = {
user: string
}
getProjectFlags: { project: string }
getProjectFlag: { project: string; id: string }
getRolesPermissionUsers: { organisation_id: string; role_id: string }
deleteRolesPermissionUsers: {
organisation_id: string
Expand Down
12 changes: 7 additions & 5 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ export type LaunchDarklyProjectImport = {
updated_at: string
completed_at: string
status: {
requested_environment_count: number
requested_flag_count: number
result: string || null
error_message: string || null
},
requested_environment_count: number
requested_flag_count: number
result: string | null
error_message: string | null
}
project: number
}

Expand Down Expand Up @@ -257,6 +257,7 @@ export type ProjectFlag = {
num_identity_overrides: number | null
num_segment_overrides: number | null
owners: User[]
owner_groups: UserGroupSummary[]
project: number
tags: number[]
type: string
Expand Down Expand Up @@ -371,6 +372,7 @@ export type Res = {
rolePermission: { id: string }

projectFlags: PagedResponse<ProjectFlag>
projectFlag: ProjectFlag
identityFeatureStates: IdentityFeatureState[]
rolesPermissionUsers: RolePermissionUser
rolePermissionGroup: { id: string }
Expand Down
122 changes: 122 additions & 0 deletions frontend/web/components/FlagOwnerGroups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React, { Component } from 'react'
import data from 'common/data/base/_data'
import UserSelect from './UserSelect'
import ConfigProvider from 'common/providers/ConfigProvider'
import Icon from './Icon'
import { close } from 'ionicons/icons'
import { IonIcon } from '@ionic/react'
import GroupSelect from './GroupSelect'
import { getProjectFlag } from 'common/services/useProjectFlag'
import { getStore } from 'common/store'

class TheComponent extends Component {
state = {}
componentDidMount() {
this.getData()
}

getData = () => {
getProjectFlag(getStore(), {
id: this.props.id,
project: this.props.projectId,
}).then((res) => {
const groupOwners = (res.data.group_owners || []).map((v) => v.id)
this.setState({ groupOwners })
})
}

addOwner = (id) => {
this.setState({ groupOwners: (this.state.groupOwners || []).concat(id) })
data.post(
`${Project.api}projects/${this.props.projectId}/features/${this.props.id}/add-group-owners/`,
{
group_ids: [id],
},
)
}

removeOwner = (id) => {
this.setState({
groupOwners: (this.state.groupOwners || []).filter((v) => v !== id),
})
data.post(
`${Project.api}projects/${this.props.projectId}/features/${this.props.id}/remove-group-owners/`,
{
group_ids: [id],
},
)
}

getGroupOwners = (users, groupOwners) =>
users ? users.filter((v) => groupOwners.includes(v.id)) : []

render() {
const hasPermission = Utils.getPlansPermission('FLAG_OWNERS')

return (
<OrganisationProvider>
{({ groups }) => {
const ownerUsers = this.getGroupOwners(
groups,
this.state.groupOwners || [],
)
const res = (
<div>
<Row
className='clickable'
onClick={() => {
if (hasPermission) this.setState({ showUsers: true })
}}
>
<label className='cols-sm-2 control-label'>
Assigned groups{' '}
<Icon name='setting' width={20} fill={'#656D7B'} />
</label>
</Row>
<Row style={{ rowGap: '12px' }}>
{hasPermission &&
ownerUsers.map((u) => (
<Row
key={u.id}
onClick={() => this.removeOwner(u.id)}
className='chip mr-2'
>
<span className='font-weight-bold'>{u.name}</span>
<span className='chip-icon ion'>
<IonIcon icon={close} />
</span>
</Row>
))}
{!ownerUsers.length && (
<div>This flag has no assigned groups</div>
)}
</Row>
<GroupSelect
groups={groups}
value={this.state.groupOwners}
isOpen={this.state.showUsers}
size={null}
onAdd={this.addOwner}
onRemove={this.removeOwner}
onToggle={() =>
this.setState({ showUsers: !this.state.showUsers })
}
/>
</div>
)
return hasPermission ? (
res
) : (
<div>
{res}
The add flag assignees feature is available with our{' '}
<strong>Scale-up</strong> plan.
</div>
)
}}
</OrganisationProvider>
)
}
}

export default ConfigProvider(TheComponent)
24 changes: 14 additions & 10 deletions frontend/web/components/FlagOwners.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import ConfigProvider from 'common/providers/ConfigProvider'
import Icon from './Icon'
import { close } from 'ionicons/icons'
import { IonIcon } from '@ionic/react'
import { getProjectFlag } from 'common/services/useProjectFlag'
import { getStore } from 'common/store'

class TheComponent extends Component {
state = {}
Expand All @@ -13,14 +15,13 @@ class TheComponent extends Component {
}

getData = () => {
data
.get(
`${Project.api}projects/${this.props.projectId}/features/${this.props.id}/`,
)
.then((res) => {
const owners = (res.owners || []).map((v) => v.id)
this.setState({ owners })
})
getProjectFlag(getStore(), {
id: this.props.id,
project: this.props.projectId,
}).then((res) => {
const owners = (res.data.owners || []).map((v) => v.id)
this.setState({ owners })
})
}

addOwner = (id) => {
Expand Down Expand Up @@ -62,7 +63,8 @@ class TheComponent extends Component {
}}
>
<label className='cols-sm-2 control-label'>
Assignees <Icon name='setting' width={20} fill={'#656D7B'} />
Assigned users{' '}
<Icon name='setting' width={20} fill={'#656D7B'} />
</label>
</Row>
<Row style={{ rowGap: '12px' }}>
Expand All @@ -81,7 +83,9 @@ class TheComponent extends Component {
</span>
</Row>
))}
{!ownerUsers.length && <div>This flag has no assignees</div>}
{!ownerUsers.length && (
<div>This flag has no assigned users</div>
)}
</Row>

<UserSelect
Expand Down
21 changes: 15 additions & 6 deletions frontend/web/components/modals/CreateFlag.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { setInterceptClose } from './base/ModalDefault'
import Icon from 'components/Icon'
import ModalHR from './ModalHR'
import FeatureValue from 'components/FeatureValue'
import FlagOwnerGroups from 'components/FlagOwnerGroups'

const CreateFlag = class extends Component {
static displayName = 'CreateFlag'
Expand Down Expand Up @@ -518,12 +519,20 @@ const CreateFlag = class extends Component {
>
{({ permission: projectAdmin }) =>
projectAdmin && (
<FormGroup className='mb-5 setting'>
<FlagOwners
projectId={this.props.projectId}
id={projectFlag.id}
/>
</FormGroup>
<>
<FormGroup className='mb-5 setting'>
<FlagOwners
projectId={this.props.projectId}
id={projectFlag.id}
/>
</FormGroup>
<FormGroup className='mb-5 setting'>
<FlagOwnerGroups
projectId={this.props.projectId}
id={projectFlag.id}
/>
</FormGroup>
</>
)
}
</Permission>
Expand Down