diff --git a/frontend/src/components/HostMetricsDetail/HostMetricsDetail.styles.scss b/frontend/src/components/HostMetricsDetail/HostMetricsDetail.styles.scss index 511348c463c1..219c0bd46457 100644 --- a/frontend/src/components/HostMetricsDetail/HostMetricsDetail.styles.scss +++ b/frontend/src/components/HostMetricsDetail/HostMetricsDetail.styles.scss @@ -169,6 +169,7 @@ box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); } } + .ant-drawer-close { padding: 0px; } diff --git a/frontend/src/components/InputWithLabel/InputWithLabel.styles.scss b/frontend/src/components/InputWithLabel/InputWithLabel.styles.scss new file mode 100644 index 000000000000..2aaca3eb9f9c --- /dev/null +++ b/frontend/src/components/InputWithLabel/InputWithLabel.styles.scss @@ -0,0 +1,66 @@ +.input-with-label { + display: flex; + flex-direction: row; + + border-radius: 2px 0px 0px 2px; + + .label { + color: var(--bg-vanilla-400); + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 128.571% */ + letter-spacing: 0.56px; + text-transform: uppercase; + + max-width: 150px; + min-width: 120px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + padding: 0px 8px; + + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + + display: flex; + justify-content: flex-start; + align-items: center; + font-weight: var(--font-weight-light); + } + + .input { + flex: 1; + min-width: 150px; + font-family: 'Space Mono', monospace !important; + + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + + border-right: none; + border-left: none; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + } + + .close-btn { + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + height: 38px; + width: 38px; + } + + &.labelAfter { + .input { + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + } + } +} diff --git a/frontend/src/components/InputWithLabel/InputWithLabel.tsx b/frontend/src/components/InputWithLabel/InputWithLabel.tsx new file mode 100644 index 000000000000..7461c88438c2 --- /dev/null +++ b/frontend/src/components/InputWithLabel/InputWithLabel.tsx @@ -0,0 +1,60 @@ +import './InputWithLabel.styles.scss'; + +import { Button, Input, Typography } from 'antd'; +import cx from 'classnames'; +import { X } from 'lucide-react'; +import { useState } from 'react'; + +function InputWithLabel({ + label, + initialValue, + placeholder, + type, + onClose, + labelAfter, +}: { + label: string; + initialValue?: string | number; + placeholder: string; + type?: string; + onClose?: () => void; + labelAfter?: boolean; +}): JSX.Element { + const [inputValue, setInputValue] = useState( + initialValue ? initialValue.toString() : '', + ); + + return ( +
+ {!labelAfter && {label}} + setInputValue(e.target.value)} + /> + {labelAfter && {label}} + {onClose && ( +
+ ); +} + +InputWithLabel.defaultProps = { + type: 'text', + onClose: undefined, + labelAfter: false, + initialValue: undefined, +}; + +export default InputWithLabel; diff --git a/frontend/src/components/QueryBuilderV2/QueryAddOns/QueryAddOns.tsx b/frontend/src/components/QueryBuilderV2/QueryAddOns/QueryAddOns.tsx new file mode 100644 index 000000000000..dc63513c0a7a --- /dev/null +++ b/frontend/src/components/QueryBuilderV2/QueryAddOns/QueryAddOns.tsx @@ -0,0 +1,265 @@ +import { Button, Radio, RadioChangeEvent } from 'antd'; +import InputWithLabel from 'components/InputWithLabel/InputWithLabel'; +import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter'; +import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter'; +import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; +import { BarChart2, ScrollText, X } from 'lucide-react'; +import { useCallback, useState } from 'react'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource } from 'types/common/queryBuilder'; + +interface AddOn { + icon: React.ReactNode; + label: string; + key: string; +} + +const ADD_ONS: Record = { + GROUP_BY: { + icon: , + label: 'Group By', + key: 'group_by', + }, + HAVING: { + icon: , + label: 'Having', + key: 'having', + }, + ORDER_BY: { + icon: , + label: 'Order By', + key: 'order_by', + }, + LIMIT: { + icon: , + label: 'Limit', + key: 'limit', + }, + LEGEND_FORMAT: { + icon: , + label: 'Legend format', + key: 'legend_format', + }, +}; + +function QueryAddOns({ + query, + version, + isListViewPanel, +}: { + query: IBuilderQuery; + version: string; + isListViewPanel: boolean; +}): JSX.Element { + const [selectedViews, setSelectedViews] = useState([]); + + const { handleChangeQueryData } = useQueryOperations({ + index: 0, + query, + entityVersion: '', + }); + + const handleOptionClick = (e: RadioChangeEvent): void => { + if (selectedViews.find((view) => view.key === e.target.value.key)) { + setSelectedViews( + selectedViews.filter((view) => view.key !== e.target.value.key), + ); + } else { + setSelectedViews([...selectedViews, e.target.value]); + } + }; + + const handleChangeGroupByKeys = useCallback( + (value: IBuilderQuery['groupBy']) => { + handleChangeQueryData('groupBy', value); + }, + [handleChangeQueryData], + ); + + const handleChangeOrderByKeys = useCallback( + (value: IBuilderQuery['orderBy']) => { + handleChangeQueryData('orderBy', value); + }, + [handleChangeQueryData], + ); + + const handleRemoveView = useCallback( + (key: string): void => { + setSelectedViews(selectedViews.filter((view) => view.key !== key)); + }, + [selectedViews], + ); + + return ( +
+ {selectedViews.length > 0 && ( +
+ {selectedViews.find((view) => view.key === 'group_by') && ( +
+
+
Group By
+
+ +
+
+
+ )} + {selectedViews.find((view) => view.key === 'having') && ( +
+ { + setSelectedViews( + selectedViews.filter((view) => view.key !== 'having'), + ); + }} + /> +
+ )} + {selectedViews.find((view) => view.key === 'limit') && ( +
+ { + setSelectedViews(selectedViews.filter((view) => view.key !== 'limit')); + }} + /> +
+ )} + {selectedViews.find((view) => view.key === 'order_by') && ( +
+
+
Order By
+
+ +
+
+
+ )} + {selectedViews.find((view) => view.key === 'legend_format') && ( +
+ { + setSelectedViews( + selectedViews.filter((view) => view.key !== 'legend_format'), + ); + }} + /> +
+ )} +
+ )} + +
+ + {Object.values(ADD_ONS).map((addOn) => ( + view.key === addOn.key) + ? 'selected_view tab' + : 'tab' + } + value={addOn} + > +
+ {addOn.icon} + {addOn.label} +
+
+ ))} +
+
+
+ ); +} + +export default QueryAddOns; + +/* + + +
+ + {selectedView.label} +
+
+ +
+ + {selectedView.label} +
+
+ +
+ + {selectedView.label} +
+
+ +
+ + {selectedView.label} +
+
+ +
+ + {selectedView.label} +
+
+ +*/ diff --git a/frontend/src/components/QueryBuilderV2/QueryAggregationOptions/QueryAggregationOptions.styles.scss b/frontend/src/components/QueryBuilderV2/QueryAggregationOptions/QueryAggregationOptions.styles.scss new file mode 100644 index 000000000000..9747aa472300 --- /dev/null +++ b/frontend/src/components/QueryBuilderV2/QueryAggregationOptions/QueryAggregationOptions.styles.scss @@ -0,0 +1,29 @@ +.query-aggregation-container { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + .query-aggregation-options-input { + width: 100%; + height: 36px; + line-height: 36px; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + + font-family: 'Space Mono', monospace !important; + + &::placeholder { + color: var(--bg-vanilla-100); + opacity: 0.5; + } + } + + .query-aggregation-interval { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + } +} diff --git a/frontend/src/components/QueryBuilderV2/QueryAggregationOptions/QueryAggregationOptions.tsx b/frontend/src/components/QueryBuilderV2/QueryAggregationOptions/QueryAggregationOptions.tsx new file mode 100644 index 000000000000..a6778f13600f --- /dev/null +++ b/frontend/src/components/QueryBuilderV2/QueryAggregationOptions/QueryAggregationOptions.tsx @@ -0,0 +1,31 @@ +import './QueryAggregationOptions.styles.scss'; + +import { Input } from 'antd'; +import InputWithLabel from 'components/InputWithLabel/InputWithLabel'; + +function QueryAggregationOptions(): JSX.Element { + return ( +
+ + +
+
every
+
+ {}} + /> +
+
+
+ ); +} + +export default QueryAggregationOptions; diff --git a/frontend/src/components/QueryBuilderV2/QueryBuilderV2.styles.scss b/frontend/src/components/QueryBuilderV2/QueryBuilderV2.styles.scss new file mode 100644 index 000000000000..d18743718732 --- /dev/null +++ b/frontend/src/components/QueryBuilderV2/QueryBuilderV2.styles.scss @@ -0,0 +1,81 @@ +.query-builder-v2 { + display: flex; + flex-direction: column; + gap: 1rem; + + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + padding: 8px; + gap: 4px; + font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', sans-serif; + + border: 1px solid var(--bg-slate-400); + border-right: none; + border-left: none; + + .add-ons-list { + display: flex; + justify-content: space-between; + align-items: center; + + .add-ons-tabs { + display: flex; + + .add-on-tab-title { + display: flex; + gap: var(--margin-2); + align-items: center; + justify-content: center; + font-size: var(--font-size-xs); + font-style: normal; + font-weight: var(--font-weight-normal); + } + + .tab { + border: 1px solid var(--bg-slate-400); + min-width: 114px; + height: 36px; + line-height: 36px; + } + + .tab::before { + background: var(--bg-slate-400); + } + + .selected_view { + color: var(--text-robin-500); + border: 1px solid var(--bg-slate-400); + } + + .selected_view::before { + background: var(--bg-slate-400); + } + } + + .compass-button { + width: 30px; + height: 30px; + + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + } + } + + .selected-add-ons-content { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px; + padding-bottom: 4px; + + .add-on-content { + display: flex; + flex-direction: column; + gap: 8px; + } + } +} diff --git a/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx b/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx index 4e3b2a9ef902..b0dc3ced69a7 100644 --- a/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx +++ b/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx @@ -1,11 +1,23 @@ -import CodeMirrorWhereClause from './CodeMirrorWhereClause/CodeMirrorWhereClause'; -// import QueryWhereClause from './WhereClause/WhereClause'; +import './QueryBuilderV2.styles.scss'; + +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; + +import QueryAddOns from './QueryAddOns/QueryAddOns'; +import QueryAggregationOptions from './QueryAggregationOptions/QueryAggregationOptions'; +import QuerySearch from './QuerySearch/QuerySearch'; function QueryBuilderV2(): JSX.Element { + const { currentQuery } = useQueryBuilder(); + return (
- {/* */} - + + +
); } diff --git a/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.styles.scss b/frontend/src/components/QueryBuilderV2/QuerySearch/QuerySearch.styles.scss similarity index 93% rename from frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.styles.scss rename to frontend/src/components/QueryBuilderV2/QuerySearch/QuerySearch.styles.scss index 413f9258d54c..0b86084e4f2e 100644 --- a/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.styles.scss +++ b/frontend/src/components/QueryBuilderV2/QuerySearch/QuerySearch.styles.scss @@ -3,7 +3,6 @@ height: 100%; display: flex; flex-direction: column; - padding: 16px; gap: 8px; font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; @@ -11,18 +10,17 @@ .cm-editor { border-radius: 2px; overflow: hidden; - margin-bottom: 4px; - background-color: var(--bg-ink-400); + background-color: transparent !important; &:focus-within { border-color: var(--bg-robin-500); - background-color: var(--bg-ink-400); } .cm-content { border-radius: 2px; border: 1px solid var(--Slate-400, #1d212d); - background: var(--Ink-300, #16181d); + padding: 0px !important; + background-color: #121317 !important; &:focus-within { border-color: var(--bg-ink-200); @@ -42,12 +40,20 @@ border-radius: 4px; border: 1px solid var(--bg-slate-200, #1d212d); + background: linear-gradient( + 139deg, + rgba(18, 19, 23, 0.8) 0%, + rgba(18, 19, 23, 0.9) 98.68% + ) !important; + backdrop-filter: blur(20px); + box-sizing: border-box; font-family: 'Space Mono', monospace !important; ul { width: 100% !important; max-width: 100% !important; font-family: 'Space Mono', monospace !important; + min-height: 200px !important; &::-webkit-scrollbar { width: 0.3rem; @@ -66,12 +72,15 @@ li { width: 100% !important; max-width: 100% !important; - line-height: 24px !important; + line-height: 36px !important; + height: 36px !important; padding: 4px 8px !important; display: flex !important; align-items: center !important; gap: 8px !important; + box-sizing: border-box; + overflow: hidden; font-family: 'Space Mono', monospace !important; @@ -80,7 +89,8 @@ } &[aria-selected='true'] { - background-color: rgba(78, 116, 248, 0.7) !important; + // background-color: rgba(78, 116, 248, 0.7) !important; + background: rgba(171, 189, 255, 0.04) !important; } } } @@ -91,9 +101,10 @@ } .cm-line { - line-height: 24px !important; - background: var(--bg-ink-300) !important; + line-height: 34px !important; font-family: 'Space Mono', monospace !important; + background-color: #121317 !important; + ::-moz-selection { background: var(--bg-ink-100) !important; opacity: 0.5 !important; diff --git a/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx b/frontend/src/components/QueryBuilderV2/QuerySearch/QuerySearch.tsx similarity index 96% rename from frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx rename to frontend/src/components/QueryBuilderV2/QuerySearch/QuerySearch.tsx index 70d5b6466811..366fbb71aaed 100644 --- a/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx +++ b/frontend/src/components/QueryBuilderV2/QuerySearch/QuerySearch.tsx @@ -5,7 +5,7 @@ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable no-nested-ternary */ -import './CodeMirrorWhereClause.styles.scss'; +import './QuerySearch.styles.scss'; import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons'; import { @@ -162,7 +162,7 @@ const disallowMultipleSpaces: Extension = EditorView.inputHandler.of( }, ); -function CodeMirrorWhereClause(): JSX.Element { +function QuerySearch(): JSX.Element { const [query, setQuery] = useState(''); const [valueSuggestions, setValueSuggestions] = useState([ { label: 'error', type: 'value' }, @@ -181,6 +181,8 @@ function CodeMirrorWhereClause(): JSX.Element { QueryKeySuggestionsProps[] | null >(null); + const [showExamples] = useState(false); + const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 }); const lastPosRef = useRef<{ line: number; ch: number }>({ line: 0, ch: 0 }); @@ -1088,44 +1090,46 @@ function CodeMirrorWhereClause(): JSX.Element { )} - - - -
- {queryExamples.map((example) => ( -
handleExampleClick(example.query)} - role="button" - tabIndex={0} - onKeyDown={(e): void => { - if (e.key === 'Enter' || e.key === ' ') { - handleExampleClick(example.query); - } - }} - > - -
- ))} -
-
-
-
+ {showExamples && ( + + + +
+ {queryExamples.map((example) => ( +
handleExampleClick(example.query)} + role="button" + tabIndex={0} + onKeyDown={(e): void => { + if (e.key === 'Enter' || e.key === ' ') { + handleExampleClick(example.query); + } + }} + > + +
+ ))} +
+
+
+
+ )} {/* {queryContext && ( @@ -1172,4 +1176,4 @@ function CodeMirrorWhereClause(): JSX.Element { ); } -export default CodeMirrorWhereClause; +export default QuerySearch; diff --git a/frontend/src/components/QueryBuilderV2/WhereClause/WhereClause.styles.scss b/frontend/src/components/QueryBuilderV2/WhereClause/WhereClause.styles.scss deleted file mode 100644 index c3e58d70861e..000000000000 --- a/frontend/src/components/QueryBuilderV2/WhereClause/WhereClause.styles.scss +++ /dev/null @@ -1,88 +0,0 @@ -.where-clause { - width: 100%; - border: 1px solid #d9d9d9; - border-radius: 2px; - background-color: #fff; - - &-header { - padding: 8px 12px; - border-bottom: 1px solid #f0f0f0; - } - - &-content { - padding: 12px; - - .query-input { - width: 100%; - margin-bottom: 8px; - font-family: monospace; - - &.error { - border-color: #ff4d4f; - } - - &.valid { - border-color: #52c41a; - } - } - - .error-alert, - .success-alert { - margin-bottom: 8px; - } - - .query-examples { - margin-top: 8px; - - ul { - margin: 8px 0; - padding-left: 0; - list-style-type: none; - - li { - margin: 4px 0; - } - } - } - } -} - -.condition-builder { - display: flex; - align-items: center; - margin-bottom: 16px; -} - -.conditions-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.condition-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px; - background: #f5f5f5; - color: #000; - border-radius: 4px; -} - -.where-clause-content { - .query-examples { - ul { - li { - margin: 4px 0; - padding-left: 0; - list-style-type: none; - - color: #000; - - code { - color: #000; - } - } - } - } -} diff --git a/frontend/src/components/QueryBuilderV2/WhereClause/WhereClause.tsx b/frontend/src/components/QueryBuilderV2/WhereClause/WhereClause.tsx deleted file mode 100644 index f778e970b52a..000000000000 --- a/frontend/src/components/QueryBuilderV2/WhereClause/WhereClause.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* eslint-disable no-nested-ternary */ -import './WhereClause.styles.scss'; - -import { Input, Typography } from 'antd'; -import { useCallback, useEffect, useState } from 'react'; -import { IQueryContext, IValidationResult } from 'types/antlrQueryTypes'; -import { getQueryContextAtCursor, validateQuery } from 'utils/antlrQueryUtils'; - -const { Text } = Typography; - -function QueryWhereClause(): JSX.Element { - const [query, setQuery] = useState(''); - const [cursorPosition, setCursorPosition] = useState(0); - const [isLoading, setIsLoading] = useState(false); - const [queryContext, setQueryContext] = useState(null); - const [validation, setValidation] = useState({ - isValid: false, - message: '', - errors: [], - }); - - console.log({ - cursorPosition, - queryContext, - validation, - isLoading, - }); - - const handleQueryChange = useCallback(async (newQuery: string) => { - setIsLoading(true); - setQuery(newQuery); - - try { - const validationResponse = validateQuery(newQuery); - setValidation(validationResponse); - } catch (error) { - setValidation({ - isValid: false, - message: 'Failed to process query', - errors: [ - { - message: error instanceof Error ? error.message : 'Unknown error', - line: 0, - column: 0, - }, - ], - }); - } finally { - setIsLoading(false); - } - }, []); - - useEffect(() => { - if (query) { - const context = getQueryContextAtCursor(query, cursorPosition); - setQueryContext(context as IQueryContext); - } - }, [query, cursorPosition]); - - const handleChange = (e: React.ChangeEvent): void => { - const { value, selectionStart } = e.target; - setQuery(value); - setCursorPosition(selectionStart || 0); - handleQueryChange(value); - }; - - const handleCursorMove = (e: React.SyntheticEvent): void => { - const { selectionStart } = e.currentTarget; - setCursorPosition(selectionStart || 0); - }; - - return ( -
-
- Where -
-
- - - {queryContext && ( -
- Current Context -
-

- Token: {queryContext.currentToken} -

-

- Type: {queryContext.tokenType} -

-

- Context:{' '} - {queryContext.isInValue - ? 'Value' - : queryContext.isInKey - ? 'Key' - : queryContext.isInOperator - ? 'Operator' - : queryContext.isInFunction - ? 'Function' - : 'Unknown'} -

-
-
- )} - -
- Examples: -
    -
  • - status = 'error' -
  • -
  • - - service = 'frontend' AND level = 'error' - -
  • -
  • - message LIKE '%timeout%' -
  • -
  • - duration {'>'} 1000 -
  • -
  • - tags IN ['prod', 'frontend'] -
  • -
  • - - NOT (status = 'error' OR level = 'error') - -
  • -
-
-
-
- ); -} - -export default QueryWhereClause; diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index 8d5a972cc169..35976d8efbdb 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -34,7 +34,7 @@ import { SELECTED_VIEWS } from './utils'; function LogsExplorer(): JSX.Element { const [showFrequencyChart, setShowFrequencyChart] = useState(true); const [selectedView, setSelectedView] = useState( - SELECTED_VIEWS.SEARCH, + SELECTED_VIEWS.QUERY_BUILDER_V2, ); const { preferences, loading: preferencesLoading } = usePreferenceContext(); diff --git a/frontend/src/periscope.scss b/frontend/src/periscope.scss index a5f0f598f9d9..306313e35bf8 100644 --- a/frontend/src/periscope.scss +++ b/frontend/src/periscope.scss @@ -75,6 +75,69 @@ } } +.periscope-input-with-label { + display: flex; + flex-direction: row; + border-radius: 2px 0px 0px 2px; + + .label { + min-width: 114px; + font-size: 12px; + + color: var(--bg-vanilla-400); + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; + letter-spacing: 0.56px; + text-transform: uppercase; + max-width: 150px; + min-width: 120px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0px 8px; + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + display: flex; + justify-content: flex-start; + align-items: center; + font-weight: var(--font-weight-light); + } + + .input { + flex: 1; + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + border-right: none; + border-left: none; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + + background: var(--bg-ink-300); + + .ant-select { + border: none; + height: 36px; + } + + .ant-select-selector { + border: none; + } + } + + .close-btn { + border-radius: 0px 2px 2px 0px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-300); + height: 38px; + width: 38px; + } +} + .lightMode { .periscope-btn { border-color: var(--bg-vanilla-300); diff --git a/frontend/src/utils/queryContextUtils.ts b/frontend/src/utils/queryContextUtils.ts index 95f491a7c5f8..1d8db01e9de6 100644 --- a/frontend/src/utils/queryContextUtils.ts +++ b/frontend/src/utils/queryContextUtils.ts @@ -309,6 +309,23 @@ function determineContextBoundaries( }; break; } + + // Check if cursor is right after the closing bracket (with a space) + // We need to handle this case to transition to conjunction context + if ( + matchingOpen && + token.stop + 1 < cursorIndex && + cursorIndex <= token.stop + 2 && + query[token.stop + 1] === ' ' + ) { + // We'll set a special flag to indicate we're after a closing bracket + bracketContext = { + start: matchingOpen.token.start, + end: token.stop, + isForList: matchingOpen.isForList, + }; + break; + } } // If we're at the cursor position and not in a closing bracket check @@ -725,6 +742,14 @@ export function getQueryContextAtCursor( adjustedCursorIndex >= contextBoundaries.bracketContext.start && adjustedCursorIndex <= contextBoundaries.bracketContext.end + 1; + // Check if we're right after a closing bracket for a list (IN operator) + // This helps transition to conjunction context after a multi-value list + const isAfterClosingBracketList = + contextBoundaries.bracketContext && + contextBoundaries.bracketContext.isForList && + adjustedCursorIndex === contextBoundaries.bracketContext.end + 2 && + query[contextBoundaries.bracketContext.end + 1] === ' '; + // If cursor is within a specific context boundary, this takes precedence if ( isInKeyBoundary || @@ -732,7 +757,8 @@ export function getQueryContextAtCursor( isInValueBoundary || isInConjunctionBoundary || isInBracketListBoundary || - isInParenthesisBoundary + isInParenthesisBoundary || + isAfterClosingBracketList ) { // Extract information from the current pair (if available) const keyToken = currentPair?.key || ''; @@ -747,6 +773,10 @@ export function getQueryContextAtCursor( const finalIsInValue = isInValueBoundary || (isInBracketListBoundary && isForMultiValueOperator); + // If we're right after a closing bracket for a list, transition to conjunction context + const finalIsInConjunction = + isInConjunctionBoundary || isAfterClosingBracketList; + return { tokenType: -1, text: '', @@ -756,7 +786,7 @@ export function getQueryContextAtCursor( isInKey: isInKeyBoundary || false, isInOperator: isInOperatorBoundary || false, isInValue: finalIsInValue || false, - isInConjunction: isInConjunctionBoundary || false, + isInConjunction: finalIsInConjunction || false, isInFunction: false, isInParenthesis: isInParenthesisBoundary || false, isInBracketList: isInBracketListBoundary || false,