archive, export into archive

This commit is contained in:
Jan Prochazka
2020-10-17 17:59:36 +02:00
parent 39a4c39b6d
commit b0f0710a75
22 changed files with 357 additions and 33 deletions

View File

@@ -8,6 +8,7 @@ import {
SavedSqlFilesProvider,
OpenedConnectionsProvider,
LeftPanelWidthProvider,
CurrentArchiveProvider,
} from './utility/globalState';
import { SocketProvider } from './utility/SocketProvider';
import ConnectionsPinger from './utility/ConnectionsPinger';
@@ -24,7 +25,9 @@ function App() {
<LeftPanelWidthProvider>
<ConnectionsPinger>
<ModalLayerProvider>
<Screen />
<CurrentArchiveProvider>
<Screen />
</CurrentArchiveProvider>
</ModalLayerProvider>
</ConnectionsPinger>
</LeftPanelWidthProvider>

View File

@@ -0,0 +1,39 @@
import React from 'react';
import _ from 'lodash';
import moment from 'moment';
import { DatabaseIcon, getIconImage, ArchiveTableIcon } from '../icons';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import { openNewTab } from '../utility/common';
function Menu({ data, setOpenedTabs }) {
const handleDelete = () => {
// setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid));
};
return (
<>
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
</>
);
}
const archiveFileAppObject = () => ({ fileName, folderName }, { setOpenedTabs }) => {
const key = fileName;
// const Icon = (props) => <i className="fas fa-archive" />;
const Icon = ArchiveTableIcon;
const onClick = () => {
openNewTab(setOpenedTabs, {
title: fileName,
icon: 'archtable.svg',
tooltip: `${folderName}\n${fileName}`,
tabComponent: 'ArchiveFileTab',
props: {
fileName,
folderName,
},
});
};
return { title: fileName, key, Icon, Menu, onClick };
};
export default archiveFileAppObject;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import _ from 'lodash';
import moment from 'moment';
import { LocalDbIcon, getIconImage } from '../icons';
import { DropDownMenuItem } from '../modals/DropDownMenu';
function Menu({ data, setOpenedTabs }) {
const handleDelete = () => {
// setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid));
};
return (
<>
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
</>
);
}
const archiveFolderAppObject = () => ({ name }, { setOpenedTabs, currentArchive }) => {
const key = name;
// const Icon = (props) => <i className="fas fa-archive" />;
const Icon = LocalDbIcon;
const isBold = name == currentArchive;
return { title: name, key, Icon, isBold, Menu };
};
export default archiveFolderAppObject;

View File

@@ -58,6 +58,7 @@ export function ExpandIcon({
export const TableIcon = (props) => getIconImage('table2.svg', props);
export const ViewIcon = (props) => getIconImage('view2.svg', props);
export const ArchiveTableIcon = (props) => getIconImage('archtable.svg', props);
export const DatabaseIcon = (props) => getIconImage('database.svg', props);
export const ServerIcon = (props) => getIconImage('server.svg', props);

View File

@@ -9,6 +9,7 @@ import {
FormDatabaseSelect,
FormTablesSelect,
FormSchemaSelect,
FormArchiveFolderSelect,
} from '../utility/forms';
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import TableControl, { TableColumn } from '../utility/TableControl';
@@ -147,6 +148,7 @@ function SourceTargetConfig({
storageTypeField,
connectionIdField,
databaseNameField,
archiveFolderField,
schemaNameField,
tablesField = undefined,
engine = undefined,
@@ -161,6 +163,7 @@ function SourceTargetConfig({
{ value: 'jsonl', label: 'JSON lines file(s)', directions: ['source', 'target'] },
{ value: 'excel', label: 'MS Excel file(s)', directions: ['source'] },
{ value: 'query', label: 'SQL Query', directions: ['source'] },
{ value: 'archive', label: 'Archive', directions: ['source', 'target'] },
];
const storageType = values[storageTypeField];
const dbinfo = useDatabaseInfo({ conid: values[connectionIdField], database: values[databaseNameField] });
@@ -231,6 +234,13 @@ function SourceTargetConfig({
</>
)}
{storageType == 'archive' && (
<>
<Label>Archive folder</Label>
<FormArchiveFolderSelect name={archiveFolderField} />
</>
)}
{isFileStorage(storageType) && direction == 'source' && <FilesInput />}
</Column>
);
@@ -270,6 +280,7 @@ export default function ImportExportConfigurator() {
storageTypeField="sourceStorageType"
connectionIdField="sourceConnectionId"
databaseNameField="sourceDatabaseName"
archiveFolderField="sourceArchiveFolder"
schemaNameField="sourceSchemaName"
tablesField="sourceList"
engine={sourceEngine}
@@ -279,6 +290,7 @@ export default function ImportExportConfigurator() {
storageTypeField="targetStorageType"
connectionIdField="targetConnectionId"
databaseNameField="targetDatabaseName"
archiveFolderField="targetArchiveFolder"
schemaNameField="targetSchemaName"
/>
</Wrapper>

View File

@@ -68,7 +68,7 @@ function getSourceExpr(sourceName, values, sourceConnection, sourceDriver) {
if (sourceStorageType == 'jsldata') {
return ['jslDataReader', { jslid: values.sourceJslId }];
}
throw new Error(`Unknown storage type: ${sourceStorageType}`);
throw new Error(`Unknown source storage type: ${sourceStorageType}`);
}
function getFlagsFroAction(action) {
@@ -91,7 +91,8 @@ function getFlagsFroAction(action) {
}
function getTargetExpr(sourceName, values, targetConnection, targetDriver) {
if (values.targetStorageType == 'csv') {
const { targetStorageType } = values;
if (targetStorageType == 'csv') {
return [
'csvWriter',
{
@@ -99,7 +100,7 @@ function getTargetExpr(sourceName, values, targetConnection, targetDriver) {
},
];
}
if (values.targetStorageType == 'jsonl') {
if (targetStorageType == 'jsonl') {
return [
'jsonLinesWriter',
{
@@ -107,7 +108,7 @@ function getTargetExpr(sourceName, values, targetConnection, targetDriver) {
},
];
}
if (values.targetStorageType == 'database') {
if (targetStorageType == 'database') {
return [
'tableWriter',
{
@@ -118,6 +119,17 @@ function getTargetExpr(sourceName, values, targetConnection, targetDriver) {
},
];
}
if (targetStorageType == 'archive') {
return [
'archiveWriter',
{
folderName: values.targetArchiveFolder,
fileName: getTargetName(sourceName, values),
},
];
}
throw new Error(`Unknown target storage type: ${targetStorageType}`);
}
export default async function createImpExpScript(values, addEditorInfo = true) {

View File

@@ -9,7 +9,7 @@ import ModalContent from './ModalContent';
import ImportExportConfigurator from '../impexp/ImportExportConfigurator';
import createImpExpScript from '../impexp/createImpExpScript';
import { openNewTab } from '../utility/common';
import { useSetOpenedTabs } from '../utility/globalState';
import { useCurrentArchive, useSetOpenedTabs } from '../utility/globalState';
import RunnerOutputPane from '../query/RunnerOutputPane';
import axios from '../utility/axios';
@@ -41,6 +41,7 @@ function GenerateSctriptButton({ modalState }) {
export default function ImportExportModal({ modalState, initialValues }) {
const [executeNumber, setExecuteNumber] = React.useState(0);
const [runnerId, setRunnerId] = React.useState(null);
const archive = useCurrentArchive();
const handleExecute = async (values) => {
const script = await createImpExpScript(values);
@@ -57,7 +58,13 @@ export default function ImportExportModal({ modalState, initialValues }) {
<ModalBase modalState={modalState}>
<Formik
onSubmit={handleExecute}
initialValues={{ sourceStorageType: 'database', targetStorageType: 'csv', ...initialValues }}
initialValues={{
sourceStorageType: 'database',
targetStorageType: 'csv',
sourceArchiveFolder: archive,
targetArchiveFolder: archive,
...initialValues,
}}
>
<Form>
<ModalHeader modalState={modalState}>Import/Export</ModalHeader>

View File

@@ -0,0 +1,6 @@
import React from 'react';
import JslDataGrid from '../sqleditor/JslDataGrid';
export default function ArchiveFileTab({ folderName, fileName, tabVisible, toolbarPortalRef, tabid }) {
return <JslDataGrid jslid={`archive://${folderName}/${fileName}`} />;
}

View File

@@ -4,6 +4,7 @@ import TableStructureTab from './TableStructureTab';
import QueryTab from './QueryTab';
import ShellTab from './ShellTab';
import InfoPageTab from './InfoPageTab';
import ArchiveFileTab from './ArchiveFileTab';
export default {
TableDataTab,
@@ -12,4 +13,5 @@ export default {
QueryTab,
InfoPageTab,
ShellTab,
ArchiveFileTab,
};

View File

@@ -1,12 +1,14 @@
import React from 'react';
import styled from 'styled-components';
import Select from 'react-select';
import Creatable from 'react-select/creatable';
import { TextField, SelectField } from './inputs';
import { Field, useFormikContext } from 'formik';
import FormStyledButton from '../widgets/FormStyledButton';
import { useConnectionList, useDatabaseList, useDatabaseInfo } from './metadataLoaders';
import { useConnectionList, useDatabaseList, useDatabaseInfo, useArchiveFolders } from './metadataLoaders';
import useSocket from './SocketProvider';
import getAsArray from './getAsArray';
import axios from './axios';
export const FormRow = styled.div`
display: flex;
@@ -85,11 +87,11 @@ export function FormRadioGroupItem({ name, text, value }) {
);
}
export function FormReactSelect({ options, name, isMulti = false }) {
export function FormReactSelect({ options, name, isMulti = false, Component = Select, ...other }) {
const { setFieldValue, values } = useFormikContext();
return (
<Select
<Component
options={options}
value={
isMulti
@@ -102,6 +104,7 @@ export function FormReactSelect({ options, name, isMulti = false }) {
menuPortalTarget={document.body}
isMulti={isMulti}
closeMenuOnSelect={!isMulti}
{...other}
/>
);
}
@@ -170,3 +173,25 @@ export function FormTablesSelect({ conidName, databaseName, schemaName, name })
if (tablesOptions.length == 0) return <div>Not available</div>;
return <FormReactSelect options={tablesOptions} name={name} isMulti />;
}
export function FormArchiveFolderSelect({ name }) {
const { setFieldValue } = useFormikContext();
const folders = useArchiveFolders();
const folderOptions = React.useMemo(
() =>
(folders || []).map((folder) => ({
value: folder.name,
label: folder.name,
})),
[folders]
);
const handleCreateOption = (folder) => {
axios.post('archive/create-folder', { folder });
setFieldValue(name, folder);
};
return (
<FormReactSelect options={folderOptions} name={name} Component={Creatable} onCreateOption={handleCreateOption} />
);
}

View File

@@ -87,12 +87,14 @@ export function useAppObjectParams() {
const setSavedSqlFiles = useSetSavedSqlFiles();
const openedConnections = useOpenedConnections();
const setOpenedConnections = useSetOpenedConnections();
const currentArchive = useCurrentArchive();
const showModal = useShowModal();
const config = useConfig();
return {
setOpenedTabs,
currentDatabase,
currentArchive,
newQuery,
openedTabs,
setSavedSqlFiles,
@@ -113,3 +115,7 @@ export { OpenedConnectionsProvider, useOpenedConnections, useSetOpenedConnection
const [LeftPanelWidthProvider, useLeftPanelWidth, useSetLeftPanelWidth] = createGlobalState(300);
export { LeftPanelWidthProvider, useLeftPanelWidth, useSetLeftPanelWidth };
const [CurrentArchiveProvider, useCurrentArchive, useSetCurrentArchive] = createGlobalState('default');
export { CurrentArchiveProvider, useCurrentArchive, useSetCurrentArchive };

View File

@@ -64,6 +64,18 @@ const databaseListLoader = ({ conid }) => ({
reloadTrigger: `database-list-changed-${conid}`,
});
const archiveFoldersLoader = () => ({
url: 'archive/folders',
params: {},
reloadTrigger: `archive-folders-changed`,
});
const archiveFilesLoader = ({ folder }) => ({
url: 'archive/files',
params: { folder },
reloadTrigger: `archive-files-changed-${folder}`,
});
const serverStatusLoader = () => ({
url: 'server-connections/server-status',
params: {},
@@ -217,3 +229,17 @@ export function getConfig() {
export function useConfig() {
return useCore(configLoader, {}) || {};
}
export function getArchiveFiles(args) {
return getCore(archiveFilesLoader, args);
}
export function useArchiveFiles(args) {
return useCore(archiveFilesLoader, args);
}
export function getArchiveFolders(args) {
return getCore(archiveFoldersLoader, args);
}
export function useArchiveFolders(args) {
return useCore(archiveFoldersLoader, args);
}

View File

@@ -0,0 +1,70 @@
import React from 'react';
import styled from 'styled-components';
import _ from 'lodash';
import { AppObjectList } from '../appobj/AppObjectList';
import { useCurrentArchive, useOpenedTabs, useSavedSqlFiles, useSetCurrentArchive } from '../utility/globalState';
import closedTabAppObject from '../appobj/closedTabAppObject';
import {
SearchBoxWrapper,
WidgetsInnerContainer,
WidgetsMainContainer,
WidgetsOuterContainer,
WidgetTitle,
} from './WidgetStyles';
import savedSqlFileAppObject from '../appobj/savedSqlFileAppObject';
import { useArchiveFiles, useArchiveFolders } from '../utility/metadataLoaders';
import archiveFolderAppObject from '../appobj/archiveFolderAppObject';
import archiveFileAppObject from '../appobj/archiveFileAppObject';
function ArchiveFolderList() {
const folders = useArchiveFolders();
const setArchive = useSetCurrentArchive();
return (
<>
<WidgetTitle>Archive folder</WidgetTitle>
<WidgetsInnerContainer>
<AppObjectList
list={_.sortBy(folders, 'name')}
makeAppObj={archiveFolderAppObject()}
onObjectClick={(archive) => setArchive(archive.name)}
/>
</WidgetsInnerContainer>
</>
);
}
function ArchiveFilesList() {
const folder = useCurrentArchive();
const files = useArchiveFiles({ folder });
return (
<>
<WidgetTitle>Archive files</WidgetTitle>
<WidgetsInnerContainer>
<AppObjectList
list={(files || []).map((file) => ({
fileName: file.name,
folderName: folder,
}))}
makeAppObj={archiveFileAppObject()}
/>
</WidgetsInnerContainer>
</>
);
}
export default function ArchiveWidget() {
return (
<WidgetsMainContainer>
<WidgetsOuterContainer>
<ArchiveFolderList />
</WidgetsOuterContainer>
<WidgetsOuterContainer>
<ArchiveFilesList />
</WidgetsOuterContainer>
</WidgetsMainContainer>
);
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { useCurrentWidget } from '../utility/globalState';
import ArchiveWidget from './ArchiveWidget';
import DatabaseWidget from './DatabaseWidget';
import FilesWidget from './FilesWidget';
@@ -7,5 +8,6 @@ export default function WidgetContainer() {
const currentWidget = useCurrentWidget();
if (currentWidget === 'database') return <DatabaseWidget />;
if (currentWidget === 'file') return <FilesWidget />;
if (currentWidget === 'archive') return <ArchiveWidget />;
return null;
}

View File

@@ -10,7 +10,7 @@ const IconWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
background-color: ${props =>
background-color: ${(props) =>
// @ts-ignore
props.isSelected ? theme.widgetMenu.backgroundSelected : 'inherit'};
&:hover {
@@ -23,6 +23,7 @@ export default function WidgetIconPanel() {
{
icon: 'fa-database',
name: 'database',
title: 'Database connections',
},
// {
// icon: 'fa-table',
@@ -31,6 +32,12 @@ export default function WidgetIconPanel() {
{
icon: 'fa-file-alt',
name: 'file',
title: 'Closed tabs & Saved SQL files',
},
{
icon: 'fa-archive',
name: 'archive',
title: 'Archive (saved tabular data)',
},
// {
// icon: 'fa-cog',
@@ -47,14 +54,15 @@ export default function WidgetIconPanel() {
return (
<>
{widgets.map(({ icon, name }) => (
{widgets.map(({ icon, name, title }) => (
<IconWrapper
key={icon}
// @ts-ignore
isSelected={name === currentWidget}
onClick={() => setCurrentWidget(name === currentWidget ? null : name)}
title={title}
>
<i className={`fas ${icon}`}/>
<i className={`fas ${icon}`} />
</IconWrapper>
))}
</>