|
@@ -17,28 +17,33 @@ import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
|
|
|
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
|
|
|
import Panel from 'sentry/components/panels/panel';
|
|
|
import PanelBody from 'sentry/components/panels/panelBody';
|
|
|
-import PanelHeader from 'sentry/components/panels/panelHeader';
|
|
|
import PanelTable from 'sentry/components/panels/panelTable';
|
|
|
+import Tag from 'sentry/components/tag';
|
|
|
import {IconSearch} from 'sentry/icons';
|
|
|
+import {t} from 'sentry/locale';
|
|
|
import {space} from 'sentry/styles/space';
|
|
|
import {MetricsTag, TagCollection} from 'sentry/types';
|
|
|
-import getDynamicText from 'sentry/utils/getDynamicText';
|
|
|
import {
|
|
|
+ defaultMetricDisplayType,
|
|
|
+ getNameFromMRI,
|
|
|
+ getReadableMetricType,
|
|
|
+ getUnitFromMRI,
|
|
|
getUseCaseFromMri,
|
|
|
+ MetricDisplayType,
|
|
|
MetricsData,
|
|
|
MetricsDataProps,
|
|
|
+ tooltipFormatterUsingUnit,
|
|
|
useMetricsData,
|
|
|
useMetricsMeta,
|
|
|
useMetricsTags,
|
|
|
} from 'sentry/utils/metrics';
|
|
|
import theme from 'sentry/utils/theme';
|
|
|
import useApi from 'sentry/utils/useApi';
|
|
|
+import useKeyPress from 'sentry/utils/useKeyPress';
|
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
|
import usePageFilters from 'sentry/utils/usePageFilters';
|
|
|
import useProjects from 'sentry/utils/useProjects';
|
|
|
-
|
|
|
-const displayTypes = ['Line Chart', 'Bar Chart', 'Area Chart', 'Table'] as const;
|
|
|
-type DisplayType = (typeof displayTypes)[number];
|
|
|
+import useRouter from 'sentry/utils/useRouter';
|
|
|
|
|
|
const useProjectSelectionSlugs = () => {
|
|
|
const {selection} = usePageFilters();
|
|
@@ -57,18 +62,17 @@ function MetricsExplorer() {
|
|
|
const {selection} = usePageFilters();
|
|
|
|
|
|
const slugs = useProjectSelectionSlugs();
|
|
|
+ const router = useRouter();
|
|
|
|
|
|
const [query, setQuery] = useState<QueryBuilderState>();
|
|
|
- const [displayType, setDisplayType] = useState<DisplayType>('Line Chart');
|
|
|
|
|
|
return (
|
|
|
<MetricsExplorerPanel>
|
|
|
- <MetricsExplorerHeader displayType={displayType} setDisplayType={setDisplayType} />
|
|
|
<PanelBody>
|
|
|
<QueryBuilder setQuery={setQuery} />
|
|
|
{query && (
|
|
|
<MetricsExplorerDisplayOuter
|
|
|
- displayType={displayType}
|
|
|
+ displayType={router.location.query.display ?? defaultMetricDisplayType}
|
|
|
datetime={selection.datetime}
|
|
|
projects={slugs}
|
|
|
{...query}
|
|
@@ -79,31 +83,6 @@ function MetricsExplorer() {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-type MetricsExplorerHeaderProps = {
|
|
|
- displayType: DisplayType;
|
|
|
- setDisplayType: (displayType: DisplayType) => void;
|
|
|
-};
|
|
|
-
|
|
|
-function MetricsExplorerHeader({
|
|
|
- displayType,
|
|
|
- setDisplayType,
|
|
|
-}: MetricsExplorerHeaderProps) {
|
|
|
- return (
|
|
|
- <PanelHeader>
|
|
|
- <div>Metrics Explorer</div>
|
|
|
- <CompactSelect
|
|
|
- triggerProps={{size: 'xs', prefix: 'Display'}}
|
|
|
- value={displayType}
|
|
|
- options={displayTypes.map(opt => ({
|
|
|
- value: opt,
|
|
|
- label: opt,
|
|
|
- }))}
|
|
|
- onChange={opt => setDisplayType(opt.value as DisplayType)}
|
|
|
- />
|
|
|
- </PanelHeader>
|
|
|
- );
|
|
|
-}
|
|
|
-
|
|
|
type QueryBuilderProps = {
|
|
|
setQuery: (query: QueryBuilderState) => void;
|
|
|
};
|
|
@@ -135,6 +114,15 @@ type QueryBuilderAction =
|
|
|
|
|
|
function QueryBuilder({setQuery}: QueryBuilderProps) {
|
|
|
const meta = useMetricsMeta();
|
|
|
+ const mriModeKeyPressed = useKeyPress('`', undefined, true);
|
|
|
+ const [mriMode, setMriMode] = useState(false);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (mriModeKeyPressed) {
|
|
|
+ setMriMode(!mriMode);
|
|
|
+ }
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
+ }, [mriModeKeyPressed]);
|
|
|
|
|
|
const isAllowedOp = (op: string) =>
|
|
|
!['max_timestamp', 'min_timestamp', 'histogram'].includes(op);
|
|
@@ -177,18 +165,28 @@ function QueryBuilder({setQuery}: QueryBuilderProps) {
|
|
|
<PageFilterBar condensed>
|
|
|
<CompactSelect
|
|
|
searchable
|
|
|
- triggerProps={{prefix: 'MRI', size: 'sm'}}
|
|
|
- options={Object.keys(meta).map(mri => ({
|
|
|
- label: mri,
|
|
|
- value: mri,
|
|
|
- }))}
|
|
|
+ triggerProps={{prefix: t('Metric'), size: 'sm'}}
|
|
|
+ options={Object.values(meta)
|
|
|
+ .filter(metric => (mriMode ? true : metric.mri.includes(':custom/')))
|
|
|
+ .map(metric => ({
|
|
|
+ label: mriMode ? metric.mri : metric.name,
|
|
|
+ value: metric.mri,
|
|
|
+ trailingItems: mriMode ? undefined : (
|
|
|
+ <Fragment>
|
|
|
+ <Tag tooltipText={t('Type')}>
|
|
|
+ {getReadableMetricType(metric.type)}
|
|
|
+ </Tag>
|
|
|
+ <Tag tooltipText={t('Unit')}>{metric.unit}</Tag>
|
|
|
+ </Fragment>
|
|
|
+ ),
|
|
|
+ }))}
|
|
|
value={state.mri}
|
|
|
onChange={option => {
|
|
|
dispatch({type: 'mri', value: option.value});
|
|
|
}}
|
|
|
/>
|
|
|
<CompactSelect
|
|
|
- triggerProps={{prefix: 'Operation', size: 'sm'}}
|
|
|
+ triggerProps={{prefix: t('Operation'), size: 'sm'}}
|
|
|
options={selectedMetric.operations.filter(isAllowedOp).map(op => ({
|
|
|
label: op,
|
|
|
value: op,
|
|
@@ -198,7 +196,7 @@ function QueryBuilder({setQuery}: QueryBuilderProps) {
|
|
|
/>
|
|
|
<CompactSelect
|
|
|
multiple
|
|
|
- triggerProps={{prefix: 'Group by', size: 'sm'}}
|
|
|
+ triggerProps={{prefix: t('Group by'), size: 'sm'}}
|
|
|
options={tags.map(tag => ({
|
|
|
label: tag.key,
|
|
|
value: tag.key,
|
|
@@ -271,7 +269,7 @@ function MetricSearchBar({tags, mri, disabled, onChange}: MetricSearchBarProps)
|
|
|
supportedTags={supportedTags}
|
|
|
onClose={handleChange}
|
|
|
onSearch={handleChange}
|
|
|
- placeholder="Search for tags"
|
|
|
+ placeholder={t('Filter by tags')}
|
|
|
/>
|
|
|
);
|
|
|
}
|
|
@@ -298,16 +296,18 @@ type Group = {
|
|
|
};
|
|
|
|
|
|
type DisplayProps = MetricsDataProps & {
|
|
|
- displayType: DisplayType;
|
|
|
+ displayType: MetricDisplayType;
|
|
|
};
|
|
|
|
|
|
function MetricsExplorerDisplayOuter(props?: DisplayProps) {
|
|
|
if (!props?.mri) {
|
|
|
return (
|
|
|
<DisplayWrapper>
|
|
|
- <EmptyMessage icon={<IconSearch size="xxl" />}>
|
|
|
- Nothing to show. Choose an MRI to display data!
|
|
|
- </EmptyMessage>
|
|
|
+ <EmptyMessage
|
|
|
+ icon={<IconSearch size="xxl" />}
|
|
|
+ title={t('Nothing to show!')}
|
|
|
+ description={t('Choose a metric to display data.')}
|
|
|
+ />
|
|
|
</DisplayWrapper>
|
|
|
);
|
|
|
}
|
|
@@ -321,7 +321,7 @@ function MetricsExplorerDisplay({displayType, ...metricsDataProps}: DisplayProps
|
|
|
return (
|
|
|
<DisplayWrapper>
|
|
|
{isLoading && <LoadingIndicator />}
|
|
|
- {isError && <Alert type="error">Error while fetching metrics data</Alert>}
|
|
|
+ {isError && <Alert type="error">{t('Error while fetching metrics data')}</Alert>}
|
|
|
</DisplayWrapper>
|
|
|
);
|
|
|
}
|
|
@@ -330,7 +330,7 @@ function MetricsExplorerDisplay({displayType, ...metricsDataProps}: DisplayProps
|
|
|
|
|
|
return (
|
|
|
<DisplayWrapper>
|
|
|
- {displayType === 'Table' ? (
|
|
|
+ {displayType === MetricDisplayType.TABLE ? (
|
|
|
<Table data={sorted} />
|
|
|
) : (
|
|
|
<Chart data={sorted} displayType={displayType} />
|
|
@@ -344,7 +344,9 @@ function getSeriesName(group: Group, isOnlyGroup = false) {
|
|
|
return Object.keys(group.series)?.[0] ?? '(none)';
|
|
|
}
|
|
|
|
|
|
- return Object.values(group.by).join('-') ?? '(none)';
|
|
|
+ return Object.entries(group.by)
|
|
|
+ .map(([key, value]) => `${key}:${String(value).length ? value : t('none')}`)
|
|
|
+ .join(', ');
|
|
|
}
|
|
|
|
|
|
function sortData(data: MetricsData): MetricsData {
|
|
@@ -397,9 +399,11 @@ function normalizeChartTimeParams(data: MetricsData) {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
-function Chart({data, displayType}: {data: MetricsData; displayType: DisplayType}) {
|
|
|
+function Chart({data, displayType}: {data: MetricsData; displayType: MetricDisplayType}) {
|
|
|
const {start, end, period, utc} = normalizeChartTimeParams(data);
|
|
|
|
|
|
+ const unit = getUnitFromMRI(Object.keys(data.groups[0].series)[0]); // this assumes that all series have the same unit
|
|
|
+
|
|
|
const series = data.groups.map(g => {
|
|
|
return {
|
|
|
values: Object.values(g.series)[0],
|
|
@@ -424,29 +428,32 @@ function Chart({data, displayType}: {data: MetricsData; displayType: DisplayType
|
|
|
bottom: 20,
|
|
|
data: chartSeries.map(s => s.seriesName),
|
|
|
theme: theme as Theme,
|
|
|
+ formatter: mri => getNameFromMRI(mri),
|
|
|
}),
|
|
|
grid: {top: 30, bottom: 40, left: 20, right: 20},
|
|
|
+ tooltip: {
|
|
|
+ valueFormatter: (value: number) => tooltipFormatterUsingUnit(value, unit),
|
|
|
+ nameFormatter: mri => getNameFromMRI(mri),
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ axisLabel: {
|
|
|
+ formatter: (value: number) => tooltipFormatterUsingUnit(value, unit),
|
|
|
+ },
|
|
|
+ },
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
- <Fragment>
|
|
|
- {getDynamicText({
|
|
|
- value: (
|
|
|
- <ChartZoom period={period} start={start} end={end} utc={utc}>
|
|
|
- {zoomRenderProps =>
|
|
|
- displayType === 'Line Chart' ? (
|
|
|
- <LineChart {...chartProps} {...zoomRenderProps} />
|
|
|
- ) : displayType === 'Area Chart' ? (
|
|
|
- <AreaChart {...chartProps} {...zoomRenderProps} />
|
|
|
- ) : (
|
|
|
- <BarChart stacked {...chartProps} {...zoomRenderProps} />
|
|
|
- )
|
|
|
- }
|
|
|
- </ChartZoom>
|
|
|
- ),
|
|
|
- fixed: 'Metrics Chart',
|
|
|
- })}
|
|
|
- </Fragment>
|
|
|
+ <ChartZoom period={period} start={start} end={end} utc={utc}>
|
|
|
+ {zoomRenderProps =>
|
|
|
+ displayType === MetricDisplayType.LINE ? (
|
|
|
+ <LineChart {...chartProps} {...zoomRenderProps} />
|
|
|
+ ) : displayType === MetricDisplayType.AREA ? (
|
|
|
+ <AreaChart {...chartProps} {...zoomRenderProps} />
|
|
|
+ ) : (
|
|
|
+ <BarChart stacked {...chartProps} {...zoomRenderProps} />
|
|
|
+ )
|
|
|
+ }
|
|
|
+ </ChartZoom>
|
|
|
);
|
|
|
}
|
|
|
|