From f624223afcf77fa709150c2195e94991d3488d50 Mon Sep 17 00:00:00 2001 From: Novak Zaballa <41410593+novakzaballa@users.noreply.github.com> Date: Fri, 17 May 2024 14:37:23 -0400 Subject: [PATCH] fix: Improve the UI/UX for GitHub integrations (#3907) --- frontend/common/constants.ts | 8 + frontend/common/types/responses.ts | 6 +- .../components/DeleteGithubIntegracion.tsx | 54 ------ .../components/DeleteGithubIntegration.tsx | 64 +++++++ .../components/ExternalResourcesLinkTab.tsx | 162 +++++++++++++++++ .../web/components/ExternalResourcesTable.tsx | 169 ++++++++++++------ .../components/GitHubRepositoriesSelect.tsx | 38 ++-- .../components/GithubRepositoriesTable.tsx | 16 +- frontend/web/components/Icon.tsx | 22 +++ frontend/web/components/IntegrationList.js | 45 +++-- .../MyGitHubRepositoriesComponent.tsx | 97 ++++++++++ .../components/MyGitHubRepositoriesSelect.tsx | 32 ---- frontend/web/components/MyIssuesSelect.tsx | 26 ++- .../web/components/MyPullRequestsSelect.tsx | 28 ++- .../web/components/MyRepositoriesSelect.tsx | 26 ++- frontend/web/components/ProjectFilter.tsx | 11 +- .../web/components/RepositoriesSelect.tsx | 6 +- .../modals/CreateEditIntegrationModal.js | 33 +--- frontend/web/components/modals/CreateFlag.js | 144 +++------------ .../web/components/pages/GitHubSetupPage.tsx | 41 +++-- 20 files changed, 652 insertions(+), 376 deletions(-) delete mode 100644 frontend/web/components/DeleteGithubIntegracion.tsx create mode 100644 frontend/web/components/DeleteGithubIntegration.tsx create mode 100644 frontend/web/components/ExternalResourcesLinkTab.tsx create mode 100644 frontend/web/components/MyGitHubRepositoriesComponent.tsx delete mode 100644 frontend/web/components/MyGitHubRepositoriesSelect.tsx diff --git a/frontend/common/constants.ts b/frontend/common/constants.ts index 9b81a859db23..a707d529cabe 100644 --- a/frontend/common/constants.ts +++ b/frontend/common/constants.ts @@ -435,6 +435,10 @@ export default { 'TRAITS_ID': 150, }, }, + githubType: { + githubIssue: 'GitHub Issue', + githubPR: 'Github PR', + }, modals: { 'PAYMENT': 'Payment Modal', }, @@ -472,6 +476,10 @@ export default { ], projectPermissions: (perm: string) => `To use this feature you need the ${perm} permission for this project.
Please contact a member of this project who has administrator privileges.`, + resourceTypes: { + GITHUB_ISSUE: { id: 1, label: 'GitHub Issue', type: 'GITHUB' }, + GITHUB_PR: { id: 2, label: 'GitHub PR', type: 'GITHUB' }, + }, roles: { 'ADMIN': 'Organisation Administrator', 'USER': 'User', diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index ab32cd6b99e4..f5e52816c3cd 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -111,8 +111,8 @@ export type ExternalResource = { id?: number url: string type: string - project: number - status: null | string + project?: number + metadata: null | { status: string } feature: number } @@ -769,7 +769,7 @@ export type Res = { supportedContentType: ContentType[] externalResource: PagedResponse githubIntegrations: PagedResponse - githubRepository: PagedResponse | { data: { id: string } } + githubRepository: PagedResponse githubIssues: Issue[] githubPulls: PullRequest[] githubRepos: GithubPaginatedRepos diff --git a/frontend/web/components/DeleteGithubIntegracion.tsx b/frontend/web/components/DeleteGithubIntegracion.tsx deleted file mode 100644 index 1131ed375ef8..000000000000 --- a/frontend/web/components/DeleteGithubIntegracion.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { FC } from 'react' -import Button from './base/forms/Button' -import { useDeleteGithubIntegrationMutation } from 'common/services/useGithubIntegration' - -type DeleteGithubIntegracionType = { - organisationId: string - githubId: string - onConfirm: () => void -} - -const DeleteGithubIntegracion: FC = ({ - githubId, - onConfirm, - organisationId, -}) => { - const [deleteGithubIntegration] = useDeleteGithubIntegrationMutation() - - return ( - - ) -} - -export default DeleteGithubIntegracion diff --git a/frontend/web/components/DeleteGithubIntegration.tsx b/frontend/web/components/DeleteGithubIntegration.tsx new file mode 100644 index 000000000000..b740e3e4b84d --- /dev/null +++ b/frontend/web/components/DeleteGithubIntegration.tsx @@ -0,0 +1,64 @@ +import React, { FC } from 'react' +import Button from './base/forms/Button' +import { useDeleteGithubIntegrationMutation } from 'common/services/useGithubIntegration' + +type DeleteGithubIntegrationType = { + organisationId: string + githubId: string +} + +const DeleteGithubIntegration: FC = ({ + githubId, + organisationId, +}) => { + const [deleteGithubIntegration] = useDeleteGithubIntegrationMutation() + + return ( + + + + , + ) + } + size='small' + > + Delete Integration + + ) +} + +export default DeleteGithubIntegration diff --git a/frontend/web/components/ExternalResourcesLinkTab.tsx b/frontend/web/components/ExternalResourcesLinkTab.tsx new file mode 100644 index 000000000000..8a78df862643 --- /dev/null +++ b/frontend/web/components/ExternalResourcesLinkTab.tsx @@ -0,0 +1,162 @@ +import React, { FC, useState } from 'react' +import MyRepositoriesSelect from './MyRepositoriesSelect' +import ExternalResourcesTable, { + ExternalResourcesTableType, +} from './ExternalResourcesTable' +import { ExternalResource } from 'common/types/responses' +import MyIssuesSelect from './MyIssuesSelect' +import MyPullRequestsSelect from './MyPullRequestsSelect' +import { useCreateExternalResourceMutation } from 'common/services/useExternalResource' +import Constants from 'common/constants' +import Button from './base/forms/Button' + +type ExternalResourcesLinkTabType = { + githubId: string + organisationId: string + featureId: string + projectId: string +} + +type AddExternalResourceRowType = ExternalResourcesTableType & { + linkedExternalResources?: ExternalResource[] +} + +type GitHubStatusType = { + value: number + label: string +} + +const AddExternalResourceRow: FC = ({ + featureId, + linkedExternalResources, + organisationId, + projectId, + repoName, + repoOwner, +}) => { + const [externalResourceType, setExternalResourceType] = useState('') + const [featureExternalResource, setFeatureExternalResource] = + useState('') + + const [createExternalResource] = useCreateExternalResourceMutation() + const githubTypes = Object.values(Constants.resourceTypes).filter( + (v) => v.type === 'GITHUB', + ) + return ( + + + { const repoData = r.value.split('/') - setRepositoryName(repoData[0]) - setRepositoryOwner(repoData[1]) + createGithubRepository({ + body: { + project: projectId, + repository_name: repoData[1], + repository_owner: repoData[0], + }, + github_id: githubId, + organisation_id: organisationId, + }) }} options={repositories?.map((r: Repository) => { return { @@ -54,25 +57,6 @@ const GitHubRepositoriesSelect: FC = ({ } })} /> -
- -
) } diff --git a/frontend/web/components/GithubRepositoriesTable.tsx b/frontend/web/components/GithubRepositoriesTable.tsx index bffc0f21d02d..d8fdc7f4eaf0 100644 --- a/frontend/web/components/GithubRepositoriesTable.tsx +++ b/frontend/web/components/GithubRepositoriesTable.tsx @@ -1,14 +1,12 @@ import React, { FC, useEffect } from 'react' -import { - useDeleteGithubRepositoryMutation, - useGetGithubRepositoriesQuery, -} from 'common/services/useGithubRepository' +import { useDeleteGithubRepositoryMutation } from 'common/services/useGithubRepository' import Button from './base/forms/Button' import Icon from './Icon' import PanelSearch from './PanelSearch' import { GithubRepository } from 'common/types/responses' export type GithubRepositoriesTableType = { + repos: GithubRepository[] | undefined githubId: string organisationId: string } @@ -16,12 +14,8 @@ export type GithubRepositoriesTableType = { const GithubRepositoriesTable: FC = ({ githubId, organisationId, + repos, }) => { - const { data } = useGetGithubRepositoriesQuery({ - github_id: githubId, - organisation_id: organisationId, - }) - const [deleteGithubRepository, { isSuccess: isDeleted }] = useDeleteGithubRepositoryMutation() @@ -34,9 +28,9 @@ const GithubRepositoriesTable: FC = ({ return (
Repository diff --git a/frontend/web/components/Icon.tsx b/frontend/web/components/Icon.tsx index db7a2542fca2..dd4bdd56969d 100644 --- a/frontend/web/components/Icon.tsx +++ b/frontend/web/components/Icon.tsx @@ -54,6 +54,7 @@ export type IconName = | 'people' | 'required' | 'more-vertical' + | 'open-external-link' export type IconType = React.DetailedHTMLProps< React.HTMLAttributes, @@ -1040,6 +1041,27 @@ const Icon: FC = ({ fill, fill2, height, name, width, ...rest }) => { ) } + case 'open-external-link': { + return ( + + + + + + ) + } case 'arrow-right': { return ( { - this.setState({ - githubId: res?.data?.results[0]?.id, - hasIntegrationWithGithub: !!res?.data?.results?.length, - installationId: res?.data?.results[0]?.installation_id, - }) - }) + this.fetchGithubIntegration() } } + fetchGithubIntegration = () => { + getGithubIntegration(getStore(), { + organisation_id: AccountStore.getOrganisation().id, + }).then((res) => { + this.setState({ + githubId: res?.data?.results[0]?.id, + hasIntegrationWithGithub: !!res?.data?.results?.length, + installationId: res?.data?.results[0]?.installation_id, + }) + }) + } + fetch = () => { const integrationList = Utils.getFlagsmithValue('integration_data') && @@ -271,11 +276,13 @@ class IntegrationList extends Component { return allItems }) } - return _data - .get( - `${Project.api}projects/${this.props.projectId}/integrations/${key}/`, - ) - .catch(() => {}) + if (key !== 'github') { + return _data + .get( + `${Project.api}projects/${this.props.projectId}/integrations/${key}/`, + ) + .catch(() => {}) + } } }), ).then((res) => { @@ -364,7 +371,7 @@ class IntegrationList extends Component { } githubMeta={{ githubId: githubId, installationId: installationId }} projectId={this.props.projectId} - onComplete={this.fetch} + onComplete={githubId ? this.fetch : this.fetchGithubIntegration} />, 'side-modal', ) @@ -391,7 +398,11 @@ class IntegrationList extends Component { JSON.parse(Utils.getFlagsmithValue('integration_data')) return (
-
+
{ + this.fetchGithubIntegration() + }} + > {this.props.integrations && !this.state.isLoading && this.state.activeIntegrations && diff --git a/frontend/web/components/MyGitHubRepositoriesComponent.tsx b/frontend/web/components/MyGitHubRepositoriesComponent.tsx new file mode 100644 index 000000000000..1c5e1c43dd45 --- /dev/null +++ b/frontend/web/components/MyGitHubRepositoriesComponent.tsx @@ -0,0 +1,97 @@ +import { FC, useEffect, useState } from 'react' +import { useGetGithubReposQuery } from 'common/services/useGithub' +import { useGetGithubRepositoriesQuery } from 'common/services/useGithubRepository' +import Button from './base/forms/Button' +import GitHubRepositoriesSelect from './GitHubRepositoriesSelect' +import GithubRepositoriesTable from './GithubRepositoriesTable' +import DeleteGithubIntegration from './DeleteGithubIntegration' +import { Repository } from 'common/types/responses' + +type MyGitHubRepositoriesComponentType = { + installationId: string + organisationId: string + projectId: string + githubId: string + openGitHubWinInstallations: () => void +} + +const MyGitHubRepositoriesComponent: FC = ({ + githubId, + installationId, + openGitHubWinInstallations, + organisationId, + projectId, +}) => { + const [reposSelect, setReposSelect] = useState([]) + const { data: reposFromGithub, isLoading: fetchingReposGH } = + useGetGithubReposQuery({ + installation_id: installationId, + organisation_id: organisationId, + }) + + const { data: GithubReposFromFlagsmith, isLoading: fetchingReposFlagsmith } = + useGetGithubRepositoriesQuery({ + github_id: githubId, + organisation_id: organisationId, + }) + + useEffect(() => { + if (reposFromGithub && GithubReposFromFlagsmith) { + setReposSelect( + reposFromGithub.repositories.filter((repo) => { + const same = GithubReposFromFlagsmith.results.some( + (res) => repo.name === res.repository_name, + ) + return !same + }), + ) + } + }, [reposFromGithub, GithubReposFromFlagsmith]) + + return ( + <> + {fetchingReposGH || fetchingReposFlagsmith ? ( +
+ +
+ ) : ( + <> + {!!reposSelect.length && ( + <> +
Add Your Repository
+ + + )} + + +
+ + +
+ + )} + + ) +} + +export default MyGitHubRepositoriesComponent diff --git a/frontend/web/components/MyGitHubRepositoriesSelect.tsx b/frontend/web/components/MyGitHubRepositoriesSelect.tsx deleted file mode 100644 index 0bfe00a593e1..000000000000 --- a/frontend/web/components/MyGitHubRepositoriesSelect.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { FC } from 'react' -import { useGetGithubReposQuery } from 'common/services/useGithub' -import GitHubRepositoriesSelect from './GitHubRepositoriesSelect' - -type MyGitHubRepositoriesSelectType = { - installationId: string - organisationId: string - projectId: string - githubId: string -} - -const MyGitHubRepositoriesSelect: FC = ({ - githubId, - installationId, - organisationId, - projectId, -}) => { - const { data } = useGetGithubReposQuery({ - installation_id: installationId, - organisation_id: organisationId, - }) - return ( - - ) -} - -export default MyGitHubRepositoriesSelect diff --git a/frontend/web/components/MyIssuesSelect.tsx b/frontend/web/components/MyIssuesSelect.tsx index 3755486cf01b..6230c1edaea3 100644 --- a/frontend/web/components/MyIssuesSelect.tsx +++ b/frontend/web/components/MyIssuesSelect.tsx @@ -1,26 +1,46 @@ -import { FC } from 'react' +import { FC, useEffect, useState } from 'react' import { useGetGithubIssuesQuery } from 'common/services/useGithub' import IssueSelect from './IssueSelect' +import { ExternalResource, Issue } from 'common/types/responses' type MyIssuesSelectType = { orgId: string repoOwner: string repoName: string - onChange: () => void + linkedExternalResources: ExternalResource[] + onChange: (v: string) => void } const MyIssuesSelect: FC = ({ + linkedExternalResources, onChange, orgId, repoName, repoOwner, }) => { + const [extenalResourcesSelect, setExtenalResourcesSelect] = + useState() const { data } = useGetGithubIssuesQuery({ organisation_id: orgId, repo_name: repoName, repo_owner: repoOwner, }) - return + + useEffect(() => { + if (data && linkedExternalResources) { + setExtenalResourcesSelect( + data.filter((i: Issue) => { + const same = linkedExternalResources?.some( + (r) => i.html_url === r.url, + ) + return !same + }), + ) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, linkedExternalResources]) + + return } export default MyIssuesSelect diff --git a/frontend/web/components/MyPullRequestsSelect.tsx b/frontend/web/components/MyPullRequestsSelect.tsx index ba927a1f0602..2e9fe642a80f 100644 --- a/frontend/web/components/MyPullRequestsSelect.tsx +++ b/frontend/web/components/MyPullRequestsSelect.tsx @@ -1,15 +1,18 @@ -import { FC } from 'react' +import { FC, useEffect, useState } from 'react' import { useGetGithubPullsQuery } from 'common/services/useGithub' import PullRequestSelect from './PullRequestSelect' +import { ExternalResource, PullRequest } from 'common/types/responses' type MyGithubPullRequestSelectType = { orgId: string repoOwner: string repoName: string onChange: (value: string) => void + linkedExternalResources: ExternalResource[] } const MyGithubPullRequests: FC = ({ + linkedExternalResources, onChange, orgId, repoName, @@ -20,7 +23,28 @@ const MyGithubPullRequests: FC = ({ repo_name: repoName, repo_owner: repoOwner, }) - return + const [extenalResourcesSelect, setExtenalResourcesSelect] = + useState() + + useEffect(() => { + if (data && linkedExternalResources) { + setExtenalResourcesSelect( + data.filter((pr: PullRequest) => { + const same = linkedExternalResources?.some( + (r) => pr.html_url === r.url, + ) + return !same + }), + ) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, linkedExternalResources]) + return ( + + ) } export default MyGithubPullRequests diff --git a/frontend/web/components/MyRepositoriesSelect.tsx b/frontend/web/components/MyRepositoriesSelect.tsx index 16f12c29371b..36457bfb8129 100644 --- a/frontend/web/components/MyRepositoriesSelect.tsx +++ b/frontend/web/components/MyRepositoriesSelect.tsx @@ -1,11 +1,11 @@ -import { FC } from 'react' +import { FC, useEffect } from 'react' import { useGetGithubRepositoriesQuery } from 'common/services/useGithubRepository' import RepositoriesSelect from './RepositoriesSelect' type MyRepositoriesSelectType = { githubId: string orgId: string - onChange: () => void + onChange: (value: string) => void } const MyRepositoriesSelect: FC = ({ @@ -17,7 +17,27 @@ const MyRepositoriesSelect: FC = ({ github_id: githubId, organisation_id: orgId, }) - return + + useEffect(() => { + if (data?.results.length === 1) { + const repo = data?.results[0] + onChange(`${repo.repository_name}/${repo.repository_owner}`) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]) + + return ( +
+ {!!data?.results.length && data?.results.length !== 1 && ( + <> + + + )} +
+ ) } export default MyRepositoriesSelect diff --git a/frontend/web/components/ProjectFilter.tsx b/frontend/web/components/ProjectFilter.tsx index b5d13d3f627a..7d875f157fee 100644 --- a/frontend/web/components/ProjectFilter.tsx +++ b/frontend/web/components/ProjectFilter.tsx @@ -1,4 +1,4 @@ -import { FC, useMemo } from 'react' +import { FC, useEffect, useMemo } from 'react' import { useGetProjectsQuery } from 'common/services/useProject' export type ProjectFilterType = { @@ -18,6 +18,15 @@ const ProjectFilter: FC = ({ { organisationId: `${organisationId}` }, { skip: isNaN(organisationId) }, ) + + useEffect(() => { + if (data && data.length === 1) { + const project = data[0] + onChange(`${project.id}`, project.name) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]) + const foundValue = useMemo( () => data?.find((project) => `${project.id}` === value), [value, data], diff --git a/frontend/web/components/RepositoriesSelect.tsx b/frontend/web/components/RepositoriesSelect.tsx index 707d387897a2..02074c28ffd7 100644 --- a/frontend/web/components/RepositoriesSelect.tsx +++ b/frontend/web/components/RepositoriesSelect.tsx @@ -13,7 +13,7 @@ const RepositoriesSelect: FC = ({ repositories, }) => { return ( -
+
- this.setState({ externalResourceType: v.label }) - } - options={[ - { id: 1, type: 'Github Issue' }, - { id: 2, type: 'Github PR' }, - ].map((e) => { - return { label: e.type, value: e.id } - })} - /> -
- - {externalResourceType == 'Github Issue' ? ( - - this.setState({ - featureExternalResource: v, - status: 'open', - }) - } - repoOwner={repoOwner} - repoName={repoName} - /> - ) : externalResourceType == 'Github PR' ? ( - - this.setState({ - featureExternalResource: v.value, - }) - } - repoOwner={repoOwner} - repoName={repoName} - /> - ) : ( - <> - )} - - {(externalResourceType == 'Github Issue' || - externalResourceType == 'Github PR') && ( - - )} - - - )} - - - - )} {!identity && ( @@ -1858,6 +1740,30 @@ const CreateFlag = class extends Component { )} + {Utils.getFlagsmithHasFeature( + 'github_integration', + ) && + hasIntegrationWithGithub && + projectFlag?.id && ( + + Links{' '} + + } + > + + + )} {!existingChangeRequest && createFeature && ( = ({ location }) => { localStorage?.githubIntegrationSetupFromFlagsmith const [organisation, setOrganisation] = useState('') const [project, setProject] = useState({}) - const [projects, setProjects] = useState([]) + const [projects, setProjects] = useState([]) const [repositoryName, setRepositoryName] = useState('') const [repositoryOwner, setRepositoryOwner] = useState('') const [repositories, setRepositories] = useState([]) @@ -47,7 +49,7 @@ const GitHubSetupPage: FC = ({ location }) => { installation_id: installationId, organisation_id: organisation, }, - { skip: !installationId }, + { skip: !installationId || !organisation }, ) const [createGithubIntegration] = useCreateGithubIntegrationMutation() @@ -72,6 +74,7 @@ const GitHubSetupPage: FC = ({ location }) => { ) { window.location.href = `${baseUrl}/` } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSuccessCreatedGithubRepository]) useEffect(() => { @@ -124,7 +127,9 @@ const GitHubSetupPage: FC = ({ location }) => { { - setOrganisation(organisationId) + setOrganisation(`${organisationId}`) + AppActions.selectOrganisation(organisationId) + AppActions.getOrganisation(organisationId) }} showSettings={false} firstOrganisation @@ -170,13 +175,21 @@ const GitHubSetupPage: FC = ({ location }) => {