mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-21 01:16:01 +00:00
remove web
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { useOpenedConnections, useCurrentDatabase } from './globalState';
|
||||
import axios from './axios';
|
||||
|
||||
export default function ConnectionsPinger({ children }) {
|
||||
const openedConnections = useOpenedConnections();
|
||||
const currentDatabase = useCurrentDatabase();
|
||||
|
||||
const doServerPing = () => {
|
||||
axios.post('server-connections/ping', { connections: openedConnections });
|
||||
};
|
||||
|
||||
const doDatabasePing = () => {
|
||||
const database = _.get(currentDatabase, 'name');
|
||||
const conid = _.get(currentDatabase, 'connection._id');
|
||||
if (conid && database) {
|
||||
axios.post('database-connections/ping', { conid, database });
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
doServerPing();
|
||||
const handle = window.setInterval(doServerPing, 30 * 1000);
|
||||
return () => window.clearInterval(handle);
|
||||
}, [openedConnections]);
|
||||
|
||||
React.useEffect(() => {
|
||||
doDatabasePing();
|
||||
const handle = window.setInterval(doDatabasePing, 30 * 1000);
|
||||
return () => window.clearInterval(handle);
|
||||
}, [currentDatabase]);
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import ErrorInfo from '../widgets/ErrorInfo';
|
||||
import styled from 'styled-components';
|
||||
import localforage from 'localforage';
|
||||
import FormStyledButton from '../widgets/FormStyledButton';
|
||||
|
||||
const Stack = styled.pre`
|
||||
margin-left: 20px;
|
||||
`;
|
||||
|
||||
const WideButton = styled(FormStyledButton)`
|
||||
width: 150px;
|
||||
`;
|
||||
|
||||
const Info = styled.div`
|
||||
margin: 20px;
|
||||
`;
|
||||
|
||||
export function ErrorScreen({ error }) {
|
||||
let message;
|
||||
try {
|
||||
message = 'Error: ' + (error.message || error).toString();
|
||||
} catch (e) {
|
||||
message = 'DbGate internal error detected';
|
||||
}
|
||||
|
||||
const handleReload = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const handleClearReload = async () => {
|
||||
localStorage.clear();
|
||||
try {
|
||||
await localforage.clear();
|
||||
} catch (err) {
|
||||
console.error('Error clearing app data', err);
|
||||
}
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ErrorInfo message={message} />
|
||||
<WideButton type="button" value="Reload app" onClick={handleReload} />
|
||||
<WideButton type="button" value="Clear and reload" onClick={handleClearReload} />
|
||||
<Info>
|
||||
If reloading doesn't help, you can try to clear all browser data (opened tabs, history of opened windows)
|
||||
and reload app. Your connections and saved files are not touched by this clear operation. <br />
|
||||
If you see this error in the tab, closing the tab should solve the problem.
|
||||
</Info>
|
||||
<Stack>{_.isString(error.stack) ? error.stack : null}</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// Update state so the next render will show the fallback UI.
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
};
|
||||
}
|
||||
componentDidCatch(error, errorInfo) {
|
||||
// You can also log the error to an error reporting service
|
||||
// logErrorToMyService(error, errorInfo);
|
||||
console.error(error);
|
||||
// console.log('errorInfo', errorInfo);
|
||||
// console.log('error', error);
|
||||
}
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return <ErrorScreen error={this.state.error} />;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export function ErrorBoundaryTest({ children }) {
|
||||
let error;
|
||||
try {
|
||||
const x = 1;
|
||||
// @ts-ignore
|
||||
x.log();
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
return <ErrorScreen error={error} />;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import { FormTextField, FormSelectField, FormCheckboxField } from './forms';
|
||||
import { useForm } from './FormProvider';
|
||||
|
||||
const FormArgumentsWrapper = styled.div``;
|
||||
|
||||
function FormArgument({ arg, namePrefix }) {
|
||||
const name = `${namePrefix}${arg.name}`;
|
||||
if (arg.type == 'text') {
|
||||
return <FormTextField label={arg.label} name={name} />;
|
||||
}
|
||||
if (arg.type == 'checkbox') {
|
||||
return <FormCheckboxField label={arg.label} name={name} defaultValue={arg.default} />;
|
||||
}
|
||||
if (arg.type == 'select') {
|
||||
return (
|
||||
<FormSelectField label={arg.label} name={name}>
|
||||
{arg.options.map(opt =>
|
||||
_.isString(opt) ? <option value={opt}>{opt}</option> : <option value={opt.value}>{opt.name}</option>
|
||||
)}
|
||||
</FormSelectField>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function FormArgumentList({ args, onChangeValues = undefined, namePrefix }) {
|
||||
const { values } = useForm();
|
||||
React.useEffect(() => {
|
||||
if (onChangeValues) onChangeValues(values);
|
||||
}, [values]);
|
||||
return (
|
||||
<FormArgumentsWrapper>
|
||||
{' '}
|
||||
{args.map(arg => (
|
||||
<FormArgument arg={arg} key={arg.name} namePrefix={namePrefix} />
|
||||
))}
|
||||
</FormArgumentsWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormFieldTemplateDefault } from './formStyle';
|
||||
import keycodes from './keycodes';
|
||||
|
||||
const FormContext = React.createContext(null);
|
||||
const FormFieldTemplateContext = React.createContext(null);
|
||||
|
||||
export function FormProvider({ children, initialValues = {}, template = FormFieldTemplateDefault }) {
|
||||
const [values, setValues] = React.useState(initialValues);
|
||||
return (
|
||||
<FormProviderCore values={values} setValues={setValues} template={template}>
|
||||
{children}
|
||||
</FormProviderCore>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormProviderCore({ children, values, setValues, template = FormFieldTemplateDefault }) {
|
||||
const [submitAction, setSubmitAction] = React.useState(null);
|
||||
const handleEnter = React.useCallback(
|
||||
e => {
|
||||
if (e.keyCode == keycodes.enter && submitAction && submitAction.action) {
|
||||
e.preventDefault();
|
||||
submitAction.action(values);
|
||||
}
|
||||
},
|
||||
[submitAction, values]
|
||||
);
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('keyup', handleEnter);
|
||||
return () => {
|
||||
document.removeEventListener('keyup', handleEnter);
|
||||
};
|
||||
}, [handleEnter]);
|
||||
const setFieldValue = React.useCallback(
|
||||
(field, value) =>
|
||||
setValues(v => ({
|
||||
...v,
|
||||
[field]: value,
|
||||
})),
|
||||
[setValues]
|
||||
);
|
||||
const provider = {
|
||||
values,
|
||||
setValues,
|
||||
setFieldValue,
|
||||
setSubmitAction,
|
||||
};
|
||||
return (
|
||||
<FormContext.Provider value={provider}>
|
||||
<FormFieldTemplateProvider template={template}>{children}</FormFieldTemplateProvider>
|
||||
</FormContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useForm() {
|
||||
return React.useContext(FormContext);
|
||||
}
|
||||
|
||||
export function FormFieldTemplateProvider({ children, template = FormFieldTemplateDefault }) {
|
||||
return <FormFieldTemplateContext.Provider value={template}>{children}</FormFieldTemplateContext.Provider>;
|
||||
}
|
||||
|
||||
export function useFormFieldTemplate() {
|
||||
return React.useContext(FormFieldTemplateContext);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import TableControl, { TableColumn } from './TableControl';
|
||||
// import { AppObjectControl } from '../appobj/AppObjects';
|
||||
import useTheme from '../theme/useTheme';
|
||||
|
||||
const ObjectListWrapper = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const ObjectListHeader = styled.div`
|
||||
background-color: ${props => props.theme.gridheader_background};
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
const ObjectListHeaderTitle = styled.span`
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
`;
|
||||
|
||||
const ObjectListBody = styled.div`
|
||||
margin: 20px;
|
||||
// margin-left: 20px;
|
||||
// margin-right: 20px;
|
||||
// margin-top: 3px;
|
||||
`;
|
||||
|
||||
export default function ObjectListControl({ collection = [], title, showIfEmpty = false, NameComponent, children }) {
|
||||
const theme = useTheme();
|
||||
if (collection.length == 0 && !showIfEmpty) return null;
|
||||
|
||||
return (
|
||||
<ObjectListWrapper>
|
||||
<ObjectListHeader theme={theme}>
|
||||
<ObjectListHeaderTitle>{title}</ObjectListHeaderTitle>
|
||||
</ObjectListHeader>
|
||||
<ObjectListBody>
|
||||
<TableControl rows={collection}>
|
||||
<TableColumn fieldName="displayName" header="Name" formatter={data => <NameComponent data={data} />} />
|
||||
{children}
|
||||
</TableControl>
|
||||
</ObjectListBody>
|
||||
</ObjectListWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import ToolbarButton, { ToolbarDropDownButton } from '../widgets/ToolbarButton';
|
||||
|
||||
export default function SaveFileToolbarButton({ tabid, save, saveAs }) {
|
||||
if (!saveAs) return null;
|
||||
|
||||
if (save) {
|
||||
return (
|
||||
<ToolbarDropDownButton icon="icon save" text="Save">
|
||||
<DropDownMenuItem onClick={save} keyText="Ctrl+S">
|
||||
Save
|
||||
</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={saveAs} keyText="Ctrl+Shift+S">
|
||||
Save As
|
||||
</DropDownMenuItem>
|
||||
</ToolbarDropDownButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarButton onClick={saveAs} icon="icon save">
|
||||
Save As
|
||||
</ToolbarButton>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import io from 'socket.io-client';
|
||||
import React from 'react';
|
||||
import resolveApi from './resolveApi';
|
||||
import { cacheClean } from './cache';
|
||||
|
||||
const SocketContext = React.createContext(null);
|
||||
|
||||
export function SocketProvider({ children }) {
|
||||
const [socket, setSocket] = React.useState();
|
||||
React.useEffect(() => {
|
||||
// const newSocket = io('http://localhost:3000', { transports: ['websocket'] });
|
||||
const newSocket = io(resolveApi());
|
||||
setSocket(newSocket);
|
||||
newSocket.on('clean-cache', reloadTrigger => cacheClean(reloadTrigger));
|
||||
}, []);
|
||||
return <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>;
|
||||
}
|
||||
|
||||
export default function useSocket() {
|
||||
return React.useContext(SocketContext);
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import keycodes from './keycodes';
|
||||
import useTheme from '../theme/useTheme';
|
||||
|
||||
const Table = styled.table`
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
user-select: ${props =>
|
||||
// @ts-ignore
|
||||
props.focusable ? 'none' : ''};
|
||||
// outline: none;
|
||||
`;
|
||||
const TableHead = styled.thead``;
|
||||
const TableBody = styled.tbody``;
|
||||
const TableHeaderRow = styled.tr``;
|
||||
const TableBodyRow = styled.tr`
|
||||
background-color: ${props =>
|
||||
// @ts-ignore
|
||||
props.isSelected ? props.theme.gridbody_background_blue[1] : props.theme.gridbody_background};
|
||||
`;
|
||||
const TableHeaderCell = styled.td`
|
||||
border: 1px solid ${props => props.theme.gridheader_background};
|
||||
background-color: ${props => props.theme.gridheader_background};
|
||||
padding: 5px;
|
||||
`;
|
||||
const TableBodyCell = styled.td`
|
||||
border: 1px solid ${props => props.theme.gridbody_background2};
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
export function TableColumn({ fieldName, header, sortable = false, formatter = undefined }) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function format(row, col) {
|
||||
const { formatter, fieldName } = col;
|
||||
if (formatter) return formatter(row);
|
||||
return row[fieldName];
|
||||
}
|
||||
|
||||
export default function TableControl({
|
||||
rows = [],
|
||||
children,
|
||||
focusOnCreate = false,
|
||||
onKeyDown = undefined,
|
||||
tabIndex = -1,
|
||||
setSelectedIndex = undefined,
|
||||
selectedIndex = undefined,
|
||||
tableRef = undefined,
|
||||
}) {
|
||||
const columns = (children instanceof Array ? _.flatten(children) : [children])
|
||||
.filter(child => child && child.props && child.props.fieldName)
|
||||
.map(child => child.props);
|
||||
|
||||
const myTableRef = React.useRef(null);
|
||||
const currentTableRef = tableRef || myTableRef;
|
||||
const theme = useTheme();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (focusOnCreate) {
|
||||
currentTableRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
event => {
|
||||
if (event.keyCode == keycodes.downArrow) {
|
||||
setSelectedIndex(i => Math.min(i + 1, rows.length - 1));
|
||||
}
|
||||
if (event.keyCode == keycodes.upArrow) {
|
||||
setSelectedIndex(i => Math.max(0, i - 1));
|
||||
}
|
||||
if (onKeyDown) onKeyDown(event);
|
||||
},
|
||||
[setSelectedIndex, rows]
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
ref={currentTableRef}
|
||||
onKeyDown={selectedIndex != null ? handleKeyDown : undefined}
|
||||
tabIndex={selectedIndex != null ? tabIndex : undefined}
|
||||
// @ts-ignore
|
||||
focusable={selectedIndex != null}
|
||||
>
|
||||
<TableHead>
|
||||
<TableHeaderRow>
|
||||
{columns.map(x => (
|
||||
<TableHeaderCell key={x.fieldName} theme={theme}>
|
||||
{x.header}
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</TableHeaderRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, index) => (
|
||||
<TableBodyRow
|
||||
key={index}
|
||||
theme={theme}
|
||||
// @ts-ignore
|
||||
isSelected={index == selectedIndex}
|
||||
onClick={
|
||||
selectedIndex != null
|
||||
? () => {
|
||||
setSelectedIndex(index);
|
||||
currentTableRef.current.focus();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{columns.map(col => (
|
||||
<TableBodyCell key={col.fieldName} theme={theme}>
|
||||
{format(row, col)}
|
||||
</TableBodyCell>
|
||||
))}
|
||||
</TableBodyRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
export default function ToolbarPortal({ toolbarPortalRef, tabVisible, children }) {
|
||||
return (
|
||||
(toolbarPortalRef &&
|
||||
toolbarPortalRef.current &&
|
||||
tabVisible &&
|
||||
children &&
|
||||
ReactDOM.createPortal(children, toolbarPortalRef.current)) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import { FormStyledLabel } from '../widgets/FormStyledButton';
|
||||
import styled from 'styled-components';
|
||||
import { useUploadFiles } from './UploadsProvider';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin: 10px;
|
||||
`;
|
||||
|
||||
export default function UploadButton() {
|
||||
const theme = useTheme();
|
||||
const uploadFiles = useUploadFiles();
|
||||
const handleChange = e => {
|
||||
const files = [...e.target.files];
|
||||
uploadFiles(files);
|
||||
};
|
||||
return (
|
||||
<Wrapper>
|
||||
<FormStyledLabel htmlFor="uploadFileButton" theme={theme}>
|
||||
Upload file
|
||||
</FormStyledLabel>
|
||||
<input type="file" id="uploadFileButton" hidden onChange={handleChange} />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import { findFileFormat } from './fileformats';
|
||||
import getElectron from './getElectron';
|
||||
import resolveApi from './resolveApi';
|
||||
import useExtensions from './useExtensions';
|
||||
import { useOpenElectronFileCore, canOpenByElectron } from './useOpenElectronFile';
|
||||
|
||||
const UploadsContext = React.createContext(null);
|
||||
|
||||
export default function UploadsProvider({ children }) {
|
||||
const [uploadListener, setUploadListener] = React.useState(null);
|
||||
return <UploadsContext.Provider value={{ uploadListener, setUploadListener }}>{children}</UploadsContext.Provider>;
|
||||
}
|
||||
|
||||
export function useUploadsProvider() {
|
||||
return React.useContext(UploadsContext);
|
||||
}
|
||||
|
||||
export function useUploadFiles() {
|
||||
const { uploadListener } = useUploadsProvider();
|
||||
const showModal = useShowModal();
|
||||
const extensions = useExtensions();
|
||||
const electron = getElectron();
|
||||
const openElectronFileCore = useOpenElectronFileCore();
|
||||
|
||||
const handleUploadFiles = React.useCallback(
|
||||
files => {
|
||||
files.forEach(async file => {
|
||||
if (parseInt(file.size, 10) >= 4 * 1024 * 1024) {
|
||||
// to big file
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('FILE', file);
|
||||
|
||||
if (electron && canOpenByElectron(file.path, extensions)) {
|
||||
openElectronFileCore(file.path);
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('data', file);
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
};
|
||||
|
||||
const apiBase = resolveApi();
|
||||
const resp = await fetch(`${apiBase}/uploads/upload`, fetchOptions);
|
||||
const fileData = await resp.json();
|
||||
|
||||
fileData.shortName = file.name;
|
||||
|
||||
for (const format of extensions.fileFormats) {
|
||||
if (file.name.endsWith('.' + format.extension)) {
|
||||
fileData.shortName = file.name.slice(0, -format.extension.length - 1);
|
||||
fileData.storageType = format.storageType;
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadListener) {
|
||||
uploadListener(fileData);
|
||||
} else {
|
||||
if (findFileFormat(extensions, fileData.storageType)) {
|
||||
showModal(modalState => (
|
||||
<ImportExportModal
|
||||
uploadedFile={fileData}
|
||||
modalState={modalState}
|
||||
importToArchive
|
||||
initialValues={{
|
||||
sourceStorageType: fileData.storageType,
|
||||
// sourceConnectionId: data.conid,
|
||||
// sourceDatabaseName: data.database,
|
||||
// sourceSchemaName: data.schemaName,
|
||||
// sourceList: [data.pureName],
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// const reader = new FileReader();
|
||||
|
||||
// reader.onabort = () => console.log('file reading was aborted');
|
||||
// reader.onerror = () => console.log('file reading has failed');
|
||||
// reader.onload = () => {
|
||||
// // Do whatever you want with the file contents
|
||||
// const binaryStr = reader.result;
|
||||
// console.log(binaryStr);
|
||||
// };
|
||||
// reader.readAsArrayBuffer(file);
|
||||
});
|
||||
},
|
||||
[uploadListener, extensions]
|
||||
);
|
||||
|
||||
return handleUploadFiles;
|
||||
}
|
||||
|
||||
export function useUploadsZone() {
|
||||
const onDrop = useUploadFiles();
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
|
||||
|
||||
return { getRootProps, getInputProps, isDragActive };
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { getDbCore, getConnectionInfo, getSqlObjectInfo } from '../utility/metadataLoaders';
|
||||
import sqlFormatter from 'sql-formatter';
|
||||
import { driverBase, findEngineDriver } from 'dbgate-tools';
|
||||
|
||||
export default async function applySqlTemplate(sqlTemplate, extensions, props) {
|
||||
if (sqlTemplate == 'CREATE TABLE') {
|
||||
const tableInfo = await getDbCore(props, props.objectTypeField || 'tables');
|
||||
const connection = await getConnectionInfo(props);
|
||||
const driver = findEngineDriver(connection, extensions) || driverBase;
|
||||
const dmp = driver.createDumper();
|
||||
if (tableInfo) dmp.createTable(tableInfo);
|
||||
return dmp.s;
|
||||
}
|
||||
if (sqlTemplate == 'CREATE OBJECT') {
|
||||
const objectInfo = await getSqlObjectInfo(props);
|
||||
if (objectInfo) {
|
||||
if (objectInfo.requiresFormat && objectInfo.createSql) return sqlFormatter.format(objectInfo.createSql);
|
||||
else return objectInfo.createSql;
|
||||
}
|
||||
}
|
||||
if (sqlTemplate == 'EXECUTE PROCEDURE') {
|
||||
const procedureInfo = await getSqlObjectInfo(props);
|
||||
const connection = await getConnectionInfo(props);
|
||||
|
||||
const driver = findEngineDriver(connection, extensions) || driverBase;
|
||||
const dmp = driver.createDumper();
|
||||
if (procedureInfo) dmp.put('^execute %f', procedureInfo);
|
||||
return dmp.s;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import resolveApi from './resolveApi';
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: resolveApi(),
|
||||
});
|
||||
|
||||
axiosInstance.defaults.headers = {
|
||||
'Cache-Control': 'no-cache',
|
||||
Pragma: 'no-cache',
|
||||
Expires: '0',
|
||||
};
|
||||
|
||||
export default axiosInstance;
|
||||
@@ -1,40 +0,0 @@
|
||||
import getAsArray from './getAsArray';
|
||||
|
||||
let cachedByKey = {};
|
||||
let cachedPromisesByKey = {};
|
||||
const cachedKeysByReloadTrigger = {};
|
||||
|
||||
export function cacheGet(key) {
|
||||
return cachedByKey[key];
|
||||
}
|
||||
|
||||
export function cacheSet(key, value, reloadTrigger) {
|
||||
cachedByKey[key] = value;
|
||||
for (const item of getAsArray(reloadTrigger)) {
|
||||
if (!(item in cachedKeysByReloadTrigger)) {
|
||||
cachedKeysByReloadTrigger[item] = [];
|
||||
}
|
||||
cachedKeysByReloadTrigger[item].push(key);
|
||||
}
|
||||
delete cachedPromisesByKey[key];
|
||||
}
|
||||
|
||||
export function cacheClean(reloadTrigger) {
|
||||
for (const item of getAsArray(reloadTrigger)) {
|
||||
const keys = cachedKeysByReloadTrigger[item];
|
||||
if (keys) {
|
||||
for (const key of keys) {
|
||||
delete cachedByKey[key];
|
||||
delete cachedPromisesByKey[key];
|
||||
}
|
||||
}
|
||||
delete cachedKeysByReloadTrigger[item];
|
||||
}
|
||||
}
|
||||
|
||||
export function getCachedPromise(key, func) {
|
||||
if (key in cachedPromisesByKey) return cachedPromisesByKey[key];
|
||||
const promise = func();
|
||||
cachedPromisesByKey[key] = promise;
|
||||
return promise;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
export function copyTextToClipboard(text) {
|
||||
const textArea = document.createElement('textarea');
|
||||
|
||||
//
|
||||
// *** This styling is an extra step which is likely not required. ***
|
||||
//
|
||||
// Why is it here? To ensure:
|
||||
// 1. the element is able to have focus and selection.
|
||||
// 2. if element was to flash render it has minimal visual impact.
|
||||
// 3. less flakyness with selection and copying which **might** occur if
|
||||
// the textarea element is not visible.
|
||||
//
|
||||
// The likelihood is the element won't even render, not even a flash,
|
||||
// so some of these are just precautions. However in IE the element
|
||||
// is visible whilst the popup box asking the user for permission for
|
||||
// the web page to copy to the clipboard.
|
||||
//
|
||||
|
||||
// Place in top-left corner of screen regardless of scroll position.
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.top = '0';
|
||||
textArea.style.left = '0';
|
||||
|
||||
// Ensure it has a small width and height. Setting to 1px / 1em
|
||||
// doesn't work as this gives a negative w/h on some browsers.
|
||||
textArea.style.width = '2em';
|
||||
textArea.style.height = '2em';
|
||||
|
||||
// We don't need padding, reducing the size if it does flash render.
|
||||
textArea.style.padding = '0';
|
||||
|
||||
// Clean up any borders.
|
||||
textArea.style.border = 'none';
|
||||
textArea.style.outline = 'none';
|
||||
textArea.style.boxShadow = 'none';
|
||||
|
||||
// Avoid flash of white box if rendered for any reason.
|
||||
textArea.style.background = 'transparent';
|
||||
|
||||
textArea.value = text;
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
let successful = document.execCommand('copy');
|
||||
if (!successful) {
|
||||
console.log('Failed copy to clipboard');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Failed copy to clipboard: ' + err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
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 })),
|
||||
];
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
const jsonlFormat = {
|
||||
storageType: 'jsonl',
|
||||
extension: 'jsonl',
|
||||
name: 'JSON lines',
|
||||
readerFunc: 'jsonLinesReader',
|
||||
writerFunc: 'jsonLinesWriter',
|
||||
};
|
||||
|
||||
/** @returns {import('dbgate-types').FileFormatDefinition[]} */
|
||||
export function buildFileFormats(plugins) {
|
||||
const res = [jsonlFormat];
|
||||
for (const { content } of plugins) {
|
||||
const { fileFormats } = content;
|
||||
if (fileFormats) res.push(...fileFormats);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function findFileFormat(extensions, storageType) {
|
||||
return extensions.fileFormats.find(x => x.storageType == storageType);
|
||||
}
|
||||
|
||||
export function getFileFormatDirections(format) {
|
||||
if (!format) return [];
|
||||
const res = [];
|
||||
if (format.readerFunc) res.push('source');
|
||||
if (format.writerFunc) res.push('target');
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getDefaultFileFormat(extensions) {
|
||||
return extensions.fileFormats.find(x => x.storageType == 'csv') || jsonlFormat;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const FlexCol3 = styled.div`
|
||||
flex-basis: 25%;
|
||||
max-width: 25%;
|
||||
${props =>
|
||||
!!props.marginRight &&
|
||||
`
|
||||
margin-right: ${props.marginRight}px;
|
||||
`}
|
||||
${props =>
|
||||
!!props.marginLeft &&
|
||||
`
|
||||
margin-left: ${props.marginLeft}px;
|
||||
`}
|
||||
`;
|
||||
export const FlexCol4 = styled.div`
|
||||
flex-basis: 33.3333%;
|
||||
max-width: 33.3333%;
|
||||
${props =>
|
||||
!!props.marginRight &&
|
||||
`
|
||||
margin-right: ${props.marginRight}px;
|
||||
`}
|
||||
${props =>
|
||||
!!props.marginLeft &&
|
||||
`
|
||||
margin-left: ${props.marginLeft}px;
|
||||
`}
|
||||
`;
|
||||
export const FlexCol6 = styled.div`
|
||||
flex-basis: 50%;
|
||||
max-width: 50%;
|
||||
${props =>
|
||||
!!props.marginRight &&
|
||||
`
|
||||
margin-right: ${props.marginRight}px;
|
||||
`}
|
||||
${props =>
|
||||
!!props.marginLeft &&
|
||||
`
|
||||
margin-left: ${props.marginLeft}px;
|
||||
`}
|
||||
`;
|
||||
export const FlexCol8 = styled.div`
|
||||
flex-basis: 66.6667%;
|
||||
max-width: 66.6667%;
|
||||
${props =>
|
||||
!!props.marginRight &&
|
||||
`
|
||||
margin-right: ${props.marginRight}px;
|
||||
`}
|
||||
${props =>
|
||||
!!props.marginLeft &&
|
||||
`
|
||||
margin-left: ${props.marginLeft}px;
|
||||
`}
|
||||
`;
|
||||
export const FlexCol9 = styled.div`
|
||||
flex-basis: 75%;
|
||||
max-width: 75%;
|
||||
${props =>
|
||||
!!props.marginRight &&
|
||||
`
|
||||
margin-right: ${props.marginRight}px;
|
||||
`}
|
||||
${props =>
|
||||
!!props.marginLeft &&
|
||||
`
|
||||
margin-left: ${props.marginLeft}px;
|
||||
`}
|
||||
`;
|
||||
@@ -1,122 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import React from 'react';
|
||||
import useTheme from '../theme/useTheme';
|
||||
|
||||
export const FormRow = styled.div`
|
||||
display: flex;
|
||||
margin: 10px;
|
||||
`;
|
||||
|
||||
export const FormLabel = styled.div`
|
||||
width: 10vw;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
export const FormValue = styled.div``;
|
||||
|
||||
export function FormFieldTemplateDefault({ label, children, labelProps, type }) {
|
||||
return (
|
||||
<FormRow>
|
||||
<FormLabel {...labelProps}>{label}</FormLabel>
|
||||
<FormValue>{children}</FormValue>
|
||||
</FormRow>
|
||||
);
|
||||
}
|
||||
|
||||
export const FormRowTiny = styled.div`
|
||||
margin: 5px;
|
||||
`;
|
||||
|
||||
export const FormLabelTiny = styled.div`
|
||||
color: ${props => props.theme.manager_font3};
|
||||
`;
|
||||
|
||||
export const FormValueTiny = styled.div`
|
||||
margin-left: 15px;
|
||||
margin-top: 3px;
|
||||
`;
|
||||
|
||||
const FormLabelSpan = styled.span`
|
||||
${props =>
|
||||
// @ts-ignore
|
||||
props.disabled &&
|
||||
`
|
||||
color: ${props.theme.manager_font3};
|
||||
`}
|
||||
`;
|
||||
|
||||
export function FormFieldTemplateTiny({ label, children, labelProps, type }) {
|
||||
const theme = useTheme();
|
||||
if (type == 'checkbox') {
|
||||
return (
|
||||
<FormRowTiny>
|
||||
{children}{' '}
|
||||
<FormLabelSpan theme={theme} {...labelProps}>
|
||||
{label}
|
||||
</FormLabelSpan>
|
||||
</FormRowTiny>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormRowTiny>
|
||||
<FormLabelTiny theme={theme}>
|
||||
<FormLabelSpan theme={theme} {...labelProps}>
|
||||
{label}
|
||||
</FormLabelSpan>
|
||||
</FormLabelTiny>
|
||||
<FormValueTiny>{children}</FormValueTiny>
|
||||
</FormRowTiny>
|
||||
);
|
||||
}
|
||||
|
||||
const FormRowLargeTemplate = styled.div`
|
||||
${props =>
|
||||
// @ts-ignore
|
||||
!props.noMargin &&
|
||||
`
|
||||
margin: 20px;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const FormRowLarge = styled.div`
|
||||
margin: 20px;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const FormLabelLarge = styled.div`
|
||||
margin-bottom: 3px;
|
||||
color: ${props => props.theme.manager_font3};
|
||||
`;
|
||||
|
||||
export const FormValueLarge = styled.div``;
|
||||
|
||||
export function FormFieldTemplateLarge({ label, labelProps, children, type, noMargin = false }) {
|
||||
const theme = useTheme();
|
||||
if (type == 'checkbox') {
|
||||
return (
|
||||
<FormRowLargeTemplate
|
||||
// @ts-ignore
|
||||
noMargin={noMargin}
|
||||
>
|
||||
{children}{' '}
|
||||
<FormLabelSpan {...labelProps} theme={theme}>
|
||||
{label}
|
||||
</FormLabelSpan>
|
||||
</FormRowLargeTemplate>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormRowLargeTemplate
|
||||
className="largeFormMarker"
|
||||
// @ts-ignore
|
||||
noMargin={noMargin}
|
||||
>
|
||||
<FormLabelLarge theme={theme}>
|
||||
<FormLabelSpan theme={theme} {...labelProps}>
|
||||
{label}
|
||||
</FormLabelSpan>
|
||||
</FormLabelLarge>
|
||||
<FormValueLarge>{children}</FormValueLarge>
|
||||
</FormRowLargeTemplate>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export default function formatFileSize(size) {
|
||||
if (size > 1000000000) return `${Math.round(size / 10000000000) * 10} GB`;
|
||||
if (size > 1000000) return `${Math.round(size / 10000000) * 10} MB`;
|
||||
if (size > 1000) return `${Math.round(size / 10000) * 10} KB`;
|
||||
return `${size} bytes`;
|
||||
}
|
||||
@@ -1,370 +0,0 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import Creatable from 'react-select/creatable';
|
||||
import { TextField, SelectField, CheckboxField } from './inputs';
|
||||
import FormStyledButton from '../widgets/FormStyledButton';
|
||||
import {
|
||||
useConnectionList,
|
||||
useDatabaseList,
|
||||
useDatabaseInfo,
|
||||
useArchiveFolders,
|
||||
useArchiveFiles,
|
||||
} from './metadataLoaders';
|
||||
import getAsArray from './getAsArray';
|
||||
import axios from './axios';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import { useForm, useFormFieldTemplate } from './FormProvider';
|
||||
import { FontIcon } from '../icons';
|
||||
import getElectron from './getElectron';
|
||||
import InlineButton from '../widgets/InlineButton';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const FlexContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export function FormFieldTemplate({ label, children, type }) {
|
||||
const FieldTemplate = useFormFieldTemplate();
|
||||
return (
|
||||
<FieldTemplate label={label} type={type}>
|
||||
{children}
|
||||
</FieldTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormCondition({ condition, children }) {
|
||||
const { values } = useForm();
|
||||
if (condition(values)) return children;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function FormTextFieldRaw({ name, focused = false, ...other }) {
|
||||
const { values, setFieldValue } = useForm();
|
||||
const handleChange = event => {
|
||||
setFieldValue(name, event.target.value);
|
||||
};
|
||||
const textFieldRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (textFieldRef.current && focused) textFieldRef.current.focus();
|
||||
}, [textFieldRef.current, focused]);
|
||||
|
||||
return <TextField {...other} value={values[name]} onChange={handleChange} editorRef={textFieldRef} />;
|
||||
}
|
||||
|
||||
export function FormPasswordFieldRaw({ name, focused = false, ...other }) {
|
||||
const { values, setFieldValue } = useForm();
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
const handleChange = event => {
|
||||
setFieldValue(name, event.target.value);
|
||||
};
|
||||
const textFieldRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (textFieldRef.current && focused) textFieldRef.current.focus();
|
||||
}, [textFieldRef.current, focused]);
|
||||
const value = values[name];
|
||||
const isCrypted = value && value.startsWith('crypt:');
|
||||
|
||||
return (
|
||||
<FlexContainer>
|
||||
<TextField
|
||||
{...other}
|
||||
value={isCrypted ? '' : value}
|
||||
onChange={handleChange}
|
||||
editorRef={textFieldRef}
|
||||
placeholder={isCrypted ? '(Password is encrypted)' : undefined}
|
||||
type={isCrypted || showPassword ? 'text' : 'password'}
|
||||
/>
|
||||
{!isCrypted && (
|
||||
<InlineButton onClick={() => setShowPassword(x => !x)} disabled={other.disabled}>
|
||||
<FontIcon icon="icon eye" />
|
||||
</InlineButton>
|
||||
)}
|
||||
</FlexContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormTextField({ name, label, focused = false, templateProps = undefined, ...other }) {
|
||||
const FieldTemplate = useFormFieldTemplate();
|
||||
return (
|
||||
<FieldTemplate label={label} type="text" {...templateProps}>
|
||||
<FormTextFieldRaw name={name} focused={focused} {...other} />
|
||||
</FieldTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormPasswordField({ name, label, focused = false, templateProps = undefined, ...other }) {
|
||||
const FieldTemplate = useFormFieldTemplate();
|
||||
return (
|
||||
<FieldTemplate label={label} type="text" {...templateProps}>
|
||||
<FormPasswordFieldRaw name={name} focused={focused} {...other} />
|
||||
</FieldTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormCheckboxFieldRaw({ name = undefined, defaultValue = undefined, ...other }) {
|
||||
const { values, setFieldValue } = useForm();
|
||||
const handleChange = event => {
|
||||
setFieldValue(name, event.target.checked);
|
||||
};
|
||||
let isChecked = values[name];
|
||||
if (isChecked == null) isChecked = defaultValue;
|
||||
return <CheckboxField name={name} checked={!!isChecked} onChange={handleChange} {...other} />;
|
||||
// return <Field {...other} as={CheckboxField} />;
|
||||
}
|
||||
|
||||
export function FormCheckboxField({ label, templateProps = undefined, ...other }) {
|
||||
const { values, setFieldValue } = useForm();
|
||||
const FieldTemplate = useFormFieldTemplate();
|
||||
return (
|
||||
<FieldTemplate
|
||||
label={label}
|
||||
type="checkbox"
|
||||
labelProps={
|
||||
other.disabled ? { disabled: true } : { onClick: () => setFieldValue(other.name, !values[other.name]) }
|
||||
}
|
||||
{...templateProps}
|
||||
>
|
||||
<FormCheckboxFieldRaw {...other} />
|
||||
</FieldTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormSelectFieldRaw({ children, name, ...other }) {
|
||||
const { values, setFieldValue } = useForm();
|
||||
const handleChange = event => {
|
||||
setFieldValue(name, event.target.value);
|
||||
};
|
||||
return (
|
||||
<SelectField {...other} value={values[name]} onChange={handleChange}>
|
||||
{children}
|
||||
</SelectField>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormSelectField({ label, name, children = null, templateProps = undefined, ...other }) {
|
||||
const FieldTemplate = useFormFieldTemplate();
|
||||
return (
|
||||
<FieldTemplate label={label} type="select" {...templateProps}>
|
||||
<FormSelectFieldRaw name={name} {...other}>
|
||||
{children}
|
||||
</FormSelectFieldRaw>
|
||||
</FieldTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormSubmit({ onClick, value, ...other }) {
|
||||
const { values, setSubmitAction } = useForm();
|
||||
React.useEffect(() => {
|
||||
setSubmitAction({ action: onClick });
|
||||
}, [onClick]);
|
||||
return <FormStyledButton type="submit" value={value} onClick={() => onClick(values)} {...other} />;
|
||||
}
|
||||
|
||||
export function FormButton({ onClick, value, ...other }) {
|
||||
const { values } = useForm();
|
||||
return <FormStyledButton type="button" value={value} onClick={() => onClick(values)} {...other} />;
|
||||
}
|
||||
|
||||
export function FormRadioGroupItem({ name, text, value }) {
|
||||
const { setFieldValue, values } = useForm();
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
id={`${name}_${value}`}
|
||||
defaultChecked={values[name] == value}
|
||||
onClick={() => setFieldValue(name, value)}
|
||||
/>
|
||||
<label htmlFor={`multiple_values_option_${value}`}>{text}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormReactSelect({ options, name, isMulti = false, Component = Select, ...other }) {
|
||||
const { setFieldValue, values } = useForm();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Component
|
||||
theme={t => ({
|
||||
...t,
|
||||
colors: {
|
||||
...t.colors,
|
||||
neutral0: theme.input_background,
|
||||
neutral10: theme.input_background2,
|
||||
neutral20: theme.input_background3,
|
||||
neutral30: theme.input_background4,
|
||||
neutral40: theme.input_font3,
|
||||
neutral50: theme.input_font3,
|
||||
neutral60: theme.input_font2,
|
||||
neutral70: theme.input_font2,
|
||||
neutral80: theme.input_font2,
|
||||
neutral90: theme.input_font1,
|
||||
primary: theme.input_background_blue[5],
|
||||
primary75: theme.input_background_blue[3],
|
||||
primary50: theme.input_background_blue[2],
|
||||
primary25: theme.input_background_blue[0],
|
||||
danger: theme.input_background_red[5],
|
||||
dangerLight: theme.input_background_red[1],
|
||||
},
|
||||
})}
|
||||
options={options}
|
||||
value={
|
||||
isMulti
|
||||
? options.filter(x => values[name] && values[name].includes(x.value))
|
||||
: options.find(x => x.value == values[name])
|
||||
}
|
||||
onChange={item => setFieldValue(name, isMulti ? getAsArray(item).map(x => x.value) : item ? item.value : null)}
|
||||
menuPortalTarget={document.body}
|
||||
isMulti={isMulti}
|
||||
closeMenuOnSelect={!isMulti}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormConnectionSelect({ name }) {
|
||||
const connections = useConnectionList();
|
||||
const connectionOptions = React.useMemo(
|
||||
() =>
|
||||
(connections || []).map(conn => ({
|
||||
value: conn._id,
|
||||
label: conn.displayName || conn.server,
|
||||
})),
|
||||
[connections]
|
||||
);
|
||||
|
||||
if (connectionOptions.length == 0) return <div>Not available</div>;
|
||||
return <FormReactSelect options={connectionOptions} name={name} />;
|
||||
}
|
||||
|
||||
export function FormDatabaseSelect({ conidName, name }) {
|
||||
const { values } = useForm();
|
||||
const databases = useDatabaseList({ conid: values[conidName] });
|
||||
const databaseOptions = React.useMemo(
|
||||
() =>
|
||||
(databases || []).map(db => ({
|
||||
value: db.name,
|
||||
label: db.name,
|
||||
})),
|
||||
[databases]
|
||||
);
|
||||
|
||||
if (databaseOptions.length == 0) return <div>Not available</div>;
|
||||
return <FormReactSelect options={databaseOptions} name={name} />;
|
||||
}
|
||||
|
||||
export function FormSchemaSelect({ conidName, databaseName, name }) {
|
||||
const { values } = useForm();
|
||||
const dbinfo = useDatabaseInfo({ conid: values[conidName], database: values[databaseName] });
|
||||
const schemaOptions = React.useMemo(
|
||||
() =>
|
||||
((dbinfo && dbinfo.schemas) || []).map(schema => ({
|
||||
value: schema.schemaName,
|
||||
label: schema.schemaName,
|
||||
})),
|
||||
[dbinfo]
|
||||
);
|
||||
|
||||
if (schemaOptions.length == 0) return <div>Not available</div>;
|
||||
return <FormReactSelect options={schemaOptions} name={name} />;
|
||||
}
|
||||
|
||||
export function FormTablesSelect({ conidName, databaseName, schemaName, name }) {
|
||||
const { values } = useForm();
|
||||
const dbinfo = useDatabaseInfo({ conid: values[conidName], database: values[databaseName] });
|
||||
const tablesOptions = React.useMemo(
|
||||
() =>
|
||||
[...((dbinfo && dbinfo.tables) || []), ...((dbinfo && dbinfo.views) || [])]
|
||||
.filter(x => !values[schemaName] || x.schemaName == values[schemaName])
|
||||
.map(x => ({
|
||||
value: x.pureName,
|
||||
label: x.pureName,
|
||||
})),
|
||||
[dbinfo, values[schemaName]]
|
||||
);
|
||||
|
||||
if (tablesOptions.length == 0) return <div>Not available</div>;
|
||||
return <FormReactSelect options={tablesOptions} name={name} isMulti />;
|
||||
}
|
||||
|
||||
export function FormArchiveFilesSelect({ folderName, name }) {
|
||||
// const { values } = useFormikContext();
|
||||
const files = useArchiveFiles({ folder: folderName });
|
||||
const filesOptions = React.useMemo(
|
||||
() =>
|
||||
(files || []).map(x => ({
|
||||
value: x.name,
|
||||
label: x.name,
|
||||
})),
|
||||
[files]
|
||||
);
|
||||
|
||||
if (filesOptions.length == 0) return <div>Not available</div>;
|
||||
return <FormReactSelect options={filesOptions} name={name} isMulti />;
|
||||
}
|
||||
|
||||
export function FormArchiveFolderSelect({ name, additionalFolders = [], ...other }) {
|
||||
const { setFieldValue } = useForm();
|
||||
const folders = useArchiveFolders();
|
||||
const folderOptions = React.useMemo(
|
||||
() => [
|
||||
...(folders || []).map(folder => ({
|
||||
value: folder.name,
|
||||
label: folder.name,
|
||||
})),
|
||||
...additionalFolders
|
||||
.filter(x => !(folders || []).find(y => y.name == x))
|
||||
.map(folder => ({
|
||||
value: folder,
|
||||
label: folder,
|
||||
})),
|
||||
],
|
||||
[folders]
|
||||
);
|
||||
|
||||
const handleCreateOption = folder => {
|
||||
axios.post('archive/create-folder', { folder });
|
||||
setFieldValue(name, folder);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormReactSelect
|
||||
{...other}
|
||||
options={folderOptions}
|
||||
name={name}
|
||||
Component={Creatable}
|
||||
onCreateOption={handleCreateOption}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormElectronFileSelectorRaw({ name, ...other }) {
|
||||
const { values, setFieldValue } = useForm();
|
||||
const handleBrowse = () => {
|
||||
const electron = getElectron();
|
||||
if (!electron) return;
|
||||
const filePaths = electron.remote.dialog.showOpenDialogSync(electron.remote.getCurrentWindow(), {
|
||||
defaultPath: values[name],
|
||||
properties: ['showHiddenFiles'],
|
||||
});
|
||||
const filePath = filePaths && filePaths[0];
|
||||
if (filePath) setFieldValue(name, filePath);
|
||||
};
|
||||
return (
|
||||
<FlexContainer>
|
||||
<TextField value={values[name]} onClick={handleBrowse} {...other} readOnly />
|
||||
<InlineButton onClick={handleBrowse} disabled={other.disabled}>Browse</InlineButton>
|
||||
</FlexContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormElectronFileSelector({ label, name, templateProps = undefined, ...other }) {
|
||||
const FieldTemplate = useFormFieldTemplate();
|
||||
return (
|
||||
<FieldTemplate label={label} type="select" {...templateProps}>
|
||||
<FormElectronFileSelectorRaw name={name} {...other} />
|
||||
</FieldTemplate>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export default function fullDisplayName({ schemaName, pureName }) {
|
||||
if (schemaName) return `${schemaName}.${pureName}`;
|
||||
return pureName;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function getAsArray(obj) {
|
||||
if (_.isArray(obj)) return obj;
|
||||
if (obj != null) return [obj];
|
||||
return [];
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function getElectron() {
|
||||
if (window.require) {
|
||||
const electron = window.require('electron');
|
||||
return electron;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import useStorage from './useStorage';
|
||||
import { useConnectionInfo, useConfig, getConnectionInfo } from './metadataLoaders';
|
||||
import usePrevious from './usePrevious';
|
||||
import useNewQuery from '../query/useNewQuery';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import useExtensions from './useExtensions';
|
||||
|
||||
function createGlobalState(defaultValue) {
|
||||
const Context = React.createContext(null);
|
||||
|
||||
function Provider({ children }) {
|
||||
const [currentvalue, setCurrentValue] = React.useState(defaultValue);
|
||||
return <Context.Provider value={[currentvalue, setCurrentValue]}>{children}</Context.Provider>;
|
||||
}
|
||||
|
||||
function useValue() {
|
||||
return React.useContext(Context)[0];
|
||||
}
|
||||
|
||||
function useSetValue() {
|
||||
return React.useContext(Context)[1];
|
||||
}
|
||||
|
||||
return [Provider, useValue, useSetValue];
|
||||
}
|
||||
|
||||
function createStorageState(storageKey, defaultValue) {
|
||||
const Context = React.createContext(null);
|
||||
|
||||
function Provider({ children }) {
|
||||
const [currentvalue, setCurrentValue] = useStorage(storageKey, localStorage, defaultValue);
|
||||
return <Context.Provider value={[currentvalue, setCurrentValue]}>{children}</Context.Provider>;
|
||||
}
|
||||
|
||||
function useValue() {
|
||||
return React.useContext(Context)[0];
|
||||
}
|
||||
|
||||
function useSetValue() {
|
||||
return React.useContext(Context)[1];
|
||||
}
|
||||
|
||||
return [Provider, useValue, useSetValue];
|
||||
}
|
||||
|
||||
const [CurrentWidgetProvider, useCurrentWidget, useSetCurrentWidget] = createGlobalState('database');
|
||||
export { CurrentWidgetProvider, useCurrentWidget, useSetCurrentWidget };
|
||||
|
||||
const [CurrentDatabaseProvider, useCurrentDatabaseCore, useSetCurrentDatabaseCore] = createGlobalState(null);
|
||||
|
||||
function useSetCurrentDatabase() {
|
||||
const setDb = useSetCurrentDatabaseCore();
|
||||
const db = useCurrentDatabaseCore();
|
||||
return value => {
|
||||
if (_.get(db, 'name') !== _.get(value, 'name') || _.get(db, 'connection._id') != _.get(value, 'connection._id')) {
|
||||
setDb(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function useCurrentDatabase() {
|
||||
const config = useConfig();
|
||||
const db = useCurrentDatabaseCore();
|
||||
|
||||
const [connection, setConnection] = React.useState(null);
|
||||
const loadSingleConnection = async () => {
|
||||
if (config && config.singleDatabase) {
|
||||
const conn = await getConnectionInfo({ conid: config.singleDatabase.conid });
|
||||
setConnection(conn);
|
||||
}
|
||||
};
|
||||
React.useEffect(() => {
|
||||
loadSingleConnection();
|
||||
}, [config]);
|
||||
|
||||
if (config && config.singleDatabase) {
|
||||
if (connection) {
|
||||
return {
|
||||
connection,
|
||||
name: config.singleDatabase.database,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
export { CurrentDatabaseProvider, useCurrentDatabase, useSetCurrentDatabase };
|
||||
|
||||
const [OpenedTabsProvider, useOpenedTabs, useSetOpenedTabs] = createStorageState('openedTabs', []);
|
||||
export { OpenedTabsProvider, useOpenedTabs, useSetOpenedTabs };
|
||||
|
||||
export function useUpdateDatabaseForTab(tabVisible, conid, database) {
|
||||
const connection = useConnectionInfo({ conid });
|
||||
const setDb = useSetCurrentDatabase();
|
||||
const currentDb = useCurrentDatabase();
|
||||
const previousTabVisible = usePrevious(!!(tabVisible && connection));
|
||||
|
||||
if (!conid || !database) return;
|
||||
|
||||
if (!previousTabVisible && tabVisible && connection) {
|
||||
if (currentDb && currentDb.connection && currentDb.connection._id == conid && currentDb.name == database) {
|
||||
return;
|
||||
}
|
||||
setDb({
|
||||
name: database,
|
||||
connection,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// export function useAppObjectParams() {
|
||||
// const setOpenedTabs = useSetOpenedTabs();
|
||||
// const currentDatabase = useCurrentDatabase();
|
||||
// const newQuery = useNewQuery();
|
||||
// const openedTabs = useOpenedTabs();
|
||||
// const openedConnections = useOpenedConnections();
|
||||
// const setOpenedConnections = useSetOpenedConnections();
|
||||
// const currentArchive = useCurrentArchive();
|
||||
// const showModal = useShowModal();
|
||||
// const config = useConfig();
|
||||
// const extensions = useExtensions();
|
||||
|
||||
// return {
|
||||
// setOpenedTabs,
|
||||
// currentDatabase,
|
||||
// currentArchive,
|
||||
// newQuery,
|
||||
// openedTabs,
|
||||
// openedConnections,
|
||||
// setOpenedConnections,
|
||||
// config,
|
||||
// showModal,
|
||||
// extensions,
|
||||
// };
|
||||
// }
|
||||
|
||||
const [OpenedConnectionsProvider, useOpenedConnections, useSetOpenedConnections] = createGlobalState([]);
|
||||
|
||||
export { OpenedConnectionsProvider, useOpenedConnections, useSetOpenedConnections };
|
||||
|
||||
const [LeftPanelWidthProvider, useLeftPanelWidth, useSetLeftPanelWidth] = createGlobalState(300);
|
||||
|
||||
export { LeftPanelWidthProvider, useLeftPanelWidth, useSetLeftPanelWidth };
|
||||
|
||||
const [CurrentArchiveProvider, useCurrentArchive, useSetCurrentArchive] = createGlobalState('default');
|
||||
|
||||
export { CurrentArchiveProvider, useCurrentArchive, useSetCurrentArchive };
|
||||
|
||||
const [CurrentThemeProvider, useCurrentTheme, useSetCurrentTheme] = createStorageState('selectedTheme', 'light');
|
||||
|
||||
export { CurrentThemeProvider, useCurrentTheme, useSetCurrentTheme };
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export function TextField({ editorRef = undefined, ...other }) {
|
||||
return <input type="text" {...other} ref={editorRef}></input>;
|
||||
}
|
||||
|
||||
export function SelectField({ children = null, options = [], ...other }) {
|
||||
return (
|
||||
<select {...other}>
|
||||
{children}
|
||||
{options.map(x => (
|
||||
<option value={x.value} key={x.value}>
|
||||
{x.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
export function CheckboxField({ editorRef = undefined, ...other }) {
|
||||
return <input type="checkbox" {...other} ref={editorRef}></input>;
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
export default {
|
||||
backspace: 8,
|
||||
tab: 9,
|
||||
enter: 13,
|
||||
shift: 16,
|
||||
ctrl: 17,
|
||||
alt: 18,
|
||||
pauseBreak: 19,
|
||||
capsLock: 20,
|
||||
escape: 27,
|
||||
pageUp: 33,
|
||||
pageDown: 34,
|
||||
end: 35,
|
||||
home: 36,
|
||||
leftArrow: 37,
|
||||
upArrow: 38,
|
||||
rightArrow: 39,
|
||||
downArrow: 40,
|
||||
insert: 45,
|
||||
delete: 46,
|
||||
n0: 48,
|
||||
n1: 49,
|
||||
n2: 50,
|
||||
n3: 51,
|
||||
n4: 52,
|
||||
n5: 53,
|
||||
n6: 54,
|
||||
n7: 55,
|
||||
n8: 56,
|
||||
n9: 57,
|
||||
a: 65,
|
||||
b: 66,
|
||||
c: 67,
|
||||
d: 68,
|
||||
e: 69,
|
||||
f: 70,
|
||||
g: 71,
|
||||
h: 72,
|
||||
i: 73,
|
||||
j: 74,
|
||||
k: 75,
|
||||
l: 76,
|
||||
m: 77,
|
||||
n: 78,
|
||||
o: 79,
|
||||
p: 80,
|
||||
q: 81,
|
||||
r: 82,
|
||||
s: 83,
|
||||
t: 84,
|
||||
u: 85,
|
||||
v: 86,
|
||||
w: 87,
|
||||
x: 88,
|
||||
y: 89,
|
||||
z: 90,
|
||||
leftWindowKey: 91,
|
||||
rightWindowKey: 92,
|
||||
selectKey: 93,
|
||||
numPad0: 96,
|
||||
numPad1: 97,
|
||||
numPad2: 98,
|
||||
numPad3: 99,
|
||||
numPad4: 100,
|
||||
numPad5: 101,
|
||||
numPad6: 102,
|
||||
numPad7: 103,
|
||||
numPad8: 104,
|
||||
numPad9: 105,
|
||||
multiply: 106,
|
||||
add: 107,
|
||||
subtract: 109,
|
||||
decimalPoint: 110,
|
||||
divide: 111,
|
||||
f1: 112,
|
||||
f2: 113,
|
||||
f3: 114,
|
||||
f4: 115,
|
||||
f5: 116,
|
||||
f6: 117,
|
||||
f7: 118,
|
||||
f8: 119,
|
||||
f9: 120,
|
||||
f10: 121,
|
||||
f12: 123,
|
||||
numLock: 144,
|
||||
scrollLock: 145,
|
||||
semiColon: 186,
|
||||
equalSign: 187,
|
||||
comma: 188,
|
||||
dash: 189,
|
||||
period: 190,
|
||||
forwardSlash: 191,
|
||||
graveAccent: 192,
|
||||
openBracket: 219,
|
||||
backSlash: 220,
|
||||
closeBracket: 221,
|
||||
singleQuote: 222,
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Grid = styled.div``;
|
||||
|
||||
export const Row = styled.div``;
|
||||
|
||||
export const Col = styled.div``;
|
||||
@@ -1,35 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import localforage from 'localforage';
|
||||
|
||||
export default async function localStorageGarbageCollector() {
|
||||
const openedTabsJson = localStorage.getItem('openedTabs');
|
||||
let openedTabs = openedTabsJson ? JSON.parse(openedTabsJson) : [];
|
||||
|
||||
const closeLimit = moment().add(-7, 'day').valueOf();
|
||||
|
||||
openedTabs = openedTabs.filter(x => !x.closedTime || x.closedTime > closeLimit);
|
||||
localStorage.setItem('openedTabs', JSON.stringify(openedTabs));
|
||||
|
||||
const toRemove = [];
|
||||
for (const key in localStorage) {
|
||||
if (!key.startsWith('tabdata_')) continue;
|
||||
if (openedTabs.find(x => key.endsWith('_' + x.tabid))) continue;
|
||||
toRemove.push(key);
|
||||
}
|
||||
|
||||
for (const key of toRemove) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
const keysForage = await localforage.keys();
|
||||
const toRemoveForage = [];
|
||||
for (const key in keysForage) {
|
||||
if (!key.startsWith('tabdata_')) continue;
|
||||
if (openedTabs.find(x => key.endsWith('_' + x.tabid))) continue;
|
||||
toRemoveForage.push(key);
|
||||
}
|
||||
|
||||
for (const key of toRemoveForage) {
|
||||
await localforage.removeItem(key);
|
||||
}
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
import useFetch from './useFetch';
|
||||
import axios from './axios';
|
||||
import _ from 'lodash';
|
||||
import { cacheGet, cacheSet, getCachedPromise } from './cache';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
|
||||
const databaseInfoLoader = ({ conid, database }) => ({
|
||||
url: 'database-connections/structure',
|
||||
params: { conid, database },
|
||||
reloadTrigger: `database-structure-changed-${conid}-${database}`,
|
||||
transform: db => {
|
||||
const allForeignKeys = _.flatten(db.tables.map(x => x.foreignKeys));
|
||||
return {
|
||||
...db,
|
||||
tables: db.tables.map(table => ({
|
||||
...table,
|
||||
dependencies: allForeignKeys.filter(
|
||||
x => x.refSchemaName == table.schemaName && x.refTableName == table.pureName
|
||||
),
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// const tableInfoLoader = ({ conid, database, schemaName, pureName }) => ({
|
||||
// url: 'metadata/table-info',
|
||||
// params: { conid, database, schemaName, pureName },
|
||||
// reloadTrigger: `database-structure-changed-${conid}-${database}`,
|
||||
// });
|
||||
|
||||
// const sqlObjectInfoLoader = ({ objectTypeField, conid, database, schemaName, pureName }) => ({
|
||||
// url: 'metadata/sql-object-info',
|
||||
// params: { objectTypeField, conid, database, schemaName, pureName },
|
||||
// reloadTrigger: `database-structure-changed-${conid}-${database}`,
|
||||
// });
|
||||
|
||||
const connectionInfoLoader = ({ conid }) => ({
|
||||
url: 'connections/get',
|
||||
params: { conid },
|
||||
reloadTrigger: 'connection-list-changed',
|
||||
});
|
||||
|
||||
const configLoader = () => ({
|
||||
url: 'config/get',
|
||||
params: {},
|
||||
reloadTrigger: 'config-changed',
|
||||
});
|
||||
|
||||
const platformInfoLoader = () => ({
|
||||
url: 'config/platform-info',
|
||||
params: {},
|
||||
reloadTrigger: 'platform-info-changed',
|
||||
});
|
||||
|
||||
const favoritesLoader = () => ({
|
||||
url: 'files/favorites',
|
||||
params: {},
|
||||
reloadTrigger: 'files-changed-favorites',
|
||||
});
|
||||
|
||||
// const sqlObjectListLoader = ({ conid, database }) => ({
|
||||
// url: 'metadata/list-objects',
|
||||
// params: { conid, database },
|
||||
// reloadTrigger: `database-structure-changed-${conid}-${database}`,
|
||||
// });
|
||||
|
||||
const databaseStatusLoader = ({ conid, database }) => ({
|
||||
url: 'database-connections/status',
|
||||
params: { conid, database },
|
||||
reloadTrigger: `database-status-changed-${conid}-${database}`,
|
||||
});
|
||||
|
||||
const databaseListLoader = ({ conid }) => ({
|
||||
url: 'server-connections/list-databases',
|
||||
params: { 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: {},
|
||||
reloadTrigger: `server-status-changed`,
|
||||
});
|
||||
|
||||
const connectionListLoader = () => ({
|
||||
url: 'connections/list',
|
||||
params: {},
|
||||
reloadTrigger: `connection-list-changed`,
|
||||
});
|
||||
|
||||
const installedPluginsLoader = () => ({
|
||||
url: 'plugins/installed',
|
||||
params: {},
|
||||
reloadTrigger: `installed-plugins-changed`,
|
||||
});
|
||||
|
||||
const filesLoader = ({ folder }) => ({
|
||||
url: 'files/list',
|
||||
params: { folder },
|
||||
reloadTrigger: `files-changed-${folder}`,
|
||||
});
|
||||
const allFilesLoader = () => ({
|
||||
url: 'files/list-all',
|
||||
params: {},
|
||||
reloadTrigger: `all-files-changed`,
|
||||
});
|
||||
|
||||
async function getCore(loader, args) {
|
||||
const { url, params, reloadTrigger, transform } = loader(args);
|
||||
const key = stableStringify({ url, ...params });
|
||||
|
||||
async function doLoad() {
|
||||
const resp = await axios.request({
|
||||
method: 'get',
|
||||
url,
|
||||
params,
|
||||
});
|
||||
return (transform || (x => x))(resp.data);
|
||||
}
|
||||
|
||||
const fromCache = cacheGet(key);
|
||||
if (fromCache) return fromCache;
|
||||
const res = await getCachedPromise(key, doLoad);
|
||||
|
||||
cacheSet(key, res, reloadTrigger);
|
||||
return res;
|
||||
}
|
||||
|
||||
function useCore(loader, args) {
|
||||
const { url, params, reloadTrigger, transform } = loader(args);
|
||||
const cacheKey = stableStringify({ url, ...params });
|
||||
|
||||
const res = useFetch({
|
||||
url,
|
||||
params,
|
||||
reloadTrigger,
|
||||
cacheKey,
|
||||
transform,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/** @returns {Promise<import('dbgate-types').DatabaseInfo>} */
|
||||
export function getDatabaseInfo(args) {
|
||||
return getCore(databaseInfoLoader, args);
|
||||
}
|
||||
|
||||
/** @returns {import('dbgate-types').DatabaseInfo} */
|
||||
export function useDatabaseInfo(args) {
|
||||
return useCore(databaseInfoLoader, args);
|
||||
}
|
||||
|
||||
export async function getDbCore(args, objectTypeField = undefined) {
|
||||
const db = await getDatabaseInfo(args);
|
||||
if (!db) return null;
|
||||
return db[objectTypeField || args.objectTypeField].find(
|
||||
x => x.pureName == args.pureName && x.schemaName == args.schemaName
|
||||
);
|
||||
}
|
||||
|
||||
export function useDbCore(args, objectTypeField = undefined) {
|
||||
const db = useDatabaseInfo(args);
|
||||
if (!db) return null;
|
||||
return db[objectTypeField || args.objectTypeField].find(
|
||||
x => x.pureName == args.pureName && x.schemaName == args.schemaName
|
||||
);
|
||||
}
|
||||
|
||||
/** @returns {Promise<import('dbgate-types').TableInfo>} */
|
||||
export function getTableInfo(args) {
|
||||
return getDbCore(args, 'tables');
|
||||
}
|
||||
|
||||
/** @returns {import('dbgate-types').TableInfo} */
|
||||
export function useTableInfo(args) {
|
||||
return useDbCore(args, 'tables');
|
||||
}
|
||||
|
||||
/** @returns {Promise<import('dbgate-types').ViewInfo>} */
|
||||
export function getViewInfo(args) {
|
||||
return getDbCore(args, 'views');
|
||||
}
|
||||
|
||||
/** @returns {import('dbgate-types').ViewInfo} */
|
||||
export function useViewInfo(args) {
|
||||
return useDbCore(args, 'views');
|
||||
}
|
||||
|
||||
export function getSqlObjectInfo(args) {
|
||||
return getDbCore(args);
|
||||
}
|
||||
|
||||
export function useSqlObjectInfo(args) {
|
||||
return useDbCore(args);
|
||||
}
|
||||
|
||||
/** @returns {Promise<import('dbgate-types').StoredConnection>} */
|
||||
export function getConnectionInfo(args) {
|
||||
return getCore(connectionInfoLoader, args);
|
||||
}
|
||||
|
||||
/** @returns {import('dbgate-types').StoredConnection} */
|
||||
export function useConnectionInfo(args) {
|
||||
return useCore(connectionInfoLoader, args);
|
||||
}
|
||||
|
||||
// export function getSqlObjectList(args) {
|
||||
// return getCore(sqlObjectListLoader, args);
|
||||
// }
|
||||
// export function useSqlObjectList(args) {
|
||||
// return useCore(sqlObjectListLoader, args);
|
||||
// }
|
||||
|
||||
export function getDatabaseStatus(args) {
|
||||
return getCore(databaseStatusLoader, args);
|
||||
}
|
||||
export function useDatabaseStatus(args) {
|
||||
return useCore(databaseStatusLoader, args);
|
||||
}
|
||||
|
||||
export function getDatabaseList(args) {
|
||||
return getCore(databaseListLoader, args);
|
||||
}
|
||||
export function useDatabaseList(args) {
|
||||
return useCore(databaseListLoader, args);
|
||||
}
|
||||
|
||||
export function getServerStatus() {
|
||||
return getCore(serverStatusLoader, {});
|
||||
}
|
||||
export function useServerStatus() {
|
||||
return useCore(serverStatusLoader, {});
|
||||
}
|
||||
|
||||
export function getConnectionList() {
|
||||
return getCore(connectionListLoader, {});
|
||||
}
|
||||
export function useConnectionList() {
|
||||
return useCore(connectionListLoader, {});
|
||||
}
|
||||
|
||||
export function getConfig() {
|
||||
return getCore(configLoader, {}) || {};
|
||||
}
|
||||
export function useConfig() {
|
||||
return useCore(configLoader, {}) || {};
|
||||
}
|
||||
|
||||
export function getPlatformInfo() {
|
||||
return getCore(platformInfoLoader, {}) || {};
|
||||
}
|
||||
export function usePlatformInfo() {
|
||||
return useCore(platformInfoLoader, {}) || {};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function getInstalledPlugins(args) {
|
||||
return getCore(installedPluginsLoader, args) || [];
|
||||
}
|
||||
export function useInstalledPlugins(args) {
|
||||
return useCore(installedPluginsLoader, args) || [];
|
||||
}
|
||||
|
||||
export function getFiles(args) {
|
||||
return getCore(filesLoader, args);
|
||||
}
|
||||
export function useFiles(args) {
|
||||
return useCore(filesLoader, args);
|
||||
}
|
||||
|
||||
export function getAllFiles(args) {
|
||||
return getCore(allFilesLoader, args);
|
||||
}
|
||||
export function useAllFiles(args) {
|
||||
return useCore(allFilesLoader, args);
|
||||
}
|
||||
|
||||
export function getFavorites(args) {
|
||||
return getCore(favoritesLoader, args);
|
||||
}
|
||||
export function useFavorites(args) {
|
||||
return useCore(favoritesLoader, args);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
export default function resolveApi() {
|
||||
if (window.require) {
|
||||
const electron = window.require('electron');
|
||||
|
||||
if (electron) {
|
||||
const port = electron.remote.getGlobal('port');
|
||||
if (port) {
|
||||
return `http://localhost:${port}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
const apiUrl = process.env.REACT_APP_API_URL;
|
||||
if (apiUrl) {
|
||||
if (apiUrl == 'ORIGIN') return window.location.origin;
|
||||
return apiUrl;
|
||||
}
|
||||
|
||||
return 'http://localhost:3000';
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
// import { useState, useCallback, useLayoutEffect } from 'react';
|
||||
|
||||
// function getDimensionObject(node) {
|
||||
// const rect = node.getBoundingClientRect();
|
||||
|
||||
// return {
|
||||
// width: rect.width,
|
||||
// height: rect.height,
|
||||
// top: 'x' in rect ? rect.x : rect.top,
|
||||
// left: 'y' in rect ? rect.y : rect.left,
|
||||
// x: 'x' in rect ? rect.x : rect.left,
|
||||
// y: 'y' in rect ? rect.y : rect.top,
|
||||
// right: rect.right,
|
||||
// bottom: rect.bottom,
|
||||
// };
|
||||
// }
|
||||
|
||||
// function useDimensions({ liveMeasure = true } = {}) {
|
||||
// const [dimensions, setDimensions] = useState({});
|
||||
// const [node, setNode] = useState(null);
|
||||
|
||||
// const ref = useCallback(node => {
|
||||
// setNode(node);
|
||||
// }, []);
|
||||
|
||||
// useLayoutEffect(() => {
|
||||
// if (node) {
|
||||
// const measure = () => window.requestAnimationFrame(() => setDimensions(getDimensionObject(node)));
|
||||
// measure();
|
||||
|
||||
// if (liveMeasure) {
|
||||
// window.addEventListener('resize', measure);
|
||||
// window.addEventListener('scroll', measure);
|
||||
|
||||
// return () => {
|
||||
// window.removeEventListener('resize', measure);
|
||||
// window.removeEventListener('scroll', measure);
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// }, [node]);
|
||||
|
||||
// return [ref, dimensions, node];
|
||||
// }
|
||||
|
||||
// export default useDimensions;
|
||||
|
||||
import { useLayoutEffect, useState, useCallback } from 'react';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
// Export hook
|
||||
export default function useDimensions(dependencies = []) {
|
||||
const [node, setNode] = useState(null);
|
||||
const ref = useCallback(newNode => {
|
||||
setNode(newNode);
|
||||
}, []);
|
||||
|
||||
// Keep track of measurements
|
||||
const [dimensions, setDimensions] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
// Define measure function
|
||||
const measure = useCallback(innerNode => {
|
||||
const rect = innerNode.getBoundingClientRect();
|
||||
setDimensions({
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set initial measurements
|
||||
measure(node);
|
||||
|
||||
// Observe resizing of element
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
measure(node);
|
||||
});
|
||||
|
||||
resizeObserver.observe(node);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [node, measure, ...dependencies]);
|
||||
|
||||
return [ref, dimensions, node];
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function useDocumentClick(callback) {
|
||||
const mouseUpListener = React.useCallback(e => {
|
||||
callback();
|
||||
document.removeEventListener('mouseup', mouseUpListener, true);
|
||||
}, []);
|
||||
const mouseDownListener = React.useCallback(e => {
|
||||
document.addEventListener('mouseup', mouseUpListener, true);
|
||||
document.removeEventListener('mousedown', mouseDownListener, true);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('mousedown', mouseDownListener, true);
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', mouseUpListener, true);
|
||||
document.removeEventListener('mousedown', mouseDownListener, true);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import localforage from 'localforage';
|
||||
import { changeTab } from './common';
|
||||
import { useSetOpenedTabs } from './globalState';
|
||||
|
||||
function getParsedLocalStorage(key) {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value != null) {
|
||||
try {
|
||||
const res = JSON.parse(value);
|
||||
return res;
|
||||
} catch (e) {
|
||||
// console.log('FAILED LOAD FROM STORAGE', e);
|
||||
// console.log('VALUE', value);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = null }) {
|
||||
const localStorageKey = `tabdata_editor_${tabid}`;
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const changeCounterRef = React.useRef(0);
|
||||
const savedCounterRef = React.useRef(0);
|
||||
const [errorMessage, setErrorMessage] = React.useState(null);
|
||||
|
||||
const [value, setValue] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const initialDataRef = React.useRef(null);
|
||||
|
||||
const valueRef = React.useRef(null);
|
||||
|
||||
const initialLoad = async () => {
|
||||
if (loadFromArgs) {
|
||||
try {
|
||||
const init = await loadFromArgs();
|
||||
changeTab(tabid, setOpenedTabs, tab => ({
|
||||
...tab,
|
||||
props: _.omit(tab.props, ['initialArgs']),
|
||||
}));
|
||||
setValue(init);
|
||||
valueRef.current = init;
|
||||
initialDataRef.current = init;
|
||||
// mark as not saved
|
||||
changeCounterRef.current += 1;
|
||||
} catch (err) {
|
||||
const message = (err && err.response && err.response.data && err.response.data.error) || 'Loading failed';
|
||||
setErrorMessage(message);
|
||||
console.error(err.response);
|
||||
}
|
||||
} else {
|
||||
const initFallback = getParsedLocalStorage(localStorageKey);
|
||||
if (initFallback != null) {
|
||||
setValue(initFallback);
|
||||
valueRef.current = initFallback;
|
||||
// move to local forage
|
||||
await localforage.setItem(localStorageKey, initFallback);
|
||||
localStorage.removeItem(localStorageKey);
|
||||
initialDataRef.current = initFallback;
|
||||
} else {
|
||||
const init = await localforage.getItem(localStorageKey);
|
||||
if (init) {
|
||||
setValue(init);
|
||||
valueRef.current = init;
|
||||
initialDataRef.current = init;
|
||||
}
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
initialLoad();
|
||||
}, [reloadToken]);
|
||||
|
||||
const saveToStorage = React.useCallback(async () => {
|
||||
if (valueRef.current == null) return;
|
||||
try {
|
||||
await localforage.setItem(localStorageKey, valueRef.current);
|
||||
localStorage.removeItem(localStorageKey);
|
||||
savedCounterRef.current = changeCounterRef.current;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, [localStorageKey, valueRef]);
|
||||
|
||||
const saveToStorageSync = React.useCallback(() => {
|
||||
if (valueRef.current == null) return;
|
||||
if (savedCounterRef.current == changeCounterRef.current) return; // all saved
|
||||
// on window unload must be synchronous actions, save to local storage instead
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(valueRef.current));
|
||||
}, [localStorageKey, valueRef]);
|
||||
|
||||
const saveToStorageDebounced = React.useMemo(() => _.debounce(saveToStorage, 5000), [saveToStorage]);
|
||||
|
||||
const handleChange = newValue => {
|
||||
if (_.isFunction(newValue)) {
|
||||
valueRef.current = newValue(valueRef.current);
|
||||
} else {
|
||||
if (newValue != null) valueRef.current = newValue;
|
||||
}
|
||||
setValue(valueRef.current);
|
||||
changeCounterRef.current += 1;
|
||||
saveToStorageDebounced();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('beforeunload', saveToStorageSync);
|
||||
return () => {
|
||||
saveToStorage();
|
||||
window.removeEventListener('beforeunload', saveToStorageSync);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
editorData: value,
|
||||
setEditorData: handleChange,
|
||||
isLoading,
|
||||
initialData: initialDataRef.current,
|
||||
errorMessage,
|
||||
saveToStorage,
|
||||
saveToStorageSync,
|
||||
};
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { usePlugins } from '../plugins/PluginsProvider';
|
||||
import { buildFileFormats } from './fileformats';
|
||||
|
||||
const ExtensionsContext = React.createContext(buildExtensions([]));
|
||||
|
||||
export function ExtensionsProvider({ children }) {
|
||||
const plugins = usePlugins();
|
||||
const extensions = React.useMemo(() => buildExtensions(plugins), [plugins]);
|
||||
return <ExtensionsContext.Provider value={extensions}>{children}</ExtensionsContext.Provider>;
|
||||
}
|
||||
|
||||
function buildDrivers(plugins) {
|
||||
const res = [];
|
||||
for (const { content } of plugins) {
|
||||
if (content.driver) res.push(content.driver);
|
||||
if (content.drivers) res.push(...content.drivers);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function buildExtensions(plugins) {
|
||||
/** @type {import('dbgate-types').ExtensionsDirectory} */
|
||||
const extensions = {
|
||||
plugins,
|
||||
fileFormats: buildFileFormats(plugins),
|
||||
drivers: buildDrivers(plugins),
|
||||
};
|
||||
return extensions;
|
||||
}
|
||||
|
||||
/** @returns {import('dbgate-types').ExtensionsDirectory} */
|
||||
export default function useExtensions() {
|
||||
return React.useContext(ExtensionsContext);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import axios from './axios';
|
||||
import useSocket from './SocketProvider';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { getCachedPromise, cacheGet, cacheSet, cacheClean } from './cache';
|
||||
import getAsArray from './getAsArray';
|
||||
|
||||
export default function useFetch({
|
||||
url,
|
||||
data = undefined,
|
||||
params = undefined,
|
||||
defaultValue = undefined,
|
||||
reloadTrigger = undefined,
|
||||
cacheKey = undefined,
|
||||
transform = x => x,
|
||||
...config
|
||||
}) {
|
||||
const [value, setValue] = React.useState([defaultValue, []]);
|
||||
const [loadCounter, setLoadCounter] = React.useState(0);
|
||||
const socket = useSocket();
|
||||
|
||||
const handleReload = React.useCallback(() => {
|
||||
setLoadCounter(counter => counter + 1);
|
||||
}, []);
|
||||
|
||||
const indicators = [url, stableStringify(data), stableStringify(params), loadCounter];
|
||||
|
||||
async function loadValue(loadedIndicators) {
|
||||
async function doLoad() {
|
||||
const resp = await axios.request({
|
||||
method: 'get',
|
||||
params,
|
||||
url,
|
||||
data,
|
||||
...config,
|
||||
});
|
||||
return transform(resp.data);
|
||||
}
|
||||
|
||||
if (cacheKey) {
|
||||
const fromCache = cacheGet(cacheKey);
|
||||
if (fromCache) {
|
||||
setValue([fromCache, loadedIndicators]);
|
||||
} else {
|
||||
try {
|
||||
const res = await getCachedPromise(cacheKey, doLoad);
|
||||
cacheSet(cacheKey, res, reloadTrigger);
|
||||
setValue([res, loadedIndicators]);
|
||||
} catch (err) {
|
||||
console.error('Error when using cached promise', err);
|
||||
cacheClean(cacheKey);
|
||||
const res = await doLoad();
|
||||
cacheSet(cacheKey, res, reloadTrigger);
|
||||
setValue([res, loadedIndicators]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const res = await doLoad();
|
||||
setValue([res, loadedIndicators]);
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
loadValue(indicators);
|
||||
}, [...indicators]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (reloadTrigger && !socket) {
|
||||
console.error('Socket not available, reloadTrigger not planned');
|
||||
}
|
||||
if (reloadTrigger && socket) {
|
||||
for (const item of getAsArray(reloadTrigger)) {
|
||||
socket.on(item, handleReload);
|
||||
}
|
||||
return () => {
|
||||
for (const item of getAsArray(reloadTrigger)) {
|
||||
socket.off(item, handleReload);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [socket, reloadTrigger]);
|
||||
|
||||
const [returnValue, loadedIndicators] = value;
|
||||
if (_.isEqual(indicators, loadedIndicators)) return returnValue;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { createGridConfig } from 'dbgate-datalib';
|
||||
import React from 'react';
|
||||
|
||||
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, setConfig] = React.useState(loadGridConfigFunc(tabid));
|
||||
|
||||
React.useEffect(() => {
|
||||
localStorage.setItem(`tabdata_grid_${tabid}`, JSON.stringify(config));
|
||||
}, [config]);
|
||||
|
||||
return [config, setConfig];
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useConfig } from './metadataLoaders';
|
||||
import { compilePermissions, testPermission } from 'dbgate-tools';
|
||||
|
||||
export default function useHasPermission() {
|
||||
const config = useConfig();
|
||||
const compiled = React.useMemo(() => compilePermissions(config.permissions), [config]);
|
||||
const hasPermission = tested => testPermission(tested, compiled);
|
||||
return hasPermission;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import useNewQuery from '../query/useNewQuery';
|
||||
import getElectron from './getElectron';
|
||||
import useExtensions from './useExtensions';
|
||||
|
||||
export function canOpenByElectron(file, extensions) {
|
||||
if (!file) return false;
|
||||
const nameLower = file.toLowerCase();
|
||||
if (nameLower.endsWith('.sql')) return true;
|
||||
for (const format of extensions.fileFormats) {
|
||||
if (nameLower.endsWith(`.${format.extension}`)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function useOpenElectronFileCore() {
|
||||
const newQuery = useNewQuery();
|
||||
const extensions = useExtensions();
|
||||
const showModal = useShowModal();
|
||||
|
||||
return filePath => {
|
||||
const nameLower = filePath.toLowerCase();
|
||||
const path = window.require('path');
|
||||
const fs = window.require('fs');
|
||||
const parsed = path.parse(filePath);
|
||||
|
||||
if (nameLower.endsWith('.sql')) {
|
||||
const data = fs.readFileSync(filePath, { encoding: 'utf-8' });
|
||||
|
||||
newQuery({
|
||||
title: parsed.name,
|
||||
initialData: data,
|
||||
// @ts-ignore
|
||||
savedFilePath: filePath,
|
||||
savedFormat: 'text',
|
||||
});
|
||||
}
|
||||
for (const format of extensions.fileFormats) {
|
||||
if (nameLower.endsWith(`.${format.extension}`)) {
|
||||
showModal(modalState => (
|
||||
<ImportExportModal
|
||||
openedFile={{
|
||||
filePath,
|
||||
storageType: format.storageType,
|
||||
shortName: parsed.name,
|
||||
}}
|
||||
modalState={modalState}
|
||||
importToArchive
|
||||
initialValues={{
|
||||
sourceStorageType: format.storageType,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getFileFormatFilters(extensions) {
|
||||
return extensions.fileFormats.filter(x => x.readerFunc).map(x => ({ name: x.name, extensions: [x.extension] }));
|
||||
}
|
||||
|
||||
function getFileFormatExtensions(extensions) {
|
||||
return extensions.fileFormats.filter(x => x.readerFunc).map(x => x.extension);
|
||||
}
|
||||
|
||||
export default function useOpenElectronFile() {
|
||||
const electron = getElectron();
|
||||
const openElectronFileCore = useOpenElectronFileCore();
|
||||
const extensions = useExtensions();
|
||||
|
||||
return () => {
|
||||
const filePaths = electron.remote.dialog.showOpenDialogSync(electron.remote.getCurrentWindow(), {
|
||||
filters: [
|
||||
{ name: `All supported files`, extensions: ['sql', ...getFileFormatExtensions(extensions)] },
|
||||
{ name: `SQL files`, extensions: ['sql'] },
|
||||
...getFileFormatFilters(extensions),
|
||||
],
|
||||
});
|
||||
const filePath = filePaths && filePaths[0];
|
||||
if (canOpenByElectron(filePath, extensions)) {
|
||||
openElectronFileCore(filePath);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import React from 'react';
|
||||
import localforage from 'localforage';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import _ from 'lodash';
|
||||
import { useOpenedTabs, useSetOpenedTabs } from './globalState';
|
||||
import tabs from '../tabs';
|
||||
import { setSelectedTabFunc } from './common';
|
||||
|
||||
function findFreeNumber(numbers) {
|
||||
if (numbers.length == 0) return 1;
|
||||
return _.max(numbers) + 1;
|
||||
// let res = 1;
|
||||
// while (numbers.includes(res)) res += 1;
|
||||
// return res;
|
||||
}
|
||||
|
||||
export default function useOpenNewTab() {
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const openedTabs = useOpenedTabs();
|
||||
|
||||
const openNewTab = React.useCallback(
|
||||
async (newTab, initialData = undefined, options) => {
|
||||
let existing = null;
|
||||
|
||||
const { savedFile, savedFolder, savedFilePath } = newTab.props || {};
|
||||
if (savedFile || savedFilePath) {
|
||||
existing = openedTabs.find(
|
||||
x =>
|
||||
x.props &&
|
||||
x.tabComponent == newTab.tabComponent &&
|
||||
x.closedTime == null &&
|
||||
x.props.savedFile == savedFile &&
|
||||
x.props.savedFolder == savedFolder &&
|
||||
x.props.savedFilePath == savedFilePath
|
||||
);
|
||||
}
|
||||
|
||||
const { forceNewTab } = options || {};
|
||||
|
||||
const component = tabs[newTab.tabComponent];
|
||||
if (!existing && !forceNewTab && component && component.matchingProps) {
|
||||
const testString = stableStringify(_.pick(newTab.props || {}, component.matchingProps));
|
||||
existing = openedTabs.find(
|
||||
x =>
|
||||
x.props &&
|
||||
x.tabComponent == newTab.tabComponent &&
|
||||
x.closedTime == null &&
|
||||
stableStringify(_.pick(x.props || {}, component.matchingProps)) == testString
|
||||
);
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
setOpenedTabs(tabs => setSelectedTabFunc(tabs, existing.tabid));
|
||||
return;
|
||||
}
|
||||
|
||||
// new tab will be created
|
||||
if (newTab.title.endsWith('#')) {
|
||||
const numbers = openedTabs
|
||||
.filter(x => x.closedTime == null && x.title && x.title.startsWith(newTab.title))
|
||||
.map(x => parseInt(x.title.substring(newTab.title.length)));
|
||||
|
||||
newTab.title = `${newTab.title}${findFreeNumber(numbers)}`;
|
||||
}
|
||||
|
||||
const tabid = uuidv1();
|
||||
if (initialData) {
|
||||
for (const key of _.keys(initialData)) {
|
||||
if (key == 'editor') {
|
||||
await localforage.setItem(`tabdata_${key}_${tabid}`, initialData[key]);
|
||||
} else {
|
||||
localStorage.setItem(`tabdata_${key}_${tabid}`, JSON.stringify(initialData[key]));
|
||||
}
|
||||
}
|
||||
}
|
||||
setOpenedTabs(files => [
|
||||
...(files || []).map(x => ({ ...x, selected: false })),
|
||||
{
|
||||
tabid,
|
||||
selected: true,
|
||||
...newTab,
|
||||
},
|
||||
]);
|
||||
},
|
||||
[setOpenedTabs, openedTabs]
|
||||
);
|
||||
|
||||
return openNewTab;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// copied from https://usehooks.com/usePrevious/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default function usePrevious(value) {
|
||||
const ref = React.useRef();
|
||||
|
||||
// Store current value in ref
|
||||
|
||||
React.useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]); // Only re-run if value changes
|
||||
|
||||
// Return previous value (happens before update in useEffect above)
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import usePrevious from './usePrevious';
|
||||
|
||||
export default function usePropsCompare(props) {
|
||||
const prevProps = usePrevious(props);
|
||||
if (!prevProps) return;
|
||||
for (const key of _.union(_.keys(props), _.keys(prevProps))) {
|
||||
if (props[key] !== prevProps[key]) {
|
||||
console.log(`Different prop value found: prop=${key}, old, new`, prevProps[key], prevProps[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function useStorage(key, storageObject, initialValue) {
|
||||
// State to store our value
|
||||
// Pass initial state function to useState so logic is only executed once
|
||||
const [storedValue, setStoredValue] = React.useState(() => {
|
||||
try {
|
||||
// Get from local storage by key
|
||||
const item = storageObject.getItem(key);
|
||||
// Parse stored json or if none return initialValue
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
// If error also return initialValue
|
||||
console.error(error);
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
// use storedValue to ref, so that setValue with function argument works without changeing setValue itself
|
||||
const storedValueRef = React.useRef(storedValue);
|
||||
storedValueRef.current = storedValue;
|
||||
|
||||
// Return a wrapped version of useState's setter function that ...
|
||||
// ... persists the new value to localStorage.
|
||||
const setValue = React.useCallback(value => {
|
||||
try {
|
||||
// Allow value to be a function so we have same API as useState
|
||||
const valueToStore = value instanceof Function ? value(storedValueRef.current) : value;
|
||||
// Save state
|
||||
setStoredValue(valueToStore);
|
||||
// Save to local storage
|
||||
storageObject.setItem(key, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
// A more advanced implementation would handle the error case
|
||||
console.error(error);
|
||||
console.log('Error saving storage value', key, value);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
function formatSeconds(duration) {
|
||||
if (duration == null) return '';
|
||||
const hours = _.padStart(Math.floor(duration / 3600).toString(), 2, '0');
|
||||
const minutes = _.padStart((Math.floor(duration / 60) % 60).toString(), 2, '0');
|
||||
const seconds = _.padStart((duration % 60).toString(), 2, '0');
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
export default function useTimerLabel() {
|
||||
const [duration, setDuration] = React.useState(null);
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
if (busy) {
|
||||
setDuration(0);
|
||||
const handle = setInterval(() => setDuration(x => x + 1), 1000);
|
||||
return () => window.clearInterval(handle);
|
||||
}
|
||||
}, [busy]);
|
||||
|
||||
const start = React.useCallback(() => {
|
||||
setBusy(true);
|
||||
}, []);
|
||||
|
||||
const stop = React.useCallback(() => {
|
||||
setBusy(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
text: formatSeconds(duration),
|
||||
duration,
|
||||
};
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
const reducer = options => (state, action) => {
|
||||
const { mergeNearActions } = options || {};
|
||||
|
||||
const useMerge =
|
||||
action.useMerge || (mergeNearActions && state.lastActionTm && new Date().getTime() - state.lastActionTm < 100);
|
||||
|
||||
switch (action.type) {
|
||||
case 'set':
|
||||
return {
|
||||
history: [...state.history.slice(0, useMerge ? state.current : state.current + 1), action.value],
|
||||
current: useMerge ? state.current : state.current + 1,
|
||||
value: action.value,
|
||||
canUndo: true,
|
||||
canRedo: false,
|
||||
lastActionTm: new Date().getTime(),
|
||||
};
|
||||
case 'compute': {
|
||||
const newValue = action.compute(state.history[state.current]);
|
||||
return {
|
||||
history: [...state.history.slice(0, useMerge ? state.current : state.current + 1), newValue],
|
||||
current: useMerge ? state.current : state.current + 1,
|
||||
value: newValue,
|
||||
canUndo: true,
|
||||
canRedo: false,
|
||||
lastActionTm: new Date().getTime(),
|
||||
};
|
||||
}
|
||||
case 'undo':
|
||||
if (state.current > 0)
|
||||
return {
|
||||
history: state.history,
|
||||
current: state.current - 1,
|
||||
value: state.history[state.current - 1],
|
||||
canUndo: state.current > 1,
|
||||
canRedo: true,
|
||||
lastActionTm: null,
|
||||
};
|
||||
return state;
|
||||
case 'redo':
|
||||
if (state.current < state.history.length - 1)
|
||||
return {
|
||||
history: state.history,
|
||||
current: state.current + 1,
|
||||
value: state.history[state.current + 1],
|
||||
canUndo: true,
|
||||
canRedo: state.current < state.history.length - 2,
|
||||
lastActionTm: null,
|
||||
};
|
||||
return state;
|
||||
case 'reset':
|
||||
return {
|
||||
history: [action.value],
|
||||
current: 0,
|
||||
value: action.value,
|
||||
lastActionTm: null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default function useUndoReducer(initialValue, options) {
|
||||
return React.useReducer(reducer(options), {
|
||||
history: [initialValue],
|
||||
current: 0,
|
||||
value: initialValue,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user