remove web

This commit is contained in:
Jan Prochazka
2021-02-20 19:15:11 +01:00
parent dd7db5904c
commit daf9e9d18b
240 changed files with 0 additions and 22572 deletions

View File

@@ -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;
}

View File

@@ -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&apos;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} />;
}

View File

@@ -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>
);
}

View File

@@ -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);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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);
}

View File

@@ -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>
);
}

View File

@@ -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
);
}

View File

@@ -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>
);
}

View File

@@ -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 };
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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 })),
];
}

View File

@@ -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;
}

View File

@@ -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;
`}
`;

View File

@@ -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>
);
}

View File

@@ -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`;
}

View File

@@ -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>
);
}

View File

@@ -1,4 +0,0 @@
export default function fullDisplayName({ schemaName, pureName }) {
if (schemaName) return `${schemaName}.${pureName}`;
return pureName;
}

View File

@@ -1,7 +0,0 @@
import _ from 'lodash';
export default function getAsArray(obj) {
if (_.isArray(obj)) return obj;
if (obj != null) return [obj];
return [];
}

View File

@@ -1,7 +0,0 @@
export default function getElectron() {
if (window.require) {
const electron = window.require('electron');
return electron;
}
return null;
}

View File

@@ -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 };

View File

@@ -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>;
}

View File

@@ -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,
};

View File

@@ -1,7 +0,0 @@
import styled from 'styled-components';
export const Grid = styled.div``;
export const Row = styled.div``;
export const Col = styled.div``;

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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';
}

View File

@@ -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];
}

View File

@@ -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);
};
}, []);
}

View File

@@ -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,
};
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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];
}

View File

@@ -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;
}

View File

@@ -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);
}
};
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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]);
}
}
}

View File

@@ -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];
}

View File

@@ -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,
};
}

View File

@@ -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,
});
}