|  | 
|  | 1 | +import { FC, memo, useCallback, useEffect } from 'react'; | 
|  | 2 | +import { Controller, useFieldArray, useFormContext } from 'react-hook-form'; | 
|  | 3 | +import { useIntl } from 'react-intl'; | 
|  | 4 | +import { messages } from '../../lang/messages'; | 
|  | 5 | +import { Select } from '../Select'; | 
|  | 6 | +import { useCurrentEnvironment } from '../../modules/me'; | 
|  | 7 | +import { AppDispatch } from '../../store'; | 
|  | 8 | +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; | 
|  | 9 | +import { | 
|  | 10 | +  listFeatures, | 
|  | 11 | +  selectAll as selectAllFeatures | 
|  | 12 | +} from '../../modules/features'; | 
|  | 13 | +import { ListFeaturesRequest } from '../../proto/feature/service_pb'; | 
|  | 14 | +import { AppState } from '../../modules'; | 
|  | 15 | +import { Feature } from '../../proto/feature/feature_pb'; | 
|  | 16 | +import { PlusIcon, TrashIcon } from '@heroicons/react/outline'; | 
|  | 17 | +import { v4 as uuid } from 'uuid'; | 
|  | 18 | + | 
|  | 19 | +export interface DebuggerEvaluateFormProps { | 
|  | 20 | +  onSubmit: () => void; | 
|  | 21 | +} | 
|  | 22 | + | 
|  | 23 | +export const DebuggerEvaluateForm: FC<DebuggerEvaluateFormProps> = memo( | 
|  | 24 | +  ({ onSubmit }) => { | 
|  | 25 | +    const currentEnvironment = useCurrentEnvironment(); | 
|  | 26 | +    const dispatch = useDispatch<AppDispatch>(); | 
|  | 27 | +    const { formatMessage: f } = useIntl(); | 
|  | 28 | + | 
|  | 29 | +    const methods = useFormContext(); | 
|  | 30 | +    const { | 
|  | 31 | +      register, | 
|  | 32 | +      control, | 
|  | 33 | +      formState: { errors, isValid, isSubmitting } | 
|  | 34 | +    } = methods; | 
|  | 35 | + | 
|  | 36 | +    const { | 
|  | 37 | +      append: appendUserAttributes, | 
|  | 38 | +      remove: removeUserAttribute, | 
|  | 39 | +      fields: userAttributes | 
|  | 40 | +    } = useFieldArray({ | 
|  | 41 | +      control, | 
|  | 42 | +      name: 'userAttributes' | 
|  | 43 | +    }); | 
|  | 44 | + | 
|  | 45 | +    const features = useSelector<AppState, Feature.AsObject[]>( | 
|  | 46 | +      (state) => selectAllFeatures(state.features), | 
|  | 47 | +      shallowEqual | 
|  | 48 | +    ); | 
|  | 49 | + | 
|  | 50 | +    useEffect(() => { | 
|  | 51 | +      dispatch( | 
|  | 52 | +        listFeatures({ | 
|  | 53 | +          environmentNamespace: currentEnvironment.id, | 
|  | 54 | +          pageSize: 0, | 
|  | 55 | +          cursor: '', | 
|  | 56 | +          tags: [], | 
|  | 57 | +          searchKeyword: null, | 
|  | 58 | +          maintainerId: null, | 
|  | 59 | +          orderBy: ListFeaturesRequest.OrderBy.DEFAULT, | 
|  | 60 | +          orderDirection: ListFeaturesRequest.OrderDirection.ASC, | 
|  | 61 | +          archived: false | 
|  | 62 | +        }) | 
|  | 63 | +      ); | 
|  | 64 | +    }, []); | 
|  | 65 | + | 
|  | 66 | +    const handleAddAttribute = () => { | 
|  | 67 | +      appendUserAttributes({ | 
|  | 68 | +        id: uuid(), | 
|  | 69 | +        key: '', | 
|  | 70 | +        value: '' | 
|  | 71 | +      }); | 
|  | 72 | +    }; | 
|  | 73 | + | 
|  | 74 | +    const handleDeleteAttribute = useCallback((index) => { | 
|  | 75 | +      removeUserAttribute(index); | 
|  | 76 | +    }, []); | 
|  | 77 | + | 
|  | 78 | +    return ( | 
|  | 79 | +      <form className="flex flex-col space-y-6"> | 
|  | 80 | +        <div className=""> | 
|  | 81 | +          <label htmlFor="flag"> | 
|  | 82 | +            <span className="input-label">Flag</span> | 
|  | 83 | +          </label> | 
|  | 84 | +          <Controller | 
|  | 85 | +            name="flag" | 
|  | 86 | +            control={control} | 
|  | 87 | +            render={({ field }) => { | 
|  | 88 | +              const selectedOptions = features | 
|  | 89 | +                .filter((feature) => field.value.includes(feature.id)) | 
|  | 90 | +                .map((feature) => ({ | 
|  | 91 | +                  label: feature.name, | 
|  | 92 | +                  value: feature.id | 
|  | 93 | +                })); | 
|  | 94 | + | 
|  | 95 | +              return ( | 
|  | 96 | +                <Select | 
|  | 97 | +                  isMulti | 
|  | 98 | +                  options={features.map((feature) => ({ | 
|  | 99 | +                    label: feature.name, | 
|  | 100 | +                    value: feature.id | 
|  | 101 | +                  }))} | 
|  | 102 | +                  value={selectedOptions} // Ensure the value prop is set correctly | 
|  | 103 | +                  onChange={(selected) => { | 
|  | 104 | +                    const newValues = selected | 
|  | 105 | +                      ? selected.map((o) => o.value) | 
|  | 106 | +                      : []; | 
|  | 107 | +                    field.onChange(newValues); | 
|  | 108 | +                  }} | 
|  | 109 | +                  placeholder="Select a feature flag" | 
|  | 110 | +                  disabled={isSubmitting} | 
|  | 111 | +                /> | 
|  | 112 | +              ); | 
|  | 113 | +            }} | 
|  | 114 | +          /> | 
|  | 115 | +          <p className="input-error"> | 
|  | 116 | +            {errors.flag && <span role="alert">{errors.flag.message}</span>} | 
|  | 117 | +          </p> | 
|  | 118 | +        </div> | 
|  | 119 | +        <div className=""> | 
|  | 120 | +          <label htmlFor="userId"> | 
|  | 121 | +            <span className="input-label">User ID</span> | 
|  | 122 | +          </label> | 
|  | 123 | +          <div className="mt-1"> | 
|  | 124 | +            <input | 
|  | 125 | +              {...register('userId')} | 
|  | 126 | +              placeholder="Enter a user ID" | 
|  | 127 | +              type="text" | 
|  | 128 | +              id="userId" | 
|  | 129 | +              className="input-text w-full" | 
|  | 130 | +              disabled={isSubmitting} | 
|  | 131 | +            /> | 
|  | 132 | +            <p className="input-error"> | 
|  | 133 | +              {errors.userId && ( | 
|  | 134 | +                <span role="alert">{errors.userId.message}</span> | 
|  | 135 | +              )} | 
|  | 136 | +            </p> | 
|  | 137 | +          </div> | 
|  | 138 | +        </div> | 
|  | 139 | +        <div className="space-y-4"> | 
|  | 140 | +          {userAttributes.length > 0 && ( | 
|  | 141 | +            <div> | 
|  | 142 | +              <div> | 
|  | 143 | +                <p>User Attributes</p> | 
|  | 144 | +                <p className="text-sm"> | 
|  | 145 | +                  <a | 
|  | 146 | +                    href="https://docs.bucketeer.io/feature-flags/creating-feature-flags/targeting#user-attributes" | 
|  | 147 | +                    target="_blank" | 
|  | 148 | +                    rel="noreferrer" | 
|  | 149 | +                    className="link" | 
|  | 150 | +                  > | 
|  | 151 | +                    {f(messages.readMore)} | 
|  | 152 | +                  </a> | 
|  | 153 | +                </p> | 
|  | 154 | +              </div> | 
|  | 155 | +              <div> | 
|  | 156 | +                {userAttributes.map((attr, index) => ( | 
|  | 157 | +                  <div key={attr.id} className="flex space-x-4 mt-4 items-end"> | 
|  | 158 | +                    <div className="flex flex-col flex-1"> | 
|  | 159 | +                      <label htmlFor="key"> | 
|  | 160 | +                        <span className="input-label">Key</span> | 
|  | 161 | +                      </label> | 
|  | 162 | +                      <input | 
|  | 163 | +                        {...register(`userAttributes.${index}.key`)} | 
|  | 164 | +                        type="text" | 
|  | 165 | +                        id="key" | 
|  | 166 | +                        className="input-text w-full" | 
|  | 167 | +                      /> | 
|  | 168 | +                    </div> | 
|  | 169 | +                    <div className="flex flex-col flex-1"> | 
|  | 170 | +                      <label htmlFor="value"> | 
|  | 171 | +                        <span className="input-label">Value</span> | 
|  | 172 | +                      </label> | 
|  | 173 | +                      <input | 
|  | 174 | +                        {...register(`userAttributes.${index}.value`)} | 
|  | 175 | +                        type="text" | 
|  | 176 | +                        id="value" | 
|  | 177 | +                        className="input-text w-full" | 
|  | 178 | +                      /> | 
|  | 179 | +                    </div> | 
|  | 180 | +                    <TrashIcon | 
|  | 181 | +                      width={18} | 
|  | 182 | +                      className="cursor-pointer text-gray-400 mb-3" | 
|  | 183 | +                      onClick={() => handleDeleteAttribute(index)} | 
|  | 184 | +                    /> | 
|  | 185 | +                  </div> | 
|  | 186 | +                ))} | 
|  | 187 | +              </div> | 
|  | 188 | +            </div> | 
|  | 189 | +          )} | 
|  | 190 | +          <button | 
|  | 191 | +            className="flex whitespace-nowrap space-x-2 text-primary max-w-min py-2 items-center" | 
|  | 192 | +            type="button" | 
|  | 193 | +            onClick={handleAddAttribute} | 
|  | 194 | +          > | 
|  | 195 | +            <PlusIcon width={18} /> | 
|  | 196 | +            <span>Add a user attribute</span> | 
|  | 197 | +          </button> | 
|  | 198 | +        </div> | 
|  | 199 | +        <div className="flex"> | 
|  | 200 | +          <button | 
|  | 201 | +            type="button" | 
|  | 202 | +            className="btn-submit" | 
|  | 203 | +            disabled={!isValid || isSubmitting} | 
|  | 204 | +            onClick={onSubmit} | 
|  | 205 | +          > | 
|  | 206 | +            Evaluate | 
|  | 207 | +          </button> | 
|  | 208 | +        </div> | 
|  | 209 | +      </form> | 
|  | 210 | +    ); | 
|  | 211 | +  } | 
|  | 212 | +); | 
0 commit comments