feat: use new qb in logs explorer

This commit is contained in:
Yunus M 2025-04-28 18:40:53 +05:30 committed by SagarRajput-7
parent 0ffa666903
commit 2520718afb
7 changed files with 370 additions and 162 deletions

View File

@ -4,7 +4,7 @@
display: flex;
flex-direction: column;
padding: 16px;
gap: 16px;
gap: 8px;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', sans-serif;
@ -42,10 +42,12 @@
border-radius: 4px;
border: 1px solid var(--bg-slate-200, #1d212d);
font-family: 'Space Mono', monospace !important;
ul {
width: 100% !important;
max-width: 100% !important;
font-family: 'Space Mono', monospace !important;
&::-webkit-scrollbar {
width: 0.3rem;
@ -71,6 +73,8 @@
align-items: center !important;
gap: 8px !important;
font-family: 'Space Mono', monospace !important;
.cm-completionIcon {
display: none !important;
}
@ -87,9 +91,9 @@
}
.cm-line {
line-height: 1.8 !important;
line-height: 24px !important;
background: var(--bg-ink-300) !important;
font-family: 'Space Mono', monospace !important;
::-moz-selection {
background: var(--bg-ink-100) !important;
opacity: 0.5 !important;
@ -124,6 +128,7 @@
flex-direction: column;
gap: 8px;
margin-bottom: 8px;
margin-top: 16px;
.valid,
.invalid {
@ -196,22 +201,109 @@
}
}
.query-examples {
padding: 12px;
background-color: var(--bg-vanilla-100);
border: 1px solid var(--bg-vanilla-300);
border-radius: 4px;
.code-mirror-card {
.ant-card-body {
padding: 8px;
}
}
ul {
margin-top: 8px;
margin-bottom: 0;
padding-left: 16px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 8px;
.query-text-preview-title {
font-size: 13px;
color: var(--bg-vanilla-100);
background-color: var(--bg-robin-500);
padding: 2px 6px;
border-radius: 2px;
margin-right: 4px;
}
li {
margin-bottom: 4px;
.query-text-preview {
font-family: 'Space Mono', monospace;
font-size: 13px;
color: var(--bg-vanilla-200);
padding: 2px 6px;
font-style: italic;
}
.query-examples-card {
background-color: var(--bg-ink-400);
border: 1px solid var(--bg-slate-200);
.ant-card-body {
padding: 0;
}
.query-examples {
.ant-collapse-header {
padding: 8px 16px !important;
color: var(--bg-vanilla-300) !important;
font-weight: 500;
}
.ant-collapse-content {
background-color: transparent !important;
}
.query-examples-list {
display: flex;
flex-direction: row;
gap: 8px;
flex-wrap: wrap;
}
.query-example-tag {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 12px;
background-color: var(--bg-ink-400);
border: 1px solid var(--bg-slate-200);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
&:hover {
background-color: var(--bg-ink-300);
border-color: var(--bg-robin-500);
}
&:focus-visible {
outline: 2px solid var(--bg-robin-500);
outline-offset: 2px;
}
.query-example-content {
display: flex;
align-items: center;
gap: 8px;
}
.query-example-label {
font-weight: 500;
color: var(--bg-vanilla-300);
font-size: 13px;
}
.query-example-query {
font-family: 'Space Mono', monospace;
font-size: 12px;
color: var(--bg-vanilla-200);
background-color: var(--bg-ink-300);
padding: 2px 6px;
border-radius: 2px;
}
.query-example-description {
font-size: 12px;
color: var(--bg-vanilla-200);
opacity: 0.8;
}
}
.query-example-content {
display: inline-flex;
cursor: pointer;
}
}
}
@ -247,10 +339,36 @@
}
}
.query-examples {
.query-examples-card {
background-color: var(--bg-ink-400);
border-color: var(--bg-slate-500);
color: var(--bg-vanilla-100);
.ant-collapse-header {
color: var(--bg-vanilla-100) !important;
}
.query-example-tag {
background-color: var(--bg-ink-400);
border-color: var(--bg-slate-500);
&:hover {
background-color: var(--bg-ink-300);
border-color: var(--bg-robin-500);
}
.query-example-label {
color: var(--bg-vanilla-100);
}
.query-example-query {
color: var(--bg-vanilla-100);
background-color: var(--bg-ink-300);
}
.query-example-description {
color: var(--bg-vanilla-100);
}
}
}
}
}

View File

@ -14,8 +14,8 @@ import {
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { copilot } from '@uiw/codemirror-theme-copilot';
import CodeMirror, { EditorView, Extension } from '@uiw/react-codemirror';
import { Badge, Card, Divider, Space, Typography } from 'antd';
import CodeMirror, { EditorView } from '@uiw/react-codemirror';
import { Card, Collapse, Space, Typography } from 'antd';
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import { useCallback, useEffect, useRef, useState } from 'react';
@ -32,58 +32,100 @@ import {
} from 'utils/antlrQueryUtils';
const { Text } = Typography;
const { Panel } = Collapse;
function collapseSpacesOutsideStrings(): Extension {
return EditorView.inputHandler.of((view, from, to, text) => {
// Get the current line text
const { state } = view;
const line = state.doc.lineAt(from);
// Find the position within the line
const before = line.text.slice(0, from - line.from);
const after = line.text.slice(to - line.from);
const fullText = before + text + after;
let insideString = false;
let escaped = false;
let processed = '';
for (let i = 0; i < fullText.length; i++) {
const char = fullText[i];
if (char === '"' && !escaped) {
insideString = !insideString;
}
if (char === '\\' && !escaped) {
escaped = true;
} else {
escaped = false;
}
if (!insideString && char === ' ' && processed.endsWith(' ')) {
// Collapse multiple spaces outside strings
// Skip this space
} else {
processed += char;
}
}
// Only dispatch if the processed text differs
if (processed !== fullText) {
view.dispatch({
changes: {
from: line.from,
to: line.to,
insert: processed,
},
});
return true;
}
const queryExamples = [
{
label: 'Basic Query',
query: "status = 'error'",
description: 'Find all errors',
},
{
label: 'Multiple Conditions',
query: "status = 'error' AND service = 'frontend'",
description: 'Find errors from frontend service',
},
{
label: 'IN Operator',
query: "status IN ['error', 'warning']",
description: 'Find items with specific statuses',
},
{
label: 'Function Usage',
query: "HAS(service, 'frontend')",
description: 'Use HAS function',
},
{
label: 'Numeric Comparison',
query: 'duration > 1000',
description: 'Find items with duration greater than 1000ms',
},
{
label: 'Range Query',
query: 'duration BETWEEN 100 AND 1000',
description: 'Find items with duration between 100ms and 1000ms',
},
{
label: 'Pattern Matching',
query: "service LIKE 'front%'",
description: 'Find services starting with "front"',
},
{
label: 'Complex Conditions',
query: "(status = 'error' OR status = 'warning') AND service = 'frontend'",
description: 'Find errors or warnings from frontend service',
},
{
label: 'Multiple Functions',
query: "HAS(service, 'frontend') AND HAS(status, 'error')",
description: 'Use multiple HAS functions',
},
{
label: 'NOT Operator',
query: "NOT status = 'success'",
description: 'Find items that are not successful',
},
{
label: 'Array Contains',
query: "tags CONTAINS 'production'",
description: 'Find items with production tag',
},
{
label: 'Regex Pattern',
query: "service REGEXP '^prod-.*'",
description: 'Find services matching regex pattern',
},
{
label: 'Null Check',
query: 'error IS NULL',
description: 'Find items without errors',
},
{
label: 'Multiple Attributes',
query:
"service = 'frontend' AND environment = 'production' AND status = 'error'",
description: 'Find production frontend errors',
},
{
label: 'Nested Conditions',
query:
"(service = 'frontend' OR service = 'backend') AND (status = 'error' OR status = 'warning')",
description: 'Find errors or warnings from frontend or backend',
},
];
// Custom extension to stop events
const stopEventsExtension = EditorView.domEventHandlers({
keydown: (event) => {
event.stopPropagation();
// Optionally: event.preventDefault();
return false; // Important for CM to know you handled it
},
input: (event) => {
event.stopPropagation();
return false;
});
}
},
});
function CodeMirrorWhereClause(): JSX.Element {
const [query, setQuery] = useState<string>('');
@ -328,35 +370,42 @@ function CodeMirrorWhereClause(): JSX.Element {
handleQueryChange(value);
};
const renderContextBadge = (): JSX.Element | null => {
if (!queryContext) return null;
let color = 'black';
let text = 'Unknown';
if (queryContext.isInKey) {
color = 'blue';
text = 'Key';
} else if (queryContext.isInOperator) {
color = 'purple';
text = 'Operator';
} else if (queryContext.isInValue) {
color = 'green';
text = 'Value';
} else if (queryContext.isInFunction) {
color = 'orange';
text = 'Function';
} else if (queryContext.isInConjunction) {
color = 'magenta';
text = 'Conjunction';
} else if (queryContext.isInParenthesis) {
color = 'grey';
text = 'Parenthesis';
}
return <Badge color={color} text={text} />;
const handleExampleClick = (exampleQuery: string): void => {
// If there's an existing query, append the example with AND
const newQuery = query ? `${query} AND ${exampleQuery}` : exampleQuery;
setQuery(newQuery);
handleQueryChange(newQuery);
};
// const renderContextBadge = (): JSX.Element | null => {
// if (!queryContext) return null;
// let color = 'black';
// let text = 'Unknown';
// if (queryContext.isInKey) {
// color = 'blue';
// text = 'Key';
// } else if (queryContext.isInOperator) {
// color = 'purple';
// text = 'Operator';
// } else if (queryContext.isInValue) {
// color = 'green';
// text = 'Value';
// } else if (queryContext.isInFunction) {
// color = 'orange';
// text = 'Function';
// } else if (queryContext.isInConjunction) {
// color = 'magenta';
// text = 'Conjunction';
// } else if (queryContext.isInParenthesis) {
// color = 'grey';
// text = 'Parenthesis';
// }
// return <Badge color={color} text={text} />;
// };
function myCompletions(context: CompletionContext): CompletionResult | null {
const word = context.matchBefore(/\w*/);
if (word?.from === word?.to && !context.explicit) return null;
@ -552,7 +601,7 @@ function CodeMirrorWhereClause(): JSX.Element {
return (
<div className="code-mirror-where-clause">
<Card size="small">
<Card className="code-mirror-card">
<CodeMirror
value={query}
theme={copilot}
@ -568,62 +617,95 @@ function CodeMirrorWhereClause(): JSX.Element {
activateOnTyping: true,
maxRenderedOptions: 50,
}),
collapseSpacesOutsideStrings(),
javascript({ jsx: false, typescript: false }),
EditorView.lineWrapping,
stopEventsExtension,
// customTheme,
]}
basicSetup={{
lineNumbers: false,
}}
/>
{query && (
<>
<Divider style={{ margin: '8px 0' }} />
<Space direction="vertical" size={4}>
<Text>Query:</Text>
<Text code>{query}</Text>
</Space>
</>
)}
{query && (
<>
<Divider style={{ margin: '8px 0' }} />
<div className="query-validation">
<div className="query-validation-status">
<Text>Status:</Text>
<div className={validation.isValid ? 'valid' : 'invalid'}>
{validation.isValid ? (
<Space>
<CheckCircleFilled /> Valid
</Space>
) : (
<Space>
<CloseCircleFilled /> Invalid
</Space>
)}
</div>
</div>
<div className="query-validation-errors">
{validation.errors.map((error) => (
<div key={error.message} className="query-validation-error">
<div className="query-validation-error-line">
{error.line}:{error.column}
</div>
<div className="query-validation-error-message">{error.message}</div>
</div>
))}
</div>
</div>
</>
)}
</Card>
{queryContext && (
{query && (
<Card size="small">
<Space direction="vertical" size={4}>
<Text className="query-text-preview-title">searchExpr</Text>
<Text className="query-text-preview">{query}</Text>
</Space>
<div className="query-validation">
<div className="query-validation-status">
<Text>Status:</Text>
<div className={validation.isValid ? 'valid' : 'invalid'}>
{validation.isValid ? (
<Space>
<CheckCircleFilled /> Valid
</Space>
) : (
<Space>
<CloseCircleFilled /> Invalid
</Space>
)}
</div>
</div>
<div className="query-validation-errors">
{validation.errors.map((error) => (
<div key={error.message} className="query-validation-error">
<div className="query-validation-error-line">
{error.line}:{error.column}
</div>
<div className="query-validation-error-message">{error.message}</div>
</div>
))}
</div>
</div>
</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>
{/* {queryContext && (
<Card size="small" title="Current Context" className="query-context">
<div className="context-details">
<Space direction="vertical" size={4}>
@ -640,7 +722,6 @@ function CodeMirrorWhereClause(): JSX.Element {
{renderContextBadge()}
</Space>
{/* Display the key-operator-value triplet when available */}
{queryContext.keyToken && (
<Space>
<Text strong>Key:</Text>
@ -664,7 +745,7 @@ function CodeMirrorWhereClause(): JSX.Element {
</Space>
</div>
</Card>
)}
)} */}
</div>
);
}

View File

@ -1,11 +0,0 @@
import QueryBuilderV2 from 'components/QueryBuilderV2/QueryBuilderV2';
function Home2(): JSX.Element {
return (
<div>
<QueryBuilderV2 />
</div>
);
}
export default Home2;

View File

@ -1,5 +1,6 @@
import './LogsExplorerQuerySection.styles.scss';
import QueryBuilderV2 from 'components/QueryBuilderV2/QueryBuilderV2';
import {
initialQueriesMap,
OPERATORS,
@ -107,6 +108,8 @@ function LogExplorerQuerySection({
version="v3" // setting this to v3 as we this is rendered in logs explorer
/>
)}
{selectedView === SELECTED_VIEWS.QUERY_BUILDER_V2 && <QueryBuilderV2 />}
</>
);
}

View File

@ -3,7 +3,7 @@ import './ToolbarActions.styles.scss';
import { FilterOutlined } from '@ant-design/icons';
import { Button, Switch, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { Atom, SquareMousePointer, Terminal } from 'lucide-react';
import { Atom, Binoculars, SquareMousePointer, Terminal } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
interface LeftToolbarActionsProps {
@ -19,7 +19,7 @@ interface LeftToolbarActionsProps {
const activeTab = 'active-tab';
const actionBtn = 'action-btn';
export const queryBuilder = 'query-builder';
export const queryBuilderV2 = 'query-builder-v2';
export default function LeftToolbarActions({
items,
selectedView,
@ -81,6 +81,22 @@ export default function LeftToolbarActions({
<Terminal size={14} data-testid="clickhouse-view" />
</Button>
)}
<Tooltip title="Query Builder V2">
<Button
disabled={QB.disabled}
className={cx(
queryBuilderV2,
actionBtn,
selectedView === queryBuilderV2 ? activeTab : '',
)}
onClick={(): void =>
onChangeSelectedView(SELECTED_VIEWS.QUERY_BUILDER_V2)
}
>
<Binoculars size={14} data-testid="query-builder-view-v2" />
</Button>
</Tooltip>
</div>
<div className="frequency-chart-view-controller">

View File

@ -1,7 +1,7 @@
import Home2 from 'container/Home/Home2';
import Home from 'container/Home/Home';
function HomePage(): JSX.Element {
return <Home2 />;
return <Home />;
}
export default HomePage;

View File

@ -21,6 +21,7 @@ export enum SELECTED_VIEWS {
SEARCH = 'search',
QUERY_BUILDER = 'query-builder',
CLICKHOUSE = 'clickhouse',
QUERY_BUILDER_V2 = 'query-builder-v2',
}
export const LogsQuickFiltersConfig: IQuickFiltersConfig[] = [