|
@@ -22,7 +22,7 @@ import {
|
|
|
Cluster,
|
|
Cluster,
|
|
|
LoadBalancer,
|
|
LoadBalancer,
|
|
|
LoadBalancerType,
|
|
LoadBalancerType,
|
|
|
- EKSLogging
|
|
|
|
|
|
|
+ EKSLogging,
|
|
|
} from "@porter-dev/api-contracts";
|
|
} from "@porter-dev/api-contracts";
|
|
|
import { ClusterType } from "shared/types";
|
|
import { ClusterType } from "shared/types";
|
|
|
import Button from "./porter/Button";
|
|
import Button from "./porter/Button";
|
|
@@ -104,20 +104,27 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
const [clusterName, setClusterName] = useState("");
|
|
const [clusterName, setClusterName] = useState("");
|
|
|
const [awsRegion, setAwsRegion] = useState("us-east-1");
|
|
const [awsRegion, setAwsRegion] = useState("us-east-1");
|
|
|
const [machineType, setMachineType] = useState("t3.xlarge");
|
|
const [machineType, setMachineType] = useState("t3.xlarge");
|
|
|
- const [guardDutyEnabled, setGuardDutyEnabled] = useState<boolean>(false)
|
|
|
|
|
|
|
+ const [guardDutyEnabled, setGuardDutyEnabled] = useState<boolean>(false);
|
|
|
|
|
+ const [kmsEncryptionEnabled, setKmsEncryptionEnabled] = useState<boolean>(
|
|
|
|
|
+ false
|
|
|
|
|
+ );
|
|
|
const [loadBalancerType, setLoadBalancerType] = useState(false);
|
|
const [loadBalancerType, setLoadBalancerType] = useState(false);
|
|
|
- const [wildCardDomain, setWildCardDomain] = useState("")
|
|
|
|
|
- const [IPAllowList, setIPAllowList] = useState<string>("")
|
|
|
|
|
- const [controlPlaneLogs, setControlPlaneLogs] = useState<EKSLogging>(new EKSLogging())
|
|
|
|
|
|
|
+ const [wildCardDomain, setWildCardDomain] = useState("");
|
|
|
|
|
+ const [IPAllowList, setIPAllowList] = useState<string>("");
|
|
|
|
|
+ const [controlPlaneLogs, setControlPlaneLogs] = useState<EKSLogging>(
|
|
|
|
|
+ new EKSLogging()
|
|
|
|
|
+ );
|
|
|
//const [accessS3Logs, setAccessS3Logs] = useState<boolean>(false)
|
|
//const [accessS3Logs, setAccessS3Logs] = useState<boolean>(false)
|
|
|
- const [wafV2Enabled, setWaf2Enabled] = useState<boolean>(false)
|
|
|
|
|
- const [awsTags, setAwsTags] = useState<string>("")
|
|
|
|
|
- const [wafV2ARN, setwafV2ARN] = useState("")
|
|
|
|
|
- const [certificateARN, seCertificateARN] = useState("")
|
|
|
|
|
|
|
+ const [wafV2Enabled, setWaf2Enabled] = useState<boolean>(false);
|
|
|
|
|
+ const [awsTags, setAwsTags] = useState<string>("");
|
|
|
|
|
+ const [wafV2ARN, setwafV2ARN] = useState("");
|
|
|
|
|
+ const [certificateARN, seCertificateARN] = useState("");
|
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
|
const [minInstances, setMinInstances] = useState(1);
|
|
const [minInstances, setMinInstances] = useState(1);
|
|
|
const [maxInstances, setMaxInstances] = useState(10);
|
|
const [maxInstances, setMaxInstances] = useState(10);
|
|
|
- const [additionalNodePolicies, setAdditionalNodePolicies] = useState<string[]>([]);
|
|
|
|
|
|
|
+ const [additionalNodePolicies, setAdditionalNodePolicies] = useState<
|
|
|
|
|
+ string[]
|
|
|
|
|
+ >([]);
|
|
|
const [cidrRange, setCidrRange] = useState("10.78.0.0/16");
|
|
const [cidrRange, setCidrRange] = useState("10.78.0.0/16");
|
|
|
const [clusterVersion, setClusterVersion] = useState("v1.24.0");
|
|
const [clusterVersion, setClusterVersion] = useState("v1.24.0");
|
|
|
const [isReadOnly, setIsReadOnly] = useState(false);
|
|
const [isReadOnly, setIsReadOnly] = useState(false);
|
|
@@ -125,14 +132,16 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
const [isClicked, setIsClicked] = useState(false);
|
|
const [isClicked, setIsClicked] = useState(false);
|
|
|
const markStepStarted = async (step: string, errMessage?: string) => {
|
|
const markStepStarted = async (step: string, errMessage?: string) => {
|
|
|
try {
|
|
try {
|
|
|
- await api.updateOnboardingStep("<token>", {
|
|
|
|
|
- step,
|
|
|
|
|
- error_message: errMessage,
|
|
|
|
|
- region: awsRegion,
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ await api.updateOnboardingStep(
|
|
|
|
|
+ "<token>",
|
|
|
{
|
|
{
|
|
|
- project_id: currentProject.id,
|
|
|
|
|
|
|
+ step,
|
|
|
|
|
+ error_message: errMessage,
|
|
|
|
|
+ region: awsRegion,
|
|
|
},
|
|
},
|
|
|
|
|
+ {
|
|
|
|
|
+ project_id: currentProject.id,
|
|
|
|
|
+ }
|
|
|
);
|
|
);
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
// console.log(err);
|
|
// console.log(err);
|
|
@@ -159,19 +168,18 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
};
|
|
};
|
|
|
const validateInput = (wildCardDomainer) => {
|
|
const validateInput = (wildCardDomainer) => {
|
|
|
if (!wildCardDomainer) {
|
|
if (!wildCardDomainer) {
|
|
|
- return "Required for ALB Load Balancer"
|
|
|
|
|
|
|
+ return "Required for ALB Load Balancer";
|
|
|
}
|
|
}
|
|
|
if (wildCardDomainer?.charAt(0) == "*") {
|
|
if (wildCardDomainer?.charAt(0) == "*") {
|
|
|
- return "Wildcard domain cannot start with *"
|
|
|
|
|
|
|
+ return "Wildcard domain cannot start with *";
|
|
|
}
|
|
}
|
|
|
return false;
|
|
return false;
|
|
|
-
|
|
|
|
|
};
|
|
};
|
|
|
function validateIPInput(IPAllowList) {
|
|
function validateIPInput(IPAllowList) {
|
|
|
// This regular expression checks for an IP address with a subnet mask.
|
|
// This regular expression checks for an IP address with a subnet mask.
|
|
|
const regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
|
const regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/([0-9]|[1-2][0-9]|3[0-2])$/;
|
|
|
if (!IPAllowList) {
|
|
if (!IPAllowList) {
|
|
|
- return false
|
|
|
|
|
|
|
+ return false;
|
|
|
}
|
|
}
|
|
|
// Split the input string by comma and remove any empty elements
|
|
// Split the input string by comma and remove any empty elements
|
|
|
const ipAddresses = IPAllowList.split(",").filter(Boolean);
|
|
const ipAddresses = IPAllowList.split(",").filter(Boolean);
|
|
@@ -201,30 +209,27 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
const clusterNameDoesNotExist = () => {
|
|
const clusterNameDoesNotExist = () => {
|
|
|
- return (!clusterName)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return !clusterName;
|
|
|
|
|
+ };
|
|
|
const userProvisioning = () => {
|
|
const userProvisioning = () => {
|
|
|
- //If the cluster is updating or updating unavailabe but there are no errors do not allow re-provisioning
|
|
|
|
|
- return (isReadOnly && (props.provisionerError === ""))
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ //If the cluster is updating or updating unavailabe but there are no errors do not allow re-provisioning
|
|
|
|
|
+ return isReadOnly && props.provisionerError === "";
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
const isDisabled = () => {
|
|
const isDisabled = () => {
|
|
|
-
|
|
|
|
|
return (
|
|
return (
|
|
|
!user?.isPorterUser &&
|
|
!user?.isPorterUser &&
|
|
|
- (clusterNameDoesNotExist() ||
|
|
|
|
|
- userProvisioning() ||
|
|
|
|
|
- isClicked)
|
|
|
|
|
|
|
+ (clusterNameDoesNotExist() || userProvisioning() || isClicked)
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
|
function convertStringToTags(tagString) {
|
|
function convertStringToTags(tagString) {
|
|
|
- if (typeof tagString !== 'string' || tagString.trim() === '') {
|
|
|
|
|
|
|
+ if (typeof tagString !== "string" || tagString.trim() === "") {
|
|
|
return [];
|
|
return [];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Split the input string by comma, then reduce the resulting array to an object
|
|
// Split the input string by comma, then reduce the resulting array to an object
|
|
|
const tags = tagString.split(",").reduce((obj, item) => {
|
|
const tags = tagString.split(",").reduce((obj, item) => {
|
|
|
- // Split each item by "=",
|
|
|
|
|
|
|
+ // Split each item by "=",
|
|
|
const [key, value] = item.split("=");
|
|
const [key, value] = item.split("=");
|
|
|
// Add the key-value pair to the object
|
|
// Add the key-value pair to the object
|
|
|
obj[key] = value;
|
|
obj[key] = value;
|
|
@@ -246,12 +251,11 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
loadBalancerObj.tags = convertStringToTags(awsTags);
|
|
loadBalancerObj.tags = convertStringToTags(awsTags);
|
|
|
}
|
|
}
|
|
|
if (IPAllowList) {
|
|
if (IPAllowList) {
|
|
|
- loadBalancerObj.allowlistIpRanges = IPAllowList
|
|
|
|
|
|
|
+ loadBalancerObj.allowlistIpRanges = IPAllowList;
|
|
|
}
|
|
}
|
|
|
if (wafV2Enabled) {
|
|
if (wafV2Enabled) {
|
|
|
loadBalancerObj.enableWafv2 = wafV2Enabled;
|
|
loadBalancerObj.enableWafv2 = wafV2Enabled;
|
|
|
- }
|
|
|
|
|
- else {
|
|
|
|
|
|
|
+ } else {
|
|
|
loadBalancerObj.enableWafv2 = false;
|
|
loadBalancerObj.enableWafv2 = false;
|
|
|
}
|
|
}
|
|
|
if (wafV2ARN) {
|
|
if (wafV2ARN) {
|
|
@@ -262,7 +266,6 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
let data = new Contract({
|
|
let data = new Contract({
|
|
|
cluster: new Cluster({
|
|
cluster: new Cluster({
|
|
|
projectId: currentProject.id,
|
|
projectId: currentProject.id,
|
|
@@ -279,6 +282,7 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
loadBalancer: loadBalancerObj,
|
|
loadBalancer: loadBalancerObj,
|
|
|
logging: controlPlaneLogs,
|
|
logging: controlPlaneLogs,
|
|
|
enableGuardDuty: guardDutyEnabled,
|
|
enableGuardDuty: guardDutyEnabled,
|
|
|
|
|
+ enableKmsEncryption: kmsEncryptionEnabled,
|
|
|
nodeGroups: [
|
|
nodeGroups: [
|
|
|
new EKSNodeGroup({
|
|
new EKSNodeGroup({
|
|
|
instanceType: "t3.medium",
|
|
instanceType: "t3.medium",
|
|
@@ -390,8 +394,8 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
setIsReadOnly(
|
|
setIsReadOnly(
|
|
|
props.clusterId &&
|
|
props.clusterId &&
|
|
|
- (currentCluster.status === "UPDATING" ||
|
|
|
|
|
- currentCluster.status === "UPDATING_UNAVAILABLE")
|
|
|
|
|
|
|
+ (currentCluster.status === "UPDATING" ||
|
|
|
|
|
+ currentCluster.status === "UPDATING_UNAVAILABLE")
|
|
|
);
|
|
);
|
|
|
setClusterName(
|
|
setClusterName(
|
|
|
`${currentProject.name}-cluster-${Math.random()
|
|
`${currentProject.name}-cluster-${Math.random()
|
|
@@ -406,10 +410,12 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
if (contract?.cluster) {
|
|
if (contract?.cluster) {
|
|
|
let eksValues: EKS = contract.cluster?.eksKind as EKS;
|
|
let eksValues: EKS = contract.cluster?.eksKind as EKS;
|
|
|
if (eksValues == null) {
|
|
if (eksValues == null) {
|
|
|
- return
|
|
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
eksValues.nodeGroups.map((nodeGroup: EKSNodeGroup) => {
|
|
eksValues.nodeGroups.map((nodeGroup: EKSNodeGroup) => {
|
|
|
- if (nodeGroup.nodeGroupType.toString() === "NODE_GROUP_TYPE_APPLICATION") {
|
|
|
|
|
|
|
+ if (
|
|
|
|
|
+ nodeGroup.nodeGroupType.toString() === "NODE_GROUP_TYPE_APPLICATION"
|
|
|
|
|
+ ) {
|
|
|
setMachineType(nodeGroup.instanceType);
|
|
setMachineType(nodeGroup.instanceType);
|
|
|
setMinInstances(nodeGroup.minInstances);
|
|
setMinInstances(nodeGroup.minInstances);
|
|
|
setMaxInstances(nodeGroup.maxInstances);
|
|
setMaxInstances(nodeGroup.maxInstances);
|
|
@@ -426,19 +432,24 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
setClusterVersion(eksValues.clusterVersion);
|
|
setClusterVersion(eksValues.clusterVersion);
|
|
|
setCidrRange(eksValues.cidrRange);
|
|
setCidrRange(eksValues.cidrRange);
|
|
|
if (eksValues.loadBalancer != null) {
|
|
if (eksValues.loadBalancer != null) {
|
|
|
- setIPAllowList(eksValues.loadBalancer.allowlistIpRanges)
|
|
|
|
|
- setWildCardDomain(eksValues.loadBalancer.wildcardDomain)
|
|
|
|
|
|
|
+ setIPAllowList(eksValues.loadBalancer.allowlistIpRanges);
|
|
|
|
|
+ setWildCardDomain(eksValues.loadBalancer.wildcardDomain);
|
|
|
//setAccessS3Logs(eksValues.loadBalancer.enableS3AccessLogs)
|
|
//setAccessS3Logs(eksValues.loadBalancer.enableS3AccessLogs)
|
|
|
|
|
|
|
|
if (eksValues.loadBalancer.tags) {
|
|
if (eksValues.loadBalancer.tags) {
|
|
|
- setAwsTags(Object.entries(eksValues.loadBalancer.tags)
|
|
|
|
|
- .map(([key, value]) => `${key}=${value}`)
|
|
|
|
|
- .join(','));
|
|
|
|
|
|
|
+ setAwsTags(
|
|
|
|
|
+ Object.entries(eksValues.loadBalancer.tags)
|
|
|
|
|
+ .map(([key, value]) => `${key}=${value}`)
|
|
|
|
|
+ .join(",")
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- setLoadBalancerType(eksValues.loadBalancer.loadBalancerType?.toString() === "LOAD_BALANCER_TYPE_ALB")
|
|
|
|
|
- setwafV2ARN(eksValues.loadBalancer.wafv2Arn)
|
|
|
|
|
- setWaf2Enabled(eksValues.loadBalancer.enableWafv2)
|
|
|
|
|
|
|
+ setLoadBalancerType(
|
|
|
|
|
+ eksValues.loadBalancer.loadBalancerType?.toString() ===
|
|
|
|
|
+ "LOAD_BALANCER_TYPE_ALB"
|
|
|
|
|
+ );
|
|
|
|
|
+ setwafV2ARN(eksValues.loadBalancer.wafv2Arn);
|
|
|
|
|
+ setWaf2Enabled(eksValues.loadBalancer.enableWafv2);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (eksValues.logging != null) {
|
|
if (eksValues.logging != null) {
|
|
@@ -446,13 +457,14 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
l.enableApiServerLogs = eksValues.logging.enableApiServerLogs;
|
|
l.enableApiServerLogs = eksValues.logging.enableApiServerLogs;
|
|
|
l.enableAuditLogs = eksValues.logging.enableAuditLogs;
|
|
l.enableAuditLogs = eksValues.logging.enableAuditLogs;
|
|
|
l.enableAuthenticatorLogs = eksValues.logging.enableAuthenticatorLogs;
|
|
l.enableAuthenticatorLogs = eksValues.logging.enableAuthenticatorLogs;
|
|
|
- l.enableControllerManagerLogs = eksValues.logging.enableControllerManagerLogs;
|
|
|
|
|
|
|
+ l.enableControllerManagerLogs =
|
|
|
|
|
+ eksValues.logging.enableControllerManagerLogs;
|
|
|
l.enableSchedulerLogs = eksValues.logging.enableSchedulerLogs;
|
|
l.enableSchedulerLogs = eksValues.logging.enableSchedulerLogs;
|
|
|
setControlPlaneLogs(l);
|
|
setControlPlaneLogs(l);
|
|
|
}
|
|
}
|
|
|
- setGuardDutyEnabled(eksValues.enableGuardDuty)
|
|
|
|
|
|
|
+ setGuardDutyEnabled(eksValues.enableGuardDuty);
|
|
|
|
|
+ setKmsEncryptionEnabled(eksValues.enableKmsEncryption);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
}, [isExpanded, props.selectedClusterVersion]);
|
|
}, [isExpanded, props.selectedClusterVersion]);
|
|
|
|
|
|
|
|
const renderForm = () => {
|
|
const renderForm = () => {
|
|
@@ -508,14 +520,16 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
|
|
|
|
|
{isExpanded && (
|
|
{isExpanded && (
|
|
|
<>
|
|
<>
|
|
|
- {user?.isPorterUser && (<Select
|
|
|
|
|
- options={clusterVersionOptions}
|
|
|
|
|
- width="350px"
|
|
|
|
|
- disabled={isReadOnly}
|
|
|
|
|
- value={clusterVersion}
|
|
|
|
|
- setValue={setClusterVersion}
|
|
|
|
|
- label="Cluster version"
|
|
|
|
|
- />)}
|
|
|
|
|
|
|
+ {user?.isPorterUser && (
|
|
|
|
|
+ <Select
|
|
|
|
|
+ options={clusterVersionOptions}
|
|
|
|
|
+ width="350px"
|
|
|
|
|
+ disabled={isReadOnly}
|
|
|
|
|
+ value={clusterVersion}
|
|
|
|
|
+ setValue={setClusterVersion}
|
|
|
|
|
+ label="Cluster version"
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
<Spacer y={1} />
|
|
<Spacer y={1} />
|
|
|
<Select
|
|
<Select
|
|
|
options={machineTypeOptions}
|
|
options={machineTypeOptions}
|
|
@@ -555,20 +569,27 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
label="VPC CIDR range"
|
|
label="VPC CIDR range"
|
|
|
placeholder="ex: 10.78.0.0/16"
|
|
placeholder="ex: 10.78.0.0/16"
|
|
|
/>
|
|
/>
|
|
|
-
|
|
|
|
|
- {!currentProject.simplified_view_enabled &&
|
|
|
|
|
|
|
+ {!currentProject.simplified_view_enabled && (
|
|
|
<>
|
|
<>
|
|
|
-
|
|
|
|
|
<Spacer y={1} />
|
|
<Spacer y={1} />
|
|
|
<Checkbox
|
|
<Checkbox
|
|
|
checked={controlPlaneLogs.enableApiServerLogs}
|
|
checked={controlPlaneLogs.enableApiServerLogs}
|
|
|
disabled={isReadOnly}
|
|
disabled={isReadOnly}
|
|
|
toggleChecked={() => {
|
|
toggleChecked={() => {
|
|
|
- setControlPlaneLogs(new EKSLogging({ ...controlPlaneLogs, enableApiServerLogs: !controlPlaneLogs.enableApiServerLogs }))
|
|
|
|
|
|
|
+ setControlPlaneLogs(
|
|
|
|
|
+ new EKSLogging({
|
|
|
|
|
+ ...controlPlaneLogs,
|
|
|
|
|
+ enableApiServerLogs: !controlPlaneLogs.enableApiServerLogs,
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
}}
|
|
}}
|
|
|
- disabledTooltip={"Wait for provisioning to complete before editing this field."}
|
|
|
|
|
|
|
+ disabledTooltip={
|
|
|
|
|
+ "Wait for provisioning to complete before editing this field."
|
|
|
|
|
+ }
|
|
|
>
|
|
>
|
|
|
- <Text color="helper">Enable API Server logs in CloudWatch for this cluster</Text>
|
|
|
|
|
|
|
+ <Text color="helper">
|
|
|
|
|
+ Enable API Server logs in CloudWatch for this cluster
|
|
|
|
|
+ </Text>
|
|
|
</Checkbox>
|
|
</Checkbox>
|
|
|
|
|
|
|
|
<Spacer y={1} />
|
|
<Spacer y={1} />
|
|
@@ -576,11 +597,20 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
checked={controlPlaneLogs.enableAuditLogs}
|
|
checked={controlPlaneLogs.enableAuditLogs}
|
|
|
disabled={isReadOnly}
|
|
disabled={isReadOnly}
|
|
|
toggleChecked={() => {
|
|
toggleChecked={() => {
|
|
|
- setControlPlaneLogs(new EKSLogging({ ...controlPlaneLogs, enableAuditLogs: !controlPlaneLogs.enableAuditLogs }))
|
|
|
|
|
|
|
+ setControlPlaneLogs(
|
|
|
|
|
+ new EKSLogging({
|
|
|
|
|
+ ...controlPlaneLogs,
|
|
|
|
|
+ enableAuditLogs: !controlPlaneLogs.enableAuditLogs,
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
}}
|
|
}}
|
|
|
- disabledTooltip={"Wait for provisioning to complete before editing this field."}
|
|
|
|
|
|
|
+ disabledTooltip={
|
|
|
|
|
+ "Wait for provisioning to complete before editing this field."
|
|
|
|
|
+ }
|
|
|
>
|
|
>
|
|
|
- <Text color="helper">Enable Audit logs in CloudWatch for this cluster</Text>
|
|
|
|
|
|
|
+ <Text color="helper">
|
|
|
|
|
+ Enable Audit logs in CloudWatch for this cluster
|
|
|
|
|
+ </Text>
|
|
|
</Checkbox>
|
|
</Checkbox>
|
|
|
|
|
|
|
|
<Spacer y={1} />
|
|
<Spacer y={1} />
|
|
@@ -588,11 +618,20 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
checked={controlPlaneLogs.enableAuthenticatorLogs}
|
|
checked={controlPlaneLogs.enableAuthenticatorLogs}
|
|
|
disabled={isReadOnly}
|
|
disabled={isReadOnly}
|
|
|
toggleChecked={() => {
|
|
toggleChecked={() => {
|
|
|
- setControlPlaneLogs(new EKSLogging({ ...controlPlaneLogs, enableAuthenticatorLogs: !controlPlaneLogs.enableAuthenticatorLogs }))
|
|
|
|
|
|
|
+ setControlPlaneLogs(
|
|
|
|
|
+ new EKSLogging({
|
|
|
|
|
+ ...controlPlaneLogs,
|
|
|
|
|
+ enableAuthenticatorLogs: !controlPlaneLogs.enableAuthenticatorLogs,
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
}}
|
|
}}
|
|
|
- disabledTooltip={"Wait for provisioning to complete before editing this field."}
|
|
|
|
|
|
|
+ disabledTooltip={
|
|
|
|
|
+ "Wait for provisioning to complete before editing this field."
|
|
|
|
|
+ }
|
|
|
>
|
|
>
|
|
|
- <Text color="helper">Enable Authenticator logs in CloudWatch for this cluster</Text>
|
|
|
|
|
|
|
+ <Text color="helper">
|
|
|
|
|
+ Enable Authenticator logs in CloudWatch for this cluster
|
|
|
|
|
+ </Text>
|
|
|
</Checkbox>
|
|
</Checkbox>
|
|
|
|
|
|
|
|
<Spacer y={1} />
|
|
<Spacer y={1} />
|
|
@@ -600,11 +639,21 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
checked={controlPlaneLogs.enableControllerManagerLogs}
|
|
checked={controlPlaneLogs.enableControllerManagerLogs}
|
|
|
disabled={isReadOnly}
|
|
disabled={isReadOnly}
|
|
|
toggleChecked={() => {
|
|
toggleChecked={() => {
|
|
|
- setControlPlaneLogs(new EKSLogging({ ...controlPlaneLogs, enableControllerManagerLogs: !controlPlaneLogs.enableControllerManagerLogs }))
|
|
|
|
|
|
|
+ setControlPlaneLogs(
|
|
|
|
|
+ new EKSLogging({
|
|
|
|
|
+ ...controlPlaneLogs,
|
|
|
|
|
+ enableControllerManagerLogs: !controlPlaneLogs.enableControllerManagerLogs,
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
}}
|
|
}}
|
|
|
- disabledTooltip={"Wait for provisioning to complete before editing this field."}
|
|
|
|
|
|
|
+ disabledTooltip={
|
|
|
|
|
+ "Wait for provisioning to complete before editing this field."
|
|
|
|
|
+ }
|
|
|
>
|
|
>
|
|
|
- <Text color="helper">Enable Controller Manager logs in CloudWatch for this cluster</Text>
|
|
|
|
|
|
|
+ <Text color="helper">
|
|
|
|
|
+ Enable Controller Manager logs in CloudWatch for this
|
|
|
|
|
+ cluster
|
|
|
|
|
+ </Text>
|
|
|
</Checkbox>
|
|
</Checkbox>
|
|
|
|
|
|
|
|
<Spacer y={1} />
|
|
<Spacer y={1} />
|
|
@@ -612,11 +661,20 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
checked={controlPlaneLogs.enableSchedulerLogs}
|
|
checked={controlPlaneLogs.enableSchedulerLogs}
|
|
|
disabled={isReadOnly}
|
|
disabled={isReadOnly}
|
|
|
toggleChecked={() => {
|
|
toggleChecked={() => {
|
|
|
- setControlPlaneLogs(new EKSLogging({ ...controlPlaneLogs, enableSchedulerLogs: !controlPlaneLogs.enableSchedulerLogs }))
|
|
|
|
|
|
|
+ setControlPlaneLogs(
|
|
|
|
|
+ new EKSLogging({
|
|
|
|
|
+ ...controlPlaneLogs,
|
|
|
|
|
+ enableSchedulerLogs: !controlPlaneLogs.enableSchedulerLogs,
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
}}
|
|
}}
|
|
|
- disabledTooltip={"Wait for provisioning to complete before editing this field."}
|
|
|
|
|
|
|
+ disabledTooltip={
|
|
|
|
|
+ "Wait for provisioning to complete before editing this field."
|
|
|
|
|
+ }
|
|
|
>
|
|
>
|
|
|
- <Text color="helper">Enable Scheduler logs in CloudWatch for this cluster</Text>
|
|
|
|
|
|
|
+ <Text color="helper">
|
|
|
|
|
+ Enable Scheduler logs in CloudWatch for this cluster
|
|
|
|
|
+ </Text>
|
|
|
</Checkbox>
|
|
</Checkbox>
|
|
|
|
|
|
|
|
<Spacer y={1} />
|
|
<Spacer y={1} />
|
|
@@ -634,171 +692,184 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
|
|
|
//setAccessS3Logs(false);
|
|
//setAccessS3Logs(false);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- setLoadBalancerType(!loadBalancerType)
|
|
|
|
|
|
|
+ setLoadBalancerType(!loadBalancerType);
|
|
|
}}
|
|
}}
|
|
|
- disabledTooltip={"Wait for provisioning to complete before editing this field."}
|
|
|
|
|
|
|
+ disabledTooltip={
|
|
|
|
|
+ "Wait for provisioning to complete before editing this field."
|
|
|
|
|
+ }
|
|
|
>
|
|
>
|
|
|
<Text color="helper">Set Load Balancer Type to ALB</Text>
|
|
<Text color="helper">Set Load Balancer Type to ALB</Text>
|
|
|
</Checkbox>
|
|
</Checkbox>
|
|
|
<Spacer y={1} />
|
|
<Spacer y={1} />
|
|
|
- {loadBalancerType && (<>
|
|
|
|
|
-
|
|
|
|
|
- <FlexCenter>
|
|
|
|
|
- <Input
|
|
|
|
|
- width="350px"
|
|
|
|
|
- disabled={isReadOnly}
|
|
|
|
|
- value={wildCardDomain}
|
|
|
|
|
- setValue={(x: string) => setWildCardDomain(x)}
|
|
|
|
|
- label="Wildcard domain"
|
|
|
|
|
- placeholder="user-2.porter.run"
|
|
|
|
|
- />
|
|
|
|
|
- <Wrapper>
|
|
|
|
|
- <Tooltip
|
|
|
|
|
- children={<Icon src={info} />}
|
|
|
|
|
- content={'The provided domain should have a wildcard subdomain pointed to the LoadBalancer address. Using testing.porter.run will create a certificate for testing.porter.run with a SAN *.testing.porter.run'}
|
|
|
|
|
- position="right"
|
|
|
|
|
- />
|
|
|
|
|
- </Wrapper>
|
|
|
|
|
-
|
|
|
|
|
- </FlexCenter>
|
|
|
|
|
-
|
|
|
|
|
- {validateInput(wildCardDomain) && <ErrorInLine>
|
|
|
|
|
- <i className="material-icons">error</i>
|
|
|
|
|
- {validateInput(wildCardDomain)}
|
|
|
|
|
- </ErrorInLine>}
|
|
|
|
|
- <Spacer y={1} />
|
|
|
|
|
-
|
|
|
|
|
- <FlexCenter>
|
|
|
|
|
- <>
|
|
|
|
|
|
|
+ {loadBalancerType && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <FlexCenter>
|
|
|
<Input
|
|
<Input
|
|
|
width="350px"
|
|
width="350px"
|
|
|
disabled={isReadOnly}
|
|
disabled={isReadOnly}
|
|
|
- value={IPAllowList}
|
|
|
|
|
- setValue={(x: string) => setIPAllowList(x)}
|
|
|
|
|
- label="IP Allow List"
|
|
|
|
|
- placeholder="160.72.72.58/32,160.72.72.59/32"
|
|
|
|
|
|
|
+ value={wildCardDomain}
|
|
|
|
|
+ setValue={(x: string) => setWildCardDomain(x)}
|
|
|
|
|
+ label="Wildcard domain"
|
|
|
|
|
+ placeholder="user-2.porter.run"
|
|
|
/>
|
|
/>
|
|
|
<Wrapper>
|
|
<Wrapper>
|
|
|
<Tooltip
|
|
<Tooltip
|
|
|
children={<Icon src={info} />}
|
|
children={<Icon src={info} />}
|
|
|
- content={'Each range should be a CIDR, including netmask such as 10.1.2.3/21. To use multiple values, they should be comma-separated with no spaces'}
|
|
|
|
|
|
|
+ content={
|
|
|
|
|
+ "The provided domain should have a wildcard subdomain pointed to the LoadBalancer address. Using testing.porter.run will create a certificate for testing.porter.run with a SAN *.testing.porter.run"
|
|
|
|
|
+ }
|
|
|
position="right"
|
|
position="right"
|
|
|
/>
|
|
/>
|
|
|
</Wrapper>
|
|
</Wrapper>
|
|
|
- </>
|
|
|
|
|
- </FlexCenter>
|
|
|
|
|
- {validateIPInput(IPAllowList) && <ErrorInLine>
|
|
|
|
|
- <i className="material-icons">error</i>
|
|
|
|
|
- {"Needs to be Comma Separated Valid IP addresses"}
|
|
|
|
|
- </ErrorInLine>}
|
|
|
|
|
- <Spacer y={1} />
|
|
|
|
|
-
|
|
|
|
|
- <Input
|
|
|
|
|
- width="350px"
|
|
|
|
|
- disabled={isReadOnly}
|
|
|
|
|
- value={certificateARN}
|
|
|
|
|
- setValue={(x: string) => seCertificateARN(x)}
|
|
|
|
|
- label="Certificate ARN"
|
|
|
|
|
- placeholder="arn:aws:acm:REGION:ACCOUNT_ID:certificate/ACM_ID"
|
|
|
|
|
- />
|
|
|
|
|
- <Spacer y={1} />
|
|
|
|
|
|
|
+ </FlexCenter>
|
|
|
|
|
|
|
|
|
|
+ {validateInput(wildCardDomain) && (
|
|
|
|
|
+ <ErrorInLine>
|
|
|
|
|
+ <i className="material-icons">error</i>
|
|
|
|
|
+ {validateInput(wildCardDomain)}
|
|
|
|
|
+ </ErrorInLine>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <Spacer y={1} />
|
|
|
|
|
|
|
|
- <FlexCenter>
|
|
|
|
|
- <>
|
|
|
|
|
- <Input
|
|
|
|
|
- width="350px"
|
|
|
|
|
- disabled={isReadOnly}
|
|
|
|
|
- value={awsTags}
|
|
|
|
|
- setValue={(x: string) => setAwsTags(x)}
|
|
|
|
|
- label="AWS Tags"
|
|
|
|
|
- placeholder="costcenter=1,environment=10,project=32"
|
|
|
|
|
- />
|
|
|
|
|
- <Wrapper>
|
|
|
|
|
- <Tooltip
|
|
|
|
|
- children={<Icon src={info} />}
|
|
|
|
|
- content={"Each tag should be of the format 'key=value'. To use multiple values, they should be comma-separated with no spaces."}
|
|
|
|
|
- position="right"
|
|
|
|
|
|
|
+ <FlexCenter>
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ width="350px"
|
|
|
|
|
+ disabled={isReadOnly}
|
|
|
|
|
+ value={IPAllowList}
|
|
|
|
|
+ setValue={(x: string) => setIPAllowList(x)}
|
|
|
|
|
+ label="IP Allow List"
|
|
|
|
|
+ placeholder="160.72.72.58/32,160.72.72.59/32"
|
|
|
/>
|
|
/>
|
|
|
- </Wrapper>
|
|
|
|
|
- </>
|
|
|
|
|
- </FlexCenter>
|
|
|
|
|
- {validateTags(awsTags) && <ErrorInLine>
|
|
|
|
|
- <i className="material-icons">error</i>
|
|
|
|
|
- {"Needs to be Comma Separated Valid Tags"}
|
|
|
|
|
- </ErrorInLine>}
|
|
|
|
|
-
|
|
|
|
|
- <Spacer y={1} />
|
|
|
|
|
- {/* <Checkbox
|
|
|
|
|
- checked={accessS3Logs}
|
|
|
|
|
- disabled={isReadOnly}
|
|
|
|
|
- toggleChecked={() => {
|
|
|
|
|
- {
|
|
|
|
|
- console.log(!accessS3Logs)
|
|
|
|
|
- }
|
|
|
|
|
- setAccessS3Logs(!accessS3Logs)
|
|
|
|
|
- }}
|
|
|
|
|
- disabledTooltip={"Wait for provisioning to complete before editing this field."}
|
|
|
|
|
- >
|
|
|
|
|
- <Text color="helper">Access Logs to S3</Text>
|
|
|
|
|
- </Checkbox> */}
|
|
|
|
|
- <Spacer y={1} />
|
|
|
|
|
- <Checkbox
|
|
|
|
|
- checked={wafV2Enabled}
|
|
|
|
|
- disabled={isReadOnly}
|
|
|
|
|
- toggleChecked={() => {
|
|
|
|
|
- if (wafV2Enabled) {
|
|
|
|
|
- setwafV2ARN("");
|
|
|
|
|
- }
|
|
|
|
|
- setWaf2Enabled(!wafV2Enabled);
|
|
|
|
|
- }}
|
|
|
|
|
- disabledTooltip={"Wait for provisioning to complete before editing this field."}
|
|
|
|
|
- >
|
|
|
|
|
- <Text color="helper">WAFv2 Enabled</Text>
|
|
|
|
|
- </Checkbox>
|
|
|
|
|
- {wafV2Enabled && <>
|
|
|
|
|
|
|
+ <Wrapper>
|
|
|
|
|
+ <Tooltip
|
|
|
|
|
+ children={<Icon src={info} />}
|
|
|
|
|
+ content={
|
|
|
|
|
+ "Each range should be a CIDR, including netmask such as 10.1.2.3/21. To use multiple values, they should be comma-separated with no spaces"
|
|
|
|
|
+ }
|
|
|
|
|
+ position="right"
|
|
|
|
|
+ />
|
|
|
|
|
+ </Wrapper>
|
|
|
|
|
+ </>
|
|
|
|
|
+ </FlexCenter>
|
|
|
|
|
+ {validateIPInput(IPAllowList) && (
|
|
|
|
|
+ <ErrorInLine>
|
|
|
|
|
+ <i className="material-icons">error</i>
|
|
|
|
|
+ {"Needs to be Comma Separated Valid IP addresses"}
|
|
|
|
|
+ </ErrorInLine>
|
|
|
|
|
+ )}
|
|
|
<Spacer y={1} />
|
|
<Spacer y={1} />
|
|
|
|
|
|
|
|
|
|
+ <Input
|
|
|
|
|
+ width="350px"
|
|
|
|
|
+ disabled={isReadOnly}
|
|
|
|
|
+ value={certificateARN}
|
|
|
|
|
+ setValue={(x: string) => seCertificateARN(x)}
|
|
|
|
|
+ label="Certificate ARN"
|
|
|
|
|
+ placeholder="arn:aws:acm:REGION:ACCOUNT_ID:certificate/ACM_ID"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Spacer y={1} />
|
|
|
|
|
|
|
|
<FlexCenter>
|
|
<FlexCenter>
|
|
|
<>
|
|
<>
|
|
|
<Input
|
|
<Input
|
|
|
- width="500px"
|
|
|
|
|
- type="string"
|
|
|
|
|
- label="WAFv2 ARN"
|
|
|
|
|
|
|
+ width="350px"
|
|
|
disabled={isReadOnly}
|
|
disabled={isReadOnly}
|
|
|
- value={wafV2ARN}
|
|
|
|
|
- setValue={(x: string) => setwafV2ARN(x)}
|
|
|
|
|
- placeholder="arn:aws:wafv2:REGION:ACCOUNT_ID:regional/webacl/ACL_NAME/RULE_ID"
|
|
|
|
|
-
|
|
|
|
|
|
|
+ value={awsTags}
|
|
|
|
|
+ setValue={(x: string) => setAwsTags(x)}
|
|
|
|
|
+ label="AWS Tags"
|
|
|
|
|
+ placeholder="costcenter=1,environment=10,project=32"
|
|
|
/>
|
|
/>
|
|
|
<Wrapper>
|
|
<Wrapper>
|
|
|
<Tooltip
|
|
<Tooltip
|
|
|
children={<Icon src={info} />}
|
|
children={<Icon src={info} />}
|
|
|
- content={'Only Regional WAFv2 is supported. To find your ARN, navigate to the WAF console, click the Gear icon in the top right, and toggle "ARN" to on'}
|
|
|
|
|
|
|
+ content={
|
|
|
|
|
+ "Each tag should be of the format 'key=value'. To use multiple values, they should be comma-separated with no spaces."
|
|
|
|
|
+ }
|
|
|
position="right"
|
|
position="right"
|
|
|
/>
|
|
/>
|
|
|
</Wrapper>
|
|
</Wrapper>
|
|
|
</>
|
|
</>
|
|
|
</FlexCenter>
|
|
</FlexCenter>
|
|
|
-
|
|
|
|
|
- {(wafV2ARN == undefined || wafV2ARN?.length == 0) &&
|
|
|
|
|
-
|
|
|
|
|
|
|
+ {validateTags(awsTags) && (
|
|
|
<ErrorInLine>
|
|
<ErrorInLine>
|
|
|
<i className="material-icons">error</i>
|
|
<i className="material-icons">error</i>
|
|
|
- {"Required if WafV2 is enabled"}
|
|
|
|
|
|
|
+ {"Needs to be Comma Separated Valid Tags"}
|
|
|
</ErrorInLine>
|
|
</ErrorInLine>
|
|
|
|
|
+ )}
|
|
|
|
|
|
|
|
- }
|
|
|
|
|
- </>}
|
|
|
|
|
- <Spacer y={1} />
|
|
|
|
|
- </>
|
|
|
|
|
|
|
+ <Spacer y={1} />
|
|
|
|
|
+ {/* <Checkbox
|
|
|
|
|
+ checked={accessS3Logs}
|
|
|
|
|
+ disabled={isReadOnly}
|
|
|
|
|
+ toggleChecked={() => {
|
|
|
|
|
+ {
|
|
|
|
|
+ console.log(!accessS3Logs)
|
|
|
|
|
+ }
|
|
|
|
|
+ setAccessS3Logs(!accessS3Logs)
|
|
|
|
|
+ }}
|
|
|
|
|
+ disabledTooltip={"Wait for provisioning to complete before editing this field."}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Text color="helper">Access Logs to S3</Text>
|
|
|
|
|
+ </Checkbox> */}
|
|
|
|
|
+ <Spacer y={1} />
|
|
|
|
|
+ <Checkbox
|
|
|
|
|
+ checked={wafV2Enabled}
|
|
|
|
|
+ disabled={isReadOnly}
|
|
|
|
|
+ toggleChecked={() => {
|
|
|
|
|
+ if (wafV2Enabled) {
|
|
|
|
|
+ setwafV2ARN("");
|
|
|
|
|
+ }
|
|
|
|
|
+ setWaf2Enabled(!wafV2Enabled);
|
|
|
|
|
+ }}
|
|
|
|
|
+ disabledTooltip={
|
|
|
|
|
+ "Wait for provisioning to complete before editing this field."
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ <Text color="helper">WAFv2 Enabled</Text>
|
|
|
|
|
+ </Checkbox>
|
|
|
|
|
+ {wafV2Enabled && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Spacer y={1} />
|
|
|
|
|
+
|
|
|
|
|
+ <FlexCenter>
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ width="500px"
|
|
|
|
|
+ type="string"
|
|
|
|
|
+ label="WAFv2 ARN"
|
|
|
|
|
+ disabled={isReadOnly}
|
|
|
|
|
+ value={wafV2ARN}
|
|
|
|
|
+ setValue={(x: string) => setwafV2ARN(x)}
|
|
|
|
|
+ placeholder="arn:aws:wafv2:REGION:ACCOUNT_ID:regional/webacl/ACL_NAME/RULE_ID"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Wrapper>
|
|
|
|
|
+ <Tooltip
|
|
|
|
|
+ children={<Icon src={info} />}
|
|
|
|
|
+ content={
|
|
|
|
|
+ 'Only Regional WAFv2 is supported. To find your ARN, navigate to the WAF console, click the Gear icon in the top right, and toggle "ARN" to on'
|
|
|
|
|
+ }
|
|
|
|
|
+ position="right"
|
|
|
|
|
+ />
|
|
|
|
|
+ </Wrapper>
|
|
|
|
|
+ </>
|
|
|
|
|
+ </FlexCenter>
|
|
|
|
|
+
|
|
|
|
|
+ {(wafV2ARN == undefined || wafV2ARN?.length == 0) && (
|
|
|
|
|
+ <ErrorInLine>
|
|
|
|
|
+ <i className="material-icons">error</i>
|
|
|
|
|
+ {"Required if WafV2 is enabled"}
|
|
|
|
|
+ </ErrorInLine>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <Spacer y={1} />
|
|
|
|
|
+ </>
|
|
|
)}
|
|
)}
|
|
|
</>
|
|
</>
|
|
|
- }
|
|
|
|
|
|
|
+ )}
|
|
|
</>
|
|
</>
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ )}
|
|
|
</>
|
|
</>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
@@ -828,7 +899,7 @@ const ExpandHeader = styled.div<{ isExpanded: boolean }>`
|
|
|
margin-right: 7px;
|
|
margin-right: 7px;
|
|
|
margin-left: -7px;
|
|
margin-left: -7px;
|
|
|
transform: ${(props) =>
|
|
transform: ${(props) =>
|
|
|
- props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
|
|
|
|
|
|
|
+ props.isExpanded ? "rotate(0deg)" : "rotate(-90deg)"};
|
|
|
}
|
|
}
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
@@ -844,9 +915,9 @@ const StyledForm = styled.div`
|
|
|
|
|
|
|
|
const FlexCenter = styled.div`
|
|
const FlexCenter = styled.div`
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- align-items: center ;
|
|
|
|
|
|
|
+ align-items: center;
|
|
|
gap: 3px;
|
|
gap: 3px;
|
|
|
-`
|
|
|
|
|
|
|
+`;
|
|
|
const Wrapper = styled.div`
|
|
const Wrapper = styled.div`
|
|
|
transform: translateY(+13px);
|
|
transform: translateY(+13px);
|
|
|
`;
|
|
`;
|
|
@@ -1150,4 +1221,4 @@ const errorMessageToModal = (errorMessage: string) => {
|
|
|
default:
|
|
default:
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
-};
|
|
|
|
|
|
|
+};
|