feat: add groupBy, having, order by, limit and legend format

This commit is contained in:
Yunus M 2025-05-11 17:47:30 +05:30 committed by ahrefabhi
parent 104b14a478
commit 800968a5d0
15 changed files with 710 additions and 289 deletions

View File

@ -169,6 +169,7 @@
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
}
.ant-drawer-close {
padding: 0px;
}

View File

@ -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);
}
}
}

View File

@ -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<string>(
initialValue ? initialValue.toString() : '',
);
return (
<div
className={cx('input-with-label', {
labelAfter,
})}
>
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
<Input
className="input"
placeholder={placeholder}
type={type}
value={inputValue}
onChange={(e): void => setInputValue(e.target.value)}
/>
{labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
{onClose && (
<Button
className="periscope-btn ghost close-btn"
icon={<X size={16} />}
onClick={onClose}
/>
)}
</div>
);
}
InputWithLabel.defaultProps = {
type: 'text',
onClose: undefined,
labelAfter: false,
initialValue: undefined,
};
export default InputWithLabel;

View File

@ -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<string, AddOn> = {
GROUP_BY: {
icon: <BarChart2 size={14} />,
label: 'Group By',
key: 'group_by',
},
HAVING: {
icon: <ScrollText size={14} />,
label: 'Having',
key: 'having',
},
ORDER_BY: {
icon: <ScrollText size={14} />,
label: 'Order By',
key: 'order_by',
},
LIMIT: {
icon: <ScrollText size={14} />,
label: 'Limit',
key: 'limit',
},
LEGEND_FORMAT: {
icon: <ScrollText size={14} />,
label: 'Legend format',
key: 'legend_format',
},
};
function QueryAddOns({
query,
version,
isListViewPanel,
}: {
query: IBuilderQuery;
version: string;
isListViewPanel: boolean;
}): JSX.Element {
const [selectedViews, setSelectedViews] = useState<AddOn[]>([]);
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 (
<div className="query-add-ons">
{selectedViews.length > 0 && (
<div className="selected-add-ons-content">
{selectedViews.find((view) => view.key === 'group_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Group By</div>
<div className="input">
<GroupByFilter
disabled={
query.dataSource === DataSource.METRICS &&
!query.aggregateAttribute.key
}
query={query}
onChange={handleChangeGroupByKeys}
/>
</div>
<Button
className="close-btn periscope-btn ghost"
icon={<X size={16} />}
onClick={(): void => handleRemoveView('group_by')}
/>
</div>
</div>
)}
{selectedViews.find((view) => view.key === 'having') && (
<div className="add-on-content">
<InputWithLabel
label="Having"
placeholder="Select a field"
onClose={(): void => {
setSelectedViews(
selectedViews.filter((view) => view.key !== 'having'),
);
}}
/>
</div>
)}
{selectedViews.find((view) => view.key === 'limit') && (
<div className="add-on-content">
<InputWithLabel
label="Limit"
placeholder="Select a field"
type="number"
onClose={(): void => {
setSelectedViews(selectedViews.filter((view) => view.key !== 'limit'));
}}
/>
</div>
)}
{selectedViews.find((view) => view.key === 'order_by') && (
<div className="add-on-content">
<div className="periscope-input-with-label">
<div className="label">Order By</div>
<div className="input">
<OrderByFilter
entityVersion={version}
query={query}
onChange={handleChangeOrderByKeys}
isListViewPanel={isListViewPanel}
/>
</div>
<Button
className="close-btn periscope-btn ghost"
icon={<X size={16} />}
onClick={(): void => handleRemoveView('order_by')}
/>
</div>
</div>
)}
{selectedViews.find((view) => view.key === 'legend_format') && (
<div className="add-on-content">
<InputWithLabel
label="Legend format"
placeholder="Write legend format"
onClose={(): void => {
setSelectedViews(
selectedViews.filter((view) => view.key !== 'legend_format'),
);
}}
/>
</div>
)}
</div>
)}
<div className="add-ons-list">
<Radio.Group
className="add-ons-tabs"
onChange={handleOptionClick}
value={selectedViews}
>
{Object.values(ADD_ONS).map((addOn) => (
<Radio.Button
key={addOn.label}
className={
selectedViews.find((view) => view.key === addOn.key)
? 'selected_view tab'
: 'tab'
}
value={addOn}
>
<div className="add-on-tab-title">
{addOn.icon}
{addOn.label}
</div>
</Radio.Button>
))}
</Radio.Group>
</div>
</div>
);
}
export default QueryAddOns;
/*
<Radio.Button
className={
// eslint-disable-next-line sonarjs/no-duplicate-string
selectedView === ADD_ONS.GROUP_BY ? 'selected_view tab' : 'tab'
}
value={ADD_ONS.GROUP_BY}
>
<div className="add-on-tab-title">
<BarChart2 size={14} />
{selectedView.label}
</div>
</Radio.Button>
<Radio.Button
className={selectedView === ADD_ONS.HAVING ? 'selected_view tab' : 'tab'}
value={ADD_ONS.HAVING}
>
<div className="add-on-tab-title">
<ScrollText size={14} />
{selectedView.label}
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === ADD_ONS.ORDER_BY ? 'selected_view tab' : 'tab'
}
value={ADD_ONS.ORDER_BY}
>
<div className="add-on-tab-title">
<DraftingCompass size={14} />
{selectedView.label}
</div>
</Radio.Button>
<Radio.Button
className={selectedView === ADD_ONS.LIMIT ? 'selected_view tab' : 'tab'}
value={ADD_ONS.LIMIT}
>
<div className="add-on-tab-title">
<Package2 size={14} />
{selectedView.label}
</div>
</Radio.Button>
<Radio.Button
className={
selectedView === ADD_ONS.LEGEND_FORMAT ? 'selected_view tab' : 'tab'
}
value={ADD_ONS.LEGEND_FORMAT}
>
<div className="add-on-tab-title">
<ChevronsLeftRight size={14} />
{selectedView.label}
</div>
</Radio.Button>
*/

View File

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

View File

@ -0,0 +1,31 @@
import './QueryAggregationOptions.styles.scss';
import { Input } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
function QueryAggregationOptions(): JSX.Element {
return (
<div className="query-aggregation-container">
<Input
placeholder="Search aggregation options..."
className="query-aggregation-options-input"
/>
<div className="query-aggregation-interval">
<div className="query-aggregation-interval-label">every</div>
<div className="query-aggregation-interval-input-container">
<InputWithLabel
initialValue="60"
label="Seconds"
placeholder="60"
type="number"
labelAfter
onClose={(): void => {}}
/>
</div>
</div>
</div>
);
}
export default QueryAggregationOptions;

View File

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

View File

@ -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 (
<div className="query-builder-v2">
{/* <QueryWhereClause /> */}
<CodeMirrorWhereClause />
<QuerySearch />
<QueryAggregationOptions />
<QueryAddOns
query={currentQuery.builder.queryData[0]}
version="v3"
isListViewPanel={false}
/>
</div>
);
}

View File

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

View File

@ -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<string>('');
const [valueSuggestions, setValueSuggestions] = useState<any[]>([
{ 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 {
</Card>
)}
<Card size="small" className="query-examples-card">
<Collapse
ghost
size="small"
className="query-examples"
defaultActiveKey={[]}
>
<Panel header="Query Examples" key="1">
<div className="query-examples-list">
{queryExamples.map((example) => (
<div
className="query-example-content"
key={example.label}
onClick={(): void => handleExampleClick(example.query)}
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleExampleClick(example.query);
}
}}
>
<CodeMirror
value={example.query}
theme={copilot}
extensions={[
javascript({ jsx: false, typescript: false }),
EditorView.editable.of(false),
]}
basicSetup={{ lineNumbers: false }}
className="query-example-code-mirror"
/>
</div>
))}
</div>
</Panel>
</Collapse>
</Card>
{showExamples && (
<Card size="small" className="query-examples-card">
<Collapse
ghost
size="small"
className="query-examples"
defaultActiveKey={[]}
>
<Panel header="Query Examples" key="1">
<div className="query-examples-list">
{queryExamples.map((example) => (
<div
className="query-example-content"
key={example.label}
onClick={(): void => handleExampleClick(example.query)}
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleExampleClick(example.query);
}
}}
>
<CodeMirror
value={example.query}
theme={copilot}
extensions={[
javascript({ jsx: false, typescript: false }),
EditorView.editable.of(false),
]}
basicSetup={{ lineNumbers: false }}
className="query-example-code-mirror"
/>
</div>
))}
</div>
</Panel>
</Collapse>
</Card>
)}
{/* {queryContext && (
<Card size="small" title="Current Context" className="query-context">
@ -1172,4 +1176,4 @@ function CodeMirrorWhereClause(): JSX.Element {
);
}
export default CodeMirrorWhereClause;
export default QuerySearch;

View File

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

View File

@ -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<string>('');
const [cursorPosition, setCursorPosition] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [queryContext, setQueryContext] = useState<IQueryContext | null>(null);
const [validation, setValidation] = useState<IValidationResult>({
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<HTMLInputElement>): void => {
const { value, selectionStart } = e.target;
setQuery(value);
setCursorPosition(selectionStart || 0);
handleQueryChange(value);
};
const handleCursorMove = (e: React.SyntheticEvent<HTMLInputElement>): void => {
const { selectionStart } = e.currentTarget;
setCursorPosition(selectionStart || 0);
};
return (
<div className="where-clause">
<div className="where-clause-header">
<Text strong>Where</Text>
</div>
<div className="where-clause-content">
<Input
value={query}
onChange={handleChange}
onSelect={handleCursorMove}
onKeyUp={handleCursorMove}
placeholder="Enter your query (e.g., status = 'error' AND service = 'frontend')"
/>
{queryContext && (
<div className="query-context">
<Text strong>Current Context</Text>
<div className="context-details">
<p>
<strong>Token:</strong> {queryContext.currentToken}
</p>
<p>
<strong>Type:</strong> {queryContext.tokenType}
</p>
<p>
<strong>Context:</strong>{' '}
{queryContext.isInValue
? 'Value'
: queryContext.isInKey
? 'Key'
: queryContext.isInOperator
? 'Operator'
: queryContext.isInFunction
? 'Function'
: 'Unknown'}
</p>
</div>
</div>
)}
<div className="query-examples">
<Text type="secondary">Examples:</Text>
<ul>
<li>
<Text code>status = &apos;error&apos;</Text>
</li>
<li>
<Text code>
service = &apos;frontend&apos; AND level = &apos;error&apos;
</Text>
</li>
<li>
<Text code>message LIKE &apos;%timeout%&apos;</Text>
</li>
<li>
<Text code>duration {'>'} 1000</Text>
</li>
<li>
<Text code>tags IN [&apos;prod&apos;, &apos;frontend&apos;]</Text>
</li>
<li>
<Text code>
NOT (status = &apos;error&apos; OR level = &apos;error&apos;)
</Text>
</li>
</ul>
</div>
</div>
</div>
);
}
export default QueryWhereClause;

View File

@ -34,7 +34,7 @@ import { SELECTED_VIEWS } from './utils';
function LogsExplorer(): JSX.Element {
const [showFrequencyChart, setShowFrequencyChart] = useState(true);
const [selectedView, setSelectedView] = useState<SELECTED_VIEWS>(
SELECTED_VIEWS.SEARCH,
SELECTED_VIEWS.QUERY_BUILDER_V2,
);
const { preferences, loading: preferencesLoading } = usePreferenceContext();

View File

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

View File

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