mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-22 21:36:00 +00:00
free table editor - save and load
This commit is contained in:
@@ -1,7 +1,11 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
const stream = require('stream');
|
||||||
|
const readline = require('readline');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { formatWithOptions } = require('util');
|
||||||
const { archivedir } = require('../utility/directories');
|
const { archivedir } = require('../utility/directories');
|
||||||
const socket = require('../utility/socket');
|
const socket = require('../utility/socket');
|
||||||
|
const JsonLinesDatastore = require('../utility/JsonLinesDatastore');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
folders_meta: 'get',
|
folders_meta: 'get',
|
||||||
@@ -61,4 +65,37 @@ module.exports = {
|
|||||||
await fs.rmdir(path.join(archivedir(), folder), { recursive: true });
|
await fs.rmdir(path.join(archivedir(), folder), { recursive: true });
|
||||||
socket.emitChanged(`archive-folders-changed`);
|
socket.emitChanged(`archive-folders-changed`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
saveFreeTable_meta: 'post',
|
||||||
|
async saveFreeTable({ folder, file, data }) {
|
||||||
|
const { structure, rows } = data;
|
||||||
|
const fileStream = fs.createWriteStream(path.join(archivedir(), folder, `${file}.jsonl`));
|
||||||
|
await fileStream.write(JSON.stringify(structure) + '\n');
|
||||||
|
for (const row of rows) {
|
||||||
|
await fileStream.write(JSON.stringify(row) + '\n');
|
||||||
|
}
|
||||||
|
await fileStream.close();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
loadFreeTable_meta: 'post',
|
||||||
|
async loadFreeTable({ folder, file }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const fileStream = fs.createReadStream(path.join(archivedir(), folder, `${file}.jsonl`));
|
||||||
|
const liner = readline.createInterface({
|
||||||
|
input: fileStream,
|
||||||
|
});
|
||||||
|
let structure = null;
|
||||||
|
const rows = [];
|
||||||
|
liner.on('line', (line) => {
|
||||||
|
const data = JSON.parse(line);
|
||||||
|
if (structure) rows.push(data);
|
||||||
|
else structure = data;
|
||||||
|
});
|
||||||
|
liner.on('close', () => {
|
||||||
|
resolve({ structure, rows });
|
||||||
|
fileStream.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export class FreeTableGridDisplay extends GridDisplay {
|
|||||||
) {
|
) {
|
||||||
super(config, setConfig, cache, setCache);
|
super(config, setConfig, cache, setCache);
|
||||||
this.columns = this.getDisplayColumns(model);
|
this.columns = this.getDisplayColumns(model);
|
||||||
this.filterable = true;
|
this.filterable = false;
|
||||||
this.sortable = true;
|
this.sortable = false;
|
||||||
this.editable = true;
|
this.editable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,46 @@ import { openNewTab } from '../utility/common';
|
|||||||
import { filterName } from '@dbgate/datalib';
|
import { filterName } from '@dbgate/datalib';
|
||||||
import axios from '../utility/axios';
|
import axios from '../utility/axios';
|
||||||
|
|
||||||
|
function openArchive(setOpenedTabs, fileName, folderName) {
|
||||||
|
openNewTab(setOpenedTabs, {
|
||||||
|
title: fileName,
|
||||||
|
icon: 'archtable.svg',
|
||||||
|
tooltip: `${folderName}\n${fileName}`,
|
||||||
|
tabComponent: 'ArchiveFileTab',
|
||||||
|
props: {
|
||||||
|
archiveFile: fileName,
|
||||||
|
archiveFolder: folderName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function Menu({ data, setOpenedTabs }) {
|
function Menu({ data, setOpenedTabs }) {
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
axios.post('archive/delete-file', { file: data.fileName, folder: data.folderName });
|
axios.post('archive/delete-file', { file: data.fileName, folder: data.folderName });
|
||||||
// setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid));
|
// setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid));
|
||||||
};
|
};
|
||||||
|
const handleOpenRead = () => {
|
||||||
|
openArchive(setOpenedTabs, data.fileName, data.folderName);
|
||||||
|
};
|
||||||
|
const handleOpenWrite = async () => {
|
||||||
|
const resp = await axios.post('archive/load-free-table', { file: data.fileName, folder: data.folderName });
|
||||||
|
|
||||||
|
openNewTab(setOpenedTabs, {
|
||||||
|
title: data.fileName,
|
||||||
|
icon: 'freetable.svg',
|
||||||
|
tabComponent: 'FreeTableTab',
|
||||||
|
props: {
|
||||||
|
initialData: resp.data,
|
||||||
|
archiveFile: data.fileName,
|
||||||
|
archiveFolder: data.folderName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DropDownMenuItem onClick={handleOpenRead}>Open (readonly)</DropDownMenuItem>
|
||||||
|
<DropDownMenuItem onClick={handleOpenWrite}>Open in free table editor</DropDownMenuItem>
|
||||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -24,16 +57,7 @@ const archiveFileAppObject = () => ({ fileName, folderName }, { setOpenedTabs })
|
|||||||
// const Icon = (props) => <i className="fas fa-archive" />;
|
// const Icon = (props) => <i className="fas fa-archive" />;
|
||||||
const Icon = ArchiveTableIcon;
|
const Icon = ArchiveTableIcon;
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
openNewTab(setOpenedTabs, {
|
openArchive(setOpenedTabs, fileName, folderName);
|
||||||
title: fileName,
|
|
||||||
icon: 'archtable.svg',
|
|
||||||
tooltip: `${folderName}\n${fileName}`,
|
|
||||||
tabComponent: 'ArchiveFileTab',
|
|
||||||
props: {
|
|
||||||
archiveFile: fileName,
|
|
||||||
archiveFolder: folderName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
const matcher = (filter) => filterName(filter, fileName);
|
const matcher = (filter) => filterName(filter, fileName);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useSetOpenedTabs, useCurrentDatabase } from '../utility/globalState';
|
import { useSetOpenedTabs } from '../utility/globalState';
|
||||||
import { openNewTab } from '../utility/common';
|
import { openNewTab } from '../utility/common';
|
||||||
|
|
||||||
export default function useNewFreeTable() {
|
export default function useNewFreeTable() {
|
||||||
@@ -10,6 +10,6 @@ export default function useNewFreeTable() {
|
|||||||
title: title || 'Table',
|
title: title || 'Table',
|
||||||
icon: 'freetable.svg',
|
icon: 'freetable.svg',
|
||||||
tabComponent: 'FreeTableTab',
|
tabComponent: 'FreeTableTab',
|
||||||
props: {},
|
props,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
44
packages/web/src/modals/SaveArchiveModal.js
Normal file
44
packages/web/src/modals/SaveArchiveModal.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ModalBase from './ModalBase';
|
||||||
|
import { FormTextField, FormSubmit, FormArchiveFolderSelect, FormRow, FormLabel } from '../utility/forms';
|
||||||
|
import { Formik, Form } from 'formik';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import ModalHeader from './ModalHeader';
|
||||||
|
import ModalContent from './ModalContent';
|
||||||
|
import ModalFooter from './ModalFooter';
|
||||||
|
|
||||||
|
const SelectWrapper = styled.div`
|
||||||
|
width: 150px;
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function SaveArchiveModal({ file = 'new-table', folder = 'default', modalState, onSave }) {
|
||||||
|
const handleSubmit = async (values) => {
|
||||||
|
const { file, folder } = values;
|
||||||
|
modalState.close();
|
||||||
|
if (onSave) onSave(folder, file);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<ModalBase modalState={modalState}>
|
||||||
|
<ModalHeader modalState={modalState}>Save to archive</ModalHeader>
|
||||||
|
<Formik onSubmit={handleSubmit} initialValues={{ file, folder }}>
|
||||||
|
<Form>
|
||||||
|
<ModalContent>
|
||||||
|
{/* <Label>Archive folder</Label> */}
|
||||||
|
<FormRow>
|
||||||
|
<FormLabel>Folder</FormLabel>
|
||||||
|
<SelectWrapper>
|
||||||
|
<FormArchiveFolderSelect name="folder" />
|
||||||
|
</SelectWrapper>
|
||||||
|
</FormRow>
|
||||||
|
<FormTextField label="File name" name="file" />
|
||||||
|
</ModalContent>
|
||||||
|
<ModalFooter>
|
||||||
|
<FormSubmit text="Save" />
|
||||||
|
</ModalFooter>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
</ModalBase>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,15 +2,21 @@ import React from 'react';
|
|||||||
import { createGridCache, createChangeSet, createGridConfig, createFreeTableModel } from '@dbgate/datalib';
|
import { createGridCache, createChangeSet, createGridConfig, createFreeTableModel } from '@dbgate/datalib';
|
||||||
import useUndoReducer from '../utility/useUndoReducer';
|
import useUndoReducer from '../utility/useUndoReducer';
|
||||||
import usePropsCompare from '../utility/usePropsCompare';
|
import usePropsCompare from '../utility/usePropsCompare';
|
||||||
import { useUpdateDatabaseForTab } from '../utility/globalState';
|
import { useSetOpenedTabs, useUpdateDatabaseForTab } from '../utility/globalState';
|
||||||
import TableDataGrid from '../datagrid/TableDataGrid';
|
import TableDataGrid from '../datagrid/TableDataGrid';
|
||||||
import useGridConfig from '../utility/useGridConfig';
|
import useGridConfig from '../utility/useGridConfig';
|
||||||
import FreeTableGrid from '../freetable/FreeTableGrid';
|
import FreeTableGrid from '../freetable/FreeTableGrid';
|
||||||
|
import SaveArchiveModal from '../modals/SaveArchiveModal';
|
||||||
|
import useModalState from '../modals/useModalState';
|
||||||
|
import axios from '../utility/axios';
|
||||||
|
import { changeTab } from '../utility/common';
|
||||||
|
|
||||||
export default function FreeDataTab({ conid, database, schemaName, pureName, tabVisible, toolbarPortalRef, tabid }) {
|
export default function FreeDataTab({ archiveFolder, archiveFile, tabVisible, toolbarPortalRef, tabid, initialData }) {
|
||||||
const [config, setConfig] = useGridConfig(tabid);
|
const [config, setConfig] = useGridConfig(tabid);
|
||||||
const [modelState, dispatchModel] = useUndoReducer(createFreeTableModel());
|
const [modelState, dispatchModel] = useUndoReducer(initialData || createFreeTableModel());
|
||||||
const storageKey = `tabdata_freetable_${tabid}`;
|
const storageKey = `tabdata_freetable_${tabid}`;
|
||||||
|
const saveSqlFileModalState = useModalState();
|
||||||
|
const setOpenedTabs = useSetOpenedTabs();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const existingData = localStorage.getItem(storageKey);
|
const existingData = localStorage.getItem(storageKey);
|
||||||
@@ -25,15 +31,32 @@ export default function FreeDataTab({ conid, database, schemaName, pureName, tab
|
|||||||
localStorage.setItem(storageKey, JSON.stringify(modelState.value));
|
localStorage.setItem(storageKey, JSON.stringify(modelState.value));
|
||||||
}, [modelState]);
|
}, [modelState]);
|
||||||
|
|
||||||
|
const handleSave = async (folder, file) => {
|
||||||
|
await axios.post('archive/save-free-table', { folder, file, data: modelState.value });
|
||||||
|
changeTab(tabid, setOpenedTabs, (tab) => ({
|
||||||
|
...tab,
|
||||||
|
title: file,
|
||||||
|
props: { archiveFIle: file, archiveFolder: folder },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FreeTableGrid
|
<>
|
||||||
conid={conid}
|
<FreeTableGrid
|
||||||
config={config}
|
config={config}
|
||||||
setConfig={setConfig}
|
setConfig={setConfig}
|
||||||
modelState={modelState}
|
modelState={modelState}
|
||||||
dispatchModel={dispatchModel}
|
dispatchModel={dispatchModel}
|
||||||
tabVisible={tabVisible}
|
tabVisible={tabVisible}
|
||||||
toolbarPortalRef={toolbarPortalRef}
|
toolbarPortalRef={toolbarPortalRef}
|
||||||
/>
|
onSave={() => saveSqlFileModalState.open()}
|
||||||
|
/>
|
||||||
|
<SaveArchiveModal
|
||||||
|
modalState={saveSqlFileModalState}
|
||||||
|
folder={archiveFolder}
|
||||||
|
file={archiveFile}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export function FormArchiveFilesSelect({ folderName, name }) {
|
|||||||
return <FormReactSelect options={filesOptions} name={name} isMulti />;
|
return <FormReactSelect options={filesOptions} name={name} isMulti />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormArchiveFolderSelect({ name }) {
|
export function FormArchiveFolderSelect({ name, ...other }) {
|
||||||
const { setFieldValue } = useFormikContext();
|
const { setFieldValue } = useFormikContext();
|
||||||
const folders = useArchiveFolders();
|
const folders = useArchiveFolders();
|
||||||
const folderOptions = React.useMemo(
|
const folderOptions = React.useMemo(
|
||||||
@@ -214,6 +214,12 @@ export function FormArchiveFolderSelect({ name }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormReactSelect options={folderOptions} name={name} Component={Creatable} onCreateOption={handleCreateOption} />
|
<FormReactSelect
|
||||||
|
{...other}
|
||||||
|
options={folderOptions}
|
||||||
|
name={name}
|
||||||
|
Component={Creatable}
|
||||||
|
onCreateOption={handleCreateOption}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user