mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Observability overview page (#69141)
* creating overview page and menu * styling the home page * adjusting breadcrumb * renaming isnt working * renaming isnt working * renaming isnt working * fixing import * fixing scroll when resize window * fixing eslint errors * prepending links * adding target option * refactoring * adding dark mode support * fixing prettier format * fixing i18n * reverting some unnecessary changes * addressing PR comments * fixing functional tests * ordering observability menu * fixing tests * addressing PR comments * fixing scroll * addressing pr comments * addressing pr comments * creating overview page * mocking data * mocking data * refactoring * crearting apm chart * adding overview page * adding metric charts * adding charts * changing mock data location * adding mock registry * adding date picker * adding route validation * adding io-ts * adding io-ts * adding io-ts support * fixing imports and mock data * adding app folder * creating a section for each plugin * adding stats * adding domain min max * refactoring xcoordinaters * fixing route * adding bucket size * adding group property on logs * adding home page * dont break page if location state is undefined * each component fetches its own data * Refactoring * adding loading indicator to chart * fixing uptime chart * adding brush functionality to charts * fixing refresh button and auto refresh function * adding horizontal line to accordion section * adding emptySection to dashboard page * adding add data button * adding resources section * removing margins from horizontal rule * changing min interval to 60s * fixing empty section * removing unnecessary code * adding unit tests * fixing imports * adding initial story book for observability * removeing uptime mock data * fixing xDomain to show correct data on x-axis * fixing empty state alignment * adding story book and other improvements * adding news component * adding support to custom colors on EuiProgress and EuiStats * removing infra mock data * adding error message when api throwns an error * adding alert section * Adding alerts * adding alert api call * addressing PR comments * adding storybook * adding feedback button * addressing PR comments * chamging plugins return data * fixing kibana app navigation * fixing unit test * fixing ts issues * addressing PR comments * using lodash truncate * adding comment * updating public documentation * fixing alerts request * fixing unit test * fixing unit test * aligin beta badge to the center * adding moment duration to get the units as seconds * addressing PR comments * addressing PR comments
This commit is contained in:
parent
595e9c2d8d
commit
203fde92ac
75 changed files with 8149 additions and 389 deletions
|
@ -8,32 +8,33 @@
|
|||
|
||||
```typescript
|
||||
UI_SETTINGS: {
|
||||
META_FIELDS: string;
|
||||
DOC_HIGHLIGHT: string;
|
||||
QUERY_STRING_OPTIONS: string;
|
||||
QUERY_ALLOW_LEADING_WILDCARDS: string;
|
||||
SEARCH_QUERY_LANGUAGE: string;
|
||||
SORT_OPTIONS: string;
|
||||
COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string;
|
||||
COURIER_SET_REQUEST_PREFERENCE: string;
|
||||
COURIER_CUSTOM_REQUEST_PREFERENCE: string;
|
||||
COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string;
|
||||
COURIER_BATCH_SEARCHES: string;
|
||||
SEARCH_INCLUDE_FROZEN: string;
|
||||
HISTOGRAM_BAR_TARGET: string;
|
||||
HISTOGRAM_MAX_BARS: string;
|
||||
HISTORY_LIMIT: string;
|
||||
SHORT_DOTS_ENABLE: string;
|
||||
FORMAT_DEFAULT_TYPE_MAP: string;
|
||||
FORMAT_NUMBER_DEFAULT_PATTERN: string;
|
||||
FORMAT_PERCENT_DEFAULT_PATTERN: string;
|
||||
FORMAT_BYTES_DEFAULT_PATTERN: string;
|
||||
FORMAT_CURRENCY_DEFAULT_PATTERN: string;
|
||||
FORMAT_NUMBER_DEFAULT_LOCALE: string;
|
||||
TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string;
|
||||
TIMEPICKER_QUICK_RANGES: string;
|
||||
INDEXPATTERN_PLACEHOLDER: string;
|
||||
FILTERS_PINNED_BY_DEFAULT: string;
|
||||
FILTERS_EDITOR_SUGGEST_VALUES: string;
|
||||
readonly META_FIELDS: "metaFields";
|
||||
readonly DOC_HIGHLIGHT: "doc_table:highlight";
|
||||
readonly QUERY_STRING_OPTIONS: "query:queryString:options";
|
||||
readonly QUERY_ALLOW_LEADING_WILDCARDS: "query:allowLeadingWildcards";
|
||||
readonly SEARCH_QUERY_LANGUAGE: "search:queryLanguage";
|
||||
readonly SORT_OPTIONS: "sort:options";
|
||||
readonly COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: "courier:ignoreFilterIfFieldNotInIndex";
|
||||
readonly COURIER_SET_REQUEST_PREFERENCE: "courier:setRequestPreference";
|
||||
readonly COURIER_CUSTOM_REQUEST_PREFERENCE: "courier:customRequestPreference";
|
||||
readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests";
|
||||
readonly COURIER_BATCH_SEARCHES: "courier:batchSearches";
|
||||
readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen";
|
||||
readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget";
|
||||
readonly HISTOGRAM_MAX_BARS: "histogram:maxBars";
|
||||
readonly HISTORY_LIMIT: "history:limit";
|
||||
readonly SHORT_DOTS_ENABLE: "shortDots:enable";
|
||||
readonly FORMAT_DEFAULT_TYPE_MAP: "format:defaultTypeMap";
|
||||
readonly FORMAT_NUMBER_DEFAULT_PATTERN: "format:number:defaultPattern";
|
||||
readonly FORMAT_PERCENT_DEFAULT_PATTERN: "format:percent:defaultPattern";
|
||||
readonly FORMAT_BYTES_DEFAULT_PATTERN: "format:bytes:defaultPattern";
|
||||
readonly FORMAT_CURRENCY_DEFAULT_PATTERN: "format:currency:defaultPattern";
|
||||
readonly FORMAT_NUMBER_DEFAULT_LOCALE: "format:number:defaultLocale";
|
||||
readonly TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: "timepicker:refreshIntervalDefaults";
|
||||
readonly TIMEPICKER_QUICK_RANGES: "timepicker:quickRanges";
|
||||
readonly TIMEPICKER_TIME_DEFAULTS: "timepicker:timeDefaults";
|
||||
readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
|
||||
readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
|
||||
readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
|
||||
}
|
||||
```
|
||||
|
|
|
@ -8,32 +8,33 @@
|
|||
|
||||
```typescript
|
||||
UI_SETTINGS: {
|
||||
META_FIELDS: string;
|
||||
DOC_HIGHLIGHT: string;
|
||||
QUERY_STRING_OPTIONS: string;
|
||||
QUERY_ALLOW_LEADING_WILDCARDS: string;
|
||||
SEARCH_QUERY_LANGUAGE: string;
|
||||
SORT_OPTIONS: string;
|
||||
COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string;
|
||||
COURIER_SET_REQUEST_PREFERENCE: string;
|
||||
COURIER_CUSTOM_REQUEST_PREFERENCE: string;
|
||||
COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string;
|
||||
COURIER_BATCH_SEARCHES: string;
|
||||
SEARCH_INCLUDE_FROZEN: string;
|
||||
HISTOGRAM_BAR_TARGET: string;
|
||||
HISTOGRAM_MAX_BARS: string;
|
||||
HISTORY_LIMIT: string;
|
||||
SHORT_DOTS_ENABLE: string;
|
||||
FORMAT_DEFAULT_TYPE_MAP: string;
|
||||
FORMAT_NUMBER_DEFAULT_PATTERN: string;
|
||||
FORMAT_PERCENT_DEFAULT_PATTERN: string;
|
||||
FORMAT_BYTES_DEFAULT_PATTERN: string;
|
||||
FORMAT_CURRENCY_DEFAULT_PATTERN: string;
|
||||
FORMAT_NUMBER_DEFAULT_LOCALE: string;
|
||||
TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string;
|
||||
TIMEPICKER_QUICK_RANGES: string;
|
||||
INDEXPATTERN_PLACEHOLDER: string;
|
||||
FILTERS_PINNED_BY_DEFAULT: string;
|
||||
FILTERS_EDITOR_SUGGEST_VALUES: string;
|
||||
readonly META_FIELDS: "metaFields";
|
||||
readonly DOC_HIGHLIGHT: "doc_table:highlight";
|
||||
readonly QUERY_STRING_OPTIONS: "query:queryString:options";
|
||||
readonly QUERY_ALLOW_LEADING_WILDCARDS: "query:allowLeadingWildcards";
|
||||
readonly SEARCH_QUERY_LANGUAGE: "search:queryLanguage";
|
||||
readonly SORT_OPTIONS: "sort:options";
|
||||
readonly COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: "courier:ignoreFilterIfFieldNotInIndex";
|
||||
readonly COURIER_SET_REQUEST_PREFERENCE: "courier:setRequestPreference";
|
||||
readonly COURIER_CUSTOM_REQUEST_PREFERENCE: "courier:customRequestPreference";
|
||||
readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests";
|
||||
readonly COURIER_BATCH_SEARCHES: "courier:batchSearches";
|
||||
readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen";
|
||||
readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget";
|
||||
readonly HISTOGRAM_MAX_BARS: "histogram:maxBars";
|
||||
readonly HISTORY_LIMIT: "history:limit";
|
||||
readonly SHORT_DOTS_ENABLE: "shortDots:enable";
|
||||
readonly FORMAT_DEFAULT_TYPE_MAP: "format:defaultTypeMap";
|
||||
readonly FORMAT_NUMBER_DEFAULT_PATTERN: "format:number:defaultPattern";
|
||||
readonly FORMAT_PERCENT_DEFAULT_PATTERN: "format:percent:defaultPattern";
|
||||
readonly FORMAT_BYTES_DEFAULT_PATTERN: "format:bytes:defaultPattern";
|
||||
readonly FORMAT_CURRENCY_DEFAULT_PATTERN: "format:currency:defaultPattern";
|
||||
readonly FORMAT_NUMBER_DEFAULT_LOCALE: "format:number:defaultLocale";
|
||||
readonly TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: "timepicker:refreshIntervalDefaults";
|
||||
readonly TIMEPICKER_QUICK_RANGES: "timepicker:quickRanges";
|
||||
readonly TIMEPICKER_TIME_DEFAULTS: "timepicker:timeDefaults";
|
||||
readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
|
||||
readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
|
||||
readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
|
||||
}
|
||||
```
|
||||
|
|
|
@ -26,4 +26,5 @@ export const storybookAliases = {
|
|||
infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js',
|
||||
security_solution: 'x-pack/plugins/security_solution/scripts/storybook.js',
|
||||
ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/scripts/storybook.js',
|
||||
observability: 'x-pack/plugins/observability/scripts/storybook.js',
|
||||
};
|
||||
|
|
|
@ -44,7 +44,8 @@ export const UI_SETTINGS = {
|
|||
FORMAT_NUMBER_DEFAULT_LOCALE: 'format:number:defaultLocale',
|
||||
TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: 'timepicker:refreshIntervalDefaults',
|
||||
TIMEPICKER_QUICK_RANGES: 'timepicker:quickRanges',
|
||||
TIMEPICKER_TIME_DEFAULTS: 'timepicker:timeDefaults',
|
||||
INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder',
|
||||
FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault',
|
||||
FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues',
|
||||
};
|
||||
} as const;
|
||||
|
|
|
@ -1906,33 +1906,34 @@ export interface TimeRange {
|
|||
//
|
||||
// @public (undocumented)
|
||||
export const UI_SETTINGS: {
|
||||
META_FIELDS: string;
|
||||
DOC_HIGHLIGHT: string;
|
||||
QUERY_STRING_OPTIONS: string;
|
||||
QUERY_ALLOW_LEADING_WILDCARDS: string;
|
||||
SEARCH_QUERY_LANGUAGE: string;
|
||||
SORT_OPTIONS: string;
|
||||
COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string;
|
||||
COURIER_SET_REQUEST_PREFERENCE: string;
|
||||
COURIER_CUSTOM_REQUEST_PREFERENCE: string;
|
||||
COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string;
|
||||
COURIER_BATCH_SEARCHES: string;
|
||||
SEARCH_INCLUDE_FROZEN: string;
|
||||
HISTOGRAM_BAR_TARGET: string;
|
||||
HISTOGRAM_MAX_BARS: string;
|
||||
HISTORY_LIMIT: string;
|
||||
SHORT_DOTS_ENABLE: string;
|
||||
FORMAT_DEFAULT_TYPE_MAP: string;
|
||||
FORMAT_NUMBER_DEFAULT_PATTERN: string;
|
||||
FORMAT_PERCENT_DEFAULT_PATTERN: string;
|
||||
FORMAT_BYTES_DEFAULT_PATTERN: string;
|
||||
FORMAT_CURRENCY_DEFAULT_PATTERN: string;
|
||||
FORMAT_NUMBER_DEFAULT_LOCALE: string;
|
||||
TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string;
|
||||
TIMEPICKER_QUICK_RANGES: string;
|
||||
INDEXPATTERN_PLACEHOLDER: string;
|
||||
FILTERS_PINNED_BY_DEFAULT: string;
|
||||
FILTERS_EDITOR_SUGGEST_VALUES: string;
|
||||
readonly META_FIELDS: "metaFields";
|
||||
readonly DOC_HIGHLIGHT: "doc_table:highlight";
|
||||
readonly QUERY_STRING_OPTIONS: "query:queryString:options";
|
||||
readonly QUERY_ALLOW_LEADING_WILDCARDS: "query:allowLeadingWildcards";
|
||||
readonly SEARCH_QUERY_LANGUAGE: "search:queryLanguage";
|
||||
readonly SORT_OPTIONS: "sort:options";
|
||||
readonly COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: "courier:ignoreFilterIfFieldNotInIndex";
|
||||
readonly COURIER_SET_REQUEST_PREFERENCE: "courier:setRequestPreference";
|
||||
readonly COURIER_CUSTOM_REQUEST_PREFERENCE: "courier:customRequestPreference";
|
||||
readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests";
|
||||
readonly COURIER_BATCH_SEARCHES: "courier:batchSearches";
|
||||
readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen";
|
||||
readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget";
|
||||
readonly HISTOGRAM_MAX_BARS: "histogram:maxBars";
|
||||
readonly HISTORY_LIMIT: "history:limit";
|
||||
readonly SHORT_DOTS_ENABLE: "shortDots:enable";
|
||||
readonly FORMAT_DEFAULT_TYPE_MAP: "format:defaultTypeMap";
|
||||
readonly FORMAT_NUMBER_DEFAULT_PATTERN: "format:number:defaultPattern";
|
||||
readonly FORMAT_PERCENT_DEFAULT_PATTERN: "format:percent:defaultPattern";
|
||||
readonly FORMAT_BYTES_DEFAULT_PATTERN: "format:bytes:defaultPattern";
|
||||
readonly FORMAT_CURRENCY_DEFAULT_PATTERN: "format:currency:defaultPattern";
|
||||
readonly FORMAT_NUMBER_DEFAULT_LOCALE: "format:number:defaultLocale";
|
||||
readonly TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: "timepicker:refreshIntervalDefaults";
|
||||
readonly TIMEPICKER_QUICK_RANGES: "timepicker:quickRanges";
|
||||
readonly TIMEPICKER_TIME_DEFAULTS: "timepicker:timeDefaults";
|
||||
readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
|
||||
readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
|
||||
readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -767,33 +767,34 @@ export type TStrategyTypes = typeof ES_SEARCH_STRATEGY | string;
|
|||
//
|
||||
// @public (undocumented)
|
||||
export const UI_SETTINGS: {
|
||||
META_FIELDS: string;
|
||||
DOC_HIGHLIGHT: string;
|
||||
QUERY_STRING_OPTIONS: string;
|
||||
QUERY_ALLOW_LEADING_WILDCARDS: string;
|
||||
SEARCH_QUERY_LANGUAGE: string;
|
||||
SORT_OPTIONS: string;
|
||||
COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string;
|
||||
COURIER_SET_REQUEST_PREFERENCE: string;
|
||||
COURIER_CUSTOM_REQUEST_PREFERENCE: string;
|
||||
COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string;
|
||||
COURIER_BATCH_SEARCHES: string;
|
||||
SEARCH_INCLUDE_FROZEN: string;
|
||||
HISTOGRAM_BAR_TARGET: string;
|
||||
HISTOGRAM_MAX_BARS: string;
|
||||
HISTORY_LIMIT: string;
|
||||
SHORT_DOTS_ENABLE: string;
|
||||
FORMAT_DEFAULT_TYPE_MAP: string;
|
||||
FORMAT_NUMBER_DEFAULT_PATTERN: string;
|
||||
FORMAT_PERCENT_DEFAULT_PATTERN: string;
|
||||
FORMAT_BYTES_DEFAULT_PATTERN: string;
|
||||
FORMAT_CURRENCY_DEFAULT_PATTERN: string;
|
||||
FORMAT_NUMBER_DEFAULT_LOCALE: string;
|
||||
TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string;
|
||||
TIMEPICKER_QUICK_RANGES: string;
|
||||
INDEXPATTERN_PLACEHOLDER: string;
|
||||
FILTERS_PINNED_BY_DEFAULT: string;
|
||||
FILTERS_EDITOR_SUGGEST_VALUES: string;
|
||||
readonly META_FIELDS: "metaFields";
|
||||
readonly DOC_HIGHLIGHT: "doc_table:highlight";
|
||||
readonly QUERY_STRING_OPTIONS: "query:queryString:options";
|
||||
readonly QUERY_ALLOW_LEADING_WILDCARDS: "query:allowLeadingWildcards";
|
||||
readonly SEARCH_QUERY_LANGUAGE: "search:queryLanguage";
|
||||
readonly SORT_OPTIONS: "sort:options";
|
||||
readonly COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: "courier:ignoreFilterIfFieldNotInIndex";
|
||||
readonly COURIER_SET_REQUEST_PREFERENCE: "courier:setRequestPreference";
|
||||
readonly COURIER_CUSTOM_REQUEST_PREFERENCE: "courier:customRequestPreference";
|
||||
readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests";
|
||||
readonly COURIER_BATCH_SEARCHES: "courier:batchSearches";
|
||||
readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen";
|
||||
readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget";
|
||||
readonly HISTOGRAM_MAX_BARS: "histogram:maxBars";
|
||||
readonly HISTORY_LIMIT: "history:limit";
|
||||
readonly SHORT_DOTS_ENABLE: "shortDots:enable";
|
||||
readonly FORMAT_DEFAULT_TYPE_MAP: "format:defaultTypeMap";
|
||||
readonly FORMAT_NUMBER_DEFAULT_PATTERN: "format:number:defaultPattern";
|
||||
readonly FORMAT_PERCENT_DEFAULT_PATTERN: "format:percent:defaultPattern";
|
||||
readonly FORMAT_BYTES_DEFAULT_PATTERN: "format:bytes:defaultPattern";
|
||||
readonly FORMAT_CURRENCY_DEFAULT_PATTERN: "format:currency:defaultPattern";
|
||||
readonly FORMAT_NUMBER_DEFAULT_LOCALE: "format:number:defaultLocale";
|
||||
readonly TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: "timepicker:refreshIntervalDefaults";
|
||||
readonly TIMEPICKER_QUICK_RANGES: "timepicker:quickRanges";
|
||||
readonly TIMEPICKER_TIME_DEFAULTS: "timepicker:timeDefaults";
|
||||
readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
|
||||
readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
|
||||
readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { lazy } from 'react';
|
||||
import { euiThemeVars as theme } from '@kbn/ui-shared-deps/theme';
|
||||
import { ConfigSchema } from '.';
|
||||
import { ObservabilityPluginSetup } from '../../observability/public';
|
||||
import {
|
||||
|
@ -83,7 +82,7 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
|
|||
plugins.observability.dashboard.register({
|
||||
appName: 'apm',
|
||||
fetchData: async (params) => {
|
||||
return fetchLandingPageData(params, { theme });
|
||||
return fetchLandingPageData(params);
|
||||
},
|
||||
hasData,
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { fetchLandingPageData, hasData } from './observability_dashboard';
|
||||
import * as createCallApmApi from './createCallApmApi';
|
||||
import { euiThemeVars as theme } from '@kbn/ui-shared-deps/theme';
|
||||
|
||||
describe('Observability dashboard data', () => {
|
||||
const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi');
|
||||
|
@ -38,39 +37,31 @@ describe('Observability dashboard data', () => {
|
|||
],
|
||||
})
|
||||
);
|
||||
const response = await fetchLandingPageData(
|
||||
{
|
||||
startTime: '1',
|
||||
endTime: '2',
|
||||
bucketSize: '3',
|
||||
},
|
||||
{ theme }
|
||||
);
|
||||
const response = await fetchLandingPageData({
|
||||
startTime: '1',
|
||||
endTime: '2',
|
||||
bucketSize: '3',
|
||||
});
|
||||
expect(response).toEqual({
|
||||
title: 'APM',
|
||||
appLink: '/app/apm',
|
||||
stats: {
|
||||
services: {
|
||||
type: 'number',
|
||||
label: 'Services',
|
||||
value: 10,
|
||||
},
|
||||
transactions: {
|
||||
type: 'number',
|
||||
label: 'Transactions',
|
||||
value: 2,
|
||||
color: '#6092c0',
|
||||
},
|
||||
},
|
||||
series: {
|
||||
transactions: {
|
||||
label: 'Transactions',
|
||||
coordinates: [
|
||||
{ x: 1, y: 1 },
|
||||
{ x: 2, y: 2 },
|
||||
{ x: 3, y: 3 },
|
||||
],
|
||||
color: '#6092c0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -82,35 +73,27 @@ describe('Observability dashboard data', () => {
|
|||
transactionCoordinates: [],
|
||||
})
|
||||
);
|
||||
const response = await fetchLandingPageData(
|
||||
{
|
||||
startTime: '1',
|
||||
endTime: '2',
|
||||
bucketSize: '3',
|
||||
},
|
||||
{ theme }
|
||||
);
|
||||
const response = await fetchLandingPageData({
|
||||
startTime: '1',
|
||||
endTime: '2',
|
||||
bucketSize: '3',
|
||||
});
|
||||
expect(response).toEqual({
|
||||
title: 'APM',
|
||||
appLink: '/app/apm',
|
||||
stats: {
|
||||
services: {
|
||||
type: 'number',
|
||||
label: 'Services',
|
||||
value: 0,
|
||||
},
|
||||
transactions: {
|
||||
type: 'number',
|
||||
label: 'Transactions',
|
||||
value: 0,
|
||||
color: '#6092c0',
|
||||
},
|
||||
},
|
||||
series: {
|
||||
transactions: {
|
||||
label: 'Transactions',
|
||||
coordinates: [],
|
||||
color: '#6092c0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -122,35 +105,27 @@ describe('Observability dashboard data', () => {
|
|||
transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }],
|
||||
})
|
||||
);
|
||||
const response = await fetchLandingPageData(
|
||||
{
|
||||
startTime: '1',
|
||||
endTime: '2',
|
||||
bucketSize: '3',
|
||||
},
|
||||
{ theme }
|
||||
);
|
||||
const response = await fetchLandingPageData({
|
||||
startTime: '1',
|
||||
endTime: '2',
|
||||
bucketSize: '3',
|
||||
});
|
||||
expect(response).toEqual({
|
||||
title: 'APM',
|
||||
appLink: '/app/apm',
|
||||
stats: {
|
||||
services: {
|
||||
type: 'number',
|
||||
label: 'Services',
|
||||
value: 0,
|
||||
},
|
||||
transactions: {
|
||||
type: 'number',
|
||||
label: 'Transactions',
|
||||
value: 0,
|
||||
color: '#6092c0',
|
||||
},
|
||||
},
|
||||
series: {
|
||||
transactions: {
|
||||
label: 'Transactions',
|
||||
coordinates: [{ x: 1 }, { x: 2 }, { x: 3 }],
|
||||
color: '#6092c0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,21 +6,17 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { mean } from 'lodash';
|
||||
import { Theme } from '@kbn/ui-shared-deps/theme';
|
||||
import {
|
||||
ApmFetchDataResponse,
|
||||
FetchDataParams,
|
||||
} from '../../../../observability/public';
|
||||
import { callApmApi } from './createCallApmApi';
|
||||
|
||||
interface Options {
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
export const fetchLandingPageData = async (
|
||||
{ startTime, endTime, bucketSize }: FetchDataParams,
|
||||
{ theme }: Options
|
||||
): Promise<ApmFetchDataResponse> => {
|
||||
export const fetchLandingPageData = async ({
|
||||
startTime,
|
||||
endTime,
|
||||
bucketSize,
|
||||
}: FetchDataParams): Promise<ApmFetchDataResponse> => {
|
||||
const data = await callApmApi({
|
||||
pathname: '/api/apm/observability_dashboard',
|
||||
params: { query: { start: startTime, end: endTime, bucketSize } },
|
||||
|
@ -36,34 +32,20 @@ export const fetchLandingPageData = async (
|
|||
stats: {
|
||||
services: {
|
||||
type: 'number',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.observabilityDashboard.stats.services',
|
||||
{ defaultMessage: 'Services' }
|
||||
),
|
||||
value: serviceCount,
|
||||
},
|
||||
transactions: {
|
||||
type: 'number',
|
||||
label: i18n.translate(
|
||||
'xpack.apm.observabilityDashboard.stats.transactions',
|
||||
{ defaultMessage: 'Transactions' }
|
||||
),
|
||||
value:
|
||||
mean(
|
||||
transactionCoordinates
|
||||
.map(({ y }) => y)
|
||||
.filter((y) => y && isFinite(y))
|
||||
) || 0,
|
||||
color: theme.euiColorVis1,
|
||||
},
|
||||
},
|
||||
series: {
|
||||
transactions: {
|
||||
label: i18n.translate(
|
||||
'xpack.apm.observabilityDashboard.chart.transactions',
|
||||
{ defaultMessage: 'Transactions' }
|
||||
),
|
||||
color: theme.euiColorVis1,
|
||||
coordinates: transactionCoordinates,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -91,7 +91,6 @@ Object {
|
|||
"y": 3.5,
|
||||
},
|
||||
],
|
||||
"label": "Inbound traffic",
|
||||
},
|
||||
"outboundTraffic": Object {
|
||||
"coordinates": Array [
|
||||
|
@ -180,32 +179,26 @@ Object {
|
|||
"y": 4,
|
||||
},
|
||||
],
|
||||
"label": "Outbound traffic",
|
||||
},
|
||||
},
|
||||
"stats": Object {
|
||||
"cpu": Object {
|
||||
"label": "CPU usage",
|
||||
"type": "percent",
|
||||
"value": 0.0015,
|
||||
},
|
||||
"hosts": Object {
|
||||
"label": "Hosts",
|
||||
"type": "number",
|
||||
"value": 2,
|
||||
},
|
||||
"inboundTraffic": Object {
|
||||
"label": "Inbound traffic",
|
||||
"type": "bytesPerSecond",
|
||||
"value": 3.5,
|
||||
},
|
||||
"memory": Object {
|
||||
"label": "Memory usage",
|
||||
"type": "percent",
|
||||
"value": 0.0015,
|
||||
},
|
||||
"outboundTraffic": Object {
|
||||
"label": "Outbound traffic",
|
||||
"type": "bytesPerSecond",
|
||||
"value": 3,
|
||||
},
|
||||
|
|
|
@ -103,14 +103,6 @@ export const createMetricsFetchData = (
|
|||
body: JSON.stringify(snapshotRequest),
|
||||
});
|
||||
|
||||
const inboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.rxLabel', {
|
||||
defaultMessage: 'Inbound traffic',
|
||||
});
|
||||
|
||||
const outboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.txLabel', {
|
||||
defaultMessage: 'Outbound traffic',
|
||||
});
|
||||
|
||||
return {
|
||||
title: i18n.translate('xpack.infra.observabilityHomepage.metrics.title', {
|
||||
defaultMessage: 'Metrics',
|
||||
|
@ -119,43 +111,30 @@ export const createMetricsFetchData = (
|
|||
stats: {
|
||||
hosts: {
|
||||
type: 'number',
|
||||
label: i18n.translate('xpack.infra.observabilityHomepage.metrics.hostsLabel', {
|
||||
defaultMessage: 'Hosts',
|
||||
}),
|
||||
value: results.nodes.length,
|
||||
},
|
||||
cpu: {
|
||||
type: 'percent',
|
||||
label: i18n.translate('xpack.infra.observabilityHomepage.metrics.cpuLabel', {
|
||||
defaultMessage: 'CPU usage',
|
||||
}),
|
||||
value: combineNodesBy('cpu', results.nodes, average),
|
||||
},
|
||||
memory: {
|
||||
type: 'percent',
|
||||
label: i18n.translate('xpack.infra.observabilityHomepage.metrics.memoryLabel', {
|
||||
defaultMessage: 'Memory usage',
|
||||
}),
|
||||
value: combineNodesBy('memory', results.nodes, average),
|
||||
},
|
||||
inboundTraffic: {
|
||||
type: 'bytesPerSecond',
|
||||
label: inboundLabel,
|
||||
value: combineNodesBy('rx', results.nodes, average),
|
||||
},
|
||||
outboundTraffic: {
|
||||
type: 'bytesPerSecond',
|
||||
label: outboundLabel,
|
||||
value: combineNodesBy('tx', results.nodes, average),
|
||||
},
|
||||
},
|
||||
series: {
|
||||
inboundTraffic: {
|
||||
label: inboundLabel,
|
||||
coordinates: combineNodeTimeseriesBy('rx', results.nodes, average),
|
||||
},
|
||||
outboundTraffic: {
|
||||
label: outboundLabel,
|
||||
coordinates: combineNodeTimeseriesBy('tx', results.nodes, average),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3,23 +3,64 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { createHashHistory } from 'history';
|
||||
import React, { useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components';
|
||||
import { Route, Router, Switch } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { AppMountParameters, CoreStart } from '../../../../../src/core/public';
|
||||
import { Home } from '../pages/home';
|
||||
import { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components';
|
||||
import { PluginContext } from '../context/plugin_context';
|
||||
import { useUrlParams } from '../hooks/use_url_params';
|
||||
import { routes } from '../routes';
|
||||
import { usePluginContext } from '../hooks/use_plugin_context';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<>
|
||||
<Switch>
|
||||
{Object.keys(routes).map((key) => {
|
||||
const path = key as keyof typeof routes;
|
||||
const route = routes[path];
|
||||
const Wrapper = () => {
|
||||
const { core } = usePluginContext();
|
||||
useEffect(() => {
|
||||
core.chrome.setBreadcrumbs([
|
||||
{
|
||||
text: i18n.translate('xpack.observability.observability.breadcrumb.', {
|
||||
defaultMessage: 'Observability',
|
||||
}),
|
||||
},
|
||||
...route.breadcrumb,
|
||||
]);
|
||||
}, [core]);
|
||||
|
||||
const { query, path: pathParams } = useUrlParams(route.params);
|
||||
return route.handler({ query, path: pathParams });
|
||||
};
|
||||
return <Route key={path} path={path} exact={true} component={Wrapper} />;
|
||||
})}
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderApp = (core: CoreStart, { element }: AppMountParameters) => {
|
||||
const i18nCore = core.i18n;
|
||||
const isDarkMode = core.uiSettings.get('theme:darkMode');
|
||||
const history = createHashHistory();
|
||||
ReactDOM.render(
|
||||
<PluginContext.Provider value={{ core }}>
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<i18nCore.Context>
|
||||
<Home />
|
||||
</i18nCore.Context>
|
||||
</EuiThemeProvider>
|
||||
<Router history={history}>
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<i18nCore.Context>
|
||||
<RedirectAppLinks application={core.application}>
|
||||
<App />
|
||||
</RedirectAppLinks>
|
||||
</i18nCore.Context>
|
||||
</EuiThemeProvider>
|
||||
</Router>
|
||||
</PluginContext.Provider>,
|
||||
element
|
||||
);
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { ChartContainer } from './';
|
||||
|
||||
describe('chart container', () => {
|
||||
it('shows loading indicator', () => {
|
||||
const component = render(
|
||||
<ChartContainer height={100} isInitialLoad={true}>
|
||||
<div>My amazing component</div>
|
||||
</ChartContainer>
|
||||
);
|
||||
expect(component.getByTestId('loading')).toBeInTheDocument();
|
||||
expect(component.queryByText('My amazing component')).not.toBeInTheDocument();
|
||||
});
|
||||
it("doesn't show loading indicator", () => {
|
||||
const component = render(
|
||||
<ChartContainer height={100} isInitialLoad={false}>
|
||||
<div>My amazing component</div>
|
||||
</ChartContainer>
|
||||
);
|
||||
expect(component.queryByTestId('loading')).not.toBeInTheDocument();
|
||||
expect(component.getByText('My amazing component')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { Chart } from '@elastic/charts';
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import { EuiLoadingChartSize } from '@elastic/eui/src/components/loading/loading_chart';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
isInitialLoad: boolean;
|
||||
height?: number;
|
||||
width?: number;
|
||||
iconSize?: EuiLoadingChartSize;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const CHART_HEIGHT = 170;
|
||||
|
||||
export const ChartContainer = ({
|
||||
isInitialLoad,
|
||||
children,
|
||||
iconSize = 'xl',
|
||||
height = CHART_HEIGHT,
|
||||
}: Props) => {
|
||||
if (isInitialLoad) {
|
||||
return (
|
||||
<div
|
||||
data-test-subj="loading"
|
||||
style={{
|
||||
height,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<EuiLoadingChart size={iconSize} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Chart size={{ height }}>{children}</Chart>;
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { ISection } from '../../../typings/section';
|
||||
import { render } from '../../../utils/test_helper';
|
||||
import { EmptySection } from './';
|
||||
|
||||
describe('EmptySection', () => {
|
||||
it('renders without action button', () => {
|
||||
const section: ISection = {
|
||||
id: 'apm',
|
||||
title: 'APM',
|
||||
icon: 'logoAPM',
|
||||
description: 'foo bar',
|
||||
};
|
||||
const { getByText, queryAllByText } = render(<EmptySection section={section} />);
|
||||
|
||||
expect(getByText('APM')).toBeInTheDocument();
|
||||
expect(getByText('foo bar')).toBeInTheDocument();
|
||||
expect(queryAllByText('Install agent')).toEqual([]);
|
||||
});
|
||||
it('renders with action button', () => {
|
||||
const section: ISection = {
|
||||
id: 'apm',
|
||||
title: 'APM',
|
||||
icon: 'logoAPM',
|
||||
description: 'foo bar',
|
||||
linkTitle: 'install agent',
|
||||
href: 'https://www.elastic.co',
|
||||
};
|
||||
const { getByText, getByTestId } = render(<EmptySection section={section} />);
|
||||
|
||||
expect(getByText('APM')).toBeInTheDocument();
|
||||
expect(getByText('foo bar')).toBeInTheDocument();
|
||||
const linkButton = getByTestId('empty-apm') as HTMLAnchorElement;
|
||||
expect(linkButton.href).toEqual('https://www.elastic.co/');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiButton, EuiEmptyPrompt, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { ISection } from '../../../typings/section';
|
||||
|
||||
interface Props {
|
||||
section: ISection;
|
||||
}
|
||||
|
||||
export const EmptySection = ({ section }: Props) => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
style={{ maxWidth: 'none' }}
|
||||
iconType={section.icon}
|
||||
iconColor="default"
|
||||
title={<h2>{section.title}</h2>}
|
||||
titleSize="xs"
|
||||
body={<EuiText color="default">{section.description}</EuiText>}
|
||||
actions={
|
||||
<>
|
||||
{section.linkTitle && (
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="primary"
|
||||
fill
|
||||
href={section.href}
|
||||
target={section.target}
|
||||
data-test-subj={`empty-${section.id}`}
|
||||
>
|
||||
{section.linkTitle}
|
||||
</EuiButton>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render } from '../../../utils/test_helper';
|
||||
import { Header } from './';
|
||||
|
||||
describe('Header', () => {
|
||||
it('renders without add data button', () => {
|
||||
const { getByText, queryAllByText, getByTestId } = render(<Header color="#fff" />);
|
||||
expect(getByTestId('observability-logo')).toBeInTheDocument();
|
||||
expect(getByText('Observability')).toBeInTheDocument();
|
||||
expect(queryAllByText('Add data')).toEqual([]);
|
||||
});
|
||||
it('renders with add data button', () => {
|
||||
const { getByText, getByTestId } = render(<Header color="#fff" showAddData />);
|
||||
expect(getByTestId('observability-logo')).toBeInTheDocument();
|
||||
expect(getByText('Observability')).toBeInTheDocument();
|
||||
expect(getByText('Add data')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiBetaBadge,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { usePluginContext } from '../../../hooks/use_plugin_context';
|
||||
|
||||
const Container = styled.div<{ color: string }>`
|
||||
background: ${(props) => props.color};
|
||||
border-bottom: ${(props) => props.theme.eui.euiBorderThin};
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div<{ restrictWidth?: number }>`
|
||||
width: 100%;
|
||||
max-width: ${(props) => `${props.restrictWidth}px`};
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
padding: ${(props) => (props.restrictWidth ? 0 : '0 24px')};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
color: string;
|
||||
showAddData?: boolean;
|
||||
restrictWidth?: number;
|
||||
showGiveFeedback?: boolean;
|
||||
}
|
||||
|
||||
export const Header = ({
|
||||
color,
|
||||
restrictWidth,
|
||||
showAddData = false,
|
||||
showGiveFeedback = false,
|
||||
}: Props) => {
|
||||
const { core } = usePluginContext();
|
||||
return (
|
||||
<Container color={color}>
|
||||
<Wrapper restrictWidth={restrictWidth}>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="logoObservability" size="xxl" data-test-subj="observability-logo" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="m">
|
||||
<h1>
|
||||
{i18n.translate('xpack.observability.home.title', {
|
||||
defaultMessage: 'Observability',
|
||||
})}{' '}
|
||||
<EuiBetaBadge
|
||||
className="eui-alignMiddle"
|
||||
label={i18n.translate('xpack.observability.beta', { defaultMessage: 'Beta' })}
|
||||
tooltipContent="This feature is in beta. Please help us improve it by reporting any bugs or give us feedback."
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{showGiveFeedback && (
|
||||
<EuiFlexItem style={{ alignItems: 'flex-end' }} grow={false}>
|
||||
<EuiButtonEmpty
|
||||
href={'https://discuss.elastic.co/c/observability/'}
|
||||
iconType="popout"
|
||||
>
|
||||
{i18n.translate('xpack.observability.home.feedback', {
|
||||
defaultMessage: 'Give us feedback',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{showAddData && (
|
||||
<EuiFlexItem style={{ alignItems: 'flex-end' }} grow={false}>
|
||||
<EuiButtonEmpty
|
||||
href={core.http.basePath.prepend('/app/home#/tutorial_directory/logging')}
|
||||
iconType="plusInCircle"
|
||||
>
|
||||
{i18n.translate('xpack.observability.home.addData', { defaultMessage: 'Add data' })}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
</Wrapper>
|
||||
</Container>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiPage, EuiPageBody, EuiPageProps } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Header } from '../header/index';
|
||||
|
||||
const getPaddingSize = (props: EuiPageProps) => (props.restrictWidth ? 0 : '24px');
|
||||
|
||||
const Page = styled(EuiPage)<EuiPageProps>`
|
||||
background: transparent;
|
||||
padding-right: ${getPaddingSize};
|
||||
padding-left: ${getPaddingSize};
|
||||
`;
|
||||
|
||||
const Container = styled.div<{ color?: string }>`
|
||||
overflow-y: hidden;
|
||||
min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize});
|
||||
background: ${(props) => props.color};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
headerColor: string;
|
||||
bodyColor: string;
|
||||
children?: React.ReactNode;
|
||||
restrictWidth?: number;
|
||||
showAddData?: boolean;
|
||||
showGiveFeedback?: boolean;
|
||||
}
|
||||
|
||||
export const WithHeaderLayout = ({
|
||||
headerColor,
|
||||
bodyColor,
|
||||
children,
|
||||
restrictWidth,
|
||||
showAddData,
|
||||
showGiveFeedback,
|
||||
}: Props) => (
|
||||
<Container color={bodyColor}>
|
||||
<Header
|
||||
color={headerColor}
|
||||
restrictWidth={restrictWidth}
|
||||
showAddData={showAddData}
|
||||
showGiveFeedback={showGiveFeedback}
|
||||
/>
|
||||
<Page restrictWidth={restrictWidth}>
|
||||
<EuiPageBody>{children}</EuiPageBody>
|
||||
</Page>
|
||||
</Container>
|
||||
);
|
|
@ -0,0 +1,3 @@
|
|||
.obsNewsFeed__itemImg{
|
||||
@include euiBottomShadowSmall;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render } from '../../../utils/test_helper';
|
||||
import { News } from './';
|
||||
|
||||
describe('News', () => {
|
||||
it('renders resources with all elements', () => {
|
||||
const { getByText, getAllByText } = render(<News />);
|
||||
expect(getByText("What's new")).toBeInTheDocument();
|
||||
expect(getAllByText('Read full story')).not.toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiLink,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useContext } from 'react';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import './index.scss';
|
||||
import { truncate } from 'lodash';
|
||||
import { news as newsMockData } from './mock/news.mock.data';
|
||||
|
||||
interface NewsItem {
|
||||
title: string;
|
||||
description: string;
|
||||
link_url: string;
|
||||
image_url: string;
|
||||
}
|
||||
|
||||
export const News = () => {
|
||||
const newsItems: NewsItem[] = newsMockData;
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h4>
|
||||
{i18n.translate('xpack.observability.news.title', {
|
||||
defaultMessage: "What's new",
|
||||
})}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{newsItems.map((item, index) => (
|
||||
<EuiFlexItem key={index} grow={false}>
|
||||
<NewsItem item={item} />
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const limitString = (string: string, limit: number) => truncate(string, { length: limit });
|
||||
|
||||
const NewsItem = ({ item }: { item: NewsItem }) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxxs">
|
||||
<h4>{item.title}</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="s" alignItems="baseline">
|
||||
<EuiFlexItem>
|
||||
<EuiText grow={false} size="xs" color="subdued">
|
||||
{limitString(item.description, 128)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink href={item.link_url} target="_blank">
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.observability.news.readFullStory', {
|
||||
defaultMessage: 'Read full story',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<img
|
||||
style={{ border: theme.eui.euiBorderThin }}
|
||||
width={48}
|
||||
height={48}
|
||||
alt={item.title}
|
||||
src={item.image_url}
|
||||
className="obsNewsFeed__itemImg"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const news = [
|
||||
{
|
||||
title: 'Have SIEM questions?',
|
||||
description:
|
||||
'Join our growing community of Elastic SIEM users to discuss the configuration and use of Elastic SIEM for threat detection and response.',
|
||||
link_url: 'https://discuss.elastic.co/c/security/siem/?blade=securitysolutionfeed',
|
||||
image_url:
|
||||
'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed',
|
||||
},
|
||||
{
|
||||
title: 'Elastic SIEM on-demand training course — free for a limited time',
|
||||
description:
|
||||
'With this self-paced, on-demand course, you will learn how to leverage Elastic SIEM to drive your security operations and threat hunting. This course is designed for security analysts and practitioners who have used other SIEMs or are familiar with SIEM concepts.',
|
||||
link_url:
|
||||
'https://training.elastic.co/elearning/security-analytics/elastic-siem-fundamentals-promo?blade=securitysolutionfeed',
|
||||
image_url:
|
||||
'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt50f58e0358ebea9d/5c30508693d9791a70cd73ad/illustration-specialization-course-page-security.svg?blade=securitysolutionfeed',
|
||||
},
|
||||
{
|
||||
title: 'New to Elastic SIEM? Take our on-demand training course',
|
||||
description:
|
||||
'With this self-paced, on-demand course, you will learn how to leverage Elastic SIEM to drive your security operations and threat hunting. This course is designed for security analysts and practitioners who have used other SIEMs or are familiar with SIEM concepts.',
|
||||
link_url:
|
||||
'https://www.elastic.co/training/specializations/security-analytics/elastic-siem-fundamentals?blade=securitysolutionfeed',
|
||||
image_url:
|
||||
'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt50f58e0358ebea9d/5c30508693d9791a70cd73ad/illustration-specialization-course-page-security.svg?blade=securitysolutionfeed',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Resources } from './';
|
||||
|
||||
describe('Resources', () => {
|
||||
it('renders resources with all elements', () => {
|
||||
const { getByText } = render(<Resources />);
|
||||
expect(getByText('Documentation')).toBeInTheDocument();
|
||||
expect(getByText('Discuss forum')).toBeInTheDocument();
|
||||
expect(getByText('Observability fundamentals')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiListGroup, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
const resources = [
|
||||
{
|
||||
iconType: 'documents',
|
||||
label: i18n.translate('xpack.observability.resources.documentation', {
|
||||
defaultMessage: 'Documentation',
|
||||
}),
|
||||
href: 'https://www.elastic.co/guide/en/observability/current/observability-ui.html',
|
||||
},
|
||||
{
|
||||
iconType: 'editorComment',
|
||||
label: i18n.translate('xpack.observability.resources.forum', {
|
||||
defaultMessage: 'Discuss forum',
|
||||
}),
|
||||
href: 'https://discuss.elastic.co/c/observability/',
|
||||
},
|
||||
{
|
||||
iconType: 'training',
|
||||
label: i18n.translate('xpack.observability.resources.training', {
|
||||
defaultMessage: 'Observability fundamentals',
|
||||
}),
|
||||
href: 'https://www.elastic.co/training/observability-fundamentals',
|
||||
},
|
||||
];
|
||||
|
||||
export const Resources = () => {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" alignItems="flexStart" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h4>
|
||||
{i18n.translate('xpack.observability.resources.title', {
|
||||
defaultMessage: 'Resources',
|
||||
})}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiListGroup flush listItems={resources} data-test-subj="list-group" />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiIconTip,
|
||||
EuiLink,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import React, { useState } from 'react';
|
||||
import { EuiSelect } from '@elastic/eui';
|
||||
import { uniqBy } from 'lodash';
|
||||
import { Alert } from '../../../../../../alerts/common';
|
||||
import { usePluginContext } from '../../../../hooks/use_plugin_context';
|
||||
import { SectionContainer } from '..';
|
||||
|
||||
const ALL_TYPES = 'ALL_TYPES';
|
||||
const allTypes = {
|
||||
value: ALL_TYPES,
|
||||
text: i18n.translate('xpack.observability.overview.alert.allTypes', {
|
||||
defaultMessage: 'All types',
|
||||
}),
|
||||
};
|
||||
|
||||
interface Props {
|
||||
alerts: Alert[];
|
||||
}
|
||||
|
||||
export const AlertsSection = ({ alerts }: Props) => {
|
||||
const { core } = usePluginContext();
|
||||
const [filter, setFilter] = useState(ALL_TYPES);
|
||||
|
||||
const filterOptions = uniqBy(alerts, (alert) => alert.consumer).map(({ consumer }) => ({
|
||||
value: consumer,
|
||||
text: consumer,
|
||||
}));
|
||||
|
||||
return (
|
||||
<SectionContainer
|
||||
title="Alerts"
|
||||
appLink={'/app/management/insightsAndAlerting/triggersActions/alerts'}
|
||||
hasError={false}
|
||||
appLinkName={i18n.translate('xpack.observability.overview.alert.appLink', {
|
||||
defaultMessage: 'Manage alerts',
|
||||
})}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
compressed
|
||||
id="filterAlerts"
|
||||
options={[allTypes, ...filterOptions]}
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
prepend={i18n.translate('xpack.observability.overview.alert.view', {
|
||||
defaultMessage: 'View',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<EuiFlexItem>
|
||||
{alerts
|
||||
.filter((alert) => filter === ALL_TYPES || alert.consumer === filter)
|
||||
.map((alert, index) => {
|
||||
const isLastElement = index === alerts.length - 1;
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s" key={alert.id}>
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
href={core.http.basePath.prepend(
|
||||
`/app/management/insightsAndAlerting/triggersActions/alert/${alert.id}`
|
||||
)}
|
||||
>
|
||||
{alert.name}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow">{alert.alertTypeId}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
{alert.tags.map((tag, idx) => {
|
||||
return (
|
||||
<EuiFlexItem key={idx} grow={false}>
|
||||
<EuiBadge color="default">{tag}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" size="xs">
|
||||
Updated {moment.duration(moment().diff(alert.updatedAt)).humanize()} ago
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{alert.muteAll && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
type="minusInCircle"
|
||||
content={i18n.translate('xpack.observability.overview.alerts.muted', {
|
||||
defaultMessage: 'Muted',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{!isLastElement && <EuiHorizontalRule margin="xs" />}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</SectionContainer>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import * as fetcherHook from '../../../../hooks/use_fetcher';
|
||||
import { render } from '../../../../utils/test_helper';
|
||||
import { APMSection } from './';
|
||||
import { response } from './mock_data/apm.mock';
|
||||
|
||||
describe('APMSection', () => {
|
||||
it('renders with transaction series and stats', () => {
|
||||
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
|
||||
data: response,
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
const { getByText, queryAllByTestId } = render(
|
||||
<APMSection
|
||||
startTime="2020-06-29T11:38:23.747Z"
|
||||
endTime="2020-06-29T12:08:23.748Z"
|
||||
bucketSize="60s"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByText('APM')).toBeInTheDocument();
|
||||
expect(getByText('View in app')).toBeInTheDocument();
|
||||
expect(getByText('Services 11')).toBeInTheDocument();
|
||||
expect(getByText('Transactions per minute 312.00k')).toBeInTheDocument();
|
||||
expect(queryAllByTestId('loading')).toEqual([]);
|
||||
});
|
||||
it('shows loading state', () => {
|
||||
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
|
||||
data: undefined,
|
||||
status: fetcherHook.FETCH_STATUS.LOADING,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
const { getByText, queryAllByText, getByTestId } = render(
|
||||
<APMSection
|
||||
startTime="2020-06-29T11:38:23.747Z"
|
||||
endTime="2020-06-29T12:08:23.748Z"
|
||||
bucketSize="60s"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByText('APM')).toBeInTheDocument();
|
||||
expect(getByTestId('loading')).toBeInTheDocument();
|
||||
expect(queryAllByText('View in app')).toEqual([]);
|
||||
expect(queryAllByText('Services 11')).toEqual([]);
|
||||
expect(queryAllByText('Transactions per minute 312.00k')).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import React, { useContext } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import { SectionContainer } from '../';
|
||||
import { getDataHandler } from '../../../../data_handler';
|
||||
import { useChartTheme } from '../../../../hooks/use_chart_theme';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { ChartContainer } from '../../chart_container';
|
||||
import { StyledStat } from '../../styled_stat';
|
||||
import { onBrushEnd } from '../helper';
|
||||
|
||||
interface Props {
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
bucketSize?: string;
|
||||
}
|
||||
|
||||
function formatTpm(value?: number) {
|
||||
return numeral(value).format('0.00a');
|
||||
}
|
||||
|
||||
export const APMSection = ({ startTime, endTime, bucketSize }: Props) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const history = useHistory();
|
||||
|
||||
const { data, status } = useFetcher(() => {
|
||||
if (startTime && endTime && bucketSize) {
|
||||
return getDataHandler('apm')?.fetchData({ startTime, endTime, bucketSize });
|
||||
}
|
||||
}, [startTime, endTime, bucketSize]);
|
||||
|
||||
const { title = 'APM', appLink, stats, series } = data || {};
|
||||
|
||||
const min = moment.utc(startTime).valueOf();
|
||||
const max = moment.utc(endTime).valueOf();
|
||||
|
||||
const formatter = niceTimeFormatter([min, max]);
|
||||
|
||||
const isLoading = status === FETCH_STATUS.LOADING;
|
||||
|
||||
const transactionsColor = theme.eui.euiColorVis1;
|
||||
|
||||
return (
|
||||
<SectionContainer
|
||||
title={title || 'APM'}
|
||||
appLink={appLink}
|
||||
hasError={status === FETCH_STATUS.FAILURE}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledStat
|
||||
title={numeral(stats?.services.value).format('0a')}
|
||||
description={i18n.translate('xpack.observability.overview.apm.services', {
|
||||
defaultMessage: 'Services',
|
||||
})}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledStat
|
||||
title={formatTpm(stats?.transactions.value)}
|
||||
description={i18n.translate('xpack.observability.overview.apm.transactionsPerMinute', {
|
||||
defaultMessage: 'Transactions per minute',
|
||||
})}
|
||||
isLoading={isLoading}
|
||||
color={transactionsColor}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<ChartContainer isInitialLoad={isLoading && !data}>
|
||||
<Settings
|
||||
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
|
||||
theme={useChartTheme()}
|
||||
showLegend={false}
|
||||
xDomain={{ min, max }}
|
||||
/>
|
||||
{series?.transactions.coordinates && (
|
||||
<>
|
||||
<BarSeries
|
||||
id="transactions"
|
||||
name="Transactions"
|
||||
data={series?.transactions.coordinates}
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
color={transactionsColor}
|
||||
/>
|
||||
<Axis
|
||||
id="y-axis"
|
||||
position={Position.Left}
|
||||
showGridLines
|
||||
tickFormat={(value) => `${formatTpm(value)} tpm`}
|
||||
/>
|
||||
<Axis id="x-axis" position={Position.Bottom} tickFormat={formatter} />
|
||||
</>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</SectionContainer>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ApmFetchDataResponse } from '../../../../../typings';
|
||||
|
||||
export const response: ApmFetchDataResponse = {
|
||||
title: 'APM',
|
||||
|
||||
appLink: '/app/apm',
|
||||
stats: {
|
||||
services: { value: 11, type: 'number' },
|
||||
transactions: { value: 312000, type: 'number' },
|
||||
},
|
||||
series: {
|
||||
transactions: {
|
||||
coordinates: [
|
||||
{ x: 1591365600000, y: 32 },
|
||||
{ x: 1591366200000, y: 43 },
|
||||
{ x: 1591366800000, y: 22 },
|
||||
{ x: 1591367400000, y: 29 },
|
||||
{ x: 1591368000000, y: 39 },
|
||||
{ x: 1591368600000, y: 36 },
|
||||
{ x: 1591369200000, y: 50 },
|
||||
{ x: 1591369800000, y: 31 },
|
||||
{ x: 1591370400000, y: 39 },
|
||||
{ x: 1591371000000, y: 26 },
|
||||
{ x: 1591371600000, y: 45 },
|
||||
{ x: 1591372200000, y: 27 },
|
||||
{ x: 1591372800000, y: 37 },
|
||||
{ x: 1591373400000, y: 55 },
|
||||
{ x: 1591374000000, y: 31 },
|
||||
{ x: 1591374600000, y: 26 },
|
||||
{ x: 1591375200000, y: 57 },
|
||||
{ x: 1591375800000, y: 25 },
|
||||
{ x: 1591376400000, y: 28 },
|
||||
{ x: 1591377000000, y: 40 },
|
||||
{ x: 1591377600000, y: 33 },
|
||||
{ x: 1591378200000, y: 33 },
|
||||
{ x: 1591378800000, y: 31 },
|
||||
{ x: 1591379400000, y: 32 },
|
||||
{ x: 1591380000000, y: 34 },
|
||||
{ x: 1591380600000, y: 31 },
|
||||
{ x: 1591381200000, y: 16 },
|
||||
{ x: 1591381800000, y: 34 },
|
||||
{ x: 1591382400000, y: 33 },
|
||||
{ x: 1591383000000, y: 35 },
|
||||
{ x: 1591383600000, y: 47 },
|
||||
{ x: 1591384200000, y: 44 },
|
||||
{ x: 1591384800000, y: 21 },
|
||||
{ x: 1591385400000, y: 25 },
|
||||
{ x: 1591386000000, y: 34 },
|
||||
{ x: 1591386600000, y: 37 },
|
||||
{ x: 1591387200000, y: 38 },
|
||||
{ x: 1591387800000, y: 28 },
|
||||
{ x: 1591388400000, y: 32 },
|
||||
{ x: 1591389000000, y: 37 },
|
||||
{ x: 1591389600000, y: 25 },
|
||||
{ x: 1591390200000, y: 33 },
|
||||
{ x: 1591390800000, y: 34 },
|
||||
{ x: 1591391400000, y: 30 },
|
||||
{ x: 1591392000000, y: 45 },
|
||||
{ x: 1591392600000, y: 42 },
|
||||
{ x: 1591393200000, y: 23 },
|
||||
{ x: 1591393800000, y: 33 },
|
||||
{ x: 1591394400000, y: 38 },
|
||||
{ x: 1591395000000, y: 30 },
|
||||
{ x: 1591395600000, y: 25 },
|
||||
{ x: 1591396200000, y: 33 },
|
||||
{ x: 1591396800000, y: 37 },
|
||||
{ x: 1591397400000, y: 43 },
|
||||
{ x: 1591398000000, y: 30 },
|
||||
{ x: 1591398600000, y: 36 },
|
||||
{ x: 1591399200000, y: 28 },
|
||||
{ x: 1591399800000, y: 39 },
|
||||
{ x: 1591400400000, y: 27 },
|
||||
{ x: 1591401000000, y: 41 },
|
||||
{ x: 1591401600000, y: 25 },
|
||||
{ x: 1591402200000, y: 31 },
|
||||
{ x: 1591402800000, y: 28 },
|
||||
{ x: 1591403400000, y: 29 },
|
||||
{ x: 1591404000000, y: 49 },
|
||||
{ x: 1591404600000, y: 24 },
|
||||
{ x: 1591405200000, y: 41 },
|
||||
{ x: 1591405800000, y: 30 },
|
||||
{ x: 1591406400000, y: 36 },
|
||||
{ x: 1591407000000, y: 39 },
|
||||
{ x: 1591407600000, y: 23 },
|
||||
{ x: 1591408200000, y: 40 },
|
||||
{ x: 1591408800000, y: 34 },
|
||||
{ x: 1591409400000, y: 28 },
|
||||
{ x: 1591410000000, y: 33 },
|
||||
{ x: 1591410600000, y: 31 },
|
||||
{ x: 1591411200000, y: 39 },
|
||||
{ x: 1591411800000, y: 33 },
|
||||
{ x: 1591412400000, y: 35 },
|
||||
{ x: 1591413000000, y: 31 },
|
||||
{ x: 1591413600000, y: 35 },
|
||||
{ x: 1591414200000, y: 37 },
|
||||
{ x: 1591414800000, y: 26 },
|
||||
{ x: 1591415400000, y: 27 },
|
||||
{ x: 1591416000000, y: 26 },
|
||||
{ x: 1591416600000, y: 34 },
|
||||
{ x: 1591417200000, y: 33 },
|
||||
{ x: 1591417800000, y: 38 },
|
||||
{ x: 1591418400000, y: 34 },
|
||||
{ x: 1591419000000, y: 37 },
|
||||
{ x: 1591419600000, y: 24 },
|
||||
{ x: 1591420200000, y: 25 },
|
||||
{ x: 1591420800000, y: 20 },
|
||||
{ x: 1591421400000, y: 35 },
|
||||
{ x: 1591422000000, y: 41 },
|
||||
{ x: 1591422600000, y: 40 },
|
||||
{ x: 1591423200000, y: 33 },
|
||||
{ x: 1591423800000, y: 24 },
|
||||
{ x: 1591424400000, y: 44 },
|
||||
{ x: 1591425000000, y: 24 },
|
||||
{ x: 1591425600000, y: 32 },
|
||||
{ x: 1591426200000, y: 37 },
|
||||
{ x: 1591426800000, y: 34 },
|
||||
{ x: 1591427400000, y: 28 },
|
||||
{ x: 1591428000000, y: 26 },
|
||||
{ x: 1591428600000, y: 37 },
|
||||
{ x: 1591429200000, y: 36 },
|
||||
{ x: 1591429800000, y: 37 },
|
||||
{ x: 1591430400000, y: 23 },
|
||||
{ x: 1591431000000, y: 47 },
|
||||
{ x: 1591431600000, y: 41 },
|
||||
{ x: 1591432200000, y: 24 },
|
||||
{ x: 1591432800000, y: 34 },
|
||||
{ x: 1591433400000, y: 27 },
|
||||
{ x: 1591434000000, y: 34 },
|
||||
{ x: 1591434600000, y: 44 },
|
||||
{ x: 1591435200000, y: 20 },
|
||||
{ x: 1591435800000, y: 34 },
|
||||
{ x: 1591436400000, y: 29 },
|
||||
{ x: 1591437000000, y: 28 },
|
||||
{ x: 1591437600000, y: 36 },
|
||||
{ x: 1591438200000, y: 34 },
|
||||
{ x: 1591438800000, y: 26 },
|
||||
{ x: 1591439400000, y: 29 },
|
||||
{ x: 1591440000000, y: 45 },
|
||||
{ x: 1591440600000, y: 34 },
|
||||
{ x: 1591441200000, y: 25 },
|
||||
{ x: 1591441800000, y: 34 },
|
||||
{ x: 1591442400000, y: 28 },
|
||||
{ x: 1591443000000, y: 34 },
|
||||
{ x: 1591443600000, y: 31 },
|
||||
{ x: 1591444200000, y: 24 },
|
||||
{ x: 1591444800000, y: 34 },
|
||||
{ x: 1591445400000, y: 21 },
|
||||
{ x: 1591446000000, y: 40 },
|
||||
{ x: 1591446600000, y: 37 },
|
||||
{ x: 1591447200000, y: 31 },
|
||||
{ x: 1591447800000, y: 21 },
|
||||
{ x: 1591448400000, y: 24 },
|
||||
{ x: 1591449000000, y: 30 },
|
||||
{ x: 1591449600000, y: 22 },
|
||||
{ x: 1591450200000, y: 27 },
|
||||
{ x: 1591450800000, y: 30 },
|
||||
{ x: 1591451400000, y: 22 },
|
||||
{ x: 1591452000000, y: 9 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
export const ErrorPanel = () => {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="center" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued">
|
||||
{i18n.translate('xpack.observability.section.errorPanel', {
|
||||
defaultMessage: 'An error happened when trying to fetch data. Please try again',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { onBrushEnd } from './helper';
|
||||
import { History } from 'history';
|
||||
|
||||
describe('Chart helper', () => {
|
||||
describe('onBrushEnd', () => {
|
||||
const history = ({
|
||||
push: jest.fn(),
|
||||
location: {
|
||||
search: '',
|
||||
},
|
||||
} as unknown) as History;
|
||||
it("doesn't push a new history when x is not defined", () => {
|
||||
onBrushEnd({ x: undefined, history });
|
||||
expect(history.push).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('pushes a new history with time range converted to ISO', () => {
|
||||
onBrushEnd({ x: [1593409448167, 1593415727797], history });
|
||||
expect(history.push).toBeCalledWith({
|
||||
search: 'rangeFrom=2020-06-29T05:44:08.167Z&rangeTo=2020-06-29T07:28:47.797Z',
|
||||
});
|
||||
});
|
||||
|
||||
it('pushes a new history keeping current search', () => {
|
||||
history.location.search = '?foo=bar';
|
||||
onBrushEnd({ x: [1593409448167, 1593415727797], history });
|
||||
expect(history.push).toBeCalledWith({
|
||||
search: 'foo=bar&rangeFrom=2020-06-29T05:44:08.167Z&rangeTo=2020-06-29T07:28:47.797Z',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { XYBrushArea } from '@elastic/charts';
|
||||
import { History } from 'history';
|
||||
import { fromQuery, toQuery } from '../../../utils/url';
|
||||
|
||||
export const onBrushEnd = ({ x, history }: { x: XYBrushArea['x']; history: History }) => {
|
||||
if (x) {
|
||||
const start = x[0];
|
||||
const end = x[1];
|
||||
|
||||
const currentSearch = toQuery(history.location.search);
|
||||
const nextSearch = {
|
||||
rangeFrom: new Date(start).toISOString(),
|
||||
rangeTo: new Date(end).toISOString(),
|
||||
};
|
||||
history.push({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...currentSearch,
|
||||
...nextSearch,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render } from '../../../utils/test_helper';
|
||||
import { SectionContainer } from './';
|
||||
|
||||
describe('SectionContainer', () => {
|
||||
it('renders section without app link', () => {
|
||||
const component = render(
|
||||
<SectionContainer title="Foo" hasError={false}>
|
||||
<div>I am a very nice component</div>
|
||||
</SectionContainer>
|
||||
);
|
||||
expect(component.getByText('I am a very nice component')).toBeInTheDocument();
|
||||
expect(component.getByText('Foo')).toBeInTheDocument();
|
||||
expect(component.queryAllByText('View in app')).toEqual([]);
|
||||
});
|
||||
it('renders section with app link', () => {
|
||||
const component = render(
|
||||
<SectionContainer title="Foo" appLink="/foo/bar" hasError={false}>
|
||||
<div>I am a very nice component</div>
|
||||
</SectionContainer>
|
||||
);
|
||||
expect(component.getByText('I am a very nice component')).toBeInTheDocument();
|
||||
expect(component.getByText('Foo')).toBeInTheDocument();
|
||||
expect(component.getByText('View in app')).toBeInTheDocument();
|
||||
});
|
||||
it('renders section with error', () => {
|
||||
const component = render(
|
||||
<SectionContainer title="Foo" hasError={true}>
|
||||
<div>I am a very nice component</div>
|
||||
</SectionContainer>
|
||||
);
|
||||
expect(component.queryByText('I am a very nice component')).not.toBeInTheDocument();
|
||||
expect(component.getByText('Foo')).toBeInTheDocument();
|
||||
expect(
|
||||
component.getByText('An error happened when trying to fetch data. Please try again')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiAccordion, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { ErrorPanel } from './error_panel';
|
||||
import { usePluginContext } from '../../../hooks/use_plugin_context';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
hasError: boolean;
|
||||
children: React.ReactNode;
|
||||
minHeight?: number;
|
||||
appLink?: string;
|
||||
appLinkName?: string;
|
||||
}
|
||||
|
||||
export const SectionContainer = ({ title, appLink, children, hasError, appLinkName }: Props) => {
|
||||
const { core } = usePluginContext();
|
||||
return (
|
||||
<EuiAccordion
|
||||
initialIsOpen
|
||||
id={title}
|
||||
buttonContentClassName="accordion-button"
|
||||
buttonContent={
|
||||
<EuiTitle size="s">
|
||||
<h5>{title}</h5>
|
||||
</EuiTitle>
|
||||
}
|
||||
extraAction={
|
||||
appLink && (
|
||||
<EuiLink href={core.http.basePath.prepend(appLink)}>
|
||||
<EuiText size="s">
|
||||
{appLinkName
|
||||
? appLinkName
|
||||
: i18n.translate('xpack.observability.chart.viewInAppLabel', {
|
||||
defaultMessage: 'View in app',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiLink>
|
||||
)
|
||||
}
|
||||
>
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiPanel hasShadow>
|
||||
{hasError ? (
|
||||
<ErrorPanel />
|
||||
) : (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</>
|
||||
</EuiAccordion>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts';
|
||||
import { EuiFlexGroup, EuiFlexItem, euiPaletteColorBlind } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { isEmpty } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React, { Fragment } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { SectionContainer } from '../';
|
||||
import { getDataHandler } from '../../../../data_handler';
|
||||
import { useChartTheme } from '../../../../hooks/use_chart_theme';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { LogsFetchDataResponse } from '../../../../typings';
|
||||
import { formatStatValue } from '../../../../utils/format_stat_value';
|
||||
import { ChartContainer } from '../../chart_container';
|
||||
import { StyledStat } from '../../styled_stat';
|
||||
import { onBrushEnd } from '../helper';
|
||||
|
||||
interface Props {
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
bucketSize?: string;
|
||||
}
|
||||
|
||||
function getColorPerItem(series?: LogsFetchDataResponse['series']) {
|
||||
if (!series) {
|
||||
return {};
|
||||
}
|
||||
const availableColors = euiPaletteColorBlind({
|
||||
rotations: Math.ceil(Object.keys(series).length / 10),
|
||||
});
|
||||
const colorsPerItem = Object.keys(series).reduce((acc: Record<string, string>, key, index) => {
|
||||
acc[key] = availableColors[index];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return colorsPerItem;
|
||||
}
|
||||
|
||||
export const LogsSection = ({ startTime, endTime, bucketSize }: Props) => {
|
||||
const history = useHistory();
|
||||
|
||||
const { data, status } = useFetcher(() => {
|
||||
if (startTime && endTime && bucketSize) {
|
||||
return getDataHandler('infra_logs')?.fetchData({ startTime, endTime, bucketSize });
|
||||
}
|
||||
}, [startTime, endTime, bucketSize]);
|
||||
|
||||
const min = moment.utc(startTime).valueOf();
|
||||
const max = moment.utc(endTime).valueOf();
|
||||
|
||||
const formatter = niceTimeFormatter([min, max]);
|
||||
|
||||
const { title, appLink, stats, series } = data || {};
|
||||
|
||||
const colorsPerItem = getColorPerItem(series);
|
||||
|
||||
const isLoading = status === FETCH_STATUS.LOADING;
|
||||
|
||||
return (
|
||||
<SectionContainer
|
||||
title={title || 'Logs'}
|
||||
appLink={appLink}
|
||||
hasError={status === FETCH_STATUS.FAILURE}
|
||||
>
|
||||
<EuiTitle size="xs">
|
||||
<h4>
|
||||
{i18n.translate('xpack.observability.overview.logs.subtitle', {
|
||||
defaultMessage: 'Logs rate per minute',
|
||||
})}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup>
|
||||
{!stats || isEmpty(stats) ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledStat isLoading={isLoading} />
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
Object.keys(stats).map((key) => {
|
||||
const stat = stats[key];
|
||||
return (
|
||||
<EuiFlexItem grow={false} key={key}>
|
||||
<StyledStat
|
||||
title={formatStatValue(stat)}
|
||||
description={stat.label}
|
||||
isLoading={isLoading}
|
||||
color={colorsPerItem[key]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<ChartContainer isInitialLoad={isLoading && !data}>
|
||||
<Settings
|
||||
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
|
||||
theme={useChartTheme()}
|
||||
showLegend
|
||||
legendPosition={Position.Right}
|
||||
xDomain={{ min, max }}
|
||||
showLegendExtra
|
||||
/>
|
||||
{series &&
|
||||
Object.keys(series).map((key) => {
|
||||
const serie = series[key];
|
||||
const chartData = serie.coordinates.map((coordinate) => ({
|
||||
...coordinate,
|
||||
g: serie.label,
|
||||
}));
|
||||
return (
|
||||
<Fragment key={key}>
|
||||
<BarSeries
|
||||
id={key}
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
stackAccessors={['x']}
|
||||
splitSeriesAccessors={['g']}
|
||||
data={chartData}
|
||||
color={colorsPerItem[key]}
|
||||
/>
|
||||
<Axis
|
||||
id="x-axis"
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks={false}
|
||||
showOverlappingLabels={false}
|
||||
tickFormat={formatter}
|
||||
/>
|
||||
<Axis
|
||||
id="y-axis"
|
||||
showGridLines
|
||||
position={Position.Left}
|
||||
tickFormat={(d: number) => numeral(d).format('0a')}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</ChartContainer>
|
||||
</SectionContainer>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { AreaSeries, ScaleType, Settings } from '@elastic/charts';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useContext } from 'react';
|
||||
import styled, { ThemeContext } from 'styled-components';
|
||||
import { SectionContainer } from '../';
|
||||
import { getDataHandler } from '../../../../data_handler';
|
||||
import { useChartTheme } from '../../../../hooks/use_chart_theme';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { Series } from '../../../../typings';
|
||||
import { ChartContainer } from '../../chart_container';
|
||||
import { StyledStat } from '../../styled_stat';
|
||||
|
||||
interface Props {
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
bucketSize?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* EuiProgress doesn't support custom color, when it does this component can be removed.
|
||||
*/
|
||||
const StyledProgress = styled.div<{ color?: string }>`
|
||||
progress {
|
||||
&.euiProgress--native {
|
||||
&::-webkit-progress-value {
|
||||
background-color: ${(props) => props.color};
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background-color: ${(props) => props.color};
|
||||
}
|
||||
}
|
||||
|
||||
&.euiProgress--indeterminate {
|
||||
&:before {
|
||||
background-color: ${(props) => props.color};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MetricsSection = ({ startTime, endTime, bucketSize }: Props) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const { data, status } = useFetcher(() => {
|
||||
if (startTime && endTime && bucketSize) {
|
||||
return getDataHandler('infra_metrics')?.fetchData({ startTime, endTime, bucketSize });
|
||||
}
|
||||
}, [startTime, endTime, bucketSize]);
|
||||
|
||||
const isLoading = status === FETCH_STATUS.LOADING;
|
||||
|
||||
const { title = 'Metrics', appLink, stats, series } = data || {};
|
||||
|
||||
const cpuColor = theme.eui.euiColorVis7;
|
||||
const memoryColor = theme.eui.euiColorVis0;
|
||||
const inboundTrafficColor = theme.eui.euiColorVis3;
|
||||
const outboundTrafficColor = theme.eui.euiColorVis2;
|
||||
|
||||
return (
|
||||
<SectionContainer
|
||||
minHeight={135}
|
||||
title={title}
|
||||
appLink={appLink}
|
||||
hasError={status === FETCH_STATUS.FAILURE}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<StyledStat
|
||||
title={numeral(stats?.hosts.value).format('0a')}
|
||||
description={i18n.translate('xpack.observability.overview.metrics.hosts', {
|
||||
defaultMessage: 'Hosts',
|
||||
})}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<StyledStat
|
||||
title={numeral(stats?.cpu.value).format('0.0%')}
|
||||
description={i18n.translate('xpack.observability.overview.metrics.cpuUsage', {
|
||||
defaultMessage: 'CPU Usage',
|
||||
})}
|
||||
isLoading={isLoading}
|
||||
color={cpuColor}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<StyledProgress color={cpuColor}>
|
||||
<EuiProgress
|
||||
value={stats?.cpu.value}
|
||||
max={1}
|
||||
style={{ width: '100px' }}
|
||||
color="accent"
|
||||
/>
|
||||
</StyledProgress>
|
||||
</StyledStat>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<StyledStat
|
||||
title={numeral(stats?.memory.value).format('0.0%')}
|
||||
description={i18n.translate('xpack.observability.overview.metrics.memoryUsage', {
|
||||
defaultMessage: 'Memory usage',
|
||||
})}
|
||||
isLoading={isLoading}
|
||||
color={memoryColor}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<StyledProgress color={memoryColor}>
|
||||
<EuiProgress value={stats?.memory.value} max={1} style={{ width: '100px' }} />
|
||||
</StyledProgress>
|
||||
</StyledStat>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<StyledStat
|
||||
title={`${numeral(stats?.inboundTraffic.value).format('0.0b')}/s`}
|
||||
description={i18n.translate('xpack.observability.overview.metrics.inboundTraffic', {
|
||||
defaultMessage: 'Inbound traffic',
|
||||
})}
|
||||
isLoading={isLoading}
|
||||
color={inboundTrafficColor}
|
||||
>
|
||||
<AreaChart
|
||||
serie={series?.inboundTraffic}
|
||||
isLoading={isLoading}
|
||||
color={inboundTrafficColor}
|
||||
/>
|
||||
</StyledStat>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<StyledStat
|
||||
title={`${numeral(stats?.outboundTraffic.value).format('0.0b')}/s`}
|
||||
description={i18n.translate('xpack.observability.overview.metrics.outboundTraffic', {
|
||||
defaultMessage: 'Outbound traffic',
|
||||
})}
|
||||
isLoading={isLoading}
|
||||
color={outboundTrafficColor}
|
||||
>
|
||||
<AreaChart
|
||||
serie={series?.outboundTraffic}
|
||||
isLoading={isLoading}
|
||||
color={outboundTrafficColor}
|
||||
/>
|
||||
</StyledStat>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</SectionContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const AreaChart = ({
|
||||
serie,
|
||||
isLoading,
|
||||
color,
|
||||
}: {
|
||||
serie?: Series;
|
||||
isLoading: boolean;
|
||||
color: string;
|
||||
}) => {
|
||||
const chartTheme = useChartTheme();
|
||||
|
||||
return (
|
||||
<ChartContainer height={30} isInitialLoad={isLoading && !serie} iconSize="m">
|
||||
<Settings theme={chartTheme} showLegend={false} tooltip="none" />
|
||||
{serie && (
|
||||
<AreaSeries
|
||||
id="area"
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
data={serie.coordinates}
|
||||
color={color}
|
||||
/>
|
||||
)}
|
||||
</ChartContainer>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Axis,
|
||||
BarSeries,
|
||||
niceTimeFormatter,
|
||||
Position,
|
||||
ScaleType,
|
||||
Settings,
|
||||
TickFormatter,
|
||||
} from '@elastic/charts';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import React, { useContext } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import { SectionContainer } from '../';
|
||||
import { getDataHandler } from '../../../../data_handler';
|
||||
import { useChartTheme } from '../../../../hooks/use_chart_theme';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { Series } from '../../../../typings';
|
||||
import { ChartContainer } from '../../chart_container';
|
||||
import { StyledStat } from '../../styled_stat';
|
||||
import { onBrushEnd } from '../helper';
|
||||
|
||||
interface Props {
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
bucketSize?: string;
|
||||
}
|
||||
|
||||
export const UptimeSection = ({ startTime, endTime, bucketSize }: Props) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const history = useHistory();
|
||||
|
||||
const { data, status } = useFetcher(() => {
|
||||
if (startTime && endTime && bucketSize) {
|
||||
return getDataHandler('uptime')?.fetchData({ startTime, endTime, bucketSize });
|
||||
}
|
||||
}, [startTime, endTime, bucketSize]);
|
||||
|
||||
const min = moment.utc(startTime).valueOf();
|
||||
const max = moment.utc(endTime).valueOf();
|
||||
const formatter = niceTimeFormatter([min, max]);
|
||||
|
||||
const isLoading = status === FETCH_STATUS.LOADING;
|
||||
|
||||
const { title = 'Uptime', appLink, stats, series } = data || {};
|
||||
|
||||
const downColor = theme.eui.euiColorVis2;
|
||||
const upColor = theme.eui.euiColorLightShade;
|
||||
|
||||
return (
|
||||
<SectionContainer
|
||||
minHeight={273}
|
||||
title={title}
|
||||
appLink={appLink}
|
||||
hasError={status === FETCH_STATUS.FAILURE}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
{/* Stats section */}
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledStat
|
||||
title={numeral(stats?.monitors.value).format('0a')}
|
||||
description={i18n.translate('xpack.observability.overview.uptime.monitors', {
|
||||
defaultMessage: 'Monitors',
|
||||
})}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledStat
|
||||
title={numeral(stats?.up.value).format('0a')}
|
||||
description={i18n.translate('xpack.observability.overview.uptime.up', {
|
||||
defaultMessage: 'Up',
|
||||
})}
|
||||
isLoading={isLoading}
|
||||
color={upColor}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<StyledStat
|
||||
title={numeral(stats?.down.value).format('0a')}
|
||||
description={i18n.translate('xpack.observability.overview.uptime.down', {
|
||||
defaultMessage: 'Down',
|
||||
})}
|
||||
isLoading={isLoading}
|
||||
color={downColor}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{/* Chart section */}
|
||||
<ChartContainer isInitialLoad={isLoading && !data}>
|
||||
<Settings
|
||||
onBrushEnd={({ x }) => onBrushEnd({ x, history })}
|
||||
theme={useChartTheme()}
|
||||
showLegend={false}
|
||||
legendPosition={Position.Right}
|
||||
xDomain={{ min, max }}
|
||||
/>
|
||||
<UptimeBarSeries
|
||||
id="down"
|
||||
label={i18n.translate('xpack.observability.overview.uptime.chart.down', {
|
||||
defaultMessage: 'Down',
|
||||
})}
|
||||
series={series?.down}
|
||||
ticktFormatter={formatter}
|
||||
color={downColor}
|
||||
/>
|
||||
<UptimeBarSeries
|
||||
id="up"
|
||||
label={i18n.translate('xpack.observability.overview.uptime.chart.up', {
|
||||
defaultMessage: 'Up',
|
||||
})}
|
||||
series={series?.up}
|
||||
ticktFormatter={formatter}
|
||||
color={upColor}
|
||||
/>
|
||||
</ChartContainer>
|
||||
</SectionContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const UptimeBarSeries = ({
|
||||
id,
|
||||
label,
|
||||
series,
|
||||
color,
|
||||
ticktFormatter,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
series?: Series;
|
||||
color: string;
|
||||
ticktFormatter: TickFormatter;
|
||||
}) => {
|
||||
if (!series) {
|
||||
return null;
|
||||
}
|
||||
const chartData = series.coordinates.map((coordinate) => ({
|
||||
...coordinate,
|
||||
g: label,
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
<BarSeries
|
||||
id={id}
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
color={color}
|
||||
stackAccessors={['x']}
|
||||
splitSeriesAccessors={['g']}
|
||||
data={chartData}
|
||||
/>
|
||||
<Axis
|
||||
id="x-axis"
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks={false}
|
||||
showOverlappingLabels={false}
|
||||
tickFormat={ticktFormatter}
|
||||
/>
|
||||
<Axis
|
||||
id="y-axis"
|
||||
showGridLines
|
||||
position={Position.Left}
|
||||
tickFormat={(x: any) => numeral(x).format('0a')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import styled from 'styled-components';
|
||||
import { EuiStat } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { EuiStatProps } from '@elastic/eui/src/components/stat/stat';
|
||||
|
||||
const Stat = styled(EuiStat)`
|
||||
.euiStat__title {
|
||||
color: ${(props) => props.color};
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props extends Partial<EuiStatProps> {
|
||||
children?: React.ReactNode;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const EMPTY_VALUE = '--';
|
||||
|
||||
export const StyledStat = (props: Props) => {
|
||||
const { description = EMPTY_VALUE, title = EMPTY_VALUE, ...rest } = props;
|
||||
return <Stat description={description} title={title} titleSize="s" {...rest} />;
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiSuperDatePicker } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { UI_SETTINGS, useKibanaUISettings } from '../../../hooks/use_kibana_ui_settings';
|
||||
import { fromQuery, toQuery } from '../../../utils/url';
|
||||
|
||||
export interface TimePickerTime {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface TimePickerQuickRange extends TimePickerTime {
|
||||
display: string;
|
||||
}
|
||||
|
||||
export interface TimePickerRefreshInterval {
|
||||
pause: boolean;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
refreshPaused: boolean;
|
||||
refreshInterval: number;
|
||||
}
|
||||
|
||||
export const DatePicker = ({ rangeFrom, rangeTo, refreshPaused, refreshInterval }: Props) => {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
const timePickerQuickRanges = useKibanaUISettings<TimePickerQuickRange[]>(
|
||||
UI_SETTINGS.TIMEPICKER_QUICK_RANGES
|
||||
);
|
||||
|
||||
const commonlyUsedRanges = timePickerQuickRanges.map(({ from, to, display }) => ({
|
||||
start: from,
|
||||
end: to,
|
||||
label: display,
|
||||
}));
|
||||
|
||||
function updateUrl(nextQuery: {
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
refreshPaused?: boolean;
|
||||
refreshInterval?: number;
|
||||
}) {
|
||||
history.push({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
...nextQuery,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function onRefreshChange({
|
||||
isPaused,
|
||||
refreshInterval: interval,
|
||||
}: {
|
||||
isPaused: boolean;
|
||||
refreshInterval: number;
|
||||
}) {
|
||||
updateUrl({ refreshPaused: isPaused, refreshInterval: interval });
|
||||
}
|
||||
|
||||
function onTimeChange({ start, end }: { start: string; end: string }) {
|
||||
updateUrl({ rangeFrom: start, rangeTo: end });
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiSuperDatePicker
|
||||
start={rangeFrom}
|
||||
end={rangeTo}
|
||||
onTimeChange={onTimeChange}
|
||||
isPaused={refreshPaused}
|
||||
refreshInterval={refreshInterval}
|
||||
onRefreshChange={onRefreshChange}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
onRefresh={onTimeChange}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -17,9 +17,20 @@ export function registerDataHandler<T extends ObservabilityApp>({
|
|||
dataHandlers[appName] = { fetchData, hasData };
|
||||
}
|
||||
|
||||
export function unregisterDataHandler<T extends ObservabilityApp>({ appName }: { appName: T }) {
|
||||
delete dataHandlers[appName];
|
||||
}
|
||||
|
||||
export function getDataHandler<T extends ObservabilityApp>(appName: T) {
|
||||
const dataHandler = dataHandlers[appName];
|
||||
if (dataHandler) {
|
||||
return dataHandler as DataHandler<T>;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchHasData() {
|
||||
const apps: ObservabilityApp[] = ['apm', 'uptime', 'infra_logs', 'infra_metrics'];
|
||||
const promises = apps.map((app) => getDataHandler(app)?.hasData());
|
||||
const [apm, uptime, logs, metrics] = await Promise.all(promises);
|
||||
return { apm, uptime, infra_logs: logs, infra_metrics: metrics };
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
|
||||
export function useChartTheme() {
|
||||
const theme = useContext(ThemeContext);
|
||||
return theme.darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme;
|
||||
}
|
84
x-pack/plugins/observability/public/hooks/use_fetcher.tsx
Normal file
84
x-pack/plugins/observability/public/hooks/use_fetcher.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
|
||||
export enum FETCH_STATUS {
|
||||
LOADING = 'loading',
|
||||
SUCCESS = 'success',
|
||||
FAILURE = 'failure',
|
||||
PENDING = 'pending',
|
||||
}
|
||||
|
||||
export interface FetcherResult<Data> {
|
||||
data?: Data;
|
||||
status: FETCH_STATUS;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
// fetcher functions can return undefined OR a promise. Previously we had a more simple type
|
||||
// but it led to issues when using object destructuring with default values
|
||||
type InferResponseType<TReturn> = Exclude<TReturn, undefined> extends Promise<infer TResponseType>
|
||||
? TResponseType
|
||||
: unknown;
|
||||
|
||||
export function useFetcher<TReturn>(
|
||||
fn: () => TReturn,
|
||||
fnDeps: any[],
|
||||
options: {
|
||||
preservePreviousData?: boolean;
|
||||
} = {}
|
||||
): FetcherResult<InferResponseType<TReturn>> & { refetch: () => void } {
|
||||
const { preservePreviousData = true } = options;
|
||||
|
||||
const [result, setResult] = useState<FetcherResult<InferResponseType<TReturn>>>({
|
||||
data: undefined,
|
||||
status: FETCH_STATUS.PENDING,
|
||||
});
|
||||
const [counter, setCounter] = useState(0);
|
||||
useEffect(() => {
|
||||
async function doFetch() {
|
||||
const promise = fn();
|
||||
if (!promise) {
|
||||
return;
|
||||
}
|
||||
|
||||
setResult((prevResult) => ({
|
||||
data: preservePreviousData ? prevResult.data : undefined,
|
||||
status: FETCH_STATUS.LOADING,
|
||||
error: undefined,
|
||||
}));
|
||||
|
||||
try {
|
||||
const data = await promise;
|
||||
setResult({
|
||||
data,
|
||||
status: FETCH_STATUS.SUCCESS,
|
||||
error: undefined,
|
||||
} as FetcherResult<InferResponseType<TReturn>>);
|
||||
} catch (e) {
|
||||
setResult((prevResult) => ({
|
||||
data: preservePreviousData ? prevResult.data : undefined,
|
||||
status: FETCH_STATUS.FAILURE,
|
||||
error: e,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
doFetch();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [counter, ...fnDeps]);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...result,
|
||||
refetch: () => {
|
||||
// this will invalidate the deps to `useEffect` and will result in a new request
|
||||
setCounter((count) => count + 1);
|
||||
},
|
||||
};
|
||||
}, [result]);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { usePluginContext } from './use_plugin_context';
|
||||
import { UI_SETTINGS } from '../../../../../src/plugins/data/public';
|
||||
|
||||
export { UI_SETTINGS };
|
||||
|
||||
type SettingKeys = keyof typeof UI_SETTINGS;
|
||||
type SettingValues = typeof UI_SETTINGS[SettingKeys];
|
||||
|
||||
export function useKibanaUISettings<T>(key: SettingValues): T {
|
||||
const { core } = usePluginContext();
|
||||
return core.uiSettings.get<T>(key);
|
||||
}
|
52
x-pack/plugins/observability/public/hooks/use_url_params.tsx
Normal file
52
x-pack/plugins/observability/public/hooks/use_url_params.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { isLeft } from 'fp-ts/lib/Either';
|
||||
import { PathReporter } from 'io-ts/lib/PathReporter';
|
||||
import { Params } from '../routes';
|
||||
|
||||
function getQueryParams(location: ReturnType<typeof useLocation>) {
|
||||
const urlSearchParms = new URLSearchParams(location.search);
|
||||
const queryParams: Record<string, string> = {};
|
||||
urlSearchParms.forEach((value, key) => {
|
||||
queryParams[key] = value;
|
||||
});
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts query and path params from the url and validate it against the type defined in the route file.
|
||||
* It removes any aditional item which is not declared in the type.
|
||||
* @param params
|
||||
*/
|
||||
export function useUrlParams(params: Params) {
|
||||
const location = useLocation();
|
||||
const pathParams = useParams();
|
||||
const queryParams = getQueryParams(location);
|
||||
|
||||
const rts = {
|
||||
queryRt: params.query ? t.exact(params.query) : t.strict({}),
|
||||
pathRt: params.path ? t.exact(params.path) : t.strict({}),
|
||||
};
|
||||
|
||||
const queryResult = rts.queryRt.decode(queryParams);
|
||||
const pathResult = rts.pathRt.decode(pathParams);
|
||||
if (isLeft(queryResult)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(PathReporter.report(queryResult)[0]);
|
||||
}
|
||||
|
||||
if (isLeft(pathResult)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(PathReporter.report(pathResult)[0]);
|
||||
}
|
||||
|
||||
return {
|
||||
query: isLeft(queryResult) ? {} : queryResult.right,
|
||||
path: isLeft(pathResult) ? {} : pathResult.right,
|
||||
};
|
||||
}
|
|
@ -15,7 +15,7 @@ export const plugin: PluginInitializer<ObservabilityPluginSetup, ObservabilityPl
|
|||
return new Plugin(context);
|
||||
};
|
||||
|
||||
export * from './components/action_menu';
|
||||
export * from './components/shared/action_menu/';
|
||||
|
||||
export {
|
||||
useTrackPageview,
|
||||
|
|
|
@ -3,162 +3,24 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { fetchHasData } from '../../data_handler';
|
||||
import { useFetcher } from '../../hooks/use_fetcher';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCard,
|
||||
EuiFlexGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiImage,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { appsSection } from './section';
|
||||
export const HomePage = () => {
|
||||
const history = useHistory();
|
||||
const { data = {} } = useFetcher(() => fetchHasData(), []);
|
||||
|
||||
const Container = styled.div`
|
||||
min-height: calc(100vh - 48px);
|
||||
background: ${(props) => props.theme.eui.euiColorEmptyShade};
|
||||
`;
|
||||
const values = Object.values(data);
|
||||
const hasSomeData = values.length ? values.some((hasData) => hasData) : null;
|
||||
|
||||
const Title = styled.div`
|
||||
background-color: ${(props) => props.theme.eui.euiPageBackgroundColor};
|
||||
border-bottom: ${(props) => props.theme.eui.euiBorderThin};
|
||||
`;
|
||||
if (hasSomeData === true) {
|
||||
history.push({ pathname: '/overview' });
|
||||
}
|
||||
if (hasSomeData === false) {
|
||||
history.push({ pathname: '/landing' });
|
||||
}
|
||||
|
||||
const Page = styled.div`
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
const EuiCardWithoutPadding = styled(EuiCard)`
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export const Home = () => {
|
||||
const { core } = usePluginContext();
|
||||
|
||||
useEffect(() => {
|
||||
core.chrome.setBreadcrumbs([
|
||||
{
|
||||
text: i18n.translate('xpack.observability.home.breadcrumb.observability', {
|
||||
defaultMessage: 'Observability',
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: i18n.translate('xpack.observability.home.breadcrumb.gettingStarted', {
|
||||
defaultMessage: 'Getting started',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
}, [core]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Title>
|
||||
<Page>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="logoObservability" size="xxl" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="m">
|
||||
<h1>
|
||||
{i18n.translate('xpack.observability.home.title', {
|
||||
defaultMessage: 'Observability',
|
||||
})}
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="xxl" />
|
||||
</Page>
|
||||
</Title>
|
||||
<Page>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiFlexGroup direction="column">
|
||||
{/* title and description */}
|
||||
<EuiFlexItem style={{ maxWidth: '50%' }}>
|
||||
<EuiTitle size="s">
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.home.sectionTitle', {
|
||||
defaultMessage: 'Unified visibility across your entire ecosystem',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="s" color="subdued">
|
||||
{i18n.translate('xpack.observability.home.sectionsubtitle', {
|
||||
defaultMessage:
|
||||
'Monitor, analyze, and react to events happening anywhere in your environment by bringing logs, metrics, and traces together at scale in a single stack.',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
{/* Apps sections */}
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGrid columns={2}>
|
||||
{appsSection.map((app) => (
|
||||
<EuiFlexItem key={app.id}>
|
||||
<EuiCardWithoutPadding
|
||||
display="plain"
|
||||
layout="horizontal"
|
||||
icon={<EuiIcon size="l" type={app.icon} />}
|
||||
title={
|
||||
<EuiTitle size="xs" className="title">
|
||||
<h3>{app.title}</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={app.description}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGrid>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiImage
|
||||
size="xl"
|
||||
alt="observability overview image"
|
||||
url={core.http.basePath.prepend(
|
||||
'/plugins/observability/assets/observability_overview.png'
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
{/* Get started button */}
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="sortRight"
|
||||
iconSide="right"
|
||||
href={core.http.basePath.prepend('/app/home#/tutorial_directory/logging')}
|
||||
>
|
||||
{i18n.translate('xpack.observability.home.getStatedButton', {
|
||||
defaultMessage: 'Get started',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Page>
|
||||
</Container>
|
||||
);
|
||||
return <></>;
|
||||
};
|
||||
|
|
|
@ -4,19 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface ISection {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
href?: string;
|
||||
target?: '_blank';
|
||||
}
|
||||
import { ISection } from '../../typings/section';
|
||||
|
||||
export const appsSection: ISection[] = [
|
||||
{
|
||||
id: 'logs',
|
||||
id: 'infra_logs',
|
||||
title: i18n.translate('xpack.observability.section.apps.logs.title', {
|
||||
defaultMessage: 'Logs',
|
||||
}),
|
||||
|
@ -25,6 +17,7 @@ export const appsSection: ISection[] = [
|
|||
defaultMessage:
|
||||
'Centralize logs from any source. Search, tail, automate anomaly detection, and visualize trends so you can take action quicker.',
|
||||
}),
|
||||
href: 'https://www.elastic.co',
|
||||
},
|
||||
{
|
||||
id: 'apm',
|
||||
|
@ -36,9 +29,10 @@ export const appsSection: ISection[] = [
|
|||
defaultMessage:
|
||||
'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.',
|
||||
}),
|
||||
href: 'https://www.elastic.co',
|
||||
},
|
||||
{
|
||||
id: 'metrics',
|
||||
id: 'infra_metrics',
|
||||
title: i18n.translate('xpack.observability.section.apps.metrics.title', {
|
||||
defaultMessage: 'Metrics',
|
||||
}),
|
||||
|
@ -47,6 +41,7 @@ export const appsSection: ISection[] = [
|
|||
defaultMessage:
|
||||
'Analyze metrics from your infrastructure, apps, and services. Discover trends, forecast behavior, get alerts on anomalies, and more.',
|
||||
}),
|
||||
href: 'https://www.elastic.co',
|
||||
},
|
||||
{
|
||||
id: 'uptime',
|
||||
|
@ -58,5 +53,6 @@ export const appsSection: ISection[] = [
|
|||
defaultMessage:
|
||||
'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users’ experience.',
|
||||
}),
|
||||
href: 'https://www.elastic.co',
|
||||
},
|
||||
];
|
||||
|
|
116
x-pack/plugins/observability/public/pages/landing/index.tsx
Normal file
116
x-pack/plugins/observability/public/pages/landing/index.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCard,
|
||||
EuiFlexGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiImage,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useContext } from 'react';
|
||||
import styled, { ThemeContext } from 'styled-components';
|
||||
import { WithHeaderLayout } from '../../components/app/layout/with_header';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { appsSection } from '../home/section';
|
||||
|
||||
const EuiCardWithoutPadding = styled(EuiCard)`
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export const LandingPage = () => {
|
||||
const { core } = usePluginContext();
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout
|
||||
restrictWidth={1200}
|
||||
headerColor={theme.eui.euiPageBackgroundColor}
|
||||
bodyColor={theme.eui.euiColorEmptyShade}
|
||||
>
|
||||
<EuiFlexGroup direction="column">
|
||||
{/* title and description */}
|
||||
<EuiFlexItem style={{ maxWidth: '50%' }}>
|
||||
<EuiTitle size="s">
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.home.sectionTitle', {
|
||||
defaultMessage: 'Unified visibility across your entire ecosystem',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="s" color="subdued">
|
||||
{i18n.translate('xpack.observability.home.sectionsubtitle', {
|
||||
defaultMessage:
|
||||
'Monitor, analyze, and react to events happening anywhere in your environment by bringing logs, metrics, and traces together at scale in a single stack.',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
|
||||
{/* Apps sections */}
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGrid columns={2}>
|
||||
{appsSection.map((app) => (
|
||||
<EuiFlexItem key={app.id}>
|
||||
<EuiCardWithoutPadding
|
||||
display="plain"
|
||||
layout="horizontal"
|
||||
icon={<EuiIcon size="l" type={app.icon} />}
|
||||
title={
|
||||
<EuiTitle size="xs" className="title">
|
||||
<h3>{app.title}</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={app.description}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGrid>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiImage
|
||||
size="xl"
|
||||
alt="observability overview image"
|
||||
url={core.http.basePath.prepend(
|
||||
'/plugins/observability/assets/observability_overview.png'
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiSpacer size="xxl" />
|
||||
|
||||
{/* Get started button */}
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="sortRight"
|
||||
iconSide="right"
|
||||
href={core.http.basePath.prepend('/app/home#/tutorial_directory/logging')}
|
||||
>
|
||||
{i18n.translate('xpack.observability.home.getStatedButton', {
|
||||
defaultMessage: 'Get started',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AppMountContext } from 'kibana/public';
|
||||
import { ISection } from '../../typings/section';
|
||||
|
||||
export const getEmptySections = ({ core }: { core: AppMountContext['core'] }): ISection[] => {
|
||||
return [
|
||||
{
|
||||
id: 'infra_logs',
|
||||
title: i18n.translate('xpack.observability.emptySection.apps.logs.title', {
|
||||
defaultMessage: 'Logs',
|
||||
}),
|
||||
icon: 'logoLogging',
|
||||
description: i18n.translate('xpack.observability.emptySection.apps.logs.description', {
|
||||
defaultMessage:
|
||||
'Centralize logs from any source. Search, tail, automate anomaly detection, and visualize trends so you can take action quicker.',
|
||||
}),
|
||||
linkTitle: i18n.translate('xpack.observability.emptySection.apps.logs.link', {
|
||||
defaultMessage: 'Install Filebeat',
|
||||
}),
|
||||
href: 'https://www.elastic.co',
|
||||
},
|
||||
{
|
||||
id: 'apm',
|
||||
title: i18n.translate('xpack.observability.emptySection.apps.apm.title', {
|
||||
defaultMessage: 'APM',
|
||||
}),
|
||||
icon: 'logoAPM',
|
||||
description: i18n.translate('xpack.observability.emptySection.apps.apm.description', {
|
||||
defaultMessage:
|
||||
'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.',
|
||||
}),
|
||||
linkTitle: i18n.translate('xpack.observability.emptySection.apps.apm.link', {
|
||||
defaultMessage: 'Install agent',
|
||||
}),
|
||||
href: 'https://www.elastic.co',
|
||||
},
|
||||
{
|
||||
id: 'infra_metrics',
|
||||
title: i18n.translate('xpack.observability.emptySection.apps.metrics.title', {
|
||||
defaultMessage: 'Metrics',
|
||||
}),
|
||||
icon: 'logoMetrics',
|
||||
description: i18n.translate('xpack.observability.emptySection.apps.metrics.description', {
|
||||
defaultMessage:
|
||||
'Analyze metrics from your infrastructure, apps, and services. Discover trends, forecast behavior, get alerts on anomalies, and more.',
|
||||
}),
|
||||
linkTitle: i18n.translate('xpack.observability.emptySection.apps.metrics.link', {
|
||||
defaultMessage: 'Install metrics module',
|
||||
}),
|
||||
href: 'https://www.elastic.co',
|
||||
},
|
||||
{
|
||||
id: 'uptime',
|
||||
title: i18n.translate('xpack.observability.emptySection.apps.uptime.title', {
|
||||
defaultMessage: 'Uptime',
|
||||
}),
|
||||
icon: 'logoUptime',
|
||||
description: i18n.translate('xpack.observability.emptySection.apps.uptime.description', {
|
||||
defaultMessage:
|
||||
'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users’ experience.',
|
||||
}),
|
||||
linkTitle: i18n.translate('xpack.observability.emptySection.apps.uptime.link', {
|
||||
defaultMessage: 'Install Heartbeat',
|
||||
}),
|
||||
href: 'https://www.elastic.co',
|
||||
},
|
||||
{
|
||||
id: 'alert',
|
||||
title: i18n.translate('xpack.observability.emptySection.apps.alert.title', {
|
||||
defaultMessage: 'No alerts found.',
|
||||
}),
|
||||
icon: 'watchesApp',
|
||||
description: i18n.translate('xpack.observability.emptySection.apps.alert.description', {
|
||||
defaultMessage:
|
||||
'503 errors stacking up. Applications not responding. CPU and RAM utilization jumping. See these warnings as they happen - not as part of the post-mortem.',
|
||||
}),
|
||||
linkTitle: i18n.translate('xpack.observability.emptySection.apps.alert.link', {
|
||||
defaultMessage: 'Create alert',
|
||||
}),
|
||||
href: core.http.basePath.prepend(
|
||||
'/app/management/insightsAndAlerting/triggersActions/alerts'
|
||||
),
|
||||
},
|
||||
];
|
||||
};
|
198
x-pack/plugins/observability/public/pages/overview/index.tsx
Normal file
198
x-pack/plugins/observability/public/pages/overview/index.tsx
Normal file
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import React, { useContext } from 'react';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import { EmptySection } from '../../components/app/empty_section';
|
||||
import { WithHeaderLayout } from '../../components/app/layout/with_header';
|
||||
import { Resources } from '../../components/app/resources';
|
||||
import { AlertsSection } from '../../components/app/section/alerts';
|
||||
import { APMSection } from '../../components/app/section/apm';
|
||||
import { LogsSection } from '../../components/app/section/logs';
|
||||
import { MetricsSection } from '../../components/app/section/metrics';
|
||||
import { UptimeSection } from '../../components/app/section/uptime';
|
||||
import { DatePicker, TimePickerTime } from '../../components/shared/data_picker';
|
||||
import { fetchHasData } from '../../data_handler';
|
||||
import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher';
|
||||
import { UI_SETTINGS, useKibanaUISettings } from '../../hooks/use_kibana_ui_settings';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { RouteParams } from '../../routes';
|
||||
import { getObservabilityAlerts } from '../../services/get_observability_alerts';
|
||||
import { getParsedDate } from '../../utils/date';
|
||||
import { getBucketSize } from '../../utils/get_bucket_size';
|
||||
import { getEmptySections } from './empty_section';
|
||||
import { LoadingObservability } from './loading_observability';
|
||||
|
||||
interface Props {
|
||||
routeParams: RouteParams<'/overview'>;
|
||||
}
|
||||
|
||||
function calculatetBucketSize({ startTime, endTime }: { startTime?: string; endTime?: string }) {
|
||||
if (startTime && endTime) {
|
||||
return getBucketSize({
|
||||
start: moment.utc(startTime).valueOf(),
|
||||
end: moment.utc(endTime).valueOf(),
|
||||
minInterval: '60s',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const OverviewPage = ({ routeParams }: Props) => {
|
||||
const { core } = usePluginContext();
|
||||
|
||||
const { data: alerts = [], status: alertStatus } = useFetcher(() => {
|
||||
return getObservabilityAlerts({ core });
|
||||
}, []);
|
||||
|
||||
const theme = useContext(ThemeContext);
|
||||
const timePickerTime = useKibanaUISettings<TimePickerTime>(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS);
|
||||
|
||||
const result = useFetcher(() => fetchHasData(), []);
|
||||
const hasData = result.data;
|
||||
|
||||
if (!hasData) {
|
||||
return <LoadingObservability />;
|
||||
}
|
||||
|
||||
const {
|
||||
rangeFrom = timePickerTime.from,
|
||||
rangeTo = timePickerTime.to,
|
||||
refreshInterval = 10000,
|
||||
refreshPaused = true,
|
||||
} = routeParams.query;
|
||||
|
||||
const startTime = getParsedDate(rangeFrom);
|
||||
const endTime = getParsedDate(rangeTo, { roundUp: true });
|
||||
const bucketSize = calculatetBucketSize({ startTime, endTime });
|
||||
|
||||
const appEmptySections = getEmptySections({ core }).filter(({ id }) => {
|
||||
if (id === 'alert') {
|
||||
return alertStatus !== FETCH_STATUS.FAILURE && !alerts.length;
|
||||
}
|
||||
return !hasData[id];
|
||||
});
|
||||
|
||||
// Hides the data section when all 'hasData' is false or undefined
|
||||
const showDataSections = Object.values(hasData).some((hasPluginData) => hasPluginData);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout
|
||||
headerColor={theme.eui.euiColorEmptyShade}
|
||||
bodyColor={theme.eui.euiPageBackgroundColor}
|
||||
showAddData
|
||||
showGiveFeedback
|
||||
>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePicker
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
refreshInterval={refreshInterval}
|
||||
refreshPaused={refreshPaused}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiHorizontalRule
|
||||
style={{
|
||||
width: 'auto', // full width
|
||||
margin: '24px -24px', // counteract page paddings
|
||||
}}
|
||||
/>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={6}>
|
||||
{/* Data sections */}
|
||||
{showDataSections && (
|
||||
<EuiFlexGroup direction="column">
|
||||
{hasData.infra_logs && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<LogsSection
|
||||
startTime={startTime}
|
||||
endTime={endTime}
|
||||
bucketSize={bucketSize?.intervalString}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{hasData.infra_metrics && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<MetricsSection
|
||||
startTime={startTime}
|
||||
endTime={endTime}
|
||||
bucketSize={bucketSize?.intervalString}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{hasData.apm && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<APMSection
|
||||
startTime={startTime}
|
||||
endTime={endTime}
|
||||
bucketSize={bucketSize?.intervalString}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{hasData.uptime && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<UptimeSection
|
||||
startTime={startTime}
|
||||
endTime={endTime}
|
||||
bucketSize={bucketSize?.intervalString}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
|
||||
{/* Empty sections */}
|
||||
{!!appEmptySections.length && (
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGrid
|
||||
columns={
|
||||
// when more than 2 empty sections are available show them on 2 columns, otherwise 1
|
||||
appEmptySections.length > 2 ? 2 : 1
|
||||
}
|
||||
gutterSize="s"
|
||||
>
|
||||
{appEmptySections.map((app) => {
|
||||
return (
|
||||
<EuiFlexItem
|
||||
key={app.id}
|
||||
style={{
|
||||
border: `1px dashed ${theme.eui.euiBorderColor}`,
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<EmptySection section={app} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGrid>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
||||
{/* Alert section */}
|
||||
{!!alerts.length && (
|
||||
<EuiFlexItem grow={3}>
|
||||
<AlertsSection alerts={alerts} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{/* Resources section */}
|
||||
<EuiFlexItem grow={1}>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<Resources />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useContext } from 'react';
|
||||
import styled, { ThemeContext } from 'styled-components';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { WithHeaderLayout } from '../../components/app/layout/with_header';
|
||||
|
||||
const CentralizedFlexGroup = styled(EuiFlexGroup)`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// place the element in the center of the page
|
||||
min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize});
|
||||
`;
|
||||
|
||||
export const LoadingObservability = () => {
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout
|
||||
headerColor={theme.eui.euiColorEmptyShade}
|
||||
bodyColor={theme.eui.euiPageBackgroundColor}
|
||||
showAddData
|
||||
showGiveFeedback
|
||||
>
|
||||
<CentralizedFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="xl" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ justifyContent: 'center' }}>
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.observability.overview.loadingObservability', {
|
||||
defaultMessage: 'Loading Observability',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</CentralizedFlexGroup>
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const alertsFetchData = async () => {
|
||||
return Promise.resolve({
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
consumer: 'apm',
|
||||
name: 'Error rate | opbeans-java',
|
||||
alertTypeId: 'apm.error_rate',
|
||||
tags: ['apm', 'service.name:opbeans-java'],
|
||||
updatedAt: '2020-07-03T14:27:51.488Z',
|
||||
muteAll: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
consumer: 'apm',
|
||||
name: 'Transaction duration | opbeans-java',
|
||||
alertTypeId: 'apm.transaction_duration',
|
||||
tags: ['apm', 'service.name:opbeans-java'],
|
||||
updatedAt: '2020-07-02T14:27:51.488Z',
|
||||
muteAll: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
consumer: 'logs',
|
||||
name: 'Logs obs test',
|
||||
alertTypeId: 'logs.alert.document.count',
|
||||
tags: ['logs', 'observability'],
|
||||
updatedAt: '2020-06-30T14:27:51.488Z',
|
||||
muteAll: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
consumer: 'metrics',
|
||||
name: 'Metrics obs test',
|
||||
alertTypeId: 'metrics.alert.inventory.threshold',
|
||||
tags: ['metrics', 'observability'],
|
||||
updatedAt: '2020-03-20T14:27:51.488Z',
|
||||
muteAll: true,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
consumer: 'uptime',
|
||||
name: 'Uptime obs test',
|
||||
alertTypeId: 'xpack.uptime.alerts.monitorStatus',
|
||||
tags: ['uptime', 'observability'],
|
||||
updatedAt: '2020-03-25T17:27:51.488Z',
|
||||
muteAll: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
|
@ -0,0 +1,627 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { ApmFetchDataResponse, FetchData } from '../../../typings';
|
||||
|
||||
export const fetchApmData: FetchData<ApmFetchDataResponse> = () => {
|
||||
return Promise.resolve(response);
|
||||
};
|
||||
|
||||
const response: ApmFetchDataResponse = {
|
||||
title: 'APM',
|
||||
appLink: '/app/apm',
|
||||
stats: {
|
||||
services: {
|
||||
type: 'number',
|
||||
value: 7,
|
||||
},
|
||||
transactions: {
|
||||
type: 'number',
|
||||
value: 125808,
|
||||
},
|
||||
},
|
||||
series: {
|
||||
transactions: {
|
||||
coordinates: [
|
||||
{
|
||||
x: 1593295200000,
|
||||
y: 891,
|
||||
},
|
||||
{
|
||||
x: 1593297000000,
|
||||
y: 902,
|
||||
},
|
||||
{
|
||||
x: 1593298800000,
|
||||
y: 924,
|
||||
},
|
||||
{
|
||||
x: 1593300600000,
|
||||
y: 944,
|
||||
},
|
||||
{
|
||||
x: 1593302400000,
|
||||
y: 935,
|
||||
},
|
||||
{
|
||||
x: 1593304200000,
|
||||
y: 915,
|
||||
},
|
||||
{
|
||||
x: 1593306000000,
|
||||
y: 917,
|
||||
},
|
||||
{
|
||||
x: 1593307800000,
|
||||
y: 941,
|
||||
},
|
||||
{
|
||||
x: 1593309600000,
|
||||
y: 906,
|
||||
},
|
||||
{
|
||||
x: 1593311400000,
|
||||
y: 939,
|
||||
},
|
||||
{
|
||||
x: 1593313200000,
|
||||
y: 961,
|
||||
},
|
||||
{
|
||||
x: 1593315000000,
|
||||
y: 911,
|
||||
},
|
||||
{
|
||||
x: 1593316800000,
|
||||
y: 958,
|
||||
},
|
||||
{
|
||||
x: 1593318600000,
|
||||
y: 861,
|
||||
},
|
||||
{
|
||||
x: 1593320400000,
|
||||
y: 906,
|
||||
},
|
||||
{
|
||||
x: 1593322200000,
|
||||
y: 899,
|
||||
},
|
||||
{
|
||||
x: 1593324000000,
|
||||
y: 785,
|
||||
},
|
||||
{
|
||||
x: 1593325800000,
|
||||
y: 952,
|
||||
},
|
||||
{
|
||||
x: 1593327600000,
|
||||
y: 910,
|
||||
},
|
||||
{
|
||||
x: 1593329400000,
|
||||
y: 869,
|
||||
},
|
||||
{
|
||||
x: 1593331200000,
|
||||
y: 895,
|
||||
},
|
||||
{
|
||||
x: 1593333000000,
|
||||
y: 924,
|
||||
},
|
||||
{
|
||||
x: 1593334800000,
|
||||
y: 930,
|
||||
},
|
||||
{
|
||||
x: 1593336600000,
|
||||
y: 947,
|
||||
},
|
||||
{
|
||||
x: 1593338400000,
|
||||
y: 905,
|
||||
},
|
||||
{
|
||||
x: 1593340200000,
|
||||
y: 963,
|
||||
},
|
||||
{
|
||||
x: 1593342000000,
|
||||
y: 877,
|
||||
},
|
||||
{
|
||||
x: 1593343800000,
|
||||
y: 839,
|
||||
},
|
||||
{
|
||||
x: 1593345600000,
|
||||
y: 884,
|
||||
},
|
||||
{
|
||||
x: 1593347400000,
|
||||
y: 934,
|
||||
},
|
||||
{
|
||||
x: 1593349200000,
|
||||
y: 908,
|
||||
},
|
||||
{
|
||||
x: 1593351000000,
|
||||
y: 982,
|
||||
},
|
||||
{
|
||||
x: 1593352800000,
|
||||
y: 897,
|
||||
},
|
||||
{
|
||||
x: 1593354600000,
|
||||
y: 903,
|
||||
},
|
||||
{
|
||||
x: 1593356400000,
|
||||
y: 877,
|
||||
},
|
||||
{
|
||||
x: 1593358200000,
|
||||
y: 893,
|
||||
},
|
||||
{
|
||||
x: 1593360000000,
|
||||
y: 919,
|
||||
},
|
||||
{
|
||||
x: 1593361800000,
|
||||
y: 844,
|
||||
},
|
||||
{
|
||||
x: 1593363600000,
|
||||
y: 940,
|
||||
},
|
||||
{
|
||||
x: 1593365400000,
|
||||
y: 951,
|
||||
},
|
||||
{
|
||||
x: 1593367200000,
|
||||
y: 869,
|
||||
},
|
||||
{
|
||||
x: 1593369000000,
|
||||
y: 901,
|
||||
},
|
||||
{
|
||||
x: 1593370800000,
|
||||
y: 940,
|
||||
},
|
||||
{
|
||||
x: 1593372600000,
|
||||
y: 942,
|
||||
},
|
||||
{
|
||||
x: 1593374400000,
|
||||
y: 881,
|
||||
},
|
||||
{
|
||||
x: 1593376200000,
|
||||
y: 935,
|
||||
},
|
||||
{
|
||||
x: 1593378000000,
|
||||
y: 892,
|
||||
},
|
||||
{
|
||||
x: 1593379800000,
|
||||
y: 861,
|
||||
},
|
||||
{
|
||||
x: 1593381600000,
|
||||
y: 868,
|
||||
},
|
||||
{
|
||||
x: 1593383400000,
|
||||
y: 990,
|
||||
},
|
||||
{
|
||||
x: 1593385200000,
|
||||
y: 931,
|
||||
},
|
||||
{
|
||||
x: 1593387000000,
|
||||
y: 898,
|
||||
},
|
||||
{
|
||||
x: 1593388800000,
|
||||
y: 906,
|
||||
},
|
||||
{
|
||||
x: 1593390600000,
|
||||
y: 928,
|
||||
},
|
||||
{
|
||||
x: 1593392400000,
|
||||
y: 975,
|
||||
},
|
||||
{
|
||||
x: 1593394200000,
|
||||
y: 842,
|
||||
},
|
||||
{
|
||||
x: 1593396000000,
|
||||
y: 940,
|
||||
},
|
||||
{
|
||||
x: 1593397800000,
|
||||
y: 922,
|
||||
},
|
||||
{
|
||||
x: 1593399600000,
|
||||
y: 962,
|
||||
},
|
||||
{
|
||||
x: 1593401400000,
|
||||
y: 940,
|
||||
},
|
||||
{
|
||||
x: 1593403200000,
|
||||
y: 974,
|
||||
},
|
||||
{
|
||||
x: 1593405000000,
|
||||
y: 887,
|
||||
},
|
||||
{
|
||||
x: 1593406800000,
|
||||
y: 920,
|
||||
},
|
||||
{
|
||||
x: 1593408600000,
|
||||
y: 854,
|
||||
},
|
||||
{
|
||||
x: 1593410400000,
|
||||
y: 898,
|
||||
},
|
||||
{
|
||||
x: 1593412200000,
|
||||
y: 952,
|
||||
},
|
||||
{
|
||||
x: 1593414000000,
|
||||
y: 987,
|
||||
},
|
||||
{
|
||||
x: 1593415800000,
|
||||
y: 932,
|
||||
},
|
||||
{
|
||||
x: 1593417600000,
|
||||
y: 1009,
|
||||
},
|
||||
{
|
||||
x: 1593419400000,
|
||||
y: 989,
|
||||
},
|
||||
{
|
||||
x: 1593421200000,
|
||||
y: 939,
|
||||
},
|
||||
{
|
||||
x: 1593423000000,
|
||||
y: 929,
|
||||
},
|
||||
{
|
||||
x: 1593424800000,
|
||||
y: 929,
|
||||
},
|
||||
{
|
||||
x: 1593426600000,
|
||||
y: 864,
|
||||
},
|
||||
{
|
||||
x: 1593428400000,
|
||||
y: 895,
|
||||
},
|
||||
{
|
||||
x: 1593430200000,
|
||||
y: 876,
|
||||
},
|
||||
{
|
||||
x: 1593432000000,
|
||||
y: 68,
|
||||
},
|
||||
{
|
||||
x: 1593433800000,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
x: 1593435600000,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
x: 1593437400000,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
x: 1593439200000,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
x: 1593441000000,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
x: 1593442800000,
|
||||
y: 700,
|
||||
},
|
||||
{
|
||||
x: 1593444600000,
|
||||
y: 930,
|
||||
},
|
||||
{
|
||||
x: 1593446400000,
|
||||
y: 953,
|
||||
},
|
||||
{
|
||||
x: 1593448200000,
|
||||
y: 995,
|
||||
},
|
||||
{
|
||||
x: 1593450000000,
|
||||
y: 883,
|
||||
},
|
||||
{
|
||||
x: 1593451800000,
|
||||
y: 902,
|
||||
},
|
||||
{
|
||||
x: 1593453600000,
|
||||
y: 988,
|
||||
},
|
||||
{
|
||||
x: 1593455400000,
|
||||
y: 947,
|
||||
},
|
||||
{
|
||||
x: 1593457200000,
|
||||
y: 889,
|
||||
},
|
||||
{
|
||||
x: 1593459000000,
|
||||
y: 982,
|
||||
},
|
||||
{
|
||||
x: 1593460800000,
|
||||
y: 919,
|
||||
},
|
||||
{
|
||||
x: 1593462600000,
|
||||
y: 854,
|
||||
},
|
||||
{
|
||||
x: 1593464400000,
|
||||
y: 894,
|
||||
},
|
||||
{
|
||||
x: 1593466200000,
|
||||
y: 901,
|
||||
},
|
||||
{
|
||||
x: 1593468000000,
|
||||
y: 970,
|
||||
},
|
||||
{
|
||||
x: 1593469800000,
|
||||
y: 840,
|
||||
},
|
||||
{
|
||||
x: 1593471600000,
|
||||
y: 857,
|
||||
},
|
||||
{
|
||||
x: 1593473400000,
|
||||
y: 943,
|
||||
},
|
||||
{
|
||||
x: 1593475200000,
|
||||
y: 825,
|
||||
},
|
||||
{
|
||||
x: 1593477000000,
|
||||
y: 955,
|
||||
},
|
||||
{
|
||||
x: 1593478800000,
|
||||
y: 959,
|
||||
},
|
||||
{
|
||||
x: 1593480600000,
|
||||
y: 921,
|
||||
},
|
||||
{
|
||||
x: 1593482400000,
|
||||
y: 924,
|
||||
},
|
||||
{
|
||||
x: 1593484200000,
|
||||
y: 840,
|
||||
},
|
||||
{
|
||||
x: 1593486000000,
|
||||
y: 943,
|
||||
},
|
||||
{
|
||||
x: 1593487800000,
|
||||
y: 919,
|
||||
},
|
||||
{
|
||||
x: 1593489600000,
|
||||
y: 882,
|
||||
},
|
||||
{
|
||||
x: 1593491400000,
|
||||
y: 900,
|
||||
},
|
||||
{
|
||||
x: 1593493200000,
|
||||
y: 930,
|
||||
},
|
||||
{
|
||||
x: 1593495000000,
|
||||
y: 854,
|
||||
},
|
||||
{
|
||||
x: 1593496800000,
|
||||
y: 905,
|
||||
},
|
||||
{
|
||||
x: 1593498600000,
|
||||
y: 922,
|
||||
},
|
||||
{
|
||||
x: 1593500400000,
|
||||
y: 863,
|
||||
},
|
||||
{
|
||||
x: 1593502200000,
|
||||
y: 966,
|
||||
},
|
||||
{
|
||||
x: 1593504000000,
|
||||
y: 910,
|
||||
},
|
||||
{
|
||||
x: 1593505800000,
|
||||
y: 851,
|
||||
},
|
||||
{
|
||||
x: 1593507600000,
|
||||
y: 867,
|
||||
},
|
||||
{
|
||||
x: 1593509400000,
|
||||
y: 904,
|
||||
},
|
||||
{
|
||||
x: 1593511200000,
|
||||
y: 913,
|
||||
},
|
||||
{
|
||||
x: 1593513000000,
|
||||
y: 889,
|
||||
},
|
||||
{
|
||||
x: 1593514800000,
|
||||
y: 907,
|
||||
},
|
||||
{
|
||||
x: 1593516600000,
|
||||
y: 965,
|
||||
},
|
||||
{
|
||||
x: 1593518400000,
|
||||
y: 868,
|
||||
},
|
||||
{
|
||||
x: 1593520200000,
|
||||
y: 919,
|
||||
},
|
||||
{
|
||||
x: 1593522000000,
|
||||
y: 945,
|
||||
},
|
||||
{
|
||||
x: 1593523800000,
|
||||
y: 883,
|
||||
},
|
||||
{
|
||||
x: 1593525600000,
|
||||
y: 902,
|
||||
},
|
||||
{
|
||||
x: 1593527400000,
|
||||
y: 900,
|
||||
},
|
||||
{
|
||||
x: 1593529200000,
|
||||
y: 829,
|
||||
},
|
||||
{
|
||||
x: 1593531000000,
|
||||
y: 919,
|
||||
},
|
||||
{
|
||||
x: 1593532800000,
|
||||
y: 942,
|
||||
},
|
||||
{
|
||||
x: 1593534600000,
|
||||
y: 924,
|
||||
},
|
||||
{
|
||||
x: 1593536400000,
|
||||
y: 958,
|
||||
},
|
||||
{
|
||||
x: 1593538200000,
|
||||
y: 867,
|
||||
},
|
||||
{
|
||||
x: 1593540000000,
|
||||
y: 844,
|
||||
},
|
||||
{
|
||||
x: 1593541800000,
|
||||
y: 976,
|
||||
},
|
||||
{
|
||||
x: 1593543600000,
|
||||
y: 937,
|
||||
},
|
||||
{
|
||||
x: 1593545400000,
|
||||
y: 891,
|
||||
},
|
||||
{
|
||||
x: 1593547200000,
|
||||
y: 936,
|
||||
},
|
||||
{
|
||||
x: 1593549000000,
|
||||
y: 895,
|
||||
},
|
||||
{
|
||||
x: 1593550800000,
|
||||
y: 850,
|
||||
},
|
||||
{
|
||||
x: 1593552600000,
|
||||
y: 899,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const emptyResponse: ApmFetchDataResponse = {
|
||||
title: 'APM',
|
||||
appLink: '/app/apm',
|
||||
stats: {
|
||||
services: {
|
||||
type: 'number',
|
||||
value: 0,
|
||||
},
|
||||
transactions: {
|
||||
type: 'number',
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
series: {
|
||||
transactions: {
|
||||
coordinates: [],
|
||||
},
|
||||
},
|
||||
};
|
2326
x-pack/plugins/observability/public/pages/overview/mock/logs.mock.ts
Normal file
2326
x-pack/plugins/observability/public/pages/overview/mock/logs.mock.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { MetricsFetchDataResponse, FetchData } from '../../../typings';
|
||||
|
||||
export const fetchMetricsData: FetchData<MetricsFetchDataResponse> = () => {
|
||||
return Promise.resolve(response);
|
||||
};
|
||||
|
||||
const response: MetricsFetchDataResponse = {
|
||||
title: 'Metrics',
|
||||
appLink: '/app/apm',
|
||||
stats: {
|
||||
hosts: { value: 11, type: 'number' },
|
||||
cpu: { value: 0.8, type: 'percent' },
|
||||
memory: { value: 0.362, type: 'percent' },
|
||||
inboundTraffic: { value: 1024, type: 'bytesPerSecond' },
|
||||
outboundTraffic: { value: 1024, type: 'bytesPerSecond' },
|
||||
},
|
||||
series: {
|
||||
outboundTraffic: {
|
||||
coordinates: [
|
||||
{
|
||||
x: 1589805437549,
|
||||
y: 331514,
|
||||
},
|
||||
{
|
||||
x: 1590047357549,
|
||||
y: 319208,
|
||||
},
|
||||
{
|
||||
x: 1590289277549,
|
||||
y: 309648,
|
||||
},
|
||||
{
|
||||
x: 1590531197549,
|
||||
y: 280568,
|
||||
},
|
||||
{
|
||||
x: 1590773117549,
|
||||
y: 337180,
|
||||
},
|
||||
{
|
||||
x: 1591015037549,
|
||||
y: 122468,
|
||||
},
|
||||
{
|
||||
x: 1591256957549,
|
||||
y: 184164,
|
||||
},
|
||||
{
|
||||
x: 1591498877549,
|
||||
y: 316323,
|
||||
},
|
||||
{
|
||||
x: 1591740797549,
|
||||
y: 307351,
|
||||
},
|
||||
{
|
||||
x: 1591982717549,
|
||||
y: 290262,
|
||||
},
|
||||
],
|
||||
},
|
||||
inboundTraffic: {
|
||||
coordinates: [
|
||||
{
|
||||
x: 1589805437549,
|
||||
y: 331514,
|
||||
},
|
||||
{
|
||||
x: 1590047357549,
|
||||
y: 319208,
|
||||
},
|
||||
{
|
||||
x: 1590289277549,
|
||||
y: 309648,
|
||||
},
|
||||
{
|
||||
x: 1590531197549,
|
||||
y: 280568,
|
||||
},
|
||||
{
|
||||
x: 1590773117549,
|
||||
y: 337180,
|
||||
},
|
||||
{
|
||||
x: 1591015037549,
|
||||
y: 122468,
|
||||
},
|
||||
{
|
||||
x: 1591256957549,
|
||||
y: 184164,
|
||||
},
|
||||
{
|
||||
x: 1591498877549,
|
||||
y: 316323,
|
||||
},
|
||||
{
|
||||
x: 1591740797549,
|
||||
y: 307351,
|
||||
},
|
||||
{
|
||||
x: 1591982717549,
|
||||
y: 290262,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const emptyResponse: MetricsFetchDataResponse = {
|
||||
title: 'Metrics',
|
||||
appLink: '/app/apm',
|
||||
stats: {
|
||||
hosts: { value: 0, type: 'number' },
|
||||
cpu: { value: 0, type: 'percent' },
|
||||
memory: { value: 0, type: 'percent' },
|
||||
inboundTraffic: { value: 0, type: 'bytesPerSecond' },
|
||||
outboundTraffic: { value: 0, type: 'bytesPerSecond' },
|
||||
},
|
||||
series: {
|
||||
outboundTraffic: {
|
||||
coordinates: [],
|
||||
},
|
||||
inboundTraffic: {
|
||||
coordinates: [],
|
||||
},
|
||||
},
|
||||
};
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,534 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { AppMountContext } from 'kibana/public';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { UI_SETTINGS } from '../../../../../../src/plugins/data/public';
|
||||
import { PluginContext } from '../../context/plugin_context';
|
||||
import { registerDataHandler, unregisterDataHandler } from '../../data_handler';
|
||||
import { emptyResponse as emptyAPMResponse, fetchApmData } from './mock/apm.mock';
|
||||
import { fetchLogsData, emptyResponse as emptyLogsResponse } from './mock/logs.mock';
|
||||
import { fetchMetricsData, emptyResponse as emptyMetricsResponse } from './mock/metrics.mock';
|
||||
import { fetchUptimeData, emptyResponse as emptyUptimeResponse } from './mock/uptime.mock';
|
||||
import { EuiThemeProvider } from '../../typings';
|
||||
import { OverviewPage } from './';
|
||||
import { alertsFetchData } from './mock/alerts.mock';
|
||||
|
||||
const core = {
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: (link) => `http://localhost:5601${link}`,
|
||||
},
|
||||
},
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
const euiSettings = {
|
||||
[UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
[UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: {
|
||||
pause: true,
|
||||
value: 1000,
|
||||
},
|
||||
[UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [
|
||||
{
|
||||
from: 'now/d',
|
||||
to: 'now/d',
|
||||
display: 'Today',
|
||||
},
|
||||
{
|
||||
from: 'now/w',
|
||||
to: 'now/w',
|
||||
display: 'This week',
|
||||
},
|
||||
{
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
display: 'Last 15 minutes',
|
||||
},
|
||||
{
|
||||
from: 'now-30m',
|
||||
to: 'now',
|
||||
display: 'Last 30 minutes',
|
||||
},
|
||||
{
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
display: 'Last 1 hour',
|
||||
},
|
||||
{
|
||||
from: 'now-24h',
|
||||
to: 'now',
|
||||
display: 'Last 24 hours',
|
||||
},
|
||||
{
|
||||
from: 'now-7d',
|
||||
to: 'now',
|
||||
display: 'Last 7 days',
|
||||
},
|
||||
{
|
||||
from: 'now-30d',
|
||||
to: 'now',
|
||||
display: 'Last 30 days',
|
||||
},
|
||||
{
|
||||
from: 'now-90d',
|
||||
to: 'now',
|
||||
display: 'Last 90 days',
|
||||
},
|
||||
{
|
||||
from: 'now-1y',
|
||||
to: 'now',
|
||||
display: 'Last 1 year',
|
||||
},
|
||||
],
|
||||
};
|
||||
// @ts-expect-error
|
||||
return euiSettings[key];
|
||||
},
|
||||
},
|
||||
} as AppMountContext['core'];
|
||||
|
||||
const coreWithAlerts = ({
|
||||
...core,
|
||||
http: {
|
||||
...core.http,
|
||||
get: alertsFetchData,
|
||||
},
|
||||
} as unknown) as AppMountContext['core'];
|
||||
|
||||
function unregisterAll() {
|
||||
unregisterDataHandler({ appName: 'apm' });
|
||||
unregisterDataHandler({ appName: 'infra_logs' });
|
||||
unregisterDataHandler({ appName: 'infra_metrics' });
|
||||
unregisterDataHandler({ appName: 'uptime' });
|
||||
}
|
||||
|
||||
storiesOf('app/Overview', module)
|
||||
.addDecorator((storyFn) => (
|
||||
<MemoryRouter>
|
||||
<PluginContext.Provider value={{ core }}>
|
||||
<EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
|
||||
</PluginContext.Provider>
|
||||
</MemoryRouter>
|
||||
))
|
||||
.add('Empty state', () => {
|
||||
unregisterAll();
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => false,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => false,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => false,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'uptime',
|
||||
fetchData: fetchUptimeData,
|
||||
hasData: async () => false,
|
||||
});
|
||||
|
||||
return <OverviewPage routeParams={{ query: {} }} />;
|
||||
});
|
||||
|
||||
storiesOf('app/Overview', module)
|
||||
.addDecorator((storyFn) => (
|
||||
<MemoryRouter>
|
||||
<PluginContext.Provider value={{ core }}>
|
||||
<EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
|
||||
</PluginContext.Provider>
|
||||
</MemoryRouter>
|
||||
))
|
||||
.add('single panel', () => {
|
||||
unregisterAll();
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
return (
|
||||
<OverviewPage
|
||||
routeParams={{
|
||||
query: { rangeFrom: '2020-06-27T22:00:00.000Z', rangeTo: '2020-06-30T21:59:59.999Z' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
storiesOf('app/Overview', module)
|
||||
.addDecorator((storyFn) => (
|
||||
<MemoryRouter>
|
||||
<PluginContext.Provider value={{ core }}>
|
||||
<EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
|
||||
</PluginContext.Provider>
|
||||
</MemoryRouter>
|
||||
))
|
||||
.add('logs and metrics', () => {
|
||||
unregisterAll();
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
return (
|
||||
<OverviewPage
|
||||
routeParams={{
|
||||
query: { rangeFrom: '2020-06-27T22:00:00.000Z', rangeTo: '2020-06-30T21:59:59.999Z' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
storiesOf('app/Overview', module)
|
||||
.addDecorator((storyFn) => (
|
||||
<MemoryRouter>
|
||||
<PluginContext.Provider value={{ core: coreWithAlerts }}>
|
||||
<EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
|
||||
</PluginContext.Provider>
|
||||
</MemoryRouter>
|
||||
))
|
||||
.add('logs, metrics and alerts', () => {
|
||||
unregisterAll();
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
return (
|
||||
<OverviewPage
|
||||
routeParams={{
|
||||
query: { rangeFrom: '2020-06-27T22:00:00.000Z', rangeTo: '2020-06-30T21:59:59.999Z' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
storiesOf('app/Overview', module)
|
||||
.addDecorator((storyFn) => (
|
||||
<MemoryRouter>
|
||||
<PluginContext.Provider value={{ core: coreWithAlerts }}>
|
||||
<EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
|
||||
</PluginContext.Provider>
|
||||
</MemoryRouter>
|
||||
))
|
||||
.add('logs, metrics, APM and alerts', () => {
|
||||
unregisterAll();
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
return (
|
||||
<OverviewPage
|
||||
routeParams={{
|
||||
query: { rangeFrom: '2020-06-27T22:00:00.000Z', rangeTo: '2020-06-30T21:59:59.999Z' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
storiesOf('app/Overview', module)
|
||||
.addDecorator((storyFn) => (
|
||||
<MemoryRouter>
|
||||
<PluginContext.Provider value={{ core }}>
|
||||
<EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
|
||||
</PluginContext.Provider>
|
||||
</MemoryRouter>
|
||||
))
|
||||
.add('logs, metrics, APM and Uptime', () => {
|
||||
unregisterAll();
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'uptime',
|
||||
fetchData: fetchUptimeData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
return (
|
||||
<OverviewPage
|
||||
routeParams={{
|
||||
query: { rangeFrom: '2020-06-27T22:00:00.000Z', rangeTo: '2020-06-30T21:59:59.999Z' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
storiesOf('app/Overview', module)
|
||||
.addDecorator((storyFn) => (
|
||||
<MemoryRouter>
|
||||
<PluginContext.Provider value={{ core: coreWithAlerts }}>
|
||||
<EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
|
||||
</PluginContext.Provider>
|
||||
</MemoryRouter>
|
||||
))
|
||||
.add('logs, metrics, APM, Uptime and Alerts', () => {
|
||||
unregisterAll();
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'uptime',
|
||||
fetchData: fetchUptimeData,
|
||||
hasData: async () => true,
|
||||
});
|
||||
return (
|
||||
<OverviewPage
|
||||
routeParams={{
|
||||
query: { rangeFrom: '2020-06-27T22:00:00.000Z', rangeTo: '2020-06-30T21:59:59.999Z' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
storiesOf('app/Overview', module)
|
||||
.addDecorator((storyFn) => (
|
||||
<MemoryRouter>
|
||||
<PluginContext.Provider value={{ core }}>
|
||||
<EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
|
||||
</PluginContext.Provider>
|
||||
</MemoryRouter>
|
||||
))
|
||||
.add('no data', () => {
|
||||
unregisterAll();
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: async () => emptyAPMResponse,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: async () => emptyLogsResponse,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: async () => emptyMetricsResponse,
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'uptime',
|
||||
fetchData: async () => emptyUptimeResponse,
|
||||
hasData: async () => true,
|
||||
});
|
||||
return (
|
||||
<OverviewPage
|
||||
routeParams={{
|
||||
query: { rangeFrom: '2020-06-27T22:00:00.000Z', rangeTo: '2020-06-30T21:59:59.999Z' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const coreAlertsThrowsError = ({
|
||||
...core,
|
||||
http: {
|
||||
...core.http,
|
||||
get: async () => {
|
||||
throw new Error('Error fetching Alerts data');
|
||||
},
|
||||
},
|
||||
} as unknown) as AppMountContext['core'];
|
||||
storiesOf('app/Overview', module)
|
||||
.addDecorator((storyFn) => (
|
||||
<MemoryRouter>
|
||||
<PluginContext.Provider value={{ core: coreAlertsThrowsError }}>
|
||||
<EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
|
||||
</PluginContext.Provider>
|
||||
</MemoryRouter>
|
||||
))
|
||||
.add('fetch data with error', () => {
|
||||
unregisterAll();
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: async () => {
|
||||
throw new Error('Error fetching APM data');
|
||||
},
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: async () => {
|
||||
throw new Error('Error fetching Logs data');
|
||||
},
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: async () => {
|
||||
throw new Error('Error fetching Metric data');
|
||||
},
|
||||
hasData: async () => true,
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'uptime',
|
||||
fetchData: async () => {
|
||||
throw new Error('Error fetching Uptime data');
|
||||
},
|
||||
hasData: async () => true,
|
||||
});
|
||||
return (
|
||||
<OverviewPage
|
||||
routeParams={{
|
||||
query: { rangeFrom: '2020-06-27T22:00:00.000Z', rangeTo: '2020-06-30T21:59:59.999Z' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
storiesOf('app/Overview', module)
|
||||
.addDecorator((storyFn) => (
|
||||
<MemoryRouter>
|
||||
<PluginContext.Provider value={{ core: coreWithAlerts }}>
|
||||
<EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
|
||||
</PluginContext.Provider>
|
||||
</MemoryRouter>
|
||||
))
|
||||
.add('hasData with error and alerts', () => {
|
||||
unregisterAll();
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
// @ts-ignore thows an error instead
|
||||
hasData: async () => {
|
||||
new Error('Error has data');
|
||||
},
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
// @ts-ignore thows an error instead
|
||||
hasData: async () => {
|
||||
new Error('Error has data');
|
||||
},
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
// @ts-ignore thows an error instead
|
||||
hasData: async () => {
|
||||
new Error('Error has data');
|
||||
},
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'uptime',
|
||||
fetchData: fetchUptimeData,
|
||||
// @ts-ignore thows an error instead
|
||||
hasData: async () => {
|
||||
new Error('Error has data');
|
||||
},
|
||||
});
|
||||
return (
|
||||
<OverviewPage
|
||||
routeParams={{
|
||||
query: { rangeFrom: '2020-06-27T22:00:00.000Z', rangeTo: '2020-06-30T21:59:59.999Z' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
storiesOf('app/Overview', module)
|
||||
.addDecorator((storyFn) => (
|
||||
<MemoryRouter>
|
||||
<PluginContext.Provider value={{ core }}>
|
||||
<EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
|
||||
</PluginContext.Provider>
|
||||
</MemoryRouter>
|
||||
))
|
||||
.add('hasData with error', () => {
|
||||
unregisterAll();
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
// @ts-ignore thows an error instead
|
||||
hasData: async () => {
|
||||
new Error('Error has data');
|
||||
},
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
// @ts-ignore thows an error instead
|
||||
hasData: async () => {
|
||||
new Error('Error has data');
|
||||
},
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
// @ts-ignore thows an error instead
|
||||
hasData: async () => {
|
||||
new Error('Error has data');
|
||||
},
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'uptime',
|
||||
fetchData: fetchUptimeData,
|
||||
// @ts-ignore thows an error instead
|
||||
hasData: async () => {
|
||||
new Error('Error has data');
|
||||
},
|
||||
});
|
||||
return (
|
||||
<OverviewPage
|
||||
routeParams={{
|
||||
query: { rangeFrom: '2020-06-27T22:00:00.000Z', rangeTo: '2020-06-30T21:59:59.999Z' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
71
x-pack/plugins/observability/public/routes/index.tsx
Normal file
71
x-pack/plugins/observability/public/routes/index.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import * as t from 'io-ts';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { HomePage } from '../pages/home';
|
||||
import { LandingPage } from '../pages/landing';
|
||||
import { OverviewPage } from '../pages/overview';
|
||||
import { jsonRt } from './json_rt';
|
||||
|
||||
export type RouteParams<T extends keyof typeof routes> = DecodeParams<typeof routes[T]['params']>;
|
||||
|
||||
type DecodeParams<TParams extends Params | undefined> = {
|
||||
[key in keyof TParams]: TParams[key] extends t.Any ? t.TypeOf<TParams[key]> : never;
|
||||
};
|
||||
|
||||
export interface Params {
|
||||
query?: t.HasProps;
|
||||
path?: t.HasProps;
|
||||
}
|
||||
export const routes = {
|
||||
'/': {
|
||||
handler: () => {
|
||||
return <HomePage />;
|
||||
},
|
||||
params: {},
|
||||
breadcrumb: [
|
||||
{
|
||||
text: i18n.translate('xpack.observability.home.breadcrumb', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
'/landing': {
|
||||
handler: () => {
|
||||
return <LandingPage />;
|
||||
},
|
||||
params: {},
|
||||
breadcrumb: [
|
||||
{
|
||||
text: i18n.translate('xpack.observability.landing.breadcrumb', {
|
||||
defaultMessage: 'Getting started',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
'/overview': {
|
||||
handler: ({ query }: any) => {
|
||||
return <OverviewPage routeParams={{ query }} />;
|
||||
},
|
||||
params: {
|
||||
query: t.partial({
|
||||
rangeFrom: t.string,
|
||||
rangeTo: t.string,
|
||||
refreshPaused: jsonRt.pipe(t.boolean),
|
||||
refreshInterval: jsonRt.pipe(t.number),
|
||||
}),
|
||||
},
|
||||
breadcrumb: [
|
||||
{
|
||||
text: i18n.translate('xpack.observability.overview.breadcrumb', {
|
||||
defaultMessage: 'Overview',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
21
x-pack/plugins/observability/public/routes/json_rt.ts
Normal file
21
x-pack/plugins/observability/public/routes/json_rt.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { either } from 'fp-ts/lib/Either';
|
||||
|
||||
export const jsonRt = new t.Type<any, string, unknown>(
|
||||
'JSON',
|
||||
t.any.is,
|
||||
(input, context) =>
|
||||
either.chain(t.string.validate(input, context), (str) => {
|
||||
try {
|
||||
return t.success(JSON.parse(str));
|
||||
} catch (e) {
|
||||
return t.failure(input, context);
|
||||
}
|
||||
}),
|
||||
(a) => JSON.stringify(a)
|
||||
);
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AppMountContext } from 'kibana/public';
|
||||
import { getObservabilityAlerts } from './get_observability_alerts';
|
||||
|
||||
describe('getObservabilityAlerts', () => {
|
||||
it('Returns empty array when api throws exception', async () => {
|
||||
const core = ({
|
||||
http: {
|
||||
get: async () => {
|
||||
throw new Error('Boom');
|
||||
},
|
||||
},
|
||||
} as unknown) as AppMountContext['core'];
|
||||
|
||||
const alerts = await getObservabilityAlerts({ core });
|
||||
expect(alerts).toEqual([]);
|
||||
});
|
||||
|
||||
it('Returns empty array when api return undefined', async () => {
|
||||
const core = ({
|
||||
http: {
|
||||
get: async () => {
|
||||
return {
|
||||
data: undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
} as unknown) as AppMountContext['core'];
|
||||
|
||||
const alerts = await getObservabilityAlerts({ core });
|
||||
expect(alerts).toEqual([]);
|
||||
});
|
||||
|
||||
it('Shows alerts from Observability', async () => {
|
||||
const core = ({
|
||||
http: {
|
||||
get: async () => {
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
consumer: 'siem',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
consumer: 'apm',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
consumer: 'uptime',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
consumer: 'logs',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
consumer: 'metrics',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
} as unknown) as AppMountContext['core'];
|
||||
|
||||
const alerts = await getObservabilityAlerts({ core });
|
||||
expect(alerts).toEqual([
|
||||
{
|
||||
id: 2,
|
||||
consumer: 'apm',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
consumer: 'uptime',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
consumer: 'logs',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
consumer: 'metrics',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AppMountContext } from 'kibana/public';
|
||||
import { Alert } from '../../../alerts/common';
|
||||
|
||||
export async function getObservabilityAlerts({ core }: { core: AppMountContext['core'] }) {
|
||||
try {
|
||||
const { data = [] }: { data: Alert[] } = await core.http.get('/api/alerts/_find', {
|
||||
query: {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
},
|
||||
});
|
||||
|
||||
return data.filter(({ consumer }) => {
|
||||
return (
|
||||
consumer === 'apm' || consumer === 'uptime' || consumer === 'logs' || consumer === 'metrics'
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -6,11 +6,9 @@
|
|||
|
||||
import { ObservabilityApp } from '../../../typings/common';
|
||||
|
||||
interface Stat {
|
||||
export interface Stat {
|
||||
type: 'number' | 'percent' | 'bytesPerSecond';
|
||||
label: string;
|
||||
value: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface Coordinates {
|
||||
|
@ -18,10 +16,8 @@ export interface Coordinates {
|
|||
y?: number;
|
||||
}
|
||||
|
||||
interface Series {
|
||||
label: string;
|
||||
export interface Series {
|
||||
coordinates: Coordinates[];
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface FetchDataParams {
|
||||
|
@ -50,8 +46,8 @@ export interface FetchDataResponse {
|
|||
}
|
||||
|
||||
export interface LogsFetchDataResponse extends FetchDataResponse {
|
||||
stats: Record<string, Stat>;
|
||||
series: Record<string, Series>;
|
||||
stats: Record<string, Stat & { label: string }>;
|
||||
series: Record<string, Series & { label: string }>;
|
||||
}
|
||||
|
||||
export interface MetricsFetchDataResponse extends FetchDataResponse {
|
||||
|
|
17
x-pack/plugins/observability/public/typings/section/index.ts
Normal file
17
x-pack/plugins/observability/public/typings/section/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ObservabilityApp } from '../../../typings/common';
|
||||
|
||||
export interface ISection {
|
||||
id: ObservabilityApp | 'alert';
|
||||
title: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
href?: string;
|
||||
linkTitle?: string;
|
||||
target?: '_blank';
|
||||
}
|
15
x-pack/plugins/observability/public/utils/date.ts
Normal file
15
x-pack/plugins/observability/public/utils/date.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import datemath from '@elastic/datemath';
|
||||
|
||||
export function getParsedDate(range?: string, opts = {}) {
|
||||
if (range) {
|
||||
const parsed = datemath.parse(range, opts);
|
||||
if (parsed) {
|
||||
return parsed.toISOString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { formatStatValue } from './format_stat_value';
|
||||
import { Stat } from '../typings';
|
||||
|
||||
describe('formatStatValue', () => {
|
||||
it('formats value as number', () => {
|
||||
const stat = {
|
||||
type: 'number',
|
||||
label: 'numeral stat',
|
||||
value: 1000,
|
||||
} as Stat;
|
||||
expect(formatStatValue(stat)).toEqual('1k');
|
||||
});
|
||||
it('formats value as bytes', () => {
|
||||
expect(
|
||||
formatStatValue({
|
||||
type: 'bytesPerSecond',
|
||||
label: 'bytes stat',
|
||||
value: 1,
|
||||
} as Stat)
|
||||
).toEqual('1.0B/s');
|
||||
expect(
|
||||
formatStatValue({
|
||||
type: 'bytesPerSecond',
|
||||
label: 'bytes stat',
|
||||
value: 1048576,
|
||||
} as Stat)
|
||||
).toEqual('1.0MB/s');
|
||||
expect(
|
||||
formatStatValue({
|
||||
type: 'bytesPerSecond',
|
||||
label: 'bytes stat',
|
||||
value: 1073741824,
|
||||
} as Stat)
|
||||
).toEqual('1.0GB/s');
|
||||
});
|
||||
it('formats value as percent', () => {
|
||||
const stat = {
|
||||
type: 'percent',
|
||||
label: 'percent stat',
|
||||
value: 0.841,
|
||||
} as Stat;
|
||||
expect(formatStatValue(stat)).toEqual('84.1%');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import numeral from '@elastic/numeral';
|
||||
import { Stat } from '../typings';
|
||||
|
||||
export function formatStatValue(stat: Stat) {
|
||||
const { value, type } = stat;
|
||||
switch (type) {
|
||||
case 'bytesPerSecond':
|
||||
return `${numeral(value).format('0.0b')}/s`;
|
||||
case 'number':
|
||||
return numeral(value).format('0a');
|
||||
case 'percent':
|
||||
return numeral(value).format('0.0%');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
const d = moment.duration;
|
||||
|
||||
const roundingRules = [
|
||||
[d(500, 'ms'), d(100, 'ms')],
|
||||
[d(5, 'second'), d(1, 'second')],
|
||||
[d(7.5, 'second'), d(5, 'second')],
|
||||
[d(15, 'second'), d(10, 'second')],
|
||||
[d(45, 'second'), d(30, 'second')],
|
||||
[d(3, 'minute'), d(1, 'minute')],
|
||||
[d(9, 'minute'), d(5, 'minute')],
|
||||
[d(20, 'minute'), d(10, 'minute')],
|
||||
[d(45, 'minute'), d(30, 'minute')],
|
||||
[d(2, 'hour'), d(1, 'hour')],
|
||||
[d(6, 'hour'), d(3, 'hour')],
|
||||
[d(24, 'hour'), d(12, 'hour')],
|
||||
[d(1, 'week'), d(1, 'd')],
|
||||
[d(3, 'week'), d(1, 'week')],
|
||||
[d(1, 'year'), d(1, 'month')],
|
||||
[Infinity, d(1, 'year')],
|
||||
];
|
||||
|
||||
const revRoundingRules = roundingRules.slice(0).reverse();
|
||||
|
||||
function find(rules, check, last) {
|
||||
function pick(buckets, duration) {
|
||||
const target = duration / buckets;
|
||||
let lastResp = null;
|
||||
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i];
|
||||
const resp = check(rule[0], rule[1], target);
|
||||
|
||||
if (resp == null) {
|
||||
if (!last) continue;
|
||||
if (lastResp) return lastResp;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!last) return resp;
|
||||
lastResp = resp;
|
||||
}
|
||||
|
||||
// fallback to just a number of milliseconds, ensure ms is >= 1
|
||||
const ms = Math.max(Math.floor(target), 1);
|
||||
return moment.duration(ms, 'ms');
|
||||
}
|
||||
|
||||
return (buckets, duration) => {
|
||||
const interval = pick(buckets, duration);
|
||||
if (interval) return moment.duration(interval._data);
|
||||
};
|
||||
}
|
||||
|
||||
export const calculateAuto = {
|
||||
near: find(
|
||||
revRoundingRules,
|
||||
function near(bound, interval, target) {
|
||||
if (bound > target) return interval;
|
||||
},
|
||||
true
|
||||
),
|
||||
|
||||
lessThan: find(revRoundingRules, function lessThan(_bound, interval, target) {
|
||||
if (interval < target) return interval;
|
||||
}),
|
||||
|
||||
atLeast: find(revRoundingRules, function atLeast(_bound, interval, target) {
|
||||
if (interval <= target) return interval;
|
||||
}),
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { getBucketSize } from './index';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('getBuckets', () => {
|
||||
describe("minInterval 'auto'", () => {
|
||||
it('last 15 minutes', () => {
|
||||
const start = moment().subtract(15, 'minutes').valueOf();
|
||||
const end = moment.now();
|
||||
expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({
|
||||
bucketSize: 10,
|
||||
intervalString: '10s',
|
||||
});
|
||||
});
|
||||
it('last 1 hour', () => {
|
||||
const start = moment().subtract(1, 'hour').valueOf();
|
||||
const end = moment.now();
|
||||
expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({
|
||||
bucketSize: 30,
|
||||
intervalString: '30s',
|
||||
});
|
||||
});
|
||||
it('last 1 week', () => {
|
||||
const start = moment().subtract(1, 'week').valueOf();
|
||||
const end = moment.now();
|
||||
expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({
|
||||
bucketSize: 3600,
|
||||
intervalString: '3600s',
|
||||
});
|
||||
});
|
||||
it('last 30 days', () => {
|
||||
const start = moment().subtract(30, 'days').valueOf();
|
||||
const end = moment.now();
|
||||
expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({
|
||||
bucketSize: 43200,
|
||||
intervalString: '43200s',
|
||||
});
|
||||
});
|
||||
it('last 1 year', () => {
|
||||
const start = moment().subtract(1, 'year').valueOf();
|
||||
const end = moment.now();
|
||||
expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({
|
||||
bucketSize: 86400,
|
||||
intervalString: '86400s',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("minInterval '30s'", () => {
|
||||
it('last 15 minutes', () => {
|
||||
const start = moment().subtract(15, 'minutes').valueOf();
|
||||
const end = moment.now();
|
||||
expect(getBucketSize({ start, end, minInterval: '30s' })).toEqual({
|
||||
bucketSize: 30,
|
||||
intervalString: '30s',
|
||||
});
|
||||
});
|
||||
it('last 1 year', () => {
|
||||
const start = moment().subtract(1, 'year').valueOf();
|
||||
const end = moment.now();
|
||||
expect(getBucketSize({ start, end, minInterval: '30s' })).toEqual({
|
||||
bucketSize: 86400,
|
||||
intervalString: '86400s',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
// @ts-ignore
|
||||
import { calculateAuto } from './calculate_auto';
|
||||
import { unitToSeconds } from './unit_to_seconds';
|
||||
|
||||
export function getBucketSize({
|
||||
start,
|
||||
end,
|
||||
minInterval,
|
||||
}: {
|
||||
start: number;
|
||||
end: number;
|
||||
minInterval: string;
|
||||
}) {
|
||||
const duration = moment.duration(end - start, 'ms');
|
||||
const bucketSize = Math.max(calculateAuto.near(100, duration).asSeconds(), 1);
|
||||
const intervalString = `${bucketSize}s`;
|
||||
const matches = minInterval && minInterval.match(/^([\d]+)([shmdwMy]|ms)$/);
|
||||
const minBucketSize = matches ? Number(matches[1]) * unitToSeconds(matches[2]) : 0;
|
||||
|
||||
if (bucketSize < minBucketSize) {
|
||||
return {
|
||||
bucketSize: minBucketSize,
|
||||
intervalString: minInterval,
|
||||
};
|
||||
}
|
||||
|
||||
return { bucketSize, intervalString };
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import moment, { unitOfTime as UnitOfTIme } from 'moment';
|
||||
|
||||
function getDurationAsSeconds(value: number, unitOfTime: UnitOfTIme.Base) {
|
||||
return moment.duration(value, unitOfTime).asSeconds();
|
||||
}
|
||||
|
||||
const units = {
|
||||
ms: getDurationAsSeconds(1, 'millisecond'),
|
||||
s: getDurationAsSeconds(1, 'second'),
|
||||
m: getDurationAsSeconds(1, 'minute'),
|
||||
h: getDurationAsSeconds(1, 'hour'),
|
||||
d: getDurationAsSeconds(1, 'day'),
|
||||
w: getDurationAsSeconds(1, 'week'),
|
||||
M: getDurationAsSeconds(1, 'month'),
|
||||
y: getDurationAsSeconds(1, 'year'),
|
||||
};
|
||||
|
||||
export function unitToSeconds(unit: string) {
|
||||
return units[unit as keyof typeof units];
|
||||
}
|
26
x-pack/plugins/observability/public/utils/test_helper.tsx
Normal file
26
x-pack/plugins/observability/public/utils/test_helper.tsx
Normal file
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render as testLibRender } from '@testing-library/react';
|
||||
import { AppMountContext } from 'kibana/public';
|
||||
import { PluginContext } from '../context/plugin_context';
|
||||
import { EuiThemeProvider } from '../typings';
|
||||
|
||||
export const core = ({
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: jest.fn(),
|
||||
},
|
||||
},
|
||||
} as unknown) as AppMountContext['core'];
|
||||
|
||||
export const render = (component: React.ReactNode) => {
|
||||
return testLibRender(
|
||||
<PluginContext.Provider value={{ core }}>
|
||||
<EuiThemeProvider>{component}</EuiThemeProvider>
|
||||
</PluginContext.Provider>
|
||||
);
|
||||
};
|
19
x-pack/plugins/observability/public/utils/url.ts
Normal file
19
x-pack/plugins/observability/public/utils/url.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { parse, stringify } from 'query-string';
|
||||
import { url } from '../../../../../src/plugins/kibana_utils/public';
|
||||
|
||||
export function toQuery(search?: string) {
|
||||
return search ? parse(search.slice(1), { sort: false }) : {};
|
||||
}
|
||||
|
||||
export function fromQuery(query: Record<string, any>) {
|
||||
const encodedQuery = url.encodeQuery(query, (value) =>
|
||||
encodeURIComponent(value).replace(/%3A/g, ':')
|
||||
);
|
||||
|
||||
return stringify(encodedQuery, { sort: false, encode: false });
|
||||
}
|
16
x-pack/plugins/observability/scripts/storybook.js
Normal file
16
x-pack/plugins/observability/scripts/storybook.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { join } from 'path';
|
||||
|
||||
// eslint-disable-next-line
|
||||
require('@kbn/storybook').runStorybookCli({
|
||||
name: 'observability',
|
||||
storyGlobs: [
|
||||
join(__dirname, '..', 'public', 'components', '**', '*.stories.tsx'),
|
||||
join(__dirname, '..', 'public', 'pages', '**', '*.stories.tsx'),
|
||||
],
|
||||
});
|
|
@ -29,29 +29,24 @@ export async function fetchUptimeOverviewData({
|
|||
stats: {
|
||||
monitors: {
|
||||
type: 'number',
|
||||
label: 'Monitors',
|
||||
value: snapshot.total,
|
||||
},
|
||||
up: {
|
||||
type: 'number',
|
||||
label: 'Up',
|
||||
value: snapshot.up,
|
||||
},
|
||||
down: {
|
||||
type: 'number',
|
||||
label: 'Down',
|
||||
value: snapshot.down,
|
||||
},
|
||||
},
|
||||
series: {
|
||||
up: {
|
||||
label: 'Up',
|
||||
coordinates: pings.histogram.map((p) => {
|
||||
return { x: p.x!, y: p.upCount || 0 };
|
||||
}),
|
||||
},
|
||||
down: {
|
||||
label: 'Down',
|
||||
coordinates: pings.histogram.map((p) => {
|
||||
return { x: p.x!, y: p.downCount || 0 };
|
||||
}),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue