mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
refactor(kubernetes): move react codebase [EE-3349] (#7953)
This commit is contained in:
parent
2868da296a
commit
77c29ff87e
33 changed files with 18 additions and 21 deletions
604
app/react/kubernetes/ingresses/CreateIngressView/IngressForm.tsx
Normal file
604
app/react/kubernetes/ingresses/CreateIngressView/IngressForm.tsx
Normal file
|
@ -0,0 +1,604 @@
|
|||
import { ChangeEvent, ReactNode } from 'react';
|
||||
import { Plus, RefreshCw, Trash2 } from 'react-feather';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
import { Icon } from '@@/Icon';
|
||||
import { Select, Option } from '@@/form-components/Input/Select';
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { Annotations } from './Annotations';
|
||||
import { Rule, ServicePorts } from './types';
|
||||
|
||||
import '../style.css';
|
||||
|
||||
const PathTypes: Record<string, string[]> = {
|
||||
nginx: ['ImplementationSpecific', 'Prefix', 'Exact'],
|
||||
traefik: ['Prefix', 'Exact'],
|
||||
other: ['Prefix', 'Exact'],
|
||||
};
|
||||
const PlaceholderAnnotations: Record<string, string[]> = {
|
||||
nginx: ['e.g. nginx.ingress.kubernetes.io/rewrite-target', '/$1'],
|
||||
traefik: ['e.g. traefik.ingress.kubernetes.io/router.tls', 'true'],
|
||||
other: ['e.g. app.kubernetes.io/name', 'examplename'],
|
||||
};
|
||||
|
||||
interface Props {
|
||||
environmentID: number;
|
||||
rule: Rule;
|
||||
|
||||
errors: Record<string, ReactNode>;
|
||||
isLoading: boolean;
|
||||
isEdit: boolean;
|
||||
namespace: string;
|
||||
|
||||
servicePorts: ServicePorts;
|
||||
ingressClassOptions: Option<string>[];
|
||||
serviceOptions: Option<string>[];
|
||||
tlsOptions: Option<string>[];
|
||||
namespacesOptions: Option<string>[];
|
||||
|
||||
removeIngressRoute: (hostIndex: number, pathIndex: number) => void;
|
||||
removeIngressHost: (hostIndex: number) => void;
|
||||
removeAnnotation: (index: number) => void;
|
||||
|
||||
addNewIngressHost: (noHost?: boolean) => void;
|
||||
addNewIngressRoute: (hostIndex: number) => void;
|
||||
addNewAnnotation: (type?: 'rewrite' | 'regex' | 'ingressClass') => void;
|
||||
|
||||
handleNamespaceChange: (val: string) => void;
|
||||
handleHostChange: (hostIndex: number, val: string) => void;
|
||||
handleTLSChange: (hostIndex: number, tls: string) => void;
|
||||
handleIngressChange: (
|
||||
key: 'IngressName' | 'IngressClassName',
|
||||
value: string
|
||||
) => void;
|
||||
handleAnnotationChange: (
|
||||
index: number,
|
||||
key: 'Key' | 'Value',
|
||||
val: string
|
||||
) => void;
|
||||
handlePathChange: (
|
||||
hostIndex: number,
|
||||
pathIndex: number,
|
||||
key: 'Route' | 'PathType' | 'ServiceName' | 'ServicePort',
|
||||
val: string
|
||||
) => void;
|
||||
|
||||
reloadTLSCerts: () => void;
|
||||
}
|
||||
|
||||
export function IngressForm({
|
||||
environmentID,
|
||||
rule,
|
||||
isLoading,
|
||||
isEdit,
|
||||
servicePorts,
|
||||
tlsOptions,
|
||||
handleTLSChange,
|
||||
addNewIngressHost,
|
||||
serviceOptions,
|
||||
handleHostChange,
|
||||
handleIngressChange,
|
||||
handlePathChange,
|
||||
addNewIngressRoute,
|
||||
removeIngressRoute,
|
||||
removeIngressHost,
|
||||
addNewAnnotation,
|
||||
removeAnnotation,
|
||||
reloadTLSCerts,
|
||||
handleAnnotationChange,
|
||||
ingressClassOptions,
|
||||
errors,
|
||||
namespacesOptions,
|
||||
handleNamespaceChange,
|
||||
namespace,
|
||||
}: Props) {
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
const hasNoHostRule = rule.Hosts?.some((host) => host.NoHost);
|
||||
const placeholderAnnotation =
|
||||
PlaceholderAnnotations[rule.IngressType || 'other'] ||
|
||||
PlaceholderAnnotations.other;
|
||||
const pathTypes = PathTypes[rule.IngressType || 'other'] || PathTypes.other;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<WidgetTitle icon="svg-route" title="Ingress" />
|
||||
<WidgetBody key={rule.Key + rule.Namespace}>
|
||||
<div className="row">
|
||||
<div className="form-horizontal">
|
||||
<div className="form-group">
|
||||
<label
|
||||
className="control-label text-muted col-sm-3 col-lg-2 required"
|
||||
htmlFor="namespace"
|
||||
>
|
||||
Namespace
|
||||
</label>
|
||||
<div className="col-sm-4">
|
||||
{isEdit ? (
|
||||
namespace
|
||||
) : (
|
||||
<Select
|
||||
name="namespaces"
|
||||
options={namespacesOptions || []}
|
||||
onChange={(e) => handleNamespaceChange(e.target.value)}
|
||||
defaultValue={namespace}
|
||||
disabled={isEdit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{namespace && (
|
||||
<div className="row">
|
||||
<div className="form-horizontal">
|
||||
<div className="form-group">
|
||||
<label
|
||||
className="control-label text-muted col-sm-3 col-lg-2 required"
|
||||
htmlFor="ingress_name"
|
||||
>
|
||||
Ingress name
|
||||
</label>
|
||||
<div className="col-sm-4">
|
||||
{isEdit ? (
|
||||
rule.IngressName
|
||||
) : (
|
||||
<input
|
||||
name="ingress_name"
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Ingress name"
|
||||
defaultValue={rule.IngressName}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
handleIngressChange('IngressName', e.target.value)
|
||||
}
|
||||
disabled={isEdit}
|
||||
/>
|
||||
)}
|
||||
{errors.ingressName && !isEdit && (
|
||||
<FormError className="mt-1 error-inline">
|
||||
{errors.ingressName}
|
||||
</FormError>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group" key={ingressClassOptions.toString()}>
|
||||
<label
|
||||
className="control-label text-muted col-sm-3 col-lg-2 required"
|
||||
htmlFor="ingress_class"
|
||||
>
|
||||
Ingress class
|
||||
</label>
|
||||
<div className="col-sm-4">
|
||||
<Select
|
||||
name="ingress_class"
|
||||
className="form-control"
|
||||
placeholder="Ingress name"
|
||||
defaultValue={rule.IngressClassName}
|
||||
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||
handleIngressChange('IngressClassName', e.target.value)
|
||||
}
|
||||
options={ingressClassOptions}
|
||||
/>
|
||||
{errors.className && (
|
||||
<FormError className="mt-1 error-inline">
|
||||
{errors.className}
|
||||
</FormError>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-12 px-0 text-muted !mb-0">
|
||||
<div className="mb-2">Annotations</div>
|
||||
<p className="vertical-center text-muted small">
|
||||
<Icon icon="info" mode="primary" feather />
|
||||
<span>
|
||||
You can specify{' '}
|
||||
<a
|
||||
href="https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/"
|
||||
target="_black"
|
||||
>
|
||||
annotations
|
||||
</a>{' '}
|
||||
for the object. See further Kubernetes documentation on{' '}
|
||||
<a
|
||||
href="https://kubernetes.io/docs/reference/labels-annotations-taints/"
|
||||
target="_black"
|
||||
>
|
||||
well-known annotations
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{rule?.Annotations && (
|
||||
<Annotations
|
||||
placeholder={placeholderAnnotation}
|
||||
annotations={rule.Annotations}
|
||||
handleAnnotationChange={handleAnnotationChange}
|
||||
removeAnnotation={removeAnnotation}
|
||||
errors={errors}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="col-sm-12 p-0 anntation-actions">
|
||||
<Button
|
||||
className="btn btn-sm btn-light mb-2 !ml-0"
|
||||
onClick={() => addNewAnnotation()}
|
||||
icon={Plus}
|
||||
title="Use annotations to configure options for an ingress. Review Nginx or Traefik documentation to find the annotations supported by your choice of ingress type."
|
||||
>
|
||||
{' '}
|
||||
add annotation
|
||||
</Button>
|
||||
|
||||
{rule.IngressType === 'nginx' && (
|
||||
<>
|
||||
<Button
|
||||
className="btn btn-sm btn-light mb-2 ml-2"
|
||||
onClick={() => addNewAnnotation('rewrite')}
|
||||
icon={Plus}
|
||||
title="When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to."
|
||||
data-cy="add-rewrite-annotation"
|
||||
>
|
||||
{' '}
|
||||
Add rewrite annotation
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="btn btn-sm btn-light mb-2 ml-2"
|
||||
onClick={() => addNewAnnotation('regex')}
|
||||
icon={Plus}
|
||||
title="When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to."
|
||||
data-cy="add-regex-annotation"
|
||||
>
|
||||
Add regular expression annotation
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{rule.IngressType === 'custom' && (
|
||||
<Button
|
||||
className="btn btn-sm btn-light mb-2 ml-2"
|
||||
onClick={() => addNewAnnotation('ingressClass')}
|
||||
icon={Plus}
|
||||
data-cy="add-ingress-class-annotation"
|
||||
>
|
||||
Add kubernetes.io/ingress.class annotation
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="col-sm-12 px-0 text-muted">Rules</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{namespace &&
|
||||
rule?.Hosts?.map((host, hostIndex) => (
|
||||
<div className="row mb-5 rule bordered" key={host.Key}>
|
||||
<div className="col-sm-12">
|
||||
<div className="row mt-5 rule-actions">
|
||||
<div className="col-sm-3 p-0">
|
||||
{!host.NoHost ? 'Rule' : 'Fallback rule'}
|
||||
</div>
|
||||
<div className="col-sm-9 p-0 text-right">
|
||||
{!host.NoHost && (
|
||||
<Button
|
||||
className="btn btn-light btn-sm"
|
||||
onClick={() => reloadTLSCerts()}
|
||||
icon={RefreshCw}
|
||||
>
|
||||
Reload TLS secrets
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="btn btn-sm ml-2"
|
||||
color="dangerlight"
|
||||
type="button"
|
||||
data-cy={`k8sAppCreate-rmHostButton_${hostIndex}`}
|
||||
onClick={() => removeIngressHost(hostIndex)}
|
||||
disabled={rule.Hosts.length === 1}
|
||||
icon={Trash2}
|
||||
>
|
||||
Remove rule
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{!host.NoHost && (
|
||||
<div className="row">
|
||||
<div className="form-group !pl-0 col-sm-6 col-lg-4 !pr-2">
|
||||
<div className="input-group input-group-sm">
|
||||
<span className="input-group-addon required">
|
||||
Hostname
|
||||
</span>
|
||||
<input
|
||||
name={`ingress_host_${hostIndex}`}
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="e.g. example.com"
|
||||
defaultValue={host.Host}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
handleHostChange(hostIndex, e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{errors[`hosts[${hostIndex}].host`] && (
|
||||
<FormError className="mt-1 !mb-0">
|
||||
{errors[`hosts[${hostIndex}].host`]}
|
||||
</FormError>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group !pr-0 col-sm-6 col-lg-4 !pl-2">
|
||||
<div className="input-group input-group-sm">
|
||||
<span className="input-group-addon">TLS secret</span>
|
||||
<Select
|
||||
key={tlsOptions.toString() + host.Secret}
|
||||
name={`ingress_tls_${hostIndex}`}
|
||||
options={tlsOptions}
|
||||
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||
handleTLSChange(hostIndex, e.target.value)
|
||||
}
|
||||
defaultValue={host.Secret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="vertical-center text-muted small whitespace-nowrap col-sm-12 !p-0">
|
||||
<Icon icon="info" mode="primary" size="md" feather />
|
||||
<span>
|
||||
Add a secret via{' '}
|
||||
<Link
|
||||
to="kubernetes.configurations"
|
||||
params={{ id: environmentID }}
|
||||
className="text-primary"
|
||||
target="_blank"
|
||||
>
|
||||
ConfigMaps & Secrets
|
||||
</Link>
|
||||
{', '}
|
||||
then select 'Reload TLS secrets' above to
|
||||
populate the dropdown with your changes.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{host.NoHost && (
|
||||
<p className="vertical-center text-muted small whitespace-nowrap col-sm-12 !p-0">
|
||||
<Icon icon="info" mode="primary" size="md" feather />A
|
||||
fallback rule has no host specified. This rule only applies
|
||||
when an inbound request has a hostname that does not match
|
||||
with any of your other rules.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12 px-0 !mb-0 mt-2 text-muted">
|
||||
Paths
|
||||
</div>
|
||||
</div>
|
||||
{host.Paths.map((path, pathIndex) => (
|
||||
<div
|
||||
className="mt-5 !mb-5 row path"
|
||||
key={`path_${path.Key}}`}
|
||||
>
|
||||
<div className="form-group !pl-0 col-sm-3 col-xl-2 !m-0">
|
||||
<div className="input-group input-group-sm">
|
||||
<span className="input-group-addon required">
|
||||
Service
|
||||
</span>
|
||||
<Select
|
||||
key={serviceOptions.toString() + path.ServiceName}
|
||||
name={`ingress_service_${hostIndex}_${pathIndex}`}
|
||||
options={serviceOptions}
|
||||
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||
handlePathChange(
|
||||
hostIndex,
|
||||
pathIndex,
|
||||
'ServiceName',
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
defaultValue={path.ServiceName}
|
||||
/>
|
||||
</div>
|
||||
{errors[
|
||||
`hosts[${hostIndex}].paths[${pathIndex}].servicename`
|
||||
] && (
|
||||
<FormError className="mt-1 !mb-0 error-inline">
|
||||
{
|
||||
errors[
|
||||
`hosts[${hostIndex}].paths[${pathIndex}].servicename`
|
||||
]
|
||||
}
|
||||
</FormError>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group !pl-0 col-sm-2 col-xl-2 !m-0">
|
||||
{servicePorts && (
|
||||
<>
|
||||
<div className="input-group input-group-sm">
|
||||
<span className="input-group-addon required">
|
||||
Service port
|
||||
</span>
|
||||
<Select
|
||||
key={servicePorts.toString() + path.ServicePort}
|
||||
name={`ingress_servicePort_${hostIndex}_${pathIndex}`}
|
||||
options={
|
||||
path.ServiceName &&
|
||||
servicePorts[path.ServiceName]
|
||||
? servicePorts[path.ServiceName]
|
||||
: [
|
||||
{
|
||||
label: 'Select port',
|
||||
value: '',
|
||||
},
|
||||
]
|
||||
}
|
||||
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||
handlePathChange(
|
||||
hostIndex,
|
||||
pathIndex,
|
||||
'ServicePort',
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
defaultValue={path.ServicePort}
|
||||
/>
|
||||
</div>
|
||||
{errors[
|
||||
`hosts[${hostIndex}].paths[${pathIndex}].serviceport`
|
||||
] && (
|
||||
<FormError className="mt-1 !mb-0">
|
||||
{
|
||||
errors[
|
||||
`hosts[${hostIndex}].paths[${pathIndex}].serviceport`
|
||||
]
|
||||
}
|
||||
</FormError>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group !pl-0 col-sm-3 col-xl-2 !m-0">
|
||||
<div className="input-group input-group-sm">
|
||||
<span className="input-group-addon required">
|
||||
Path type
|
||||
</span>
|
||||
<Select
|
||||
key={servicePorts.toString() + path.PathType}
|
||||
name={`ingress_pathType_${hostIndex}_${pathIndex}`}
|
||||
options={
|
||||
pathTypes
|
||||
? pathTypes.map((type) => ({
|
||||
label: type,
|
||||
value: type,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
|
||||
handlePathChange(
|
||||
hostIndex,
|
||||
pathIndex,
|
||||
'PathType',
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
defaultValue={path.PathType}
|
||||
/>
|
||||
</div>
|
||||
{errors[
|
||||
`hosts[${hostIndex}].paths[${pathIndex}].pathType`
|
||||
] && (
|
||||
<FormError className="mt-1 !mb-0">
|
||||
{
|
||||
errors[
|
||||
`hosts[${hostIndex}].paths[${pathIndex}].pathType`
|
||||
]
|
||||
}
|
||||
</FormError>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group !pl-0 col-sm-3 col-xl-3 !m-0">
|
||||
<div className="input-group input-group-sm">
|
||||
<span className="input-group-addon required">Path</span>
|
||||
<input
|
||||
className="form-control"
|
||||
name={`ingress_route_${hostIndex}-${pathIndex}`}
|
||||
placeholder="/example"
|
||||
data-pattern="/^(\/?[a-zA-Z0-9]+([a-zA-Z0-9-/_]*[a-zA-Z0-9])?|[a-zA-Z0-9]+)|(\/){1}$/"
|
||||
data-cy={`k8sAppCreate-route_${hostIndex}-${pathIndex}`}
|
||||
defaultValue={path.Route}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
handlePathChange(
|
||||
hostIndex,
|
||||
pathIndex,
|
||||
'Route',
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{errors[
|
||||
`hosts[${hostIndex}].paths[${pathIndex}].path`
|
||||
] && (
|
||||
<FormError className="mt-1 !mb-0">
|
||||
{
|
||||
errors[
|
||||
`hosts[${hostIndex}].paths[${pathIndex}].path`
|
||||
]
|
||||
}
|
||||
</FormError>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group !pl-0 col-sm-1 !m-0">
|
||||
<Button
|
||||
className="btn btn-sm btn-only-icon !ml-0 vertical-center"
|
||||
color="dangerlight"
|
||||
type="button"
|
||||
data-cy={`k8sAppCreate-rmPortButton_${hostIndex}-${pathIndex}`}
|
||||
onClick={() => removeIngressRoute(hostIndex, pathIndex)}
|
||||
disabled={host.Paths.length === 1}
|
||||
icon={Trash2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="row mt-5">
|
||||
<Button
|
||||
className="btn btn-sm btn-light !ml-0"
|
||||
type="button"
|
||||
onClick={() => addNewIngressRoute(hostIndex)}
|
||||
icon={Plus}
|
||||
>
|
||||
Add path
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{namespace && (
|
||||
<div className="row p-0 rules-action">
|
||||
<div className="col-sm-12 p-0 vertical-center">
|
||||
<Button
|
||||
className="btn btn-sm btn-light !ml-0"
|
||||
type="button"
|
||||
onClick={() => addNewIngressHost()}
|
||||
icon={Plus}
|
||||
>
|
||||
Add new host
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="btn btn-sm btn-light ml-2"
|
||||
type="button"
|
||||
onClick={() => addNewIngressHost(true)}
|
||||
disabled={hasNoHostRule}
|
||||
icon={Plus}
|
||||
>
|
||||
Add fallback rule
|
||||
</Button>
|
||||
<Tooltip message="A fallback rule will be applied to all requests that do not match any of the defined hosts." />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue