mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-20 06:36:00 +00:00
table data grid
This commit is contained in:
@@ -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>
|
||||
|
||||
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;
|
||||
openNewTab({
|
||||
title: data.pureName,
|
||||
icon: 'icon table',
|
||||
icon: 'img table',
|
||||
tabComponent: 'TableDataTab',
|
||||
props: {
|
||||
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';
|
||||
|
||||
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);
|
||||
|
||||
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';
|
||||
|
||||
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 })),
|
||||
|
||||
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 { 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>
|
||||
|
||||
Reference in New Issue
Block a user