Provisioning: Add branch selection in Onboarding Wizard and Config page (#110217)
* Provisioning: Add branch selection component with fetching logic * Simplify api call * Use fetch * Simplify branch logic * Remove branch selector * Add repo fetching * Restructure * Add missing files * Ad branch selection to config form * Cleanup * Remove isDfault * useAsync * useAsync for branch loading * Remove repo fetching * Refactor * Allow custom value * Add pagination * dedupe * useBranchOptions * format * Remove types * comments * Fix test * Add translations
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query/react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
@@ -6,6 +7,7 @@ import { t } from '@grafana/i18n';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
ControlledCollapse,
|
||||
Field,
|
||||
Input,
|
||||
@@ -14,7 +16,7 @@ import {
|
||||
Stack,
|
||||
Switch,
|
||||
} from '@grafana/ui';
|
||||
import { Repository } from 'app/api/clients/provisioning/v0alpha1';
|
||||
import { Repository, useGetRepositoryRefsQuery } from 'app/api/clients/provisioning/v0alpha1';
|
||||
import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt';
|
||||
|
||||
import { TokenPermissionsInfo } from '../Shared/TokenPermissionsInfo';
|
||||
@@ -41,7 +43,8 @@ export interface ConfigFormProps {
|
||||
data?: Repository;
|
||||
}
|
||||
export function ConfigForm({ data }: ConfigFormProps) {
|
||||
const [submitData, request] = useCreateOrUpdateRepository(data?.metadata?.name);
|
||||
const repositoryName = data?.metadata?.name;
|
||||
const [submitData, request] = useCreateOrUpdateRepository(repositoryName);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -54,7 +57,7 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
getValues,
|
||||
} = useForm<RepositoryFormData>({ defaultValues: getDefaultValues(data?.spec) });
|
||||
|
||||
const isEdit = Boolean(data?.metadata?.name);
|
||||
const isEdit = Boolean(repositoryName);
|
||||
const [tokenConfigured, setTokenConfigured] = useState(isEdit);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
@@ -62,6 +65,23 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
const targetOptions = useMemo(() => getTargetOptions(), []);
|
||||
const isGitBased = isGitProvider(type);
|
||||
|
||||
const {
|
||||
data: refsData,
|
||||
isLoading: refsLoading,
|
||||
error: refsError,
|
||||
} = useGetRepositoryRefsQuery(!repositoryName || !isGitBased ? skipToken : { name: repositoryName });
|
||||
|
||||
const branchOptions = useMemo(() => {
|
||||
if (!refsData?.items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return refsData.items.map((ref) => ({
|
||||
label: ref.name,
|
||||
value: ref.name,
|
||||
}));
|
||||
}, [refsData?.items]);
|
||||
|
||||
// Get field configurations based on provider type
|
||||
const gitFields = isGitBased ? getGitProviderFields(type) : null;
|
||||
const localFields = type === 'local' ? getLocalProviderFields(type) : null;
|
||||
@@ -187,8 +207,32 @@ export function ConfigForm({ data }: ConfigFormProps) {
|
||||
placeholder={gitFields.urlConfig.placeholder}
|
||||
/>
|
||||
</Field>
|
||||
<Field noMargin label={gitFields.branchConfig.label} description={gitFields.branchConfig.description}>
|
||||
<Input {...register('branch')} placeholder={gitFields.branchConfig.placeholder} />
|
||||
<Field
|
||||
noMargin
|
||||
label={gitFields.branchConfig.label}
|
||||
description={gitFields.branchConfig.description}
|
||||
error={
|
||||
errors?.branch?.message ||
|
||||
(refsError ? t('provisioning.config-form.error-fetch-branches', 'Failed to fetch branches') : undefined)
|
||||
}
|
||||
invalid={Boolean(errors?.branch?.message || refsError)}
|
||||
>
|
||||
<Controller
|
||||
name="branch"
|
||||
control={control}
|
||||
rules={gitFields.branchConfig.validation}
|
||||
render={({ field: { ref, onChange, ...field } }) => (
|
||||
<Combobox
|
||||
invalid={Boolean(errors?.branch?.message || refsError)}
|
||||
onChange={(option) => onChange(option?.value || '')}
|
||||
placeholder={gitFields.branchConfig.placeholder}
|
||||
options={branchOptions}
|
||||
loading={refsLoading}
|
||||
isClearable
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
<Field noMargin label={gitFields.pathConfig.label} description={gitFields.pathConfig.description}>
|
||||
<Input {...register('path')} />
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Field, Input, SecretInput, Stack } from '@grafana/ui';
|
||||
import { Combobox, Field, Input, SecretInput, Stack } from '@grafana/ui';
|
||||
|
||||
import { TokenPermissionsInfo } from '../Shared/TokenPermissionsInfo';
|
||||
import { useBranchOptions } from '../hooks/useBranchOptions';
|
||||
import { getHasTokenInstructions } from '../utils/git';
|
||||
import { isGitProvider } from '../utils/repositoryTypes';
|
||||
|
||||
@@ -17,14 +18,26 @@ export function ConnectStep() {
|
||||
setValue,
|
||||
formState: { errors },
|
||||
getValues,
|
||||
watch,
|
||||
} = useFormContext<WizardFormData>();
|
||||
|
||||
const [tokenConfigured, setTokenConfigured] = useState(false);
|
||||
|
||||
// We don't need to dynamically react on repo type changes, so we use getValues for it
|
||||
const type = getValues('repository.type');
|
||||
const [repositoryUrl = '', repositoryToken = ''] = watch(['repository.url', 'repository.token']);
|
||||
const isGitBased = isGitProvider(type);
|
||||
|
||||
// Get field configurations based on provider type
|
||||
const {
|
||||
options: branchOptions,
|
||||
loading: branchesLoading,
|
||||
error: branchesError,
|
||||
} = useBranchOptions({
|
||||
repositoryType: type,
|
||||
repositoryUrl,
|
||||
repositoryToken,
|
||||
});
|
||||
|
||||
const gitFields = isGitBased ? getGitProviderFields(type) : null;
|
||||
const localFields = !isGitBased ? getLocalProviderFields(type) : null;
|
||||
const hasTokenInstructions = getHasTokenInstructions(type);
|
||||
@@ -84,12 +97,12 @@ export function ConnectStep() {
|
||||
label={gitFields.urlConfig.label}
|
||||
description={gitFields.urlConfig.description}
|
||||
error={errors?.repository?.url?.message}
|
||||
invalid={!!errors?.repository?.url?.message}
|
||||
invalid={Boolean(errors?.repository?.url?.message)}
|
||||
required={gitFields.urlConfig.required}
|
||||
>
|
||||
<Input
|
||||
{...register('repository.url', gitFields.urlConfig.validation)}
|
||||
id="url"
|
||||
id="repository-url"
|
||||
placeholder={gitFields.urlConfig.placeholder}
|
||||
/>
|
||||
</Field>
|
||||
@@ -99,13 +112,25 @@ export function ConnectStep() {
|
||||
label={gitFields.branchConfig.label}
|
||||
description={gitFields.branchConfig.description}
|
||||
error={errors?.repository?.branch?.message}
|
||||
invalid={!!errors?.repository?.branch?.message}
|
||||
required={gitFields.branchConfig.required}
|
||||
invalid={Boolean(errors?.repository?.branch?.message || branchesError)}
|
||||
>
|
||||
<Input
|
||||
{...register('repository.branch', gitFields.branchConfig.validation)}
|
||||
id="branch"
|
||||
placeholder={gitFields.branchConfig.placeholder}
|
||||
<Controller
|
||||
name="repository.branch"
|
||||
control={control}
|
||||
rules={gitFields.branchConfig.validation}
|
||||
render={({ field: { ref, onChange, ...field } }) => (
|
||||
<Combobox
|
||||
invalid={Boolean(errors?.repository?.branch?.message || branchesError)}
|
||||
onChange={(option) => onChange(option?.value || '')}
|
||||
placeholder={gitFields.branchConfig.placeholder}
|
||||
options={branchOptions}
|
||||
loading={branchesLoading}
|
||||
createCustomValue
|
||||
isClearable
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useGetResourceStatsQuery,
|
||||
} from 'app/api/clients/provisioning/v0alpha1';
|
||||
|
||||
import { useBranchOptions } from '../hooks/useBranchOptions';
|
||||
import { useCreateOrUpdateRepository } from '../hooks/useCreateOrUpdateRepository';
|
||||
|
||||
import { ProvisioningWizard } from './ProvisioningWizard';
|
||||
@@ -23,6 +24,7 @@ jest.mock('react-router-dom-v5-compat', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('../hooks/useCreateOrUpdateRepository');
|
||||
jest.mock('../hooks/useBranchOptions');
|
||||
jest.mock('app/api/clients/provisioning/v0alpha1', () => ({
|
||||
...jest.requireActual('app/api/clients/provisioning/v0alpha1'),
|
||||
useGetFrontendSettingsQuery: jest.fn(),
|
||||
@@ -34,6 +36,7 @@ jest.mock('app/api/clients/provisioning/v0alpha1', () => ({
|
||||
const mockUseCreateOrUpdateRepository = useCreateOrUpdateRepository as jest.MockedFunction<
|
||||
typeof useCreateOrUpdateRepository
|
||||
>;
|
||||
const mockUseBranchOptions = useBranchOptions as jest.MockedFunction<typeof useBranchOptions>;
|
||||
const mockUseGetFrontendSettingsQuery = useGetFrontendSettingsQuery as jest.MockedFunction<
|
||||
typeof useGetFrontendSettingsQuery
|
||||
>;
|
||||
@@ -87,7 +90,7 @@ async function fillConnectionForm(
|
||||
}
|
||||
|
||||
if (type !== 'local' && data.branch) {
|
||||
await user.type(screen.getByRole('textbox', { name: /Branch/i }), data.branch);
|
||||
await user.type(screen.getByRole('combobox'), data.branch);
|
||||
}
|
||||
|
||||
if (data.path) {
|
||||
@@ -99,6 +102,16 @@ describe('ProvisioningWizard', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Mock useBranchOptions to prevent real API calls
|
||||
mockUseBranchOptions.mockReturnValue({
|
||||
options: [
|
||||
{ label: 'main', value: 'main' },
|
||||
{ label: 'develop', value: 'develop' },
|
||||
],
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
mockUseGetFrontendSettingsQuery.mockReturnValue({
|
||||
data: {
|
||||
items: [],
|
||||
@@ -178,7 +191,7 @@ describe('ProvisioningWizard', () => {
|
||||
expect(screen.getByRole('heading', { name: /1\. Connect to external storage/i })).toBeInTheDocument();
|
||||
expect(screen.getByText('Personal Access Token *')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Repository URL/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Branch/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Path/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -499,7 +512,7 @@ describe('ProvisioningWizard', () => {
|
||||
|
||||
expect(screen.getByText('Project Access Token *')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Repository URL/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Branch/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Path/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -509,7 +522,7 @@ describe('ProvisioningWizard', () => {
|
||||
expect(screen.getByText('App Password *')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Username/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Repository URL/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Branch/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Path/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -519,7 +532,7 @@ describe('ProvisioningWizard', () => {
|
||||
expect(screen.getByText('Access Token *')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Username/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Repository URL/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Branch/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox', { name: /Path/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -531,7 +544,7 @@ describe('ProvisioningWizard', () => {
|
||||
expect(screen.queryByPlaceholderText('glpat-xxxxxxxxxxxxxxxxxxxx')).not.toBeInTheDocument();
|
||||
expect(screen.queryByPlaceholderText('ATBBxxxxxxxxxxxxxxxx')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('textbox', { name: /Repository URL/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('textbox', { name: /Branch/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should accept tokenUser input for Bitbucket provider', async () => {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface HttpError extends Error {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
export function isSupportedGitProvider(provider: string): provider is 'github' | 'gitlab' | 'bitbucket' {
|
||||
return ['github', 'gitlab', 'bitbucket'].includes(provider);
|
||||
}
|
||||
|
||||
export function isHttpError(err: unknown): err is HttpError {
|
||||
return err instanceof Error && 'status' in err;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* A hook to fetch all branches from a given repository.
|
||||
* Used to populate the branch dropdown in the repository selection.
|
||||
* We can't use the '/ref` endpoint at this point because the repository connection hasn't been created yet.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { RepoType } from '../Wizard/types';
|
||||
import { isSupportedGitProvider } from '../guards';
|
||||
import { fetchAllBranches, getErrorMessage, parseRepositoryUrl } from '../utils/httpUtils';
|
||||
|
||||
export interface UseBranchOptionsProps {
|
||||
repositoryType: RepoType;
|
||||
repositoryUrl: string;
|
||||
repositoryToken: string;
|
||||
}
|
||||
|
||||
export function useBranchOptions({ repositoryType, repositoryUrl = '', repositoryToken = '' }: UseBranchOptionsProps) {
|
||||
const trimmedUrl = repositoryUrl.trim();
|
||||
const trimmedToken = repositoryToken.trim();
|
||||
|
||||
const hasRequiredData = useMemo(() => {
|
||||
if (!isSupportedGitProvider(repositoryType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasUrl = trimmedUrl.length > 0;
|
||||
const hasToken = trimmedToken.length > 0;
|
||||
const repoInfo = hasUrl ? parseRepositoryUrl(trimmedUrl, repositoryType) : null;
|
||||
|
||||
return hasUrl && hasToken && repoInfo !== null;
|
||||
}, [trimmedUrl, trimmedToken, repositoryType]);
|
||||
|
||||
const fetchOptions = useMemo(
|
||||
() => async (): Promise<Array<{ label: string; value: string }>> => {
|
||||
if (!hasRequiredData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const repoInfo = parseRepositoryUrl(trimmedUrl, repositoryType);
|
||||
|
||||
if (!repoInfo) {
|
||||
throw new Error('Invalid repository URL format');
|
||||
}
|
||||
|
||||
const branchData = await fetchAllBranches(repositoryType, repoInfo.owner, repoInfo.repo, trimmedToken);
|
||||
|
||||
return branchData.map((branch) => ({
|
||||
label: branch.name,
|
||||
value: branch.name,
|
||||
}));
|
||||
},
|
||||
[hasRequiredData, trimmedUrl, trimmedToken, repositoryType]
|
||||
);
|
||||
|
||||
const asyncState = useAsync(fetchOptions, [fetchOptions]);
|
||||
|
||||
return {
|
||||
options: asyncState.value || [],
|
||||
loading: asyncState.loading,
|
||||
error: asyncState.error ? getErrorMessage(asyncState.error) : null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { t } from '@grafana/i18n';
|
||||
|
||||
import { HttpError, isHttpError } from '../guards';
|
||||
|
||||
export interface RepositoryInfo {
|
||||
owner: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
export interface ApiRequest {
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
const githubUrlRegex = /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/?$/;
|
||||
const gitlabUrlRegex = /^https:\/\/gitlab\.com\/([^\/]+)\/([^\/]+)\/?$/;
|
||||
const bitbucketUrlRegex = /^https:\/\/bitbucket\.org\/([^\/]+)\/([^\/]+)\/?$/;
|
||||
|
||||
export function parseRepositoryUrl(url: string, type: string): RepositoryInfo | null {
|
||||
let match: RegExpMatchArray | null = null;
|
||||
|
||||
switch (type) {
|
||||
case 'github':
|
||||
match = url.match(githubUrlRegex);
|
||||
break;
|
||||
case 'gitlab':
|
||||
match = url.match(gitlabUrlRegex);
|
||||
break;
|
||||
case 'bitbucket':
|
||||
match = url.match(bitbucketUrlRegex);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (match && match[1] && match[2]) {
|
||||
return {
|
||||
owner: match[1],
|
||||
repo: match[2].replace(/\.git$/, ''),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getProviderHeaders(repositoryType: string, token: string): Record<string, string> {
|
||||
switch (repositoryType) {
|
||||
case 'github':
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
case 'gitlab':
|
||||
return { 'Private-Token': token };
|
||||
case 'bitbucket':
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
default:
|
||||
throw new Error(
|
||||
t('provisioning.http-utils.unsupported-repository-type', 'Unsupported repository type: {{repositoryType}}', {
|
||||
repositoryType,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function makeApiRequest(request: ApiRequest) {
|
||||
const response = await window.fetch(request.url, {
|
||||
method: 'GET',
|
||||
headers: request.headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
console.error('API Error Response:', errorData);
|
||||
const error: HttpError = new Error(
|
||||
t('provisioning.http-utils.http-error', 'HTTP {{status}}: {{statusText}}', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// GitHub and GitLab limit results to 100 items per page, so we need to paginate
|
||||
async function fetchWithPagination(
|
||||
buildUrl: (page: number) => string,
|
||||
headers: Record<string, string>
|
||||
): Promise<Array<{ name: string }>> {
|
||||
const allBranches = [];
|
||||
let page = 1;
|
||||
let hasMorePages = true;
|
||||
|
||||
while (hasMorePages && page <= 10) {
|
||||
const url = buildUrl(page);
|
||||
const data = await makeApiRequest({ url, headers });
|
||||
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
allBranches.push(...data);
|
||||
hasMorePages = data.length === 100;
|
||||
page++;
|
||||
} else {
|
||||
hasMorePages = false;
|
||||
}
|
||||
}
|
||||
|
||||
return allBranches;
|
||||
}
|
||||
|
||||
export async function fetchAllGitHubBranches(
|
||||
owner: string,
|
||||
repo: string,
|
||||
headers: Record<string, string>
|
||||
): Promise<Array<{ name: string }>> {
|
||||
return fetchWithPagination(
|
||||
(page) => `https://api.github.com/repos/${owner}/${repo}/branches?per_page=100&page=${page}`,
|
||||
headers
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchAllGitLabBranches(
|
||||
owner: string,
|
||||
repo: string,
|
||||
headers: Record<string, string>
|
||||
): Promise<Array<{ name: string }>> {
|
||||
const encodedPath = encodeURIComponent(`${owner}/${repo}`);
|
||||
return fetchWithPagination(
|
||||
(page) => `https://gitlab.com/api/v4/projects/${encodedPath}/repository/branches?per_page=100&page=${page}`,
|
||||
headers
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchAllBitbucketBranches(
|
||||
owner: string,
|
||||
repo: string,
|
||||
headers: Record<string, string>
|
||||
): Promise<Array<{ name: string }>> {
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=1000`;
|
||||
const data = await makeApiRequest({ url, headers });
|
||||
|
||||
if (data && Array.isArray(data.values)) {
|
||||
return data.values;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function fetchAllBranches(
|
||||
repositoryType: string,
|
||||
owner: string,
|
||||
repo: string,
|
||||
token: string
|
||||
): Promise<Array<{ name: string }>> {
|
||||
const headers = getProviderHeaders(repositoryType, token);
|
||||
|
||||
switch (repositoryType) {
|
||||
case 'github':
|
||||
return fetchAllGitHubBranches(owner, repo, headers);
|
||||
case 'gitlab':
|
||||
return fetchAllGitLabBranches(owner, repo, headers);
|
||||
case 'bitbucket':
|
||||
return fetchAllBitbucketBranches(owner, repo, headers);
|
||||
default:
|
||||
throw new Error(
|
||||
t('provisioning.http-utils.unsupported-repository-type', 'Unsupported repository type: {{repositoryType}}', {
|
||||
repositoryType,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorMessage(err: unknown) {
|
||||
let errorMessage = t('provisioning.http-utils.request-failed', 'Request failed');
|
||||
|
||||
if (isHttpError(err)) {
|
||||
if (err.status === 401) {
|
||||
errorMessage = t(
|
||||
'provisioning.http-utils.authentication-failed',
|
||||
'Authentication failed. Please check your access token.'
|
||||
);
|
||||
} else if (err.status === 404) {
|
||||
errorMessage = t(
|
||||
'provisioning.http-utils.resource-not-found',
|
||||
'Resource not found. Please check the URL or repository.'
|
||||
);
|
||||
} else if (err.status === 403) {
|
||||
errorMessage = t('provisioning.http-utils.access-denied', 'Access denied. Please check your token permissions.');
|
||||
} else if (err.message) {
|
||||
errorMessage = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
return errorMessage;
|
||||
}
|
||||
@@ -11232,6 +11232,7 @@
|
||||
"description-enabled": "Once automatic pulling is enabled, the target cannot be changed.",
|
||||
"description-read-only": "Resources can't be modified through Grafana.",
|
||||
"description-title": "A human-readable name for the config",
|
||||
"error-fetch-branches": "Failed to fetch branches",
|
||||
"error-required": "This field is required.",
|
||||
"error-save-repository": "Failed to save repository settings",
|
||||
"label-automatic-pulling": "Automatic pulling",
|
||||
@@ -11444,6 +11445,14 @@
|
||||
"title-delete-all-configured-repositories": "Delete all configured repositories",
|
||||
"title-legacy-storage-detected": "Legacy storage detected"
|
||||
},
|
||||
"http-utils": {
|
||||
"access-denied": "Access denied. Please check your token permissions.",
|
||||
"authentication-failed": "Authentication failed. Please check your access token.",
|
||||
"http-error": "HTTP {{status}}: {{statusText}}",
|
||||
"request-failed": "Request failed",
|
||||
"resource-not-found": "Resource not found. Please check the URL or repository.",
|
||||
"unsupported-repository-type": "Unsupported repository type: {{repositoryType}}"
|
||||
},
|
||||
"inline-secure-values-warning": "You need to save your access tokens again due to a system update",
|
||||
"inline-token-warning-badge-text": "Token needs to be saved again",
|
||||
"job-status": {
|
||||
|
||||
Reference in New Issue
Block a user