Compare commits

..
Author SHA1 Message Date
Wroud 438475a99c chore: editing draft 2025-05-30 22:19:04 +08:00
Wroud f967a20edf dbeaver/pro#5802 feat: table api 2025-05-29 19:41:38 +08:00
Serge Rider 5bc1734cb6 dbeaver/pro#5832 25.1.1 version bump 2025-05-27 19:37:07 +02:00
aba27cf9d3 dbeaver/pro#5711 Add validation for team id (#3459)
* dbeaver/pro#5711 Add validation for team id

* dbeaver/pro#5711 Add validation for user id

* dbeaver/pro#5711 add user name and team id validation

---------

Co-authored-by: naumov <iamemptyhuh@gmail.com>
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2025-05-27 17:53:09 +02:00
sergeyteleshev a098db62c3 [CB] hotkeys with "/" and "\" dont work after hotkeys library version upgrade #5781 (#3478)
* dbeaver/pro#5781 fixes shortcuts with slashes symbols + correctly translates it in the shortcut tips section

* moves display logic in SHORCUTS_DATA helpers

* adds replace all for shorcuts to consider sequential shorcuts cases

* adds formatShortcutKeys helper

* removes useless code

* adds alt -> option conversion

* refactors formatKey logic to switch

* cleanup

* cleanup

* cleanup

* switch with lowerCaseCode

* moves shortcut helpers to core-utils + uses it everywhere

* Revert "moves shortcut helpers to core-utils + uses it everywhere"

This reverts commit b4eeda5adf.
2025-05-27 17:50:17 +02:00
c4886127a6 dbeaver/pro#5821 Style fixes (#3479)
* dbeaver/pro#5821 fix: add cursor pointer to icon button

* dbeaver/pro#5821 refactor: change view button type

* dbeaver/pro#5821 feat: add UiSpinner styles for theme integration

* dbeaver/pro#5821 fix: update button class

* dbeaver/pro#5821 fix: improve buttons in driver management

---------

Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2025-05-27 17:49:05 +02:00
Alexander Skoblikovandkseniaguzeeva 343d671575 dbeaver/pro#5778 custom certs folder in conf folder (#3477)
Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
2025-05-27 16:14:11 +02:00
AlexeyandEvgenia 06a0411f84 dbeaver/pro#5200 feat: fast connection / folder creation in the explorer (#3464)
* dbeaver/pro#5200 feat: fast connection / folder creation in the explorer

* chore: update icons

* chore: update icons

* chore: remove important from style

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2025-05-27 21:33:23 +08:00
ffe3d45017 dbeaver/pro#5789 replace Buttons styles (#3468)
* dbeaver/pro#5789 refactor: add ButtonIcon component instead of Button.Icon

* dbeaver/pro#5789 refactor: button components across whole application to use UIKit button and UIKit variables

- Rewrite Button.css(former button.module.css) file to define button styles for better maintainability and scalability
- Added props to the Button component for Icons: 'size' and 'placement', currently used only in PropertiesTable (Add property)
- Changed button modifiers to 'variant' swapping unelevated mod to primary(default one, so mod just deleted) and outlined mod to variant secondary.
- In one case changed raised mod to danger variant (deleting a license)

* dbeaver/pro#5789 refactor: fix Button component styling and removed redundant css file

* dbeaver/pro#5789 style: clean up button formatting across multiple components

* dbeaver/pro#5789 refactor: update Tailwind CSS import to use layer directive

* dbeaver/pro#5789 refactor[kit]: add disabled state styles for buttons

* dbeaver/pro#5789 refactor: enhance button styles for disabled state

* dbeaver/pro#5789 fix button problems

* dbeaver/pro#5789 fix: change layer name from theme to base in color styles

* dbeaver/pro#5789 refactor: update button padding from 16px to 15px

* dbeaver/pro#5789 refactor: adjust button gap and update InputFiles button props

* dbeaver/pro#5789 style: center align button in SSHKeyUploader component

* dbeaver/pro#5789 fix: update button classes to use inline-flex for better alignment

* dbeaver/pro#5789 fix: remove initial background from tab styles

* dbeaver/pro#5789 fix: move cursor pointer rule to button container

* dbeaver/pro#5789 fix: update checkbox SVG fill property for proper rendering

* dbeaver/pro#5789 fix: add variant prop to Button component for styling options

* dbeaver/pro#5789 fix: add InlineEditor to CSS layer to compete with theme Important! border-color

* dbeaver/pro#5789 fix: add min-width to button styles for consistency

* dbeaver/pro#5789 refactor: adjust margin for trigger element in context menu

first of all it's done to fix autotests and the second reason is to visually balance the cell with menu

* dbeaver/pro#5789 fix: make boolean great again

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
2025-05-27 16:59:01 +08:00
ElizabethandDiana 16005be3e6 dbeaver/dbeaver#37602 Add the setting for column names completion (#3474)
* dbeaver/dbeaver#37602 Add the setting for column names completion

* dbeaver/dbeaver#37602 Fix formatting

---------

Co-authored-by: Diana <31996417+uslss@users.noreply.github.com>
2025-05-27 06:20:47 +02:00
alexandEvgenia 005adb10ce dbeaver/pro#5215 track close event on layout close (#3472)
* dbeaver/pro#5215 track close event on layout close

* dbeaver/pro#5215 support old close behaviour

* dbeaver/pro#5215 add event data check to user form

* dbeaver/pro#5215 change naming

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2025-05-27 00:39:06 +08:00
Sychev Andrey 0a7d9e6a25 dbeaver/pro#5814 fix: wrap checkbox styles in base layer (#3473)
* dbeaver/pro#5814 ашч: wrap checkbox styles in base layer

* dbeaver/pro#5814 style: wrap disabled styles in base layer
2025-05-26 21:24:09 +08:00
Ainur 0edfb29851 dbeaver/pro#5523 add reason for enable user log (#3463) 2025-05-26 08:31:34 +00:00
52f567fded dbeaver/pro#5553 Add Sorting functionality to base grid (#3456)
* dbeaver/pro#5553 feat: add column sorting functionality to DataGrid

* dbeaver/pro#5553 feat: enhance column sorting functionality to support multiple selection

* dbeaver/pro#5553 feat: add keyboard navigation to order button

* dbeaver/pro#5553 feat: add constraints handling logic for column sorting in DataGrid

* Revert "dbeaver/pro#5553 feat: add constraints handling logic for column sorting in DataGrid"

This reverts commit 6028a48b13.

* dbeaver/pro#5553 feat: add icon generation script to UIKit, add sample icons

* dbeaver/pro#5553 refactor: use inner react-data-grid API for column sorting

* dbeaver/pro#5553 feat: add IconCross component and update icon-related files

* dbeaver/pro#5553 feat: replace Button with IconButton in OrderButton component

* dbeaver/pro#5553 style: update foreground color variables for IconButton component

* dbeaver/pro#5553 refactor: add normalization rules to layers, use IconButton in Common DialogHeader

checked if new IconButton works well in light and dark mode by using in CommonDialogHeader

* dbeaver/pro#5553 style: format code and update dependencies in CommonDialogHeader and DataGridTable components

* dbeaver/pro#5553 style: move onColumnSort prop to correct position in DataGridTable component

* dbeaver/pro#5553 refactor: remove icons component generation, use svg sprite

* dbeaver/pro#5553 refactor: update OrderButton component and inline icons sprite in Body

we decided to go with this solution for now just adding icons to a sprite and use them by id. Imports by ?url  don't work in VSCode if try to use them in <use> href, so we need to find another solution or stick to that approach.

* dbeaver/pro#5553 refactor: update column sorting and sortable logic in DataGrid components

* dbeaver/pro#5553 style: format whitespace in Body component

* dbeaver/pro#5553 feat: add new sorting icons for ascending, descending, and unsorted states

* dbeaver/pro#5553 refactor:styling for header cell and order button components

* dbeaver/pro#5553 refactor: update sorting logic in DataGrid and OrderButton components

* dbeaver/pro#5553 refactor: value deps

* dbeaver/pro#5553 feat: update sorting icons for ascending, descending, and unsorted states

* dbeaver/pro#5553 style: update aria-label and title for OrderButton component

* dbeaver/pro#5553 fix: double focus

* dbeaver/pro#5553 fix: after merge

* dbeaver/pro#5553 refactor: removed outline offset for sort button

* dbeaver/pro#5553 refactor: update Tailwind CSS import to use layer directive

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: Alexey <wrouds@gmail.com>
2025-05-24 00:21:37 +08:00
Ainur 116b1a3be8 dbeaver/pro#5160 check length of meta param values (#3471) 2025-05-23 16:01:09 +02:00
AlexeyandDaria Marutkina 84cc6fcf60 dbeaver/pro#5753 feat: react-dnd (#3454)
* dbeaver/pro#5753 feat: react-dnd

* chore: fix after review

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2025-05-23 18:52:22 +08:00
1f4266ec95 dbeaver/pro#5745 [CB] Bump NPM dependencies #5745 (#3457)
* dbeaver/pro#5745 updates frontend deps

* updates graphql-request

* updates yarn locks for common-typescript, common-react

* updates engines node version

* react-hotkeys-hook + fixes hotkeys so it continues to work

* code cleanup for parseHotKey

* uses fixed version of tailwindcss

* adds 4.0.6 versions for tailwindcss with resolutions

* reverts tailwind 4.0.6 version for yarn.lock

* removes redundant resolutions for tailwindcss

* Update readme.md with new NodeJS version

* Update README.md with correct nodejs version according to our documentation

* updates talewindcss version 4.0.7

* fix: hotkeys types

---------

Co-authored-by: Wroud <wrouds@gmail.com>
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2025-05-23 17:31:13 +08:00
Alexey c85b3ccd24 dbeaver/pro#5779 fix: external app plugin naming (#3467) 2025-05-22 17:05:18 +02:00
Ruslan MusaevandEvgenia 7687943ecf dbeaver/pro#5649 Do not send update event if a user teams are same (#3453)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2025-05-22 11:14:21 +02:00
alexandEvgenia 587d25bb40 dbeaver/pro#5709 close panel before deleting user (#3458)
* dbeaver/pro#5709 close panel before deleting user

* dbeaver/pro#5709 bump licence year

* dbeaver/pro#5709 remove unnecessary resource hook

* dbeaver/pro#5709 remove unused import

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2025-05-22 16:48:42 +08:00
5561d37ce8 dbeaver/pro#4888 Ldap support ssl (#3438)
* dbeaver/pro#4888 Ldap support ssl

* dbeaver/pro#4888 Ldap support ssl

* dbeaver/pro#4888 Ldap support ssl

* dbeaver/pro#4888 Ldap support ssl

* dbeaver/pro#4888 fix connection on TE

* dbeaver/pro#4888 ldaps fix

* fix: check configuration id for auth token

* dbeaver/pro#4888 one authentication per provider and configuration

* dbeaver/pro#4888 one authentication per provider and configuration

* Revert "fix: check configuration id for auth token"

This reverts commit 517cc717a8.

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: Wroud <wrouds@gmail.com>
2025-05-21 12:26:11 +02:00
alexandkseniaguzeeva fffbea22af dbeaver/pro#5458 add toggleDisabled setting (#3455)
* dbeaver/pro#5458 add toggleDisabled setting

* dbeaver/pro#5458 change setting name

---------

Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
2025-05-21 17:39:50 +08:00
Ainur 934d45617a dbeaver/pro#4372 add new attribute for a property group (#3461) 2025-05-20 18:13:38 +02:00
Ainur ccb6f2208c dbeaver/pro#5404 add complex auth servlet handler (#3418) 2025-05-20 14:33:40 +00:00
alexandDaria Marutkina d9e4383b22 dbeaver/pro#5382 fallback undefined to false (#3452)
* dbeaver/pro#5382 fallback undefined to false

* dbeaver/pro#5382 bump year

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2025-05-20 16:34:18 +08:00
Ruslan MusaevandEvgenia cbd8458776 dbeaver/pro#5638 Multi team mapping (#3441)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2025-05-20 10:32:59 +02:00
Ruslan MusaevandEvgenia 3352b66a26 dbeaver/pro#5694 Fix auth providers init (#3442)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2025-05-20 10:32:27 +02:00
Ruslan MusaevandEvgenia 9b5ceed501 dbeaver/pro#5693 Fix ldap bruteforce protection (#3446)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2025-05-20 10:32:02 +02:00
alexandDaria Marutkina 3687a0807d dbeaver/pro#5678 use new api command (#3440)
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2025-05-19 15:29:30 +02:00
sergeyteleshevandDaria Marutkina bda9966330 dbeaver/pro#5561 removes tiny style from checkboxes for AuthModelCredentialsForm (#3445)
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2025-05-19 19:39:19 +08:00
Sychev Andreyandmr-anton-t f0be7b6af6 dbeaver/pro#5554 [CB] [UI kit] create IconButton component (#3424)
* dbeaver/pro#5554 feat: first iteration of IconButton

* dbeaver/pro#5554 refactor: css var changes

* dbeaver/pro#5554 feat: add loading state to IconButton component

* dbeaver/pro#5554 style: update background color for loading state in IconButton

* dbeaver/pro#5554 feat: separate icon button styles and loader variables

* dbeaver/pro#5554 refactor: extract ButtonRoot component to use as a base for Button and IconButton components

That approach lets us use common logic (loading) but don't mixup styles and tokens and have better control

* dbeaver/pro#5554 refactor: update loading state styles for IconButton component

* dbeaver/pro#5554 refactor: ButtonRootProps

* dbeaver/pro#5554 refactor: remove opacity from loading state in Button and IconButton components

* dbeaver/pro#5554 refactor: create content wrapper, move general styles there, add loader wrapper

---------

Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2025-05-19 19:38:30 +08:00
serge-rider 3b0f0ddf1e Merge branch 'devel' of https://github.com/dbeaver/cloudbeaver into devel 2025-05-19 11:44:14 +02:00
serge-rider 85526b45dd Revert "dbeaver/dbeaver#37928 Downgrade ClickHouse driver to 0.7.2 (#3447)"
This reverts commit 7a48102df4.
2025-05-19 11:42:15 +02:00
Daria Marutkina a6a7aeb97f dbeaver/pro#5684 Update README file (#3451)
* dbeaver/pro#5684 Update README file
2025-05-19 11:14:21 +02:00
serge-rider edf54d3b72 dbeaver/pro#5668 Annotations 2025-05-18 10:07:17 +02:00
ShadelessFox 7a48102df4 dbeaver/dbeaver#37928 Downgrade ClickHouse driver to 0.7.2 (#3447) 2025-05-17 05:14:19 +02:00
Ruslan Musaev a16d03d5a6 dbeaver/pro#5695 Revert "Add Driver for Databend (#3174)" (#3443) 2025-05-15 11:12:01 +02:00
serge-rider 8410df4fa9 dbeaver/pro#5684 25.1.0 version bump 2025-05-13 19:23:55 +02:00
367 changed files with 8501 additions and 8053 deletions
+6
View File
@@ -25,6 +25,12 @@ You can see a live demo of CloudBeaver here: https://demo.cloudbeaver.io
## Changelog
### 25.0.5 2025-05-19
- Added the ability to configure the default database or schema in the connection configuration;
- A new setting to autosave scripts in the SQL editor appeared in global and users' preferences;
- SQLite: added information about a table Strict mode to the table metadata section (thanks to @eusebe-cda);
- Vietnamese localization has been added (thanks to @0xhanh).
### 25.0.4 2025-05-05
- Important:
- Connection templates were removed from the application. You can use the Connection Editor to create new connections.
@@ -33,6 +33,7 @@
"typescript": "^5"
},
"dependencies": {
"@dbeaver/react-dnd": "workspace:^",
"@dbeaver/ui-kit": "workspace:^",
"react": "^19",
"react-data-grid": "file:./artifacts",
@@ -4,19 +4,24 @@ import { DataGridCellContext, type IDataGridCellRenderer } from './DataGridCellC
import { createCellMouseEvent } from './eventUtils.js';
import { DataGridCellInnerContext, type IDataGridCellInnerContext } from './DataGridCellInnerContext.js';
import { useGridReactiveValue } from './useGridReactiveValue.js';
import { HeaderDnDContext } from './useHeaderDnD.js';
export const BaseCell = memo(function BaseCell<TRow, TSummaryRow>(props: CellRendererProps<TRow, TSummaryRow>) {
const cellContext = use(DataGridCellContext);
const tooltip = useGridReactiveValue(cellContext?.cellTooltip, props.rowIdx, props.column.idx);
const dndContext = use(HeaderDnDContext)!;
const virtualColIdx = props.column.idx;
const dataColIdx = dndContext.getDataColIdx(props.column.idx);
const rowIdx = props.rowIdx;
const tooltip = useGridReactiveValue(cellContext?.cellTooltip, rowIdx, dataColIdx);
function handleClick(event: React.MouseEvent<HTMLDivElement>) {
props.onClick?.(
{
rowIdx: props.rowIdx,
rowIdx,
row: props.row,
column: props.column,
selectCell(enableEditor) {
props.selectCell({ rowIdx: props.rowIdx, idx: props.column.idx }, enableEditor);
props.selectCell({ rowIdx, idx: virtualColIdx }, enableEditor);
},
},
createCellMouseEvent(event),
@@ -26,11 +31,11 @@ export const BaseCell = memo(function BaseCell<TRow, TSummaryRow>(props: CellRen
function handleDoubleClick(event: React.MouseEvent<HTMLDivElement>) {
props.onDoubleClick?.(
{
rowIdx: props.rowIdx,
rowIdx,
row: props.row,
column: props.column,
selectCell(enableEditor) {
props.selectCell({ rowIdx: props.rowIdx, idx: props.column.idx }, enableEditor);
props.selectCell({ rowIdx, idx: virtualColIdx }, enableEditor);
},
},
createCellMouseEvent(event),
@@ -40,11 +45,11 @@ export const BaseCell = memo(function BaseCell<TRow, TSummaryRow>(props: CellRen
function handleContextMenu(event: React.MouseEvent<HTMLDivElement>) {
props.onContextMenu?.(
{
rowIdx: props.rowIdx,
rowIdx,
row: props.row,
column: props.column,
selectCell(enableEditor) {
props.selectCell({ rowIdx: props.rowIdx, idx: props.column.idx }, enableEditor);
props.selectCell({ rowIdx, idx: virtualColIdx }, enableEditor);
},
},
createCellMouseEvent(event),
@@ -74,7 +79,7 @@ export const BaseCell = memo(function BaseCell<TRow, TSummaryRow>(props: CellRen
[...Object.values(props), tooltip],
);
const cellElement = useGridReactiveValue(cellContext?.cellElement, props.rowIdx, props.column.idx, mappedProps, renderDefaultCell);
const cellElement = useGridReactiveValue(cellContext?.cellElement, rowIdx, dataColIdx, mappedProps, renderDefaultCell);
const innerCellContext = useMemo<IDataGridCellInnerContext>(() => ({ isFocused: props.isCellSelected }), [props.isCellSelected]);
return <DataGridCellInnerContext value={innerCellContext}>{cellElement ?? <Cell title={tooltip} {...props} />}</DataGridCellInnerContext>;
@@ -8,6 +8,10 @@
}
@layer components.rdg.Cell {
.rdg {
--dbv-kit-icon-btn-outline-width: 1px;
}
@media (prefers-color-scheme: dark) {
.rdg-cell-frozen {
&:nth-last-child(1 of &) {
@@ -12,6 +12,7 @@ import { mapRenderHeaderCell } from './mapRenderHeaderCell.js';
import { mapEditCellRenderer } from './mapEditCellRenderer.js';
import { DataGridRowContext, type IDataGridRowContext } from './DataGridRowContext.js';
import './DataGrid.css';
import { HeaderDnDContext, useHeaderDnD } from './useHeaderDnD.js';
export interface ICellPosition {
rowIdx: number;
@@ -46,9 +47,13 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
headerElement,
getHeaderWidth,
headerText,
getHeaderOrder,
getHeaderResizable,
columnSortable,
getHeaderHeight,
getHeaderPinned,
columnSortingState,
getHeaderDnD,
cell,
cellText,
cellElement,
@@ -59,10 +64,12 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
rowCount,
getRowId,
getRowHeight,
onHeaderReorder,
onScroll,
onScrollToBottom,
onFocus,
onCellChange,
onColumnSort,
children,
className,
onCellKeyDown,
@@ -87,21 +94,23 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
minWidth: 26,
editable: row => getCellEditable?.(row.idx, i) ?? false,
frozen: getHeaderPinned?.(i),
renderHeaderCell: mapRenderHeaderCell,
renderCell: mapCellContentRenderer,
renderEditCell: mapEditCellRenderer,
renderHeaderCell: mapRenderHeaderCell(i),
renderCell: mapCellContentRenderer(i),
renderEditCell: mapEditCellRenderer(i),
};
});
const dndHeaderContext = useHeaderDnD({ columns, onReorder: onHeaderReorder, getCanDrag: getHeaderDnD, getHeaderOrder });
useImperativeHandle(ref, () => ({
selectCell: (position: ICellPosition) => {
innerGridRef.current?.selectCell({ idx: position.colIdx, rowIdx: position.rowIdx });
innerGridRef.current?.selectCell({ idx: dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx });
},
scrollToCell: (position: Partial<ICellPosition>) => {
innerGridRef.current?.scrollToCell({ idx: position.colIdx, rowIdx: position.rowIdx });
innerGridRef.current?.scrollToCell({ idx: position.colIdx && dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx });
},
openEditor: (position: ICellPosition) => {
innerGridRef.current?.selectCell({ idx: position.colIdx, rowIdx: position.rowIdx }, true);
innerGridRef.current?.selectCell({ idx: dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx }, true);
},
}));
@@ -124,34 +133,40 @@ export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid
);
function handleCellFocus(args: CellSelectArgs<IInnerRow, unknown>) {
onFocus?.({ colIdx: args.column.idx, rowIdx: args.rowIdx });
onFocus?.({ colIdx: dndHeaderContext.getDataColIdx(args.column.idx), rowIdx: args.rowIdx });
}
function handleCellKeyDown(args: CellSelectArgs<IInnerRow, unknown>, event: DataGridCellKeyboardEvent) {
onCellKeyDown?.({ colIdx: dndHeaderContext.getDataColIdx(args.column.idx), rowIdx: args.rowIdx }, event);
}
return (
<DataGridRowContext value={{ rowCount, onScrollToBottom }}>
<DataGridCellContext value={{ cell, cellText, cellElement, cellTooltip, onCellChange }}>
<DataGridCellHeaderContext value={{ headerElement, headerText }}>
<DataGridBase
key={gridKey.current}
ref={innerGridRef}
columns={columns}
rows={rows}
className={className}
headerRowHeight={getHeaderHeight?.()}
onScroll={onScroll}
rowHeight={getRowHeight ? row => getRowHeight(row.idx) : undefined}
rowKeyGetter={getRowId ? row => getRowId(row.idx) : undefined}
onSelectedCellChange={handleCellFocus}
onCellKeyDown={onCellKeyDown ? (args, event) => onCellKeyDown({ rowIdx: args.rowIdx, colIdx: args.column.idx }, event) : undefined}
renderers={{
renderRow: rowRenderer,
renderCell: cellRenderer,
noRowsFallback: children,
}}
minimumRowsToRender={100}
/>
</DataGridCellHeaderContext>
</DataGridCellContext>
</DataGridRowContext>
<HeaderDnDContext value={dndHeaderContext}>
<DataGridRowContext value={{ rowCount, onScrollToBottom }}>
<DataGridCellContext value={{ cell, cellText, cellElement, cellTooltip, onCellChange }}>
<DataGridCellHeaderContext value={{ headerElement, headerText, getHeaderDnD, columnSortable, onColumnSort, columnSortingState }}>
<DataGridBase
key={gridKey.current}
ref={innerGridRef}
columns={dndHeaderContext.columns}
rows={rows}
className={className}
headerRowHeight={getHeaderHeight?.()}
onScroll={onScroll}
rowHeight={getRowHeight ? row => getRowHeight(row.idx) : undefined}
rowKeyGetter={getRowId ? row => getRowId(row.idx) : undefined}
onSelectedCellChange={handleCellFocus}
onCellKeyDown={handleCellKeyDown}
renderers={{
renderRow: rowRenderer,
renderCell: cellRenderer,
noRowsFallback: children,
}}
minimumRowsToRender={100}
/>
</DataGridCellHeaderContext>
</DataGridCellContext>
</DataGridRowContext>
</HeaderDnDContext>
);
});
@@ -4,10 +4,16 @@ import type { IGridReactiveValue } from './IGridReactiveValue.js';
export interface IDataGridHeaderCellContext {
headerElement?: IGridReactiveValue<React.ReactNode, [colIdx: number]>;
headerText?: IGridReactiveValue<string, [colIdx: number]>;
getHeaderOrder?: () => number[];
getHeaderWidth?: (colIdx: number) => number | string | null;
getHeaderResizable?: (colIdx: number) => boolean;
getHeaderHeight?: () => number;
getHeaderPinned?: (colIdx: number) => boolean;
getHeaderDnD?: (colIdx: number) => boolean;
onHeaderReorder?: (from: number, to: number) => void;
columnSortingState?: IGridReactiveValue<'asc' | 'desc' | undefined | null, [colIdx: number]>;
columnSortable?: IGridReactiveValue<boolean, [colIdx: number]>;
onColumnSort?: (colIdx: number, order: 'asc' | 'desc' | null, isMultiple: boolean) => void;
}
export const DataGridCellHeaderContext = createContext<IDataGridHeaderCellContext | null>(null);
@@ -21,7 +21,7 @@ export function BooleanFormatter({ value, className, onClick, onKeyDown }: Props
onKeyDown={handleKeyDown}
onClick={onClick}
>
{value === null ? <NullFormatter /> : <Checkbox className="tw:data-disabled:opacity-100" disabled size="small" checked={value} />}
{value === null ? <NullFormatter /> : <Checkbox className="tw:data-disabled:opacity-100!" disabled size="small" checked={value} />}
</Focusable>
);
}
@@ -4,6 +4,7 @@ export * from './DataGridCellInnerContext.js';
export * from './IGridReactiveValue.js';
export * from './useCreateGridReactiveValue.js';
export * from './useGridReactiveValue.js';
export * from './reorderArray.js';
export * from './formatters/NullFormatter.js';
export * from './formatters/BooleanFormatter.js';
@@ -2,6 +2,8 @@ import type { RenderCellProps } from 'react-data-grid';
import type { IInnerRow } from './IInnerRow.js';
import { CellContentRenderer } from './renderers/CellContentRenderer.js';
export function mapCellContentRenderer({ rowIdx, column }: RenderCellProps<IInnerRow, unknown>) {
return <CellContentRenderer rowIdx={rowIdx} colIdx={column.idx} />;
export function mapCellContentRenderer(colIdx: number) {
return function RenderCell({ rowIdx }: RenderCellProps<IInnerRow, unknown>) {
return <CellContentRenderer rowIdx={rowIdx} colIdx={colIdx} />;
};
}
@@ -2,6 +2,8 @@ import type { RenderEditCellProps } from 'react-data-grid';
import type { IInnerRow } from './IInnerRow.js';
import { TextEditor } from './editors/TextEditor.js';
export function mapEditCellRenderer({ rowIdx, column, onClose }: RenderEditCellProps<IInnerRow, unknown>) {
return <TextEditor rowIdx={rowIdx} colIdx={column.idx} onClose={() => onClose()} />;
export function mapEditCellRenderer(colIdx: number) {
return function RenderEditCell({ rowIdx, onClose }: RenderEditCellProps<IInnerRow, unknown>) {
return <TextEditor rowIdx={rowIdx} colIdx={colIdx} onClose={() => onClose()} />;
};
}
@@ -2,6 +2,8 @@ import type { RenderHeaderCellProps } from 'react-data-grid';
import type { IInnerRow } from './IInnerRow.js';
import { HeaderCellContentRenderer } from './renderers/HeaderCellContentRenderer.js';
export function mapRenderHeaderCell({ column }: RenderHeaderCellProps<IInnerRow, unknown>) {
return <HeaderCellContentRenderer colIdx={column.idx} />;
export function mapRenderHeaderCell(colIdx: number) {
return function RenderHeaderCell({ tabIndex }: RenderHeaderCellProps<IInnerRow, unknown>) {
return <HeaderCellContentRenderer colIdx={colIdx} tabIndex={tabIndex} />;
};
}
@@ -1,13 +1,80 @@
import { use } from 'react';
import { memo, use, useRef } from 'react';
import { useDrag, useDrop, type DnDStoreProvider } from '@dbeaver/react-dnd';
import { DataGridCellHeaderContext } from '../DataGridHeaderCellContext.js';
import { useGridReactiveValue } from '../useGridReactiveValue.js';
import { HeaderDnDContext } from '../useHeaderDnD.js';
import { OrderButton } from './OrderButton.js';
interface Props {
colIdx: number;
tabIndex?: number;
}
export function HeaderCellContentRenderer({ colIdx }: Props) {
export const HeaderCellContentRenderer = memo(function HeaderCellContentRenderer({ colIdx, tabIndex }: Props) {
const dndHeaderContext = use(HeaderDnDContext);
const cellHeaderContext = use(DataGridCellHeaderContext);
const headerElement = useGridReactiveValue(cellHeaderContext?.headerElement, colIdx);
const getHeaderText = useGridReactiveValue(headerElement ? undefined : cellHeaderContext?.headerText, colIdx);
return headerElement ?? getHeaderText ?? '';
}
const isColumnSortable = useGridReactiveValue(cellHeaderContext?.columnSortable, colIdx);
const onColumnSort = cellHeaderContext?.onColumnSort;
const sortingState = useGridReactiveValue(cellHeaderContext?.columnSortingState, colIdx);
const orderButtonRef = useRef<HTMLButtonElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const draggable = dndHeaderContext?.getCanDrag?.(colIdx) ?? false;
const drag = useDrag({
draggable,
onDragStart(event, store) {
if (dndHeaderContext?.id) {
store.setData('dbeaver-react-data-grid-id', dndHeaderContext?.id);
}
store.setData('dbeaver-react-data-grid-col-idx', colIdx);
},
onDragEnd() {
dndHeaderContext?.onDragEnd(colIdx);
},
});
function canDrop(event: React.DragEvent<HTMLElement>, store: DnDStoreProvider | null) {
const gridId = store?.getData('dbeaver-react-data-grid-id');
return gridId === dndHeaderContext?.id;
}
const drop = useDrop({
canDrop,
onDragOver(event, store) {
if (!canDrop(event, store)) {
return;
}
const dragColIdx = store?.getData('dbeaver-react-data-grid-col-idx');
const position = event.clientX - event.currentTarget.getBoundingClientRect().left;
const width = event.currentTarget.offsetWidth;
const isAfter = position > width / 2;
dndHeaderContext?.onDragOver(dragColIdx, colIdx, isAfter);
},
});
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Tab' && !e.shiftKey && isColumnSortable && onColumnSort && orderButtonRef.current !== document.activeElement) {
e.preventDefault();
e.stopPropagation();
containerRef.current?.parentElement?.setAttribute('aria-selected', 'false');
orderButtonRef.current?.focus();
}
};
return (
<div
ref={containerRef}
tabIndex={tabIndex}
onKeyDown={handleKeyDown}
className="tw:w-full tw:h-full tw:content-center tw:flex tw:items-center tw:justify-between tw:gap-1 tw:outline-none tw:group"
{...drag.props}
{...drop.props}
>
<span className="tw:overflow-hidden tw:text-ellipsis">{headerElement ?? getHeaderText ?? ''}</span>
{isColumnSortable && onColumnSort && <OrderButton ref={orderButtonRef} colIdx={colIdx} sortState={sortingState} onSort={onColumnSort} />}
</div>
);
});
@@ -0,0 +1,42 @@
import { clsx, IconButton } from '@dbeaver/ui-kit';
interface OrderButtonProps {
colIdx: number;
sortState?: 'asc' | 'desc' | null;
onSort: (attributePosition: number, order: 'asc' | 'desc' | null, isMultiple: boolean) => void;
tabIndex?: number;
ref: React.Ref<HTMLButtonElement>;
}
export function OrderButton({ colIdx, sortState, onSort, tabIndex, ref }: OrderButtonProps) {
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
const nextSortState = sortState === 'asc' ? 'desc' : sortState === 'desc' ? null : 'asc';
onSort(colIdx, nextSortState, e.ctrlKey || e.metaKey);
}
const svgSortAsc = '#icon-sort-asc';
const svgSortDesc = '#icon-sort-desc';
const svgSortUnknown = '#icon-sort-unknown';
const iconSrc = sortState === 'asc' ? svgSortAsc : sortState === 'desc' ? svgSortDesc : svgSortUnknown;
return (
<IconButton
variant="secondary"
size="small"
onClick={handleClick}
tabIndex={tabIndex}
ref={ref}
title="Sort by column"
aria-label="Sort by column"
className={clsx(
'tw:opacity-0 tw:group-focus:opacity-100 tw:focus:opacity-100 tw:group-hover:opacity-100 tw:hover:opacity-100 tw:outline-offset-0',
sortState && 'tw:opacity-100',
)}
>
<svg width="16" height="16" viewBox="0 0 16 16">
<use href={iconSrc} />
</svg>
</IconButton>
);
}
@@ -0,0 +1,11 @@
export function reorderArray<T>(array: T[], from: number, to: number): T[] {
const newArray = [...array];
if (from < to) {
newArray.splice(to, 0, array[from]!);
newArray.splice(from, 1);
} else {
newArray.splice(from, 1);
newArray.splice(to, 0, array[from]!);
}
return newArray;
}
@@ -0,0 +1,94 @@
import type { ColumnOrColumnGroup } from 'react-data-grid';
import type { IInnerRow } from './IInnerRow.js';
import { createContext, useId, useMemo, useState } from 'react';
import { reorderArray } from './reorderArray.js';
interface IHeaderDnDOptions {
columns: Array<ColumnOrColumnGroup<IInnerRow, unknown>>;
getCanDrag?: (colIdx: number) => boolean;
onReorder?: (from: number, to: number) => void;
getHeaderOrder?: () => number[];
}
interface IHeaderDnD {
id: string;
columns: Array<ColumnOrColumnGroup<IInnerRow, unknown>>;
getDataColIdx: (virtualColIdx: number) => number;
getVirtualColIdx: (dataColIdx: number) => number;
getCanDrag?: (colIdx: number) => boolean;
onDragOver: (dragColIdx: number, dropColIdx: number, isAfter: boolean) => void;
onDragEnd: (colIdx: number) => void;
}
export const HeaderDnDContext = createContext<IHeaderDnD | null>(null);
export function useHeaderDnD({ columns, getCanDrag, onReorder, getHeaderOrder }: IHeaderDnDOptions): IHeaderDnD {
const id = useId();
const [activeDnDElement, setActiveDnDElement] = useState<number | null>(null);
const [dragOverElement, setDragOverElement] = useState<[number, boolean] | null>(null);
function getDataColIdx(virtualColIdx: number) {
// Given a visible (ordered) column index, return the original data column index
const order = getHeaderOrder?.();
if (order) {
return order[virtualColIdx] ?? virtualColIdx;
}
return virtualColIdx;
}
function getVirtualColIdx(dataColIdx: number) {
// Given the original data column index, return its visible (ordered) index
const order = getHeaderOrder?.();
if (order) {
return order.indexOf(dataColIdx);
}
return dataColIdx;
}
function onDragEnd(dragColIdx: number) {
setActiveDnDElement(null);
setDragOverElement(null);
if (dragOverElement) {
const isAfter = dragOverElement?.[1];
let to = dragOverElement?.[0];
if (isAfter) {
to = getDataColIdx(getVirtualColIdx(to) + 1);
}
onReorder?.(dragColIdx, to);
}
}
function onDragOver(dragColIdx: number, dropColIdx: number, isAfter: boolean) {
setActiveDnDElement(dragColIdx);
if (dragColIdx !== dropColIdx) {
setDragOverElement([dropColIdx, isAfter]);
}
}
columns = useMemo(() => columns.map((col, idx) => columns[getDataColIdx(idx)]!), [columns, getHeaderOrder]);
if (activeDnDElement !== null && dragOverElement !== null) {
const from = getVirtualColIdx(activeDnDElement);
let to = getVirtualColIdx(dragOverElement[0]);
const isAfter = dragOverElement[1];
if (isAfter) {
to++;
}
columns = reorderArray(columns, from, to);
}
return {
id,
columns,
getDataColIdx,
getVirtualColIdx,
getCanDrag,
onDragOver,
onDragEnd,
};
}
@@ -14,6 +14,9 @@
"src"
],
"references": [
{
"path": "../react-dnd"
},
{
"path": "../ui-kit"
}
@@ -0,0 +1 @@
# @dbeaver/react-dnd
@@ -0,0 +1,36 @@
{
"name": "@dbeaver/react-dnd",
"type": "module",
"sideEffects": [],
"exports": {
".": "./lib/index.js",
"./*": "./lib/*.js"
},
"scripts": {
"clear": "rimraf lib"
},
"files": [
"package.json",
"LICENSE",
"README.md",
"CHANGELOG.md",
"lib",
"!lib/**/*.d.ts.map",
"!lib/**/*.test.js",
"!lib/**/*.test.d.ts",
"!lib/**/*.test.d.ts.map",
"!lib/**/*.test.js.map",
"!lib/tests",
"!.tsbuildinfo"
],
"packageManager": "yarn@4.6.0",
"devDependencies": {
"@dbeaver/tsconfig": "workspace:^",
"@types/react": "^19",
"rimraf": "^6",
"typescript": "^5"
},
"dependencies": {
"react": "^19"
}
}
@@ -0,0 +1,3 @@
export * from './useDrag.js';
export * from './useDrop.js';
export { type DnDStoreProvider } from './store.js';
@@ -0,0 +1,46 @@
export const DND_STORE_PREFIX = 'application/dbeaver-react-dnd-';
export interface DnDStoreProvider {
getData: (key: string) => any;
setData: (key: string, value: any) => void;
removeData: () => void;
}
class DnDStore {
private data: Map<string, Record<string, any>> = new Map();
setData(id: string, key: string, value: any) {
if (!this.data.has(id)) {
this.data.set(id, {});
}
this.data.get(id)![key] = value;
}
getData(id: string, key: string) {
return this.data.get(id)?.[key];
}
removeData(id: string) {
this.data.delete(id);
}
getProvider(id: string) {
return {
getData: (key: string) => this.getData(id, key),
setData: (key: string, value: any) => {
this.setData(id, key, value);
},
removeData: () => {
this.removeData(id);
},
};
}
}
export function getStoreProvider(event: React.DragEvent<HTMLElement>): DnDStoreProvider | null {
const id = event.dataTransfer.types.find(type => type.startsWith(DND_STORE_PREFIX))?.slice(DND_STORE_PREFIX.length);
return id ? globalDnDStore.getProvider(id) : null;
}
// TODO: do we want to use react context here?
// the main idea was to use native DataTransfer, but it's available only for drag start and drop events
// so we need to use a custom store
export const globalDnDStore = new DnDStore();
@@ -0,0 +1,67 @@
import { useId, useMemo, useRef, useState } from 'react';
import { DND_STORE_PREFIX, globalDnDStore, type DnDStoreProvider } from './store.js';
interface IDragOptions {
draggable?: boolean;
onDrag?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider) => void;
onDragStart?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider) => void;
onDragEnd?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider) => void;
}
interface IDrag {
isDragging: boolean;
props: {
draggable?: boolean;
onDrag: (event: React.DragEvent<HTMLElement>) => void;
onDragStart: (event: React.DragEvent<HTMLElement>) => void;
onDragEnd: (event: React.DragEvent<HTMLElement>) => void;
};
}
export function useDrag({ draggable, onDragStart, onDrag, onDragEnd }: IDragOptions = {}): IDrag {
const id = useId();
const [dragging, setDragging] = useState(false);
const optionsRef = useRef<IDragOptions>({ draggable, onDragStart, onDrag, onDragEnd });
optionsRef.current = { draggable, onDragStart, onDrag, onDragEnd };
function handleDragStart(event: React.DragEvent<HTMLElement>) {
if (!optionsRef.current.draggable) {
return;
}
event.stopPropagation();
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData(`${DND_STORE_PREFIX}${id}`, id);
setDragging(true);
optionsRef.current.onDragStart?.(event, globalDnDStore.getProvider(id));
}
function handleDrag(event: React.DragEvent<HTMLElement>) {
if (!optionsRef.current.draggable) {
return;
}
optionsRef.current.onDrag?.(event, globalDnDStore.getProvider(id));
}
function handleDragEnd(event: React.DragEvent<HTMLElement>) {
if (!optionsRef.current.draggable) {
return;
}
setDragging(false);
optionsRef.current.onDragEnd?.(event, globalDnDStore.getProvider(id));
globalDnDStore.removeData(id);
}
return {
isDragging: dragging,
props: {
draggable,
...useMemo(
() => ({
onDrag: handleDrag,
onDragStart: handleDragStart,
onDragEnd: handleDragEnd,
}),
[],
),
},
};
}
@@ -0,0 +1,61 @@
import { useMemo, useRef } from 'react';
import { getStoreProvider, type DnDStoreProvider } from './store.js';
interface IDropOptions {
canDrop?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => boolean;
onDrop?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => void;
onDragOver?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => void;
onDragEnter?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => void;
onDragLeave?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => void;
}
interface IDrop {
props: {
onDrop: (event: React.DragEvent<HTMLElement>) => void;
onDragOver: (event: React.DragEvent<HTMLElement>) => void;
onDragEnter: (event: React.DragEvent<HTMLElement>) => void;
onDragLeave: (event: React.DragEvent<HTMLElement>) => void;
};
}
export function useDrop({ canDrop, onDrop, onDragOver, onDragEnter, onDragLeave }: IDropOptions = {}): IDrop {
const optionsRef = useRef<IDropOptions>({ canDrop, onDrop, onDragOver, onDragEnter, onDragLeave });
optionsRef.current = { canDrop, onDrop, onDragOver, onDragEnter, onDragLeave };
function handleOver(event: React.DragEvent<HTMLElement>) {
const provider = getStoreProvider(event);
const isDroppable = optionsRef.current.canDrop?.(event, provider);
if (isDroppable) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
} else {
event.dataTransfer.dropEffect = 'none';
}
optionsRef.current.onDragOver?.(event, provider);
}
function handleEnter(event: React.DragEvent<HTMLElement>) {
const provider = getStoreProvider(event);
optionsRef.current.onDragEnter?.(event, provider);
}
function handleLeave(event: React.DragEvent<HTMLElement>) {
const provider = getStoreProvider(event);
optionsRef.current.onDragLeave?.(event, provider);
}
function handleDrop(event: React.DragEvent<HTMLElement>) {
const provider = getStoreProvider(event);
optionsRef.current.onDrop?.(event, provider);
}
return {
props: useMemo(
() => ({
onDrop: handleDrop,
onDragOver: handleOver,
onDragEnter: handleEnter,
onDragLeave: handleLeave,
}),
[],
),
};
}
@@ -0,0 +1,16 @@
{
"extends": "@dbeaver/tsconfig/tsconfig.webview.json",
"compilerOptions": {
"tsBuildInfoFile": "./lib/.tsbuildinfo",
"rootDir": "src",
"rootDirs": [
"src"
],
"outDir": "lib",
"incremental": true,
"composite": true
},
"include": [
"src"
]
}
@@ -1,4 +1,4 @@
@import 'tailwindcss' prefix(tw);
@import 'tailwindcss' layer(base) prefix(tw);
@layer theme {
main {
@@ -86,6 +86,7 @@
:root[data-theme='dark'] {
--dbv-kit-color-foreground: var(--tw-color-gray-200);
--dbv-kit-color-background: var(--tw-color-gray-900);
--dbv-kit-btn-loader-base-color: var(--tw-color-gray-900);
/* Primary Button */
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="currentColor">
<polygon points="2 10 12 10 7 4 2 10" />
<rect x="6" y="6" width="2" height="22" />
<rect x="14" y="6" width="16" height="2" />
<rect x="14" y="15" width="12" height="2" />
<rect x="14" y="24" width="8" height="2" />
</svg>

After

Width:  |  Height:  |  Size: 353 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<polygon points="2 8.14 9.14 8.14 5.57 3.86 2 8.14" />
<rect x="4.86" y="5.29" width="1.43" height="15.71" />
<rect x="10.57" y="5.29" width="11.43" height="1.43" />
<rect x="10.57" y="11.71" width="8.57" height="1.43" />
<rect x="10.57" y="18.14" width="5.71" height="1.43" />
</svg>

After

Width:  |  Height:  |  Size: 414 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
<polygon points="0 4 5 4 2.5 1 0 4" />
<rect x="2" y="4" width="1" height="11" />
<rect x="6" y="3" width="10" height="1" />
<rect x="6" y="8" width="7" height="1" />
<rect x="6" y="13" width="4" height="1" />
</svg>

After

Width:  |  Height:  |  Size: 346 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="currentColor">
<polygon points="12 22 2 22 7 28 12 22" />
<rect x="6" y="4" width="2" height="22" />
<rect x="14" y="6" width="16" height="2" />
<rect x="14" y="15" width="12" height="2" />
<rect x="14" y="24" width="8" height="2" />
</svg>

After

Width:  |  Height:  |  Size: 355 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<polygon points="9.14 16.71 2 16.71 5.57 21 9.14 16.71" />
<rect x="4.86" y="3.86" width="1.43" height="15.71" />
<rect x="10.57" y="5.29" width="11.43" height="1.43" />
<rect x="10.57" y="11.71" width="8.57" height="1.43" />
<rect x="10.57" y="18.14" width="5.71" height="1.43" />
</svg>

After

Width:  |  Height:  |  Size: 418 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
<polygon points="5 12 0 12 2.5 15 5 12" />
<rect x="2" y="1" width="1" height="12" />
<rect x="6" y="3" width="10" height="1" />
<rect x="6" y="8" width="7" height="1" />
<rect x="6" y="13" width="4" height="1" />
</svg>

After

Width:  |  Height:  |  Size: 350 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="currentColor">
<polygon points="4 10 14 10 9 4 4 10" />
<rect x="8" y="6" width="2" height="22" />
<polygon points="28 22 18 22 23 28 28 22" />
<rect x="22" y="4" width="2" height="22" />
</svg>

After

Width:  |  Height:  |  Size: 307 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<polygon points="3.4 8.29 10.55 8.29 6.98 4 3.4 8.29" />
<rect x="6.26" y="5.43" width="1.43" height="15.71" />
<polygon points="20.55 16.86 13.4 16.86 16.98 21.14 20.55 16.86" />
<rect x="16.26" y="4" width="1.43" height="15.71" />
</svg>

After

Width:  |  Height:  |  Size: 367 B

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
<polygon points="2 4 7 4 4.5 1 2 4" />
<rect x="4" y="4" width="1" height="11" />
<polygon points="14 12 9 12 11.5 15 14 12" />
<rect x="11" y="1" width="1" height="11" />
</svg>

After

Width:  |  Height:  |  Size: 306 B

@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="icon-sort-unknown" viewBox="0 0 24 24" fill="currentColor">
<polygon points="3.4 8.29 10.55 8.29 6.98 4 3.4 8.29" />
<rect x="6.26" y="5.43" width="1.43" height="15.71" />
<polygon points="20.55 16.86 13.4 16.86 16.98 21.14 20.55 16.86" />
<rect x="16.26" y="4" width="1.43" height="15.71" />
</symbol>
<symbol id="icon-sort-desc" viewBox="0 0 24 24"
fill="currentColor">
<polygon points="9.14 16.71 2 16.71 5.57 21 9.14 16.71" />
<rect x="4.86" y="3.86" width="1.43" height="15.71" />
<rect x="10.57" y="5.29" width="11.43" height="1.43" />
<rect x="10.57" y="11.71" width="8.57" height="1.43" />
<rect x="10.57" y="18.14" width="5.71" height="1.43" />
</symbol>
<symbol id="icon-sort-asc" viewBox="0 0 24 24" fill="currentColor">
<polygon points="2 8.14 9.14 8.14 5.57 3.86 2 8.14" />
<rect x="4.86" y="5.29" width="1.43" height="15.71" />
<rect x="10.57" y="5.29" width="11.43" height="1.43" />
<rect x="10.57" y="11.71" width="8.57" height="1.43" />
<rect x="10.57" y="18.14" width="5.71" height="1.43" />
</symbol>
<symbol id="icon-cross" viewBox="0 0 24 24">
<path xmlns="http://www.w3.org/2000/svg" fill="currentColor" fill-rule="evenodd"
clip-rule="evenodd"
d="M3.31579 3.31579C2.89474 3.73684 2.89474 4.42105 3.31579 4.84211L10.4737 12L3.31579 19.1579C2.89474 19.5789 2.89474 20.2632 3.31579 20.6842C3.73684 21.1053 4.42105 21.1053 4.84211 20.6842L12 13.5263L19.1579 20.6842C19.5789 21.1053 20.2632 21.1053 20.6842 20.6842C21.1053 20.2632 21.1053 19.5789 20.6842 19.1579L13.5263 12L20.6842 4.84211C21.1053 4.42105 21.1053 3.73684 20.6842 3.31579C20.2632 2.89474 19.5789 2.89474 19.1579 3.31579L12 10.4737L4.84211 3.31579C4.42105 2.89474 3.73684 2.89474 3.31579 3.31579Z" />
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+4 -3
View File
@@ -7,7 +7,8 @@
"exports": {
".": "./lib/index.js",
"./*.css": "./src/*.css",
"./*": "./lib/*.js"
"./*": "./lib/*.js",
"./assets/icons/*.svg": "./assets/icons/*.svg"
},
"scripts": {
"build": "tsc -b",
@@ -18,13 +19,13 @@
"devDependencies": {
"@dbeaver/tsconfig": "workspace:^",
"@ladle/react": "^5",
"@tailwindcss/vite": "^4",
"@tailwindcss/vite": "4.0.7",
"@types/react": "^19",
"@types/react-dom": "^19",
"@wroud/vite-plugin-asset-resolver": "^0",
"@wroud/vite-plugin-tsc": "^0.11.6",
"rimraf": "^6",
"tailwindcss": "^4",
"tailwindcss": "4.0.7",
"typescript": "^5",
"vite": "^6"
},
@@ -0,0 +1,16 @@
export default function template(files) {
const exportLines = files.map((file) => {
const { path } = file;
const fileName = path.split('/').pop();
const componentNameWithExt = fileName.replace(/\.tsx$/, '.js');
const componentName = fileName.replace(/\.tsx$/, '');
return `export { default as ${componentName} } from './${componentNameWithExt}';`;
});
const header = '/* This file is automatically generated by the build:icons script. Do not modify it manually.*/\n\n ';
return header + exportLines.join('\n');
};
@@ -4,19 +4,8 @@
@layer base {
.dbv-kit-button {
display: inline-flex;
text-align: center;
text-decoration: none;
cursor: pointer;
user-select: none;
position: relative;
justify-content: center;
align-items: center;
white-space: nowrap;
height: var(--dbv-kit-btn-height);
padding-inline: var(--dbv-kit-btn-padding-inline);
gap: var(--dbv-kit-btn-gap);
background-color: var(--dbv-kit-btn-background);
color: var(--dbv-kit-btn-foreground);
@@ -29,6 +18,8 @@
font-size: var(--dbv-kit-btn-font-size);
font-weight: var(--dbv-kit-btn-font-weight);
cursor: pointer;
&:focus-visible,
&[data-focus-visible] {
outline: var(--dbv-kit-control-outline-width) solid var(--dbv-kit-btn-outline-color);
@@ -37,18 +28,16 @@
&:hover {
@media (hover: hover) {
color: var(--dbv-kit-btn-foreground-hover);
background-color: var(--dbv-kit-btn-background-hover);
}
}
&:active,
&[data-active] {
color: var(--dbv-kit-btn-foreground-active);
background-color: var(--dbv-kit-btn-background-active);
}
&:disabled {
opacity: var(--dbv-kit-control-disabled-opacity);
}
}
/* ---------------------------- */
@@ -94,55 +83,76 @@
/* Variants */
.dbv-kit-button--primary {
--dbv-kit-btn-background: var(--dbv-kit-btn-primary-background);
--dbv-kit-btn-foreground: var(--dbv-kit-btn-primary-foreground);
--dbv-kit-btn-foreground-hover: var(--dbv-kit-btn-primary-foreground-hover);
--dbv-kit-btn-foreground-active: var(--dbv-kit-btn-primary-foreground-active);
--dbv-kit-btn-background: var(--dbv-kit-btn-primary-background);
--dbv-kit-btn-background-hover: var(--dbv-kit-btn-primary-background-hover);
--dbv-kit-btn-background-active: var(--dbv-kit-btn-primary-background-active);
--dbv-kit-btn-loader-color: var(--dbv-kit-btn-background-hover);
--dbv-kit-btn-loader-color: var(--dbv-kit-btn-primary-foreground);
&:disabled {
background-color: var(--dbv-kit-control-disabled-background);
color: var(--dbv-kit-control-disabled-foreground);
}
}
.dbv-kit-button--secondary {
--dbv-kit-btn-background: var(--dbv-kit-btn-secondary-background);
--dbv-kit-btn-foreground: var(--dbv-kit-btn-secondary-foreground);
--dbv-kit-btn-foreground-hover: var(--dbv-kit-btn-secondary-foreground-hover);
--dbv-kit-btn-foreground-active: var(--dbv-kit-btn-secondary-foreground-active);
--dbv-kit-btn-background: var(--dbv-kit-btn-secondary-background);
--dbv-kit-btn-background-hover: var(--dbv-kit-btn-secondary-background-hover);
--dbv-kit-btn-background-active: var(--dbv-kit-btn-secondary-background-active);
--dbv-kit-btn-border-color: var(--dbv-kit-btn-secondary-border-color);
--dbv-kit-btn-loader-color: var(--dbv-kit-btn-secondary-foreground);
&:disabled {
color: var(--dbv-kit-control-disabled-foreground);
border-color: var(--dbv-kit-control-disabled-foreground);
}
}
.dbv-kit-button--danger {
--dbv-kit-btn-background: var(--dbv-kit-btn-danger-background);
--dbv-kit-btn-foreground: var(--dbv-kit-btn-danger-foreground);
--dbv-kit-btn-foreground-hover: var(--dbv-kit-btn-danger-foreground);
--dbv-kit-btn-foreground-active: var(--dbv-kit-btn-danger-foreground-active);
--dbv-kit-btn-background: var(--dbv-kit-btn-danger-background);
--dbv-kit-btn-background-hover: var(--dbv-kit-btn-danger-background-hover);
--dbv-kit-btn-background-active: var(--dbv-kit-btn-danger-background-active);
--dbv-kit-btn-border-color: var(--dbv-kit-btn-danger-border-color);
--dbv-kit-btn-loader-color: var(--dbv-kit-btn-danger-background);
--dbv-kit-btn-loader-color: var(--dbv-kit-btn-danger-foreground);
--dbv-kit-btn-outline-color: var(--dbv-kit-btn-danger-color-outline);
&:disabled {
background-color: var(--dbv-kit-control-disabled-background);
color: var(--dbv-kit-control-disabled-foreground);
}
}
/* ---------------------------- */
/* Loader */
.dbv-kit-button[data-loading='true'] {
opacity: 0.75;
color: color-mix(in oklch, var(--dbv-kit-btn-foreground) 25%, var(--dbv-kit-btn-loader-base-color));
background-color: color-mix(in oklch, var(--dbv-kit-btn-loader-base-color) 75%, var(--dbv-kit-btn-background));
background-color: var(--dbv-kit-btn-background-hover);
&:hover,
&:active,
&[data-active] {
background-color: color-mix(in oklch, var(--dbv-kit-btn-loader-base-color) 85%, var(--dbv-kit-btn-background));
background-color: var(--dbv-kit-btn-background-active);
}
}
.dbv-kit-button__loader {
.dbv-kit-button .dbv-kit-spinner {
--dbv-kit-spinner-stroke-color: var(--dbv-kit-btn-loader-color);
--dbv-kit-spinner-height: calc(var(--dbv-kit-btn-height) * 0.6);
--dbv-kit-spinner-stroke-width: 3px;
position: absolute;
}
/* ---------------------------- */
@@ -5,34 +5,21 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { Button as AriaButton, type ButtonProps as AriaKitButtonProps } from '@ariakit/react';
import clsx from 'clsx';
import './Button.css';
import type { ControlSize } from '../types/controls.js';
import { componentProviderWrapper } from '../componentProviderWrapper.js';
import { Spinner } from '../Spinner/Spinner.js';
import { ButtonRoot, type ButtonRootProps } from './ButtonRoot.js';
import './Button.css';
export interface ButtonProps extends Omit<AriaKitButtonProps, 'clickOnEnter' | 'clickOnSpace'> {
export interface ButtonProps extends ButtonRootProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: ControlSize;
loading?: boolean;
loader?: React.ReactNode;
}
export function ButtonBase({ className, variant = 'primary', size = 'medium', loading, loader, children, onClick, ...props }: ButtonProps) {
export function ButtonBase({ className, variant = 'primary', size = 'medium', ...props }: ButtonProps) {
const classToApply = clsx('dbv-kit-button', `dbv-kit-button--${variant}`, `dbv-kit-button--${size}`, className);
if (loading) {
props['aria-busy'] = true;
props['data-loading'] = true;
}
return (
<AriaButton onClick={loading ? () => null : onClick} className={classToApply} {...props}>
{loading && (loader ? loader : <Spinner className="dbv-kit-button__loader" />)}
{children}
</AriaButton>
);
return <ButtonRoot className={classToApply} {...props} />;
}
export interface ButtonIconProps extends React.HTMLAttributes<HTMLSpanElement> {
placement?: 'start' | 'end';
@@ -43,9 +30,10 @@ export interface ButtonIconProps extends React.HTMLAttributes<HTMLSpanElement> {
*
* @param props.placement This property is needed to adjust icon placement inside a button. The icon with placement="start" will cut the inline-start padding. placement="end" will affect the padding-inline-end accordingly. This property supports RTL and LTR, so you don't need to think about it.
*/
ButtonBase.Icon = function ButtonBaseIcon({ className, children, placement }: ButtonIconProps) {
export function ButtonBaseIcon({ className, children, placement }: ButtonIconProps) {
const classToApply = clsx('dbv-kit-button__icon', placement && `dbv-kit-button__icon--${placement}`, className);
return <span className={classToApply}>{children}</span>;
};
}
export const Button = componentProviderWrapper('Button', ButtonBase);
export const ButtonIcon = componentProviderWrapper('ButtonIcon', ButtonBaseIcon);
@@ -0,0 +1,25 @@
@layer base {
.dbv-kit-button__loader {
position: absolute;
display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
justify-content: center;
align-items: center;
}
.dbv-kit-button__content {
display: flex;
text-align: center;
text-decoration: none;
user-select: none;
position: relative;
justify-content: center;
align-items: center;
white-space: nowrap;
transition: opacity 0.2s ease-in-out;
gap: var(--dbv-kit-btn-gap);
}
}
@@ -0,0 +1,23 @@
import { Button as AriaButton, type ButtonProps as AriaKitButtonProps } from '@ariakit/react';
import { Spinner } from '../Spinner/Spinner.js';
import { clsx } from '../utils/clsx.js';
import './ButtonRoot.css';
export interface ButtonRootProps extends Omit<AriaKitButtonProps, 'clickOnEnter' | 'clickOnSpace'> {
loading?: boolean;
loader?: React.ReactNode;
}
export function ButtonRoot({ loading, loader, children, onClick, className, ...props }: ButtonRootProps) {
if (loading) {
props['aria-busy'] = true;
props['data-loading'] = true;
}
return (
<AriaButton onClick={loading ? () => null : onClick} className={clsx(className, 'tw:relative')} {...props}>
<div className={clsx('dbv-kit-button__content', loading && 'tw:opacity-0')}>{children}</div>
{loading && <div className="dbv-kit-button__loader">{loader ? loader : <Spinner />}</div>}
</AriaButton>
);
}
@@ -2,14 +2,18 @@
:root {
--dbv-kit-btn-height: var(--dbv-kit-control-height-medium);
--dbv-kit-btn-padding-inline: calc(var(--tw-spacing) * 2.5);
--dbv-kit-btn-gap: calc(var(--dbv-kit-btn-padding-inline) / 4);
--dbv-kit-btn-gap: calc(var(--dbv-kit-btn-padding-inline) / 2);
--dbv-kit-btn-outline-offset: var(--dbv-kit-control-outline-offset);
--dbv-kit-btn-outline-color: var(--dbv-kit-control-outline-color);
--dbv-kit-btn-foreground: var(--tw-color-white);
--dbv-kit-btn-foreground-hover: var(--tw-color-white);
--dbv-kit-btn-foreground-active: var(--tw-color-white);
--dbv-kit-btn-background: var(--dbv-kit-color-primary-600);
--dbv-kit-btn-background-hover: var(--dbv-kit-color-primary-700);
--dbv-kit-btn-background-active: var(--dbv-kit-color-primary-800);
--dbv-kit-btn-outline-color: var(--dbv-kit-control-outline-color);
--dbv-kit-btn-border-width: var(--dbv-kit-control-border-width);
--dbv-kit-btn-border-color: transparent;
@@ -2,22 +2,30 @@
:root {
/* Primary Button */
--dbv-kit-btn-primary-foreground: var(--tw-color-white);
--dbv-kit-btn-primary-foreground-hover: var(--tw-color-white);
--dbv-kit-btn-primary-foreground-active: var(--tw-color-white);
--dbv-kit-btn-primary-background: var(--dbv-kit-color-primary-600);
--dbv-kit-btn-primary-background-hover: var(--dbv-kit-color-primary-700);
--dbv-kit-btn-primary-background-active: var(--dbv-kit-color-primary-800);
/* Secondary Button */
--dbv-kit-btn-secondary-foreground: var(--dbv-kit-color-primary-600);
--dbv-kit-btn-secondary-foreground-hover: var(--dbv-kit-color-primary-600);
--dbv-kit-btn-secondary-foreground-active: var(--dbv-kit-color-primary-600);
--dbv-kit-btn-secondary-background: transparent;
--dbv-kit-btn-secondary-background-hover: var(--dbv-kit-color-primary-50);
--dbv-kit-btn-secondary-background-active: var(--dbv-kit-color-primary-100);
--dbv-kit-btn-secondary-border-color: var(--dbv-kit-color-primary-600);
--dbv-kit-btn-secondary-border-color-hover: var(--dbv-kit-color-primary-600);
/* Danger Button */
--dbv-kit-btn-danger-foreground: var(--tw-color-white);
--dbv-kit-btn-danger-foreground-hover: var(--tw-color-white);
--dbv-kit-btn-danger-foreground-active: var(--tw-color-white);
--dbv-kit-btn-danger-background: var(--tw-color-red-600);
--dbv-kit-btn-danger-background-hover: var(--tw-color-red-700);
--dbv-kit-btn-danger-background-active: var(--tw-color-red-800);
--dbv-kit-btn-danger-border-color: transparent;
--dbv-kit-btn-danger-color-outline: var(--tw-color-red-300);
}
}
@@ -54,8 +54,8 @@ export function CheckboxBase({ children, className, icon, indeterminate, indeter
>
{checked &&
(icon ?? (
<svg fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 16 16">
<polyline points="3,7 7,11 13,4" />
<svg stroke="currentColor" strokeWidth="2" viewBox="0 0 16 16">
<polyline fill="none" points="3,7 7,11 13,4" />
</svg>
))}
{indeterminate &&
@@ -1,5 +1,6 @@
import { createContext } from 'react';
import type { ButtonBase } from './Button/Button.js';
import type { ButtonBase, ButtonBaseIcon } from './Button/Button.js';
import type { IconButtonBase } from './IconButton/IconButton.js';
import type { CheckboxBase } from './Checkbox/Checkbox.js';
import type { Radio } from './Radio/Radio.js';
import type { InputBase } from './Input/Input.js';
@@ -9,6 +10,8 @@ import type { SelectField } from './Select/SelectField.js';
export interface IComponentProvider {
Button?: typeof ButtonBase;
ButtonIcon?: typeof ButtonBaseIcon;
IconButton?: typeof IconButtonBase;
Input?: typeof InputBase;
Checkbox?: typeof CheckboxBase;
Radio?: typeof Radio;
@@ -0,0 +1,110 @@
@import './_base.css';
@import './_sizes.css';
@import './_variants.css';
@layer base {
.dbv-kit-icon-button {
min-width: var(--dbv-kit-icon-btn-size);
width: var(--dbv-kit-icon-btn-size);
height: var(--dbv-kit-icon-btn-size);
font-size: calc(var(--dbv-kit-icon-btn-size) * 0.75);
border-radius: var(--dbv-kit-icon-btn-border-radius);
border-color: var(--dbv-kit-icon-btn-border-color);
border-width: var(--dbv-kit-icon-btn-border-width);
border-style: var(--dbv-kit-icon-btn-border-style);
color: var(--dbv-kit-icon-btn-foreground);
background-color: var(--dbv-kit-icon-btn-background);
cursor: pointer;
&:focus-visible,
&[data-focus-visible] {
outline: var(--dbv-kit-icon-btn-outline-width) solid var(--dbv-kit-icon-btn-outline-color);
outline-offset: var(--dbv-kit-icon-btn-outline-offset);
}
&:hover {
@media (hover: hover) {
color: var(--dbv-kit-icon-btn-foreground-hover);
background-color: var(--dbv-kit-icon-btn-background-hover);
}
}
&:active,
&[data-active] {
color: var(--dbv-kit-icon-btn-foreground-active);
background-color: var(--dbv-kit-icon-btn-background-active);
}
&:disabled {
opacity: var(--dbv-kit-control-disabled-opacity);
}
&[data-loading='true'] {
background-color: var(--dbv-kit-icon-btn-background);
&:hover,
&:active,
&[data-active] {
background-color: var(--dbv-kit-icon-btn-background-active);
}
}
.dbv-kit-spinner {
--dbv-kit-spinner-stroke-color: var(--dbv-kit-icon-btn-loader-color);
--dbv-kit-spinner-height: calc(var(--dbv-kit-icon-btn-size) * 0.8);
--dbv-kit-spinner-stroke-width: 3px;
}
}
/* Size variants */
.dbv-kit-icon-button--small {
--dbv-kit-icon-btn-size: var(--dbv-kit-icon-btn-small-size);
}
.dbv-kit-icon-button--medium {
--dbv-kit-icon-btn-size: var(--dbv-kit-icon-btn-medium-size);
}
.dbv-kit-icon-button--large {
--dbv-kit-icon-btn-size: var(--dbv-kit-icon-btn-large-size);
}
.dbv-kit-icon-button--xlarge {
--dbv-kit-icon-btn-size: var(--dbv-kit-icon-btn-xlarge-size);
}
/* Variant styles */
.dbv-kit-icon-button--primary {
--dbv-kit-icon-btn-foreground: var(--dbv-kit-icon-btn-primary-foreground);
--dbv-kit-icon-btn-foreground-hover: var(--dbv-kit-icon-btn-primary-foreground-hover);
--dbv-kit-icon-btn-foreground-active: var(--dbv-kit-icon-btn-primary-foreground-active);
--dbv-kit-icon-btn-background: var(--dbv-kit-icon-btn-primary-background);
--dbv-kit-icon-btn-background-hover: var(--dbv-kit-icon-btn-primary-background-hover);
--dbv-kit-icon-btn-background-active: var(--dbv-kit-icon-btn-primary-background-active);
--dbv-kit-icon-btn-loader-color: var(--dbv-kit-icon-btn-primary-foreground);
}
.dbv-kit-icon-button--secondary {
--dbv-kit-icon-btn-foreground: var(--dbv-kit-icon-btn-secondary-foreground);
--dbv-kit-icon-btn-foreground-hover: var(--dbv-kit-icon-btn-secondary-foreground-hover);
--dbv-kit-icon-btn-foreground-active: var(--dbv-kit-icon-btn-secondary-foreground-active);
--dbv-kit-icon-btn-background: var(--dbv-kit-icon-btn-secondary-background);
--dbv-kit-icon-btn-background-hover: var(--dbv-kit-icon-btn-secondary-background-hover);
--dbv-kit-icon-btn-background-active: var(--dbv-kit-icon-btn-secondary-background-active);
--dbv-kit-icon-btn-border-color: var(--dbv-kit-icon-btn-secondary-border-color);
--dbv-kit-icon-btn-loader-color: var(--dbv-kit-icon-btn-secondary-loader-color);
}
.dbv-kit-icon-button--danger {
--dbv-kit-icon-btn-foreground: var(--dbv-kit-icon-btn-danger-foreground);
--dbv-kit-icon-btn-foreground-hover: var(--dbv-kit-icon-btn-danger-foreground-hover);
--dbv-kit-icon-btn-foreground-active: var(--dbv-kit-icon-btn-danger-foreground-active);
--dbv-kit-icon-btn-background: var(--dbv-kit-icon-btn-danger-background);
--dbv-kit-icon-btn-background-hover: var(--dbv-kit-icon-btn-danger-background-hover);
--dbv-kit-icon-btn-background-active: var(--dbv-kit-icon-btn-danger-background-active);
--dbv-kit-icon-btn-loader-color: var(--dbv-kit-icon-btn-danger-loader-color);
}
}
@@ -0,0 +1,32 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { clsx } from 'clsx';
import { componentProviderWrapper } from '../componentProviderWrapper.js';
import { ButtonRoot, type ButtonRootProps } from '../Button/ButtonRoot.js';
import type { ControlSize } from '../types/controls.js';
import './IconButton.css';
export interface IconButtonProps extends ButtonRootProps {
'aria-label': string;
variant?: 'primary' | 'secondary' | 'danger';
size?: ControlSize;
}
export function IconButtonBase({ className, variant = 'primary', size = 'medium', children, ...props }: IconButtonProps) {
const classToApply = clsx('dbv-kit-icon-button', `dbv-kit-icon-button--${variant}`, `dbv-kit-icon-button--${size}`, className);
return (
<ButtonRoot className={classToApply} {...props}>
{children}
</ButtonRoot>
);
}
export const IconButton = componentProviderWrapper('IconButton', IconButtonBase);
@@ -0,0 +1,30 @@
@layer base {
:root {
/* Icon Button border variables */
--dbv-kit-icon-btn-border-radius: var(--dbv-kit-btn-border-radius);
--dbv-kit-icon-btn-border-width: var(--dbv-kit-btn-border-width);
--dbv-kit-icon-btn-border-style: var(--dbv-kit-btn-border-style);
--dbv-kit-icon-btn-border-color: var(--dbv-kit-btn-border-color);
/* Icon Button outline variables */
--dbv-kit-icon-btn-outline-width: var(--dbv-kit-control-outline-width);
--dbv-kit-icon-btn-outline-style: var(--dbv-kit-control-outline-style);
--dbv-kit-icon-btn-outline-color: var(--dbv-kit-control-outline-color);
--dbv-kit-icon-btn-outline-offset: var(--dbv-kit-control-outline-offset);
/* Default size */
--dbv-kit-icon-btn-size: var(--dbv-kit-control-height-medium);
/* Default colors */
--dbv-kit-icon-btn-foreground: var(--tw-color-gray-600);
--dbv-kit-icon-btn-foreground-hover: var(--tw-color-gray-600);
--dbv-kit-icon-btn-foreground-active: var(--tw-color-gray-700);
--dbv-kit-icon-btn-background: transparent;
--dbv-kit-icon-btn-background-hover: var(--tw-color-gray-100);
--dbv-kit-icon-btn-background-active: var(--tw-color-gray-200);
/* Loader color */
--dbv-kit-btn-loader-color: var(--dbv-kit-btn-foreground-hover);
--dbv-kit-btn-loader-base-color: transparent;
}
}
@@ -0,0 +1,8 @@
@layer base {
:root {
--dbv-kit-icon-btn-small-size: var(--dbv-kit-control-height-small);
--dbv-kit-icon-btn-medium-size: var(--dbv-kit-control-height-medium);
--dbv-kit-icon-btn-large-size: var(--dbv-kit-control-height-large);
--dbv-kit-icon-btn-xlarge-size: var(--dbv-kit-control-height-xlarge);
}
}
@@ -0,0 +1,31 @@
@layer base {
:root {
/* Primary */
--dbv-kit-icon-btn-primary-foreground: var(--tw-color-gray-600);
--dbv-kit-icon-btn-primary-foreground-hover: var(--dbv-kit-color-primary-600);
--dbv-kit-icon-btn-primary-foreground-active: var(--dbv-kit-color-primary-700);
--dbv-kit-icon-btn-primary-background: transparent;
--dbv-kit-icon-btn-primary-background-hover: var(--tw-color-gray-100);
--dbv-kit-icon-btn-primary-background-active: var(--tw-color-gray-200);
--dbv-kit-icon-btn-primary-loader-color: var(--tw-color-gray-300);
/* Secondary */
--dbv-kit-icon-btn-secondary-foreground: var(--tw-color-gray-500);
--dbv-kit-icon-btn-secondary-foreground-hover: var(--dbv-kit-color-primary-600);
--dbv-kit-icon-btn-secondary-foreground-active: var(--dbv-kit-color-primary-700);
--dbv-kit-icon-btn-secondary-background: transparent;
--dbv-kit-icon-btn-secondary-background-hover: transparent;
--dbv-kit-icon-btn-secondary-background-active: transparent;
--dbv-kit-icon-btn-secondary-border-color: transparent;
--dbv-kit-icon-btn-secondary-loader-color: var(--tw-color-gray-300);
/* Danger */
--dbv-kit-icon-btn-danger-foreground: var(--tw-color-red-600);
--dbv-kit-icon-btn-danger-foreground-hover: var(--tw-color-red-700);
--dbv-kit-icon-btn-danger-foreground-active: var(--tw-color-red-800);
--dbv-kit-icon-btn-danger-background: transparent;
--dbv-kit-icon-btn-danger-background-hover: var(--tw-color-red-100);
--dbv-kit-icon-btn-danger-background-active: var(--tw-color-red-200);
--dbv-kit-icon-btn-danger-loader-color: var(--tw-color-red-300);
}
}
@@ -7,6 +7,8 @@
--dbv-kit-control-border-radius: var(--tw-radius-sm);
--dbv-kit-control-border-width: 1px;
--dbv-kit-control-disabled-opacity: 0.5;
--dbv-kit-control-disabled-foreground: var(--tw-color-gray-400);
--dbv-kit-control-disabled-background: var(--tw-color-gray-200);
/* Outline */
--dbv-kit-control-outline-color: var(--tw-color-blue-500);
+2 -1
View File
@@ -21,7 +21,8 @@ export {
type FocusTrapRegionProps,
} from '@ariakit/react';
export { Button, ButtonBase, type ButtonProps } from './Button/Button.js';
export { Button, ButtonBase, type ButtonProps, ButtonIcon, type ButtonIconProps } from './Button/Button.js';
export { IconButton, IconButtonBase, type IconButtonProps } from './IconButton/IconButton.js';
export { Checkbox, CheckboxBase, type CheckboxProps } from './Checkbox/Checkbox.js';
export { Input, InputBase, type InputProps } from './Input/Input.js';
export {
@@ -1,5 +1,5 @@
import type { Story } from '@ladle/react';
import { Button, type ButtonProps } from '../../../index.js';
import { Button, type ButtonProps, ButtonIcon } from '../../../index.js';
import { useState } from 'react';
export { Tokens } from './button-tokens.js';
@@ -48,17 +48,24 @@ export const Variants = () => (
<code>&lt;Button variant="primary"/&gt;</code>
</div>
<div className="tw:flex tw:gap-4 tw:my-4 tw:items-center">
<Button loading>Loading</Button>
<Button>Primary</Button>
<Button disabled>Disabled</Button>
</div>
<div className="tw:flex tw:gap-4 tw:my-4 tw:items-center">
<Button variant="secondary" loading>
Secondary
</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="secondary" disabled>
Disabled
</Button>
</div>
<div className="tw:flex tw:gap-4 tw:my-4 tw:items-center">
<Button variant="danger" loading>
Danger
</Button>
<Button variant="danger">Danger</Button>
<Button variant="danger" disabled>
Disabled
@@ -136,10 +143,10 @@ export const Sizes = () => (
export const Icons = () => (
<div>
<h2>Icons</h2>
<h3>&lt;Button.Icon&gt;</h3>
<h3>&lt;ButtonIcon&gt;</h3>
<code>.dbv-kit-button__icon</code>
<p>
To properly place an icon inside a button, you have to use Button.Icon component. That component will compensate outer padding to balance the
To properly place an icon inside a button, you have to use ButtonIcon component. That component will compensate outer padding to balance the
button view. You need to specify the placement property to adjust the icon position.
</p>
<dl>
@@ -148,7 +155,7 @@ export const Icons = () => (
</dl>
<div className="codeblock">
<code>
&lt;Button&gt; <br /> &nbsp;&nbsp;&lt;Button.Icon placement="start"/&gt;
&lt;Button&gt; <br /> &nbsp;&nbsp;&lt;ButtonIcon placement="start"/&gt;
<br />
&nbsp; Text
<br /> &lt;Button/&gt;
@@ -159,14 +166,14 @@ export const Icons = () => (
<h4>Compare buttons:</h4>
<div className="tw:flex tw:gap-4 tw:items-center tw:my-4">
<Button variant="secondary">
<Button.Icon placement="start">🧤</Button.Icon>
<ButtonIcon placement="start">🧤</ButtonIcon>
Text
</Button>
<Button variant="secondary">🧤 Text</Button>
</div>
<p>
The left button uses Button.Icon component and looks more balanced. If you feel that in your case it's not needed, feel free to not use the
<b> Button.Icon </b> component.
The left button uses ButtonIcon component and looks more balanced. If you feel that in your case it's not needed, feel free to not use the
<b> ButtonIcon </b> component.
</p>
</div>
@@ -183,12 +190,12 @@ export const Icons = () => (
<div className="tw:flex tw:gap-4 tw:my-4 tw:items-center">
<Button size="small">
Love
<Button.Icon placement="end">❤️</Button.Icon>
<ButtonIcon placement="end">❤️</ButtonIcon>
</Button>
<Button className="tw:text-lime-300">
Have you seen
<Button.Icon className="w-2">
<ButtonIcon className="w-2">
<svg width="16" height="16" fill="none">
<g strokeWidth="0"></g>
<g strokeLinecap="round" strokeLinejoin="round"></g>
@@ -201,24 +208,24 @@ export const Icons = () => (
></path>
</g>
</svg>
</Button.Icon>
</ButtonIcon>
UFO?
</Button>
<Button size="large">
<Button.Icon placement="start">🎾</Button.Icon>
<ButtonIcon placement="start">🎾</ButtonIcon>
Tennis
</Button>
<Button size="xlarge">
<Button.Icon placement="start">🎾</Button.Icon>
<ButtonIcon placement="start">🎾</ButtonIcon>
Tennis is Love
<Button.Icon placement="end">❤️</Button.Icon>
<ButtonIcon placement="end">❤️</ButtonIcon>
</Button>
</div>
<div className="tw:flex tw:gap-4 tw:my-4 tw:items-center">
<Button loading>
<Button.Icon className="tw:w-8 tw:h-4" placement="start">
<ButtonIcon className="tw:w-8 tw:h-4" placement="start">
<svg width="16" height="16" fill="none">
<g strokeWidth="0"></g>
<g strokeLinecap="round" strokeLinejoin="round"></g>
@@ -231,23 +238,23 @@ export const Icons = () => (
></path>
</g>
</svg>
</Button.Icon>
</ButtonIcon>
UFO
</Button>
<Button variant="secondary" loading>
<Button.Icon placement="start">🎾</Button.Icon>
<ButtonIcon placement="start">🎾</ButtonIcon>
Tennis
</Button>
<Button disabled>
<Button.Icon placement="start">🎾</Button.Icon>
<ButtonIcon placement="start">🎾</ButtonIcon>
Tennis
</Button>
<Button variant="danger">
<Button.Icon placement="start">🎾</Button.Icon>
<ButtonIcon placement="start">🎾</ButtonIcon>
Tennis
</Button>
<Button>
<Button.Icon>🎾</Button.Icon>
<ButtonIcon>🎾</ButtonIcon>
</Button>
</div>
</div>
@@ -0,0 +1,210 @@
import { useState } from 'react';
import { IconButton } from '../../../index.js';
const example = `<IconButton
variant="primary" | "secondary" | "danger"
size="small" | "medium" | "large" | "xlarge"
loading={boolean}
disabled={boolean}
onClick={function}
aria-label="string"
>
{icon}
</IconButton>`;
export const Documentation = () => {
return (
<div className="tw:space-y-4">
<h1 className="tw:text-2xl tw:font-bold">IconButton</h1>
<p>IconButton is a button component that displays an icon. It supports various sizes and styles, and can be disabled or in a loading state.</p>
<pre className="codeblock">{example}</pre>
<h2 className="tw:text-xl tw:font-semibold">Classnames</h2>
<ul className="tw:list-disc tw:pl-5">
<li>
<code>dbv-kit-icon-button</code> - Main component class
</li>
<li>
<code>dbv-kit-icon-button--primary/secondary/danger</code> - Variant classes
</li>
<li>
<code>dbv-kit-icon-button--small/medium/large/xlarge</code> - Size classes
</li>
</ul>
<h2 className="tw:text-xl tw:font-semibold">CSS Tokens</h2>
<div className="tw:p-4 tw:rounded-md tw:overflow-auto">
<h3 className="tw:font-medium">Structure</h3>
<ul className="tw:pl-5 tw:mb-2">
<li>
<code>--dbv-kit-icon-btn-border-radius</code> - Controls button corner roundness
</li>
<li>
<code>--dbv-kit-icon-btn-border-width/style/color</code> - Border properties
</li>
<li>
<code>--dbv-kit-icon-btn-size</code> - Default button size (medium)
</li>
</ul>
<h3 className="tw:font-medium">Focus State</h3>
<ul className="tw:pl-5 tw:mb-2">
<li>
<code>--dbv-kit-icon-btn-outline-color/offset</code> - Focus outline appearance
</li>
</ul>
<h3 className="tw:font-medium">Colors</h3>
<ul className="tw:pl-5">
<li>
<code>--dbv-kit-icon-btn-foreground/background</code> - Default state colors
</li>
<li>
<code>--dbv-kit-icon-btn-foreground/background-hover</code> - Hover state colors
</li>
<li>
<code>--dbv-kit-icon-btn-foreground/background-active</code> - Active state colors
</li>
<li>
<code>--dbv-kit-btn-loader-color/base-color</code> - Loading spinner colors
</li>
</ul>
</div>
</div>
);
};
export const IconButtons = () => {
const [isLoading, setIsLoading] = useState(false);
const handleClick = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 5000);
};
return (
<div className="tw:flex tw:flex-col tw:gap-4 tw:my-4 tw:items-center">
<div>
<h2>Primary</h2>
<div className="tw:flex tw:gap-4 tw:my-4 tw:items-center">
<IconButton variant="primary" size="small" aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="primary" size="medium" aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="primary" size="large" aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="primary" size="xlarge" aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
</div>
</div>
<div>
<h2>Secondary</h2>
<div className="tw:flex tw:gap-4 tw:my-4 tw:items-center">
<IconButton variant="secondary" size="small" aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="secondary" size="medium" aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="secondary" size="large" aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="secondary" size="xlarge" aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
</div>
</div>
<div>
<h2>Danger</h2>
<div className="tw:flex tw:gap-4 tw:my-4 tw:items-center">
<IconButton variant="danger" size="small" aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="danger" size="medium" aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="danger" size="large" aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="danger" size="xlarge" aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
</div>
</div>
<div>
<h2>Disabled</h2>
<div className="tw:flex tw:gap-4 tw:my-4 tw:items-center">
<IconButton variant="primary" size="small" disabled aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="secondary" size="medium" disabled aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="danger" size="large" disabled aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
</div>
<h2>Loading</h2>
<div className="tw:flex tw:gap-4 tw:my-4 tw:items-center">
<IconButton variant="primary" size="small" loading aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="secondary" loading={isLoading} onClick={handleClick} aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton size="large" loading={isLoading} onClick={handleClick} aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
<IconButton variant="danger" size="xlarge" loading={isLoading} onClick={handleClick} aria-label="Add item">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
<circle cx="12" cy="12" r="12" fill="currentColor" />
</svg>
</IconButton>
</div>
</div>
</div>
);
};
+2
View File
@@ -0,0 +1,2 @@
/* this file required for tailwindcss vscode extension to work */
@import './@dbeaver/ui-kit/.ladle/global.css';
+936 -1500
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
# @dbeaver/table-data
@@ -0,0 +1,40 @@
{
"name": "@dbeaver/table-data",
"type": "module",
"sideEffects": [],
"exports": {
".": "./lib/index.js",
"./*": "./lib/*.js"
},
"scripts": {
"clear": "rimraf lib",
"build": "tsc -b",
"test": "dbeaver-test"
},
"files": [
"package.json",
"LICENSE",
"README.md",
"CHANGELOG.md",
"lib",
"!lib/**/*.d.ts.map",
"!lib/**/*.test.js",
"!lib/**/*.test.d.ts",
"!lib/**/*.test.d.ts.map",
"!lib/**/*.test.js.map",
"!lib/tests",
"!.tsbuildinfo"
],
"packageManager": "yarn@4.6.0",
"devDependencies": {
"@dbeaver/cli": "workspace:^",
"@dbeaver/tsconfig": "workspace:^",
"rimraf": "^6",
"typescript": "^5",
"vitest": "^3"
},
"dependencies": {
"async-mutex": "^0",
"nanoevents": "^9"
}
}
@@ -0,0 +1,18 @@
import type { ITableDataset } from './interfaces/ITableDataset.js';
import type { ITableDatasetManager } from './interfaces/ITableDatasetManager.js';
export class TableDatasetManager<TColumn, TValue> implements ITableDatasetManager<TColumn, TValue> {
get datasets(): ITableDataset<TColumn, TValue>[] {
return this.#datasets;
}
#datasets: ITableDataset<TColumn, TValue>[] = [];
constructor() {
this.#datasets = [];
}
setDatasets(datasets: ITableDataset<TColumn, TValue>[]): void {
this.#datasets = datasets;
}
}
@@ -0,0 +1,110 @@
import { Mutex } from 'async-mutex';
import type { ITableSource } from './interfaces/ITableSource.js';
import { createNanoEvents, type Emitter, type Unsubscribe } from 'nanoevents';
import type { TableSourceEvents } from './interfaces/TableSourceEvents.js';
import type { ITableDatasetManager } from './interfaces/ITableDatasetManager.js';
import type { ITableSourceOptions } from './interfaces/ITableSourceOptions.js';
export abstract class TableSource<TColumn, TValue, TOptions extends ITableSourceOptions = ITableSourceOptions>
implements ITableSource<TColumn, TValue, TOptions>
{
get isLoading(): boolean {
return this.#loadingInProgress;
}
get isOutdated(): boolean {
return this.#outdated;
}
error: Error | null;
options: Partial<TOptions>;
#outdated: boolean;
#loadingInProgress = false;
#loadingPending = false;
readonly #mutex: Mutex;
readonly #emitter: Emitter<TableSourceEvents>;
constructor(readonly datasetManager: ITableDatasetManager<TColumn, TValue>) {
this.options = {};
this.error = null;
this.#outdated = false;
this.#mutex = new Mutex();
this.#emitter = createNanoEvents();
}
setOptions(options: TOptions): void {
this.options = options;
this.setOutdated();
}
setOutdated(): void {
this.setOutdatedValue(true);
if (this.#mutex.isLocked()) {
this.#mutex.runExclusive(() => {
this.setOutdatedValue(true);
});
}
}
async save(): Promise<void> {
const release = await this.#mutex.acquire();
try {
this.setError(null);
await this.saveData();
this.emit('saved');
} catch (error) {
this.setError(error instanceof Error ? error : new Error(String(error)));
throw this.error;
} finally {
release();
}
}
async load(): Promise<void> {
if (this.#loadingInProgress) {
this.#loadingPending = true;
return await this.#mutex.waitForUnlock();
}
this.setLoadingInProgress(true);
try {
// this waits for any save to finish, then runs loadData
await this.#mutex.runExclusive(async () => {
do {
this.#loadingPending = false;
this.setError(null);
await this.loadData();
this.emit('data');
} while (this.#loadingPending);
});
} catch (error) {
this.setError(error instanceof Error ? error : new Error(String(error)));
throw this.error;
} finally {
this.setOutdatedValue(false);
this.setLoadingInProgress(false);
}
}
on<TEvent extends keyof TableSourceEvents>(event: TEvent, listener: TableSourceEvents[TEvent]): Unsubscribe {
return this.#emitter.on(event, listener);
}
protected setLoadingInProgress(state: boolean): void {
this.emit('loading', state);
this.#loadingInProgress = state;
}
protected setError(error: Error | null): void {
this.error = error;
this.emit('error', error);
}
protected setOutdatedValue(state: boolean): void {
this.#outdated = state;
this.emit('outdated', state);
}
protected emit<TEvent extends keyof TableSourceEvents>(event: TEvent, ...args: Parameters<TableSourceEvents[TEvent]>): void {
this.#emitter.emit(event, ...args);
}
protected abstract saveData(): Promise<void>;
protected abstract loadData(): Promise<void>;
}
@@ -0,0 +1,5 @@
// Cell position types
export interface ICellPosition {
readonly rowIdx: number;
readonly colIdx: number;
}
@@ -0,0 +1,27 @@
import type { ICellPosition } from './ICellPosition.js';
import type { ITableEditorHistory } from './ITableEditorHistory.js';
import type { TableEditorEventEmitter } from './TableEditorEvents.js';
// Main table editor interface
export interface ITableEditor<TValue> extends TableEditorEventEmitter<TValue> {
// Data access
readonly data: readonly (readonly TValue[])[];
readonly isEdited: boolean;
readonly rowCount: number;
// Cell operations
getCellValue(position: ICellPosition): TValue | undefined;
setCellValue(position: ICellPosition, value: TValue): void;
// Row operations
insertRow(rowIdx: number, rowData?: TValue[]): void;
deleteRow(rowIdx: number): void;
// Data operations
resetData(newData: TValue[][]): void;
// History operations
readonly history: ITableEditorHistory<TValue>;
undo(): boolean;
redo(): boolean;
}
@@ -0,0 +1,5 @@
import type { ITableEditor } from './ITableEditor.js';
export interface ITableEditorFactory {
create<TValue>(initialData?: TValue[][]): ITableEditor<TValue>;
}
@@ -0,0 +1,38 @@
import type { ICellPosition } from './ICellPosition.js';
// History entry types
export interface ITableEditorHistoryEntry<TValue> {
readonly type: 'cell-edit' | 'row-insert' | 'row-delete';
readonly timestamp: number;
readonly data: ICellEditEntry<TValue> | IRowInsertEntry<TValue> | IRowDeleteEntry<TValue>;
}
export interface ICellEditEntry<TValue> {
readonly position: ICellPosition;
readonly oldValue: TValue;
readonly newValue: TValue;
}
export interface IRowInsertEntry<TValue> {
readonly rowIdx: number;
readonly rowData: TValue[];
}
export interface IRowDeleteEntry<TValue> {
readonly rowIdx: number;
readonly rowData: TValue[];
}
// History management interface
export interface ITableEditorHistory<TValue> {
readonly canUndo: boolean;
readonly canRedo: boolean;
readonly size: number;
readonly maxSize: number;
push(entry: ITableEditorHistoryEntry<TValue>): void;
undo(): ITableEditorHistoryEntry<TValue> | null;
redo(): ITableEditorHistoryEntry<TValue> | null;
clear(): void;
setMaxSize(size: number): void;
}
@@ -0,0 +1,193 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { describe, expect, test, beforeEach } from 'vitest';
import { TableEditor } from './TableEditor.js';
import type { ICellPosition } from './ICellPosition.js';
describe('TableEditor', () => {
let tableEditor: TableEditor<string>;
const initialData = [
['A1', 'B1', 'C1'],
['A2', 'B2', 'C2'],
['A3', 'B3', 'C3'],
];
beforeEach(() => {
tableEditor = new TableEditor(initialData);
});
describe('Basic functionality', () => {
test('should initialize with provided data', () => {
expect(tableEditor.rowCount).toBe(3);
expect(tableEditor.data).toEqual(initialData);
expect(tableEditor.isEdited).toBe(false);
});
test('should get cell values correctly', () => {
const position: ICellPosition = { rowIdx: 1, colIdx: 1 };
expect(tableEditor.getCellValue(position)).toBe('B2');
});
test('should return undefined for invalid positions', () => {
const invalidPosition: ICellPosition = { rowIdx: 10, colIdx: 10 };
expect(tableEditor.getCellValue(invalidPosition)).toBeUndefined();
});
});
describe('Cell editing', () => {
test('should set cell values and track changes', () => {
const position: ICellPosition = { rowIdx: 1, colIdx: 1 };
let changeCount = 0;
tableEditor.on('data-changed', () => {
changeCount++;
});
tableEditor.setCellValue(position, 'NEW_VALUE');
expect(tableEditor.getCellValue(position)).toBe('NEW_VALUE');
expect(tableEditor.isEdited).toBe(true);
expect(changeCount).toBe(1);
});
test('should not emit events when setting same value', () => {
const position: ICellPosition = { rowIdx: 1, colIdx: 1 };
let changeCount = 0;
tableEditor.on('data-changed', () => {
changeCount++;
});
tableEditor.setCellValue(position, 'B2'); // Same value
expect(changeCount).toBe(0);
expect(tableEditor.isEdited).toBe(false);
});
test('should throw error for invalid positions', () => {
const invalidPosition: ICellPosition = { rowIdx: 10, colIdx: 10 };
expect(() => {
tableEditor.setCellValue(invalidPosition, 'value');
}).toThrow();
});
});
describe('Row operations', () => {
test('should insert rows correctly', () => {
let changeCount = 0;
tableEditor.on('data-changed', () => {
changeCount++;
});
tableEditor.insertRow(1, ['X1', 'X2', 'X3']);
expect(tableEditor.rowCount).toBe(4);
expect(tableEditor.getCellValue({ rowIdx: 1, colIdx: 0 })).toBe('X1');
expect(tableEditor.getCellValue({ rowIdx: 2, colIdx: 0 })).toBe('A2'); // Shifted down
expect(tableEditor.isEdited).toBe(true);
expect(changeCount).toBe(1);
});
test('should delete rows correctly', () => {
let changeCount = 0;
tableEditor.on('data-changed', () => {
changeCount++;
});
tableEditor.deleteRow(1);
expect(tableEditor.rowCount).toBe(2);
expect(tableEditor.getCellValue({ rowIdx: 1, colIdx: 0 })).toBe('A3');
expect(tableEditor.isEdited).toBe(true);
expect(changeCount).toBe(1);
});
});
describe('History management', () => {
test('should support undo/redo for cell edits', () => {
const position: ICellPosition = { rowIdx: 1, colIdx: 1 };
// Make a change
tableEditor.setCellValue(position, 'NEW_VALUE');
expect(tableEditor.getCellValue(position)).toBe('NEW_VALUE');
expect(tableEditor.history.canUndo).toBe(true);
expect(tableEditor.history.canRedo).toBe(false);
// Undo
const undoResult = tableEditor.undo();
expect(undoResult).toBe(true);
expect(tableEditor.getCellValue(position)).toBe('B2');
expect(tableEditor.history.canUndo).toBe(false);
expect(tableEditor.history.canRedo).toBe(true);
// Redo
const redoResult = tableEditor.redo();
expect(redoResult).toBe(true);
expect(tableEditor.getCellValue(position)).toBe('NEW_VALUE');
expect(tableEditor.history.canUndo).toBe(true);
expect(tableEditor.history.canRedo).toBe(false);
});
test('should support undo/redo for row operations', () => {
// Insert row
tableEditor.insertRow(1, ['X1', 'X2', 'X3']);
expect(tableEditor.rowCount).toBe(4);
// Undo insert
tableEditor.undo();
expect(tableEditor.rowCount).toBe(3);
expect(tableEditor.getCellValue({ rowIdx: 1, colIdx: 0 })).toBe('A2');
// Redo insert
tableEditor.redo();
expect(tableEditor.rowCount).toBe(4);
expect(tableEditor.getCellValue({ rowIdx: 1, colIdx: 0 })).toBe('X1');
});
test('should not fire change notifications for undo/redo operations', () => {
let changeCount = 0;
tableEditor.on('data-changed', () => {
changeCount++;
});
const position: ICellPosition = { rowIdx: 1, colIdx: 1 };
// Make a change - should fire change notification
tableEditor.setCellValue(position, 'NEW_VALUE');
expect(changeCount).toBe(1);
// Undo - should NOT fire change notification
tableEditor.undo();
expect(changeCount).toBe(1);
// Redo - should NOT fire change notification
tableEditor.redo();
expect(changeCount).toBe(1);
});
});
describe('Data reset', () => {
test('should reset data and clear history', () => {
// Make some changes
tableEditor.setCellValue({ rowIdx: 0, colIdx: 0 }, 'CHANGED');
expect(tableEditor.isEdited).toBe(true);
expect(tableEditor.history.canUndo).toBe(true);
// Reset data
const newData = [['Z1', 'Z2'], ['Z3', 'Z4']];
tableEditor.resetData(newData);
expect(tableEditor.data).toEqual(newData);
expect(tableEditor.rowCount).toBe(2);
expect(tableEditor.isEdited).toBe(false);
expect(tableEditor.history.canUndo).toBe(false);
});
});
});
@@ -0,0 +1,253 @@
import { createNanoEvents, type Emitter, type Unsubscribe } from 'nanoevents';
import type { ITableEditor } from './ITableEditor.js';
import type { ICellPosition } from './ICellPosition.js';
import type { ITableEditorHistory, ITableEditorHistoryEntry } from './ITableEditorHistory.js';
import type { TableEditorEvents } from './TableEditorEvents.js';
import { TableEditorHistory } from './TableEditorHistory.js';
export class TableEditor<TValue> implements ITableEditor<TValue> {
private readonly _data: TValue[][];
private readonly _history: ITableEditorHistory<TValue>;
private readonly _emitter: Emitter<TableEditorEvents<TValue>>;
private _isEdited = false;
constructor(initialData: TValue[][] = [], historyMaxSize = 100) {
this._data = initialData.map(row => [...row]);
this._history = new TableEditorHistory<TValue>(historyMaxSize);
this._emitter = createNanoEvents();
}
get data(): readonly (readonly TValue[])[] {
return this._data.map(row => Object.freeze([...row]));
}
get isEdited(): boolean {
return this._isEdited;
}
get rowCount(): number {
return this._data.length;
}
get history(): ITableEditorHistory<TValue> {
return this._history;
}
getCellValue(position: ICellPosition): TValue | undefined {
if (!this.isValidPosition(position)) {
return undefined;
}
return this._data[position.rowIdx]?.[position.colIdx];
}
setCellValue(position: ICellPosition, value: TValue): void {
if (!this.isValidPosition(position)) {
throw new Error(`Invalid cell position: row ${position.rowIdx}, column ${position.colIdx}`);
}
const row = this._data[position.rowIdx];
if (!row) {
throw new Error(`Row ${position.rowIdx} does not exist`);
}
const oldValue = row[position.colIdx];
if (oldValue === value) {
return;
}
const historyEntry: ITableEditorHistoryEntry<TValue> = {
type: 'cell-edit',
timestamp: Date.now(),
data: {
position: { ...position },
oldValue: oldValue as TValue,
newValue: value,
},
};
this._history.push(historyEntry);
row[position.colIdx] = value;
this._isEdited = true;
this.emit('data-changed');
this.emitHistoryChanged();
}
insertRow(rowIdx: number, rowData?: TValue[]): void {
if (rowIdx < 0 || rowIdx > this._data.length) {
throw new Error(`Invalid row index: ${rowIdx}. Must be between 0 and ${this._data.length}`);
}
const newRowData = rowData ? [...rowData] : [];
if (this._data.length > 0 && newRowData.length === 0) {
const columnCount = Math.max(...this._data.map(row => row.length));
newRowData.length = columnCount;
newRowData.fill(undefined as any);
}
const historyEntry: ITableEditorHistoryEntry<TValue> = {
type: 'row-insert',
timestamp: Date.now(),
data: {
rowIdx,
rowData: [...newRowData],
},
};
this._history.push(historyEntry);
this._data.splice(rowIdx, 0, newRowData);
this._isEdited = true;
this.emit('data-changed');
this.emitHistoryChanged();
}
deleteRow(rowIdx: number): void {
if (rowIdx < 0 || rowIdx >= this._data.length) {
throw new Error(`Invalid row index: ${rowIdx}. Must be between 0 and ${this._data.length - 1}`);
}
const row = this._data[rowIdx];
if (!row) {
throw new Error(`Row ${rowIdx} does not exist`);
}
const deletedRowData = [...row];
const historyEntry: ITableEditorHistoryEntry<TValue> = {
type: 'row-delete',
timestamp: Date.now(),
data: {
rowIdx,
rowData: deletedRowData,
},
};
this._history.push(historyEntry);
this._data.splice(rowIdx, 1);
this._isEdited = true;
this.emit('data-changed');
this.emitHistoryChanged();
}
resetData(newData: TValue[][]): void {
this._data.length = 0;
newData.forEach(row => {
this._data.push([...row]);
});
this._history.clear();
this._isEdited = false;
this.emit('data-reset', newData);
this.emitHistoryChanged();
}
undo(): boolean {
const entry = this._history.undo();
if (!entry) {
return false;
}
this.applyHistoryEntryReverse(entry);
this.emitHistoryChanged();
return true;
}
redo(): boolean {
const entry = this._history.redo();
if (!entry) {
return false;
}
this.applyHistoryEntry(entry);
this.emitHistoryChanged();
return true;
}
on<TEvent extends keyof TableEditorEvents<TValue>>(
event: TEvent,
listener: TableEditorEvents<TValue>[TEvent]
): Unsubscribe {
return this._emitter.on(event, listener);
}
emit<TEvent extends keyof TableEditorEvents<TValue>>(
event: TEvent,
...args: Parameters<TableEditorEvents<TValue>[TEvent]>
): void {
this._emitter.emit(event, ...args);
}
private isValidPosition(position: ICellPosition): boolean {
return (
position.rowIdx >= 0 &&
position.rowIdx < this._data.length &&
position.colIdx >= 0 &&
position.colIdx < (this._data[position.rowIdx]?.length || 0)
);
}
private emitHistoryChanged(): void {
this.emit('history-changed', this._history.canUndo, this._history.canRedo);
}
private applyHistoryEntry(entry: ITableEditorHistoryEntry<TValue>): void {
switch (entry.type) {
case 'cell-edit': {
const data = entry.data as any;
const { position, newValue } = data;
const row = this._data[position.rowIdx];
if (row) {
row[position.colIdx] = newValue;
}
break;
}
case 'row-insert': {
const data = entry.data as any;
const { rowIdx, rowData } = data;
this._data.splice(rowIdx, 0, [...rowData]);
break;
}
case 'row-delete': {
const data = entry.data as any;
const { rowIdx } = data;
this._data.splice(rowIdx, 1);
break;
}
}
}
private applyHistoryEntryReverse(entry: ITableEditorHistoryEntry<TValue>): void {
switch (entry.type) {
case 'cell-edit': {
const data = entry.data as any;
const { position, oldValue } = data;
const row = this._data[position.rowIdx];
if (row) {
row[position.colIdx] = oldValue;
}
break;
}
case 'row-insert': {
const data = entry.data as any;
const { rowIdx } = data;
this._data.splice(rowIdx, 1);
break;
}
case 'row-delete': {
const data = entry.data as any;
const { rowIdx, rowData } = data;
this._data.splice(rowIdx, 0, [...rowData]);
break;
}
}
}
}
@@ -0,0 +1,9 @@
import type { EmitterMixin } from 'nanoevents';
export interface TableEditorEvents<TValue> {
'data-changed': () => void;
'data-reset': (newData: TValue[][]) => void;
'history-changed': (canUndo: boolean, canRedo: boolean) => void;
}
export type TableEditorEventEmitter<TValue> = EmitterMixin<TableEditorEvents<TValue>>;
@@ -0,0 +1,85 @@
import type { ITableEditorHistory, ITableEditorHistoryEntry } from './ITableEditorHistory.js';
/**
* Implementation of table editor history with undo/redo functionality.
* Maintains a circular buffer of history entries with a configurable maximum size.
*/
export class TableEditorHistory<TValue> implements ITableEditorHistory<TValue> {
private entries: ITableEditorHistoryEntry<TValue>[] = [];
private currentIndex: number = -1;
private _maxSize: number;
constructor(maxSize: number = 100) {
this._maxSize = Math.max(1, maxSize);
}
get canUndo(): boolean {
return this.currentIndex >= 0;
}
get canRedo(): boolean {
return this.currentIndex < this.entries.length - 1;
}
get size(): number {
return this.entries.length;
}
get maxSize(): number {
return this._maxSize;
}
push(entry: ITableEditorHistoryEntry<TValue>): void {
// Remove any entries after the current index (when pushing after undo)
if (this.currentIndex < this.entries.length - 1) {
this.entries = this.entries.slice(0, this.currentIndex + 1);
}
// Add the new entry
this.entries.push(entry);
this.currentIndex = this.entries.length - 1;
// Maintain max size by removing oldest entries
if (this.entries.length > this._maxSize) {
const removeCount = this.entries.length - this._maxSize;
this.entries = this.entries.slice(removeCount);
this.currentIndex -= removeCount;
}
}
undo(): ITableEditorHistoryEntry<TValue> | null {
if (!this.canUndo) {
return null;
}
const entry = this.entries[this.currentIndex];
this.currentIndex--;
return entry ?? null;
}
redo(): ITableEditorHistoryEntry<TValue> | null {
if (!this.canRedo) {
return null;
}
this.currentIndex++;
const entry = this.entries[this.currentIndex];
return entry ?? null;
}
clear(): void {
this.entries = [];
this.currentIndex = -1;
}
setMaxSize(size: number): void {
this._maxSize = Math.max(1, size);
// Trim entries if the new max size is smaller
if (this.entries.length > this._maxSize) {
const removeCount = this.entries.length - this._maxSize;
this.entries = this.entries.slice(removeCount);
this.currentIndex = Math.max(-1, this.currentIndex - removeCount);
}
}
}
@@ -0,0 +1,25 @@
// Main interfaces
export type { ITableEditor } from './ITableEditor.js';
export type { ICellPosition } from './ICellPosition.js';
export type { ITableEditorFactory } from './ITableEditorFactory.js';
// History interfaces
export type {
ITableEditorHistory,
ITableEditorHistoryEntry,
ICellEditEntry,
IRowInsertEntry,
IRowDeleteEntry,
} from './ITableEditorHistory.js';
// History implementation
export { TableEditorHistory } from './TableEditorHistory.js';
// Table editor implementation
export { TableEditor } from './TableEditor.js';
// Event interfaces
export type {
TableEditorEvents,
TableEditorEventEmitter,
} from './TableEditorEvents.js';
@@ -0,0 +1,12 @@
export * from './editor/TableEditor.js';
export * from './interfaces/ITableData.js';
export * from './interfaces/ITableDataset.js';
export * from './interfaces/ITableDatasetManager.js';
export * from './interfaces/ITableSource.js';
export * from './interfaces/ITableSourceOptions.js';
export * from './interfaces/ITableSorting.js';
export * from './interfaces/TableEvents.js';
export * from './interfaces/TableSourceEvents.js';
export * from './TableSource.js';
export * from './TableDatasetManager.js';
export * from './editor/index.js';
@@ -0,0 +1,18 @@
import type { EmitterMixin } from 'nanoevents';
import type { ITableDataset } from './ITableDataset.js';
import type { ITableSource } from './ITableSource.js';
import type { TableEvents } from './TableEvents.js';
import type { ITableSourceOptions } from './ITableSourceOptions.js';
export interface ITableData<TColumn, TValue, TOptions extends ITableSourceOptions = ITableSourceOptions> extends EmitterMixin<TableEvents> {
readonly id: string;
readonly isLoading: boolean;
readonly isOutdated: boolean;
readonly datasets: ITableDataset<TColumn, TValue>[];
readonly source: ITableSource<TColumn, TValue, TOptions>;
setSource(source: ITableSource<TColumn, TValue, TOptions>): void;
save(): Promise<void>;
}
@@ -0,0 +1,5 @@
export interface ITableDataset<TColumn, TValue> {
readonly id: string;
readonly columns: TColumn[];
readonly rows: TValue[][];
}
@@ -0,0 +1,7 @@
import type { ITableDataset } from './ITableDataset.js';
export interface ITableDatasetManager<TColumn, TValue> {
readonly datasets: ITableDataset<TColumn, TValue>[];
setDatasets(datasets: ITableDataset<TColumn, TValue>[]): void;
}
@@ -0,0 +1,4 @@
export interface ITableSorting {
column: number;
order: 'asc' | 'desc';
}
@@ -0,0 +1,18 @@
import type { EmitterMixin } from 'nanoevents';
import type { TableSourceEvents } from './TableSourceEvents.js';
import type { ITableDatasetManager } from './ITableDatasetManager.js';
import type { ITableSourceOptions } from './ITableSourceOptions.js';
export interface ITableSource<TColumn, TValue, TOptions extends ITableSourceOptions = ITableSourceOptions> extends EmitterMixin<TableSourceEvents> {
readonly datasetManager: ITableDatasetManager<TColumn, TValue>;
readonly options: Partial<TOptions>;
readonly isLoading: boolean;
readonly isOutdated: boolean;
readonly error: Error | null;
setOptions(options: TOptions): void;
setOutdated(): void;
save(): Promise<void>;
load(): Promise<void>;
}
@@ -0,0 +1,5 @@
import type { ITableSorting } from './ITableSorting.js';
export interface ITableSourceOptions {
sorting?: ITableSorting[];
}
@@ -0,0 +1,3 @@
import type { TableSourceEvents } from './TableSourceEvents.js';
export type TableEvents = TableSourceEvents & {};
@@ -0,0 +1,7 @@
export type TableSourceEvents = {
loading: (state: boolean) => void;
outdated: (state: boolean) => void;
saved: () => void;
data: () => void;
error: (error: Error | null) => void;
};
@@ -0,0 +1,21 @@
{
"extends": "@dbeaver/tsconfig/tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "./lib/.tsbuildinfo",
"rootDir": "src",
"rootDirs": [
"src"
],
"outDir": "lib",
"incremental": true,
"composite": true
},
"include": [
"src"
],
"references": [
{
"path": "../cli"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: DBeaver Corp
Bundle-Name: Cloudbeaver Web Model
Bundle-SymbolicName: io.cloudbeaver.model;singleton:=true
Bundle-Version: 1.0.77.qualifier
Bundle-Release-Date: 20250519
Bundle-Version: 1.0.79.qualifier
Bundle-Release-Date: 20250616
Bundle-RequiredExecutionEnvironment: JavaSE-17
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
+1 -1
View File
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.model</artifactId>
<version>1.0.77-SNAPSHOT</version>
<version>1.0.79-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -69,4 +69,9 @@ public interface SMAuthProviderExternal<AUTH_SESSION extends SMSession> extends
@NotNull WebUser user,
boolean selfIdentity) throws DBException;
/**
* Make some post authentication actions
*/
default void postAuthentication() {}
}
@@ -134,7 +134,7 @@ public class ServletSystemInformationCollector<T extends ServletApplication> imp
}
@NotNull
@PropertyGroup(order = 31, category = "Security manager database", id = "sm")
@PropertyGroup(order = 31, category = "Security manager database", groupId = "sm")
public DBPConnectionInformation getSmDatabaseInfo() {
return smDatabaseInfo;
}
@@ -23,11 +23,9 @@ import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.impl.PropertyDescriptor;
import org.jkiss.dbeaver.registry.RegistryConstants;
import org.jkiss.utils.ArrayUtils;
import org.jkiss.utils.CommonUtils;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
public class WebAuthProviderRegistry {
@@ -36,6 +34,7 @@ public class WebAuthProviderRegistry {
private static final String TAG_AUTH_PROVIDER = "authProvider"; //$NON-NLS-1$
private static final String TAG_AUTH_PROVIDER_DISABLE = "authProviderDisable"; //$NON-NLS-1$
private static final String TAG_COMMON_PROVIDER_PROPERTIES = "commonProviderProperties"; //$NON-NLS-1$
private static final String ATTRIBUTE_EXTENDED = "extended";
private static WebAuthProviderRegistry instance = null;
@@ -54,34 +53,43 @@ public class WebAuthProviderRegistry {
}
private void loadExtensions(IExtensionRegistry registry) {
{
IConfigurationElement[] extConfigs = registry.getConfigurationElementsFor(WebAuthProviderDescriptor.EXTENSION_ID);
for (IConfigurationElement ext : extConfigs) {
// Load webServices
if (TAG_AUTH_PROVIDER.equals(ext.getName())) {
WebAuthProviderDescriptor providerDescriptor;
WebAuthProviderDescriptor webAuthProviderDescriptor = authProviders.get(ext.getAttribute(WebRegistryConstant.ATTR_EXTENDED));
if (webAuthProviderDescriptor != null) {
webAuthProviderDescriptor.loadExtraConfig(ext);
} else {
providerDescriptor = new WebAuthProviderDescriptor(ext);
this.authProviders.put(providerDescriptor.getId(), providerDescriptor);
}
} else if (TAG_COMMON_PROVIDER_PROPERTIES.equals(ext.getName())) {
var commonProperties = new WebCommonAuthProviderPropertyDescriptor(ext);
this.commonProperties.add(commonProperties);
}
}
IConfigurationElement[] extConfigs = registry.getConfigurationElementsFor(WebAuthProviderDescriptor.EXTENSION_ID);
// Sort - parse providers with parent in the end
Arrays.sort(extConfigs, (o1, o2) -> {
String p1 = o1.getAttribute(ATTRIBUTE_EXTENDED);
String p2 = o2.getAttribute(ATTRIBUTE_EXTENDED);
if (CommonUtils.equalObjects(p1, p2)) return 0;
if (p1 == null) return -1;
if (p2 == null) return 1;
return 0;
});
for (IConfigurationElement ext : extConfigs) {
// Disable auth providers
if (TAG_AUTH_PROVIDER_DISABLE.equals(ext.getName())) {
String providerId = ext.getAttribute(RegistryConstants.ATTR_ID);
if (!this.authProviders.containsKey(providerId)) {
log.warn("Can't disable auth provider '" + providerId + "' - no such provider found");
} else {
this.authProviders.remove(providerId);
}
for (IConfigurationElement ext : extConfigs) {
// Load webServices
if (TAG_AUTH_PROVIDER.equals(ext.getName())) {
WebAuthProviderDescriptor providerDescriptor;
WebAuthProviderDescriptor webAuthProviderDescriptor
= authProviders.get(ext.getAttribute(WebRegistryConstant.ATTR_EXTENDED));
if (webAuthProviderDescriptor != null) {
webAuthProviderDescriptor.loadExtraConfig(ext);
} else {
providerDescriptor = new WebAuthProviderDescriptor(ext);
this.authProviders.put(providerDescriptor.getId(), providerDescriptor);
}
} else if (TAG_COMMON_PROVIDER_PROPERTIES.equals(ext.getName())) {
var commonProperties = new WebCommonAuthProviderPropertyDescriptor(ext);
this.commonProperties.add(commonProperties);
}
}
for (IConfigurationElement ext : extConfigs) {
// Disable auth providers
if (TAG_AUTH_PROVIDER_DISABLE.equals(ext.getName())) {
String providerId = ext.getAttribute(RegistryConstants.ATTR_ID);
if (!this.authProviders.containsKey(providerId)) {
log.warn("Can't disable auth provider '" + providerId + "' - no such provider found");
} else {
this.authProviders.remove(providerId);
}
}
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@ package io.cloudbeaver.server;
public class CBConstants {
public static final int STATIC_CACHE_SECONDS = 60 * 60 * 24 * 3;
public static final String CONF_DIR_NAME = "conf";
public static final String RUNTIME_DATA_DIR_NAME = ".data";
public static final String RUNTIME_APP_CONFIG_FILE_NAME = ".cloudbeaver.runtime.conf";
public static final String RUNTIME_PRODUCT_CONFIG_FILE_NAME = ".product.runtime.conf";
@@ -20,4 +20,5 @@ public interface HttpConstants {
String CONTENT_TYPE = "Content-Type";
String TYPE_JSON = "application/json";
String HEADER_AUTHORIZATION = "Authorization";
}
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: DBeaver Corp
Bundle-Name: Cloudbeaver Community Product
Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true
Bundle-Version: 25.0.5.qualifier
Bundle-Release-Date: 20250519
Bundle-Version: 25.1.1.qualifier
Bundle-Release-Date: 20250616
Bundle-RequiredExecutionEnvironment: JavaSE-17
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.product.ce</artifactId>
<version>25.0.5-SNAPSHOT</version>
<version>25.1.1-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -2,8 +2,8 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Cloudbeaver Base JDBC drivers
Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true
Bundle-Version: 1.0.122.qualifier
Bundle-Release-Date: 20250519
Bundle-Version: 1.0.124.qualifier
Bundle-Release-Date: 20250616
Bundle-Vendor: DBeaver Corp
Bundle-ActivationPolicy: lazy
Automatic-Module-Name: io.cloudbeaver.resources.drivers.base
@@ -9,6 +9,6 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.resources.drivers.base</artifactId>
<version>1.0.122-SNAPSHOT</version>
<version>1.0.124-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: DBeaver Corp
Bundle-Name: Cloudbeaver CE Server
Bundle-SymbolicName: io.cloudbeaver.server.ce;singleton:=true
Bundle-Version: 25.0.5.qualifier
Bundle-Release-Date: 20250519
Bundle-Version: 25.1.1.qualifier
Bundle-Release-Date: 20250616
Bundle-RequiredExecutionEnvironment: JavaSE-17
Bundle-ActivationPolicy: lazy
Bundle-Activator: io.cloudbeaver.server.CBPlatformActivator
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.server.ce</artifactId>
<version>25.0.5-SNAPSHOT</version>
<version>25.1.1-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -557,8 +557,9 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
private void patchConfigurationWithProperties(Map<String, Object> configProps) {
IVariableResolver varResolver = new SystemVariablesResolver() {
@Nullable
@Override
public String get(String name) {
public String get(@NotNull String name) {
String propValue = externalProperties.get(name);
if (propValue != null) {
return propValue;
@@ -62,7 +62,7 @@ public class CBSessionManager implements WebAppSessionManager {
public BaseWebSession closeSession(@NotNull HttpServletRequest request) {
HttpSession session = request.getSession();
if (session != null) {
return closeSession(session.getId());
return closeSession(getSessionId(request));
}
return null;
}
@@ -113,8 +113,7 @@ public class CBSessionManager implements WebAppSessionManager {
@NotNull HttpServletResponse response,
boolean errorOnNoFound
) throws DBWebException {
HttpSession httpSession = request.getSession(true);
String sessionId = httpSession.getId();
String sessionId = getSessionId(request);
WebSession webSession;
synchronized (sessionMap) {
var baseWebSession = sessionMap.get(sessionId);
@@ -139,6 +138,7 @@ public class CBSessionManager implements WebAppSessionManager {
log.error("Failed to restore previous user session", e);
}
HttpSession httpSession = request.getSession(false);
if (!restored && errorOnNoFound && !httpSession.isNew()) {
throw new DBWebException("Session has expired", DBWebException.ERROR_CODE_SESSION_EXPIRED);
}
@@ -159,6 +159,12 @@ public class CBSessionManager implements WebAppSessionManager {
return webSession;
}
@NotNull
protected String getSessionId(@NotNull HttpServletRequest request) {
HttpSession httpSession = request.getSession(true);
return httpSession.getId();
}
/**
* Returns not expired session from cache, or restore it.
*
@@ -244,7 +250,7 @@ public class CBSessionManager implements WebAppSessionManager {
@Override
@Nullable
public WebSession findWebSession(HttpServletRequest request) {
String sessionId = request.getSession().getId();
String sessionId = getSessionId(request);
synchronized (sessionMap) {
var session = sessionMap.get(sessionId);
if (session instanceof WebSession) {
@@ -282,8 +288,7 @@ public class CBSessionManager implements WebAppSessionManager {
}
for (var session : expiredList) {
log.debug("> Expire session '" + session.getSessionId() + "'");
session.close();
closeExpiredSession(session);
}
}
@@ -429,4 +434,9 @@ public class CBSessionManager implements WebAppSessionManager {
}
}
}
protected void closeExpiredSession(@NotNull BaseWebSession session) {
log.debug("> Expire session '" + session.getSessionId() + "'");
session.close();
}
}
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: DBeaver Corp
Bundle-Name: Cloudbeaver Web Server
Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true
Bundle-Version: 25.0.5.qualifier
Bundle-Release-Date: 20250519
Bundle-Version: 25.1.1.qualifier
Bundle-Release-Date: 20250616
Bundle-RequiredExecutionEnvironment: JavaSE-17
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .
+1 -1
View File
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.server</artifactId>
<version>25.0.5-SNAPSHOT</version>
<version>25.1.1-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -50,7 +50,6 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -126,32 +125,16 @@ public class GraphQLEndpoint extends HttpServlet {
private void setDevelHeaders(HttpServletRequest request, HttpServletResponse response) {
if (ServletAppUtils.getServletApplication().getServerConfiguration().isDevelMode()) {
// response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*");
// response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_HEADERS, "*");
// response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "*");
String referrer = request.getHeader("referer");
String origin = request.getHeader("origin");
if (origin == null) {
return;
}
try {
URL url = new URL(referrer);
String protocol = url.getProtocol();
String host = url.getHost();
int port = url.getPort();
String origin;
// if the port is not explicitly specified in the input, it will be -1.
if (port == -1) {
origin = String.format("%s://%s", protocol, host);
} else {
origin = String.format("%s://%s:%d", protocol, host, port);
}
// for local machine must be defined explicitly:
response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, origin);
response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_HEADERS, "Set-Cookie, Content-Type");
response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
} catch (Throwable t) {}
// for local machine must be defined explicitly:
response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, origin);
response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_HEADERS, "Set-Cookie, Content-Type, Authorization");
response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}
}
@@ -203,7 +186,13 @@ public class GraphQLEndpoint extends HttpServlet {
JsonElement operNameJSON = reqObject.get("operationName");
executeQuery(request, response, query.getAsString(), variables, operNameJSON == null || operNameJSON instanceof JsonNull ? null : operNameJSON.getAsString());
executeQuery(
request,
response,
query.getAsString(),
variables,
operNameJSON == null || operNameJSON instanceof JsonNull ? null : operNameJSON.getAsString()
);
}
@Override
@@ -230,12 +219,19 @@ public class GraphQLEndpoint extends HttpServlet {
}
}
private void executeQuery(HttpServletRequest request, HttpServletResponse response, String query, Map<String, Object> variables, String operationName) throws IOException {
private void executeQuery(
HttpServletRequest request,
HttpServletResponse response,
String query,
Map<String, Object> variables,
String operationName
) throws IOException {
Map<String, Object> mapOfContext =
Map.of(
"request", request,
"response", response,
"bindingContext", bindingContext);
"bindingContext", bindingContext
);
ExecutionInput.Builder contextBuilder = ExecutionInput.newExecutionInput()
.graphQLContext(mapOfContext)
.query(query);
@@ -247,11 +243,11 @@ public class GraphQLEndpoint extends HttpServlet {
}
{
String apiCall = operationName;
// if (!CommonUtils.isEmpty(apiCall)) {
// if (variables != null) {
// apiCall += " (" + variables + ")";
// }
// }
// if (!CommonUtils.isEmpty(apiCall)) {
// if (variables != null) {
// apiCall += " (" + variables + ")";
// }
// }
String sessionId = GraphQLLoggerUtil.getSessionId(request);
String userId = GraphQLLoggerUtil.getUserId(request);
String loggerMessage = GraphQLLoggerUtil.buildLoggerMessage(sessionId, userId, variables);
@@ -288,7 +284,10 @@ public class GraphQLEndpoint extends HttpServlet {
if (exception instanceof InvocationTargetException) {
exception = ((InvocationTargetException) exception).getTargetException();
}
log.debug("GraphQL call failed at '" + handlerParameters.getPath() + "'" /*+ ", " + handlerParameters.getArgumentValues()*/, exception);
log.debug(
"GraphQL call failed at '" + handlerParameters.getPath() + "'" /*+ ", " + handlerParameters.getArgumentValues()*/,
exception
);
// Log in session
WebSession webSession = WebServiceBindingBase.findWebSession(handlerParameters.getDataFetchingEnvironment());
@@ -18,9 +18,8 @@ package io.cloudbeaver.server.websockets;
import io.cloudbeaver.model.session.WebHeadlessSession;
import io.cloudbeaver.model.session.WebHttpRequestInfo;
import io.cloudbeaver.server.HttpConstants;
import io.cloudbeaver.server.WebAppSessionManager;
import io.cloudbeaver.utils.ServletAppUtils;
import jakarta.servlet.Servlet;
import jakarta.servlet.http.HttpSession;
import jakarta.websocket.HandshakeResponse;
import jakarta.websocket.server.HandshakeRequest;
@@ -52,7 +51,7 @@ public class CBWebSocketServerConfigurator extends ServerEndpointConfig.Configur
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
String sessionId = request.getHttpSession() instanceof HttpSession httpSession ? httpSession.getId() : null;
String sessionId = getSessionId(request);
String userAgentHeader = request.getHeaders()
.get(WebHttpRequestInfo.USER_AGENT)
@@ -93,6 +92,15 @@ public class CBWebSocketServerConfigurator extends ServerEndpointConfig.Configur
}
}
@Nullable
private String getSessionId(@NotNull HandshakeRequest request) {
// complex auth uses bearer authentication
List<String> authHeaders = WSClientUtils.getHeaders(request.getHeaders(), HttpConstants.HEADER_AUTHORIZATION);
if (!CommonUtils.isEmpty(authHeaders) && authHeaders.get(0).startsWith("Bearer ")) {
return authHeaders.get(0).substring(7);
}
return request.getHttpSession() instanceof HttpSession httpSession ? httpSession.getId() : null;
}
@Nullable
private WebHeadlessSession createHeadlessSession(
@@ -121,6 +121,11 @@ public class WebSQLCompletionContext implements SQLCompletionContext {
return true;
}
@Override
public boolean isForceQualifiedColumnNames() {
return false;
}
@Override
public SQLCompletionProposalBase createProposal(@NotNull SQLCompletionRequest request, @NotNull String displayString, @NotNull String replacementString, int cursorPosition, @Nullable DBPImage image, @NotNull DBPKeywordType proposalType, @Nullable String description, @Nullable DBPNamedObject object, @NotNull Map<String, Object> params) {
return new SQLCompletionProposalBase(request, displayString, replacementString, cursorPosition, image, proposalType, description, object, params);
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: DBeaver Corp
Bundle-Name: Cloudbeaver Web Service - Administration
Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true
Bundle-Version: 1.0.121.qualifier
Bundle-Release-Date: 20250519
Bundle-Version: 1.0.123.qualifier
Bundle-Release-Date: 20250616
Bundle-RequiredExecutionEnvironment: JavaSE-17
Bundle-ActivationPolicy: lazy
Bundle-ClassPath: .

Some files were not shown because too many files have changed in this diff Show More