mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Profiling] New settings to control CO2 calculation (#166637)
- Added new Profiling settings so users can customize the CO2 variables
- Fixed Embeddable components to also read the new settings
- Moved code from APM to obs-shared to create the custom settings page
in Profiling.
- New Settings Page was created in Profiling UI so users can easily find
the settings:
<img width="2053" alt="Screenshot 2023-09-22 at 11 18 35"
src="6969b079
-745d-4302-8ff2-4f0f256c7f51">
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3550650a91
commit
12695646cf
41 changed files with 816 additions and 192 deletions
|
@ -557,6 +557,18 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
|
|||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'observability:profilingPerCoreWatt': {
|
||||
type: 'integer',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'observability:profilingCo2PerKWH': {
|
||||
type: 'integer',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'observability:profilingDatacenterPUE': {
|
||||
type: 'integer',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'observability:apmEnableCriticalPath': {
|
||||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
|
|
|
@ -155,4 +155,7 @@ export interface UsageStats {
|
|||
'securitySolution:enableGroupedNav': boolean;
|
||||
'securitySolution:showRelatedIntegrations': boolean;
|
||||
'visualization:visualize:legacyGaugeChartsLibrary': boolean;
|
||||
'observability:profilingPerCoreWatt': number;
|
||||
'observability:profilingCo2PerKWH': number;
|
||||
'observability:profilingDatacenterPUE': number;
|
||||
}
|
||||
|
|
|
@ -10025,6 +10025,24 @@
|
|||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:profilingPerCoreWatt": {
|
||||
"type": "integer",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:profilingCo2PerKWH": {
|
||||
"type": "integer",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:profilingDatacenterPUE": {
|
||||
"type": "integer",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:apmEnableCriticalPath": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
|
|
|
@ -23,8 +23,11 @@ import {
|
|||
} from '@kbn/observability-plugin/common';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React from 'react';
|
||||
import {
|
||||
useEditableSettings,
|
||||
useUiTracker,
|
||||
} from '@kbn/observability-shared-plugin/public';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useApmEditableSettings } from '../../../../hooks/use_apm_editable_settings';
|
||||
import { BottomBarActions } from '../bottom_bar_actions';
|
||||
|
||||
const apmSettingsKeys = [
|
||||
|
@ -42,6 +45,7 @@ const apmSettingsKeys = [
|
|||
];
|
||||
|
||||
export function GeneralSettings() {
|
||||
const trackApmEvent = useUiTracker({ app: 'apm' });
|
||||
const { docLinks, notifications } = useApmPluginContext().core;
|
||||
const {
|
||||
handleFieldChange,
|
||||
|
@ -50,14 +54,15 @@ export function GeneralSettings() {
|
|||
saveAll,
|
||||
isSaving,
|
||||
cleanUnsavedChanges,
|
||||
} = useApmEditableSettings(apmSettingsKeys);
|
||||
} = useEditableSettings('apm', apmSettingsKeys);
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
const reloadPage = Object.keys(unsavedChanges).some((key) => {
|
||||
return settingsEditableConfig[key].requiresPageReload;
|
||||
});
|
||||
await saveAll({ trackMetricName: 'general_settings_save' });
|
||||
await saveAll();
|
||||
trackApmEvent({ metric: 'general_settings_save' });
|
||||
if (reloadPage) {
|
||||
window.location.reload();
|
||||
}
|
||||
|
|
|
@ -24,8 +24,11 @@ import {
|
|||
import { LazyField } from '@kbn/advanced-settings-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import {
|
||||
useEditableSettings,
|
||||
useUiTracker,
|
||||
} from '@kbn/observability-shared-plugin/public';
|
||||
import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useApmEditableSettings } from '../../../../../hooks/use_apm_editable_settings';
|
||||
import { useFetcher, isPending } from '../../../../../hooks/use_fetcher';
|
||||
|
||||
interface Props {
|
||||
|
@ -33,6 +36,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export function LabsFlyout({ onClose }: Props) {
|
||||
const trackApmEvent = useUiTracker({ app: 'apm' });
|
||||
const { docLinks, notifications } = useApmPluginContext().core;
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
|
@ -48,7 +52,7 @@ export function LabsFlyout({ onClose }: Props) {
|
|||
saveAll,
|
||||
isSaving,
|
||||
cleanUnsavedChanges,
|
||||
} = useApmEditableSettings(labsItems);
|
||||
} = useEditableSettings('apm', labsItems);
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
|
@ -56,7 +60,8 @@ export function LabsFlyout({ onClose }: Props) {
|
|||
return settingsEditableConfig[key].requiresPageReload;
|
||||
});
|
||||
|
||||
await saveAll({ trackMetricName: 'labs_save' });
|
||||
await saveAll();
|
||||
trackApmEvent({ metric: 'labs_save' });
|
||||
|
||||
if (reloadPage) {
|
||||
window.location.reload();
|
||||
|
|
|
@ -41,6 +41,9 @@ export {
|
|||
enableCriticalPath,
|
||||
syntheticsThrottlingEnabled,
|
||||
apmEnableProfilingIntegration,
|
||||
profilingCo2PerKWH,
|
||||
profilingDatacenterPUE,
|
||||
profilingPerCoreWatt,
|
||||
} from './ui_settings_keys';
|
||||
|
||||
export {
|
||||
|
|
|
@ -27,3 +27,6 @@ export const apmEnableContinuousRollups = 'observability:apmEnableContinuousRoll
|
|||
export const syntheticsThrottlingEnabled = 'observability:syntheticsThrottlingEnabled';
|
||||
export const enableLegacyUptimeApp = 'observability:enableLegacyUptimeApp';
|
||||
export const apmEnableProfilingIntegration = 'observability:apmEnableProfilingIntegration';
|
||||
export const profilingPerCoreWatt = 'observability:profilingPerCoreWatt';
|
||||
export const profilingCo2PerKWH = 'observability:profilingCo2PerKWH';
|
||||
export const profilingDatacenterPUE = 'observability:profilingDatacenterPUE';
|
||||
|
|
|
@ -30,6 +30,9 @@ import {
|
|||
syntheticsThrottlingEnabled,
|
||||
enableLegacyUptimeApp,
|
||||
apmEnableProfilingIntegration,
|
||||
profilingCo2PerKWH,
|
||||
profilingDatacenterPUE,
|
||||
profilingPerCoreWatt,
|
||||
} from '../common/ui_settings_keys';
|
||||
|
||||
const betaLabel = i18n.translate('xpack.observability.uiSettings.betaLabel', {
|
||||
|
@ -374,6 +377,70 @@ export const uiSettings: Record<string, UiSettings> = {
|
|||
schema: schema.boolean(),
|
||||
requiresPageReload: false,
|
||||
},
|
||||
[profilingPerCoreWatt]: {
|
||||
category: [observabilityFeatureId],
|
||||
name: i18n.translate('xpack.observability.profilingPerCoreWattUiSettingName', {
|
||||
defaultMessage: 'Per Core Watts',
|
||||
}),
|
||||
value: 7,
|
||||
description: i18n.translate('xpack.observability.profilingPerCoreWattUiSettingDescription', {
|
||||
defaultMessage: `The average amortized per-core power consumption (based on 100% CPU utilization).`,
|
||||
}),
|
||||
schema: schema.number({ min: 0 }),
|
||||
requiresPageReload: false,
|
||||
},
|
||||
[profilingDatacenterPUE]: {
|
||||
category: [observabilityFeatureId],
|
||||
name: i18n.translate('xpack.observability.profilingDatacenterPUEUiSettingName', {
|
||||
defaultMessage: 'Data Center PUE',
|
||||
}),
|
||||
value: 1.7,
|
||||
description: i18n.translate('xpack.observability.profilingDatacenterPUEUiSettingDescription', {
|
||||
defaultMessage: `Data center power usage effectiveness (PUE) measures how efficiently a data center uses energy. Defaults to 1.7, the average on-premise data center PUE according to the {uptimeLink} survey
|
||||
</br></br>
|
||||
You can also use the PUE that corresponds with your cloud provider:
|
||||
<ul style="list-style-type: none;margin-left: 4px;">
|
||||
<li><strong>AWS:</strong> 1.135</li>
|
||||
<li><strong>GCP:</strong> 1.1</li>
|
||||
<li><strong>Azure:</strong> 1.185</li>
|
||||
</ul>
|
||||
`,
|
||||
values: {
|
||||
uptimeLink:
|
||||
'<a href="https://ela.st/uptimeinstitute" target="_blank" rel="noopener noreferrer">' +
|
||||
i18n.translate(
|
||||
'xpack.observability.profilingDatacenterPUEUiSettingDescription.uptimeLink',
|
||||
{ defaultMessage: 'Uptime Institute' }
|
||||
) +
|
||||
'</a>',
|
||||
},
|
||||
}),
|
||||
schema: schema.number({ min: 0 }),
|
||||
requiresPageReload: false,
|
||||
},
|
||||
[profilingCo2PerKWH]: {
|
||||
category: [observabilityFeatureId],
|
||||
name: i18n.translate('xpack.observability.profilingCo2PerKWHUiSettingName', {
|
||||
defaultMessage: 'Regional Carbon Intensity (ton/kWh)',
|
||||
}),
|
||||
value: 0.000379069,
|
||||
description: i18n.translate('xpack.observability.profilingCo2PerKWHUiSettingDescription', {
|
||||
defaultMessage: `Carbon intensity measures how clean your data center electricity is.
|
||||
Specifically, it measures the average amount of CO2 emitted per kilowatt-hour (kWh) of electricity consumed in a particular region.
|
||||
Use the cloud carbon footprint {datasheetLink} to update this value according to your region. Defaults to US East (N. Virginia).`,
|
||||
values: {
|
||||
datasheetLink:
|
||||
'<a href="https://ela.st/grid-datasheet" target="_blank" rel="noopener noreferrer">' +
|
||||
i18n.translate(
|
||||
'xpack.observability.profilingCo2PerKWHUiSettingDescription.datasheetLink',
|
||||
{ defaultMessage: 'datasheet' }
|
||||
) +
|
||||
'</a>',
|
||||
},
|
||||
}),
|
||||
schema: schema.number({ min: 0 }),
|
||||
requiresPageReload: false,
|
||||
},
|
||||
};
|
||||
|
||||
function throttlingDocsLink({ href }: { href: string }) {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"configPath": ["xpack", "observability_shared"],
|
||||
"requiredPlugins": ["cases", "guidedOnboarding", "uiActions", "embeddable", "share"],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": ["data", "inspector", "kibanaReact", "kibanaUtils"],
|
||||
"requiredBundles": ["data", "inspector", "kibanaReact", "kibanaUtils", "advancedSettings"],
|
||||
"extraPublicDirs": ["common"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { FieldState } from '@kbn/advanced-settings-plugin/public';
|
|||
import { toEditableConfig } from '@kbn/advanced-settings-plugin/public';
|
||||
import { IUiSettingsClient } from '@kbn/core/public';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useUiTracker } from '@kbn/observability-shared-plugin/public';
|
||||
import { ObservabilityApp } from '../../typings/common';
|
||||
|
||||
function getEditableConfig({
|
||||
settingsKeys,
|
||||
|
@ -41,15 +41,13 @@ function getEditableConfig({
|
|||
return config;
|
||||
}
|
||||
|
||||
export function useApmEditableSettings(settingsKeys: string[]) {
|
||||
export function useEditableSettings(app: ObservabilityApp, settingsKeys: string[]) {
|
||||
const { services } = useKibana();
|
||||
const trackApmEvent = useUiTracker({ app: 'apm' });
|
||||
|
||||
const { uiSettings } = services;
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [forceReloadSettings, setForceReloadSettings] = useState(0);
|
||||
const [unsavedChanges, setUnsavedChanges] = useState<
|
||||
Record<string, FieldState>
|
||||
>({});
|
||||
const [unsavedChanges, setUnsavedChanges] = useState<Record<string, FieldState>>({});
|
||||
|
||||
const settingsEditableConfig = useMemo(
|
||||
() => {
|
||||
|
@ -78,7 +76,7 @@ export function useApmEditableSettings(settingsKeys: string[]) {
|
|||
setUnsavedChanges({});
|
||||
}
|
||||
|
||||
async function saveAll({ trackMetricName }: { trackMetricName: string }) {
|
||||
async function saveAll() {
|
||||
if (uiSettings && !isEmpty(unsavedChanges)) {
|
||||
try {
|
||||
setIsSaving(true);
|
||||
|
@ -87,7 +85,6 @@ export function useApmEditableSettings(settingsKeys: string[]) {
|
|||
);
|
||||
|
||||
await Promise.all(arr);
|
||||
trackApmEvent({ metric: trackMetricName });
|
||||
setForceReloadSettings((state) => ++state);
|
||||
cleanUnsavedChanges();
|
||||
} finally {
|
|
@ -43,6 +43,7 @@ export type { AddInspectorRequest } from './contexts/inspector/inspector_context
|
|||
export { useInspectorContext } from './contexts/inspector/use_inspector_context';
|
||||
|
||||
export { useTheme } from './hooks/use_theme';
|
||||
export { useEditableSettings } from './hooks/use_editable_settings';
|
||||
export { useEsSearch, createEsParams } from './hooks/use_es_search';
|
||||
export { useFetcher, FETCH_STATUS } from './hooks/use_fetcher';
|
||||
export type { FetcherResult } from './hooks/use_fetcher';
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
"@kbn/shared-ux-router",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/profiling-utils",
|
||||
"@kbn/advanced-settings-plugin",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/share-plugin"
|
||||
],
|
||||
|
|
|
@ -1,85 +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.
|
||||
*/
|
||||
|
||||
const ANNUAL_SECONDS = 60 * 60 * 24 * 365;
|
||||
|
||||
// The assumed amortized per-core average power consumption (based on 100% CPU Utilization).
|
||||
// Reference: https://www.cloudcarbonfootprint.org/docs/methodology/#appendix-i-energy-coefficients
|
||||
const PER_CORE_WATT = 7;
|
||||
|
||||
// The assumed CO2 emissions in kg per kWh (the reference uses metric tons/kWh).
|
||||
// This value represents "regional carbon intensity" and it defaults to AWS us-east-1.
|
||||
// Reference: https://www.cloudcarbonfootprint.org/docs/methodology/#appendix-v-grid-emissions-factors
|
||||
const CO2_PER_KWH = 0.379069;
|
||||
|
||||
// The assumed PUE of the datacenter (1.7 is likely to be an on-prem value).
|
||||
const DATACENTER_PUE = 1.7;
|
||||
|
||||
// The cost of an x86 CPU core per hour, in US$.
|
||||
// (ARM is 60% less based graviton 3 data, see https://aws.amazon.com/ec2/graviton/)
|
||||
const CORE_COST_PER_HOUR = 0.0425;
|
||||
|
||||
export function calculateImpactEstimates({
|
||||
countInclusive,
|
||||
countExclusive,
|
||||
totalSamples,
|
||||
totalSeconds,
|
||||
}: {
|
||||
countInclusive: number;
|
||||
countExclusive: number;
|
||||
totalSamples: number;
|
||||
totalSeconds: number;
|
||||
}) {
|
||||
return {
|
||||
totalSamples: calculateImpact({
|
||||
samples: totalSamples,
|
||||
totalSamples,
|
||||
totalSeconds,
|
||||
}),
|
||||
totalCPU: calculateImpact({
|
||||
samples: countInclusive,
|
||||
totalSamples,
|
||||
totalSeconds,
|
||||
}),
|
||||
selfCPU: calculateImpact({
|
||||
samples: countExclusive,
|
||||
totalSamples,
|
||||
totalSeconds,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function calculateImpact({
|
||||
samples,
|
||||
totalSamples,
|
||||
totalSeconds,
|
||||
}: {
|
||||
samples: number;
|
||||
totalSamples: number;
|
||||
totalSeconds: number;
|
||||
}) {
|
||||
const annualizedScaleUp = ANNUAL_SECONDS / totalSeconds;
|
||||
const totalCoreSeconds = totalSamples / 20;
|
||||
const percentage = samples / totalSamples;
|
||||
const coreSeconds = totalCoreSeconds * percentage;
|
||||
const annualizedCoreSeconds = coreSeconds * annualizedScaleUp;
|
||||
const coreHours = coreSeconds / (60 * 60);
|
||||
const co2 = ((PER_CORE_WATT * coreHours) / 1000.0) * CO2_PER_KWH * DATACENTER_PUE;
|
||||
const annualizedCo2 = co2 * annualizedScaleUp;
|
||||
const dollarCost = coreHours * CORE_COST_PER_HOUR;
|
||||
const annualizedDollarCost = dollarCost * annualizedScaleUp;
|
||||
|
||||
return {
|
||||
percentage,
|
||||
coreSeconds,
|
||||
annualizedCoreSeconds,
|
||||
co2,
|
||||
annualizedCo2,
|
||||
dollarCost,
|
||||
annualizedDollarCost,
|
||||
};
|
||||
}
|
|
@ -172,6 +172,13 @@ describe('Differential Functions page', () => {
|
|||
'have.length.gt',
|
||||
1
|
||||
);
|
||||
cy.get(
|
||||
'[data-test-subj="topNFunctionsGrid"] [data-test-subj="profilingStackFrameSummaryLink"]'
|
||||
).contains('vmlinux');
|
||||
cy.get(
|
||||
'[data-test-subj="TopNFunctionsComparisonGrid"] [data-test-subj="profilingStackFrameSummaryLink"]'
|
||||
).contains('vmlinux');
|
||||
|
||||
cy.addKqlFilter({
|
||||
key: 'process.thread.name',
|
||||
value: '108795321966692',
|
||||
|
@ -183,38 +190,12 @@ describe('Differential Functions page', () => {
|
|||
});
|
||||
cy.wait('@getTopNFunctions');
|
||||
cy.wait('@getTopNFunctions');
|
||||
cy.get('[data-test-subj="topNFunctionsGrid"] .euiDataGridRow').should('have.length', 2);
|
||||
cy.get('[data-test-subj="TopNFunctionsComparisonGrid"] .euiDataGridRow').should(
|
||||
'have.length',
|
||||
1
|
||||
);
|
||||
[
|
||||
{ id: 'overallPerformance', value: '50.00%', icon: 'sortUp_success' },
|
||||
{
|
||||
id: 'annualizedCo2',
|
||||
value: '0.13 lbs / 0.06 kg',
|
||||
comparisonValue: '0.07 lbs / 0.03 kg (50.00%)',
|
||||
icon: 'comparison_sortUp_success',
|
||||
},
|
||||
{
|
||||
id: 'annualizedCost',
|
||||
value: '$1.24',
|
||||
comparisonValue: '$0.62 (50.00%)',
|
||||
icon: 'comparison_sortUp_success',
|
||||
},
|
||||
{
|
||||
id: 'totalNumberOfSamples',
|
||||
value: '2',
|
||||
comparisonValue: '1 (50.00%)',
|
||||
icon: 'comparison_sortUp_success',
|
||||
},
|
||||
].forEach((item) => {
|
||||
cy.get(`[data-test-subj="${item.id}_value"]`).contains(item.value);
|
||||
cy.get(`[data-test-subj="${item.id}_${item.icon}"]`).should('exist');
|
||||
if (item.comparisonValue) {
|
||||
cy.get(`[data-test-subj="${item.id}_comparison_value"]`).contains(item.comparisonValue);
|
||||
}
|
||||
});
|
||||
cy.get(
|
||||
'[data-test-subj="topNFunctionsGrid"] [data-test-subj="profilingStackFrameSummaryLink"]'
|
||||
).contains('libsystemd-shared-237.so');
|
||||
cy.get(
|
||||
'[data-test-subj="TopNFunctionsComparisonGrid"] [data-test-subj="profilingStackFrameSummaryLink"]'
|
||||
).contains('libjvm.so');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import {
|
||||
profilingCo2PerKWH,
|
||||
profilingDatacenterPUE,
|
||||
profilingPerCoreWatt,
|
||||
} from '@kbn/observability-plugin/common';
|
||||
|
||||
describe('Functions page', () => {
|
||||
const rangeFrom = '2023-04-18T00:00:00.000Z';
|
||||
|
@ -80,11 +85,10 @@ describe('Functions page', () => {
|
|||
cy.intercept('GET', '/internal/profiling/topn/functions?*').as('getTopNFunctions');
|
||||
cy.visitKibana('/app/profiling/functions', { rangeFrom, rangeTo });
|
||||
cy.wait('@getTopNFunctions');
|
||||
cy.get('.euiDataGridRow').should('have.length.gt', 1);
|
||||
const firstRowSelector = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"]';
|
||||
cy.get(firstRowSelector).eq(2).contains('vmlinux');
|
||||
cy.addKqlFilter({ key: 'Stacktrace.id', value: '-7DvnP1mizQYw8mIIpgbMg' });
|
||||
cy.wait('@getTopNFunctions');
|
||||
cy.get('.euiDataGridRow').should('have.length', 1);
|
||||
const firstRowSelector = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"]';
|
||||
cy.get(firstRowSelector).eq(2).contains('libjvm.so');
|
||||
});
|
||||
|
||||
|
@ -96,50 +100,50 @@ describe('Functions page', () => {
|
|||
{
|
||||
columnKey: 'rank',
|
||||
columnIndex: 1,
|
||||
highRank: 388,
|
||||
highRank: 4481,
|
||||
lowRank: 1,
|
||||
highValue: 388,
|
||||
highValue: 4481,
|
||||
lowValue: 1,
|
||||
},
|
||||
{
|
||||
columnKey: 'samples',
|
||||
columnIndex: 7,
|
||||
highRank: 1,
|
||||
lowRank: 44,
|
||||
lowRank: 389,
|
||||
highValue: 28,
|
||||
lowValue: 1,
|
||||
lowValue: 0,
|
||||
},
|
||||
{
|
||||
columnKey: 'selfCPU',
|
||||
columnIndex: 3,
|
||||
highRank: 1,
|
||||
lowRank: 44,
|
||||
lowRank: 389,
|
||||
highValue: '5.46%',
|
||||
lowValue: '0.19%',
|
||||
lowValue: '0.00%',
|
||||
},
|
||||
{
|
||||
columnKey: 'totalCPU',
|
||||
columnIndex: 4,
|
||||
highRank: 338,
|
||||
highRank: 3623,
|
||||
lowRank: 44,
|
||||
highValue: '10.33%',
|
||||
highValue: '60.43%',
|
||||
lowValue: '0.19%',
|
||||
},
|
||||
{
|
||||
columnKey: 'annualizedCo2',
|
||||
columnIndex: 5,
|
||||
highRank: 1,
|
||||
lowRank: 44,
|
||||
lowRank: 389,
|
||||
highValue: '1.84 lbs / 0.84 kg',
|
||||
lowValue: '0.07 lbs / 0.03 kg',
|
||||
lowValue: undefined,
|
||||
},
|
||||
{
|
||||
columnKey: 'annualizedDollarCost',
|
||||
columnIndex: 6,
|
||||
highRank: 1,
|
||||
lowRank: 44,
|
||||
lowRank: 389,
|
||||
highValue: '$17.37',
|
||||
lowValue: '$0.62',
|
||||
lowValue: undefined,
|
||||
},
|
||||
].forEach(({ columnKey, columnIndex, highRank, highValue, lowRank, lowValue }) => {
|
||||
cy.get(`[data-test-subj="dataGridHeaderCell-${columnKey}"]`).click();
|
||||
|
@ -151,7 +155,11 @@ describe('Functions page', () => {
|
|||
cy.get(`[data-test-subj="dataGridHeaderCell-${columnKey}"]`).click();
|
||||
cy.contains('Sort Low-High').click();
|
||||
cy.get(firstRowSelector).eq(1).contains(lowRank);
|
||||
cy.get(firstRowSelector).eq(columnIndex).contains(lowValue);
|
||||
if (lowValue !== undefined) {
|
||||
cy.get(firstRowSelector).eq(columnIndex).contains(lowValue);
|
||||
} else {
|
||||
cy.get(firstRowSelector).eq(columnIndex).should('not.have.value');
|
||||
}
|
||||
});
|
||||
|
||||
cy.get(`[data-test-subj="dataGridHeaderCell-frame"]`).click();
|
||||
|
@ -165,4 +173,58 @@ describe('Functions page', () => {
|
|||
cy.get(firstRowSelector).eq(1).contains('371');
|
||||
cy.get(firstRowSelector).eq(2).contains('/');
|
||||
});
|
||||
|
||||
describe('Test changing CO2 settings', () => {
|
||||
afterEach(() => {
|
||||
cy.updateAdvancedSettings({
|
||||
[profilingCo2PerKWH]: 0.000379069,
|
||||
[profilingDatacenterPUE]: 1.7,
|
||||
[profilingPerCoreWatt]: 7,
|
||||
});
|
||||
});
|
||||
|
||||
it('changes CO2 settings and validate values in the table', () => {
|
||||
cy.intercept('GET', '/internal/profiling/topn/functions?*').as('getTopNFunctions');
|
||||
cy.visitKibana('/app/profiling/functions', { rangeFrom, rangeTo });
|
||||
cy.wait('@getTopNFunctions');
|
||||
const firstRowSelector = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"]';
|
||||
cy.get(firstRowSelector).eq(1).contains('1');
|
||||
cy.get(firstRowSelector).eq(2).contains('vmlinux');
|
||||
cy.get(firstRowSelector).eq(5).contains('1.84 lbs / 0.84 kg');
|
||||
cy.contains('Settings').click();
|
||||
cy.contains('Advanced Settings');
|
||||
cy.get(`[data-test-subj="advancedSetting-editField-${profilingCo2PerKWH}"]`)
|
||||
.clear()
|
||||
.type('0.12345');
|
||||
cy.get(`[data-test-subj="advancedSetting-editField-${profilingDatacenterPUE}"]`)
|
||||
.clear()
|
||||
.type('2.4');
|
||||
cy.get(`[data-test-subj="advancedSetting-editField-${profilingPerCoreWatt}"]`)
|
||||
.clear()
|
||||
.type('20');
|
||||
cy.contains('Save changes').click();
|
||||
cy.go('back');
|
||||
cy.wait('@getTopNFunctions');
|
||||
cy.get(firstRowSelector).eq(5).contains('24.22k lbs / 10.99k');
|
||||
const firstRowSelectorActionButton =
|
||||
'[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"] .euiButtonIcon';
|
||||
cy.get(firstRowSelectorActionButton).click();
|
||||
[
|
||||
{ parentKey: 'impactEstimates', key: 'co2Emission', value: '0.02 lbs / 0.01 kg' },
|
||||
{ parentKey: 'impactEstimates', key: 'selfCo2Emission', value: '0.02 lbs / 0.01 kg' },
|
||||
{
|
||||
parentKey: 'impactEstimates',
|
||||
key: 'annualizedCo2Emission',
|
||||
value: '24.22k lbs / 10.99k kg',
|
||||
},
|
||||
{
|
||||
parentKey: 'impactEstimates',
|
||||
key: 'annualizedSelfCo2Emission',
|
||||
value: '24.22k lbs / 10.99k kg',
|
||||
},
|
||||
].forEach(({ parentKey, key, value }) => {
|
||||
cy.get(`[data-test-subj="${parentKey}_${key}"]`).contains(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* 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 {
|
||||
profilingCo2PerKWH,
|
||||
profilingDatacenterPUE,
|
||||
profilingPerCoreWatt,
|
||||
} from '@kbn/observability-plugin/common';
|
||||
|
||||
describe('Settings page', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAsElastic();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.updateAdvancedSettings({
|
||||
[profilingCo2PerKWH]: 0.000379069,
|
||||
[profilingDatacenterPUE]: 1.7,
|
||||
[profilingPerCoreWatt]: 7,
|
||||
});
|
||||
});
|
||||
|
||||
it('opens setting page', () => {
|
||||
cy.visitKibana('/app/profiling/settings');
|
||||
cy.contains('Advanced Settings');
|
||||
cy.contains('CO2');
|
||||
cy.contains('Regional Carbon Intensity (ton/kWh)');
|
||||
cy.contains('Data Center PUE');
|
||||
cy.contains('Per Core Watts');
|
||||
});
|
||||
|
||||
it('updates values', () => {
|
||||
cy.visitKibana('/app/profiling/settings');
|
||||
cy.contains('Advanced Settings');
|
||||
cy.get('[data-test-subj="profilingBottomBarActions"]').should('not.exist');
|
||||
cy.get(`[data-test-subj="advancedSetting-editField-${profilingCo2PerKWH}"]`)
|
||||
.clear()
|
||||
.type('0.12345');
|
||||
cy.get(`[data-test-subj="advancedSetting-editField-${profilingDatacenterPUE}"]`)
|
||||
.clear()
|
||||
.type('2.4');
|
||||
cy.get(`[data-test-subj="advancedSetting-editField-${profilingPerCoreWatt}"]`)
|
||||
.clear()
|
||||
.type('20');
|
||||
cy.get('[data-test-subj="profilingBottomBarActions"]').should('exist');
|
||||
cy.contains('Save changes').click();
|
||||
cy.get('[data-test-subj="profilingBottomBarActions"]').should('not.exist');
|
||||
});
|
||||
});
|
|
@ -71,3 +71,17 @@ Cypress.Commands.add(
|
|||
cy.getByTestSubj(dataTestSubj).type('{enter}');
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add('updateAdvancedSettings', (settings: Record<string, unknown>) => {
|
||||
const kibanaUrl = Cypress.env('KIBANA_URL');
|
||||
cy.request({
|
||||
log: false,
|
||||
method: 'POST',
|
||||
url: `${kibanaUrl}/internal/kibana/settings`,
|
||||
body: { changes: settings },
|
||||
headers: {
|
||||
'kbn-xsrf': 'e2e_test',
|
||||
},
|
||||
auth: { user: 'elastic', pass: 'changeme' },
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,5 +27,6 @@ declare namespace Cypress {
|
|||
dataTestSubj?: 'profilingUnifiedSearchBar' | 'profilingComparisonUnifiedSearchBar';
|
||||
waitForSuggestion?: boolean;
|
||||
}): void;
|
||||
updateAdvancedSettings(settings: Record<string, unknown>): void;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,5 +22,6 @@
|
|||
"@kbn/test",
|
||||
"@kbn/dev-utils",
|
||||
"@kbn/cypress-config",
|
||||
"@kbn/observability-plugin",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -30,7 +30,8 @@
|
|||
"requiredBundles": [
|
||||
"kibanaReact",
|
||||
"kibanaUtils",
|
||||
"observabilityAIAssistant"
|
||||
"observabilityAIAssistant",
|
||||
"advancedSettings"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,12 +18,12 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { isNumber } from 'lodash';
|
||||
import React from 'react';
|
||||
import { calculateImpactEstimates } from '../../../common/calculate_impact_estimates';
|
||||
import { asCost } from '../../utils/formatters/as_cost';
|
||||
import { asPercentage } from '../../utils/formatters/as_percentage';
|
||||
import { asWeight } from '../../utils/formatters/as_weight';
|
||||
import { CPULabelWithHint } from '../cpu_label_with_hint';
|
||||
import { TooltipRow } from './tooltip_row';
|
||||
import { useCalculateImpactEstimate } from '../../hooks/use_calculate_impact_estimates';
|
||||
|
||||
interface Props {
|
||||
isRoot: boolean;
|
||||
|
@ -57,6 +57,7 @@ export function FlameGraphTooltip({
|
|||
onShowMoreClick,
|
||||
}: Props) {
|
||||
const theme = useEuiTheme();
|
||||
const calculateImpactEstimates = useCalculateImpactEstimate();
|
||||
|
||||
const impactEstimates = calculateImpactEstimates({
|
||||
countExclusive,
|
||||
|
|
|
@ -194,7 +194,6 @@ export function FlameGraph({
|
|||
frame={selected}
|
||||
totalSeconds={primaryFlamegraph?.TotalSeconds ?? 0}
|
||||
totalSamples={totalSamples}
|
||||
showAIAssistant={!isEmbedded}
|
||||
showSymbolsStatus={!isEmbedded}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -7,24 +7,26 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { calculateImpactEstimates } from '../../../common/calculate_impact_estimates';
|
||||
import { asCost } from '../../utils/formatters/as_cost';
|
||||
import { asDuration } from '../../utils/formatters/as_duration';
|
||||
import { asNumber } from '../../utils/formatters/as_number';
|
||||
import { asPercentage } from '../../utils/formatters/as_percentage';
|
||||
import { asWeight } from '../../utils/formatters/as_weight';
|
||||
import { CPULabelWithHint } from '../cpu_label_with_hint';
|
||||
import { CalculateImpactEstimates } from '../../hooks/use_calculate_impact_estimates';
|
||||
|
||||
export function getImpactRows({
|
||||
countInclusive,
|
||||
countExclusive,
|
||||
totalSamples,
|
||||
totalSeconds,
|
||||
calculateImpactEstimates,
|
||||
}: {
|
||||
countInclusive: number;
|
||||
countExclusive: number;
|
||||
totalSamples: number;
|
||||
totalSeconds: number;
|
||||
calculateImpactEstimates: CalculateImpactEstimates;
|
||||
}) {
|
||||
const { selfCPU, totalCPU } = calculateImpactEstimates({
|
||||
countInclusive,
|
||||
|
|
|
@ -14,6 +14,7 @@ import { getImpactRows } from './get_impact_rows';
|
|||
import { getInformationRows } from './get_information_rows';
|
||||
import { KeyValueList } from './key_value_list';
|
||||
import { MissingSymbolsCallout } from './missing_symbols_callout';
|
||||
import { useCalculateImpactEstimate } from '../../hooks/use_calculate_impact_estimates';
|
||||
|
||||
export interface Frame {
|
||||
fileID: string;
|
||||
|
@ -39,9 +40,10 @@ export function FrameInformationWindow({
|
|||
frame,
|
||||
totalSamples,
|
||||
totalSeconds,
|
||||
showAIAssistant = true,
|
||||
showSymbolsStatus = true,
|
||||
}: Props) {
|
||||
const calculateImpactEstimates = useCalculateImpactEstimate();
|
||||
|
||||
if (!frame) {
|
||||
return (
|
||||
<FrameInformationPanel>
|
||||
|
@ -87,6 +89,7 @@ export function FrameInformationWindow({
|
|||
countExclusive,
|
||||
totalSamples,
|
||||
totalSeconds,
|
||||
calculateImpactEstimates,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -95,11 +98,9 @@ export function FrameInformationWindow({
|
|||
<EuiFlexItem>
|
||||
<KeyValueList data-test-subj="informationRows" rows={informationRows} />
|
||||
</EuiFlexItem>
|
||||
{showAIAssistant ? (
|
||||
<EuiFlexItem>
|
||||
<FrameInformationAIAssistant frame={frame} />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem>
|
||||
<FrameInformationAIAssistant frame={frame} />
|
||||
</EuiFlexItem>
|
||||
{showSymbolsStatus && symbolStatus !== FrameSymbolStatus.SYMBOLIZED ? (
|
||||
<EuiFlexItem>
|
||||
<MissingSymbolsCallout frameType={frame.frameType} />
|
||||
|
|
|
@ -76,6 +76,11 @@ export function ProfilingHeaderActionMenu() {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiHeaderLink>
|
||||
<EuiHeaderLink href={router.link('/settings')} color="text">
|
||||
{i18n.translate('xpack.profiling.headerActionMenu.settings', {
|
||||
defaultMessage: 'Settings',
|
||||
})}
|
||||
</EuiHeaderLink>
|
||||
<ObservabilityAIAssistantActionMenuItem />
|
||||
</EuiHeaderLinks>
|
||||
);
|
||||
|
|
|
@ -26,6 +26,7 @@ import { FrameInformationTooltip } from '../frame_information_window/frame_infor
|
|||
import { LabelWithHint } from '../label_with_hint';
|
||||
import { FunctionRow } from './function_row';
|
||||
import { getFunctionsRows, IFunctionRow } from './utils';
|
||||
import { useCalculateImpactEstimate } from '../../hooks/use_calculate_impact_estimates';
|
||||
|
||||
interface Props {
|
||||
topNFunctions?: TopNFunctions;
|
||||
|
@ -70,6 +71,7 @@ export const TopNFunctionsGrid = forwardRef(
|
|||
) => {
|
||||
const [selectedRow, setSelectedRow] = useState<IFunctionRow | undefined>();
|
||||
const trackProfilingEvent = useUiTracker({ app: 'profiling' });
|
||||
const calculateImpactEstimates = useCalculateImpactEstimate();
|
||||
|
||||
function onSort(newSortingColumns: EuiDataGridSorting['columns']) {
|
||||
const lastItem = last(newSortingColumns);
|
||||
|
@ -93,9 +95,11 @@ export const TopNFunctionsGrid = forwardRef(
|
|||
comparisonTopNFunctions,
|
||||
topNFunctions,
|
||||
totalSeconds,
|
||||
calculateImpactEstimates,
|
||||
});
|
||||
}, [
|
||||
baselineScaleFactor,
|
||||
calculateImpactEstimates,
|
||||
comparisonScaleFactor,
|
||||
comparisonTopNFunctions,
|
||||
topNFunctions,
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
*/
|
||||
import { keyBy } from 'lodash';
|
||||
import type { StackFrameMetadata, TopNFunctions } from '@kbn/profiling-utils';
|
||||
import { calculateImpactEstimates } from '../../../common/calculate_impact_estimates';
|
||||
import {
|
||||
CalculateImpactEstimates,
|
||||
ImpactEstimates,
|
||||
} from '../../hooks/use_calculate_impact_estimates';
|
||||
|
||||
export function getColorLabel(percent: number) {
|
||||
if (percent === 0) {
|
||||
|
@ -37,7 +40,7 @@ export interface IFunctionRow {
|
|||
totalCPU: number;
|
||||
selfCPUPerc: number;
|
||||
totalCPUPerc: number;
|
||||
impactEstimates?: ReturnType<typeof calculateImpactEstimates>;
|
||||
impactEstimates?: ImpactEstimates;
|
||||
diff?: {
|
||||
rank: number;
|
||||
samples: number;
|
||||
|
@ -45,7 +48,7 @@ export interface IFunctionRow {
|
|||
totalCPU: number;
|
||||
selfCPUPerc: number;
|
||||
totalCPUPerc: number;
|
||||
impactEstimates?: ReturnType<typeof calculateImpactEstimates>;
|
||||
impactEstimates?: ImpactEstimates;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -55,12 +58,14 @@ export function getFunctionsRows({
|
|||
comparisonTopNFunctions,
|
||||
topNFunctions,
|
||||
totalSeconds,
|
||||
calculateImpactEstimates,
|
||||
}: {
|
||||
baselineScaleFactor?: number;
|
||||
comparisonScaleFactor?: number;
|
||||
comparisonTopNFunctions?: TopNFunctions;
|
||||
topNFunctions?: TopNFunctions;
|
||||
totalSeconds: number;
|
||||
calculateImpactEstimates: CalculateImpactEstimates;
|
||||
}): IFunctionRow[] {
|
||||
if (!topNFunctions || !topNFunctions.TotalCount || topNFunctions.TotalCount === 0) {
|
||||
return [];
|
||||
|
@ -70,7 +75,7 @@ export function getFunctionsRows({
|
|||
? keyBy(comparisonTopNFunctions.TopN, 'Id')
|
||||
: {};
|
||||
|
||||
return topNFunctions.TopN.filter((topN) => topN.CountExclusive > 0).map((topN, i) => {
|
||||
return topNFunctions.TopN.filter((topN) => topN.CountExclusive >= 0).map((topN, i) => {
|
||||
const comparisonRow = comparisonDataById?.[topN.Id];
|
||||
|
||||
const scaledSelfCPU = scaleValue({
|
||||
|
|
|
@ -9,11 +9,11 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { TopNFunctions } from '@kbn/profiling-utils';
|
||||
import { calculateImpactEstimates } from '../../../common/calculate_impact_estimates';
|
||||
import { asCost } from '../../utils/formatters/as_cost';
|
||||
import { asWeight } from '../../utils/formatters/as_weight';
|
||||
import { calculateBaseComparisonDiff } from '../topn_functions/utils';
|
||||
import { SummaryItem } from './summary_item';
|
||||
import { useCalculateImpactEstimate } from '../../hooks/use_calculate_impact_estimates';
|
||||
|
||||
interface Props {
|
||||
baselineTopNFunctions?: TopNFunctions;
|
||||
|
@ -38,6 +38,8 @@ export function TopNFunctionsSummary({
|
|||
baselineDuration,
|
||||
comparisonDuration,
|
||||
}: Props) {
|
||||
const calculateImpactEstimates = useCalculateImpactEstimate();
|
||||
|
||||
const baselineScaledTotalSamples = baselineTopNFunctions
|
||||
? baselineTopNFunctions.TotalCount * baselineScaleFactor
|
||||
: 0;
|
||||
|
@ -87,6 +89,7 @@ export function TopNFunctionsSummary({
|
|||
baselineDuration,
|
||||
baselineScaledTotalSamples,
|
||||
baselineTopNFunctions,
|
||||
calculateImpactEstimates,
|
||||
comparisonDuration,
|
||||
comparisonScaledTotalSamples,
|
||||
comparisonTopNFunctions,
|
||||
|
|
|
@ -4,14 +4,18 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { Embeddable, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
|
||||
import { Embeddable, EmbeddableOutput, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
import { EMBEDDABLE_FLAMEGRAPH } from '@kbn/observability-shared-plugin/public';
|
||||
import { createFlameGraph } from '@kbn/profiling-utils';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { createFlameGraph } from '@kbn/profiling-utils';
|
||||
import { FlameGraph } from '../../components/flamegraph';
|
||||
import { EmbeddableFlamegraphEmbeddableInput } from './embeddable_flamegraph_factory';
|
||||
import { AsyncEmbeddableComponent } from '../async_embeddable_component';
|
||||
import {
|
||||
ProfilingEmbeddableProvider,
|
||||
ProfilingEmbeddablesDependencies,
|
||||
} from '../profiling_embeddable_provider';
|
||||
import { EmbeddableFlamegraphEmbeddableInput } from './embeddable_flamegraph_factory';
|
||||
|
||||
export class EmbeddableFlamegraph extends Embeddable<
|
||||
EmbeddableFlamegraphEmbeddableInput,
|
||||
|
@ -20,18 +24,29 @@ export class EmbeddableFlamegraph extends Embeddable<
|
|||
readonly type = EMBEDDABLE_FLAMEGRAPH;
|
||||
private _domNode?: HTMLElement;
|
||||
|
||||
constructor(
|
||||
private deps: ProfilingEmbeddablesDependencies,
|
||||
initialInput: EmbeddableFlamegraphEmbeddableInput,
|
||||
parent?: IContainer
|
||||
) {
|
||||
super(initialInput, {}, parent);
|
||||
}
|
||||
|
||||
render(domNode: HTMLElement) {
|
||||
this._domNode = domNode;
|
||||
const { data, isLoading } = this.input;
|
||||
const flamegraph = !isLoading && data ? createFlameGraph(data) : undefined;
|
||||
|
||||
render(
|
||||
<AsyncEmbeddableComponent isLoading={isLoading}>
|
||||
<>
|
||||
{flamegraph && (
|
||||
<FlameGraph primaryFlamegraph={flamegraph} id="embddable_profiling" isEmbedded />
|
||||
)}
|
||||
</>
|
||||
</AsyncEmbeddableComponent>,
|
||||
<ProfilingEmbeddableProvider deps={this.deps}>
|
||||
<AsyncEmbeddableComponent isLoading={isLoading}>
|
||||
<>
|
||||
{flamegraph && (
|
||||
<FlameGraph primaryFlamegraph={flamegraph} id="embddable_profiling" isEmbedded />
|
||||
)}
|
||||
</>
|
||||
</AsyncEmbeddableComponent>
|
||||
</ProfilingEmbeddableProvider>,
|
||||
domNode
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import {
|
||||
IContainer,
|
||||
EmbeddableInput,
|
||||
EmbeddableFactoryDefinition,
|
||||
EmbeddableInput,
|
||||
IContainer,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import type { BaseFlameGraph } from '@kbn/profiling-utils';
|
||||
import { EMBEDDABLE_FLAMEGRAPH } from '@kbn/observability-shared-plugin/public';
|
||||
import type { BaseFlameGraph } from '@kbn/profiling-utils';
|
||||
import type { GetProfilingEmbeddableDependencies } from '../profiling_embeddable_provider';
|
||||
|
||||
interface EmbeddableFlamegraphInput {
|
||||
data?: BaseFlameGraph;
|
||||
|
@ -24,13 +25,16 @@ export class EmbeddableFlamegraphFactory
|
|||
{
|
||||
readonly type = EMBEDDABLE_FLAMEGRAPH;
|
||||
|
||||
constructor(private getProfilingEmbeddableDependencies: GetProfilingEmbeddableDependencies) {}
|
||||
|
||||
async isEditable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async create(input: EmbeddableFlamegraphEmbeddableInput, parent?: IContainer) {
|
||||
const { EmbeddableFlamegraph } = await import('./embeddable_flamegraph');
|
||||
return new EmbeddableFlamegraph(input, {}, parent);
|
||||
const deps = await this.getProfilingEmbeddableDependencies();
|
||||
return new EmbeddableFlamegraph(deps, input, parent);
|
||||
}
|
||||
|
||||
getDisplayName() {
|
||||
|
|
|
@ -4,13 +4,17 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { Embeddable, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
|
||||
import { Embeddable, EmbeddableOutput, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
import { EMBEDDABLE_FUNCTIONS } from '@kbn/observability-shared-plugin/public';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { AsyncEmbeddableComponent } from '../async_embeddable_component';
|
||||
import { EmbeddableFunctionsEmbeddableInput } from './embeddable_functions_factory';
|
||||
import { EmbeddableFunctionsGrid } from './embeddable_functions_grid';
|
||||
import {
|
||||
ProfilingEmbeddableProvider,
|
||||
ProfilingEmbeddablesDependencies,
|
||||
} from '../profiling_embeddable_provider';
|
||||
|
||||
export class EmbeddableFunctions extends Embeddable<
|
||||
EmbeddableFunctionsEmbeddableInput,
|
||||
|
@ -19,16 +23,26 @@ export class EmbeddableFunctions extends Embeddable<
|
|||
readonly type = EMBEDDABLE_FUNCTIONS;
|
||||
private _domNode?: HTMLElement;
|
||||
|
||||
constructor(
|
||||
private deps: ProfilingEmbeddablesDependencies,
|
||||
initialInput: EmbeddableFunctionsEmbeddableInput,
|
||||
parent?: IContainer
|
||||
) {
|
||||
super(initialInput, {}, parent);
|
||||
}
|
||||
|
||||
render(domNode: HTMLElement) {
|
||||
this._domNode = domNode;
|
||||
const { data, isLoading, rangeFrom, rangeTo } = this.input;
|
||||
const totalSeconds = (rangeTo - rangeFrom) / 1000;
|
||||
render(
|
||||
<AsyncEmbeddableComponent isLoading={isLoading}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<EmbeddableFunctionsGrid data={data} totalSeconds={totalSeconds} />
|
||||
</div>
|
||||
</AsyncEmbeddableComponent>,
|
||||
<ProfilingEmbeddableProvider deps={this.deps}>
|
||||
<AsyncEmbeddableComponent isLoading={isLoading}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<EmbeddableFunctionsGrid data={data} totalSeconds={totalSeconds} />
|
||||
</div>
|
||||
</AsyncEmbeddableComponent>
|
||||
</ProfilingEmbeddableProvider>,
|
||||
domNode
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from '@kbn/embeddable-plugin/public';
|
||||
import { EMBEDDABLE_FUNCTIONS } from '@kbn/observability-shared-plugin/public';
|
||||
import type { TopNFunctions } from '@kbn/profiling-utils';
|
||||
import { GetProfilingEmbeddableDependencies } from '../profiling_embeddable_provider';
|
||||
|
||||
interface EmbeddableFunctionsInput {
|
||||
data?: TopNFunctions;
|
||||
|
@ -26,13 +27,16 @@ export class EmbeddableFunctionsFactory
|
|||
{
|
||||
readonly type = EMBEDDABLE_FUNCTIONS;
|
||||
|
||||
constructor(private getProfilingEmbeddableDependencies: GetProfilingEmbeddableDependencies) {}
|
||||
|
||||
async isEditable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async create(input: EmbeddableFunctionsEmbeddableInput, parent?: IContainer) {
|
||||
const { EmbeddableFunctions } = await import('./embeddable_functions');
|
||||
return new EmbeddableFunctions(input, {}, parent);
|
||||
const deps = await this.getProfilingEmbeddableDependencies();
|
||||
return new EmbeddableFunctions(deps, input, parent);
|
||||
}
|
||||
|
||||
getDisplayName() {
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { ObservabilityAIAssistantProvider } from '@kbn/observability-ai-assistant-plugin/public';
|
||||
import React, { ReactChild, useMemo } from 'react';
|
||||
import { CoreSetup, CoreStart } from '@kbn/core/public';
|
||||
import { ProfilingDependenciesContextProvider } from '../components/contexts/profiling_dependencies/profiling_dependencies_context';
|
||||
import { ProfilingPluginPublicSetupDeps, ProfilingPluginPublicStartDeps } from '../types';
|
||||
import { Services } from '../services';
|
||||
|
||||
export interface ProfilingEmbeddablesDependencies {
|
||||
coreStart: CoreStart;
|
||||
coreSetup: CoreSetup;
|
||||
pluginsStart: ProfilingPluginPublicStartDeps;
|
||||
pluginsSetup: ProfilingPluginPublicSetupDeps;
|
||||
profilingFetchServices: Services;
|
||||
}
|
||||
|
||||
export type GetProfilingEmbeddableDependencies = () => Promise<ProfilingEmbeddablesDependencies>;
|
||||
|
||||
interface Props {
|
||||
deps: ProfilingEmbeddablesDependencies;
|
||||
children: ReactChild;
|
||||
}
|
||||
|
||||
export function ProfilingEmbeddableProvider({ deps, children }: Props) {
|
||||
const profilingDependencies = useMemo(
|
||||
() => ({
|
||||
start: {
|
||||
core: deps.coreStart,
|
||||
...deps.pluginsStart,
|
||||
},
|
||||
setup: {
|
||||
core: deps.coreSetup,
|
||||
...deps.pluginsSetup,
|
||||
},
|
||||
services: deps.profilingFetchServices,
|
||||
}),
|
||||
[deps]
|
||||
);
|
||||
|
||||
return (
|
||||
<KibanaContextProvider services={{ ...deps.coreStart, ...deps.pluginsStart }}>
|
||||
<ProfilingDependenciesContextProvider value={profilingDependencies}>
|
||||
<ObservabilityAIAssistantProvider value={deps.pluginsStart.observabilityAIAssistant}>
|
||||
{children}
|
||||
</ObservabilityAIAssistantProvider>
|
||||
</ProfilingDependenciesContextProvider>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
}
|
|
@ -12,8 +12,18 @@ import {
|
|||
} from '@kbn/observability-shared-plugin/public';
|
||||
import { EmbeddableFlamegraphFactory } from './flamegraph/embeddable_flamegraph_factory';
|
||||
import { EmbeddableFunctionsFactory } from './functions/embeddable_functions_factory';
|
||||
import { GetProfilingEmbeddableDependencies } from './profiling_embeddable_provider';
|
||||
|
||||
export function registerEmbeddables(embeddable: EmbeddableSetup) {
|
||||
embeddable.registerEmbeddableFactory(EMBEDDABLE_FLAMEGRAPH, new EmbeddableFlamegraphFactory());
|
||||
embeddable.registerEmbeddableFactory(EMBEDDABLE_FUNCTIONS, new EmbeddableFunctionsFactory());
|
||||
export function registerEmbeddables(
|
||||
embeddable: EmbeddableSetup,
|
||||
getProfilingEmbeddableDependencies: GetProfilingEmbeddableDependencies
|
||||
) {
|
||||
embeddable.registerEmbeddableFactory(
|
||||
EMBEDDABLE_FLAMEGRAPH,
|
||||
new EmbeddableFlamegraphFactory(getProfilingEmbeddableDependencies)
|
||||
);
|
||||
embeddable.registerEmbeddableFactory(
|
||||
EMBEDDABLE_FUNCTIONS,
|
||||
new EmbeddableFunctionsFactory(getProfilingEmbeddableDependencies)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,10 +4,41 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { calculateImpactEstimates } from '.';
|
||||
import { useCalculateImpactEstimate } from './use_calculate_impact_estimates';
|
||||
import { useProfilingDependencies } from '../components/contexts/profiling_dependencies/use_profiling_dependencies';
|
||||
import {
|
||||
profilingCo2PerKWH,
|
||||
profilingDatacenterPUE,
|
||||
profilingPerCoreWatt,
|
||||
} from '@kbn/observability-plugin/common';
|
||||
|
||||
jest.mock('../components/contexts/profiling_dependencies/use_profiling_dependencies');
|
||||
|
||||
describe('useCalculateImpactEstimate', () => {
|
||||
beforeAll(() => {
|
||||
(useProfilingDependencies as jest.Mock).mockReturnValue({
|
||||
start: {
|
||||
core: {
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
if (key === profilingPerCoreWatt) {
|
||||
return 7;
|
||||
}
|
||||
if (key === profilingCo2PerKWH) {
|
||||
return 0.000379069;
|
||||
}
|
||||
if (key === profilingDatacenterPUE) {
|
||||
return 1.7;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateImpactEstimates', () => {
|
||||
it('calculates impact when countExclusive is lower than countInclusive', () => {
|
||||
const calculateImpactEstimates = useCalculateImpactEstimate();
|
||||
const { selfCPU, totalCPU, totalSamples } = calculateImpactEstimates({
|
||||
countExclusive: 500,
|
||||
countInclusive: 1000,
|
||||
|
@ -47,6 +78,7 @@ describe('calculateImpactEstimates', () => {
|
|||
});
|
||||
|
||||
it('calculates impact', () => {
|
||||
const calculateImpactEstimates = useCalculateImpactEstimate();
|
||||
const { selfCPU, totalCPU, totalSamples } = calculateImpactEstimates({
|
||||
countExclusive: 1000,
|
||||
countInclusive: 1000,
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
profilingCo2PerKWH,
|
||||
profilingDatacenterPUE,
|
||||
profilingPerCoreWatt,
|
||||
} from '@kbn/observability-plugin/common';
|
||||
import { useProfilingDependencies } from '../components/contexts/profiling_dependencies/use_profiling_dependencies';
|
||||
|
||||
interface Params {
|
||||
countInclusive: number;
|
||||
countExclusive: number;
|
||||
totalSamples: number;
|
||||
totalSeconds: number;
|
||||
}
|
||||
|
||||
export type CalculateImpactEstimates = ReturnType<typeof useCalculateImpactEstimate>;
|
||||
export type ImpactEstimates = ReturnType<CalculateImpactEstimates>;
|
||||
|
||||
const ANNUAL_SECONDS = 60 * 60 * 24 * 365;
|
||||
|
||||
// The cost of an x86 CPU core per hour, in US$.
|
||||
// (ARM is 60% less based graviton 3 data, see https://aws.amazon.com/ec2/graviton/)
|
||||
const CORE_COST_PER_HOUR = 0.0425;
|
||||
|
||||
export function useCalculateImpactEstimate() {
|
||||
const {
|
||||
start: { core },
|
||||
} = useProfilingDependencies();
|
||||
|
||||
const perCoreWatts = core.uiSettings.get<number>(profilingPerCoreWatt);
|
||||
const co2PerTonKWH = core.uiSettings.get<number>(profilingCo2PerKWH);
|
||||
const datacenterPUE = core.uiSettings.get<number>(profilingDatacenterPUE);
|
||||
|
||||
function calculateImpact({
|
||||
samples,
|
||||
totalSamples,
|
||||
totalSeconds,
|
||||
}: {
|
||||
samples: number;
|
||||
totalSamples: number;
|
||||
totalSeconds: number;
|
||||
}) {
|
||||
const annualizedScaleUp = ANNUAL_SECONDS / totalSeconds;
|
||||
const totalCoreSeconds = totalSamples / 20;
|
||||
const percentage = samples / totalSamples;
|
||||
const coreSeconds = totalCoreSeconds * percentage;
|
||||
const annualizedCoreSeconds = coreSeconds * annualizedScaleUp;
|
||||
const coreHours = coreSeconds / (60 * 60);
|
||||
const co2PerKWH = co2PerTonKWH * 1000;
|
||||
const co2 = ((perCoreWatts * coreHours) / 1000.0) * co2PerKWH * datacenterPUE;
|
||||
const annualizedCo2 = co2 * annualizedScaleUp;
|
||||
const dollarCost = coreHours * CORE_COST_PER_HOUR;
|
||||
const annualizedDollarCost = dollarCost * annualizedScaleUp;
|
||||
|
||||
return {
|
||||
percentage,
|
||||
coreSeconds,
|
||||
annualizedCoreSeconds,
|
||||
co2,
|
||||
annualizedCo2,
|
||||
dollarCost,
|
||||
annualizedDollarCost,
|
||||
};
|
||||
}
|
||||
|
||||
return (params: Params) => {
|
||||
return {
|
||||
totalSamples: calculateImpact({
|
||||
samples: params.totalSamples,
|
||||
totalSamples: params.totalSamples,
|
||||
totalSeconds: params.totalSeconds,
|
||||
}),
|
||||
totalCPU: calculateImpact({
|
||||
samples: params.countInclusive,
|
||||
totalSamples: params.totalSamples,
|
||||
totalSeconds: params.totalSeconds,
|
||||
}),
|
||||
selfCPU: calculateImpact({
|
||||
samples: params.countExclusive,
|
||||
totalSamples: params.totalSamples,
|
||||
totalSeconds: params.totalSeconds,
|
||||
}),
|
||||
};
|
||||
};
|
||||
}
|
|
@ -19,6 +19,7 @@ import { BehaviorSubject, combineLatest, from, map } from 'rxjs';
|
|||
import { registerEmbeddables } from './embeddables/register_embeddables';
|
||||
import { getServices } from './services';
|
||||
import type { ProfilingPluginPublicSetupDeps, ProfilingPluginPublicStartDeps } from './types';
|
||||
import { ProfilingEmbeddablesDependencies } from './embeddables/profiling_embeddable_provider';
|
||||
|
||||
export type ProfilingPluginSetup = void;
|
||||
export type ProfilingPluginStart = void;
|
||||
|
@ -81,6 +82,8 @@ export class ProfilingPlugin implements Plugin {
|
|||
|
||||
pluginsSetup.observabilityShared.navigation.registerSections(section$);
|
||||
|
||||
const profilingFetchServices = getServices();
|
||||
|
||||
coreSetup.application.register({
|
||||
id: 'profiling',
|
||||
title: 'Universal Profiling',
|
||||
|
@ -95,7 +98,6 @@ export class ProfilingPlugin implements Plugin {
|
|||
unknown
|
||||
];
|
||||
|
||||
const profilingFetchServices = getServices();
|
||||
const { renderApp } = await import('./app');
|
||||
|
||||
function pushKueryToSubject(location: Location) {
|
||||
|
@ -128,7 +130,23 @@ export class ProfilingPlugin implements Plugin {
|
|||
},
|
||||
});
|
||||
|
||||
registerEmbeddables(pluginsSetup.embeddable);
|
||||
const getProfilingEmbeddableDependencies =
|
||||
async (): Promise<ProfilingEmbeddablesDependencies> => {
|
||||
const [coreStart, pluginsStart] = (await coreSetup.getStartServices()) as [
|
||||
CoreStart,
|
||||
ProfilingPluginPublicStartDeps,
|
||||
unknown
|
||||
];
|
||||
return {
|
||||
coreStart,
|
||||
coreSetup,
|
||||
pluginsStart,
|
||||
pluginsSetup,
|
||||
profilingFetchServices,
|
||||
};
|
||||
};
|
||||
|
||||
registerEmbeddables(pluginsSetup.embeddable, getProfilingEmbeddableDependencies);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
|
|
@ -32,8 +32,21 @@ import { StackTracesView } from '../views/stack_traces_view';
|
|||
import { StorageExplorerView } from '../views/storage_explorer';
|
||||
import { RouteBreadcrumb } from './route_breadcrumb';
|
||||
import { DeleteDataView } from '../views/delete_data_view';
|
||||
import { Settings } from '../views/settings';
|
||||
|
||||
const routes = {
|
||||
'/settings': {
|
||||
element: (
|
||||
<RouteBreadcrumb
|
||||
title={i18n.translate('xpack.profiling.breadcrumb.settings', {
|
||||
defaultMessage: 'Settings',
|
||||
})}
|
||||
href="/settings"
|
||||
>
|
||||
<Settings />
|
||||
</RouteBreadcrumb>
|
||||
),
|
||||
},
|
||||
'/': {
|
||||
element: (
|
||||
<RouteBreadcrumb
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBottomBar,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHealth,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
unsavedChangesCount: number;
|
||||
isLoading: boolean;
|
||||
onDiscardChanges: () => void;
|
||||
onSave: () => void;
|
||||
saveLabel: string;
|
||||
}
|
||||
|
||||
export function BottomBarActions({
|
||||
isLoading,
|
||||
onDiscardChanges,
|
||||
onSave,
|
||||
unsavedChangesCount,
|
||||
saveLabel,
|
||||
}: Props) {
|
||||
return (
|
||||
<EuiBottomBar paddingSize="s" data-test-subj="profilingBottomBarActions">
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<EuiHealth color="warning" />
|
||||
<EuiText color="ghost">
|
||||
{i18n.translate('xpack.profiling.bottomBarActions.unsavedChanges', {
|
||||
defaultMessage:
|
||||
'{unsavedChangesCount, plural, =0{0 unsaved changes} one {1 unsaved change} other {# unsaved changes}} ',
|
||||
values: { unsavedChangesCount },
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="profilingBottomBarActionsDiscardChangesButton"
|
||||
color="ghost"
|
||||
onClick={onDiscardChanges}
|
||||
>
|
||||
{i18n.translate('xpack.profiling.bottomBarActions.discardChangesButton', {
|
||||
defaultMessage: 'Discard changes',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="profilingBottomBarActionsButton"
|
||||
onClick={onSave}
|
||||
fill
|
||||
isLoading={isLoading}
|
||||
color="success"
|
||||
iconType="check"
|
||||
>
|
||||
{saveLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiBottomBar>
|
||||
);
|
||||
}
|
115
x-pack/plugins/profiling/public/views/settings/index.tsx
Normal file
115
x-pack/plugins/profiling/public/views/settings/index.tsx
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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 { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { LazyField } from '@kbn/advanced-settings-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
profilingCo2PerKWH,
|
||||
profilingDatacenterPUE,
|
||||
profilingPerCoreWatt,
|
||||
} from '@kbn/observability-plugin/common';
|
||||
import { useEditableSettings, useUiTracker } from '@kbn/observability-shared-plugin/public';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React from 'react';
|
||||
import { useProfilingDependencies } from '../../components/contexts/profiling_dependencies/use_profiling_dependencies';
|
||||
import { ProfilingAppPageTemplate } from '../../components/profiling_app_page_template';
|
||||
import { BottomBarActions } from './bottom_bar_actions';
|
||||
|
||||
const settingKeys = [profilingCo2PerKWH, profilingDatacenterPUE, profilingPerCoreWatt];
|
||||
|
||||
export function Settings() {
|
||||
const trackProfilingEvent = useUiTracker({ app: 'profiling' });
|
||||
const {
|
||||
start: {
|
||||
core: { docLinks, notifications },
|
||||
},
|
||||
} = useProfilingDependencies();
|
||||
|
||||
const {
|
||||
handleFieldChange,
|
||||
settingsEditableConfig,
|
||||
unsavedChanges,
|
||||
saveAll,
|
||||
isSaving,
|
||||
cleanUnsavedChanges,
|
||||
} = useEditableSettings('profiling', settingKeys);
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
const reloadPage = Object.keys(unsavedChanges).some((key) => {
|
||||
return settingsEditableConfig[key].requiresPageReload;
|
||||
});
|
||||
await saveAll();
|
||||
trackProfilingEvent({ metric: 'general_settings_save' });
|
||||
if (reloadPage) {
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
notifications.toasts.addDanger({
|
||||
title: i18n.translate('xpack.profiling.settings.save.error', {
|
||||
defaultMessage: 'An error occurred while saving the settings',
|
||||
}),
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ProfilingAppPageTemplate hideSearchBar>
|
||||
<>
|
||||
<EuiTitle>
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.profiling.settings.title', {
|
||||
defaultMessage: 'Advanced Settings',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiPanel grow={false} hasShadow={false} hasBorder paddingSize="none">
|
||||
<EuiPanel color="subdued" hasShadow={false}>
|
||||
<EuiTitle size="s">
|
||||
<EuiText>
|
||||
{i18n.translate('xpack.profiling.settings.co2Sections', {
|
||||
defaultMessage: 'CO2',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiTitle>
|
||||
</EuiPanel>
|
||||
<EuiPanel hasShadow={false}>
|
||||
{settingKeys.map((settingKey) => {
|
||||
const editableConfig = settingsEditableConfig[settingKey];
|
||||
return (
|
||||
<LazyField
|
||||
key={settingKey}
|
||||
setting={editableConfig}
|
||||
handleChange={handleFieldChange}
|
||||
enableSaving
|
||||
docLinks={docLinks.links}
|
||||
toasts={notifications.toasts}
|
||||
unsavedChanges={unsavedChanges[settingKey]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</EuiPanel>
|
||||
</EuiPanel>
|
||||
{!isEmpty(unsavedChanges) && (
|
||||
<BottomBarActions
|
||||
isLoading={isSaving}
|
||||
onDiscardChanges={cleanUnsavedChanges}
|
||||
onSave={handleSave}
|
||||
saveLabel={i18n.translate('xpack.profiling.settings.saveButton', {
|
||||
defaultMessage: 'Save changes',
|
||||
})}
|
||||
unsavedChangesCount={Object.keys(unsavedChanges).length}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</ProfilingAppPageTemplate>
|
||||
);
|
||||
}
|
|
@ -49,7 +49,8 @@
|
|||
"@kbn/observability-ai-assistant-plugin",
|
||||
"@kbn/profiling-data-access-plugin",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/profiling-utils"
|
||||
"@kbn/profiling-utils",
|
||||
"@kbn/advanced-settings-plugin"
|
||||
// add references to other TypeScript projects the plugin depends on
|
||||
|
||||
// requiredPlugins from ./kibana.json
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue