mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-24 19:07:47 +00:00
feat: add groupBy, having, order by, limit and legend format
This commit is contained in:
parent
104b14a478
commit
800968a5d0
@ -169,6 +169,7 @@
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-close {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
frontend/src/components/InputWithLabel/InputWithLabel.tsx
Normal file
60
frontend/src/components/InputWithLabel/InputWithLabel.tsx
Normal 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;
|
||||
@ -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>
|
||||
|
||||
*/
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 = 'error'</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text code>
|
||||
service = 'frontend' AND level = 'error'
|
||||
</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text code>message LIKE '%timeout%'</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text code>duration {'>'} 1000</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text code>tags IN ['prod', 'frontend']</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text code>
|
||||
NOT (status = 'error' OR level = 'error')
|
||||
</Text>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueryWhereClause;
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user