replace old query builder

This commit is contained in:
Stephanie Yang 2021-07-28 10:51:35 -05:00
parent fa353faf72
commit d8ac3251f6
31 changed files with 851 additions and 2247 deletions

View file

@ -9,7 +9,7 @@ import { Nav } from 'shared/Nav';
import { NotFound } from 'App/NotFound';
import { MoleculaTablesContainer } from 'App/MoleculaTables';
import { QueryContainer } from 'App/Query';
import { QBuilderContainer } from 'App/QBuilder';
import { QueryBuilderContainer } from 'App/QueryBuilder';
import css from './App.module.scss';
const App = () => {
@ -45,7 +45,7 @@ const App = () => {
<Route exact path="/" component={Home} />
<Route path="/tables/:id?" component={MoleculaTablesContainer} />
<Route exact path="/query" component={QueryContainer} />
<Route exact path="/querybuilder" component={QBuilderContainer} />
<Route exact path="/querybuilder" component={QueryBuilderContainer} />
<Route component={NotFound} />
</Switch>
</div>

View file

@ -1,52 +0,0 @@
.sharedConfig {
padding: 32px 32px 0;
.configSelector {
margin-bottom: 24px;
}
}
.textLink {
color: var(--primary);
font-size: 12px;
.icon {
margin-right: 4px;
vertical-align: text-top;
}
&:hover {
cursor: pointer;
text-decoration: underline;
color: var(--primary);
}
}
.builder {
display: flex;
flex-direction: column;
height: 100%;
.queryMetadata {
border-bottom: 1px solid var(--divider);
padding-bottom: 16px;
margin-bottom: 16px;
}
}
.config {
padding: 24px 32px 0;
flex-grow: 1;
}
.builderActions {
padding: 16px 32px;
display: flex;
justify-content: space-between;
.mainActions {
display: grid;
grid-template-columns: auto auto;
grid-gap: 8px;
}
}

View file

@ -1,388 +0,0 @@
import React, { FC, Fragment, useEffect, useState } from 'react';
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
import Button from '@material-ui/core/Button';
import CircularProgress from '@material-ui/core/CircularProgress';
import Divider from '@material-ui/core/Divider';
import Popover from '@material-ui/core/Popover';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import {
cleanupRows,
stringifyCount,
stringifyExtract,
stringifyGroupBy
} from './utils';
import { CountBuilder } from './CountBuilder';
import { ExtractBuilder } from './ExtractBuilder';
import { GroupByBuilder } from './GroupByBuilder';
import { Select } from 'shared/Select';
import css from './QBuilder.module.scss';
type QBuilderProps = {
tables: any[];
savedQuery: number;
onRun: (
table: string,
operation: string,
query: string,
countQuery?: string
) => void;
onClear: () => void;
onExitEdit: () => void;
onSaveQuery: () => void;
};
export const QBuilder: FC<QBuilderProps> = ({
tables,
savedQuery,
onRun,
onClear,
onExitEdit,
onSaveQuery
}) => {
const [saveButtonEl, setSaveButtonEl] = useState<null | HTMLElement>(null);
const queriesList = JSON.parse(localStorage.getItem('saved-queries') || '[]');
const [editMode, setEditMode] = useState<boolean>(false);
const [selectedTable, setSelectedTable] = useState<any>(tables[0]);
const [operation, setOperation] = useState<string>('Extract');
const [query, setQuery] = useState<any>({
table: tables[0].name,
operation: 'Extract',
columns: tables[0].fields.map((field) => field.name)
});
const [showInvalid, setShowInvalid] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false);
const [queryNameIdx, setQueryNameIdx] = useState<number>(-1);
useEffect(() => {
if (savedQuery > -1) {
const updatedList = JSON.parse(
localStorage.getItem('saved-queries') || '[]'
);
setQuery(updatedList[savedQuery]);
setEditMode(true);
setOperation(updatedList[savedQuery].operation);
const table = tables.find(
(table) => table.name === updatedList[savedQuery].table
);
setSelectedTable(table);
}
}, [savedQuery, tables]);
useEffect(() => {
if (query?.name) {
setQueryNameIdx(queriesList.findIndex((q) => q.name === query.name));
}
}, [query, queriesList]);
const cleanRows = () => {
const cleanRows = cleanupRows(query?.rowCalls);
return {
...query,
rowCalls: cleanRows.cleanRowCalls,
isInvalid: cleanRows.isInvalid
};
};
const runQuery = () => {
setShowInvalid(true);
let queryString = '';
let countQuery = '';
switch (operation) {
case 'Extract':
const cleanExtractQuery = cleanRows();
setQuery(cleanExtractQuery);
const extract = stringifyExtract(cleanExtractQuery);
if (!extract.error && !cleanExtractQuery.isInvalid) {
queryString = extract.queryString;
countQuery = extract.countQuery;
}
break;
case 'GroupBy':
queryString = stringifyGroupBy(query);
break;
case 'Count':
const cleanCountQuery = cleanRows();
setQuery(cleanCountQuery);
const count = stringifyCount(cleanCountQuery);
if (!count.error && !cleanCountQuery.isInvalid) {
queryString = count.queryString;
}
break;
default:
break;
}
if (queryString && !query.isInvalid) {
setShowInvalid(false);
onRun(selectedTable.name, operation, queryString, countQuery);
} else {
setShowInvalid(true);
}
};
const reset = () => {
setQuery({
table: selectedTable.name,
columns: selectedTable.fields.map(field => field.name),
operation,
isInvalid: false
});
setOperation('Extract');
setShowInvalid(false);
setEditMode(false);
onExitEdit();
onClear();
};
const onSave = () => {
setUpdating(true);
setTimeout(() => {
setUpdating(false);
}, 1500);
const cleanQuery = cleanRows();
let queries = [...queriesList];
if (editMode && queries.length > 0) {
const clone = [...queries];
clone.splice(savedQuery, 1, cleanQuery);
localStorage.setItem('saved-queries', JSON.stringify(clone));
} else {
queries.push(cleanQuery);
localStorage.setItem('saved-queries', JSON.stringify(queries));
}
onSaveQuery();
};
const renderBuilder = () => {
switch (operation) {
case 'Extract':
return (
<ExtractBuilder
table={selectedTable}
showInvalid={showInvalid}
query={query}
onChange={(q) => setQuery(q)}
/>
);
case 'GroupBy':
return (
<GroupByBuilder
table={selectedTable}
showInvalid={showInvalid}
query={query}
onChange={(q) => setQuery(q)}
/>
);
case 'Count':
return (
<CountBuilder
table={selectedTable}
showInvalid={showInvalid}
query={query}
onChange={(q) => setQuery(q)}
/>
);
default:
return <div>Invalid Operation</div>;
}
};
return (
<div className={css.builder}>
<div className={css.sharedConfig}>
{editMode ? (
<span className={css.textLink} onClick={reset}>
<ArrowBackIcon fontSize="inherit" className={css.icon} />
Back to new query
</span>
) : null}
<Typography variant="h5" color="textSecondary" paragraph>
{editMode ? 'Edit Saved Query' : 'New Query'}
</Typography>
{editMode ? (
<div className={css.queryMetadata}>
<TextField
variant="outlined"
label="Query Name"
placeholder="Unique name to identify query"
value={query?.name}
error={
!query?.name ||
(queryNameIdx >= 0 && queryNameIdx !== savedQuery)
}
helperText={
queryNameIdx >= 0 && queryNameIdx !== savedQuery
? 'Query name must be unique'
: ''
}
onChange={(event) =>
setQuery({ ...query, name: event.target.value })
}
margin="normal"
size="small"
required
fullWidth
/>
<TextField
variant="outlined"
label="Description"
value={query?.description}
onChange={(event) =>
setQuery({ ...query, description: event.target.value })
}
margin="normal"
size="small"
fullWidth
/>
</div>
) : null}
<Select
className={css.configSelector}
label="Table"
value={selectedTable.name}
options={tables.map((table) => {
return { label: table.name, value: table.name };
})}
onChange={(value) => {
const table = tables.find((t) => t.name === value);
setSelectedTable(table);
setShowInvalid(false);
setQuery({
table: table.name,
columns: table.fields.map((field) => field.name),
operation: query.operation,
isInvalid: false
});
}}
/>
<Select
className={css.configSelector}
label="Operation"
value={operation}
options={[
{ label: 'Extract', value: 'Extract' },
{ label: 'Count', value: 'Count' },
{ label: 'GroupBy', value: 'GroupBy' }
]}
onChange={(value) => {
setOperation(value);
setShowInvalid(false);
setQuery({
table: selectedTable.name,
operation: value,
isInvalid: false
});
}}
/>
<Divider />
</div>
<div className={css.config}>{renderBuilder()}</div>
<div className={css.builderActions}>
<div className={css.mainActions}>
<Button variant="contained" color="primary" onClick={runQuery}>
Run
</Button>
{editMode ? (
<Button
variant="contained"
color="default"
onClick={onSave}
disabled={
!query?.name || (savedQuery >= 0 && queryNameIdx !== savedQuery)
}
>
{updating ? <CircularProgress size="0.875rem" /> : 'Update'}
</Button>
) : (
<Fragment>
<Button
variant="contained"
color="default"
onClick={(event) => setSaveButtonEl(event.currentTarget)}
>
Save
</Button>
<Popover
open={!!saveButtonEl}
anchorEl={saveButtonEl}
onClose={() => setSaveButtonEl(null)}
anchorOrigin={{
vertical: 'top',
horizontal: 'left'
}}
transformOrigin={{
vertical: 'bottom',
horizontal: 'left'
}}
>
<div className={css.savePopover}>
<Typography variant="caption" paragraph>
Give your query a unique name and a short description to
help you identify it for future use.
</Typography>
<TextField
variant="outlined"
label="Query Name"
placeholder="Unique name to identify query"
value={query?.name ? query.name : ''}
error={queryNameIdx >= 0}
helperText={
queryNameIdx >= 0 ? 'Query name must be unique' : ''
}
onChange={(event) =>
setQuery({ ...query, name: event.target.value })
}
margin="dense"
required
fullWidth
/>
<TextField
variant="outlined"
label="Description"
value={query?.description ? query.description : ''}
onChange={(event) =>
setQuery({ ...query, description: event.target.value })
}
margin="dense"
fullWidth
/>
<div className={css.popoverActions}>
<div className={css.saveActions}>
<Button
variant="contained"
color="default"
onClick={() => {
onSave();
setSaveButtonEl(null);
}}
disabled={!query?.name || queryNameIdx >= 0}
>
Ok
</Button>
<Button
color="default"
onClick={() => setSaveButtonEl(null)}
>
Cancel
</Button>
</div>
</div>
</div>
</Popover>
</Fragment>
)}
</div>
<Button onClick={reset}>Clear</Button>
</div>
</div>
);
};

View file

@ -1,66 +0,0 @@
.builderColumn,
.results {
height: calc(100vh - 64px);
overflow: scroll;
position: relative;
.openClose {
position: absolute;
right: 10px;
top: 10px;
}
}
.split {
display: flex;
}
.collapsedBuilder {
border-right: 1px solid rgba(var(--contrast-rgb), 0.1);
text-align: center;
padding-top: 10px;
}
.resultsBlock {
height: 100%;
}
.resultsHeader {
display: flex;
align-items: center;
justify-content: space-between;
.download {
display: flex;
align-items: center;
}
.downloadInfo {
margin-right: 8px;
}
}
.infoMessage {
padding: 16px;
border: 1px solid rgba(var(--primary-rgb), 0.5);
background: rgba(var(--primary-rgb), 0.1);
border-radius: 4px;
margin: 4px 0 16px;
}
.spacer{
padding-bottom: 16px;
}
.resultsLoading {
margin-left: 8px;
}
.savedQueries {
margin-top: 48px;
}
.noTables {
margin-top: 20px;
padding: 12px 16px;
}

View file

@ -1,323 +0,0 @@
import React, { Fragment, useState } from 'react';
import Alert from '@material-ui/lab/Alert';
import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
import Button from '@material-ui/core/Button';
import classNames from 'classnames';
import CloseIcon from '@material-ui/icons/Close';
import IconButton from '@material-ui/core/IconButton';
import InfoIcon from '@material-ui/icons/Info';
import moment, { Moment } from 'moment';
import Paper from '@material-ui/core/Paper';
import Split from 'react-split';
import Snackbar from '@material-ui/core/Snackbar';
import Tooltip from '@material-ui/core/Tooltip';
import Typography from '@material-ui/core/Typography';
import { Block } from 'shared/Block';
import { formatDuration } from 'shared/utils/formatDuration';
import { grpc } from '@improbable-eng/grpc-web';
import { pilosa } from 'services/eventServices';
import { QBuilder } from './QBuilder';
import { queryPQL } from 'services/grpcServices';
import { QueryResults } from 'App/Query/QueryResults';
import { ResultType } from 'App/Query/QueryContainer';
import { RowResponse } from 'proto/pilosa_pb';
import { SavedQueries } from 'App/QueryBuilder/SavedQueries';
import { useEffectOnce } from 'react-use';
import css from './QBuilderContainer.module.scss';
let streamingResults: ResultType = {
query: '',
operation: '',
type: 'PQL',
headers: [],
rows: [],
roundtrip: 0,
error: ''
};
export const QBuilderContainer = () => {
let startTime: Moment;
let exportRows: any[] = [];
const colSizes = JSON.parse(
localStorage.getItem('builderColSizes') || '[25, 75]'
);
const [queriesList, setQueriesList] = useState(
JSON.parse(localStorage.getItem('saved-queries') || '[]')
);
const [tables, setTables] = useState<any[]>([]);
const [showBuilder, setShowBuilder] = useState<boolean>(true);
const [results, setResults] = useState<ResultType>();
const [fullCount, setFullCount] = useState<number>();
const [recordsCount, setRecordsCount] = useState<number>();
const [errorResult, setErrorResult] = useState<ResultType>();
const [error, setError] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [savedQuery, setSavedQuery] = useState<number>(-1);
useEffectOnce(() => {
pilosa.get.schema().then((res) => {
setTables(res.data.indexes);
});
});
const handleQueryMessages = (message: RowResponse) => {
const response = message.toObject();
if (response.headersList.length > 0 && response.duration > 0) {
streamingResults.headers = response.headersList;
streamingResults.duration = response.duration;
}
streamingResults.rows.push(response.columnsList);
};
const handleQueryEnd = (status: grpc.Code, statusMessage: string) => {
if (status !== grpc.Code.OK) {
streamingResults.error = statusMessage;
setErrorResult(streamingResults);
} else {
streamingResults.roundtrip = moment
.duration(moment().diff(startTime))
.as('milliseconds');
setErrorResult(undefined);
setResults(streamingResults);
}
setLoading(false);
};
const handleExternalLookup = (message: RowResponse) => {
const response = message.toObject();
let rowStr: string[] = [];
if (exportRows.length === 0) {
const headers = response.headersList.map((header) => header.name);
exportRows.push(headers.join('\t'));
}
response.headersList.forEach((header, idx) =>
rowStr.push(response.columnsList[idx][`${header.datatype}val`])
);
exportRows.push(rowStr.join('\t'));
};
const handleExternalLookupEnd = (
status: grpc.Code,
statusMessage: string
) => {
if (status !== grpc.Code.OK) {
setError(statusMessage);
} else if (exportRows.length === 0) {
setError('No record attributes for current query.');
} else {
const dateTime = moment().unix();
const element = document.createElement('a');
const file = new Blob([exportRows.join('\n')], {
type: 'text/plain;charset=utf-8'
});
element.href = URL.createObjectURL(file);
element.download = `molecula-${results?.index}-${dateTime}.csv`;
document.body.appendChild(element);
element.click();
exportRows = [];
}
};
const onRunQuery = (
table: string,
operation: string,
query: string,
countQuery?: string
) => {
streamingResults = {
query,
operation,
type: 'PQL',
headers: [],
rows: [],
index: table,
roundtrip: 0,
error: ''
};
startTime = moment();
setLoading(true);
if (operation !== 'Count') {
if (countQuery) {
pilosa.post.query(table, countQuery).then((res) => {
setFullCount(res.data.results[0]);
});
}
pilosa.post
.query(table, `Count(All())`)
.then((res) => setRecordsCount(res.data.results[0]));
} else {
setFullCount(undefined);
}
queryPQL(table, query, handleQueryMessages, handleQueryEnd);
};
const onClear = () => {
setResults(undefined);
setFullCount(undefined);
setErrorResult(undefined);
};
const onRemoveQuery = (queryIdx: number) => {
let queries = [...queriesList];
queries.splice(queryIdx, 1);
localStorage.setItem('saved-queries', JSON.stringify(queries));
setQueriesList(queries);
};
const onSaveQuery = () => {
const updatedList = JSON.parse(
localStorage.getItem('saved-queries') || '[]'
);
if (savedQuery < 0) {
setSavedQuery(updatedList.length - 1);
}
setQueriesList(updatedList);
};
const onExportLogs = () => {
if (results) {
const columns = results.rows.map((row) => row[0].uint64val);
const table = results.index ? results.index : '';
onExternalLookup(table, columns);
}
};
const onExternalLookup = (table: string, columns: number[]) => {
const cols = JSON.stringify(columns);
const query = `ExternalLookup(ConstRow(columns=${cols}), query='select id, "rawlog" from "${table}" where id = ANY($1)')`;
queryPQL(table, query, handleExternalLookup, handleExternalLookupEnd);
};
return (
<Fragment>
{tables.length > 0 ? (
<Split
sizes={showBuilder ? colSizes : [0, 100]}
cursor="col-resize"
minSize={showBuilder ? 350 : 50}
onDragEnd={(sizes) =>
localStorage.setItem('builderColSizes', JSON.stringify(sizes))
}
gutter={(_index, direction) => {
const gutter = document.createElement('div');
gutter.className = `gutter gutter-${direction}`;
const dragbars = document.createElement('div');
dragbars.className = 'dragBar';
gutter.appendChild(dragbars);
return gutter;
}}
className={classNames(css.split, !showBuilder ? 'hide-gutter' : '')}
>
{showBuilder ? (
<div className={css.builderColumn}>
<div className={css.openClose}>
<IconButton onClick={() => setShowBuilder(false)} size="small">
<CloseIcon />
</IconButton>
</div>
<QBuilder
tables={tables}
savedQuery={savedQuery}
onRun={onRunQuery}
onClear={onClear}
onExitEdit={() => setSavedQuery(-1)}
onSaveQuery={onSaveQuery}
/>
</div>
) : (
<div className={css.collapsedBuilder}>
<IconButton onClick={() => setShowBuilder(true)} size="small">
<ArrowForwardIosIcon />
</IconButton>
</div>
)}
<div className={css.results}>
<Block className={css.resultsBlock}>
<div className={css.resultsHeader}>
<Typography variant="h5" color="textSecondary">
Results{' '}
</Typography>
{results?.query.includes('Extract(') ? (
<div className={css.download}>
<Tooltip
className={css.downloadInfo}
title="Download raw data from query results"
placement="top"
arrow
>
<InfoIcon fontSize="inherit" />
</Tooltip>
<Button onClick={onExportLogs}>Download</Button>
</div>
) : null}
</div>
{loading ? <div>Loading...</div> : null}
{results && !loading ? (
<Fragment>
{fullCount && recordsCount ? (
<div className={css.infoMessage}>
{results.duration ? (
<div>
{recordsCount.toLocaleString()} records scanned in{' '}
{formatDuration(results.duration, true)}.
</div>
) : null}
<div>
Showing{' '}
{fullCount > 1000 ? 'first 1,000 rows of' : 'all'}{' '}
{fullCount.toLocaleString()} results.
</div>
</div>
) : null}
<QueryResults results={results} />
<div className={css.spacer} />
</Fragment>
) : null}
{errorResult && !loading ? <div>{errorResult.error}</div> : null}
{!loading && !results && !error ? (
<Fragment>
<Typography variant="caption" color="textSecondary">
Build a query to see results
</Typography>
{queriesList.length > 0 ? (
<div className={css.savedQueries}>
<Typography variant="h5" color="textSecondary">
Saved Queries
</Typography>
<SavedQueries
queries={queriesList}
tables={tables}
onClickSaved={(queryIdx) => setSavedQuery(queryIdx)}
onRemoveQuery={onRemoveQuery}
/>
</div>
) : null}
</Fragment>
) : null}
</Block>
</div>
<Snackbar open={!!error}>
<Alert severity="info" onClose={() => setError('')}>
{error}
</Alert>
</Snackbar>
</Split>
) : (
<Block>
<Typography variant="h5" color="textSecondary">
Query Builder
</Typography>
<Paper className={css.noTables}>
<Typography variant="caption" color="textSecondary">
There are no tables to query.
</Typography>
</Paper>
</Block>
)}
</Fragment>
);
};

View file

@ -1 +0,0 @@
export * from './helpers';

View file

@ -1,2 +0,0 @@
export * from './QBuilderContainer';
export * from './rowTypes';

View file

@ -1,22 +0,0 @@
export type Operator = 'and' | 'or';
export const groupOperators: Operator[] = ['and', 'or'];
export type RowGrouping = {
row: RowCallType[];
isNot?: boolean;
operator?: Operator;
}
export type RowCallType = {
field: string;
rowOperator: string;
value: string;
type: string;
keys?: boolean;
};
export type RowsCallType = {
primary: string;
secondary: string;
};

View file

@ -1,5 +1,6 @@
import React, { FC } from 'react';
import { RowCallBuilder } from 'App/QBuilder/RowCallBuilder';
import { Operator, RowGrouping } from 'App/QueryBuilder/rowTypes';
import { RowCallBuilder } from 'App/QueryBuilder/RowCallBuilder';
type CountBuilderProps = {
table: any;
@ -22,12 +23,11 @@ export const CountBuilder: FC<CountBuilderProps> = ({
rowCalls={rowCalls}
fields={table.fields}
showInvalid={showInvalid}
onChange={(rowCalls, operator, isInvalid) => {
onChange={(rowCalls: RowGrouping[], operator?: Operator) => {
onChange({
...query,
rowCalls,
operator,
isInvalid
operator
});
}}
/>

View file

@ -2,7 +2,8 @@ import React, { FC, useState } from 'react';
import InfoIcon from '@material-ui/icons/Info';
import Tooltip from '@material-ui/core/Tooltip';
import { ColumnSelector } from 'App/QueryBuilder/ColumnSelector';
import { RowCallBuilder } from 'App/QBuilder/RowCallBuilder';
import { Operator, RowGrouping } from 'App/QueryBuilder/rowTypes';
import { RowCallBuilder } from 'App/QueryBuilder/RowCallBuilder';
import { useEffectOnce } from 'react-use';
import css from './ExtractBuilder.module.scss';
@ -78,12 +79,11 @@ export const ExtractBuilder: FC<ExtractBuilderProps> = ({
rowCalls={rowCalls}
fields={table.fields}
showInvalid={showInvalid}
onChange={(rowCalls, operator, isInvalid) => {
onChange={(rowCalls: RowGrouping[], operator?: Operator) => {
onChange({
...query,
rowCalls,
operator,
isInvalid
operator
});
}}
/>

View file

@ -5,7 +5,7 @@ import Typography from '@material-ui/core/Typography';
import { GroupBySort } from 'App/QueryBuilder/GroupBySort';
import { SavedQueries } from 'App/QueryBuilder/SavedQueries';
import { Select } from 'shared/Select';
import { stringifyRowData } from 'App/QueryBuilder/stringifyRowData';
import { stringifyRowData } from 'App/QueryBuilder/utils';
import css from './GroupByBuilder.module.scss';
type GroupByBuilderProps = {
@ -131,7 +131,7 @@ export const GroupByBuilder: FC<GroupByBuilderProps> = ({
const { rowCalls, operator } = filters[queryIdx];
const res = stringifyRowData(rowCalls, operator);
if (!res.error) {
onChange({ ...query, filter: res.query });
onChange({ ...query, filter: res.queryString });
}
}}
/>

View file

@ -1,154 +1,12 @@
.builderColumn,
.results {
height: calc(100vh - 64px);
overflow: scroll;
position: relative;
.openClose {
position: absolute;
right: 10px;
top: 10px;
}
}
.resultsBlock {
height: 100%;
}
.builderColumn {
display: grid;
grid-template-rows: auto 1fr auto;
}
.results {
padding-bottom: 20px;
}
.resultsHeader {
display: flex;
align-items: center;
justify-content: space-between;
.download {
display: flex;
align-items: center;
}
.downloadInfo {
margin-right: 8px;
}
}
.collapsedBuilder {
border-right: 1px solid rgba(var(--contrast-rgb), 0.1);
text-align: center;
padding-top: 10px;
}
.builderHeader {
.sharedConfig {
padding: 32px 32px 0;
}
.builder {
overflow-y: scroll;
padding: 20px 32px;
.queryMetadata,
.tableSelector,
.operationSelector {
margin-bottom: 16px;
}
.columnsSelector {
margin: 4px 14px;
display: flex;
text-align: center;
.info {
margin-left: 4px;
fill: var(--text-secondary);
}
}
.queryMetadata {
border-bottom: 1px solid var(--divider);
padding-bottom: 16px;
.configSelector {
margin-bottom: 24px;
}
}
.builderActions {
padding: 16px 32px;
display: flex;
justify-content: space-between;
.mainActions {
display: grid;
grid-template-columns: auto auto;
grid-gap: 8px;
}
}
.newGroup,
.operator {
display: flex;
align-items: center;
width: 100%;
.line {
flex-grow: 1;
height: 1px;
background: var(--divider);
}
.addButton {
margin: 0 8px;
}
}
.groupBy {
.line {
flex-grow: 1;
height: 1px;
background: var(--divider);
}
.fieldSelector {
margin-top: 16px;
}
.filterHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
}
.filterInfo {
vertical-align: text-top;
margin-left: 4px;
}
}
.savePopover {
width: 300px;
.popoverActions {
display: flex;
}
.saveActions {
display: grid;
grid-template-columns: auto auto;
grid-gap: 8px;
padding-top: 16px;
}
}
.menuPaper {
padding: 0 !important;
}
.link {
.textLink {
color: var(--primary);
font-size: 12px;
@ -164,18 +22,31 @@
}
}
.infoMessage {
padding: 16px;
border: 1px solid rgba(var(--primary-rgb), 0.5);
background: rgba(var(--primary-rgb), 0.1);
border-radius: 4px;
margin: 4px 0 16px;
.builder {
display: flex;
flex-direction: column;
height: 100%;
.queryMetadata {
border-bottom: 1px solid var(--divider);
padding-bottom: 16px;
margin-bottom: 16px;
}
}
.resultsLoading {
margin-left: 8px;
.config {
padding: 24px 32px 0;
flex-grow: 1;
}
.savedQueries {
margin-top: 48px;
.builderActions {
padding: 16px 32px;
display: flex;
justify-content: space-between;
.mainActions {
display: grid;
grid-template-columns: auto auto;
grid-gap: 8px;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,65 @@
.builderColumn,
.results {
height: calc(100vh - 64px);
overflow: scroll;
position: relative;
.openClose {
position: absolute;
right: 10px;
top: 10px;
}
}
.split {
display: flex;
}
.collapsedBuilder {
border-right: 1px solid rgba(var(--contrast-rgb), 0.1);
text-align: center;
padding-top: 10px;
}
.resultsBlock {
height: 100%;
}
.resultsHeader {
display: flex;
align-items: center;
justify-content: space-between;
.download {
display: flex;
align-items: center;
}
.downloadInfo {
margin-right: 8px;
}
}
.infoMessage {
padding: 16px;
border: 1px solid rgba(var(--primary-rgb), 0.5);
background: rgba(var(--primary-rgb), 0.1);
border-radius: 4px;
margin: 4px 0 16px;
}
.spacer{
padding-bottom: 16px;
}
.resultsLoading {
margin-left: 8px;
}
.savedQueries {
margin-top: 48px;
}
.noTables {
margin-top: 20px;
padding: 12px 16px;

View file

@ -1,20 +1,28 @@
import React, { Fragment, useState } from 'react';
import moment, { Moment } from 'moment';
import Alert from '@material-ui/lab/Alert';
import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
import Button from '@material-ui/core/Button';
import classNames from 'classnames';
import CloseIcon from '@material-ui/icons/Close';
import IconButton from '@material-ui/core/IconButton';
import InfoIcon from '@material-ui/icons/Info';
import moment, { Moment } from 'moment';
import Paper from '@material-ui/core/Paper';
import Split from 'react-split';
import Snackbar from '@material-ui/core/Snackbar';
import Tooltip from '@material-ui/core/Tooltip';
import Typography from '@material-ui/core/Typography';
import { Block } from 'shared/Block';
import { Operator, RowGrouping, RowsCallType } from './rowTypes';
import { formatDuration } from 'shared/utils/formatDuration';
import { grpc } from '@improbable-eng/grpc-web';
import { pilosa } from 'services/eventServices';
import { QueryBuilder } from './QueryBuilder';
import { useEffectOnce } from 'react-use';
import { ResultType } from 'App/Query/QueryContainer';
import { queryPQL } from 'services/grpcServices';
import { grpc } from '@improbable-eng/grpc-web';
import { QueryResults } from 'App/Query/QueryResults';
import { ResultType } from 'App/Query/QueryContainer';
import { RowResponse } from 'proto/pilosa_pb';
import { SortOption } from './GroupBySort';
import { stringifyRowData } from './stringifyRowData';
import { SavedQueries } from 'App/QueryBuilder/SavedQueries';
import { useEffectOnce } from 'react-use';
import css from './QueryBuilderContainer.module.scss';
let streamingResults: ResultType = {
@ -30,13 +38,22 @@ let streamingResults: ResultType = {
export const QueryBuilderContainer = () => {
let startTime: Moment;
let exportRows: any[] = [];
const [tables, setTables] = useState<any[]>();
const colSizes = JSON.parse(
localStorage.getItem('builderColSizes') || '[25, 75]'
);
const [queriesList, setQueriesList] = useState(
JSON.parse(localStorage.getItem('saved-queries') || '[]')
);
const [tables, setTables] = useState<any[]>([]);
const [showBuilder, setShowBuilder] = useState<boolean>(true);
const [results, setResults] = useState<ResultType>();
const [fullCount, setFullCount] = useState<number>();
const [recordsCount, setRecordsCount] = useState<number>();
const [errorResult, setErrorResult] = useState<ResultType>();
const [error, setError] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [savedQuery, setSavedQuery] = useState<number>(-1);
useEffectOnce(() => {
pilosa.get.schema().then((res) => {
@ -67,62 +84,6 @@ export const QueryBuilderContainer = () => {
setLoading(false);
};
const onRunQuery = (
table: any,
operation: string,
rowData: RowGrouping[],
columnsList: string[],
operator?: Operator
) => {
streamingResults = {
query: '',
operation,
type: 'PQL',
headers: [],
rows: [],
index: table.name,
roundtrip: 0,
error: ''
};
startTime = moment();
setLoading(true);
let query: string = '';
if (rowData.length === 0) {
query = 'All()';
} else {
const res = stringifyRowData(rowData, operator);
if (res.error) {
streamingResults.error = res.query;
} else {
query = res.query;
}
}
if (operation !== 'Count') {
const countQuery = `Count(${query})`;
pilosa.post
.query(table.name, countQuery)
.then((res) => setFullCount(res.data.results[0]));
pilosa.post
.query(table.name, `Count(All())`)
.then((res) => setRecordsCount(res.data.results[0]));
} else {
setFullCount(undefined);
}
if (operation === 'Extract') {
const fields = columnsList.map((field) => `Rows(${field})`);
const allRows = fields.join(', ');
query = `${operation}(Limit(${query}, limit=1000), ${allRows})`;
} else {
query = `${operation}(${query})`;
}
streamingResults.query = query;
queryPQL(table.name, query, handleQueryMessages, handleQueryEnd);
};
const handleExternalLookup = (message: RowResponse) => {
const response = message.toObject();
let rowStr: string[] = [];
@ -158,46 +119,71 @@ export const QueryBuilderContainer = () => {
}
};
const onRunGroupBy = (
table: any,
rowsData: RowsCallType,
sort: SortOption[],
filter?: string
const onRunQuery = (
table: string,
operation: string,
query: string,
countQuery?: string
) => {
streamingResults = {
query: '',
operation: 'GroupBy',
query,
operation,
type: 'PQL',
headers: [],
rows: [],
index: table.name,
index: table,
roundtrip: 0,
error: ''
};
startTime = moment();
setLoading(true);
let sortString = '';
let aggregateString = '';
if (sort.length > 0) {
sortString = sort[0].sortValue;
if (sort[0].sortValue.includes('sum')) {
aggregateString = `, aggregate=Sum(field=${sort[0].field})`;
if (operation !== 'Count') {
if (countQuery) {
pilosa.post.query(table, countQuery).then((res) => {
setFullCount(res.data.results[0]);
});
}
if (sort.length > 1) {
sortString = `${sortString}, ${sort[1].sortValue}`;
if (sort[1].sortValue.includes('sum')) {
aggregateString = `, aggregate=Sum(field=${sort[1].field})`;
}
}
sortString = `, sort="${sortString}"`;
pilosa.post
.query(table, `Count(All())`)
.then((res) => setRecordsCount(res.data.results[0]));
} else {
setFullCount(undefined);
}
queryPQL(table, query, handleQueryMessages, handleQueryEnd);
};
const onClear = () => {
setResults(undefined);
setFullCount(undefined);
setErrorResult(undefined);
};
const onRemoveQuery = (queryIdx: number) => {
let queries = [...queriesList];
queries.splice(queryIdx, 1);
localStorage.setItem('saved-queries', JSON.stringify(queries));
setQueriesList(queries);
};
const onSaveQuery = () => {
const updatedList = JSON.parse(
localStorage.getItem('saved-queries') || '[]'
);
if (savedQuery < 0) {
setSavedQuery(updatedList.length - 1);
}
setQueriesList(updatedList);
};
const onExportLogs = () => {
if (results) {
const columns = results.rows.map((row) => row[0].uint64val);
const table = results.index ? results.index : '';
onExternalLookup(table, columns);
}
const filterString =
filter && filter.length > 0 ? `, filter=${filter}` : '';
const query = rowsData.secondary
? `GroupBy(Rows(${rowsData.primary}), Rows(${rowsData.secondary})${filterString}${sortString}${aggregateString})`
: `GroupBy(Rows(${rowsData.primary})${filterString}${sortString}${aggregateString})`;
streamingResults.query = query;
queryPQL(table.name, query, handleQueryMessages, handleQueryEnd);
};
const onExternalLookup = (table: string, columns: number[]) => {
@ -206,40 +192,132 @@ export const QueryBuilderContainer = () => {
queryPQL(table, query, handleExternalLookup, handleExternalLookupEnd);
};
return tables && tables.length > 0 ? (
return (
<Fragment>
<QueryBuilder
tables={tables}
results={results}
fullResultsCount={fullCount}
fullRecordsCount={recordsCount}
error={errorResult}
loading={loading}
onQuery={onRunQuery}
onRunGroupBy={onRunGroupBy}
onExternalLookup={onExternalLookup}
onClear={() => {
setResults(undefined);
setFullCount(undefined);
setErrorResult(undefined);
}}
/>
<Snackbar open={!!error}>
<Alert severity="info" onClose={() => setError('')}>
{error}
</Alert>
</Snackbar>
{tables.length > 0 ? (
<Split
sizes={showBuilder ? colSizes : [0, 100]}
cursor="col-resize"
minSize={showBuilder ? 350 : 50}
onDragEnd={(sizes) =>
localStorage.setItem('builderColSizes', JSON.stringify(sizes))
}
gutter={(_index, direction) => {
const gutter = document.createElement('div');
gutter.className = `gutter gutter-${direction}`;
const dragbars = document.createElement('div');
dragbars.className = 'dragBar';
gutter.appendChild(dragbars);
return gutter;
}}
className={classNames(css.split, !showBuilder ? 'hide-gutter' : '')}
>
{showBuilder ? (
<div className={css.builderColumn}>
<div className={css.openClose}>
<IconButton onClick={() => setShowBuilder(false)} size="small">
<CloseIcon />
</IconButton>
</div>
<QueryBuilder
tables={tables}
savedQuery={savedQuery}
onRun={onRunQuery}
onClear={onClear}
onExitEdit={() => setSavedQuery(-1)}
onSaveQuery={onSaveQuery}
/>
</div>
) : (
<div className={css.collapsedBuilder}>
<IconButton onClick={() => setShowBuilder(true)} size="small">
<ArrowForwardIosIcon />
</IconButton>
</div>
)}
<div className={css.results}>
<Block className={css.resultsBlock}>
<div className={css.resultsHeader}>
<Typography variant="h5" color="textSecondary">
Results{' '}
</Typography>
{results?.query.includes('Extract(') ? (
<div className={css.download}>
<Tooltip
className={css.downloadInfo}
title="Download raw data from query results"
placement="top"
arrow
>
<InfoIcon fontSize="inherit" />
</Tooltip>
<Button onClick={onExportLogs}>Download</Button>
</div>
) : null}
</div>
{loading ? <div>Loading...</div> : null}
{results && !loading ? (
<Fragment>
{fullCount && recordsCount ? (
<div className={css.infoMessage}>
{results.duration ? (
<div>
{recordsCount.toLocaleString()} records scanned in{' '}
{formatDuration(results.duration, true)}.
</div>
) : null}
<div>
Showing{' '}
{fullCount > 1000 ? 'first 1,000 rows of' : 'all'}{' '}
{fullCount.toLocaleString()} results.
</div>
</div>
) : null}
<QueryResults results={results} />
<div className={css.spacer} />
</Fragment>
) : null}
{errorResult && !loading ? <div>{errorResult.error}</div> : null}
{!loading && !results && !error ? (
<Fragment>
<Typography variant="caption" color="textSecondary">
Build a query to see results
</Typography>
{queriesList.length > 0 ? (
<div className={css.savedQueries}>
<Typography variant="h5" color="textSecondary">
Saved Queries
</Typography>
<SavedQueries
queries={queriesList}
tables={tables}
onClickSaved={(queryIdx) => setSavedQuery(queryIdx)}
onRemoveQuery={onRemoveQuery}
/>
</div>
) : null}
</Fragment>
) : null}
</Block>
</div>
<Snackbar open={!!error}>
<Alert severity="info" onClose={() => setError('')}>
{error}
</Alert>
</Snackbar>
</Split>
) : (
<Block>
<Typography variant="h5" color="textSecondary">
Query Builder
</Typography>
<Paper className={css.noTables}>
<Typography variant="caption" color="textSecondary">
There are no tables to query.
</Typography>
</Paper>
</Block>
)}
</Fragment>
) : (
<Block>
<Typography variant="h5" color="textSecondary">
Query Builder
</Typography>
<Paper className={css.noTables}>
<Typography variant="caption" color="textSecondary">
There are no tables to query.
</Typography>
</Paper>
</Block>
);
};

View file

@ -1,52 +0,0 @@
export const operators = {
decimal: [
{ label: '>', value: '>' },
{ label: '<', value: '<' },
{ label: '>=', value: '>=' },
{ label: '<=', value: '<=' },
{ label: '==', value: '=' },
{ label: '!=', value: '!=' },
],
int: [
{ label: '>', value: '>' },
{ label: '<', value: '<' },
{ label: '>=', value: '>=' },
{ label: '<=', value: '<=' },
{ label: '==', value: '=' },
{ label: '!=', value: '!=' },
],
'mutex-id': [
{ label: 'is', value: '=' },
{ label: 'is not', value: '!=' }
],
'mutex-keys': [
{ label: 'is', value: '=' },
{ label: 'is not', value: '!=' },
{ label: 'like', value: 'like' },
{ label: 'CIDR', value: 'cidr' }
],
"set-id": [
{ label: 'is', value: '=' },
{ label: 'is not', value: '!=' }
],
"set-keys": [
{ label: 'is', value: '=' },
{ label: 'is not', value: '!=' },
{ label: 'like', value: 'like' },
{ label: 'CIDR', value: 'cidr' }
],
"time-id": [
{ label: 'is', value: '=' },
{ label: 'is not', value: '!=' }
],
"time-keys": [
{ label: 'is', value: '=' },
{ label: 'is not', value: '!=' },
{ label: 'like', value: 'like' }
],
timestamp: [
{ label: 'is before', value: '<' },
{ label: 'is after', value: '>' },
{ label: 'is', value: '=' },
]
}

View file

@ -10,7 +10,7 @@ import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import { getIPRange } from 'get-ip-range';
import { operators } from './helpers';
import { groupOperators, Operator, RowCallType } from '../rowTypes';
import { groupOperators, Operator, RowCallType } from 'App/QueryBuilder/rowTypes';
import { Select } from 'shared/Select';
import MomentUtils from '@date-io/moment';
import { MuiPickersUtilsProvider, DateTimePicker } from '@material-ui/pickers';

View file

@ -10,18 +10,15 @@ import {
RowCallType,
RowGrouping
} from 'App/QueryBuilder/rowTypes';
import { RowCall } from 'App/QueryBuilder/RowCall';
import { RowCall } from './RowCall';
import css from './RowCallBuilder.module.scss';
import { useEffect } from 'react';
type RowCallBuilderProps = {
rowCalls: RowGrouping[];
fields: any[];
showInvalid: boolean;
onChange: (
rows: RowGrouping[],
operator?: Operator,
hasInvalid?: boolean
) => void;
onChange: (rows: RowGrouping[], operator?: Operator) => void;
};
export const RowCallBuilder: FC<RowCallBuilderProps> = ({
@ -34,7 +31,7 @@ export const RowCallBuilder: FC<RowCallBuilderProps> = ({
const [operatorEl, setOperatorEl] = useState<null | HTMLElement>(null);
const [operator, setOperator] = useState<Operator>();
const onNewGroup = () => {
const onNewGroup = (op?: Operator) => {
const newRow: RowCallType[] = [
{
field: '',
@ -45,19 +42,20 @@ export const RowCallBuilder: FC<RowCallBuilderProps> = ({
}
];
onChange([...rowCalls, { row: newRow }]);
onChange([...rowCalls, { row: newRow }], op ? op : operator);
};
const onRemoveGroup = (groupIdx: number) => {
if (groupIdx === 0) {
onChange(rowCalls.slice(1));
onChange(rowCalls.slice(1), operator);
} else {
const updatedRowCalls = [...rowCalls];
updatedRowCalls.splice(groupIdx, 1);
onChange(updatedRowCalls);
onChange(updatedRowCalls, operator);
if (updatedRowCalls.length < 1) {
setOperator(undefined);
onChange(updatedRowCalls);
}
}
};
@ -65,7 +63,7 @@ export const RowCallBuilder: FC<RowCallBuilderProps> = ({
const onUpdateRow = (
groupIdx: number,
newRowData?: RowCallType[],
operator?: Operator,
rowGroupOperator?: Operator,
isNot?: boolean
) => {
const updatedRowCalls = [...rowCalls];
@ -77,14 +75,14 @@ export const RowCallBuilder: FC<RowCallBuilderProps> = ({
};
if (newRowData.length <= 1) {
delete clone.operator;
} else if (operator) {
clone.operator = operator;
} else if (rowGroupOperator) {
clone.operator = rowGroupOperator;
}
updatedRowCalls.splice(groupIdx, 1, clone);
} else {
updatedRowCalls.splice(groupIdx, 1);
}
onChange(updatedRowCalls);
onChange(updatedRowCalls, operator);
};
return (
@ -167,7 +165,7 @@ export const RowCallBuilder: FC<RowCallBuilderProps> = ({
key={`operator-${op}`}
onClick={() => {
setOperator(op);
onNewGroup();
onNewGroup(op);
setNewOperatorEl(null);
}}
>

View file

@ -1,79 +0,0 @@
import { Operator, RowGrouping } from './rowTypes';
import { getIPRange } from 'get-ip-range';
export const stringifyRowData = (rowData: RowGrouping[], operator?: Operator) => {
let query = '';
let rowsMap: string[][] = [];
rowData.forEach((group, groupIdx) => {
rowsMap.push([]);
group.row.forEach((row) => {
let rowString = '';
const { field, rowOperator, value, type, keys } = row;
const isNegatory = rowOperator === '!=';
const isUnion =
['=', '!='].includes(rowOperator) && value.split(',').length > 1;
const operator = isNegatory ? '=' : rowOperator;
if (isUnion) {
const values = value.split(',');
const unionRows = values
.map((v) => keys ? `Row(${field}="${v.trim()}")` : `Row(${field}=${v.trim()})`)
.join(', ');
rowString = `Union(${unionRows})`;
} else if (rowOperator === 'cidr') {
try {
const ipRange = getIPRange(value);
const ipRows = ipRange
.map((ip) => `Row(${field}="${ip}")`)
.join(', ');
rowString = `Union(${ipRows})`;
} catch (error) {
return { error: true, query: error.message };
}
} else if (rowOperator === 'like') {
if (value.includes('%') || value.includes('_')) {
rowString = `UnionRows(Rows(field=${field}, like="${value}"))`;
} else {
rowString = `UnionRows(Rows(field=${field}, like="%${value}%"))`;
}
} else {
rowString = keys || type === 'timestamp'
? `Row(${field}${operator}"${value}")`
: `Row(${field}${operator}${value})`;
}
if (isNegatory) {
rowsMap[groupIdx].push(`Not(${rowString})`);
} else {
rowsMap[groupIdx].push(rowString);
}
});
});
query = rowsMap
.map((group, idx) => {
let joined = '';
if (group.length > 1) {
joined = group.map((r) => r).join(', ');
const operator = rowData[idx].operator;
if (operator === 'and') {
joined = `Intersect(${joined})`;
} else if (operator === 'or') {
joined = `Union(${joined})`;
}
} else {
joined = group[0];
}
return rowData[idx].isNot ? `Not(${joined})` : joined;
})
.join(', ');
if (rowData.length > 1 && operator) {
if (operator === 'and') {
query = `Intersect(${query})`;
} else if (operator === 'or') {
query = `Union(${query})`;
}
}
return { error: false, query };
}

View file

@ -1,4 +1,9 @@
import { stringifyRowData } from './stringifyRowData';
import {
stringifyRowData,
stringifyExtract,
stringifyCount,
stringifyGroupBy
} from './utils';
describe('decimal types', () => {
it('stringifies = operator', () => {
@ -10,7 +15,7 @@ describe('decimal types', () => {
"type": "decimal",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Row(fieldName=0)' })
}])).toEqual({ error: false, queryString: 'Row(fieldName=0)' })
});
it('stringifies != operator', () => {
@ -22,7 +27,7 @@ describe('decimal types', () => {
"type": "decimal",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Not(Row(fieldName=0))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName=0))' })
});
it('stringifies > operator', () => {
@ -34,7 +39,7 @@ describe('decimal types', () => {
"type": "decimal",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Row(fieldName>0)' })
}])).toEqual({ error: false, queryString: 'Row(fieldName>0)' })
});
it('stringifies >= operator', () => {
@ -46,7 +51,7 @@ describe('decimal types', () => {
"type": "decimal",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Row(fieldName>=0)' })
}])).toEqual({ error: false, queryString: 'Row(fieldName>=0)' })
});
it('stringifies negated operations', () => {
@ -59,7 +64,7 @@ describe('decimal types', () => {
"keys": undefined
}],
isNot: true
}])).toEqual({ error: false, query: 'Not(Row(fieldName>=0))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName>=0))' })
});
});
@ -73,7 +78,7 @@ describe('int types', () => {
"type": "int",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Row(fieldName=0)' })
}])).toEqual({ error: false, queryString: 'Row(fieldName=0)' })
});
it('stringifies != operator', () => {
@ -85,7 +90,7 @@ describe('int types', () => {
"type": "int",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Not(Row(fieldName=0))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName=0))' })
});
it('stringifies > operator', () => {
@ -97,7 +102,7 @@ describe('int types', () => {
"type": "int",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Row(fieldName>0)' })
}])).toEqual({ error: false, queryString: 'Row(fieldName>0)' })
});
it('stringifies >= operator', () => {
@ -109,7 +114,7 @@ describe('int types', () => {
"type": "int",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Row(fieldName>=0)' })
}])).toEqual({ error: false, queryString: 'Row(fieldName>=0)' })
});
it('stringifies negated operations', () => {
@ -122,7 +127,7 @@ describe('int types', () => {
"keys": undefined
}],
isNot: true
}])).toEqual({ error: false, query: 'Not(Row(fieldName>=0))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName>=0))' })
});
});
@ -136,7 +141,7 @@ describe('mutex (id) types', () => {
"type": "mutex",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Row(fieldName=1234)' })
}])).toEqual({ error: false, queryString: 'Row(fieldName=1234)' })
});
it('stringifies is operator for multiple values', () => {
@ -148,7 +153,7 @@ describe('mutex (id) types', () => {
"type": "mutex",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Union(Row(fieldName=1234), Row(fieldName=5678))' })
}])).toEqual({ error: false, queryString: 'Union(Row(fieldName=1234), Row(fieldName=5678))' })
});
it('stringifies is not operator', () => {
@ -160,7 +165,7 @@ describe('mutex (id) types', () => {
"type": "mutex",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Not(Row(fieldName=1234))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName=1234))' })
});
it('stringifies is not operator for multiple values', () => {
@ -172,7 +177,7 @@ describe('mutex (id) types', () => {
"type": "mutex",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Not(Union(Row(fieldName=1234), Row(fieldName=5678)))' })
}])).toEqual({ error: false, queryString: 'Not(Union(Row(fieldName=1234), Row(fieldName=5678)))' })
});
});
@ -186,7 +191,7 @@ describe('mutex (keys) types', () => {
"type": "mutex",
"keys": true
}]
}])).toEqual({ error: false, query: 'Row(fieldName="value")' })
}])).toEqual({ error: false, queryString: 'Row(fieldName="value")' })
});
it('stringifies is operator for multiple values', () => {
@ -198,7 +203,7 @@ describe('mutex (keys) types', () => {
"type": "mutex",
"keys": true
}]
}])).toEqual({ error: false, query: 'Union(Row(fieldName="one"), Row(fieldName="two"))' })
}])).toEqual({ error: false, queryString: 'Union(Row(fieldName="one"), Row(fieldName="two"))' })
});
it('stringifies is not operator', () => {
@ -210,7 +215,7 @@ describe('mutex (keys) types', () => {
"type": "mutex",
"keys": true
}]
}])).toEqual({ error: false, query: 'Not(Row(fieldName="value"))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName="value"))' })
});
it('stringifies is not operator for multiple values', () => {
@ -222,7 +227,7 @@ describe('mutex (keys) types', () => {
"type": "mutex",
"keys": true
}]
}])).toEqual({ error: false, query: 'Not(Union(Row(fieldName="one"), Row(fieldName="two")))' })
}])).toEqual({ error: false, queryString: 'Not(Union(Row(fieldName="one"), Row(fieldName="two")))' })
});
it('stringifies like operator with a single-codepoint key', () => {
@ -234,7 +239,7 @@ describe('mutex (keys) types', () => {
"type": "mutex",
"keys": true
}]
}])).toEqual({ error: false, query: 'UnionRows(Rows(field=fieldName, like="_alue"))' })
}])).toEqual({ error: false, queryString: 'UnionRows(Rows(field=fieldName, like="_alue"))' })
});
it('stringifies like operator with a wildcard', () => {
@ -246,7 +251,7 @@ describe('mutex (keys) types', () => {
"type": "mutex",
"keys": true
}]
}])).toEqual({ error: false, query: 'UnionRows(Rows(field=fieldName, like="value%"))' })
}])).toEqual({ error: false, queryString: 'UnionRows(Rows(field=fieldName, like="value%"))' })
});
it('stringifies like operator without a single-codepoint key or wildcard', () => {
@ -258,7 +263,7 @@ describe('mutex (keys) types', () => {
"type": "mutex",
"keys": true
}]
}])).toEqual({ error: false, query: 'UnionRows(Rows(field=fieldName, like="%value%"))' })
}])).toEqual({ error: false, queryString: 'UnionRows(Rows(field=fieldName, like="%value%"))' })
});
it('stringifies CIDR operator', () => {
@ -270,7 +275,7 @@ describe('mutex (keys) types', () => {
"type": "mutex",
"keys": true
}]
}])).toEqual({ error: false, query: 'Union(Row(fieldName="10.164.124.32"), Row(fieldName="10.164.124.33"), Row(fieldName="10.164.124.34"), Row(fieldName="10.164.124.35"))' })
}])).toEqual({ error: false, queryString: 'Union(Row(fieldName="10.164.124.32"), Row(fieldName="10.164.124.33"), Row(fieldName="10.164.124.34"), Row(fieldName="10.164.124.35"))' })
});
it('stringifies negated operations', () => {
@ -283,7 +288,7 @@ describe('mutex (keys) types', () => {
"keys": true
}],
isNot: true
}])).toEqual({ error: false, query: 'Not(UnionRows(Rows(field=fieldName, like="%value%")))' })
}])).toEqual({ error: false, queryString: 'Not(UnionRows(Rows(field=fieldName, like="%value%")))' })
});
});
@ -297,7 +302,7 @@ describe('set (id) types', () => {
"type": "set",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Row(fieldName=1234)' })
}])).toEqual({ error: false, queryString: 'Row(fieldName=1234)' })
});
it('stringifies is operator for multiple values', () => {
@ -309,7 +314,7 @@ describe('set (id) types', () => {
"type": "set",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Union(Row(fieldName=1234), Row(fieldName=5678))' })
}])).toEqual({ error: false, queryString: 'Union(Row(fieldName=1234), Row(fieldName=5678))' })
});
it('stringifies is not operator', () => {
@ -321,7 +326,7 @@ describe('set (id) types', () => {
"type": "set",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Not(Row(fieldName=1234))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName=1234))' })
});
it('stringifies is not operator for multiple values', () => {
@ -333,7 +338,7 @@ describe('set (id) types', () => {
"type": "set",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Not(Union(Row(fieldName=1234), Row(fieldName=5678)))' })
}])).toEqual({ error: false, queryString: 'Not(Union(Row(fieldName=1234), Row(fieldName=5678)))' })
});
});
@ -347,7 +352,7 @@ describe('set (keys) types', () => {
"type": "set",
"keys": true
}]
}])).toEqual({ error: false, query: 'Row(fieldName="value")' })
}])).toEqual({ error: false, queryString: 'Row(fieldName="value")' })
});
it('stringifies is operator for multiple values', () => {
@ -359,7 +364,7 @@ describe('set (keys) types', () => {
"type": "set",
"keys": true
}]
}])).toEqual({ error: false, query: 'Union(Row(fieldName="one"), Row(fieldName="two"))' })
}])).toEqual({ error: false, queryString: 'Union(Row(fieldName="one"), Row(fieldName="two"))' })
});
it('stringifies is not operator', () => {
@ -371,7 +376,7 @@ describe('set (keys) types', () => {
"type": "set",
"keys": true
}]
}])).toEqual({ error: false, query: 'Not(Row(fieldName="value"))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName="value"))' })
});
it('stringifies is not operator for multiple values', () => {
@ -383,7 +388,7 @@ describe('set (keys) types', () => {
"type": "set",
"keys": true
}]
}])).toEqual({ error: false, query: 'Not(Union(Row(fieldName="one"), Row(fieldName="two")))' })
}])).toEqual({ error: false, queryString: 'Not(Union(Row(fieldName="one"), Row(fieldName="two")))' })
});
it('stringifies like operator with a single-codepoint key', () => {
@ -395,7 +400,7 @@ describe('set (keys) types', () => {
"type": "set",
"keys": true
}]
}])).toEqual({ error: false, query: 'UnionRows(Rows(field=fieldName, like="_alue"))' })
}])).toEqual({ error: false, queryString: 'UnionRows(Rows(field=fieldName, like="_alue"))' })
});
it('stringifies like operator with a wildcard', () => {
@ -407,7 +412,7 @@ describe('set (keys) types', () => {
"type": "set",
"keys": true
}]
}])).toEqual({ error: false, query: 'UnionRows(Rows(field=fieldName, like="value%"))' })
}])).toEqual({ error: false, queryString: 'UnionRows(Rows(field=fieldName, like="value%"))' })
});
it('stringifies like operator without a single-codepoint key or wildcard', () => {
@ -419,7 +424,7 @@ describe('set (keys) types', () => {
"type": "set",
"keys": true
}]
}])).toEqual({ error: false, query: 'UnionRows(Rows(field=fieldName, like="%value%"))' })
}])).toEqual({ error: false, queryString: 'UnionRows(Rows(field=fieldName, like="%value%"))' })
});
it('stringifies CIDR operator', () => {
@ -431,7 +436,7 @@ describe('set (keys) types', () => {
"type": "set",
"keys": true
}]
}])).toEqual({ error: false, query: 'Union(Row(fieldName="10.164.124.32"), Row(fieldName="10.164.124.33"), Row(fieldName="10.164.124.34"), Row(fieldName="10.164.124.35"))' })
}])).toEqual({ error: false, queryString: 'Union(Row(fieldName="10.164.124.32"), Row(fieldName="10.164.124.33"), Row(fieldName="10.164.124.34"), Row(fieldName="10.164.124.35"))' })
});
it('stringifies negated operations', () => {
@ -444,7 +449,7 @@ describe('set (keys) types', () => {
"keys": true
}],
isNot: true
}])).toEqual({ error: false, query: 'Not(UnionRows(Rows(field=fieldName, like="%value%")))' })
}])).toEqual({ error: false, queryString: 'Not(UnionRows(Rows(field=fieldName, like="%value%")))' })
});
});
@ -458,7 +463,7 @@ describe('time (ID) types', () => {
"type": "time",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Row(fieldName=1234)' })
}])).toEqual({ error: false, queryString: 'Row(fieldName=1234)' })
});
it('stringifies is not operators', () => {
@ -470,7 +475,7 @@ describe('time (ID) types', () => {
"type": "time",
"keys": undefined
}]
}])).toEqual({ error: false, query: 'Not(Row(fieldName=1234))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName=1234))' })
});
it('stringifies negated operators', () => {
@ -483,7 +488,7 @@ describe('time (ID) types', () => {
"keys": undefined
}],
isNot: true
}])).toEqual({ error: false, query: 'Not(Row(fieldName=1234))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName=1234))' })
});
});
@ -497,7 +502,7 @@ describe('time (keys) types', () => {
"type": "time",
"keys": true
}]
}])).toEqual({ error: false, query: 'Row(fieldName="1234")' })
}])).toEqual({ error: false, queryString: 'Row(fieldName="1234")' })
});
it('stringifies is not operators', () => {
@ -509,7 +514,7 @@ describe('time (keys) types', () => {
"type": "time",
"keys": true
}]
}])).toEqual({ error: false, query: 'Not(Row(fieldName="1234"))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName="1234"))' })
});
it('stringifies like operators with a single-codepoint key', () => {
@ -521,7 +526,7 @@ describe('time (keys) types', () => {
"type": "time",
"keys": true
}]
}])).toEqual({ error: false, query: 'UnionRows(Rows(field=fieldName, like="123_"))' })
}])).toEqual({ error: false, queryString: 'UnionRows(Rows(field=fieldName, like="123_"))' })
});
it('stringifies like operators with a wildcard', () => {
@ -533,7 +538,7 @@ describe('time (keys) types', () => {
"type": "time",
"keys": true
}]
}])).toEqual({ error: false, query: 'UnionRows(Rows(field=fieldName, like="%1234"))' })
}])).toEqual({ error: false, queryString: 'UnionRows(Rows(field=fieldName, like="%1234"))' })
});
it('stringifies like operators without a single-codepoint key or wildcard', () => {
@ -545,7 +550,7 @@ describe('time (keys) types', () => {
"type": "time",
"keys": true
}]
}])).toEqual({ error: false, query: 'UnionRows(Rows(field=fieldName, like="%1234%"))' })
}])).toEqual({ error: false, queryString: 'UnionRows(Rows(field=fieldName, like="%1234%"))' })
});
it('stringifies negated operators', () => {
@ -558,7 +563,7 @@ describe('time (keys) types', () => {
"keys": true
}],
isNot: true
}])).toEqual({ error: false, query: 'Not(Row(fieldName="1234"))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName="1234"))' })
});
});
@ -572,7 +577,7 @@ describe('timestamp types', () => {
"type": "timestamp",
"keys": undefined
}],
}])).toEqual({ error: false, query: 'Row(fieldName="2021-06-04T12:00:00Z")' })
}])).toEqual({ error: false, queryString: 'Row(fieldName="2021-06-04T12:00:00Z")' })
});
it('stringifies is before operators', () => {
@ -584,7 +589,7 @@ describe('timestamp types', () => {
"type": "timestamp",
"keys": undefined
}],
}])).toEqual({ error: false, query: 'Row(fieldName<"2021-06-04T12:00:00Z")' })
}])).toEqual({ error: false, queryString: 'Row(fieldName<"2021-06-04T12:00:00Z")' })
});
it('stringifies is after operators', () => {
@ -596,7 +601,7 @@ describe('timestamp types', () => {
"type": "timestamp",
"keys": undefined
}],
}])).toEqual({ error: false, query: 'Row(fieldName>"2021-06-04T12:00:00Z")' })
}])).toEqual({ error: false, queryString: 'Row(fieldName>"2021-06-04T12:00:00Z")' })
});
it('stringifies negated operators', () => {
@ -609,7 +614,7 @@ describe('timestamp types', () => {
"keys": undefined
}],
isNot: true
}])).toEqual({ error: false, query: 'Not(Row(fieldName="2021-06-04T12:00:00Z"))' })
}])).toEqual({ error: false, queryString: 'Not(Row(fieldName="2021-06-04T12:00:00Z"))' })
});
});
@ -630,7 +635,7 @@ describe('mixed types', () => {
"keys": undefined
}],
operator: "and"
}])).toEqual({ error: false, query: 'Intersect(UnionRows(Rows(field=stringType, like="%value%")), Row(intType=0))' })
}])).toEqual({ error: false, queryString: 'Intersect(UnionRows(Rows(field=stringType, like="%value%")), Row(intType=0))' })
});
it('stringifies multiple or-ed rows', () => {
@ -649,7 +654,7 @@ describe('mixed types', () => {
"keys": undefined
}],
operator: "or"
}])).toEqual({ error: false, query: 'Union(UnionRows(Rows(field=stringType, like="%value%")), Row(intType=0))' })
}])).toEqual({ error: false, queryString: 'Union(UnionRows(Rows(field=stringType, like="%value%")), Row(intType=0))' })
});
it('stringifies negated multiple rows', () => {
@ -669,6 +674,129 @@ describe('mixed types', () => {
}],
operator: "or",
isNot: true
}])).toEqual({ error: false, query: 'Not(Union(UnionRows(Rows(field=stringType, like="%value%")), Row(intType=0)))' })
}])).toEqual({ error: false, queryString: 'Not(Union(UnionRows(Rows(field=stringType, like="%value%")), Row(intType=0)))' })
});
})
describe('Query types', () => {
it('stringifies Extract queries', () => {
expect(stringifyExtract({
columns: ['one', 'two', 'three'],
rowCalls: [{
row: [{
"field": "stringType",
"rowOperator": "like",
"value": "value",
"type": "set",
"keys": true
}, {
"field": "stringType2",
"rowOperator": "is",
"value": "value2",
"type": "set",
"keys": true
}],
operator: 'or'
}, {
row: [{
"field": "intType",
"rowOperator": "=",
"value": "0",
"type": "int",
"keys": undefined
}]
}],
operator: "and"
})).toEqual({ error: false, queryString: 'Extract(Limit(Intersect(Union(UnionRows(Rows(field=stringType, like=\"%value%\")), Row(stringType2is\"value2\")), Row(intType=0)), limit=1000), Rows(one), Rows(two), Rows(three))', countQuery: 'Count(Intersect(Union(UnionRows(Rows(field=stringType, like=\"%value%\")), Row(stringType2is\"value2\")), Row(intType=0)))' })
});
it('stringifies Count queries', () => {
expect(stringifyCount({
columns: ['one', 'two', 'three'],
rowCalls: [{
row: [{
"field": "stringType",
"rowOperator": "like",
"value": "value",
"type": "set",
"keys": true
}, {
"field": "stringType2",
"rowOperator": "is",
"value": "value2",
"type": "set",
"keys": true
}],
operator: 'or'
}, {
row: [{
"field": "intType",
"rowOperator": "=",
"value": "0",
"type": "int",
"keys": undefined
}]
}],
operator: "and"
})).toEqual({ error: false, queryString: 'Count(Intersect(Union(UnionRows(Rows(field=stringType, like=\"%value%\")), Row(stringType2is\"value2\")), Row(intType=0)))' })
});
it('stringifies basic GroupBy queries', () => {
expect(stringifyGroupBy({
groupByCall: {
primary: 'primary'
}
})).toEqual('GroupBy(Rows(primary))');
});
it('stringifies GroupBy queries with secondary grouping', () => {
expect(stringifyGroupBy({
groupByCall: {
primary: 'primary',
secondary: 'secondary'
}
})).toEqual('GroupBy(Rows(primary), Rows(secondary))');
});
it('stringifies GroupBy queries with a filter', () => {
expect(stringifyGroupBy({
groupByCall: {
primary: 'primary'
},
filter: 'filterString'
})).toEqual('GroupBy(Rows(primary), filter=filterString)');
});
it('stringifies GroupBy queries with a sort', () => {
expect(stringifyGroupBy({
groupByCall: {
primary: 'primary'
},
sort: [{ sortValue: 'count desc' }]
})).toEqual('GroupBy(Rows(primary), sort=\"count desc\")');
});
it('stringifies GroupBy queries with a secondary sum sort', () => {
expect(stringifyGroupBy({
groupByCall: {
primary: 'primary'
},
sort: [
{ sortValue: 'count desc' },
{ sortValue: 'sum asc', field: 'field' }
]
})).toEqual('GroupBy(Rows(primary), sort=\"count desc, sum asc\", aggregate=Sum(field=field))');
});
it('stringifies GroupBy queries with a filter and sort', () => {
expect(stringifyGroupBy({
groupByCall: {
primary: 'primary'
},
filter: 'filterString',
sort: [
{ sortValue: 'sum asc', field: 'field' }
]
})).toEqual('GroupBy(Rows(primary), filter=filterString, sort=\"sum asc\", aggregate=Sum(field=field))');
});
});

View file

@ -1,4 +1,4 @@
import { RowGrouping } from './rowTypes';
import { Operator, RowGrouping } from './rowTypes';
import { getIPRange } from 'get-ip-range';
export const stringifyExtract = (query: any) => {
@ -29,8 +29,9 @@ export const stringifyCount = (query: any) => {
}
}
const stringifyRowData = (rowCalls, operator) => {
export const stringifyRowData = (rowCalls: RowGrouping[], operator?: Operator) => {
let queryString = '';
let error = false;
if (rowCalls.length === 0) {
queryString = 'All()';
} else {
@ -58,7 +59,7 @@ const stringifyRowData = (rowCalls, operator) => {
.join(', ');
rowString = `Union(${ipRows})`;
} catch (error) {
return { error: true, query: error.message };
return { error: true, queryString: error.message };
}
} else if (rowOperator === 'like') {
if (value.includes('%') || value.includes('_')) {
@ -80,34 +81,36 @@ const stringifyRowData = (rowCalls, operator) => {
});
});
queryString = rowsMap
.map((group, idx) => {
let joined = '';
if (group.length > 1) {
joined = group.map((r) => r).join(', ');
const operator = rowCalls[idx].operator;
if (operator === 'and') {
joined = `Intersect(${joined})`;
} else if (operator === 'or') {
joined = `Union(${joined})`;
if (!error) {
queryString = rowsMap
.map((group, idx) => {
let joined = '';
if (group.length > 1) {
joined = group.map((r) => r).join(', ');
const operator = rowCalls[idx].operator;
if (operator === 'and') {
joined = `Intersect(${joined})`;
} else if (operator === 'or') {
joined = `Union(${joined})`;
}
} else {
joined = group[0];
}
} else {
joined = group[0];
}
return rowCalls[idx].isNot ? `Not(${joined})` : joined;
})
.join(', ');
return rowCalls[idx].isNot ? `Not(${joined})` : joined;
})
.join(', ');
if (rowCalls.length > 1 && operator) {
if (operator === 'and') {
queryString = `Intersect(${queryString})`;
} else if (operator === 'or') {
queryString = `Union(${queryString})`;
if (rowCalls.length > 1 && operator) {
if (operator === 'and') {
queryString = `Intersect(${queryString})`;
} else if (operator === 'or') {
queryString = `Union(${queryString})`;
}
}
}
}
return { error: false, queryString };
return { error, queryString };
}
export const stringifyGroupBy = (query: any) => {