[Exploratory View] Mobile experience (#99565)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Bryce Buchanan <bryce.buchanan@elastic.co> Co-authored-by: Alexander Wert <alexander.wert@elastic.co>
This commit is contained in:
parent
cee33b004c
commit
303806de65
|
@ -53,10 +53,8 @@ export const fetchObservabilityOverviewPageData = async ({
|
|||
};
|
||||
|
||||
export async function getHasData() {
|
||||
const res = await callApmApi({
|
||||
return await callApmApi({
|
||||
endpoint: 'GET /api/apm/observability_overview/has_data',
|
||||
signal: null,
|
||||
});
|
||||
|
||||
return res.hasData;
|
||||
}
|
||||
|
|
|
@ -29,8 +29,14 @@ export async function getHasData({ setup }: { setup: Setup }) {
|
|||
'observability_overview_has_apm_data',
|
||||
params
|
||||
);
|
||||
return response.hits.total.value > 0;
|
||||
return {
|
||||
hasData: response.hits.total.value > 0,
|
||||
indices: setup.indices,
|
||||
};
|
||||
} catch (e) {
|
||||
return false;
|
||||
return {
|
||||
hasData: false,
|
||||
indices: setup.indices,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,22 +16,12 @@ import {
|
|||
import { APMConfig } from '../../..';
|
||||
import { APMRouteHandlerResources } from '../../../routes/typings';
|
||||
import { withApmSpan } from '../../../utils/with_apm_span';
|
||||
import { ApmIndicesConfig } from '../../../../../observability/common/typings';
|
||||
|
||||
export { ApmIndicesConfig };
|
||||
|
||||
type ISavedObjectsClient = Pick<SavedObjectsClient, 'get'>;
|
||||
|
||||
export interface ApmIndicesConfig {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
'apm_oss.sourcemapIndices': string;
|
||||
'apm_oss.errorIndices': string;
|
||||
'apm_oss.onboardingIndices': string;
|
||||
'apm_oss.spanIndices': string;
|
||||
'apm_oss.transactionIndices': string;
|
||||
'apm_oss.metricsIndices': string;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
apmAgentConfigurationIndex: string;
|
||||
apmCustomLinkIndex: string;
|
||||
}
|
||||
|
||||
export type ApmIndicesName = keyof ApmIndicesConfig;
|
||||
|
||||
async function getApmIndicesSavedObject(
|
||||
|
|
|
@ -21,8 +21,7 @@ const observabilityOverviewHasDataRoute = createApmServerRoute({
|
|||
options: { tags: ['access:apm'] },
|
||||
handler: async (resources) => {
|
||||
const setup = await setupRequest(resources);
|
||||
const res = await getHasData({ setup });
|
||||
return { hasData: res };
|
||||
return await getHasData({ setup });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -10,3 +10,16 @@ export type Maybe<T> = T | null | undefined;
|
|||
|
||||
export const alertStatusRt = t.union([t.literal('all'), t.literal('open'), t.literal('closed')]);
|
||||
export type AlertStatus = t.TypeOf<typeof alertStatusRt>;
|
||||
|
||||
export interface ApmIndicesConfig {
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
'apm_oss.sourcemapIndices': string;
|
||||
'apm_oss.errorIndices': string;
|
||||
'apm_oss.onboardingIndices': string;
|
||||
'apm_oss.spanIndices': string;
|
||||
'apm_oss.transactionIndices': string;
|
||||
'apm_oss.metricsIndices': string;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
apmAgentConfigurationIndex: string;
|
||||
apmCustomLinkIndex: string;
|
||||
}
|
||||
|
|
|
@ -13,26 +13,24 @@ import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
|||
import { useHasData } from '../../../hooks/use_has_data';
|
||||
import { usePluginContext } from '../../../hooks/use_plugin_context';
|
||||
import { getEmptySections } from '../../../pages/overview/empty_section';
|
||||
import { UXHasDataResponse } from '../../../typings';
|
||||
import { EmptySection } from './empty_section';
|
||||
|
||||
export function EmptySections() {
|
||||
const { core } = usePluginContext();
|
||||
const theme = useContext(ThemeContext);
|
||||
const { hasData } = useHasData();
|
||||
const { hasDataMap } = useHasData();
|
||||
|
||||
const appEmptySections = getEmptySections({ core }).filter(({ id }) => {
|
||||
if (id === 'alert') {
|
||||
const { status, hasData: alerts } = hasData.alert || {};
|
||||
const { status, hasData: alerts } = hasDataMap.alert || {};
|
||||
return (
|
||||
status === FETCH_STATUS.FAILURE ||
|
||||
(status === FETCH_STATUS.SUCCESS && (alerts as Alert[]).length === 0)
|
||||
);
|
||||
} else {
|
||||
const app = hasData[id];
|
||||
const app = hasDataMap[id];
|
||||
if (app) {
|
||||
const _hasData = id === 'ux' ? (app.hasData as UXHasDataResponse)?.hasData : app.hasData;
|
||||
return app.status === FETCH_STATUS.FAILURE || !_hasData;
|
||||
return app.status === FETCH_STATUS.FAILURE || !app.hasData;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -29,7 +29,7 @@ jest.mock('react-router-dom', () => ({
|
|||
describe('APMSection', () => {
|
||||
beforeAll(() => {
|
||||
jest.spyOn(hasDataHook, 'useHasData').mockReturnValue({
|
||||
hasData: {
|
||||
hasDataMap: {
|
||||
apm: {
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
hasData: true,
|
||||
|
|
|
@ -48,7 +48,7 @@ export function APMSection({ bucketSize }: Props) {
|
|||
const theme = useContext(ThemeContext);
|
||||
const chartTheme = useChartTheme();
|
||||
const history = useHistory();
|
||||
const { forceUpdate, hasData } = useHasData();
|
||||
const { forceUpdate, hasDataMap } = useHasData();
|
||||
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
|
@ -66,7 +66,7 @@ export function APMSection({ bucketSize }: Props) {
|
|||
[bucketSize, relativeStart, relativeEnd, forceUpdate]
|
||||
);
|
||||
|
||||
if (!hasData.apm?.hasData) {
|
||||
if (!hasDataMap.apm?.hasData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ function getColorPerItem(series?: LogsFetchDataResponse['series']) {
|
|||
export function LogsSection({ bucketSize }: Props) {
|
||||
const history = useHistory();
|
||||
const chartTheme = useChartTheme();
|
||||
const { forceUpdate, hasData } = useHasData();
|
||||
const { forceUpdate, hasDataMap } = useHasData();
|
||||
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
|
@ -65,7 +65,7 @@ export function LogsSection({ bucketSize }: Props) {
|
|||
[bucketSize, relativeStart, relativeEnd, forceUpdate]
|
||||
);
|
||||
|
||||
if (!hasData.infra_logs?.hasData) {
|
||||
if (!hasDataMap.infra_logs?.hasData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ const bytesPerSecondFormatter = (value: NumberOrNull) =>
|
|||
value === null ? '' : numeral(value).format('0b') + '/s';
|
||||
|
||||
export function MetricsSection({ bucketSize }: Props) {
|
||||
const { forceUpdate, hasData } = useHasData();
|
||||
const { forceUpdate, hasDataMap } = useHasData();
|
||||
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
|
||||
const [sortDirection, setSortDirection] = useState<Direction>('asc');
|
||||
const [sortField, setSortField] = useState<keyof MetricsFetchDataSeries>('uptime');
|
||||
|
@ -88,7 +88,7 @@ export function MetricsSection({ bucketSize }: Props) {
|
|||
[data, setSortField, setSortDirection]
|
||||
);
|
||||
|
||||
if (!hasData.infra_metrics?.hasData) {
|
||||
if (!hasDataMap.infra_metrics?.hasData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ export function UptimeSection({ bucketSize }: Props) {
|
|||
const theme = useContext(ThemeContext);
|
||||
const chartTheme = useChartTheme();
|
||||
const history = useHistory();
|
||||
const { forceUpdate, hasData } = useHasData();
|
||||
const { forceUpdate, hasDataMap } = useHasData();
|
||||
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
|
@ -58,7 +58,7 @@ export function UptimeSection({ bucketSize }: Props) {
|
|||
[bucketSize, relativeStart, relativeEnd, forceUpdate]
|
||||
);
|
||||
|
||||
if (!hasData.synthetics?.hasData) {
|
||||
if (!hasDataMap.synthetics?.hasData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,10 +28,11 @@ jest.mock('react-router-dom', () => ({
|
|||
describe('UXSection', () => {
|
||||
beforeAll(() => {
|
||||
jest.spyOn(hasDataHook, 'useHasData').mockReturnValue({
|
||||
hasData: {
|
||||
hasDataMap: {
|
||||
ux: {
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
hasData: { hasData: true, serviceName: 'elastic-co-frontend' },
|
||||
hasData: true,
|
||||
serviceName: 'elastic-co-frontend',
|
||||
},
|
||||
},
|
||||
} as HasDataContextValue);
|
||||
|
|
|
@ -12,7 +12,6 @@ import { getDataHandler } from '../../../../data_handler';
|
|||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { useHasData } from '../../../../hooks/use_has_data';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { UXHasDataResponse } from '../../../../typings';
|
||||
import CoreVitals from '../../../shared/core_web_vitals';
|
||||
|
||||
interface Props {
|
||||
|
@ -20,10 +19,10 @@ interface Props {
|
|||
}
|
||||
|
||||
export function UXSection({ bucketSize }: Props) {
|
||||
const { forceUpdate, hasData } = useHasData();
|
||||
const { forceUpdate, hasDataMap } = useHasData();
|
||||
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange();
|
||||
const uxHasDataResponse = (hasData.ux?.hasData as UXHasDataResponse) || {};
|
||||
const serviceName = uxHasDataResponse.serviceName as string;
|
||||
const uxHasDataResponse = hasDataMap.ux;
|
||||
const serviceName = uxHasDataResponse?.serviceName as string;
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
() => {
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
*/
|
||||
|
||||
import { FieldFormat } from '../../types';
|
||||
import { TRANSACTION_DURATION } from '../constants/elasticsearch_fieldnames';
|
||||
import {
|
||||
METRIC_SYSTEM_CPU_USAGE,
|
||||
METRIC_SYSTEM_MEMORY_USAGE,
|
||||
TRANSACTION_DURATION,
|
||||
} from '../constants/elasticsearch_fieldnames';
|
||||
|
||||
export const apmFieldFormats: FieldFormat[] = [
|
||||
{
|
||||
|
@ -18,7 +22,16 @@ export const apmFieldFormats: FieldFormat[] = [
|
|||
outputFormat: 'asMilliseconds',
|
||||
outputPrecision: 0,
|
||||
showSuffix: true,
|
||||
useShortSuffix: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: METRIC_SYSTEM_MEMORY_USAGE,
|
||||
format: { id: 'bytes', params: {} },
|
||||
},
|
||||
{
|
||||
field: METRIC_SYSTEM_CPU_USAGE,
|
||||
format: { id: 'percent', params: {} },
|
||||
},
|
||||
];
|
||||
|
|
|
@ -13,12 +13,13 @@ import {
|
|||
BROWSER_VERSION_LABEL,
|
||||
CLS_LABEL,
|
||||
CORE_WEB_VITALS_LABEL,
|
||||
DEVICE_DISTRIBUTION_LABEL,
|
||||
DEVICE_LABEL,
|
||||
ENVIRONMENT_LABEL,
|
||||
FCP_LABEL,
|
||||
FID_LABEL,
|
||||
HOST_NAME_LABEL,
|
||||
KIP_OVER_TIME_LABEL,
|
||||
KPI_OVER_TIME_LABEL,
|
||||
KPI_LABEL,
|
||||
LCP_LABEL,
|
||||
LOCATION_LABEL,
|
||||
|
@ -31,6 +32,7 @@ import {
|
|||
OS_LABEL,
|
||||
PERF_DIST_LABEL,
|
||||
PORT_LABEL,
|
||||
REQUEST_METHOD,
|
||||
SERVICE_NAME_LABEL,
|
||||
TAGS_LABEL,
|
||||
TBT_LABEL,
|
||||
|
@ -72,14 +74,17 @@ export const FieldLabels: Record<string, string> = {
|
|||
|
||||
'performance.metric': METRIC_LABEL,
|
||||
'Business.KPI': KPI_LABEL,
|
||||
'http.request.method': REQUEST_METHOD,
|
||||
};
|
||||
|
||||
export const DataViewLabels: Record<ReportViewTypeId, string> = {
|
||||
dist: PERF_DIST_LABEL,
|
||||
kpi: KIP_OVER_TIME_LABEL,
|
||||
kpi: KPI_OVER_TIME_LABEL,
|
||||
cwv: CORE_WEB_VITALS_LABEL,
|
||||
mdd: DEVICE_DISTRIBUTION_LABEL,
|
||||
};
|
||||
|
||||
export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN';
|
||||
export const FILTER_RECORDS = 'FILTER_RECORDS';
|
||||
export const TERMS_COLUMN = 'TERMS_COLUMN';
|
||||
export const OPERATION_COLUMN = 'operation';
|
||||
|
|
|
@ -86,6 +86,8 @@ export const ERROR_PAGE_URL = 'error.page.url';
|
|||
|
||||
// METRICS
|
||||
export const METRIC_SYSTEM_FREE_MEMORY = 'system.memory.actual.free';
|
||||
export const METRIC_SYSTEM_MEMORY_USAGE = 'system.memory.usage';
|
||||
export const METRIC_SYSTEM_CPU_USAGE = 'system.cpu.usage';
|
||||
export const METRIC_SYSTEM_TOTAL_MEMORY = 'system.memory.total';
|
||||
export const METRIC_SYSTEM_CPU_PERCENT = 'system.cpu.total.norm.pct';
|
||||
export const METRIC_PROCESS_CPU_PERCENT = 'system.process.cpu.total.norm.pct';
|
||||
|
|
|
@ -165,7 +165,7 @@ export const KPI_LABEL = i18n.translate('xpack.observability.expView.fieldLabels
|
|||
export const PERF_DIST_LABEL = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.performanceDistribution',
|
||||
{
|
||||
defaultMessage: 'Performance Distribution',
|
||||
defaultMessage: 'Performance distribution',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -176,6 +176,20 @@ export const CORE_WEB_VITALS_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const DEVICE_DISTRIBUTION_LABEL = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.deviceDistribution',
|
||||
{
|
||||
defaultMessage: 'Device distribution',
|
||||
}
|
||||
);
|
||||
|
||||
export const MOBILE_RESPONSE_LABEL = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.mobileResponse',
|
||||
{
|
||||
defaultMessage: 'Mobile response',
|
||||
}
|
||||
);
|
||||
|
||||
export const MEMORY_USAGE_LABEL = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.memoryUsage',
|
||||
{
|
||||
|
@ -183,7 +197,7 @@ export const MEMORY_USAGE_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const KIP_OVER_TIME_LABEL = i18n.translate(
|
||||
export const KPI_OVER_TIME_LABEL = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.kpiOverTime',
|
||||
{
|
||||
defaultMessage: 'KPI over time',
|
||||
|
@ -211,3 +225,82 @@ export const UP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.
|
|||
export const DOWN_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.downPings', {
|
||||
defaultMessage: 'Down Pings',
|
||||
});
|
||||
|
||||
export const CARRIER_NAME = i18n.translate('xpack.observability.expView.fieldLabels.carrierName', {
|
||||
defaultMessage: 'Carrier Name',
|
||||
});
|
||||
|
||||
export const REQUEST_METHOD = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.requestMethod',
|
||||
{
|
||||
defaultMessage: 'Request Method',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONNECTION_TYPE = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.connectionType',
|
||||
{
|
||||
defaultMessage: 'Connection Type',
|
||||
}
|
||||
);
|
||||
export const HOST_OS = i18n.translate('xpack.observability.expView.fieldLabels.hostOS', {
|
||||
defaultMessage: 'Host OS',
|
||||
});
|
||||
|
||||
export const SERVICE_VERSION = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.serviceVersion',
|
||||
{
|
||||
defaultMessage: 'Service Version',
|
||||
}
|
||||
);
|
||||
|
||||
export const OS_PLATFORM = i18n.translate('xpack.observability.expView.fieldLabels.osPlatform', {
|
||||
defaultMessage: 'OS Platform',
|
||||
});
|
||||
|
||||
export const DEVICE_MODEL = i18n.translate('xpack.observability.expView.fieldLabels.deviceModel', {
|
||||
defaultMessage: 'Device Model',
|
||||
});
|
||||
|
||||
export const CARRIER_LOCATION = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.carrierLocation',
|
||||
{
|
||||
defaultMessage: 'Carrier Location',
|
||||
}
|
||||
);
|
||||
|
||||
export const RESPONSE_LATENCY = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.responseLatency',
|
||||
{
|
||||
defaultMessage: 'Response latency',
|
||||
}
|
||||
);
|
||||
|
||||
export const MOBILE_APP = i18n.translate('xpack.observability.expView.fieldLabels.mobileApp', {
|
||||
defaultMessage: 'Mobile App',
|
||||
});
|
||||
|
||||
export const MEMORY_USAGE = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.mobile.memoryUsage',
|
||||
{
|
||||
defaultMessage: 'Memory Usage',
|
||||
}
|
||||
);
|
||||
|
||||
export const CPU_USAGE = i18n.translate('xpack.observability.expView.fieldLabels.cpuUsage', {
|
||||
defaultMessage: 'CPU Usage',
|
||||
});
|
||||
|
||||
export const TRANSACTIONS_PER_MINUTE = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.transactionPerMinute',
|
||||
{
|
||||
defaultMessage: 'Transactions per minute',
|
||||
}
|
||||
);
|
||||
|
||||
export const NUMBER_OF_DEVICES = i18n.translate(
|
||||
'xpack.observability.expView.fieldLabels.numberOfDevices',
|
||||
{
|
||||
defaultMessage: 'Number of Devices',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -12,6 +12,9 @@ import { getSyntheticsKPIConfig } from './synthetics/kpi_over_time_config';
|
|||
import { getKPITrendsLensConfig } from './rum/kpi_over_time_config';
|
||||
import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config';
|
||||
import { getMobileKPIConfig } from './mobile/kpi_over_time_config';
|
||||
import { getMobileKPIDistributionConfig } from './mobile/distribution_config';
|
||||
import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config';
|
||||
|
||||
interface Props {
|
||||
reportType: keyof typeof ReportViewTypes;
|
||||
|
@ -34,7 +37,14 @@ export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props)
|
|||
return getSyntheticsDistributionConfig({ indexPattern });
|
||||
}
|
||||
return getSyntheticsKPIConfig({ indexPattern });
|
||||
|
||||
case 'mobile':
|
||||
if (reportType === 'dist') {
|
||||
return getMobileKPIDistributionConfig({ indexPattern });
|
||||
}
|
||||
if (reportType === 'mdd') {
|
||||
return getMobileDeviceDistributionConfig({ indexPattern });
|
||||
}
|
||||
return getMobileKPIConfig({ indexPattern });
|
||||
default:
|
||||
return getKPITrendsLensConfig({ indexPattern });
|
||||
}
|
||||
|
|
|
@ -25,13 +25,14 @@ import {
|
|||
FieldBasedIndexPatternColumn,
|
||||
SumIndexPatternColumn,
|
||||
TermsIndexPatternColumn,
|
||||
CardinalityIndexPatternColumn,
|
||||
} from '../../../../../../lens/public';
|
||||
import {
|
||||
buildPhraseFilter,
|
||||
buildPhrasesFilter,
|
||||
IndexPattern,
|
||||
} from '../../../../../../../../src/plugins/data/common';
|
||||
import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN } from './constants';
|
||||
import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN } from './constants';
|
||||
import { ColumnFilter, DataSeries, UrlFilter, URLReportDefinition } from '../types';
|
||||
|
||||
function getLayerReferenceName(layerId: string) {
|
||||
|
@ -55,6 +56,7 @@ export const parseCustomFieldName = (
|
|||
let fieldName = sourceField;
|
||||
let columnType;
|
||||
let columnFilters;
|
||||
let timeScale;
|
||||
let columnLabel;
|
||||
|
||||
const rdf = reportViewConfig.reportDefinitions ?? [];
|
||||
|
@ -70,17 +72,19 @@ export const parseCustomFieldName = (
|
|||
);
|
||||
columnType = currField?.columnType;
|
||||
columnFilters = currField?.columnFilters;
|
||||
timeScale = currField?.timeScale;
|
||||
columnLabel = currField?.label;
|
||||
}
|
||||
} else if (customField.options?.[0].field || customField.options?.[0].id) {
|
||||
fieldName = customField.options?.[0].field || customField.options?.[0].id;
|
||||
columnType = customField.options?.[0].columnType;
|
||||
columnFilters = customField.options?.[0].columnFilters;
|
||||
timeScale = customField.options?.[0].timeScale;
|
||||
columnLabel = customField.options?.[0].label;
|
||||
}
|
||||
}
|
||||
|
||||
return { fieldName, columnType, columnFilters, columnLabel };
|
||||
return { fieldName, columnType, columnFilters, timeScale, columnLabel };
|
||||
};
|
||||
|
||||
export class LensAttributes {
|
||||
|
@ -167,10 +171,10 @@ export class LensAttributes {
|
|||
this.visualization.layers[0].splitAccessor = undefined;
|
||||
}
|
||||
|
||||
getNumberRangeColumn(sourceField: string): RangeIndexPatternColumn {
|
||||
getNumberRangeColumn(sourceField: string, label?: string): RangeIndexPatternColumn {
|
||||
return {
|
||||
sourceField,
|
||||
label: this.reportViewConfig.labels[sourceField],
|
||||
label: this.reportViewConfig.labels[sourceField] ?? label,
|
||||
dataType: 'number',
|
||||
operationType: 'range',
|
||||
isBucketed: true,
|
||||
|
@ -183,6 +187,10 @@ export class LensAttributes {
|
|||
};
|
||||
}
|
||||
|
||||
getCardinalityColumn(sourceField: string, label?: string) {
|
||||
return this.getNumberOperationColumn(sourceField, 'unique_count', label);
|
||||
}
|
||||
|
||||
getNumberColumn(
|
||||
sourceField: string,
|
||||
columnType?: string,
|
||||
|
@ -190,21 +198,30 @@ export class LensAttributes {
|
|||
label?: string
|
||||
) {
|
||||
if (columnType === 'operation' || operationType) {
|
||||
if (operationType === 'median' || operationType === 'average' || operationType === 'sum') {
|
||||
if (
|
||||
operationType === 'median' ||
|
||||
operationType === 'average' ||
|
||||
operationType === 'sum' ||
|
||||
operationType === 'unique_count'
|
||||
) {
|
||||
return this.getNumberOperationColumn(sourceField, operationType, label);
|
||||
}
|
||||
if (operationType?.includes('th')) {
|
||||
return this.getPercentileNumberColumn(sourceField, operationType);
|
||||
}
|
||||
}
|
||||
return this.getNumberRangeColumn(sourceField);
|
||||
return this.getNumberRangeColumn(sourceField, label);
|
||||
}
|
||||
|
||||
getNumberOperationColumn(
|
||||
sourceField: string,
|
||||
operationType: 'average' | 'median' | 'sum',
|
||||
operationType: 'average' | 'median' | 'sum' | 'unique_count',
|
||||
label?: string
|
||||
): AvgIndexPatternColumn | MedianIndexPatternColumn | SumIndexPatternColumn {
|
||||
):
|
||||
| AvgIndexPatternColumn
|
||||
| MedianIndexPatternColumn
|
||||
| SumIndexPatternColumn
|
||||
| CardinalityIndexPatternColumn {
|
||||
return {
|
||||
...buildNumberColumn(sourceField),
|
||||
label:
|
||||
|
@ -247,6 +264,25 @@ export class LensAttributes {
|
|||
};
|
||||
}
|
||||
|
||||
getTermsColumn(sourceField: string, label?: string): TermsIndexPatternColumn {
|
||||
return {
|
||||
operationType: 'terms',
|
||||
sourceField,
|
||||
label: label || 'Top values of ' + sourceField,
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
scale: 'ordinal',
|
||||
params: {
|
||||
size: 10,
|
||||
orderBy: {
|
||||
type: 'alphabetical',
|
||||
fallback: false,
|
||||
},
|
||||
orderDirection: 'desc',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getXAxis() {
|
||||
const { xAxisColumn } = this.reportViewConfig;
|
||||
|
||||
|
@ -263,15 +299,25 @@ export class LensAttributes {
|
|||
label?: string,
|
||||
colIndex?: number
|
||||
) {
|
||||
const { fieldMeta, columnType, fieldName, columnFilters, columnLabel } = this.getFieldMeta(
|
||||
sourceField
|
||||
);
|
||||
const {
|
||||
fieldMeta,
|
||||
columnType,
|
||||
fieldName,
|
||||
columnFilters,
|
||||
timeScale,
|
||||
columnLabel,
|
||||
} = this.getFieldMeta(sourceField);
|
||||
const { type: fieldType } = fieldMeta ?? {};
|
||||
|
||||
if (columnType === TERMS_COLUMN) {
|
||||
return this.getTermsColumn(fieldName, columnLabel || label);
|
||||
}
|
||||
|
||||
if (fieldName === 'Records' || columnType === FILTER_RECORDS) {
|
||||
return this.getRecordsColumn(
|
||||
columnLabel || label,
|
||||
colIndex !== undefined ? columnFilters?.[colIndex] : undefined
|
||||
colIndex !== undefined ? columnFilters?.[colIndex] : undefined,
|
||||
timeScale
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -281,6 +327,9 @@ export class LensAttributes {
|
|||
if (fieldType === 'number') {
|
||||
return this.getNumberColumn(fieldName, columnType, operationType, columnLabel || label);
|
||||
}
|
||||
if (operationType === 'unique_count') {
|
||||
return this.getCardinalityColumn(fieldName, columnLabel || label);
|
||||
}
|
||||
|
||||
// FIXME review my approach again
|
||||
return this.getDateHistogramColumn(fieldName);
|
||||
|
@ -291,13 +340,17 @@ export class LensAttributes {
|
|||
}
|
||||
|
||||
getFieldMeta(sourceField: string) {
|
||||
const { fieldName, columnType, columnFilters, columnLabel } = this.getCustomFieldName(
|
||||
sourceField
|
||||
);
|
||||
const {
|
||||
fieldName,
|
||||
columnType,
|
||||
columnFilters,
|
||||
timeScale,
|
||||
columnLabel,
|
||||
} = this.getCustomFieldName(sourceField);
|
||||
|
||||
const fieldMeta = this.indexPattern.getFieldByName(fieldName);
|
||||
|
||||
return { fieldMeta, fieldName, columnType, columnFilters, columnLabel };
|
||||
return { fieldMeta, fieldName, columnType, columnFilters, timeScale, columnLabel };
|
||||
}
|
||||
|
||||
getMainYAxis() {
|
||||
|
@ -330,7 +383,11 @@ export class LensAttributes {
|
|||
return lensColumns;
|
||||
}
|
||||
|
||||
getRecordsColumn(label?: string, columnFilter?: ColumnFilter): CountIndexPatternColumn {
|
||||
getRecordsColumn(
|
||||
label?: string,
|
||||
columnFilter?: ColumnFilter,
|
||||
timeScale?: string
|
||||
): CountIndexPatternColumn {
|
||||
return {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
|
@ -339,6 +396,7 @@ export class LensAttributes {
|
|||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
filter: columnFilter,
|
||||
timeScale,
|
||||
} as CountIndexPatternColumn;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConfigProps, DataSeries } from '../../types';
|
||||
import { FieldLabels, USE_BREAK_DOWN_COLUMN } from '../constants';
|
||||
import { buildPhraseFilter } from '../utils';
|
||||
import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames';
|
||||
import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels';
|
||||
import { MobileFields } from './mobile_fields';
|
||||
|
||||
export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): DataSeries {
|
||||
return {
|
||||
reportType: 'mobile-device-distribution',
|
||||
defaultSeriesType: 'bar',
|
||||
seriesTypes: ['bar', 'bar_horizontal'],
|
||||
xAxisColumn: {
|
||||
sourceField: USE_BREAK_DOWN_COLUMN,
|
||||
},
|
||||
yAxisColumns: [
|
||||
{
|
||||
sourceField: 'labels.device_id',
|
||||
operationType: 'unique_count',
|
||||
label: NUMBER_OF_DEVICES,
|
||||
},
|
||||
],
|
||||
hasOperationType: false,
|
||||
defaultFilters: Object.keys(MobileFields),
|
||||
breakdowns: Object.keys(MobileFields),
|
||||
filters: [
|
||||
...buildPhraseFilter('agent.name', 'iOS/swift', indexPattern),
|
||||
...buildPhraseFilter('processor.event', 'transaction', indexPattern),
|
||||
],
|
||||
labels: {
|
||||
...FieldLabels,
|
||||
...MobileFields,
|
||||
[SERVICE_NAME]: MOBILE_APP,
|
||||
},
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: SERVICE_NAME,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConfigProps, DataSeries } from '../../types';
|
||||
import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD } from '../constants';
|
||||
import { buildPhrasesFilter } from '../utils';
|
||||
import {
|
||||
METRIC_SYSTEM_CPU_USAGE,
|
||||
METRIC_SYSTEM_MEMORY_USAGE,
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_DURATION,
|
||||
} from '../constants/elasticsearch_fieldnames';
|
||||
|
||||
import { CPU_USAGE, MEMORY_USAGE, MOBILE_APP, RESPONSE_LATENCY } from '../constants/labels';
|
||||
import { MobileFields } from './mobile_fields';
|
||||
|
||||
export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): DataSeries {
|
||||
return {
|
||||
reportType: 'data-distribution',
|
||||
defaultSeriesType: 'bar',
|
||||
seriesTypes: ['line', 'bar'],
|
||||
xAxisColumn: {
|
||||
sourceField: 'performance.metric',
|
||||
},
|
||||
yAxisColumns: [
|
||||
{
|
||||
sourceField: RECORDS_FIELD,
|
||||
},
|
||||
],
|
||||
hasOperationType: false,
|
||||
defaultFilters: Object.keys(MobileFields),
|
||||
breakdowns: Object.keys(MobileFields),
|
||||
filters: [
|
||||
...buildPhrasesFilter('agent.name', ['iOS/swift', 'open-telemetry/swift'], indexPattern),
|
||||
],
|
||||
labels: {
|
||||
...FieldLabels,
|
||||
...MobileFields,
|
||||
[SERVICE_NAME]: MOBILE_APP,
|
||||
},
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: SERVICE_NAME,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: 'performance.metric',
|
||||
custom: true,
|
||||
options: [
|
||||
{
|
||||
label: RESPONSE_LATENCY,
|
||||
field: TRANSACTION_DURATION,
|
||||
id: TRANSACTION_DURATION,
|
||||
columnType: OPERATION_COLUMN,
|
||||
},
|
||||
{
|
||||
label: MEMORY_USAGE,
|
||||
field: METRIC_SYSTEM_MEMORY_USAGE,
|
||||
id: METRIC_SYSTEM_MEMORY_USAGE,
|
||||
columnType: OPERATION_COLUMN,
|
||||
},
|
||||
{
|
||||
label: CPU_USAGE,
|
||||
field: METRIC_SYSTEM_CPU_USAGE,
|
||||
id: METRIC_SYSTEM_CPU_USAGE,
|
||||
columnType: OPERATION_COLUMN,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConfigProps, DataSeries } from '../../types';
|
||||
import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD } from '../constants';
|
||||
import { buildPhrasesFilter } from '../utils';
|
||||
import {
|
||||
METRIC_SYSTEM_CPU_USAGE,
|
||||
METRIC_SYSTEM_MEMORY_USAGE,
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_DURATION,
|
||||
} from '../constants/elasticsearch_fieldnames';
|
||||
import {
|
||||
CPU_USAGE,
|
||||
MEMORY_USAGE,
|
||||
MOBILE_APP,
|
||||
RESPONSE_LATENCY,
|
||||
TRANSACTIONS_PER_MINUTE,
|
||||
} from '../constants/labels';
|
||||
import { MobileFields } from './mobile_fields';
|
||||
|
||||
export function getMobileKPIConfig({ indexPattern }: ConfigProps): DataSeries {
|
||||
return {
|
||||
reportType: 'kpi-over-time',
|
||||
defaultSeriesType: 'line',
|
||||
seriesTypes: ['line', 'bar', 'area'],
|
||||
xAxisColumn: {
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
yAxisColumns: [
|
||||
{
|
||||
sourceField: 'business.kpi',
|
||||
operationType: 'median',
|
||||
},
|
||||
],
|
||||
hasOperationType: true,
|
||||
defaultFilters: Object.keys(MobileFields),
|
||||
breakdowns: Object.keys(MobileFields),
|
||||
filters: [
|
||||
...buildPhrasesFilter('agent.name', ['iOS/swift', 'open-telemetry/swift'], indexPattern),
|
||||
],
|
||||
labels: {
|
||||
...FieldLabels,
|
||||
...MobileFields,
|
||||
[TRANSACTION_DURATION]: RESPONSE_LATENCY,
|
||||
[SERVICE_NAME]: MOBILE_APP,
|
||||
[METRIC_SYSTEM_MEMORY_USAGE]: MEMORY_USAGE,
|
||||
[METRIC_SYSTEM_CPU_USAGE]: CPU_USAGE,
|
||||
},
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: SERVICE_NAME,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: 'business.kpi',
|
||||
custom: true,
|
||||
options: [
|
||||
{
|
||||
label: RESPONSE_LATENCY,
|
||||
field: TRANSACTION_DURATION,
|
||||
id: TRANSACTION_DURATION,
|
||||
columnType: OPERATION_COLUMN,
|
||||
},
|
||||
{
|
||||
label: MEMORY_USAGE,
|
||||
field: METRIC_SYSTEM_MEMORY_USAGE,
|
||||
id: METRIC_SYSTEM_MEMORY_USAGE,
|
||||
columnType: OPERATION_COLUMN,
|
||||
},
|
||||
{
|
||||
label: CPU_USAGE,
|
||||
field: METRIC_SYSTEM_CPU_USAGE,
|
||||
id: METRIC_SYSTEM_CPU_USAGE,
|
||||
columnType: OPERATION_COLUMN,
|
||||
},
|
||||
{
|
||||
field: RECORDS_FIELD,
|
||||
id: RECORDS_FIELD,
|
||||
label: TRANSACTIONS_PER_MINUTE,
|
||||
columnFilters: [
|
||||
{
|
||||
language: 'kuery',
|
||||
query: `processor.event: transaction`,
|
||||
},
|
||||
],
|
||||
timeScale: 'm',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
CARRIER_LOCATION,
|
||||
CARRIER_NAME,
|
||||
CONNECTION_TYPE,
|
||||
DEVICE_MODEL,
|
||||
HOST_OS,
|
||||
OS_PLATFORM,
|
||||
SERVICE_VERSION,
|
||||
} from '../constants/labels';
|
||||
|
||||
export const MobileFields: Record<string, string> = {
|
||||
'host.os.platform': OS_PLATFORM,
|
||||
'host.os.full': HOST_OS,
|
||||
'service.version': SERVICE_VERSION,
|
||||
'network.carrier.icc': CARRIER_LOCATION,
|
||||
'network.carrier.name': CARRIER_NAME,
|
||||
'network.connection_type': CONNECTION_TYPE,
|
||||
'labels.device_model': DEVICE_MODEL,
|
||||
};
|
|
@ -63,7 +63,7 @@ describe('ExploratoryView', () => {
|
|||
render(<ExploratoryView />, { initSeries });
|
||||
|
||||
expect(await screen.findByText(/open in lens/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText('Performance Distribution')).toBeInTheDocument();
|
||||
expect((await screen.findAllByText('Performance distribution'))[0]).toBeInTheDocument();
|
||||
expect(await screen.findByText(/Lens Embeddable Component/i)).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
|
@ -12,7 +12,6 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ
|
|||
import { ObservabilityPublicPluginsStart } from '../../../../plugin';
|
||||
import { ObservabilityIndexPatterns } from '../utils/observability_index_patterns';
|
||||
import { getDataHandler } from '../../../../data_handler';
|
||||
import { HasDataResponse } from '../../../../typings/fetch_overview_data';
|
||||
|
||||
export interface IIndexPatternContext {
|
||||
loading: boolean;
|
||||
|
@ -41,17 +40,13 @@ export function IndexPatternContextProvider({ children }: ProviderProps) {
|
|||
synthetics: null,
|
||||
ux: null,
|
||||
apm: null,
|
||||
mobile: null,
|
||||
} as HasAppDataState);
|
||||
|
||||
const {
|
||||
services: { data },
|
||||
} = useKibana<ObservabilityPublicPluginsStart>();
|
||||
|
||||
const checkIfAppHasData = async (dataType: AppDataType) => {
|
||||
const handler = getDataHandler(dataType);
|
||||
return handler?.hasData();
|
||||
};
|
||||
|
||||
const loadIndexPattern: IIndexPatternContext['loadIndexPattern'] = useCallback(
|
||||
async ({ dataType }) => {
|
||||
setSelectedApp(dataType);
|
||||
|
@ -59,15 +54,27 @@ export function IndexPatternContextProvider({ children }: ProviderProps) {
|
|||
if (hasAppData[dataType] === null) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const hasDataResponse = (await checkIfAppHasData(dataType)) as HasDataResponse;
|
||||
|
||||
const hasDataT = hasDataResponse.hasData;
|
||||
|
||||
let hasDataT = false;
|
||||
let indices: string | undefined = '';
|
||||
switch (dataType) {
|
||||
case 'ux':
|
||||
case 'synthetics':
|
||||
const resultUx = await getDataHandler(dataType)?.hasData();
|
||||
hasDataT = Boolean(resultUx?.hasData);
|
||||
indices = resultUx?.indices;
|
||||
break;
|
||||
case 'apm':
|
||||
case 'mobile':
|
||||
const resultApm = await getDataHandler('apm')?.hasData();
|
||||
hasDataT = Boolean(resultApm?.hasData);
|
||||
indices = resultApm?.indices['apm_oss.transactionIndices'];
|
||||
break;
|
||||
}
|
||||
setHasAppData((prevState) => ({ ...prevState, [dataType]: hasDataT }));
|
||||
|
||||
if (hasDataT || hasAppData?.[dataType]) {
|
||||
if (hasDataT && indices) {
|
||||
const obsvIndexP = new ObservabilityIndexPatterns(data);
|
||||
const indPattern = await obsvIndexP.getIndexPattern(dataType, hasDataResponse.indices);
|
||||
const indPattern = await obsvIndexP.getIndexPattern(dataType, indices);
|
||||
|
||||
setIndexPatterns((prevState) => ({ ...prevState, [dataType]: indPattern }));
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { useSeriesStorage } from '../../hooks/use_series_storage';
|
|||
export const dataTypes: Array<{ id: AppDataType; label: string }> = [
|
||||
{ id: 'synthetics', label: 'Synthetic Monitoring' },
|
||||
{ id: 'ux', label: 'User Experience (RUM)' },
|
||||
{ id: 'mobile', label: 'Mobile Experience' },
|
||||
// { id: 'infra_logs', label: 'Logs' },
|
||||
// { id: 'infra_metrics', label: 'Metrics' },
|
||||
// { id: 'apm', label: 'APM' },
|
||||
|
|
|
@ -20,8 +20,10 @@ export function ReportFilters({
|
|||
<SeriesFilter
|
||||
series={dataViewSeries}
|
||||
defaultFilters={dataViewSeries.defaultFilters}
|
||||
filters={dataViewSeries.filters}
|
||||
seriesId={seriesId}
|
||||
isNew={true}
|
||||
labels={dataViewSeries.labels}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,16 +18,27 @@ import { ReportBreakdowns } from './columns/report_breakdowns';
|
|||
import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage';
|
||||
import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern';
|
||||
import { getDefaultConfigs } from '../configurations/default_configs';
|
||||
import {
|
||||
CORE_WEB_VITALS_LABEL,
|
||||
DEVICE_DISTRIBUTION_LABEL,
|
||||
KPI_OVER_TIME_LABEL,
|
||||
PERF_DIST_LABEL,
|
||||
} from '../configurations/constants/labels';
|
||||
|
||||
export const ReportTypes: Record<AppDataType, Array<{ id: ReportViewTypeId; label: string }>> = {
|
||||
synthetics: [
|
||||
{ id: 'kpi', label: 'KPI over time' },
|
||||
{ id: 'dist', label: 'Performance distribution' },
|
||||
{ id: 'kpi', label: KPI_OVER_TIME_LABEL },
|
||||
{ id: 'dist', label: PERF_DIST_LABEL },
|
||||
],
|
||||
ux: [
|
||||
{ id: 'kpi', label: 'KPI over time' },
|
||||
{ id: 'dist', label: 'Performance distribution' },
|
||||
{ id: 'cwv', label: 'Core Web Vitals' },
|
||||
{ id: 'kpi', label: KPI_OVER_TIME_LABEL },
|
||||
{ id: 'dist', label: PERF_DIST_LABEL },
|
||||
{ id: 'cwv', label: CORE_WEB_VITALS_LABEL },
|
||||
],
|
||||
mobile: [
|
||||
{ id: 'kpi', label: KPI_OVER_TIME_LABEL },
|
||||
{ id: 'dist', label: PERF_DIST_LABEL },
|
||||
{ id: 'mdd', label: DEVICE_DISTRIBUTION_LABEL },
|
||||
],
|
||||
apm: [],
|
||||
infra_logs: [],
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('FilterExpanded', function () {
|
|||
label={'Browser Family'}
|
||||
field={USER_AGENT_NAME}
|
||||
goBack={jest.fn()}
|
||||
filters={[]}
|
||||
/>,
|
||||
{ initSeries }
|
||||
);
|
||||
|
@ -38,6 +39,7 @@ describe('FilterExpanded', function () {
|
|||
label={'Browser Family'}
|
||||
field={USER_AGENT_NAME}
|
||||
goBack={goBack}
|
||||
filters={[]}
|
||||
/>,
|
||||
{ initSeries }
|
||||
);
|
||||
|
@ -64,6 +66,7 @@ describe('FilterExpanded', function () {
|
|||
label={'Browser Family'}
|
||||
field={USER_AGENT_NAME}
|
||||
goBack={goBack}
|
||||
filters={[]}
|
||||
/>,
|
||||
{ initSeries }
|
||||
);
|
||||
|
@ -90,6 +93,7 @@ describe('FilterExpanded', function () {
|
|||
label={'Browser Family'}
|
||||
field={USER_AGENT_NAME}
|
||||
goBack={jest.fn()}
|
||||
filters={[]}
|
||||
/>,
|
||||
{ initSeries }
|
||||
);
|
||||
|
|
|
@ -6,17 +6,21 @@
|
|||
*/
|
||||
|
||||
import React, { useState, Fragment } from 'react';
|
||||
import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup } from '@elastic/eui';
|
||||
import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup, EuiText } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types';
|
||||
import { map } from 'lodash';
|
||||
import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern';
|
||||
import { useSeriesStorage } from '../../hooks/use_series_storage';
|
||||
import { UrlFilter } from '../../types';
|
||||
import { DataSeries, UrlFilter } from '../../types';
|
||||
import { FilterValueButton } from './filter_value_btn';
|
||||
import { useValuesList } from '../../../../../hooks/use_values_list';
|
||||
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
|
||||
import { ESFilter } from '../../../../../../../../../typings/elasticsearch';
|
||||
import { PersistableFilter } from '../../../../../../../lens/common';
|
||||
import { ExistsFilter } from '../../../../../../../../../src/plugins/data/common/es_query/filters';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
|
@ -25,9 +29,18 @@ interface Props {
|
|||
isNegated?: boolean;
|
||||
goBack: () => void;
|
||||
nestedField?: string;
|
||||
filters: DataSeries['filters'];
|
||||
}
|
||||
|
||||
export function FilterExpanded({ seriesId, field, label, goBack, nestedField, isNegated }: Props) {
|
||||
export function FilterExpanded({
|
||||
seriesId,
|
||||
field,
|
||||
label,
|
||||
goBack,
|
||||
nestedField,
|
||||
isNegated,
|
||||
filters: defaultFilters,
|
||||
}: Props) {
|
||||
const { indexPattern } = useAppIndexPatternContext();
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
|
@ -38,12 +51,25 @@ export function FilterExpanded({ seriesId, field, label, goBack, nestedField, is
|
|||
|
||||
const series = getSeries(seriesId);
|
||||
|
||||
const queryFilters: ESFilter[] = [];
|
||||
|
||||
defaultFilters?.forEach((qFilter: PersistableFilter | ExistsFilter) => {
|
||||
if (qFilter.query) {
|
||||
queryFilters.push(qFilter.query);
|
||||
}
|
||||
const asExistFilter = qFilter as ExistsFilter;
|
||||
if (asExistFilter?.exists) {
|
||||
queryFilters.push(asExistFilter.exists as QueryDslQueryContainer);
|
||||
}
|
||||
});
|
||||
|
||||
const { values, loading } = useValuesList({
|
||||
query: value,
|
||||
indexPatternTitle: indexPattern?.title,
|
||||
sourceField: field,
|
||||
time: series.time,
|
||||
keepHistory: true,
|
||||
filters: queryFilters,
|
||||
});
|
||||
|
||||
const filters = series?.filters ?? [];
|
||||
|
@ -73,6 +99,13 @@ export function FilterExpanded({ seriesId, field, label, goBack, nestedField, is
|
|||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<ListWrapper>
|
||||
{displayValues.length === 0 && !loading && (
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.observability.filters.expanded.noFilter', {
|
||||
defaultMessage: 'No filters found.',
|
||||
})}
|
||||
</EuiText>
|
||||
)}
|
||||
{displayValues.map((opt) => (
|
||||
<Fragment key={opt}>
|
||||
<EuiFilterGroup fullWidth={true} color="primary">
|
||||
|
|
|
@ -24,8 +24,10 @@ import { useSeriesStorage } from '../../hooks/use_series_storage';
|
|||
interface Props {
|
||||
seriesId: string;
|
||||
defaultFilters: DataSeries['defaultFilters'];
|
||||
filters: DataSeries['filters'];
|
||||
series: DataSeries;
|
||||
isNew?: boolean;
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
|
@ -35,21 +37,28 @@ export interface Field {
|
|||
isNegated?: boolean;
|
||||
}
|
||||
|
||||
export function SeriesFilter({ series, isNew, seriesId, defaultFilters = [] }: Props) {
|
||||
export function SeriesFilter({
|
||||
series,
|
||||
isNew,
|
||||
seriesId,
|
||||
defaultFilters = [],
|
||||
filters,
|
||||
labels,
|
||||
}: Props) {
|
||||
const [isPopoverVisible, setIsPopoverVisible] = useState(false);
|
||||
|
||||
const [selectedField, setSelectedField] = useState<Field | undefined>();
|
||||
|
||||
const options: Field[] = defaultFilters.map((field) => {
|
||||
if (typeof field === 'string') {
|
||||
return { label: FieldLabels[field], field };
|
||||
return { label: labels?.[field] ?? FieldLabels[field], field };
|
||||
}
|
||||
|
||||
return {
|
||||
field: field.field,
|
||||
nested: field.nested,
|
||||
isNegated: field.isNegated,
|
||||
label: FieldLabels[field.field],
|
||||
label: labels?.[field.field] ?? FieldLabels[field.field],
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -102,6 +111,7 @@ export function SeriesFilter({ series, isNew, seriesId, defaultFilters = [] }: P
|
|||
goBack={() => {
|
||||
setSelectedField(undefined);
|
||||
}}
|
||||
filters={filters}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
|
|
|
@ -49,7 +49,12 @@ export function SeriesEditor() {
|
|||
field: 'defaultFilters',
|
||||
width: '15%',
|
||||
render: (defaultFilters: string[], { id, seriesConfig }: EditItem) => (
|
||||
<SeriesFilter defaultFilters={defaultFilters} seriesId={id} series={seriesConfig} />
|
||||
<SeriesFilter
|
||||
defaultFilters={defaultFilters}
|
||||
seriesId={id}
|
||||
series={seriesConfig}
|
||||
filters={seriesConfig.filters}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
|
@ -23,6 +23,7 @@ export const ReportViewTypes = {
|
|||
dist: 'data-distribution',
|
||||
kpi: 'kpi-over-time',
|
||||
cwv: 'core-web-vitals',
|
||||
mdd: 'mobile-device-distribution',
|
||||
} as const;
|
||||
|
||||
type ValueOf<T> = T[keyof T];
|
||||
|
@ -45,8 +46,9 @@ export interface ReportDefinition {
|
|||
field?: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
columnType?: 'range' | 'operation' | 'FILTER_RECORDS';
|
||||
columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN';
|
||||
columnFilters?: ColumnFilter[];
|
||||
timeScale?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
@ -94,15 +96,15 @@ export interface ConfigProps {
|
|||
indexPattern: IIndexPattern;
|
||||
}
|
||||
|
||||
export type AppDataType = 'synthetics' | 'ux' | 'infra_logs' | 'infra_metrics' | 'apm';
|
||||
export type AppDataType = 'synthetics' | 'ux' | 'infra_logs' | 'infra_metrics' | 'apm' | 'mobile';
|
||||
|
||||
type FormatType = 'duration' | 'number';
|
||||
type FormatType = 'duration' | 'number' | 'bytes' | 'percent';
|
||||
type InputFormat = 'microseconds' | 'milliseconds' | 'seconds';
|
||||
type OutputFormat = 'asSeconds' | 'asMilliseconds' | 'humanize' | 'humanizePrecise';
|
||||
|
||||
export interface FieldFormatParams {
|
||||
inputFormat: InputFormat;
|
||||
outputFormat: OutputFormat;
|
||||
inputFormat?: InputFormat;
|
||||
outputFormat?: OutputFormat;
|
||||
outputPrecision?: number;
|
||||
showSuffix?: boolean;
|
||||
useShortSuffix?: boolean;
|
||||
|
|
|
@ -23,6 +23,7 @@ const appFieldFormats: Record<AppDataType, FieldFormat[] | null> = {
|
|||
ux: rumFieldFormats,
|
||||
apm: apmFieldFormats,
|
||||
synthetics: syntheticsFieldFormats,
|
||||
mobile: apmFieldFormats,
|
||||
};
|
||||
|
||||
function getFieldFormatsForApp(app: AppDataType) {
|
||||
|
@ -35,6 +36,7 @@ export const indexPatternList: Record<AppDataType, string> = {
|
|||
ux: 'rum_static_index_pattern_id',
|
||||
infra_logs: 'infra_logs_static_index_pattern_id',
|
||||
infra_metrics: 'infra_metrics_static_index_pattern_id',
|
||||
mobile: 'mobile_static_index_pattern_id',
|
||||
};
|
||||
|
||||
const appToPatternMap: Record<AppDataType, string> = {
|
||||
|
@ -43,6 +45,7 @@ const appToPatternMap: Record<AppDataType, string> = {
|
|||
ux: '(rum-data-view)*',
|
||||
infra_logs: '',
|
||||
infra_metrics: '',
|
||||
mobile: '(mobile-data-view)*',
|
||||
};
|
||||
|
||||
const getAppIndicesWithPattern = (app: AppDataType, indices: string) => {
|
||||
|
@ -124,6 +127,7 @@ export class ObservabilityIndexPatterns {
|
|||
if (!this.data) {
|
||||
throw new Error('data is not defined');
|
||||
}
|
||||
|
||||
try {
|
||||
const indexPatternId = getAppIndexPatternId(app, indices);
|
||||
const indexPatternTitle = getAppIndicesWithPattern(app, indices);
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// import { act, getByText } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import React from 'react';
|
||||
|
@ -19,10 +18,17 @@ import * as pluginContext from '../hooks/use_plugin_context';
|
|||
import { PluginContextValue } from './plugin_context';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { ApmIndicesConfig } from '../../common/typings';
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
const relativeStart = '2020-10-08T06:00:00.000Z';
|
||||
const relativeEnd = '2020-10-08T07:00:00.000Z';
|
||||
|
||||
const sampleAPMIndices = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'apm_oss.transactionIndices': 'apm-*',
|
||||
} as ApmIndicesConfig;
|
||||
|
||||
function wrapper({ children }: { children: React.ReactElement }) {
|
||||
const history = createMemoryHistory();
|
||||
return (
|
||||
|
@ -76,17 +82,18 @@ describe('HasDataContextProvider', () => {
|
|||
it('hasAnyData returns false and all apps return undefined', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||
expect(result.current).toMatchObject({
|
||||
hasData: {},
|
||||
hasDataMap: {},
|
||||
hasAnyData: false,
|
||||
isAllRequestsComplete: false,
|
||||
forceUpdate: expect.any(String),
|
||||
onRefreshTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
await act(async () => {
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
hasData: {
|
||||
hasDataMap: {
|
||||
apm: { hasData: undefined, status: 'success' },
|
||||
synthetics: { hasData: undefined, status: 'success' },
|
||||
infra_logs: { hasData: undefined, status: 'success' },
|
||||
|
@ -105,16 +112,16 @@ describe('HasDataContextProvider', () => {
|
|||
describe('all apps return false', () => {
|
||||
beforeAll(() => {
|
||||
registerApps([
|
||||
{ appName: 'apm', hasData: async () => false },
|
||||
{ appName: 'apm', hasData: async () => ({ hasData: false }) },
|
||||
{ appName: 'infra_logs', hasData: async () => false },
|
||||
{ appName: 'infra_metrics', hasData: async () => false },
|
||||
{
|
||||
appName: 'synthetics',
|
||||
hasData: async () => ({ hasData: false, indices: 'heartbeat-*, synthetics-*' }),
|
||||
hasData: async () => ({ hasData: false }),
|
||||
},
|
||||
{
|
||||
appName: 'ux',
|
||||
hasData: async () => ({ hasData: false, serviceName: undefined, indices: 'apm-*' }),
|
||||
hasData: async () => ({ hasData: false }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -124,29 +131,28 @@ describe('HasDataContextProvider', () => {
|
|||
it('hasAnyData returns false and all apps return false', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||
expect(result.current).toEqual({
|
||||
hasData: {},
|
||||
hasDataMap: {},
|
||||
hasAnyData: false,
|
||||
isAllRequestsComplete: false,
|
||||
forceUpdate: expect.any(String),
|
||||
onRefreshTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
await act(async () => {
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
hasData: {
|
||||
hasDataMap: {
|
||||
apm: { hasData: false, status: 'success' },
|
||||
synthetics: {
|
||||
hasData: {
|
||||
hasData: false,
|
||||
indices: 'heartbeat-*, synthetics-*',
|
||||
},
|
||||
hasData: false,
|
||||
status: 'success',
|
||||
},
|
||||
infra_logs: { hasData: false, status: 'success' },
|
||||
infra_metrics: { hasData: false, status: 'success' },
|
||||
ux: {
|
||||
hasData: { hasData: false, serviceName: undefined, indices: 'apm-*' },
|
||||
hasData: false,
|
||||
status: 'success',
|
||||
},
|
||||
alert: { hasData: [], status: 'success' },
|
||||
|
@ -162,7 +168,7 @@ describe('HasDataContextProvider', () => {
|
|||
describe('at least one app returns true', () => {
|
||||
beforeAll(() => {
|
||||
registerApps([
|
||||
{ appName: 'apm', hasData: async () => true },
|
||||
{ appName: 'apm', hasData: async () => ({ hasData: true }) },
|
||||
{ appName: 'infra_logs', hasData: async () => false },
|
||||
{ appName: 'infra_metrics', hasData: async () => false },
|
||||
{
|
||||
|
@ -181,29 +187,30 @@ describe('HasDataContextProvider', () => {
|
|||
it('hasAnyData returns true apm returns true and all other apps return false', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||
expect(result.current).toEqual({
|
||||
hasData: {},
|
||||
hasDataMap: {},
|
||||
hasAnyData: false,
|
||||
isAllRequestsComplete: false,
|
||||
forceUpdate: expect.any(String),
|
||||
onRefreshTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
await act(async () => {
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
hasData: {
|
||||
hasDataMap: {
|
||||
apm: { hasData: true, status: 'success' },
|
||||
synthetics: {
|
||||
hasData: {
|
||||
hasData: false,
|
||||
indices: 'heartbeat-*, synthetics-*',
|
||||
},
|
||||
hasData: false,
|
||||
indices: 'heartbeat-*, synthetics-*',
|
||||
status: 'success',
|
||||
},
|
||||
infra_logs: { hasData: false, status: 'success' },
|
||||
infra_metrics: { hasData: false, status: 'success' },
|
||||
ux: {
|
||||
hasData: { hasData: false, serviceName: undefined, indices: 'apm-*' },
|
||||
hasData: false,
|
||||
indices: 'apm-*',
|
||||
status: 'success',
|
||||
},
|
||||
alert: { hasData: [], status: 'success' },
|
||||
|
@ -219,7 +226,7 @@ describe('HasDataContextProvider', () => {
|
|||
describe('all apps return true', () => {
|
||||
beforeAll(() => {
|
||||
registerApps([
|
||||
{ appName: 'apm', hasData: async () => true },
|
||||
{ appName: 'apm', hasData: async () => ({ hasData: true }) },
|
||||
{ appName: 'infra_logs', hasData: async () => true },
|
||||
{ appName: 'infra_metrics', hasData: async () => true },
|
||||
{
|
||||
|
@ -238,32 +245,34 @@ describe('HasDataContextProvider', () => {
|
|||
it('hasAnyData returns true and all apps return true', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||
expect(result.current).toEqual({
|
||||
hasData: {},
|
||||
hasDataMap: {},
|
||||
hasAnyData: false,
|
||||
isAllRequestsComplete: false,
|
||||
forceUpdate: expect.any(String),
|
||||
onRefreshTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
await act(async () => {
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
hasData: {
|
||||
hasDataMap: {
|
||||
apm: {
|
||||
hasData: true,
|
||||
status: 'success',
|
||||
},
|
||||
synthetics: {
|
||||
hasData: {
|
||||
hasData: true,
|
||||
indices: 'heartbeat-*, synthetics-*',
|
||||
},
|
||||
hasData: true,
|
||||
indices: 'heartbeat-*, synthetics-*',
|
||||
status: 'success',
|
||||
},
|
||||
infra_logs: { hasData: true, status: 'success' },
|
||||
infra_metrics: { hasData: true, status: 'success' },
|
||||
ux: {
|
||||
hasData: { hasData: true, serviceName: 'ux', indices: 'apm-*' },
|
||||
hasData: true,
|
||||
serviceName: 'ux',
|
||||
indices: 'apm-*',
|
||||
status: 'success',
|
||||
},
|
||||
alert: { hasData: [], status: 'success' },
|
||||
|
@ -279,7 +288,9 @@ describe('HasDataContextProvider', () => {
|
|||
describe('only apm is registered', () => {
|
||||
describe('when apm returns true', () => {
|
||||
beforeAll(() => {
|
||||
registerApps([{ appName: 'apm', hasData: async () => true }]);
|
||||
registerApps([
|
||||
{ appName: 'apm', hasData: async () => ({ hasData: true, indices: sampleAPMIndices }) },
|
||||
]);
|
||||
});
|
||||
|
||||
afterAll(unregisterAll);
|
||||
|
@ -289,18 +300,20 @@ describe('HasDataContextProvider', () => {
|
|||
wrapper,
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
hasData: {},
|
||||
hasDataMap: {},
|
||||
hasAnyData: false,
|
||||
isAllRequestsComplete: false,
|
||||
forceUpdate: expect.any(String),
|
||||
onRefreshTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
await act(async () => {
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
hasData: {
|
||||
apm: { hasData: true, status: 'success' },
|
||||
hasDataMap: {
|
||||
apm: { hasData: true, indices: sampleAPMIndices, status: 'success' },
|
||||
synthetics: { hasData: undefined, status: 'success' },
|
||||
infra_logs: { hasData: undefined, status: 'success' },
|
||||
infra_metrics: { hasData: undefined, status: 'success' },
|
||||
|
@ -317,7 +330,12 @@ describe('HasDataContextProvider', () => {
|
|||
|
||||
describe('when apm returns false', () => {
|
||||
beforeAll(() => {
|
||||
registerApps([{ appName: 'apm', hasData: async () => false }]);
|
||||
registerApps([
|
||||
{
|
||||
appName: 'apm',
|
||||
hasData: async () => ({ indices: sampleAPMIndices, hasData: false }),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
afterAll(unregisterAll);
|
||||
|
@ -327,18 +345,24 @@ describe('HasDataContextProvider', () => {
|
|||
wrapper,
|
||||
});
|
||||
expect(result.current).toEqual({
|
||||
hasData: {},
|
||||
hasDataMap: {},
|
||||
hasAnyData: false,
|
||||
isAllRequestsComplete: false,
|
||||
forceUpdate: expect.any(String),
|
||||
onRefreshTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
await act(async () => {
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
hasData: {
|
||||
apm: { hasData: false, status: 'success' },
|
||||
hasDataMap: {
|
||||
apm: {
|
||||
hasData: false,
|
||||
indices: sampleAPMIndices,
|
||||
status: 'success',
|
||||
},
|
||||
synthetics: { hasData: undefined, status: 'success' },
|
||||
infra_logs: { hasData: undefined, status: 'success' },
|
||||
infra_metrics: { hasData: undefined, status: 'success' },
|
||||
|
@ -381,29 +405,31 @@ describe('HasDataContextProvider', () => {
|
|||
it('hasAnyData returns true, apm is undefined and all other apps return true', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||
expect(result.current).toEqual({
|
||||
hasData: {},
|
||||
hasDataMap: {},
|
||||
hasAnyData: false,
|
||||
isAllRequestsComplete: false,
|
||||
forceUpdate: expect.any(String),
|
||||
onRefreshTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
await act(async () => {
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
hasData: {
|
||||
hasDataMap: {
|
||||
apm: { hasData: undefined, status: 'failure' },
|
||||
synthetics: {
|
||||
hasData: {
|
||||
hasData: true,
|
||||
indices: 'heartbeat-*, synthetics-*',
|
||||
},
|
||||
hasData: true,
|
||||
indices: 'heartbeat-*, synthetics-*',
|
||||
status: 'success',
|
||||
},
|
||||
infra_logs: { hasData: true, status: 'success' },
|
||||
infra_metrics: { hasData: true, status: 'success' },
|
||||
ux: {
|
||||
hasData: { hasData: true, serviceName: 'ux', indices: 'apm-*' },
|
||||
hasData: true,
|
||||
serviceName: 'ux',
|
||||
indices: 'apm-*',
|
||||
status: 'success',
|
||||
},
|
||||
alert: { hasData: [], status: 'success' },
|
||||
|
@ -457,17 +483,19 @@ describe('HasDataContextProvider', () => {
|
|||
it('hasAnyData returns false and all apps return undefined', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||
expect(result.current).toEqual({
|
||||
hasData: {},
|
||||
hasDataMap: {},
|
||||
hasAnyData: false,
|
||||
isAllRequestsComplete: false,
|
||||
forceUpdate: expect.any(String),
|
||||
onRefreshTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
await act(async () => {
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
hasData: {
|
||||
hasDataMap: {
|
||||
apm: { hasData: undefined, status: 'failure' },
|
||||
synthetics: { hasData: undefined, status: 'failure' },
|
||||
infra_logs: { hasData: undefined, status: 'failure' },
|
||||
|
@ -505,17 +533,19 @@ describe('HasDataContextProvider', () => {
|
|||
it('returns all alerts available', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useHasData(), { wrapper });
|
||||
expect(result.current).toEqual({
|
||||
hasData: {},
|
||||
hasDataMap: {},
|
||||
hasAnyData: false,
|
||||
isAllRequestsComplete: false,
|
||||
forceUpdate: expect.any(String),
|
||||
onRefreshTimeRange: expect.any(Function),
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
await act(async () => {
|
||||
await waitForNextUpdate();
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
hasData: {
|
||||
hasDataMap: {
|
||||
apm: { hasData: undefined, status: 'success' },
|
||||
synthetics: { hasData: undefined, status: 'success' },
|
||||
infra_logs: { hasData: undefined, status: 'success' },
|
||||
|
|
|
@ -14,17 +14,23 @@ import { FETCH_STATUS } from '../hooks/use_fetcher';
|
|||
import { usePluginContext } from '../hooks/use_plugin_context';
|
||||
import { useTimeRange } from '../hooks/use_time_range';
|
||||
import { getObservabilityAlerts } from '../services/get_observability_alerts';
|
||||
import { ObservabilityFetchDataPlugins, UXHasDataResponse } from '../typings/fetch_overview_data';
|
||||
import { ObservabilityFetchDataPlugins } from '../typings/fetch_overview_data';
|
||||
import { ApmIndicesConfig } from '../../common/typings';
|
||||
|
||||
type DataContextApps = ObservabilityFetchDataPlugins | 'alert';
|
||||
|
||||
export type HasDataMap = Record<
|
||||
DataContextApps,
|
||||
{ status: FETCH_STATUS; hasData?: boolean | UXHasDataResponse | Alert[] }
|
||||
{
|
||||
status: FETCH_STATUS;
|
||||
hasData?: boolean | Alert[];
|
||||
indices?: string | ApmIndicesConfig;
|
||||
serviceName?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export interface HasDataContextValue {
|
||||
hasData: Partial<HasDataMap>;
|
||||
hasDataMap: Partial<HasDataMap>;
|
||||
hasAnyData: boolean;
|
||||
isAllRequestsComplete: boolean;
|
||||
onRefreshTimeRange: () => void;
|
||||
|
@ -40,7 +46,7 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode
|
|||
const [forceUpdate, setForceUpdate] = useState('');
|
||||
const { absoluteStart, absoluteEnd } = useTimeRange();
|
||||
|
||||
const [hasData, setHasData] = useState<HasDataContextValue['hasData']>({});
|
||||
const [hasDataMap, setHasDataMap] = useState<HasDataContextValue['hasDataMap']>({});
|
||||
|
||||
const isExploratoryView = useRouteMatch('/exploratory-view');
|
||||
|
||||
|
@ -49,23 +55,53 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode
|
|||
if (!isExploratoryView)
|
||||
apps.forEach(async (app) => {
|
||||
try {
|
||||
if (app !== 'alert') {
|
||||
const params =
|
||||
app === 'ux'
|
||||
? { absoluteTime: { start: absoluteStart, end: absoluteEnd } }
|
||||
: undefined;
|
||||
|
||||
const result = await getDataHandler(app)?.hasData(params);
|
||||
setHasData((prevState) => ({
|
||||
const updateState = ({
|
||||
hasData,
|
||||
indices,
|
||||
serviceName,
|
||||
}: {
|
||||
hasData?: boolean;
|
||||
serviceName?: string;
|
||||
indices?: string | ApmIndicesConfig;
|
||||
}) => {
|
||||
setHasDataMap((prevState) => ({
|
||||
...prevState,
|
||||
[app]: {
|
||||
hasData: result,
|
||||
hasData,
|
||||
...(serviceName ? { serviceName } : {}),
|
||||
...(indices ? { indices } : {}),
|
||||
status: FETCH_STATUS.SUCCESS,
|
||||
},
|
||||
}));
|
||||
};
|
||||
switch (app) {
|
||||
case 'ux':
|
||||
const params = { absoluteTime: { start: absoluteStart, end: absoluteEnd } };
|
||||
const resultUx = await getDataHandler(app)?.hasData(params);
|
||||
updateState({
|
||||
hasData: resultUx?.hasData,
|
||||
indices: resultUx?.indices,
|
||||
serviceName: resultUx?.serviceName as string,
|
||||
});
|
||||
break;
|
||||
case 'synthetics':
|
||||
const resultSy = await getDataHandler(app)?.hasData();
|
||||
updateState({ hasData: resultSy?.hasData, indices: resultSy?.indices });
|
||||
|
||||
break;
|
||||
case 'apm':
|
||||
const resultApm = await getDataHandler(app)?.hasData();
|
||||
updateState({ hasData: resultApm?.hasData, indices: resultApm?.indices });
|
||||
|
||||
break;
|
||||
case 'infra_logs':
|
||||
case 'infra_metrics':
|
||||
const resultInfra = await getDataHandler(app)?.hasData();
|
||||
updateState({ hasData: resultInfra });
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
setHasData((prevState) => ({
|
||||
setHasDataMap((prevState) => ({
|
||||
...prevState,
|
||||
[app]: {
|
||||
hasData: undefined,
|
||||
|
@ -83,7 +119,7 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode
|
|||
async function fetchAlerts() {
|
||||
try {
|
||||
const alerts = await getObservabilityAlerts({ core });
|
||||
setHasData((prevState) => ({
|
||||
setHasDataMap((prevState) => ({
|
||||
...prevState,
|
||||
alert: {
|
||||
hasData: alerts,
|
||||
|
@ -91,7 +127,7 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode
|
|||
},
|
||||
}));
|
||||
} catch (e) {
|
||||
setHasData((prevState) => ({
|
||||
setHasDataMap((prevState) => ({
|
||||
...prevState,
|
||||
alert: {
|
||||
hasData: undefined,
|
||||
|
@ -105,18 +141,18 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode
|
|||
}, [forceUpdate, core]);
|
||||
|
||||
const isAllRequestsComplete = apps.every((app) => {
|
||||
const appStatus = hasData[app]?.status;
|
||||
const appStatus = hasDataMap[app]?.status;
|
||||
return appStatus !== undefined && appStatus !== FETCH_STATUS.LOADING;
|
||||
});
|
||||
|
||||
const hasAnyData = (Object.keys(hasData) as ObservabilityFetchDataPlugins[]).some(
|
||||
(app) => hasData[app]?.hasData === true
|
||||
const hasAnyData = (Object.keys(hasDataMap) as ObservabilityFetchDataPlugins[]).some(
|
||||
(app) => hasDataMap[app]?.hasData === true
|
||||
);
|
||||
|
||||
return (
|
||||
<HasDataContext.Provider
|
||||
value={{
|
||||
hasData,
|
||||
hasDataMap,
|
||||
hasAnyData,
|
||||
isAllRequestsComplete,
|
||||
forceUpdate,
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
|
||||
import { registerDataHandler, getDataHandler } from './data_handler';
|
||||
import moment from 'moment';
|
||||
import { ApmIndicesConfig } from '../common/typings';
|
||||
|
||||
const sampleAPMIndices = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'apm_oss.transactionIndices': 'apm-*',
|
||||
} as ApmIndicesConfig;
|
||||
|
||||
const params = {
|
||||
absoluteTime: {
|
||||
|
@ -23,7 +29,7 @@ const params = {
|
|||
describe('registerDataHandler', () => {
|
||||
const originalConsole = global.console;
|
||||
beforeAll(() => {
|
||||
// mocks console to avoid poluting the test output
|
||||
// mocks console to avoid polluting the test output
|
||||
global.console = ({ error: jest.fn() } as unknown) as typeof console;
|
||||
});
|
||||
|
||||
|
@ -58,7 +64,7 @@ describe('registerDataHandler', () => {
|
|||
},
|
||||
};
|
||||
},
|
||||
hasData: async () => true,
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
|
||||
it('registered data handler', () => {
|
||||
|
|
|
@ -24,32 +24,38 @@ describe('Home page', () => {
|
|||
});
|
||||
|
||||
it('renders loading component while requests are not returned', () => {
|
||||
jest
|
||||
.spyOn(hasData, 'useHasData')
|
||||
.mockImplementation(
|
||||
() =>
|
||||
({ hasData: {}, hasAnyData: false, isAllRequestsComplete: false } as HasDataContextValue)
|
||||
);
|
||||
jest.spyOn(hasData, 'useHasData').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
hasDataMap: {},
|
||||
hasAnyData: false,
|
||||
isAllRequestsComplete: false,
|
||||
} as HasDataContextValue)
|
||||
);
|
||||
const { getByText } = render(<HomePage />);
|
||||
expect(getByText('Loading Observability')).toBeInTheDocument();
|
||||
});
|
||||
it('renders landing page', () => {
|
||||
jest
|
||||
.spyOn(hasData, 'useHasData')
|
||||
.mockImplementation(
|
||||
() =>
|
||||
({ hasData: {}, hasAnyData: false, isAllRequestsComplete: true } as HasDataContextValue)
|
||||
);
|
||||
jest.spyOn(hasData, 'useHasData').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
hasDataMap: {},
|
||||
hasAnyData: false,
|
||||
isAllRequestsComplete: true,
|
||||
} as HasDataContextValue)
|
||||
);
|
||||
render(<HomePage />);
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith({ pathname: '/landing' });
|
||||
});
|
||||
it('renders overview page', () => {
|
||||
jest
|
||||
.spyOn(hasData, 'useHasData')
|
||||
.mockImplementation(
|
||||
() =>
|
||||
({ hasData: {}, hasAnyData: true, isAllRequestsComplete: false } as HasDataContextValue)
|
||||
);
|
||||
jest.spyOn(hasData, 'useHasData').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
hasDataMap: {},
|
||||
hasAnyData: true,
|
||||
isAllRequestsComplete: false,
|
||||
} as HasDataContextValue)
|
||||
);
|
||||
render(<HomePage />);
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith({ pathname: '/overview' });
|
||||
});
|
||||
|
|
|
@ -57,13 +57,13 @@ export function OverviewPage({ routeParams }: Props) {
|
|||
|
||||
const { data: newsFeed } = useFetcher(() => getNewsFeed({ core }), [core]);
|
||||
|
||||
const { hasData, hasAnyData } = useHasData();
|
||||
const { hasDataMap, hasAnyData } = useHasData();
|
||||
|
||||
if (hasAnyData === undefined) {
|
||||
return <LoadingObservability />;
|
||||
}
|
||||
|
||||
const alerts = (hasData.alert?.hasData as Alert[]) || [];
|
||||
const alerts = (hasDataMap.alert?.hasData as Alert[]) || [];
|
||||
|
||||
const { refreshInterval = 10000, refreshPaused = true } = routeParams.query;
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import { newsFeedFetchData } from './mock/news_feed.mock';
|
|||
import { emptyResponse as emptyUptimeResponse, fetchUptimeData } from './mock/uptime.mock';
|
||||
import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock';
|
||||
import { KibanaPageTemplate } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { ApmIndicesConfig } from '../../../common/typings';
|
||||
|
||||
function unregisterAll() {
|
||||
unregisterDataHandler({ appName: 'apm' });
|
||||
|
@ -33,6 +34,11 @@ function unregisterAll() {
|
|||
unregisterDataHandler({ appName: 'synthetics' });
|
||||
}
|
||||
|
||||
const sampleAPMIndices = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'apm_oss.transactionIndices': 'apm-*',
|
||||
} as ApmIndicesConfig;
|
||||
|
||||
const withCore = makeDecorator({
|
||||
name: 'withCore',
|
||||
parameterName: 'core',
|
||||
|
@ -177,7 +183,7 @@ storiesOf('app/Overview', module)
|
|||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => false,
|
||||
hasData: async () => ({ hasData: false, indices: sampleAPMIndices }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
|
@ -272,7 +278,7 @@ storiesOf('app/Overview', module)
|
|||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => true,
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -289,7 +295,7 @@ storiesOf('app/Overview', module)
|
|||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => true,
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
|
@ -321,7 +327,7 @@ storiesOf('app/Overview', module)
|
|||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => true,
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
|
@ -355,7 +361,7 @@ storiesOf('app/Overview', module)
|
|||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => true,
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
|
@ -386,7 +392,7 @@ storiesOf('app/Overview', module)
|
|||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: async () => emptyAPMResponse,
|
||||
hasData: async () => true,
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
|
@ -420,7 +426,7 @@ storiesOf('app/Overview', module)
|
|||
fetchData: async () => {
|
||||
throw new Error('Error fetching APM data');
|
||||
},
|
||||
hasData: async () => true,
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import { ObservabilityApp } from '../../../typings/common';
|
||||
import { UXMetrics } from '../../components/shared/core_web_vitals';
|
||||
import { ApmIndicesConfig } from '../../../common/typings';
|
||||
|
||||
export interface Stat {
|
||||
type: 'number' | 'percent' | 'bytesPerSecond';
|
||||
value: number;
|
||||
|
@ -34,11 +36,20 @@ export interface HasDataParams {
|
|||
|
||||
export interface HasDataResponse {
|
||||
hasData: boolean;
|
||||
indices: string;
|
||||
}
|
||||
|
||||
export interface UXHasDataResponse extends HasDataResponse {
|
||||
serviceName: string | number | undefined;
|
||||
serviceName?: string | number;
|
||||
indices?: string;
|
||||
}
|
||||
|
||||
export interface SyntheticsHasDataResponse extends HasDataResponse {
|
||||
indices: string;
|
||||
}
|
||||
|
||||
export interface APMHasDataResponse {
|
||||
hasData: boolean;
|
||||
indices: ApmIndicesConfig;
|
||||
}
|
||||
|
||||
export type FetchData<T extends FetchDataResponse = FetchDataResponse> = (
|
||||
|
@ -134,9 +145,9 @@ export interface ObservabilityFetchDataResponse {
|
|||
}
|
||||
|
||||
export interface ObservabilityHasDataResponse {
|
||||
apm: boolean;
|
||||
apm: APMHasDataResponse;
|
||||
infra_metrics: boolean;
|
||||
infra_logs: boolean;
|
||||
synthetics: HasDataResponse;
|
||||
synthetics: SyntheticsHasDataResponse;
|
||||
ux: UXHasDataResponse;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue