From 04422e98eb199fe523cb603d14f6f72203c5f8d7 Mon Sep 17 00:00:00 2001 From: jusrhee Date: Fri, 29 Sep 2023 14:21:13 -0400 Subject: [PATCH] Add form support for raw dictionary input and array of raw dictionary inputs (#3691) Co-authored-by: Justin Rhee --- .../src/components/porter-form/PorterForm.tsx | 8 + .../field-components/Dictionary.tsx | 67 ++++++ .../field-components/DictionaryArray.tsx | 218 ++++++++++++++++++ dashboard/src/components/porter-form/types.ts | 18 +- .../components/porter/DictionaryEditor.tsx | 160 +++++++++++++ 5 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 dashboard/src/components/porter-form/field-components/Dictionary.tsx create mode 100644 dashboard/src/components/porter-form/field-components/DictionaryArray.tsx create mode 100644 dashboard/src/components/porter/DictionaryEditor.tsx diff --git a/dashboard/src/components/porter-form/PorterForm.tsx b/dashboard/src/components/porter-form/PorterForm.tsx index 90c2cc55ca..ad84869a2a 100644 --- a/dashboard/src/components/porter-form/PorterForm.tsx +++ b/dashboard/src/components/porter-form/PorterForm.tsx @@ -13,6 +13,8 @@ import { ServiceIPListField, TextAreaField, UrlLinkField, + DictionaryField, + DictionaryArrayField, } from "./types"; import TabRegion, { TabOption } from "../TabRegion"; import Heading from "../form-components/Heading"; @@ -32,6 +34,8 @@ import CronInput from "./field-components/CronInput"; import TextAreaInput from "./field-components/TextAreaInput"; import UrlLink from "./field-components/UrlLink"; import Button from "components/porter/Button"; +import DictionaryArray from "./field-components/DictionaryArray"; +import Dictionary from "./field-components/Dictionary"; interface Props { leftTabOptions?: TabOption[]; @@ -88,6 +92,10 @@ const PorterForm: React.FC = (props) => { return {field.label}; case "input": return ; + case "dictionary": + return ; + case "dictionary-array": + return ; case "checkbox": return ; case "key-value-array": diff --git a/dashboard/src/components/porter-form/field-components/Dictionary.tsx b/dashboard/src/components/porter-form/field-components/Dictionary.tsx new file mode 100644 index 0000000000..ef19b6074e --- /dev/null +++ b/dashboard/src/components/porter-form/field-components/Dictionary.tsx @@ -0,0 +1,67 @@ +import React, { useEffect } from "react"; +import InputRow from "../../form-components/InputRow"; +import useFormField from "../hooks/useFormField"; +import { + GetFinalVariablesFunction, + DictionaryField, + DictionaryFieldState, +} from "../types"; +import DictionaryEditor from "components/porter/DictionaryEditor"; +import { hasSetValue } from "../utils"; + +const Dictionary: React.FC = (props) => { + const { + state, + variables, + setVars, + setValidation, + } = useFormField(props.id, { + initValidation: { + validated: hasSetValue(props), + }, + initVars: { + [props.variable]: hasSetValue(props) ? props.value[0] : undefined, + }, + }); + + if (state == undefined) return <>; + + return ( + { + setVars((vars) => { + return { + ...vars, + [props.variable]: x, + }; + }); + setValidation((prev) => { + return { + ...prev, + validated: true, + }; + }); + }} + /> + ); +}; + +export const getFinalVariablesForStringInput: GetFinalVariablesFunction = ( + vars, + props: DictionaryField +) => { + const val = + vars[props.variable] != undefined && vars[props.variable] != null + ? vars[props.variable] : hasSetValue(props) + ? props.value[0] : undefined; + + return { + [props.variable]: + props.settings?.unit && !props.settings.omitUnitFromValue + ? val + props.settings.unit + : val, + }; +}; + +export default Dictionary; diff --git a/dashboard/src/components/porter-form/field-components/DictionaryArray.tsx b/dashboard/src/components/porter-form/field-components/DictionaryArray.tsx new file mode 100644 index 0000000000..b79827e509 --- /dev/null +++ b/dashboard/src/components/porter-form/field-components/DictionaryArray.tsx @@ -0,0 +1,218 @@ +import React from "react"; +import styled from "styled-components"; +import { + DictionaryArrayField, + DictionaryArrayFieldState, + GetFinalVariablesFunction, +} from "../types"; +import useFormField from "../hooks/useFormField"; +import { hasSetValue } from "../utils"; +import DictionaryEditor from "components/porter/DictionaryEditor"; + +// this is used to set validation for the below form component in case +// input validation needs to get more complicated in the future +const validateArray = (arr: any[]) => { + return true; +}; + +const DictionaryArray: React.FC = (props) => { + const { + state, + variables, + setVars, + setValidation, + } = useFormField(props.id, { + initVars: { + [props.variable]: hasSetValue(props) ? props.value[0] : [], + }, + initValidation: { + validated: validateArray(hasSetValue(props) ? props.value[0] : []), + }, + }); + + if (state == undefined) return <>; + + const renderDeleteButton = (values: string[], i: number) => { + if (!props.isReadOnly) { + return ( + { + setVars((prev) => { + const val = prev[props.variable] + .slice(0, i) + .concat(prev[props.variable].slice(i + 1)); + setValidation((prev) => { + return { + ...prev, + validated: validateArray(val), + }; + }); + return { + [props.variable]: val, + }; + }); + }} + > + cancel + + ); + } + }; + + const renderInputList = (values: string[]) => { + return ( + <> + {values.length > 0 && values.map((value: string, i: number) => { + return ( + + { + setVars((prev) => { + const val = prev[props.variable]?.map( + (t: string, j: number) => { + return i == j ? e : t; + } + ); + setValidation((prev) => { + return { + ...prev, + validated: validateArray(val), + }; + }); + return { + [props.variable]: val, + }; + }); + }} + /> + {renderDeleteButton(values, i)} + + ); + })} + + ); + }; + + return ( + + + {variables[props.variable] === 0 ? ( + <> + ) : ( + renderInputList(variables[props.variable]) + )} + { + setVars((prev) => { + return { + [props.variable]: [...prev[props.variable], ""], + }; + }); + }} + > + add Create new entry + + + ); +}; + +export default DictionaryArray; + +export const getFinalVariablesForArrayInput: GetFinalVariablesFunction = ( + vars, + props: DictionaryArrayField +) => { + return vars[props.variable] != undefined && vars[props.variable] != null + ? {} + : { + [props.variable]: hasSetValue(props) ? props.value[0] : [], + }; +}; + +const AddRowButton = styled.div` + display: flex; + align-items: center; + margin-top: 5px; + width: 270px; + font-size: 13px; + color: #aaaabb; + height: 30px; + border-radius: 3px; + cursor: pointer; + background: #ffffff11; + :hover { + background: #ffffff22; + } + + > i { + color: #ffffff44; + font-size: 16px; + margin-left: 8px; + margin-right: 10px; + display: flex; + align-items: center; + justify-content: center; + } +`; + +const DeleteButton = styled.div` + width: 15px; + height: 15px; + display: flex; + align-items: center; + margin-left: 8px; + margin-top: -3px; + justify-content: center; + + > i { + font-size: 17px; + color: #ffffff44; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + :hover { + color: #ffffff88; + } + } +`; + +const InputWrapper = styled.div` + display: flex; + align-items: center; +`; + +const Input = styled.input` + outline: none; + border: none; + margin-bottom: 5px; + font-size: 13px; + background: #ffffff11; + border: 1px solid #ffffff55; + border-radius: 3px; + width: ${(props: { disabled?: boolean; width: string }) => + props.width ? props.width : "270px"}; + color: ${(props: { disabled?: boolean; width: string }) => + props.disabled ? "#ffffff44" : "white"}; + padding: 5px 10px; + height: 35px; +`; + +const Label = styled.div` + color: #ffffff; + margin-bottom: 10px; +`; + +const StyledInputArray = styled.div` + margin-bottom: 15px; + margin-top: 22px; +`; + +const Required = styled.span` + color: #fc4976; +`; diff --git a/dashboard/src/components/porter-form/types.ts b/dashboard/src/components/porter-form/types.ts index c4a74714d9..01e410b3ce 100644 --- a/dashboard/src/components/porter-form/types.ts +++ b/dashboard/src/components/porter-form/types.ts @@ -99,6 +99,16 @@ export interface ArrayInputField extends GenericInputField { label?: string; } +export interface DictionaryField extends GenericInputField { + type: "dictionary"; + label?: string; +} + +export interface DictionaryArrayField extends GenericInputField { + type: "dictionary-array"; + label?: string; +} + export interface SelectField extends GenericInputField { type: "select"; settings: @@ -168,6 +178,8 @@ export type FormField = | VariableField | CronField | TextAreaField + | DictionaryField + | DictionaryArrayField | UrlLinkField; export interface ShowIfAnd { @@ -256,6 +268,8 @@ export interface KeyValueArrayFieldState { synced_env_groups: PopulatedEnvGroup[]; } export interface ArrayInputFieldState { } +export interface DictionaryFieldState {} +export interface DictionaryArrayFieldState { } export interface SelectFieldState { } export type PorterFormFieldFieldState = @@ -263,6 +277,8 @@ export type PorterFormFieldFieldState = | CheckboxFieldState | KeyValueArrayField | ArrayInputFieldState + | DictionaryFieldState + | DictionaryArrayFieldState | SelectFieldState; // reducer interfaces @@ -324,7 +340,7 @@ export type PorterFormAction = export type GetFinalVariablesFunction = ( vars: PorterFormVariableList, - props: FormField, + props: FormField | any, state: PorterFormFieldFieldState, context: Partial ) => PorterFormVariableList; diff --git a/dashboard/src/components/porter/DictionaryEditor.tsx b/dashboard/src/components/porter/DictionaryEditor.tsx new file mode 100644 index 0000000000..ab6f836b00 --- /dev/null +++ b/dashboard/src/components/porter/DictionaryEditor.tsx @@ -0,0 +1,160 @@ +import { ok } from "assert"; +import React, { useEffect, useState } from "react"; +import styled from "styled-components"; +import Container from "./Container"; +import Text from "./Text"; +import Spacer from "./Spacer"; + +type Props = { + value: any; + onChange: any; +}; + +const DictionaryEditor: React.FC = ({ + value, + onChange, +}) => { + const [rawEditor, setRawEditor] = useState(true); + const [savedValue, setSavedValue] = useState(JSON.stringify(value, null, 2)); + const [rawValue, setRawValue] = useState(JSON.stringify(value, null, 2)); + const [changesNotSaved, setChangesNotSaved] = useState(false); + const [isValidJSON, setIsValidJSON] = useState(true); + + useEffect(() => { + setSavedValue(JSON.stringify(value, null, 2)); + }, [value]); + + useEffect(() => { + if (rawValue !== savedValue) { + setChangesNotSaved(true); + } else { + setChangesNotSaved(false); + setIsValidJSON(true); + } + }, [rawValue]); + + return ( + <> + {rawEditor ? ( +
+