Skip to content

Commit

Permalink
feat(Query): Add plotting, error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
zane committed Oct 4, 2022
1 parent ea37120 commit 33d29e6
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 67 deletions.
165 changes: 108 additions & 57 deletions src/Query.jsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,148 @@
import PropTypes from 'prop-types';
import React from 'react';
import { ArrowBackUp, Database } from 'tabler-icons-react';
import { Button, Paper, Space } from '@mantine/core';
/* eslint-disable import/no-named-default */
import { default as Input } from './HighlightInput';
// import { default as Input } from './PrismInput';
import { Button, LoadingOverlay, Paper, Tabs } from '@mantine/core';
import { getHotkeyHandler } from '@mantine/hooks';
import React from 'react';
import PropTypes from 'prop-types';
import DataTable from './DataTable';
import HighlightInput from './HighlightInput';
import PairPlot from './PairPlot';
import WorldMap from './WorldMap';

const useClearableState = (initialValue) => {
const [value, setValue] = React.useState(initialValue);
const clearValue = () => setValue();
return [value, setValue, clearValue];
};

const usePrevious = (value) => {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
const useSwitch = (initialValue) => {
const [value, setValue] = React.useState(initialValue);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
return [value, setTrue, setFalse];
};

export default function Query({ execute, initialQuery }) {
const both =
(f1, f2) =>
(...args) => {
f1.apply(this, args);
f2.apply(this, args);
};

export default function Query({ execute, initialQuery, statType }) {
const [isLoading, setIsLoading, setNotLoading] = useSwitch(false);
const [queryValue, setQueryValue] = React.useState(initialQuery || '');
const [queryResult, setQueryResult] = React.useState();
const [queryResult, setQueryResult, clearQueryResult] = useClearableState();
const [errorValue, setErrorValue, clearErrorValue] = useClearableState();
const [activeTab, setActiveTab] = React.useState(0);

const buttonRef = React.useRef();
const editorRef = React.useRef();

const handleExecute = () => {
setQueryResult(execute(queryValue));
};
const handleReset = () => {
setQueryResult();
execute(queryValue)
.then(both(setQueryResult, clearErrorValue))
.catch(both(setErrorValue, clearQueryResult))
.finally(setNotLoading);
setIsLoading();
};

const onKeyDown = (event) => {
if (event.key === 'Enter' && event.shiftKey) {
event.stopPropagation();
handleExecute();
}
};
const onKeyDown = getHotkeyHandler([
['mod+Enter', handleExecute],
['shift+Enter', handleExecute],
]);

const previousQueryResult = usePrevious(queryResult);
React.useEffect(() => {
if (!queryResult && previousQueryResult) {
editorRef.current.focus();
} else if (queryResult && !previousQueryResult) {
buttonRef.current.focus();
}
}, [queryResult, previousQueryResult]);
const mapShown =
queryResult &&
statType &&
queryResult.columns.length === 2 &&
queryResult.columns.map(statType).includes('quantitative') &&
queryResult.columns.includes('Country_of_Operator');

return (
<Paper p="sm" radius="sm" shadow="md" withBorder>
<Input
disabled={Boolean(queryResult)}
onChange={setQueryValue}
<Paper p="xs" radius="xs" shadow="md" withBorder>
<HighlightInput
disabled={isLoading}
error={Boolean(errorValue)}
onChange={both(setQueryValue, clearErrorValue)}
onKeyDown={onKeyDown}
ref={editorRef}
value={queryValue}
/>
{queryResult ? (
<Button
leftIcon={<ArrowBackUp />}
mt="md"
ref={buttonRef}
variant="default"
onClick={handleReset}
>
Reset
</Button>
) : (

<Button
disabled={isLoading}
loading={isLoading}
leftIcon={<Database size={18} />}
mt="sm"
mr="sm"
ref={buttonRef}
variant="default"
onClick={handleExecute}
>
Execute
</Button>

{queryResult && (
<Button
leftIcon={<Database />}
mt="md"
leftIcon={<ArrowBackUp size={18} />}
mt="sm"
ref={buttonRef}
variant="default"
onClick={handleExecute}
onClick={clearQueryResult}
>
Execute
Clear
</Button>
)}
{queryResult && <Space h="md" />}

{queryResult && (
<DataTable
columns={queryResult.columns}
pagination={false}
rows={queryResult.rows}
/>
<Tabs mt="sm" active={activeTab} onTabChange={setActiveTab}>
<Tabs.Tab label="Table">
<div style={{ position: 'relative' }}>
<LoadingOverlay visible={isLoading} transitionDuration={0} />
<DataTable
columns={queryResult.columns}
pagination={false}
rows={queryResult.rows}
/>
</div>
</Tabs.Tab>
{statType && (
<Tabs.Tab label="Plots">
<PairPlot
data={queryResult.rows}
types={Object.fromEntries(
queryResult.columns
.map((col) => [col, statType(col)])
.filter(([col, type]) => col && type)
)}
/>
</Tabs.Tab>
)}
{mapShown && (
<Tabs.Tab label="Map">
<WorldMap
data={queryResult.rows}
types={Object.fromEntries(
queryResult.columns
.map((col) => [col, statType(col)])
.filter(([col, type]) => col && type)
)}
/>
</Tabs.Tab>
)}
</Tabs>
)}
</Paper>
);
}

Query.propTypes = {
execute: PropTypes.func.isRequired,
statType: PropTypes.func,
initialQuery: PropTypes.string,
};

Query.defaultProps = {
initialQuery: undefined,
statType: undefined,
};
43 changes: 33 additions & 10 deletions stories/Query.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,46 @@ function Template(args) {
return <Query {...args} />;
}

const execute = () => ({
columns: ['name', 'age', 'color'],
rows: [
{ name: 'Disco', age: 16, color: 'brown' },
{ name: 'Henry', age: 14, color: 'orange' },
{ name: 'Zelda', age: 13, color: 'black' },
],
});
const makeExecute = (data, timeout) => () =>
new Promise((res) =>
setTimeout(
() =>
res({
columns: [...new Set(data.flatMap((datum) => Object.keys(datum)))],
rows: data,
}),
timeout || 250
)
);

const cats = [
{ name: 'Disco', age: 16, color: 'brown' },
{ name: 'Henry', age: 14, color: 'orange' },
{ name: 'Zelda', age: 13, color: 'black' },
];

const failingExecute = () => Promise.reject(new Error('oh no'));

export const Empty = Template.bind({});
Empty.args = {
execute,
execute: makeExecute(cats),
};

export const Populated = Template.bind({});
Populated.args = {
execute,
execute: makeExecute(cats),
initialQuery: 'SELECT * FROM data',
};

export const Slow = Template.bind({});
Slow.args = {
execute: makeExecute(cats, 5000),
initialQuery: 'SELECT * FROM data',
};

export const Fail = Template.bind({});
Fail.args = {
execute: failingExecute,
initialQuery: 'SELECT * FROM data',
};

Expand Down

0 comments on commit 33d29e6

Please sign in to comment.