Skip to content

Commit

Permalink
Merge branch 'main' into security-solution-fix-skipped-test-143718
Browse files Browse the repository at this point in the history
  • Loading branch information
YulNaumenko authored Oct 24, 2022
2 parents e70260d + b87e8d0 commit e90f0e0
Show file tree
Hide file tree
Showing 58 changed files with 1,880 additions and 941 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,62 @@ import {
EuiFormControlLayout,
EuiFormLabel,
EuiFormRow,
EuiIcon,
EuiLink,
EuiLoadingChart,
EuiPopover,
EuiText,
EuiToolTip,
} from '@elastic/eui';

import { FormattedMessage } from '@kbn/i18n-react';
import { Markdown } from '@kbn/kibana-react-plugin/public';
import { useReduxContainerContext } from '@kbn/presentation-util-plugin/public';
import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
import { ControlGroupReduxState } from '../types';
import { pluginServices } from '../../services';
import { EditControlButton } from '../editor/edit_control';
import { ControlGroupStrings } from '../control_group_strings';
import { useChildEmbeddable } from '../../hooks/use_child_embeddable';
import { TIME_SLIDER_CONTROL } from '../../../common';

interface ControlFrameErrorProps {
error: Error;
}

const ControlFrameError = ({ error }: ControlFrameErrorProps) => {
const [isPopoverOpen, setPopoverOpen] = useState(false);
const popoverButton = (
<EuiText className="errorEmbeddableCompact__button" size="xs">
<EuiLink
className="eui-textTruncate"
color="subdued"
onClick={() => setPopoverOpen((open) => !open)}
>
<EuiIcon type="alert" color="danger" />
<FormattedMessage
id="controls.frame.error.message"
defaultMessage="An error has occurred. Read more"
/>
</EuiLink>
</EuiText>
);

return (
<EuiPopover
button={popoverButton}
isOpen={isPopoverOpen}
anchorClassName="errorEmbeddableCompact__popoverAnchor"
closePopover={() => setPopoverOpen(false)}
>
<Markdown
markdown={error.message}
openLinksInNewTab={true}
data-test-subj="errorMessageMarkdown"
/>
</EuiPopover>
);
};

export interface ControlFrameProps {
customPrepend?: JSX.Element;
enableActions?: boolean;
Expand All @@ -40,7 +83,7 @@ export const ControlFrame = ({
embeddableType,
}: ControlFrameProps) => {
const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []);
const [hasFatalError, setHasFatalError] = useState(false);
const [fatalError, setFatalError] = useState<Error>();

const {
useEmbeddableSelector: select,
Expand All @@ -61,19 +104,14 @@ export const ControlFrame = ({
const usingTwoLineLayout = controlStyle === 'twoLine';

useEffect(() => {
if (embeddableRoot.current && embeddable) {
embeddable.render(embeddableRoot.current);
if (embeddableRoot.current) {
embeddable?.render(embeddableRoot.current);
}
const inputSubscription = embeddable
?.getInput$()
.subscribe((newInput) => setTitle(newInput.title));
const errorSubscription = embeddable?.getOutput$().subscribe({
error: (error: Error) => {
if (!embeddableRoot.current) return;
const errorEmbeddable = new ErrorEmbeddable(error, { id: embeddable.id }, undefined, true);
errorEmbeddable.render(embeddableRoot.current);
setHasFatalError(true);
},
error: setFatalError,
});
return () => {
inputSubscription?.unsubscribe();
Expand All @@ -88,7 +126,7 @@ export const ControlFrame = ({
'controlFrameFloatingActions--oneLine': !usingTwoLineLayout,
})}
>
{!hasFatalError && embeddableType !== TIME_SLIDER_CONTROL && (
{!fatalError && embeddableType !== TIME_SLIDER_CONTROL && (
<EuiToolTip content={ControlGroupStrings.floatingActions.getEditButtonTitle()}>
<EditControlButton embeddableId={embeddableId} />
</EuiToolTip>
Expand Down Expand Up @@ -119,7 +157,7 @@ export const ControlFrame = ({
const embeddableParentClassNames = classNames('controlFrame__control', {
'controlFrame--twoLine': controlStyle === 'twoLine',
'controlFrame--oneLine': controlStyle === 'oneLine',
'controlFrame--fatalError': hasFatalError,
'controlFrame--fatalError': !!fatalError,
});

function renderEmbeddablePrepend() {
Expand Down Expand Up @@ -149,12 +187,19 @@ export const ControlFrame = ({
</>
}
>
{embeddable && (
{embeddable && !fatalError && (
<div
className={embeddableParentClassNames}
id={`controlFrame--${embeddableId}`}
ref={embeddableRoot}
/>
>
{fatalError && <ControlFrameError error={fatalError} />}
</div>
)}
{fatalError && (
<div className={embeddableParentClassNames} id={`controlFrame--${embeddableId}`}>
{<ControlFrameError error={fatalError} />}
</div>
)}
{!embeddable && (
<div className={embeddableParentClassNames} id={`controlFrame--${embeddableId}`}>
Expand Down
155 changes: 155 additions & 0 deletions src/plugins/data/common/search/aggs/agg_configs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ import type { DataView } from '@kbn/data-views-plugin/common';
import { stubIndexPattern } from '../../stubs';
import { IEsSearchResponse } from '..';

// Mute moment.tz warnings about not finding a mock timezone
jest.mock('../utils', () => {
const original = jest.requireActual('../utils');
return {
...original,
getUserTimeZone: jest.fn(() => 'US/Pacific'),
};
});

describe('AggConfigs', () => {
const indexPattern: DataView = stubIndexPattern;
let typesRegistry: AggTypesRegistryStart;
Expand Down Expand Up @@ -563,6 +572,82 @@ describe('AggConfigs', () => {
'1-bucket>_count'
);
});

it('prepends a sampling agg whenever sampling is enabled', () => {
const configStates = [
{
enabled: true,
id: '1',
type: 'avg_bucket',
schema: 'metric',
params: {
customBucket: {
id: '1-bucket',
type: 'date_histogram',
schema: 'bucketAgg',
params: {
field: '@timestamp',
interval: '10s',
},
},
customMetric: {
id: '1-metric',
type: 'count',
schema: 'metricAgg',
params: {},
},
},
},
{
enabled: true,
id: '2',
type: 'terms',
schema: 'bucket',
params: {
field: 'clientip',
},
},
{
enabled: true,
id: '3',
type: 'terms',
schema: 'bucket',
params: {
field: 'machine.os.raw',
},
},
];

const ac = new AggConfigs(
indexPattern,
configStates,
{ typesRegistry, hierarchical: true, probability: 0.5 },
jest.fn()
);
const topLevelDsl = ac.toDsl();

expect(Object.keys(topLevelDsl)).toContain('sampling');
expect(Object.keys(topLevelDsl.sampling)).toEqual(['random_sampler', 'aggs']);
expect(Object.keys(topLevelDsl.sampling.aggs)).toContain('2');
expect(Object.keys(topLevelDsl.sampling.aggs['2'].aggs)).toEqual(['1', '3', '1-bucket']);
});

it('should not prepend a sampling agg when no nested agg is avaialble', () => {
const ac = new AggConfigs(
indexPattern,
[
{
enabled: true,
type: 'count',
schema: 'metric',
},
],
{ typesRegistry, probability: 0.5 },
jest.fn()
);
const topLevelDsl = ac.toDsl();
expect(Object.keys(topLevelDsl)).not.toContain('sampling');
});
});

describe('#postFlightTransform', () => {
Expand Down Expand Up @@ -854,4 +939,74 @@ describe('AggConfigs', () => {
`);
});
});

describe('isSamplingEnabled', () => {
it('should return false if probability is 1', () => {
const ac = new AggConfigs(
indexPattern,
[{ enabled: true, type: 'avg', schema: 'metric', params: { field: 'bytes' } }],
{ typesRegistry, probability: 1 },
jest.fn()
);

expect(ac.isSamplingEnabled()).toBeFalsy();
});

it('should return true if probability is less than 1', () => {
const ac = new AggConfigs(
indexPattern,
[{ enabled: true, type: 'avg', schema: 'metric', params: { field: 'bytes' } }],
{ typesRegistry, probability: 0.1 },
jest.fn()
);

expect(ac.isSamplingEnabled()).toBeTruthy();
});

it('should return false when all aggs have hasNoDsl flag enabled', () => {
const ac = new AggConfigs(
indexPattern,
[
{
enabled: true,
type: 'count',
schema: 'metric',
},
],
{ typesRegistry, probability: 1 },
jest.fn()
);

expect(ac.isSamplingEnabled()).toBeFalsy();
});

it('should return false when no nested aggs are avaialble', () => {
const ac = new AggConfigs(
indexPattern,
[{ enabled: false, type: 'avg', schema: 'metric', params: { field: 'bytes' } }],
{ typesRegistry, probability: 1 },
jest.fn()
);

expect(ac.isSamplingEnabled()).toBeFalsy();
});

it('should return true if at least one nested agg is available and probability < 1', () => {
const ac = new AggConfigs(
indexPattern,
[
{
enabled: true,
type: 'count',
schema: 'metric',
},
{ enabled: true, type: 'avg', schema: 'metric', params: { field: 'bytes' } },
],
{ typesRegistry, probability: 0.1 },
jest.fn()
);

expect(ac.isSamplingEnabled()).toBeTruthy();
});
});
});
36 changes: 34 additions & 2 deletions src/plugins/data/common/search/aggs/agg_configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { AggTypesDependencies, GetConfigFn, getUserTimeZone } from '../..';
import { getTime, calculateBounds } from '../..';
import type { IBucketAggConfig } from './buckets';
import { insertTimeShiftSplit, mergeTimeShifts } from './utils/time_splits';
import { createSamplerAgg, isSamplingEnabled } from './utils/sampler';

function removeParentAggs(obj: any) {
for (const prop in obj) {
Expand Down Expand Up @@ -55,6 +56,8 @@ export interface AggConfigsOptions {
hierarchical?: boolean;
aggExecutionContext?: AggTypesDependencies['aggExecutionContext'];
partialRows?: boolean;
probability?: number;
samplerSeed?: number;
}

export type CreateAggConfigParams = Assign<AggConfigSerialized, { type: string | IAggType }>;
Expand Down Expand Up @@ -107,6 +110,17 @@ export class AggConfigs {
return this.opts.partialRows ?? false;
}

public get samplerConfig() {
return { probability: this.opts.probability ?? 1, seed: this.opts.samplerSeed };
}

isSamplingEnabled() {
return (
isSamplingEnabled(this.opts.probability) &&
this.getRequestAggs().filter((agg) => !agg.type.hasNoDsl).length > 0
);
}

setTimeFields(timeFields: string[] | undefined) {
this.timeFields = timeFields;
}
Expand Down Expand Up @@ -225,7 +239,7 @@ export class AggConfigs {
}

toDsl(): Record<string, any> {
const dslTopLvl = {};
const dslTopLvl: Record<string, any> = {};
let dslLvlCursor: Record<string, any>;
let nestedMetrics: Array<{ config: AggConfig; dsl: Record<string, any> }> | [];

Expand Down Expand Up @@ -254,10 +268,21 @@ export class AggConfigs {
(config) => 'splitForTimeShift' in config.type && config.type.splitForTimeShift(config, this)
);

if (this.isSamplingEnabled()) {
dslTopLvl.sampling = createSamplerAgg({
probability: this.opts.probability ?? 1,
seed: this.opts.samplerSeed,
});
}

requestAggs.forEach((config: AggConfig, i: number, list) => {
if (!dslLvlCursor) {
// start at the top level
dslLvlCursor = dslTopLvl;
// when sampling jump directly to the aggs
if (this.isSamplingEnabled()) {
dslLvlCursor = dslLvlCursor.sampling.aggs;
}
} else {
const prevConfig: AggConfig = list[i - 1];
const prevDsl = dslLvlCursor[prevConfig.id];
Expand Down Expand Up @@ -452,7 +477,12 @@ export class AggConfigs {
doc_count: response.rawResponse.hits?.total as estypes.AggregationsAggregate,
};
}
const aggCursor = transformedRawResponse.aggregations!;
const aggCursor = this.isSamplingEnabled()
? (transformedRawResponse.aggregations!.sampling! as Record<
string,
estypes.AggregationsAggregate
>)
: transformedRawResponse.aggregations!;

mergeTimeShifts(this, aggCursor);
return {
Expand Down Expand Up @@ -531,6 +561,8 @@ export class AggConfigs {
metricsAtAllLevels: this.hierarchical,
partialRows: this.partialRows,
aggs: this.aggs.map((agg) => buildExpression(agg.toExpressionAst())),
probability: this.opts.probability,
samplerSeed: this.opts.samplerSeed,
}),
]).toAst();
}
Expand Down
Loading

0 comments on commit e90f0e0

Please sign in to comment.