mirror of https://github.com/portainer/portainer
fix(container): fix various creating container issues EE-6287 (#10595)
* fix(container): show placeholder for image field EE-6287 * fix(container): correct query params for search button field EE-6287 * fix(container): use btoa to encode registry credential EE-6287 * fix(container): allow creating non-existing option EE-6287 * fix(ui/forms): typeahead component * fix(container): select the default registry EE-6287 * fix(container): always enable deploy button when always pull is off EE-6287 * fix(container): reset command fields outside current event to avoid validation on broken values EE-6287 * fix(container): query registry with endpoint ID param EE-6287 --------- Co-authored-by: Chaim Lev-Ari <chaim.levi-ari@portainer.io>pull/10642/head
parent
e43d076269
commit
d089dfbca0
|
@ -1,5 +1,5 @@
|
|||
import _ from 'lodash-es';
|
||||
import { hideShaSum, joinCommand, nodeStatusBadge, taskStatusBadge, trimSHA } from './utils';
|
||||
import { hideShaSum, joinCommand, nodeStatusBadge, taskStatusBadge, trimSHA, trimVersionTag } from './utils';
|
||||
|
||||
function includeString(text, values) {
|
||||
return values.some(function (val) {
|
||||
|
@ -184,20 +184,7 @@ angular
|
|||
})
|
||||
.filter('trimversiontag', function () {
|
||||
'use strict';
|
||||
return function trimversiontag(fullName) {
|
||||
if (!fullName) {
|
||||
return fullName;
|
||||
}
|
||||
var versionIdx = fullName.lastIndexOf(':');
|
||||
if (versionIdx < 0) {
|
||||
return fullName;
|
||||
}
|
||||
var hostIdx = fullName.indexOf('/');
|
||||
if (hostIdx > versionIdx) {
|
||||
return fullName;
|
||||
}
|
||||
return fullName.substring(0, versionIdx);
|
||||
};
|
||||
return trimVersionTag;
|
||||
})
|
||||
.filter('unique', function () {
|
||||
return _.uniqBy;
|
||||
|
|
|
@ -1,6 +1,24 @@
|
|||
import { NodeStatus, TaskState } from 'docker-types/generated/1.41';
|
||||
import _ from 'lodash';
|
||||
|
||||
export function trimVersionTag(fullName: string) {
|
||||
if (!fullName) {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
const versionIdx = fullName.lastIndexOf(':');
|
||||
if (versionIdx < 0) {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
const hostIdx = fullName.indexOf('/');
|
||||
if (hostIdx > versionIdx) {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
return fullName.substring(0, versionIdx);
|
||||
}
|
||||
|
||||
export function trimSHA(imageName: string) {
|
||||
if (!imageName) {
|
||||
return '';
|
||||
|
|
|
@ -30,7 +30,7 @@ export function AdvancedForm({
|
|||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
setFieldValue('image', value);
|
||||
onChangeImage?.(value);
|
||||
setTimeout(() => onChangeImage?.(value), 0);
|
||||
}}
|
||||
placeholder="e.g. registry:port/my-image:my-tag"
|
||||
required
|
||||
|
|
|
@ -1,52 +1,39 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
|
||||
import { AutocompleteSelect } from '@@/form-components/AutocompleteSelect';
|
||||
import { Option } from '@@/form-components/PortainerSelect';
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
|
||||
export function InputSearch({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
placeholder,
|
||||
'data-cy': dataCy,
|
||||
inputId,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options: Option<string>[];
|
||||
placeholder?: string;
|
||||
inputId?: string;
|
||||
inputId: string;
|
||||
} & AutomationTestingProps) {
|
||||
const selectValue = options.find((option) => option.value === value) || {
|
||||
value: '',
|
||||
label: value,
|
||||
};
|
||||
const searchResults = useMemo(() => {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return options.filter((option) =>
|
||||
option.value.toLowerCase().includes(value.toLowerCase())
|
||||
);
|
||||
}, [options, value]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={options}
|
||||
value={selectValue}
|
||||
onChange={(option) => option && onChange(option.value)}
|
||||
<AutocompleteSelect
|
||||
searchResults={searchResults}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
data-cy={dataCy}
|
||||
inputId={inputId}
|
||||
onInputChange={(value, actionMeta) => {
|
||||
if (
|
||||
actionMeta.action !== 'input-change' &&
|
||||
actionMeta.action !== 'set-value'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(value);
|
||||
}}
|
||||
openMenuOnClick={false}
|
||||
openMenuOnFocus={false}
|
||||
components={{ DropdownIndicator: () => null }}
|
||||
onBlur={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { FormikErrors } from 'formik';
|
||||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { trimSHA, trimVersionTag } from 'Docker/filters/utils';
|
||||
|
||||
import DockerIcon from '@/assets/ico/vendor/docker.svg?c';
|
||||
import { useImages } from '@/react/docker/proxy/queries/images/useImages';
|
||||
|
@ -83,7 +84,9 @@ export function SimpleForm({
|
|||
title="Search image on Docker Hub"
|
||||
color="default"
|
||||
props={{
|
||||
href: 'https://hub.docker.com/search?type=image&q={ $ctrl.model.Image | trimshasum | trimversiontag }',
|
||||
href: `https://hub.docker.com/search?type=image&q=${trimVersionTag(
|
||||
trimSHA(values.image)
|
||||
)}`,
|
||||
target: '_blank',
|
||||
rel: 'noreferrer',
|
||||
}}
|
||||
|
@ -140,6 +143,14 @@ function RegistrySelector({
|
|||
label: registry.Name,
|
||||
value: registry.Id,
|
||||
})),
|
||||
onSuccess: (options) => {
|
||||
if (options && options.length) {
|
||||
const idx = options.findIndex((v) => v.value === value);
|
||||
if (idx === -1) {
|
||||
onChange(options[0].value);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -164,7 +175,7 @@ function ImageField({
|
|||
onChange: (value: string) => void;
|
||||
registry?: Registry;
|
||||
autoComplete?: boolean;
|
||||
inputId?: string;
|
||||
inputId: string;
|
||||
}) {
|
||||
return autoComplete ? (
|
||||
<ImageFieldAutoComplete
|
||||
|
@ -191,7 +202,7 @@ function ImageFieldAutoComplete({
|
|||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
registry?: Registry;
|
||||
inputId?: string;
|
||||
inputId: string;
|
||||
}) {
|
||||
const environmentId = useEnvironmentId();
|
||||
|
||||
|
|
|
@ -2,10 +2,10 @@ import { bool, number, object, SchemaOf, string } from 'yup';
|
|||
|
||||
import { Values } from './types';
|
||||
|
||||
export function validation(rateLimitExceeded: boolean): SchemaOf<Values> {
|
||||
export function validation(): SchemaOf<Values> {
|
||||
return object({
|
||||
image: string().required('Image is required'),
|
||||
registryId: number().default(0),
|
||||
useRegistry: bool().default(false),
|
||||
}).test('rate-limits', 'Rate limit exceeded', () => !rateLimitExceeded);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import '@reach/combobox/styles.css';
|
||||
|
||||
import { useState, ChangeEvent } from 'react';
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxList,
|
||||
ComboboxOption,
|
||||
ComboboxPopover,
|
||||
} from '@reach/combobox';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { useDebounce } from '@/react/hooks/useDebounce';
|
||||
|
||||
import { Option } from '@@/form-components/PortainerSelect';
|
||||
|
||||
import styles from './AutocompleteSelect.module.css';
|
||||
|
||||
export function AutocompleteSelect({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
searchResults,
|
||||
readOnly,
|
||||
inputId,
|
||||
}: {
|
||||
value: string;
|
||||
/**
|
||||
* onChange is called whenever the input is changed or an option is selected
|
||||
*
|
||||
* when the input is changed, the call is debounced
|
||||
*/
|
||||
onChange(value: string): void;
|
||||
placeholder?: string;
|
||||
searchResults?: Option<string>[];
|
||||
readOnly?: boolean;
|
||||
inputId: string;
|
||||
}) {
|
||||
const [searchTerm, setSearchTerm] = useDebounce(value, onChange);
|
||||
const [selected, setSelected] = useState(false);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
className={styles.root}
|
||||
aria-label="compose"
|
||||
onSelect={onSelect}
|
||||
data-cy="component-gitComposeInput"
|
||||
>
|
||||
<ComboboxInput
|
||||
value={searchTerm}
|
||||
className="form-control"
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
id={inputId}
|
||||
/>
|
||||
{!selected && searchResults && searchResults.length > 0 && (
|
||||
<ComboboxPopover>
|
||||
<ComboboxList>
|
||||
{searchResults.map((option: Option<string>) => (
|
||||
<ComboboxOption
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={clsx(
|
||||
`[&[aria-selected="true"]]:th-highcontrast:!bg-black [&[aria-selected="true"]]:th-dark:!bg-black`,
|
||||
`hover:th-highcontrast:!bg-black hover:th-dark:!bg-black`,
|
||||
'th-highcontrast:bg-gray-10 th-dark:bg-gray-10 '
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</ComboboxList>
|
||||
</ComboboxPopover>
|
||||
)}
|
||||
</Combobox>
|
||||
);
|
||||
|
||||
function handleChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
setSearchTerm(e.target.value);
|
||||
setSelected(false);
|
||||
}
|
||||
|
||||
function onSelect(value: string) {
|
||||
onChange(value);
|
||||
setSelected(true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { AutocompleteSelect } from './AutocompleteSelect';
|
|
@ -29,14 +29,18 @@ export function validation(
|
|||
name: string()
|
||||
.default('')
|
||||
.test('not-duplicate-portainer', () => !isDuplicatingPortainer),
|
||||
alwaysPull: boolean().default(true),
|
||||
alwaysPull: boolean()
|
||||
.default(true)
|
||||
.test('rate-limits', 'Rate limit exceeded', (alwaysPull: boolean) =>
|
||||
alwaysPull ? !isDockerhubRateLimited : true
|
||||
),
|
||||
accessControl: accessControlSchema(isAdmin),
|
||||
autoRemove: boolean().default(false),
|
||||
enableWebhook: boolean().default(false),
|
||||
nodeName: string().default(''),
|
||||
ports: portsSchema(),
|
||||
publishAllPorts: boolean().default(false),
|
||||
image: imageConfigValidation(isDockerhubRateLimited).test(
|
||||
image: imageConfigValidation().test(
|
||||
'duplicate-must-have-registry',
|
||||
'Duplicate is only possible when registry is selected',
|
||||
(value) => !isDuplicating || typeof value.registryId !== 'undefined'
|
||||
|
|
|
@ -10,6 +10,5 @@ export function encodeRegistryCredentials(registryId: Registry['Id']) {
|
|||
registryId,
|
||||
};
|
||||
|
||||
const buf = Buffer.from(JSON.stringify(credentials));
|
||||
return buf.toString('base64url');
|
||||
return window.btoa(JSON.stringify(credentials));
|
||||
}
|
||||
|
|
|
@ -276,7 +276,7 @@ function InnerForm({
|
|||
errors={errors.authentication}
|
||||
/>
|
||||
|
||||
{isBE && <RelativePathFieldset value={values.relativePath} readonly />}
|
||||
{isBE && <RelativePathFieldset value={values.relativePath} isEditing />}
|
||||
|
||||
<EnvironmentVariablesPanel
|
||||
onChange={(value) => setFieldValue('envVars', value)}
|
||||
|
|
|
@ -63,6 +63,7 @@ export function ComposePathField({
|
|||
onChange={onChange}
|
||||
placeholder={isCompose ? 'docker-compose.yml' : 'manifest.yml'}
|
||||
model={model}
|
||||
inputId="stack_repository_path"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
|
@ -71,6 +72,7 @@ export function ComposePathField({
|
|||
updateInputValue(e.target.value);
|
||||
}}
|
||||
placeholder={isCompose ? 'docker-compose.yml' : 'manifest.yml'}
|
||||
id="stack_repository_path"
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
|
|
|
@ -1,22 +1,10 @@
|
|||
import { ChangeEvent } from 'react';
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxList,
|
||||
ComboboxOption,
|
||||
ComboboxPopover,
|
||||
} from '@reach/combobox';
|
||||
import '@reach/combobox/styles.css';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { useSearch } from '@/react/portainer/gitops/queries/useSearch';
|
||||
import { useDebounce } from '@/react/hooks/useDebounce';
|
||||
|
||||
import { AutocompleteSelect } from '@@/form-components/AutocompleteSelect';
|
||||
|
||||
import { getAuthentication } from '../utils';
|
||||
import { GitFormModel } from '../types';
|
||||
|
||||
import styles from './PathSelector.module.css';
|
||||
|
||||
export function PathSelector({
|
||||
value,
|
||||
onChange,
|
||||
|
@ -24,6 +12,7 @@ export function PathSelector({
|
|||
model,
|
||||
dirOnly,
|
||||
readOnly,
|
||||
inputId,
|
||||
}: {
|
||||
value: string;
|
||||
onChange(value: string): void;
|
||||
|
@ -31,62 +20,33 @@ export function PathSelector({
|
|||
model: GitFormModel;
|
||||
dirOnly?: boolean;
|
||||
readOnly?: boolean;
|
||||
inputId: string;
|
||||
}) {
|
||||
const [searchTerm, setSearchTerm] = useDebounce(value, onChange);
|
||||
|
||||
const creds = getAuthentication(model);
|
||||
const payload = {
|
||||
repository: model.RepositoryURL,
|
||||
keyword: searchTerm,
|
||||
keyword: value,
|
||||
reference: model.RepositoryReferenceName,
|
||||
tlsSkipVerify: model.TLSSkipVerify,
|
||||
dirOnly,
|
||||
...creds,
|
||||
};
|
||||
const enabled = Boolean(
|
||||
model.RepositoryURL && model.RepositoryURLValid && searchTerm
|
||||
model.RepositoryURL && model.RepositoryURLValid && value
|
||||
);
|
||||
const { data: searchResults } = useSearch(payload, enabled);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
className={styles.root}
|
||||
aria-label="compose"
|
||||
onSelect={onSelect}
|
||||
data-cy="component-gitComposeInput"
|
||||
>
|
||||
<ComboboxInput
|
||||
value={searchTerm}
|
||||
className="form-control"
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
{searchResults && searchResults.length > 0 && (
|
||||
<ComboboxPopover>
|
||||
<ComboboxList>
|
||||
{searchResults.map((result: string, index: number) => (
|
||||
<ComboboxOption
|
||||
key={index}
|
||||
value={result}
|
||||
className={clsx(
|
||||
`[&[aria-selected="true"]]:th-highcontrast:!bg-black [&[aria-selected="true"]]:th-dark:!bg-black`,
|
||||
`hover:th-highcontrast:!bg-black hover:th-dark:!bg-black`,
|
||||
'th-highcontrast:bg-gray-10 th-dark:bg-gray-10 '
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</ComboboxList>
|
||||
</ComboboxPopover>
|
||||
)}
|
||||
</Combobox>
|
||||
<AutocompleteSelect
|
||||
searchResults={searchResults?.map((result) => ({
|
||||
value: result,
|
||||
label: result,
|
||||
}))}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
inputId={inputId}
|
||||
/>
|
||||
);
|
||||
|
||||
function handleChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
setSearchTerm(e.target.value);
|
||||
}
|
||||
|
||||
function onSelect(value: string) {
|
||||
onChange(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,14 +16,14 @@ interface Props {
|
|||
value: RelativePathModel;
|
||||
gitModel?: GitFormModel;
|
||||
onChange?: (value: Partial<RelativePathModel>) => void;
|
||||
readonly?: boolean;
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
export function RelativePathFieldset({
|
||||
value,
|
||||
gitModel,
|
||||
onChange,
|
||||
readonly,
|
||||
isEditing,
|
||||
}: Props) {
|
||||
const innerOnChange = useCallback(
|
||||
(value: Partial<RelativePathModel>) => onChange && onChange(value),
|
||||
|
@ -41,7 +41,7 @@ export function RelativePathFieldset({
|
|||
label="Enable relative path volumes"
|
||||
labelClass="col-sm-3 col-lg-2"
|
||||
tooltip="Enabling this means you can specify relative path volumes in your Compose files, with Portainer pulling the content from your git repository to the environment the stack is deployed to."
|
||||
disabled={readonly}
|
||||
disabled={isEditing}
|
||||
checked={value.SupportRelativePath}
|
||||
onChange={(value) => innerOnChange({ SupportRelativePath: value })}
|
||||
/>
|
||||
|
@ -68,7 +68,7 @@ export function RelativePathFieldset({
|
|||
<Input
|
||||
name="FilesystemPath"
|
||||
placeholder="/mnt"
|
||||
disabled={readonly}
|
||||
disabled={isEditing}
|
||||
value={value.FilesystemPath}
|
||||
onChange={(e) =>
|
||||
innerOnChange({ FilesystemPath: e.target.value })
|
||||
|
@ -94,7 +94,7 @@ export function RelativePathFieldset({
|
|||
label="GitOps Edge configurations"
|
||||
labelClass="col-sm-3 col-lg-2"
|
||||
tooltip="By enabling the GitOps Edge Configurations feature, you gain the ability to define relative path volumes in your configuration files. Portainer will then automatically fetch the content from your git repository by matching the folder name or file name with the Portainer Edge ID, and apply it to the environment where the stack is deployed"
|
||||
disabled={readonly}
|
||||
disabled={isEditing}
|
||||
checked={!!value.SupportPerDeviceConfigs}
|
||||
onChange={(value) =>
|
||||
innerOnChange({ SupportPerDeviceConfigs: value })
|
||||
|
@ -120,6 +120,7 @@ export function RelativePathFieldset({
|
|||
<FormControl
|
||||
label="Directory"
|
||||
errors={errors.PerDeviceConfigsPath}
|
||||
inputId="per_device_configs_path_input"
|
||||
>
|
||||
<PathSelector
|
||||
value={value.PerDeviceConfigsPath || ''}
|
||||
|
@ -128,8 +129,9 @@ export function RelativePathFieldset({
|
|||
}
|
||||
placeholder="config"
|
||||
model={gitModel || dummyGitForm}
|
||||
readOnly={readonly}
|
||||
readOnly={isEditing}
|
||||
dirOnly
|
||||
inputId="per_device_configs_path_input"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
@ -174,7 +176,7 @@ export function RelativePathFieldset({
|
|||
value: 'dir',
|
||||
},
|
||||
]}
|
||||
disabled={readonly}
|
||||
disabled={isEditing}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
@ -205,7 +207,7 @@ export function RelativePathFieldset({
|
|||
value: 'dir',
|
||||
},
|
||||
]}
|
||||
disabled={readonly}
|
||||
disabled={isEditing}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
|
|
@ -3,29 +3,18 @@ import angular from 'angular';
|
|||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { PathSelector } from '@/react/portainer/gitops/ComposePathField/PathSelector';
|
||||
import { RelativePathFieldset } from '@/react/portainer/gitops/RelativePathFieldset/RelativePathFieldset';
|
||||
|
||||
export const ngModule = angular
|
||||
.module('portainer.app.react.gitops', [])
|
||||
.component(
|
||||
'pathSelector',
|
||||
r2a(withUIRouter(withReactQuery(PathSelector)), [
|
||||
'value',
|
||||
'onChange',
|
||||
'placeholder',
|
||||
'model',
|
||||
'dirOnly',
|
||||
'readOnly',
|
||||
])
|
||||
)
|
||||
|
||||
.component(
|
||||
'relativePathFieldset',
|
||||
r2a(withUIRouter(withReactQuery(RelativePathFieldset)), [
|
||||
'value',
|
||||
'gitModel',
|
||||
'onChange',
|
||||
'readonly',
|
||||
'isEditing',
|
||||
])
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
|
||||
import { Registry } from '../types/registry';
|
||||
|
||||
|
@ -8,20 +9,26 @@ import { buildUrl } from './build-url';
|
|||
import { queryKeys } from './query-keys';
|
||||
|
||||
export function useRegistry(registryId?: Registry['Id']) {
|
||||
const environmentId = useEnvironmentId();
|
||||
|
||||
return useQuery(
|
||||
registryId ? queryKeys.item(registryId) : [],
|
||||
() => (registryId ? getRegistry(registryId) : undefined),
|
||||
() => (registryId ? getRegistry(registryId, environmentId) : undefined),
|
||||
{
|
||||
enabled: !!registryId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function getRegistry(registryId: Registry['Id']) {
|
||||
async function getRegistry(registryId: Registry['Id'], environmentId: number) {
|
||||
try {
|
||||
const { data } = await axios.get<Registry>(buildUrl(registryId));
|
||||
const { data } = await axios.get<Registry>(buildUrl(registryId), {
|
||||
params: {
|
||||
endpointId: environmentId,
|
||||
},
|
||||
});
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw parseAxiosError(err as Error, 'Unable to retrieve registry');
|
||||
throw parseAxiosError(err as Error, 'XXXUnable to retrieve registry');
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue