Compare commits

...

19 Commits

Author SHA1 Message Date
Guy Ben-Aharon
acf8ade23c chore(main): release 1.0.1 (#320) 2024-11-07 00:05:51 +02:00
Guy Ben-Aharon
aa884b49ce fix(offline): add support when running on isolated network (#359) 2024-11-06 23:56:05 +02:00
Jonathan Fishner
acb736e44f fix(smart query): import postgres FKs (#357) 2024-11-06 22:49:32 +02:00
Guy Ben-Aharon
180886c588 fix(template): separator in case of empty url (#355) 2024-11-06 18:24:25 +02:00
Guy Ben-Aharon
e993476fad add template url (#354) 2024-11-06 18:07:02 +02:00
Guy Ben-Aharon
efaddeebb4 fix(templates): align database icon (#351) 2024-11-06 13:41:28 +02:00
Francis Chartrand
93f623a13a fix(select-box): allow using tab & space to show choices (#336)
* styles(select-box): allow using tab & space to show choices

* use code instead of key

---------

Co-authored-by: Guy Ben-Aharon <guybenah@gmail.com>
2024-11-06 13:29:25 +02:00
Guy Ben-Aharon
87a40cff61 fix: open default diagram after deleting current diagram (#350) 2024-11-06 12:01:49 +02:00
Guy Ben-Aharon
f00c9b9a03 refactor languages menu (#347) 2024-11-06 10:33:44 +02:00
Помаранча
20b2ae436c Add uk language (#338)
* Create uk.ts

added Ukrainian language. I don't know what kind of service it is, but I just helped with the translation into my native language

* Update uk.ts

now all untranslated item (2) is translated

* fix build

* add language to menu

---------

Co-authored-by: Guy Ben-Aharon <guybenah@gmail.com>
2024-11-06 10:20:14 +02:00
Guy Ben-Aharon
820a4640da template title change (#346) 2024-11-06 09:52:21 +02:00
Jonathan Fishner
0193853035 add pokemon database to our templates (#335) 2024-11-05 21:23:31 +02:00
Jonathan Fishner
b40344675e Update README.md (#332) 2024-11-05 14:57:24 +02:00
Guy Ben-Aharon
df7e687f61 chartdb.png image update (#330) 2024-11-05 14:03:55 +02:00
Guy Ben-Aharon
ad10d26f13 chartdb.png image name update (#329) 2024-11-05 13:58:48 +02:00
Jonathan Fishner
588c64b380 Tempaltes keywords (#325)
* fix for tempaltes keywords

* remove keywords

* update templates description

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-05 13:40:57 +02:00
Guy Ben-Aharon
3d3efc5e82 add canonical link to templates (#327) 2024-11-05 10:54:05 +02:00
Guy Ben-Aharon
d8a20ebbd9 fix(templates): fetch templates data from router (#321) 2024-11-04 18:01:04 +02:00
Jonathan Fishner
ebce8827ea fix(templates): add two more templates (Airbnb, Wordpress) (#317)
* add two more templates (Airbnb, Wordpress)

* fix slugs

* fix templates sizes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-04 15:41:51 +02:00
31 changed files with 6337 additions and 242 deletions

View File

@@ -1,5 +1,19 @@
# Changelog
## [1.0.1](https://github.com/chartdb/chartdb/compare/v1.0.0...v1.0.1) (2024-11-06)
### Bug Fixes
* **offline:** add support when running on isolated network ([#359](https://github.com/chartdb/chartdb/issues/359)) ([aa884b4](https://github.com/chartdb/chartdb/commit/aa884b49ce16d70f67881bdc940993c1fe901796))
* open default diagram after deleting current diagram ([#350](https://github.com/chartdb/chartdb/issues/350)) ([87a40cf](https://github.com/chartdb/chartdb/commit/87a40cff615b04b678642ba2d6e097c38b26d239))
* **select-box:** allow using tab & space to show choices ([#336](https://github.com/chartdb/chartdb/issues/336)) ([93f623a](https://github.com/chartdb/chartdb/commit/93f623a13a61e9143638fbe7e8346f07e37a26b2))
* **smart query:** import postgres FKs ([#357](https://github.com/chartdb/chartdb/issues/357)) ([acb736e](https://github.com/chartdb/chartdb/commit/acb736e44fd50d29a85b4eff42e20780aef710ed))
* **templates:** add two more templates (Airbnb, Wordpress) ([#317](https://github.com/chartdb/chartdb/issues/317)) ([ebce882](https://github.com/chartdb/chartdb/commit/ebce8827eab049eefa0eebcb0ec2540698bc0e15))
* **templates:** align database icon ([#351](https://github.com/chartdb/chartdb/issues/351)) ([efaddee](https://github.com/chartdb/chartdb/commit/efaddeebb4f24235d82f4e2bf7423fbf48b97187))
* **template:** separator in case of empty url ([#355](https://github.com/chartdb/chartdb/issues/355)) ([180886c](https://github.com/chartdb/chartdb/commit/180886c5882f2329c797fc284b255012d21f5b5c))
* **templates:** fetch templates data from router ([#321](https://github.com/chartdb/chartdb/issues/321)) ([d8a20eb](https://github.com/chartdb/chartdb/commit/d8a20ebbd9118989690a40fcd3aa59fb156b446f))
## 1.0.0 (2024-11-04)

View File

@@ -15,8 +15,8 @@
<h3 align="center">
<a href="https://discord.gg/QeFwyWSKwC">Community</a> &bull;
<a href="https://www.chartdb.io">Website</a> &bull;
<a href="https://app.chartdb.io/examples">Demo</a>
<a href="https://www.chartdb.io?ref=github_readme">Website</a> &bull;
<a href="https://app.chartdb.io?ref=github_readme">Demo</a>
</h3>
<h4 align="center">
@@ -38,7 +38,7 @@
---
<p align="center">
<img width='700px' src="./public/ChartDB.png">
<img width='700px' src="./public/chartdb.png">
</p>
### 🎉 ChartDB
@@ -71,7 +71,7 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
## Getting Started
Use the [cloud version](https://app.chartdb.io/) or deploy locally:
Use the [cloud version](https://app.chartdb.io?ref=github_readme_2) or deploy locally:
### How To Use
@@ -105,7 +105,7 @@ Open your browser and navigate to `http://localhost:8080`.
## Try it on our website
1. Go to [ChartDB.io](https://chartdb.io)
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)
2. Click "Go to app"
3. Choose the database that you are using.
4. Take the magic query and run it in your database.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "chartdb",
"version": "1.0.0",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.0.0",
"version": "1.0.1",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dnd-kit/sortable": "^8.0.0",

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.0.0",
"version": "1.0.1",
"type": "module",
"scripts": {
"dev": "vite",

View File

Before

Width:  |  Height:  |  Size: 882 KiB

After

Width:  |  Height:  |  Size: 882 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

View File

@@ -9,6 +9,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
import { useTranslation } from 'react-i18next';
import { DarkTheme } from './themes/dark';
import { LightTheme } from './themes/light';
import './config.ts';
export interface CodeSnippetProps {
className?: string;

View File

@@ -156,9 +156,19 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
[options, value, multiple]
);
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent) => {
if (!isOpen && e.code.toLowerCase() === 'space') {
e.preventDefault();
onOpenChange(true);
}
},
[isOpen, onOpenChange]
);
return (
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
<PopoverTrigger asChild>
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
<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 ${disabled ? 'bg-muted pointer-events-none' : ''}`,

View File

@@ -11,8 +11,6 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
import { useStorage } from '@/hooks/use-storage';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
import type { Diagram } from '@/lib/domain/diagram';
import { useNavigate } from 'react-router-dom';
import { useConfig } from '@/hooks/use-config';
import type { DatabaseEdition } from '@/lib/domain/database-edition';
import type { DBSchema } from '@/lib/domain/db-schema';
import {
@@ -34,13 +32,11 @@ export const ChartDBProvider: React.FC<
> = ({ children, diagram, readonly }) => {
let db = useStorage();
const events = useEventEmitter<ChartDBEvent>();
const navigate = useNavigate();
const { setSchemasFilter, schemasFilter } = useLocalConfig();
const { addUndoAction, resetRedoStack, resetUndoStack } =
useRedoUndoStack();
const [diagramId, setDiagramId] = useState('');
const [diagramName, setDiagramName] = useState('');
const { updateConfig } = useConfig();
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
const [diagramUpdatedAt, setDiagramUpdatedAt] = useState<Date>(new Date());
const [databaseType, setDatabaseType] = useState<DatabaseType>(
@@ -173,34 +169,13 @@ export const ChartDBProvider: React.FC<
resetRedoStack();
resetUndoStack();
const [config] = await Promise.all([
db.getConfig(),
await Promise.all([
db.deleteDiagramTables(diagramId),
db.deleteDiagramRelationships(diagramId),
db.deleteDiagram(diagramId),
db.deleteDiagramDependencies(diagramId),
]);
if (config?.defaultDiagramId === diagramId) {
const diagrams = await db.listDiagrams();
if (diagrams.length > 0) {
const defaultDiagramId = diagrams[0].id;
await updateConfig({ defaultDiagramId });
navigate(`/diagrams/${defaultDiagramId}`);
} else {
await updateConfig({ defaultDiagramId: '' });
navigate('/');
}
}
}, [
db,
diagramId,
navigate,
resetRedoStack,
resetUndoStack,
updateConfig,
]);
}, [db, diagramId, resetRedoStack, resetUndoStack]);
const updateDiagramUpdatedAt: ChartDBContext['updateDiagramUpdatedAt'] =
useCallback(async () => {

View File

@@ -18,7 +18,7 @@ export const HelmetData: React.FC = () => (
/>
<meta
property="og:image"
content="https://app.chartdb.io/ChartDB.png"
content="https://app.chartdb.io/chartdb.png"
/>
<meta property="og:url" content="https://app.chartdb.io" />
<meta name="twitter:card" content="summary_large_image" />
@@ -32,7 +32,7 @@ export const HelmetData: React.FC = () => (
/>
<meta
name="twitter:image"
content="https://github.com/chartdb/chartdb/raw/main/public/ChartDB.png"
content="https://github.com/chartdb/chartdb/raw/main/public/chartdb.png"
/>
<title>ChartDB - Database schema diagrams visualizer</title>
</Helmet>

View File

@@ -1,12 +1,25 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import type { LanguageMetadata } from './types';
import { en, enMetadata } from './locales/en';
import { es } from './locales/es';
import { fr } from './locales/fr';
import { de } from './locales/de';
import { hi } from './locales/hi';
import { ja } from './locales/ja';
import { pt_BR } from './locales/pt_BR';
import { es, esMetadata } from './locales/es';
import { fr, frMetadata } from './locales/fr';
import { de, deMetadata } from './locales/de';
import { hi, hiMetadata } from './locales/hi';
import { ja, jaMetadata } from './locales/ja';
import { pt_BR, pt_BRMetadata } from './locales/pt_BR';
import { uk, ukMetadata } from './locales/uk';
export const languages: LanguageMetadata[] = [
enMetadata,
esMetadata,
frMetadata,
deMetadata,
hiMetadata,
jaMetadata,
pt_BRMetadata,
ukMetadata,
];
const resources = {
en,
@@ -16,6 +29,7 @@ const resources = {
hi,
ja,
pt_BR,
uk,
};
i18n.use(initReactI18next).init({

354
src/i18n/locales/uk.ts Normal file
View File

@@ -0,0 +1,354 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const uk: LanguageTranslation = {
translation: {
menu: {
file: {
file: 'файл',
new: 'новий',
open: 'відкрити',
save: 'зберегти',
import_database: 'Імпорт бази даних',
export_sql: 'Експорт SQL',
export_as: 'Експортувати як',
delete_diagram: 'Видалити діаграму',
exit: 'вийти',
},
edit: {
edit: 'редагувати',
undo: 'Скасувати',
redo: 'Повторити',
clear: 'очистити',
},
view: {
view: 'переглянути',
show_sidebar: 'Показати бічну панель',
hide_sidebar: 'Приховати бічну панель',
hide_cardinality: 'Приховати потужність',
show_cardinality: 'Показати кардинальність',
zoom_on_scroll: 'Збільшити прокручування',
theme: 'Тема',
change_language: 'Мова',
show_dependencies: 'Показати залежності',
hide_dependencies: 'Приховати залежності',
},
help: {
help: 'Допомога',
visit_website: 'Відвідайте ChartDB',
join_discord: 'Приєднуйтесь до нас в Діскорд',
schedule_a_call: 'Поговоріть з нами!',
},
},
delete_diagram_alert: {
title: 'Видалити діаграму',
description:
'Цю дію не можна скасувати. Це призведе до остаточного видалення діаграми.',
cancel: 'Скасувати',
delete: 'Видалити',
},
clear_diagram_alert: {
title: 'Чітка діаграма',
description:
'Цю дію не можна скасувати. Це назавжди видалить усі дані на діаграмі.',
cancel: 'Скасувати',
clear: 'очистити',
},
reorder_diagram_alert: {
title: 'Діаграма зміни порядку',
description:
'Ця дія перевпорядкує всі таблиці на діаграмі. Хочете продовжити?',
reorder: 'Змінити порядок',
cancel: 'Скасувати',
},
multiple_schemas_alert: {
title: 'Кілька схем',
description:
'{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.',
dont_show_again: 'Більше не показувати',
change_schema: 'Зміна',
none: 'немає',
},
theme: {
system: 'система',
light: 'світлий',
dark: 'Темний',
},
zoom: {
on: 'увімкнути',
off: 'вимкнути',
},
last_saved: 'Востаннє збережено',
saved: 'Збережено',
diagrams: 'Діаграми',
loading_diagram: 'Діаграма завантаження...',
deselect_all: 'Зняти вибір із усіх',
select_all: 'Вибрати усі',
clear: 'Очистити',
show_more: 'показати більше',
show_less: 'Показати менше',
copy_to_clipboard: 'Копіювати в буфер обміну',
copied: 'Скопійовано!',
side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фільтрувати за схемою',
search_schema: 'Схема пошуку...',
no_schemas_found: 'Схеми не знайдено.',
view_all_options: 'Переглянути всі параметри...',
tables_section: {
tables: 'Таблиці',
add_table: 'Додати таблицю',
filter: 'фільтр',
collapse: 'Згорнути все',
table: {
fields: 'поля',
nullable: 'Зведений нанівець?',
primary_key: 'Первинний ключ',
indexes: 'Індекси',
comments: 'Коментарі',
no_comments: 'Без коментарів',
add_field: 'Додати поле',
add_index: 'Додати індекс',
index_select_fields: 'Виберіть поля',
no_types_found: 'Типи не знайдено',
field_name: "Ім'я",
field_type: 'Тип',
field_actions: {
title: 'Атрибути полів',
unique: 'Унікальний',
comments: 'Коментарі',
no_comments: 'Без коментарів',
delete_field: 'Видалити поле',
},
index_actions: {
title: 'Атрибути індексу',
name: "Ім'я",
unique: 'Унікальний',
delete_index: 'Видалити індекс',
},
table_actions: {
title: 'Дії таблиці',
change_schema: 'Змінити схему',
add_field: 'Додати поле',
add_index: 'Додати індекс',
delete_table: 'Видалити таблицю',
},
},
empty_state: {
title: 'Без таблиць',
description: 'Щоб почати, створіть таблицю',
},
},
relationships_section: {
relationships: 'стосунки',
filter: 'фільтр',
add_relationship: "Додати зв'язок",
collapse: 'Згорнути все',
relationship: {
primary: 'Первинна таблиця',
foreign: 'Посилання на таблицю',
cardinality: 'Кардинальність',
delete_relationship: 'Видалити',
relationship_actions: {
title: 'Дії',
delete_relationship: 'Видалити',
},
},
empty_state: {
title: 'Жодних стосунків',
description: 'Створіть зв’язок для з’єднання таблиць',
},
},
dependencies_section: {
dependencies: 'Залежності',
filter: 'фільтр',
collapse: 'Згорнути все',
dependency: {
table: 'Таблиця',
dependent_table: 'Залежний вид',
delete_dependency: 'Видалити',
dependency_actions: {
title: 'Дії',
delete_dependency: 'Видалити',
},
},
empty_state: {
title: 'Жодних залежностей',
description: 'Створіть подання, щоб почати',
},
},
},
toolbar: {
zoom_in: 'Збільшити',
zoom_out: 'Зменшити',
save: 'зберегти',
show_all: 'Показати все',
undo: 'Скасувати',
redo: 'Повторити',
reorder_diagram: 'Діаграма зміни порядку',
highlight_overlapping_tables: 'Виділіть таблиці, що перекриваються',
},
new_diagram_dialog: {
database_selection: {
title: 'Що таке ваша база даних?',
description:
'Кожна база даних має свої унікальні особливості та можливості.',
check_examples_long: 'Перевірте приклади',
check_examples_short: 'Приклади',
},
import_database: {
title: 'Імпортуйте вашу базу даних',
database_edition: 'Редакція бази даних:',
step_1: 'Запустіть цей сценарій у своїй базі даних:',
step_2: 'Вставте сюди результат сценарію:',
script_results_placeholder: 'Результати сценарію тут...',
ssms_instructions: {
button_text: 'SSMS Інструкції',
title: 'Інструкції',
step_1: 'Перейдіть до Інструменти > Опції > Результати запиту > SQL Сервер.',
step_2: 'Якщо ви використовуєте «Результати в сітку», змініть максимальну кількість символів, отриманих для даних, що не є XML (встановіть на 9999999).',
},
instructions_link: 'Потрібна допомога? Подивіться як',
check_script_result: 'Перевірте результат сценарію',
},
cancel: 'Скасувати',
back: 'Назад',
empty_diagram: 'Порожня діаграма',
continue: 'Продовжити',
import: 'Імпорт',
},
open_diagram_dialog: {
title: 'Відкрита діаграма',
description:
'Виберіть діаграму, яку потрібно відкрити, зі списку нижче.',
table_columns: {
name: "Ім'я",
created_at: 'Створено в',
last_modified: 'Востаннє змінено',
tables_count: 'Таблиці',
},
cancel: 'Скасувати',
open: 'Відкрити',
},
export_sql_dialog: {
title: 'Експорт SQL',
description:
'Експортуйте свою схему діаграми в {{databaseType}} сценарій',
close: 'Закрити',
loading: {
text: 'ШІ створює SQL для {{databaseType}}...',
description: 'Це має зайняти до 30 секунд.',
},
error: {
message:
"Помилка створення сценарію SQL. Спробуйте пізніше або <0>зв'яжіться з нами</0>.",
description:
'Не соромтеся використовувати свій OPENAI_TOKEN, дивіться посібник <0>тут</0>.',
},
},
create_relationship_dialog: {
title: 'Створити відносини',
primary_table: 'Первинна таблиця',
primary_field: 'Первинне поле',
referenced_table: 'Посилання на таблицю',
referenced_field: 'Поле посилання',
primary_table_placeholder: 'Виберіть таблицю',
primary_field_placeholder: 'Виберіть поле',
referenced_table_placeholder: 'Виберіть таблицю',
referenced_field_placeholder: 'Виберіть поле',
no_tables_found: 'Таблиці не знайдено',
no_fields_found: 'Поля не знайдено',
create: 'Створити',
cancel: 'Скасувати',
},
import_database_dialog: {
title: 'Імпорт до поточної діаграми',
override_alert: {
title: 'Імпорт бази даних',
content: {
alert: 'Імпортування цієї діаграми вплине на наявні таблиці та зв’язки.',
new_tables:
'<bold>{{newTablesNumber}}</bold> будуть додані нові таблиці.',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> будуть створені нові відносини.',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> таблиці будуть перезаписані.',
proceed: 'Ви хочете продовжити?',
},
import: 'Імпорт',
cancel: 'Скасувати',
},
},
export_image_dialog: {
title: 'Експорт зображення',
description: 'Виберіть коефіцієнт масштабування для експорту:',
scale_1x: '1x Регулярний',
scale_2x: '2x (Рекомендовано)',
scale_3x: '3x',
scale_4x: '4x',
cancel: 'Скасувати',
export: 'Експорт',
},
new_table_schema_dialog: {
title: 'Виберіть Схему',
description:
'Наразі відображається кілька схем. Виберіть один для нової таблиці.',
cancel: 'Скасувати',
confirm: 'Підтвердити',
},
update_table_schema_dialog: {
title: 'Змінити схему',
description: 'Оновити таблицю "{{tableName}}" схему',
cancel: 'Скасувати',
confirm: 'Змінити',
},
star_us_dialog: {
title: 'Допоможіть нам покращитися!',
description: 'Хочете позначити нас на Ґітхаб? Це лише один клік!',
close: 'Не зараз',
confirm: 'звичайно!',
},
relationship_type: {
one_to_one: 'Один до одного',
one_to_many: 'Один до багатьох',
many_to_one: 'Багато до одного',
many_to_many: 'Багато до багатьох',
},
canvas_context_menu: {
new_table: 'Нова таблиця',
new_relationship: 'Нові стосунки',
},
table_node_context_menu: {
edit_table: 'Редагувати таблицю',
delete_table: 'Видалити таблицю',
},
},
};
export const ukMetadata: LanguageMetadata = {
name: 'Українська',
code: 'uk',
};

View File

@@ -68,29 +68,46 @@ WITH fk_info${databaseEdition ? '_' + databaseEdition : ''} AS (
',"fk_def":"', replace(fk_def, '"', ''),
'"}')), ',') as fk_metadata
FROM (
SELECT connamespace::regnamespace::text AS schema_name,
conname AS foreign_key_name,
CASE
WHEN strpos(conrelid::regclass::text, '.') > 0
THEN split_part(conrelid::regclass::text, '.', 2)
ELSE conrelid::regclass::text
END AS table_name,
(regexp_matches(pg_get_constraintdef(oid), '(?i)FOREIGN KEY \\("?(\\w+)"?\\) REFERENCES (?:"?(\\w+)"?\\.)?"?(\\w+)"?\\("?(\\w+)"?\\)', 'g'))[1] AS fk_column,
(regexp_matches(pg_get_constraintdef(oid), '(?i)FOREIGN KEY \\("?(\\w+)"?\\) REFERENCES (?:"?(\\w+)"?\\.)?"?(\\w+)"?\\("?(\\w+)"?\\)', 'g'))[2] AS reference_schema,
(regexp_matches(pg_get_constraintdef(oid), '(?i)FOREIGN KEY \\("?(\\w+)"?\\) REFERENCES (?:"?(\\w+)"?\\.)?"?(\\w+)"?\\("?(\\w+)"?\\)', 'g'))[3] AS reference_table,
(regexp_matches(pg_get_constraintdef(oid), '(?i)FOREIGN KEY \\("?(\\w+)"?\\) REFERENCES (?:"?(\\w+)"?\\.)?"?(\\w+)"?\\("?(\\w+)"?\\)', 'g'))[4] AS reference_column,
pg_get_constraintdef(oid) as fk_def
FROM
pg_constraint
WHERE
contype = 'f'
AND connamespace::regnamespace::text NOT IN ('information_schema', 'pg_catalog')${
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
? timescaleFilters
: databaseEdition === DatabaseEdition.POSTGRESQL_SUPABASE
? supabaseFilters
: ''
}
SELECT c.conname AS foreign_key_name,
n.nspname AS schema_name,
CASE
WHEN position('.' in conrelid::regclass::text) > 0
THEN split_part(conrelid::regclass::text, '.', 2)
ELSE conrelid::regclass::text
END AS table_name,
a.attname AS fk_column,
nr.nspname AS reference_schema,
CASE
WHEN position('.' in confrelid::regclass::text) > 0
THEN split_part(confrelid::regclass::text, '.', 2)
ELSE confrelid::regclass::text
END AS reference_table,
af.attname AS reference_column,
pg_get_constraintdef(c.oid) as fk_def
FROM
pg_constraint AS c
JOIN
pg_attribute AS a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
JOIN
pg_class AS cl ON cl.oid = c.conrelid
JOIN
pg_namespace AS n ON n.oid = cl.relnamespace
JOIN
pg_attribute AS af ON af.attnum = ANY(c.confkey) AND af.attrelid = c.confrelid
JOIN
pg_class AS clf ON clf.oid = c.confrelid
JOIN
pg_namespace AS nr ON nr.oid = clf.relnamespace
WHERE
c.contype = 'f'
AND connamespace::regnamespace::text NOT IN ('information_schema', 'pg_catalog')${
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
? timescaleFilters
: databaseEdition ===
DatabaseEdition.POSTGRESQL_SUPABASE
? supabaseFilters
: ''
}
) AS x
), pk_info AS (
SELECT array_to_string(array_agg(CONCAT('{"schema":"', replace(schema_name, '"', ''), '"',

View File

@@ -35,6 +35,7 @@ import { DialogProvider } from '@/context/dialog-context/dialog-provider';
import { KeyboardShortcutsProvider } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts-provider';
import { Spinner } from '@/components/spinner/spinner';
import { Helmet } from 'react-helmet-async';
import { useStorage } from '@/hooks/use-storage';
const OPEN_STAR_US_AFTER_SECONDS = 30;
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
@@ -73,6 +74,7 @@ const EditorPageComponent: React.FC = () => {
} = useLocalConfig();
const { toast } = useToast();
const { t } = useTranslation();
const { listDiagrams } = useStorage();
useEffect(() => {
if (!config) {
@@ -106,7 +108,15 @@ const EditorPageComponent: React.FC = () => {
navigate(`/diagrams/${config.defaultDiagramId}`);
}
} else {
openCreateDiagramDialog();
const diagrams = await listDiagrams();
if (diagrams.length > 0) {
const defaultDiagramId = diagrams[0].id;
await updateConfig({ defaultDiagramId });
navigate(`/diagrams/${defaultDiagramId}`);
} else {
openCreateDiagramDialog();
}
}
};
loadDefaultDiagram();
@@ -115,6 +125,7 @@ const EditorPageComponent: React.FC = () => {
openCreateDiagramDialog,
config,
navigate,
listDiagrams,
loadDiagram,
resetRedoStack,
resetUndoStack,

View File

@@ -30,16 +30,11 @@ import { useHistory } from '@/hooks/use-history';
import { useTranslation } from 'react-i18next';
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 { deMetadata } from '@/i18n/locales/de';
import { jaMetadata } from '@/i18n/locales/ja';
import { useLocalConfig } from '@/hooks/use-local-config';
import { frMetadata } from '@/i18n/locales/fr';
import { hiMetadata } from '@/i18n/locales/hi';
import { DiagramName } from './diagram-name';
import { LastSaved } from './last-saved';
import { pt_BRMetadata } from '@/i18n/locales/pt_BR';
import { languages } from '@/i18n/i18n';
import { useNavigate } from 'react-router-dom';
export interface TopNavbarProps {}
@@ -70,6 +65,12 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
const { isMd: isDesktop } = useBreakpoint('md');
const { config, updateConfig } = useConfig();
const { exportImage } = useExportImage();
const navigate = useNavigate();
const handleDeleteDiagramAction = useCallback(() => {
deleteDiagram();
navigate('/');
}, [deleteDiagram, navigate]);
const createNewDiagram = () => {
openCreateDiagramDialog();
@@ -429,7 +430,7 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
closeLabel: t(
'delete_diagram_alert.cancel'
),
onAction: deleteDiagram,
onAction: handleDeleteDiagramAction,
})
}
>
@@ -565,85 +566,22 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
{t('menu.view.change_language')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(enMetadata.code)
}
checked={
i18n.language ===
enMetadata.code
}
>
{enMetadata.name}
</MenubarCheckboxItem>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(esMetadata.code)
}
checked={
i18n.language ===
esMetadata.code
}
>
{esMetadata.name}
</MenubarCheckboxItem>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(frMetadata.code)
}
checked={
i18n.language ===
frMetadata.code
}
>
{frMetadata.name}
</MenubarCheckboxItem>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(deMetadata.code)
}
checked={
i18n.language ===
deMetadata.code
}
>
{deMetadata.name}
</MenubarCheckboxItem>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(hiMetadata.code)
}
checked={
i18n.language ===
hiMetadata.code
}
>
{hiMetadata.name}
</MenubarCheckboxItem>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(jaMetadata.code)
}
checked={
i18n.language ===
jaMetadata.code
}
>
{jaMetadata.name}
</MenubarCheckboxItem>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(
pt_BRMetadata.code
)
}
checked={
i18n.language ===
pt_BRMetadata.code
}
>
{pt_BRMetadata.name}
</MenubarCheckboxItem>
{languages.map((language) => (
<MenubarCheckboxItem
key={language.code}
onClick={() =>
changeLanguage(
language.code
)
}
checked={
i18n.language ===
language.code
}
>
{language.name}
</MenubarCheckboxItem>
))}
</MenubarSubContent>
</MenubarSub>
</MenubarContent>

View File

@@ -32,6 +32,7 @@ import { ReactFlowProvider } from '@xyflow/react';
import { ChartDBProvider } from '@/context/chartdb-context/chartdb-provider';
import { Helmet } from 'react-helmet-async';
import { APP_URL, HOST_URL } from '@/lib/env';
import { Link } from '@/components/link/link';
export interface TemplatePageLoaderData {
template: Template | undefined;
@@ -65,25 +66,22 @@ const TemplatePageComponent: React.FC = () => {
<Helmet>
{template ? (
<>
{HOST_URL !== 'https://chartdb.io' ? (
<link
rel="canonical"
href={`https://chartdb.io/templates/${templateSlug}`}
/>
) : null}
<title>
Database schema diagram for {template.name} |
ChartDB
{`Database schema diagram for - ${template.name} | ChartDB`}
</title>
<meta
name="title"
content={`Database schema for - ${template.name} | ChartDB`}
/>
<meta
name="description"
content={`${template.shortDescription}: ${template.description}`}
/>
<meta
name="keywords"
content={`${template.keywords.join(', ')}`}
/>
<meta
property="og:title"
content={`Database schema for - ${template.name} | ChartDB`}
content={`Database schema diagram for - ${template.name} | ChartDB`}
/>
<meta
property="og:url"
@@ -98,7 +96,7 @@ const TemplatePageComponent: React.FC = () => {
content={`${HOST_URL}${template.image}`}
/>
<meta property="og:type" content="website" />
<meta property="og:site_name" content="ChartDB" />
<meta
name="twitter:title"
content={`Database schema for - ${template.name} | ChartDB`}
@@ -115,6 +113,8 @@ const TemplatePageComponent: React.FC = () => {
name="twitter:card"
content="summary_large_image"
/>
<meta name="twitter:site" content="@ChartDB_io" />
<meta name="twitter:creator" content="@ChartDB_io" />
</>
) : (
<title>Database Schema Diagram | ChartDB</title>
@@ -174,8 +174,11 @@ const TemplatePageComponent: React.FC = () => {
</Breadcrumb>
<div className="flex flex-col items-center gap-4 md:flex-row md:items-start md:justify-between md:gap-0">
<div className="flex flex-col pr-0 md:pr-20">
<h1 className="font-primary text-2xl font-bold">
<h1 className="flex flex-col font-primary text-2xl font-bold">
{template?.name}
<span className="text-sm font-normal text-muted-foreground">
Database schema diagram
</span>
</h1>
<h2 className="mt-3">
<span className="font-semibold">
@@ -244,6 +247,25 @@ const TemplatePageComponent: React.FC = () => {
</span>
</div>
</div>
<Separator />
{template.url ? (
<>
<div>
<h4 className="mb-1 text-base font-semibold md:text-left">
Url
</h4>
<Link
className="break-all text-sm text-muted-foreground"
href={`${template.url}?ref=chartdb`}
target="_blank"
>
{template.url}
</Link>
</div>
<Separator />
</>
) : null}
<div>
<h4 className="mb-1 text-base font-semibold md:text-left">
Tags

View File

@@ -26,7 +26,7 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({ template }) => {
className="h-2 rounded-t-[6px]"
style={{ backgroundColor: randomColor() }}
></div>
<div className="grow overflow-hidden p-1">
<div className="overflow-hidden p-1">
<img
src={
effectiveTheme === 'dark'
@@ -43,7 +43,7 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({ template }) => {
{template.name}
</h3>
</div>
<div className="flex flex-row">
<div className="flex h-full flex-col justify-start pt-1">
<Tooltip>
<TooltipTrigger className="mr-1">
<img

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React from 'react';
import ChartDBLogo from '@/assets/logo-light.png';
import ChartDBDarkLogo from '@/assets/logo-dark.png';
import { useTheme } from '@/hooks/use-theme';
@@ -7,54 +7,67 @@ import { ThemeProvider } from '@/context/theme-context/theme-provider';
import { Component, Star } from 'lucide-react';
import { ListMenu } from '@/components/list-menu/list-menu';
import { TemplateCard } from './template-card/template-card';
import { useMatches, useParams } from 'react-router-dom';
import { useLoaderData, useMatches, useParams } from 'react-router-dom';
import type { Template } from '@/templates-data/templates-data';
import { Spinner } from '@/components/spinner/spinner';
import { removeDups } from '@/lib/utils';
import { Helmet } from 'react-helmet-async';
import { HOST_URL } from '@/lib/env';
export interface TemplatesPageLoaderData {
templates: Template[] | undefined;
allTags: string[] | undefined;
}
const TemplatesPageComponent: React.FC = () => {
const { effectiveTheme } = useTheme();
const data = useLoaderData() as TemplatesPageLoaderData;
const { templates, allTags } = data ?? {};
const { tag } = useParams<{ tag: string }>();
const matches = useMatches();
const [templates, setTemplates] = React.useState<Template[]>();
const [tags, setTags] = React.useState<string[]>();
const isFeatured = matches.some(
(match) => match.id === 'templates_featured'
);
const isAllTemplates = matches.some((match) => match.id === 'templates');
const isTags = matches.some((match) => match.id === 'templates_tags');
useEffect(() => {
const loadTemplates = async () => {
const { templates: loadedTemplates } = await import(
'@/templates-data/templates-data'
);
let templatesToLoad = loadedTemplates;
if (isFeatured) {
templatesToLoad = loadedTemplates.filter((t) => t.featured);
}
if (isTags && tag) {
templatesToLoad = loadedTemplates.filter((t) =>
t.tags.includes(tag)
);
}
setTemplates(templatesToLoad);
setTags(removeDups(loadedTemplates?.flatMap((t) => t.tags) ?? []));
};
loadTemplates();
}, [isFeatured, isTags, tag]);
return (
<>
<Helmet>
<title>ChartDB - Database Schema Templates</title>
{HOST_URL !== 'https://chartdb.io' ? (
<link rel="canonical" href="https://chartdb.io/templates" />
) : null}
<title>Database Schema Diagram Templates | ChartDB</title>
<meta
name="description"
content="Discover a collection of real-world database schema diagrams, featuring example applications and popular open-source projects."
/>
<meta
property="og:title"
content="Database Schema Diagram Templates | ChartDB"
/>
<meta property="og:url" content={`${HOST_URL}/templates`} />
<meta
property="og:description"
content="Discover a collection of real-world database schema diagrams, featuring example applications and popular open-source projects."
/>
<meta property="og:image" content={`${HOST_URL}/chartdb.png`} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="ChartDB" />
<meta
name="twitter:title"
content="Database Schema Diagram Templates | ChartDB"
/>
<meta
name="twitter:description"
content="Discover a collection of real-world database schema diagrams, featuring example applications and popular open-source projects."
/>
<meta
name="twitter:image"
content={`${HOST_URL}/chartdb.png`}
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@ChartDB_io" />
<meta name="twitter:creator" content="@ChartDB_io" />
</Helmet>
<section className="flex w-screen flex-col bg-background">
@@ -92,10 +105,9 @@ const TemplatesPageComponent: React.FC = () => {
Database Schema Templates
</h1>
<h2 className="mt-1 font-primary text-base text-muted-foreground">
Explore a collection of real-world database schemas
drawn from real-world live applications and open-source
projects. Use these as a foundation or source of
inspiration when designing your apps architecture.
Discover a collection of real-world database schema
diagrams, featuring example applications and popular
open-source projects.
</h2>
{!templates ? (
<Spinner
@@ -125,10 +137,10 @@ const TemplatesPageComponent: React.FC = () => {
<h4 className="mt-4 text-left text-sm font-semibold">
Tags
</h4>
{tags ? (
{allTags ? (
<ListMenu
className="mt-1 w-44 shrink-0"
items={tags.map((currentTag) => ({
items={allTags.map((currentTag) => ({
title: currentTag,
href: `/templates/tags/${currentTag}`,
selected: tag === currentTag,

View File

@@ -2,6 +2,8 @@ import React from 'react';
import type { RouteObject } from 'react-router-dom';
import { createBrowserRouter } from 'react-router-dom';
import type { TemplatePageLoaderData } from './pages/template-page/template-page';
import type { TemplatesPageLoaderData } from './pages/templates-page/templates-page';
import { getTemplatesAndAllTags } from './templates-data/template-utils';
const routes: RouteObject[] = [
...['', 'diagrams/:diagramId'].map((path) => ({
@@ -38,6 +40,15 @@ const routes: RouteObject[] = [
element: <TemplatesPage />,
};
},
loader: async (): Promise<TemplatesPageLoaderData> => {
const { tags, templates } = await getTemplatesAndAllTags();
return {
allTags: tags,
templates,
};
},
},
{
id: 'templates_featured',
@@ -50,6 +61,16 @@ const routes: RouteObject[] = [
element: <TemplatesPage />,
};
},
loader: async (): Promise<TemplatesPageLoaderData> => {
const { tags, templates } = await getTemplatesAndAllTags({
featured: true,
});
return {
allTags: tags,
templates,
};
},
},
{
id: 'templates_tags',
@@ -62,6 +83,16 @@ const routes: RouteObject[] = [
element: <TemplatesPage />,
};
},
loader: async ({ params }): Promise<TemplatesPageLoaderData> => {
const { tags, templates } = await getTemplatesAndAllTags({
tag: params.tag,
});
return {
allTags: tags,
templates,
};
},
},
{
id: 'templates_templateSlug',

View File

@@ -1,6 +1,6 @@
import type { Diagram } from '@/lib/domain/diagram';
import type { Template } from './templates-data';
import { generateId } from '@/lib/utils';
import { generateId, removeDups } from '@/lib/utils';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
@@ -87,3 +87,30 @@ export const convertTemplateToNewDiagram = (template: Template): Diagram => {
tables,
};
};
export const getTemplatesAndAllTags = async ({
featured,
tag,
}: {
featured?: boolean;
tag?: string;
} = {}): Promise<{ templates: Template[]; tags: string[] }> => {
const { templates } = await import('@/templates-data/templates-data');
const allTags = removeDups(templates?.flatMap((t) => t.tags) ?? []);
if (featured) {
return {
templates: templates.filter((t) => t.featured),
tags: allTags,
};
}
if (tag) {
return {
templates: templates.filter((t) => t.tags.includes(tag)),
tags: allTags,
};
}
return { templates, tags: allTags };
};

View File

@@ -1,6 +1,9 @@
import type { Diagram } from '@/lib/domain/diagram';
import { employeeDb } from './templates/employee-db';
import { visualNovelDb } from './templates/visual-novel-db';
import { airbnbDb } from './templates/airbnb-db';
import { wordpressDb } from './templates/wordpress-db';
import { pokemonDb } from './templates/pokemon-db';
export interface Template {
slug: string;
@@ -11,9 +14,14 @@ export interface Template {
imageDark: string;
diagram: Diagram;
tags: string[];
keywords: string[];
featured: boolean;
url?: string;
}
export const templates: Template[] = [employeeDb, visualNovelDb];
export const templates: Template[] = [
employeeDb,
visualNovelDb,
airbnbDb,
wordpressDb,
pokemonDb,
];

File diff suppressed because it is too large Load Diff

View File

@@ -5,28 +5,16 @@ import imageDark from '@/assets/templates/employeedb-dark.png';
export const employeeDb: Template = {
slug: 'employees-db',
name: 'Employees schema',
name: 'Employees',
shortDescription: 'Employees, departments, and salaries',
description:
'A schema for database of employees, departments, and salaries.',
image,
imageDark,
tags: ['mysql'],
keywords: [
'Employees database schema',
'Employees database template',
'database schema visualization',
'Employees database design',
'ChartDB',
'Employees schema diagram',
'relational database structure',
'Employees development',
'Employees tables',
'database template for Employees',
],
featured: true,
diagram: {
id: 'diagramexample01',
id: 'employees_db',
name: 'employees-db',
createdAt: new Date(),
updatedAt: new Date(),

File diff suppressed because it is too large Load Diff

View File

@@ -5,25 +5,13 @@ import imageDark from '@/assets/templates/visual-novel-db-dark.png';
export const visualNovelDb: Template = {
slug: 'visual-novel-db',
name: 'The Visual Novel Database | vndb',
shortDescription: 'The Visual Novel Database | vndb',
description: 'A comprehensive database for information about visual novels',
name: 'The Visual Novel Database',
shortDescription: 'The Visual Novel Database',
description:
'A comprehensive database for information about visual novels.',
image,
imageDark,
tags: ['postgres'],
keywords: [
'VNDB',
'visual novel database schema',
'visual novel database template',
'database schema visualization',
'visual novel database design',
'ChartDB',
'VNDB schema diagram',
'relational database structure',
'VNDB development',
'VNDB tables',
'database template for VNDB',
],
featured: true,
url: 'https://vndb.org',
diagram: {

File diff suppressed because it is too large Load Diff