From 084d7756864e72ef020554380e175238c39d5b3b Mon Sep 17 00:00:00 2001 From: Novak Zaballa <41410593+novakzaballa@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:11:09 -0400 Subject: [PATCH] feat: Clone identities (FE) (#3725) --- .../services/useIdentityFeatureState.ts | 49 +++++++++--- frontend/common/stores/organisation-store.js | 15 ++-- frontend/common/types/requests.ts | 9 ++- frontend/common/types/responses.ts | 5 +- frontend/web/components/App.js | 5 +- frontend/web/components/CompareIdentities.tsx | 79 ++++++++++++++++++- frontend/web/components/EditPermissions.tsx | 2 +- frontend/web/components/FeatureRow.js | 4 +- .../web/components/RolePermissionsList.tsx | 2 +- 9 files changed, 144 insertions(+), 26 deletions(-) diff --git a/frontend/common/services/useIdentityFeatureState.ts b/frontend/common/services/useIdentityFeatureState.ts index da795094d45c..ca094ca79eb4 100644 --- a/frontend/common/services/useIdentityFeatureState.ts +++ b/frontend/common/services/useIdentityFeatureState.ts @@ -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()}/${ @@ -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, ), @@ -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 */ diff --git a/frontend/common/stores/organisation-store.js b/frontend/common/stores/organisation-store.js index c1e1cdf85660..977fac78fb43 100644 --- a/frontend/common/stores/organisation-store.js +++ b/frontend/common/stores/organisation-store.js @@ -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') @@ -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) => { @@ -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) diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index d628a37d53f2..03371963ce8d 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -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 } @@ -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 diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 48f7711ed8ab..b224ccd18d1f 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -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 @@ -670,7 +671,7 @@ export type Res = { rolePermission: PagedResponse projectFlags: PagedResponse projectFlag: ProjectFlag - identityFeatureStates: IdentityFeatureState[] + identityFeatureStatesAll: IdentityFeatureState[] createRolesPermissionUsers: RolePermissionUser rolesPermissionUsers: PagedResponse createRolePermissionGroup: RolePermissionGroup @@ -707,5 +708,7 @@ export type Res = { featureImports: PagedResponse serversideEnvironmentKeys: APIKey[] userGroupPermissions: GroupPermission[] + identityFeatureStates: PagedResponse + cloneidentityFeatureStates: IdentityFeatureState // END OF TYPES } diff --git a/frontend/web/components/App.js b/frontend/web/components/App.js index 423830e29043..d177591bce23 100644 --- a/frontend/web/components/App.js +++ b/frontend/web/components/App.js @@ -371,7 +371,10 @@ const App = class extends Component { fill='#9DA4AE' /> - Organisation {AccountStore.getOrganisation()?.name} + Organisation{' '} + + {AccountStore.getOrganisation()?.name} + diff --git a/frontend/web/components/CompareIdentities.tsx b/frontend/web/components/CompareIdentities.tsx index 1ad1e7b725d0..ffbc68a5e55a 100644 --- a/frontend/web/components/CompareIdentities.tsx +++ b/frontend/web/components/CompareIdentities.tsx @@ -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' @@ -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 @@ -66,14 +71,16 @@ const CompareIdentities: FC = ({ 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 @@ -120,6 +127,38 @@ const CompareIdentities: FC = ({ ) } + const cloneIdentityValues = ( + leftIdentityName: string, + rightIdentityName: string, + leftIdentityId: string, + rightIdentityId: string, + environmentId: string, + ) => { + return openConfirm({ + body: ( +
+ {'Cloning '} {leftIdentityName}{' '} + {'will copy any Identity Overrides in '} + {`${rightIdentityName}.`} {'Are you sure?'} +
+ ), + 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 (
@@ -179,9 +218,41 @@ const CompareIdentities: FC = ({ {isReady && ( <> + + {Utils.getFlagsmithHasFeature('clone_identities') && ( + <> + { + cloneIdentityValues( + leftId?.label, + rightId?.label, + leftId?.value, + rightId?.value, + environmentId, + ) + }} + className='ms-2 me-2' + > + {'Clone Features states'} + + } + > + {`Clone the Features states from ${leftId?.label} to ${rightId?.label}`} + + + )} + + } + > )} - {!!isCompact && ( - - )} + {!!isCompact && } {description && !isCompact && (