Bläddra i källkod

Slider for Ram and CPU

Soham Dessai 3 år sedan
förälder
incheckning
9055b83b34

+ 2 - 1
dashboard/src/components/form-components/InputRow.tsx

@@ -16,6 +16,7 @@ type PropsType = {
   className?: string;
   maxLength?: number;
   hasError?: boolean;
+  inputProps?: boolean;
 };
 
 type StateType = {
@@ -120,7 +121,7 @@ const Input = styled.input<{ disabled: boolean; width: string }>`
   outline: none;
   border: none;
   font-size: 13px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   cursor: ${(props) => (props.disabled ? "not-allowed" : "")};
   width: ${(props) => (props.width ? props.width : "100%")};
   color: ${(props) => (props.disabled ? "#ffffff44" : "white")};

+ 28 - 4
dashboard/src/components/porter-form/PorterForm.tsx

@@ -6,6 +6,7 @@ import {
   FormField,
   InjectedProps,
   InputField,
+  InputSliderField,
   KeyValueArrayField,
   ResourceListField,
   Section,
@@ -31,6 +32,7 @@ import VeleroForm from "./field-components/VeleroForm";
 import CronInput from "./field-components/CronInput";
 import TextAreaInput from "./field-components/TextAreaInput";
 import UrlLink from "./field-components/UrlLink";
+import InputSlider from "components/porter/InputSlider";
 
 interface Props {
   leftTabOptions?: TabOption[];
@@ -53,7 +55,7 @@ interface Props {
   // The tab to redirect to after saving the form
   redirectTabAfterSave?: string;
   injectedProps?: InjectedProps;
-
+  isCapiEnabled?: boolean;
   absoluteSave: boolean;
 }
 
@@ -68,7 +70,11 @@ const PorterForm: React.FC<Props> = (props) => {
 
   const { currentTab, setCurrentTab } = props;
 
-  const renderSectionField = (field: FormField, num?: number, i?: number): JSX.Element => {
+  const renderSectionField = (
+    field: FormField,
+    num?: number,
+    i?: number
+  ): JSX.Element => {
     const injected = props.injectedProps?.[field.type];
 
     const bundledProps = {
@@ -81,9 +87,27 @@ const PorterForm: React.FC<Props> = (props) => {
       case "heading":
         // Remove top margin from heading if it's the first form element in the tab
         // TODO: Handle Job form and form variables more gracefully
-        return <Heading isAtTop={num + i < 1 || (formData.name === "Job" && num + i === 1) || (formData.name === "Worker" && num + i === 1)}>{field.label}</Heading>;
+        return (
+          <Heading
+            isAtTop={
+              num + i < 1 ||
+              (formData.name === "Job" && num + i === 1) ||
+              (formData.name === "Worker" && num + i === 1)
+            }
+          >
+            {field.label}
+          </Heading>
+        );
       case "subtitle":
         return <Helper>{field.label}</Helper>;
+      case "input-slider":
+        if (props.isCapiEnabled) {
+          return (
+            <InputSlider {...(bundledProps as InputSliderField)}></InputSlider>
+          );
+        } else {
+          return <Input {...(bundledProps as InputField)} />;
+        }
       case "input":
         return <Input {...(bundledProps as InputField)} />;
       case "checkbox":
@@ -268,6 +292,6 @@ const StyledPorterForm = styled.div<{ showSave?: boolean }>`
   margin-bottom: 5px;
   font-size: 13px;
   border-radius: 5px;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
   border: 1px solid #494b4f;
 `;

+ 14 - 0
dashboard/src/components/porter-form/PorterFormContextProvider.tsx

@@ -272,6 +272,20 @@ export const PorterFormContextProvider: React.FC<Props> = (props) => {
               contents: section.contents
                 ?.map((field: any, k) => {
                   const id = `${i}-${j}-${k}`;
+                  if (
+                    (field?.type == "number-input" && field?.label == "RAM") ||
+                    field?.label == "CPU"
+                  ) {
+                    return {
+                      id,
+                      ...field,
+                      type: "input-slider",
+                      settings: {
+                        ...field.settings,
+                        type: "number",
+                      },
+                    };
+                  }
                   if (field?.type == "number-input") {
                     return {
                       id,

+ 3 - 0
dashboard/src/components/porter-form/PorterFormWrapper.tsx

@@ -26,10 +26,12 @@ type PropsType = {
   includeMetadata?: boolean;
   injectedProps?: InjectedProps;
   overrideCurrentTab?: string;
+  isCapiEnabled?: boolean;
   onTabChange?: (newTab: string) => void;
 };
 
 const PorterFormWrapper: React.FC<PropsType> = ({
+  isCapiEnabled,
   formData,
   valuesToOverride,
   isReadOnly,
@@ -97,6 +99,7 @@ const PorterFormWrapper: React.FC<PropsType> = ({
         includeMetadata={includeMetadata}
       >
         <PorterForm
+          isCapiEnabled={isCapiEnabled}
           showStateDebugger={showStateDebugger}
           addendum={addendum}
           isReadOnly={isReadOnly}

+ 20 - 1
dashboard/src/components/porter-form/types.ts

@@ -71,6 +71,21 @@ export interface InputField extends GenericInputField {
   };
 }
 
+export interface InputSliderField extends GenericInputField {
+  min: string;
+  max: string;
+  type: "input-slider";
+  label?: string;
+  placeholder?: string;
+  info?: string;
+  settings?: {
+    type?: "text" | "password" | "number";
+    unit?: string;
+    omitUnitFromValue?: boolean;
+    default: string | number;
+  };
+}
+
 export interface CheckboxField extends GenericInputField {
   type: "checkbox";
   label?: string;
@@ -157,6 +172,7 @@ export interface UrlLinkField extends GenericInputField {
 export type FormField =
   | HeadingField
   | SubtitleField
+  | InputSliderField
   | InputField
   | CheckboxField
   | KeyValueArrayField
@@ -213,7 +229,10 @@ export interface PorterFormValidationInfo {
 }
 
 // internal field state interfaces
-export interface StringInputFieldState {}
+export interface StringInputFieldState {
+  variables: any;
+  vars: any;
+}
 export interface CheckboxFieldState {}
 
 export type PartialEnvGroup = {

+ 255 - 0
dashboard/src/components/porter/InputSlider.tsx

@@ -0,0 +1,255 @@
+import useFormField from "components/porter-form/hooks/useFormField";
+import {
+  GetFinalVariablesFunction,
+  InputField,
+  StringInputFieldState,
+} from "components/porter-form/types";
+import * as React from "react";
+import styled from "styled-components";
+import {
+  InputSliderField as InputSliderField,
+  GenericInputField,
+} from "../porter-form/types";
+
+export const hasSetValue = (field: GenericInputField) => {
+  return field.value && field.value.length != 0 && field.value[0] != null;
+};
+const clipOffUnit = (unit: string, x: string) => {
+  if (typeof x === "string" && unit) {
+    return unit === x.slice(x.length - unit.length, x.length)
+      ? x.slice(0, x.length - unit.length)
+      : x;
+  }
+  return x;
+};
+
+const InputSlider: React.FC<InputSliderField> = (props) => {
+  const { id, variable, label, settings, value } = props;
+
+  const {
+    state,
+    variables,
+    setVars,
+    setValidation,
+  } = useFormField<StringInputFieldState>(id, {
+    initValidation: {
+      validated: hasSetValue(props),
+    },
+    initVars: {
+      [variable]: hasSetValue(props)
+        ? clipOffUnit(settings?.unit, value[0])
+        : undefined,
+      displayUnit: "MiB", // Initialize displayUnit here
+    },
+  });
+
+  const maxValue = 8192 / 1024; // Update maxValue to be in GiB
+  const stepValue = 0.01; // Remove stepValue state and set it as a constant
+  const [popupPosition, setPopupPosition] = React.useState<React.CSSProperties>(
+    {}
+  );
+  const [showPopup, setShowPopup] = React.useState(false);
+
+  // Function to update the popup position based on the slider value
+  const updatePopupPosition = () => {
+    if (rangeInputRef.current) {
+      const rect = rangeInputRef.current.getBoundingClientRect();
+      const percentage =
+        (displayValue - parseFloat(rangeInputRef.current.min)) /
+        (parseFloat(rangeInputRef.current.max) -
+          parseFloat(rangeInputRef.current.min));
+      const x = rect.width * percentage;
+      // Add an offset to the x position calculation to center the popup above the dial
+      const xOffset = 8;
+      setPopupPosition({
+        left: x + xOffset,
+      });
+    }
+  };
+  const rangeInputRef = React.useRef<HTMLInputElement>(null);
+  if (state == undefined) {
+    return <></>;
+  }
+
+  const handleMouseMove = () => {
+    updatePopupPosition();
+  };
+
+  // Event handlers to show and hide the popup
+  const handleMouseEnter = () => {
+    updatePopupPosition();
+    setShowPopup(true);
+  };
+
+  const handleMouseLeave = () => {
+    setShowPopup(false);
+  };
+  const setValue = (x: string | number) => {
+    setVars((vars) => {
+      return {
+        ...vars,
+        [variable]: x === 0 ? 0 : x, // Add explicit handling of zero value
+      };
+    });
+    setValidation((prev) => {
+      return {
+        ...prev,
+        validated:
+          settings?.type == "number"
+            ? !isNaN(x as number)
+            : !!(x as string).trim(),
+      };
+    });
+  };
+  const handleValueChange = (newValue: number) => {
+    const convertedValue = newValue * 1024; // Always convert GiB to MiB
+    setValue(convertedValue);
+  };
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    let inputValue = parseFloat(e.target.value);
+    if (inputValue > maxValue) {
+      inputValue = maxValue;
+    }
+    handleValueChange(inputValue);
+    updatePopupPosition();
+  };
+  const curValue =
+    variables[variable] !== undefined && variables[variable] !== null
+      ? variables[variable]
+      : "";
+  const displayValue = curValue / 1024;
+
+  const showUnitToggle = label === "RAM";
+
+  return (
+    <InputSliderWrapper>
+      <Label htmlFor="input-slider">{label}</Label>
+      <InputSliderContainer>
+        <RangeInput
+          type="range"
+          id="input-slider"
+          value={displayValue}
+          onChange={handleChange}
+          onMouseMove={handleMouseMove}
+          onMouseEnter={handleMouseEnter}
+          onMouseLeave={handleMouseLeave}
+          ref={rangeInputRef}
+          min={0}
+          max={maxValue}
+          step={stepValue}
+        />
+        {/* Display the popup with the current value if showPopup is true */}
+        {showPopup && (
+          <Popup style={popupPosition}>{displayValue.toFixed(2)}</Popup>
+        )}
+        <NumberInput
+          type="number"
+          value={displayValue}
+          onChange={handleChange}
+          min={0}
+          max={maxValue}
+          step={stepValue}
+        />
+        {
+          <>
+            {showUnitToggle ? (
+              <UnitText> GiB</UnitText>
+            ) : (
+              <UnitText> {props.settings.unit}</UnitText>
+            )}
+          </>
+        }
+      </InputSliderContainer>
+    </InputSliderWrapper>
+  );
+};
+
+export const getFinalVariablesForStringInput: GetFinalVariablesFunction = (
+  vars,
+  props: InputSliderField
+) => {
+  const val =
+    vars[props.variable] != undefined && vars[props.variable] != null
+      ? vars[props.variable]
+      : hasSetValue(props)
+      ? clipOffUnit(props.settings?.unit, props.value[0])
+      : undefined;
+
+  return {
+    [props.variable]:
+      props.settings?.unit && !props.settings.omitUnitFromValue
+        ? val + props.settings.unit
+        : val,
+  };
+};
+
+export default InputSlider;
+
+const InputSliderWrapper = styled.div`
+  width: 100%;
+  font-family: Arial, sans-serif;
+  margin-bottom: 10px;
+`;
+
+const Label = styled.label`
+  color: #ffffff;
+  margin-bottom: 5px;
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  font-family: "Work Sans", sans-serif;
+`;
+
+const InputSliderContainer = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const RangeInput = styled.input`
+  flex-grow: 1;
+  margin-right: 1%;
+`;
+
+const NumberInput = styled.input`
+  outline: none;
+  border: none;
+  font-size: 16px;
+  background: ffffff05;
+  cursor: ${(props) => (props.disabled ? "not-allowed" : "")};
+  width: 6%;
+  color: ${(props) => (props.disabled ? "#ffffff44" : "white")};
+  height: 35px;
+`;
+
+const UnitText = styled.span`
+  padding: 0 10px;
+  font-size: 16px;
+
+  background: #ffffff05;
+  height: 35px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-left: solid #ffffff55;
+`;
+
+const MinLabel = styled.span`
+  font-size: 12px;
+  margin-right: 5px;
+`;
+
+const MaxLabel = styled.span`
+  font-size: 12px;
+  margin-left: 5px;
+`;
+
+const Popup = styled.div`
+  position: absolute;
+  background-color: #ffffff;
+  color: #000000;
+  border-radius: 5px;
+  padding: 5px;
+  font-size: 12px;
+  transform: translate(-50%, -100%);
+  pointer-events: none;
+`;

+ 8 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -984,6 +984,9 @@ const ExpandedChart: React.FC<Props> = (props) => {
                       {(isPreview || leftTabOptions.length > 0) && (
                         <BodyWrapper>
                           <PorterFormWrapper
+                            isCapiEnabled={
+                              currentProject.capi_provisioner_enabled
+                            }
                             formData={cloneDeep(currentChart.form)}
                             valuesToOverride={{
                               namespace: props.namespace,
@@ -1157,7 +1160,11 @@ const TabButton = styled.div`
   position: absolute;
   right: 0px;
   height: 30px;
-  background: linear-gradient(to right, #00000000, ${props => props.theme.bg} 20%);
+  background: linear-gradient(
+    to right,
+    #00000000,
+    ${(props) => props.theme.bg} 20%
+  );
   padding-left: 30px;
   display: flex;
   align-items: center;

+ 6 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -358,6 +358,7 @@ export const ExpandedJobChartFC: React.FC<{
             formData.tabs?.length > 0 ||
             rightTabOptions?.length > 0) && (
             <PorterFormWrapper
+              isCapiEnabled={currentProject.capi_provisioner_enabled}
               formData={formData}
               valuesToOverride={{
                 namespace: chart?.namespace,
@@ -752,7 +753,11 @@ const TabButton = styled.div`
   position: absolute;
   right: 0px;
   height: 30px;
-  background: linear-gradient(to right, #00000000, ${props => props.theme.bg} 20%);
+  background: linear-gradient(
+    to right,
+    #00000000,
+    ${(props) => props.theme.bg} 20%
+  );
   padding-left: 30px;
   display: flex;
   align-items: center;

+ 1 - 0
dashboard/src/main/home/cluster-dashboard/stacks/components/NewAppResourceForm.tsx

@@ -274,6 +274,7 @@ const NewAppResourceForm = (props: {
         <Heading>Application Settings</Heading>
         <Helper>Configure settings for this application.</Helper>
         <PorterFormWrapper
+          isCapiEnabled={currentProject.capi_provisioner_enabled}
           formData={template.form}
           onSubmit={handleSubmit}
           isLaunch

+ 1 - 0
dashboard/src/main/home/infrastructure/ExpandedInfra.tsx

@@ -164,6 +164,7 @@ const ExpandedInfra: React.FunctionComponent = () => {
       />
       <PorterFormContainer>
         <PorterFormWrapper
+          isCapiEnabled={currentProject.capi_provisioner_enabled}
           showStateDebugger={false}
           formData={infraForm}
           valuesToOverride={{}}

+ 1 - 0
dashboard/src/main/home/infrastructure/components/ExpandedOperation.tsx

@@ -297,6 +297,7 @@ const ExpandedOperation: React.FunctionComponent<Props> = ({
         </Description>
         <PorterFormContainer>
           <PorterFormWrapper
+            isCapiEnabled={currentProject.capi_provisioner_enabled}
             showStateDebugger={false}
             formData={operation.form}
             valuesToOverride={{}}

+ 1 - 0
dashboard/src/main/home/infrastructure/components/ProvisionInfra.tsx

@@ -288,6 +288,7 @@ const ProvisionInfra: React.FunctionComponent<Props> = () => {
         </Heading>
         <FormContainer>
           <PorterFormWrapper
+            isCapiEnabled={currentProject.capi_provisioner_enabled}
             showStateDebugger={false}
             formData={currentTemplate.form}
             valuesToOverride={{}}

+ 9 - 3
dashboard/src/main/home/launch/launch-flow/SettingsPage.tsx

@@ -74,7 +74,10 @@ class SettingsPage extends Component<PropsType, StateType> {
         let clusterOptions: { label: string; value: string }[] = [];
         let clusterMap: { [clusterId: string]: ClusterType } = {};
         res.data.forEach((cluster: ClusterType, i: number) => {
-          clusterOptions.push({ label: cluster.vanity_name || cluster.name, value: cluster.name });
+          clusterOptions.push({
+            label: cluster.vanity_name || cluster.name,
+            value: cluster.name,
+          });
           clusterMap[cluster.name] = cluster;
         });
         if (res.data.length > 0) {
@@ -139,6 +142,7 @@ class SettingsPage extends Component<PropsType, StateType> {
             Configure application settings for this template. (Optional)
           </Helper>
           <PorterFormWrapper
+            isCapiEnabled={this.context.currentProject.capi_provisioner_enabled}
             formData={form}
             saveValuesStatus={saveValuesStatus}
             valuesToOverride={{
@@ -262,7 +266,9 @@ class SettingsPage extends Component<PropsType, StateType> {
                 <Selector
                   activeValue={selectedCluster}
                   setActiveValue={(cluster: string) => {
-                    this.context.setCurrentCluster(this.state.clusterMap[cluster]);
+                    this.context.setCurrentCluster(
+                      this.state.clusterMap[cluster]
+                    );
                     this.updateNamespaces(this.state.clusterMap[cluster].id);
                     this.setState({
                       selectedCluster: cluster,
@@ -346,7 +352,7 @@ const BackButton = styled.div`
   border-radius: 100px;
   width: ${(props: { width: string }) => props.width};
   color: white;
-  background: ${props => props.theme.fg};
+  background: ${(props) => props.theme.fg};
 
   :hover {
     background: #ffffff22;