mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-23 02:16:02 +00:00
table data grid
This commit is contained in:
@@ -29,6 +29,9 @@
|
|||||||
"json-stable-stringify": "^1.0.1",
|
"json-stable-stringify": "^1.0.1",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
"dbgate-types": "^3.9.5",
|
"dbgate-types": "^3.9.5",
|
||||||
|
"dbgate-datalib": "^3.9.5",
|
||||||
|
"dbgate-sqltree": "^3.9.5",
|
||||||
|
"dbgate-tools": "^3.9.5",
|
||||||
"lodash": "^4.17.15"
|
"lodash": "^4.17.15"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -4,4 +4,5 @@
|
|||||||
--dim-left-panel-width: 300px;
|
--dim-left-panel-width: 300px;
|
||||||
--dim-tabs-panel-height: 53px;
|
--dim-tabs-panel-height: 53px;
|
||||||
--dim-splitter-thickness: 3px;
|
--dim-splitter-thickness: 3px;
|
||||||
|
--dim-content-left: calc(var(--dim-widget-icon-size) + var(--dim-left-panel-width) + var(--dim-splitter-thickness));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import WidgetIconPanel from './widgets/WidgetIconPanel.svelte';
|
import WidgetIconPanel from './widgets/WidgetIconPanel.svelte';
|
||||||
import { selectedWidget } from './stores';
|
import { selectedWidget } from './stores';
|
||||||
import TabsPanel from './widgets/TabsPanel.svelte';
|
import TabsPanel from './widgets/TabsPanel.svelte';
|
||||||
|
import TabContent from './TabContent.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="theme-light">
|
<div class="theme-light">
|
||||||
@@ -18,6 +19,9 @@
|
|||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<TabsPanel />
|
<TabsPanel />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<TabContent />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -50,7 +54,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
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);
|
height: var(--dim-tabs-panel-height);
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: var(--theme-bg-2);
|
background-color: var(--theme-bg-2);
|
||||||
@@ -61,4 +65,12 @@
|
|||||||
.tabs::-webkit-scrollbar {
|
.tabs::-webkit-scrollbar {
|
||||||
height: 7px;
|
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>
|
</style>
|
||||||
|
|||||||
71
packages/web/src/TabContent.svelte
Normal file
71
packages/web/src/TabContent.svelte
Normal 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>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
const { schemaName, pureName, conid, database, objectTypeField } = data;
|
const { schemaName, pureName, conid, database, objectTypeField } = data;
|
||||||
openNewTab({
|
openNewTab({
|
||||||
title: data.pureName,
|
title: data.pureName,
|
||||||
icon: 'icon table',
|
icon: 'img table',
|
||||||
tabComponent: 'TableDataTab',
|
tabComponent: 'TableDataTab',
|
||||||
props: {
|
props: {
|
||||||
schemaName,
|
schemaName,
|
||||||
|
|||||||
164
packages/web/src/datagrid/ChangeSetGrider.ts
Normal file
164
packages/web/src/datagrid/ChangeSetGrider.ts
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/web/src/datagrid/DataGrid.svelte
Normal file
15
packages/web/src/datagrid/DataGrid.svelte
Normal 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} />
|
||||||
56
packages/web/src/datagrid/DataGridCore.svelte
Normal file
56
packages/web/src/datagrid/DataGridCore.svelte
Normal 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>
|
||||||
61
packages/web/src/datagrid/Grider.ts
Normal file
61
packages/web/src/datagrid/Grider.ts
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
packages/web/src/datagrid/LoadingDataGridCore.svelte
Normal file
68
packages/web/src/datagrid/LoadingDataGridCore.svelte
Normal 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 />
|
||||||
65
packages/web/src/datagrid/SqlDataGridCore.svelte
Normal file
65
packages/web/src/datagrid/SqlDataGridCore.svelte
Normal 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}
|
||||||
|
/>
|
||||||
35
packages/web/src/datagrid/TableDataGrid.svelte
Normal file
35
packages/web/src/datagrid/TableDataGrid.svelte
Normal 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
|
||||||
46
packages/web/src/datagrid/types.ts
Normal file
46
packages/web/src/datagrid/types.ts
Normal 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;
|
||||||
|
// }
|
||||||
@@ -1,8 +1,27 @@
|
|||||||
import { writable } from 'svelte/store';
|
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 selectedWidget = writable('database');
|
||||||
export const openedConnections = writable([]);
|
export const openedConnections = writable([]);
|
||||||
export const currentDatabase = writable(null);
|
export const currentDatabase = writable(null);
|
||||||
export const openedTabs = writable([]);
|
export const openedTabs = writableWithStorage<TabDefinition[]>([], 'openedTabs');
|
||||||
|
|
||||||
// export const leftPanelWidth = writable(300);
|
// export const leftPanelWidth = writable(300);
|
||||||
|
|||||||
15
packages/web/src/tabs/TableDataTab.svelte
Normal file
15
packages/web/src/tabs/TableDataTab.svelte
Normal 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} />
|
||||||
33
packages/web/src/tabs/index.js
Normal file
33
packages/web/src/tabs/index.js
Normal 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,
|
||||||
|
};
|
||||||
30
packages/web/src/utility/common.js
Normal file
30
packages/web/src/utility/common.js
Normal 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));
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import uuidv1 from 'uuid/v1';
|
|||||||
import { openedTabs } from '../stores';
|
import { openedTabs } from '../stores';
|
||||||
|
|
||||||
export default async function openNewTab(newTab, initialData = undefined, options = undefined) {
|
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();
|
const tabid = uuidv1();
|
||||||
openedTabs.update(tabs => [
|
openedTabs.update(tabs => [
|
||||||
...(tabs || []).map(x => ({ ...x, selected: false })),
|
...(tabs || []).map(x => ({ ...x, selected: false })),
|
||||||
|
|||||||
21
packages/web/src/utility/useGridConfig.ts
Normal file
21
packages/web/src/utility/useGridConfig.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
import FontIcon from '../icons/FontIcon.svelte';
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
|
|
||||||
import { currentDatabase, openedTabs } from '../stores';
|
import { currentDatabase, openedTabs } from '../stores';
|
||||||
|
import { setSelectedTab } from '../utility/common';
|
||||||
|
|
||||||
$: currentDbKey =
|
$: currentDbKey =
|
||||||
$currentDatabase && $currentDatabase.name && $currentDatabase.connection
|
$currentDatabase && $currentDatabase.name && $currentDatabase.connection
|
||||||
@@ -40,6 +41,65 @@
|
|||||||
|
|
||||||
$: tabsByDb = _.groupBy(tabsWithDb, 'tabDbKey');
|
$: tabsByDb = _.groupBy(tabsWithDb, 'tabDbKey');
|
||||||
$: dbKeys = _.keys(tabsByDb).sort();
|
$: 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>
|
</script>
|
||||||
|
|
||||||
{#each dbKeys as dbKey}
|
{#each dbKeys as dbKey}
|
||||||
@@ -50,12 +110,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="db-group">
|
<div class="db-group">
|
||||||
{#each _.sortBy(tabsByDb[dbKey], ['title', 'tabid']) as tab}
|
{#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} />
|
<FontIcon icon={tab.busy ? 'icon loading' : tab.icon} />
|
||||||
<span class="file-name">
|
<span class="file-name">
|
||||||
{tab.title}
|
{tab.title}
|
||||||
</span>
|
</span>
|
||||||
<span class="close-button tabCloseButton">
|
<span class="close-button tabCloseButton" on:click={e => closeTab(tab.tabid)}>
|
||||||
<FontIcon icon="icon close" />
|
<FontIcon icon="icon close" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": false,
|
||||||
"strict": false
|
"strict": false,
|
||||||
|
"target": "es6",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user