mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
189 lines
5.5 KiB
TypeScript
189 lines
5.5 KiB
TypeScript
import {Fragment, useCallback, useEffect, useMemo} from 'react';
|
|
import styled from '@emotion/styled';
|
|
|
|
import SelectControl from 'sentry/components/forms/controls/selectControl';
|
|
import Tag from 'sentry/components/tag';
|
|
import {t} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {MetricMeta, MRI, ParsedMRI, Project} from 'sentry/types';
|
|
import {isAllowedOp} from 'sentry/utils/metrics';
|
|
import {getReadableMetricType} from 'sentry/utils/metrics/formatters';
|
|
import {
|
|
DEFAULT_METRIC_ALERT_FIELD,
|
|
formatMRI,
|
|
MRIToField,
|
|
parseField,
|
|
parseMRI,
|
|
} from 'sentry/utils/metrics/mri';
|
|
import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
|
|
import {middleEllipsis} from 'sentry/utils/middleEllipsis';
|
|
|
|
interface Props {
|
|
aggregate: string;
|
|
onChange: (value: string, meta: Record<string, any>) => void;
|
|
project: Project;
|
|
}
|
|
|
|
function filterAndSortOperations(operations: string[]) {
|
|
return operations.filter(isAllowedOp).sort((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
function MriField({aggregate, project, onChange}: Props) {
|
|
const {data: meta, isLoading} = useMetricsMeta({projects: [parseInt(project.id, 10)]}, [
|
|
'custom',
|
|
]);
|
|
|
|
const metaArr = useMemo(() => {
|
|
return meta.map(
|
|
metric =>
|
|
({
|
|
...metric,
|
|
...parseMRI(metric.mri),
|
|
}) as ParsedMRI & MetricMeta
|
|
);
|
|
}, [meta]);
|
|
|
|
const selectedValues = parseField(aggregate) ?? {mri: '' as MRI, op: ''};
|
|
|
|
const selectedMriMeta = useMemo(() => {
|
|
return meta.find(metric => metric.mri === selectedValues.mri);
|
|
}, [meta, selectedValues.mri]);
|
|
|
|
useEffect(() => {
|
|
// Auto-select the first mri if none of the available ones is selected
|
|
if (!selectedMriMeta && !isLoading) {
|
|
const newSelection = metaArr[0];
|
|
if (newSelection) {
|
|
onChange(
|
|
MRIToField(
|
|
newSelection.mri,
|
|
filterAndSortOperations(newSelection.operations)[0]
|
|
),
|
|
{}
|
|
);
|
|
} else if (aggregate !== DEFAULT_METRIC_ALERT_FIELD) {
|
|
onChange(DEFAULT_METRIC_ALERT_FIELD, {});
|
|
}
|
|
}
|
|
}, [metaArr, onChange, selectedMriMeta, isLoading, aggregate]);
|
|
|
|
const handleMriChange = useCallback(
|
|
option => {
|
|
const selectedMeta = meta.find(metric => metric.mri === option.value);
|
|
if (!selectedMeta) {
|
|
return;
|
|
}
|
|
// Make sure that the selected operation matches the new metric
|
|
const availableOps = filterAndSortOperations(selectedMeta.operations);
|
|
const selectedOp =
|
|
selectedValues.op && availableOps.includes(selectedValues.op)
|
|
? selectedValues.op
|
|
: availableOps[0];
|
|
onChange(MRIToField(option.value, selectedOp), {});
|
|
},
|
|
[meta, onChange, selectedValues.op]
|
|
);
|
|
|
|
const operationOptions = useMemo(
|
|
() =>
|
|
filterAndSortOperations(selectedMriMeta?.operations ?? []).map(op => ({
|
|
label: op,
|
|
value: op,
|
|
})),
|
|
[selectedMriMeta]
|
|
);
|
|
|
|
// As SelectControl does not support an options size limit out of the box
|
|
// we work around it by using the async variant of the control
|
|
const getMriOptions = useCallback(
|
|
(searchText: string) => {
|
|
const filteredMeta = metaArr.filter(
|
|
({name}) =>
|
|
searchText === '' || name.toLowerCase().includes(searchText.toLowerCase())
|
|
);
|
|
|
|
const options = filteredMeta.splice(0, 100).map<{
|
|
label: React.ReactNode;
|
|
value: string;
|
|
disabled?: boolean;
|
|
trailingItems?: React.ReactNode;
|
|
}>(metric => ({
|
|
label: middleEllipsis(metric.name, 50, /\.|-|_/),
|
|
value: metric.mri,
|
|
trailingItems: (
|
|
<Fragment>
|
|
<Tag tooltipText={t('Type')}>{getReadableMetricType(metric.type)}</Tag>
|
|
<Tag tooltipText={t('Unit')}>{metric.unit}</Tag>
|
|
</Fragment>
|
|
),
|
|
}));
|
|
|
|
if (filteredMeta.length > options.length) {
|
|
options.push({
|
|
label: (
|
|
<SizeLimitMessage>{t('Use search to find more options…')}</SizeLimitMessage>
|
|
),
|
|
value: '',
|
|
disabled: true,
|
|
});
|
|
}
|
|
return options;
|
|
},
|
|
[metaArr]
|
|
);
|
|
|
|
// When using the async variant of SelectControl, we need to pass in an option object instead of just the value
|
|
const selectedMriOption = selectedMriMeta && {
|
|
label: formatMRI(selectedMriMeta.mri),
|
|
value: selectedMriMeta.mri,
|
|
};
|
|
|
|
return (
|
|
<Wrapper>
|
|
<StyledSelectControl
|
|
searchable
|
|
isDisabled={isLoading}
|
|
placeholder={t('Select a metric')}
|
|
noOptionsMessage={() =>
|
|
metaArr.length === 0 ? t('No metrics in this project') : t('No options')
|
|
}
|
|
async
|
|
defaultOptions={getMriOptions('')}
|
|
loadOptions={searchText => Promise.resolve(getMriOptions(searchText))}
|
|
filterOption={() => true}
|
|
value={selectedMriOption}
|
|
onChange={handleMriChange}
|
|
/>
|
|
<StyledSelectControl
|
|
searchable
|
|
isDisabled={isLoading || !selectedMriMeta}
|
|
placeholder={t('Select an operation')}
|
|
options={operationOptions}
|
|
value={selectedValues.op}
|
|
onChange={option => {
|
|
onChange(`${option.value}(${selectedValues.mri})`, {});
|
|
}}
|
|
/>
|
|
</Wrapper>
|
|
);
|
|
}
|
|
|
|
export default MriField;
|
|
|
|
const Wrapper = styled('div')`
|
|
display: grid;
|
|
gap: ${space(1)};
|
|
grid-template-columns: 1fr 1fr;
|
|
`;
|
|
|
|
const StyledSelectControl = styled(SelectControl)`
|
|
width: 200px;
|
|
`;
|
|
|
|
const SizeLimitMessage = styled('span')`
|
|
font-size: ${p => p.theme.fontSizeSmall};
|
|
display: block;
|
|
width: 100%;
|
|
text-align: center;
|
|
`;
|