Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
114 changes: 112 additions & 2 deletions src/features/schema/ObjectDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@ import { localizeConnectionError } from "../../lib/errors";
import {
type ColumnInfo,
type ForeignKeyInfo,
type FunctionDefinition,
type IndexInfo,
type MatviewDefinition,
type ProcedureDefinition,
type ViewInfo,
schemaGetFunctionDefinition,
schemaGetMatviewDefinition,
schemaGetProcedureDefinition,
schemaListColumns,
schemaListForeignKeys,
schemaListIndexes,
schemaListViews,
} from "../../lib/tauri";
import { type NodeKey, useSchema } from "./store";
import { type NodeKey, parseRoutineKey, useSchema } from "./store";

/**
* Main-area panel for selected tree node (spec §5.4 / §7.4).
Expand Down Expand Up @@ -53,7 +57,19 @@ interface ParsedMatview {
name: string;
}

type ParsedNode = ParsedTable | ParsedView | ParsedMatview | { kind: "placeholder" };
interface ParsedRoutine {
kind: "function" | "procedure";
schema: string;
name: string;
args: string;
}

type ParsedNode =
| ParsedTable
| ParsedView
| ParsedMatview
| ParsedRoutine
| { kind: "placeholder" };

function parseNode(node: NodeKey | null): ParsedNode {
if (node === null) return { kind: "placeholder" };
Expand All @@ -75,6 +91,10 @@ function parseNode(node: NodeKey | null): ParsedNode {
if (slash === -1) return { kind: "placeholder" };
return { kind: "matview", schema: rest.slice(0, slash), name: rest.slice(slash + 1) };
}
const routine = parseRoutineKey(node);
if (routine) {
return routine;
}
return { kind: "placeholder" };
}

Expand Down Expand Up @@ -139,6 +159,19 @@ export function ObjectDetails({ node }: { node: NodeKey | null }): JSX.Element {
);
}

if (parsed.kind === "function" || parsed.kind === "procedure") {
return (
<RoutineDetails
kind={parsed.kind}
connId={connection.connId}
schema={parsed.schema}
name={parsed.name}
args={parsed.args}
key={`${parsed.kind}:${parsed.schema}/${parsed.name}#${parsed.args}`}
/>
);
}

return (
<ViewDetails
connId={connection.connId}
Expand Down Expand Up @@ -453,6 +486,83 @@ function MatviewDetails({
);
}

function RoutineDetails({
kind,
connId,
schema,
name,
args,
}: {
kind: "function" | "procedure";
connId: string;
schema: string;
name: string;
args: string;
}): JSX.Element {
const { t } = useTranslation();
const toast = useToast();
const [state, setState] =
useState<FetchState<FunctionDefinition | ProcedureDefinition>>(initialFetch);

const load = useCallback(() => {
setState({ loading: true, data: null, error: null });
const fetchDef =
kind === "function"
? schemaGetFunctionDefinition(connId, schema, name, args)
: schemaGetProcedureDefinition(connId, schema, name, args);
fetchDef
.then((def) => {
setState({ loading: false, data: def, error: null });
})
.catch((err: unknown) => {
const message = localizeConnectionError(err, t);
setState({ loading: false, data: null, error: message });
toast.error(message);
});
}, [kind, connId, schema, name, args, t, toast]);

useEffect(() => {
load();
}, [load]);

return (
<div style={{ height: "100%", display: "flex", flexDirection: "column", minHeight: 0 }}>
<div
role="tablist"
aria-label={t("object_details.section.definition")}
className="q-subtabbar"
>
<TabButton active onClick={() => {}} controls="panel-definition" id="tab-definition">
{t("object_details.section.definition")}
</TabButton>
</div>
<div className="q-scroll" style={{ flex: 1, overflow: "auto", padding: 18, minHeight: 0 }}>
<div role="tabpanel" id="panel-definition" aria-labelledby="tab-definition">
{state.loading ? <LoadingRow /> : null}
{state.error ? <ErrorRow message={state.error} onRetry={load} /> : null}
{state.data ? (
<pre
style={{
overflow: "auto",
background: "var(--bg-sunken)",
padding: 12,
borderRadius: 8,
margin: 0,
fontFamily: "var(--font-mono-q)",
fontSize: 11.5,
color: "var(--ink-2)",
whiteSpace: "pre-wrap",
}}
>
<code>{`-- ${state.data.name}(${args}) · language: ${state.data.language}\n\n${state.data.body}`}</code>
</pre>
) : null}
</div>
</div>
</div>
);
}

function TabButton({
active,
onClick,
Expand Down
37 changes: 33 additions & 4 deletions src/features/schema/SchemaTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
Database,
Eye,
Folder,
FunctionSquare,
Layers,
SquareFunction,
Table2,
} from "lucide-react";
import {
Expand Down Expand Up @@ -95,7 +97,9 @@ function buildTreeNodes(
const displayName =
child.kind === "tables-group" ||
child.kind === "views-group" ||
child.kind === "matviews-group"
child.kind === "matviews-group" ||
child.kind === "functions-group" ||
child.kind === "procedures-group"
? translate(child.name)
: child.name;
return {
Expand All @@ -120,12 +124,20 @@ function iconForKind(kind: NodeKind): JSX.Element | null {
return <Folder {...props} />;
case "matviews-group":
return <Folder {...props} />;
case "functions-group":
return <Folder {...props} />;
case "procedures-group":
return <Folder {...props} />;
case "table":
return <Table2 {...props} />;
case "view":
return <Eye {...props} />;
case "matview":
return <Layers {...props} />;
case "function":
return <FunctionSquare {...props} />;
case "procedure":
return <SquareFunction {...props} />;
case "column":
return <Columns {...props} />;
}
Expand Down Expand Up @@ -219,7 +231,13 @@ function Row({
// column rows: click is a no-op; only the JSONB chevron toggles the panel
return;
}
if (kind === "table" || kind === "view" || kind === "matview") {
if (
kind === "table" ||
kind === "view" ||
kind === "matview" ||
kind === "function" ||
kind === "procedure"
) {
// Tables and views: select the node AND toggle column expansion.
// Only call onToggle (which triggers loadChildren) if the node is a
// non-leaf in the tree — i.e., the cache already knows it has children.
Expand Down Expand Up @@ -254,7 +272,12 @@ function Row({

// "isLeaf" is used only to apply the "ident" CSS class — originally tables/views
// were leaves; they still get the same class treatment.
const isLeaf = kind === "table" || kind === "view" || kind === "matview";
const isLeaf =
kind === "table" ||
kind === "view" ||
kind === "matview" ||
kind === "function" ||
kind === "procedure";
const isColumn = kind === "column";

// S28 — hypertable badge: looks up isHypertable in useTimescale's per-conn map.
Expand Down Expand Up @@ -516,7 +539,13 @@ export function SchemaTree({ onOpenBuilder, onOpenSuggester }: SchemaTreeProps =

const handleSelect = useCallback(
(id: NodeKey, kind: NodeKind) => {
if (kind === "table" || kind === "view" || kind === "matview") {
if (
kind === "table" ||
kind === "view" ||
kind === "matview" ||
kind === "function" ||
kind === "procedure"
) {
selectNode(id);
// also dispatch an editor object tab so the user gets a
// pinned ObjectDetails view in the main area without losing other
Expand Down
Loading
Loading