184 lines · 6.6 KB · TypeScript
RawDownload
import { PlusIcon, TriangleAlertIcon, XIcon } from "lucide-react"; import {  emptyPlacement,  PlacementLine,  type Carrier,  type Placement,} from "../lib/auth-placements";import { Button } from "./button";import { Input } from "./input";import { Label } from "./label"; // ---------------------------------------------------------------------------// Placement editor — compact cards for where an API key is sent.//// Each card uses a segmented Header/Query choice, labeled name/prefix fields,// and an inline preview of the rendered credential placement.// --------------------------------------------------------------------------- const PLACEMENT_PRESETS: readonly {  readonly label: string;  readonly placements: readonly Placement[];}[] = [  {    label: "Bearer header",    placements: [{ carrier: "header", name: "Authorization", prefix: "Bearer " }],  },  {    label: "API key header",    placements: [{ carrier: "header", name: "X-API-Key", prefix: "" }],  },  {    label: "API key query",    placements: [{ carrier: "query", name: "api_key", prefix: "" }],  },]; export function PlacementEditor(props: {  readonly placements: readonly Placement[];  readonly onChange: (placements: Placement[]) => void;}) {  const { placements, onChange } = props;   const set = (index: number, patch: Partial<Placement>): void =>    onChange(placements.map((p: Placement, j: number) => (j === index ? { ...p, ...patch } : p)));   const remove = (index: number): void =>    onChange(placements.filter((_p: Placement, j: number) => j !== index));   // A non-empty prefix with no trailing space is sent JOINED to the credential  // (e.g. "Bearer" + token -> "Bearertoken"). Almost always a mistake: warn.  const barePrefix = placements.some((p) => p.prefix.length > 0 && !p.prefix.endsWith(" "));   return (    <div className="flex flex-col gap-3">      <div className="flex flex-wrap gap-1.5">        {PLACEMENT_PRESETS.map((preset) => (          <Button            key={preset.label}            type="button"            variant="outline"            size="sm"            className="h-7 rounded-full px-2.5 text-xs"            onClick={() => onChange(preset.placements.map((placement) => ({ ...placement })))}          >            {preset.label}          </Button>        ))}      </div>       {placements.map((placement: Placement, index: number) => (        <div          key={index}          className="rounded-md border border-border/70 bg-background p-3.5 shadow-xs"        >          <div className="flex items-start justify-between gap-3">            <p className="min-w-0 text-xs text-muted-foreground">              Send the value in a request header or query string.            </p>            {placements.length > 1 && (              <Button                type="button"                variant="ghost"                size="icon-sm"                aria-label="Remove location"                className="shrink-0 text-muted-foreground hover:text-foreground"                onClick={() => remove(index)}              >                <XIcon />              </Button>            )}          </div>           <div className="mt-3 grid grid-cols-2 gap-1 rounded-md border border-border/60 bg-muted/30 p-1">            {(["header", "query"] as const).map((carrier: Carrier) => (              <Button                key={carrier}                type="button"                variant={placement.carrier === carrier ? "secondary" : "ghost"}                size="sm"                className="h-8 rounded-sm text-xs"                onClick={() => set(index, { carrier })}              >                {carrier === "header" ? "Header" : "Query param"}              </Button>            ))}          </div>           <div className="mt-3 grid gap-3 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">            <div className="space-y-1.5">              <Label                htmlFor={`placement-name-${index}`}                className="text-xs font-medium text-muted-foreground"              >                {placement.carrier === "header" ? "Header name" : "Query parameter"}              </Label>              <Input                id={`placement-name-${index}`}                className="h-9"                placeholder={placement.carrier === "header" ? "Authorization" : "api_key"}                value={placement.name}                onChange={(e: React.ChangeEvent<HTMLInputElement>) =>                  set(index, { name: e.target.value })                }              />            </div>            <div className="space-y-1.5">              <Label                htmlFor={`placement-prefix-${index}`}                className="text-xs font-medium text-muted-foreground"              >                Prefix              </Label>              <Input                id={`placement-prefix-${index}`}                className="h-9"                placeholder="Bearer "                value={placement.prefix}                onChange={(e: React.ChangeEvent<HTMLInputElement>) =>                  set(index, { prefix: e.target.value })                }              />            </div>          </div>           {placement.name ? (            <div className="mt-3 flex flex-wrap items-center gap-2 rounded-md bg-muted/35 px-3 py-2 text-xs text-muted-foreground">              <span>Preview</span>              <PlacementLine placement={placement} />            </div>          ) : null}        </div>      ))}      {barePrefix ? (        <section className="grid grid-cols-[auto_1fr] gap-x-2.5 gap-y-1 rounded-md border border-l-[3px] border-amber-300/70 border-l-amber-500 bg-amber-50 px-3 py-2.5 text-[12px] leading-5 dark:border-amber-500/25 dark:border-l-amber-500/80 dark:bg-amber-500/10">          <TriangleAlertIcon            className="mt-0.5 size-4 shrink-0 text-amber-600 dark:text-amber-400"            aria-hidden          />          <div className="min-w-0 space-y-1">            <p className="font-medium text-amber-900 dark:text-amber-100">              Prefix has no trailing space            </p>            <p className="text-amber-900/80 dark:text-amber-100/80">              It is sent joined to the value (<span className="font-mono">Bearer••••••</span>). Most              APIs expect a space, like <span className="font-mono">"Bearer "</span>.            </p>          </div>        </section>      ) : null}       <Button        type="button"        variant="outline"        size="sm"        className="w-fit border-dashed"        onClick={() => onChange([...placements, emptyPlacement()])}      >        <PlusIcon />        Add location      </Button>    </div>  );}