[Lens] Shard failure notices make it impossible to use Lens (#142985)

* [Lens] Shard failure notices make it impossible to use Lens

* rename getTSDBRollupWarningMessages -> getShardFailuresWarningMessages

* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'

* update UI

* push some logic

* fix CI

* push some changes

* delete outdated test

* fix CI

* push some logic

* add KibanaThemeProvider

* apply UI changes for shard_failure_open_modal_button

* cleanup

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
This commit is contained in:
Alexey Antonov 2022-11-01 15:58:05 +03:00 committed by GitHub
parent 1d06b5ffc1
commit d9adcfee0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 267 additions and 146 deletions

View file

@ -169,6 +169,7 @@ export type {
IEsError,
Reason,
WaitUntilNextSessionCompletesOptions,
SearchResponseWarning,
} from './search';
export {
@ -270,6 +271,9 @@ export type {
GlobalQueryStateFromUrl,
} from './query';
export type { ShardFailureRequest } from './shard_failure_modal';
export { ShardFailureOpenModalButton } from './shard_failure_modal';
export type { AggsStart } from './search/aggs';
export { getTime } from '../common';

View file

@ -13,6 +13,7 @@ import { setNotifications } from '../../services';
import { SearchResponseWarning } from '../types';
import { filterWarnings, handleWarnings } from './handle_warnings';
import * as extract from './extract_warnings';
import { SearchRequest } from '../../../common';
jest.mock('@kbn/i18n', () => {
return {
@ -152,6 +153,8 @@ describe('Filtering and showing warnings', () => {
describe('filterWarnings', () => {
const callback = jest.fn();
const request = {} as SearchRequest;
const response = {} as estypes.SearchResponse;
beforeEach(() => {
callback.mockImplementation(() => {
@ -161,19 +164,19 @@ describe('Filtering and showing warnings', () => {
it('filters out all', () => {
callback.mockImplementation(() => true);
expect(filterWarnings(warnings, callback)).toEqual([]);
expect(filterWarnings(warnings, callback, request, response, 'id')).toEqual([]);
});
it('filters out some', () => {
callback.mockImplementation(
(warning: SearchResponseWarning) => warning.reason?.type !== 'generic_shard_failure'
);
expect(filterWarnings(warnings, callback)).toEqual([warnings[2]]);
expect(filterWarnings(warnings, callback, request, response, 'id')).toEqual([warnings[2]]);
});
it('filters out none', () => {
callback.mockImplementation(() => false);
expect(filterWarnings(warnings, callback)).toEqual(warnings);
expect(filterWarnings(warnings, callback, request, response, 'id')).toEqual(warnings);
});
});
});

View file

@ -8,7 +8,7 @@
import { estypes } from '@elastic/elasticsearch';
import { debounce } from 'lodash';
import { EuiSpacer } from '@elastic/eui';
import { EuiSpacer, EuiTextAlign } from '@elastic/eui';
import { ThemeServiceStart } from '@kbn/core/public';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import React from 'react';
@ -57,19 +57,23 @@ export function handleWarnings({
theme,
callback,
sessionId = '',
requestId,
}: {
request: SearchRequest;
response: estypes.SearchResponse;
theme: ThemeServiceStart;
callback?: WarningHandlerCallback;
sessionId?: string;
requestId?: string;
}) {
const warnings = extractWarnings(response);
if (warnings.length === 0) {
return;
}
const internal = callback ? filterWarnings(warnings, callback) : warnings;
const internal = callback
? filterWarnings(warnings, callback, request, response, requestId)
: warnings;
if (internal.length === 0) {
return;
}
@ -95,12 +99,16 @@ export function handleWarnings({
<>
{warning.text}
<EuiSpacer size="s" />
<ShardFailureOpenModalButton
request={request as ShardFailureRequest}
response={response}
theme={theme}
title={title}
/>
<EuiTextAlign textAlign="right">
<ShardFailureOpenModalButton
theme={theme}
title={title}
getRequestMeta={() => ({
request: request as ShardFailureRequest,
response,
})}
/>
</EuiTextAlign>
</>,
{ theme$: theme.theme$ }
);
@ -116,12 +124,22 @@ export function handleWarnings({
/**
* @internal
*/
export function filterWarnings(warnings: SearchResponseWarning[], cb: WarningHandlerCallback) {
export function filterWarnings(
warnings: SearchResponseWarning[],
cb: WarningHandlerCallback,
request: SearchRequest,
response: estypes.SearchResponse,
requestId: string | undefined
) {
const unfiltered: SearchResponseWarning[] = [];
// use the consumer's callback as a filter to receive warnings to handle on our side
warnings.forEach((warning) => {
const consumerHandled = cb?.(warning);
const consumerHandled = cb?.(warning, {
requestId,
request,
response,
});
if (!consumerHandled) {
unfiltered.push(warning);
}

View file

@ -9,6 +9,7 @@
export * from './expressions';
export type {
SearchResponseWarning,
ISearchSetup,
ISearchStart,
ISearchStartSearchSource,

View file

@ -241,11 +241,13 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
onResponse: (request, response, options) => {
if (!options.disableShardFailureWarning) {
const { rawResponse } = response;
handleWarnings({
request: request.body,
response: rawResponse,
theme,
sessionId: options.sessionId,
requestId: request.id,
});
}
return response;
@ -286,12 +288,12 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
if (!rawResponse) {
return;
}
handleWarnings({
request: request.json as SearchRequest,
response: rawResponse,
theme,
callback,
requestId: request.id,
});
});
},

View file

@ -11,7 +11,7 @@ import type { PackageInfo } from '@kbn/core/server';
import { DataViewsContract } from '@kbn/data-views-plugin/common';
import { RequestAdapter } from '@kbn/inspector-plugin/public';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { ISearchGeneric, ISearchStartSearchSource } from '../../common/search';
import { ISearchGeneric, ISearchStartSearchSource, SearchRequest } from '../../common/search';
import { AggsSetup, AggsSetupDependencies, AggsStart, AggsStartDependencies } from './aggs';
import { SearchUsageCollector } from './collectors';
import { ISessionsClient, ISessionService } from './session';
@ -159,4 +159,11 @@ export type SearchResponseWarning =
* function to prevent the search service from showing warning notifications by default.
* @public
*/
export type WarningHandlerCallback = (warnings: SearchResponseWarning) => boolean | undefined;
export type WarningHandlerCallback = (
warnings: SearchResponseWarning,
meta: {
request: SearchRequest;
response: estypes.SearchResponse;
requestId: string | undefined;
}
) => boolean | undefined;

View file

@ -21,8 +21,10 @@ describe('ShardFailureOpenModalButton', () => {
it('triggers the openModal function when "Show details" button is clicked', () => {
const component = mountWithIntl(
<ShardFailureOpenModalButton
request={shardFailureRequest}
response={shardFailureResponse}
getRequestMeta={() => ({
request: shardFailureRequest,
response: shardFailureResponse,
})}
theme={theme}
title="test"
/>

View file

@ -6,33 +6,41 @@
* Side Public License, v 1.
*/
import React from 'react';
import React, { useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiTextAlign } from '@elastic/eui';
import { EuiLink, EuiButton, EuiButtonProps } from '@elastic/eui';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ThemeServiceStart } from '@kbn/core/public';
import type { ThemeServiceStart } from '@kbn/core/public';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { getOverlays } from '../services';
import { ShardFailureModal } from './shard_failure_modal';
import { ShardFailureRequest } from './shard_failure_types';
import type { ShardFailureRequest } from './shard_failure_types';
// @internal
export interface ShardFailureOpenModalButtonProps {
request: ShardFailureRequest;
response: estypes.SearchResponse<any>;
theme: ThemeServiceStart;
title: string;
size?: EuiButtonProps['size'];
color?: EuiButtonProps['color'];
getRequestMeta: () => {
request: ShardFailureRequest;
response: estypes.SearchResponse<any>;
};
isButtonEmpty?: boolean;
}
// Needed for React.lazy
// eslint-disable-next-line import/no-default-export
export default function ShardFailureOpenModalButton({
request,
response,
getRequestMeta,
theme,
title,
size = 's',
color = 'warning',
isButtonEmpty = false,
}: ShardFailureOpenModalButtonProps) {
function onClick() {
const onClick = useCallback(() => {
const { request, response } = getRequestMeta();
const modal = getOverlays().openModal(
toMountPoint(
<ShardFailureModal
@ -47,21 +55,22 @@ export default function ShardFailureOpenModalButton({
className: 'shardFailureModal',
}
);
}
}, [getRequestMeta, theme.theme$, title]);
const Component = isButtonEmpty ? EuiLink : EuiButton;
return (
<EuiTextAlign textAlign="right">
<EuiButton
color="warning"
size="s"
onClick={onClick}
data-test-subj="openShardFailureModalBtn"
>
<FormattedMessage
id="data.search.searchSource.fetch.shardsFailedModal.showDetails"
defaultMessage="Show details"
description="Open the modal to show details"
/>
</EuiButton>
</EuiTextAlign>
<Component
color={color}
size={size}
onClick={onClick}
data-test-subj="openShardFailureModalBtn"
>
<FormattedMessage
id="data.search.searchSource.fetch.shardsFailedModal.showDetails"
defaultMessage="Show details"
description="Open the modal to show details"
/>
</Component>
);
}

View file

@ -65,7 +65,7 @@ import {
import {
getFiltersInLayer,
getTSDBRollupWarningMessages,
getShardFailuresWarningMessages,
getVisualDefaultsForLayer,
isColumnInvalid,
cloneLayer,
@ -89,10 +89,10 @@ import {
} from './operations/layer_helpers';
import { FormBasedPrivateState, FormBasedPersistedState, DataViewDragDropOperation } from './types';
import { mergeLayer, mergeLayers } from './state_helpers';
import { Datasource, VisualizeEditorContext } from '../../types';
import type { Datasource, VisualizeEditorContext } from '../../types';
import { deleteColumn, isReferenced } from './operations';
import { GeoFieldWorkspacePanel } from '../../editor_frame_service/editor_frame/workspace_panel/geo_field_workspace_panel';
import { DraggingIdentifier } from '../../drag_drop';
import type { DraggingIdentifier } from '../../drag_drop';
import { getStateTimeShiftWarningMessages } from './time_shift_utils';
import { getPrecisionErrorWarningMessages } from './utils';
import { DOCUMENT_FIELD_NAME } from '../../../common/constants';
@ -897,8 +897,8 @@ export function getFormBasedDatasource({
),
];
},
getSearchWarningMessages: (state, warning) => {
return [...getTSDBRollupWarningMessages(state, warning)];
getSearchWarningMessages: (state, warning, request, response) => {
return [...getShardFailuresWarningMessages(state, warning, request, response, core.theme)];
},
getDeprecationMessages: () => {
const deprecatedMessages: React.ReactNode[] = [];

View file

@ -8,15 +8,23 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { DocLinksStart } from '@kbn/core/public';
import type { DocLinksStart, ThemeServiceStart } from '@kbn/core/public';
import type { DatatableUtilitiesService } from '@kbn/data-plugin/common';
import { TimeRange } from '@kbn/es-query';
import { EuiLink, EuiTextColor, EuiButton, EuiSpacer } from '@elastic/eui';
import { EuiLink, EuiTextColor, EuiButton, EuiSpacer, EuiText } from '@elastic/eui';
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { groupBy, escape, uniq } from 'lodash';
import type { Query } from '@kbn/data-plugin/common';
import { SearchResponseWarning } from '@kbn/data-plugin/public/search/types';
import { SearchRequest } from '@kbn/data-plugin/common';
import {
SearchResponseWarning,
ShardFailureOpenModalButton,
ShardFailureRequest,
} from '@kbn/data-plugin/public';
import { estypes } from '@elastic/elasticsearch';
import type { FramePublicAPI, IndexPattern, StateSetter } from '../../types';
import { renewIDs } from '../../utils';
import type { FormBasedLayer, FormBasedPersistedState, FormBasedPrivateState } from './types';
@ -162,43 +170,67 @@ const accuracyModeEnabledWarning = (columnName: string, docLink: string) => (
/>
);
export function getTSDBRollupWarningMessages(
export function getShardFailuresWarningMessages(
state: FormBasedPersistedState,
warning: SearchResponseWarning
) {
warning: SearchResponseWarning,
request: SearchRequest,
response: estypes.SearchResponse,
theme: ThemeServiceStart
): Array<string | React.ReactNode> {
if (state) {
const hasTSDBRollupWarnings =
warning.type === 'shard_failure' &&
warning.reason.type === 'unsupported_aggregation_on_downsampled_index';
if (!hasTSDBRollupWarnings) {
return [];
if (warning.type === 'shard_failure') {
switch (warning.reason.type) {
case 'unsupported_aggregation_on_downsampled_index':
return Object.values(state.layers).flatMap((layer) =>
uniq(
Object.values(layer.columns)
.filter((col) =>
[
'median',
'percentile',
'percentile_rank',
'last_value',
'unique_count',
'standard_deviation',
].includes(col.operationType)
)
.map((col) => col.label)
).map((label) =>
i18n.translate('xpack.lens.indexPattern.tsdbRollupWarning', {
defaultMessage:
'{label} uses a function that is unsupported by rolled up data. Select a different function or change the time range.',
values: {
label,
},
})
)
);
default:
return [
<>
<EuiText size="s">
<strong>{warning.message}</strong>
<p>{warning.text}</p>
</EuiText>
<EuiSpacer size="s" />
{warning.text ? (
<ShardFailureOpenModalButton
theme={theme}
title={warning.message}
size="m"
getRequestMeta={() => ({
request: request as ShardFailureRequest,
response,
})}
color="primary"
isButtonEmpty={true}
/>
) : null}
</>,
];
}
}
return Object.values(state.layers).flatMap((layer) =>
uniq(
Object.values(layer.columns)
.filter((col) =>
[
'median',
'percentile',
'percentile_rank',
'last_value',
'unique_count',
'standard_deviation',
].includes(col.operationType)
)
.map((col) => col.label)
).map((label) =>
i18n.translate('xpack.lens.indexPattern.tsdbRollupWarning', {
defaultMessage:
'{label} uses a function that is unsupported by rolled up data. Select a different function or change the time range.',
values: {
label,
},
})
)
);
}
return [];
}

View file

@ -36,6 +36,7 @@ import type { DefaultInspectorAdapters } from '@kbn/expressions-plugin/common';
import type { Datatable } from '@kbn/expressions-plugin/public';
import { DropIllustration } from '@kbn/chart-icons';
import { trackUiCounterEvents } from '../../../lens_ui_telemetry';
import { getSearchWarningMessages } from '../../../utils';
import {
FramePublicAPI,
isLensBrushEvent,
@ -231,32 +232,37 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
(data: unknown, adapters?: Partial<DefaultInspectorAdapters>) => {
if (renderDeps.current) {
const [defaultLayerId] = Object.keys(renderDeps.current.datasourceLayers);
const datasource = Object.values(renderDeps.current.datasourceMap)[0];
const datasourceState = Object.values(renderDeps.current.datasourceStates)[0].state;
let requestWarnings: Array<React.ReactNode | string> = [];
const requestWarnings: string[] = [];
const datasource = Object.values(renderDeps.current?.datasourceMap)[0];
const datasourceState = Object.values(renderDeps.current?.datasourceStates)[0].state;
if (adapters?.requests) {
plugins.data.search.showWarnings(adapters.requests, (warning) => {
const warningMessage = datasource.getSearchWarningMessages?.(datasourceState, warning);
requestWarnings.push(...(warningMessage || []));
if (warningMessage && warningMessage.length) return true;
});
}
if (adapters && adapters.tables) {
dispatchLens(
onActiveDataChange({
activeData: Object.entries(adapters.tables?.tables).reduce<Record<string, Datatable>>(
(acc, [key, value], index, tables) => ({
...acc,
[tables.length === 1 ? defaultLayerId : key]: value,
}),
{}
),
requestWarnings,
})
requestWarnings = getSearchWarningMessages(
adapters.requests,
datasource,
datasourceState,
{
searchService: plugins.data.search,
}
);
}
dispatchLens(
onActiveDataChange({
activeData:
adapters && adapters.tables
? Object.entries(adapters.tables?.tables).reduce<Record<string, Datatable>>(
(acc, [key, value], index, tables) => ({
...acc,
[tables.length === 1 ? defaultLayerId : key]: value,
}),
{}
)
: undefined,
requestWarnings,
})
);
}
},
[dispatchLens, plugins.data.search]

View file

@ -87,7 +87,12 @@ import { LensAttributeService } from '../lens_attribute_service';
import type { ErrorMessage, TableInspectorAdapter } from '../editor_frame_service/types';
import { getLensInspectorService, LensInspector } from '../lens_inspector_service';
import { SharingSavedObjectProps, VisualizationDisplayOptions } from '../types';
import { getActiveDatasourceIdFromDoc, getIndexPatternsObjects, inferTimeField } from '../utils';
import {
getActiveDatasourceIdFromDoc,
getIndexPatternsObjects,
getSearchWarningMessages,
inferTimeField,
} from '../utils';
import { getLayerMetaInfo, combineQueryAndFilters } from '../app_plugin/show_underlying_data';
import { convertDataViewIntoLensIndexPattern } from '../data_views_service/loader';
@ -531,21 +536,30 @@ export class Embeddable
private handleWarnings(adapters?: Partial<DefaultInspectorAdapters>) {
const activeDatasourceId = getActiveDatasourceIdFromDoc(this.savedVis);
if (!activeDatasourceId || !adapters?.requests) return;
if (!activeDatasourceId || !adapters?.requests) {
return;
}
const activeDatasource = this.deps.datasourceMap[activeDatasourceId];
const docDatasourceState = this.savedVis?.state.datasourceStates[activeDatasourceId];
const warnings: React.ReactNode[] = [];
this.deps.data.search.showWarnings(adapters.requests, (warning) => {
const warningMessage = activeDatasource.getSearchWarningMessages?.(
docDatasourceState,
warning
);
warnings.push(...(warningMessage || []));
if (warningMessage && warningMessage.length) return true;
});
if (warnings && this.warningDomNode) {
render(<Warnings warnings={warnings} />, this.warningDomNode);
const requestWarnings = getSearchWarningMessages(
adapters.requests,
activeDatasource,
docDatasourceState,
{
searchService: this.deps.data.search,
}
);
if (requestWarnings.length && this.warningDomNode) {
render(
<KibanaThemeProvider theme$={this.deps.theme.theme$}>
<Warnings warnings={requestWarnings} />
</KibanaThemeProvider>,
this.warningDomNode
);
}
}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { ReactNode } from 'react';
import { createAction, createReducer, current, PayloadAction } from '@reduxjs/toolkit';
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
import { mapValues, uniq } from 'lodash';
@ -98,8 +99,8 @@ export const getPreloadedState = ({
export const setState = createAction<Partial<LensAppState>>('lens/setState');
export const onActiveDataChange = createAction<{
activeData: TableInspectorAdapter;
requestWarnings?: string[];
activeData?: TableInspectorAdapter;
requestWarnings?: Array<ReactNode | string>;
}>('lens/onActiveDataChange');
export const setSaveable = createAction<boolean>('lens/setSaveable');
export const enableAutoApply = createAction<void>('lens/enableAutoApply');
@ -265,8 +266,8 @@ export const makeLensReducer = (storeDeps: LensStoreDeps) => {
) => {
return {
...state,
activeData,
requestWarnings,
...(activeData ? { activeData } : {}),
...(requestWarnings ? { requestWarnings } : {}),
};
},
[setSaveable.type]: (state, { payload }: PayloadAction<boolean>) => {

View file

@ -31,6 +31,9 @@ import type { FieldSpec, DataViewSpec } from '@kbn/data-views-plugin/common';
import type { FieldFormatParams } from '@kbn/field-formats-plugin/common';
import { SearchResponseWarning } from '@kbn/data-plugin/public/search/types';
import type { EuiButtonIconColor } from '@elastic/eui';
import { SearchRequest } from '@kbn/data-plugin/public';
import { estypes } from '@elastic/elasticsearch';
import React from 'react';
import type { DraggingIdentifier, DragDropIdentifier, DragContextState } from './drag_drop';
import type { DateRange, LayerType, SortingHint } from '../common';
import type {
@ -428,7 +431,13 @@ export interface Datasource<T = unknown, P = unknown> {
/**
* The embeddable calls this function to display warnings about visualization on the dashboard
*/
getSearchWarningMessages?: (state: P, warning: SearchResponseWarning) => string[] | undefined;
getSearchWarningMessages?: (
state: P,
warning: SearchResponseWarning,
request: SearchRequest,
response: estypes.SearchResponse
) => Array<string | React.ReactNode> | undefined;
/**
* Checks if the visualization created is time based, for example date histogram
*/

View file

@ -14,6 +14,9 @@ import type { IUiSettingsClient, SavedObjectReference } from '@kbn/core/public';
import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/public';
import type { DatatableUtilitiesService } from '@kbn/data-plugin/common';
import { BrushTriggerEvent, ClickTriggerEvent } from '@kbn/charts-plugin/public';
import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { ISearchStart } from '@kbn/data-plugin/public';
import React from 'react';
import type { Document } from './persistence/saved_object_store';
import {
Datasource,
@ -285,3 +288,36 @@ export const isOperationFromTheSameGroup = (op1?: DraggingIdentifier, op2?: Drag
op1.layerId === op2.layerId
);
};
export const getSearchWarningMessages = (
adapter: RequestAdapter,
datasource: Datasource,
state: unknown,
deps: {
searchService: ISearchStart;
}
) => {
const warningsMap: Map<string, Array<string | React.ReactNode>> = new Map();
deps.searchService.showWarnings(adapter, (warning, meta) => {
const { request, response, requestId } = meta;
const warningMessages = datasource.getSearchWarningMessages?.(
state,
warning,
request,
response
);
if (warningMessages?.length) {
const key = (requestId ?? '') + warning.type + warning.reason?.type ?? '';
if (!warningsMap.has(key)) {
warningsMap.set(key, warningMessages);
}
return true;
}
return false;
});
return [...warningsMap.values()].flat();
};

View file

@ -9,10 +9,8 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const queryBar = getService('queryBar');
const PageObjects = getPageObjects(['common', 'timePicker', 'lens', 'dashboard']);
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const es = getService('es');
const log = getService('log');
const indexPatterns = getService('indexPatterns');
@ -130,27 +128,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'Median of kubernetes.container.memory.available.bytes uses a function that is unsupported by rolled up data. Select a different function or change the time range.'
);
});
it('still shows other warnings as toast', async () => {
await es.indices.delete({ index: [testRollupIndex] });
// index a document which will produce a shard failure because a string field doesn't support median
await es.create({
id: '1',
index: testRollupIndex,
document: {
'kubernetes.container.memory.available.bytes': 'fsdfdsf',
'@timestamp': '2022-06-20',
},
wait_for_active_shards: 1,
});
await retry.try(async () => {
await queryBar.clickQuerySubmitButton();
expect(
await (await testSubjects.find('euiToastHeader__title', 1000)).getVisibleText()
).to.equal('1 of 3 shards failed');
});
// as the rollup index is gone, there is no inline warning left
await PageObjects.lens.assertNoInlineWarning();
});
});
});
}