refresh(onItemAdd)(undefined)}
+ onRemove={() => refresh(onItemRemove)(undefined)}
+ />
+
+ >
+ );
+ }
+}
diff --git a/src/components/pages/query/builder/loaders/PostgresLoader.tsx b/src/components/pages/query/builder/loaders/PostgresLoader.tsx
new file mode 100644
index 0000000..2890e9c
--- /dev/null
+++ b/src/components/pages/query/builder/loaders/PostgresLoader.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { LoaderMethods, SqlPqlLoader } from '../../../../../state/pql/loaders';
+import { Input, Label, Textarea } from '../../../../common/Inputs';
+import { Operator, OperatorKind, RefreshCallback } from '../../../../../state/pql/pql';
+
+export default class implements Operator {
+ title = 'Postgres database';
+
+ kind = OperatorKind.Loader;
+
+ private uri = '';
+
+ private query = '';
+
+ constructor(uri: string, query: string) {
+ this.uri = uri;
+ this.query = query;
+ }
+
+ build(): SqlPqlLoader {
+ return {
+ uri: this.uri,
+ query: this.query,
+ step: 'extract',
+ method: LoaderMethods.Postgres,
+ };
+ }
+
+ renderConfig(refresh: RefreshCallback): JSX.Element {
+ const setUri = (uri: string): void => {
+ this.uri = uri;
+ };
+ const setQuery = (query: string): void => {
+ this.query = query;
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+ }
+}
diff --git a/src/components/pages/query/builder/operators/AggregateOperator.tsx b/src/components/pages/query/builder/operators/AggregateOperator.tsx
new file mode 100644
index 0000000..7ba0599
--- /dev/null
+++ b/src/components/pages/query/builder/operators/AggregateOperator.tsx
@@ -0,0 +1,78 @@
+import React from 'react';
+import { Label, Option, Select, Textarea } from '../../../../common/Inputs';
+import { OperatorKind, OutsideOperator, RefreshCallback } from '../../../../../state/pql/pql';
+import { AggregationMethods, AggregationQueryParams, PqlAggregation } from '../../../../../state/pql/aggregators';
+
+export default class AggregatorOperator implements OutsideOperator {
+ title = 'Aggregate';
+
+ kind = OperatorKind.Operation;
+
+ method: AggregationMethods = AggregationMethods.Max;
+
+ param: AggregationQueryParams = AggregationQueryParams.None;
+
+ query = '';
+
+ constructor(method: AggregationMethods, query = '', param = AggregationQueryParams.None) {
+ this.param = param;
+ this.query = query;
+ this.method = method;
+ }
+
+ build(): PqlAggregation {
+ if (this.method === AggregationMethods.Query)
+ return {
+ result: true,
+ params: this.param,
+ query: this.query,
+ method: AggregationMethods.Query,
+ };
+ return {
+ method: this.method,
+ };
+ }
+
+ renderConfig(refresh: RefreshCallback): JSX.Element {
+ const onQueryChange = (value: string): void => {
+ this.query = value;
+ };
+ const onMethodChange = (value: string): void => {
+ this.method = value as AggregationMethods;
+ };
+ const onParamChange = (value: string): void => {
+ this.param = value as AggregationQueryParams;
+ };
+
+ return (
+ <>
+
+
+
+
+ {this.method === AggregationMethods.Query && (
+
+
+
+
+ )}
+ {this.method === AggregationMethods.Query && (
+
+
+
+
+ )}
+ >
+ );
+ }
+}
diff --git a/src/components/pages/query/builder/operators/GetIndexOperator.tsx b/src/components/pages/query/builder/operators/GetIndexOperator.tsx
new file mode 100644
index 0000000..2760b47
--- /dev/null
+++ b/src/components/pages/query/builder/operators/GetIndexOperator.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { GetIndexPqlOperator, OperatorStep } from '../../../../../state/pql/operators';
+import { Input, Label } from '../../../../common/Inputs';
+import { Operator, OperatorKind, RefreshCallback } from '../../../../../state/pql/pql';
+
+export default class GetIndexOperator implements Operator {
+ title = 'Get index';
+
+ kind = OperatorKind.Operation;
+
+ private params = 0;
+
+ constructor(params: number) {
+ this.params = params;
+ }
+
+ build(): GetIndexPqlOperator {
+ return {
+ params: this.params,
+ step: OperatorStep.GetIndex,
+ };
+ }
+
+ renderConfig(refresh: RefreshCallback): JSX.Element {
+ const onChange = (value: string): void => {
+ this.params = parseInt(value, 10);
+ };
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/src/components/pages/query/builder/operators/MathOperator.tsx b/src/components/pages/query/builder/operators/MathOperator.tsx
new file mode 100644
index 0000000..c87f8d6
--- /dev/null
+++ b/src/components/pages/query/builder/operators/MathOperator.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { MathDirection, MathMethod, MathPqlOperator, OperatorStep } from '../../../../../state/pql/operators';
+import { Checkbox, Input, Label, Option, Select } from '../../../../common/Inputs';
+import { Operator, OperatorKind, RefreshCallback } from '../../../../../state/pql/pql';
+
+export default class MathOperator implements Operator {
+ title = 'Math';
+
+ kind = OperatorKind.Operation;
+
+ private method: MathMethod = MathMethod.Add;
+
+ private params = 0;
+
+ private direction = false;
+
+ constructor(method: MathMethod, params: number, direction?: MathDirection) {
+ this.method = method;
+ this.params = params;
+ this.direction = direction === 'reverse';
+ }
+
+ build(): MathPqlOperator {
+ const config = {
+ method: this.method,
+ params: this.params,
+ step: OperatorStep.Math,
+ };
+ if (this.direction) return { ...config, direction: 'reverse' };
+ return config;
+ }
+
+ renderConfig(refresh: RefreshCallback): JSX.Element {
+ const onMethodChange = (method: string): void => {
+ this.method = method as MathMethod;
+ };
+ const onConstantChange = (value: string): void => {
+ this.params = parseFloat(value);
+ };
+ const onDirectionChange = (): void => {
+ this.direction = !this.direction;
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ refresh(onDirectionChange)(undefined)} />
+
+ >
+ );
+ }
+}
diff --git a/src/components/pages/query/builder/operators/QuerySqlOperator.tsx b/src/components/pages/query/builder/operators/QuerySqlOperator.tsx
new file mode 100644
index 0000000..8a93eab
--- /dev/null
+++ b/src/components/pages/query/builder/operators/QuerySqlOperator.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { Checkbox, Input, Label, Option, Select, Textarea } from '../../../../common/Inputs';
+import { OperatorStep, QuerySqlPqlOperator, SqlMethod } from '../../../../../state/pql/operators';
+import { Operator, OperatorKind, RefreshCallback } from '../../../../../state/pql/pql';
+
+export default class QuerySqlOperator implements Operator {
+ title = 'SQL query';
+
+ kind = OperatorKind.Operation;
+
+ private method: SqlMethod = SqlMethod.None;
+
+ private query = '';
+
+ private result = true;
+
+ constructor(method: SqlMethod, query: string, result: boolean) {
+ this.method = method;
+ this.query = query;
+ this.result = result;
+ }
+
+ build(): QuerySqlPqlOperator {
+ return {
+ method: this.method,
+ query: this.query,
+ result: this.result,
+ step: OperatorStep.QuerySql,
+ };
+ }
+
+ renderConfig(refresh: RefreshCallback): JSX.Element {
+ const onQueryChange = (query: string): void => {
+ this.query = query;
+ };
+ const onMethodChange = (method: string): void => {
+ this.method = method as SqlMethod;
+ };
+ const onResultChange = (): void => {
+ this.result = !this.result;
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ refresh(onResultChange)(undefined)} />
+
+ >
+ );
+ }
+}
diff --git a/src/components/pages/query/builder/operators/TraverseOperator.tsx b/src/components/pages/query/builder/operators/TraverseOperator.tsx
new file mode 100644
index 0000000..d5cd31b
--- /dev/null
+++ b/src/components/pages/query/builder/operators/TraverseOperator.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { OperatorStep, TraversePqlOperator } from '../../../../../state/pql/operators';
+import { Operator, OperatorKind, RefreshCallback } from '../../../../../state/pql/pql';
+import { Button } from '../../../../common/Buttons';
+import { Input, Label } from '../../../../common/Inputs';
+import { ListHeaderAddRemove } from '../../../../common/Lists';
+
+export default class TraverseOperator implements Operator {
+ title = 'Traverse';
+
+ kind = OperatorKind.Operation;
+
+ private params: string[] = [];
+
+ constructor(params: string[]) {
+ this.params = [...params];
+ }
+
+ build(): TraversePqlOperator {
+ return {
+ params: this.params,
+ method: 'json',
+ step: OperatorStep.Traverse,
+ };
+ }
+
+ renderConfig(refresh: RefreshCallback): JSX.Element {
+ const onItemAdd = (): void => {
+ this.params = [...this.params, ''];
+ };
+ const onItemRemove = (): string[] => this.params.splice(-1, 1);
+ const onItemUpdate = (index: number) => (value: string): void => {
+ this.params[index] = value;
+ };
+
+ const itemView = this.params.map((param, index) => (
+
+
+
+ ));
+
+ return (
+ <>
+ refresh(onItemAdd)(undefined)}
+ onRemove={() => refresh(onItemRemove)(undefined)}
+ />
+
+ >
+ );
+ }
+}
diff --git a/src/components/pages/query/result/QueryResult.tsx b/src/components/pages/query/result/QueryResult.tsx
new file mode 100644
index 0000000..da6bce4
--- /dev/null
+++ b/src/components/pages/query/result/QueryResult.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+interface QueryResult {
+ result: string;
+}
+
+const QueryResult = ({ result }: QueryResult): JSX.Element => (
+ {result}
+);
+
+export default QueryResult;
diff --git a/src/components/pages/query/selectors/LoaderSelector.tsx b/src/components/pages/query/selectors/LoaderSelector.tsx
new file mode 100644
index 0000000..e0ae1c8
--- /dev/null
+++ b/src/components/pages/query/selectors/LoaderSelector.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { Operator } from '../../../../state/pql/pql';
+import EthereumBalanceLoader from '../builder/loaders/EthereumBalanceLoader';
+import EthereumFunctionLoader from '../builder/loaders/EthereumFunctionLoader';
+import HttpGetLoader from '../builder/loaders/HttpGetLoader';
+import HttpPostLoader from '../builder/loaders/HttpPostLoader';
+import PostgresLoader from '../builder/loaders/PostgresLoader';
+import QuerySelectorContainer from '../QueryClosableContainer';
+import SelectorButton from './SelectorButton';
+
+interface LoaderSelector extends QuerySelectorContainer {
+ addOperator: (operator: Operator) => void;
+}
+
+const LoaderSelector = ({ addOperator, onClose }: LoaderSelector): JSX.Element => {
+ const onHttpGetClick = (): void => addOperator(new HttpGetLoader(''));
+ const onHttpPostClick = (): void => addOperator(new HttpPostLoader('', { '': '' }));
+ const onPostgressClick = (): void => addOperator(new PostgresLoader('', ''));
+ const onEthBalanceClick = (): void => addOperator(new EthereumBalanceLoader('', '', undefined));
+ const onEthFunctionClick = (): void => addOperator(new EthereumFunctionLoader('', '', '', ['']));
+
+ return (
+
+ Http Get
+ Http Post
+ Postgress
+ Ethereum Balance
+ Ethereum Function
+
+ );
+};
+
+export default LoaderSelector;
diff --git a/src/components/pages/query/selectors/OperationSelector.tsx b/src/components/pages/query/selectors/OperationSelector.tsx
new file mode 100644
index 0000000..1ad6050
--- /dev/null
+++ b/src/components/pages/query/selectors/OperationSelector.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import { MathMethod, SqlMethod } from '../../../../state/pql/operators';
+import { Operator } from '../../../../state/pql/pql';
+import GetIndexOperator from '../builder/operators/GetIndexOperator';
+import MathOperator from '../builder/operators/MathOperator';
+import QuerySqlOperator from '../builder/operators/QuerySqlOperator';
+import TraverseOperator from '../builder/operators/TraverseOperator';
+import QuerySelectorContainer from '../QueryClosableContainer';
+import SelectorButton from './SelectorButton';
+
+interface OperatorSelection extends QuerySelectorContainer {
+ addOperator: (operator: Operator) => void;
+}
+
+const OperationSelector = ({ addOperator, onClose }: OperatorSelection): JSX.Element => {
+ const onGetIndexClick = (): void => addOperator(new GetIndexOperator(0));
+ const onTraverseClick = (): void => addOperator(new TraverseOperator(['']));
+ const onMathClick = (): void => addOperator(new MathOperator(MathMethod.Add, 0));
+ const onSqlQueryClick = (): void => addOperator(new QuerySqlOperator(SqlMethod.None, '', false));
+
+ return (
+
+ Traverse
+ Get index
+ Math
+ SQL query
+
+ );
+};
+export default OperationSelector;
diff --git a/src/components/pages/query/selectors/QuerySelector.tsx b/src/components/pages/query/selectors/QuerySelector.tsx
new file mode 100644
index 0000000..7becef6
--- /dev/null
+++ b/src/components/pages/query/selectors/QuerySelector.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import LoaderSelector from './LoaderSelector';
+import OperationSelector from './OperationSelector';
+import QuerySelectorContainer from '../QueryClosableContainer';
+import { Operator } from '../../../../state/pql/pql';
+
+export const SELECTOR_LOADER = 'loader';
+export const SELECTOR_OPERATOR = 'operator';
+
+export type SelectorKind = typeof SELECTOR_LOADER | typeof SELECTOR_OPERATOR;
+
+interface QuerySelector extends QuerySelectorContainer {
+ kind: SelectorKind;
+ addOperator: (operator: Operator) => void;
+}
+
+const QuerySelector = ({ kind, addOperator, onClose }: QuerySelector): JSX.Element => {
+ switch (kind) {
+ case SELECTOR_LOADER:
+ return ;
+ case SELECTOR_OPERATOR:
+ return ;
+ default:
+ throw new Error('Selector is not implemented!');
+ }
+};
+
+export default QuerySelector;
diff --git a/src/components/pages/query/selectors/SelectorButton.tsx b/src/components/pages/query/selectors/SelectorButton.tsx
new file mode 100644
index 0000000..00fee55
--- /dev/null
+++ b/src/components/pages/query/selectors/SelectorButton.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { Button } from '../../../common/Buttons';
+
+interface SelectorButton {
+ onClick?: () => void;
+}
+
+const SelectorButton: React.FC = ({ onClick, children }) => (
+
+);
+
+export default SelectorButton;
diff --git a/src/components/routes.ts b/src/components/routes.ts
new file mode 100644
index 0000000..0023df1
--- /dev/null
+++ b/src/components/routes.ts
@@ -0,0 +1,24 @@
+export const HOME_PAGE_ROUTE = '/';
+
+// Authentication pages
+export const LOGIN_PAGE_ROUTE = '/login';
+export const REGISTER_PAGE_ROUTE = '/register';
+
+// IPFS pages
+export const IPFS_BASE_PAGE_ROUTE = '/ipfs';
+export const IPFS_PAGE_ROUTE = `${IPFS_BASE_PAGE_ROUTE}/:hash`;
+
+// Query builder routes
+export const QUERY_LIST_ROUTE = '/query-builder';
+export const QUERY_BUILDER_ROUTE = `${QUERY_LIST_ROUTE}/:hash`;
+
+// Error pages
+export const ERROR_404_PAGE_ROUTE = '/error404';
+
+export const USER_ROUTES = [
+ HOME_PAGE_ROUTE,
+ IPFS_BASE_PAGE_ROUTE,
+ IPFS_PAGE_ROUTE,
+ QUERY_LIST_ROUTE,
+ QUERY_BUILDER_ROUTE,
+];
diff --git a/src/config/api.ts b/src/config/api.ts
new file mode 100644
index 0000000..9df0189
--- /dev/null
+++ b/src/config/api.ts
@@ -0,0 +1,5 @@
+export const apis = {
+ // TODO: Replace this by the BE api urls we would be using. Probably storing this in a config file
+ url: 'https://jsonplaceholder.typicode.com',
+ timeout: 2 * 60 * 1000, // 2 minutes timeout
+};
diff --git a/src/config/index.ts b/src/config/index.ts
new file mode 100644
index 0000000..f8cb76b
--- /dev/null
+++ b/src/config/index.ts
@@ -0,0 +1,2 @@
+export { apis } from './api';
+export { storageNames } from './storage-names';
diff --git a/src/config/storage-names.ts b/src/config/storage-names.ts
new file mode 100644
index 0000000..b36ebf7
--- /dev/null
+++ b/src/config/storage-names.ts
@@ -0,0 +1,3 @@
+export const storageNames = {
+ user: 'pl11',
+};
diff --git a/src/index.scss b/src/index.scss
index ec2585e..41aee48 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -1,13 +1,27 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+#root {
+ height: 100vh;
+}
+
body {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
+ 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
+}
+
+.tooltip {
+ @apply invisible absolute;
+}
+
+.has-tooltip:hover .tooltip {
+ @apply visible z-50;
}
diff --git a/src/pages/ipfs/Ipfs.tsx b/src/pages/ipfs/Ipfs.tsx
deleted file mode 100644
index b87e0b8..0000000
--- a/src/pages/ipfs/Ipfs.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import React, { ReactNode } from 'react';
-import { Link } from 'react-router-dom';
-import { Button, Grid, Header, Message, Segment } from 'semantic-ui-react';
-
-import ace from 'brace';
-import 'brace/mode/json';
-import 'brace/theme/github';
-
-// No typings for jsoneditor-react for now just using @ts-ignore for getting around the issue
-// Later on with advanced design we might not used that library. If we do we can as well define typings for it
-// @ts-ignore
-import { JsonEditor as Editor } from 'jsoneditor-react';
-import 'jsoneditor-react/es/editor.min.css';
-
-interface IpfsProps {
- match: { params: { hash: string } };
-}
-
-interface IpfsState {
- hash: string;
- result: string;
- error: any;
-}
-
-class Ipfs extends React.Component {
- API_PATH = 'http://127.0.0.1:7424/api';
-
- editor: any;
-
- constructor(props: IpfsProps) {
- super(props);
- this.state = {
- hash: '',
- result: 'Click "Test" button to get the result of the above PQL definition.',
- error: null,
- };
- this.editor = React.createRef();
-
- this.pqlAction = this.pqlAction.bind(this);
- }
-
- componentDidMount(): void {
- const { hash } = this.props.match.params;
-
- fetch(`${this.API_PATH}/ipfs/${hash}`)
- .then((res) => {
- if (!res.ok) {
- return res.json().then((json) => {
- throw json;
- });
- }
- return res.json();
- })
- .then((res) => {
- this.setState({
- hash: res.hash,
- error: null,
- });
- this.editor.current.jsonEditor.set(res.pql);
- })
- .catch((res) => {
- this.setState({
- error: res.error,
- });
- });
- }
-
- pqlAction(endpoint: string): void {
- const text = this.editor.current.jsonEditor.getText();
-
- const requestOptions = {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: text,
- };
-
- fetch(`${this.API_PATH}/${endpoint}`, requestOptions)
- .then((res) => res.json())
- .then((res) => this.setState({ result: JSON.stringify(res) }));
- }
-
- render(): ReactNode {
- const { hash } = this.props.match.params;
- if (this.state.error) {
- return (
-
-
- Error: {this.state.error}
-
-
- An error occurred retrieving IPFS hash {hash} . The selected IPFS hash was not a valid PQL definition
- file.
-
-
- );
- }
- return (
-
-
-
-
IPFS
-
-
{this.state.hash}
-
-
-
-
-
-
-
-
-
-
-
-
- Result:
- {this.state.result}
-
-
-
- );
- }
-}
-
-export default Ipfs;
diff --git a/src/pages/ipfs/IpfsList.tsx b/src/pages/ipfs/IpfsList.tsx
deleted file mode 100644
index a12d3bf..0000000
--- a/src/pages/ipfs/IpfsList.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import React, { ReactNode } from 'react';
-import { Link } from 'react-router-dom';
-import { Button, Header, Table } from 'semantic-ui-react';
-
-interface IpfsListState {
- ipfsHashes: any[];
-}
-
-class IpfsList extends React.Component<{}, IpfsListState> {
- API_PATH = 'http://127.0.0.1:7424/api';
-
- constructor(props: {}) {
- super(props);
- this.state = {
- ipfsHashes: [],
- };
- }
-
- componentDidMount(): void {
- fetch(`${this.API_PATH}/ipfs`)
- .then((res) => res.json())
- .then(
- (res) => {
- this.setState({
- ipfsHashes: res.hashes,
- });
- },
- (error) => {
- this.setState({
- ipfsHashes: [],
- });
- console.log(error);
- },
- );
- }
-
- render(): ReactNode {
- return (
-
-
-
-
-
-
List of local IPFS hashes
-
-
-
-
- IPFS hash
-
-
-
-
- {this.state.ipfsHashes.map((hash) => (
-
-
- {hash}
-
-
- ))}
-
-
-
- );
- }
-}
-
-export default IpfsList;
diff --git a/src/state/pql/aggregators.ts b/src/state/pql/aggregators.ts
new file mode 100644
index 0000000..831157f
--- /dev/null
+++ b/src/state/pql/aggregators.ts
@@ -0,0 +1,45 @@
+export const AGGREGATOR_CONFIG = 'aggregate';
+
+export enum AggregationMethods {
+ Max = 'max',
+ Min = 'min',
+ Mean = 'mean',
+ Median = 'median',
+ Query = 'query.sql',
+}
+
+export enum AggregationQueryParams {
+ Json = 'json',
+ List = 'list',
+ None = 'None',
+}
+
+type AggregationBaseMethods =
+ | AggregationMethods.Max
+ | AggregationMethods.Min
+ | AggregationMethods.Mean
+ | AggregationMethods.Median;
+
+interface AggregationBasePql {
+ method: AggregationBaseMethods;
+}
+
+interface AggregationQueryPql {
+ params: AggregationQueryParams;
+ query: string;
+ result: boolean;
+ method: AggregationMethods.Query;
+}
+
+export type PqlAggregation = AggregationBasePql | AggregationQueryPql;
+
+export const aggregatePql = (method: AggregationBaseMethods): AggregationBasePql => ({
+ method,
+});
+
+export const aggregateQueryPql = (query: string, params: AggregationQueryParams): AggregationQueryPql => ({
+ query,
+ params,
+ result: true,
+ method: AggregationMethods.Query,
+});
diff --git a/src/state/pql/loaders.ts b/src/state/pql/loaders.ts
new file mode 100644
index 0000000..35cbe54
--- /dev/null
+++ b/src/state/pql/loaders.ts
@@ -0,0 +1,61 @@
+const LATEST = 'latest';
+
+export enum LoaderMethods {
+ Get = 'http.get',
+ Post = 'http.post',
+ Postgres = 'sql.postgres',
+ EthBalance = 'eth.balance',
+ EthFunction = 'eth.function',
+}
+
+export type ObjectParams = { [key: string]: string };
+
+interface DefaultPqlLoader {
+ step: string;
+ method: LoaderMethods;
+}
+
+// Http loaders
+export interface HttpGetPqlLoader extends DefaultPqlLoader {
+ uri: string;
+}
+
+export interface HttpPostPqlLoader extends HttpGetPqlLoader {
+ params: ObjectParams; // TODO ensure string value is awailable!
+}
+
+// Sql loaders
+export interface SqlPqlLoader extends HttpGetPqlLoader {
+ query: string;
+}
+
+// Ethereum loaders
+export type BlockType = typeof LATEST | number;
+
+export interface DefaultEthereumPqlLoader extends DefaultPqlLoader {
+ address: string;
+ chain: string;
+}
+
+export interface EthereumBalancePqlLoader extends DefaultEthereumPqlLoader {
+ params: {
+ block: BlockType;
+ num_confirmations?: number;
+ };
+}
+
+export interface EthereumFunctionPqlLoader extends DefaultEthereumPqlLoader {
+ params: {
+ args: string[];
+ block: BlockType;
+ function: string;
+ num_confirmations?: number;
+ };
+}
+
+export type PqlLoader =
+ | HttpGetPqlLoader
+ | HttpPostPqlLoader
+ | SqlPqlLoader
+ | EthereumBalancePqlLoader
+ | EthereumFunctionPqlLoader;
diff --git a/src/state/pql/operators.ts b/src/state/pql/operators.ts
new file mode 100644
index 0000000..ab97c21
--- /dev/null
+++ b/src/state/pql/operators.ts
@@ -0,0 +1,53 @@
+export const MATH_DIRECTION = 'reverse';
+export type MathDirection = typeof MATH_DIRECTION;
+
+export enum MathMethod {
+ Add = 'add',
+ Mul = 'mul',
+ Sub = 'sub',
+ Div = 'div',
+}
+
+export enum SqlMethod {
+ Json = 'json',
+ List = 'list',
+ Dict = 'dict',
+ None = 'None',
+}
+
+export enum OperatorStep {
+ Math = 'math',
+ Traverse = 'traverse',
+ GetIndex = 'get_index',
+ QuerySql = 'query.sql',
+}
+
+interface DefaultPqlOperator {
+ step: OperatorStep;
+}
+
+export interface TraversePqlOperator extends DefaultPqlOperator {
+ method: string;
+ params: string[];
+}
+
+export interface GetIndexPqlOperator extends DefaultPqlOperator {
+ params: number;
+}
+
+export interface MathPqlOperator extends DefaultPqlOperator {
+ method: MathMethod;
+ params: number;
+ direction?: MathDirection;
+}
+
+export interface QuerySqlPqlOperator extends DefaultPqlOperator {
+ method: SqlMethod;
+ query: string;
+ result: boolean;
+}
+
+// interface CustomStepPqlOperator extends GetIndexPqlOperator { }
+
+export type PqlOperator = TraversePqlOperator | GetIndexPqlOperator | MathPqlOperator | QuerySqlPqlOperator;
+// | CustomStepPqlOperator;
diff --git a/src/state/pql/pql.ts b/src/state/pql/pql.ts
new file mode 100644
index 0000000..eb20be8
--- /dev/null
+++ b/src/state/pql/pql.ts
@@ -0,0 +1,47 @@
+import { EOPNOTSUPP } from 'constants';
+import { PqlAggregation } from './aggregators';
+import { PqlLoader } from './loaders';
+import { PqlOperator } from './operators';
+
+export type SourceOperation = PqlLoader | PqlOperator;
+
+export interface Source {
+ name: string;
+ pipeline: SourceOperation[];
+}
+
+export interface Pql {
+ name: string;
+ psql_version: string; // TODO suggestion: changed from snake cased to camel cased!
+ sources: Source[];
+ aggregate?: PqlAggregation;
+}
+
+export const emptyPql: Pql = {
+ name: '',
+ psql_version: '0.1',
+ sources: [],
+};
+
+// type EmptyRefreshCallBack = (fun: () => void) => () => void;
+// type ValueRefreshCallBack = (fun: (value: T) => void) => (value: T) => void;
+export type RefreshCallback = (fun: (value: T) => void) => (value: T) => void;
+
+export enum OperatorKind {
+ Loader,
+ Operation,
+}
+
+interface BaseOperator {
+ title: string;
+ kind: OperatorKind;
+ renderConfig: (refreshCallback: RefreshCallback) => JSX.Element;
+}
+
+export interface Operator extends BaseOperator {
+ build: () => SourceOperation;
+}
+
+export interface OutsideOperator extends BaseOperator {
+ build: () => PqlAggregation;
+}
diff --git a/src/state/query-builder.ts b/src/state/query-builder.ts
new file mode 100644
index 0000000..59dfb54
--- /dev/null
+++ b/src/state/query-builder.ts
@@ -0,0 +1,110 @@
+import AggregatorOperator from '../components/pages/query/builder/operators/AggregateOperator';
+import { AggregationMethods, AGGREGATOR_CONFIG } from './pql/aggregators';
+import { Operator, OperatorKind, OutsideOperator } from './pql/pql';
+
+export interface ExtendedOperator {
+ id: string;
+ operator: Operator;
+}
+
+export interface ExtendedSource {
+ id: string;
+ title: string;
+ operators: string[];
+}
+
+export interface QueryData {
+ operators: { [key: string]: ExtendedOperator };
+ sources: { [key: string]: ExtendedSource };
+ sourceOrder: string[];
+
+ sourceIndex: number;
+ operatorIndex: number;
+
+ aggregate?: OutsideOperator;
+}
+
+const removeSource = (data: QueryData, sourceId: string): QueryData => {
+ const sources = { ...data.sources };
+ delete sources[sourceId];
+ return { ...data, sources, sourceOrder: data.sourceOrder.filter((item) => item !== sourceId) };
+};
+
+const removeOperator = (data: QueryData, sourceId: string, operatorId: string): QueryData => {
+ const source = { ...data.sources[sourceId] };
+ return {
+ ...data,
+ sources: {
+ ...data.sources,
+ [sourceId]: { ...source, operators: [...source.operators].filter((operator) => operator !== operatorId) },
+ },
+ };
+};
+
+export const onOperatorRemoveAction = (
+ data: QueryData,
+ sourceId: string,
+ operatorId: string,
+): QueryData => // removeOperator(data, sourceId, operatorId);
+ data.operators[operatorId].operator.kind === OperatorKind.Loader
+ ? removeSource(data, sourceId)
+ : removeOperator(data, sourceId, operatorId);
+
+export const createNewOperator = (data: QueryData, sourceId: string, operator: Operator): [QueryData, string] => {
+ const id = `operator-${data.operatorIndex}`;
+ const source = data.sources[sourceId];
+
+ return [
+ {
+ ...data,
+ operators: {
+ ...data.operators,
+ [id]: {
+ id,
+ operator,
+ },
+ },
+ sources: { ...data.sources, [sourceId]: { ...source, operators: [...source.operators, id] } },
+ operatorIndex: data.operatorIndex + 1,
+ },
+ id,
+ ];
+};
+
+export const createNewSource = (data: QueryData, title = ''): [QueryData, string] => {
+ const id = `source-${data.sourceIndex}`;
+ return [
+ {
+ ...data,
+ sources: {
+ ...data.sources,
+ [id]: {
+ id,
+ title: title !== '' ? title : `Source-${data.sourceIndex + 1}`,
+ operators: [],
+ },
+ },
+ sourceOrder: [...data.sourceOrder, id],
+ sourceIndex: data.sourceIndex + 1,
+ },
+ id,
+ ];
+};
+
+export const createNewLoader = (data: QueryData, operator: Operator, title = ''): [QueryData, string] => {
+ const [sourceData, sourceId] = createNewSource(data, title);
+ return createNewOperator(sourceData, sourceId, operator);
+};
+
+export const createNewAggregator = (data: QueryData): [QueryData, string] => {
+ return [{ ...data, aggregate: new AggregatorOperator(AggregationMethods.Max) }, AGGREGATOR_CONFIG];
+};
+
+export const emptyQueryData: QueryData = {
+ operators: {},
+ sources: {},
+ sourceOrder: [],
+
+ sourceIndex: 0,
+ operatorIndex: 0,
+};
diff --git a/src/state/user.ts b/src/state/user.ts
new file mode 100644
index 0000000..d218a9e
--- /dev/null
+++ b/src/state/user.ts
@@ -0,0 +1,27 @@
+import { createContext } from 'react';
+
+interface User {
+ email: string;
+ isLoggedIn: boolean;
+}
+
+interface UserContextState {
+ user: User;
+ login: (email: string) => void;
+ logout: () => void;
+}
+
+export const emptyUser: User = {
+ isLoggedIn: false,
+ email: '',
+};
+
+export const emptyUserState: UserContextState = {
+ user: { ...emptyUser },
+ login: () => {},
+ logout: () => {},
+};
+
+const UserContext = createContext({ ...emptyUserState });
+
+export default UserContext;
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..e54e8f8
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,12 @@
+module.exports = {
+ purge: [],
+ purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
+ darkMode: false, // or 'media' or 'class'
+ theme: {
+ extend: {},
+ },
+ variants: {
+ extend: {},
+ },
+ plugins: [require('@tailwindcss/forms')],
+};