diff --git a/public/app/features/provisioning/Repository/RepositoryLink.tsx b/public/app/features/provisioning/Repository/RepositoryLink.tsx index f97a448efa4..fe4dbd4b9c5 100644 --- a/public/app/features/provisioning/Repository/RepositoryLink.tsx +++ b/public/app/features/provisioning/Repository/RepositoryLink.tsx @@ -4,7 +4,7 @@ import { Trans } from '@grafana/i18n'; import { LinkButton, Stack, Text, TextLink } from '@grafana/ui'; import { useGetRepositoryQuery } from 'app/api/clients/provisioning/v0alpha1'; -import { getRepoHref } from '../utils/git'; +import { getRepoHrefForProvider } from '../utils/git'; type RepositoryLinkProps = { name?: string; @@ -19,7 +19,7 @@ export function RepositoryLink({ name, jobType }: RepositoryLinkProps) { return null; } - const repoHref = getRepoHref(repo.spec?.github); + const repoHref = getRepoHrefForProvider(repo.spec); if (jobType === 'sync') { return ( diff --git a/public/app/features/provisioning/Repository/RepositoryCard.tsx b/public/app/features/provisioning/Repository/RepositoryListItem.tsx similarity index 84% rename from public/app/features/provisioning/Repository/RepositoryCard.tsx rename to public/app/features/provisioning/Repository/RepositoryListItem.tsx index 8a8205e9a2f..841ec3bd928 100644 --- a/public/app/features/provisioning/Repository/RepositoryCard.tsx +++ b/public/app/features/provisioning/Repository/RepositoryListItem.tsx @@ -2,12 +2,13 @@ import { ReactNode } from 'react'; import { t, Trans } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; -import { Stack, Text, TextLink, Icon, Card, LinkButton, Badge } from '@grafana/ui'; +import { Badge, Card, LinkButton, Stack, Text, TextLink } from '@grafana/ui'; import { Repository, ResourceCount } from 'app/api/clients/provisioning/v0alpha1'; import { RepoIcon } from '../Shared/RepoIcon'; import { StatusBadge } from '../Shared/StatusBadge'; import { PROVISIONING_URL } from '../constants'; +import { getRepoHrefForProvider } from '../utils/git'; import { getIsReadOnlyWorkflows } from '../utils/repository'; import { SyncRepository } from './SyncRepository'; @@ -16,7 +17,7 @@ interface Props { repository: Repository; } -export function RepositoryCard({ repository }: Props) { +export function RepositoryListItem({ repository }: Props) { const isReadOnlyRepo = getIsReadOnlyWorkflows(repository.spec?.workflows); const { metadata, spec, status } = repository; const name = metadata?.name ?? ''; @@ -27,24 +28,13 @@ export function RepositoryCard({ repository }: Props) { if (spec?.type === 'github') { const { url = '', branch } = spec.github ?? {}; const branchUrl = branch ? `${url}/tree/${branch}` : url; + const href = getRepoHrefForProvider(spec) || branchUrl; meta.push( - - {branchUrl} + + {href.split('/').slice(3).join('/')} ); - - if (status?.webhook?.id) { - const webhookUrl = `${url}/settings/hooks/${status.webhook.id}`; - meta.push( - - - Webhook - - - - ); - } } else if (spec?.type === 'local') { meta.push( diff --git a/public/app/features/provisioning/Shared/RepositoryList.tsx b/public/app/features/provisioning/Shared/RepositoryList.tsx index 3bfadfe872a..038359c6e8b 100644 --- a/public/app/features/provisioning/Shared/RepositoryList.tsx +++ b/public/app/features/provisioning/Shared/RepositoryList.tsx @@ -4,7 +4,7 @@ import { t, Trans } from '@grafana/i18n'; import { Alert, Box, EmptyState, FilterInput, Icon, Stack, TextLink } from '@grafana/ui'; import { Repository } from 'app/api/clients/provisioning/v0alpha1'; -import { RepositoryCard } from '../Repository/RepositoryCard'; +import { RepositoryListItem } from '../Repository/RepositoryListItem'; import { useResourceStats } from '../Wizard/hooks/useResourceStats'; import { UPGRADE_URL } from '../constants'; import { useIsProvisionedInstance } from '../hooks/useIsProvisionedInstance'; @@ -89,7 +89,7 @@ export function RepositoryList({ items }: Props) { )} {filteredItems.length ? ( - filteredItems.map((item) => ) + filteredItems.map((item) => ) ) : ( { return `${github.url}/tree/${github.branch}`; }; +// Remove leading and trailing slashes from a string. +const stripSlashes = (s: string) => s.replace(/^\/+|\/+$/g, ''); + +// Split a path into segments and URL-encode each segment. +// Ensures the final URL remains valid for all providers (GitHub, GitLab, etc.). +const splitAndEncode = (s: string) => stripSlashes(s).split('/').map(encodeURIComponent); + +type BuildRepoUrlParams = { + baseUrl?: string; + branch?: string | null; + providerSegments: string[]; + path?: string | null; +}; + +const buildRepoUrl = ({ baseUrl, branch, providerSegments, path }: BuildRepoUrlParams) => { + if (!baseUrl) { + return undefined; + } + + // Normalize base URL: trim whitespace + remove trailing slashes. + const cleanBase = stripSlashes(baseUrl.trim()); + const cleanBranch = branch?.trim() || undefined; + + // Start composing URL parts: + // base URL + provider-specific segments (e.g., "tree", "blob", etc.) + const parts = [cleanBase, ...providerSegments]; + + // Append the branch name if present. + if (cleanBranch) { + parts.push(cleanBranch); + } + + // Append encoded path segments if provided. + // This ensures nested files like "src/utils/index.ts" produce safe URLs. + if (path) { + parts.push(...splitAndEncode(path.trim())); + } + + return parts.join('/'); +}; + export const getRepoHrefForProvider = (spec?: RepositorySpec) => { if (!spec || !spec.type) { return undefined; } switch (spec.type) { - case 'github': { - const url = spec.github?.url; - const branch = spec.github?.branch; - if (!url) { - return undefined; - } - return branch ? `${url}/tree/${branch}` : url; - } - - case 'gitlab': { - const url = spec.gitlab?.url; - const branch = spec.gitlab?.branch; - if (!url) { - return undefined; - } - return branch ? `${url}/-/tree/${branch}` : url; - } - case 'bitbucket': { - const url = spec.bitbucket?.url; - const branch = spec.bitbucket?.branch; - if (!url) { - return undefined; - } - return branch ? `${url}/src/${branch}` : url; - } - case 'git': { - // Return a generic URL for pure git repositories - return spec.git?.url; - } + case 'github': + return buildRepoUrl({ + baseUrl: spec.github?.url, + branch: spec.github?.branch, + providerSegments: ['tree'], + path: spec.github?.path, + }); + case 'gitlab': + return buildRepoUrl({ + baseUrl: spec.gitlab?.url, + branch: spec.gitlab?.branch, + providerSegments: ['-', 'tree'], + path: spec.gitlab?.path, + }); + case 'bitbucket': + return buildRepoUrl({ + baseUrl: spec.bitbucket?.url, + branch: spec.bitbucket?.branch, + providerSegments: ['src'], + path: spec.bitbucket?.path, + }); + case 'git': + return buildRepoUrl({ + baseUrl: spec.git?.url, + branch: spec.git?.branch, + providerSegments: ['tree'], + path: spec.git?.path, + }); default: return undefined; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index c525ce2e768..5a55ab0843f 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11920,9 +11920,6 @@ "source-code": "Source code" }, "repository-card": { - "get-repository-meta": { - "webhook": "Webhook" - }, "read-only-badge": "Read only", "settings": "Settings", "view": "View"