mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
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>
This commit is contained in:
parent
e43d076269
commit
d089dfbca0
17 changed files with 190 additions and 137 deletions
|
@ -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,19 @@
|
|||
.root [data-reach-combobox-popover] {
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border-form-control-color);
|
||||
background-color: var(--bg-dropdown-menu-color);
|
||||
color: var(--text-form-control-color);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.root [data-reach-combobox-option]:hover {
|
||||
background-color: var(--bg-dropdown-hover);
|
||||
}
|
||||
|
||||
.root [data-suggested-value] {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.root [data-user-value] {
|
||||
font-weight: bold;
|
||||
}
|
|
@ -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';
|
Loading…
Add table
Add a link
Reference in a new issue