table data grid

This commit is contained in:
Jan Prochazka
2021-02-22 17:34:24 +01:00
parent 60c1090d6c
commit 1e540b3fe9
22 changed files with 928 additions and 5678 deletions

View File

@@ -3,6 +3,7 @@
import WidgetIconPanel from './widgets/WidgetIconPanel.svelte';
import { selectedWidget } from './stores';
import TabsPanel from './widgets/TabsPanel.svelte';
import TabContent from './TabContent.svelte';
</script>
<div class="theme-light">
@@ -18,6 +19,9 @@
<div class="tabs">
<TabsPanel />
</div>
<div class="content">
<TabContent />
</div>
</div>
<style>
@@ -50,7 +54,7 @@
display: flex;
position: fixed;
top: 0;
left: calc(var(--dim-widget-icon-size) + var(--dim-left-panel-width) + var(--dim-splitter-thickness));
left: var(--dim-content-left);
height: var(--dim-tabs-panel-height);
right: 0;
background-color: var(--theme-bg-2);
@@ -61,4 +65,12 @@
.tabs::-webkit-scrollbar {
height: 7px;
}
.content {
position: fixed;
top: var(--dim-tabs-panel-height);
left: var(--dim-content-left);
bottom: var(--dim-statusbar-height);
right: 0;
background-color: var(--theme-bg-1);
}
</style>

View File

@@ -0,0 +1,71 @@
<script context="module" lang="ts">
function createTabComponent(selectedTab) {
const tabComponent = tabs[selectedTab.tabComponent];
if (tabComponent) {
return {
tabComponent,
props: selectedTab.props,
};
}
return null;
}
</script>
<script lang="ts">
import _ from 'lodash';
import { openedTabs } from './stores';
import tabs from './tabs';
let mountedTabs = {};
$: selectedTab = $openedTabs.find(x => x.selected && x.closedTime == null);
// cleanup closed tabs
$: {
if (
_.difference(
_.keys(mountedTabs),
_.map(
$openedTabs.filter(x => x.closedTime == null),
'tabid'
)
).length > 0
) {
mountedTabs = _.pickBy(mountedTabs, (v, k) => $openedTabs.find(x => x.tabid == k && x.closedTime == null));
}
}
// open missing tabs
$: {
if (selectedTab) {
const { tabid } = selectedTab;
if (tabid && !mountedTabs[tabid])
mountedTabs = {
...mountedTabs,
[tabid]: createTabComponent(selectedTab),
};
}
}
</script>
{#each _.keys(mountedTabs) as tabid (tabid)}
<div class:tabVisible={tabid == (selectedTab && selectedTab.tabid)}>
<svelte:component this={mountedTabs[tabid].tabComponent} {...mountedTabs[tabid].props} {tabid} />
</div>
{/each}
<style>
div {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
}
.tabVisible {
visibility: visible;
}
:not(.tabVisible) {
visibility: hidden;
}
</style>

View File

@@ -18,7 +18,7 @@
const { schemaName, pureName, conid, database, objectTypeField } = data;
openNewTab({
title: data.pureName,
icon: 'icon table',
icon: 'img table',
tabComponent: 'TableDataTab',
props: {
schemaName,

View File

@@ -0,0 +1,164 @@
import {
ChangeSet,
changeSetContainsChanges,
changeSetInsertNewRow,
createChangeSet,
deleteChangeSetRows,
findExistingChangeSetItem,
getChangeSetInsertedRows,
GridDisplay,
revertChangeSetRowChanges,
setChangeSetValue,
} from 'dbgate-datalib';
import Grider, { GriderRowStatus } from './Grider';
export default class ChangeSetGrider extends Grider {
public insertedRows: any[];
public changeSet: ChangeSet;
public setChangeSet: Function;
private rowCacheIndexes: Set<number>;
private rowDataCache;
private rowStatusCache;
private rowDefinitionsCache;
private batchChangeSet: ChangeSet;
constructor(public sourceRows: any[], public changeSetState, public dispatchChangeSet, public display: GridDisplay) {
super();
this.changeSet = changeSetState && changeSetState.value;
this.insertedRows = getChangeSetInsertedRows(this.changeSet, display.baseTable);
this.setChangeSet = value => dispatchChangeSet({ type: 'set', value });
this.rowCacheIndexes = new Set();
this.rowDataCache = {};
this.rowStatusCache = {};
this.rowDefinitionsCache = {};
this.batchChangeSet = null;
}
getRowSource(index: number) {
if (index < this.sourceRows.length) return this.sourceRows[index];
return null;
}
getInsertedRowIndex(index) {
return index >= this.sourceRows.length ? index - this.sourceRows.length : null;
}
requireRowCache(index: number) {
if (this.rowCacheIndexes.has(index)) return;
const row = this.getRowSource(index);
const insertedRowIndex = this.getInsertedRowIndex(index);
const rowDefinition = this.display.getChangeSetRow(row, insertedRowIndex);
const [matchedField, matchedChangeSetItem] = findExistingChangeSetItem(this.changeSet, rowDefinition);
const rowUpdated = matchedChangeSetItem ? { ...row, ...matchedChangeSetItem.fields } : row;
let status = 'regular';
if (matchedChangeSetItem && matchedField == 'updates') status = 'updated';
if (matchedField == 'deletes') status = 'deleted';
if (insertedRowIndex != null) status = 'inserted';
const rowStatus = {
status,
modifiedFields:
matchedChangeSetItem && matchedChangeSetItem.fields ? new Set(Object.keys(matchedChangeSetItem.fields)) : null,
};
this.rowDataCache[index] = rowUpdated;
this.rowStatusCache[index] = rowStatus;
this.rowDefinitionsCache[index] = rowDefinition;
this.rowCacheIndexes.add(index);
}
get editable() {
return this.display.editable;
}
get canInsert() {
return !!this.display.baseTable;
}
getRowData(index: number) {
this.requireRowCache(index);
return this.rowDataCache[index];
}
getRowStatus(index): GriderRowStatus {
this.requireRowCache(index);
return this.rowStatusCache[index];
}
get rowCount() {
return this.sourceRows.length + this.insertedRows.length;
}
applyModification(changeSetReducer) {
if (this.batchChangeSet) {
this.batchChangeSet = changeSetReducer(this.batchChangeSet);
} else {
this.setChangeSet(changeSetReducer(this.changeSet));
}
}
setCellValue(index: number, uniqueName: string, value: any) {
const row = this.getRowSource(index);
const definition = this.display.getChangeSetField(row, uniqueName, this.getInsertedRowIndex(index));
this.applyModification(chs => setChangeSetValue(chs, definition, value));
}
deleteRow(index: number) {
this.requireRowCache(index);
this.applyModification(chs => deleteChangeSetRows(chs, this.rowDefinitionsCache[index]));
}
get rowCountInUpdate() {
if (this.batchChangeSet) {
const newRows = getChangeSetInsertedRows(this.batchChangeSet, this.display.baseTable);
return this.sourceRows.length + newRows.length;
} else {
return this.rowCount;
}
}
insertRow(): number {
const res = this.rowCountInUpdate;
this.applyModification(chs => changeSetInsertNewRow(chs, this.display.baseTable));
return res;
}
beginUpdate() {
this.batchChangeSet = this.changeSet;
}
endUpdate() {
this.setChangeSet(this.batchChangeSet);
this.batchChangeSet = null;
}
revertRowChanges(index: number) {
this.requireRowCache(index);
this.applyModification(chs => revertChangeSetRowChanges(chs, this.rowDefinitionsCache[index]));
}
revertAllChanges() {
this.applyModification(chs => createChangeSet());
}
undo() {
this.dispatchChangeSet({ type: 'undo' });
}
redo() {
this.dispatchChangeSet({ type: 'redo' });
}
get canUndo() {
return this.changeSetState.canUndo;
}
get canRedo() {
return this.changeSetState.canRedo;
}
get containsChanges() {
return changeSetContainsChanges(this.changeSet);
}
get disableLoadNextPage() {
return this.insertedRows.length > 0;
}
static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
}
static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
}
}

View File

@@ -0,0 +1,15 @@
<script lang="ts">
export let config;
export let gridCoreComponent;
export let loadNextData = undefined;
export let grider = undefined;
$: firstVisibleRowScrollIndex = 0;
$: visibleRowCountUpperBound = 25;
if (loadNextData && firstVisibleRowScrollIndex + visibleRowCountUpperBound >= grider.rowCount) {
loadNextData();
}
</script>
<svelte:component this={gridCoreComponent} {...$$props} />

View File

@@ -0,0 +1,56 @@
<div class="container">
<input type="text" class="focus-field" />
<table class="table">
<thead>
<tr>
<td class="header=cell" data-row="header" data-col="header" />
</tr>
</thead>
<tbody />
</table>
</div>
<style>
.container {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
user-select: none;
}
.table {
position: absolute;
left: 0;
top: 0;
bottom: 20px;
overflow: scroll;
border-collapse: collapse;
outline: none;
}
.header-cell {
border: 1px solid var(---theme-border);
text-align: left;
padding: 0;
margin: 0;
background-color: var(---theme-bg-1);
overflow: hidden;
}
.filter-cell {
text-align: left;
overflow: hidden;
margin: 0;
padding: 0;
}
.focus-field {
position: absolute;
left: -1000px;
top: -1000px;
}
.row-count-label {
position: absolute;
background-color: var(---theme-bg-2);
right: 40px;
bottom: 20px;
}
</style>

View File

@@ -0,0 +1,61 @@
export interface GriderRowStatus {
status: 'regular' | 'updated' | 'deleted' | 'inserted';
modifiedFields?: Set<string>;
insertedFields?: Set<string>;
deletedFields?: Set<string>;
}
export default abstract class Grider {
abstract getRowData(index): any;
abstract get rowCount(): number;
getRowStatus(index): GriderRowStatus {
const res: GriderRowStatus = {
status: 'regular',
};
return res;
}
beginUpdate() {}
endUpdate() {}
setCellValue(index: number, uniqueName: string, value: any) {}
deleteRow(index: number) {}
insertRow(): number {
return null;
}
revertRowChanges(index: number) {}
revertAllChanges() {}
undo() {}
redo() {}
get editable() {
return false;
}
get canInsert() {
return false;
}
get allowSave() {
return this.containsChanges;
}
get rowCountInUpdate() {
return this.rowCount;
}
get canUndo() {
return false;
}
get canRedo() {
return false;
}
get containsChanges() {
return false;
}
get disableLoadNextPage() {
return false;
}
get errors() {
return null;
}
updateRow(index, changeObject) {
for (const key of Object.keys(changeObject)) {
this.setCellValue(index, key, changeObject[key]);
}
}
}

View File

@@ -0,0 +1,68 @@
<script lang="ts">
import DataGridCore from './DataGridCore.svelte';
export let loadDataPage;
export let dataPageAvailable;
export let loadRowCount;
export let griderFactory;
let loadProps = {
isLoading: false,
loadedRows: [],
isLoadedAll: false,
loadedTime: new Date().getTime(),
allRowCount: null,
errorMessage: null,
loadNextDataToken: 0,
};
async function loadNextData() {
if (loadProps.isLoading) return;
loadProps.isLoading = true;
const loadStart = new Date().getTime();
// loadedTimeRef.current = loadStart;
const nextRows = await loadDataPage($$props, loadProps.loadedRows.length, 100);
// if (loadedTimeRef.current !== loadStart) {
// // new load was dispatched
// return;
// }
loadProps.isLoading = false;
if (nextRows.errorMessage) {
loadProps.errorMessage = nextRows.errorMessage;
} else {
// if (allRowCount == null) handleLoadRowCount();
loadProps.loadedRows = [...loadProps.loadedRows, ...nextRows];
loadProps.isLoadedAll = nextRows.length === 0;
// const loadedInfo = {
// loadedRows: [...loadedRows, ...nextRows],
// loadedTime,
// };
// setLoadProps(oldLoadProps => ({
// ...oldLoadProps,
// isLoading: false,
// isLoadedAll: oldLoadProps.loadNextDataToken == loadNextDataToken && nextRows.length === 0,
// loadNextDataToken,
// ...loadedInfo,
// }));
}
}
$: griderProps = { ...$$props, sourceRows: loadProps.loadedRows };
$: grider = griderFactory(griderProps);
const handleLoadNextData = () => {
if (!loadProps.isLoadedAll && !loadProps.errorMessage && !grider.disableLoadNextPage) {
if (dataPageAvailable($$props)) {
// If not, callbacks to load missing metadata are dispatched
loadNextData();
}
}
};
</script>
<DataGridCore />

View File

@@ -0,0 +1,65 @@
<script context="module" lang="ts">
async function loadDataPage(props, offset, limit) {
const { display, conid, database } = props;
const sql = display.getPageQuery(offset, limit);
const response = await axios.request({
url: 'database-connections/query-data',
method: 'post',
params: {
conid,
database,
},
data: { sql },
});
if (response.data.errorMessage) return response.data;
return response.data.rows;
}
function dataPageAvailable(props) {
const { display } = props;
const sql = display.getPageQuery(0, 1);
return !!sql;
}
async function loadRowCount(props) {
const { display, conid, database } = props;
const sql = display.getCountQuery();
const response = await axios.request({
url: 'database-connections/query-data',
method: 'post',
params: {
conid,
database,
},
data: { sql },
});
return parseInt(response.data.rows[0].count);
}
</script>
<script lang="ts">
import axios from '../utility/axios';
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
export let conid;
export let database;
export let schemaName;
export let pureName;
export let config;
</script>
<LoadingDataGridCore
{...$$props}
{loadDataPage}
{dataPageAvailable}
{loadRowCount}
griderFactory={ChangeSetGrider.factory}
/>

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { TableFormViewDisplay } from 'dbgate-datalib';
import { findEngineDriver } from 'dbgate-tools';
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import DataGrid from './DataGrid.svelte';
import SqlDataGridCore from './SqlDataGridCore.svelte';
export let conid;
export let database;
export let schemaName;
export let pureName;
export let config;
$: connection = useConnectionInfo({ conid });
$: dbinfo = useDatabaseInfo({ conid, database });
// $: display = connection
// ? new TableFormViewDisplay(
// { schemaName, pureName },
// findEngineDriver(connection, extensions),
// config,
// setConfig,
// cache || myCache,
// setCache || setMyCache,
// dbinfo
// )
// : null;;
</script>
<!-- <DataGrid {...$$props} gridCoreComponent={SqlDataGridCore} /> -->
XXX

View File

@@ -0,0 +1,46 @@
import { GridDisplay, ChangeSet, GridReferenceDefinition } from 'dbgate-datalib';
import Grider from './Grider';
export interface DataGridProps {
display: GridDisplay;
tabVisible?: boolean;
changeSetState?: { value: ChangeSet };
dispatchChangeSet?: Function;
toolbarPortalRef?: any;
showReferences?: boolean;
onReferenceClick?: (def: GridReferenceDefinition) => void;
onReferenceSourceChanged?: Function;
refReloadToken?: string;
masterLoadedTime?: number;
managerSize?: number;
grider?: Grider;
conid?: string;
database?: string;
jslid?: string;
[field: string]: any;
}
// export interface DataGridCoreProps extends DataGridProps {
// rows: any[];
// loadNextData?: Function;
// exportGrid?: Function;
// openQuery?: Function;
// undo?: Function;
// redo?: Function;
// errorMessage?: string;
// isLoadedAll?: boolean;
// loadedTime?: any;
// allRowCount?: number;
// conid?: string;
// database?: string;
// insertedRowCount?: number;
// isLoading?: boolean;
// }
// export interface LoadingDataGridProps extends DataGridProps {
// conid?: string;
// database?: string;
// jslid?: string;
// }

View File

@@ -1,8 +1,27 @@
import { writable } from 'svelte/store';
interface TabDefinition {
title: string;
closedTime?: number;
icon: string;
props: any;
selected: boolean;
busy: boolean;
tabid: string;
}
export function writableWithStorage<T>(defaultValue: T, storageName) {
const init = localStorage.getItem(storageName);
const res = writable<T>(init ? JSON.parse(init) : defaultValue);
res.subscribe(value => {
localStorage.setItem(storageName, JSON.stringify(value));
});
return res;
}
export const selectedWidget = writable('database');
export const openedConnections = writable([]);
export const currentDatabase = writable(null);
export const openedTabs = writable([]);
export const openedTabs = writableWithStorage<TabDefinition[]>([], 'openedTabs');
// export const leftPanelWidth = writable(300);

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import App from '../App.svelte';
import TableDataGrid from '../datagrid/TableDataGrid.svelte';
import useGridConfig from '../utility/useGridConfig';
export let tabid;
export let conid;
export let database;
export let schemaName;
export let pureName;
const config = useGridConfig(tabid);
</script>
<TableDataGrid {...$$props} {config} />

View File

@@ -0,0 +1,33 @@
import TableDataTab from './TableDataTab.svelte';
// import ViewDataTab from './ViewDataTab';
// import TableStructureTab from './TableStructureTab';
// import QueryTab from './QueryTab';
// import ShellTab from './ShellTab';
// import InfoPageTab from './InfoPageTab';
// import ArchiveFileTab from './ArchiveFileTab';
// import FreeTableTab from './FreeTableTab';
// import PluginTab from './PluginTab';
// import ChartTab from './ChartTab';
// import MarkdownEditorTab from './MarkdownEditorTab';
// import MarkdownViewTab from './MarkdownViewTab';
// import MarkdownPreviewTab from './MarkdownPreviewTab';
// import FavoriteEditorTab from './FavoriteEditorTab';
// import QueryDesignTab from './QueryDesignTab';
export default {
TableDataTab,
// ViewDataTab,
// TableStructureTab,
// QueryTab,
// InfoPageTab,
// ShellTab,
// ArchiveFileTab,
// FreeTableTab,
// PluginTab,
// ChartTab,
// MarkdownEditorTab,
// MarkdownViewTab,
// MarkdownPreviewTab,
// FavoriteEditorTab,
// QueryDesignTab,
};

View File

@@ -0,0 +1,30 @@
import { openedTabs } from '../stores';
export class LoadingToken {
constructor() {
this.isCanceled = false;
}
cancel() {
this.isCanceled = true;
}
}
export function sleep(milliseconds) {
return new Promise(resolve => window.setTimeout(() => resolve(null), milliseconds));
}
export function changeTab(tabid, setOpenedTabs, changeFunc) {
setOpenedTabs(files => files.map(tab => (tab.tabid == tabid ? changeFunc(tab) : tab)));
}
export function setSelectedTabFunc(files, tabid) {
return [
...(files || []).filter(x => x.tabid != tabid).map(x => ({ ...x, selected: false })),
...(files || []).filter(x => x.tabid == tabid).map(x => ({ ...x, selected: true })),
];
}
export function setSelectedTab(tabid) {
openedTabs.update(tabs => setSelectedTabFunc(tabs, tabid));
}

View File

@@ -2,7 +2,7 @@ import uuidv1 from 'uuid/v1';
import { openedTabs } from '../stores';
export default async function openNewTab(newTab, initialData = undefined, options = undefined) {
console.log('OPENING NEW TAB', newTab);
// console.log('OPENING NEW TAB', newTab);
const tabid = uuidv1();
openedTabs.update(tabs => [
...(tabs || []).map(x => ({ ...x, selected: false })),

View File

@@ -0,0 +1,21 @@
import { createGridConfig } from 'dbgate-datalib';
import { writable } from 'svelte/store';
import { onDestroy } from 'svelte';
const loadGridConfigFunc = tabid => () => {
const existing = localStorage.getItem(`tabdata_grid_${tabid}`);
if (existing) {
return {
...createGridConfig(),
...JSON.parse(existing),
};
}
return createGridConfig();
};
export default function useGridConfig(tabid) {
const config = writable(loadGridConfigFunc(tabid));
const unsubscribe = config.subscribe(value => localStorage.setItem(`tabdata_grid_${tabid}`, JSON.stringify(value)));
onDestroy(unsubscribe)
return config;
}

View File

@@ -24,6 +24,7 @@
import FontIcon from '../icons/FontIcon.svelte';
import { currentDatabase, openedTabs } from '../stores';
import { setSelectedTab } from '../utility/common';
$: currentDbKey =
$currentDatabase && $currentDatabase.name && $currentDatabase.connection
@@ -40,6 +41,65 @@
$: tabsByDb = _.groupBy(tabsWithDb, 'tabDbKey');
$: dbKeys = _.keys(tabsByDb).sort();
const handleTabClick = (e, tabid) => {
if (e.target.closest('.tabCloseButton')) {
return;
}
setSelectedTab(tabid);
};
const closeTabFunc = closeCondition => tabid => {
openedTabs.update(files => {
const active = files.find(x => x.tabid == tabid);
if (!active) return files;
const newFiles = files.map(x => ({
...x,
closedTime: x.closedTime || (closeCondition(x, active) ? new Date().getTime() : undefined),
}));
if (newFiles.find(x => x.selected && x.closedTime == null)) {
return newFiles;
}
const selectedIndex = _.findLastIndex(newFiles, x => x.closedTime == null);
return newFiles.map((x, index) => ({
...x,
selected: index == selectedIndex,
}));
});
};
const closeTab = closeTabFunc((x, active) => x.tabid == active.tabid);
const closeAll = () => {
const closedTime = new Date().getTime();
openedTabs.update(tabs =>
tabs.map(tab => ({
...tab,
closedTime: tab.closedTime || closedTime,
selected: false,
}))
);
};
const closeWithSameDb = closeTabFunc(
(x, active) =>
_.get(x, 'props.conid') == _.get(active, 'props.conid') &&
_.get(x, 'props.database') == _.get(active, 'props.database')
);
const closeWithOtherDb = closeTabFunc(
(x, active) =>
_.get(x, 'props.conid') != _.get(active, 'props.conid') ||
_.get(x, 'props.database') != _.get(active, 'props.database')
);
const closeOthers = closeTabFunc((x, active) => x.tabid != active.tabid);
const handleMouseUp = (e, tabid) => {
if (e.button == 1) {
e.preventDefault();
closeTab(tabid);
}
};
</script>
{#each dbKeys as dbKey}
@@ -50,12 +110,17 @@
</div>
<div class="db-group">
{#each _.sortBy(tabsByDb[dbKey], ['title', 'tabid']) as tab}
<div class="file-tab-item" class:selected={tab.selected}>
<div
class="file-tab-item"
class:selected={tab.selected}
on:click={e => handleTabClick(e, tab.tabid)}
on:mouseup={e => handleMouseUp(e, tab.tabid)}
>
<FontIcon icon={tab.busy ? 'icon loading' : tab.icon} />
<span class="file-name">
{tab.title}
</span>
<span class="close-button tabCloseButton">
<span class="close-button tabCloseButton" on:click={e => closeTab(tab.tabid)}>
<FontIcon icon="icon close" />
</span>
</div>