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: Clone identities (FE) #3725

Merged
merged 11 commits into from
Apr 30, 2024
49 changes: 40 additions & 9 deletions frontend/common/services/useIdentityFeatureState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,29 @@ export const identityFeatureStateService = service
.enhanceEndpoints({ addTagTypes: ['IdentityFeatureState'] })
.injectEndpoints({
endpoints: (builder) => ({
getIdentityFeatureStates: builder.query<
createCloneIdentityFeatureStates: builder.mutation<
Res['cloneidentityFeatureStates'],
Req['createCloneIdentityFeatureStates']
>({
invalidatesTags: [{ type: 'IdentityFeatureState' }],
query: (query: Req['createCloneIdentityFeatureStates']) => ({
body: query.body,
method: 'POST',
url: `environments/${
query.environment_id
}/${Utils.getIdentitiesEndpoint()}/${
query.identity_id
}/${Utils.getFeatureStatesEndpoint()}/clone-from-given-identity/`,
}),
}),
getIdentityFeatureStatesAll: builder.query<
Res['identityFeatureStates'],
Req['getIdentityFeatureStates']
Req['getIdentityFeatureStatesAll']
>({
providesTags: (res, _, req) => [
{ id: req.user, type: 'IdentityFeatureState' },
],
query: (query: Req['getIdentityFeatureStates']) => ({
query: (query: Req['getIdentityFeatureStatesAll']) => ({
url: `environments/${
query.environment
}/${Utils.getIdentitiesEndpoint()}/${
Expand All @@ -26,15 +41,30 @@ export const identityFeatureStateService = service
}),
})

export async function getIdentityFeatureStates(
export async function getIdentityFeatureStateAll(
store: any,
data: Req['getIdentityFeatureStatesAll'],
options?: Parameters<
typeof identityFeatureStateService.endpoints.getIdentityFeatureStatesAll.initiate
>[1],
) {
return store.dispatch(
identityFeatureStateService.endpoints.getIdentityFeatureStatesAll.initiate(
data,
options,
),
)
}

export async function createIdentityFeatureStates(
store: any,
data: Req['getIdentityFeatureStates'],
data: Req['createCloneIdentityFeatureStates'],
options?: Parameters<
typeof identityFeatureStateService.endpoints.getIdentityFeatureStates.initiate
typeof identityFeatureStateService.endpoints.createCloneIdentityFeatureStates.initiate
>[1],
) {
return store.dispatch(
identityFeatureStateService.endpoints.getIdentityFeatureStates.initiate(
identityFeatureStateService.endpoints.createCloneIdentityFeatureStates.initiate(
data,
options,
),
Expand All @@ -43,12 +73,13 @@ export async function getIdentityFeatureStates(
// END OF FUNCTION_EXPORTS

export const {
useGetIdentityFeatureStatesQuery,
useCreateCloneIdentityFeatureStatesMutation,
useGetIdentityFeatureStatesAllQuery,
// END OF EXPORTS
} = identityFeatureStateService

/* Usage examples:
const { data, isLoading } = useGetIdentityFeatureStatesQuery({ id: 2 }, {}) //get hook
const [createIdentityFeatureStates, { isLoading, data, isSuccess }] = useCreateIdentityFeatureStatesMutation() //create hook
identityFeatureStateService.endpoints.getIdentityFeatureStates.select({id: 2})(store.getState()) //access data from any function
identityFeatureStateService.endpoints.getIdentityFeatureStatesAll.select({id: 2})(store.getState()) //access data from any function
*/
15 changes: 10 additions & 5 deletions frontend/common/stores/organisation-store.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Constants from 'common/constants'
import { projectService } from "common/services/useProject";
import { getStore } from "common/store";
import { projectService } from 'common/services/useProject'
import { getStore } from 'common/store'
import sortBy from 'lodash/sortBy'

const Dispatcher = require('../dispatcher/dispatcher')
Expand Down Expand Up @@ -30,8 +30,13 @@ const controller = {
API.trackEvent(Constants.events.CREATE_FIRST_PROJECT)
}
API.trackEvent(Constants.events.CREATE_PROJECT)
const defaultEnvironmentNames = Utils.getFlagsmithHasFeature('default_environment_names_for_new_project')
? JSON.parse(Utils.getFlagsmithValue('default_environment_names_for_new_project')) : ['Development', 'Production']
const defaultEnvironmentNames = Utils.getFlagsmithHasFeature(
'default_environment_names_for_new_project',
)
? JSON.parse(
Utils.getFlagsmithValue('default_environment_names_for_new_project'),
)
: ['Development', 'Production']
data
.post(`${Project.api}projects/`, { name, organisation: store.id })
.then((project) => {
Expand All @@ -43,7 +48,7 @@ const controller = {
project: project.id,
})
.then((res) => createSampleUser(res, envName, project))
})
}),
).then((res) => {
project.environments = res
store.model.projects = store.model.projects.concat(project)
Expand Down
9 changes: 8 additions & 1 deletion frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export type Req = {
updateRolePermission: Req['createRolePermission'] & { id: number }
deleteRolePermission: { organisation_id: number; role_id: number }

getIdentityFeatureStates: {
getIdentityFeatureStatesAll: {
environment: string
user: string
}
Expand Down Expand Up @@ -380,6 +380,13 @@ export type Req = {
usersToAddAdmin: number[] | null
}
getUserGroupPermission: { project_id: string }
createCloneIdentityFeatureStates: {
environment_id: string
identity_id: string
body: {
source_identity_id: string
}
}
updateGroup: Req['createGroup'] & {
orgId: string
data: UserGroup
Expand Down
5 changes: 4 additions & 1 deletion frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ export type IdentityFeatureState = {
enabled: boolean
feature_state_value: FlagsmithValue
segment: null
overridden_by: string | null
multivariate_feature_state_values?: {
multivariate_feature_option: {
value: number
Expand Down Expand Up @@ -670,7 +671,7 @@ export type Res = {
rolePermission: PagedResponse<UserPermission>
projectFlags: PagedResponse<ProjectFlag>
projectFlag: ProjectFlag
identityFeatureStates: IdentityFeatureState[]
identityFeatureStatesAll: IdentityFeatureState[]
createRolesPermissionUsers: RolePermissionUser
rolesPermissionUsers: PagedResponse<RolePermissionUser>
createRolePermissionGroup: RolePermissionGroup
Expand Down Expand Up @@ -707,5 +708,7 @@ export type Res = {
featureImports: PagedResponse<FeatureImport>
serversideEnvironmentKeys: APIKey[]
userGroupPermissions: GroupPermission[]
identityFeatureStates: PagedResponse<FeatureState>
cloneidentityFeatureStates: IdentityFeatureState
// END OF TYPES
}
5 changes: 4 additions & 1 deletion frontend/web/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,10 @@ const App = class extends Component {
fill='#9DA4AE'
/>
</span>
Organisation <strong>{AccountStore.getOrganisation()?.name}</strong>
Organisation{' '}
<strong>
{AccountStore.getOrganisation()?.name}
</strong>
</NavLink>
</Row>
<Row>
Expand Down
79 changes: 75 additions & 4 deletions frontend/web/components/CompareIdentities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import React, { FC, useEffect, useMemo, useState } from 'react'
import IdentitySelect, { IdentitySelectType } from './IdentitySelect'
import Utils from 'common/utils/utils'
import EnvironmentSelect from './EnvironmentSelect'
import { useGetIdentityFeatureStatesQuery } from 'common/services/useIdentityFeatureState'
import {
useGetIdentityFeatureStatesAllQuery,
useCreateCloneIdentityFeatureStatesMutation,
} from 'common/services/useIdentityFeatureState'
import { useGetProjectFlagsQuery } from 'common/services/useProjectFlag'
import Tag from './tags/Tag'
import PanelSearch from './PanelSearch'
Expand All @@ -17,6 +20,8 @@ import Button from './base/forms/Button'
import ProjectStore from 'common/stores/project-store'
import SegmentOverridesIcon from './SegmentOverridesIcon'
import IdentityOverridesIcon from './IdentityOverridesIcon'
import Tooltip from './Tooltip'
import PageTitle from './PageTitle'

type CompareIdentitiesType = {
projectId: string
Expand Down Expand Up @@ -66,14 +71,16 @@ const CompareIdentities: FC<CompareIdentitiesType> = ({
permission: Utils.getViewIdentitiesPermission(),
})

const { data: leftUser } = useGetIdentityFeatureStatesQuery(
const { data: leftUser } = useGetIdentityFeatureStatesAllQuery(
{ environment: environmentId, user: `${leftId?.value}` },
{ skip: !leftId },
)
const { data: rightUser } = useGetIdentityFeatureStatesQuery(
const { data: rightUser } = useGetIdentityFeatureStatesAllQuery(
{ environment: environmentId, user: `${rightId?.value}` },
{ skip: !rightId },
)
const [createCloneIdentityFeatureStates] =
useCreateCloneIdentityFeatureStatesMutation()

useEffect(() => {
// Clear users whenever environment or project is changed
Expand Down Expand Up @@ -120,6 +127,38 @@ const CompareIdentities: FC<CompareIdentitiesType> = ({
)
}

const cloneIdentityValues = (
leftIdentityName: string,
rightIdentityName: string,
leftIdentityId: string,
rightIdentityId: string,
environmentId: string,
) => {
return openConfirm({
body: (
<div>
{'Cloning '} <strong>{leftIdentityName}</strong>{' '}
{'will copy any Identity Overrides in '}
<strong>{`${rightIdentityName}.`}</strong> {'Are you sure?'}
</div>
),
destructive: true,
onYes: () => {
createCloneIdentityFeatureStates({
body: {
source_identity_id: leftIdentityId,
},
environment_id: environmentId,
identity_id: rightIdentityId,
}).then(() => {
toast('Clonation Completed!')
})
},
title: 'Clone Identity',
yesText: 'Confirm',
})
}

return (
<div>
<div className='col-md-8'>
Expand Down Expand Up @@ -179,9 +218,41 @@ const CompareIdentities: FC<CompareIdentitiesType> = ({

{isReady && (
<>
<PageTitle
title={'Changed Flags'}
className='mt-3'
cta={
<>
{Utils.getFlagsmithHasFeature('clone_identities') && (
<>
<Tooltip
title={
<Button
disabled={!leftId || !rightId || !environmentId}
onClick={() => {
cloneIdentityValues(
leftId?.label,
rightId?.label,
leftId?.value,
rightId?.value,
environmentId,
)
}}
className='ms-2 me-2'
>
{'Clone Features states'}
</Button>
}
>
{`Clone the Features states from ${leftId?.label} to ${rightId?.label}`}
</Tooltip>
</>
)}
</>
}
></PageTitle>
<PanelSearch
className='no-pad mt-4'
title={'Changed Flags'}
searchPanel={
<Row className='mb-2'>
<Tag
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/components/EditPermissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import Panel from './base/grid/Panel'
import InputGroup from './base/forms/InputGroup'
import classNames from 'classnames'
import OrganisationProvider from 'common/providers/OrganisationProvider'
import { useHasPermission } from 'common/providers/Permission';
import { useHasPermission } from 'common/providers/Permission'
const Project = require('common/project')

type EditPermissionModalType = {
Expand Down
4 changes: 1 addition & 3 deletions frontend/web/components/FeatureRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,7 @@ class TheComponent extends Component {
<Tag className='chip--xs' tag={Constants.archivedTag} />
)}
</TagValues>
{!!isCompact && (
<StaleFlagWarning projectFlag={projectFlag} />
)}
{!!isCompact && <StaleFlagWarning projectFlag={projectFlag} />}
</Row>
{description && !isCompact && (
<div
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/components/RolePermissionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
useGetRoleProjectPermissionsQuery,
} from 'common/services/useRolePermission'
import { PermissionLevel } from 'common/types/requests'
import { Role, User, UserGroup, UserGroupSummary } from "common/types/responses";
import { Role, User, UserGroup, UserGroupSummary } from 'common/types/responses'
import PanelSearch from './PanelSearch'
import PermissionsSummaryList from './PermissionsSummaryList'

Expand Down