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: Import export environment flags #3161

Merged
merged 32 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b272bcf
Import export progress
kyle-ssg Dec 7, 2023
2df3f13
Import export progress
kyle-ssg Dec 7, 2023
5482cee
Split import
kyle-ssg Dec 8, 2023
6cf9ec2
Merge branch 'main' into feat/import-export-environment-flags
kyle-ssg Dec 13, 2023
78b872d
Import
kyle-ssg Dec 13, 2023
1d589a8
Import
kyle-ssg Dec 13, 2023
abd40b5
Merge branch 'main' into feat/import-export-environment-flags
kyle-ssg Dec 13, 2023
28793f9
Add import
kyle-ssg Dec 13, 2023
6f87c88
Improve import copy
kyle-ssg Dec 13, 2023
50ce450
Poll imports, make imports admin only
kyle-ssg Dec 15, 2023
81dad29
Add tag strategy for features
kyle-ssg Dec 15, 2023
a9c8606
add tag strategy for export
kyle-ssg Dec 15, 2023
82a99e0
add tag strategy for export
kyle-ssg Dec 15, 2023
1d64459
show default values for other environments
kyle-ssg Dec 15, 2023
d524ab4
QA tweaks
kyle-ssg Dec 18, 2023
87e4ebf
Updates
kyle-ssg Dec 18, 2023
14ba6a1
Overwrite destructive wording
kyle-ssg Dec 18, 2023
30a2c50
Merge branch 'main' into feat/import-export-environment-flags
kyle-ssg Dec 20, 2023
52643e1
Add filtering to import feature
kyle-ssg Dec 21, 2023
e070f72
Add filtering to import feature
kyle-ssg Dec 21, 2023
80407c2
Merge branch 'main' into feat/import-export-environment-flags
kyle-ssg Dec 21, 2023
15bd1e3
Add persist storage to tag strategy
kyle-ssg Dec 21, 2023
d5e07d7
Add import status to butter bar and import page
kyle-ssg Jan 10, 2024
1145eb2
Adjust to feature imports data structure
kyle-ssg Jan 10, 2024
1f31df0
Update frontend/web/components/import-export/FeatureImport.tsx
kyle-ssg Jan 24, 2024
2ed2bbc
Update frontend/web/components/import-export/FeatureExport.tsx
kyle-ssg Jan 24, 2024
157a21d
Adjust copy
kyle-ssg Jan 24, 2024
21f95c5
Merge branch 'main' into feat/import-export-environment-flags
kyle-ssg Jan 25, 2024
e3e74e7
Fix LD import
kyle-ssg Jan 25, 2024
e29704a
Fix type
kyle-ssg Jan 25, 2024
0938c07
Merge branch 'main' into feat/import-export-environment-flags
kyle-ssg Feb 7, 2024
600f862
Fix import preview for new flags
kyle-ssg Feb 7, 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
1 change: 1 addition & 0 deletions frontend/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,4 +494,5 @@ export default {
'#AAC200',
'#DE3163',
],
untaggedTag: { color: '#dedede', label: 'Untagged' },
}
88 changes: 88 additions & 0 deletions frontend/common/services/useFeatureExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'

export const featureExportService = service
.enhanceEndpoints({ addTagTypes: ['FeatureExport'] })
.injectEndpoints({
endpoints: (builder) => ({
createFeatureExport: builder.mutation<
Res['featureExport'],
Req['createFeatureExport']
>({
invalidatesTags: [{ id: 'LIST', type: 'FeatureExport' }],
query: (query: Req['createFeatureExport']) => ({
body: query,
method: 'POST',
url: `features/create-feature-export/`,
}),
}),
getFeatureExport: builder.query<
Res['featureExport'],
Req['getFeatureExport']
>({
providesTags: (res) => [{ id: res?.id, type: 'FeatureExport' }],
query: (query: Req['getFeatureExport']) => ({
url: `features/download-feature-export/${query.id}/`,
}),
}),
getFeatureExports: builder.query<
Res['featureExports'],
Req['getFeatureExports']
>({
providesTags: [{ id: 'LIST', type: 'FeatureExport' }],
query: (query) => ({
url: `projects/${query.projectId}/feature-exports/`,
}),
}),
// END OF ENDPOINTS
}),
})

export async function createFeatureExport(
store: any,
data: Req['createFeatureExport'],
options?: Parameters<
typeof featureExportService.endpoints.createFeatureExport.initiate
>[1],
) {
return store.dispatch(
featureExportService.endpoints.createFeatureExport.initiate(data, options),
)
}
export async function getFeatureExport(
store: any,
data: Req['getFeatureExport'],
options?: Parameters<
typeof featureExportService.endpoints.getFeatureExport.initiate
>[1],
) {
return store.dispatch(
featureExportService.endpoints.getFeatureExport.initiate(data, options),
)
}
export async function getFeatureExports(
store: any,
data: Req['getFeatureExports'],
options?: Parameters<
typeof featureExportService.endpoints.getFeatureExports.initiate
>[1],
) {
return store.dispatch(
featureExportService.endpoints.getFeatureExports.initiate(data, options),
)
}
// END OF FUNCTION_EXPORTS

export const {
useCreateFeatureExportMutation,
useGetFeatureExportQuery,
useGetFeatureExportsQuery,
// END OF EXPORTS
} = featureExportService

/* Usage examples:
const { data, isLoading } = useGetFeatureExportQuery({ id: 2 }, {}) //get hook
const [createFeatureExport, { isLoading, data, isSuccess }] = useCreateFeatureExportMutation() //create hook
featureExportService.endpoints.getFeatureExport.select({id: 2})(store.getState()) //access data from any function
*/
44 changes: 44 additions & 0 deletions frontend/common/services/useFeatureImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'

export const featureImportService = service
.enhanceEndpoints({ addTagTypes: ['FeatureImport'] })
.injectEndpoints({
endpoints: (builder) => ({
getFeatureImports: builder.query<
Res['featureImports'],
Req['getFeatureImports']
>({
providesTags: [{ id: 'LIST', type: 'FeatureImport' }],
query: (query) => ({
url: `projects/${query.projectId}/feature-imports/`,
}),
}),
// END OF ENDPOINTS
}),
})

export async function getFeatureImports(
store: any,
data: Req['getFeatureImports'],
options?: Parameters<
typeof featureImportService.endpoints.getFeatureImports.initiate
>[1],
) {
return Promise.all(
store.dispatch(featureImportService.util.getRunningQueriesThunk()),
)
}
// END OF FUNCTION_EXPORTS

export const {
useGetFeatureImportsQuery,
// END OF EXPORTS
} = featureImportService

/* Usage examples:
const { data, isLoading } = useGetFeatureImportsQuery({ id: 2 }, {}) //get hook
const [createFeatureImports, { isLoading, data, isSuccess }] = useCreateFeatureImportsMutation() //create hook
featureImportService.endpoints.getFeatureImports.select({id: 2})(store.getState()) //access data from any function
*/
59 changes: 59 additions & 0 deletions frontend/common/services/useFlagsmithProjectImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'
import toFormData from 'common/utils/toFormData'

export const flagsmithProjectImportService = service
.enhanceEndpoints({ addTagTypes: ['FlagsmithProjectImport'] })
.injectEndpoints({
endpoints: (builder) => ({
createFlagsmithProjectImport: builder.mutation<
Res['flagsmithProjectImport'],
Req['createFlagsmithProjectImport']
>({
invalidatesTags: [{ id: 'LIST', type: 'FlagsmithProjectImport' }],
queryFn: async (query, baseQueryApi, extraOptions, baseQuery) => {
const { environment_id, ...rest } = query
const formData = toFormData({ ...rest })

const { data, error } = await baseQuery({
body: formData,
method: 'POST',
url: `features/feature-import/${environment_id}`,
})
return { data, error }
},
}),
// END OF ENDPOINTS
}),
})

export async function createFlagsmithProjectImport(
store: any,
data: Req['createFlagsmithProjectImport'],
options?: Parameters<
typeof flagsmithProjectImportService.endpoints.createFlagsmithProjectImport.initiate
>[1],
) {
store.dispatch(
flagsmithProjectImportService.endpoints.createFlagsmithProjectImport.initiate(
data,
options,
),
)
return Promise.all(
store.dispatch(flagsmithProjectImportService.util.getRunningQueriesThunk()),
)
}
// END OF FUNCTION_EXPORTS

export const {
useCreateFlagsmithProjectImportMutation,
// END OF EXPORTS
} = flagsmithProjectImportService

/* Usage examples:
const { data, isLoading } = useGetFlagsmithProjectImportQuery({ id: 2 }, {}) //get hook
const [createFlagsmithProjectImport, { isLoading, data, isSuccess }] = useCreateFlagsmithProjectImportMutation() //create hook
flagsmithProjectImportService.endpoints.getFlagsmithProjectImport.select({id: 2})(store.getState()) //access data from any function
*/
5 changes: 4 additions & 1 deletion frontend/common/services/useGroupWithRole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ export const groupWithRoleService = service
Res['groupWithRole'],
Req['deleteGroupWithRole']
>({
invalidatesTags: [{ type: 'GroupWithRole' }, { type: 'RolePermissionGroup' }],
invalidatesTags: [
{ type: 'GroupWithRole' },
{ type: 'RolePermissionGroup' },
],
query: (query: Req['deleteGroupWithRole']) => ({
body: query,
method: 'DELETE',
Expand Down
5 changes: 1 addition & 4 deletions frontend/common/services/useRolesUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ export const rolesUserService = service
Res['rolesUsers'],
Req['deleteRolesPermissionUsers']
>({
invalidatesTags: [
{ type: 'User-role' },
{ type: 'RolesUser' },
],
invalidatesTags: [{ type: 'User-role' }, { type: 'RolesUser' }],
query: (query: Req['deleteRolesPermissionUsers']) => ({
body: query,
method: 'DELETE',
Expand Down
2 changes: 2 additions & 0 deletions frontend/common/stores/project-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ const controller = {
const store = Object.assign({}, BaseStore, {
getEnvironment: (api_key) =>
store.model && _.find(store.model.environments, { api_key }),
getEnvironmentById: (id) =>
store.model && _.find(store.model.environments, { id }),
getEnvironmentIdFromKey: (api_key) => {
const env = _.find(store.model.environments, { api_key })
return env && env.id
Expand Down
27 changes: 26 additions & 1 deletion frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Account, Segment, Tag, FeatureStateValue, Role } from './responses'
import {
Account,
Segment,
Tag,
FeatureStateValue,
Role,
ImportStrategy,
} from './responses'

export type PagedRequest<T> = T & {
page?: number
Expand Down Expand Up @@ -139,6 +146,24 @@ export type Req = {
token: string
}
}
createFeatureExport: {
environment_id: string
tag_ids?: (number | string)[]
}
getFeatureExport: {
id: string
}
getFeatureExports: {
projectId: string
}
createFlagsmithProjectImport: {
environment_id: number | string
strategy: ImportStrategy
file: File
}
getFeatureImports: {
projectId: string
}
getLaunchDarklyProjectImport: { project_id: string; import_id: string }
getLaunchDarklyProjectsImport: { project_id: string }
getUserWithRoles: { org_id: string; user_id: string }
Expand Down
44 changes: 39 additions & 5 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export type Environment = {
name: string
api_key: string
description?: string
banner_text?: string
banner_colour?: string
project: number
minimum_change_request_approvals?: number
allow_client_traits: boolean
Expand All @@ -83,7 +85,34 @@ export type Project = {
total_segments?: number
environments: Environment[]
}
export type ImportStrategy = 'SKIP' | 'OVERWRITE_DESTRUCTIVE'

export type ImportExportStatus = 'SUCCESS' | 'PROCESSING' | 'FAILED'

export type FeatureImport = {
id: number
status: ImportExportStatus
strategy: string
environment_id: number
created_at: string
}

export type FeatureExport = {
id: string
name: string
environment_id: string
status: ImportExportStatus
created_at: string
}
export type FeatureImportItem = {
name: string
default_enabled: boolean
is_server_key_only: boolean
initial_value: FlagsmithValue
value: FlagsmithValue
enabled: false
multivariate: []
}
export type LaunchDarklyProjectImport = {
id: number
created_by: string
Expand Down Expand Up @@ -228,6 +257,7 @@ export type MultivariateOption = {
}

export type FeatureType = 'STANDARD' | 'MULTIVARIATE'
export type TagStrategy = 'INTERSECTION' | 'UNION'

export type IdentityFeatureState = {
feature: {
Expand All @@ -248,7 +278,7 @@ export type IdentityFeatureState = {

export type FeatureState = {
id: number
feature_state_value: string
feature_state_value: FlagsmithValue
multivariate_feature_state_values: MultivariateFeatureStateValue[]
identity?: string
uuid: string
Expand All @@ -265,11 +295,11 @@ export type FeatureState = {
}

export type ProjectFlag = {
created_date: Date
created_date: string
default_enabled: boolean
description?: string
id: number
initial_value: string
initial_value: FlagsmithValue
is_archived: boolean
is_server_key_only: boolean
multivariate_options: MultivariateOption[]
Expand Down Expand Up @@ -400,10 +430,14 @@ export type Res = {
environment: Environment
launchDarklyProjectImport: LaunchDarklyProjectImport
launchDarklyProjectsImport: LaunchDarklyProjectImport[]
userWithRoles: PagedResponse<Roles>
groupWithRole: PagedResponse<Roles>
userWithRoles: PagedResponse<Role>
groupWithRole: PagedResponse<Role>
changeRequests: PagedResponse<ChangeRequestSummary>
groupSummaries: UserGroupSummary[]
auditLogItem: AuditLogDetail
featureExport: { id: string }
featureExports: PagedResponse<FeatureExport>
flagsmithProjectImport: { id: string }
featureImports: PagedResponse<FeatureImport>
// END OF TYPES
}
Loading
Loading