mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-19 08:26:06 +00:00
355 lines
9.2 KiB
TypeScript
355 lines
9.2 KiB
TypeScript
|
|
import './QueryAddOns.styles.scss';
|
||
|
|
|
||
|
|
import { Button, Radio, RadioChangeEvent } from 'antd';
|
||
|
|
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
|
||
|
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||
|
|
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
|
||
|
|
import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter';
|
||
|
|
import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter';
|
||
|
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||
|
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||
|
|
import { isEmpty } from 'lodash-es';
|
||
|
|
import { BarChart2, ChevronUp, ScrollText } from 'lucide-react';
|
||
|
|
import { useCallback, useEffect, useState } from 'react';
|
||
|
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||
|
|
import { MetricAggregation } from 'types/api/v5/queryRange';
|
||
|
|
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||
|
|
|
||
|
|
import HavingFilter from './HavingFilter/HavingFilter';
|
||
|
|
|
||
|
|
interface AddOn {
|
||
|
|
icon: React.ReactNode;
|
||
|
|
label: string;
|
||
|
|
key: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const ADD_ONS_KEYS = {
|
||
|
|
GROUP_BY: 'group_by',
|
||
|
|
HAVING: 'having',
|
||
|
|
ORDER_BY: 'order_by',
|
||
|
|
LIMIT: 'limit',
|
||
|
|
LEGEND_FORMAT: 'legend_format',
|
||
|
|
};
|
||
|
|
|
||
|
|
const ADD_ONS = [
|
||
|
|
{
|
||
|
|
icon: <BarChart2 size={14} />,
|
||
|
|
label: 'Group By',
|
||
|
|
key: 'group_by',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
icon: <ScrollText size={14} />,
|
||
|
|
label: 'Having',
|
||
|
|
key: 'having',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
icon: <ScrollText size={14} />,
|
||
|
|
label: 'Order By',
|
||
|
|
key: 'order_by',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
icon: <ScrollText size={14} />,
|
||
|
|
label: 'Limit',
|
||
|
|
key: 'limit',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
icon: <ScrollText size={14} />,
|
||
|
|
label: 'Legend format',
|
||
|
|
key: 'legend_format',
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const REDUCE_TO = {
|
||
|
|
icon: <ScrollText size={14} />,
|
||
|
|
label: 'Reduce to',
|
||
|
|
key: 'reduce_to',
|
||
|
|
};
|
||
|
|
|
||
|
|
function QueryAddOns({
|
||
|
|
query,
|
||
|
|
version,
|
||
|
|
isListViewPanel,
|
||
|
|
showReduceTo,
|
||
|
|
panelType,
|
||
|
|
index,
|
||
|
|
}: {
|
||
|
|
query: IBuilderQuery;
|
||
|
|
version: string;
|
||
|
|
isListViewPanel: boolean;
|
||
|
|
showReduceTo: boolean;
|
||
|
|
panelType: PANEL_TYPES | null;
|
||
|
|
index: number;
|
||
|
|
}): JSX.Element {
|
||
|
|
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
|
||
|
|
|
||
|
|
const [selectedViews, setSelectedViews] = useState<AddOn[]>([]);
|
||
|
|
|
||
|
|
const { handleChangeQueryData } = useQueryOperations({
|
||
|
|
index,
|
||
|
|
query,
|
||
|
|
entityVersion: '',
|
||
|
|
});
|
||
|
|
|
||
|
|
const { handleSetQueryData } = useQueryBuilder();
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (isListViewPanel) {
|
||
|
|
setAddOns([]);
|
||
|
|
|
||
|
|
setSelectedViews([
|
||
|
|
ADD_ONS.find((addOn) => addOn.key === ADD_ONS_KEYS.ORDER_BY) as AddOn,
|
||
|
|
]);
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let filteredAddOns: AddOn[];
|
||
|
|
if (panelType === PANEL_TYPES.VALUE) {
|
||
|
|
// Filter out all add-ons except legend format
|
||
|
|
filteredAddOns = ADD_ONS.filter(
|
||
|
|
(addOn) => addOn.key === ADD_ONS_KEYS.LEGEND_FORMAT,
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
filteredAddOns = Object.values(ADD_ONS);
|
||
|
|
|
||
|
|
// Filter out group_by for metrics data source
|
||
|
|
if (query.dataSource === DataSource.METRICS) {
|
||
|
|
filteredAddOns = filteredAddOns.filter(
|
||
|
|
(addOn) => addOn.key !== ADD_ONS_KEYS.GROUP_BY,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// add reduce to if showReduceTo is true
|
||
|
|
if (showReduceTo) {
|
||
|
|
filteredAddOns = [...filteredAddOns, REDUCE_TO];
|
||
|
|
}
|
||
|
|
|
||
|
|
setAddOns(filteredAddOns);
|
||
|
|
|
||
|
|
// Filter selectedViews to only include add-ons present in filteredAddOns
|
||
|
|
setSelectedViews((prevSelectedViews) =>
|
||
|
|
prevSelectedViews.filter((view) =>
|
||
|
|
filteredAddOns.some((addOn) => addOn.key === view.key),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
|
|
}, [panelType, isListViewPanel, query.dataSource]);
|
||
|
|
|
||
|
|
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 handleChangeReduceToV5 = useCallback(
|
||
|
|
(value: ReduceOperators) => {
|
||
|
|
handleSetQueryData(index, {
|
||
|
|
...query,
|
||
|
|
aggregations: [
|
||
|
|
{
|
||
|
|
...(query.aggregations?.[0] as MetricAggregation),
|
||
|
|
reduceTo: value,
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
},
|
||
|
|
[handleSetQueryData, index, query],
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleRemoveView = useCallback(
|
||
|
|
(key: string): void => {
|
||
|
|
setSelectedViews(selectedViews.filter((view) => view.key !== key));
|
||
|
|
},
|
||
|
|
[selectedViews],
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleChangeQueryLegend = useCallback(
|
||
|
|
(value: string) => {
|
||
|
|
handleChangeQueryData('legend', value);
|
||
|
|
},
|
||
|
|
[handleChangeQueryData],
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleChangeLimit = useCallback(
|
||
|
|
(value: string) => {
|
||
|
|
handleChangeQueryData('limit', Number(value) || null);
|
||
|
|
},
|
||
|
|
[handleChangeQueryData],
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleChangeHaving = useCallback(
|
||
|
|
(value: string) => {
|
||
|
|
handleChangeQueryData('having', {
|
||
|
|
expression: value,
|
||
|
|
});
|
||
|
|
},
|
||
|
|
[handleChangeQueryData],
|
||
|
|
);
|
||
|
|
|
||
|
|
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.aggregations?.[0] as MetricAggregation)?.metricName
|
||
|
|
}
|
||
|
|
query={query}
|
||
|
|
onChange={handleChangeGroupByKeys}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<Button
|
||
|
|
className="close-btn periscope-btn ghost"
|
||
|
|
icon={<ChevronUp size={16} />}
|
||
|
|
onClick={(): void => handleRemoveView('group_by')}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{selectedViews.find((view) => view.key === 'having') && (
|
||
|
|
<div className="add-on-content">
|
||
|
|
<div className="periscope-input-with-label">
|
||
|
|
<div className="label">Having</div>
|
||
|
|
<div className="input">
|
||
|
|
<HavingFilter
|
||
|
|
onClose={(): void => {
|
||
|
|
setSelectedViews(
|
||
|
|
selectedViews.filter((view) => view.key !== 'having'),
|
||
|
|
);
|
||
|
|
}}
|
||
|
|
onChange={handleChangeHaving}
|
||
|
|
queryData={query}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{selectedViews.find((view) => view.key === 'limit') && (
|
||
|
|
<div className="add-on-content">
|
||
|
|
<InputWithLabel
|
||
|
|
label="Limit"
|
||
|
|
onChange={handleChangeLimit}
|
||
|
|
initialValue={query?.limit ?? undefined}
|
||
|
|
placeholder="Enter limit"
|
||
|
|
onClose={(): void => {
|
||
|
|
setSelectedViews(selectedViews.filter((view) => view.key !== 'limit'));
|
||
|
|
}}
|
||
|
|
closeIcon={<ChevronUp size={16} />}
|
||
|
|
/>
|
||
|
|
</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}
|
||
|
|
isNewQueryV2
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
{!isListViewPanel && (
|
||
|
|
<Button
|
||
|
|
className="close-btn periscope-btn ghost"
|
||
|
|
icon={<ChevronUp size={16} />}
|
||
|
|
onClick={(): void => handleRemoveView('order_by')}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
|
||
|
|
<div className="add-on-content">
|
||
|
|
<div className="periscope-input-with-label">
|
||
|
|
<div className="label">Reduce to</div>
|
||
|
|
<div className="input">
|
||
|
|
<ReduceToFilter query={query} onChange={handleChangeReduceToV5} />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Button
|
||
|
|
className="close-btn periscope-btn ghost"
|
||
|
|
icon={<ChevronUp size={16} />}
|
||
|
|
onClick={(): void => handleRemoveView('reduce_to')}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{selectedViews.find((view) => view.key === 'legend_format') && (
|
||
|
|
<div className="add-on-content">
|
||
|
|
<InputWithLabel
|
||
|
|
label="Legend format"
|
||
|
|
placeholder="Write legend format"
|
||
|
|
onChange={handleChangeQueryLegend}
|
||
|
|
initialValue={isEmpty(query?.legend) ? undefined : query?.legend}
|
||
|
|
onClose={(): void => {
|
||
|
|
setSelectedViews(
|
||
|
|
selectedViews.filter((view) => view.key !== 'legend_format'),
|
||
|
|
);
|
||
|
|
}}
|
||
|
|
closeIcon={<ChevronUp size={16} />}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="add-ons-list">
|
||
|
|
<Radio.Group
|
||
|
|
className="add-ons-tabs"
|
||
|
|
onChange={handleOptionClick}
|
||
|
|
value={selectedViews}
|
||
|
|
>
|
||
|
|
{addOns.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;
|