diff --git a/lattice/src/App.tsx b/lattice/src/App.tsx index 89bff9fce..f465bd480 100644 --- a/lattice/src/App.tsx +++ b/lattice/src/App.tsx @@ -9,7 +9,7 @@ import { Nav } from 'shared/Nav'; import { NotFound } from 'App/NotFound'; import { MoleculaTablesContainer } from 'App/MoleculaTables'; import { QueryContainer } from 'App/Query'; -import { QBuilderContainer } from 'App/QBuilder'; +import { QueryBuilderContainer } from 'App/QueryBuilder'; import css from './App.module.scss'; const App = () => { @@ -45,7 +45,7 @@ const App = () => { - + diff --git a/lattice/src/App/QBuilder/QBuilder.module.scss b/lattice/src/App/QBuilder/QBuilder.module.scss deleted file mode 100644 index 33c34ede7..000000000 --- a/lattice/src/App/QBuilder/QBuilder.module.scss +++ /dev/null @@ -1,52 +0,0 @@ -.sharedConfig { - padding: 32px 32px 0; - - .configSelector { - margin-bottom: 24px; - } -} - -.textLink { - color: var(--primary); - font-size: 12px; - - .icon { - margin-right: 4px; - vertical-align: text-top; - } - - &:hover { - cursor: pointer; - text-decoration: underline; - color: var(--primary); - } -} - -.builder { - display: flex; - flex-direction: column; - height: 100%; - - .queryMetadata { - border-bottom: 1px solid var(--divider); - padding-bottom: 16px; - margin-bottom: 16px; - } -} - -.config { - padding: 24px 32px 0; - flex-grow: 1; -} - -.builderActions { - padding: 16px 32px; - display: flex; - justify-content: space-between; - - .mainActions { - display: grid; - grid-template-columns: auto auto; - grid-gap: 8px; - } -} diff --git a/lattice/src/App/QBuilder/QBuilder.tsx b/lattice/src/App/QBuilder/QBuilder.tsx deleted file mode 100644 index 3e5e0a8be..000000000 --- a/lattice/src/App/QBuilder/QBuilder.tsx +++ /dev/null @@ -1,388 +0,0 @@ -import React, { FC, Fragment, useEffect, useState } from 'react'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; -import Button from '@material-ui/core/Button'; -import CircularProgress from '@material-ui/core/CircularProgress'; -import Divider from '@material-ui/core/Divider'; -import Popover from '@material-ui/core/Popover'; -import TextField from '@material-ui/core/TextField'; -import Typography from '@material-ui/core/Typography'; -import { - cleanupRows, - stringifyCount, - stringifyExtract, - stringifyGroupBy -} from './utils'; -import { CountBuilder } from './CountBuilder'; -import { ExtractBuilder } from './ExtractBuilder'; -import { GroupByBuilder } from './GroupByBuilder'; -import { Select } from 'shared/Select'; -import css from './QBuilder.module.scss'; - -type QBuilderProps = { - tables: any[]; - savedQuery: number; - onRun: ( - table: string, - operation: string, - query: string, - countQuery?: string - ) => void; - onClear: () => void; - onExitEdit: () => void; - onSaveQuery: () => void; -}; - -export const QBuilder: FC = ({ - tables, - savedQuery, - onRun, - onClear, - onExitEdit, - onSaveQuery -}) => { - const [saveButtonEl, setSaveButtonEl] = useState(null); - const queriesList = JSON.parse(localStorage.getItem('saved-queries') || '[]'); - const [editMode, setEditMode] = useState(false); - const [selectedTable, setSelectedTable] = useState(tables[0]); - const [operation, setOperation] = useState('Extract'); - const [query, setQuery] = useState({ - table: tables[0].name, - operation: 'Extract', - columns: tables[0].fields.map((field) => field.name) - }); - const [showInvalid, setShowInvalid] = useState(false); - const [updating, setUpdating] = useState(false); - const [queryNameIdx, setQueryNameIdx] = useState(-1); - - useEffect(() => { - if (savedQuery > -1) { - const updatedList = JSON.parse( - localStorage.getItem('saved-queries') || '[]' - ); - setQuery(updatedList[savedQuery]); - setEditMode(true); - setOperation(updatedList[savedQuery].operation); - const table = tables.find( - (table) => table.name === updatedList[savedQuery].table - ); - setSelectedTable(table); - } - }, [savedQuery, tables]); - - useEffect(() => { - if (query?.name) { - setQueryNameIdx(queriesList.findIndex((q) => q.name === query.name)); - } - }, [query, queriesList]); - - const cleanRows = () => { - const cleanRows = cleanupRows(query?.rowCalls); - return { - ...query, - rowCalls: cleanRows.cleanRowCalls, - isInvalid: cleanRows.isInvalid - }; - }; - - const runQuery = () => { - setShowInvalid(true); - let queryString = ''; - let countQuery = ''; - - switch (operation) { - case 'Extract': - const cleanExtractQuery = cleanRows(); - setQuery(cleanExtractQuery); - const extract = stringifyExtract(cleanExtractQuery); - if (!extract.error && !cleanExtractQuery.isInvalid) { - queryString = extract.queryString; - countQuery = extract.countQuery; - } - break; - case 'GroupBy': - queryString = stringifyGroupBy(query); - break; - case 'Count': - const cleanCountQuery = cleanRows(); - setQuery(cleanCountQuery); - const count = stringifyCount(cleanCountQuery); - if (!count.error && !cleanCountQuery.isInvalid) { - queryString = count.queryString; - } - break; - default: - break; - } - - if (queryString && !query.isInvalid) { - setShowInvalid(false); - onRun(selectedTable.name, operation, queryString, countQuery); - } else { - setShowInvalid(true); - } - }; - - const reset = () => { - setQuery({ - table: selectedTable.name, - columns: selectedTable.fields.map(field => field.name), - operation, - isInvalid: false - }); - setOperation('Extract'); - setShowInvalid(false); - setEditMode(false); - onExitEdit(); - onClear(); - }; - - const onSave = () => { - setUpdating(true); - setTimeout(() => { - setUpdating(false); - }, 1500); - const cleanQuery = cleanRows(); - let queries = [...queriesList]; - - if (editMode && queries.length > 0) { - const clone = [...queries]; - clone.splice(savedQuery, 1, cleanQuery); - localStorage.setItem('saved-queries', JSON.stringify(clone)); - } else { - queries.push(cleanQuery); - localStorage.setItem('saved-queries', JSON.stringify(queries)); - } - onSaveQuery(); - }; - - const renderBuilder = () => { - switch (operation) { - case 'Extract': - return ( - setQuery(q)} - /> - ); - case 'GroupBy': - return ( - setQuery(q)} - /> - ); - case 'Count': - return ( - setQuery(q)} - /> - ); - default: - return
Invalid Operation
; - } - }; - - return ( -
-
- {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 - /> -
- ) : null} - - { - setOperation(value); - setShowInvalid(false); - setQuery({ - table: selectedTable.name, - operation: value, - 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/QBuilder/QBuilderContainer.module.scss b/lattice/src/App/QBuilder/QBuilderContainer.module.scss deleted file mode 100644 index a5cd6c77d..000000000 --- a/lattice/src/App/QBuilder/QBuilderContainer.module.scss +++ /dev/null @@ -1,66 +0,0 @@ -.builderColumn, -.results { - height: calc(100vh - 64px); - overflow: scroll; - position: relative; - - .openClose { - position: absolute; - right: 10px; - top: 10px; - } -} - -.split { - display: flex; -} - -.collapsedBuilder { - border-right: 1px solid rgba(var(--contrast-rgb), 0.1); - text-align: center; - padding-top: 10px; -} - -.resultsBlock { - height: 100%; -} - -.resultsHeader { - display: flex; - align-items: center; - justify-content: space-between; - - .download { - display: flex; - align-items: center; - } - - .downloadInfo { - margin-right: 8px; - } -} - -.infoMessage { - padding: 16px; - border: 1px solid rgba(var(--primary-rgb), 0.5); - background: rgba(var(--primary-rgb), 0.1); - border-radius: 4px; - margin: 4px 0 16px; -} - -.spacer{ - padding-bottom: 16px; -} - -.resultsLoading { - margin-left: 8px; -} - -.savedQueries { - margin-top: 48px; -} - -.noTables { - margin-top: 20px; - padding: 12px 16px; -} diff --git a/lattice/src/App/QBuilder/QBuilderContainer.tsx b/lattice/src/App/QBuilder/QBuilderContainer.tsx deleted file mode 100644 index 7161cb5a3..000000000 --- a/lattice/src/App/QBuilder/QBuilderContainer.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import React, { Fragment, useState } from 'react'; -import Alert from '@material-ui/lab/Alert'; -import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; -import Button from '@material-ui/core/Button'; -import classNames from 'classnames'; -import CloseIcon from '@material-ui/icons/Close'; -import IconButton from '@material-ui/core/IconButton'; -import InfoIcon from '@material-ui/icons/Info'; -import moment, { Moment } from 'moment'; -import Paper from '@material-ui/core/Paper'; -import Split from 'react-split'; -import Snackbar from '@material-ui/core/Snackbar'; -import Tooltip from '@material-ui/core/Tooltip'; -import Typography from '@material-ui/core/Typography'; -import { Block } from 'shared/Block'; -import { formatDuration } from 'shared/utils/formatDuration'; -import { grpc } from '@improbable-eng/grpc-web'; -import { pilosa } from 'services/eventServices'; -import { QBuilder } from './QBuilder'; -import { queryPQL } from 'services/grpcServices'; -import { QueryResults } from 'App/Query/QueryResults'; -import { ResultType } from 'App/Query/QueryContainer'; -import { RowResponse } from 'proto/pilosa_pb'; -import { SavedQueries } from 'App/QueryBuilder/SavedQueries'; -import { useEffectOnce } from 'react-use'; -import css from './QBuilderContainer.module.scss'; - -let streamingResults: ResultType = { - query: '', - operation: '', - type: 'PQL', - headers: [], - rows: [], - roundtrip: 0, - error: '' -}; - -export const QBuilderContainer = () => { - let startTime: Moment; - let exportRows: any[] = []; - const colSizes = JSON.parse( - localStorage.getItem('builderColSizes') || '[25, 75]' - ); - const [queriesList, setQueriesList] = useState( - JSON.parse(localStorage.getItem('saved-queries') || '[]') - ); - - const [tables, setTables] = useState([]); - 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) => { - setTables(res.data.indexes); - }); - }); - - const handleQueryMessages = (message: RowResponse) => { - const response = message.toObject(); - if (response.headersList.length > 0 && response.duration > 0) { - streamingResults.headers = response.headersList; - streamingResults.duration = response.duration; - } - streamingResults.rows.push(response.columnsList); - }; - - const handleQueryEnd = (status: grpc.Code, statusMessage: string) => { - if (status !== grpc.Code.OK) { - streamingResults.error = statusMessage; - setErrorResult(streamingResults); - } else { - streamingResults.roundtrip = moment - .duration(moment().diff(startTime)) - .as('milliseconds'); - setErrorResult(undefined); - setResults(streamingResults); - } - setLoading(false); - }; - - const handleExternalLookup = (message: RowResponse) => { - const response = message.toObject(); - let rowStr: string[] = []; - if (exportRows.length === 0) { - const headers = response.headersList.map((header) => header.name); - exportRows.push(headers.join('\t')); - } - response.headersList.forEach((header, idx) => - rowStr.push(response.columnsList[idx][`${header.datatype}val`]) - ); - exportRows.push(rowStr.join('\t')); - }; - - const handleExternalLookupEnd = ( - status: grpc.Code, - statusMessage: string - ) => { - if (status !== grpc.Code.OK) { - setError(statusMessage); - } else if (exportRows.length === 0) { - setError('No record attributes for current query.'); - } else { - const dateTime = moment().unix(); - const element = document.createElement('a'); - const file = new Blob([exportRows.join('\n')], { - type: 'text/plain;charset=utf-8' - }); - element.href = URL.createObjectURL(file); - element.download = `molecula-${results?.index}-${dateTime}.csv`; - document.body.appendChild(element); - element.click(); - exportRows = []; - } - }; - - const onRunQuery = ( - table: string, - operation: string, - query: string, - countQuery?: string - ) => { - streamingResults = { - query, - operation, - type: 'PQL', - headers: [], - rows: [], - index: table, - roundtrip: 0, - error: '' - }; - startTime = moment(); - setLoading(true); - - if (operation !== 'Count') { - if (countQuery) { - pilosa.post.query(table, countQuery).then((res) => { - setFullCount(res.data.results[0]); - }); - } - - pilosa.post - .query(table, `Count(All())`) - .then((res) => setRecordsCount(res.data.results[0])); - } else { - setFullCount(undefined); - } - - queryPQL(table, query, handleQueryMessages, handleQueryEnd); - }; - - const onClear = () => { - setResults(undefined); - setFullCount(undefined); - setErrorResult(undefined); - }; - - const onRemoveQuery = (queryIdx: number) => { - let queries = [...queriesList]; - queries.splice(queryIdx, 1); - localStorage.setItem('saved-queries', JSON.stringify(queries)); - setQueriesList(queries); - }; - - const onSaveQuery = () => { - const updatedList = JSON.parse( - localStorage.getItem('saved-queries') || '[]' - ); - if (savedQuery < 0) { - setSavedQuery(updatedList.length - 1); - } - setQueriesList(updatedList); - }; - - const onExportLogs = () => { - if (results) { - const columns = results.rows.map((row) => row[0].uint64val); - const table = results.index ? results.index : ''; - onExternalLookup(table, columns); - } - }; - - const onExternalLookup = (table: string, columns: number[]) => { - const cols = JSON.stringify(columns); - const query = `ExternalLookup(ConstRow(columns=${cols}), query='select id, "rawlog" from "${table}" where id = ANY($1)')`; - queryPQL(table, query, handleExternalLookup, handleExternalLookupEnd); - }; - - return ( - - {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. - - - - )} -
- ); -}; diff --git a/lattice/src/App/QBuilder/RowCallBuilder/RowCall/index.ts b/lattice/src/App/QBuilder/RowCallBuilder/RowCall/index.ts deleted file mode 100644 index c5f595cf9..000000000 --- a/lattice/src/App/QBuilder/RowCallBuilder/RowCall/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './helpers'; diff --git a/lattice/src/App/QBuilder/index.ts b/lattice/src/App/QBuilder/index.ts deleted file mode 100644 index 058f256f4..000000000 --- a/lattice/src/App/QBuilder/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './QBuilderContainer'; -export * from './rowTypes'; diff --git a/lattice/src/App/QBuilder/rowTypes.ts b/lattice/src/App/QBuilder/rowTypes.ts deleted file mode 100644 index 7ed00293f..000000000 --- a/lattice/src/App/QBuilder/rowTypes.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type Operator = 'and' | 'or'; - -export const groupOperators: Operator[] = ['and', 'or']; - -export type RowGrouping = { - row: RowCallType[]; - isNot?: boolean; - operator?: Operator; -} - -export type RowCallType = { - field: string; - rowOperator: string; - value: string; - type: string; - keys?: boolean; -}; - -export type RowsCallType = { - primary: string; - secondary: string; -}; diff --git a/lattice/src/App/QBuilder/CountBuilder/CountBuilder.tsx b/lattice/src/App/QueryBuilder/CountBuilder/CountBuilder.tsx similarity index 71% rename from lattice/src/App/QBuilder/CountBuilder/CountBuilder.tsx rename to lattice/src/App/QueryBuilder/CountBuilder/CountBuilder.tsx index 03391ec02..edc708b83 100644 --- a/lattice/src/App/QBuilder/CountBuilder/CountBuilder.tsx +++ b/lattice/src/App/QueryBuilder/CountBuilder/CountBuilder.tsx @@ -1,5 +1,6 @@ import React, { FC } from 'react'; -import { RowCallBuilder } from 'App/QBuilder/RowCallBuilder'; +import { Operator, RowGrouping } from 'App/QueryBuilder/rowTypes'; +import { RowCallBuilder } from 'App/QueryBuilder/RowCallBuilder'; type CountBuilderProps = { table: any; @@ -22,12 +23,11 @@ export const CountBuilder: FC = ({ rowCalls={rowCalls} fields={table.fields} showInvalid={showInvalid} - onChange={(rowCalls, operator, isInvalid) => { + onChange={(rowCalls: RowGrouping[], operator?: Operator) => { onChange({ ...query, rowCalls, - operator, - isInvalid + operator }); }} /> diff --git a/lattice/src/App/QBuilder/CountBuilder/index.ts b/lattice/src/App/QueryBuilder/CountBuilder/index.ts similarity index 100% rename from lattice/src/App/QBuilder/CountBuilder/index.ts rename to lattice/src/App/QueryBuilder/CountBuilder/index.ts diff --git a/lattice/src/App/QBuilder/ExtractBuilder/ExtractBuilder.module.scss b/lattice/src/App/QueryBuilder/ExtractBuilder/ExtractBuilder.module.scss similarity index 100% rename from lattice/src/App/QBuilder/ExtractBuilder/ExtractBuilder.module.scss rename to lattice/src/App/QueryBuilder/ExtractBuilder/ExtractBuilder.module.scss diff --git a/lattice/src/App/QBuilder/ExtractBuilder/ExtractBuilder.tsx b/lattice/src/App/QueryBuilder/ExtractBuilder/ExtractBuilder.tsx similarity index 90% rename from lattice/src/App/QBuilder/ExtractBuilder/ExtractBuilder.tsx rename to lattice/src/App/QueryBuilder/ExtractBuilder/ExtractBuilder.tsx index eaae42714..9c52f7f4b 100644 --- a/lattice/src/App/QBuilder/ExtractBuilder/ExtractBuilder.tsx +++ b/lattice/src/App/QueryBuilder/ExtractBuilder/ExtractBuilder.tsx @@ -2,7 +2,8 @@ import React, { FC, useState } from 'react'; import InfoIcon from '@material-ui/icons/Info'; import Tooltip from '@material-ui/core/Tooltip'; import { ColumnSelector } from 'App/QueryBuilder/ColumnSelector'; -import { RowCallBuilder } from 'App/QBuilder/RowCallBuilder'; +import { Operator, RowGrouping } from 'App/QueryBuilder/rowTypes'; +import { RowCallBuilder } from 'App/QueryBuilder/RowCallBuilder'; import { useEffectOnce } from 'react-use'; import css from './ExtractBuilder.module.scss'; @@ -78,12 +79,11 @@ export const ExtractBuilder: FC = ({ rowCalls={rowCalls} fields={table.fields} showInvalid={showInvalid} - onChange={(rowCalls, operator, isInvalid) => { + onChange={(rowCalls: RowGrouping[], operator?: Operator) => { onChange({ ...query, rowCalls, - operator, - isInvalid + operator }); }} /> diff --git a/lattice/src/App/QBuilder/ExtractBuilder/index.ts b/lattice/src/App/QueryBuilder/ExtractBuilder/index.ts similarity index 100% rename from lattice/src/App/QBuilder/ExtractBuilder/index.ts rename to lattice/src/App/QueryBuilder/ExtractBuilder/index.ts diff --git a/lattice/src/App/QBuilder/GroupByBuilder/GroupByBuilder.module.scss b/lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.module.scss similarity index 100% rename from lattice/src/App/QBuilder/GroupByBuilder/GroupByBuilder.module.scss rename to lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.module.scss diff --git a/lattice/src/App/QBuilder/GroupByBuilder/GroupByBuilder.tsx b/lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.tsx similarity index 97% rename from lattice/src/App/QBuilder/GroupByBuilder/GroupByBuilder.tsx rename to lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.tsx index b53bf6c59..56a535d0f 100644 --- a/lattice/src/App/QBuilder/GroupByBuilder/GroupByBuilder.tsx +++ b/lattice/src/App/QueryBuilder/GroupByBuilder/GroupByBuilder.tsx @@ -5,7 +5,7 @@ import Typography from '@material-ui/core/Typography'; import { GroupBySort } from 'App/QueryBuilder/GroupBySort'; import { SavedQueries } from 'App/QueryBuilder/SavedQueries'; import { Select } from 'shared/Select'; -import { stringifyRowData } from 'App/QueryBuilder/stringifyRowData'; +import { stringifyRowData } from 'App/QueryBuilder/utils'; import css from './GroupByBuilder.module.scss'; type GroupByBuilderProps = { @@ -131,7 +131,7 @@ export const GroupByBuilder: FC = ({ const { rowCalls, operator } = filters[queryIdx]; const res = stringifyRowData(rowCalls, operator); if (!res.error) { - onChange({ ...query, filter: res.query }); + onChange({ ...query, filter: res.queryString }); } }} /> diff --git a/lattice/src/App/QBuilder/GroupByBuilder/index.ts b/lattice/src/App/QueryBuilder/GroupByBuilder/index.ts similarity index 100% rename from lattice/src/App/QBuilder/GroupByBuilder/index.ts rename to lattice/src/App/QueryBuilder/GroupByBuilder/index.ts 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..5ab545f1f 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,248 @@ 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({ + table: table.name, + columns: table.fields.map((field) => field.name), + operation: query.operation, + 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); + setQuery({ + table: selectedTable.name, + operation: value, + 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/helpers.ts b/lattice/src/App/QueryBuilder/RowCall/helpers.ts deleted file mode 100644 index ebbd97470..000000000 --- a/lattice/src/App/QueryBuilder/RowCall/helpers.ts +++ /dev/null @@ -1,52 +0,0 @@ -export const operators = { - decimal: [ - { label: '>', value: '>' }, - { label: '<', value: '<' }, - { label: '>=', value: '>=' }, - { label: '<=', value: '<=' }, - { label: '==', value: '=' }, - { label: '!=', value: '!=' }, - ], - int: [ - { label: '>', value: '>' }, - { label: '<', value: '<' }, - { label: '>=', value: '>=' }, - { label: '<=', value: '<=' }, - { label: '==', value: '=' }, - { label: '!=', value: '!=' }, - ], - 'mutex-id': [ - { label: 'is', value: '=' }, - { label: 'is not', value: '!=' } - ], - 'mutex-keys': [ - { label: 'is', value: '=' }, - { label: 'is not', value: '!=' }, - { label: 'like', value: 'like' }, - { label: 'CIDR', value: 'cidr' } - ], - "set-id": [ - { label: 'is', value: '=' }, - { label: 'is not', value: '!=' } - ], - "set-keys": [ - { label: 'is', value: '=' }, - { label: 'is not', value: '!=' }, - { label: 'like', value: 'like' }, - { label: 'CIDR', value: 'cidr' } - ], - "time-id": [ - { label: 'is', value: '=' }, - { label: 'is not', value: '!=' } - ], - "time-keys": [ - { label: 'is', value: '=' }, - { label: 'is not', value: '!=' }, - { label: 'like', value: 'like' } - ], - timestamp: [ - { label: 'is before', value: '<' }, - { label: 'is after', value: '>' }, - { label: 'is', value: '=' }, - ] -} 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/QBuilder/RowCallBuilder/RowCall/helpers.ts b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCall/helpers.ts similarity index 100% rename from lattice/src/App/QBuilder/RowCallBuilder/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/QBuilder/RowCallBuilder/RowCallBuilder.module.scss b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.module.scss similarity index 100% rename from lattice/src/App/QBuilder/RowCallBuilder/RowCallBuilder.module.scss rename to lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.module.scss diff --git a/lattice/src/App/QBuilder/RowCallBuilder/RowCallBuilder.tsx b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.tsx similarity index 88% rename from lattice/src/App/QBuilder/RowCallBuilder/RowCallBuilder.tsx rename to lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.tsx index 48f8e81ea..ba8a1ad8f 100644 --- a/lattice/src/App/QBuilder/RowCallBuilder/RowCallBuilder.tsx +++ b/lattice/src/App/QueryBuilder/RowCallBuilder/RowCallBuilder.tsx @@ -10,18 +10,15 @@ import { RowCallType, RowGrouping } from 'App/QueryBuilder/rowTypes'; -import { RowCall } from 'App/QueryBuilder/RowCall'; +import { RowCall } from './RowCall'; import css from './RowCallBuilder.module.scss'; +import { useEffect } from 'react'; type RowCallBuilderProps = { rowCalls: RowGrouping[]; fields: any[]; showInvalid: boolean; - onChange: ( - rows: RowGrouping[], - operator?: Operator, - hasInvalid?: boolean - ) => void; + onChange: (rows: RowGrouping[], operator?: Operator) => void; }; export const RowCallBuilder: FC = ({ @@ -34,7 +31,7 @@ export const RowCallBuilder: FC = ({ const [operatorEl, setOperatorEl] = useState(null); const [operator, setOperator] = useState(); - const onNewGroup = () => { + const onNewGroup = (op?: Operator) => { const newRow: RowCallType[] = [ { field: '', @@ -45,19 +42,20 @@ export const RowCallBuilder: FC = ({ } ]; - onChange([...rowCalls, { row: newRow }]); + onChange([...rowCalls, { row: newRow }], op ? op : operator); }; const onRemoveGroup = (groupIdx: number) => { if (groupIdx === 0) { - onChange(rowCalls.slice(1)); + onChange(rowCalls.slice(1), operator); } else { const updatedRowCalls = [...rowCalls]; updatedRowCalls.splice(groupIdx, 1); - onChange(updatedRowCalls); + onChange(updatedRowCalls, operator); if (updatedRowCalls.length < 1) { setOperator(undefined); + onChange(updatedRowCalls); } } }; @@ -65,7 +63,7 @@ export const RowCallBuilder: FC = ({ const onUpdateRow = ( groupIdx: number, newRowData?: RowCallType[], - operator?: Operator, + rowGroupOperator?: Operator, isNot?: boolean ) => { const updatedRowCalls = [...rowCalls]; @@ -77,14 +75,14 @@ export const RowCallBuilder: FC = ({ }; if (newRowData.length <= 1) { delete clone.operator; - } else if (operator) { - clone.operator = operator; + } else if (rowGroupOperator) { + clone.operator = rowGroupOperator; } updatedRowCalls.splice(groupIdx, 1, clone); } else { updatedRowCalls.splice(groupIdx, 1); } - onChange(updatedRowCalls); + onChange(updatedRowCalls, operator); }; return ( @@ -167,7 +165,7 @@ export const RowCallBuilder: FC = ({ key={`operator-${op}`} onClick={() => { setOperator(op); - onNewGroup(); + onNewGroup(op); setNewOperatorEl(null); }} > diff --git a/lattice/src/App/QBuilder/RowCallBuilder/index.ts b/lattice/src/App/QueryBuilder/RowCallBuilder/index.ts similarity index 100% rename from lattice/src/App/QBuilder/RowCallBuilder/index.ts rename to lattice/src/App/QueryBuilder/RowCallBuilder/index.ts 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/QBuilder/utils.ts b/lattice/src/App/QueryBuilder/utils.ts similarity index 81% rename from lattice/src/App/QBuilder/utils.ts rename to lattice/src/App/QueryBuilder/utils.ts index 5722fb31f..441b0ab8d 100644 --- a/lattice/src/App/QBuilder/utils.ts +++ b/lattice/src/App/QueryBuilder/utils.ts @@ -1,4 +1,4 @@ -import { RowGrouping } from './rowTypes'; +import { Operator, RowGrouping } from './rowTypes'; import { getIPRange } from 'get-ip-range'; export const stringifyExtract = (query: any) => { @@ -29,8 +29,9 @@ export const stringifyCount = (query: any) => { } } -const stringifyRowData = (rowCalls, operator) => { +export const stringifyRowData = (rowCalls: RowGrouping[], operator?: Operator) => { let queryString = ''; + let error = false; if (rowCalls.length === 0) { queryString = 'All()'; } else { @@ -58,7 +59,7 @@ const stringifyRowData = (rowCalls, operator) => { .join(', '); rowString = `Union(${ipRows})`; } catch (error) { - return { error: true, query: error.message }; + return { error: true, queryString: error.message }; } } else if (rowOperator === 'like') { if (value.includes('%') || value.includes('_')) { @@ -80,34 +81,36 @@ const stringifyRowData = (rowCalls, operator) => { }); }); - queryString = rowsMap - .map((group, idx) => { - let joined = ''; - if (group.length > 1) { - joined = group.map((r) => r).join(', '); - const operator = rowCalls[idx].operator; - if (operator === 'and') { - joined = `Intersect(${joined})`; - } else if (operator === 'or') { - joined = `Union(${joined})`; + if (!error) { + queryString = rowsMap + .map((group, idx) => { + let joined = ''; + if (group.length > 1) { + joined = group.map((r) => r).join(', '); + const operator = rowCalls[idx].operator; + if (operator === 'and') { + joined = `Intersect(${joined})`; + } else if (operator === 'or') { + joined = `Union(${joined})`; + } + } else { + joined = group[0]; } - } else { - joined = group[0]; - } - return rowCalls[idx].isNot ? `Not(${joined})` : joined; - }) - .join(', '); + return rowCalls[idx].isNot ? `Not(${joined})` : joined; + }) + .join(', '); - if (rowCalls.length > 1 && operator) { - if (operator === 'and') { - queryString = `Intersect(${queryString})`; - } else if (operator === 'or') { - queryString = `Union(${queryString})`; + if (rowCalls.length > 1 && operator) { + if (operator === 'and') { + queryString = `Intersect(${queryString})`; + } else if (operator === 'or') { + queryString = `Union(${queryString})`; + } } } } - return { error: false, queryString }; + return { error, queryString }; } export const stringifyGroupBy = (query: any) => {