diff --git a/frontend/common/data/base/_data.js b/frontend/common/data/base/_data.js index c1613a07fd5b..17c7e529d13a 100644 --- a/frontend/common/data/base/_data.js +++ b/frontend/common/data/base/_data.js @@ -1,3 +1,4 @@ +import Project from 'common/project' const getQueryString = (params) => { const esc = encodeURIComponent return Object.keys(params) @@ -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', @@ -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' @@ -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) { diff --git a/frontend/common/services/useLaunchDarklyProjectImport.ts b/frontend/common/services/useLaunchDarklyProjectImport.ts new file mode 100644 index 000000000000..0f867df32d08 --- /dev/null +++ b/frontend/common/services/useLaunchDarklyProjectImport.ts @@ -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 +*/ diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index af3ad316bc30..039802097876 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -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 } diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 843f9a2c1c83..8d16bec8eda2 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -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 @@ -344,5 +359,7 @@ export type Res = { identityFeatureStates: IdentityFeatureState[] getSubscriptionMetadata: { id: string } environment: Environment + launchDarklyProjectImport: LaunchDarklyProjectImport + launchDarklyProjectsImport: LaunchDarklyProjectImport[] // END OF TYPES } diff --git a/frontend/web/components/TestWebhook.tsx b/frontend/web/components/TestWebhook.tsx index bc287548ee0b..53d6f0b2b25b 100644 --- a/frontend/web/components/TestWebhook.tsx +++ b/frontend/web/components/TestWebhook.tsx @@ -19,7 +19,7 @@ const TestWebhook: FC = ({ 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) diff --git a/frontend/web/components/pages/ImportPage.tsx b/frontend/web/components/pages/ImportPage.tsx new file mode 100644 index 000000000000..e2f7be5c8ea8 --- /dev/null +++ b/frontend/web/components/pages/ImportPage.tsx @@ -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 = ({ projectId, projectName }) => { + const [LDKey, setLDKey] = useState('') + const [importId, setImportId] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [isAppLoading, setAppIsLoading] = useState(false) + const [projects, setProjects] = useState([]) + 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 && ( +
+
Importing Project
+ +
+ )} +
+
Import LaunchDarkly Projects
+ + + + + setLDKey(Utils.safeParseEventValue(e))} + type='text' + placeholder='My LaunchDarkly key' + /> + + + + + {isLoading ? ( +
+ +
+ ) : ( + projects.length > 0 && ( +
+ + { + return ( + <> + + + ) + }} + renderNoResults={ +
+ +
No Projects
+
+
+ } + /> +
+
+ ) + )} +
+ + ) +} +export default ImportPage diff --git a/frontend/web/components/pages/ProjectSettingsPage.js b/frontend/web/components/pages/ProjectSettingsPage.js index 028a4a2a8758..37cb388ebb08 100644 --- a/frontend/web/components/pages/ProjectSettingsPage.js +++ b/frontend/web/components/pages/ProjectSettingsPage.js @@ -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' @@ -463,6 +464,14 @@ const ProjectSettingsPage = class extends Component { level='project' /> + {Utils.getFlagsmithHasFeature('import_project') && ( + + + + )} } diff --git a/frontend/web/styles/project/_index.scss b/frontend/web/styles/project/_index.scss index cae9edc8e934..7596b660a021 100644 --- a/frontend/web/styles/project/_index.scss +++ b/frontend/web/styles/project/_index.scss @@ -18,3 +18,4 @@ @import "spacing-utils"; @import "tooltips"; @import "base"; +@import "overlay"; diff --git a/frontend/web/styles/project/_overlay.scss b/frontend/web/styles/project/_overlay.scss new file mode 100644 index 000000000000..2ce5877f36c5 --- /dev/null +++ b/frontend/web/styles/project/_overlay.scss @@ -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 + } +} \ No newline at end of file