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: LaunchDarkly importer UI #2837

Merged
merged 7 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions frontend/common/data/base/_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ module.exports = {
return this._request('delete', url, data, headers)
},

get(url, data, headers) {
return this._request('get', url, data || null, headers)
get(url, data, headers, isExternal = false) {
Copy link
Member

@kyle-ssg kyle-ssg Oct 17, 2023

Choose a reason for hiding this comment

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

I think I mentioned this in another PR, but we can just base this on if the url is Project.api in _request

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

return this._request('get', url, data || null, headers, isExternal)
},

post(url, data, headers, isExternal = false) {
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
}
181 changes: 181 additions & 0 deletions frontend/web/components/pages/ImportPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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,
},
true,
)
.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 Prokects</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
}
}