Skip to content

Commit

Permalink
feat: LaunchDarkly importer UI (#2837)
Browse files Browse the repository at this point in the history
Co-authored-by: Kim Gustyr <kim.gustyr@flagsmith.com>
  • Loading branch information
novakzaballa and khvn26 committed Oct 17, 2023
1 parent 7964e49 commit a78eeaf
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 4 deletions.
8 changes: 5 additions & 3 deletions frontend/common/data/base/_data.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Project from 'common/project'
const getQueryString = (params) => {
const esc = encodeURIComponent
return Object.keys(params)
Expand All @@ -6,7 +7,7 @@ const getQueryString = (params) => {
}

module.exports = {
_request(method, _url, data, headers = {}, isExternal) {
_request(method, _url, data, headers = {}) {
const options = {
headers: {
'Accept': 'application/json',
Expand All @@ -15,6 +16,7 @@ module.exports = {
method,
timeout: 60000,
}
const isExternal = !_url.startsWith(Project.api)

if (method !== 'get')
options.headers['Content-Type'] = 'application/json; charset=utf-8'
Expand Down Expand Up @@ -78,8 +80,8 @@ module.exports = {
return this._request('get', url, data || null, headers)
},

post(url, data, headers, isExternal = false) {
return this._request('post', url, data, headers, isExternal)
post(url, data, headers) {
return this._request('post', url, data, headers)
},

put(url, data, headers) {
Expand Down
94 changes: 94 additions & 0 deletions frontend/common/services/useLaunchDarklyProjectImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'

export const launchDarklyService = service
.enhanceEndpoints({ addTagTypes: ['launchDarklyProjectImport'] })
.injectEndpoints({
endpoints: (builder) => ({
createLaunchDarklyProjectImport: builder.mutation<
Res['launchDarklyProjectImport'],
Req['createLaunchDarklyProjectImport']
>({
invalidatesTags: [{ id: 'LIST', type: 'launchDarklyProjectImport' }],
query: (query: Req['createLaunchDarklyProjectImport']) => ({
body: query.body,
method: 'POST',
url: `projects/${query.project_id}/imports/launch-darkly/`,
}),
}),
getLaunchDarklyProjectImport: builder.query<
Res['launchDarklyProjectImport'],
Req['getLaunchDarklyProjectImport']
>({
providesTags: [{ id: 'LIST', type: 'launchDarklyProjectImport' }],
query: (query) => ({
url: `projects/${query.project_id}/imports/launch-darkly/${query.import_id}/`,
}),
}),
getLaunchDarklyProjectsImport: builder.query<
Res['launchDarklyProjectsImport'],
Req['getLaunchDarklyProjectsImport']
>({
providesTags: [{ id: 'LIST', type: 'launchDarklyProjectImport' }],
query: (query) => ({
url: `projects/${query.project_id}/imports/launch-darkly/`,
}),
}),
// END OF ENDPOINTS
}),
})

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

export const {
useCreateLaunchDarklyProjectImportMutation,
useGetLaunchDarklyProjectImportQuery,
useGetLaunchDarklyProjectsImportQuery,
// END OF EXPORTS
} = launchDarklyService

/* Usage examples:
const { data, isLoading } = useGetLaunchDarklyProjectQuery({ id: 2 }, {}) //get hook
const [createLaunchDarklyProjectImport, { isLoading, data, isSuccess }] = useCreateLaunchDarklyProjectImportMutation() //create hook
launchDarklyService.endpoints.getLaunchDarklyProjectImport.select({id: 2})(store.getState()) //access data from any function
*/
3 changes: 3 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,8 @@ export type Req = {
getGetSubscriptionMetadata: { id: string }
getEnvironment: { id: string }
getSubscriptionMetadata: { id: string }
createLaunchDarklyProjectImport: { project_id: string }
getLaunchDarklyProjectImport: { project_id: string }
getLaunchDarklyProjectsImport: { project_id: string; import_id: string }
// END OF TYPES
}
17 changes: 17 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ export type Project = {
environments: Environment[]
}

export type LaunchDarklyProjectImport = {
id: number
created_by: string
created_at: string
updated_at: string
completed_at: string
status: {
requested_environment_count: number
requested_flag_count: number
result: string || null
error_message: string || null
},
project: number
}

export type User = {
id: number
email: string
Expand Down Expand Up @@ -344,5 +359,7 @@ export type Res = {
identityFeatureStates: IdentityFeatureState[]
getSubscriptionMetadata: { id: string }
environment: Environment
launchDarklyProjectImport: LaunchDarklyProjectImport
launchDarklyProjectsImport: LaunchDarklyProjectImport[]
// END OF TYPES
}
2 changes: 1 addition & 1 deletion frontend/web/components/TestWebhook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const TestWebhook: FC<TestWebhookType> = ({ json, webhook }) => {
setLoading(true)
setSuccess(false)
data
.post(webhook, JSON.parse(json), null, true)
.post(webhook, JSON.parse(json), null)
.then(() => {
setLoading(false)
setSuccess(true)
Expand Down
176 changes: 176 additions & 0 deletions frontend/web/components/pages/ImportPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import React, { useEffect, useState } from 'react'
import _data from 'common/data/base/_data'
import {
useCreateLaunchDarklyProjectImportMutation,
useGetLaunchDarklyProjectImportQuery,
} from 'common/services/useLaunchDarklyProjectImport'
import AppLoader from 'components/AppLoader'

type ImportPageType = {
projectId: string
projectName: string
}

const ImportPage: FC<ImportPageType> = ({ projectId, projectName }) => {
const [LDKey, setLDKey] = useState<string>('')
const [importId, setImportId] = useState<string>('')
const [isLoading, setIsLoading] = useState<boolean>(false)
const [isAppLoading, setAppIsLoading] = useState<boolean>(false)
const [projects, setProjects] = useState<string>([])
const [createLaunchDarklyProjectImport, { data, isSuccess }] =
useCreateLaunchDarklyProjectImportMutation()

const {
data: status,
isSuccess: statusLoaded,
refetch,
} = useGetLaunchDarklyProjectImportQuery({
import_id: importId,
project_id: projectId,
})

useEffect(() => {
const checkImportStatus = async () => {
setAppIsLoading(true)
const intervalId = setInterval(async () => {
await refetch()

if (statusLoaded && status && status.status.result === 'success') {
clearInterval(intervalId)
setAppIsLoading(false)
window.location.reload()
}
}, 1000)
}

if (statusLoaded) {
checkImportStatus()
}
}, [statusLoaded, status, refetch])

useEffect(() => {
if (isSuccess) {
setImportId(data.id)
refetch()
}
}, [isSuccess, data, refetch])

const getProjectList = (LDKey: string) => {
setIsLoading(true)
_data
.get(`https://app.launchdarkly.com/api/v2/projects`, '', {
'Authorization': LDKey,
})
.then((res) => {
setIsLoading(false)
setProjects(res.items)
})
}

const createImportLDProjects = (LDKey: string, projectId: string) => {
createLaunchDarklyProjectImport({
body: { project_key: 'default', token: LDKey },
project_id: projectId,
})
}

return (
<>
{isAppLoading && (
<div className='overlay'>
<div className='title'>Importing Project</div>
<AppLoader />
</div>
)}
<div className='mt-4'>
<h5>Import LaunchDarkly Projects</h5>
<label>Set LaunchDarkly key</label>
<FormGroup>
<Row className='align-items-start col-md-8'>
<Flex className='ml-0'>
<Input
value={LDKey}
name='ldkey'
onChange={(e) => setLDKey(Utils.safeParseEventValue(e))}
type='text'
placeholder='My LaunchDarkly key'
/>
</Flex>
<Button
id='save-proj-btn'
disabled={!LDKey}
className='ml-3'
onClick={() => getProjectList(LDKey)}
>
{'Next'}
</Button>
</Row>
</FormGroup>
{isLoading ? (
<div className='text-center'>
<Loader />
</div>
) : (
projects.length > 0 && (
<div>
<FormGroup>
<PanelSearch
id='projects-list'
className='no-pad panel-projects'
listClassName='row mt-n2 gy-4'
title='Launch Darkly Projects'
items={projects}
renderRow={({ name }, i) => {
return (
<>
<Button
className='btn-project'
onClick={() =>
openConfirm(
'Import LaunchDarkly project',
<div>
{`Are you sure you want import ${name} to ${projectName}`}
</div>,
() => {
createImportLDProjects(LDKey, projectId)
},
() => {
return
},
)
}
>
<Row className='flex-nowrap'>
<h2
style={{
backgroundColor: Utils.getProjectColour(i),
}}
className='btn-project-letter mb-0'
>
{name[0]}
</h2>
<div className='font-weight-medium btn-project-title'>
{name}
</div>
</Row>
</Button>
</>
)
}}
renderNoResults={
<div>
<Row>
<div className='font-weight-medium'>No Projects</div>
</Row>
</div>
}
/>
</FormGroup>
</div>
)
)}
</div>
</>
)
}
export default ImportPage
9 changes: 9 additions & 0 deletions frontend/web/components/pages/ProjectSettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Constants from 'common/constants'
import JSONReference from 'components/JSONReference'
import PageTitle from 'components/PageTitle'
import Icon from 'components/Icon'
import ImportPage from './ImportPage'

const ProjectSettingsPage = class extends Component {
static displayName = 'ProjectSettingsPage'
Expand Down Expand Up @@ -463,6 +464,14 @@ const ProjectSettingsPage = class extends Component {
level='project'
/>
</TabItem>
{Utils.getFlagsmithHasFeature('import_project') && (
<TabItem data-test='js-import-page' tabLabel='Import'>
<ImportPage
projectId={this.props.match.params.projectId}
projectName={project.name}
/>
</TabItem>
)}
</Tabs>
}
</div>
Expand Down
1 change: 1 addition & 0 deletions frontend/web/styles/project/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
@import "spacing-utils";
@import "tooltips";
@import "base";
@import "overlay";
15 changes: 15 additions & 0 deletions frontend/web/styles/project/_overlay.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.544);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
.title{
color: #fff
}
}

3 comments on commit a78eeaf

@vercel
Copy link

@vercel vercel bot commented on a78eeaf Oct 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on a78eeaf Oct 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on a78eeaf Oct 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./docs

docs-flagsmith.vercel.app
docs-git-main-flagsmith.vercel.app
docs.bullet-train.io
docs.flagsmith.com

Please sign in to comment.