mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
feat(slo): remove feature flag (#152834)
This commit is contained in:
parent
b449264e61
commit
a04c420569
28 changed files with 1144 additions and 697 deletions
|
@ -134,6 +134,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"siem-ui-timeline": "e9d6b3a9fd7af6dc502293c21cbdb309409f3996",
|
||||
"siem-ui-timeline-note": "13c9d4c142f96624a93a623c6d7cba7e1ae9b5a6",
|
||||
"siem-ui-timeline-pinned-event": "96a43d59b9e2fc11f12255a0cb47ef0a3d83af4c",
|
||||
"slo": "9a138b459c7efef7fecfda91f22db8b7655d0e61",
|
||||
"space": "9542afcd6fd71558623c09151e453c5e84b4e5e1",
|
||||
"spaces-usage-stats": "084bd0f080f94fb5735d7f3cf12f13ec92f36bad",
|
||||
"synthetics-monitor": "96cc312bfa597022f83dfb3b5d1501e27a73e8d5",
|
||||
|
|
|
@ -107,6 +107,7 @@ const previouslyRegisteredTypes = [
|
|||
'siem-ui-timeline',
|
||||
'siem-ui-timeline-note',
|
||||
'siem-ui-timeline-pinned-event',
|
||||
'slo',
|
||||
'space',
|
||||
'spaces-usage-stats',
|
||||
'synthetics-monitor',
|
||||
|
|
|
@ -300,7 +300,6 @@ kibana_vars=(
|
|||
xpack.ingestManager.fleet.tlsCheckDisabled
|
||||
xpack.ingestManager.registryUrl
|
||||
xpack.observability.annotations.index
|
||||
xpack.observability.unsafe.slo.enabled
|
||||
xpack.observability.unsafe.alertDetails.apm.enabled
|
||||
xpack.observability.unsafe.alertDetails.metrics.enabled
|
||||
xpack.observability.unsafe.alertDetails.logs.enabled
|
||||
|
|
|
@ -232,7 +232,6 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.observability.unsafe.alertDetails.metrics.enabled (boolean)',
|
||||
'xpack.observability.unsafe.alertDetails.logs.enabled (boolean)',
|
||||
'xpack.observability.unsafe.alertDetails.uptime.enabled (boolean)',
|
||||
'xpack.observability.unsafe.slo.enabled (boolean)',
|
||||
];
|
||||
// We don't assert that actualExposedConfigKeys and expectedExposedConfigKeys are equal, because test failure messages with large
|
||||
// arrays are hard to grok. Instead, we take the difference between the two arrays and assert them separately, that way it's
|
||||
|
|
|
@ -13,16 +13,6 @@ xpack.ruleRegistry.write.enabled: true
|
|||
|
||||
When this is set to `true`, your alerts should show on the alerts page.
|
||||
|
||||
## SLOs
|
||||
|
||||
|
||||
If you have:
|
||||
|
||||
```yaml
|
||||
xpack.observability.unsafe.slo.enabled: true
|
||||
```
|
||||
|
||||
In your Kibana configuration, the SLO feature will be available.
|
||||
|
||||
## Shared navigation
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# SLO
|
||||
|
||||
Add the feature flag: `xpack.observability.unsafe.slo.enabled: true` in your Kibana config to enable the various SLO APIs.
|
||||
Starting in 8.8, SLO is enabled by default.
|
||||
|
||||
## Supported SLI
|
||||
|
||||
|
|
|
@ -0,0 +1,522 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { makeDecorator } from '@storybook/addons';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { AppMountParameters, CoreStart } from '@kbn/core/public';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { HasDataContextProvider } from '../../context/has_data_context';
|
||||
import { PluginContext } from '../../context/plugin_context';
|
||||
import { registerDataHandler, unregisterDataHandler } from '../../data_handler';
|
||||
import { OverviewPage } from './overview';
|
||||
import { alertsFetchData } from './mock/alerts.mock';
|
||||
import { emptyResponse as emptyAPMResponse, fetchApmData } from './mock/apm.mock';
|
||||
import { emptyResponse as emptyLogsResponse, fetchLogsData } from './mock/logs.mock';
|
||||
import { emptyResponse as emptyMetricsResponse, fetchMetricsData } from './mock/metrics.mock';
|
||||
import { newsFeedFetchData } from './mock/news_feed.mock';
|
||||
import { emptyResponse as emptyUptimeResponse, fetchUptimeData } from './mock/uptime.mock';
|
||||
import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock';
|
||||
import { ApmIndicesConfig } from '../../../common/typings';
|
||||
import { ConfigSchema } from '../../plugin';
|
||||
|
||||
function unregisterAll() {
|
||||
unregisterDataHandler({ appName: 'apm' });
|
||||
unregisterDataHandler({ appName: 'infra_logs' });
|
||||
unregisterDataHandler({ appName: 'infra_metrics' });
|
||||
unregisterDataHandler({ appName: 'synthetics' });
|
||||
}
|
||||
|
||||
const sampleAPMIndices = { transaction: 'apm-*' } as ApmIndicesConfig;
|
||||
|
||||
const withCore = makeDecorator({
|
||||
name: 'withCore',
|
||||
parameterName: 'core',
|
||||
wrapper: (storyFn, context) => {
|
||||
unregisterAll();
|
||||
const KibanaReactContext = createKibanaReactContext({
|
||||
application: {
|
||||
getUrlForApp: () => '',
|
||||
capabilities: { navLinks: { integrations: true } },
|
||||
currentAppId$: {
|
||||
subscribe: () => {},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
query: {
|
||||
timefilter: {
|
||||
timefilter: {
|
||||
setTime: () => {},
|
||||
getTime: () => ({}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: (link: string) => `http://localhost:5601${link}`,
|
||||
},
|
||||
},
|
||||
chrome: {
|
||||
docTitle: {
|
||||
change: () => {},
|
||||
},
|
||||
},
|
||||
uiSettings: { get: () => [] },
|
||||
usageCollection: {
|
||||
reportUiCounter: () => {},
|
||||
},
|
||||
} as unknown as Partial<CoreStart>);
|
||||
|
||||
const config: ConfigSchema = {
|
||||
unsafe: {
|
||||
alertDetails: {
|
||||
apm: { enabled: false },
|
||||
logs: { enabled: false },
|
||||
metrics: { enabled: false },
|
||||
uptime: { enabled: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<KibanaReactContext.Provider>
|
||||
<PluginContext.Provider
|
||||
value={{
|
||||
appMountParameters: {
|
||||
setHeaderActionMenu: () => {},
|
||||
} as unknown as AppMountParameters,
|
||||
config,
|
||||
observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(),
|
||||
ObservabilityPageTemplate: KibanaPageTemplate,
|
||||
}}
|
||||
>
|
||||
<HasDataContextProvider>{storyFn(context) as ReactNode}</HasDataContextProvider>
|
||||
</PluginContext.Provider>
|
||||
</KibanaReactContext.Provider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const core = {
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: (link: string) => `http://localhost:5601${link}`,
|
||||
},
|
||||
get: () => Promise.resolve({ data: [] }),
|
||||
},
|
||||
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];
|
||||
},
|
||||
},
|
||||
docLinks: {
|
||||
links: {
|
||||
observability: {
|
||||
guide: 'alink',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
|
||||
const coreWithAlerts = {
|
||||
...core,
|
||||
http: {
|
||||
...core.http,
|
||||
get: alertsFetchData,
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
|
||||
const coreWithNewsFeed = {
|
||||
...core,
|
||||
http: {
|
||||
...core.http,
|
||||
get: newsFeedFetchData,
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
|
||||
const coreAlertsThrowsError = {
|
||||
...core,
|
||||
http: {
|
||||
...core.http,
|
||||
get: async () => {
|
||||
throw new Error('Error fetching Alerts data');
|
||||
},
|
||||
},
|
||||
} as unknown as CoreStart;
|
||||
|
||||
storiesOf('app/Overview', module)
|
||||
.addDecorator(withCore(core))
|
||||
.add('Empty State', () => {
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => ({ hasData: false, indices: sampleAPMIndices }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => ({ hasData: false, indices: 'test-index' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => ({ hasData: false, indices: 'metric-*' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'synthetics',
|
||||
fetchData: fetchUptimeData,
|
||||
hasData: async () => ({ hasData: false, indices: 'heartbeat-*,synthetics-*' }),
|
||||
});
|
||||
|
||||
return <OverviewPage />;
|
||||
})
|
||||
.add('Single Panel', () => {
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'test-index' }),
|
||||
});
|
||||
|
||||
return <OverviewPage />;
|
||||
})
|
||||
.add('Logs and Metrics', () => {
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'test-index' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'metric-*' }),
|
||||
});
|
||||
|
||||
return <OverviewPage />;
|
||||
})
|
||||
.add(
|
||||
'Logs, Metrics, and Alerts',
|
||||
() => {
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'test-index' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'metric-*' }),
|
||||
});
|
||||
|
||||
return <OverviewPage />;
|
||||
},
|
||||
{ core: coreWithAlerts }
|
||||
)
|
||||
.add(
|
||||
'Logs, Metrics, APM, and Alerts',
|
||||
() => {
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'test-index' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'metric-*' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
|
||||
return <OverviewPage />;
|
||||
},
|
||||
{ core: coreWithAlerts }
|
||||
)
|
||||
.add('Logs, Metrics, APM, and Uptime', () => {
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'test-index' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'metric-*' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'synthetics',
|
||||
fetchData: fetchUptimeData,
|
||||
hasData: async () => ({ hasData: true, indices: 'heartbeat-*,synthetics-*' }),
|
||||
});
|
||||
|
||||
return <OverviewPage />;
|
||||
})
|
||||
.add(
|
||||
'Logs, Metrics, APM, Uptime, and Alerts',
|
||||
() => {
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'test-index' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'metric-*' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'synthetics',
|
||||
fetchData: fetchUptimeData,
|
||||
hasData: async () => ({ hasData: true, indices: 'heartbeat-*,synthetics-*' }),
|
||||
});
|
||||
|
||||
return <OverviewPage />;
|
||||
},
|
||||
{ core: coreWithAlerts }
|
||||
)
|
||||
.add(
|
||||
'Logs, Metrics, APM, Uptime, and News Feed',
|
||||
() => {
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'test-index' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
hasData: async () => ({ hasData: true, indices: 'metric-*' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'synthetics',
|
||||
fetchData: fetchUptimeData,
|
||||
hasData: async () => ({ hasData: true, indices: 'heartbeat-*,synthetics-*' }),
|
||||
});
|
||||
return <OverviewPage />;
|
||||
},
|
||||
{ core: coreWithNewsFeed }
|
||||
)
|
||||
.add('No Data', () => {
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: async () => emptyAPMResponse,
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: async () => emptyLogsResponse,
|
||||
hasData: async () => ({ hasData: true, indices: 'test-index' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: async () => emptyMetricsResponse,
|
||||
hasData: async () => ({ hasData: true, indices: 'metric-*' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'synthetics',
|
||||
fetchData: async () => emptyUptimeResponse,
|
||||
hasData: async () => ({ hasData: true, indices: 'heartbeat-*,synthetics-*' }),
|
||||
});
|
||||
|
||||
return <OverviewPage />;
|
||||
})
|
||||
.add(
|
||||
'Fetch Data with Error',
|
||||
() => {
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: async () => {
|
||||
throw new Error('Error fetching APM data');
|
||||
},
|
||||
hasData: async () => ({ hasData: true, indices: sampleAPMIndices }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: async () => {
|
||||
throw new Error('Error fetching Logs data');
|
||||
},
|
||||
hasData: async () => ({ hasData: true, indices: 'test-index' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: async () => {
|
||||
throw new Error('Error fetching Metric data');
|
||||
},
|
||||
hasData: async () => ({ hasData: true, indices: 'metric-*' }),
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'synthetics',
|
||||
fetchData: async () => {
|
||||
throw new Error('Error fetching Uptime data');
|
||||
},
|
||||
hasData: async () => ({ hasData: true, indices: 'heartbeat-*,synthetics-*' }),
|
||||
});
|
||||
return <OverviewPage />;
|
||||
},
|
||||
{ core: coreAlertsThrowsError }
|
||||
)
|
||||
.add(
|
||||
'hasData with Error and Alerts',
|
||||
() => {
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
// @ts-ignore throws an error instead
|
||||
hasData: async () => {
|
||||
throw new Error('Error has data');
|
||||
},
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
// @ts-ignore throws an error instead
|
||||
hasData: async () => {
|
||||
throw new Error('Error has data');
|
||||
},
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
// @ts-ignore throws an error instead
|
||||
hasData: async () => {
|
||||
throw new Error('Error has data');
|
||||
},
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'synthetics',
|
||||
fetchData: fetchUptimeData,
|
||||
// @ts-ignore throws an error instead
|
||||
hasData: async () => {
|
||||
throw new Error('Error has data');
|
||||
},
|
||||
});
|
||||
return <OverviewPage />;
|
||||
},
|
||||
{ core: coreWithAlerts }
|
||||
)
|
||||
.add('hasData with Error', () => {
|
||||
registerDataHandler({
|
||||
appName: 'apm',
|
||||
fetchData: fetchApmData,
|
||||
// @ts-ignore throws an error instead
|
||||
hasData: async () => {
|
||||
throw new Error('Error has data');
|
||||
},
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_logs',
|
||||
fetchData: fetchLogsData,
|
||||
// @ts-ignore throws an error instead
|
||||
hasData: async () => {
|
||||
throw new Error('Error has data');
|
||||
},
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'infra_metrics',
|
||||
fetchData: fetchMetricsData,
|
||||
// @ts-ignore throws an error instead
|
||||
hasData: async () => {
|
||||
throw new Error('Error has data');
|
||||
},
|
||||
});
|
||||
registerDataHandler({
|
||||
appName: 'synthetics',
|
||||
fetchData: fetchUptimeData,
|
||||
// @ts-ignore throws an error instead
|
||||
hasData: async () => {
|
||||
throw new Error('Error has data');
|
||||
},
|
||||
});
|
||||
|
||||
return <OverviewPage />;
|
||||
});
|
|
@ -15,8 +15,6 @@ import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
|
|||
import { render } from '../../utils/test_helper';
|
||||
import { SloDetailsPage } from './slo_details';
|
||||
import { buildSlo } from '../../data/slo/slo';
|
||||
import type { ConfigSchema } from '../../plugin';
|
||||
import type { Subset } from '../../typings';
|
||||
import { paths } from '../../config';
|
||||
import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_summary';
|
||||
import { useCapabilities } from '../../hooks/slo/use_capabilities';
|
||||
|
@ -65,12 +63,6 @@ const mockKibana = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const config: Subset<ConfigSchema> = {
|
||||
unsafe: {
|
||||
slo: { enabled: true },
|
||||
},
|
||||
};
|
||||
|
||||
describe('SLO Details Page', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -82,67 +74,52 @@ describe('SLO Details Page', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when the feature flag is not enabled', () => {
|
||||
it('renders the not found page', async () => {
|
||||
describe('when the incorrect license is found', () => {
|
||||
it('navigates to the SLO List page', async () => {
|
||||
const slo = buildSlo();
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(mockNavigate).toBeCalledWith(mockBasePathPrepend(paths.observability.slos));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the correct license is found', () => {
|
||||
it('renders the not found page when the SLO cannot be found', async () => {
|
||||
useParamsMock.mockReturnValue('nonexistent');
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo: undefined });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the loading spinner when fetching the SLO', async () => {
|
||||
const slo = buildSlo();
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: true, slo: undefined });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeFalsy();
|
||||
expect(screen.queryByTestId('loadingTitle')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloDetailsLoading')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the SLO details page', async () => {
|
||||
const slo = buildSlo();
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />, { unsafe: { slo: { enabled: false } } });
|
||||
render(<SloDetailsPage />);
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the feature flag is enabled', () => {
|
||||
describe('when the incorrect license is found', () => {
|
||||
it('navigates to the SLO List page', async () => {
|
||||
const slo = buildSlo();
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
|
||||
render(<SloDetailsPage />, { unsafe: { slo: { enabled: true } } });
|
||||
|
||||
expect(mockNavigate).toBeCalledWith(mockBasePathPrepend(paths.observability.slos));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the correct license is found', () => {
|
||||
it('renders the not found page when the SLO cannot be found', async () => {
|
||||
useParamsMock.mockReturnValue('nonexistent');
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo: undefined });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the loading spinner when fetching the SLO', async () => {
|
||||
const slo = buildSlo();
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: true, slo: undefined });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeFalsy();
|
||||
expect(screen.queryByTestId('loadingTitle')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloDetailsLoading')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the SLO details page', async () => {
|
||||
const slo = buildSlo();
|
||||
useParamsMock.mockReturnValue(slo.id);
|
||||
useFetchSloDetailsMock.mockReturnValue({ isLoading: false, slo });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
|
||||
render(<SloDetailsPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('sloDetailsPage')).toBeTruthy();
|
||||
});
|
||||
expect(screen.queryByTestId('sloDetailsPage')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,7 +19,6 @@ import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
|
|||
import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import PageNotFound from '../404';
|
||||
import { isSloFeatureEnabled } from '../slos/helpers/is_slo_feature_enabled';
|
||||
import { SloDetails } from './components/slo_details';
|
||||
import { HeaderTitle } from './components/header_title';
|
||||
import { paths } from '../../config';
|
||||
|
@ -32,7 +31,7 @@ export function SloDetailsPage() {
|
|||
application: { navigateToUrl },
|
||||
http: { basePath },
|
||||
} = useKibana<ObservabilityAppServices>().services;
|
||||
const { ObservabilityPageTemplate, config } = usePluginContext();
|
||||
const { ObservabilityPageTemplate } = usePluginContext();
|
||||
const { hasAtLeast } = useLicense();
|
||||
const hasRightLicense = hasAtLeast('platinum');
|
||||
|
||||
|
@ -41,7 +40,7 @@ export function SloDetailsPage() {
|
|||
useBreadcrumbs(getBreadcrumbs(basePath, slo));
|
||||
|
||||
const isSloNotFound = !isLoading && slo === undefined;
|
||||
if (!isSloFeatureEnabled(config) || isSloNotFound) {
|
||||
if (isSloNotFound) {
|
||||
return <PageNotFound />;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,8 +19,6 @@ import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
|
|||
import { useCreateSlo } from '../../hooks/slo/use_create_slo';
|
||||
import { useUpdateSlo } from '../../hooks/slo/use_update_slo';
|
||||
import { kibanaStartMock } from '../../utils/kibana_react.mock';
|
||||
import { ConfigSchema } from '../../plugin';
|
||||
import { Subset } from '../../typings';
|
||||
import { SLO_EDIT_FORM_DEFAULT_VALUES } from './constants';
|
||||
import { buildSlo } from '../../data/slo/slo';
|
||||
import { paths } from '../../config';
|
||||
|
@ -102,12 +100,6 @@ const mockKibana = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const config: Subset<ConfigSchema> = {
|
||||
unsafe: {
|
||||
slo: { enabled: true },
|
||||
},
|
||||
};
|
||||
|
||||
describe('SLO Edit Page', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -119,38 +111,51 @@ describe('SLO Edit Page', () => {
|
|||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('when the feature flag is disabled', () => {
|
||||
it('renders the not found page when no sloId param is passed', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
||||
|
||||
describe('when the incorrect license is found', () => {
|
||||
beforeEach(() => {
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo: undefined });
|
||||
|
||||
render(<SloEditPage />, { unsafe: { slo: { enabled: false } } });
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders the not found page when sloId param is passed', async () => {
|
||||
it('navigates to the SLO List page', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '1234' });
|
||||
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo: undefined });
|
||||
|
||||
render(<SloEditPage />, { unsafe: { slo: { enabled: false } } });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
useCreateSloMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
mutate: jest.fn(),
|
||||
mutateAsync: jest.fn(),
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
mutate: jest.fn(),
|
||||
mutateAsync: jest.fn(),
|
||||
});
|
||||
|
||||
render(<SloEditPage />);
|
||||
|
||||
expect(mockNavigate).toBeCalledWith(mockBasePathPrepend(paths.observability.slos));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the feature flag is enabled', () => {
|
||||
describe('when the incorrect license is found', () => {
|
||||
beforeEach(() => {
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
});
|
||||
describe('when the correct license is found', () => {
|
||||
beforeEach(() => {
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
});
|
||||
|
||||
it('navigates to the SLO List page', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '1234' });
|
||||
describe('when no sloId route param is provided', () => {
|
||||
it('renders the SLO Edit page in pristine state', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo: undefined });
|
||||
|
||||
|
@ -160,154 +165,110 @@ describe('SLO Edit Page', () => {
|
|||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
mutate: jest.fn(),
|
||||
mutateAsync: jest.fn(),
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
mutate: jest.fn(),
|
||||
mutateAsync: jest.fn(),
|
||||
});
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
render(<SloEditPage />);
|
||||
|
||||
expect(mockNavigate).toBeCalledWith(mockBasePathPrepend(paths.observability.slos));
|
||||
});
|
||||
});
|
||||
expect(screen.queryByTestId('slosEditPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloForm')).toBeTruthy();
|
||||
|
||||
describe('when the correct license is found', () => {
|
||||
beforeEach(() => {
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
expect(screen.queryByTestId('sloFormIndicatorTypeSelect')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('indexSelectionSelectedValue')).toBeNull();
|
||||
|
||||
expect(screen.queryByTestId('customKqlIndicatorFormQueryFilterInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.filter
|
||||
: ''
|
||||
);
|
||||
expect(screen.queryByTestId('customKqlIndicatorFormGoodQueryInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.good
|
||||
: ''
|
||||
);
|
||||
expect(screen.queryByTestId('customKqlIndicatorFormTotalQueryInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.total
|
||||
: ''
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormBudgetingMethodSelect')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.budgetingMethod
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormTimeWindowDurationSelect')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.timeWindow.duration as any
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormObjectiveTargetInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.objective.target
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormNameInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.name
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormDescriptionTextArea')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.description
|
||||
);
|
||||
});
|
||||
|
||||
describe('when no sloId route param is provided', () => {
|
||||
it('renders the SLO Edit page in pristine state', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo: undefined });
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('slosEditPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloForm')).toBeTruthy();
|
||||
|
||||
expect(screen.queryByTestId('sloFormIndicatorTypeSelect')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('indexSelectionSelectedValue')).toBeNull();
|
||||
|
||||
expect(screen.queryByTestId('customKqlIndicatorFormQueryFilterInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.filter
|
||||
: ''
|
||||
);
|
||||
expect(screen.queryByTestId('customKqlIndicatorFormGoodQueryInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.good
|
||||
: ''
|
||||
);
|
||||
expect(screen.queryByTestId('customKqlIndicatorFormTotalQueryInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.indicator.type === 'sli.kql.custom'
|
||||
? SLO_EDIT_FORM_DEFAULT_VALUES.indicator.params.total
|
||||
: ''
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormBudgetingMethodSelect')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.budgetingMethod
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormTimeWindowDurationSelect')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.timeWindow.duration as any
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormObjectiveTargetInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.objective.target
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormNameInput')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.name
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormDescriptionTextArea')).toHaveValue(
|
||||
SLO_EDIT_FORM_DEFAULT_VALUES.description
|
||||
);
|
||||
it.skip('calls the createSlo hook if all required values are filled in', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
it.skip('calls the createSlo hook if all required values are filled in', async () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo: undefined });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo: undefined });
|
||||
const mockCreate = jest.fn();
|
||||
const mockUpdate = jest.fn();
|
||||
|
||||
const mockCreate = jest.fn();
|
||||
const mockUpdate = jest.fn();
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: mockCreate,
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: mockCreate,
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: mockUpdate,
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: mockUpdate,
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
render(<SloEditPage />);
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
userEvent.type(screen.getByTestId('indexSelection'), 'some-index');
|
||||
userEvent.type(screen.getByTestId('customKqlIndicatorFormQueryFilterInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('customKqlIndicatorFormGoodQueryInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('customKqlIndicatorFormTotalQueryInput'), 'irrelevant');
|
||||
userEvent.selectOptions(screen.getByTestId('sloFormBudgetingMethodSelect'), 'occurrences');
|
||||
userEvent.selectOptions(screen.getByTestId('sloFormTimeWindowDurationSelect'), '7d');
|
||||
userEvent.clear(screen.getByTestId('sloFormObjectiveTargetInput'));
|
||||
userEvent.type(screen.getByTestId('sloFormObjectiveTargetInput'), '98.5');
|
||||
userEvent.type(screen.getByTestId('sloFormNameInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('sloFormDescriptionTextArea'), 'irrelevant');
|
||||
|
||||
userEvent.type(screen.getByTestId('indexSelection'), 'some-index');
|
||||
userEvent.type(
|
||||
screen.getByTestId('customKqlIndicatorFormQueryFilterInput'),
|
||||
'irrelevant'
|
||||
);
|
||||
userEvent.type(screen.getByTestId('customKqlIndicatorFormGoodQueryInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('customKqlIndicatorFormTotalQueryInput'), 'irrelevant');
|
||||
userEvent.selectOptions(
|
||||
screen.getByTestId('sloFormBudgetingMethodSelect'),
|
||||
'occurrences'
|
||||
);
|
||||
userEvent.selectOptions(screen.getByTestId('sloFormTimeWindowDurationSelect'), '7d');
|
||||
userEvent.clear(screen.getByTestId('sloFormObjectiveTargetInput'));
|
||||
userEvent.type(screen.getByTestId('sloFormObjectiveTargetInput'), '98.5');
|
||||
userEvent.type(screen.getByTestId('sloFormNameInput'), 'irrelevant');
|
||||
userEvent.type(screen.getByTestId('sloFormDescriptionTextArea'), 'irrelevant');
|
||||
const t = Date.now();
|
||||
await waitFor(() => expect(screen.getByTestId('sloFormSubmitButton')).toBeEnabled());
|
||||
console.log('end waiting for submit button: ', Math.ceil(Date.now() - t));
|
||||
|
||||
const t = Date.now();
|
||||
await waitFor(() => expect(screen.getByTestId('sloFormSubmitButton')).toBeEnabled());
|
||||
console.log('end waiting for submit button: ', Math.ceil(Date.now() - t));
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton')!);
|
||||
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton')!);
|
||||
|
||||
expect(mockCreate).toMatchInlineSnapshot(`
|
||||
expect(mockCreate).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
|
@ -342,108 +303,106 @@ describe('SLO Edit Page', () => {
|
|||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a sloId route param is provided', () => {
|
||||
it('renders the SLO Edit page with prefilled form values', async () => {
|
||||
const slo = buildSlo({ id: '123' });
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo });
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<SloEditPage />);
|
||||
|
||||
expect(screen.queryByTestId('slosEditPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloForm')).toBeTruthy();
|
||||
|
||||
expect(screen.queryByTestId('sloFormIndicatorTypeSelect')).toHaveValue(slo.indicator.type);
|
||||
|
||||
expect(screen.queryByTestId('indexSelectionSelectedValue')).toHaveTextContent(
|
||||
slo.indicator.params.index!
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('customKqlIndicatorFormQueryFilterInput')).toHaveValue(
|
||||
slo.indicator.type === 'sli.kql.custom' ? slo.indicator.params.filter : ''
|
||||
);
|
||||
expect(screen.queryByTestId('customKqlIndicatorFormGoodQueryInput')).toHaveValue(
|
||||
slo.indicator.type === 'sli.kql.custom' ? slo.indicator.params.good : ''
|
||||
);
|
||||
expect(screen.queryByTestId('customKqlIndicatorFormTotalQueryInput')).toHaveValue(
|
||||
slo.indicator.type === 'sli.kql.custom' ? slo.indicator.params.total : ''
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormBudgetingMethodSelect')).toHaveValue(
|
||||
slo.budgetingMethod
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormTimeWindowDurationSelect')).toHaveValue(
|
||||
slo.timeWindow.duration
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormObjectiveTargetInput')).toHaveValue(
|
||||
slo.objective.target * 100
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormNameInput')).toHaveValue(slo.name);
|
||||
expect(screen.queryByTestId('sloFormDescriptionTextArea')).toHaveValue(slo.description);
|
||||
});
|
||||
|
||||
describe('when a sloId route param is provided', () => {
|
||||
it('renders the SLO Edit page with prefilled form values', async () => {
|
||||
const slo = buildSlo({ id: '123' });
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
it('calls the updateSlo hook if all required values are filled in', async () => {
|
||||
const slo = buildSlo({ id: '123' });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo });
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
expect(screen.queryByTestId('slosEditPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloForm')).toBeTruthy();
|
||||
|
||||
expect(screen.queryByTestId('sloFormIndicatorTypeSelect')).toHaveValue(
|
||||
slo.indicator.type
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('indexSelectionSelectedValue')).toHaveTextContent(
|
||||
slo.indicator.params.index!
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('customKqlIndicatorFormQueryFilterInput')).toHaveValue(
|
||||
slo.indicator.type === 'sli.kql.custom' ? slo.indicator.params.filter : ''
|
||||
);
|
||||
expect(screen.queryByTestId('customKqlIndicatorFormGoodQueryInput')).toHaveValue(
|
||||
slo.indicator.type === 'sli.kql.custom' ? slo.indicator.params.good : ''
|
||||
);
|
||||
expect(screen.queryByTestId('customKqlIndicatorFormTotalQueryInput')).toHaveValue(
|
||||
slo.indicator.type === 'sli.kql.custom' ? slo.indicator.params.total : ''
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormBudgetingMethodSelect')).toHaveValue(
|
||||
slo.budgetingMethod
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormTimeWindowDurationSelect')).toHaveValue(
|
||||
slo.timeWindow.duration
|
||||
);
|
||||
expect(screen.queryByTestId('sloFormObjectiveTargetInput')).toHaveValue(
|
||||
slo.objective.target * 100
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('sloFormNameInput')).toHaveValue(slo.name);
|
||||
expect(screen.queryByTestId('sloFormDescriptionTextArea')).toHaveValue(slo.description);
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
it('calls the updateSlo hook if all required values are filled in', async () => {
|
||||
const slo = buildSlo({ id: '123' });
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo });
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
const mockCreate = jest.fn();
|
||||
const mockUpdate = jest.fn();
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: mockCreate,
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo });
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: mockUpdate,
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
const mockCreate = jest.fn();
|
||||
const mockUpdate = jest.fn();
|
||||
render(<SloEditPage />);
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: mockCreate,
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
await waitFor(() => expect(screen.queryByTestId('sloFormSubmitButton')).toBeEnabled());
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: mockUpdate,
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
fireEvent.click(screen.queryByTestId('sloFormSubmitButton')!);
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('sloFormSubmitButton')).toBeEnabled());
|
||||
|
||||
fireEvent.click(screen.queryByTestId('sloFormSubmitButton')!);
|
||||
|
||||
expect(mockUpdate).toMatchInlineSnapshot(`
|
||||
expect(mockUpdate).toMatchInlineSnapshot(`
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
|
@ -486,138 +445,137 @@ describe('SLO Edit Page', () => {
|
|||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('blocks submitting if not all required values are filled in', async () => {
|
||||
const slo = buildSlo();
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [],
|
||||
});
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo: { ...slo, name: '' } });
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('sloFormSubmitButton')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when submitting has completed successfully', () => {
|
||||
it('renders a success toast', async () => {
|
||||
const slo = buildSlo();
|
||||
it('blocks submitting if not all required values are filled in', async () => {
|
||||
const slo = buildSlo();
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo });
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue('success'),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue('success'),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('sloFormSubmitButton')).toBeEnabled();
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton'));
|
||||
});
|
||||
|
||||
expect(mockAddSuccess).toBeCalled();
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [],
|
||||
});
|
||||
|
||||
it('navigates to the SLO List page', async () => {
|
||||
const slo = buildSlo();
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo: { ...slo, name: '' } });
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
render(<SloEditPage />);
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo });
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('sloFormSubmitButton')).toBeEnabled();
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton'));
|
||||
});
|
||||
|
||||
expect(mockNavigate).toBeCalledWith(mockBasePathPrepend(paths.observability.slos));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('sloFormSubmitButton')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when submitting has not completed successfully', () => {
|
||||
it('renders an error toast', async () => {
|
||||
const slo = buildSlo();
|
||||
describe('when submitting has completed successfully', () => {
|
||||
it('renders a success toast', async () => {
|
||||
const slo = buildSlo();
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo });
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo });
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn().mockRejectedValue('argh, I died'),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn().mockRejectedValue('argh, I died'),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<SloEditPage />, config);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('sloFormSubmitButton')).toBeEnabled();
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton'));
|
||||
});
|
||||
|
||||
expect(mockAddError).toBeCalled();
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue('success'),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn().mockResolvedValue('success'),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<SloEditPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('sloFormSubmitButton')).toBeEnabled();
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton'));
|
||||
});
|
||||
|
||||
expect(mockAddSuccess).toBeCalled();
|
||||
});
|
||||
|
||||
it('navigates to the SLO List page', async () => {
|
||||
const slo = buildSlo();
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo });
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<SloEditPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('sloFormSubmitButton')).toBeEnabled();
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton'));
|
||||
});
|
||||
|
||||
expect(mockNavigate).toBeCalledWith(mockBasePathPrepend(paths.observability.slos));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when submitting has not completed successfully', () => {
|
||||
it('renders an error toast', async () => {
|
||||
const slo = buildSlo();
|
||||
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
useFetchSloMock.mockReturnValue({ isLoading: false, slo });
|
||||
|
||||
useFetchIndicesMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
indices: [{ name: 'some-index' }],
|
||||
});
|
||||
|
||||
useCreateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn().mockRejectedValue('argh, I died'),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
useUpdateSloMock.mockReturnValue({
|
||||
mutateAsync: jest.fn().mockRejectedValue('argh, I died'),
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<SloEditPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('sloFormSubmitButton')).toBeEnabled();
|
||||
fireEvent.click(screen.getByTestId('sloFormSubmitButton'));
|
||||
});
|
||||
|
||||
expect(mockAddError).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,15 +16,13 @@ import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
|
|||
import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details';
|
||||
import { useLicense } from '../../hooks/use_license';
|
||||
import { SloEditForm } from './components/slo_edit_form';
|
||||
import PageNotFound from '../404';
|
||||
import { isSloFeatureEnabled } from '../slos/helpers/is_slo_feature_enabled';
|
||||
|
||||
export function SloEditPage() {
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
http: { basePath },
|
||||
} = useKibana().services;
|
||||
const { ObservabilityPageTemplate, config } = usePluginContext();
|
||||
const { ObservabilityPageTemplate } = usePluginContext();
|
||||
|
||||
const { sloId } = useParams<{ sloId: string | undefined }>();
|
||||
|
||||
|
@ -42,10 +40,6 @@ export function SloEditPage() {
|
|||
|
||||
const { slo, isLoading } = useFetchSloDetails(sloId || '');
|
||||
|
||||
if (!isSloFeatureEnabled(config)) {
|
||||
return <PageNotFound />;
|
||||
}
|
||||
|
||||
if (hasRightLicense === false) {
|
||||
navigateToUrl(basePath.prepend(paths.observability.slos));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ComponentStory } from '@storybook/react';
|
||||
|
||||
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
|
||||
import { HeaderTitle as Component } from './header_title';
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
title: 'app/SLO/ListPage/HeaderTitle',
|
||||
decorators: [KibanaReactStorybookDecorator],
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof Component> = () => <Component />;
|
||||
|
||||
const defaultProps = {};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = defaultProps;
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
export function HeaderTitle() {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="m"
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{i18n.translate('xpack.observability.slosPageTitle', {
|
||||
defaultMessage: 'SLOs',
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBetaBadge
|
||||
label="Beta"
|
||||
tooltipPosition="bottom"
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.observability.slo.slosPage.headerTitle.betaBadgeDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'This functionality is in beta and is subject to change. The design and code is less mature than official generally available features and is being provided as-is with no warranties. Beta features are not subject to the support service level agreement of official generally available features.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConfigSchema } from '../../../plugin';
|
||||
|
||||
export function isSloFeatureEnabled(config: ConfigSchema): boolean {
|
||||
return config.unsafe.slo.enabled === true;
|
||||
}
|
|
@ -21,8 +21,6 @@ import { useFetchHistoricalSummary } from '../../hooks/slo/use_fetch_historical_
|
|||
import { useLicense } from '../../hooks/use_license';
|
||||
import { SlosPage } from '.';
|
||||
import { emptySloList, sloList } from '../../data/slo/slo';
|
||||
import type { ConfigSchema } from '../../plugin';
|
||||
import type { Subset } from '../../typings';
|
||||
import { historicalSummaryData } from '../../data/slo/historical_summary_data';
|
||||
import { useCapabilities } from '../../hooks/slo/use_capabilities';
|
||||
|
||||
|
@ -83,12 +81,6 @@ const mockKibana = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const config: Subset<ConfigSchema> = {
|
||||
unsafe: {
|
||||
slo: { enabled: true },
|
||||
},
|
||||
};
|
||||
|
||||
describe('SLOs Page', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -96,51 +88,68 @@ describe('SLOs Page', () => {
|
|||
useCapabilitiesMock.mockReturnValue({ hasWriteCapabilities: true, hasReadCapabilities: true });
|
||||
});
|
||||
|
||||
describe('when the feature flag is not enabled', () => {
|
||||
it('renders the not found page ', async () => {
|
||||
describe('when the incorrect license is found', () => {
|
||||
it('renders the welcome prompt with subscription buttons', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList: emptySloList });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />, { unsafe: { slo: { enabled: false } } });
|
||||
render(<SlosPage />);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('pageNotFound')).toBeTruthy();
|
||||
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
|
||||
expect(screen.queryByTestId('slosPageWelcomePromptSignupForCloudButton')).toBeTruthy();
|
||||
expect(screen.queryByTestId('slosPageWelcomePromptSignupForLicenseButton')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the feature flag is enabled', () => {
|
||||
describe('when the incorrect license is found', () => {
|
||||
it('renders the welcome prompt with subscription buttons', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList: emptySloList });
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => false });
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />, config);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
|
||||
expect(screen.queryByTestId('slosPageWelcomePromptSignupForCloudButton')).toBeTruthy();
|
||||
expect(screen.queryByTestId('slosPageWelcomePromptSignupForLicenseButton')).toBeTruthy();
|
||||
});
|
||||
describe('when the correct license is found', () => {
|
||||
beforeEach(() => {
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
});
|
||||
|
||||
describe('when the correct license is found', () => {
|
||||
beforeEach(() => {
|
||||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
it('renders the SLOs Welcome Prompt when the API has finished loading and there are no results', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList: emptySloList });
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />);
|
||||
});
|
||||
|
||||
it('renders the SLOs Welcome Prompt when the API has finished loading and there are no results', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList: emptySloList });
|
||||
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />, config);
|
||||
});
|
||||
it('should have a create new SLO button', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
|
||||
|
||||
expect(screen.queryByTestId('slosPageWelcomePrompt')).toBeTruthy();
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
sloHistoricalSummaryResponse: historicalSummaryData,
|
||||
});
|
||||
|
||||
it('should have a create new SLO button', async () => {
|
||||
await act(async () => {
|
||||
render(<SlosPage />);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Create new SLO')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have an Auto Refresh button', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
|
||||
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
sloHistoricalSummaryResponse: historicalSummaryData,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('autoRefreshButton')).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when API has returned results', () => {
|
||||
it('renders the SLO list with SLO items', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
|
||||
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
|
@ -149,13 +158,16 @@ describe('SLOs Page', () => {
|
|||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />, config);
|
||||
render(<SlosPage />);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Create new SLO')).toBeTruthy();
|
||||
expect(screen.queryByTestId('slosPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloList')).toBeTruthy();
|
||||
expect(screen.queryAllByTestId('sloItem')).toBeTruthy();
|
||||
expect(screen.queryAllByTestId('sloItem').length).toBe(sloList.results.length);
|
||||
});
|
||||
|
||||
it('should have an Auto Refresh button', async () => {
|
||||
it('allows editing an SLO', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
|
||||
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
|
@ -164,107 +176,72 @@ describe('SLOs Page', () => {
|
|||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />, config);
|
||||
render(<SlosPage />);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('autoRefreshButton')).toBeTruthy();
|
||||
screen.getAllByLabelText('Actions').at(0)?.click();
|
||||
|
||||
await waitForEuiPopoverOpen();
|
||||
|
||||
const button = screen.getByTestId('sloActionsEdit');
|
||||
|
||||
expect(button).toBeTruthy();
|
||||
|
||||
button.click();
|
||||
|
||||
expect(mockNavigate).toBeCalled();
|
||||
});
|
||||
|
||||
describe('when API has returned results', () => {
|
||||
it('renders the SLO list with SLO items', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
|
||||
it('allows deleting an SLO', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
|
||||
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
sloHistoricalSummaryResponse: historicalSummaryData,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />, config);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('slosPage')).toBeTruthy();
|
||||
expect(screen.queryByTestId('sloList')).toBeTruthy();
|
||||
expect(screen.queryAllByTestId('sloItem')).toBeTruthy();
|
||||
expect(screen.queryAllByTestId('sloItem').length).toBe(sloList.results.length);
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
sloHistoricalSummaryResponse: historicalSummaryData,
|
||||
});
|
||||
|
||||
it('allows editing an SLO', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
|
||||
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
sloHistoricalSummaryResponse: historicalSummaryData,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />, config);
|
||||
});
|
||||
|
||||
screen.getAllByLabelText('Actions').at(0)?.click();
|
||||
|
||||
await waitForEuiPopoverOpen();
|
||||
|
||||
const button = screen.getByTestId('sloActionsEdit');
|
||||
|
||||
expect(button).toBeTruthy();
|
||||
|
||||
button.click();
|
||||
|
||||
expect(mockNavigate).toBeCalled();
|
||||
await act(async () => {
|
||||
render(<SlosPage />);
|
||||
});
|
||||
|
||||
it('allows deleting an SLO', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
|
||||
screen.getAllByLabelText('Actions').at(0)?.click();
|
||||
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
sloHistoricalSummaryResponse: historicalSummaryData,
|
||||
});
|
||||
await waitForEuiPopoverOpen();
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />, config);
|
||||
});
|
||||
const button = screen.getByTestId('sloActionsDelete');
|
||||
|
||||
screen.getAllByLabelText('Actions').at(0)?.click();
|
||||
expect(button).toBeTruthy();
|
||||
|
||||
await waitForEuiPopoverOpen();
|
||||
button.click();
|
||||
|
||||
const button = screen.getByTestId('sloActionsDelete');
|
||||
screen.getByTestId('confirmModalConfirmButton').click();
|
||||
|
||||
expect(button).toBeTruthy();
|
||||
expect(mockDeleteSlo).toBeCalledWith({ id: sloList.results.at(0)?.id });
|
||||
});
|
||||
|
||||
button.click();
|
||||
it('allows cloning an SLO', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
|
||||
|
||||
screen.getByTestId('confirmModalConfirmButton').click();
|
||||
|
||||
expect(mockDeleteSlo).toBeCalledWith({ id: sloList.results.at(0)?.id });
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
sloHistoricalSummaryResponse: historicalSummaryData,
|
||||
});
|
||||
|
||||
it('allows cloning an SLO', async () => {
|
||||
useFetchSloListMock.mockReturnValue({ isLoading: false, sloList });
|
||||
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
sloHistoricalSummaryResponse: historicalSummaryData,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(<SlosPage />, config);
|
||||
});
|
||||
|
||||
screen.getAllByLabelText('Actions').at(0)?.click();
|
||||
|
||||
await waitForEuiPopoverOpen();
|
||||
|
||||
const button = screen.getByTestId('sloActionsClone');
|
||||
|
||||
expect(button).toBeTruthy();
|
||||
|
||||
button.click();
|
||||
|
||||
expect(mockCloneSlo).toBeCalled();
|
||||
await act(async () => {
|
||||
render(<SlosPage />);
|
||||
});
|
||||
|
||||
screen.getAllByLabelText('Actions').at(0)?.click();
|
||||
|
||||
await waitForEuiPopoverOpen();
|
||||
|
||||
const button = screen.getByTestId('sloActionsClone');
|
||||
|
||||
expect(button).toBeTruthy();
|
||||
|
||||
button.click();
|
||||
|
||||
expect(mockCloneSlo).toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,17 +18,16 @@ import { useFetchSloList } from '../../hooks/slo/use_fetch_slo_list';
|
|||
import { SloList } from './components/slo_list';
|
||||
import { SloListWelcomePrompt } from './components/slo_list_welcome_prompt';
|
||||
import { AutoRefreshButton } from './components/auto_refresh_button';
|
||||
import PageNotFound from '../404';
|
||||
import { paths } from '../../config';
|
||||
import { isSloFeatureEnabled } from './helpers/is_slo_feature_enabled';
|
||||
import type { ObservabilityAppServices } from '../../application/types';
|
||||
import { HeaderTitle } from './components/header_title';
|
||||
|
||||
export function SlosPage() {
|
||||
const {
|
||||
application: { navigateToUrl },
|
||||
http: { basePath },
|
||||
} = useKibana<ObservabilityAppServices>().services;
|
||||
const { ObservabilityPageTemplate, config } = usePluginContext();
|
||||
const { ObservabilityPageTemplate } = usePluginContext();
|
||||
const { hasWriteCapabilities } = useCapabilities();
|
||||
const { hasAtLeast } = useLicense();
|
||||
|
||||
|
@ -55,10 +54,6 @@ export function SlosPage() {
|
|||
setIsAutoRefreshing(!isAutoRefreshing);
|
||||
};
|
||||
|
||||
if (!isSloFeatureEnabled(config)) {
|
||||
return <PageNotFound />;
|
||||
}
|
||||
|
||||
if (isInitialLoading) {
|
||||
return null;
|
||||
}
|
||||
|
@ -70,9 +65,7 @@ export function SlosPage() {
|
|||
return (
|
||||
<ObservabilityPageTemplate
|
||||
pageHeader={{
|
||||
pageTitle: i18n.translate('xpack.observability.slosPageTitle', {
|
||||
defaultMessage: 'SLOs',
|
||||
}),
|
||||
pageTitle: <HeaderTitle />,
|
||||
rightSideItems: [
|
||||
<EuiButton
|
||||
color="primary"
|
||||
|
|
|
@ -62,9 +62,6 @@ import { registerObservabilityRuleTypes } from './rules/register_observability_r
|
|||
|
||||
export interface ConfigSchema {
|
||||
unsafe: {
|
||||
slo: {
|
||||
enabled: boolean;
|
||||
};
|
||||
alertDetails: {
|
||||
apm: {
|
||||
enabled: boolean;
|
||||
|
@ -291,11 +288,11 @@ export class Plugin
|
|||
// See https://github.com/elastic/kibana/issues/103325.
|
||||
const otherLinks: NavigationEntry[] = deepLinks
|
||||
.filter((link) => link.navLinkStatus === AppNavLinkStatus.visible)
|
||||
.filter((link) => (link.id === 'slos' ? config.unsafe.slo.enabled : link)) // might not be useful anymore
|
||||
.map((link) => ({
|
||||
app: observabilityAppId,
|
||||
label: link.title,
|
||||
path: link.path ?? '',
|
||||
isBetaFeature: link.id === 'slos' ? true : false,
|
||||
}));
|
||||
|
||||
const sections = [
|
||||
|
@ -325,12 +322,9 @@ export class Plugin
|
|||
const { application } = coreStart;
|
||||
const config = this.initContext.config.get();
|
||||
|
||||
const filterSlo = (link: AppDeepLink) =>
|
||||
link.id === 'slos' ? config.unsafe.slo.enabled : link;
|
||||
|
||||
updateGlobalNavigation({
|
||||
capabilities: application.capabilities,
|
||||
deepLinks: this.deepLinks.filter(filterSlo),
|
||||
deepLinks: this.deepLinks,
|
||||
updater$: this.appUpdater$,
|
||||
});
|
||||
|
||||
|
|
|
@ -17,32 +17,30 @@ export const registerObservabilityRuleTypes = (
|
|||
config: ConfigSchema,
|
||||
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry
|
||||
) => {
|
||||
if (config.unsafe.slo.enabled) {
|
||||
observabilityRuleTypeRegistry.register({
|
||||
id: SLO_BURN_RATE_RULE_ID,
|
||||
description: i18n.translate('xpack.observability.slo.rules.burnRate.description', {
|
||||
defaultMessage: 'Alert when your SLO burn rate is too high over a defined period of time.',
|
||||
}),
|
||||
format: ({ fields }) => {
|
||||
return {
|
||||
reason: fields[ALERT_REASON] ?? '-',
|
||||
link: '/app/observability/slos',
|
||||
};
|
||||
},
|
||||
iconClass: 'bell',
|
||||
documentationUrl(docLinks) {
|
||||
return '/unknown/docs';
|
||||
},
|
||||
ruleParamsExpression: lazy(() => import('../components/app/burn_rate_rule_editor')),
|
||||
validate: validateBurnRateRule,
|
||||
requiresAppContext: false,
|
||||
defaultActionMessage: i18n.translate(
|
||||
'xpack.observability.slo.rules.burnRate.defaultActionMessage',
|
||||
{
|
||||
defaultMessage: `\\{\\{rule.name\\}\\} is firing:
|
||||
observabilityRuleTypeRegistry.register({
|
||||
id: SLO_BURN_RATE_RULE_ID,
|
||||
description: i18n.translate('xpack.observability.slo.rules.burnRate.description', {
|
||||
defaultMessage: 'Alert when your SLO burn rate is too high over a defined period of time.',
|
||||
}),
|
||||
format: ({ fields }) => {
|
||||
return {
|
||||
reason: fields[ALERT_REASON] ?? '-',
|
||||
link: '/app/observability/slos',
|
||||
};
|
||||
},
|
||||
iconClass: 'bell',
|
||||
documentationUrl(docLinks) {
|
||||
return '/unknown/docs';
|
||||
},
|
||||
ruleParamsExpression: lazy(() => import('../components/app/burn_rate_rule_editor')),
|
||||
validate: validateBurnRateRule,
|
||||
requiresAppContext: false,
|
||||
defaultActionMessage: i18n.translate(
|
||||
'xpack.observability.slo.rules.burnRate.defaultActionMessage',
|
||||
{
|
||||
defaultMessage: `\\{\\{rule.name\\}\\} is firing:
|
||||
- Reason: \\{\\{context.reason\\}\\}`,
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -22,9 +22,6 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) {
|
|||
|
||||
const config: ConfigSchema = {
|
||||
unsafe: {
|
||||
slo: {
|
||||
enabled: false,
|
||||
},
|
||||
alertDetails: {
|
||||
apm: { enabled: false },
|
||||
logs: { enabled: false },
|
||||
|
|
|
@ -31,9 +31,6 @@ export const data = dataPluginMock.createStartContract();
|
|||
|
||||
const defaultConfig: ConfigSchema = {
|
||||
unsafe: {
|
||||
slo: {
|
||||
enabled: false,
|
||||
},
|
||||
alertDetails: {
|
||||
apm: { enabled: false },
|
||||
logs: { enabled: false },
|
||||
|
|
|
@ -29,9 +29,6 @@ const configSchema = schema.object({
|
|||
index: schema.string({ defaultValue: 'observability-annotations' }),
|
||||
}),
|
||||
unsafe: schema.object({
|
||||
slo: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
}),
|
||||
alertDetails: schema.object({
|
||||
apm: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
|
|
|
@ -167,76 +167,72 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
|
||||
const { ruleDataService } = plugins.ruleRegistry;
|
||||
|
||||
if (config.unsafe.slo.enabled) {
|
||||
plugins.features.registerKibanaFeature({
|
||||
id: sloFeatureId,
|
||||
name: i18n.translate('xpack.observability.featureRegistry.linkSloTitle', {
|
||||
defaultMessage: 'SLOs',
|
||||
}),
|
||||
order: 1200,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
app: [sloFeatureId, 'kibana'],
|
||||
catalogue: [sloFeatureId, 'observability'],
|
||||
alerting: [SLO_BURN_RATE_RULE_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
app: [sloFeatureId, 'kibana'],
|
||||
catalogue: [sloFeatureId, 'observability'],
|
||||
api: ['slo_write', 'slo_read', 'rac'],
|
||||
savedObject: {
|
||||
all: [SO_SLO_TYPE],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
all: [SLO_BURN_RATE_RULE_ID],
|
||||
},
|
||||
alert: {
|
||||
all: [SLO_BURN_RATE_RULE_ID],
|
||||
},
|
||||
},
|
||||
ui: ['read', 'write'],
|
||||
plugins.features.registerKibanaFeature({
|
||||
id: sloFeatureId,
|
||||
name: i18n.translate('xpack.observability.featureRegistry.linkSloTitle', {
|
||||
defaultMessage: 'SLOs',
|
||||
}),
|
||||
order: 1200,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
app: [sloFeatureId, 'kibana'],
|
||||
catalogue: [sloFeatureId, 'observability'],
|
||||
alerting: [SLO_BURN_RATE_RULE_ID],
|
||||
privileges: {
|
||||
all: {
|
||||
app: [sloFeatureId, 'kibana'],
|
||||
catalogue: [sloFeatureId, 'observability'],
|
||||
api: ['slo_write', 'slo_read', 'rac'],
|
||||
savedObject: {
|
||||
all: [SO_SLO_TYPE],
|
||||
read: [],
|
||||
},
|
||||
read: {
|
||||
app: [sloFeatureId, 'kibana'],
|
||||
catalogue: [sloFeatureId, 'observability'],
|
||||
api: ['slo_read', 'rac'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [SO_SLO_TYPE],
|
||||
alerting: {
|
||||
rule: {
|
||||
all: [SLO_BURN_RATE_RULE_ID],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
read: [SLO_BURN_RATE_RULE_ID],
|
||||
},
|
||||
alert: {
|
||||
read: [SLO_BURN_RATE_RULE_ID],
|
||||
},
|
||||
alert: {
|
||||
all: [SLO_BURN_RATE_RULE_ID],
|
||||
},
|
||||
ui: ['read'],
|
||||
},
|
||||
ui: ['read', 'write'],
|
||||
},
|
||||
});
|
||||
|
||||
core.savedObjects.registerType(slo);
|
||||
|
||||
const ruleDataClient = ruleDataService.initializeIndex({
|
||||
feature: sloFeatureId,
|
||||
registrationContext: SLO_RULE_REGISTRATION_CONTEXT,
|
||||
dataset: Dataset.alerts,
|
||||
componentTemplateRefs: [ECS_COMPONENT_TEMPLATE_NAME],
|
||||
componentTemplates: [
|
||||
{
|
||||
name: 'mappings',
|
||||
mappings: mappingFromFieldMap(legacyExperimentalFieldMap, 'strict'),
|
||||
read: {
|
||||
app: [sloFeatureId, 'kibana'],
|
||||
catalogue: [sloFeatureId, 'observability'],
|
||||
api: ['slo_read', 'rac'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [SO_SLO_TYPE],
|
||||
},
|
||||
],
|
||||
});
|
||||
alerting: {
|
||||
rule: {
|
||||
read: [SLO_BURN_RATE_RULE_ID],
|
||||
},
|
||||
alert: {
|
||||
read: [SLO_BURN_RATE_RULE_ID],
|
||||
},
|
||||
},
|
||||
ui: ['read'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registerRuleTypes(plugins.alerting, this.logger, ruleDataClient);
|
||||
core.savedObjects.registerType(slo);
|
||||
|
||||
registerSloUsageCollector(plugins.usageCollection);
|
||||
}
|
||||
const ruleDataClient = ruleDataService.initializeIndex({
|
||||
feature: sloFeatureId,
|
||||
registrationContext: SLO_RULE_REGISTRATION_CONTEXT,
|
||||
dataset: Dataset.alerts,
|
||||
componentTemplateRefs: [ECS_COMPONENT_TEMPLATE_NAME],
|
||||
componentTemplates: [
|
||||
{
|
||||
name: 'mappings',
|
||||
mappings: mappingFromFieldMap(legacyExperimentalFieldMap, 'strict'),
|
||||
},
|
||||
],
|
||||
});
|
||||
registerRuleTypes(plugins.alerting, this.logger, ruleDataClient);
|
||||
registerSloUsageCollector(plugins.usageCollection);
|
||||
|
||||
registerRoutes({
|
||||
core,
|
||||
|
@ -244,7 +240,7 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
ruleDataService,
|
||||
},
|
||||
logger: this.logger,
|
||||
repository: getObservabilityServerRouteRepository(config),
|
||||
repository: getObservabilityServerRouteRepository(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,16 +4,14 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { ObservabilityConfig } from '..';
|
||||
|
||||
import { rulesRouteRepository } from './rules/route';
|
||||
import { slosRouteRepository } from './slo/route';
|
||||
|
||||
export function getObservabilityServerRouteRepository(config: ObservabilityConfig) {
|
||||
const isSloFeatureEnabled = config.unsafe.slo.enabled;
|
||||
|
||||
export function getObservabilityServerRouteRepository() {
|
||||
const repository = {
|
||||
...rulesRouteRepository,
|
||||
...(isSloFeatureEnabled ? slosRouteRepository : {}),
|
||||
...slosRouteRepository,
|
||||
};
|
||||
return repository;
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ export default function createRegisteredRuleTypeTests({ getService }: FtrProvide
|
|||
'siem.thresholdRule',
|
||||
'siem.newTermsRule',
|
||||
'siem.notifications',
|
||||
'slo.rules.burnRate',
|
||||
'metrics.alert.anomaly',
|
||||
'logs.alert.document.count',
|
||||
'metrics.alert.inventory.threshold',
|
||||
|
|
|
@ -121,6 +121,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'rulesSettings',
|
||||
'uptime',
|
||||
'siem',
|
||||
'slo',
|
||||
'securitySolutionCases',
|
||||
'fleet',
|
||||
'fleetv2',
|
||||
|
|
|
@ -24,6 +24,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
generalCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
||||
observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
||||
slo: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
fleet: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
actions: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
|
|
|
@ -32,6 +32,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
generalCases: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
slo: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
canvas: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
logs: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
|
@ -94,6 +95,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
generalCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
||||
observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_delete'],
|
||||
slo: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
fleet: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
actions: ['all', 'read', 'minimal_all', 'minimal_read'],
|
||||
|
|
|
@ -97,6 +97,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'alerting:siem.queryRule',
|
||||
'alerting:siem.savedQueryRule',
|
||||
'alerting:siem.thresholdRule',
|
||||
'alerting:slo.rules.burnRate',
|
||||
'alerting:transform_health',
|
||||
'alerting:xpack.ml.anomaly_detection_alert',
|
||||
'alerting:xpack.ml.anomaly_detection_jobs_health',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue