Browse Source

Intelligent sliders (#3557)

sdess09 2 years ago
parent
commit
a92ad1f3fd

+ 159 - 34
dashboard/src/components/porter/InputSlider.tsx

@@ -1,11 +1,13 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 import Slider, { Mark } from '@material-ui/core/Slider';
 import Slider, { Mark } from '@material-ui/core/Slider';
 import Tooltip from '@material-ui/core/Tooltip';
 import Tooltip from '@material-ui/core/Tooltip';
 import Typography from '@material-ui/core/Typography';
 import Typography from '@material-ui/core/Typography';
 import styled from 'styled-components';
 import styled from 'styled-components';
 import { withStyles } from '@material-ui/core/styles';
 import { withStyles } from '@material-ui/core/styles';
-import Container from './Container';
-
+import Text from './Text';
+import Spacer from './Spacer';
+import SmartOptModal from 'main/home/app-dashboard/new-app-flow/tabs/SmartOptModal';
+import NodeInfoModal from 'main/home/app-dashboard/new-app-flow/tabs/NodeInfoModal';
 
 
 type InputSliderProps = {
 type InputSliderProps = {
   label?: string;
   label?: string;
@@ -19,6 +21,9 @@ type InputSliderProps = {
   color?: string;
   color?: string;
   width?: string;
   width?: string;
   step?: number;
   step?: number;
+  smartLimit?: number;
+  override?: boolean;
+  nodeCount?: number;
 };
 };
 
 
 const ValueLabelComponent: React.FC<any> = (props) => {
 const ValueLabelComponent: React.FC<any> = (props) => {
@@ -47,41 +52,129 @@ const InputSlider: React.FC<InputSliderProps> = ({
   color,
   color,
   step,
   step,
   width,
   width,
-
+  smartLimit,
+  override,
+  nodeCount
 }) => {
 }) => {
-  const marks: Mark[] = [
+  const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
 
 
+  const optimal = nodeCount ? Math.round((max / nodeCount) * 10) / 10 : 0;
+
+  const mid = min + (max - min) * 0.25;
+  const quarter = min + (max - min) * 0.125;
+  const marks: Mark[] = [
     {
     {
       value: max,
       value: max,
       label: max.toString(),
       label: max.toString(),
     },
     },
   ];
   ];
+  var isExceedingLimit = false;
+  var displayOptimalText = false;
+  //Optimal Marks only give useful information to user if they are using more than 2 nodes
+  // if (optimal != 0 && nodeCount && nodeCount > 2) {
+  //   marks.push({
+  //     value: optimal,
+  //     label: (
+  //       <Text color="helper" size={10}>
+  //         Recommended
+  //       </Text>
+
+  //     )
+  //   });
+  //   displayOptimalText = Number(value) == optimal;
+  // }
+
+  if (smartLimit) {
+
+    marks.push({
+      value: smartLimit,
+      label: smartLimit.toString(),
+    },
+      {
+        value: mid,
+        label: "",
+      },
+      {
+        value: quarter,
+        label: "",
+      },);
+    displayOptimalText = Number(value) == mid || Number(value) == quarter;
+    isExceedingLimit = Number(value) > smartLimit;
+  }
+  const isCloseToMark = (value, marks, threshold = 0.1) => {
+    return marks.some(mark => Math.abs(mark.value - value) < threshold);
+  };
+
+  const getClosestMark = (value, marks) => {
+    return marks.reduce((prev, curr) => (
+      Math.abs(curr.value - value) < Math.abs(prev.value - value) ? curr : prev
+    )).value;
+  };
+
 
 
   return (
   return (
     <SliderContainer width={width}>
     <SliderContainer width={width}>
       <LabelContainer>
       <LabelContainer>
-        {label && <Label>{label}</Label>}
-        <Value>{`${value} ${unit}`}</Value>
+        <>
+          {label && <Label>{label}</Label>}
+          <Value>{`${Math.floor(value * 100) / 100} ${unit}`}</Value>
+          {displayOptimalText &&
+            <><Spacer inline x={1} /><Label>Recommended based on the available compute </Label>  <StyledIcon
+              className="material-icons"
+              onClick={() => {
+                setShowNeedHelpModal(true)
+              }}
+            >
+              help_outline
+            </StyledIcon></>}
+          {showNeedHelpModal &&
+            <NodeInfoModal
+              setModalVisible={setShowNeedHelpModal}
+            />}
+          {isExceedingLimit &&
+            <><Spacer inline x={1} /><Label color="#FFBF00"> Value is not optimal for cost</Label></>}
+        </>
       </LabelContainer>
       </LabelContainer>
-      <DisabledTooltip title={disabled ? disabledTooltip || '' : ''} arrow>
 
 
+      <DisabledTooltip title={disabled ? disabledTooltip || '' : ''} arrow>
         <div style={{ position: 'relative' }}>
         <div style={{ position: 'relative' }}>
-          <StyledSlider
-            ValueLabelComponent={ValueLabelComponent}
-            aria-label="input slider"
-            min={min}
-            max={max}
-            value={Number(value)}
-            onChange={(event, newValue) => {
-              setValue(newValue as number);
-            }}
-            disabled={disabled}
-            marks={marks}
-            step={step ? step : 1}
-            style={{
-              color: disabled ? "gray" : color,
-            }}
-          />
+          {/* <div style={{ position: 'absolute', bottom: '100%', left: `calc(${((threeQuarter - min) / (max - min)) * 100}% - 50px)` }}>
+            Recommended
+          </div> */}
+          <MaxedOutToolTip title={smartLimit?.toString() == value && !override ? "Using resources beyond this limit is not cost optimal - to override toggle off Smart Optimization" || '' : ''} arrow>
+            <div style={{ position: 'relative' }}>
+
+              <StyledSlider
+                ValueLabelComponent={ValueLabelComponent}
+                aria-label="input slider"
+                isExceedingLimit={isExceedingLimit}
+                min={min}
+                max={max}
+                value={(!override && isExceedingLimit) ? smartLimit : Number(value)}
+                onChange={(event, newValue) => {
+                  if (!override && smartLimit && newValue > smartLimit) {
+                    setValue(smartLimit);
+                  } else if (!override) {
+                    const closestMarkValue = getClosestMark(newValue, marks);
+                    setValue(closestMarkValue);
+                  } else {
+                    setValue(newValue as number);
+                  }
+                }}
+                classes={{
+                  track: isExceedingLimit ? 'exceeds-limit' : '',
+                  rail: isExceedingLimit ? 'exceeds-limit' : ''
+                }}
+                valueLabelDisplay={smartLimit && Number(value) > smartLimit ? "off" : "auto"}
+                disabled={disabled}
+                marks={marks}
+                step={(step ? step : 1)}
+                style={{
+                  color: disabled ? "gray" : color,
+                }}
+              />
+            </div>
+          </MaxedOutToolTip>
           {disabled && (
           {disabled && (
             <div
             <div
               style={{
               style={{
@@ -106,7 +199,7 @@ const InputSlider: React.FC<InputSliderProps> = ({
 export default InputSlider;
 export default InputSlider;
 
 
 const SliderContainer = styled.div<{ width?: string }>`
 const SliderContainer = styled.div<{ width?: string }>`
-  width: ${({ width }) => width || '300px'};
+  width: ${({ width }) => width || '800px'};
   margin: 1px 0;
   margin: 1px 0;
 `;
 `;
 
 
@@ -114,7 +207,7 @@ const Label = styled.div<{ color?: string }>`
   font-size: 13px;
   font-size: 13px;
   margin-right: 5px;
   margin-right: 5px;
   margin-bottom: 10px;
   margin-bottom: 10px;
-  color: #aaaabb;
+  color: ${props => props.color ? props.color : '#aaaabb'};
 `;
 `;
 
 
 const Value = styled.div<{ color?: string }>`
 const Value = styled.div<{ color?: string }>`
@@ -144,6 +237,23 @@ const DisabledTooltip = withStyles(theme => ({
   },
   },
 }))(Tooltip);
 }))(Tooltip);
 
 
+const MaxedOutToolTip = withStyles(theme => ({
+  tooltip: {
+    backgroundColor: '#333',
+    color: '#fff',
+    padding: '5px',
+    borderRadius: '2px',
+    fontSize: '12px',
+    textAlign: 'center',
+    whiteSpace: 'pre-wrap',
+    wordWrap: 'break-word',
+    maxWidth: '200px',
+    width: '200px',
+    [theme.breakpoints.up('sm')]: {
+      margin: '0 2px',
+    },
+  },
+}))(Tooltip);
 
 
 const StyledSlider = withStyles({
 const StyledSlider = withStyles({
   root: {
   root: {
@@ -164,7 +274,10 @@ const StyledSlider = withStyles({
     color: '#6e717d',
     color: '#6e717d',
     fontSize: '12px',
     fontSize: '12px',
     marginRight: 5,
     marginRight: 5,
-
+    '&[data-mark-value="Recommended"]': { // targeting the Recommended label
+      transform: 'translateY(-100%)', // move it upwards
+      marginBottom: '15px', // adjust the margin to position it
+    },
   },
   },
   markLabelActive: {
   markLabelActive: {
     color: '#6e717d',
     color: '#6e717d',
@@ -183,21 +296,24 @@ const StyledSlider = withStyles({
       width: 16,
       width: 16,
     },
     },
   },
   },
-  track: {
-    height: 8, // Same as root height for consistency
+  track: (props) => ({
+    height: 8,
     borderRadius: 4,
     borderRadius: 4,
-  },
-  rail: {
-    height: 8, // Same as root height for consistency
+    backgroundColor: props.isExceedingLimit ? '#FFBF00' : '',  // setting color conditionally
+  }),
+  rail: (props) => ({
+    height: 8,
     borderRadius: 4,
     borderRadius: 4,
-  },
+    backgroundColor: props.isExceedingLimit ? '#FFBF00' : '',  // setting color conditionally
+  }),
   valueLabel: {
   valueLabel: {
     top: -22,
     top: -22,
     '& *': {
     '& *': {
       background: 'transparent',
       background: 'transparent',
       border: 'none', // remove the default border
       border: 'none', // remove the default border
     },
     },
-  },
+  }
+  ,
   disabled: {},
   disabled: {},
 })(Slider);
 })(Slider);
 
 
@@ -213,4 +329,13 @@ const StyledTooltip = withStyles({
 const LabelContainer = styled.div`
 const LabelContainer = styled.div`
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
+`;
+
+const StyledIcon = styled.i`
+  cursor: pointer;
+  font-size: 16px; 
+  margin-bottom : 10px;
+  &:hover {
+    color: #666;  
+  }
 `;
 `;

+ 16 - 13
dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx

@@ -1,7 +1,7 @@
 import React, { useContext, useEffect, useState } from "react";
 import React, { useContext, useEffect, useState } from "react";
 import AnimateHeight, { Height } from "react-animate-height";
 import AnimateHeight, { Height } from "react-animate-height";
 import styled from "styled-components";
 import styled from "styled-components";
-import _ from "lodash";
+import _, { set } from "lodash";
 
 
 import web from "assets/web.png";
 import web from "assets/web.png";
 import worker from "assets/worker.png";
 import worker from "assets/worker.png";
@@ -34,11 +34,9 @@ const ServiceContainer: React.FC<ServiceProps> = ({
   setExpandedJob,
   setExpandedJob,
 }) => {
 }) => {
   const [height, setHeight] = React.useState<Height>("auto");
   const [height, setHeight] = React.useState<Height>("auto");
-
-  const UPPER_BOUND = .5;
-
-  const [maxCPU, setMaxCPU] = useState(2 * UPPER_BOUND); //default is set to a t3 medium 
-  const [maxRAM, setMaxRAM] = useState(4 * UPPER_BOUND); //default is set to a t3 medium
+  const [applicationNodeCount, setApplicationNodeCount] = useState<number>(1);
+  const [maxCPU, setMaxCPU] = useState(2); //default is set to a t3 medium 
+  const [maxRAM, setMaxRAM] = useState(4); //default is set to a t3 medium
   const context = useContext(Context);
   const context = useContext(Context);
 
 
   useEffect(() => {
   useEffect(() => {
@@ -57,12 +55,13 @@ const ServiceContainer: React.FC<ServiceProps> = ({
         instanceType = chart?.config?.[`${serviceName}-${service.type}`]?.nodeSelector?.["beta.kubernetes.io/instance-type"]
         instanceType = chart?.config?.[`${serviceName}-${service.type}`]?.nodeSelector?.["beta.kubernetes.io/instance-type"]
         const [instanceClass, instanceSize] = instanceType.split('.');
         const [instanceClass, instanceSize] = instanceType.split('.');
         const currentInstance = AWS_INSTANCE_LIMITS[instanceClass][instanceSize];
         const currentInstance = AWS_INSTANCE_LIMITS[instanceClass][instanceSize];
-        setMaxCPU(currentInstance.vCPU * UPPER_BOUND);
-        setMaxRAM(currentInstance.RAM * UPPER_BOUND);
+        setMaxCPU(currentInstance.vCPU);
+        setMaxRAM(currentInstance.RAM);
       }
       }
     }
     }
     //Query the given nodes if no instance type is specified
     //Query the given nodes if no instance type is specified
     if (instanceType == "") {
     if (instanceType == "") {
+
       api
       api
         .getClusterNodes(
         .getClusterNodes(
           "<token>",
           "<token>",
@@ -74,7 +73,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
         )
         )
         .then(({ data }) => {
         .then(({ data }) => {
           if (data) {
           if (data) {
-
+            var nodeCount = 0
             let largestInstanceType = {
             let largestInstanceType = {
               vCPUs: 2,
               vCPUs: 2,
               RAM: 4,
               RAM: 4,
@@ -83,6 +82,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
             // TODO: type this response
             // TODO: type this response
             data.forEach((node: any) => {
             data.forEach((node: any) => {
               if (node.labels['porter.run/workload-kind'] == "application") {
               if (node.labels['porter.run/workload-kind'] == "application") {
+                nodeCount += 1
                 var instanceType: string = node.labels['beta.kubernetes.io/instance-type'];
                 var instanceType: string = node.labels['beta.kubernetes.io/instance-type'];
                 const [instanceClass, instanceSize] = instanceType.split('.');
                 const [instanceClass, instanceSize] = instanceType.split('.');
                 if (instanceClass && instanceSize) {
                 if (instanceClass && instanceSize) {
@@ -90,14 +90,13 @@ const ServiceContainer: React.FC<ServiceProps> = ({
                     let currentInstance = AWS_INSTANCE_LIMITS[instanceClass][instanceSize];
                     let currentInstance = AWS_INSTANCE_LIMITS[instanceClass][instanceSize];
                     largestInstanceType.vCPUs = currentInstance.vCPU;
                     largestInstanceType.vCPUs = currentInstance.vCPU;
                     largestInstanceType.RAM = currentInstance.RAM;
                     largestInstanceType.RAM = currentInstance.RAM;
-
                   }
                   }
                 }
                 }
               }
               }
             });
             });
-
-            setMaxCPU(largestInstanceType.vCPUs * UPPER_BOUND);
-            setMaxRAM(largestInstanceType.RAM * UPPER_BOUND);
+            setApplicationNodeCount(nodeCount);
+            setMaxCPU(largestInstanceType.vCPUs);
+            setMaxRAM(largestInstanceType.RAM);
           }
           }
         }).catch((error) => {
         }).catch((error) => {
 
 
@@ -115,6 +114,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
             setHeight={setHeight}
             setHeight={setHeight}
             maxCPU={maxCPU}
             maxCPU={maxCPU}
             maxRAM={maxRAM}
             maxRAM={maxRAM}
+            nodeCount={applicationNodeCount}
           />
           />
         );
         );
       case "worker":
       case "worker":
@@ -125,6 +125,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
             setHeight={setHeight}
             setHeight={setHeight}
             maxCPU={maxCPU}
             maxCPU={maxCPU}
             maxRAM={maxRAM}
             maxRAM={maxRAM}
+            nodeCount={applicationNodeCount}
           />
           />
         );
         );
       case "job":
       case "job":
@@ -135,6 +136,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
             setHeight={setHeight}
             setHeight={setHeight}
             maxCPU={maxCPU}
             maxCPU={maxCPU}
             maxRAM={maxRAM}
             maxRAM={maxRAM}
+            nodeCount={applicationNodeCount}
           />
           />
         );
         );
       case "release":
       case "release":
@@ -145,6 +147,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
             setHeight={setHeight}
             setHeight={setHeight}
             maxCPU={maxCPU}
             maxCPU={maxCPU}
             maxRAM={maxRAM}
             maxRAM={maxRAM}
+            nodeCount={applicationNodeCount}
           />
           />
         );
         );
     }
     }

+ 21 - 8
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -148,6 +148,7 @@ type SharedServiceParams = {
     type: ServiceType;
     type: ServiceType;
     canDelete: boolean;
     canDelete: boolean;
     expanded: boolean;
     expanded: boolean;
+    smartOptimization: boolean;
     cloudsql: CloudSql;
     cloudsql: CloudSql;
 }
 }
 
 
@@ -160,9 +161,10 @@ export type WebService = SharedServiceParams & Omit<WorkerService, 'type'> & {
 const WebService = {
 const WebService = {
     default: (name: string, porterJson?: PorterJson): WebService => ({
     default: (name: string, porterJson?: PorterJson): WebService => ({
         name,
         name,
+        smartOptimization: true,
         expanded: true,
         expanded: true,
-        cpu: ServiceField.string('100', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-        ram: ServiceField.string('256', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
+        cpu: ServiceField.string('187.5', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
+        ram: ServiceField.string('384', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
         startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
         startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
         type: 'web',
         type: 'web',
         replicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.replicaCount),
         replicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.replicaCount),
@@ -211,6 +213,7 @@ const WebService = {
     }),
     }),
     serialize: (service: WebService) => {
     serialize: (service: WebService) => {
         return {
         return {
+            smartOptimization: service.smartOptimization,
             replicaCount: service.replicas.value,
             replicaCount: service.replicas.value,
             resources: {
             resources: {
                 requests: {
                 requests: {
@@ -271,6 +274,7 @@ const WebService = {
         return {
         return {
             name,
             name,
             expanded: false,
             expanded: false,
+            smartOptimization: values.smartOptimization,
             cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
             cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
             ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
             ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
             startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
             startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
@@ -331,8 +335,9 @@ const WorkerService = {
     default: (name: string, porterJson?: PorterJson): WorkerService => ({
     default: (name: string, porterJson?: PorterJson): WorkerService => ({
         name,
         name,
         expanded: true,
         expanded: true,
-        cpu: ServiceField.string('100', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-        ram: ServiceField.string('256', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
+        smartOptimization: true,
+        cpu: ServiceField.string('187.5', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
+        ram: ServiceField.string('384', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
         startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
         startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
         type: 'worker',
         type: 'worker',
         replicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.replicaCount),
         replicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.replicaCount),
@@ -376,12 +381,14 @@ const WorkerService = {
                 dbPort: service.cloudsql.dbPort.value,
                 dbPort: service.cloudsql.dbPort.value,
                 serviceAccountJSON: service.cloudsql.serviceAccountJSON.value,
                 serviceAccountJSON: service.cloudsql.serviceAccountJSON.value,
             },
             },
+            smartOptimization: service.smartOptimization,
         }
         }
     },
     },
     deserialize: (name: string, values: any, porterJson?: PorterJson): WorkerService => {
     deserialize: (name: string, values: any, porterJson?: PorterJson): WorkerService => {
         return {
         return {
             name,
             name,
             expanded: false,
             expanded: false,
+            smartOptimization: values.smartOptimization,
             cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
             cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
             ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
             ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
             startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
             startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
@@ -414,8 +421,9 @@ const JobService = {
     default: (name: string, porterJson?: PorterJson): JobService => ({
     default: (name: string, porterJson?: PorterJson): JobService => ({
         name,
         name,
         expanded: true,
         expanded: true,
-        cpu: ServiceField.string('100', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-        ram: ServiceField.string('256', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
+        smartOptimization: true,
+        cpu: ServiceField.string('187.', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
+        ram: ServiceField.string('384', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
         startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
         startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
         type: 'job',
         type: 'job',
         jobsExecuteConcurrently: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.allowConcurrent),
         jobsExecuteConcurrently: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.allowConcurrent),
@@ -430,6 +438,7 @@ const JobService = {
     }),
     }),
     serialize: (service: JobService) => {
     serialize: (service: JobService) => {
         return {
         return {
+            smartOptimization: service.smartOptimization,
             allowConcurrent: service.jobsExecuteConcurrently.value,
             allowConcurrent: service.jobsExecuteConcurrently.value,
             container: {
             container: {
                 command: service.startCommand.value,
                 command: service.startCommand.value,
@@ -457,6 +466,7 @@ const JobService = {
         return {
         return {
             name,
             name,
             expanded: false,
             expanded: false,
+            smartOptimization: values.smartOptimization,
             cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
             cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
             ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
             ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.memory ? porterJson?.apps?.[name]?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
             startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
             startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
@@ -481,8 +491,9 @@ const ReleaseService = {
     default: (name: string, porterJson?: PorterJson): ReleaseService => ({
     default: (name: string, porterJson?: PorterJson): ReleaseService => ({
         name,
         name,
         expanded: true,
         expanded: true,
-        cpu: ServiceField.string('100', porterJson?.release?.config?.resources?.requests?.cpu ? porterJson?.release?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
-        ram: ServiceField.string('256', porterJson?.release?.config?.resources?.requests?.memory ? porterJson?.release?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
+        smartOptimization: true,
+        cpu: ServiceField.string('187.5', porterJson?.release?.config?.resources?.requests?.cpu ? porterJson?.release?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
+        ram: ServiceField.string('384', porterJson?.release?.config?.resources?.requests?.memory ? porterJson?.release?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
         startCommand: ServiceField.string('', porterJson?.release?.run),
         startCommand: ServiceField.string('', porterJson?.release?.run),
         type: 'release',
         type: 'release',
         canDelete: porterJson?.release == null,
         canDelete: porterJson?.release == null,
@@ -496,6 +507,7 @@ const ReleaseService = {
 
 
     serialize: (service: ReleaseService) => {
     serialize: (service: ReleaseService) => {
         return {
         return {
+            smartOptimization: service.smartOptimization,
             container: {
             container: {
                 command: service.startCommand.value,
                 command: service.startCommand.value,
             },
             },
@@ -519,6 +531,7 @@ const ReleaseService = {
         return {
         return {
             name,
             name,
             expanded: false,
             expanded: false,
+            smartOptimization: values.smartOptimization,
             cpu: ServiceField.string(values?.resources?.requests?.cpu?.replace('m', ''), porterJson?.release?.config?.resources?.requests?.cpu ? porterJson?.release?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
             cpu: ServiceField.string(values?.resources?.requests?.cpu?.replace('m', ''), porterJson?.release?.config?.resources?.requests?.cpu ? porterJson?.release?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
             ram: ServiceField.string(values?.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.release?.config?.resources?.requests?.memory ? porterJson?.release?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
             ram: ServiceField.string(values?.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.release?.config?.resources?.requests?.memory ? porterJson?.release?.config?.resources?.requests?.memory.replace('Mi', '') : undefined),
             startCommand: ServiceField.string(values?.container?.command ?? '', porterJson?.release?.run),
             startCommand: ServiceField.string(values?.container?.command ?? '', porterJson?.release?.run),

+ 106 - 33
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/JobTabs.tsx

@@ -1,5 +1,5 @@
 import Input from "components/porter/Input";
 import Input from "components/porter/Input";
-import React, { useContext } from "react"
+import React, { useContext, useState } from "react"
 import Text from "components/porter/Text";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import Spacer from "components/porter/Spacer";
 import TabSelector from "components/TabSelector";
 import TabSelector from "components/TabSelector";
@@ -9,8 +9,11 @@ import AnimateHeight, { Height } from "react-animate-height";
 import cronstrue from 'cronstrue';
 import cronstrue from 'cronstrue';
 import Link from "components/porter/Link";
 import Link from "components/porter/Link";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, MIB_TO_GIB, MILI_TO_CORE } from "./utils";
+import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, MIB_TO_GIB, MILI_TO_CORE, RESOURCE_ALLOCATION_RAM, UPPER_BOUND_SMART } from "./utils";
 import InputSlider from "components/porter/InputSlider";
 import InputSlider from "components/porter/InputSlider";
+import SmartOptModal from "./SmartOptModal";
+import { Switch } from "@material-ui/core";
+import styled from "styled-components";
 
 
 interface Props {
 interface Props {
   service: JobService;
   service: JobService;
@@ -18,6 +21,7 @@ interface Props {
   setHeight: (height: Height) => void;
   setHeight: (height: Height) => void;
   maxRAM: number;
   maxRAM: number;
   maxCPU: number;
   maxCPU: number;
+  nodeCount: number;
 }
 }
 
 
 const JobTabs: React.FC<Props> = ({
 const JobTabs: React.FC<Props> = ({
@@ -26,10 +30,37 @@ const JobTabs: React.FC<Props> = ({
   setHeight,
   setHeight,
   maxRAM,
   maxRAM,
   maxCPU,
   maxCPU,
+  nodeCount,
 }) => {
 }) => {
   const [currentTab, setCurrentTab] = React.useState<string>('main');
   const [currentTab, setCurrentTab] = React.useState<string>('main');
   const { currentCluster } = useContext(Context);
   const { currentCluster } = useContext(Context);
+  const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
+  const smartLimitRAM = (maxRAM - RESOURCE_ALLOCATION_RAM) * UPPER_BOUND_SMART
+  const smartLimitCPU = (maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * UPPER_BOUND_SMART
+  const handleSwitch = (event: React.ChangeEvent<HTMLInputElement>) => {
+    if ((service.cpu.value / MILI_TO_CORE) > (smartLimitCPU) || (service.ram.value / MILI_TO_CORE) > (smartLimitRAM)) {
 
 
+      editService({
+        ...service,
+        cpu: {
+          readOnly: false,
+          value: (smartLimitCPU * MILI_TO_CORE).toString()
+        },
+        ram: {
+          readOnly: false,
+          value: (smartLimitRAM * MIB_TO_GIB).toString()
+        },
+        smartOptimization: !service.smartOptimization
+      })
+    }
+    else {
+      editService({
+        ...service,
+        smartOptimization: !service.smartOptimization
+      })
+    }
+
+  };
   const getScheduleDescription = () => {
   const getScheduleDescription = () => {
     try {
     try {
       return <Text color="helper">This job runs: {cronstrue.toString(service.cronSchedule.value)}</Text>;
       return <Text color="helper">This job runs: {cronstrue.toString(service.cronSchedule.value)}</Text>;
@@ -78,39 +109,72 @@ const JobTabs: React.FC<Props> = ({
   };
   };
 
 
   const renderResources = () => {
   const renderResources = () => {
-    setHeight(292);
+    setHeight(316);
     return (
     return (
       <>
       <>
         <Spacer y={1} />
         <Spacer y={1} />
-        <InputSlider
-          label="CPUs: "
-          unit="Cores"
-          min={0}
-          max={maxCPU}
-          color={"#3a48ca"}
-          value={(service.cpu.value / MILI_TO_CORE).toString()}
-          setValue={(e) => {
-            editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } });
-          }}
-          step={0.01}
-          disabled={service.cpu.readOnly}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-        <Spacer y={1} />
-        <InputSlider
-          label="RAM: "
-          unit="GiB"
-          min={0}
-          max={maxRAM}
-          color={"#3a48ca"}
-          value={(service.ram.value / MIB_TO_GIB).toString()}
-          setValue={(e) => {
-            editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } });
-          }}
-          disabled={service.ram.readOnly}
-          step={0.01}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
+        <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
+          <StyledIcon
+            className="material-icons"
+            onClick={() => {
+              setShowNeedHelpModal(true)
+            }}
+          >
+            help_outline
+          </StyledIcon>
+          <Text style={{ marginRight: '10px' }}>Smart Optimization</Text>
+          <Switch
+            size="small"
+            color="primary"
+            checked={service.smartOptimization}
+            onChange={handleSwitch}
+            inputProps={{ 'aria-label': 'controlled' }}
+          />
+        </div>
+        {showNeedHelpModal &&
+          <SmartOptModal
+            setModalVisible={setShowNeedHelpModal}
+          />}
+        <>
+          <InputSlider
+            label="CPUs: "
+            unit="Cores"
+            override={!service.smartOptimization}
+            min={0}
+            max={Math.floor((maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * 10) / 10}
+            nodeCount={nodeCount}
+            color={"#3f51b5"}
+            smartLimit={smartLimitCPU}
+            value={(service.cpu.value / MILI_TO_CORE).toString()}
+            setValue={(e) => {
+              service.smartOptimization ? editService({ ...service, cpu: { readOnly: false, value: Math.round(e * MILI_TO_CORE * 10) / 10 }, ram: { readOnly: false, value: Math.round((e * maxRAM / maxCPU * MIB_TO_GIB) * 10) / 10 } }) :
+                editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } });
+            }}
+            step={0.1}
+            disabled={false}
+            disabledTooltip={"You may only edit this field in your porter.yaml."} />
+
+          <Spacer y={1} />
+
+          <InputSlider
+            label="RAM: "
+            unit="GiB"
+            min={0}
+            override={!service.smartOptimization}
+            nodeCount={nodeCount}
+            smartLimit={smartLimitRAM}
+            max={Math.floor((maxRAM - RESOURCE_ALLOCATION_RAM) * 10) / 10}
+            color={"#3f51b5"}
+            value={(service.ram.value / MIB_TO_GIB).toString()}
+            setValue={(e) => {
+              service.smartOptimization ? editService({ ...service, ram: { readOnly: false, value: Math.round(e * MIB_TO_GIB * 10) / 10 }, cpu: { readOnly: false, value: Math.round((e * (maxCPU / maxRAM) * MILI_TO_CORE) * 10) / 10 } }) :
+                editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } });
+            }}
+
+            disabled={service.ram.readOnly}
+            step={0.1}
+            disabledTooltip={"You may only edit this field in your porter.yaml."} />
+        </>
       </>
       </>
     )
     )
   };
   };
@@ -249,4 +313,13 @@ const JobTabs: React.FC<Props> = ({
   )
   )
 }
 }
 
 
-export default JobTabs;
+export default JobTabs;
+
+const StyledIcon = styled.i`
+  cursor: pointer;
+  font-size: 16px; 
+  margin-right : 5px;
+  &:hover {
+    color: #666;  
+  }
+`;

+ 35 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/NodeInfoModal.tsx

@@ -0,0 +1,35 @@
+
+import React, { useEffect, useRef, useState } from "react";
+import Modal from "components/porter/Modal";
+
+import Text from "components/porter/Text";
+
+import Spacer from "components/porter/Spacer";
+import Step from "components/porter/Step";
+import Link from "components/porter/Link";
+
+type Props = {
+    setModalVisible: (x: boolean) => void;
+};
+
+const NodeInfoModal: React.FC<Props> = ({
+    setModalVisible,
+}) => {
+    return (
+        <Modal closeModal={() => setModalVisible(false)} width={"800px"}>
+            <Text size={16}>Resource Optimization on Porter</Text>
+            <Spacer y={1} />
+            <Text color="helper">
+                The recommended marks are so that the application can run cost-efficiently on Porter.
+            </Text>
+            <Spacer y={1} />
+            <Text color="helper">
+                <Link to="https://docs.porter.run/other/kubernetes-101" target="_blank">
+                    For more information about Kubernetes resource management visit our docs.
+                </Link>
+            </Text>
+
+        </Modal>
+    );
+};
+export default NodeInfoModal;

+ 106 - 33
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/ReleaseTabs.tsx

@@ -1,5 +1,5 @@
 import Input from "components/porter/Input";
 import Input from "components/porter/Input";
-import React, { useContext } from "react"
+import React, { useContext, useState } from "react"
 import Spacer from "components/porter/Spacer";
 import Spacer from "components/porter/Spacer";
 import TabSelector from "components/TabSelector";
 import TabSelector from "components/TabSelector";
 import { ReleaseService } from "../serviceTypes";
 import { ReleaseService } from "../serviceTypes";
@@ -7,8 +7,11 @@ import AnimateHeight, { Height } from "react-animate-height";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import Checkbox from "components/porter/Checkbox";
 import Checkbox from "components/porter/Checkbox";
 import Text from "components/porter/Text";
 import Text from "components/porter/Text";
-import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, MIB_TO_GIB, MILI_TO_CORE } from "./utils";
+import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, MIB_TO_GIB, MILI_TO_CORE, RESOURCE_ALLOCATION_RAM, UPPER_BOUND_SMART } from "./utils";
 import InputSlider from "components/porter/InputSlider";
 import InputSlider from "components/porter/InputSlider";
+import SmartOptModal from "./SmartOptModal";
+import { Switch } from "@material-ui/core";
+import styled from "styled-components";
 
 
 interface Props {
 interface Props {
     service: ReleaseService;
     service: ReleaseService;
@@ -16,6 +19,7 @@ interface Props {
     setHeight: (height: Height) => void;
     setHeight: (height: Height) => void;
     maxRAM: number;
     maxRAM: number;
     maxCPU: number;
     maxCPU: number;
+    nodeCount?: number;
 }
 }
 
 
 const ReleaseTabs: React.FC<Props> = ({
 const ReleaseTabs: React.FC<Props> = ({
@@ -24,10 +28,37 @@ const ReleaseTabs: React.FC<Props> = ({
     setHeight,
     setHeight,
     maxRAM,
     maxRAM,
     maxCPU,
     maxCPU,
+    nodeCount,
 }) => {
 }) => {
     const [currentTab, setCurrentTab] = React.useState<string>('main');
     const [currentTab, setCurrentTab] = React.useState<string>('main');
     const { currentCluster } = useContext(Context);
     const { currentCluster } = useContext(Context);
+    const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
+    const smartLimitRAM = (maxRAM - RESOURCE_ALLOCATION_RAM) * UPPER_BOUND_SMART
+    const smartLimitCPU = (maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * UPPER_BOUND_SMART
+    const handleSwitch = (event: React.ChangeEvent<HTMLInputElement>) => {
+        if ((service.cpu.value / MILI_TO_CORE) > (smartLimitCPU) || (service.ram.value / MILI_TO_CORE) > (smartLimitRAM)) {
 
 
+            editService({
+                ...service,
+                cpu: {
+                    readOnly: false,
+                    value: (smartLimitCPU * MILI_TO_CORE).toString()
+                },
+                ram: {
+                    readOnly: false,
+                    value: (smartLimitRAM * MIB_TO_GIB).toString()
+                },
+                smartOptimization: !service.smartOptimization
+            })
+        }
+        else {
+            editService({
+                ...service,
+                smartOptimization: !service.smartOptimization
+            })
+        }
+
+    };
     const renderMain = () => {
     const renderMain = () => {
         setHeight(159);
         setHeight(159);
         return (
         return (
@@ -47,39 +78,72 @@ const ReleaseTabs: React.FC<Props> = ({
     };
     };
 
 
     const renderResources = () => {
     const renderResources = () => {
-        setHeight(292);
+        setHeight(316);
         return (
         return (
             <>
             <>
                 <Spacer y={1} />
                 <Spacer y={1} />
-                <InputSlider
-                    label="CPUs: "
-                    unit="Cores"
-                    min={0}
-                    max={maxCPU}
-                    color={"#3a48ca"}
-                    value={(service.cpu.value / MILI_TO_CORE).toString()}
-                    setValue={(e) => {
-                        editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } });
-                    }}
-                    step={0.01}
-                    disabled={service.cpu.readOnly}
-                    disabledTooltip={"You may only edit this field in your porter.yaml."}
-                />
-                <Spacer y={1} />
-                <InputSlider
-                    label="RAM: "
-                    unit="GiB"
-                    min={0}
-                    max={maxRAM}
-                    color={"#3a48ca"}
-                    value={(service.ram.value / MIB_TO_GIB).toString()}
-                    setValue={(e) => {
-                        editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } });
-                    }}
-                    disabled={service.ram.readOnly}
-                    step={0.01}
-                    disabledTooltip={"You may only edit this field in your porter.yaml."}
-                />
+                <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
+                    <StyledIcon
+                        className="material-icons"
+                        onClick={() => {
+                            setShowNeedHelpModal(true)
+                        }}
+                    >
+                        help_outline
+                    </StyledIcon>
+                    <Text style={{ marginRight: '10px' }}>Smart Optimization</Text>
+                    <Switch
+                        size="small"
+                        color="primary"
+                        checked={service.smartOptimization}
+                        onChange={handleSwitch}
+                        inputProps={{ 'aria-label': 'controlled' }}
+                    />
+                </div>
+                {showNeedHelpModal &&
+                    <SmartOptModal
+                        setModalVisible={setShowNeedHelpModal}
+                    />}
+                <>
+                    <InputSlider
+                        label="CPUs: "
+                        unit="Cores"
+                        override={!service.smartOptimization}
+                        min={0}
+                        max={Math.floor((maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * 10) / 10}
+                        nodeCount={nodeCount}
+                        color={"#3f51b5"}
+                        smartLimit={smartLimitCPU}
+                        value={(service.cpu.value / MILI_TO_CORE).toString()}
+                        setValue={(e) => {
+                            service.smartOptimization ? editService({ ...service, cpu: { readOnly: false, value: Math.round(e * MILI_TO_CORE * 10) / 10 }, ram: { readOnly: false, value: Math.round((e * maxRAM / maxCPU * MIB_TO_GIB) * 10) / 10 } }) :
+                                editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } });
+                        }}
+                        step={0.1}
+                        disabled={false}
+                        disabledTooltip={"You may only edit this field in your porter.yaml."} />
+
+                    <Spacer y={1} />
+
+                    <InputSlider
+                        label="RAM: "
+                        unit="GiB"
+                        min={0}
+                        override={!service.smartOptimization}
+                        nodeCount={nodeCount}
+                        smartLimit={smartLimitRAM}
+                        max={Math.floor((maxRAM - RESOURCE_ALLOCATION_RAM) * 10) / 10}
+                        color={"#3f51b5"}
+                        value={(service.ram.value / MIB_TO_GIB).toString()}
+                        setValue={(e) => {
+                            service.smartOptimization ? editService({ ...service, ram: { readOnly: false, value: Math.round(e * MIB_TO_GIB * 10) / 10 }, cpu: { readOnly: false, value: Math.round((e * (maxCPU / maxRAM) * MILI_TO_CORE) * 10) / 10 } }) :
+                                editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } });
+                        }}
+
+                        disabled={service.ram.readOnly}
+                        step={0.1}
+                        disabledTooltip={"You may only edit this field in your porter.yaml."} />
+                </>
             </>
             </>
         )
         )
     };
     };
@@ -199,4 +263,13 @@ const ReleaseTabs: React.FC<Props> = ({
     )
     )
 }
 }
 
 
-export default ReleaseTabs;
+export default ReleaseTabs;
+
+const StyledIcon = styled.i`
+  cursor: pointer;
+  font-size: 16px; 
+  margin-right : 5px;
+  &:hover {
+    color: #666;  
+  }
+`;

+ 50 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/SmartOptModal.tsx

@@ -0,0 +1,50 @@
+
+import React, { useEffect, useRef, useState } from "react";
+import Modal from "components/porter/Modal";
+
+import Text from "components/porter/Text";
+
+import Spacer from "components/porter/Spacer";
+import Step from "components/porter/Step";
+import Link from "components/porter/Link";
+
+type Props = {
+    setModalVisible: (x: boolean) => void;
+};
+
+const SmartOptModal: React.FC<Props> = ({
+    setModalVisible,
+}) => {
+    return (
+        <Modal closeModal={() => setModalVisible(false)} width={"800px"}>
+            <Text size={16}>Resource Optimization on Porter</Text>
+            <Spacer y={1} />
+            <Text color="helper">
+                Smart Optimization ensures that your app runs smoothly while minimizing costs. Smart Optimization performs the following:
+            </Text>
+            <Spacer y={1} />
+            <Step number={1}>
+                Maintains a consistent ratio between RAM and CPU based on the instance type.
+            </Step>
+            <Spacer y={1} />
+            <Step number={2}>
+                Enforces limits so that your app does not consume resources beyond the instance type's limits.
+            </Step>
+            <Spacer y={1} />
+            <Step number={3}> Determines an optimal resource threshold to save cost.</Step>
+            <Spacer y={1} />
+
+            <Text color="helper">
+                Turning off Smart Optimization will allow you to specify your own resource values. This is not recommended unless you are familiar with Kubernetes resource management.
+            </Text>
+            <Spacer y={1} />
+            <Text color="helper">
+                <Link to="https://docs.porter.run/other/kubernetes-101" target="_blank">
+                    For more information about Kubernetes resource management visit our docs.
+                </Link>
+            </Text>
+
+        </Modal>
+    );
+};
+export default SmartOptModal;

+ 155 - 103
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WebTabs.tsx

@@ -7,12 +7,21 @@ import Checkbox from "components/porter/Checkbox";
 import { Service, WebService } from "../serviceTypes";
 import { Service, WebService } from "../serviceTypes";
 import AnimateHeight, { Height } from "react-animate-height";
 import AnimateHeight, { Height } from "react-animate-height";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, RESOURCE_HEIGHT_WITHOUT_AUTOSCALING, RESOURCE_HEIGHT_WITH_AUTOSCALING, AWS_INSTANCE_LIMITS, MILI_TO_CORE, MIB_TO_GIB } from "./utils";
+import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, RESOURCE_HEIGHT_WITHOUT_AUTOSCALING, RESOURCE_HEIGHT_WITH_AUTOSCALING, AWS_INSTANCE_LIMITS, MILI_TO_CORE, MIB_TO_GIB, UPPER_BOUND_SMART, UPPER_BOUND_REG, RESOURCE_ALLOCATION, RESOURCE_ALLOCATION_CPU, RESOURCE_ALLOCATION_RAM } from "./utils";
 import IngressCustomAnnotations from "./IngressCustomAnnotations";
 import IngressCustomAnnotations from "./IngressCustomAnnotations";
 import CustomDomains from "./CustomDomains";
 import CustomDomains from "./CustomDomains";
 import InputSlider from "components/porter/InputSlider";
 import InputSlider from "components/porter/InputSlider";
 import api from "shared/api";
 import api from "shared/api";
-
+import Toggle from "components/porter/Toggle";
+import Container from "components/porter/Container";
+import { FormControlLabel, Switch } from "@material-ui/core";
+import DialToggle from "components/porter/DialToggle";
+import { max } from "lodash";
+import styled from "styled-components";
+import Step from "components/porter/Step";
+import Link from "components/porter/Link";
+import Modal from "components/porter/Modal";
+import SmartOptModal from "./SmartOptModal";
 interface Props {
 interface Props {
   service: WebService;
   service: WebService;
   editService: (service: WebService) => void;
   editService: (service: WebService) => void;
@@ -20,6 +29,7 @@ interface Props {
   chart?: any;
   chart?: any;
   maxRAM: number;
   maxRAM: number;
   maxCPU: number;
   maxCPU: number;
+  nodeCount: number;
 }
 }
 
 
 const NETWORKING_HEIGHT_WITHOUT_INGRESS = 204;
 const NETWORKING_HEIGHT_WITHOUT_INGRESS = 204;
@@ -34,10 +44,38 @@ const WebTabs: React.FC<Props> = ({
   setHeight,
   setHeight,
   maxRAM,
   maxRAM,
   maxCPU,
   maxCPU,
+  nodeCount,
 }) => {
 }) => {
   const [currentTab, setCurrentTab] = React.useState<string>("main");
   const [currentTab, setCurrentTab] = React.useState<string>("main");
   const { currentCluster } = useContext(Context);
   const { currentCluster } = useContext(Context);
+  const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
+  const smartLimitRAM = (maxRAM - RESOURCE_ALLOCATION_RAM) * UPPER_BOUND_SMART
+  const smartLimitCPU = (maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * UPPER_BOUND_SMART
+
+  const handleSwitch = (event: React.ChangeEvent<HTMLInputElement>) => {
+    if ((service.cpu.value / MILI_TO_CORE) > (smartLimitCPU) || (service.ram.value / MILI_TO_CORE) > (smartLimitRAM)) {
+
+      editService({
+        ...service,
+        cpu: {
+          readOnly: false,
+          value: (smartLimitCPU * MILI_TO_CORE).toString()
+        },
+        ram: {
+          readOnly: false,
+          value: (smartLimitRAM * MIB_TO_GIB).toString()
+        },
+        smartOptimization: !service.smartOptimization
+      })
+    }
+    else {
+      editService({
+        ...service,
+        smartOptimization: !service.smartOptimization
+      })
+    }
 
 
+  };
   const renderMain = () => {
   const renderMain = () => {
     setHeight(159);
     setHeight(159);
     return (
     return (
@@ -249,43 +287,75 @@ const WebTabs: React.FC<Props> = ({
     return (
     return (
       <>
       <>
         <Spacer y={1} />
         <Spacer y={1} />
-        <InputSlider
-          label="CPUs: "
-          unit="Cores"
-          min={0}
-          max={maxCPU}
-          color={"#3a48ca"}
-          value={(service.cpu.value / MILI_TO_CORE).toString()}
-          setValue={(e) => {
-            editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } });
-          }}
-          step={0.01}
-          disabled={service.cpu.readOnly}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-        <Spacer y={1} />
-        <InputSlider
-          label="RAM: "
-          unit="GiB"
-          min={0}
-          max={maxRAM}
-          color={"#3a48ca"}
-          value={(service.ram.value / MIB_TO_GIB).toString()}
-          setValue={(e) => {
-            editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } });
-          }}
-          disabled={service.ram.readOnly}
-          step={0.01}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
+        <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
+          <StyledIcon
+            className="material-icons"
+            onClick={() => {
+              setShowNeedHelpModal(true)
+            }}
+          >
+            help_outline
+          </StyledIcon>
+          <Text style={{ marginRight: '10px' }}>Smart Optimization</Text>
+          <Switch
+            size="small"
+            color="primary"
+            checked={service.smartOptimization}
+            onChange={handleSwitch}
+            inputProps={{ 'aria-label': 'controlled' }}
+          />
+        </div>
+        {showNeedHelpModal &&
+          <SmartOptModal
+            setModalVisible={setShowNeedHelpModal}
+          />}
+        <>
+          <InputSlider
+            label="CPUs: "
+            unit="Cores"
+            override={!service.smartOptimization}
+            min={0}
+            max={Math.floor((maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * 10) / 10}
+            nodeCount={nodeCount}
+            color={"#3f51b5"}
+            smartLimit={smartLimitCPU}
+            value={(service.cpu.value / MILI_TO_CORE).toString()}
+            setValue={(e) => {
+              service.smartOptimization ? editService({ ...service, cpu: { readOnly: false, value: Math.round(e * MILI_TO_CORE * 10) / 10 }, ram: { readOnly: false, value: Math.round((e * maxRAM / maxCPU * MIB_TO_GIB) * 10) / 10 } }) :
+                editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } });
+            }}
+            step={0.1}
+            disabled={false}
+            disabledTooltip={"You may only edit this field in your porter.yaml."} />
+
+          <Spacer y={1} />
+
+          <InputSlider
+            label="RAM: "
+            unit="GiB"
+            min={0}
+            override={!service.smartOptimization}
+            nodeCount={nodeCount}
+            smartLimit={smartLimitRAM}
+            max={Math.floor((maxRAM - RESOURCE_ALLOCATION_RAM) * 10) / 10}
+            color={"#3f51b5"}
+            value={(service.ram.value / MIB_TO_GIB).toString()}
+            setValue={(e) => {
+              service.smartOptimization ? editService({ ...service, ram: { readOnly: false, value: Math.round(e * MIB_TO_GIB * 10) / 10 }, cpu: { readOnly: false, value: Math.round((e * (maxCPU / maxRAM) * MILI_TO_CORE) * 10) / 10 } }) :
+                editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } });
+            }}
+
+            disabled={service.ram.readOnly}
+            step={0.1}
+            disabledTooltip={"You may only edit this field in your porter.yaml."} />
+        </>
         <Spacer y={1} />
         <Spacer y={1} />
+
         <Input
         <Input
           label="Replicas"
           label="Replicas"
           placeholder="ex: 1"
           placeholder="ex: 1"
           value={service.replicas.value}
           value={service.replicas.value}
-          disabled={
-            service.replicas.readOnly || service.autoscaling.enabled.value
-          }
+          disabled={service.replicas.readOnly || service.autoscaling.enabled.value}
           width="300px"
           width="300px"
           setValue={(e) => {
           setValue={(e) => {
             editService({
             editService({
@@ -293,43 +363,35 @@ const WebTabs: React.FC<Props> = ({
               replicas: { readOnly: false, value: e },
               replicas: { readOnly: false, value: e },
             });
             });
           }}
           }}
-          disabledTooltip={
-            service.replicas.readOnly
-              ? "You may only edit this field in your porter.yaml."
-              : "Disable autoscaling to specify replicas."
-          }
-        />
-        <Spacer y={1} />
-        <Checkbox
-          checked={service.autoscaling.enabled.value}
-          toggleChecked={() => {
-            editService({
-              ...service,
-              autoscaling: {
-                ...service.autoscaling,
-                enabled: {
-                  readOnly: false,
-                  value: !service.autoscaling.enabled.value,
-                },
-              },
-            });
-            setHeight(service.autoscaling.enabled.value ? RESOURCE_HEIGHT_WITHOUT_AUTOSCALING : RESOURCE_HEIGHT_WITH_AUTOSCALING);
-          }}
-          disabled={service.autoscaling.enabled.readOnly}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        >
+          disabledTooltip={service.replicas.readOnly
+            ? "You may only edit this field in your porter.yaml."
+            : "Disable autoscaling to specify replicas."} /><Spacer y={1} /><Checkbox
+              checked={service.autoscaling.enabled.value}
+              toggleChecked={() => {
+                editService({
+                  ...service,
+                  autoscaling: {
+                    ...service.autoscaling,
+                    enabled: {
+                      readOnly: false,
+                      value: !service.autoscaling.enabled.value,
+                    },
+                  },
+                });
+                setHeight(service.autoscaling.enabled.value ? RESOURCE_HEIGHT_WITHOUT_AUTOSCALING : RESOURCE_HEIGHT_WITH_AUTOSCALING);
+              }}
+              disabled={service.autoscaling.enabled.readOnly}
+              disabledTooltip={"You may only edit this field in your porter.yaml."}
+            >
           <Text color="helper">Enable autoscaling (overrides replicas)</Text>
           <Text color="helper">Enable autoscaling (overrides replicas)</Text>
-        </Checkbox>
-        <AnimateHeight height={service.autoscaling.enabled.value ? 'auto' : 0}>
+        </Checkbox><AnimateHeight height={service.autoscaling.enabled.value ? 'auto' : 0}>
           <Spacer y={1} />
           <Spacer y={1} />
           <Input
           <Input
             label="Min replicas"
             label="Min replicas"
             placeholder="ex: 1"
             placeholder="ex: 1"
             value={service.autoscaling.minReplicas.value}
             value={service.autoscaling.minReplicas.value}
-            disabled={
-              service.autoscaling.minReplicas.readOnly ||
-              !service.autoscaling.enabled.value
-            }
+            disabled={service.autoscaling.minReplicas.readOnly ||
+              !service.autoscaling.enabled.value}
             width="300px"
             width="300px"
             setValue={(e) => {
             setValue={(e) => {
               editService({
               editService({
@@ -340,21 +402,16 @@ const WebTabs: React.FC<Props> = ({
                 },
                 },
               });
               });
             }}
             }}
-            disabledTooltip={
-              service.autoscaling.minReplicas.readOnly
-                ? "You may only edit this field in your porter.yaml."
-                : "Enable autoscaling to specify min replicas."
-            }
-          />
+            disabledTooltip={service.autoscaling.minReplicas.readOnly
+              ? "You may only edit this field in your porter.yaml."
+              : "Enable autoscaling to specify min replicas."} />
           <Spacer y={1} />
           <Spacer y={1} />
           <Input
           <Input
             label="Max replicas"
             label="Max replicas"
             placeholder="ex: 10"
             placeholder="ex: 10"
             value={service.autoscaling.maxReplicas.value}
             value={service.autoscaling.maxReplicas.value}
-            disabled={
-              service.autoscaling.maxReplicas.readOnly ||
-              !service.autoscaling.enabled.value
-            }
+            disabled={service.autoscaling.maxReplicas.readOnly ||
+              !service.autoscaling.enabled.value}
             width="300px"
             width="300px"
             setValue={(e) => {
             setValue={(e) => {
               editService({
               editService({
@@ -365,12 +422,9 @@ const WebTabs: React.FC<Props> = ({
                 },
                 },
               });
               });
             }}
             }}
-            disabledTooltip={
-              service.autoscaling.maxReplicas.readOnly
-                ? "You may only edit this field in your porter.yaml."
-                : "Enable autoscaling to specify max replicas."
-            }
-          />
+            disabledTooltip={service.autoscaling.maxReplicas.readOnly
+              ? "You may only edit this field in your porter.yaml."
+              : "Enable autoscaling to specify max replicas."} />
           <Spacer y={1} />
           <Spacer y={1} />
           <InputSlider
           <InputSlider
             label="Target CPU utilization: "
             label="Target CPU utilization: "
@@ -378,10 +432,8 @@ const WebTabs: React.FC<Props> = ({
             min={0}
             min={0}
             max={100}
             max={100}
             value={service.autoscaling.targetCPUUtilizationPercentage.value}
             value={service.autoscaling.targetCPUUtilizationPercentage.value}
-            disabled={
-              service.autoscaling.targetCPUUtilizationPercentage.readOnly ||
-              !service.autoscaling.enabled.value
-            }
+            disabled={service.autoscaling.targetCPUUtilizationPercentage.readOnly ||
+              !service.autoscaling.enabled.value}
             width="300px"
             width="300px"
             setValue={(e) => {
             setValue={(e) => {
               editService({
               editService({
@@ -392,12 +444,9 @@ const WebTabs: React.FC<Props> = ({
                 },
                 },
               });
               });
             }}
             }}
-            disabledTooltip={
-              service.autoscaling.targetCPUUtilizationPercentage.readOnly
-                ? "You may only edit this field in your porter.yaml."
-                : "Enable autoscaling to specify target CPU utilization."
-            }
-          />
+            disabledTooltip={service.autoscaling.targetCPUUtilizationPercentage.readOnly
+              ? "You may only edit this field in your porter.yaml."
+              : "Enable autoscaling to specify target CPU utilization."} />
           <Spacer y={1} />
           <Spacer y={1} />
           <InputSlider
           <InputSlider
             label="Target RAM utilization: "
             label="Target RAM utilization: "
@@ -405,10 +454,8 @@ const WebTabs: React.FC<Props> = ({
             min={0}
             min={0}
             max={100}
             max={100}
             value={service.autoscaling.targetMemoryUtilizationPercentage.value}
             value={service.autoscaling.targetMemoryUtilizationPercentage.value}
-            disabled={
-              service.autoscaling.targetMemoryUtilizationPercentage.readOnly ||
-              !service.autoscaling.enabled.value
-            }
+            disabled={service.autoscaling.targetMemoryUtilizationPercentage.readOnly ||
+              !service.autoscaling.enabled.value}
             width="300px"
             width="300px"
             setValue={(e) => {
             setValue={(e) => {
               editService({
               editService({
@@ -422,14 +469,10 @@ const WebTabs: React.FC<Props> = ({
                 },
                 },
               });
               });
             }}
             }}
-            disabledTooltip={
-              service.autoscaling.targetMemoryUtilizationPercentage.readOnly
-                ? "You may only edit this field in your porter.yaml."
-                : "Enable autoscaling to specify target RAM utilization."
-            }
-          />
-        </AnimateHeight>
-      </>
+            disabledTooltip={service.autoscaling.targetMemoryUtilizationPercentage.readOnly
+              ? "You may only edit this field in your porter.yaml."
+              : "Enable autoscaling to specify target RAM utilization."} />
+        </AnimateHeight></>
     );
     );
   };
   };
 
 
@@ -860,3 +903,12 @@ const WebTabs: React.FC<Props> = ({
 };
 };
 
 
 export default WebTabs;
 export default WebTabs;
+
+const StyledIcon = styled.i`
+  cursor: pointer;
+  font-size: 16px; 
+  margin-right : 5px;
+  &:hover {
+    color: #666;  
+  }
+`;

+ 105 - 32
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/WorkerTabs.tsx

@@ -1,14 +1,17 @@
 import Input from "components/porter/Input";
 import Input from "components/porter/Input";
-import React, { useContext } from "react"
+import React, { useContext, useState } from "react"
 import Text from "components/porter/Text";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import Spacer from "components/porter/Spacer";
 import TabSelector from "components/TabSelector";
 import TabSelector from "components/TabSelector";
 import Checkbox from "components/porter/Checkbox";
 import Checkbox from "components/porter/Checkbox";
 import { WorkerService } from "../serviceTypes";
 import { WorkerService } from "../serviceTypes";
 import AnimateHeight, { Height } from "react-animate-height";
 import AnimateHeight, { Height } from "react-animate-height";
-import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, MIB_TO_GIB, MILI_TO_CORE, RESOURCE_HEIGHT_WITHOUT_AUTOSCALING, RESOURCE_HEIGHT_WITH_AUTOSCALING } from "./utils";
+import { DATABASE_HEIGHT_DISABLED, DATABASE_HEIGHT_ENABLED, MIB_TO_GIB, MILI_TO_CORE, RESOURCE_ALLOCATION_RAM, RESOURCE_HEIGHT_WITHOUT_AUTOSCALING, RESOURCE_HEIGHT_WITH_AUTOSCALING, UPPER_BOUND_SMART } from "./utils";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
 import InputSlider from "components/porter/InputSlider";
 import InputSlider from "components/porter/InputSlider";
+import styled from "styled-components";
+import { Switch } from "@material-ui/core";
+import SmartOptModal from "./SmartOptModal";
 
 
 interface Props {
 interface Props {
   service: WorkerService;
   service: WorkerService;
@@ -16,6 +19,7 @@ interface Props {
   setHeight: (height: Height) => void;
   setHeight: (height: Height) => void;
   maxRAM: number;
   maxRAM: number;
   maxCPU: number;
   maxCPU: number;
+  nodeCount: number;
 }
 }
 
 
 const WorkerTabs: React.FC<Props> = ({
 const WorkerTabs: React.FC<Props> = ({
@@ -24,10 +28,37 @@ const WorkerTabs: React.FC<Props> = ({
   setHeight,
   setHeight,
   maxCPU,
   maxCPU,
   maxRAM,
   maxRAM,
+  nodeCount,
 }) => {
 }) => {
   const [currentTab, setCurrentTab] = React.useState<string>('main');
   const [currentTab, setCurrentTab] = React.useState<string>('main');
   const { currentCluster } = useContext(Context);
   const { currentCluster } = useContext(Context);
+  const [showNeedHelpModal, setShowNeedHelpModal] = useState(false);
+  const smartLimitRAM = (maxRAM - RESOURCE_ALLOCATION_RAM) * UPPER_BOUND_SMART
+  const smartLimitCPU = (maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * UPPER_BOUND_SMART
+  const handleSwitch = (event: React.ChangeEvent<HTMLInputElement>) => {
+    if ((service.cpu.value / MILI_TO_CORE) > (smartLimitCPU) || (service.ram.value / MILI_TO_CORE) > (smartLimitRAM)) {
 
 
+      editService({
+        ...service,
+        cpu: {
+          readOnly: false,
+          value: (smartLimitCPU * MILI_TO_CORE).toString()
+        },
+        ram: {
+          readOnly: false,
+          value: (smartLimitRAM * MIB_TO_GIB).toString()
+        },
+        smartOptimization: !service.smartOptimization
+      })
+    }
+    else {
+      editService({
+        ...service,
+        smartOptimization: !service.smartOptimization
+      })
+    }
+
+  };
   const renderMain = () => {
   const renderMain = () => {
     setHeight(159);
     setHeight(159);
     return (
     return (
@@ -51,35 +82,68 @@ const WorkerTabs: React.FC<Props> = ({
     return (
     return (
       <>
       <>
         <Spacer y={1} />
         <Spacer y={1} />
-        <InputSlider
-          label="CPUs: "
-          unit="Cores"
-          min={0}
-          max={maxCPU}
-          color={"#3a48ca"}
-          value={(service.cpu.value / MILI_TO_CORE).toString()}
-          setValue={(e) => {
-            editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } });
-          }}
-          step={0.01}
-          disabled={service.cpu.readOnly}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
-        <Spacer y={1} />
-        <InputSlider
-          label="RAM: "
-          unit="GiB"
-          min={0}
-          max={maxRAM}
-          color={"#3a48ca"}
-          value={(service.ram.value / MIB_TO_GIB).toString()}
-          setValue={(e) => {
-            editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } });
-          }}
-          disabled={service.ram.readOnly}
-          step={0.01}
-          disabledTooltip={"You may only edit this field in your porter.yaml."}
-        />
+        <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
+          <StyledIcon
+            className="material-icons"
+            onClick={() => {
+              setShowNeedHelpModal(true)
+            }}
+          >
+            help_outline
+          </StyledIcon>
+          <Text style={{ marginRight: '10px' }}>Smart Optimization</Text>
+          <Switch
+            size="small"
+            color="primary"
+            checked={service.smartOptimization}
+            onChange={handleSwitch}
+            inputProps={{ 'aria-label': 'controlled' }}
+          />
+        </div>
+        {showNeedHelpModal &&
+          <SmartOptModal
+            setModalVisible={setShowNeedHelpModal}
+          />}
+        <>
+          <InputSlider
+            label="CPUs: "
+            unit="Cores"
+            override={!service.smartOptimization}
+            min={0}
+            max={Math.floor((maxCPU - (RESOURCE_ALLOCATION_RAM * maxCPU / maxRAM)) * 10) / 10}
+            nodeCount={nodeCount}
+            color={"#3f51b5"}
+            smartLimit={smartLimitCPU}
+            value={(service.cpu.value / MILI_TO_CORE).toString()}
+            setValue={(e) => {
+              service.smartOptimization ? editService({ ...service, cpu: { readOnly: false, value: Math.round(e * MILI_TO_CORE * 10) / 10 }, ram: { readOnly: false, value: Math.round((e * maxRAM / maxCPU * MIB_TO_GIB) * 10) / 10 } }) :
+                editService({ ...service, cpu: { readOnly: false, value: e * MILI_TO_CORE } });
+            }}
+            step={0.1}
+            disabled={false}
+            disabledTooltip={"You may only edit this field in your porter.yaml."} />
+
+          <Spacer y={1} />
+
+          <InputSlider
+            label="RAM: "
+            unit="GiB"
+            min={0}
+            override={!service.smartOptimization}
+            nodeCount={nodeCount}
+            smartLimit={smartLimitRAM}
+            max={Math.floor((maxRAM - RESOURCE_ALLOCATION_RAM) * 10) / 10}
+            color={"#3f51b5"}
+            value={(service.ram.value / MIB_TO_GIB).toString()}
+            setValue={(e) => {
+              service.smartOptimization ? editService({ ...service, ram: { readOnly: false, value: Math.round(e * MIB_TO_GIB * 10) / 10 }, cpu: { readOnly: false, value: Math.round((e * (maxCPU / maxRAM) * MILI_TO_CORE) * 10) / 10 } }) :
+                editService({ ...service, ram: { readOnly: false, value: e * MIB_TO_GIB } });
+            }}
+
+            disabled={service.ram.readOnly}
+            step={0.1}
+            disabledTooltip={"You may only edit this field in your porter.yaml."} />
+        </>
         <Spacer y={1} />
         <Spacer y={1} />
         <Input
         <Input
           label="Replicas"
           label="Replicas"
@@ -326,4 +390,13 @@ const WorkerTabs: React.FC<Props> = ({
   )
   )
 }
 }
 
 
-export default WorkerTabs;
+export default WorkerTabs;
+
+const StyledIcon = styled.i`
+  cursor: pointer;
+  font-size: 16px; 
+  margin-right : 5px;
+  &:hover {
+    color: #666;  
+  }
+`;

+ 7 - 3
dashboard/src/main/home/app-dashboard/new-app-flow/tabs/utils.ts

@@ -1,7 +1,7 @@
 export const DATABASE_HEIGHT_ENABLED = 374;
 export const DATABASE_HEIGHT_ENABLED = 374;
 export const DATABASE_HEIGHT_DISABLED = 119;
 export const DATABASE_HEIGHT_DISABLED = 119;
-export const RESOURCE_HEIGHT_WITHOUT_AUTOSCALING = 422;
-export const RESOURCE_HEIGHT_WITH_AUTOSCALING = 809;
+export const RESOURCE_HEIGHT_WITHOUT_AUTOSCALING = 446;
+export const RESOURCE_HEIGHT_WITH_AUTOSCALING = 833;
 export const MIB_TO_GIB = 1024;
 export const MIB_TO_GIB = 1024;
 export const MILI_TO_CORE = 1000;
 export const MILI_TO_CORE = 1000;
 interface InstanceDetails {
 interface InstanceDetails {
@@ -81,4 +81,8 @@ export const AWS_INSTANCE_LIMITS: InstanceTypes = {
         "large": { "vCPU": 2, "RAM": 32 },
         "large": { "vCPU": 2, "RAM": 32 },
         "xlarge": { "vCPU": 4, "RAM": 64 },
         "xlarge": { "vCPU": 4, "RAM": 64 },
     }
     }
-}
+}
+
+
+export const UPPER_BOUND_SMART = .5
+export const RESOURCE_ALLOCATION_RAM = 1