mirror of https://github.com/portainer/portainer
feat(app): rearrange app form services [EE-5566] (#9056)
parent
d7fc2046d7
commit
2d69e93efa
@ -1,132 +0,0 @@
|
||||
import { FormikErrors } from 'formik';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { ButtonSelector } from '@@/form-components/ButtonSelector/ButtonSelector';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { isServicePortError, newPort } from './utils';
|
||||
import { ServicePort } from './types';
|
||||
import { ServicePortInput } from './ServicePortInput';
|
||||
import { ContainerPortInput } from './ContainerPortInput';
|
||||
|
||||
interface Props {
|
||||
values: ServicePort[];
|
||||
onChange: (servicePorts: ServicePort[]) => void;
|
||||
serviceName?: string;
|
||||
errors?: string | string[] | FormikErrors<ServicePort>[];
|
||||
}
|
||||
|
||||
export function ClusterIpForm({
|
||||
values: servicePorts,
|
||||
onChange,
|
||||
errors,
|
||||
serviceName,
|
||||
}: Props) {
|
||||
const newClusterIpPort = newPort(serviceName);
|
||||
return (
|
||||
<>
|
||||
<div className="control-label !mb-2 !pt-0 text-left">Published ports</div>
|
||||
<div className="mb-2 flex flex-col gap-4">
|
||||
{servicePorts.map((servicePort, index) => {
|
||||
const error = errors?.[index];
|
||||
const servicePortError = isServicePortError<ServicePort>(error)
|
||||
? error
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div key={index} className="flex flex-grow flex-wrap gap-2">
|
||||
<div className="flex w-1/3 min-w-min flex-col">
|
||||
<ContainerPortInput
|
||||
index={index}
|
||||
value={servicePort.targetPort}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
const newValue =
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value);
|
||||
newServicePorts[index] = {
|
||||
...newServicePorts[index],
|
||||
targetPort: newValue,
|
||||
port: newValue,
|
||||
};
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
/>
|
||||
{servicePortError?.targetPort && (
|
||||
<FormError>{servicePortError.targetPort}</FormError>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex w-1/3 min-w-min flex-col">
|
||||
<ServicePortInput
|
||||
index={index}
|
||||
value={servicePort.port}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[index] = {
|
||||
...newServicePorts[index],
|
||||
port:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
};
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
/>
|
||||
{servicePortError?.port && (
|
||||
<FormError>{servicePortError.port}</FormError>
|
||||
)}
|
||||
</div>
|
||||
<ButtonSelector
|
||||
className="h-[30px]"
|
||||
onChange={(value) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[index] = {
|
||||
...newServicePorts[index],
|
||||
protocol: value,
|
||||
};
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
value={servicePort.protocol || 'TCP'}
|
||||
options={[{ value: 'TCP' }, { value: 'UDP' }]}
|
||||
/>
|
||||
<Button
|
||||
disabled={servicePorts.length === 1}
|
||||
size="small"
|
||||
className="!ml-0 h-[30px]"
|
||||
color="danger"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// remove the port at the index in an immutable way
|
||||
const newServicePorts = [
|
||||
...servicePorts.slice(0, index),
|
||||
...servicePorts.slice(index + 1),
|
||||
];
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
data-cy={`k8sAppCreate-rmPortButton_${index}`}
|
||||
icon={Trash2}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex">
|
||||
<Button
|
||||
icon={Plus}
|
||||
color="default"
|
||||
className="!ml-0"
|
||||
onClick={() => {
|
||||
const newServicesPorts = [...servicePorts, newClusterIpPort];
|
||||
onChange(newServicesPorts);
|
||||
}}
|
||||
>
|
||||
Publish a new port
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
import { FormikErrors } from 'formik';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { ButtonSelector } from '@@/form-components/ButtonSelector/ButtonSelector';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Card } from '@@/Card';
|
||||
import { Widget } from '@@/Widget';
|
||||
|
||||
import { isServicePortError, newPort } from './utils';
|
||||
import { ServiceFormValues, ServicePort } from './types';
|
||||
import { ServicePortInput } from './ServicePortInput';
|
||||
import { ContainerPortInput } from './ContainerPortInput';
|
||||
|
||||
interface Props {
|
||||
services: ServiceFormValues[];
|
||||
serviceIndex: number;
|
||||
onChangeService: (services: ServiceFormValues[]) => void;
|
||||
servicePorts: ServicePort[];
|
||||
onChangePort: (servicePorts: ServicePort[]) => void;
|
||||
serviceName?: string;
|
||||
errors?: string | string[] | FormikErrors<ServicePort>[];
|
||||
}
|
||||
|
||||
export function ClusterIpServiceForm({
|
||||
services,
|
||||
serviceIndex,
|
||||
onChangeService,
|
||||
servicePorts,
|
||||
onChangePort,
|
||||
errors,
|
||||
serviceName,
|
||||
}: Props) {
|
||||
const newClusterIpPort = newPort(serviceName);
|
||||
return (
|
||||
<Widget key={serviceIndex}>
|
||||
<Widget.Body>
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="text-muted vertical-center">ClusterIP service</div>
|
||||
<Button
|
||||
icon={Trash2}
|
||||
color="dangerlight"
|
||||
className="!ml-0 flex-none"
|
||||
onClick={() => {
|
||||
// remove the service at index in an immutable way
|
||||
const newServices = [
|
||||
...services.slice(0, serviceIndex),
|
||||
...services.slice(serviceIndex + 1),
|
||||
];
|
||||
onChangeService(newServices);
|
||||
}}
|
||||
>
|
||||
Remove service
|
||||
</Button>
|
||||
</div>
|
||||
<div className="control-label !mb-2 !pt-0 text-left">Ports</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{servicePorts.map((servicePort, portIndex) => {
|
||||
const error = errors?.[portIndex];
|
||||
const servicePortError = isServicePortError<ServicePort>(error)
|
||||
? error
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={portIndex}
|
||||
className="flex flex-grow flex-wrap justify-between gap-x-4 gap-y-1"
|
||||
>
|
||||
<div className="inline-flex min-w-min flex-grow basis-3/4 flex-wrap gap-2">
|
||||
<div className="flex min-w-min basis-1/3 flex-col">
|
||||
<ContainerPortInput
|
||||
index={portIndex}
|
||||
value={servicePort.targetPort}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
const newValue =
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value);
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
targetPort: newValue,
|
||||
port: newValue,
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
/>
|
||||
{servicePortError?.targetPort && (
|
||||
<FormError>{servicePortError.targetPort}</FormError>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-min basis-1/3 flex-col">
|
||||
<ServicePortInput
|
||||
index={portIndex}
|
||||
value={servicePort.port}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
port:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
/>
|
||||
{servicePortError?.port && (
|
||||
<FormError>{servicePortError.port}</FormError>
|
||||
)}
|
||||
</div>
|
||||
<ButtonSelector
|
||||
className="h-[30px]"
|
||||
onChange={(value) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
protocol: value,
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
value={servicePort.protocol || 'TCP'}
|
||||
options={[{ value: 'TCP' }, { value: 'UDP' }]}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
disabled={servicePorts.length === 1}
|
||||
size="small"
|
||||
className="!ml-0 h-[30px]"
|
||||
color="dangerlight"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// remove the port at the index in an immutable way
|
||||
const newServicePorts = [
|
||||
...servicePorts.slice(0, portIndex),
|
||||
...servicePorts.slice(portIndex + 1),
|
||||
];
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
data-cy={`k8sAppCreate-rmPortButton_${portIndex}`}
|
||||
icon={Trash2}
|
||||
>
|
||||
Remove port
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<div className="flex">
|
||||
<Button
|
||||
icon={Plus}
|
||||
color="default"
|
||||
className="!ml-0"
|
||||
onClick={() => {
|
||||
const newServicesPorts = [...servicePorts, newClusterIpPort];
|
||||
onChangePort(newServicesPorts);
|
||||
}}
|
||||
>
|
||||
Add port
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
);
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
import { Plus } from 'lucide-react';
|
||||
import { FormikErrors } from 'formik';
|
||||
|
||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
||||
|
||||
import { Card } from '@@/Card';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { generateUniqueName, newPort, serviceFormDefaultValues } from './utils';
|
||||
import { ServiceFormValues, ServicePort } from './types';
|
||||
import { ClusterIpServiceForm } from './ClusterIpServiceForm';
|
||||
|
||||
interface Props {
|
||||
services: ServiceFormValues[];
|
||||
onChangeService: (services: ServiceFormValues[]) => void;
|
||||
errors?: FormikErrors<ServiceFormValues[]>;
|
||||
appName: string;
|
||||
selector: Record<string, string>;
|
||||
}
|
||||
|
||||
export function ClusterIpServicesForm({
|
||||
services,
|
||||
onChangeService,
|
||||
errors,
|
||||
appName,
|
||||
selector,
|
||||
}: Props) {
|
||||
const clusterIPServiceCount = services.filter(
|
||||
(service) =>
|
||||
service.Type === KubernetesApplicationPublishingTypes.CLUSTER_IP
|
||||
).length;
|
||||
return (
|
||||
<Card className="pb-5">
|
||||
<div className="flex flex-col gap-6">
|
||||
<TextTip color="blue">
|
||||
Publish <b>internally</b> in the cluster via a{' '}
|
||||
<b>ClusterIP service</b>, optionally exposing <b>externally</b> to the
|
||||
outside world via an <b>ingress</b>.
|
||||
</TextTip>
|
||||
{clusterIPServiceCount > 0 && (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{services.map((service, index) =>
|
||||
service.Type ===
|
||||
KubernetesApplicationPublishingTypes.CLUSTER_IP ? (
|
||||
<ClusterIpServiceForm
|
||||
key={index}
|
||||
serviceName={service.Name}
|
||||
servicePorts={service.Ports}
|
||||
errors={errors?.[index]?.Ports}
|
||||
onChangePort={(servicePorts: ServicePort[]) => {
|
||||
const newServices = [...services];
|
||||
newServices[index].Ports = servicePorts;
|
||||
onChangeService(newServices);
|
||||
}}
|
||||
services={services}
|
||||
serviceIndex={index}
|
||||
onChangeService={onChangeService}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<Button
|
||||
color="secondary"
|
||||
className="!ml-0"
|
||||
icon={Plus}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
// create a new service form value and add it to the list of services
|
||||
const newService = structuredClone(serviceFormDefaultValues);
|
||||
newService.Name = generateUniqueName(
|
||||
appName,
|
||||
services.length + 1,
|
||||
services
|
||||
);
|
||||
newService.Type = KubernetesApplicationPublishingTypes.CLUSTER_IP;
|
||||
const newServicePort = newPort(newService.Name);
|
||||
newService.Ports = [newServicePort];
|
||||
newService.Selector = selector;
|
||||
onChangeService([...services, newService]);
|
||||
}}
|
||||
data-cy="k8sAppCreate-createServiceButton"
|
||||
>
|
||||
Create service
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
import { FormikErrors } from 'formik';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
import { InputGroup } from '@@/form-components/InputGroup';
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { ButtonSelector } from '@@/form-components/ButtonSelector/ButtonSelector';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { isServicePortError, newPort } from './utils';
|
||||
import { ContainerPortInput } from './ContainerPortInput';
|
||||
import { ServicePortInput } from './ServicePortInput';
|
||||
import { ServicePort } from './types';
|
||||
|
||||
interface Props {
|
||||
values: ServicePort[];
|
||||
onChange: (loadBalancerPorts: ServicePort[]) => void;
|
||||
loadBalancerEnabled: boolean;
|
||||
serviceName?: string;
|
||||
errors?: string | string[] | FormikErrors<ServicePort>[];
|
||||
}
|
||||
|
||||
export function LoadBalancerForm({
|
||||
values: loadBalancerPorts,
|
||||
onChange,
|
||||
loadBalancerEnabled,
|
||||
serviceName,
|
||||
errors,
|
||||
}: Props) {
|
||||
const newLoadBalancerPort = newPort(serviceName);
|
||||
return (
|
||||
<>
|
||||
{loadBalancerEnabled && (
|
||||
<>
|
||||
<div className="control-label !mb-2 !pt-0 text-left">
|
||||
Published ports
|
||||
</div>
|
||||
<div className="mb-2 flex flex-col gap-4">
|
||||
{loadBalancerPorts.map((lbPort, index) => {
|
||||
const error = errors?.[index];
|
||||
const servicePortError = isServicePortError<ServicePort>(error)
|
||||
? error
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div key={index} className="flex flex-grow flex-wrap gap-2">
|
||||
<div className="flex w-1/4 min-w-min flex-col">
|
||||
<ContainerPortInput
|
||||
index={index}
|
||||
value={lbPort.targetPort}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...loadBalancerPorts];
|
||||
const newValue =
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value);
|
||||
newServicePorts[index] = {
|
||||
...newServicePorts[index],
|
||||
targetPort: newValue,
|
||||
port: newValue,
|
||||
};
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
/>
|
||||
{servicePortError?.targetPort && (
|
||||
<FormError>{servicePortError.targetPort}</FormError>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-1/4 min-w-min flex-col">
|
||||
<ServicePortInput
|
||||
index={index}
|
||||
value={lbPort.port}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...loadBalancerPorts];
|
||||
newServicePorts[index] = {
|
||||
...newServicePorts[index],
|
||||
port:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
};
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
/>
|
||||
{servicePortError?.port && (
|
||||
<FormError>{servicePortError.port}</FormError>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-1/4 min-w-min flex-col">
|
||||
<InputGroup size="small">
|
||||
<InputGroup.Addon required>
|
||||
Loadbalancer port
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Input
|
||||
type="number"
|
||||
className="form-control min-w-max"
|
||||
name={`loadbalancer_port_${index}`}
|
||||
placeholder="80"
|
||||
min="1"
|
||||
max="65535"
|
||||
value={lbPort.port || ''}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...loadBalancerPorts];
|
||||
newServicePorts[index] = {
|
||||
...newServicePorts[index],
|
||||
port:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
};
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
required
|
||||
data-cy={`k8sAppCreate-loadbalancerPort_${index}`}
|
||||
/>
|
||||
</InputGroup>
|
||||
{servicePortError?.nodePort && (
|
||||
<FormError>{servicePortError.nodePort}</FormError>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ButtonSelector
|
||||
className="h-[30px]"
|
||||
onChange={(value) => {
|
||||
const newServicePorts = [...loadBalancerPorts];
|
||||
newServicePorts[index] = {
|
||||
...newServicePorts[index],
|
||||
protocol: value,
|
||||
};
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
value={lbPort.protocol || 'TCP'}
|
||||
options={[{ value: 'TCP' }, { value: 'UDP' }]}
|
||||
/>
|
||||
<Button
|
||||
disabled={loadBalancerPorts.length === 1}
|
||||
size="small"
|
||||
className="!ml-0 h-[30px]"
|
||||
color="danger"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// remove the port at the index in an immutable way
|
||||
const newServicePorts = [
|
||||
...loadBalancerPorts.slice(0, index),
|
||||
...loadBalancerPorts.slice(index + 1),
|
||||
];
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
data-cy={`k8sAppCreate-rmPortButton_${index}`}
|
||||
icon={Trash2}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex">
|
||||
<Button
|
||||
icon={Plus}
|
||||
color="default"
|
||||
className="!ml-0"
|
||||
onClick={() => {
|
||||
const newServicesPorts = [
|
||||
...loadBalancerPorts,
|
||||
newLoadBalancerPort,
|
||||
];
|
||||
onChange(newServicesPorts);
|
||||
}}
|
||||
>
|
||||
Publish a new port
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
import { FormikErrors } from 'formik';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { ButtonSelector } from '@@/form-components/ButtonSelector/ButtonSelector';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Widget } from '@@/Widget';
|
||||
import { Card } from '@@/Card';
|
||||
import { InputGroup } from '@@/form-components/InputGroup';
|
||||
|
||||
import { isServicePortError, newPort } from './utils';
|
||||
import { ContainerPortInput } from './ContainerPortInput';
|
||||
import { ServicePortInput } from './ServicePortInput';
|
||||
import { ServiceFormValues, ServicePort } from './types';
|
||||
|
||||
interface Props {
|
||||
services: ServiceFormValues[];
|
||||
serviceIndex: number;
|
||||
onChangeService: (services: ServiceFormValues[]) => void;
|
||||
servicePorts: ServicePort[];
|
||||
onChangePort: (servicePorts: ServicePort[]) => void;
|
||||
serviceName?: string;
|
||||
errors?: string | string[] | FormikErrors<ServicePort>[];
|
||||
}
|
||||
|
||||
export function LoadBalancerServiceForm({
|
||||
services,
|
||||
serviceIndex,
|
||||
onChangeService,
|
||||
servicePorts,
|
||||
onChangePort,
|
||||
errors,
|
||||
serviceName,
|
||||
}: Props) {
|
||||
const newLoadBalancerPort = newPort(serviceName);
|
||||
return (
|
||||
<Widget key={serviceIndex}>
|
||||
<Widget.Body>
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="text-muted vertical-center">LoadBalancer service</div>
|
||||
<Button
|
||||
icon={Trash2}
|
||||
color="dangerlight"
|
||||
className="!ml-0"
|
||||
onClick={() => {
|
||||
// remove the service at index in an immutable way
|
||||
const newServices = [
|
||||
...services.slice(0, serviceIndex),
|
||||
...services.slice(serviceIndex + 1),
|
||||
];
|
||||
onChangeService(newServices);
|
||||
}}
|
||||
>
|
||||
Remove service
|
||||
</Button>
|
||||
</div>
|
||||
<div className="control-label !mb-2 !pt-0 text-left">Ports</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{servicePorts.map((servicePort, portIndex) => {
|
||||
const error = errors?.[portIndex];
|
||||
const servicePortError = isServicePortError<ServicePort>(error)
|
||||
? error
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={portIndex}
|
||||
className="flex flex-grow flex-wrap justify-between gap-x-4 gap-y-1"
|
||||
>
|
||||
<div className="inline-flex min-w-min flex-grow basis-3/4 flex-wrap gap-2">
|
||||
<div className="flex min-w-min basis-1/4 flex-col">
|
||||
<ContainerPortInput
|
||||
index={portIndex}
|
||||
value={servicePort.targetPort}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
const newValue =
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value);
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
targetPort: newValue,
|
||||
port: newValue,
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
/>
|
||||
{servicePortError?.targetPort && (
|
||||
<FormError>{servicePortError.targetPort}</FormError>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-min basis-1/4 flex-col">
|
||||
<ServicePortInput
|
||||
index={portIndex}
|
||||
value={servicePort.port}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
port:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
/>
|
||||
{servicePortError?.port && (
|
||||
<FormError>{servicePortError.port}</FormError>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex min-w-min basis-1/4 flex-col">
|
||||
<InputGroup size="small">
|
||||
<InputGroup.Addon required>
|
||||
Loadbalancer port
|
||||
</InputGroup.Addon>
|
||||
<InputGroup.Input
|
||||
type="number"
|
||||
className="form-control min-w-max"
|
||||
name={`loadbalancer_port_${portIndex}`}
|
||||
placeholder="80"
|
||||
min="1"
|
||||
max="65535"
|
||||
value={servicePort.port || ''}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
port:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
required
|
||||
data-cy={`k8sAppCreate-loadbalancerPort_${portIndex}`}
|
||||
/>
|
||||
</InputGroup>
|
||||
{servicePortError?.nodePort && (
|
||||
<FormError>{servicePortError.nodePort}</FormError>
|
||||
)}
|
||||
</div>
|
||||
<ButtonSelector
|
||||
className="h-[30px]"
|
||||
onChange={(value) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
protocol: value,
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
value={servicePort.protocol || 'TCP'}
|
||||
options={[{ value: 'TCP' }, { value: 'UDP' }]}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
disabled={servicePorts.length === 1}
|
||||
size="small"
|
||||
className="!ml-0 h-[30px]"
|
||||
color="dangerlight"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// remove the port at the index in an immutable way
|
||||
const newServicePorts = [
|
||||
...servicePorts.slice(0, portIndex),
|
||||
...servicePorts.slice(portIndex + 1),
|
||||
];
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
data-cy={`k8sAppCreate-rmPortButton_${portIndex}`}
|
||||
icon={Trash2}
|
||||
>
|
||||
Remove port
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<div className="flex">
|
||||
<Button
|
||||
icon={Plus}
|
||||
color="default"
|
||||
className="!ml-0"
|
||||
onClick={() => {
|
||||
const newServicesPorts = [...servicePorts, newLoadBalancerPort];
|
||||
onChangePort(newServicesPorts);
|
||||
}}
|
||||
>
|
||||
Add port
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
);
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
import { Plus, RotateCw } from 'lucide-react';
|
||||
import { FormikErrors } from 'formik';
|
||||
|
||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||
import { useEnvironment } from '@/react/portainer/environments/queries';
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
|
||||
import { Card } from '@@/Card';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Button } from '@@/buttons';
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { Link } from '@@/Link';
|
||||
|
||||
import { generateUniqueName, newPort, serviceFormDefaultValues } from './utils';
|
||||
import { ServiceFormValues, ServicePort } from './types';
|
||||
import { LoadBalancerServiceForm } from './LoadBalancerServiceForm';
|
||||
|
||||
interface Props {
|
||||
services: ServiceFormValues[];
|
||||
onChangeService: (services: ServiceFormValues[]) => void;
|
||||
errors?: FormikErrors<ServiceFormValues[]>;
|
||||
appName: string;
|
||||
selector: Record<string, string>;
|
||||
}
|
||||
|
||||
export function LoadBalancerServicesForm({
|
||||
services,
|
||||
onChangeService,
|
||||
errors,
|
||||
appName,
|
||||
selector,
|
||||
}: Props) {
|
||||
const { isAdmin } = useCurrentUser();
|
||||
const environmentId = useEnvironmentId();
|
||||
const { data: loadBalancerEnabled, ...loadBalancerEnabledQuery } =
|
||||
useEnvironment(
|
||||
environmentId,
|
||||
(environment) => environment?.Kubernetes.Configuration.UseLoadBalancer
|
||||
);
|
||||
|
||||
const loadBalancerServiceCount = services.filter(
|
||||
(service) =>
|
||||
service.Type === KubernetesApplicationPublishingTypes.LOAD_BALANCER
|
||||
).length;
|
||||
return (
|
||||
<Card className="pb-5">
|
||||
<div className="flex flex-col gap-6">
|
||||
<TextTip color="blue">
|
||||
Allow access to traffic <b>external</b> to the cluster via a{' '}
|
||||
<b>LoadBalancer service</b>. If running on a cloud platform, this auto
|
||||
provisions a cloud load balancer.
|
||||
</TextTip>
|
||||
{!loadBalancerEnabled && loadBalancerEnabledQuery.isSuccess && (
|
||||
<div className="flex flex-col">
|
||||
<FormError>
|
||||
{isAdmin ? (
|
||||
<>
|
||||
Load balancer use is not currently enabled in this cluster.
|
||||
Configure via{' '}
|
||||
<Link
|
||||
to="kubernetes.cluster.setup"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Cluster Setup
|
||||
</Link>{' '}
|
||||
and then refresh this tab
|
||||
</>
|
||||
) : (
|
||||
'Load balancer use is not currently enabled in this cluster, contact your administrator.'
|
||||
)}
|
||||
</FormError>
|
||||
<div className="flex">
|
||||
<Button
|
||||
icon={RotateCw}
|
||||
color="default"
|
||||
className="!ml-0"
|
||||
onClick={() => loadBalancerEnabledQuery.refetch()}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{loadBalancerServiceCount > 0 && (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{services.map((service, index) =>
|
||||
service.Type ===
|
||||
KubernetesApplicationPublishingTypes.LOAD_BALANCER ? (
|
||||
<LoadBalancerServiceForm
|
||||
key={index}
|
||||
serviceName={service.Name}
|
||||
servicePorts={service.Ports}
|
||||
errors={errors?.[index]?.Ports}
|
||||
onChangePort={(servicePorts: ServicePort[]) => {
|
||||
const newServices = [...services];
|
||||
newServices[index].Ports = servicePorts;
|
||||
onChangeService(newServices);
|
||||
}}
|
||||
services={services}
|
||||
serviceIndex={index}
|
||||
onChangeService={onChangeService}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<Button
|
||||
color="secondary"
|
||||
className="!ml-0"
|
||||
icon={Plus}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
// create a new service form value and add it to the list of services
|
||||
const newService = structuredClone(serviceFormDefaultValues);
|
||||
newService.Name = generateUniqueName(
|
||||
appName,
|
||||
services.length + 1,
|
||||
services
|
||||
);
|
||||
newService.Type =
|
||||
KubernetesApplicationPublishingTypes.LOAD_BALANCER;
|
||||
const newServicePort = newPort(newService.Name);
|
||||
newService.Ports = [newServicePort];
|
||||
newService.Selector = selector;
|
||||
onChangeService([...services, newService]);
|
||||
}}
|
||||
disabled={!loadBalancerEnabled}
|
||||
data-cy="k8sAppCreate-createServiceButton"
|
||||
>
|
||||
Create service
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
import { FormikErrors } from 'formik';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
import { InputGroup } from '@@/form-components/InputGroup';
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { ButtonSelector } from '@@/form-components/ButtonSelector/ButtonSelector';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { isServicePortError, newPort } from './utils';
|
||||
import { ContainerPortInput } from './ContainerPortInput';
|
||||
import { ServicePortInput } from './ServicePortInput';
|
||||
import { ServicePort } from './types';
|
||||
|
||||
interface Props {
|
||||
values: ServicePort[];
|
||||
onChange: (nodePorts: ServicePort[]) => void;
|
||||
serviceName?: string;
|
||||
errors?: string | string[] | FormikErrors<ServicePort>[];
|
||||
}
|
||||
|
||||
export function NodePortForm({
|
||||
values: nodePorts,
|
||||
onChange,
|
||||
errors,
|
||||
serviceName,
|
||||
}: Props) {
|
||||
const newNodePortPort = newPort(serviceName);
|
||||
return (
|
||||
<>
|
||||
<div className="control-label !mb-2 !pt-0 text-left">Published ports</div>
|
||||
<div className="mb-2 flex flex-col gap-4">
|
||||
{nodePorts.map((nodePort, index) => {
|
||||
const error = errors?.[index];
|
||||
const servicePortError = isServicePortError<ServicePort>(error)
|
||||
? error
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div key={index} className="flex flex-grow flex-wrap gap-2">
|
||||
<div className="flex w-1/4 min-w-min flex-col">
|
||||
<ContainerPortInput
|
||||
index={index}
|
||||
value={nodePort.targetPort}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...nodePorts];
|
||||
const newValue =
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value);
|
||||
newServicePorts[index] = {
|
||||
...newServicePorts[index],
|
||||
targetPort: newValue,
|
||||
port: newValue,
|
||||
};
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
/>
|
||||
{servicePortError?.targetPort && (
|
||||
<FormError>{servicePortError.targetPort}</FormError>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-1/4 min-w-min flex-col">
|
||||
<ServicePortInput
|
||||
index={index}
|
||||
value={nodePort.port}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...nodePorts];
|
||||
newServicePorts[index] = {
|
||||
...newServicePorts[index],
|
||||
port:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
};
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
/>
|
||||
{servicePortError?.port && (
|
||||
<FormError>{servicePortError.port}</FormError>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-1/4 min-w-min flex-col">
|
||||
<InputGroup size="small">
|
||||
<InputGroup.Addon>Nodeport</InputGroup.Addon>
|
||||
<InputGroup.Input
|
||||
type="number"
|
||||
className="form-control min-w-max"
|
||||
name={`node_port_${index}`}
|
||||
placeholder="30080"
|
||||
min="30000"
|
||||
max="32767"
|
||||
value={nodePort.nodePort ?? ''}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...nodePorts];
|
||||
newServicePorts[index] = {
|
||||
...newServicePorts[index],
|
||||
nodePort:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
};
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
data-cy={`k8sAppCreate-nodePort_${index}`}
|
||||
/>
|
||||
</InputGroup>
|
||||
{servicePortError?.nodePort && (
|
||||
<FormError>{servicePortError.nodePort}</FormError>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ButtonSelector
|
||||
className="h-[30px]"
|
||||
onChange={(value) => {
|
||||
const newServicePorts = [...nodePorts];
|
||||
newServicePorts[index] = {
|
||||
...newServicePorts[index],
|
||||
protocol: value,
|
||||
};
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
value={nodePort.protocol || 'TCP'}
|
||||
options={[{ value: 'TCP' }, { value: 'UDP' }]}
|
||||
/>
|
||||
<Button
|
||||
disabled={nodePorts.length === 1}
|
||||
size="small"
|
||||
className="!ml-0 h-[30px]"
|
||||
color="danger"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// remove the port at the index in an immutable way
|
||||
const newServicePorts = [
|
||||
...nodePorts.slice(0, index),
|
||||
...nodePorts.slice(index + 1),
|
||||
];
|
||||
onChange(newServicePorts);
|
||||
}}
|
||||
data-cy={`k8sAppCreate-rmPortButton_${index}`}
|
||||
icon={Trash2}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex">
|
||||
<Button
|
||||
icon={Plus}
|
||||
color="default"
|
||||
className="!ml-0"
|
||||
onClick={() => {
|
||||
const newServicesPorts = [...nodePorts, newNodePortPort];
|
||||
onChange(newServicesPorts);
|
||||
}}
|
||||
>
|
||||
Publish a new port
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
import { FormikErrors } from 'formik';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { ButtonSelector } from '@@/form-components/ButtonSelector/ButtonSelector';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Widget } from '@@/Widget';
|
||||
import { Card } from '@@/Card';
|
||||
import { InputGroup } from '@@/form-components/InputGroup';
|
||||
|
||||
import { isServicePortError, newPort } from './utils';
|
||||
import { ContainerPortInput } from './ContainerPortInput';
|
||||
import { ServicePortInput } from './ServicePortInput';
|
||||
import { ServiceFormValues, ServicePort } from './types';
|
||||
|
||||
interface Props {
|
||||
services: ServiceFormValues[];
|
||||
serviceIndex: number;
|
||||
onChangeService: (services: ServiceFormValues[]) => void;
|
||||
servicePorts: ServicePort[];
|
||||
onChangePort: (servicePorts: ServicePort[]) => void;
|
||||
serviceName?: string;
|
||||
errors?: string | string[] | FormikErrors<ServicePort>[];
|
||||
}
|
||||
|
||||
export function NodePortServiceForm({
|
||||
services,
|
||||
serviceIndex,
|
||||
onChangeService,
|
||||
servicePorts,
|
||||
onChangePort,
|
||||
errors,
|
||||
serviceName,
|
||||
}: Props) {
|
||||
const newNodePortPort = newPort(serviceName);
|
||||
return (
|
||||
<Widget key={serviceIndex}>
|
||||
<Widget.Body>
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="text-muted vertical-center">NodePort service</div>
|
||||
<Button
|
||||
icon={Trash2}
|
||||
color="dangerlight"
|
||||
className="!ml-0"
|
||||
onClick={() => {
|
||||
// remove the service at index in an immutable way
|
||||
const newServices = [
|
||||
...services.slice(0, serviceIndex),
|
||||
...services.slice(serviceIndex + 1),
|
||||
];
|
||||
onChangeService(newServices);
|
||||
}}
|
||||
>
|
||||
Remove service
|
||||
</Button>
|
||||
</div>
|
||||
<div className="control-label !mb-2 !pt-0 text-left">Ports</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{servicePorts.map((servicePort, portIndex) => {
|
||||
const error = errors?.[portIndex];
|
||||
const servicePortError = isServicePortError<ServicePort>(error)
|
||||
? error
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={portIndex}
|
||||
className="flex flex-grow flex-wrap justify-between gap-x-4 gap-y-1"
|
||||
>
|
||||
<div className="inline-flex min-w-min flex-grow basis-3/4 flex-wrap gap-2">
|
||||
<div className="flex min-w-min basis-1/4 flex-col">
|
||||
<ContainerPortInput
|
||||
index={portIndex}
|
||||
value={servicePort.targetPort}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
const newValue =
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value);
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
targetPort: newValue,
|
||||
port: newValue,
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
/>
|
||||
{servicePortError?.targetPort && (
|
||||
<FormError>{servicePortError.targetPort}</FormError>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-min basis-1/4 flex-col">
|
||||
<ServicePortInput
|
||||
index={portIndex}
|
||||
value={servicePort.port}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
port:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
/>
|
||||
{servicePortError?.port && (
|
||||
<FormError>{servicePortError.port}</FormError>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex min-w-min basis-1/4 flex-col">
|
||||
<InputGroup size="small">
|
||||
<InputGroup.Addon>Nodeport</InputGroup.Addon>
|
||||
<InputGroup.Input
|
||||
type="number"
|
||||
className="form-control min-w-max"
|
||||
name={`node_port_${portIndex}`}
|
||||
placeholder="30080"
|
||||
min="30000"
|
||||
max="32767"
|
||||
value={servicePort.nodePort ?? ''}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
nodePort:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
data-cy={`k8sAppCreate-nodePort_${portIndex}`}
|
||||
/>
|
||||
</InputGroup>
|
||||
{servicePortError?.nodePort && (
|
||||
<FormError>{servicePortError.nodePort}</FormError>
|
||||
)}
|
||||
</div>
|
||||
<ButtonSelector
|
||||
className="h-[30px]"
|
||||
onChange={(value) => {
|
||||
const newServicePorts = [...servicePorts];
|
||||
newServicePorts[portIndex] = {
|
||||
...newServicePorts[portIndex],
|
||||
protocol: value,
|
||||
};
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
value={servicePort.protocol || 'TCP'}
|
||||
options={[{ value: 'TCP' }, { value: 'UDP' }]}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
disabled={servicePorts.length === 1}
|
||||
size="small"
|
||||
className="!ml-0 h-[30px]"
|
||||
color="dangerlight"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// remove the port at the index in an immutable way
|
||||
const newServicePorts = [
|
||||
...servicePorts.slice(0, portIndex),
|
||||
...servicePorts.slice(portIndex + 1),
|
||||
];
|
||||
onChangePort(newServicePorts);
|
||||
}}
|
||||
data-cy={`k8sAppCreate-rmPortButton_${portIndex}`}
|
||||
icon={Trash2}
|
||||
>
|
||||
Remove port
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<div className="flex">
|
||||
<Button
|
||||
icon={Plus}
|
||||
color="default"
|
||||
className="!ml-0"
|
||||
onClick={() => {
|
||||
const newServicesPorts = [...servicePorts, newNodePortPort];
|
||||
onChangePort(newServicesPorts);
|
||||
}}
|
||||
>
|
||||
Add port
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
);
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
import { FormikErrors } from 'formik';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
import { KubernetesApplicationPublishingTypes } from '@/kubernetes/models/application/models';
|
||||
|
||||
import { Card } from '@@/Card';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { serviceFormDefaultValues, generateUniqueName, newPort } from './utils';
|
||||
import { ServiceFormValues, ServicePort } from './types';
|
||||
import { NodePortServiceForm } from './NodePortServiceForm';
|
||||
|
||||
interface Props {
|
||||
services: ServiceFormValues[];
|
||||
onChangeService: (services: ServiceFormValues[]) => void;
|
||||
errors?: FormikErrors<ServiceFormValues[]>;
|
||||
appName: string;
|
||||
selector: Record<string, string>;
|
||||
}
|
||||
|
||||
export function NodePortServicesForm({
|
||||
services,
|
||||
onChangeService,
|
||||
errors,
|
||||
appName,
|
||||
selector,
|
||||
}: Props) {
|
||||
const nodePortServiceCount = services.filter(
|
||||
(service) => service.Type === KubernetesApplicationPublishingTypes.NODE_PORT
|
||||
).length;
|
||||
return (
|
||||
<Card className="pb-5">
|
||||
<div className="flex flex-col gap-6">
|
||||
<TextTip color="blue">
|
||||
Allow access to traffic <b>external</b> to the cluster via a{' '}
|
||||
<b>NodePort service</b>. Not generally recommended for Production use.
|
||||
</TextTip>
|
||||
{nodePortServiceCount > 0 && (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{services.map((service, index) =>
|
||||
service.Type ===
|
||||
KubernetesApplicationPublishingTypes.NODE_PORT ? (
|
||||
<NodePortServiceForm
|
||||
key={index}
|
||||
serviceName={service.Name}
|
||||
servicePorts={service.Ports}
|
||||
errors={errors?.[index]?.Ports}
|
||||
onChangePort={(servicePorts: ServicePort[]) => {
|
||||
const newServices = [...services];
|
||||
newServices[index].Ports = servicePorts;
|
||||
onChangeService(newServices);
|
||||
}}
|
||||
services={services}
|
||||
serviceIndex={index}
|
||||
onChangeService={onChangeService}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<Button
|
||||
color="secondary"
|
||||
className="!ml-0"
|
||||
icon={Plus}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
// create a new service form value and add it to the list of services
|
||||
const newService = structuredClone(serviceFormDefaultValues);
|
||||
newService.Name = generateUniqueName(
|
||||
appName,
|
||||
services.length + 1,
|
||||
services
|
||||
);
|
||||
newService.Type = KubernetesApplicationPublishingTypes.NODE_PORT;
|
||||
const newServicePort = newPort(newService.Name);
|
||||
newService.Ports = [newServicePort];
|
||||
newService.Selector = selector;
|
||||
onChangeService([...services, newService]);
|
||||
}}
|
||||
data-cy="k8sAppCreate-createServiceButton"
|
||||
>
|
||||
Create service
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { ServiceTypeOption, ServiceTypeValue } from './types';
|
||||
|
||||
type Props = {
|
||||
serviceTypeOptions: ServiceTypeOption[];
|
||||
selectedServiceType: ServiceTypeValue;
|
||||
setSelectedServiceType: (serviceTypeValue: ServiceTypeValue) => void;
|
||||
};
|
||||
|
||||
export function ServiceTabs({
|
||||
serviceTypeOptions,
|
||||
selectedServiceType,
|
||||
setSelectedServiceType,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="flex pl-2">
|
||||
{serviceTypeOptions.map(({ label }, index) => (
|
||||
<label
|
||||
key={index}
|
||||
className={clsx(
|
||||
'!mb-0 inline-flex cursor-pointer items-center gap-2 border-0 border-b-2 border-solid bg-transparent px-4 py-2 font-medium',
|
||||
selectedServiceType === serviceTypeOptions[index].value
|
||||
? 'border-blue-8 text-blue-8 th-highcontrast:border-blue-6 th-highcontrast:text-blue-6 th-dark:border-blue-6 th-dark:text-blue-6'
|
||||
: 'border-transparent'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="widget-tabs"
|
||||
className="hidden"
|
||||
value={serviceTypeOptions[index].value}
|
||||
checked={selectedServiceType === serviceTypeOptions[index].value}
|
||||
onChange={(e) =>
|
||||
setSelectedServiceType(e.target.value as ServiceTypeValue)
|
||||
}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in new issue