Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Data catalog] search table and columns #327

Merged
merged 4 commits into from
Jan 31, 2025
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
32 changes: 4 additions & 28 deletions src/components/listview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import React, {
useRef,
useState,
} from "react";
import HighlightText from "../ui/highlight-text";

export interface ListViewItem<T = unknown> {
key: string;
Expand Down Expand Up @@ -57,31 +58,6 @@ interface ListViewRendererProps<T> extends ListViewProps<T> {
contextOpen: boolean;
}

function Highlight({ text, highlight }: { text: string; highlight?: string }) {
if (!highlight) return <span>{text}</span>;

const regex = new RegExp(
"(" + (highlight ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")",
"i"
);

const splitedText = text.split(regex);

return (
<span>
{splitedText.map((text, idx) => {
return text.toLowerCase() === (highlight ?? "").toLowerCase() ? (
<span key={idx} className="bg-yellow-300 text-black">
{text}
</span>
) : (
<span key={idx}>{text}</span>
);
})}
</span>
);
}

function Indentation({ depth }: { depth: number }) {
if (depth <= 0) return null;

Expand Down Expand Up @@ -219,7 +195,7 @@ function renderList<T>(props: ListViewRendererProps<T>): React.ReactElement {
{item.iconBadgeColor && (
<div
className={cn(
"absolute -bottom-0.5 -right-0.5 h-2 w-2 rounded-full",
"absolute -right-0.5 -bottom-0.5 h-2 w-2 rounded-full",
item.iconBadgeColor
)}
></div>
Expand All @@ -228,7 +204,7 @@ function renderList<T>(props: ListViewRendererProps<T>): React.ReactElement {
)}

<div className="line-clamp-1 flex-1 text-xs">
<Highlight text={item.name} highlight={highlight} />
<HighlightText text={item.name} highlight={highlight} />
{item.badgeContent && (
<span
className={cn(
Expand All @@ -242,7 +218,7 @@ function renderList<T>(props: ListViewRendererProps<T>): React.ReactElement {
</div>

{item.progressBarValue && item.progressBarMax && (
<div className="relative flex h-full w-[50px] items-center text-muted-foreground">
<div className="text-muted-foreground relative flex h-full w-[50px] items-center">
<div
className="h-[20px] rounded-sm border border-gray-200 bg-gray-100 dark:border-gray-700 dark:bg-gray-800"
style={{
Expand Down
29 changes: 29 additions & 0 deletions src/components/ui/highlight-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";
interface Props {
text: string;
highlight?: string;
}
export default function HighlightText({ text, highlight }: Props) {
if (!highlight) return <span>{text}</span>;

const regex = new RegExp(
"(" + (highlight ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")",
"i"
);

const splitedText = text.split(regex);

return (
<span>
{splitedText.map((text, idx) => {
return text.toLowerCase() === (highlight ?? "").toLowerCase() ? (
<span key={idx} className="bg-yellow-300 text-black">
{text}
</span>
) : (
<span key={idx}>{text}</span>
);
})}
</span>
);
}
160 changes: 138 additions & 22 deletions src/extensions/data-catalog/data-model-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import SchemaNameSelect from "@/components/gui/schema-editor/schema-name-select";
import { Toolbar } from "@/components/gui/toolbar";
import { Toolbar, ToolbarFiller } from "@/components/gui/toolbar";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
Expand All @@ -10,6 +11,8 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import HighlightText from "@/components/ui/highlight-text";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useConfig } from "@/context/config-provider";
Expand Down Expand Up @@ -63,6 +66,34 @@ function DataCatalogTableColumnModal({
.finally(() => setSampleLoading(false));
}, [databaseDriver, columnName, schemaName, tableName]);

const onSaveUpdateColumn = useCallback(() => {
setLoading(true);

driver
.updateColumn(schemaName, tableName, columnName, {
definition,
samples:
samples && samples.trim()
? samples.split(",").map((s) => s.trim())
: [],
hideFromEzql: modelColumn?.hideFromEzql ?? false,
})
.then()
.finally(() => {
setLoading(false);
onClose();
});
}, [
driver,
modelColumn,
samples,
columnName,
onClose,
schemaName,
tableName,
definition,
]);

return (
<>
<DialogHeader>
Expand Down Expand Up @@ -113,24 +144,7 @@ function DataCatalogTableColumnModal({
</div>

<DialogFooter>
<Button
disabled={loading}
onClick={() => {
setLoading(true);

driver
.updateColumn(schemaName, tableName, columnName, {
definition,
samples: samples.split(",").map((s) => s.trim()),
hideFromEzql: modelColumn?.hideFromEzql ?? false,
})
.then()
.finally(() => {
setLoading(false);
onClose();
});
}}
>
<Button disabled={loading} onClick={onSaveUpdateColumn}>
{loading && <LucideLoader className="mr-1 h-4 w-4 animate-spin" />}
Save
</Button>
Expand All @@ -143,12 +157,16 @@ interface DataCatalogTableColumnProps {
table: DatabaseTableSchema;
column: DatabaseTableColumn;
driver: DataCatalogDriver;
search?: string;
hasDefinitionOnly?: boolean;
}

function DataCatalogTableColumn({
column,
table,
driver,
search,
hasDefinitionOnly,
}: DataCatalogTableColumnProps) {
const modelColumn = driver.getColumn(
table.schemaName,
Expand All @@ -161,9 +179,15 @@ function DataCatalogTableColumn({

const [open, setOpen] = useState(false);

if (hasDefinitionOnly && !definition) {
return null;
}

return (
<div key={column.name} className="flex border-t">
<div className="w-[175px] p-2">{column.name}</div>
<div className="flex w-[150px] items-center p-2">
<HighlightText text={column.name} highlight={search} />
</div>
<div className="text-muted-foreground flex-1 p-2">
{definition || "No description"}
</div>
Expand Down Expand Up @@ -199,25 +223,96 @@ function DataCatalogTableColumn({
interface DataCatalogTableAccordionProps {
table: DatabaseTableSchema;
driver: DataCatalogDriver;
search?: string;
columnName?: string;
hasDefinitionOnly?: boolean;
}

function DataCatalogTableAccordion({
table,
driver,
search,
hasDefinitionOnly,
}: DataCatalogTableAccordionProps) {
const modelTable = driver.getTable(table.schemaName, table.tableName!);

const [definition, setDefinition] = useState(modelTable?.definition || "");

const onUpdateTable = useCallback(() => {
if (
definition &&
definition.trim() &&
definition !== modelTable?.definition
) {
driver.updateTable(table?.schemaName, table.tableName!, {
definition,
});
}
}, [driver, table, definition, modelTable]);

// Check if any of the column match?
const matchColumns = useMemo(() => {
if (!search || search.toLowerCase() === table.tableName!.toLowerCase()) {
return table.columns;
}
return table.columns.filter((column) =>
column.name.toLowerCase().includes(search.toLowerCase())
);
}, [search, table]);

const matchedTableName = useMemo(() => {
if (search) {
return table.tableName!.toLowerCase().includes(search?.toLowerCase());
}
return true;
}, [search, table]);

// this will work only toggle check box
if (hasDefinitionOnly) {
const columnsDefinition = table.columns
.map((col) => {
const modelColumn = driver.getColumn(
table.schemaName,
table.tableName!,
col.name
);
return !!modelColumn?.definition;
})
.filter(Boolean);

if (columnsDefinition.length === 0) {
return null;
}
}

if (!matchedTableName && matchColumns.length === 0 && search) {
return null;
}

return (
<div className="rounded-lg border text-sm">
<div className="p-2">
<div className="font-bold">{table.tableName}</div>
<div>No description</div>
<input
value={definition}
placeholder="No description"
onBlur={onUpdateTable}
onChange={(e) => {
e.preventDefault();
setDefinition(e.currentTarget.value);
}}
className="h-[30px] w-[150px] p-0 text-[13px] focus-visible:outline-none"
/>
</div>
{table.columns.map((column) => {
{matchColumns.map((column) => {
return (
<DataCatalogTableColumn
key={column.name}
table={table}
column={column}
driver={driver}
search={search}
hasDefinitionOnly={hasDefinitionOnly}
/>
);
})}
Expand All @@ -227,6 +322,8 @@ function DataCatalogTableAccordion({

export default function DataCatalogModelTab() {
const { currentSchemaName, schema } = useSchema();
const [search, setSearch] = useState("");
const [hasDefinitionOnly, setHasDefinitionOnly] = useState(false);
const [selectedSchema, setSelectedSchema] = useState(currentSchemaName);

const { extensions } = useConfig();
Expand Down Expand Up @@ -259,15 +356,34 @@ export default function DataCatalogModelTab() {
value={selectedSchema}
onChange={setSelectedSchema}
/>
<div className="ml-2 flex items-center gap-2">
<Checkbox
checked={hasDefinitionOnly}
onCheckedChange={() => setHasDefinitionOnly(!hasDefinitionOnly)}
/>
<label className="text-sm">Definition only?</label>
</div>
<ToolbarFiller />
<div>
<Input
value={search}
onChange={(e) => {
setSearch(e.currentTarget.value);
}}
placeholder="Search tables, columns"
/>
</div>
</Toolbar>
</div>

<div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
{currentSchema.map((table) => (
<DataCatalogTableAccordion
search={search}
key={table.tableName}
table={table}
driver={driver}
hasDefinitionOnly={hasDefinitionOnly}
/>
))}
</div>
Expand Down
Loading