Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

title: ""
labels: ""
assignees: ""
---

**Inspector Version**

- [e.g. 0.16.5)

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:

1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
Expand All @@ -27,8 +28,9 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.

**Environment (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]

- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]

**Additional context**
Add any other context about the problem here.
4 changes: 4 additions & 0 deletions client/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export default tseslint.config(
"warn",
{ allowConstantExport: true },
],
"@typescript-eslint/no-unused-vars": [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this being added?

"error",
{ argsIgnorePattern: "^_" },
],
},
},
);
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@radix-ui/react-popover": "^1.1.3",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8",
Expand Down Expand Up @@ -69,6 +70,7 @@
"globals": "^15.9.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fixed-jsdom": "^0.0.9",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.13",
"tailwindcss-animate": "^1.0.7",
Expand Down
46 changes: 46 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ import ElicitationTab, {
PendingElicitationRequest,
ElicitationResponse,
} from "./components/ElicitationTab";
import {
CustomHeaders,
migrateFromLegacyAuth,
} from "./lib/types/customHeaders";

const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";

Expand Down Expand Up @@ -127,6 +131,28 @@ const App = () => {
return localStorage.getItem("lastOauthScope") || "";
});

// Custom headers state with migration from legacy auth
const [customHeaders, setCustomHeaders] = useState<CustomHeaders>(() => {
const savedHeaders = localStorage.getItem("lastCustomHeaders");
if (savedHeaders) {
try {
return JSON.parse(savedHeaders);
} catch {
// Fall back to migration if JSON parsing fails
}
}

// Migrate from legacy auth if available
const legacyToken = localStorage.getItem("lastBearerToken") || "";
const legacyHeaderName = localStorage.getItem("lastHeaderName") || "";

if (legacyToken) {
return migrateFromLegacyAuth(legacyToken, legacyHeaderName);
}

return [];
});

const [pendingSampleRequests, setPendingSampleRequests] = useState<
Array<
PendingRequest & {
Expand Down Expand Up @@ -215,6 +241,7 @@ const App = () => {
env,
bearerToken,
headerName,
customHeaders,
oauthClientId,
oauthScope,
config,
Expand Down Expand Up @@ -310,6 +337,23 @@ const App = () => {
localStorage.setItem("lastHeaderName", headerName);
}, [headerName]);

useEffect(() => {
localStorage.setItem("lastCustomHeaders", JSON.stringify(customHeaders));
}, [customHeaders]);

// Auto-migrate from legacy auth when custom headers are empty but legacy auth exists
useEffect(() => {
if (customHeaders.length === 0 && (bearerToken || headerName)) {
const migratedHeaders = migrateFromLegacyAuth(bearerToken, headerName);
if (migratedHeaders.length > 0) {
setCustomHeaders(migratedHeaders);
// Clear legacy auth after migration
setBearerToken("");
setHeaderName("");
}
}
}, [bearerToken, headerName, customHeaders, setCustomHeaders]);

useEffect(() => {
localStorage.setItem("lastOauthClientId", oauthClientId);
}, [oauthClientId]);
Expand Down Expand Up @@ -814,6 +858,8 @@ const App = () => {
setBearerToken={setBearerToken}
headerName={headerName}
setHeaderName={setHeaderName}
customHeaders={customHeaders}
setCustomHeaders={setCustomHeaders}
oauthClientId={oauthClientId}
setOauthClientId={setOauthClientId}
oauthScope={oauthScope}
Expand Down
238 changes: 238 additions & 0 deletions client/src/components/CustomHeaders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { Plus, Trash2, Eye, EyeOff } from "lucide-react";
import {
CustomHeaders as CustomHeadersType,
CustomHeader,
createEmptyHeader,
} from "@/lib/types/customHeaders";

interface CustomHeadersProps {
headers: CustomHeadersType;
onChange: (headers: CustomHeadersType) => void;
className?: string;
}

const CustomHeaders = ({
headers,
onChange,
className,
}: CustomHeadersProps) => {
const [isJsonMode, setIsJsonMode] = useState(false);
const [jsonValue, setJsonValue] = useState("");
const [jsonError, setJsonError] = useState<string | null>(null);
const [visibleValues, setVisibleValues] = useState<Set<number>>(new Set());

const updateHeader = (
index: number,
field: keyof CustomHeader,
value: string | boolean,
) => {
const newHeaders = [...headers];
newHeaders[index] = { ...newHeaders[index], [field]: value };
onChange(newHeaders);
};

const addHeader = () => {
onChange([...headers, createEmptyHeader()]);
};

const removeHeader = (index: number) => {
const newHeaders = headers.filter((_, i) => i !== index);
onChange(newHeaders);
};

const toggleValueVisibility = (index: number) => {
const newVisible = new Set(visibleValues);
if (newVisible.has(index)) {
newVisible.delete(index);
} else {
newVisible.add(index);
}
setVisibleValues(newVisible);
};

const switchToJsonMode = () => {
const jsonObject: Record<string, string> = {};
headers.forEach((header) => {
if (header.enabled && header.name.trim() && header.value.trim()) {
jsonObject[header.name.trim()] = header.value.trim();
}
});
setJsonValue(JSON.stringify(jsonObject, null, 2));
setJsonError(null);
setIsJsonMode(true);
};

const switchToFormMode = () => {
try {
const parsed = JSON.parse(jsonValue);
if (
typeof parsed !== "object" ||
parsed === null ||
Array.isArray(parsed)
) {
setJsonError("JSON must be an object with string key-value pairs");
return;
}

const newHeaders: CustomHeadersType = Object.entries(parsed).map(
([name, value]) => ({
name,
value: String(value),
enabled: true,
}),
);

onChange(newHeaders);
setJsonError(null);
setIsJsonMode(false);
} catch {
setJsonError("Invalid JSON format");
}
};

const handleJsonChange = (value: string) => {
setJsonValue(value);
setJsonError(null);
};

if (isJsonMode) {
return (
<div className={`space-y-3 ${className}`}>
<div className="flex justify-between items-center gap-2">
<h4 className="text-sm font-semibold flex-shrink-0">
Custom Headers (JSON)
</h4>
<Button
type="button"
variant="outline"
size="sm"
onClick={switchToFormMode}
className="flex-shrink-0"
>
Switch to Form
</Button>
</div>
<div className="space-y-2">
<Textarea
value={jsonValue}
onChange={(e) => handleJsonChange(e.target.value)}
placeholder='{\n "Authorization": "Bearer token123",\n "X-Tenant-ID": "acme-inc",\n "X-Environment": "staging"\n}'
className="font-mono text-sm min-h-[100px] resize-none"
/>
{jsonError && <p className="text-sm text-red-600">{jsonError}</p>}
<p className="text-xs text-muted-foreground">
Enter headers as a JSON object with string key-value pairs.
</p>
</div>
</div>
);
}

return (
<div className={`space-y-3 ${className}`}>
<div className="flex justify-between items-center gap-2">
<h4 className="text-sm font-semibold flex-shrink-0">Custom Headers</h4>
<div className="flex gap-1 flex-shrink-0">
<Button
type="button"
variant="outline"
size="sm"
onClick={switchToJsonMode}
className="text-xs px-2"
>
JSON
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={addHeader}
className="text-xs px-2"
>
<Plus className="w-3 h-3 mr-1" />
Add
</Button>
</div>
</div>

{headers.length === 0 ? (
<div className="text-center py-4 text-muted-foreground">
<p className="text-sm">No custom headers configured</p>
<p className="text-xs mt-1">Click "Add" to get started</p>
</div>
) : (
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{headers.map((header, index) => (
<div
key={index}
className="flex items-start gap-2 p-2 border rounded-md"
>
<Switch
checked={header.enabled}
onCheckedChange={(enabled) =>
updateHeader(index, "enabled", enabled)
}
className="shrink-0 mt-2"
/>
<div className="flex-1 min-w-0 space-y-2">
<Input
placeholder="Header Name"
value={header.name}
onChange={(e) => updateHeader(index, "name", e.target.value)}
className="font-mono text-xs"
/>
<div className="relative">
<Input
placeholder="Header Value"
value={header.value}
onChange={(e) =>
updateHeader(index, "value", e.target.value)
}
type={visibleValues.has(index) ? "text" : "password"}
className="font-mono text-xs pr-8"
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => toggleValueVisibility(index)}
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0"
>
{visibleValues.has(index) ? (
<EyeOff className="w-3 h-3" />
) : (
<Eye className="w-3 h-3" />
)}
</Button>
</div>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeHeader(index)}
className="shrink-0 text-red-600 hover:text-red-700 hover:bg-red-50 h-6 w-6 p-0 mt-2"
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
))}
</div>
)}

{headers.length > 0 && (
<p className="text-xs text-muted-foreground">
Use the toggle to enable/disable headers. Only enabled headers with
both name and value will be sent.
</p>
)}
</div>
);
};

export default CustomHeaders;
Loading
Loading