[SLO] Implement federated views (#178050)

This commit is contained in:
Kevin Delemme 2024-04-16 09:15:51 -04:00 committed by GitHub
parent 73079879c8
commit 721d354a13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
206 changed files with 3813 additions and 1902 deletions

View file

@ -218,6 +218,7 @@ export const HASH_TO_VERSION_MAP = {
'siem-ui-timeline-note|28393dfdeb4e4413393eb5f7ec8c5436': '10.0.0',
'siem-ui-timeline-pinned-event|293fce142548281599060e07ad2c9ddb': '10.0.0',
'siem-ui-timeline|f6739fd4b17646a6c86321a746c247ef': '10.1.0',
'slo-settings|3d1b76c39bfb2cc8296b024d73854724': '10.0.0',
'slo|dc7f35c0cf07d71bb36f154996fe10c6': '10.1.0',
'space|c3aec2a5d4afcb75554fed96411170e1': '10.0.0',
'spaces-usage-stats|3d1b76c39bfb2cc8296b024d73854724': '10.0.0',

View file

@ -912,6 +912,7 @@
"tags",
"version"
],
"slo-settings": [],
"space": [
"name"
],

View file

@ -2996,6 +2996,10 @@
}
}
},
"slo-settings": {
"dynamic": false,
"properties": {}
},
"space": {
"dynamic": false,
"properties": {

View file

@ -8,9 +8,9 @@
import * as t from 'io-ts';
import { omitBy, isPlainObject, isEmpty } from 'lodash';
import { isLeft } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';
import Boom from '@hapi/boom';
import { strictKeysRt } from '@kbn/io-ts-utils';
import { formatErrors } from '@kbn/securitysolution-io-ts-utils';
import { RouteParamsRT } from './typings';
interface KibanaRequestParams {
@ -36,7 +36,7 @@ export function decodeRequestParams<T extends RouteParamsRT>(
const result = strictKeysRt(paramsRt).decode(paramMap);
if (isLeft(result)) {
throw Boom.badRequest(PathReporter.report(result)[0]);
throw Boom.badRequest(formatErrors(result.left).join('|'));
}
return result.right;

View file

@ -17,7 +17,8 @@
"@kbn/core-http-request-handler-context-server",
"@kbn/core-http-server",
"@kbn/core-lifecycle-server",
"@kbn/logging"
"@kbn/logging",
"@kbn/securitysolution-io-ts-utils"
],
"exclude": [
"target/**/*",

View file

@ -146,6 +146,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"siem-ui-timeline-note": "0a32fb776907f596bedca292b8c646496ae9c57b",
"siem-ui-timeline-pinned-event": "082daa3ce647b33873f6abccf340bdfa32057c8d",
"slo": "9a9995e4572de1839651c43b5fc4dc8276bb5815",
"slo-settings": "f6b5ed339470a6a2cda272bde1750adcf504a11b",
"space": "8de4ec513e9bbc6b2f1d635161d850be7747d38e",
"spaces-usage-stats": "3abca98713c52af8b30300e386c7779b3025a20e",
"synthetics-monitor": "5ceb25b6249bd26902c9b34273c71c3dce06dbea",

View file

@ -120,6 +120,7 @@ const previouslyRegisteredTypes = [
'siem-ui-timeline-note',
'siem-ui-timeline-pinned-event',
'slo',
'slo-settings',
'space',
'spaces-usage-stats',
'synthetics-monitor',

View file

@ -265,6 +265,7 @@ describe('split .kibana index into multiple system indices', () => {
"siem-ui-timeline-note",
"siem-ui-timeline-pinned-event",
"slo",
"slo-settings",
"space",
"spaces-usage-stats",
"synthetics-monitor",

View file

@ -7,5 +7,4 @@
export * from './src/schema';
export * from './src/rest_specs';
export * from './src/models/duration';
export * from './src/models/pagination';
export * from './src/models';

View file

@ -5,4 +5,5 @@
* 2.0.
*/
export function useErrorBudgetActions() {}
export * from './pagination';
export * from './duration';

View 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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import {
budgetingMethodSchema,
groupSummarySchema,
objectiveSchema,
timeWindowTypeSchema,
} from '../schema';
type BudgetingMethod = t.OutputOf<typeof budgetingMethodSchema>;
type TimeWindowType = t.OutputOf<typeof timeWindowTypeSchema>;
type GroupSummary = t.TypeOf<typeof groupSummarySchema>;
type Objective = t.OutputOf<typeof objectiveSchema>;
export type { BudgetingMethod, Objective, TimeWindowType, GroupSummary };

View file

@ -6,3 +6,6 @@
*/
export * from './slo';
export * from './routes';
export * from './indicators';
export * from './common';

View file

@ -0,0 +1,60 @@
/*
* 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 * as t from 'io-ts';
import {
apmTransactionDurationIndicatorSchema,
apmTransactionErrorRateIndicatorSchema,
histogramIndicatorSchema,
indicatorSchema,
indicatorTypesSchema,
kqlCustomIndicatorSchema,
kqlWithFiltersSchema,
metricCustomIndicatorSchema,
querySchema,
syntheticsAvailabilityIndicatorSchema,
timesliceMetricBasicMetricWithField,
timesliceMetricDocCountMetric,
timesliceMetricIndicatorSchema,
timesliceMetricPercentileMetric,
} from '../schema';
type IndicatorType = t.OutputOf<typeof indicatorTypesSchema>;
type Indicator = t.OutputOf<typeof indicatorSchema>;
type APMTransactionErrorRateIndicator = t.OutputOf<typeof apmTransactionErrorRateIndicatorSchema>;
type APMTransactionDurationIndicator = t.OutputOf<typeof apmTransactionDurationIndicatorSchema>;
type SyntheticsAvailabilityIndicator = t.OutputOf<typeof syntheticsAvailabilityIndicatorSchema>;
type MetricCustomIndicator = t.OutputOf<typeof metricCustomIndicatorSchema>;
type TimesliceMetricIndicator = t.OutputOf<typeof timesliceMetricIndicatorSchema>;
type TimesliceMetricBasicMetricWithField = t.OutputOf<typeof timesliceMetricBasicMetricWithField>;
type TimesliceMetricDocCountMetric = t.OutputOf<typeof timesliceMetricDocCountMetric>;
type TimesclieMetricPercentileMetric = t.OutputOf<typeof timesliceMetricPercentileMetric>;
type HistogramIndicator = t.OutputOf<typeof histogramIndicatorSchema>;
type KQLCustomIndicator = t.OutputOf<typeof kqlCustomIndicatorSchema>;
type KqlWithFiltersSchema = t.TypeOf<typeof kqlWithFiltersSchema>;
type QuerySchema = t.TypeOf<typeof querySchema>;
export type {
APMTransactionDurationIndicator,
APMTransactionErrorRateIndicator,
SyntheticsAvailabilityIndicator,
IndicatorType,
Indicator,
MetricCustomIndicator,
TimesliceMetricIndicator,
TimesliceMetricBasicMetricWithField,
TimesclieMetricPercentileMetric,
TimesliceMetricDocCountMetric,
HistogramIndicator,
KQLCustomIndicator,
KqlWithFiltersSchema,
QuerySchema,
};

View file

@ -0,0 +1,47 @@
/*
* 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 * as t from 'io-ts';
import { indicatorSchema, timeWindowSchema } from '../../schema';
import { allOrAnyStringOrArray } from '../../schema/common';
import {
budgetingMethodSchema,
objectiveSchema,
optionalSettingsSchema,
sloIdSchema,
tagsSchema,
} from '../../schema/slo';
const createSLOParamsSchema = t.type({
body: t.intersection([
t.type({
name: t.string,
description: t.string,
indicator: indicatorSchema,
timeWindow: timeWindowSchema,
budgetingMethod: budgetingMethodSchema,
objective: objectiveSchema,
}),
t.partial({
id: sloIdSchema,
settings: optionalSettingsSchema,
tags: tagsSchema,
groupBy: allOrAnyStringOrArray,
revision: t.number,
}),
]),
});
const createSLOResponseSchema = t.type({
id: sloIdSchema,
});
type CreateSLOInput = t.OutputOf<typeof createSLOParamsSchema.props.body>; // Raw payload sent by the frontend
type CreateSLOParams = t.TypeOf<typeof createSLOParamsSchema.props.body>; // Parsed payload used by the backend
type CreateSLOResponse = t.TypeOf<typeof createSLOResponseSchema>; // Raw response sent to the frontend
export { createSLOParamsSchema, createSLOResponseSchema };
export type { CreateSLOInput, CreateSLOParams, CreateSLOResponse };

View file

@ -4,10 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { sloIdSchema } from '../../schema/slo';
export * from './compute_burn_rate';
export * from './error_budget';
export * from './compute_sli';
export * from './compute_summary_status';
export * from './date_range';
export * from './validate_slo';
const deleteSLOParamsSchema = t.type({
path: t.type({
id: sloIdSchema,
}),
});
export { deleteSLOParamsSchema };

View 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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { sloIdSchema } from '../../schema/slo';
const deleteSLOInstancesParamsSchema = t.type({
body: t.type({ list: t.array(t.type({ sloId: sloIdSchema, instanceId: t.string })) }),
});
type DeleteSLOInstancesInput = t.OutputOf<typeof deleteSLOInstancesParamsSchema.props.body>;
type DeleteSLOInstancesParams = t.TypeOf<typeof deleteSLOInstancesParamsSchema.props.body>;
export { deleteSLOInstancesParamsSchema };
export type { DeleteSLOInstancesInput, DeleteSLOInstancesParams };

View file

@ -0,0 +1,68 @@
/*
* 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 * as t from 'io-ts';
import {
budgetingMethodSchema,
objectiveSchema,
sloIdSchema,
timeWindowSchema,
} from '../../schema';
import {
allOrAnyString,
allOrAnyStringOrArray,
dateType,
summarySchema,
} from '../../schema/common';
const fetchHistoricalSummaryParamsSchema = t.type({
body: t.type({
list: t.array(
t.intersection([
t.type({
sloId: sloIdSchema,
instanceId: t.string,
timeWindow: timeWindowSchema,
budgetingMethod: budgetingMethodSchema,
objective: objectiveSchema,
groupBy: allOrAnyStringOrArray,
revision: t.number,
}),
t.partial({ remoteName: t.string }),
])
),
}),
});
const historicalSummarySchema = t.intersection([
t.type({
date: dateType,
}),
summarySchema,
]);
const fetchHistoricalSummaryResponseSchema = t.array(
t.type({
sloId: sloIdSchema,
instanceId: allOrAnyString,
data: t.array(historicalSummarySchema),
})
);
type FetchHistoricalSummaryParams = t.TypeOf<typeof fetchHistoricalSummaryParamsSchema.props.body>;
type FetchHistoricalSummaryResponse = t.OutputOf<typeof fetchHistoricalSummaryResponseSchema>;
type HistoricalSummaryResponse = t.OutputOf<typeof historicalSummarySchema>;
export {
fetchHistoricalSummaryParamsSchema,
fetchHistoricalSummaryResponseSchema,
historicalSummarySchema,
};
export type {
FetchHistoricalSummaryParams,
FetchHistoricalSummaryResponse,
HistoricalSummaryResponse,
};

View file

@ -0,0 +1,40 @@
/*
* 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 * as t from 'io-ts';
import { sloWithDataResponseSchema } from '../slo';
const sortDirectionSchema = t.union([t.literal('asc'), t.literal('desc')]);
const sortBySchema = t.union([
t.literal('error_budget_consumed'),
t.literal('error_budget_remaining'),
t.literal('sli_value'),
t.literal('status'),
]);
const findSLOParamsSchema = t.partial({
query: t.partial({
filters: t.string,
kqlQuery: t.string,
page: t.string,
perPage: t.string,
sortBy: sortBySchema,
sortDirection: sortDirectionSchema,
}),
});
const findSLOResponseSchema = t.type({
page: t.number,
perPage: t.number,
total: t.number,
results: t.array(sloWithDataResponseSchema),
});
type FindSLOParams = t.TypeOf<typeof findSLOParamsSchema.props.query>;
type FindSLOResponse = t.OutputOf<typeof findSLOResponseSchema>;
export { findSLOParamsSchema, findSLOResponseSchema };
export type { FindSLOParams, FindSLOResponse };

View file

@ -0,0 +1,31 @@
/*
* 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 { toBooleanRt } from '@kbn/io-ts-utils/src/to_boolean_rt';
import * as t from 'io-ts';
import { sloDefinitionSchema } from '../../schema';
const findSloDefinitionsParamsSchema = t.partial({
query: t.partial({
search: t.string,
includeOutdatedOnly: toBooleanRt,
page: t.string,
perPage: t.string,
}),
});
const findSloDefinitionsResponseSchema = t.type({
page: t.number,
perPage: t.number,
total: t.number,
results: t.array(sloDefinitionSchema),
});
type FindSLODefinitionsParams = t.TypeOf<typeof findSloDefinitionsParamsSchema.props.query>;
type FindSLODefinitionsResponse = t.OutputOf<typeof findSloDefinitionsResponseSchema>;
export { findSloDefinitionsParamsSchema, findSloDefinitionsResponseSchema };
export type { FindSLODefinitionsParams, FindSLODefinitionsResponse };

View file

@ -0,0 +1,50 @@
/*
* 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 * as t from 'io-ts';
import { groupSummarySchema } from '../../schema/common';
const groupBySchema = t.union([
t.literal('ungrouped'),
t.literal('slo.tags'),
t.literal('status'),
t.literal('slo.indicator.type'),
t.literal('_index'),
]);
const findSLOGroupsParamsSchema = t.partial({
query: t.partial({
page: t.string,
perPage: t.string,
groupBy: groupBySchema,
groupsFilter: t.union([t.array(t.string), t.string]),
kqlQuery: t.string,
filters: t.string,
}),
});
const sloGroupWithSummaryResponseSchema = t.type({
group: t.string,
groupBy: t.string,
summary: groupSummarySchema,
});
const findSLOGroupsResponseSchema = t.type({
page: t.number,
perPage: t.number,
total: t.number,
results: t.array(sloGroupWithSummaryResponseSchema),
});
type FindSLOGroupsParams = t.TypeOf<typeof findSLOGroupsParamsSchema.props.query>;
type FindSLOGroupsResponse = t.OutputOf<typeof findSLOGroupsResponseSchema>;
export {
findSLOGroupsParamsSchema,
findSLOGroupsResponseSchema,
sloGroupWithSummaryResponseSchema,
};
export type { FindSLOGroupsParams, FindSLOGroupsResponse };

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { allOrAnyString } from '../../schema/common';
import { sloIdSchema } from '../../schema/slo';
import { sloWithDataResponseSchema } from '../slo';
const getSLOQuerySchema = t.partial({
query: t.partial({
instanceId: allOrAnyString,
remoteName: t.string,
}),
});
const getSLOParamsSchema = t.intersection([
t.type({
path: t.type({
id: sloIdSchema,
}),
}),
getSLOQuerySchema,
]);
const getSLOResponseSchema = sloWithDataResponseSchema;
type GetSLOParams = t.TypeOf<typeof getSLOQuerySchema.props.query>;
type GetSLOResponse = t.OutputOf<typeof getSLOResponseSchema>;
export { getSLOParamsSchema, getSLOResponseSchema };
export type { GetSLOParams, GetSLOResponse };

View file

@ -0,0 +1,40 @@
/*
* 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 * as t from 'io-ts';
import { durationType } from '../../schema';
import { allOrAnyString } from '../../schema/common';
const getSLOBurnRatesResponseSchema = t.type({
burnRates: t.array(
t.type({
name: t.string,
burnRate: t.number,
sli: t.number,
})
),
});
const getSLOBurnRatesParamsSchema = t.type({
path: t.type({ id: t.string }),
body: t.intersection([
t.type({
instanceId: allOrAnyString,
windows: t.array(
t.type({
name: t.string,
duration: durationType,
})
),
}),
t.partial({ remoteName: t.string }),
]),
});
type GetSLOBurnRatesResponse = t.OutputOf<typeof getSLOBurnRatesResponseSchema>;
export { getSLOBurnRatesParamsSchema, getSLOBurnRatesResponseSchema };
export type { GetSLOBurnRatesResponse };

View 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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
const getSLOInstancesParamsSchema = t.type({
path: t.type({ id: t.string }),
});
const getSLOInstancesResponseSchema = t.type({
groupBy: t.union([t.string, t.array(t.string)]),
instances: t.array(t.string),
});
type GetSLOInstancesResponse = t.OutputOf<typeof getSLOInstancesResponseSchema>;
export { getSLOInstancesParamsSchema, getSLOInstancesResponseSchema };
export type { GetSLOInstancesResponse };

View file

@ -0,0 +1,50 @@
/*
* 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 * as t from 'io-ts';
import { indicatorSchema, objectiveSchema } from '../../schema';
import { dateType } from '../../schema/common';
const getPreviewDataParamsSchema = t.type({
body: t.intersection([
t.type({
indicator: indicatorSchema,
range: t.type({
start: t.number,
end: t.number,
}),
}),
t.partial({
objective: objectiveSchema,
instanceId: t.string,
groupBy: t.string,
remoteName: t.string,
groupings: t.record(t.string, t.unknown),
}),
]),
});
const getPreviewDataResponseSchema = t.array(
t.intersection([
t.type({
date: dateType,
sliValue: t.number,
}),
t.partial({
events: t.type({
good: t.number,
bad: t.number,
total: t.number,
}),
}),
])
);
type GetPreviewDataParams = t.TypeOf<typeof getPreviewDataParamsSchema.props.body>;
type GetPreviewDataResponse = t.OutputOf<typeof getPreviewDataResponseSchema>;
export { getPreviewDataParamsSchema, getPreviewDataResponseSchema };
export type { GetPreviewDataParams, GetPreviewDataResponse };

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './create';
export * from './update';
export * from './delete';
export * from './find';
export * from './find_group';
export * from './find_definition';
export * from './get';
export * from './get_burn_rates';
export * from './get_instances';
export * from './get_preview_data';
export * from './reset';
export * from './manage';
export * from './delete_instance';
export * from './fetch_historical_summary';
export * from './put_settings';

View 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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { sloIdSchema } from '../../schema/slo';
const manageSLOParamsSchema = t.type({
path: t.type({ id: sloIdSchema }),
});
type ManageSLOParams = t.TypeOf<typeof manageSLOParamsSchema.props.path>;
export { manageSLOParamsSchema };
export type { ManageSLOParams };

View 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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { sloSettingsSchema } from '../../schema/settings';
const putSLOSettingsParamsSchema = t.type({
body: sloSettingsSchema,
});
const putSLOSettingsResponseSchema = sloSettingsSchema;
type PutSLOSettingsParams = t.TypeOf<typeof putSLOSettingsParamsSchema.props.body>;
type PutSLOSettingsResponse = t.OutputOf<typeof putSLOSettingsResponseSchema>;
type GetSLOSettingsResponse = t.OutputOf<typeof sloSettingsSchema>;
export { putSLOSettingsParamsSchema, putSLOSettingsResponseSchema };
export type { PutSLOSettingsParams, PutSLOSettingsResponse, GetSLOSettingsResponse };

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { sloDefinitionSchema, sloIdSchema } from '../../schema/slo';
const resetSLOParamsSchema = t.type({
path: t.type({ id: sloIdSchema }),
});
const resetSLOResponseSchema = sloDefinitionSchema;
type ResetSLOParams = t.TypeOf<typeof resetSLOParamsSchema.props.path>;
type ResetSLOResponse = t.OutputOf<typeof resetSLOResponseSchema>;
export { resetSLOParamsSchema, resetSLOResponseSchema };
export type { ResetSLOParams, ResetSLOResponse };

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { indicatorSchema, timeWindowSchema } from '../../schema';
import { allOrAnyStringOrArray } from '../../schema/common';
import {
budgetingMethodSchema,
objectiveSchema,
optionalSettingsSchema,
sloDefinitionSchema,
sloIdSchema,
tagsSchema,
} from '../../schema/slo';
const updateSLOParamsSchema = t.type({
path: t.type({
id: sloIdSchema,
}),
body: t.partial({
name: t.string,
description: t.string,
indicator: indicatorSchema,
timeWindow: timeWindowSchema,
budgetingMethod: budgetingMethodSchema,
objective: objectiveSchema,
settings: optionalSettingsSchema,
tags: tagsSchema,
groupBy: allOrAnyStringOrArray,
}),
});
const updateSLOResponseSchema = sloDefinitionSchema;
type UpdateSLOInput = t.OutputOf<typeof updateSLOParamsSchema.props.body>;
type UpdateSLOParams = t.TypeOf<typeof updateSLOParamsSchema.props.body>;
type UpdateSLOResponse = t.OutputOf<typeof updateSLOResponseSchema>;
export { updateSLOParamsSchema, updateSLOResponseSchema };
export type { UpdateSLOInput, UpdateSLOParams, UpdateSLOResponse };

View file

@ -6,428 +6,27 @@
*/
import * as t from 'io-ts';
import { toBooleanRt } from '@kbn/io-ts-utils';
import {
allOrAnyString,
apmTransactionDurationIndicatorSchema,
apmTransactionErrorRateIndicatorSchema,
syntheticsAvailabilityIndicatorSchema,
budgetingMethodSchema,
dateType,
durationType,
groupingsSchema,
histogramIndicatorSchema,
historicalSummarySchema,
indicatorSchema,
indicatorTypesSchema,
kqlCustomIndicatorSchema,
metricCustomIndicatorSchema,
metaSchema,
timesliceMetricIndicatorSchema,
objectiveSchema,
optionalSettingsSchema,
previewDataSchema,
settingsSchema,
sloIdSchema,
remoteSchema,
sloDefinitionSchema,
summarySchema,
groupSummarySchema,
tagsSchema,
timeWindowSchema,
timeWindowTypeSchema,
timesliceMetricBasicMetricWithField,
timesliceMetricDocCountMetric,
timesliceMetricPercentileMetric,
allOrAnyStringOrArray,
kqlWithFiltersSchema,
querySchema,
} from '../schema';
const createSLOParamsSchema = t.type({
body: t.intersection([
t.type({
name: t.string,
description: t.string,
indicator: indicatorSchema,
timeWindow: timeWindowSchema,
budgetingMethod: budgetingMethodSchema,
objective: objectiveSchema,
}),
t.partial({
id: sloIdSchema,
settings: optionalSettingsSchema,
tags: tagsSchema,
groupBy: allOrAnyStringOrArray,
revision: t.number,
}),
]),
});
const createSLOResponseSchema = t.type({
id: sloIdSchema,
});
const getPreviewDataParamsSchema = t.type({
body: t.intersection([
t.type({
indicator: indicatorSchema,
range: t.type({
start: t.number,
end: t.number,
}),
}),
t.partial({
objective: objectiveSchema,
instanceId: t.string,
groupBy: t.string,
groupings: t.record(t.string, t.unknown),
}),
]),
});
const getPreviewDataResponseSchema = t.array(previewDataSchema);
const deleteSLOParamsSchema = t.type({
path: t.type({
id: sloIdSchema,
}),
});
const sortDirectionSchema = t.union([t.literal('asc'), t.literal('desc')]);
const sortBySchema = t.union([
t.literal('error_budget_consumed'),
t.literal('error_budget_remaining'),
t.literal('sli_value'),
t.literal('status'),
]);
const findSLOParamsSchema = t.partial({
query: t.partial({
filters: t.string,
kqlQuery: t.string,
page: t.string,
perPage: t.string,
sortBy: sortBySchema,
sortDirection: sortDirectionSchema,
}),
});
const groupBySchema = t.union([
t.literal('ungrouped'),
t.literal('slo.tags'),
t.literal('status'),
t.literal('slo.indicator.type'),
]);
const findSLOGroupsParamsSchema = t.partial({
query: t.partial({
page: t.string,
perPage: t.string,
groupBy: groupBySchema,
groupsFilter: t.union([t.array(t.string), t.string]),
kqlQuery: t.string,
filters: t.string,
}),
});
const sloResponseSchema = t.intersection([
t.type({
id: sloIdSchema,
name: t.string,
description: t.string,
indicator: indicatorSchema,
timeWindow: timeWindowSchema,
budgetingMethod: budgetingMethodSchema,
objective: objectiveSchema,
revision: t.number,
settings: settingsSchema,
enabled: t.boolean,
tags: tagsSchema,
groupBy: allOrAnyStringOrArray,
createdAt: dateType,
updatedAt: dateType,
version: t.number,
}),
const sloWithDataResponseSchema = t.intersection([
sloDefinitionSchema,
t.type({ summary: summarySchema, groupings: groupingsSchema }),
t.partial({
instanceId: allOrAnyString,
meta: metaSchema,
remote: remoteSchema,
}),
]);
const sloWithSummaryResponseSchema = t.intersection([
sloResponseSchema,
t.intersection([
t.type({ summary: summarySchema, groupings: groupingsSchema }),
t.partial({ meta: metaSchema }),
]),
]);
type SLODefinitionResponse = t.OutputOf<typeof sloDefinitionSchema>;
type SLOWithSummaryResponse = t.OutputOf<typeof sloWithDataResponseSchema>;
const sloGroupWithSummaryResponseSchema = t.type({
group: t.string,
groupBy: t.string,
summary: groupSummarySchema,
});
const getSLOQuerySchema = t.partial({
query: t.partial({
instanceId: allOrAnyString,
}),
});
const getSLOParamsSchema = t.intersection([
t.type({
path: t.type({
id: sloIdSchema,
}),
}),
getSLOQuerySchema,
]);
const getSLOResponseSchema = sloWithSummaryResponseSchema;
const updateSLOParamsSchema = t.type({
path: t.type({
id: sloIdSchema,
}),
body: t.partial({
name: t.string,
description: t.string,
indicator: indicatorSchema,
timeWindow: timeWindowSchema,
budgetingMethod: budgetingMethodSchema,
objective: objectiveSchema,
settings: optionalSettingsSchema,
tags: tagsSchema,
groupBy: allOrAnyStringOrArray,
}),
});
const manageSLOParamsSchema = t.type({
path: t.type({ id: sloIdSchema }),
});
const resetSLOParamsSchema = t.type({
path: t.type({ id: sloIdSchema }),
});
const resetSLOResponseSchema = sloResponseSchema;
const updateSLOResponseSchema = sloResponseSchema;
const findSLOResponseSchema = t.type({
page: t.number,
perPage: t.number,
total: t.number,
results: t.array(sloWithSummaryResponseSchema),
});
const findSLOGroupsResponseSchema = t.type({
page: t.number,
perPage: t.number,
total: t.number,
results: t.array(sloGroupWithSummaryResponseSchema),
});
const deleteSLOInstancesParamsSchema = t.type({
body: t.type({ list: t.array(t.type({ sloId: sloIdSchema, instanceId: t.string })) }),
});
const fetchHistoricalSummaryParamsSchema = t.type({
body: t.type({
list: t.array(t.type({ sloId: sloIdSchema, instanceId: t.string })),
}),
});
const fetchHistoricalSummaryResponseSchema = t.array(
t.type({
sloId: sloIdSchema,
instanceId: allOrAnyString,
data: t.array(historicalSummarySchema),
})
);
const findSloDefinitionsParamsSchema = t.partial({
query: t.partial({
search: t.string,
includeOutdatedOnly: toBooleanRt,
page: t.string,
perPage: t.string,
}),
});
const findSloDefinitionsResponseSchema = t.type({
page: t.number,
perPage: t.number,
total: t.number,
results: t.array(sloResponseSchema),
});
const getSLOBurnRatesResponseSchema = t.type({
burnRates: t.array(
t.type({
name: t.string,
burnRate: t.number,
sli: t.number,
})
),
});
const getSLOBurnRatesParamsSchema = t.type({
path: t.type({ id: t.string }),
body: t.type({
instanceId: allOrAnyString,
windows: t.array(
t.type({
name: t.string,
duration: durationType,
})
),
}),
});
const getSLOInstancesParamsSchema = t.type({
path: t.type({ id: t.string }),
});
const getSLOInstancesResponseSchema = t.type({
groupBy: t.union([t.string, t.array(t.string)]),
instances: t.array(t.string),
});
type SLOResponse = t.OutputOf<typeof sloResponseSchema>;
type SLOWithSummaryResponse = t.OutputOf<typeof sloWithSummaryResponseSchema>;
type SLOGroupWithSummaryResponse = t.OutputOf<typeof sloGroupWithSummaryResponseSchema>;
type CreateSLOInput = t.OutputOf<typeof createSLOParamsSchema.props.body>; // Raw payload sent by the frontend
type CreateSLOParams = t.TypeOf<typeof createSLOParamsSchema.props.body>; // Parsed payload used by the backend
type CreateSLOResponse = t.TypeOf<typeof createSLOResponseSchema>; // Raw response sent to the frontend
type GetSLOParams = t.TypeOf<typeof getSLOQuerySchema.props.query>;
type GetSLOResponse = t.OutputOf<typeof getSLOResponseSchema>;
type ManageSLOParams = t.TypeOf<typeof manageSLOParamsSchema.props.path>;
type ResetSLOParams = t.TypeOf<typeof resetSLOParamsSchema.props.path>;
type ResetSLOResponse = t.OutputOf<typeof resetSLOResponseSchema>;
type UpdateSLOInput = t.OutputOf<typeof updateSLOParamsSchema.props.body>;
type UpdateSLOParams = t.TypeOf<typeof updateSLOParamsSchema.props.body>;
type UpdateSLOResponse = t.OutputOf<typeof updateSLOResponseSchema>;
type FindSLOParams = t.TypeOf<typeof findSLOParamsSchema.props.query>;
type FindSLOResponse = t.OutputOf<typeof findSLOResponseSchema>;
type FindSLOGroupsParams = t.TypeOf<typeof findSLOGroupsParamsSchema.props.query>;
type FindSLOGroupsResponse = t.OutputOf<typeof findSLOGroupsResponseSchema>;
type DeleteSLOInstancesInput = t.OutputOf<typeof deleteSLOInstancesParamsSchema.props.body>;
type DeleteSLOInstancesParams = t.TypeOf<typeof deleteSLOInstancesParamsSchema.props.body>;
type FetchHistoricalSummaryParams = t.TypeOf<typeof fetchHistoricalSummaryParamsSchema.props.body>;
type FetchHistoricalSummaryResponse = t.OutputOf<typeof fetchHistoricalSummaryResponseSchema>;
type HistoricalSummaryResponse = t.OutputOf<typeof historicalSummarySchema>;
type FindSLODefinitionsParams = t.TypeOf<typeof findSloDefinitionsParamsSchema.props.query>;
type FindSLODefinitionsResponse = t.OutputOf<typeof findSloDefinitionsResponseSchema>;
type GetPreviewDataParams = t.TypeOf<typeof getPreviewDataParamsSchema.props.body>;
type GetPreviewDataResponse = t.OutputOf<typeof getPreviewDataResponseSchema>;
type GetSLOInstancesResponse = t.OutputOf<typeof getSLOInstancesResponseSchema>;
type GetSLOBurnRatesResponse = t.OutputOf<typeof getSLOBurnRatesResponseSchema>;
type BudgetingMethod = t.OutputOf<typeof budgetingMethodSchema>;
type TimeWindow = t.OutputOf<typeof timeWindowTypeSchema>;
type IndicatorType = t.OutputOf<typeof indicatorTypesSchema>;
type Indicator = t.OutputOf<typeof indicatorSchema>;
type Objective = t.OutputOf<typeof objectiveSchema>;
type APMTransactionErrorRateIndicator = t.OutputOf<typeof apmTransactionErrorRateIndicatorSchema>;
type APMTransactionDurationIndicator = t.OutputOf<typeof apmTransactionDurationIndicatorSchema>;
type SyntheticsAvailabilityIndicator = t.OutputOf<typeof syntheticsAvailabilityIndicatorSchema>;
type MetricCustomIndicator = t.OutputOf<typeof metricCustomIndicatorSchema>;
type TimesliceMetricIndicator = t.OutputOf<typeof timesliceMetricIndicatorSchema>;
type TimesliceMetricBasicMetricWithField = t.OutputOf<typeof timesliceMetricBasicMetricWithField>;
type TimesliceMetricDocCountMetric = t.OutputOf<typeof timesliceMetricDocCountMetric>;
type TimesclieMetricPercentileMetric = t.OutputOf<typeof timesliceMetricPercentileMetric>;
type HistogramIndicator = t.OutputOf<typeof histogramIndicatorSchema>;
type KQLCustomIndicator = t.OutputOf<typeof kqlCustomIndicatorSchema>;
type GroupSummary = t.TypeOf<typeof groupSummarySchema>;
type KqlWithFiltersSchema = t.TypeOf<typeof kqlWithFiltersSchema>;
type QuerySchema = t.TypeOf<typeof querySchema>;
export {
createSLOParamsSchema,
deleteSLOParamsSchema,
deleteSLOInstancesParamsSchema,
findSLOParamsSchema,
findSLOResponseSchema,
findSLOGroupsParamsSchema,
findSLOGroupsResponseSchema,
getPreviewDataParamsSchema,
getPreviewDataResponseSchema,
getSLOParamsSchema,
getSLOResponseSchema,
fetchHistoricalSummaryParamsSchema,
fetchHistoricalSummaryResponseSchema,
findSloDefinitionsParamsSchema,
findSloDefinitionsResponseSchema,
manageSLOParamsSchema,
resetSLOParamsSchema,
resetSLOResponseSchema,
sloResponseSchema,
sloWithSummaryResponseSchema,
sloGroupWithSummaryResponseSchema,
updateSLOParamsSchema,
updateSLOResponseSchema,
getSLOBurnRatesParamsSchema,
getSLOBurnRatesResponseSchema,
getSLOInstancesParamsSchema,
getSLOInstancesResponseSchema,
};
export type {
BudgetingMethod,
CreateSLOInput,
CreateSLOParams,
CreateSLOResponse,
DeleteSLOInstancesInput,
DeleteSLOInstancesParams,
FindSLOParams,
FindSLOResponse,
FindSLOGroupsParams,
FindSLOGroupsResponse,
GetPreviewDataParams,
GetPreviewDataResponse,
GetSLOParams,
GetSLOResponse,
FetchHistoricalSummaryParams,
FetchHistoricalSummaryResponse,
HistoricalSummaryResponse,
FindSLODefinitionsParams,
FindSLODefinitionsResponse,
ManageSLOParams,
ResetSLOParams,
ResetSLOResponse,
SLOResponse,
SLOWithSummaryResponse,
SLOGroupWithSummaryResponse,
UpdateSLOInput,
UpdateSLOParams,
UpdateSLOResponse,
APMTransactionDurationIndicator,
APMTransactionErrorRateIndicator,
SyntheticsAvailabilityIndicator,
GetSLOBurnRatesResponse,
GetSLOInstancesResponse,
IndicatorType,
Indicator,
Objective,
MetricCustomIndicator,
TimesliceMetricIndicator,
TimesliceMetricBasicMetricWithField,
TimesclieMetricPercentileMetric,
TimesliceMetricDocCountMetric,
HistogramIndicator,
KQLCustomIndicator,
TimeWindow,
GroupSummary,
KqlWithFiltersSchema,
QuerySchema,
};
export { sloWithDataResponseSchema };
export type { SLODefinitionResponse, SLOWithSummaryResponse };

View file

@ -59,6 +59,11 @@ const metaSchema = t.partial({
}),
});
const remoteSchema = t.type({
remoteName: t.string,
kibanaUrl: t.string,
});
const groupSummarySchema = t.type({
total: t.number,
worst: t.type({
@ -76,58 +81,8 @@ const groupSummarySchema = t.type({
noData: t.number,
});
const historicalSummarySchema = t.intersection([
t.type({
date: dateType,
}),
summarySchema,
]);
const previewDataSchema = t.intersection([
t.type({
date: dateType,
sliValue: t.number,
}),
t.partial({
events: t.type({
good: t.number,
bad: t.number,
total: t.number,
}),
}),
]);
const dateRangeSchema = t.type({ from: dateType, to: dateType });
const kqlQuerySchema = t.string;
const kqlWithFiltersSchema = t.type({
kqlQuery: t.string,
filters: t.array(
t.type({
meta: t.partial({
alias: t.union([t.string, t.null]),
disabled: t.boolean,
negate: t.boolean,
// controlledBy is there to identify who owns the filter
controlledBy: t.string,
// allows grouping of filters
group: t.string,
// index and type are optional only because when you create a new filter, there are no defaults
index: t.string,
isMultiIndex: t.boolean,
type: t.string,
key: t.string,
params: t.any,
value: t.string,
}),
query: t.record(t.string, t.any),
})
),
});
const querySchema = t.union([kqlQuerySchema, kqlWithFiltersSchema]);
export {
ALL_VALUE,
allOrAnyString,
@ -136,13 +91,9 @@ export {
dateType,
errorBudgetSchema,
groupingsSchema,
historicalSummarySchema,
previewDataSchema,
statusSchema,
summarySchema,
metaSchema,
groupSummarySchema,
kqlWithFiltersSchema,
querySchema,
kqlQuerySchema,
remoteSchema,
};

View file

@ -10,3 +10,4 @@ export * from './duration';
export * from './indicators';
export * from './time_window';
export * from './slo';
export * from './settings';

View file

@ -6,7 +6,36 @@
*/
import * as t from 'io-ts';
import { allOrAnyString, dateRangeSchema, querySchema } from './common';
import { allOrAnyString, dateRangeSchema } from './common';
const kqlQuerySchema = t.string;
const kqlWithFiltersSchema = t.type({
kqlQuery: t.string,
filters: t.array(
t.type({
meta: t.partial({
alias: t.union([t.string, t.null]),
disabled: t.boolean,
negate: t.boolean,
// controlledBy is there to identify who owns the filter
controlledBy: t.string,
// allows grouping of filters
group: t.string,
// index and type are optional only because when you create a new filter, there are no defaults
index: t.string,
isMultiIndex: t.boolean,
type: t.string,
key: t.string,
params: t.any,
value: t.string,
}),
query: t.record(t.string, t.any),
})
),
});
const querySchema = t.union([kqlQuerySchema, kqlWithFiltersSchema]);
const apmTransactionDurationIndicatorTypeSchema = t.literal('sli.apm.transactionDuration');
const apmTransactionDurationIndicatorSchema = t.type({
@ -288,6 +317,9 @@ const indicatorSchema = t.union([
]);
export {
kqlQuerySchema,
kqlWithFiltersSchema,
querySchema,
apmTransactionDurationIndicatorSchema,
apmTransactionDurationIndicatorTypeSchema,
apmTransactionErrorRateIndicatorSchema,

View file

@ -6,8 +6,8 @@
*/
import * as t from 'io-ts';
import { errorBudgetSchema } from '@kbn/slo-schema';
type ErrorBudget = t.TypeOf<typeof errorBudgetSchema>;
export type { ErrorBudget };
export const sloSettingsSchema = t.type({
useAllRemoteClusters: t.boolean,
selectedRemoteClusters: t.array(t.string),
});

View file

@ -6,7 +6,7 @@
*/
import * as t from 'io-ts';
import { allOrAnyStringOrArray, dateType, summarySchema, groupingsSchema } from './common';
import { allOrAnyStringOrArray, dateType } from './common';
import { durationType } from './duration';
import { indicatorSchema } from './indicators';
import { timeWindowSchema } from './time_window';
@ -31,11 +31,13 @@ const settingsSchema = t.type({
frequency: durationType,
});
const groupBySchema = allOrAnyStringOrArray;
const optionalSettingsSchema = t.partial({ ...settingsSchema.props });
const tagsSchema = t.array(t.string);
const sloIdSchema = t.string;
const sloSchema = t.type({
const sloDefinitionSchema = t.type({
id: sloIdSchema,
name: t.string,
description: t.string,
@ -49,24 +51,19 @@ const sloSchema = t.type({
tags: tagsSchema,
createdAt: dateType,
updatedAt: dateType,
groupBy: allOrAnyStringOrArray,
groupBy: groupBySchema,
version: t.number,
});
const sloWithSummarySchema = t.intersection([
sloSchema,
t.type({ summary: summarySchema, groupings: groupingsSchema }),
]);
export {
budgetingMethodSchema,
objectiveSchema,
groupBySchema,
occurrencesBudgetingMethodSchema,
optionalSettingsSchema,
settingsSchema,
sloDefinitionSchema,
sloIdSchema,
sloSchema,
sloWithSummarySchema,
tagsSchema,
targetSchema,
timeslicesBudgetingMethodSchema,

View file

@ -270,8 +270,7 @@ describe('createApi', () => {
expect(response.custom).toHaveBeenCalledWith({
body: {
attributes: { _inspect: [], data: null },
message:
'Invalid value 1 supplied to : Partial<{| query: Partial<{| _inspect: pipe(JSON, boolean) |}> |}>/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)',
message: 'Invalid value "1" supplied to "query,_inspect"',
},
statusCode: 400,
});

View file

@ -16,6 +16,7 @@ export const RULES_PATH = '/alerts/rules' as const;
export const RULES_LOGS_PATH = '/alerts/rules/logs' as const;
export const RULE_DETAIL_PATH = '/alerts/rules/:ruleId' as const;
export const CASES_PATH = '/cases' as const;
export const SETTINGS_PATH = '/slos/settings' as const;
// // SLOs have been moved to its own app (slo). Keeping around for redirecting purposes.
export const OLD_SLOS_PATH = '/slos' as const;

View file

@ -1,27 +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 * as t from 'io-ts';
import {
dateRangeSchema,
historicalSummarySchema,
statusSchema,
summarySchema,
groupingsSchema,
groupSummarySchema,
metaSchema,
} from '@kbn/slo-schema';
type Status = t.TypeOf<typeof statusSchema>;
type DateRange = t.TypeOf<typeof dateRangeSchema>;
type HistoricalSummary = t.TypeOf<typeof historicalSummarySchema>;
type Summary = t.TypeOf<typeof summarySchema>;
type Groupings = t.TypeOf<typeof groupingsSchema>;
type Meta = t.TypeOf<typeof metaSchema>;
type GroupSummary = t.TypeOf<typeof groupSummarySchema>;
export type { DateRange, Groupings, GroupSummary, HistoricalSummary, Meta, Status, Summary };

View file

@ -1,13 +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.
*/
export * from './common';
export { Duration, DurationUnit, toDurationUnit, toMomentUnitOfTime } from '@kbn/slo-schema';
export * from './error_budget';
export * from './indicators';
export * from './slo';
export * from './time_window';

View file

@ -1,38 +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 * as t from 'io-ts';
import {
apmTransactionDurationIndicatorSchema,
apmTransactionErrorRateIndicatorSchema,
syntheticsAvailabilityIndicatorSchema,
indicatorDataSchema,
indicatorSchema,
indicatorTypesSchema,
kqlCustomIndicatorSchema,
metricCustomIndicatorSchema,
} from '@kbn/slo-schema';
type APMTransactionErrorRateIndicator = t.TypeOf<typeof apmTransactionErrorRateIndicatorSchema>;
type APMTransactionDurationIndicator = t.TypeOf<typeof apmTransactionDurationIndicatorSchema>;
type SyntheticsAvailabilityIndicator = t.TypeOf<typeof syntheticsAvailabilityIndicatorSchema>;
type KQLCustomIndicator = t.TypeOf<typeof kqlCustomIndicatorSchema>;
type MetricCustomIndicator = t.TypeOf<typeof metricCustomIndicatorSchema>;
type Indicator = t.TypeOf<typeof indicatorSchema>;
type IndicatorTypes = t.TypeOf<typeof indicatorTypesSchema>;
type IndicatorData = t.TypeOf<typeof indicatorDataSchema>;
export type {
Indicator,
IndicatorTypes,
APMTransactionErrorRateIndicator,
APMTransactionDurationIndicator,
SyntheticsAvailabilityIndicator,
KQLCustomIndicator,
MetricCustomIndicator,
IndicatorData,
};

View file

@ -1,16 +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 * as t from 'io-ts';
import { sloIdSchema, sloSchema, sloWithSummarySchema } from '@kbn/slo-schema';
type SLO = t.TypeOf<typeof sloSchema>;
type SLOId = t.TypeOf<typeof sloIdSchema>;
type SLOWithSummary = t.TypeOf<typeof sloWithSummarySchema>;
type StoredSLO = t.OutputOf<typeof sloSchema>;
export type { SLO, SLOWithSummary, SLOId, StoredSLO };

View file

@ -1,46 +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 {
calendarAlignedTimeWindowSchema,
rollingTimeWindowSchema,
timeWindowSchema,
} from '@kbn/slo-schema';
import moment from 'moment';
import * as t from 'io-ts';
type TimeWindow = t.TypeOf<typeof timeWindowSchema>;
type RollingTimeWindow = t.TypeOf<typeof rollingTimeWindowSchema>;
type CalendarAlignedTimeWindow = t.TypeOf<typeof calendarAlignedTimeWindowSchema>;
export type { RollingTimeWindow, TimeWindow, CalendarAlignedTimeWindow };
export function toCalendarAlignedTimeWindowMomentUnit(
timeWindow: CalendarAlignedTimeWindow
): moment.unitOfTime.StartOf {
const unit = timeWindow.duration.unit;
switch (unit) {
case 'w':
return 'isoWeeks';
case 'M':
return 'months';
default:
throw new Error(`Invalid calendar aligned time window duration unit: ${unit}`);
}
}
export function toRollingTimeWindowMomentUnit(
timeWindow: RollingTimeWindow
): moment.unitOfTime.Diff {
const unit = timeWindow.duration.unit;
switch (unit) {
case 'd':
return 'days';
default:
throw new Error(`Invalid rolling time window duration unit: ${unit}`);
}
}

View file

@ -1,24 +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 { toHighPrecision } from '../../utils/number';
import { IndicatorData, SLO } from '../models';
/**
* A Burn Rate is computed with the Indicator Data retrieved from a specific lookback period
* It tells how fast we are consumming our error budget during a specific period
*/
export function computeBurnRate(slo: SLO, sliData: IndicatorData): number {
const { good, total } = sliData;
if (total === 0 || good >= total) {
return 0;
}
const errorBudget = 1 - slo.objective.target;
const errorRate = 1 - good / total;
return toHighPrecision(errorRate / errorBudget);
}

View file

@ -1,26 +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 { computeSLI } from './compute_sli';
describe('computeSLI', () => {
it('returns -1 when no total events', () => {
expect(computeSLI(100, 0)).toEqual(-1);
});
it('returns the sli value', () => {
expect(computeSLI(100, 1000)).toEqual(0.1);
});
it('returns when good is greater than total events', () => {
expect(computeSLI(9999, 9)).toEqual(1111);
});
it('returns rounds the value to 6 digits', () => {
expect(computeSLI(33, 90)).toEqual(0.366667);
});
});

View file

@ -1,18 +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 { toHighPrecision } from '../../utils/number';
const NO_DATA = -1;
export function computeSLI(good: number, total: number): number {
if (total === 0) {
return NO_DATA;
}
return toHighPrecision(good / total);
}

View file

@ -1,20 +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 { ErrorBudget, SLO, Status } from '../models';
export function computeSummaryStatus(slo: SLO, sliValue: number, errorBudget: ErrorBudget): Status {
if (sliValue === -1) {
return 'NO_DATA';
}
if (sliValue >= slo.objective.target) {
return 'HEALTHY';
} else {
return errorBudget.remaining > 0 ? 'DEGRADING' : 'VIOLATED';
}
}

View file

@ -1,40 +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 { calendarAlignedTimeWindowSchema, rollingTimeWindowSchema } from '@kbn/slo-schema';
import { assertNever } from '@kbn/std';
import moment from 'moment';
import { DateRange } from '../models';
import {
TimeWindow,
toCalendarAlignedTimeWindowMomentUnit,
toRollingTimeWindowMomentUnit,
} from '../models/time_window';
export const toDateRange = (timeWindow: TimeWindow, currentDate: Date = new Date()): DateRange => {
if (calendarAlignedTimeWindowSchema.is(timeWindow)) {
const unit = toCalendarAlignedTimeWindowMomentUnit(timeWindow);
const from = moment.utc(currentDate).startOf(unit);
const to = moment.utc(currentDate).endOf(unit);
return { from: from.toDate(), to: to.toDate() };
}
if (rollingTimeWindowSchema.is(timeWindow)) {
const unit = toRollingTimeWindowMomentUnit(timeWindow);
const now = moment.utc(currentDate).startOf('minute');
const from = now.clone().subtract(timeWindow.duration.value, unit);
const to = now.clone();
return {
from: from.toDate(),
to: to.toDate(),
};
}
assertNever(timeWindow);
};

View file

@ -1,22 +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 { toHighPrecision } from '../../utils/number';
import { ErrorBudget } from '../models';
export function toErrorBudget(
initial: number,
consumed: number,
isEstimated: boolean = false
): ErrorBudget {
return {
initial: toHighPrecision(initial),
consumed: toHighPrecision(consumed),
remaining: toHighPrecision(1 - consumed),
isEstimated,
};
}

View file

@ -1,18 +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 { timeslicesBudgetingMethodSchema } from '@kbn/slo-schema';
import { SLO } from '../models';
export function getDelayInSecondsFromSLO(slo: SLO) {
const fixedInterval = timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)
? slo.objective.timesliceWindow!.asSeconds()
: 60;
const syncDelay = slo.settings.syncDelay.asSeconds();
const frequency = slo.settings.frequency.asSeconds();
return fixedInterval + syncDelay + frequency;
}

View file

@ -1,23 +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 moment from 'moment';
import { Duration, toMomentUnitOfTime } from '../models';
export function getLookbackDateRange(
startedAt: Date,
duration: Duration,
delayInSeconds = 0
): { from: Date; to: Date } {
const unit = toMomentUnitOfTime(duration.unit);
const now = moment(startedAt).subtract(delayInSeconds, 'seconds').startOf('minute');
const from = now.clone().subtract(duration.value, unit).startOf('minute');
return {
from: from.toDate(),
to: now.toDate(),
};
}

View file

@ -1,122 +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 {
timeslicesBudgetingMethodSchema,
Duration,
DurationUnit,
rollingTimeWindowSchema,
calendarAlignedTimeWindowSchema,
} from '@kbn/slo-schema';
import { IllegalArgumentError } from '../../errors';
import { SLO } from '../models';
/**
* Asserts the SLO is valid from a business invariants point of view.
* e.g. a 'target' objective requires a number between ]0, 1]
* e.g. a 'timeslices' budgeting method requires an objective's timeslice_target to be defined.
*
* @param slo {SLO}
*/
export function validateSLO(slo: SLO) {
if (!isValidId(slo.id)) {
throw new IllegalArgumentError('Invalid id');
}
if (!isValidTargetNumber(slo.objective.target)) {
throw new IllegalArgumentError('Invalid objective.target');
}
if (
rollingTimeWindowSchema.is(slo.timeWindow) &&
!isValidRollingTimeWindowDuration(slo.timeWindow.duration)
) {
throw new IllegalArgumentError('Invalid time_window.duration');
}
if (
calendarAlignedTimeWindowSchema.is(slo.timeWindow) &&
!isValidCalendarAlignedTimeWindowDuration(slo.timeWindow.duration)
) {
throw new IllegalArgumentError('Invalid time_window.duration');
}
if (timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)) {
if (
slo.objective.timesliceTarget === undefined ||
!isValidTargetNumber(slo.objective.timesliceTarget)
) {
throw new IllegalArgumentError('Invalid objective.timeslice_target');
}
if (
slo.objective.timesliceWindow === undefined ||
!isValidTimesliceWindowDuration(slo.objective.timesliceWindow, slo.timeWindow.duration)
) {
throw new IllegalArgumentError('Invalid objective.timeslice_window');
}
}
validateSettings(slo);
}
function validateSettings(slo: SLO) {
if (!isValidFrequencySettings(slo.settings.frequency)) {
throw new IllegalArgumentError('Invalid settings.frequency');
}
if (!isValidSyncDelaySettings(slo.settings.syncDelay)) {
throw new IllegalArgumentError('Invalid settings.sync_delay');
}
}
function isValidId(id: string): boolean {
const MIN_ID_LENGTH = 8;
const MAX_ID_LENGTH = 36;
return MIN_ID_LENGTH <= id.length && id.length <= MAX_ID_LENGTH;
}
function isValidTargetNumber(value: number): boolean {
return value > 0 && value < 1;
}
function isValidRollingTimeWindowDuration(duration: Duration): boolean {
// 7, 30 or 90days accepted
return duration.unit === DurationUnit.Day && [7, 30, 90].includes(duration.value);
}
function isValidCalendarAlignedTimeWindowDuration(duration: Duration): boolean {
// 1 week or 1 month
return [DurationUnit.Week, DurationUnit.Month].includes(duration.unit) && duration.value === 1;
}
function isValidTimesliceWindowDuration(timesliceWindow: Duration, timeWindow: Duration): boolean {
return (
[DurationUnit.Minute, DurationUnit.Hour].includes(timesliceWindow.unit) &&
timesliceWindow.isShorterThan(timeWindow)
);
}
/**
* validate that 1 minute <= frequency < 1 hour
*/
function isValidFrequencySettings(frequency: Duration): boolean {
return (
frequency.isLongerOrEqualThan(new Duration(1, DurationUnit.Minute)) &&
frequency.isShorterThan(new Duration(1, DurationUnit.Hour))
);
}
/**
* validate that 1 minute <= sync_delay < 6 hour
*/
function isValidSyncDelaySettings(syncDelay: Duration): boolean {
return (
syncDelay.isLongerOrEqualThan(new Duration(1, DurationUnit.Minute)) &&
syncDelay.isShorterThan(new Duration(6, DurationUnit.Hour))
);
}

View file

@ -101,3 +101,7 @@ export async function typedSearch<
): Promise<ESSearchResponse<DocumentSource, TParams>> {
return (await esClient.search(params)) as unknown as ESSearchResponse<DocumentSource, TParams>;
}
export function createEsParams<T extends estypes.SearchRequest>(params: T): T {
return params;
}

View file

@ -12,21 +12,23 @@ export const SLO_DETAIL_PATH = '/:sloId' as const;
export const SLO_CREATE_PATH = '/create' as const;
export const SLO_EDIT_PATH = '/edit/:sloId' as const;
export const SLOS_OUTDATED_DEFINITIONS_PATH = '/outdated-definitions' as const;
export const SLO_SETTINGS_PATH = '/settings' as const;
export const paths = {
slos: `${SLOS_BASE_PATH}${SLOS_PATH}`,
slosSettings: `${SLOS_BASE_PATH}${SLO_SETTINGS_PATH}`,
slosWelcome: `${SLOS_BASE_PATH}${SLOS_WELCOME_PATH}`,
slosOutdatedDefinitions: `${SLOS_BASE_PATH}${SLOS_OUTDATED_DEFINITIONS_PATH}`,
sloCreate: `${SLOS_BASE_PATH}${SLO_CREATE_PATH}`,
sloCreateWithEncodedForm: (encodedParams: string) =>
`${SLOS_BASE_PATH}${SLO_CREATE_PATH}?_a=${encodedParams}`,
sloEdit: (sloId: string) => `${SLOS_BASE_PATH}${SLOS_PATH}/edit/${encodeURIComponent(sloId)}`,
sloEdit: (sloId: string) => `${SLOS_BASE_PATH}/edit/${encodeURIComponent(sloId)}`,
sloEditWithEncodedForm: (sloId: string, encodedParams: string) =>
`${SLOS_BASE_PATH}${SLOS_PATH}/edit/${encodeURIComponent(sloId)}?_a=${encodedParams}`,
sloDetails: (sloId: string, instanceId?: string) =>
!!instanceId
? `${SLOS_BASE_PATH}/${encodeURIComponent(sloId)}?instanceId=${encodeURIComponent(
instanceId
)}`
: `${SLOS_BASE_PATH}/${encodeURIComponent(sloId)}`,
`${SLOS_BASE_PATH}/edit/${encodeURIComponent(sloId)}?_a=${encodedParams}`,
sloDetails: (sloId: string, instanceId?: string, remoteName?: string) => {
const qs = new URLSearchParams();
if (!!instanceId) qs.append('instanceId', instanceId);
if (!!remoteName) qs.append('remoteName', remoteName);
return `${SLOS_BASE_PATH}/${encodeURIComponent(sloId)}?${qs.toString()}`;
},
};

View file

@ -0,0 +1,50 @@
/*
* 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 { getListOfSloSummaryIndices } from './summary_indices';
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from './constants';
describe('getListOfSloSummaryIndices', () => {
it('should return default index if disabled', function () {
const settings = {
useAllRemoteClusters: false,
selectedRemoteClusters: [],
};
const result = getListOfSloSummaryIndices(settings, []);
expect(result).toBe(SLO_SUMMARY_DESTINATION_INDEX_PATTERN);
});
it('should return all remote clusters when enabled', function () {
const settings = {
useAllRemoteClusters: true,
selectedRemoteClusters: [],
};
const clustersByName = [
{ name: 'cluster1', isConnected: true },
{ name: 'cluster2', isConnected: true },
];
const result = getListOfSloSummaryIndices(settings, clustersByName);
expect(result).toBe(
`${SLO_SUMMARY_DESTINATION_INDEX_PATTERN},cluster1:${SLO_SUMMARY_DESTINATION_INDEX_PATTERN},cluster2:${SLO_SUMMARY_DESTINATION_INDEX_PATTERN}`
);
});
it('should return selected when enabled', function () {
const settings = {
useAllRemoteClusters: false,
selectedRemoteClusters: ['cluster1'],
};
const clustersByName = [
{ name: 'cluster1', isConnected: true },
{ name: 'cluster2', isConnected: true },
];
const result = getListOfSloSummaryIndices(settings, clustersByName);
expect(result).toBe(
`${SLO_SUMMARY_DESTINATION_INDEX_PATTERN},cluster1:${SLO_SUMMARY_DESTINATION_INDEX_PATTERN}`
);
});
});

View file

@ -0,0 +1,28 @@
/*
* 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 { GetSLOSettingsResponse } from '@kbn/slo-schema';
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from './constants';
export const getListOfSloSummaryIndices = (
settings: GetSLOSettingsResponse,
clustersByName: Array<{ name: string; isConnected: boolean }>
) => {
const { useAllRemoteClusters, selectedRemoteClusters } = settings;
if (!useAllRemoteClusters && selectedRemoteClusters.length === 0) {
return SLO_SUMMARY_DESTINATION_INDEX_PATTERN;
}
const indices: string[] = [SLO_SUMMARY_DESTINATION_INDEX_PATTERN];
clustersByName.forEach(({ name, isConnected }) => {
if (isConnected && (useAllRemoteClusters || selectedRemoteClusters.includes(name))) {
indices.push(`${name}:${SLO_SUMMARY_DESTINATION_INDEX_PATTERN}`);
}
});
return indices.join(',');
};

View file

@ -6,15 +6,15 @@
*/
import { EuiBasicTable, EuiSpacer, EuiText, EuiTitle, HorizontalAlignment } from '@elastic/eui';
import { SLOResponse } from '@kbn/slo-schema';
import React from 'react';
import { i18n } from '@kbn/i18n';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import { SLODefinitionResponse } from '@kbn/slo-schema';
import React from 'react';
import { WindowSchema } from '../../typings';
import { toDuration, toMinutes } from '../../utils/slo/duration';
interface AlertTimeTableProps {
slo: SLOResponse;
slo: SLODefinitionResponse;
windows: WindowSchema[];
}

View file

@ -7,7 +7,7 @@
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import React, { useEffect, useState } from 'react';
import { ALL_VALUE, SLOResponse } from '@kbn/slo-schema';
import { ALL_VALUE, SLODefinitionResponse } from '@kbn/slo-schema';
import { EuiCallOut, EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -33,7 +33,7 @@ export function BurnRateRuleEditor(props: Props) {
sloId: ruleParams?.sloId,
});
const [selectedSlo, setSelectedSlo] = useState<SLOResponse | undefined>(undefined);
const [selectedSlo, setSelectedSlo] = useState<SLODefinitionResponse | undefined>(undefined);
const [windowDefs, setWindowDefs] = useState<WindowSchema[]>(ruleParams?.windows || []);
const [dependencies, setDependencies] = useState<Dependency[]>(ruleParams?.dependencies || []);
@ -47,7 +47,7 @@ export function BurnRateRuleEditor(props: Props) {
});
}, [initialSlo]);
const onSelectedSlo = (slo: SLOResponse | undefined) => {
const onSelectedSlo = (slo: SLODefinitionResponse | undefined) => {
setSelectedSlo(slo);
setWindowDefs(() => {
return createDefaultWindows(slo);
@ -111,7 +111,7 @@ export function BurnRateRuleEditor(props: Props) {
);
}
function createDefaultWindows(slo: SLOResponse | undefined) {
function createDefaultWindows(slo: SLODefinitionResponse | undefined) {
const burnRateDefaults = slo ? BURN_RATE_DEFAULTS[slo.timeWindow.duration] : [];
return burnRateDefaults.map((partialWindow) => createNewWindow(slo, partialWindow));
}

View file

@ -5,11 +5,10 @@
* 2.0.
*/
import React from 'react';
import { ComponentStory } from '@storybook/react';
import { SLOResponse } from '@kbn/slo-schema';
import { KibanaReactStorybookDecorator } from '@kbn/observability-plugin/public';
import { SLODefinitionResponse } from '@kbn/slo-schema';
import { ComponentStory } from '@storybook/react';
import React from 'react';
import { SloSelector as Component } from './slo_selector';
export default {
@ -20,7 +19,7 @@ export default {
const Template: ComponentStory<typeof Component> = () => (
// eslint-disable-next-line no-console
<Component onSelected={(slo: SLOResponse | undefined) => console.log(slo)} />
<Component onSelected={(slo: SLODefinitionResponse | undefined) => console.log(slo)} />
);
const defaultProps = {};

View file

@ -7,15 +7,15 @@
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SLOResponse } from '@kbn/slo-schema';
import { SLODefinitionResponse } from '@kbn/slo-schema';
import { debounce } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useFetchSloDefinitions } from '../../hooks/use_fetch_slo_definitions';
interface Props {
initialSlo?: SLOResponse;
initialSlo?: SLODefinitionResponse;
errors?: string[];
onSelected: (slo: SLOResponse | undefined) => void;
onSelected: (slo: SLODefinitionResponse | undefined) => void;
}
function SloSelector({ initialSlo, onSelected, errors }: Props) {

View file

@ -17,7 +17,7 @@ import {
EuiTitle,
EuiSwitch,
} from '@elastic/eui';
import { CreateSLOInput, SLOResponse } from '@kbn/slo-schema';
import { CreateSLOInput, SLODefinitionResponse } from '@kbn/slo-schema';
import { i18n } from '@kbn/i18n';
import numeral from '@elastic/numeral';
import { v4 } from 'uuid';
@ -36,7 +36,7 @@ import { WindowResult } from './validation';
import { BudgetConsumed } from './budget_consumed';
interface WindowProps extends WindowSchema {
slo?: SLOResponse;
slo?: SLODefinitionResponse;
onChange: (windowDef: WindowSchema) => void;
onDelete: (id: string) => void;
disableDelete: boolean;
@ -53,7 +53,7 @@ const ACTION_GROUP_OPTIONS = [
export const calculateMaxBurnRateThreshold = (
longWindow: Duration,
slo?: SLOResponse | CreateSLOInput
slo?: SLODefinitionResponse | CreateSLOInput
) => {
return slo
? Math.floor(toMinutes(toDuration(slo.timeWindow.duration)) / toMinutes(longWindow))
@ -246,7 +246,7 @@ const getErrorBudgetExhaustionText = (
},
});
export const createNewWindow = (
slo?: SLOResponse | CreateSLOInput,
slo?: SLODefinitionResponse | CreateSLOInput,
partialWindow: Partial<WindowSchema> = {}
): WindowSchema => {
const longWindow = partialWindow.longWindow || { value: 1, unit: 'h' };
@ -264,7 +264,7 @@ export const createNewWindow = (
interface WindowsProps {
windows: WindowSchema[];
onChange: (windows: WindowSchema[]) => void;
slo?: SLOResponse;
slo?: SLODefinitionResponse;
errors: WindowResult[];
totalNumberOfWindows?: number;
}

View file

@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiHeaderLink, EuiHeaderLinks } from '@elast
import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public';
import { useKibana } from '../../utils/kibana_react';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { SLOS_BASE_PATH, SLO_SETTINGS_PATH } from '../../../common/locators/paths';
export function HeaderMenu(): React.ReactElement | null {
const { http, theme } = useKibana().services;
@ -33,6 +34,15 @@ export function HeaderMenu(): React.ReactElement | null {
defaultMessage: 'Add integrations',
})}
</EuiHeaderLink>
<EuiHeaderLink
color="primary"
href={http.basePath.prepend(`${SLOS_BASE_PATH}${SLO_SETTINGS_PATH}`)}
iconType="gear"
>
{i18n.translate('xpack.slo.headerMenu.settings', {
defaultMessage: 'Settings',
})}
</EuiHeaderLink>
</EuiHeaderLinks>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -8,13 +8,13 @@
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat, EuiText, EuiTextColor } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import { SLOResponse } from '@kbn/slo-schema';
import { SLODefinitionResponse } from '@kbn/slo-schema';
import moment from 'moment';
import React from 'react';
import { toDuration, toMinutes } from '../../../utils/slo/duration';
export interface BurnRateParams {
slo: SLOResponse;
slo: SLODefinitionResponse;
threshold: number;
burnRate?: number;
isLoading?: boolean;
@ -40,7 +40,7 @@ function getTitleFromStatus(status: Status): string {
function getSubtitleFromStatus(
status: Status,
burnRate: number | undefined = 1,
slo: SLOResponse
slo: SLODefinitionResponse
): string {
if (status === 'NO_DATA')
return i18n.translate('xpack.slo.burnRate.noDataStatusSubtitle', {

View file

@ -7,12 +7,12 @@
import { EuiConfirmModal } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ALL_VALUE, SLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { ALL_VALUE, SLODefinitionResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
import React from 'react';
import { getGroupKeysProse } from '../../../utils/slo/groupings';
export interface SloDeleteConfirmationModalProps {
slo: SLOWithSummaryResponse | SLOResponse;
slo: SLOWithSummaryResponse | SLODefinitionResponse;
onCancel: () => void;
onConfirm: () => void;
}

View file

@ -6,7 +6,7 @@
*/
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { SLOResponse } from '@kbn/slo-schema';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import moment from 'moment';
import React from 'react';
import { useKibana } from '../../../utils/kibana_react';
@ -14,7 +14,7 @@ import { getDelayInSecondsFromSLO } from '../../../utils/slo/get_delay_in_second
import { AlertAnnotation, TimeRange, useLensDefinition } from './use_lens_definition';
interface Props {
slo: SLOResponse;
slo: SLOWithSummaryResponse;
dataTimeRange: TimeRange;
threshold: number;
alertTimeRange?: TimeRange;

View file

@ -9,7 +9,7 @@ import { transparentize, useEuiTheme } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { ALL_VALUE, SLOResponse } from '@kbn/slo-schema';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import moment from 'moment';
import { v4 as uuidv4 } from 'uuid';
import { SLO_DESTINATION_INDEX_PATTERN } from '../../../../common/constants';
@ -25,7 +25,7 @@ export interface AlertAnnotation {
}
export function useLensDefinition(
slo: SLOResponse,
slo: SLOWithSummaryResponse,
threshold: number,
alertTimeRange?: TimeRange,
annotations?: AlertAnnotation[],
@ -466,7 +466,9 @@ export function useLensDefinition(
adHocDataViews: {
'32ca1ad4-81c0-4daf-b9d1-07118044bdc5': {
id: '32ca1ad4-81c0-4daf-b9d1-07118044bdc5',
title: SLO_DESTINATION_INDEX_PATTERN,
title: !!slo.remote
? `${slo.remote.remoteName}:${SLO_DESTINATION_INDEX_PATTERN}`
: SLO_DESTINATION_INDEX_PATTERN,
timeFieldName: '@timestamp',
sourceFilters: [],
fieldFormats: {},

View file

@ -7,11 +7,11 @@
import { EuiConfirmModal } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { SLODefinitionResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
import React from 'react';
export interface SloResetConfirmationModalProps {
slo: SLOWithSummaryResponse | SLOResponse;
slo: SLOWithSummaryResponse | SLODefinitionResponse;
onCancel: () => void;
onConfirm: () => void;
}

View file

@ -33,25 +33,31 @@ export function SloStatusBadge({ slo }: SloStatusProps) {
</EuiToolTip>
)}
{slo.summary.status === 'HEALTHY' && (
<EuiBadge color="success">
{i18n.translate('xpack.slo.sloStatusBadge.healthy', {
defaultMessage: 'Healthy',
})}
</EuiBadge>
<div>
<EuiBadge color="success">
{i18n.translate('xpack.slo.sloStatusBadge.healthy', {
defaultMessage: 'Healthy',
})}
</EuiBadge>
</div>
)}
{slo.summary.status === 'DEGRADING' && (
<EuiBadge color="warning">
{i18n.translate('xpack.slo.sloStatusBadge.degrading', {
defaultMessage: 'Degrading',
})}
</EuiBadge>
<div>
<EuiBadge color="warning">
{i18n.translate('xpack.slo.sloStatusBadge.degrading', {
defaultMessage: 'Degrading',
})}
</EuiBadge>
</div>
)}
{slo.summary.status === 'VIOLATED' && (
<EuiBadge color="danger">
{i18n.translate('xpack.slo.sloStatusBadge.violated', {
defaultMessage: 'Violated',
})}
</EuiBadge>
<div>
<EuiBadge color="danger">
{i18n.translate('xpack.slo.sloStatusBadge.violated', {
defaultMessage: 'Violated',
})}
</EuiBadge>
</div>
)}
</EuiFlexItem>

View file

@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiLink } from '@elastic/eu
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { useFetchSloList } from '../../../hooks/use_fetch_slo_list';
import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter';
import { useFetchHistoricalSummary } from '../../../hooks/use_fetch_historical_summary';
import { useFetchSloDetails } from '../../../hooks/use_fetch_slo_details';
@ -40,9 +41,15 @@ export function SloErrorBudget({
};
}, [reloadSubject]);
const kqlQuery = `slo.id:"${sloId}" and slo.instanceId:"${sloInstanceId}"`;
const { data: sloList } = useFetchSloList({
kqlQuery,
});
const { isLoading: historicalSummaryLoading, data: historicalSummaries = [] } =
useFetchHistoricalSummary({
list: [{ sloId: sloId!, instanceId: sloInstanceId ?? ALL_VALUE }],
sloList: sloList?.results ?? [],
shouldRefetch: false,
});

View file

@ -64,6 +64,7 @@ function SingleSloConfiguration({ overviewMode, onCreate, onCancel }: SingleConf
showAllGroupByInstances,
sloId: selectedSlo?.sloId,
sloInstanceId: selectedSlo?.sloInstanceId,
remoteName: selectedSlo?.remoteName,
overviewMode,
});
@ -73,14 +74,18 @@ function SingleSloConfiguration({ overviewMode, onCreate, onCancel }: SingleConf
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexItem grow>
<SloSelector
singleSelection={true}
hasError={hasError}
onSelected={(slo) => {
setHasError(slo === undefined);
if (slo && 'id' in slo) {
setSelectedSlo({ sloId: slo.id, sloInstanceId: slo.instanceId });
setSelectedSlo({
sloId: slo.id,
sloInstanceId: slo.instanceId,
remoteName: slo.remote?.remoteName,
});
}
}}
/>

View file

@ -92,8 +92,14 @@ export class SLOEmbeddable extends AbstractEmbeddable<SloEmbeddableInput, Embedd
this.node = node;
// required for the export feature to work
this.node.setAttribute('data-shared-item', '');
const { sloId, sloInstanceId, showAllGroupByInstances, overviewMode, groupFilters } =
this.getInput();
const {
sloId,
sloInstanceId,
showAllGroupByInstances,
overviewMode,
groupFilters,
remoteName,
} = this.getInput();
const queryClient = new QueryClient();
const { observabilityRuleTypeRegistry } = this.deps.observability;
const I18nContext = this.deps.i18n.Context;
@ -149,6 +155,7 @@ export class SLOEmbeddable extends AbstractEmbeddable<SloEmbeddableInput, Embedd
sloInstanceId={sloInstanceId}
reloadSubject={this.reloadSubject}
showAllGroupByInstances={showAllGroupByInstances}
remoteName={remoteName}
/>
);
}

View file

@ -24,6 +24,7 @@ import { SingleSloProps } from './types';
export function SloOverview({
sloId,
sloInstanceId,
remoteName,
onRenderComplete,
reloadSubject,
}: SingleSloProps) {
@ -45,6 +46,7 @@ export function SloOverview({
isRefetching,
} = useFetchSloDetails({
sloId,
remoteName,
instanceId: sloInstanceId,
});
@ -57,7 +59,7 @@ export function SloOverview({
});
const { data: historicalSummaries = [] } = useFetchHistoricalSummary({
list: slo ? [{ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE }] : [],
sloList: slo ? [slo] : [],
});
const [selectedSlo, setSelectedSlo] = useState<SLOWithSummaryResponse | null>(null);

View file

@ -96,12 +96,7 @@ export function SloCardChartList({ sloId }: { sloId: string }) {
});
const { data: historicalSummaries = [] } = useFetchHistoricalSummary({
list: [
{
sloId,
instanceId: ALL_VALUE,
},
],
sloList: sloList?.results ?? [],
});
const { colors } = useSloCardColor();

View file

@ -7,7 +7,6 @@
import { EmbeddableInput } from '@kbn/embeddable-plugin/public';
import { Subject } from 'rxjs';
import { Filter } from '@kbn/es-query';
export type OverviewMode = 'single' | 'groups';
export type GroupBy = 'slo.tags' | 'status' | 'slo.indicator.type';
export interface GroupFilters {
@ -28,6 +27,9 @@ export type GroupSloProps = EmbeddableSloProps & {
};
export interface EmbeddableSloProps {
sloId?: string;
sloInstanceId?: string;
remoteName?: string;
reloadSubject?: Subject<boolean>;
onRenderComplete?: () => void;
overviewMode?: OverviewMode;

View file

@ -10,13 +10,13 @@ import { HEALTHY_ROLLING_SLO, historicalSummaryData } from '../../data/slo/histo
import { Params, UseFetchHistoricalSummaryResponse } from '../use_fetch_historical_summary';
export const useFetchHistoricalSummary = ({
list = [],
sloList = [],
}: Params): UseFetchHistoricalSummaryResponse => {
const data: FetchHistoricalSummaryResponse = [];
list.forEach(({ sloId, instanceId }) =>
sloList.forEach(({ id, instanceId }) =>
data.push({
sloId,
instanceId,
sloId: id,
instanceId: instanceId!,
data: historicalSummaryData.find((datum) => datum.sloId === HEALTHY_ROLLING_SLO)!.data,
})
);

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { ALL_VALUE, SLOResponse } from '@kbn/slo-schema';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
type SLO = Pick<SLOResponse, 'id' | 'instanceId'>;
type SLO = Pick<SLOWithSummaryResponse, 'id' | 'instanceId'>;
export class ActiveAlerts {
private data: Map<string, number> = new Map();

View file

@ -6,27 +6,31 @@
*/
import { encode } from '@kbn/rison';
import { useCallback } from 'react';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import { useKibana } from '../utils/kibana_react';
import { useCallback } from 'react';
import { paths } from '../../common/locators/paths';
import { useKibana } from '../utils/kibana_react';
import { createRemoteSloCloneUrl } from '../utils/slo/remote_slo_urls';
import { useSpace } from './use_space';
export function useCloneSlo() {
const {
http: { basePath },
application: { navigateToUrl },
} = useKibana().services;
const spaceId = useSpace();
return useCallback(
(slo: SLOWithSummaryResponse) => {
navigateToUrl(
basePath.prepend(
paths.sloCreateWithEncodedForm(
encode({ ...slo, name: `[Copy] ${slo.name}`, id: undefined })
)
)
);
if (slo.remote) {
window.open(createRemoteSloCloneUrl(slo, spaceId), '_blank');
} else {
const clonePath = paths.sloCreateWithEncodedForm(
encode({ ...slo, name: `[Copy] ${slo.name}`, id: undefined })
);
navigateToUrl(basePath.prepend(clonePath));
}
},
[navigateToUrl, basePath]
[navigateToUrl, basePath, spaceId]
);
}

View file

@ -10,7 +10,7 @@ import { DataView } from '@kbn/data-views-plugin/common';
import { useKibana } from '../utils/kibana_react';
interface UseCreateDataViewProps {
indexPatternString: string | undefined;
indexPatternString?: string;
}
export function useCreateDataView({ indexPatternString }: UseCreateDataViewProps) {

View file

@ -6,7 +6,7 @@
*/
import { useQuery } from '@tanstack/react-query';
import { FetchHistoricalSummaryResponse } from '@kbn/slo-schema';
import { ALL_VALUE, FetchHistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { useKibana } from '../utils/kibana_react';
import { sloKeys } from './query_key_factory';
import { SLO_LONG_REFETCH_INTERVAL } from '../constants';
@ -21,16 +21,27 @@ export interface UseFetchHistoricalSummaryResponse {
}
export interface Params {
list: Array<{ sloId: string; instanceId: string }>;
sloList: SLOWithSummaryResponse[];
shouldRefetch?: boolean;
}
export function useFetchHistoricalSummary({
list = [],
sloList = [],
shouldRefetch,
}: Params): UseFetchHistoricalSummaryResponse {
const { http } = useKibana().services;
const list = sloList.map((slo) => ({
sloId: slo.id,
instanceId: slo.instanceId ?? ALL_VALUE,
remoteName: slo.remote?.remoteName,
timeWindow: slo.timeWindow,
groupBy: slo.groupBy,
revision: slo.revision,
objective: slo.objective,
budgetingMethod: slo.budgetingMethod,
}));
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
queryKey: sloKeys.historicalSummary(list),
queryFn: async ({ signal }) => {

View file

@ -47,7 +47,11 @@ export function useFetchSloBurnRates({
const response = await http.post<GetSLOBurnRatesResponse>(
`/internal/observability/slos/${slo.id}/_burn_rates`,
{
body: JSON.stringify({ windows, instanceId: slo.instanceId ?? ALL_VALUE }),
body: JSON.stringify({
windows,
instanceId: slo.instanceId ?? ALL_VALUE,
remoteName: slo.remote?.remoteName,
}),
signal,
}
);

View file

@ -31,10 +31,12 @@ export interface UseFetchSloDetailsResponse {
export function useFetchSloDetails({
sloId,
instanceId,
remoteName,
shouldRefetch,
}: {
sloId?: string;
instanceId?: string;
remoteName?: string | null;
shouldRefetch?: boolean;
}): UseFetchSloDetailsResponse {
const { http } = useKibana().services;
@ -47,6 +49,7 @@ export function useFetchSloDetails({
const response = await http.get<GetSLOResponse>(`/api/observability/slos/${sloId}`, {
query: {
...(!!instanceId && instanceId !== ALL_VALUE && { instanceId }),
...(remoteName && { remoteName }),
},
signal,
});

View file

@ -6,12 +6,12 @@
*/
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { CreateSLOInput, SLOResponse } from '@kbn/slo-schema';
import type { CreateSLOInput, SLODefinitionResponse } from '@kbn/slo-schema';
import { useQuery } from '@tanstack/react-query';
import { useKibana } from '../utils/kibana_react';
interface SLOInspectResponse {
slo: SLOResponse;
slo: SLODefinitionResponse;
pipeline: Record<string, any>;
rollUpTransform: TransformPutTransformRequest;
summaryTransform: TransformPutTransformRequest;

View file

@ -26,10 +26,12 @@ export function useGetPreviewData({
groupBy,
groupings,
instanceId,
remoteName,
}: {
isValid: boolean;
groupBy?: string;
instanceId?: string;
remoteName?: string;
groupings?: Record<string, unknown>;
objective?: Objective;
indicator: Indicator;
@ -49,6 +51,7 @@ export function useGetPreviewData({
groupBy,
instanceId,
groupings,
remoteName,
...(objective ? { objective } : null),
}),
signal,

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useState, useEffect } from 'react';
import { useKibana } from '../utils/kibana_react';
export function useSpace() {
const { spaces } = useKibana().services;
const [spaceId, setSpaceId] = useState<string>();
useEffect(() => {
if (spaces) {
spaces.getActiveSpace().then((space) => setSpaceId(space.id));
}
}, [spaces]);
return spaceId;
}

View file

@ -65,6 +65,7 @@ export function EventsChartPanel({ slo, range }: Props) {
indicator: slo.indicator,
groupings: slo.groupings,
instanceId: slo.instanceId,
remoteName: slo.remote?.remoteName,
});
const dateFormat = uiSettings.get('dateFormat');

View file

@ -5,25 +5,30 @@
* 2.0.
*/
import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import {
EuiButton,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiIcon,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import React, { useCallback, useState } from 'react';
import type { RulesParams } from '@kbn/observability-plugin/public';
import { rulesLocatorID } from '@kbn/observability-plugin/common';
import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils';
import { sloFeatureId } from '@kbn/observability-plugin/common';
import { EditBurnRateRuleFlyout } from '../../slos/components/common/edit_burn_rate_rule_flyout';
import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo';
import { useKibana } from '../../../utils/kibana_react';
import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import React, { useCallback, useEffect, useState } from 'react';
import { paths } from '../../../../common/locators/paths';
import { SloDeleteConfirmationModal } from '../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal';
import { useCapabilities } from '../../../hooks/use_capabilities';
import { useCloneSlo } from '../../../hooks/use_clone_slo';
import { useDeleteSlo } from '../../../hooks/use_delete_slo';
import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo';
import { useKibana } from '../../../utils/kibana_react';
import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url';
import { isApmIndicatorType } from '../../../utils/slo/indicator';
import { EditBurnRateRuleFlyout } from '../../slos/components/common/edit_burn_rate_rule_flyout';
import { useGetQueryParams } from '../hooks/use_get_query_params';
import { useSloActions } from '../hooks/use_slo_actions';
export interface Props {
slo?: SLOWithSummaryResponse;
@ -34,18 +39,17 @@ export function HeaderControl({ isLoading, slo }: Props) {
const {
application: { navigateToUrl, capabilities },
http: { basePath },
share: {
url: { locators },
},
triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout },
} = useKibana().services;
const hasApmReadCapabilities = capabilities.apm.show;
const { hasWriteCapabilities } = useCapabilities();
const { isDeletingSlo, removeDeleteQueryParam } = useGetQueryParams();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isRuleFlyoutVisible, setRuleFlyoutVisibility] = useState<boolean>(false);
const [isEditRuleFlyoutOpen, setIsEditRuleFlyoutOpen] = useState(false);
const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false);
const { mutate: deleteSlo } = useDeleteSlo();
@ -59,11 +63,11 @@ export function HeaderControl({ isLoading, slo }: Props) {
const handleActionsClick = () => setIsPopoverOpen((value) => !value);
const closePopover = () => setIsPopoverOpen(false);
const handleEdit = () => {
if (slo) {
navigate(basePath.prepend(paths.sloEdit(slo.id)));
useEffect(() => {
if (isDeletingSlo) {
setDeleteConfirmationModalOpen(true);
}
};
}, [isDeletingSlo]);
const onCloseRuleFlyout = () => {
setRuleFlyoutVisibility(false);
@ -74,18 +78,12 @@ export function HeaderControl({ isLoading, slo }: Props) {
setRuleFlyoutVisibility(true);
};
const handleNavigateToRules = async () => {
if (rules.length === 1) {
setIsEditRuleFlyoutOpen(true);
setIsPopoverOpen(false);
} else {
const locator = locators.get<RulesParams>(rulesLocatorID);
if (slo?.id && locator) {
locator.navigate({ params: { sloId: slo.id } }, { replace: false });
}
}
};
const { handleNavigateToRules, sloEditUrl, remoteDeleteUrl } = useSloActions({
slo,
rules,
setIsEditRuleFlyoutOpen,
setIsActionsPopoverOpen: setIsPopoverOpen,
});
const handleNavigateToApm = () => {
if (!slo) {
@ -108,11 +106,16 @@ export function HeaderControl({ isLoading, slo }: Props) {
};
const handleDelete = () => {
setDeleteConfirmationModalOpen(true);
setIsPopoverOpen(false);
if (!!remoteDeleteUrl) {
window.open(remoteDeleteUrl, '_blank');
} else {
setDeleteConfirmationModalOpen(true);
setIsPopoverOpen(false);
}
};
const handleDeleteCancel = () => {
removeDeleteQueryParam();
setDeleteConfirmationModalOpen(false);
};
@ -128,6 +131,19 @@ export function HeaderControl({ isLoading, slo }: Props) {
[navigateToUrl]
);
const isRemote = !!slo?.remote;
const hasUndefinedRemoteKibanaUrl = !!slo?.remote && slo?.remote?.kibanaUrl === '';
const showRemoteLinkIcon = isRemote ? (
<EuiIcon
type="popout"
size="s"
css={{
marginLeft: '10px',
}}
/>
) : null;
return (
<>
<EuiPopover
@ -140,7 +156,8 @@ export function HeaderControl({ isLoading, slo }: Props) {
iconType="arrowDown"
iconSize="s"
onClick={handleActionsClick}
disabled={isLoading || !slo}
isLoading={isLoading}
disabled={isLoading}
>
{i18n.translate('xpack.slo.sloDetails.headerControl.actions', {
defaultMessage: 'Actions',
@ -155,21 +172,27 @@ export function HeaderControl({ isLoading, slo }: Props) {
items={[
<EuiContextMenuItem
key="edit"
disabled={!hasWriteCapabilities}
disabled={!hasWriteCapabilities || hasUndefinedRemoteKibanaUrl}
icon="pencil"
onClick={handleEdit}
href={sloEditUrl}
target={isRemote ? '_blank' : undefined}
toolTipContent={
hasUndefinedRemoteKibanaUrl ? NOT_AVAILABLE_FOR_UNDEFINED_REMOTE_KIBANA_URL : ''
}
data-test-subj="sloDetailsHeaderControlPopoverEdit"
>
{i18n.translate('xpack.slo.sloDetails.headerControl.edit', {
defaultMessage: 'Edit',
})}
{showRemoteLinkIcon}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="createBurnRateRule"
disabled={!hasWriteCapabilities}
disabled={!hasWriteCapabilities || isRemote}
icon="bell"
onClick={handleOpenRuleFlyout}
data-test-subj="sloDetailsHeaderControlPopoverCreateRule"
toolTipContent={isRemote ? NOT_AVAILABLE_FOR_REMOTE : ''}
>
{i18n.translate('xpack.slo.sloDetails.headerControl.createBurnRateRule', {
defaultMessage: 'Create new alert rule',
@ -177,15 +200,19 @@ export function HeaderControl({ isLoading, slo }: Props) {
</EuiContextMenuItem>,
<EuiContextMenuItem
key="manageRules"
disabled={!hasWriteCapabilities}
disabled={!hasWriteCapabilities || hasUndefinedRemoteKibanaUrl}
icon="gear"
onClick={handleNavigateToRules}
data-test-subj="sloDetailsHeaderControlPopoverManageRules"
toolTipContent={
hasUndefinedRemoteKibanaUrl ? NOT_AVAILABLE_FOR_UNDEFINED_REMOTE_KIBANA_URL : ''
}
>
{i18n.translate('xpack.slo.sloDetails.headerControl.manageRules', {
defaultMessage: 'Manage burn rate {count, plural, one {rule} other {rules}}',
values: { count: rules.length },
})}
{showRemoteLinkIcon}
</EuiContextMenuItem>,
]
.concat(
@ -193,9 +220,10 @@ export function HeaderControl({ isLoading, slo }: Props) {
<EuiContextMenuItem
key="exploreInApm"
icon="bullseye"
disabled={!hasApmReadCapabilities}
disabled={!hasApmReadCapabilities || isRemote}
onClick={handleNavigateToApm}
data-test-subj="sloDetailsHeaderControlPopoverExploreInApm"
toolTipContent={isRemote ? NOT_AVAILABLE_FOR_REMOTE : ''}
>
{i18n.translate('xpack.slo.sloDetails.headerControl.exploreInApm', {
defaultMessage: 'Service details',
@ -208,25 +236,33 @@ export function HeaderControl({ isLoading, slo }: Props) {
.concat(
<EuiContextMenuItem
key="clone"
disabled={!hasWriteCapabilities}
disabled={!hasWriteCapabilities || hasUndefinedRemoteKibanaUrl}
icon="copy"
onClick={handleClone}
data-test-subj="sloDetailsHeaderControlPopoverClone"
toolTipContent={
hasUndefinedRemoteKibanaUrl ? NOT_AVAILABLE_FOR_UNDEFINED_REMOTE_KIBANA_URL : ''
}
>
{i18n.translate('xpack.slo.slo.item.actions.clone', {
defaultMessage: 'Clone',
})}
{showRemoteLinkIcon}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="delete"
icon="trash"
disabled={!hasWriteCapabilities}
disabled={!hasWriteCapabilities || hasUndefinedRemoteKibanaUrl}
onClick={handleDelete}
data-test-subj="sloDetailsHeaderControlPopoverDelete"
toolTipContent={
hasUndefinedRemoteKibanaUrl ? NOT_AVAILABLE_FOR_UNDEFINED_REMOTE_KIBANA_URL : ''
}
>
{i18n.translate('xpack.slo.slo.item.actions.delete', {
defaultMessage: 'Delete',
})}
{showRemoteLinkIcon}
</EuiContextMenuItem>
)}
/>
@ -259,3 +295,14 @@ export function HeaderControl({ isLoading, slo }: Props) {
</>
);
}
const NOT_AVAILABLE_FOR_REMOTE = i18n.translate('xpack.slo.item.actions.notAvailable', {
defaultMessage: 'This action is not available for remote SLOs',
});
const NOT_AVAILABLE_FOR_UNDEFINED_REMOTE_KIBANA_URL = i18n.translate(
'xpack.slo.item.actions.remoteKibanaUrlUndefined',
{
defaultMessage: 'This action is not available for remote SLOs with undefined kibanaUrl',
}
);

View file

@ -5,11 +5,12 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiSkeletonText, EuiText } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSkeletonText, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import moment from 'moment';
import React from 'react';
import { SloRemoteBadge } from '../../slos/components/badges/slo_remote_badge';
import { SLOGroupings } from '../../slos/components/common/slo_groupings';
import { SloStatusBadge } from '../../../components/slo/slo_status_badge';
@ -36,6 +37,7 @@ export function HeaderTitle({ isLoading, slo }: Props) {
wrap={true}
>
<SloStatusBadge slo={slo} />
<SloRemoteBadge slo={slo} />
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs">
<strong>
@ -59,6 +61,7 @@ export function HeaderTitle({ isLoading, slo }: Props) {
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</EuiFlexGroup>
);
}

View file

@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import React, { useEffect, useState } from 'react';
import { BurnRateOption, BurnRates } from '../../../components/slo/burn_rate/burn_rates';
import { useFetchHistoricalSummary } from '../../../hooks/use_fetch_historical_summary';
import { useFetchRulesForSlo } from '../../../hooks/use_fetch_rules_for_slo';
import { formatHistoricalData } from '../../../utils/slo/chart_data_formatter';
@ -19,6 +18,7 @@ import { EventsChartPanel } from './events_chart_panel';
import { Overview } from './overview/overview';
import { SliChartPanel } from './sli_chart_panel';
import { SloDetailsAlerts } from './slo_detail_alerts';
import { SloRemoteCallout } from './slo_remote_callout';
export const TAB_ID_URL_PARAM = 'tabId';
export const OVERVIEW_TAB_ID = 'overview';
@ -91,7 +91,7 @@ export function SloDetails({ slo, isAutoRefreshing, selectedTabId }: Props) {
const { data: historicalSummaries = [], isLoading: historicalSummaryLoading } =
useFetchHistoricalSummary({
list: [{ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE }],
sloList: [slo],
shouldRefetch: isAutoRefreshing,
});
@ -125,6 +125,7 @@ export function SloDetails({ slo, isAutoRefreshing, selectedTabId }: Props) {
return selectedTabId === OVERVIEW_TAB_ID ? (
<EuiFlexGroup direction="column" gutterSize="xl">
<SloRemoteCallout slo={slo} />
<EuiFlexItem>
<Overview slo={slo} />
</EuiFlexItem>

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import { createRemoteSloDetailsUrl } from '../../../utils/slo/remote_slo_urls';
import { useSpace } from '../../../hooks/use_space';
export function SloRemoteCallout({ slo }: { slo: SLOWithSummaryResponse }) {
const spaceId = useSpace();
const sloDetailsUrl = createRemoteSloDetailsUrl(slo, spaceId);
if (!slo.remote) {
return null;
}
return (
<EuiCallOut
title={i18n.translate('xpack.slo.sloDetails.headerTitle.calloutMessage', {
defaultMessage: 'Remote SLO',
})}
>
<p>
<FormattedMessage
id="xpack.slo.sloDetails.headerTitle.calloutDescription"
defaultMessage="This is a remote SLO which belongs to another Kibana instance. It is fetched from the remote cluster: {remoteName} with Kibana URL {kibanaUrl}."
values={{
remoteName: <strong>{slo.remote.remoteName}</strong>,
kibanaUrl: (
<EuiLink
data-test-subj="sloSloRemoteCalloutLink"
href={slo.remote.kibanaUrl}
target="_blank"
>
{slo.remote.kibanaUrl}
</EuiLink>
),
}}
/>
</p>
<EuiButton
data-test-subj="o11yHeaderTitleLinkButtonButton"
href={sloDetailsUrl}
color="primary"
target="_blank"
iconType="popout"
iconSide="right"
>
{i18n.translate('xpack.slo.headerTitle.linkButtonButtonLabel', {
defaultMessage: 'View remote SLO details',
})}
</EuiButton>
</EuiCallOut>
);
}

View file

@ -1,20 +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 { ALL_VALUE } from '@kbn/slo-schema';
import { useLocation } from 'react-router-dom';
export const INSTANCE_SEARCH_PARAM = 'instanceId';
export function useGetInstanceIdQueryParam(): string | undefined {
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const instanceId = searchParams.get(INSTANCE_SEARCH_PARAM);
return !!instanceId && instanceId !== ALL_VALUE ? instanceId : undefined;
}

View file

@ -0,0 +1,44 @@
/*
* 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 { ALL_VALUE } from '@kbn/slo-schema';
import { useHistory, useLocation } from 'react-router-dom';
import { useCallback } from 'react';
export const INSTANCE_SEARCH_PARAM = 'instanceId';
export const REMOTE_NAME_PARAM = 'remoteName';
export const DELETE_SLO = 'delete';
export function useGetQueryParams() {
const { search, pathname } = useLocation();
const history = useHistory();
const searchParams = new URLSearchParams(search);
const instanceId = searchParams.get(INSTANCE_SEARCH_PARAM);
const remoteName = searchParams.get(REMOTE_NAME_PARAM);
const deleteSlo = searchParams.get(DELETE_SLO);
const removeDeleteQueryParam = useCallback(() => {
const qParams = new URLSearchParams(search);
// remote delete param from url after initial load
if (deleteSlo === 'true') {
qParams.delete(DELETE_SLO);
history.replace({
pathname,
search: qParams.toString(),
});
}
}, [deleteSlo, history, pathname, search]);
return {
instanceId: !!instanceId && instanceId !== ALL_VALUE ? instanceId : undefined,
remoteName,
isDeletingSlo: deleteSlo === 'true',
removeDeleteQueryParam,
};
}

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { rulesLocatorID, RulesParams } from '@kbn/observability-plugin/public';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import path from 'path';
import { paths } from '../../../../common/locators/paths';
import { useSpace } from '../../../hooks/use_space';
import { BurnRateRuleParams } from '../../../typings';
import { useKibana } from '../../../utils/kibana_react';
import {
createRemoteSloDeleteUrl,
createRemoteSloEditUrl,
} from '../../../utils/slo/remote_slo_urls';
export const useSloActions = ({
slo,
rules,
setIsEditRuleFlyoutOpen,
setIsActionsPopoverOpen,
}: {
slo?: SLOWithSummaryResponse;
rules?: Array<Rule<BurnRateRuleParams>>;
setIsEditRuleFlyoutOpen: (val: boolean) => void;
setIsActionsPopoverOpen: (val: boolean) => void;
}) => {
const {
share: {
url: { locators },
},
http,
} = useKibana().services;
const spaceId = useSpace();
if (!slo) {
return {
sloEditUrl: '',
handleNavigateToRules: () => {},
remoteDeleteUrl: undefined,
sloDetailsUrl: '',
};
}
const handleNavigateToRules = async () => {
if (rules?.length === 1) {
// if there is only one rule we can edit inline in flyout
setIsEditRuleFlyoutOpen(true);
setIsActionsPopoverOpen(false);
} else {
const locator = locators.get<RulesParams>(rulesLocatorID);
if (!locator) return undefined;
if (slo.remote && slo.remote.kibanaUrl !== '') {
const basePath = http.basePath.get(); // "/kibana/s/my-space"
const url = await locator.getUrl({ params: { sloId: slo.id } }); // "/kibana/s/my-space/app/rules/123"
// since basePath is already included in the locatorUrl, we need to remove it from the start of url
const urlWithoutBasePath = url?.replace(basePath, ''); // "/app/rules/123"
const spacePath = spaceId !== 'default' ? `/s/${spaceId}` : '';
const remoteUrl = new URL(path.join(spacePath, urlWithoutBasePath), slo.remote.kibanaUrl); // "kibanaUrl/s/my-space/app/rules/123"
window.open(remoteUrl, '_blank');
} else {
locator.navigate({ params: { sloId: slo.id } }, { replace: false });
}
}
};
const detailsUrl = paths.sloDetails(
slo.id,
![slo.groupBy].flat().includes(ALL_VALUE) && slo.instanceId ? slo.instanceId : undefined,
slo.remote?.remoteName
);
const remoteDeleteUrl = createRemoteSloDeleteUrl(slo, spaceId);
const sloEditUrl = slo.remote
? createRemoteSloEditUrl(slo, spaceId)
: http.basePath.prepend(paths.sloEdit(slo.id));
return {
sloEditUrl,
handleNavigateToRules,
remoteDeleteUrl,
sloDetailsUrl: http.basePath.prepend(detailsUrl),
};
};

View file

@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import { EuiNotificationBadge } from '@elastic/eui';
import { EuiNotificationBadge, EuiToolTip } from '@elastic/eui';
import React from 'react';
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { useFetchActiveAlerts } from '../../../hooks/use_fetch_active_alerts';
@ -18,8 +18,8 @@ export const useSloDetailsTabs = ({
selectedTabId,
setSelectedTabId,
}: {
isAutoRefreshing: boolean;
slo?: SLOWithSummaryResponse | null;
isAutoRefreshing: boolean;
selectedTabId: SloTabId;
setSelectedTabId: (val: SloTabId) => void;
}) => {
@ -28,6 +28,8 @@ export const useSloDetailsTabs = ({
shouldRefetch: isAutoRefreshing,
});
const isRemote = !!slo?.remote;
const tabs = [
{
id: OVERVIEW_TAB_ID,
@ -40,19 +42,34 @@ export const useSloDetailsTabs = ({
},
{
id: ALERTS_TAB_ID,
label: i18n.translate('xpack.slo.sloDetails.tab.alertsLabel', {
defaultMessage: 'Alerts',
}),
label: isRemote ? (
<EuiToolTip
content={i18n.translate('xpack.slo.sloDetails.tab.alertsDisabledTooltip', {
defaultMessage: 'Alerts are not available for remote SLOs',
})}
position="right"
>
<>{ALERTS_LABEL}</>
</EuiToolTip>
) : (
ALERTS_LABEL
),
'data-test-subj': 'alertsTab',
disabled: Boolean(isRemote),
isSelected: selectedTabId === ALERTS_TAB_ID,
append: slo ? (
<EuiNotificationBadge className="eui-alignCenter" size="m">
{(activeAlerts && activeAlerts.get(slo)) ?? 0}
</EuiNotificationBadge>
) : null,
append:
slo && !isRemote ? (
<EuiNotificationBadge className="eui-alignCenter" size="m">
{(activeAlerts && activeAlerts.get(slo)) ?? 0}
</EuiNotificationBadge>
) : null,
onClick: () => setSelectedTabId(ALERTS_TAB_ID),
},
];
return { tabs };
};
const ALERTS_LABEL = i18n.translate('xpack.slo.sloDetails.tab.alertsLabel', {
defaultMessage: 'Alerts',
});

View file

@ -35,7 +35,7 @@ import { HeaderControl } from './components/header_control';
import { paths } from '../../../common/locators/paths';
import type { SloDetailsPathParams } from './types';
import { AutoRefreshButton } from '../../components/slo/auto_refresh_button';
import { useGetInstanceIdQueryParam } from './hooks/use_get_instance_id_query_param';
import { useGetQueryParams } from './hooks/use_get_query_params';
import { useAutoRefreshStorage } from '../../components/slo/auto_refresh_button/hooks/use_auto_refresh_storage';
export function SloDetailsPage() {
@ -50,11 +50,12 @@ export function SloDetailsPage() {
const hasRightLicense = hasAtLeast('platinum');
const { sloId } = useParams<SloDetailsPathParams>();
const sloInstanceId = useGetInstanceIdQueryParam();
const { instanceId: sloInstanceId, remoteName } = useGetQueryParams();
const { storeAutoRefreshState, getAutoRefreshState } = useAutoRefreshStorage();
const [isAutoRefreshing, setIsAutoRefreshing] = useState(getAutoRefreshState());
const { isLoading, data: slo } = useFetchSloDetails({
sloId,
remoteName,
instanceId: sloInstanceId,
shouldRefetch: isAutoRefreshing,
});

View file

@ -18,7 +18,7 @@ import {
useGeneratedHtmlId,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TimeWindow } from '@kbn/slo-schema';
import { TimeWindowType } from '@kbn/slo-schema';
import React, { useEffect, useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { FormattedMessage } from '@kbn/i18n-react';
@ -46,7 +46,7 @@ export function SloEditFormObjectiveSection() {
const timeWindowType = watch('timeWindow.type');
const indicator = watch('indicator.type');
const [timeWindowTypeState, setTimeWindowTypeState] = useState<TimeWindow | undefined>(
const [timeWindowTypeState, setTimeWindowTypeState] = useState<TimeWindowType | undefined>(
defaultValues?.timeWindow?.type
);

View file

@ -17,7 +17,7 @@ import {
KQLCustomIndicator,
MetricCustomIndicator,
TimesliceMetricIndicator,
TimeWindow,
TimeWindowType,
} from '@kbn/slo-schema';
import {
BUDGETING_METHOD_OCCURRENCES,
@ -78,7 +78,7 @@ export const BUDGETING_METHOD_OPTIONS: Array<{ value: BudgetingMethod; text: str
},
];
export const TIMEWINDOW_TYPE_OPTIONS: Array<{ value: TimeWindow; text: string }> = [
export const TIMEWINDOW_TYPE_OPTIONS: Array<{ value: TimeWindowType; text: string }> = [
{
value: 'rolling',
text: i18n.translate('xpack.slo.sloEdit.timeWindow.rolling', {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { BudgetingMethod, Indicator, TimeWindow } from '@kbn/slo-schema';
import { BudgetingMethod, Indicator, TimeWindowType } from '@kbn/slo-schema';
export interface CreateSLOForm<IndicatorType = Indicator> {
name: string;
@ -13,7 +13,7 @@ export interface CreateSLOForm<IndicatorType = Indicator> {
indicator: IndicatorType;
timeWindow: {
duration: string;
type: TimeWindow;
type: TimeWindowType;
};
tags: string[];
budgetingMethod: BudgetingMethod;

View file

@ -4,19 +4,19 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiButton } from '@elastic/eui';
import { SLOResponse } from '@kbn/slo-schema';
import { SLODefinitionResponse } from '@kbn/slo-schema';
import React, { useState } from 'react';
import { SloDeleteConfirmationModal } from '../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal';
import { SloTimeWindowBadge } from '../slos/components/badges/slo_time_window_badge';
import { SloIndicatorTypeBadge } from '../slos/components/badges/slo_indicator_type_badge';
import { SloResetConfirmationModal } from '../../components/slo/reset_confirmation_modal/slo_reset_confirmation_modal';
import { useDeleteSlo } from '../../hooks/use_delete_slo';
import { useResetSlo } from '../../hooks/use_reset_slo';
import { SloResetConfirmationModal } from '../../components/slo/reset_confirmation_modal/slo_reset_confirmation_modal';
import { SloIndicatorTypeBadge } from '../slos/components/badges/slo_indicator_type_badge';
import { SloTimeWindowBadge } from '../slos/components/badges/slo_time_window_badge';
interface OutdatedSloProps {
slo: SLOResponse;
slo: SLODefinitionResponse;
onReset: () => void;
onDelete: () => void;
}

View file

@ -0,0 +1,162 @@
/*
* 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 { i18n } from '@kbn/i18n';
import {
EuiForm,
EuiFormRow,
EuiSwitch,
EuiDescribedFormGroup,
EuiComboBox,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiButton,
EuiSpacer,
} from '@elastic/eui';
import React, { useEffect, useState } from 'react';
import { useFetcher } from '@kbn/observability-shared-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { isEqual } from 'lodash';
import { useGetSettings } from './use_get_settings';
import { usePutSloSettings } from './use_put_slo_settings';
export function SettingsForm() {
const [useAllRemoteClusters, setUseAllRemoteClusters] = useState(false);
const [selectedRemoteClusters, setSelectedRemoteClusters] = useState<string[]>([]);
const { http } = useKibana().services;
const { data: currentSettings } = useGetSettings();
const { mutateAsync: updateSettings } = usePutSloSettings();
const { data, loading } = useFetcher(() => {
return http?.get<Array<{ name: string }>>('/api/remote_clusters');
}, [http]);
useEffect(() => {
if (currentSettings) {
setUseAllRemoteClusters(currentSettings.useAllRemoteClusters);
setSelectedRemoteClusters(currentSettings.selectedRemoteClusters);
}
}, [currentSettings]);
const onSubmit = async () => {
updateSettings({
settings: {
useAllRemoteClusters,
selectedRemoteClusters,
},
});
};
return (
<EuiForm component="form">
<EuiDescribedFormGroup
title={
<h3>
{i18n.translate('xpack.slo.settingsForm.h3.sourceSettingsLabel', {
defaultMessage: 'Source settings',
})}
</h3>
}
description={
<p>
{i18n.translate('xpack.slo.settingsForm.p.fetchSlosFromAllLabel', {
defaultMessage: 'Fetch SLOs from all remote clusters.',
})}
</p>
}
>
<EuiFormRow
label={i18n.translate('xpack.slo.settingsForm.euiFormRow.useAllRemoteClustersLabel', {
defaultMessage: 'Use all remote clusters',
})}
>
<EuiSwitch
label=""
checked={useAllRemoteClusters}
onChange={(evt) => setUseAllRemoteClusters(evt.target.checked)}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
<EuiDescribedFormGroup
title={
<h3>
{i18n.translate('xpack.slo.settingsForm.h3.remoteSettingsLabel', {
defaultMessage: 'Remote clusters',
})}
</h3>
}
description={
<p>
{i18n.translate('xpack.slo.settingsForm.select.fetchSlosFromAllLabel', {
defaultMessage: 'Select remote clusters to fetch SLOs from.',
})}
</p>
}
>
<EuiFormRow
label={i18n.translate(
'xpack.slo.settingsForm.euiFormRow.select.selectRemoteClustersLabel',
{ defaultMessage: 'Select remote clusters' }
)}
>
<EuiComboBox
options={data?.map((cluster) => ({ label: cluster.name, value: cluster.name })) || []}
selectedOptions={selectedRemoteClusters.map((cluster) => ({
label: cluster,
value: cluster,
}))}
onChange={(sels) => {
setSelectedRemoteClusters(sels.map((s) => s.value as string));
}}
isDisabled={useAllRemoteClusters}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
isLoading={loading}
data-test-subj="o11ySettingsFormCancelButton"
onClick={() => {
setUseAllRemoteClusters(currentSettings?.useAllRemoteClusters || false);
setSelectedRemoteClusters(currentSettings?.selectedRemoteClusters || []);
}}
isDisabled={isEqual(currentSettings, {
useAllRemoteClusters,
selectedRemoteClusters,
})}
>
{i18n.translate('xpack.slo.settingsForm.euiButtonEmpty.cancelLabel', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
isLoading={loading}
data-test-subj="o11ySettingsFormSaveButton"
color="primary"
fill
onClick={() => onSubmit()}
isDisabled={isEqual(currentSettings, {
useAllRemoteClusters,
selectedRemoteClusters,
})}
>
{i18n.translate('xpack.slo.settingsForm.applyButtonEmptyLabel', {
defaultMessage: 'Apply',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiDescribedFormGroup>
</EuiForm>
);
}

View file

@ -0,0 +1,45 @@
/*
* 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 { i18n } from '@kbn/i18n';
import React from 'react';
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
import { SettingsForm } from './settings_form';
import { useKibana } from '../../utils/kibana_react';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { paths } from '../../../common/locators/paths';
import { HeaderMenu } from '../../components/header_menu/header_menu';
export function SloSettingsPage() {
const {
http: { basePath },
} = useKibana().services;
const { ObservabilityPageTemplate } = usePluginContext();
useBreadcrumbs([
{
href: basePath.prepend(paths.slosSettings),
text: i18n.translate('xpack.slo.breadcrumbs.slosSettingsText', {
defaultMessage: 'SLOs Settings',
}),
},
]);
return (
<ObservabilityPageTemplate
data-test-subj="slosSettingsPage"
pageHeader={{
pageTitle: i18n.translate('xpack.slo.pageHeader.title.', {
defaultMessage: 'SLOs Settings',
}),
rightSideItems: [],
}}
>
<HeaderMenu />
<SettingsForm />
</ObservabilityPageTemplate>
);
}

View file

@ -0,0 +1,33 @@
/*
* 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 { GetSLOSettingsResponse } from '@kbn/slo-schema';
import { useQuery } from '@tanstack/react-query';
import { useKibana } from '../../utils/kibana_react';
export const useGetSettings = () => {
const { http } = useKibana().services;
const { isLoading, data } = useQuery({
queryKey: ['getSloSettings'],
queryFn: async ({ signal }) => {
try {
return http.get<GetSLOSettingsResponse>('/internal/slo/settings', { signal });
} catch (error) {
return defaultSettings;
}
},
keepPreviousData: true,
refetchOnWindowFocus: false,
});
return { isLoading, data };
};
const defaultSettings: GetSLOSettingsResponse = {
useAllRemoteClusters: false,
selectedRemoteClusters: [],
};

Some files were not shown because too many files have changed in this diff Show more