mirror of https://github.com/portainer/portainer
fix(ingress): ingress ui feedback [EE-5852] (#9982)
Co-authored-by: testa113 <testa113>pull/9986/head
parent
9845518aa9
commit
acf9203580
|
@ -199,7 +199,7 @@ export function IngressClassDatatable({
|
||||||
</p>
|
</p>
|
||||||
<ul className="ml-6">
|
<ul className="ml-6">
|
||||||
{usedControllersToDisallow.map((controller) => (
|
{usedControllersToDisallow.map((controller) => (
|
||||||
<li key={controller.ClassName}>${controller.ClassName}</li>
|
<li key={controller.ClassName}>{controller.ClassName}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -66,10 +66,8 @@ export function CreateIngressView() {
|
||||||
environmentId,
|
environmentId,
|
||||||
namespaces ? Object.keys(namespaces || {}) : []
|
namespaces ? Object.keys(namespaces || {}) : []
|
||||||
);
|
);
|
||||||
const ingressControllersQuery = useIngressControllers(
|
const { data: ingressControllers, ...ingressControllersQuery } =
|
||||||
environmentId,
|
useIngressControllers(environmentId, namespace);
|
||||||
namespace
|
|
||||||
);
|
|
||||||
|
|
||||||
const createIngressMutation = useCreateIngress();
|
const createIngressMutation = useCreateIngress();
|
||||||
const updateIngressMutation = useUpdateIngress();
|
const updateIngressMutation = useUpdateIngress();
|
||||||
|
@ -171,39 +169,84 @@ export function CreateIngressView() {
|
||||||
|
|
||||||
const existingIngressClass = useMemo(
|
const existingIngressClass = useMemo(
|
||||||
() =>
|
() =>
|
||||||
ingressControllersQuery.data?.find(
|
ingressControllers?.find(
|
||||||
(i) =>
|
(controller) =>
|
||||||
i.ClassName === ingressRule.IngressClassName ||
|
controller.ClassName === ingressRule.IngressClassName ||
|
||||||
(i.Type === 'custom' && ingressRule.IngressClassName === '')
|
(controller.Type === 'custom' && ingressRule.IngressClassName === '')
|
||||||
),
|
),
|
||||||
[ingressControllersQuery.data, ingressRule.IngressClassName]
|
[ingressControllers, ingressRule.IngressClassName]
|
||||||
);
|
);
|
||||||
|
|
||||||
const ingressClassOptions: Option<string>[] = useMemo(
|
const ingressClassOptions: Option<string>[] = useMemo(() => {
|
||||||
() =>
|
const allowedIngressClassOptions =
|
||||||
ingressControllersQuery.data
|
ingressControllers
|
||||||
?.filter((cls) => cls.Availability)
|
?.filter((controller) => !!controller.Availability)
|
||||||
.map((cls) => ({
|
.map((cls) => ({
|
||||||
label: cls.ClassName,
|
label: cls.ClassName,
|
||||||
value: cls.ClassName,
|
value: cls.ClassName,
|
||||||
})) || [],
|
})) || [];
|
||||||
[ingressControllersQuery.data]
|
|
||||||
|
// if the ingress class is not set, return only the allowed ingress classes
|
||||||
|
if (ingressRule.IngressClassName === '' || !isEdit) {
|
||||||
|
return allowedIngressClassOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the ingress class is set and it exists (even if disallowed), return the allowed ingress classes + the disallowed option
|
||||||
|
const disallowedIngressClasses =
|
||||||
|
ingressControllers
|
||||||
|
?.filter(
|
||||||
|
(controller) =>
|
||||||
|
!controller.Availability &&
|
||||||
|
existingIngressClass?.ClassName === controller.ClassName
|
||||||
|
)
|
||||||
|
.map((controller) => ({
|
||||||
|
label: `${controller.ClassName} - DISALLOWED`,
|
||||||
|
value: controller.ClassName,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const existingIngressClassFound = ingressControllers?.find(
|
||||||
|
(controller) => existingIngressClass?.ClassName === controller.ClassName
|
||||||
|
);
|
||||||
|
if (existingIngressClassFound) {
|
||||||
|
return [...allowedIngressClassOptions, ...disallowedIngressClasses];
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the ingress class is set and it doesn't exist, return the allowed ingress classes + the not found option
|
||||||
|
const notFoundIngressClassOption = {
|
||||||
|
label: `${ingressRule.IngressClassName} - NOT FOUND`,
|
||||||
|
value: ingressRule.IngressClassName || '',
|
||||||
|
};
|
||||||
|
return [...allowedIngressClassOptions, notFoundIngressClassOption];
|
||||||
|
}, [
|
||||||
|
existingIngressClass?.ClassName,
|
||||||
|
ingressControllers,
|
||||||
|
ingressRule.IngressClassName,
|
||||||
|
isEdit,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleIngressChange = useCallback(
|
||||||
|
(key: string, val: string) => {
|
||||||
|
setIngressRule((prevRules) => {
|
||||||
|
const rule = { ...prevRules, [key]: val };
|
||||||
|
if (key === 'IngressClassName') {
|
||||||
|
rule.IngressType = ingressControllers?.find(
|
||||||
|
(c) => c.ClassName === val
|
||||||
|
)?.Type;
|
||||||
|
}
|
||||||
|
return rule;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[ingressControllers]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
// when the ingress class options update the value to an available one
|
||||||
(!existingIngressClass ||
|
useEffect(() => {
|
||||||
(existingIngressClass && !existingIngressClass.Availability)) &&
|
const ingressClasses = ingressClassOptions.map((option) => option.value);
|
||||||
ingressRule.IngressClassName &&
|
if (!ingressClasses.includes(ingressRule.IngressClassName)) {
|
||||||
!ingressControllersQuery.isLoading
|
// setting to the first available option (or undefined when there are no options)
|
||||||
) {
|
handleIngressChange('IngressClassName', ingressClasses[0]);
|
||||||
const optionLabel = !ingressRule.IngressType
|
}
|
||||||
? `${ingressRule.IngressClassName} - NOT FOUND`
|
}, [handleIngressChange, ingressClassOptions, ingressRule.IngressClassName]);
|
||||||
: `${ingressRule.IngressClassName} - DISALLOWED`;
|
|
||||||
ingressClassOptions.push({
|
|
||||||
label: optionLabel,
|
|
||||||
value: ingressRule.IngressClassName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchedConfigs = configResults?.data?.filter(
|
const matchedConfigs = configResults?.data?.filter(
|
||||||
(config) =>
|
(config) =>
|
||||||
|
@ -234,7 +277,7 @@ export function CreateIngressView() {
|
||||||
(ing) => ing.Name === params.name && ing.Namespace === params.namespace
|
(ing) => ing.Name === params.name && ing.Namespace === params.namespace
|
||||||
);
|
);
|
||||||
if (ing) {
|
if (ing) {
|
||||||
const type = ingressControllersQuery.data?.find(
|
const type = ingressControllers?.find(
|
||||||
(c) =>
|
(c) =>
|
||||||
c.ClassName === ing.ClassName ||
|
c.ClassName === ing.ClassName ||
|
||||||
(c.Type === 'custom' && !ing.ClassName)
|
(c.Type === 'custom' && !ing.ClassName)
|
||||||
|
@ -248,7 +291,7 @@ export function CreateIngressView() {
|
||||||
}, [
|
}, [
|
||||||
params.name,
|
params.name,
|
||||||
ingressesResults.data,
|
ingressesResults.data,
|
||||||
ingressControllersQuery.data,
|
ingressControllers,
|
||||||
ingressRule.IngressName,
|
ingressRule.IngressName,
|
||||||
params.namespace,
|
params.namespace,
|
||||||
]);
|
]);
|
||||||
|
@ -559,18 +602,6 @@ export function CreateIngressView() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleIngressChange(key: string, val: string) {
|
|
||||||
setIngressRule((prevRules) => {
|
|
||||||
const rule = { ...prevRules, [key]: val };
|
|
||||||
if (key === 'IngressClassName') {
|
|
||||||
rule.IngressType = ingressControllersQuery.data?.find(
|
|
||||||
(c) => c.ClassName === val
|
|
||||||
)?.Type;
|
|
||||||
}
|
|
||||||
return rule;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTLSChange(hostIndex: number, tls: string) {
|
function handleTLSChange(hostIndex: number, tls: string) {
|
||||||
setIngressRule((prevRules) => {
|
setIngressRule((prevRules) => {
|
||||||
const rule = { ...prevRules };
|
const rule = { ...prevRules };
|
||||||
|
|
|
@ -119,17 +119,6 @@ export function IngressForm({
|
||||||
}
|
}
|
||||||
}, [namespacesOptions, namespace, handleNamespaceChange]);
|
}, [namespacesOptions, namespace, handleNamespaceChange]);
|
||||||
|
|
||||||
// when the ingress class options update update the value to an available one
|
|
||||||
useEffect(() => {
|
|
||||||
const ingressClasses = ingressClassOptions.map((option) => option.value);
|
|
||||||
if (
|
|
||||||
!ingressClasses.includes(rule.IngressClassName) &&
|
|
||||||
ingressClasses.length > 0
|
|
||||||
) {
|
|
||||||
handleIngressChange('IngressClassName', ingressClasses[0]);
|
|
||||||
}
|
|
||||||
}, [ingressClassOptions, rule.IngressClassName, handleIngressChange]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<WidgetTitle icon={Route} title="Ingress" />
|
<WidgetTitle icon={Route} title="Ingress" />
|
||||||
|
@ -157,12 +146,22 @@ export function IngressForm({
|
||||||
) : (
|
) : (
|
||||||
<Select
|
<Select
|
||||||
name="namespaces"
|
name="namespaces"
|
||||||
options={namespacesOptions || []}
|
options={namespacesOptions}
|
||||||
value={{ value: namespace, label: namespace }}
|
value={
|
||||||
|
namespace
|
||||||
|
? { value: namespace, label: namespace }
|
||||||
|
: null
|
||||||
|
}
|
||||||
isDisabled={isEdit}
|
isDisabled={isEdit}
|
||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
handleNamespaceChange(val?.value || '')
|
handleNamespaceChange(val?.value || '')
|
||||||
}
|
}
|
||||||
|
placeholder={
|
||||||
|
namespacesOptions.length
|
||||||
|
? 'Select a namespace'
|
||||||
|
: 'No namespaces available'
|
||||||
|
}
|
||||||
|
noOptionsMessage={() => 'No namespaces available'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -222,18 +221,27 @@ export function IngressForm({
|
||||||
<>
|
<>
|
||||||
<Select
|
<Select
|
||||||
name="ingress_class"
|
name="ingress_class"
|
||||||
placeholder="Ingress name"
|
placeholder={
|
||||||
|
ingressClassOptions.length
|
||||||
|
? 'Select an ingress class'
|
||||||
|
: 'No ingress classes available'
|
||||||
|
}
|
||||||
options={ingressClassOptions}
|
options={ingressClassOptions}
|
||||||
value={{
|
value={
|
||||||
label: rule.IngressClassName,
|
rule.IngressClassName
|
||||||
value: rule.IngressClassName,
|
? {
|
||||||
}}
|
label: rule.IngressClassName,
|
||||||
|
value: rule.IngressClassName,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
onChange={(ingressClassOption) =>
|
onChange={(ingressClassOption) =>
|
||||||
handleIngressChange(
|
handleIngressChange(
|
||||||
'IngressClassName',
|
'IngressClassName',
|
||||||
ingressClassOption?.value || ''
|
ingressClassOption?.value || ''
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
noOptionsMessage={() => 'No ingress classes available'}
|
||||||
/>
|
/>
|
||||||
{errors.className && (
|
{errors.className && (
|
||||||
<FormError className="error-inline mt-1">
|
<FormError className="error-inline mt-1">
|
||||||
|
@ -242,11 +250,6 @@ export function IngressForm({
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{errors.className && (
|
|
||||||
<FormError className="error-inline mt-1">
|
|
||||||
{errors.className}
|
|
||||||
</FormError>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -401,13 +404,23 @@ export function IngressForm({
|
||||||
<Select
|
<Select
|
||||||
key={tlsOptions.toString() + host.Secret}
|
key={tlsOptions.toString() + host.Secret}
|
||||||
name={`ingress_tls_${hostIndex}`}
|
name={`ingress_tls_${hostIndex}`}
|
||||||
value={{
|
value={
|
||||||
value: rule.Hosts[hostIndex].Secret,
|
host.Secret !== undefined
|
||||||
label: rule.Hosts[hostIndex].Secret || 'No TLS',
|
? {
|
||||||
}}
|
value: host.Secret,
|
||||||
|
label: host.Secret || 'No TLS',
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
onChange={(TLSOption) =>
|
onChange={(TLSOption) =>
|
||||||
handleTLSChange(hostIndex, TLSOption?.value || '')
|
handleTLSChange(hostIndex, TLSOption?.value || '')
|
||||||
}
|
}
|
||||||
|
placeholder={
|
||||||
|
tlsOptions.length
|
||||||
|
? 'Select a TLS secret'
|
||||||
|
: 'No TLS secrets available'
|
||||||
|
}
|
||||||
|
noOptionsMessage={() => 'No TLS secrets available'}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
{!host.NoHost && (
|
{!host.NoHost && (
|
||||||
|
@ -472,10 +485,14 @@ export function IngressForm({
|
||||||
key={serviceOptions.toString() + path.ServiceName}
|
key={serviceOptions.toString() + path.ServiceName}
|
||||||
name={`ingress_service_${hostIndex}_${pathIndex}`}
|
name={`ingress_service_${hostIndex}_${pathIndex}`}
|
||||||
options={serviceOptions}
|
options={serviceOptions}
|
||||||
value={{
|
value={
|
||||||
value: path.ServiceName,
|
path.ServiceName
|
||||||
label: path.ServiceName || 'Select a service',
|
? {
|
||||||
}}
|
value: path.ServiceName,
|
||||||
|
label: path.ServiceName,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
onChange={(serviceOption) =>
|
onChange={(serviceOption) =>
|
||||||
handlePathChange(
|
handlePathChange(
|
||||||
hostIndex,
|
hostIndex,
|
||||||
|
@ -484,6 +501,12 @@ export function IngressForm({
|
||||||
serviceOption?.value || ''
|
serviceOption?.value || ''
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
placeholder={
|
||||||
|
serviceOptions.length
|
||||||
|
? 'Select a service'
|
||||||
|
: 'No services available'
|
||||||
|
}
|
||||||
|
noOptionsMessage={() => 'No services available'}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
@ -526,15 +549,20 @@ export function IngressForm({
|
||||||
option?.value || ''
|
option?.value || ''
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
value={{
|
value={
|
||||||
label: (
|
path.ServicePort
|
||||||
path.ServicePort || 'Select a port'
|
? {
|
||||||
).toString(),
|
label: path.ServicePort.toString(),
|
||||||
value:
|
value: path.ServicePort.toString(),
|
||||||
rule.Hosts[hostIndex].Paths[
|
}
|
||||||
pathIndex
|
: null
|
||||||
].ServicePort.toString(),
|
}
|
||||||
}}
|
placeholder={
|
||||||
|
servicePorts[path.ServiceName]?.length
|
||||||
|
? 'Select a port'
|
||||||
|
: 'No ports available'
|
||||||
|
}
|
||||||
|
noOptionsMessage={() => 'No ports available'}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
@ -573,10 +601,20 @@ export function IngressForm({
|
||||||
option?.value || ''
|
option?.value || ''
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
value={{
|
value={
|
||||||
label: path.PathType || 'Select a path type',
|
path.PathType
|
||||||
value: path.PathType || '',
|
? {
|
||||||
}}
|
label: path.PathType,
|
||||||
|
value: path.PathType,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
pathTypes?.length
|
||||||
|
? 'Select a path type'
|
||||||
|
: 'No path types available'
|
||||||
|
}
|
||||||
|
noOptionsMessage={() => 'No path types available'}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
Loading…
Reference in New Issue