mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-19 18:00:41 +01:00
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description
- `queryBuilderFields` on a panel definition had no effect.
`QueryBuilderV2` discarded the prop for list panels (the only kind that
declared anything), nothing downstream read `isHidden`/`isDisabled`, and
the `filters` / `whereClauseConfig` entry had no consumer anywhere in
the repo. The behaviour it appeared to configure came entirely from
`isListViewPanel`.
- Replaces it with a config in the builder's own vocabulary — a
per-field `hidden` / `disabled` / `pinned` rule over
`QueryBuilderField`, covering per-query controls plus `Formula` and
`AdditionalQueries`. A config can only narrow what the builder already
supports for the current data source and panel type, so definitions
never restate the builder's rules. `reason` is required on `disabled` so
an inert control always explains itself.
- `isListViewPanel` becomes `isRawQuery`: it was named for a dashboard
panel type but lives in a component three explorers use. It supplies the
defaults for `fieldsConfig` and the new `allowedDataSources`, which
callers override per field. On the dashboards side it is read from the
`requestType` a kind already declares, replacing a hardcoded
`signoz/ListPanel` check.
- Deletes the dead plumbing this uncovered:
`FilterConfigs`/`WhereClauseConfig`, the
`queryComponents`/`renderOrderBy` prop, Formula's
`isAdditionalFilterEnable` block and the four modules only it reached.
Net -900 lines. No behaviour change intended.
#### Additional Information
- Reviewing by commit is easier than by file; the four are split by
concern.
- **Formula-level HAVING is gone for real.** It only rendered behind
`isAdditionalFilterEnable`, whose sole call site passed `false`, and
QBv2 never reimplemented it — so this removes the only implementation
rather than one of two. Shout if that was on someone's roadmap.
- **`renderOrderBy` was already dead**, which means Logs and Traces
Explorer silently lost their `ExplorerOrderBy` control when QBv2 landed.
I removed the prop but left the component on disk, since that looks like
an unintended regression rather than intended cleanup.
- **Known gap:** the metrics aggregation section is outside the config.
`MetricsAggregateSection` renders its own group-by, space aggregation
and step interval, so `{ groupBy: { state: 'hidden' } }` looks like it
works on a metrics query and does not. Worth closing separately.
- `disabled` is implemented, not just declared — greyed control,
`reason` in the tooltip, refuses activation — but nothing declares it
yet; ListPanel still hides. Switching any field over is a one-key edit.
619 lines
18 KiB
TypeScript
619 lines
18 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { Button, Tooltip } from 'antd';
|
|
import cx from 'classnames';
|
|
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
|
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 { get, isEmpty } from 'lodash-es';
|
|
import { BarChart, ChevronUp, ExternalLink, ScrollText } from '@signozhq/icons';
|
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
|
import { MetricAggregation } from 'types/api/v5/queryRange';
|
|
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
|
|
|
import {
|
|
QueryBuilderField,
|
|
QueryBuilderFieldsConfig,
|
|
} from '../../queryBuilderFields.types';
|
|
import {
|
|
mergeQueryBuilderFieldsConfig,
|
|
RAW_QUERY_FIELDS,
|
|
resolveQueryBuilderFields,
|
|
} from '../../queryBuilderFields.utils';
|
|
|
|
import HavingFilter from './HavingFilter/HavingFilter';
|
|
import { buildDefaultLegendFromGroupBy } from './utils';
|
|
|
|
import './QueryAddOns.styles.scss';
|
|
|
|
interface AddOn {
|
|
icon: React.ReactNode;
|
|
label: string;
|
|
key: QueryBuilderField;
|
|
description?: string;
|
|
docLink?: string;
|
|
}
|
|
|
|
/** Fields the add-on bar does not own: each has its own control elsewhere in the query. */
|
|
type NonAddOnField =
|
|
| QueryBuilderField.Aggregation
|
|
| QueryBuilderField.StepInterval
|
|
| QueryBuilderField.Functions
|
|
| QueryBuilderField.Formula
|
|
| QueryBuilderField.AdditionalQueries;
|
|
|
|
// Omit rather than Partial, so a field added to the enum has to be placed on one side.
|
|
const ADD_ONS_KEYS_TO_QUERY_PATH: Omit<
|
|
Record<QueryBuilderField, string>,
|
|
NonAddOnField
|
|
> = {
|
|
[QueryBuilderField.GroupBy]: 'groupBy',
|
|
[QueryBuilderField.Having]: 'having.expression',
|
|
[QueryBuilderField.OrderBy]: 'orderBy',
|
|
[QueryBuilderField.Limit]: 'limit',
|
|
[QueryBuilderField.Legend]: 'legend',
|
|
[QueryBuilderField.ReduceTo]: 'reduceTo',
|
|
};
|
|
|
|
const ADD_ONS: AddOn[] = [
|
|
{
|
|
icon: <BarChart size={14} />,
|
|
label: 'Group By',
|
|
key: QueryBuilderField.GroupBy,
|
|
description:
|
|
'Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments.',
|
|
docLink: 'https://signoz.io/docs/querying/aggregation-grouping/#grouping',
|
|
},
|
|
{
|
|
icon: <ScrollText size={14} />,
|
|
label: 'Having',
|
|
key: QueryBuilderField.Having,
|
|
description:
|
|
'Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500',
|
|
docLink:
|
|
'https://signoz.io/docs/querying/result-manipulation/#conditional-filtering-with-having',
|
|
},
|
|
{
|
|
icon: <ScrollText size={14} />,
|
|
label: 'Order By',
|
|
key: QueryBuilderField.OrderBy,
|
|
description:
|
|
'Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers.',
|
|
docLink:
|
|
'https://signoz.io/docs/querying/result-manipulation/#sorting--limiting',
|
|
},
|
|
{
|
|
icon: <ScrollText size={14} />,
|
|
label: 'Limit',
|
|
key: QueryBuilderField.Limit,
|
|
description:
|
|
'Show only the top/bottom N results. Perfect for focusing on outliers, reducing noise, and improving dashboard performance.',
|
|
docLink:
|
|
'https://signoz.io/docs/querying/result-manipulation/#how-limit-works-for-time-series',
|
|
},
|
|
{
|
|
icon: <ScrollText size={14} />,
|
|
label: 'Legend format',
|
|
key: QueryBuilderField.Legend,
|
|
description:
|
|
'Customize series labels using variables like {{service.name}}-{{endpoint}}. Makes charts readable at a glance during incident investigation.',
|
|
docLink:
|
|
'https://signoz.io/docs/querying/aggregation-grouping/#legend-formatting',
|
|
},
|
|
];
|
|
|
|
const REDUCE_TO: AddOn = {
|
|
icon: <ScrollText size={14} />,
|
|
label: 'Reduce to',
|
|
key: QueryBuilderField.ReduceTo,
|
|
description:
|
|
'Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value.',
|
|
docLink:
|
|
'https://signoz.io/docs/userguide/query-builder-v5/#result-manipulation',
|
|
};
|
|
|
|
const hasValue = (value: unknown): boolean =>
|
|
value != null && value !== '' && !(Array.isArray(value) && value.length === 0);
|
|
|
|
// Custom tooltip content component
|
|
function TooltipContent({
|
|
label,
|
|
description,
|
|
docLink,
|
|
}: {
|
|
label: string;
|
|
description?: string;
|
|
docLink?: string;
|
|
}): JSX.Element {
|
|
return (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '8px',
|
|
maxWidth: '300px',
|
|
}}
|
|
>
|
|
<strong style={{ fontSize: '14px' }}>{label}</strong>
|
|
{description && (
|
|
<span style={{ fontSize: '12px', lineHeight: '1.5' }}>{description}</span>
|
|
)}
|
|
{docLink && (
|
|
<a
|
|
href={docLink}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={(e): void => e.stopPropagation()}
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
color: '#4096ff',
|
|
fontSize: '12px',
|
|
marginTop: '4px',
|
|
}}
|
|
>
|
|
Learn more
|
|
<ExternalLink size={12} />
|
|
</a>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function QueryAddOns({
|
|
query,
|
|
version,
|
|
isRawQuery,
|
|
showReduceTo,
|
|
panelType,
|
|
index,
|
|
fieldsConfig,
|
|
isForTraceOperator = false,
|
|
}: {
|
|
query: IBuilderQuery;
|
|
version: string;
|
|
isRawQuery: boolean;
|
|
showReduceTo: boolean;
|
|
panelType: PANEL_TYPES | null;
|
|
index: number;
|
|
fieldsConfig?: QueryBuilderFieldsConfig;
|
|
isForTraceOperator?: boolean;
|
|
}): JSX.Element {
|
|
const [selectedViews, setSelectedViews] = useState<AddOn[]>([]);
|
|
|
|
const initializedRef = useRef(false);
|
|
const prevAvailableKeysRef = useRef<Set<QueryBuilderField> | null>(null);
|
|
|
|
const { handleChangeQueryData } = useQueryOperations({
|
|
index,
|
|
query,
|
|
entityVersion: '',
|
|
isForTraceOperator,
|
|
});
|
|
|
|
const { handleSetQueryData } = useQueryBuilder();
|
|
|
|
const supportedAddOns = useMemo((): AddOn[] => {
|
|
let addOns: AddOn[];
|
|
|
|
if (panelType === PANEL_TYPES.VALUE) {
|
|
addOns = ADD_ONS.filter((addOn) => addOn.key === QueryBuilderField.Legend);
|
|
} else if (query.dataSource === DataSource.METRICS) {
|
|
// Group by for metrics is offered by MetricsAggregateSection instead.
|
|
addOns = ADD_ONS.filter((addOn) => addOn.key !== QueryBuilderField.GroupBy);
|
|
} else {
|
|
addOns = [...ADD_ONS];
|
|
}
|
|
|
|
return showReduceTo ? [...addOns, REDUCE_TO] : addOns;
|
|
}, [panelType, query.dataSource, showReduceTo]);
|
|
|
|
const resolvedFields = useMemo(
|
|
() =>
|
|
resolveQueryBuilderFields(
|
|
supportedAddOns.map((addOn) => addOn.key),
|
|
mergeQueryBuilderFieldsConfig(
|
|
isRawQuery ? RAW_QUERY_FIELDS : undefined,
|
|
fieldsConfig,
|
|
),
|
|
),
|
|
[supportedAddOns, fieldsConfig, isRawQuery],
|
|
);
|
|
|
|
const offeredAddOns = useMemo(
|
|
() =>
|
|
supportedAddOns.filter((addOn) => !resolvedFields.get(addOn.key)?.hidden),
|
|
[supportedAddOns, resolvedFields],
|
|
);
|
|
|
|
const pinnedAddOns = useMemo(
|
|
() => offeredAddOns.filter((addOn) => resolvedFields.get(addOn.key)?.pinned),
|
|
[offeredAddOns, resolvedFields],
|
|
);
|
|
|
|
const togglableAddOns = useMemo(
|
|
() => offeredAddOns.filter((addOn) => !resolvedFields.get(addOn.key)?.pinned),
|
|
[offeredAddOns, resolvedFields],
|
|
);
|
|
|
|
const isPinned = useCallback(
|
|
(key: QueryBuilderField): boolean => Boolean(resolvedFields.get(key)?.pinned),
|
|
[resolvedFields],
|
|
);
|
|
|
|
const isDisabled = useCallback(
|
|
(key: QueryBuilderField): boolean =>
|
|
Boolean(resolvedFields.get(key)?.disabled),
|
|
[resolvedFields],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const availableAddOnKeys = new Set(offeredAddOns.map((a) => a.key));
|
|
const previousKeys = prevAvailableKeysRef.current;
|
|
const hasAvailabilityItemsChanged =
|
|
previousKeys !== null &&
|
|
(previousKeys.size !== availableAddOnKeys.size ||
|
|
[...availableAddOnKeys].some((key) => !previousKeys.has(key)));
|
|
prevAvailableKeysRef.current = availableAddOnKeys;
|
|
|
|
if (!initializedRef.current || hasAvailabilityItemsChanged) {
|
|
initializedRef.current = true;
|
|
|
|
const activeAddOnKeys = new Set(
|
|
Object.entries(ADD_ONS_KEYS_TO_QUERY_PATH)
|
|
.filter(([, path]) => hasValue(get(query, path)))
|
|
.map(([key]) => key as QueryBuilderField),
|
|
);
|
|
|
|
// Initial seeding from query values on mount. A disabled field never opens.
|
|
setSelectedViews(
|
|
offeredAddOns.filter((addOn) => {
|
|
const resolved = resolvedFields.get(addOn.key);
|
|
|
|
return (
|
|
resolved?.pinned ||
|
|
(activeAddOnKeys.has(addOn.key) && !resolved?.disabled)
|
|
);
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setSelectedViews((prev) => {
|
|
const kept = prev.filter((view) => availableAddOnKeys.has(view.key));
|
|
|
|
const reopenedPinned = pinnedAddOns.filter(
|
|
(addOn) => !kept.some((view) => view.key === addOn.key),
|
|
);
|
|
|
|
return [...kept, ...reopenedPinned];
|
|
});
|
|
}, [offeredAddOns, pinnedAddOns, query]);
|
|
|
|
const handleOptionClick = (clickedAddOn: AddOn): void => {
|
|
if (isDisabled(clickedAddOn.key)) {
|
|
return;
|
|
}
|
|
|
|
const isAlreadySelected = selectedViews.some(
|
|
(view) => view.key === clickedAddOn.key,
|
|
);
|
|
|
|
if (isAlreadySelected) {
|
|
setSelectedViews((prev) =>
|
|
prev.filter((view) => view.key !== clickedAddOn.key),
|
|
);
|
|
} else {
|
|
// When enabling Legend format for the first time with an empty legend
|
|
// and existing group-by keys, prefill the legend using all group-by keys.
|
|
// This keeps existing custom legends intact and only helps seed a sensible default.
|
|
if (
|
|
clickedAddOn.key === QueryBuilderField.Legend &&
|
|
isEmpty(query?.legend) &&
|
|
Array.isArray(query.groupBy) &&
|
|
query.groupBy.length > 0
|
|
) {
|
|
const defaultLegend = buildDefaultLegendFromGroupBy(query.groupBy);
|
|
|
|
if (defaultLegend) {
|
|
handleChangeQueryLegend(defaultLegend);
|
|
}
|
|
}
|
|
|
|
setSelectedViews((prev) => [...prev, clickedAddOn]);
|
|
}
|
|
};
|
|
|
|
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: QueryBuilderField): void => {
|
|
if (isPinned(key)) {
|
|
return;
|
|
}
|
|
|
|
setSelectedViews((prev) => prev.filter((view) => view.key !== key));
|
|
},
|
|
[isPinned],
|
|
);
|
|
|
|
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" data-testid="query-add-ons">
|
|
{selectedViews.length > 0 && (
|
|
<div className="selected-add-ons-content">
|
|
{selectedViews.find((view) => view.key === QueryBuilderField.GroupBy) && (
|
|
<div className="add-on-content" data-testid="group-by-content">
|
|
<div className="periscope-input-with-label">
|
|
<Tooltip
|
|
title={
|
|
<TooltipContent
|
|
label="Group By"
|
|
description="Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments."
|
|
docLink="https://signoz.io/docs/querying/aggregation-grouping/#grouping"
|
|
/>
|
|
}
|
|
placement="top"
|
|
mouseEnterDelay={0.5}
|
|
>
|
|
<div className="label" style={{ cursor: 'help' }}>
|
|
Group By
|
|
</div>
|
|
</Tooltip>
|
|
<div className="input">
|
|
<GroupByFilter
|
|
disabled={
|
|
query.dataSource === DataSource.METRICS &&
|
|
!(query.aggregations?.[0] as MetricAggregation)?.metricName
|
|
}
|
|
query={query}
|
|
onChange={handleChangeGroupByKeys}
|
|
/>
|
|
</div>
|
|
{!isPinned(QueryBuilderField.GroupBy) && (
|
|
<Button
|
|
className="close-btn periscope-btn ghost"
|
|
icon={<ChevronUp size={16} />}
|
|
onClick={(): void => handleRemoveView(QueryBuilderField.GroupBy)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{selectedViews.find((view) => view.key === QueryBuilderField.Having) && (
|
|
<div className="add-on-content" data-testid="having-content">
|
|
<div className="periscope-input-with-label">
|
|
<Tooltip
|
|
title={
|
|
<TooltipContent
|
|
label="Having"
|
|
description="Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500"
|
|
docLink="https://signoz.io/docs/querying/result-manipulation/#conditional-filtering-with-having"
|
|
/>
|
|
}
|
|
placement="top"
|
|
mouseEnterDelay={0.5}
|
|
>
|
|
<div className="label" style={{ cursor: 'help' }}>
|
|
Having
|
|
</div>
|
|
</Tooltip>
|
|
<div className="input">
|
|
<HavingFilter
|
|
onClose={(): void => handleRemoveView(QueryBuilderField.Having)}
|
|
onChange={handleChangeHaving}
|
|
queryData={query}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{selectedViews.find((view) => view.key === QueryBuilderField.Limit) && (
|
|
<div className="add-on-content" data-testid="limit-content">
|
|
<InputWithLabel
|
|
label="Limit"
|
|
type="number"
|
|
onChange={handleChangeLimit}
|
|
initialValue={query?.limit ?? undefined}
|
|
placeholder="Enter limit"
|
|
onClose={(): void => handleRemoveView(QueryBuilderField.Limit)}
|
|
closeIcon={<ChevronUp size={16} />}
|
|
/>
|
|
</div>
|
|
)}
|
|
{selectedViews.find((view) => view.key === QueryBuilderField.OrderBy) && (
|
|
<div className="add-on-content" data-testid="order-by-content">
|
|
<div className="periscope-input-with-label">
|
|
<Tooltip
|
|
title={
|
|
<TooltipContent
|
|
label="Order By"
|
|
description="Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers."
|
|
docLink="https://signoz.io/docs/querying/result-manipulation/#sorting--limiting"
|
|
/>
|
|
}
|
|
placement="top"
|
|
mouseEnterDelay={0.5}
|
|
>
|
|
<div className="label" style={{ cursor: 'help' }}>
|
|
Order By
|
|
</div>
|
|
</Tooltip>
|
|
<div className="input">
|
|
<OrderByFilter
|
|
entityVersion={version}
|
|
query={query}
|
|
onChange={handleChangeOrderByKeys}
|
|
isRawQuery={isRawQuery}
|
|
isNewQueryV2
|
|
/>
|
|
</div>
|
|
{!isPinned(QueryBuilderField.OrderBy) && (
|
|
<Button
|
|
className="close-btn periscope-btn ghost"
|
|
icon={<ChevronUp size={16} />}
|
|
onClick={(): void => handleRemoveView(QueryBuilderField.OrderBy)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{selectedViews.find((view) => view.key === QueryBuilderField.ReduceTo) &&
|
|
showReduceTo && (
|
|
<div className="add-on-content" data-testid="reduce-to-content">
|
|
<div className="periscope-input-with-label">
|
|
<Tooltip
|
|
title={
|
|
<TooltipContent
|
|
label="Reduce to"
|
|
description="Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value."
|
|
docLink="https://signoz.io/docs/userguide/query-builder-v5/#result-manipulation"
|
|
/>
|
|
}
|
|
placement="top"
|
|
mouseEnterDelay={0.5}
|
|
>
|
|
<div className="label" style={{ cursor: 'help' }}>
|
|
Reduce to
|
|
</div>
|
|
</Tooltip>
|
|
<div className="input">
|
|
<ReduceToFilter query={query} onChange={handleChangeReduceToV5} />
|
|
</div>
|
|
|
|
{!isPinned(QueryBuilderField.ReduceTo) && (
|
|
<Button
|
|
className="close-btn periscope-btn ghost"
|
|
icon={<ChevronUp size={16} />}
|
|
onClick={(): void => handleRemoveView(QueryBuilderField.ReduceTo)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{selectedViews.find((view) => view.key === QueryBuilderField.Legend) && (
|
|
<div className="add-on-content" data-testid="legend-format-content">
|
|
<InputWithLabel
|
|
label="Legend format"
|
|
placeholder="Write legend format"
|
|
onChange={handleChangeQueryLegend}
|
|
initialValue={isEmpty(query?.legend) ? undefined : query?.legend}
|
|
onClose={(): void => handleRemoveView(QueryBuilderField.Legend)}
|
|
closeIcon={<ChevronUp size={16} />}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<ToggleGroupSimple
|
|
type="multiple"
|
|
className="add-ons-tabs"
|
|
value={selectedViews.map((view) => view.key)}
|
|
onChange={(newKeys: string[]): void => {
|
|
const oldKeys: string[] = selectedViews.map((view) => view.key);
|
|
const toggledKey =
|
|
newKeys.find((key) => !oldKeys.includes(key)) ??
|
|
oldKeys.find((key) => !newKeys.includes(key));
|
|
if (!toggledKey) {
|
|
return;
|
|
}
|
|
const clickedAddOn = togglableAddOns.find((a) => a.key === toggledKey);
|
|
if (clickedAddOn) {
|
|
handleOptionClick(clickedAddOn);
|
|
}
|
|
}}
|
|
items={togglableAddOns.map((addOn) => {
|
|
const resolved = resolvedFields.get(addOn.key);
|
|
|
|
return {
|
|
value: addOn.key,
|
|
label: (
|
|
<Tooltip
|
|
title={
|
|
<TooltipContent
|
|
label={addOn.label}
|
|
description={resolved?.reason ?? addOn.description}
|
|
docLink={resolved?.disabled ? undefined : addOn.docLink}
|
|
/>
|
|
}
|
|
placement="top"
|
|
mouseEnterDelay={0.5}
|
|
>
|
|
<span
|
|
className={cx('add-on-tab-title', {
|
|
'add-on-tab-title--disabled': resolved?.disabled,
|
|
})}
|
|
aria-disabled={resolved?.disabled}
|
|
data-testid={`query-add-on-${addOn.key}`}
|
|
>
|
|
{addOn.icon}
|
|
{addOn.label}
|
|
</span>
|
|
</Tooltip>
|
|
),
|
|
};
|
|
})}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default QueryAddOns;
|