|  | 
|  | 1 | +import { useState } from "react"; | 
|  | 2 | +import { Button } from "@/components/ui/button"; | 
|  | 3 | +import { Input } from "@/components/ui/input"; | 
|  | 4 | +import { Textarea } from "@/components/ui/textarea"; | 
|  | 5 | +import { Switch } from "@/components/ui/switch"; | 
|  | 6 | +import { Plus, Trash2, Eye, EyeOff } from "lucide-react"; | 
|  | 7 | +import { | 
|  | 8 | +  CustomHeaders as CustomHeadersType, | 
|  | 9 | +  CustomHeader, | 
|  | 10 | +  createEmptyHeader, | 
|  | 11 | +} from "@/lib/types/customHeaders"; | 
|  | 12 | + | 
|  | 13 | +interface CustomHeadersProps { | 
|  | 14 | +  headers: CustomHeadersType; | 
|  | 15 | +  onChange: (headers: CustomHeadersType) => void; | 
|  | 16 | +  className?: string; | 
|  | 17 | +} | 
|  | 18 | + | 
|  | 19 | +const CustomHeaders = ({ | 
|  | 20 | +  headers, | 
|  | 21 | +  onChange, | 
|  | 22 | +  className, | 
|  | 23 | +}: CustomHeadersProps) => { | 
|  | 24 | +  const [isJsonMode, setIsJsonMode] = useState(false); | 
|  | 25 | +  const [jsonValue, setJsonValue] = useState(""); | 
|  | 26 | +  const [jsonError, setJsonError] = useState<string | null>(null); | 
|  | 27 | +  const [visibleValues, setVisibleValues] = useState<Set<number>>(new Set()); | 
|  | 28 | + | 
|  | 29 | +  const updateHeader = ( | 
|  | 30 | +    index: number, | 
|  | 31 | +    field: keyof CustomHeader, | 
|  | 32 | +    value: string | boolean, | 
|  | 33 | +  ) => { | 
|  | 34 | +    const newHeaders = [...headers]; | 
|  | 35 | +    newHeaders[index] = { ...newHeaders[index], [field]: value }; | 
|  | 36 | +    onChange(newHeaders); | 
|  | 37 | +  }; | 
|  | 38 | + | 
|  | 39 | +  const addHeader = () => { | 
|  | 40 | +    onChange([...headers, createEmptyHeader()]); | 
|  | 41 | +  }; | 
|  | 42 | + | 
|  | 43 | +  const removeHeader = (index: number) => { | 
|  | 44 | +    const newHeaders = headers.filter((_, i) => i !== index); | 
|  | 45 | +    onChange(newHeaders); | 
|  | 46 | +  }; | 
|  | 47 | + | 
|  | 48 | +  const toggleValueVisibility = (index: number) => { | 
|  | 49 | +    const newVisible = new Set(visibleValues); | 
|  | 50 | +    if (newVisible.has(index)) { | 
|  | 51 | +      newVisible.delete(index); | 
|  | 52 | +    } else { | 
|  | 53 | +      newVisible.add(index); | 
|  | 54 | +    } | 
|  | 55 | +    setVisibleValues(newVisible); | 
|  | 56 | +  }; | 
|  | 57 | + | 
|  | 58 | +  const switchToJsonMode = () => { | 
|  | 59 | +    const jsonObject: Record<string, string> = {}; | 
|  | 60 | +    headers.forEach((header) => { | 
|  | 61 | +      if (header.enabled && header.name.trim() && header.value.trim()) { | 
|  | 62 | +        jsonObject[header.name.trim()] = header.value.trim(); | 
|  | 63 | +      } | 
|  | 64 | +    }); | 
|  | 65 | +    setJsonValue(JSON.stringify(jsonObject, null, 2)); | 
|  | 66 | +    setJsonError(null); | 
|  | 67 | +    setIsJsonMode(true); | 
|  | 68 | +  }; | 
|  | 69 | + | 
|  | 70 | +  const switchToFormMode = () => { | 
|  | 71 | +    try { | 
|  | 72 | +      const parsed = JSON.parse(jsonValue); | 
|  | 73 | +      if ( | 
|  | 74 | +        typeof parsed !== "object" || | 
|  | 75 | +        parsed === null || | 
|  | 76 | +        Array.isArray(parsed) | 
|  | 77 | +      ) { | 
|  | 78 | +        setJsonError("JSON must be an object with string key-value pairs"); | 
|  | 79 | +        return; | 
|  | 80 | +      } | 
|  | 81 | + | 
|  | 82 | +      const newHeaders: CustomHeadersType = Object.entries(parsed).map( | 
|  | 83 | +        ([name, value]) => ({ | 
|  | 84 | +          name, | 
|  | 85 | +          value: String(value), | 
|  | 86 | +          enabled: true, | 
|  | 87 | +        }), | 
|  | 88 | +      ); | 
|  | 89 | + | 
|  | 90 | +      onChange(newHeaders); | 
|  | 91 | +      setJsonError(null); | 
|  | 92 | +      setIsJsonMode(false); | 
|  | 93 | +    } catch { | 
|  | 94 | +      setJsonError("Invalid JSON format"); | 
|  | 95 | +    } | 
|  | 96 | +  }; | 
|  | 97 | + | 
|  | 98 | +  const handleJsonChange = (value: string) => { | 
|  | 99 | +    setJsonValue(value); | 
|  | 100 | +    setJsonError(null); | 
|  | 101 | +  }; | 
|  | 102 | + | 
|  | 103 | +  if (isJsonMode) { | 
|  | 104 | +    return ( | 
|  | 105 | +      <div className={`space-y-3 ${className}`}> | 
|  | 106 | +        <div className="flex justify-between items-center gap-2"> | 
|  | 107 | +          <h4 className="text-sm font-semibold flex-shrink-0"> | 
|  | 108 | +            Custom Headers (JSON) | 
|  | 109 | +          </h4> | 
|  | 110 | +          <Button | 
|  | 111 | +            type="button" | 
|  | 112 | +            variant="outline" | 
|  | 113 | +            size="sm" | 
|  | 114 | +            onClick={switchToFormMode} | 
|  | 115 | +            className="flex-shrink-0" | 
|  | 116 | +          > | 
|  | 117 | +            Switch to Form | 
|  | 118 | +          </Button> | 
|  | 119 | +        </div> | 
|  | 120 | +        <div className="space-y-2"> | 
|  | 121 | +          <Textarea | 
|  | 122 | +            value={jsonValue} | 
|  | 123 | +            onChange={(e) => handleJsonChange(e.target.value)} | 
|  | 124 | +            placeholder='{\n  "Authorization": "Bearer token123",\n  "X-Tenant-ID": "acme-inc",\n  "X-Environment": "staging"\n}' | 
|  | 125 | +            className="font-mono text-sm min-h-[100px] resize-none" | 
|  | 126 | +          /> | 
|  | 127 | +          {jsonError && <p className="text-sm text-red-600">{jsonError}</p>} | 
|  | 128 | +          <p className="text-xs text-muted-foreground"> | 
|  | 129 | +            Enter headers as a JSON object with string key-value pairs. | 
|  | 130 | +          </p> | 
|  | 131 | +        </div> | 
|  | 132 | +      </div> | 
|  | 133 | +    ); | 
|  | 134 | +  } | 
|  | 135 | + | 
|  | 136 | +  return ( | 
|  | 137 | +    <div className={`space-y-3 ${className}`}> | 
|  | 138 | +      <div className="flex justify-between items-center gap-2"> | 
|  | 139 | +        <h4 className="text-sm font-semibold flex-shrink-0">Custom Headers</h4> | 
|  | 140 | +        <div className="flex gap-1 flex-shrink-0"> | 
|  | 141 | +          <Button | 
|  | 142 | +            type="button" | 
|  | 143 | +            variant="outline" | 
|  | 144 | +            size="sm" | 
|  | 145 | +            onClick={switchToJsonMode} | 
|  | 146 | +            className="text-xs px-2" | 
|  | 147 | +          > | 
|  | 148 | +            JSON | 
|  | 149 | +          </Button> | 
|  | 150 | +          <Button | 
|  | 151 | +            type="button" | 
|  | 152 | +            variant="outline" | 
|  | 153 | +            size="sm" | 
|  | 154 | +            onClick={addHeader} | 
|  | 155 | +            className="text-xs px-2" | 
|  | 156 | +            data-testid="add-header-button" | 
|  | 157 | +          > | 
|  | 158 | +            <Plus className="w-3 h-3 mr-1" /> | 
|  | 159 | +            Add | 
|  | 160 | +          </Button> | 
|  | 161 | +        </div> | 
|  | 162 | +      </div> | 
|  | 163 | + | 
|  | 164 | +      {headers.length === 0 ? ( | 
|  | 165 | +        <div className="text-center py-4 text-muted-foreground"> | 
|  | 166 | +          <p className="text-sm">No custom headers configured</p> | 
|  | 167 | +          <p className="text-xs mt-1">Click "Add" to get started</p> | 
|  | 168 | +        </div> | 
|  | 169 | +      ) : ( | 
|  | 170 | +        <div className="space-y-2 max-h-[300px] overflow-y-auto"> | 
|  | 171 | +          {headers.map((header, index) => ( | 
|  | 172 | +            <div | 
|  | 173 | +              key={index} | 
|  | 174 | +              className="flex items-start gap-2 p-2 border rounded-md" | 
|  | 175 | +            > | 
|  | 176 | +              <Switch | 
|  | 177 | +                checked={header.enabled} | 
|  | 178 | +                onCheckedChange={(enabled) => | 
|  | 179 | +                  updateHeader(index, "enabled", enabled) | 
|  | 180 | +                } | 
|  | 181 | +                className="shrink-0 mt-2" | 
|  | 182 | +              /> | 
|  | 183 | +              <div className="flex-1 min-w-0 space-y-2"> | 
|  | 184 | +                <Input | 
|  | 185 | +                  placeholder="Header Name" | 
|  | 186 | +                  value={header.name} | 
|  | 187 | +                  onChange={(e) => updateHeader(index, "name", e.target.value)} | 
|  | 188 | +                  className="font-mono text-xs" | 
|  | 189 | +                  data-testid={`header-name-input-${index}`} | 
|  | 190 | +                /> | 
|  | 191 | +                <div className="relative"> | 
|  | 192 | +                  <Input | 
|  | 193 | +                    placeholder="Header Value" | 
|  | 194 | +                    value={header.value} | 
|  | 195 | +                    onChange={(e) => | 
|  | 196 | +                      updateHeader(index, "value", e.target.value) | 
|  | 197 | +                    } | 
|  | 198 | +                    type={visibleValues.has(index) ? "text" : "password"} | 
|  | 199 | +                    className="font-mono text-xs pr-8" | 
|  | 200 | +                    data-testid={`header-value-input-${index}`} | 
|  | 201 | +                  /> | 
|  | 202 | +                  <Button | 
|  | 203 | +                    type="button" | 
|  | 204 | +                    variant="ghost" | 
|  | 205 | +                    size="sm" | 
|  | 206 | +                    onClick={() => toggleValueVisibility(index)} | 
|  | 207 | +                    className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0" | 
|  | 208 | +                  > | 
|  | 209 | +                    {visibleValues.has(index) ? ( | 
|  | 210 | +                      <EyeOff className="w-3 h-3" /> | 
|  | 211 | +                    ) : ( | 
|  | 212 | +                      <Eye className="w-3 h-3" /> | 
|  | 213 | +                    )} | 
|  | 214 | +                  </Button> | 
|  | 215 | +                </div> | 
|  | 216 | +              </div> | 
|  | 217 | +              <Button | 
|  | 218 | +                type="button" | 
|  | 219 | +                variant="ghost" | 
|  | 220 | +                size="sm" | 
|  | 221 | +                onClick={() => removeHeader(index)} | 
|  | 222 | +                className="shrink-0 text-red-600 hover:text-red-700 hover:bg-red-50 h-6 w-6 p-0 mt-2" | 
|  | 223 | +              > | 
|  | 224 | +                <Trash2 className="w-3 h-3" /> | 
|  | 225 | +              </Button> | 
|  | 226 | +            </div> | 
|  | 227 | +          ))} | 
|  | 228 | +        </div> | 
|  | 229 | +      )} | 
|  | 230 | + | 
|  | 231 | +      {headers.length > 0 && ( | 
|  | 232 | +        <p className="text-xs text-muted-foreground"> | 
|  | 233 | +          Use the toggle to enable/disable headers. Only enabled headers with | 
|  | 234 | +          both name and value will be sent. | 
|  | 235 | +        </p> | 
|  | 236 | +      )} | 
|  | 237 | +    </div> | 
|  | 238 | +  ); | 
|  | 239 | +}; | 
|  | 240 | + | 
|  | 241 | +export default CustomHeaders; | 
0 commit comments