mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-10-08 03:47:51 +00:00
Merge pull request #1665 from molecula/refactor/syang/query-builder
CLOUD-136: Refactor query builder
This commit is contained in:
commit
317f1e0145
23 changed files with 1589 additions and 1290 deletions
36
lattice/src/App/QueryBuilder/CountBuilder/CountBuilder.tsx
Normal file
36
lattice/src/App/QueryBuilder/CountBuilder/CountBuilder.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
lattice/src/App/QueryBuilder/CountBuilder/index.ts
Normal file
1
lattice/src/App/QueryBuilder/CountBuilder/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './CountBuilder';
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
1
lattice/src/App/QueryBuilder/ExtractBuilder/index.ts
Normal file
1
lattice/src/App/QueryBuilder/ExtractBuilder/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './ExtractBuilder';
|
||||
|
|
@ -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;
|
||||
}
|
||||
187
lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.tsx
Normal file
187
lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
lattice/src/App/QueryBuilder/GroupByBuilder/index.ts
Normal file
1
lattice/src/App/QueryBuilder/GroupByBuilder/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './GroupByBuilder';
|
||||
|
|
@ -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') ? (
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
177
lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.tsx
Normal file
177
lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
lattice/src/App/QueryBuilder/RowCallBuilder/index.ts
Normal file
1
lattice/src/App/QueryBuilder/RowCallBuilder/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './RowCallBuilder';
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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))');
|
||||
});
|
||||
});
|
||||
177
lattice/src/App/QueryBuilder/utils.ts
Normal file
177
lattice/src/App/QueryBuilder/utils.ts
Normal 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 };
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue