feat/ida66 #3

Merged
rov merged 6 commits from feat/ida66 into main 2026-06-10 11:57:14 -03:00
6 changed files with 607 additions and 7 deletions
Showing only changes of commit 249a6b81b5 - Show all commits

View File

@ -20,11 +20,13 @@ import type {
import type { import type {
AnalysisResponse, AnalysisResponse,
DataLabelResponse, DataLabelResponse,
EnumResponse,
ImportResponse, ImportResponse,
ListFunctionsParams, ListFunctionsParams,
PageFunctionSummaryResponse, PageFunctionSummaryResponse,
SegmentResponse, SegmentResponse,
StringResponse, StringResponse,
StructResponse,
} from "../api.schemas"; } from "../api.schemas";
import { customInstance } from "../../api"; import { customInstance } from "../../api";
@ -251,6 +253,132 @@ export function useGetAnalysis<TData = Awaited<ReturnType<typeof getAnalysis>>,
return { ...query, queryKey: queryOptions.queryKey }; return { ...query, queryKey: queryOptions.queryKey };
} }
/**
* Retrieve all struct/union definitions identified during a static analysis.
* @summary List structures in an analysis
*/
export const listStructures = (
analysisId: string,
options?: SecondParameter<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<StructResponse[]>(
{
url: `/analysis/${encodeURIComponent(String(analysisId))}/structures`,
method: "GET",
signal,
},
options
);
};
export const getListStructuresQueryKey = (analysisId: string) => {
return [`/analysis/${analysisId}/structures`] as const;
};
export const getListStructuresQueryOptions = <
TData = Awaited<ReturnType<typeof listStructures>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStructures>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
}
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListStructuresQueryKey(analysisId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listStructures>>> = ({ signal }) =>
listStructures(analysisId, requestOptions, signal);
return {
queryKey,
queryFn,
enabled: analysisId !== null && analysisId !== undefined,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof listStructures>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ListStructuresQueryResult = NonNullable<Awaited<ReturnType<typeof listStructures>>>;
export type ListStructuresQueryError = unknown;
export function useListStructures<
TData = Awaited<ReturnType<typeof listStructures>>,
TError = unknown,
>(
analysisId: string,
options: {
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStructures>>, TError, TData>> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listStructures>>,
TError,
Awaited<ReturnType<typeof listStructures>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListStructures<
TData = Awaited<ReturnType<typeof listStructures>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStructures>>, TError, TData>> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listStructures>>,
TError,
Awaited<ReturnType<typeof listStructures>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListStructures<
TData = Awaited<ReturnType<typeof listStructures>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStructures>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
/**
* @summary List structures in an analysis
*/
export function useListStructures<
TData = Awaited<ReturnType<typeof listStructures>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStructures>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getListStructuresQueryOptions(analysisId, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/** /**
* Retrieve all strings extracted during a static analysis. * Retrieve all strings extracted during a static analysis.
* @summary List strings in an analysis * @summary List strings in an analysis
@ -839,3 +967,113 @@ export function useListFunctions<
return { ...query, queryKey: queryOptions.queryKey }; return { ...query, queryKey: queryOptions.queryKey };
} }
/**
* Retrieve all enum definitions identified during a static analysis.
* @summary List enums in an analysis
*/
export const listEnums = (
analysisId: string,
options?: SecondParameter<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<EnumResponse[]>(
{ url: `/analysis/${encodeURIComponent(String(analysisId))}/enums`, method: "GET", signal },
options
);
};
export const getListEnumsQueryKey = (analysisId: string) => {
return [`/analysis/${analysisId}/enums`] as const;
};
export const getListEnumsQueryOptions = <
TData = Awaited<ReturnType<typeof listEnums>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
}
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListEnumsQueryKey(analysisId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listEnums>>> = ({ signal }) =>
listEnums(analysisId, requestOptions, signal);
return {
queryKey,
queryFn,
enabled: analysisId !== null && analysisId !== undefined,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ListEnumsQueryResult = NonNullable<Awaited<ReturnType<typeof listEnums>>>;
export type ListEnumsQueryError = unknown;
export function useListEnums<TData = Awaited<ReturnType<typeof listEnums>>, TError = unknown>(
analysisId: string,
options: {
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listEnums>>,
TError,
Awaited<ReturnType<typeof listEnums>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListEnums<TData = Awaited<ReturnType<typeof listEnums>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listEnums>>,
TError,
Awaited<ReturnType<typeof listEnums>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListEnums<TData = Awaited<ReturnType<typeof listEnums>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
/**
* @summary List enums in an analysis
*/
export function useListEnums<TData = Awaited<ReturnType<typeof listEnums>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getListEnumsQueryOptions(analysisId, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
}

View File

@ -186,6 +186,21 @@ export interface AnalysisResponse {
updatedAt?: string; updatedAt?: string;
} }
export interface StructMemberResponse {
offset?: number;
size?: number;
name?: string;
type?: string;
}
export interface StructResponse {
id?: string;
name?: string;
size?: number;
isUnion?: boolean;
members?: StructMemberResponse[];
}
export interface StringResponse { export interface StringResponse {
id?: string; id?: string;
address?: string; address?: string;
@ -270,6 +285,18 @@ export interface PageFunctionSummaryResponse {
empty?: boolean; empty?: boolean;
} }
export interface EnumConstResponse {
name?: string;
value?: number;
}
export interface EnumResponse {
id?: string;
name?: string;
width?: number;
constants?: EnumConstResponse[];
}
export type GetWorkspacesParams = { export type GetWorkspacesParams = {
/** /**
* Filter workspaces whose name contains this value (case-insensitive) * Filter workspaces whose name contains this value (case-insensitive)

View File

@ -0,0 +1,121 @@
import { useState, Fragment } from "react";
import { Search, ChevronRight, ChevronDown } from "lucide-react";
import type { EnumResponse } from "@/api/generated/api.schemas";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface EnumTableProps {
enums: EnumResponse[];
}
export function EnumTable({ enums }: EnumTableProps) {
const [search, setSearch] = useState("");
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const filtered = enums.filter(
(e) => !search || e.name?.toLowerCase().includes(search.toLowerCase())
);
const toggleExpanded = (id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="border-border shrink-0 border-b p-3">
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Filter enums..."
className="bg-muted h-8 border-transparent pl-9"
/>
</div>
</div>
<ScrollArea className="min-h-0 flex-1">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="w-[28px]" />
<TableHead>Name</TableHead>
<TableHead className="w-[80px] text-right">Width</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((e) => {
const hasConstants = e.constants && e.constants.length > 0;
const expanded = !!(e.id && expandedIds.has(e.id));
return (
<Fragment key={e.id}>
<TableRow
className="cursor-pointer"
onClick={() => e.id && hasConstants && toggleExpanded(e.id)}
>
<TableCell className="px-1">
{hasConstants &&
(expanded ? (
<ChevronDown className="text-muted-foreground h-3.5 w-3.5" />
) : (
<ChevronRight className="text-muted-foreground h-3.5 w-3.5" />
))}
</TableCell>
<TableCell className="font-mono text-xs">{e.name}</TableCell>
<TableCell className="text-right text-xs">{e.width}</TableCell>
</TableRow>
{expanded && hasConstants && (
<TableRow key={`consts-${e.id}`}>
<TableCell colSpan={3} className="bg-muted/30 p-0">
<div className="flex flex-col">
<div className="text-muted-foreground grid shrink-0 grid-cols-[1fr_100px] px-2 text-xs">
<span>Name</span>
<span className="text-right">Value</span>
</div>
<div className="max-h-40 overflow-y-auto">
{e.constants!.map((c, ci) => (
<div
key={ci}
className="even:bg-muted/30 grid grid-cols-[1fr_100px] px-2 font-mono text-xs"
>
<span className="truncate">{c.name}</span>
<span className="text-right">{c.value}</span>
</div>
))}
</div>
</div>
</TableCell>
</TableRow>
)}
</Fragment>
);
})}
</TableBody>
</Table>
{filtered.length === 0 && (
<div className="text-muted-foreground py-4 text-center text-xs">
{search ? "No matching enums" : "No enums found"}
</div>
)}
</ScrollArea>
<div className="border-border text-muted-foreground border-t p-3 text-xs">
{search ? `${filtered.length} of ${enums.length} enums` : `${enums.length} enums`}
</div>
</div>
);
}

View File

@ -0,0 +1,136 @@
import { useState, Fragment } from "react";
import { Search, ChevronRight, ChevronDown } from "lucide-react";
import type { StructResponse } from "@/api/generated/api.schemas";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface StructureTableProps {
structures: StructResponse[];
}
export function StructureTable({ structures }: StructureTableProps) {
const [search, setSearch] = useState("");
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const filtered = structures.filter(
(s) => !search || s.name?.toLowerCase().includes(search.toLowerCase())
);
const toggleExpanded = (id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="border-border shrink-0 border-b p-3">
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Filter structures..."
className="bg-muted h-8 border-transparent pl-9"
/>
</div>
</div>
<ScrollArea className="min-h-0 flex-1">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="w-[28px]" />
<TableHead>Name</TableHead>
<TableHead className="w-[80px] text-right">Size</TableHead>
<TableHead className="w-[90px]">Type</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((s) => {
const expanded = !!(s.id && expandedIds.has(s.id));
return (
<Fragment key={s.id}>
<TableRow className="cursor-pointer" onClick={() => s.id && toggleExpanded(s.id)}>
<TableCell className="px-1">
{expanded ? (
<ChevronDown className="text-muted-foreground h-3.5 w-3.5" />
) : (
<ChevronRight className="text-muted-foreground h-3.5 w-3.5" />
)}
</TableCell>
<TableCell className="font-mono text-xs">{s.name}</TableCell>
<TableCell className="text-right font-mono text-xs">{s.size}</TableCell>
<TableCell>
<Badge
variant="outline"
className={
s.isUnion
? "border-purple-500 text-[10px] text-purple-400"
: "border-blue-500 text-[10px] text-blue-400"
}
>
{s.isUnion ? "Union" : "Struct"}
</Badge>
</TableCell>
</TableRow>
{expanded && s.members && s.members.length > 0 && (
<TableRow key={`members-${s.id}`}>
<TableCell colSpan={4} className="bg-muted/30 p-0">
<div className="flex flex-col">
<div className="text-muted-foreground grid shrink-0 grid-cols-[60px_1fr_50px_90px] px-2 text-xs">
<span>Offset</span>
<span>Name</span>
<span>Size</span>
<span>Type</span>
</div>
<div className="max-h-40 overflow-y-auto">
{s.members.map((m, mi) => (
<div
key={mi}
className="even:bg-muted/30 grid grid-cols-[60px_1fr_50px_90px] px-2 font-mono text-xs"
>
<span>{m.offset}</span>
<span className="truncate">{m.name}</span>
<span>{m.size}</span>
<span>{m.type}</span>
</div>
))}
</div>
</div>
</TableCell>
</TableRow>
)}
</Fragment>
);
})}
</TableBody>
</Table>
{filtered.length === 0 && (
<div className="text-muted-foreground py-4 text-center text-xs">
{search ? "No matching structures" : "No structures found"}
</div>
)}
</ScrollArea>
<div className="border-border text-muted-foreground border-t p-3 text-xs">
{search
? `${filtered.length} of ${structures.length} structures`
: `${structures.length} structures`}
</div>
</div>
);
}

View File

@ -10,6 +10,8 @@ import {
useListStrings, useListStrings,
useListImports, useListImports,
useListGlobalVars, useListGlobalVars,
useListStructures,
useListEnums,
} from "@/api/generated/analysis/analysis.ts"; } from "@/api/generated/analysis/analysis.ts";
import { useGetFunction } from "@/api/generated/function/function.ts"; import { useGetFunction } from "@/api/generated/function/function.ts";
import { useListEngines } from "@/api/generated/engine/engine.ts"; import { useListEngines } from "@/api/generated/engine/engine.ts";
@ -97,6 +99,16 @@ export function useBinaryPage() {
query: { enabled: !!analysisId }, query: { enabled: !!analysisId },
}); });
// Fetch structures for the analysis
const { data: structures } = useListStructures(analysisId ?? "", {
query: { enabled: !!analysisId },
});
// Fetch enums for the analysis
const { data: enums } = useListEnums(analysisId ?? "", {
query: { enabled: !!analysisId },
});
// Infinite query for functions pagination — single page with all functions // Infinite query for functions pagination — single page with all functions
const functionsQuery = useInfiniteQuery({ const functionsQuery = useInfiniteQuery({
queryKey: ["functions", analysisId], queryKey: ["functions", analysisId],
@ -257,6 +269,8 @@ export function useBinaryPage() {
strings: stringsList, strings: stringsList,
imports: imports ?? [], imports: imports ?? [],
globalVars: globalVars ?? [], globalVars: globalVars ?? [],
structures: structures ?? [],
enums: enums ?? [],
functionNameToId, functionNameToId,
functionStrings, functionStrings,
selectedFunction, selectedFunction,

View File

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft, ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/EmptyState"; import { EmptyState } from "@/components/EmptyState";
import { Breadcrumbs } from "@/components/Breadcrumbs.tsx"; import { Breadcrumbs } from "@/components/Breadcrumbs.tsx";
@ -8,6 +8,8 @@ import { FunctionTree } from "@/features/binary/FunctionTree";
import { StringTable } from "@/features/binary/StringTable"; import { StringTable } from "@/features/binary/StringTable";
import { ImportTable } from "@/features/binary/ImportTable"; import { ImportTable } from "@/features/binary/ImportTable";
import { GlobalVarTable } from "@/features/binary/GlobalVarTable"; import { GlobalVarTable } from "@/features/binary/GlobalVarTable";
import { StructureTable } from "@/features/binary/StructureTable";
import { EnumTable } from "@/features/binary/EnumTable";
import { XrefPanel } from "@/features/binary/XrefPanel"; import { XrefPanel } from "@/features/binary/XrefPanel";
import { import {
ResizableHandle, ResizableHandle,
@ -19,6 +21,13 @@ import { CodeViewer } from "@/features/binary/CodeViewer";
import { AIChatPanel } from "@/features/binary/AIChatPanel"; import { AIChatPanel } from "@/features/binary/AIChatPanel";
import { AnalyzeDialog } from "@/features/binary/AnalyzeDialog"; import { AnalyzeDialog } from "@/features/binary/AnalyzeDialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
const Binary = () => { const Binary = () => {
const { const {
@ -55,6 +64,8 @@ const Binary = () => {
enginesLoading, enginesLoading,
imports, imports,
globalVars, globalVars,
structures,
enums,
} = useBinaryPage(); } = useBinaryPage();
const navigateToEntryPoint = () => { const navigateToEntryPoint = () => {
@ -76,6 +87,20 @@ const Binary = () => {
setHighlightedStringAddress(undefined); setHighlightedStringAddress(undefined);
}; };
// Build data tabs for the "More" dropdown
const dataTabs = [
...(imports && imports.length > 0 ? ([{ value: "imports", label: "Imports" }] as const) : []),
...(globalVars && globalVars.length > 0
? ([{ value: "global-vars", label: "Global Vars" }] as const)
: []),
...(structures && structures.length > 0
? ([{ value: "structures", label: "Structures" }] as const)
: []),
...(enums && enums.length > 0 ? ([{ value: "enums", label: "Enums" }] as const) : []),
];
const hasDataTabs = dataTabs.length > 0;
const isDataTabActive = dataTabs.some((t) => t.value === activeTab);
if (binaryLoading) return null; if (binaryLoading) return null;
if (!binary) { if (!binary) {
@ -132,14 +157,37 @@ const Binary = () => {
onValueChange={handleTabChange} onValueChange={handleTabChange}
className="flex min-h-0 flex-1 flex-col" className="flex min-h-0 flex-1 flex-col"
> >
<TabsList className="mx-3 mt-2 shrink-0"> <TabsList className="mx-auto mt-2">
<TabsTrigger value="functions">Functions</TabsTrigger> <TabsTrigger value="functions">Functions</TabsTrigger>
<TabsTrigger value="strings">Strings</TabsTrigger> <TabsTrigger value="strings">Strings</TabsTrigger>
{imports && imports.length > 0 && ( {hasDataTabs && (
<TabsTrigger value="imports">Imports</TabsTrigger> <DropdownMenu>
)} <DropdownMenuTrigger asChild>
{globalVars && globalVars.length > 0 && ( <button
<TabsTrigger value="global-vars">Global Vars</TabsTrigger> className={cn(
"ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
isDataTabActive && "bg-background text-foreground shadow-sm"
)}
>
{isDataTabActive
? dataTabs.find((t) => t.value === activeTab)?.label
: "More"}
<ChevronDown className="ml-1 h-3.5 w-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{dataTabs.map((tab) => (
<DropdownMenuItem
key={tab.value}
onClick={() => handleTabChange(tab.value)}
>
<span className={activeTab === tab.value ? "font-medium" : ""}>
{tab.label}
</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)} )}
</TabsList> </TabsList>
<TabsContent <TabsContent
@ -193,6 +241,22 @@ const Binary = () => {
/> />
</TabsContent> </TabsContent>
)} )}
{structures && structures.length > 0 && (
<TabsContent
value="structures"
className="mt-1 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
>
<StructureTable structures={structures} />
</TabsContent>
)}
{enums && enums.length > 0 && (
<TabsContent
value="enums"
className="mt-1 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
>
<EnumTable enums={enums} />
</TabsContent>
)}
</Tabs> </Tabs>
</div> </div>
</ResizablePanel> </ResizablePanel>