diff --git a/ui-cra/package.json b/ui-cra/package.json index 8b98d0c534..cc9d5307e8 100644 --- a/ui-cra/package.json +++ b/ui-cra/package.json @@ -23,6 +23,7 @@ "@types/react-router-dom": "^5.1.7", "@types/react-syntax-highlighter": "^13.5.2", "@types/styled-components": "^5.1.9", + "@types/urijs": "^1.19.19", "@weaveworks/progressive-delivery": "0.0.0-rc13", "@weaveworks/weave-gitops": "npm:@weaveworks/weave-gitops-main@0.20.0-7-gb6d3961f", "@weaveworks/weave-gitops-main": "0.20.0-12-gccf5178b", @@ -53,6 +54,7 @@ "remark-gfm": "^3.0.1", "styled-components": "^5.3.0", "typescript": "^4.1.2", + "urijs": "^1.19.11", "web-vitals": "^1.0.1", "yaml": "^2.2.1" }, diff --git a/ui-cra/src/components/Applications/__tests__/__snapshots__/index.test.tsx.snap b/ui-cra/src/components/Applications/__tests__/__snapshots__/index.test.tsx.snap index 2cc0351217..a989e061be 100644 --- a/ui-cra/src/components/Applications/__tests__/__snapshots__/index.test.tsx.snap +++ b/ui-cra/src/components/Applications/__tests__/__snapshots__/index.test.tsx.snap @@ -374,31 +374,9 @@ exports[`Applications index test snapshots loading 1`] = ` ADD AN APPLICATION - +
+ Loading... +
Applications @@ -1251,34 +1229,9 @@ exports[`Applications index test snapshots success 1`] = ` class="MuiTouchRipple-root" /> - +
+ Git Repos not found +
` } `; -const useStyles = makeStyles(() => - createStyles({ - externalIcon: { - marginRight: theme.spacing.small, - }, - }), -); - const WGApplicationsDashboard: FC = () => { const { data: automations, isLoading } = useListAutomations(); const history = useHistory(); - const listConfigContext = useListConfigContext(); - const repoLink = listConfigContext?.repoLink || ''; - const classes = useStyles(); const handleAddApplication = () => history.push(Routes.AddApplication); @@ -70,14 +56,7 @@ const WGApplicationsDashboard: FC = () => { > ADD AN APPLICATION - +
{isLoading ? ( diff --git a/ui-cra/src/components/Clusters/OpenedPullRequest.tsx b/ui-cra/src/components/Clusters/OpenedPullRequest.tsx new file mode 100644 index 0000000000..d8e9350ec7 --- /dev/null +++ b/ui-cra/src/components/Clusters/OpenedPullRequest.tsx @@ -0,0 +1,173 @@ +import React, { useMemo } from 'react'; +import { + Button, + GitRepository, + Icon, + IconType, +} from '@weaveworks/weave-gitops'; +import ButtonGroup from '@material-ui/core/ButtonGroup'; +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; +import ClickAwayListener from '@material-ui/core/ClickAwayListener'; +import Grow from '@material-ui/core/Grow'; +import Paper from '@material-ui/core/Paper'; +import Popper from '@material-ui/core/Popper'; +import MenuItem from '@material-ui/core/MenuItem'; +import MenuList from '@material-ui/core/MenuList'; +import { createStyles, makeStyles } from '@material-ui/core'; +import { openLinkHandler } from '../../utils/link-checker'; +import useConfig from '../../hooks/config'; +import { GetConfigResponse } from '../../cluster-services/cluster_services.pb'; +import { + getDefaultGitRepo, + getProvider, + getRepositoryUrl, +} from '../Templates/Form/utils'; +import { useGitRepos } from '../../hooks/gitrepos'; + +const useStyles = makeStyles(() => + createStyles({ + optionsButton: { + marginRight: '0px', + }, + externalLink: { + marginRight: '5px', + }, + }), +); + +function getPullRequestUrl(gitRepo: GitRepository, config: GetConfigResponse) { + const provider = getProvider(gitRepo, config); + + const baseUrl = getRepositoryUrl(gitRepo); + if (provider === 'gitlab') { + return baseUrl + '/-/merge_requests'; + } + + // FIXME: this is not correct + if (provider === 'bitbucket-server') { + return baseUrl + '/pull-requests'; + } + + // FIXME: this is not correct + if (provider === 'azure-devops') { + return baseUrl + '/pullrequests'; + } + + // github is the default + return baseUrl + '/pulls'; +} + +export default function OpenedPullRequest() { + const [open, setOpen] = React.useState(false); + const anchorRef = React.useRef(null); + + const { gitRepos } = useGitRepos(); + + const Classes = useStyles(); + + const { data: config, isLoading } = useConfig(); + + const options = useMemo( + () => + !config + ? ([] as string[]) + : gitRepos.map(repo => getPullRequestUrl(repo, config)), + [gitRepos, config], + ); + + if (isLoading) { + return
Loading...
; + } + + if (!config) { + return
Config not found
; + } + + if (!gitRepos || gitRepos.length === 0) { + return
Git Repos not found
; + } + + const defaultRepo = getDefaultGitRepo(gitRepos); + + const handleToggle = () => { + setOpen(prevOpen => !prevOpen); + }; + + const handleClose = (event: React.MouseEvent) => { + if ( + anchorRef.current && + anchorRef.current.contains(event.target as HTMLElement) + ) { + return; + } + + setOpen(false); + }; + + return ( + <> + + + + + + {({ TransitionProps, placement }) => ( + + + + + {options.map((option, index) => ( + + {option} + + ))} + + + + + )} + + + ); +} diff --git a/ui-cra/src/components/Clusters/index.tsx b/ui-cra/src/components/Clusters/index.tsx index 91f986b0dc..1237212aae 100644 --- a/ui-cra/src/components/Clusters/index.tsx +++ b/ui-cra/src/components/Clusters/index.tsx @@ -50,7 +50,6 @@ import { Rancher, Vsphere, } from '../../utils/icons'; -import { openLinkHandler } from '../../utils/link-checker'; import { ContentWrapper } from '../Layout/ContentWrapper'; import { PageTemplate } from '../Layout/PageTemplate'; import PoliciesViolations from '../PolicyViolations'; @@ -66,6 +65,7 @@ import LoadingWrapper from '../Workspaces/WorkspaceDetails/Tabs/WorkspaceTabsWra import { ConnectClusterDialog } from './ConnectInfoBox'; import { DashboardsList } from './DashboardsList'; import { DeleteClusterDialog } from './Delete'; +import OpenedPullRequest from './OpenedPullRequest'; const ClustersTableWrapper = styled(TableWrapper)` thead { @@ -104,9 +104,6 @@ const useStyles = makeStyles(() => marginRight: theme.spacing.small, color: theme.colors.neutral30, }, - externalIcon: { - marginRight: theme.spacing.small, - }, }), ); @@ -232,9 +229,7 @@ const MCCP: FC<{ () => getGitRepos(sources?.result), [sources?.result], ); - const listConfigContext = useListConfigContext(); - const repoLink = listConfigContext?.repoLink || ''; const provider = listConfigContext?.provider; const capiClusters = useMemo( @@ -253,7 +248,6 @@ const MCCP: FC<{ const [random, setRandom] = useState( Math.random().toString(36).substring(7), ); - const classes = useStyles(); useEffect(() => { if (openDeletePR === true) { @@ -452,14 +446,7 @@ const MCCP: FC<{ onFinish={() => setOpenConnectInfo(false)} /> )} - + diff --git a/ui-cra/src/components/Templates/Form/__tests__/utils.test.tsx b/ui-cra/src/components/Templates/Form/__tests__/utils.test.tsx index b5c50c8406..9187efd680 100644 --- a/ui-cra/src/components/Templates/Form/__tests__/utils.test.tsx +++ b/ui-cra/src/components/Templates/Form/__tests__/utils.test.tsx @@ -2,11 +2,12 @@ import { GitRepository } from '@weaveworks/weave-gitops'; import { getInitialGitRepo, getRepositoryUrl } from '../utils'; describe('getRepositoryUrl', () => { - it('should return nil on a git@github.com: style url as flux does not support these', () => { + it("should return something, but we don't care what it is as git@github.com: style url as flux does not support these", () => { const url = 'git@github.com:org/repo.git'; - expect(getRepositoryUrl({ obj: { spec: { url } } } as GitRepository)).toBe( - url, - ); + + expect( + getRepositoryUrl({ obj: { spec: { url } } } as GitRepository), + ).toBeTruthy(); }); it('should normalize ssh/https urls to https preserving .git if present', () => { diff --git a/ui-cra/src/components/Templates/Form/utils.tsx b/ui-cra/src/components/Templates/Form/utils.tsx index 9146a3fb14..5ac0ed2b0b 100644 --- a/ui-cra/src/components/Templates/Form/utils.tsx +++ b/ui-cra/src/components/Templates/Form/utils.tsx @@ -8,6 +8,8 @@ import { GetTerraformObjectResponse } from '../../../api/terraform/terraform.pb' import { GitopsClusterEnriched } from '../../../types/custom'; import { Resource } from '../Edit/EditButton'; import GitUrlParse from 'git-url-parse'; +import URI from 'urijs'; +import { GetConfigResponse } from '../../../cluster-services/cluster_services.pb'; const yamlConverter = require('js-yaml'); @@ -50,23 +52,36 @@ export const getCreateRequestAnnotation = (resource: Resource) => { return maybeParseJSON(getAnnotation(resource)); }; -export const getRepositoryUrl = (repo: GitRepository) => { +export function getRepositoryUrl(repo: GitRepository) { // the https url can be overridden with an annotation const httpsUrl = repo?.obj?.metadata?.annotations?.['weave.works/repo-https-url']; if (httpsUrl) { return httpsUrl; } - let repositoryUrl = repo?.obj?.spec?.url; - let parsedUrl = GitUrlParse(repositoryUrl); - if (parsedUrl?.protocol === 'ssh') { - repositoryUrl = parsedUrl.href.replace('ssh://git@', 'https://'); + let uri = URI(repo?.obj?.spec?.url); + if (uri.hostname() === 'ssh.dev.azure.com') { + uri = azureSshToHttps(uri.toString()); } - // flux does not support "git@github.com:org/repo.git" style urls - // so we return the original url, the BE handler will fail and return - // an error to the user - return repositoryUrl; -}; + return uri.protocol('https').port('').userinfo('').toString(); +} + +function azureSshToHttps(sshUrl: string) { + const parts = sshUrl.split('/'); + const organization = parts[4]; + const project = parts[5]; + const repository = parts[6]; + + const httpsUrl = `https://dev.azure.com/${organization}/${project}/_git/${repository}`; + + return URI(httpsUrl); +} + +export function getProvider(repo: GitRepository, config: GetConfigResponse) { + const url = getRepositoryUrl(repo); + const domain = URI(url).hostname(); + return config?.gitHostTypes?.[domain] || 'github'; +} export function getInitialGitRepo( initialUrl: string | null, @@ -92,6 +107,10 @@ export function getInitialGitRepo( } } + return getDefaultGitRepo(gitRepos); +} + +export function getDefaultGitRepo(gitRepos: GitRepository[]) { const annoRepo = gitRepos.find( repo => repo?.obj?.metadata?.annotations?.['weave.works/repo-role'] === 'default', @@ -102,9 +121,12 @@ export function getInitialGitRepo( const mainRepo = gitRepos.find( repo => + // FIXME: we should also be checking the management cluster name + // repo.clusterName === config.managementClusterName && repo?.obj?.metadata?.name === 'flux-system' && repo?.obj?.metadata?.namespace === 'flux-system', ); + if (mainRepo) { return mainRepo; } diff --git a/ui-cra/src/hooks/gitrepos.tsx b/ui-cra/src/hooks/gitrepos.tsx new file mode 100644 index 0000000000..11aeaa9f7a --- /dev/null +++ b/ui-cra/src/hooks/gitrepos.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import _ from 'lodash'; + +import { Kind, useListSources } from '@weaveworks/weave-gitops'; +import { GitRepository, Source } from '@weaveworks/weave-gitops/ui/lib/objects'; + +export const getGitRepos = (sources: Source[] | undefined) => + _.orderBy( + _.uniqBy( + _.filter( + sources, + (item): item is GitRepository => item.type === Kind.GitRepository, + ), + repo => repo?.obj?.spec?.url, + ), + ['name'], + ['asc'], + ); + +export const useGitRepos = () => { + const { data, error, isLoading } = useListSources('', '', { retry: false }); + const gitRepos = React.useMemo( + () => getGitRepos(data?.result), + [data?.result], + ); + + return { gitRepos, error, isLoading }; +}; diff --git a/ui-cra/src/hooks/versions.ts b/ui-cra/src/hooks/versions.ts index e7eb5f8ce9..5ec8f64ede 100644 --- a/ui-cra/src/hooks/versions.ts +++ b/ui-cra/src/hooks/versions.ts @@ -3,7 +3,6 @@ import { useQuery } from 'react-query'; import { GetConfigResponse } from '../cluster-services/cluster_services.pb'; import { EnterpriseClientContext } from '../contexts/EnterpriseClient'; import { useRequest } from '../contexts/Request'; -import GitUrlParse from 'git-url-parse'; import { GitAuth } from '../contexts/GitAuth'; export function useListVersion() { @@ -14,14 +13,12 @@ export function useListVersion() { ); } export interface ListConfigResponse extends GetConfigResponse { - repoLink: string; uiConfig: any; [key: string]: any; } export function useListConfig() { const { api } = useContext(EnterpriseClientContext); - const [repoLink, setRepoLink] = useState(''); const [provider, setProvider] = useState(''); const queryResponse = useQuery('config', () => api.GetConfig({}), @@ -33,21 +30,12 @@ export function useListConfig() { useEffect(() => { repositoryURL && gitAuthClient.ParseRepoURL({ url: repositoryURL }).then(res => { - const { resource, full_name, protocol } = GitUrlParse(repositoryURL); setProvider(res.provider || ''); - if (res.provider === 'GitHub') { - setRepoLink(`${protocol}://${resource}/${full_name}/pulls`); - } else if (res.provider === 'GitLab') { - setRepoLink( - `${protocol}://${resource}/${full_name}/-/merge_requests`, - ); - } }); }, [repositoryURL, gitAuthClient]); return { ...queryResponse, - repoLink, uiConfig, provider, }; diff --git a/ui-cra/yarn.lock b/ui-cra/yarn.lock index c5df2f6b2a..fbf27ade51 100644 --- a/ui-cra/yarn.lock +++ b/ui-cra/yarn.lock @@ -2224,6 +2224,11 @@ resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/urijs@^1.19.19": + version "1.19.19" + resolved "https://registry.yarnpkg.com/@types/urijs/-/urijs-1.19.19.tgz#2789369799907fc11e2bc6e3a00f6478c2281b95" + integrity sha512-FDJNkyhmKLw7uEvTxx5tSXfPeQpO0iy73Ry+PmYZJvQy0QIWX8a7kJ4kLWRf+EbTPJEPDSgPXHaM7pzr5lmvCg== + "@types/ws@^8.2.2": version "8.5.1" resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.1.tgz" @@ -10504,6 +10509,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +urijs@^1.19.11: + version "1.19.11" + resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.11.tgz#204b0d6b605ae80bea54bea39280cdb7c9f923cc" + integrity sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"