Merge pull request #1665 from molecula/refactor/syang/query-builder

CLOUD-136: Refactor query builder
This commit is contained in:
Stephanie Yang 2021-07-30 09:52:45 -05:00 • committed by GitHub
commit 317f1e0145
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1589 additions and 1290 deletions

View file

@ -0,0 +1,36 @@
import React, { FC } from 'react';
import { Operator, RowGrouping } from 'App/QueryBuilder/rowTypes';
import { RowCallBuilder } from 'App/QueryBuilder/RowCallBuilder';
type CountBuilderProps = {
table: any;
query: any;
showInvalid: boolean;
onChange: (query: any) => void;
};
export const CountBuilder: FC<CountBuilderProps> = ({
table,
query,
showInvalid,
onChange
}) => {
const { rowCalls } = query ? query : { rowCalls: [] };
return (
<div>
<RowCallBuilder
rowCalls={rowCalls}
fields={table.fields}
showInvalid={showInvalid}
onChange={(rowCalls: RowGrouping[], operator?: Operator) => {
onChange({
...query,
rowCalls,
operator
});
}}
/>
</div>
);
};

View file

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

View file

@ -0,0 +1,21 @@
.columnsSelector {
margin: 4px 14px 4px 0;
display: flex;
text-align: center;
.info {
margin-left: 4px;
fill: var(--text-secondary);
}
}
.textLink {
color: var(--primary);
font-size: 12px;
&:hover {
cursor: pointer;
text-decoration: underline;
color: var(--primary);
}
}

View file

@ -0,0 +1,92 @@
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 { Operator, RowGrouping } from 'App/QueryBuilder/rowTypes';
import { RowCallBuilder } from 'App/QueryBuilder/RowCallBuilder';
import { useEffectOnce } from 'react-use';
import css from './ExtractBuilder.module.scss';
type ExtractBuilderProps = {
table: any;
query: any;
showInvalid: boolean;
onChange: (query: any) => void;
};
export const ExtractBuilder: FC<ExtractBuilderProps> = ({
table,
query,
showInvalid,
onChange
}) => {
const { columns, rowCalls } = query
? query
: {
columns: [],
rowCalls: []
};
const [showColumnSelector, setShowColumnSelector] = useState<boolean>(false);
useEffectOnce(() => {
if (!query) {
onChange({
columns: table.fields.map((field) => field.name),
operation: 'Extract',
rowCalls: []
});
}
});
return (
<div className={css.extract}>
<div className={css.columnsSelector}>
<span
className={css.textLink}
onClick={() => setShowColumnSelector(true)}
>
Configure result fields
</span>
<Tooltip
className={css.info}
title="This controls the fields that will show up in the results table after querying"
placement="top"
arrow
>
<InfoIcon fontSize="inherit" />
</Tooltip>
</div>
<ColumnSelector
open={showColumnSelector}
fieldsList={table.fields.map((field) => ({
name: field.name,
show: columns ? columns.includes(field.name) : true
}))}
onChange={(allColumns) => {
let cols: string[] = [];
allColumns.forEach((col) => {
if (col.show) {
cols.push(col.name);
}
});
onChange({ ...query, columns: cols });
}}
onClose={() => setShowColumnSelector(false)}
/>
<RowCallBuilder
rowCalls={rowCalls}
fields={table.fields}
showInvalid={showInvalid}
onChange={(rowCalls: RowGrouping[], operator?: Operator) => {
onChange({
...query,
rowCalls,
operator
});
}}
/>
</div>
);
};

View file

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

View file

@ -0,0 +1,32 @@
.groupBy {
.fieldSelector {
margin-bottom: 16px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
}
.filtersInfo {
vertical-align: text-top;
margin-left: 4px;
}
}
.textLink {
color: var(--primary);
font-size: 12px;
&:hover {
cursor: pointer;
text-decoration: underline;
color: var(--primary);
}
}
.filtersSection {
margin-bottom: 16px;
}

View file

@ -0,0 +1,187 @@
import React, { FC } from 'react';
import InfoIcon from '@material-ui/icons/Info';
import Tooltip from '@material-ui/core/Tooltip';
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/utils';
import css from './GroupByBuilder.module.scss';
type GroupByBuilderProps = {
table: any;
query: any;
showInvalid: boolean;
onChange: (query: any) => void;
};
export const GroupByBuilder: FC<GroupByBuilderProps> = ({
table,
query,
showInvalid,
onChange
}) => {
const { groupByCall, filter, sort } = query;
const filters = JSON.parse(
localStorage.getItem('saved-queries') || '[]'
).filter(
(q) =>
q.table === table.name &&
q.operation === 'Extract' &&
q.rowCalls.length > 0
);
return (
<div className={css.groupBy}>
<Select
className={css.fieldSelector}
label="Primary Field"
value={
groupByCall ? (groupByCall.primary ? groupByCall.primary : '') : ''
}
options={table.fields
.filter((f) =>
['set', 'time', 'mutex', 'bool', 'int', 'timestamp'].includes(
f.options.type
)
)
.map((field) => {
return {
label: `${field.name} (${field.options.type})`,
value: field.name
};
})}
onChange={(value) =>
onChange({
...query,
groupByCall: {
...query.groupByCall,
primary: value.toString()
}
})
}
error={showInvalid && !groupByCall.primary}
/>
<Select
className={css.fieldSelector}
label="Secondary Field (optional)"
value={
groupByCall
? groupByCall.secondary
? groupByCall.secondary
: ''
: ''
}
allowEmpty={true}
options={table.fields
.filter((f) =>
['set', 'time', 'mutex', 'bool', 'int', 'timestamp'].includes(
f.options.type
)
)
.map((field) => {
return {
label: `${field.name} (${field.options.type})`,
value: field.name
};
})}
onChange={(value) =>
onChange({
...query,
groupByCall: {
...query.groupByCall,
secondary: value.toString()
}
})
}
/>
<div className={css.filtersSection}>
<div className={css.header}>
<Typography variant="caption" color="textSecondary">
Filter (optional)
{filters.length > 0 && (
<Tooltip
className={css.filtersInfo}
title={`To use filters, save an Extract query for ${table.name} with at least one field constraint.`}
placement="top"
arrow
>
<InfoIcon fontSize="inherit" />
</Tooltip>
)}
</Typography>
{filter ? (
<span
className={css.textLink}
onClick={() => onChange({ ...query, filter: undefined })}
>
Clear Filter
</span>
) : null}
</div>
{filter ? (
<Typography variant="caption">{filter}</Typography>
) : filters.length > 0 ? (
<SavedQueries
queries={filters}
tables={[table]}
onClickSaved={(queryIdx) => {
const { rowCalls, operator } = filters[queryIdx];
const res = stringifyRowData(rowCalls, operator);
if (!res.error) {
onChange({ ...query, filter: res.queryString });
}
}}
/>
) : (
<div className={css.infoMessage}>
No available filters. To use filters, save an Extract query for{' '}
{table.name} with at least one field constraint.
</div>
)}
</div>
<div className={css.sortSection}>
<div className={css.header}>
<Typography variant="caption" color="textSecondary">
Sort (optional)
</Typography>
<span
className={css.textLink}
onClick={() => onChange({ ...query, sort: [] })}
>
Clear
</span>
</div>
<GroupBySort
sort={sort ? sort : []}
onUpdate={(value) => {
let isInvalid = false;
if (
value.length > 0 &&
value[0].sortValue.includes('sum') &&
!value[0].field
) {
isInvalid = true;
} else if (
value.length > 1 &&
value[1].sortValue.includes('sum') &&
!value[1].field
) {
isInvalid = true;
}
onChange({ ...query, sort: value, isInvalid });
}}
fields={table.fields
.filter((field) => field.options.type === 'int')
.map((field) => {
return { label: field.name, value: field.name };
})}
showErrors={showInvalid}
/>
</div>
</div>
);
};

View file

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

View file

@ -20,7 +20,7 @@ export const GroupBySort: FC<GroupBySortProps> = ({
showErrors,
onUpdate
}) => {
const hasPrimary = sort.length > 0;
const hasPrimary = sort.length > 0 && sort[0].sortValue;
const primary = hasPrimary ? sort[0].sortValue.split(' ')[0] : '';
const hasSecondary = sort.length > 1;
const secondary = hasSecondary ? sort[1].sortValue.split(' ')[0] : '';
@ -76,6 +76,7 @@ export const GroupBySort: FC<GroupBySortProps> = ({
value={hasPrimary ? sort[0].sortValue : ''}
options={sortOptions}
onChange={(value) => onPrimaryChange(value)}
allowEmpty={true}
/>
{primary.includes('sum') ? (
@ -99,6 +100,7 @@ export const GroupBySort: FC<GroupBySortProps> = ({
(option) => !option.value.includes(primary)
)}
onChange={(value) => onSecondaryChange(value)}
allowEmpty={true}
/>
{secondary.includes('sum') ? (

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

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

@ -0,0 +1,40 @@
.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;
}
}

View file

@ -0,0 +1,177 @@
import React, { FC, Fragment, useState } from 'react';
import AddIcon from '@material-ui/icons/Add';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import {
groupOperators,
Operator,
RowCallType,
RowGrouping
} from 'App/QueryBuilder/rowTypes';
import { RowCall } from './RowCall';
import css from './RowCallBuilder.module.scss';
type RowCallBuilderProps = {
rowCalls: RowGrouping[];
fields: any[];
showInvalid: boolean;
onChange: (rows: RowGrouping[], operator?: Operator) => void;
};
export const RowCallBuilder: FC<RowCallBuilderProps> = ({
rowCalls = [],
fields,
showInvalid,
onChange
}) => {
const [newOperatorEl, setNewOperatorEl] = useState<null | HTMLElement>(null);
const [operatorEl, setOperatorEl] = useState<null | HTMLElement>(null);
const [operator, setOperator] = useState<Operator>();
const onNewGroup = (op?: Operator) => {
const newRow: RowCallType[] = [
{
field: '',
rowOperator: '=',
value: '',
type: 'set',
keys: true
}
];
onChange([...rowCalls, { row: newRow }], op ? op : operator);
};
const onRemoveGroup = (groupIdx: number) => {
if (groupIdx === 0) {
onChange(rowCalls.slice(1), operator);
} else {
const updatedRowCalls = [...rowCalls];
updatedRowCalls.splice(groupIdx, 1);
onChange(updatedRowCalls, operator);
if (updatedRowCalls.length < 1) {
setOperator(undefined);
onChange(updatedRowCalls);
}
}
};
const onUpdateRow = (
groupIdx: number,
newRowData?: RowCallType[],
rowGroupOperator?: Operator,
isNot?: boolean
) => {
const updatedRowCalls = [...rowCalls];
if (newRowData) {
const clone: RowGrouping = {
...rowCalls[groupIdx],
row: newRowData,
isNot
};
if (newRowData.length <= 1) {
delete clone.operator;
} else if (rowGroupOperator) {
clone.operator = rowGroupOperator;
}
updatedRowCalls.splice(groupIdx, 1, clone);
} else {
updatedRowCalls.splice(groupIdx, 1);
}
onChange(updatedRowCalls, operator);
};
return (
<Fragment>
{rowCalls?.map((rowCall, idx) => (
<Fragment key={`group-${idx}`}>
{idx > 0 ? (
<Fragment>
<div className={css.operator}>
<div className={css.line} />
<Button
className={css.addButton}
size="small"
onClick={(event) => setOperatorEl(event.currentTarget)}
>
{operator}
</Button>
<div className={css.line} />
</div>
<Menu
open={!!operatorEl}
anchorEl={operatorEl}
classes={{ paper: css.menuPaper }}
onClose={() => setOperatorEl(null)}
>
{groupOperators.map((op: Operator) => (
<MenuItem
key={`${idx}-${op}`}
onClick={() => {
setOperator(op);
setOperatorEl(null);
}}
>
{op}
</MenuItem>
))}
</Menu>
</Fragment>
) : null}
<RowCall
fields={fields}
rowData={rowCall.row}
isNot={rowCall.isNot}
operator={rowCall.operator}
showErrors={showInvalid}
onUpdate={(updatedRow, operator, isNot) =>
onUpdateRow(idx, updatedRow, operator, isNot)
}
onRemoveGroup={() => onRemoveGroup(idx)}
/>
</Fragment>
))}
<div className={css.newGroup}>
<div className={css.line} />
<IconButton
className={css.addButton}
size="small"
onClick={(event) => {
if (rowCalls.length === 0 || !!operator) {
onNewGroup();
} else {
setNewOperatorEl(event.currentTarget);
}
}}
>
<AddIcon fontSize="inherit" />
</IconButton>
<div className={css.line} />
</div>
<Menu
open={!!newOperatorEl}
anchorEl={newOperatorEl}
classes={{ paper: css.menuPaper }}
onClose={() => setNewOperatorEl(null)}
>
{groupOperators.map((op: Operator) => (
<MenuItem
key={`operator-${op}`}
onClick={() => {
setOperator(op);
onNewGroup(op);
setNewOperatorEl(null);
}}
>
{op}
</MenuItem>
))}
</Menu>
</Fragment>
);
};

View file

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

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

@ -0,0 +1,177 @@
import { Operator, RowGrouping } from './rowTypes';
import { getIPRange } from 'get-ip-range';
export const stringifyExtract = (query: any) => {
const { columns, operator, rowCalls } = query;
const rowData = stringifyRowData(rowCalls, operator);
if (!rowData.error) {
const fields = columns.map((field) => `Rows(${field})`);
const allRows = fields.join(', ');
return {
error: false,
queryString: `Extract(Limit(${rowData.queryString}, limit=1000), ${allRows})`,
countQuery: `Count(${rowData.queryString})`
};
} else {
return { ...rowData, countQuery: '' };
}
}
export const stringifyCount = (query: any) => {
const { operator, rowCalls } = query;
const rowData = stringifyRowData(rowCalls, operator);
if (!rowData.error) {
return { error: false, queryString: `Count(${rowData.queryString})` };
} else {
return rowData;
}
}
export const stringifyRowData = (rowCalls: RowGrouping[], operator?: Operator) => {
let queryString = '';
let error = false;
if (rowCalls.length === 0) {
queryString = 'All()';
} else {
let rowsMap: string[][] = [];
rowCalls.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, queryString: 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);
}
});
});
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];
}
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})`;
}
}
}
}
return { error, queryString };
}
export const stringifyGroupBy = (query: any) => {
const { groupByCall, filter, sort } = query;
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 (sort.length > 1) {
sortString = `${sortString}, ${sort[1].sortValue}`;
if (sort[1].sortValue.includes('sum')) {
aggregateString = `, aggregate=Sum(field=${sort[1].field})`;
}
}
sortString = `, sort="${sortString}"`;
}
const filterString = filter ? `, filter=${filter}` : '';
const queryString = groupByCall.secondary
? `GroupBy(Rows(${groupByCall.primary}), Rows(${groupByCall.secondary})${filterString}${sortString}${aggregateString})`
: `GroupBy(Rows(${groupByCall.primary})${filterString}${sortString}${aggregateString})`;
return queryString;
}
export const cleanupRows = (rowCalls: RowGrouping[]) => {
// remove empty groups and row calls
let isInvalid = false;
let cleanRowCalls: RowGrouping[] = [];
let cleanGroups: RowGrouping[] = [];
if (rowCalls?.length > 0) {
rowCalls.forEach((group, groupIdx) => {
cleanGroups.push({ ...group, row: [] });
group.row.forEach((row) => {
const { field, value, rowOperator } = row;
if (field || value) {
cleanGroups[groupIdx].row.push(row);
if (!field || !value) {
isInvalid = true;
} else if (rowOperator === 'cidr') {
try {
getIPRange(value);
} catch (err) {
isInvalid = true;
}
}
}
});
});
cleanGroups.forEach((group) => {
if (group.row.length > 0) {
cleanRowCalls.push(group);
}
});
}
return { cleanRowCalls, isInvalid };
};