diff --git a/lattice/src/App/QueryBuilder/CountBuilder/CountBuilder.tsx b/lattice/src/App/QueryBuilder/CountBuilder/CountBuilder.tsx new file mode 100644 index 000000000..edc708b83 --- /dev/null +++ b/lattice/src/App/QueryBuilder/CountBuilder/CountBuilder.tsx @@ -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 = ({ + table, + query, + showInvalid, + onChange +}) => { + const { rowCalls } = query ? query : { rowCalls: [] }; + + return ( +
+ { + onChange({ + ...query, + rowCalls, + operator + }); + }} + /> +
+ ); +}; diff --git a/lattice/src/App/QueryBuilder/CountBuilder/index.ts b/lattice/src/App/QueryBuilder/CountBuilder/index.ts new file mode 100644 index 000000000..b83081751 --- /dev/null +++ b/lattice/src/App/QueryBuilder/CountBuilder/index.ts @@ -0,0 +1 @@ +export * from './CountBuilder'; diff --git a/lattice/src/App/QueryBuilder/ExtractBuilder/ExtractBuilder.module.scss b/lattice/src/App/QueryBuilder/ExtractBuilder/ExtractBuilder.module.scss new file mode 100644 index 000000000..38dc38646 --- /dev/null +++ b/lattice/src/App/QueryBuilder/ExtractBuilder/ExtractBuilder.module.scss @@ -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); + } +} diff --git a/lattice/src/App/QueryBuilder/ExtractBuilder/ExtractBuilder.tsx b/lattice/src/App/QueryBuilder/ExtractBuilder/ExtractBuilder.tsx new file mode 100644 index 000000000..9c52f7f4b --- /dev/null +++ b/lattice/src/App/QueryBuilder/ExtractBuilder/ExtractBuilder.tsx @@ -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 = ({ + table, + query, + showInvalid, + onChange +}) => { + const { columns, rowCalls } = query + ? query + : { + columns: [], + rowCalls: [] + }; + const [showColumnSelector, setShowColumnSelector] = useState(false); + + useEffectOnce(() => { + if (!query) { + onChange({ + columns: table.fields.map((field) => field.name), + operation: 'Extract', + rowCalls: [] + }); + } + }); + + return ( +
+
+ setShowColumnSelector(true)} + > + Configure result fields + + + + +
+ + ({ + 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)} + /> + + { + onChange({ + ...query, + rowCalls, + operator + }); + }} + /> +
+ ); +}; diff --git a/lattice/src/App/QueryBuilder/ExtractBuilder/index.ts b/lattice/src/App/QueryBuilder/ExtractBuilder/index.ts new file mode 100644 index 000000000..a923f7767 --- /dev/null +++ b/lattice/src/App/QueryBuilder/ExtractBuilder/index.ts @@ -0,0 +1 @@ +export * from './ExtractBuilder'; diff --git a/lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.module.scss b/lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.module.scss new file mode 100644 index 000000000..74cdd6f6e --- /dev/null +++ b/lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.module.scss @@ -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; +} diff --git a/lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.tsx b/lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.tsx new file mode 100644 index 000000000..c0486352f --- /dev/null +++ b/lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.tsx @@ -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 = ({ + 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 ( +
+ + ['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() + } + }) + } + /> + +
+
+ + Filter (optional) + {filters.length > 0 && ( + + + + )} + + {filter ? ( + onChange({ ...query, filter: undefined })} + > + Clear Filter + + ) : null} +
+ {filter ? ( + {filter} + ) : filters.length > 0 ? ( + { + const { rowCalls, operator } = filters[queryIdx]; + const res = stringifyRowData(rowCalls, operator); + if (!res.error) { + onChange({ ...query, filter: res.queryString }); + } + }} + /> + ) : ( +
+ No available filters. To use filters, save an Extract query for{' '} + {table.name} with at least one field constraint. +
+ )} +
+ +
+
+ + Sort (optional) + + onChange({ ...query, sort: [] })} + > + Clear + +
+ { + 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} + /> +
+
+ ); +}; diff --git a/lattice/src/App/QueryBuilder/GroupByBuilder/index.ts b/lattice/src/App/QueryBuilder/GroupByBuilder/index.ts new file mode 100644 index 000000000..11f595080 --- /dev/null +++ b/lattice/src/App/QueryBuilder/GroupByBuilder/index.ts @@ -0,0 +1 @@ +export * from './GroupByBuilder'; diff --git a/lattice/src/App/QueryBuilder/GroupBySort/GroupBySort.tsx b/lattice/src/App/QueryBuilder/GroupBySort/GroupBySort.tsx index fca83a954..c75f9320b 100644 --- a/lattice/src/App/QueryBuilder/GroupBySort/GroupBySort.tsx +++ b/lattice/src/App/QueryBuilder/GroupBySort/GroupBySort.tsx @@ -20,7 +20,7 @@ export const GroupBySort: FC = ({ 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 = ({ value={hasPrimary ? sort[0].sortValue : ''} options={sortOptions} onChange={(value) => onPrimaryChange(value)} + allowEmpty={true} /> {primary.includes('sum') ? ( @@ -99,6 +100,7 @@ export const GroupBySort: FC = ({ (option) => !option.value.includes(primary) )} onChange={(value) => onSecondaryChange(value)} + allowEmpty={true} /> {secondary.includes('sum') ? ( diff --git a/lattice/src/App/QueryBuilder/QueryBuilder.module.scss b/lattice/src/App/QueryBuilder/QueryBuilder.module.scss index b5bc0985a..33c34ede7 100644 --- a/lattice/src/App/QueryBuilder/QueryBuilder.module.scss +++ b/lattice/src/App/QueryBuilder/QueryBuilder.module.scss @@ -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; + } } diff --git a/lattice/src/App/QueryBuilder/QueryBuilder.tsx b/lattice/src/App/QueryBuilder/QueryBuilder.tsx index 08a9f02de..d4c890324 100644 --- a/lattice/src/App/QueryBuilder/QueryBuilder.tsx +++ b/lattice/src/App/QueryBuilder/QueryBuilder.tsx @@ -1,276 +1,139 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; -import AddIcon from '@material-ui/icons/Add'; -import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; import ArrowBackIcon from '@material-ui/icons/ArrowBack'; import Button from '@material-ui/core/Button'; import CircularProgress from '@material-ui/core/CircularProgress'; -import CloseIcon from '@material-ui/icons/Close'; -import IconButton from '@material-ui/core/IconButton'; -import InfoIcon from '@material-ui/icons/Info'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; +import Divider from '@material-ui/core/Divider'; import Popover from '@material-ui/core/Popover'; -import Split from 'react-split'; import TextField from '@material-ui/core/TextField'; -import Tooltip from '@material-ui/core/Tooltip'; import Typography from '@material-ui/core/Typography'; -import { Block } from 'shared/Block'; -import { ColumnSelector } from './ColumnSelector'; -import { formatDuration } from 'shared/utils/formatDuration'; import { - Operator, - groupOperators, - RowCallType, - RowGrouping, - RowsCallType -} from './rowTypes'; -import { getIPRange } from 'get-ip-range'; -import { QueryResults } from 'App/Query/QueryResults'; -import { ResultType } from 'App/Query/QueryContainer'; -import { RowCall } from './RowCall'; -import { SavedQueries } from './SavedQueries'; + cleanupRows, + stringifyCount, + stringifyExtract, + stringifyGroupBy +} from './utils'; +import { CountBuilder } from './CountBuilder'; +import { ExtractBuilder } from './ExtractBuilder'; +import { GroupByBuilder } from './GroupByBuilder'; import { Select } from 'shared/Select'; -import { GroupBySort, SortOption } from './GroupBySort'; -import { stringifyRowData } from './stringifyRowData'; import css from './QueryBuilder.module.scss'; type QueryBuilderProps = { tables: any[]; - results?: ResultType; - fullResultsCount?: number; - fullRecordsCount?: number; - error?: ResultType; - loading: boolean; - onQuery: ( - table: any, + savedQuery: number; + onRun: ( + table: string, operation: string, - rowData: RowGrouping[], - columns: string[], - operator?: Operator + query: string, + countQuery?: string ) => void; - onRunGroupBy: ( - table: any, - rowsData: RowsCallType, - sort: SortOption[], - filter?: string - ) => void; - onExternalLookup: (table: string, columns: number[]) => void; onClear: () => void; + onExitEdit: () => void; + onSaveQuery: () => void; }; export const QueryBuilder: FC = ({ tables, - results, - fullResultsCount, - fullRecordsCount, - error, - loading, - onQuery, - onRunGroupBy, - onExternalLookup, - onClear + savedQuery, + onRun, + onClear, + onExitEdit, + onSaveQuery }) => { - const [newOperatorEl, setNewOperatorEl] = useState(null); - const [operatorEl, setOperatorEl] = useState(null); const [saveButtonEl, setSaveButtonEl] = useState(null); - const [showBuilder, setShowBuilder] = useState(true); - const [showColumnSelector, setShowColumnSelector] = useState(false); - const [hasInvalid, setHasInvalid] = useState(false); + const queriesList = JSON.parse(localStorage.getItem('saved-queries') || '[]'); + const [editMode, setEditMode] = useState(false); const [selectedTable, setSelectedTable] = useState(tables[0]); - const [selectedColumns, setSelectedColumns] = useState< - { name: string; show: boolean }[] - >( - tables[0].fields.map((field) => ({ - name: field.name, - show: true - })) - ); const [operation, setOperation] = useState('Extract'); - const [groupByCall, setGroupByCall] = useState({ - primary: '', - secondary: '' + const [query, setQuery] = useState({ + table: tables[0].name, + operation: 'Extract', + columns: tables[0].fields.map((field) => field.name) }); - const [rowCalls, setRowCalls] = useState([]); - const [operator, setOperator] = useState(); - const [editSavedIdx, setEditSavedIdx] = useState(-1); - const [queryName, setQueryName] = useState(''); - const [queryDescription, setQueryDescription] = useState(''); + const [showInvalid, setShowInvalid] = useState(false); const [updating, setUpdating] = useState(false); - const [queriesList, setQueriesList] = useState( - JSON.parse(localStorage.getItem('saved-queries') || '[]') - ); - const [groupByFilters, setGroupByFilters] = useState([]); - const [filter, setFilter] = useState(); - const [sort, setSort] = useState([]); - const colSizes = JSON.parse( - localStorage.getItem('builderColSizes') || '[25, 75]' - ); - const queryNameIdx = queriesList.findIndex((q) => q.name === queryName); - const inEditMode = editSavedIdx >= 0; + const [queryNameIdx, setQueryNameIdx] = useState(-1); useEffect(() => { - setGroupByFilters( - queriesList.filter( - (q) => - q.table === selectedTable.name && - q.operation === 'Extract' && - q.rowCalls.length > 0 - ) - ); - }, [selectedTable, queriesList]); + if (savedQuery > -1) { + const updatedList = JSON.parse( + localStorage.getItem('saved-queries') || '[]' + ); + setQuery(updatedList[savedQuery]); + setEditMode(true); + setOperation(updatedList[savedQuery].operation); + const table = tables.find( + (table) => table.name === updatedList[savedQuery].table + ); + setSelectedTable(table); + } + }, [savedQuery, tables]); - const getColumnsToShow = (cols?: { name: string; show: boolean }[]) => { - let columns: string[] = []; - const list = cols ? cols : selectedColumns; - list.forEach((field) => { - if (field.show) { - columns.push(field.name); - } - }); + useEffect(() => { + if (query.name) { + setQueryNameIdx(queriesList.findIndex((q) => q.name === query.name)); + } + }, [query, queriesList]); - return columns; + const cleanRows = () => { + const cleanRows = cleanupRows(query?.rowCalls); + return { + ...query, + rowCalls: cleanRows.cleanRowCalls, + isInvalid: cleanRows.isInvalid + }; }; - const onChangeColumns = (cols) => { - setSelectedColumns(cols); - if (results) { - const { cleanRowCalls, isInvalid } = cleanupRows(); + const runQuery = () => { + setShowInvalid(true); + let queryString = ''; + let countQuery = ''; - setHasInvalid(isInvalid); - setRowCalls(cleanRowCalls); - if (!isInvalid) { - let columns: string[] = []; - if (operation === 'Extract') { - columns = getColumnsToShow(cols); + switch (operation) { + case 'Extract': + const cleanExtractQuery = cleanRows(); + setQuery(cleanExtractQuery); + const extract = stringifyExtract(cleanExtractQuery); + if (!extract.error && !cleanExtractQuery.isInvalid) { + queryString = extract.queryString; + countQuery = extract.countQuery; } - onQuery(selectedTable, operation, cleanRowCalls, columns, operator); - } - } - }; - - const onNewGroup = () => { - const newRow: RowCallType[] = [ - { - field: '', - rowOperator: '=', - value: '', - type: 'set', - keys: true - } - ]; - - setRowCalls([...rowCalls, { row: newRow }]); - }; - - const onRemoveGroup = (groupIdx: number) => { - if (groupIdx === 0) { - setRowCalls(rowCalls.slice(1)); - } else { - const updatedRowCalls = [...rowCalls]; - updatedRowCalls.splice(groupIdx, 1); - setRowCalls(updatedRowCalls); - - if (updatedRowCalls.length < 1) { - setOperator(undefined); - } - } - }; - - const onUpdateRow = ( - groupIdx: number, - newRowData?: RowCallType[], - operator?: 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 (operator) { - clone.operator = operator; - } - updatedRowCalls.splice(groupIdx, 1, clone); - } else { - updatedRowCalls.splice(groupIdx, 1); - } - setRowCalls(updatedRowCalls); - }; - - const cleanupRows = () => { - // remove empty groups and row calls - let isInvalid = false; - let cleanRowCalls: RowGrouping[] = []; - let cleanGroups: RowGrouping[] = []; - - 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; - } - } + break; + case 'GroupBy': + queryString = stringifyGroupBy(query); + break; + case 'Count': + const cleanCountQuery = cleanRows(); + setQuery(cleanCountQuery); + const count = stringifyCount(cleanCountQuery); + if (!count.error && !cleanCountQuery.isInvalid) { + queryString = count.queryString; } - }); - }); + break; + default: + break; + } - cleanGroups.forEach((group) => { - if (group.row.length > 0) { - cleanRowCalls.push(group); - } - }); - - return { cleanRowCalls, isInvalid }; + if (queryString && !query.isInvalid) { + setShowInvalid(false); + onRun(selectedTable.name, operation, queryString, countQuery); + } else { + setShowInvalid(true); + } }; - const onRunClick = () => { - if (operation === 'GroupBy') { - let isInvalid = false; - if (!groupByCall.primary) { - isInvalid = true; - } else if ( - sort.length > 0 && - sort[0].sortValue.includes('sum') && - !sort[0].field - ) { - isInvalid = true; - } else if ( - sort.length > 1 && - sort[1].sortValue.includes('sum') && - !sort[1].field - ) { - isInvalid = true; - } - setHasInvalid(isInvalid); - if (!isInvalid) { - onRunGroupBy(selectedTable, groupByCall, sort, filter); - } - } else { - const { cleanRowCalls, isInvalid } = cleanupRows(); - - setHasInvalid(isInvalid); - setRowCalls(cleanRowCalls); - if (!isInvalid) { - let columns: string[] = []; - if (operation === 'Extract') { - columns = getColumnsToShow(); - } - onQuery(selectedTable, operation, cleanRowCalls, columns, operator); - } - } + const reset = () => { + setQuery({ + table: selectedTable.name, + columns: selectedTable.fields.map((field) => field.name), + operation, + isInvalid: false + }); + setShowInvalid(false); + setEditMode(false); + setQueryNameIdx(-1); + onExitEdit(); + onClear(); }; const onSave = () => { @@ -278,662 +141,270 @@ export const QueryBuilder: FC = ({ setTimeout(() => { setUpdating(false); }, 1500); - const { cleanRowCalls, isInvalid } = cleanupRows(); + const cleanQuery = cleanRows(); let queries = [...queriesList]; - const queryObj = { - name: queryName, - description: queryDescription, - table: selectedTable.name, - operation, - columns: operation === 'Extract' ? getColumnsToShow() : undefined, - operator, - rowCalls: cleanRowCalls, - groupByCall, - filter, - isInvalid - }; - if (inEditMode && queries.length > 0) { + if (editMode && queries.length > 0) { const clone = [...queries]; - clone.splice(editSavedIdx, 1, queryObj); + clone.splice(savedQuery, 1, cleanQuery); localStorage.setItem('saved-queries', JSON.stringify(clone)); - setQueriesList(clone); } else { - queries.push(queryObj); + queries.push(cleanQuery); localStorage.setItem('saved-queries', JSON.stringify(queries)); - setQueriesList(queries); - setEditSavedIdx(queries.length - 1); } + onSaveQuery(); }; - const onExportLogs = () => { - if (results) { - const columns = results.rows.map((row) => row[0].uint64val); - const table = results.index ? results.index : ''; - onExternalLookup(table, columns); + const renderBuilder = () => { + switch (operation) { + case 'Extract': + return ( + setQuery(q)} + /> + ); + case 'GroupBy': + return ( + setQuery(q)} + /> + ); + case 'Count': + return ( + setQuery(q)} + /> + ); + default: + return
Invalid Operation
; } }; - const onSavedQueryClick = (idx: number) => { - const { - name, - description, - table, - operation, - columns, - operator, - rowCalls, - groupByCall, - filter, - isInvalid - } = queriesList[idx]; - const tableDetails = tables.find((t) => t.name === table); - setEditSavedIdx(idx); - setSelectedTable(tableDetails); - if (columns) { - setSelectedColumns( - tableDetails.fields.map((field) => - columns.includes(field.name) - ? { name: field.name, show: true } - : { name: field.name, show: false } - ) - ); - } - setOperation(operation); - setOperator(operator); - setRowCalls(rowCalls); - setGroupByCall(groupByCall); - setQueryName(name); - setQueryDescription(description || ''); - setHasInvalid(!!isInvalid); - setFilter(filter); - - if (!isInvalid) { - if (operation === 'GroupBy') { - onRunGroupBy(tableDetails, groupByCall, sort, filter); - } else { - onQuery(tableDetails, operation, rowCalls, columns, operator); - } - } - }; - - const onRemoveQuery = (queryIdx: number) => { - let queries = [...queriesList]; - queries.splice(queryIdx, 1); - localStorage.setItem('saved-queries', JSON.stringify(queries)); - setQueriesList(queries); - }; - - const reset = () => { - setEditSavedIdx(-1); - setSelectedTable(tables[0]); - setSelectedColumns( - tables[0].fields.map((field) => ({ - name: field.name, - show: true - })) - ); - setOperation('Extract'); - setRowCalls([]); - setQueryName(''); - setQueryDescription(''); - setHasInvalid(false); - setGroupByCall({ - primary: '', - secondary: '' - }); - setSort([]); - setFilter(undefined); - onClear(); - }; - return ( - - 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; - }} - style={{ display: 'flex' }} - className={!showBuilder ? 'hide-gutter' : undefined} - > - {showBuilder ? ( -
-
- setShowBuilder(false)} size="small"> - - +
+
+ {editMode ? ( + + + Back to new query + + ) : null} + + {editMode ? 'Edit Saved Query' : 'New Query'} + + + {editMode ? ( +
+ = 0 && queryNameIdx !== savedQuery) + } + helperText={ + queryNameIdx >= 0 && queryNameIdx !== savedQuery + ? 'Query name must be unique' + : '' + } + onChange={(event) => + setQuery({ ...query, name: event.target.value }) + } + margin="normal" + size="small" + required + fullWidth + /> + + + setQuery({ ...query, description: event.target.value }) + } + margin="normal" + size="small" + fullWidth + />
-
- {inEditMode ? ( - - - Back to new query - - ) : null} - - {inEditMode ? 'Edit Saved Query' : 'New Query'} - -
-
- {inEditMode ? ( -
- = 0 && queryNameIdx !== editSavedIdx) - } - helperText={ - queryNameIdx >= 0 && queryNameIdx !== editSavedIdx - ? 'Query name must be unique' - : '' - } - onChange={(event) => setQueryName(event.target.value)} - margin="normal" - size="small" - required - fullWidth - /> + ) : null} - setQueryDescription(event.target.value)} - margin="normal" - size="small" - fullWidth - /> -
- ) : null} -
- { + return { label: table.name, value: table.name }; + })} + onChange={(value) => { + const table = tables.find((t) => t.name === value); + setSelectedTable(table); + setShowInvalid(false); + setQuery({ + ...query, + table: table.name, + columns: table.fields.map((field) => field.name), + operation: query.operation, + rowCalls: [], + operator: undefined, + groupByCall: undefined, + sort: undefined, + filter: undefined, + isInvalid: false + }); + }} + /> -
- - [ - 'set', - 'time', - 'mutex', - 'bool', - 'int', - 'timestamp' - ].includes(f.options.type) - ) - .map((field) => { - return { - label: `${field.name} (${field.options.type})`, - value: field.name - }; - })} - onChange={(value) => - setGroupByCall({ - ...groupByCall, - primary: value.toString() - }) - } - error={hasInvalid && !groupByCall.primary} - /> - { + setOperation(value); + setShowInvalid(false); + if ( + ['Extract', 'Count'].includes(operation) && + ['Extract', 'Count'].includes(value) + ) { + setQuery({ + ...query, + operation: value + }); + } else { + setQuery({ + ...query, + operation: value, + columns: selectedTable.fields.map((field) => field.name), + rowCalls: [], + operator: undefined, + groupByCall: undefined, + sort: undefined, + filter: undefined, + isInvalid: false + }); + } + }} + /> +
- +
{renderBuilder()}
+
+
+ + {editMode ? ( + + ) : ( + + + setSaveButtonEl(null)} + anchorOrigin={{ + vertical: 'top', + horizontal: 'left' + }} + transformOrigin={{ + vertical: 'bottom', + horizontal: 'left' + }} + > +
+ + Give your query a unique name and a short description to + help you identify it for future use. + + + = 0} + helperText={ + queryNameIdx >= 0 ? 'Query name must be unique' : '' + } + onChange={(event) => + setQuery({ ...query, name: event.target.value }) + } + margin="dense" + required + fullWidth + /> + + + setQuery({ ...query, description: event.target.value }) + } + margin="dense" + fullWidth + /> + +
+
+ + +
+
+
+
+
+ )} +
+ +
+
); }; diff --git a/lattice/src/App/QueryBuilder/QueryBuilderContainer.module.scss b/lattice/src/App/QueryBuilder/QueryBuilderContainer.module.scss index cd406d4e8..a5cd6c77d 100644 --- a/lattice/src/App/QueryBuilder/QueryBuilderContainer.module.scss +++ b/lattice/src/App/QueryBuilder/QueryBuilderContainer.module.scss @@ -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; diff --git a/lattice/src/App/QueryBuilder/QueryBuilderContainer.tsx b/lattice/src/App/QueryBuilder/QueryBuilderContainer.tsx index 40e42fb8c..ef9f9062a 100644 --- a/lattice/src/App/QueryBuilder/QueryBuilderContainer.tsx +++ b/lattice/src/App/QueryBuilder/QueryBuilderContainer.tsx @@ -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(); + const colSizes = JSON.parse( + localStorage.getItem('builderColSizes') || '[25, 75]' + ); + const [queriesList, setQueriesList] = useState( + JSON.parse(localStorage.getItem('saved-queries') || '[]') + ); + + const [tables, setTables] = useState([]); + const [showBuilder, setShowBuilder] = useState(true); const [results, setResults] = useState(); const [fullCount, setFullCount] = useState(); const [recordsCount, setRecordsCount] = useState(); const [errorResult, setErrorResult] = useState(); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + const [savedQuery, setSavedQuery] = useState(-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 ( - { - setResults(undefined); - setFullCount(undefined); - setErrorResult(undefined); - }} - /> - - setError('')}> - {error} - - + {tables.length > 0 ? ( + + 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 ? ( +
+
+ setShowBuilder(false)} size="small"> + + +
+ setSavedQuery(-1)} + onSaveQuery={onSaveQuery} + /> +
+ ) : ( +
+ setShowBuilder(true)} size="small"> + + +
+ )} +
+ +
+ + Results{' '} + + {results?.query.includes('Extract(') ? ( +
+ + + + +
+ ) : null} +
+ {loading ?
Loading...
: null} + {results && !loading ? ( + + {fullCount && recordsCount ? ( +
+ {results.duration ? ( +
+ {recordsCount.toLocaleString()} records scanned in{' '} + {formatDuration(results.duration, true)}. +
+ ) : null} +
+ Showing{' '} + {fullCount > 1000 ? 'first 1,000 rows of' : 'all'}{' '} + {fullCount.toLocaleString()} results. +
+
+ ) : null} + +
+ + ) : null} + {errorResult && !loading ?
{errorResult.error}
: null} + {!loading && !results && !error ? ( + + + Build a query to see results + + {queriesList.length > 0 ? ( +
+ + Saved Queries + + setSavedQuery(queryIdx)} + onRemoveQuery={onRemoveQuery} + /> +
+ ) : null} +
+ ) : null} + +
+ + setError('')}> + {error} + + + + ) : ( + + + Query Builder + + + + There are no tables to query. + + + + )}
- ) : ( - - - Query Builder - - - - There are no tables to query. - - - ); }; diff --git a/lattice/src/App/QueryBuilder/RowCall/RowCall.module.scss b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCall/RowCall.module.scss similarity index 100% rename from lattice/src/App/QueryBuilder/RowCall/RowCall.module.scss rename to lattice/src/App/QueryBuilder/RowCallBuilder/RowCall/RowCall.module.scss diff --git a/lattice/src/App/QueryBuilder/RowCall/RowCall.tsx b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCall/RowCall.tsx similarity index 99% rename from lattice/src/App/QueryBuilder/RowCall/RowCall.tsx rename to lattice/src/App/QueryBuilder/RowCallBuilder/RowCall/RowCall.tsx index cd0501060..a5f35f971 100644 --- a/lattice/src/App/QueryBuilder/RowCall/RowCall.tsx +++ b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCall/RowCall.tsx @@ -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'; diff --git a/lattice/src/App/QueryBuilder/RowCall/helpers.ts b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCall/helpers.ts similarity index 100% rename from lattice/src/App/QueryBuilder/RowCall/helpers.ts rename to lattice/src/App/QueryBuilder/RowCallBuilder/RowCall/helpers.ts diff --git a/lattice/src/App/QueryBuilder/RowCall/index.ts b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCall/index.ts similarity index 100% rename from lattice/src/App/QueryBuilder/RowCall/index.ts rename to lattice/src/App/QueryBuilder/RowCallBuilder/RowCall/index.ts diff --git a/lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.module.scss b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.module.scss new file mode 100644 index 000000000..326b78876 --- /dev/null +++ b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.module.scss @@ -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; + } +} diff --git a/lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.tsx b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.tsx new file mode 100644 index 000000000..7ba6fc7ae --- /dev/null +++ b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.tsx @@ -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 = ({ + rowCalls = [], + fields, + showInvalid, + onChange +}) => { + const [newOperatorEl, setNewOperatorEl] = useState(null); + const [operatorEl, setOperatorEl] = useState(null); + const [operator, setOperator] = useState(); + + 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 ( + + {rowCalls?.map((rowCall, idx) => ( + + {idx > 0 ? ( + +
+
+ +
+
+ setOperatorEl(null)} + > + {groupOperators.map((op: Operator) => ( + { + setOperator(op); + setOperatorEl(null); + }} + > + {op} + + ))} + + + ) : null} + + onUpdateRow(idx, updatedRow, operator, isNot) + } + onRemoveGroup={() => onRemoveGroup(idx)} + /> + + ))} + +
+
+ { + if (rowCalls.length === 0 || !!operator) { + onNewGroup(); + } else { + setNewOperatorEl(event.currentTarget); + } + }} + > + + +
+
+ + setNewOperatorEl(null)} + > + {groupOperators.map((op: Operator) => ( + { + setOperator(op); + onNewGroup(op); + setNewOperatorEl(null); + }} + > + {op} + + ))} + + + ); +}; diff --git a/lattice/src/App/QueryBuilder/RowCallBuilder/index.ts b/lattice/src/App/QueryBuilder/RowCallBuilder/index.ts new file mode 100644 index 000000000..2f2d839f8 --- /dev/null +++ b/lattice/src/App/QueryBuilder/RowCallBuilder/index.ts @@ -0,0 +1 @@ +export * from './RowCallBuilder'; diff --git a/lattice/src/App/QueryBuilder/stringifyRowData.ts b/lattice/src/App/QueryBuilder/stringifyRowData.ts deleted file mode 100644 index 0faa642c5..000000000 --- a/lattice/src/App/QueryBuilder/stringifyRowData.ts +++ /dev/null @@ -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 }; -} diff --git a/lattice/src/App/QueryBuilder/stringifyRowData.test.ts b/lattice/src/App/QueryBuilder/utils.test.ts similarity index 61% rename from lattice/src/App/QueryBuilder/stringifyRowData.test.ts rename to lattice/src/App/QueryBuilder/utils.test.ts index 22f439e82..d34e15ef9 100644 --- a/lattice/src/App/QueryBuilder/stringifyRowData.test.ts +++ b/lattice/src/App/QueryBuilder/utils.test.ts @@ -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))'); + }); +}); diff --git a/lattice/src/App/QueryBuilder/utils.ts b/lattice/src/App/QueryBuilder/utils.ts new file mode 100644 index 000000000..441b0ab8d --- /dev/null +++ b/lattice/src/App/QueryBuilder/utils.ts @@ -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 }; +};