mirror of
https://github.com/chartdb/chartdb.git
synced 2026-05-19 12:39:14 -05:00
filter by schema
This commit is contained in:
committed by
Guy Ben-Aharon
parent
0e6ebf3ecd
commit
50ef457fe2
@@ -0,0 +1,293 @@
|
||||
import { CaretSortIcon, CheckIcon, Cross2Icon } from '@radix-ui/react-icons';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/command/command';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/popover/popover';
|
||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
||||
|
||||
export interface SelectBoxOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface SelectBoxProps {
|
||||
options: SelectBoxOption[];
|
||||
value?: string[] | string;
|
||||
onChange?: (values: string[] | string) => void;
|
||||
placeholder?: string;
|
||||
inputPlaceholder?: string;
|
||||
emptyPlaceholder?: string;
|
||||
className?: string;
|
||||
multiple?: boolean;
|
||||
oneLine?: boolean;
|
||||
selectAll?: boolean;
|
||||
deselectAll?: boolean;
|
||||
onSelectAll?: () => void;
|
||||
onDeselectAll?: () => void;
|
||||
}
|
||||
|
||||
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
(
|
||||
{
|
||||
inputPlaceholder,
|
||||
emptyPlaceholder,
|
||||
placeholder,
|
||||
className,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
multiple,
|
||||
oneLine,
|
||||
selectAll,
|
||||
deselectAll,
|
||||
onSelectAll,
|
||||
onDeselectAll,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [searchTerm, setSearchTerm] = React.useState<string>('');
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(selectedValue: string) => {
|
||||
if (multiple) {
|
||||
const newValue =
|
||||
value?.includes(selectedValue) && Array.isArray(value)
|
||||
? value.filter((v) => v !== selectedValue)
|
||||
: [...(value ?? []), selectedValue];
|
||||
onChange?.(newValue);
|
||||
} else {
|
||||
onChange?.(selectedValue);
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
[multiple, onChange, value]
|
||||
);
|
||||
|
||||
const handleClear = React.useCallback(() => {
|
||||
onChange?.(multiple ? [] : '');
|
||||
}, [multiple, onChange]);
|
||||
|
||||
const selectedMultipleOptions = React.useMemo(
|
||||
() =>
|
||||
options
|
||||
.filter(
|
||||
(option) =>
|
||||
Array.isArray(value) && value.includes(option.value)
|
||||
)
|
||||
.map((option) => (
|
||||
<span
|
||||
key={option.value}
|
||||
className={`inline-flex min-w-0 shrink-0 items-center gap-1 rounded-md border py-0.5 pl-2 pr-1 text-xs font-medium text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${oneLine ? 'mx-0.5' : ''}`}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleSelect(option.value);
|
||||
}}
|
||||
className="flex items-center rounded-sm px-px text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground"
|
||||
>
|
||||
<Cross2Icon />
|
||||
</span>
|
||||
</span>
|
||||
)),
|
||||
[options, value, handleSelect, oneLine]
|
||||
);
|
||||
|
||||
const isAllSelected = React.useMemo(
|
||||
() =>
|
||||
multiple &&
|
||||
Array.isArray(value) &&
|
||||
options.every((option) => value.includes(option.value)),
|
||||
[options, value, multiple]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'items-center gap-1 overflow-hidden text-sm',
|
||||
multiple
|
||||
? 'flex flex-grow flex-wrap'
|
||||
: 'inline-flex whitespace-nowrap'
|
||||
)}
|
||||
>
|
||||
{value && value.length > 0 ? (
|
||||
multiple ? (
|
||||
oneLine ? (
|
||||
<div className="block w-full min-w-0 shrink-0 truncate">
|
||||
{selectedMultipleOptions}
|
||||
</div>
|
||||
) : (
|
||||
selectedMultipleOptions
|
||||
)
|
||||
) : (
|
||||
options.find((opt) => opt.value === value)
|
||||
?.label
|
||||
)
|
||||
) : (
|
||||
<span className="mr-auto text-muted-foreground">
|
||||
{placeholder}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center self-stretch pl-1 text-muted-foreground/60 hover:text-foreground [&>div]:flex [&>div]:items-center [&>div]:self-stretch">
|
||||
{value && value.length > 0 ? (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleClear();
|
||||
}}
|
||||
>
|
||||
<span className="text-xs">Clear</span>
|
||||
{/* <Cross2Icon className="size-4" /> */}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<CaretSortIcon className="size-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[var(--radix-popover-trigger-width)] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command>
|
||||
<div className="relative">
|
||||
<CommandInput
|
||||
value={searchTerm}
|
||||
onValueChange={(e) => setSearchTerm(e)}
|
||||
ref={ref}
|
||||
placeholder={inputPlaceholder ?? 'Search...'}
|
||||
className="h-9"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<div
|
||||
className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setSearchTerm('')}
|
||||
>
|
||||
<Cross2Icon className="size-4" />
|
||||
</div>
|
||||
)}
|
||||
{!searchTerm &&
|
||||
multiple &&
|
||||
selectAll &&
|
||||
!isAllSelected && (
|
||||
<div
|
||||
className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={() => onSelectAll?.()}
|
||||
>
|
||||
Select All
|
||||
</div>
|
||||
)}
|
||||
{!searchTerm &&
|
||||
multiple &&
|
||||
deselectAll &&
|
||||
isAllSelected && (
|
||||
<div
|
||||
className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={() => onDeselectAll?.()}
|
||||
>
|
||||
Deselect All
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CommandEmpty>
|
||||
{emptyPlaceholder ?? 'No results found.'}
|
||||
</CommandEmpty>
|
||||
|
||||
<ScrollArea>
|
||||
<div className="max-h-64 w-full">
|
||||
<CommandGroup>
|
||||
<CommandList className="max-h-64 w-full">
|
||||
{options.map((option) => {
|
||||
const isSelected =
|
||||
Array.isArray(value) &&
|
||||
value.includes(option.value);
|
||||
return (
|
||||
<CommandItem
|
||||
className="flex items-center"
|
||||
key={option.value}
|
||||
// value={option.value}
|
||||
onSelect={() =>
|
||||
handleSelect(
|
||||
option.value
|
||||
)
|
||||
}
|
||||
>
|
||||
{multiple && (
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
)}
|
||||
>
|
||||
<CheckIcon />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{option.label}
|
||||
</span>
|
||||
{option.description && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">
|
||||
{
|
||||
option.description
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!multiple &&
|
||||
option.value ===
|
||||
value && (
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
option.value ===
|
||||
value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SelectBox.displayName = 'SelectBox';
|
||||
@@ -7,15 +7,20 @@ import { DBIndex } from '@/lib/domain/db-index';
|
||||
import { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
import { Diagram } from '@/lib/domain/diagram';
|
||||
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||
import { DBSchema } from '@/lib/domain/db-schema';
|
||||
|
||||
export interface ChartDBContext {
|
||||
diagramId: string;
|
||||
diagramName: string;
|
||||
databaseType: DatabaseType;
|
||||
tables: DBTable[];
|
||||
schemas: DBSchema[];
|
||||
relationships: DBRelationship[];
|
||||
currentDiagram: Diagram;
|
||||
|
||||
filteredSchemas?: string[];
|
||||
filterSchemas: (schemaIds: string[]) => void;
|
||||
|
||||
// General operations
|
||||
updateDiagramId: (id: string) => Promise<void>;
|
||||
updateDiagramName: (
|
||||
@@ -129,6 +134,9 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
diagramId: '',
|
||||
tables: [],
|
||||
relationships: [],
|
||||
schemas: [],
|
||||
filteredSchemas: [],
|
||||
filterSchemas: emptyFn,
|
||||
currentDiagram: {
|
||||
id: '',
|
||||
name: '',
|
||||
|
||||
@@ -13,12 +13,16 @@ import { Diagram } from '@/lib/domain/diagram';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||
import { DBSchema, schemaNameToSchemaId } from '@/lib/domain/db-schema';
|
||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
|
||||
export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const db = useStorage();
|
||||
const navigate = useNavigate();
|
||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||
const { addUndoAction, resetRedoStack, resetUndoStack } =
|
||||
useRedoUndoStack();
|
||||
const [diagramId, setDiagramId] = useState('');
|
||||
@@ -35,6 +39,56 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const [tables, setTables] = useState<DBTable[]>([]);
|
||||
const [relationships, setRelationships] = useState<DBRelationship[]>([]);
|
||||
|
||||
const defaultSchemaName = defaultSchemas[databaseType];
|
||||
|
||||
const schemas = useMemo(
|
||||
() =>
|
||||
databaseType === DatabaseType.POSTGRESQL ||
|
||||
databaseType === DatabaseType.SQL_SERVER
|
||||
? [
|
||||
...new Set(
|
||||
tables
|
||||
.map((table) => table.schema)
|
||||
.filter((schema) => !!schema) as string[]
|
||||
),
|
||||
]
|
||||
.sort((a, b) =>
|
||||
a === defaultSchemaName ? -1 : a.localeCompare(b)
|
||||
)
|
||||
.map(
|
||||
(schema): DBSchema => ({
|
||||
id: schemaNameToSchemaId(schema),
|
||||
name: schema,
|
||||
tableCount: tables.filter(
|
||||
(table) => table.schema === schema
|
||||
).length,
|
||||
})
|
||||
)
|
||||
: [],
|
||||
[tables, defaultSchemaName, databaseType]
|
||||
);
|
||||
|
||||
const filterSchemas: ChartDBContext['filterSchemas'] = useCallback(
|
||||
(schemaIds) => {
|
||||
setSchemasFilter((prev) => ({
|
||||
...prev,
|
||||
[diagramId]: schemaIds,
|
||||
}));
|
||||
},
|
||||
[diagramId, setSchemasFilter]
|
||||
);
|
||||
|
||||
const filteredSchemas: ChartDBContext['filteredSchemas'] = useMemo(
|
||||
() =>
|
||||
schemas.length > 0
|
||||
? (schemasFilter[diagramId] ?? [
|
||||
schemas.find((s) => s.name === defaultSchemaName)?.id ??
|
||||
schemas[0]?.id,
|
||||
])
|
||||
: undefined,
|
||||
[schemasFilter, diagramId, schemas, defaultSchemaName]
|
||||
);
|
||||
|
||||
const currentDiagram: Diagram = useMemo(
|
||||
() => ({
|
||||
id: diagramId,
|
||||
@@ -1010,6 +1064,9 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
|
||||
tables,
|
||||
relationships,
|
||||
currentDiagram,
|
||||
schemas,
|
||||
filteredSchemas,
|
||||
filterSchemas,
|
||||
updateDiagramId,
|
||||
updateDiagramName,
|
||||
loadDiagram,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createContext } from 'react';
|
||||
import { emptyFn } from '@/lib/utils';
|
||||
import { Theme } from '../theme-context/theme-context';
|
||||
|
||||
export type ScrollAction = 'pan' | 'zoom';
|
||||
|
||||
export type SchemasFilter = Record<string, string[]>;
|
||||
|
||||
export interface LocalConfigContext {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
|
||||
scrollAction: ScrollAction;
|
||||
setScrollAction: (action: ScrollAction) => void;
|
||||
|
||||
schemasFilter: SchemasFilter;
|
||||
setSchemasFilter: React.Dispatch<React.SetStateAction<SchemasFilter>>;
|
||||
}
|
||||
|
||||
export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||
theme: 'system',
|
||||
setTheme: emptyFn,
|
||||
|
||||
scrollAction: 'pan',
|
||||
setScrollAction: emptyFn,
|
||||
|
||||
schemasFilter: {},
|
||||
setSchemasFilter: emptyFn,
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
LocalConfigContext,
|
||||
SchemasFilter,
|
||||
ScrollAction,
|
||||
} from './local-config-context';
|
||||
import { Theme } from '../theme-context/theme-context';
|
||||
|
||||
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [theme, setTheme] = React.useState<Theme>(
|
||||
(localStorage.getItem('theme') as Theme) || 'system'
|
||||
);
|
||||
|
||||
const [scrollAction, setScrollAction] = React.useState<ScrollAction>(
|
||||
(localStorage.getItem('scroll_action') as ScrollAction) || 'pan'
|
||||
);
|
||||
|
||||
const [schemasFilter, setSchemasFilter] = React.useState<SchemasFilter>(
|
||||
JSON.parse(
|
||||
localStorage.getItem('schemas_filter') || '{}'
|
||||
) as SchemasFilter
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('scroll_action', scrollAction);
|
||||
}, [scrollAction]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('schemas_filter', JSON.stringify(schemasFilter));
|
||||
}, [schemasFilter]);
|
||||
|
||||
return (
|
||||
<LocalConfigContext.Provider
|
||||
value={{
|
||||
theme,
|
||||
setTheme,
|
||||
scrollAction,
|
||||
setScrollAction,
|
||||
schemasFilter,
|
||||
setSchemasFilter,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LocalConfigContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
import { emptyFn } from '@/lib/utils';
|
||||
|
||||
export type ScrollActionType = 'pan' | 'zoom';
|
||||
|
||||
export interface ScrollContext {
|
||||
scrollAction: ScrollActionType;
|
||||
setScrollAction: (action: ScrollActionType) => void;
|
||||
}
|
||||
|
||||
export const ScrollContext = createContext<ScrollContext>({
|
||||
scrollAction: 'pan',
|
||||
setScrollAction: emptyFn,
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ScrollContext, ScrollActionType } from './scroll-context';
|
||||
|
||||
export const ScrollProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [scrollAction, setScrollAction] = useState<ScrollActionType>(() => {
|
||||
const savedAction = localStorage.getItem(
|
||||
'scrollAction'
|
||||
) as ScrollActionType | null;
|
||||
return savedAction || 'pan';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('scrollAction', scrollAction);
|
||||
}, [scrollAction]);
|
||||
|
||||
return (
|
||||
<ScrollContext.Provider value={{ scrollAction, setScrollAction }}>
|
||||
{children}
|
||||
</ScrollContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createContext } from 'react';
|
||||
import { emptyFn } from '@/lib/utils';
|
||||
|
||||
export type ThemeType = 'light' | 'dark' | 'system';
|
||||
export type EffectiveThemeType = Exclude<ThemeType, 'system'>;
|
||||
export type Theme = 'light' | 'dark' | 'system';
|
||||
export type EffectiveTheme = Exclude<Theme, 'system'>;
|
||||
|
||||
export interface ThemeContext {
|
||||
theme: ThemeType;
|
||||
setTheme: (theme: ThemeType) => void;
|
||||
effectiveTheme: EffectiveThemeType;
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
effectiveTheme: EffectiveTheme;
|
||||
}
|
||||
|
||||
export const ThemeContext = createContext<ThemeContext>({
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EffectiveThemeType, ThemeContext, ThemeType } from './theme-context';
|
||||
import { EffectiveTheme, ThemeContext } from './theme-context';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
|
||||
export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [theme, setTheme] = useState<ThemeType>(() => {
|
||||
const savedTheme = localStorage.getItem('theme') as ThemeType | null;
|
||||
return savedTheme || 'system';
|
||||
});
|
||||
const { theme, setTheme } = useLocalConfig();
|
||||
const isDarkSystemTheme = useMediaQuery({
|
||||
query: '(prefers-color-scheme: dark)',
|
||||
});
|
||||
@@ -16,10 +14,9 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const systemTheme = isDarkSystemTheme ? 'dark' : 'light';
|
||||
|
||||
const [effectiveTheme, setEffectiveTheme] =
|
||||
useState<EffectiveThemeType>(systemTheme);
|
||||
useState<EffectiveTheme>(systemTheme);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('theme', theme);
|
||||
setEffectiveTheme(theme === 'system' ? systemTheme : theme);
|
||||
}, [theme, systemTheme]);
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { useContext } from 'react';
|
||||
import { LocalConfigContext } from '@/context/local-config-context/local-config-context';
|
||||
|
||||
export const useLocalConfig = () => useContext(LocalConfigContext);
|
||||
@@ -1,4 +0,0 @@
|
||||
import { ScrollContext } from '@/context/scroll-context/scroll-context';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const useScrollAction = () => useContext(ScrollContext);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { DatabaseType } from '../domain/database-type';
|
||||
|
||||
export const defaultSchemas: { [key in DatabaseType]?: string } = {
|
||||
[DatabaseType.POSTGRESQL]: 'public',
|
||||
[DatabaseType.SQL_SERVER]: 'dbo',
|
||||
};
|
||||
@@ -15,7 +15,7 @@ import SqliteLogo2 from '@/assets/sqlite_logo_2.png';
|
||||
import SqlServerLogo2 from '@/assets/sql_server_logo_2.png';
|
||||
import GeneralDBLogo2 from '@/assets/general_db_logo_2.png';
|
||||
import { DatabaseType } from './domain/database-type';
|
||||
import { EffectiveThemeType } from '@/context/theme-context/theme-context';
|
||||
import { EffectiveTheme } from '@/context/theme-context/theme-context';
|
||||
|
||||
export const databaseTypeToLabelMap: Record<DatabaseType, string> = {
|
||||
[DatabaseType.GENERIC]: 'Generic',
|
||||
@@ -46,7 +46,7 @@ export const databaseDarkLogoMap: Record<DatabaseType, string> = {
|
||||
|
||||
export const getDatabaseLogo = (
|
||||
databaseType: DatabaseType,
|
||||
theme: EffectiveThemeType
|
||||
theme: EffectiveTheme
|
||||
) =>
|
||||
theme === 'dark'
|
||||
? databaseDarkLogoMap[databaseType]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface DBSchema {
|
||||
id: string;
|
||||
name: string;
|
||||
tableCount: number;
|
||||
}
|
||||
|
||||
export const schemaNameToSchemaId = (schema: string): string =>
|
||||
schema.toLowerCase().split(' ').join('_');
|
||||
@@ -22,6 +22,7 @@ export interface DBTable {
|
||||
createdAt: number;
|
||||
width?: number;
|
||||
comments?: string;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export const createTablesFromMetadata = ({
|
||||
|
||||
@@ -31,13 +31,17 @@ import { Badge } from '@/components/badge/badge';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DBTable } from '@/lib/domain/db-table';
|
||||
import { useScrollAction } from '@/hooks/use-scroll-action';
|
||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
|
||||
|
||||
type AddEdgeParams = Parameters<typeof addEdge<TableEdgeType>>[0];
|
||||
|
||||
const initialEdges: TableEdgeType[] = [];
|
||||
|
||||
const tableToTableNode = (table: DBTable): TableNodeType => ({
|
||||
const tableToTableNode = (
|
||||
table: DBTable,
|
||||
filteredSchemas?: string[]
|
||||
): TableNodeType => ({
|
||||
id: table.id,
|
||||
type: 'table',
|
||||
position: { x: table.x, y: table.y },
|
||||
@@ -45,6 +49,10 @@ const tableToTableNode = (table: DBTable): TableNodeType => ({
|
||||
table,
|
||||
},
|
||||
width: table.width ?? MIN_TABLE_SIZE,
|
||||
hidden:
|
||||
!!table.schema &&
|
||||
!!filteredSchemas &&
|
||||
!filteredSchemas.includes(schemaNameToSchemaId(table.schema)),
|
||||
});
|
||||
|
||||
export interface CanvasProps {
|
||||
@@ -53,6 +61,7 @@ export interface CanvasProps {
|
||||
|
||||
export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
const { getEdge, getInternalNode, fitView } = useReactFlow();
|
||||
const { filteredSchemas } = useChartDB();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
@@ -65,14 +74,14 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
} = useChartDB();
|
||||
const { showSidePanel } = useLayout();
|
||||
const { effectiveTheme } = useTheme();
|
||||
const { scrollAction } = useScrollAction();
|
||||
const { scrollAction } = useLocalConfig();
|
||||
const { isMd: isDesktop } = useBreakpoint('md');
|
||||
const nodeTypes = useMemo(() => ({ table: TableNode }), []);
|
||||
const edgeTypes = useMemo(() => ({ 'table-edge': TableEdge }), []);
|
||||
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<TableNodeType>(
|
||||
initialTables.map(tableToTableNode)
|
||||
initialTables.map((table) => tableToTableNode(table, filteredSchemas))
|
||||
);
|
||||
const [edges, setEdges, onEdgesChange] =
|
||||
useEdgesState<TableEdgeType>(initialEdges);
|
||||
@@ -82,11 +91,13 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
}, [initialTables]);
|
||||
|
||||
useEffect(() => {
|
||||
const initialNodes = initialTables.map(tableToTableNode);
|
||||
const initialNodes = initialTables.map((table) =>
|
||||
tableToTableNode(table, filteredSchemas)
|
||||
);
|
||||
if (equal(initialNodes, nodes)) {
|
||||
setIsInitialLoadingNodes(false);
|
||||
}
|
||||
}, [initialTables, nodes]);
|
||||
}, [initialTables, nodes, filteredSchemas]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialLoadingNodes) {
|
||||
@@ -118,8 +129,10 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
}, [relationships, setEdges]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodes(tables.map(tableToTableNode));
|
||||
}, [tables, setNodes]);
|
||||
setNodes(
|
||||
tables.map((table) => tableToTableNode(table, filteredSchemas))
|
||||
);
|
||||
}, [tables, setNodes, filteredSchemas]);
|
||||
|
||||
const onConnectHandler = useCallback(
|
||||
async (params: AddEdgeParams) => {
|
||||
|
||||
@@ -18,20 +18,29 @@ import {
|
||||
export interface RelationshipsSectionProps {}
|
||||
|
||||
export const RelationshipsSection: React.FC<RelationshipsSectionProps> = () => {
|
||||
const { relationships } = useChartDB();
|
||||
const { relationships, filteredSchemas } = useChartDB();
|
||||
const [filterText, setFilterText] = React.useState('');
|
||||
const { closeAllRelationshipsInSidebar } = useLayout();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const filteredRelationships = useMemo(() => {
|
||||
const filter: (relationship: DBRelationship) => boolean = (
|
||||
const filterName: (relationship: DBRelationship) => boolean = (
|
||||
relationship
|
||||
) =>
|
||||
!filterText?.trim?.() ||
|
||||
relationship.name.toLowerCase().includes(filterText.toLowerCase());
|
||||
|
||||
return relationships.filter(filter);
|
||||
}, [relationships, filterText]);
|
||||
const filterSchema: (relationship: DBRelationship) => boolean = (
|
||||
relationship
|
||||
) =>
|
||||
!filteredSchemas ||
|
||||
!relationship.sourceSchema ||
|
||||
!relationship.targetSchema ||
|
||||
(filteredSchemas.includes(relationship.sourceSchema) &&
|
||||
filteredSchemas.includes(relationship.targetSchema));
|
||||
|
||||
return relationships.filter(filterSchema).filter(filterName);
|
||||
}, [relationships, filterText, filteredSchemas]);
|
||||
|
||||
return (
|
||||
<section className="flex flex-1 flex-col overflow-hidden px-2">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -12,14 +12,65 @@ import { RelationshipsSection } from './relationships-section/relationships-sect
|
||||
import { useLayout } from '@/hooks/use-layout';
|
||||
import { SidebarSection } from '@/context/layout-context/layout-context';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SelectBox, SelectBoxOption } from '@/components/select-box/select-box';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
|
||||
export interface SidePanelProps {}
|
||||
|
||||
export const SidePanel: React.FC<SidePanelProps> = () => {
|
||||
const { t } = useTranslation();
|
||||
const { schemas, filterSchemas, filteredSchemas } = useChartDB();
|
||||
const { selectSidebarSection, selectedSidebarSection } = useLayout();
|
||||
|
||||
const schemasOptions: SelectBoxOption[] = useMemo(
|
||||
() =>
|
||||
schemas.map(
|
||||
(schema): SelectBoxOption => ({
|
||||
label: schema.name,
|
||||
value: schema.id,
|
||||
description: `(${schema.tableCount} tables)`,
|
||||
})
|
||||
),
|
||||
[schemas]
|
||||
);
|
||||
|
||||
const deselectAllSchemas = () => {
|
||||
filterSchemas([]);
|
||||
};
|
||||
|
||||
const selectAllSchemas = () => {
|
||||
filterSchemas(schemas.map((schema) => schema.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="flex h-full flex-col overflow-hidden">
|
||||
{schemasOptions.length > 0 && (
|
||||
<div className="flex items-center justify-center border-b pl-3 pt-0.5">
|
||||
<div className="shrink-0 text-sm font-semibold">
|
||||
Schema:
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1">
|
||||
<SelectBox
|
||||
oneLine
|
||||
className="w-full rounded-none border-none"
|
||||
selectAll
|
||||
deselectAll
|
||||
options={schemasOptions}
|
||||
value={filteredSchemas ?? []}
|
||||
onChange={(values) => {
|
||||
filterSchemas(values as string[]);
|
||||
}}
|
||||
onSelectAll={selectAllSchemas}
|
||||
onDeselectAll={deselectAllSchemas}
|
||||
placeholder="Filter by schema"
|
||||
inputPlaceholder="Search schema"
|
||||
emptyPlaceholder="No schema found."
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-center border-b pt-0.5">
|
||||
<Select
|
||||
value={selectedSidebarSection}
|
||||
|
||||
@@ -19,18 +19,23 @@ import {
|
||||
export interface TablesSectionProps {}
|
||||
|
||||
export const TablesSection: React.FC<TablesSectionProps> = () => {
|
||||
const { createTable, tables } = useChartDB();
|
||||
const { createTable, tables, filteredSchemas } = useChartDB();
|
||||
const { t } = useTranslation();
|
||||
const { closeAllTablesInSidebar } = useLayout();
|
||||
const [filterText, setFilterText] = React.useState('');
|
||||
|
||||
const filteredTables = useMemo(() => {
|
||||
const filter: (table: DBTable) => boolean = (table) =>
|
||||
const filterTableName: (table: DBTable) => boolean = (table) =>
|
||||
!filterText?.trim?.() ||
|
||||
table.name.toLowerCase().includes(filterText.toLowerCase());
|
||||
|
||||
return tables.filter(filter);
|
||||
}, [tables, filterText]);
|
||||
const filterSchema: (table: DBTable) => boolean = (table) =>
|
||||
!filteredSchemas ||
|
||||
!table.schema ||
|
||||
filteredSchemas.includes(table.schema);
|
||||
|
||||
return tables.filter(filterSchema).filter(filterTableName);
|
||||
}, [tables, filterText, filteredSchemas]);
|
||||
|
||||
return (
|
||||
<section
|
||||
|
||||
@@ -45,7 +45,7 @@ import { useLayout } from '@/hooks/use-layout';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import { enMetadata } from '@/i18n/locales/en';
|
||||
import { esMetadata } from '@/i18n/locales/es';
|
||||
import { useScrollAction } from '@/hooks/use-scroll-action';
|
||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
|
||||
export interface TopNavbarProps {}
|
||||
|
||||
@@ -66,7 +66,7 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
||||
} = useDialog();
|
||||
const { setTheme, theme } = useTheme();
|
||||
const { hideSidePanel, isSidePanelShowed, showSidePanel } = useLayout();
|
||||
const { scrollAction, setScrollAction } = useScrollAction();
|
||||
const { scrollAction, setScrollAction } = useLocalConfig();
|
||||
const { effectiveTheme } = useTheme();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { redo, undo, hasRedo, hasUndo } = useHistory();
|
||||
|
||||
+24
-22
@@ -5,7 +5,6 @@ import { EditorPage } from './pages/editor-page/editor-page';
|
||||
import { ChartDBProvider } from './context/chartdb-context/chartdb-provider';
|
||||
import { ReactFlowProvider } from '@xyflow/react';
|
||||
import { StorageProvider } from './context/storage-context/storage-provider';
|
||||
import { ScrollProvider } from './context/scroll-context/scroll-provider';
|
||||
import { ConfigProvider } from './context/config-context/config-provider';
|
||||
import { HistoryProvider } from './context/history-context/history-provider';
|
||||
import { RedoUndoStackProvider } from './context/history-context/redo-undo-stack-provider';
|
||||
@@ -16,6 +15,7 @@ import { FullScreenLoaderProvider } from './context/full-screen-spinner-context/
|
||||
import { ExamplesPage } from './pages/examples-page/examples-page';
|
||||
import { KeyboardShortcutsProvider } from './context/keyboard-shortcuts-context/keyboard-shortcuts-provider';
|
||||
import { ThemeProvider } from './context/theme-context/theme-provider';
|
||||
import { LocalConfigProvider } from './context/local-config-context/local-config-provider';
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
...['', 'diagrams/:diagramId'].map((path) => ({
|
||||
@@ -23,14 +23,14 @@ const routes: RouteObject[] = [
|
||||
element: (
|
||||
<FullScreenLoaderProvider>
|
||||
<LayoutProvider>
|
||||
<StorageProvider>
|
||||
<ConfigProvider>
|
||||
<RedoUndoStackProvider>
|
||||
<ChartDBProvider>
|
||||
<HistoryProvider>
|
||||
<ThemeProvider>
|
||||
<DialogProvider>
|
||||
<ScrollProvider>
|
||||
<LocalConfigProvider>
|
||||
<StorageProvider>
|
||||
<ConfigProvider>
|
||||
<RedoUndoStackProvider>
|
||||
<ChartDBProvider>
|
||||
<HistoryProvider>
|
||||
<ThemeProvider>
|
||||
<DialogProvider>
|
||||
<ReactFlowProvider>
|
||||
<ExportImageProvider>
|
||||
<KeyboardShortcutsProvider>
|
||||
@@ -38,14 +38,14 @@ const routes: RouteObject[] = [
|
||||
</KeyboardShortcutsProvider>
|
||||
</ExportImageProvider>
|
||||
</ReactFlowProvider>
|
||||
</ScrollProvider>
|
||||
</DialogProvider>
|
||||
</ThemeProvider>
|
||||
</HistoryProvider>
|
||||
</ChartDBProvider>
|
||||
</RedoUndoStackProvider>
|
||||
</ConfigProvider>
|
||||
</StorageProvider>
|
||||
</DialogProvider>
|
||||
</ThemeProvider>
|
||||
</HistoryProvider>
|
||||
</ChartDBProvider>
|
||||
</RedoUndoStackProvider>
|
||||
</ConfigProvider>
|
||||
</StorageProvider>
|
||||
</LocalConfigProvider>
|
||||
</LayoutProvider>
|
||||
</FullScreenLoaderProvider>
|
||||
),
|
||||
@@ -53,11 +53,13 @@ const routes: RouteObject[] = [
|
||||
{
|
||||
path: 'examples',
|
||||
element: (
|
||||
<StorageProvider>
|
||||
<ThemeProvider>
|
||||
<ExamplesPage />
|
||||
</ThemeProvider>
|
||||
</StorageProvider>
|
||||
<LocalConfigProvider>
|
||||
<StorageProvider>
|
||||
<ThemeProvider>
|
||||
<ExamplesPage />
|
||||
</ThemeProvider>
|
||||
</StorageProvider>
|
||||
</LocalConfigProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user