diff --git a/src/gitops/components/application/ApplicationDetailsTab.tsx b/src/gitops/components/application/ApplicationDetailsTab.tsx index 84d39ea46..2256594fc 100644 --- a/src/gitops/components/application/ApplicationDetailsTab.tsx +++ b/src/gitops/components/application/ApplicationDetailsTab.tsx @@ -11,11 +11,12 @@ import Revision from '@gitops/Revision/Revision'; import HealthStatus from '@gitops/Statuses/HealthStatus'; import { OperationState } from '@gitops/Statuses/OperationState'; import SyncStatus from '@gitops/Statuses/SyncStatus'; -import { ArgoServer, getArgoServer, getFriendlyClusterName } from '@gitops/utils/gitops'; +import { useArgoServer } from '@gitops/hooks/useArgoServer'; +import { getApplicationArgoUrl, getFriendlyClusterName } from '@gitops/utils/gitops'; import { labelControllerNamespaceKey } from '@gitops/utils/gitops'; import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; import { useObjectModifyPermissions } from '@gitops/utils/utils'; -import { k8sUpdate, ResourceLink, useK8sModel } from '@openshift-console/dynamic-plugin-sdk'; +import { k8sUpdate, ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; import { Label as PfLabel, ToggleGroup, ToggleGroupItem } from '@patternfly/react-core'; import { DescriptionList, @@ -43,22 +44,10 @@ type ApplicationDetailsTabProps = RouteComponentProps<{ const ApplicationDetailsTab: React.FC = ({ obj }) => { const { t } = useGitOpsTranslation(); - const [model] = useK8sModel({ group: 'route.openshift.io', version: 'v1', kind: 'Route' }); const [canPatch, canUpdate] = useObjectModifyPermissions(obj, ApplicationModel); - - const [argoServer, setArgoServer] = React.useState({ host: '', protocol: '' }); - React.useEffect(() => { - (async () => { - getArgoServer(model, obj) - .then((server) => { - setArgoServer(server); - }) - .catch((err) => { - console.error('Error:', err); - }); - })(); - }, [model, obj]); + const argoServer = useArgoServer(obj); + const argoUrl = getApplicationArgoUrl(argoServer, obj); const onChangeAutomated = (event: React.MouseEvent | React.KeyboardEvent | MouseEvent) => { const id = event.currentTarget.id; @@ -144,17 +133,7 @@ const ApplicationDetailsTab: React.FC = ({ obj }) => model={ApplicationModel} nameLink={ <> - + } /> diff --git a/src/gitops/components/application/ApplicationNavPage.tsx b/src/gitops/components/application/ApplicationNavPage.tsx index 936454ae2..79697a15c 100644 --- a/src/gitops/components/application/ApplicationNavPage.tsx +++ b/src/gitops/components/application/ApplicationNavPage.tsx @@ -1,7 +1,9 @@ import * as React from 'react'; import { useApplicationActionsProvider } from '@gitops/hooks/useApplicationActionsProvider'; +import { useArgoServer } from '@gitops/hooks/useArgoServer'; import { ApplicationKind, ApplicationModel } from '@gitops/models/ApplicationModel'; +import { getApplicationArgoUrl } from '@gitops/utils/gitops'; import { useGitOpsTranslation } from '@gitops/utils/hooks/useGitOpsTranslation'; import { HorizontalNav, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; import { Bullseye, Spinner } from '@patternfly/react-core'; @@ -35,7 +37,9 @@ const ApplicationNavPage: React.FC = ({ name, namespace, k namespace, }); - const [actions] = useApplicationActionsProvider(application); + const argoServer = useArgoServer(application); + const argoUrl = getApplicationArgoUrl(argoServer, application); + const [actions] = useApplicationActionsProvider(application, argoUrl); const pages = React.useMemo( () => [ diff --git a/src/gitops/components/application/ApplicationResourcesTab.tsx b/src/gitops/components/application/ApplicationResourcesTab.tsx index ea2528954..927aeddb0 100644 --- a/src/gitops/components/application/ApplicationResourcesTab.tsx +++ b/src/gitops/components/application/ApplicationResourcesTab.tsx @@ -3,6 +3,7 @@ import { RouteComponentProps } from 'react-router'; import classNames from 'classnames'; import { useResourceActionsProvider } from '@gitops/hooks/useResourceActionsProvider'; +import { useArgoServer } from '@gitops/hooks/useArgoServer'; import HealthStatus from '@gitops/Statuses/HealthStatus'; import SyncStatus from '@gitops/Statuses/SyncStatus'; import ActionDropDown from '@gitops/utils/components/ActionDropDown/ActionDropDown'; @@ -15,7 +16,6 @@ import { ResourceLink, RowFilter, RowFilterItem, - useK8sModel, useListPageFilter, } from '@openshift-console/dynamic-plugin-sdk'; import { @@ -33,7 +33,7 @@ import { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/Dat import { CubesIcon } from '@patternfly/react-icons'; import { Tbody, Td, Tr } from '@patternfly/react-table'; -import { ArgoServer, getArgoServer } from '../../utils/gitops'; +import { getApplicationArgoUrl } from '../../utils/gitops'; import ArgoCDLink from '../shared/ArgoCDLink/ArgoCDLink'; import { GitOpsDataViewTable, useGitOpsDataViewSort } from '../shared/DataView'; @@ -47,21 +47,8 @@ type ApplicationResourcesTabProps = RouteComponentProps<{ }; const ApplicationResourcesTab: React.FC = ({ obj }) => { - const [model] = useK8sModel({ group: 'route.openshift.io', version: 'v1', kind: 'Route' }); - - const [argoServer, setArgoServer] = React.useState({ host: '', protocol: '' }); - - React.useEffect(() => { - (async () => { - getArgoServer(model, obj) - .then((server) => { - setArgoServer(server); - }) - .catch((err) => { - console.error('Error:', err); - }); - })(); - }, [model, obj]); + const argoServer = useArgoServer(obj); + const argoUrl = getApplicationArgoUrl(argoServer, obj); let resources: ApplicationResourceStatus[]; if (obj?.status?.resources) { @@ -91,17 +78,7 @@ const ApplicationResourcesTab: React.FC = ({ obj } const memoizedFilteredResources = React.useMemo(() => [...filteredData], [filteredData]); const isEmptyResources = memoizedFilteredResources.length === 0; - const rows = useResourceRowsDV( - memoizedFilteredResources, - obj, - argoServer.protocol + - '://' + - argoServer.host + - '/applications/' + - obj?.metadata?.namespace + - '/' + - obj?.metadata?.name, - ); + const rows = useResourceRowsDV(memoizedFilteredResources, obj, argoUrl); const empty = ( @@ -135,17 +112,7 @@ const ApplicationResourcesTab: React.FC = ({ obj } - + <> diff --git a/src/gitops/components/application/ApplicationSourcesTab.tsx b/src/gitops/components/application/ApplicationSourcesTab.tsx index 08dfc0a45..223e7888e 100644 --- a/src/gitops/components/application/ApplicationSourcesTab.tsx +++ b/src/gitops/components/application/ApplicationSourcesTab.tsx @@ -6,10 +6,10 @@ import ExternalLink from '@gitops/utils/components/ExternalLink/ExternalLink'; import { GitIcon } from '@gitops/utils/components/Icons/GitIcon'; import { HelmIcon } from '@gitops/utils/components/Icons/HelmIcon'; import { OciIcon } from '@gitops/utils/components/Icons/OciIcon'; -import { ArgoServer, getArgoServer } from '@gitops/utils/gitops'; +import { useArgoServer } from '@gitops/hooks/useArgoServer'; +import { ArgoServer, getApplicationArgoUrl } from '@gitops/utils/gitops'; import { t } from '@gitops/utils/hooks/useGitOpsTranslation'; import { repoUrl, revisionUrl } from '@gitops/utils/urls'; -import { useK8sModel } from '@openshift-console/dynamic-plugin-sdk'; import { EmptyState, EmptyStateBody, @@ -199,13 +199,7 @@ export const SourceList: React.FC = ({ sources, obj, argoServer <> = ({ sources, obj, argoServer }; const ApplicationSourcesTab: React.FC = ({ obj }) => { - const [model] = useK8sModel({ group: 'route.openshift.io', version: 'v1', kind: 'Route' }); + const argoServer = useArgoServer(obj); let sources: ApplicationSource[]; if (obj?.spec?.source) { @@ -237,20 +231,6 @@ const ApplicationSourcesTab: React.FC = ({ obj }) => sources = []; } - const [argoServer, setArgoServer] = React.useState({ host: '', protocol: '' }); - - React.useEffect(() => { - (async () => { - getArgoServer(model, obj) - .then((server) => { - setArgoServer(server); - }) - .catch((err) => { - console.error('Error:', err); - }); - })(); - }, [model, obj]); - return (
= ({ obj }) => { - const [model] = useK8sModel({ group: 'route.openshift.io', version: 'v1', kind: 'Route' }); - - const [argoServer, setArgoServer] = React.useState({ host: '', protocol: '' }); - - React.useEffect(() => { - (async () => { - getArgoServer(model, obj) - .then((server) => { - setArgoServer(server); - }) - .catch((err) => { - console.error('Error:', err); - }); - })(); - }, [model, obj]); + const argoServer = useArgoServer(obj); + const argoUrl = getApplicationArgoUrl(argoServer, obj); let resources: ApplicationResourceStatus[]; if (obj?.status?.operationState?.syncResult?.resources) { @@ -83,17 +70,7 @@ const ApplicationSyncStatusTab: React.FC = ({ obj return sortData(resources, sortBy, direction); }, [resources, sortBy, direction]); - const rows = useResourceRowsDV( - sortedResources, - obj, - argoServer.protocol + - '://' + - argoServer.host + - '/applications/' + - obj?.metadata?.namespace + - '/' + - obj?.metadata?.name, - ); + const rows = useResourceRowsDV(sortedResources, obj, argoUrl); const empty = ( diff --git a/src/gitops/components/application/History/History.tsx b/src/gitops/components/application/History/History.tsx index 94f2cb46f..4f45d849c 100644 --- a/src/gitops/components/application/History/History.tsx +++ b/src/gitops/components/application/History/History.tsx @@ -4,10 +4,11 @@ import { useTranslation } from 'react-i18next'; import ArgoCDLink from '@gitops/components/shared/ArgoCDLink/ArgoCDLink'; import Revision from '@gitops/Revision/Revision'; import ExternalLink from '@gitops/utils/components/ExternalLink/ExternalLink'; -import { ArgoServer, getArgoServer } from '@gitops/utils/gitops'; +import { useArgoServer } from '@gitops/hooks/useArgoServer'; +import { getApplicationArgoUrl } from '@gitops/utils/gitops'; import { repoUrl } from '@gitops/utils/urls'; import { ApplicationHistory, ApplicationKind } from '@gitops-models/ApplicationModel'; -import { Timestamp, useK8sModel } from '@openshift-console/dynamic-plugin-sdk'; +import { Timestamp } from '@openshift-console/dynamic-plugin-sdk'; import { EmptyState, EmptyStateBody } from '@patternfly/react-core'; import { DataViewTh, DataViewTr } from '@patternfly/react-data-view/dist/esm/DataViewTable'; import { CubesIcon } from '@patternfly/react-icons'; @@ -38,20 +39,8 @@ const HistoryList: React.FC = ({ history, obj }) => { const rows = useRowsDV(sortedHistory, obj); - const [model] = useK8sModel({ group: 'route.openshift.io', version: 'v1', kind: 'Route' }); - const [argoServer, setArgoServer] = React.useState({ host: '', protocol: '' }); - - React.useEffect(() => { - (async () => { - getArgoServer(model, obj) - .then((server) => { - setArgoServer(server); - }) - .catch((err) => { - console.error('Error:', err); - }); - })(); - }, [model, obj]); + const argoServer = useArgoServer(obj); + const argoUrl = getApplicationArgoUrl(argoServer, obj); const empty = ( @@ -71,13 +60,7 @@ const HistoryList: React.FC = ({ history, obj }) => {
= ({ application, resources }) => { - const [model] = useK8sModel({ group: 'route.openshift.io', version: 'v1', kind: 'Route' }); const [allK8sModels] = useK8sModels(); const [groupNodeStates, setGroupNodeStates] = React.useState([]); // Save the group node setting so it will be used whenever the user re-enters the graph view @@ -328,29 +327,10 @@ export const ApplicationGraphView: React.FC<{ true, false, ); - const [argoServer, setArgoServer] = React.useState({ host: '', protocol: '' }); + const argoServer = useArgoServer(application); + const argoUrl = getApplicationArgoUrl(argoServer, application); const hrefRef = React.useRef(''); - React.useEffect(() => { - (async () => { - getArgoServer(model, application) - .then((server) => { - setArgoServer(server); - }) - .catch((err) => { - console.error('Graph view error: ' + err); - }); - })(); - }, [model, application]); - const href = argoServer - ? argoServer.protocol + - '://' + - argoServer.host + - '/applications/' + - application?.metadata?.namespace + - '/' + - application?.metadata?.name - : ''; - hrefRef.current = href; + hrefRef.current = argoUrl; const [selectedIds, setSelectedIds] = React.useState([]); const [renderKey, setRenderKey] = React.useState(0); @@ -360,12 +340,12 @@ export const ApplicationGraphView: React.FC<{ const navigate = useNavigate(); const contextMenuParamsRef = React.useRef({ - application, - navigate, - launchLabelsModal, - launchAnnotationsModal, - launchDeleteModal, - setGroupNodeStates, + application, + navigate, + launchLabelsModal, + launchAnnotationsModal, + launchDeleteModal, + setGroupNodeStates, }); React.useEffect(() => { diff --git a/src/gitops/hooks/useApplicationActionsProvider.tsx b/src/gitops/hooks/useApplicationActionsProvider.tsx index d5ab271b3..a4fd18f85 100644 --- a/src/gitops/hooks/useApplicationActionsProvider.tsx +++ b/src/gitops/hooks/useApplicationActionsProvider.tsx @@ -151,20 +151,21 @@ export const useApplicationActionsProvider: UseApplicationActionsProvider = (app }, cta: () => launchDeleteModal(), }, - { - id: 'gitops-action-view-in-argocd', - disabled: href === undefined, - label: t('View in Argo CD'), - accessReview: { - group: ApplicationModel.apiGroup, - verb: 'get' as K8sVerb, - resource: ApplicationModel.plural, - namespace: application?.metadata?.namespace, - }, - cta: () => { - window.open(href, '_blank'); - }, - }, + ...(href ? [{ + id: 'gitops-action-view-in-argocd', + disabled: href === undefined, + label: t('View in Argo CD'), + accessReview: { + group: ApplicationModel.apiGroup, + verb: 'get' as K8sVerb, + resource: ApplicationModel.plural, + namespace: application?.metadata?.namespace, + }, + cta: () => { + window.open(href, '_blank'); + } + }] : [] + ), ], [application, navigate, launchLabelsModal, launchAnnotationsModal, launchDeleteModal, href], ); diff --git a/src/gitops/hooks/useArgoServer.ts b/src/gitops/hooks/useArgoServer.ts new file mode 100644 index 000000000..80d1af016 --- /dev/null +++ b/src/gitops/hooks/useArgoServer.ts @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import { ApplicationKind } from '@gitops/models/ApplicationModel'; +import { useK8sModel } from '@openshift-console/dynamic-plugin-sdk'; + +import { ArgoServer, getArgoServer } from '../utils/gitops'; + +export const useArgoServer = (app?: ApplicationKind): ArgoServer => { + const [model] = useK8sModel({ group: 'route.openshift.io', version: 'v1', kind: 'Route' }); + const [argoServer, setArgoServer] = React.useState({ host: '', protocol: '' }); + + React.useEffect(() => { + if (!app) { + return undefined; + } + + getArgoServer(model, app) + .then(setArgoServer) + .catch((err) => { + console.error('Error:', err); + }); + }, [model, app]); + + return argoServer; +}; diff --git a/src/gitops/utils/gitops.test.ts b/src/gitops/utils/gitops.test.ts index d3dcb009c..10901ae57 100644 --- a/src/gitops/utils/gitops.test.ts +++ b/src/gitops/utils/gitops.test.ts @@ -1,5 +1,6 @@ import { createRevisionURL, + getApplicationArgoUrl, getFriendlyClusterName, getDuration, isApplicationRefreshing, @@ -7,6 +8,23 @@ import { getAppSetStatus, } from './gitops'; +describe('getApplicationArgoUrl', () => { + it('builds application URLs', () => { + expect( + getApplicationArgoUrl({ protocol: 'https', host: 'openshift-gitops-server-openshift-gitops' }, { + metadata: { name: 'guestbook', namespace: 'openshift-gitops' }, + } as any), + ).toBe('https://openshift-gitops-server-openshift-gitops/applications/openshift-gitops/guestbook'); + }); + + it('returns empty string when server or app metadata is missing', () => { + expect(getApplicationArgoUrl({ protocol: 'https', host: '' }, {} as any)).toBe(''); + expect( + getApplicationArgoUrl({ protocol: 'https', host: 'openshift-gitops-server-openshift-gitops' }, undefined), + ).toBe(''); + }); +}); + describe('createRevisionURL', () => { it('builds commit URLs', () => { expect(createRevisionURL('https://github.com/foo/bar.git', 'abc123')).toMatchInlineSnapshot( diff --git a/src/gitops/utils/gitops.ts b/src/gitops/utils/gitops.ts index fcb3adb33..2394386b4 100644 --- a/src/gitops/utils/gitops.ts +++ b/src/gitops/utils/gitops.ts @@ -99,6 +99,14 @@ export const getArgoServer = async (model, app: ApplicationKind): Promise { + if (!argoServer.host || !app?.metadata?.name || !app?.metadata?.namespace) { + return ''; + } + + return `${argoServer.protocol}://${argoServer.host}/applications/${app.metadata.namespace}/${app.metadata.name}`; +}; + export const getArgoServerForProject = async ( model, project: { metadata?: { namespace?: string; labels?: Record } },