[Upgrade Assistant] Support Kibana deprecations (#97159) (#98911)

This commit is contained in:
Alison Goryachev 2021-04-30 11:03:21 -04:00 committed by GitHub
parent 2837125175
commit 92ead83fcb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 2284 additions and 959 deletions

View file

@ -5314,7 +5314,10 @@
"deprecation_logging": {
"properties": {
"enabled": {
"type": "boolean"
"type": "boolean",
"_meta": {
"description": "Whether user has enabled Elasticsearch deprecation logging"
}
}
}
}
@ -5323,13 +5326,28 @@
"ui_open": {
"properties": {
"cluster": {
"type": "long"
"type": "long",
"_meta": {
"description": "Number of times a user viewed the list of Elasticsearch cluster deprecations."
}
},
"indices": {
"type": "long"
"type": "long",
"_meta": {
"description": "Number of times a user viewed the list of Elasticsearch index deprecations."
}
},
"overview": {
"type": "long"
"type": "long",
"_meta": {
"description": "Number of times a user viewed the Overview page."
}
},
"kibana": {
"type": "long",
"_meta": {
"description": "Number of times a user viewed the list of Kibana deprecations"
}
}
}
},

View file

@ -22111,15 +22111,9 @@
"xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "無効なフォーマット。例:{exampleUrl}",
"xpack.upgradeAssistant.appTitle": "{version} アップグレードアシスタント",
"xpack.upgradeAssistant.checkupTab.changeFiltersShowMoreLabel": "より多く表示させるにはフィルターを変更します。",
"xpack.upgradeAssistant.checkupTab.controls.collapseAllButtonLabel": "すべて縮小",
"xpack.upgradeAssistant.checkupTab.controls.expandAllButtonLabel": "すべて拡張",
"xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel": "致命的",
"xpack.upgradeAssistant.checkupTab.controls.filterErrorMessageLabel": "フィルター無効:{searchTermError}",
"xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIndexLabel": "インデックス別",
"xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel": "問題別",
"xpack.upgradeAssistant.checkupTab.controls.refreshButtonLabel": "更新",
"xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholder": "フィルター",
"xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholderAriaLabel": "フィルター",
"xpack.upgradeAssistant.checkupTab.deprecations.criticalActionTooltip": "アップグレード前にこの問題を解決してください。",
"xpack.upgradeAssistant.checkupTab.deprecations.criticalLabel": "致命的",
"xpack.upgradeAssistant.checkupTab.deprecations.documentationButtonLabel": "ドキュメント",
@ -22128,9 +22122,6 @@
"xpack.upgradeAssistant.checkupTab.deprecations.warningActionTooltip": "アップグレード前にこの問題を解決することをお勧めしますが、必須ではありません。",
"xpack.upgradeAssistant.checkupTab.deprecations.warningLabel": "警告",
"xpack.upgradeAssistant.checkupTab.noDeprecationsLabel": "説明がありません",
"xpack.upgradeAssistant.checkupTab.noIssues.nextStepsDetail": "{overviewTabButton} で次のステップを確認してください。",
"xpack.upgradeAssistant.checkupTab.noIssues.nextStepsDetail.overviewTabButtonLabel": "概要タブ",
"xpack.upgradeAssistant.checkupTab.noIssues.noIssuesTitle": "完璧です!",
"xpack.upgradeAssistant.checkupTab.numDeprecationsShownLabel": "{total} 件中 {numShown} 件を表示中",
"xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.cancelButtonLabel": "キャンセル",
"xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.closeButtonLabel": "閉じる",

View file

@ -22462,15 +22462,9 @@
"xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage": "格式无效。例如:{exampleUrl}",
"xpack.upgradeAssistant.appTitle": "{version} 升级助手",
"xpack.upgradeAssistant.checkupTab.changeFiltersShowMoreLabel": "更改筛选以显示更多内容。",
"xpack.upgradeAssistant.checkupTab.controls.collapseAllButtonLabel": "折叠全部",
"xpack.upgradeAssistant.checkupTab.controls.expandAllButtonLabel": "展开全部",
"xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel": "紧急",
"xpack.upgradeAssistant.checkupTab.controls.filterErrorMessageLabel": "筛选无效:{searchTermError}",
"xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIndexLabel": "按索引",
"xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel": "按问题",
"xpack.upgradeAssistant.checkupTab.controls.refreshButtonLabel": "刷新",
"xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholder": "筛选",
"xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholderAriaLabel": "筛选",
"xpack.upgradeAssistant.checkupTab.deprecations.criticalActionTooltip": "请解决此问题后再升级。",
"xpack.upgradeAssistant.checkupTab.deprecations.criticalLabel": "紧急",
"xpack.upgradeAssistant.checkupTab.deprecations.documentationButtonLabel": "文档",
@ -22480,9 +22474,6 @@
"xpack.upgradeAssistant.checkupTab.deprecations.warningLabel": "警告",
"xpack.upgradeAssistant.checkupTab.indicesBadgeLabel": "{numIndices, plural, other { 个索引}}",
"xpack.upgradeAssistant.checkupTab.noDeprecationsLabel": "无弃用内容",
"xpack.upgradeAssistant.checkupTab.noIssues.nextStepsDetail": "选中 {overviewTabButton} 以执行后续步骤。",
"xpack.upgradeAssistant.checkupTab.noIssues.nextStepsDetail.overviewTabButtonLabel": "“概述”选项卡",
"xpack.upgradeAssistant.checkupTab.noIssues.noIssuesTitle": "全部清除!",
"xpack.upgradeAssistant.checkupTab.numDeprecationsShownLabel": "显示 {numShown} 个,共 {total} 个",
"xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.cancelButtonLabel": "取消",
"xpack.upgradeAssistant.checkupTab.reindexing.flyout.checklistStep.closeButtonLabel": "关闭",

View file

@ -117,13 +117,14 @@ export enum IndexGroup {
// Telemetry types
export const UPGRADE_ASSISTANT_TYPE = 'upgrade-assistant-telemetry';
export const UPGRADE_ASSISTANT_DOC_ID = 'upgrade-assistant-telemetry';
export type UIOpenOption = 'overview' | 'cluster' | 'indices';
export type UIOpenOption = 'overview' | 'cluster' | 'indices' | 'kibana';
export type UIReindexOption = 'close' | 'open' | 'start' | 'stop';
export interface UIOpen {
overview: boolean;
cluster: boolean;
indices: boolean;
kibana: boolean;
}
export interface UIReindex {
@ -138,6 +139,7 @@ export interface UpgradeAssistantTelemetrySavedObject {
overview: number;
cluster: number;
indices: number;
kibana: number;
};
ui_reindex: {
close: number;
@ -152,6 +154,7 @@ export interface UpgradeAssistantTelemetry {
overview: number;
cluster: number;
indices: number;
kibana: number;
};
ui_reindex: {
close: number;

View file

@ -11,6 +11,7 @@ import { I18nStart, ScopedHistory } from 'src/core/public';
import { AppContextProvider, ContextValue, useAppContext } from './app_context';
import { ComingSoonPrompt } from './components/coming_soon_prompt';
import { EsDeprecationsContent } from './components/es_deprecations';
import { KibanaDeprecationsContent } from './components/kibana_deprecations';
import { DeprecationsOverview } from './components/overview';
export interface AppDependencies extends ContextValue {
@ -30,6 +31,7 @@ const App: React.FunctionComponent = () => {
<Switch>
<Route exact path="/overview" component={DeprecationsOverview} />
<Route exact path="/es_deprecations/:tabName" component={EsDeprecationsContent} />
<Route exact path="/kibana_deprecations" component={KibanaDeprecationsContent} />
<Redirect from="/" to="/overview" />
</Switch>
);

View file

@ -5,7 +5,13 @@
* 2.0.
*/
import { CoreStart, DocLinksStart, HttpSetup, NotificationsStart } from 'src/core/public';
import {
CoreStart,
DeprecationsServiceStart,
DocLinksStart,
HttpSetup,
NotificationsStart,
} from 'src/core/public';
import React, { createContext, useContext } from 'react';
import { ApiService } from './lib/api';
import { BreadcrumbService } from './lib/breadcrumbs';
@ -26,6 +32,7 @@ export interface ContextValue {
api: ApiService;
breadcrumbs: BreadcrumbService;
getUrlForApp: CoreStart['application']['getUrlForApp'];
deprecations: DeprecationsServiceStart;
}
export const AppContext = createContext<ContextValue>({} as any);

View file

@ -7,7 +7,7 @@
import { IconColor } from '@elastic/eui';
import { invert } from 'lodash';
import { DeprecationInfo } from '../../../../common/types';
import { DeprecationInfo } from '../../../common/types';
export const LEVEL_MAP: { [level: string]: number } = {
warning: 0,
@ -24,3 +24,5 @@ export const COLOR_MAP: { [level: string]: IconColor } = {
warning: 'default',
critical: 'danger',
};
export const DEPRECATIONS_PER_PAGE = 25;

View file

@ -1,108 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { DeprecationInfo } from '../../../../common/types';
import { validateRegExpString } from '../../lib/utils';
import { GroupByOption, LevelFilterOption } from '../types';
import { FilterBar } from './filter_bar';
import { GroupByBar } from './group_by_bar';
interface CheckupControlsProps {
allDeprecations?: DeprecationInfo[];
isLoading: boolean;
loadData: () => void;
currentFilter: LevelFilterOption;
onFilterChange: (filter: LevelFilterOption) => void;
onSearchChange: (filter: string) => void;
availableGroupByOptions: GroupByOption[];
currentGroupBy: GroupByOption;
onGroupByChange: (groupBy: GroupByOption) => void;
}
export const CheckupControls: FunctionComponent<CheckupControlsProps> = ({
allDeprecations,
isLoading,
loadData,
currentFilter,
onFilterChange,
onSearchChange,
availableGroupByOptions,
currentGroupBy,
onGroupByChange,
}) => {
const [searchTermError, setSearchTermError] = useState<null | string>(null);
const filterInvalid = Boolean(searchTermError);
return (
<EuiFlexGroup direction="column" responsive={false}>
<EuiFlexItem grow={true}>
<EuiFlexGroup alignItems="center" wrap={true} responsive={false}>
<EuiFlexItem>
<EuiFieldSearch
isInvalid={filterInvalid}
aria-label={i18n.translate(
'xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholderAriaLabel',
{ defaultMessage: 'Filter' }
)}
placeholder={i18n.translate(
'xpack.upgradeAssistant.checkupTab.controls.searchBarPlaceholder',
{
defaultMessage: 'Filter',
}
)}
onChange={(e) => {
const string = e.target.value;
const errorMessage = validateRegExpString(string);
if (errorMessage) {
// Emit an empty search term to listeners if search term is invalid.
onSearchChange('');
setSearchTermError(errorMessage);
} else {
onSearchChange(e.target.value);
if (searchTermError) {
setSearchTermError(null);
}
}
}}
/>
</EuiFlexItem>
{/* These two components provide their own EuiFlexItem wrappers */}
<FilterBar {...{ allDeprecations, currentFilter, onFilterChange }} />
<GroupByBar {...{ availableGroupByOptions, currentGroupBy, onGroupByChange }} />
<EuiFlexItem grow={false}>
<EuiButton fill onClick={loadData} iconType="refresh" isLoading={isLoading}>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.controls.refreshButtonLabel"
defaultMessage="Refresh"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{filterInvalid && (
<EuiFlexItem grow={false}>
<EuiCallOut
color="danger"
title={i18n.translate(
'xpack.upgradeAssistant.checkupTab.controls.filterErrorMessageLabel',
{
defaultMessage: 'Filter invalid: {searchTermError}',
values: { searchTermError },
}
)}
iconType="faceSad"
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};

View file

@ -5,18 +5,24 @@
* 2.0.
*/
import { find } from 'lodash';
import React, { FunctionComponent, useState } from 'react';
import { EuiEmptyPrompt, EuiLink, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { find, groupBy } from 'lodash';
import React, { FunctionComponent, useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiHorizontalRule } from '@elastic/eui';
import { EnrichedDeprecationInfo } from '../../../../common/types';
import { SectionLoading } from '../../../shared_imports';
import { GroupByOption, LevelFilterOption, UpgradeAssistantTabProps } from '../types';
import { CheckupControls } from './controls';
import { GroupedDeprecations } from './deprecations/grouped';
import {
NoDeprecationsPrompt,
SearchBar,
DeprecationPagination,
DeprecationListBar,
} from '../shared';
import { DEPRECATIONS_PER_PAGE } from '../constants';
import { EsDeprecationErrors } from './es_deprecation_errors';
import { EsDeprecationAccordion } from './deprecations';
const i18nTexts = {
isLoading: i18n.translate('xpack.upgradeAssistant.esDeprecations.loadingText', {
@ -28,6 +34,54 @@ export interface CheckupTabProps extends UpgradeAssistantTabProps {
checkupLabel: string;
}
export const createDependenciesFilter = (level: LevelFilterOption, search: string = '') => {
const conditions: Array<(dep: EnrichedDeprecationInfo) => boolean> = [];
if (level !== 'all') {
conditions.push((dep: EnrichedDeprecationInfo) => dep.level === level);
}
if (search.length > 0) {
conditions.push((dep) => {
try {
// 'i' is used for case-insensitive matching
const searchReg = new RegExp(search, 'i');
return searchReg.test(dep.message);
} catch (e) {
// ignore any regexp errors.
return true;
}
});
}
// Return true if every condition function returns true (boolean AND)
return (dep: EnrichedDeprecationInfo) => conditions.map((c) => c(dep)).every((t) => t);
};
const filterDeprecations = (
deprecations: EnrichedDeprecationInfo[] = [],
currentFilter: LevelFilterOption,
search: string
) => deprecations.filter(createDependenciesFilter(currentFilter, search));
const groupDeprecations = (
deprecations: EnrichedDeprecationInfo[],
currentFilter: LevelFilterOption,
search: string,
currentGroupBy: GroupByOption
) => groupBy(filterDeprecations(deprecations, currentFilter, search), currentGroupBy);
const getPageCount = (
deprecations: EnrichedDeprecationInfo[],
currentFilter: LevelFilterOption,
search: string,
currentGroupBy: GroupByOption
) =>
Math.ceil(
Object.keys(groupDeprecations(deprecations, currentFilter, search, currentGroupBy)).length /
DEPRECATIONS_PER_PAGE
);
/**
* Displays a list of deprecations that are filterable and groupable. Can be used for cluster,
* nodes, or indices deprecations.
@ -40,11 +94,16 @@ export const DeprecationTabContent: FunctionComponent<CheckupTabProps> = ({
refreshCheckupData,
navigateToOverviewPage,
}) => {
const [currentFilter, setCurrentFilter] = useState<LevelFilterOption>(LevelFilterOption.all);
const [currentFilter, setCurrentFilter] = useState<LevelFilterOption>('all');
const [search, setSearch] = useState<string>('');
const [currentGroupBy, setCurrentGroupBy] = useState<GroupByOption>(GroupByOption.message);
const [expandState, setExpandState] = useState({
forceExpand: false,
expandNumber: 0,
});
const [currentPage, setCurrentPage] = useState(0);
const availableGroupByOptions = () => {
const getAvailableGroupByOptions = () => {
if (!deprecations) {
return [];
}
@ -52,46 +111,28 @@ export const DeprecationTabContent: FunctionComponent<CheckupTabProps> = ({
return Object.keys(GroupByOption).filter((opt) => find(deprecations, opt)) as GroupByOption[];
};
const setExpandAll = (expandAll: boolean) => {
setExpandState({ forceExpand: expandAll, expandNumber: expandState.expandNumber + 1 });
};
useEffect(() => {
if (deprecations) {
const pageCount = getPageCount(deprecations, currentFilter, search, currentGroupBy);
if (currentPage >= pageCount) {
setCurrentPage(0);
}
}
}, [currentPage, deprecations, currentFilter, search, currentGroupBy]);
if (deprecations && deprecations.length === 0) {
return (
<EuiEmptyPrompt
iconType="faceHappy"
data-test-subj="noDeprecationsPrompt"
title={
<h2>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.noIssues.noIssuesTitle"
defaultMessage="Ready to upgrade!"
/>
</h2>
}
body={
<>
<p data-test-subj="upgradeAssistantIssueSummary">
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.noIssues.noIssuesLabel"
defaultMessage="Your configuration is up to date."
/>
</p>
<p>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.noIssues.nextStepsDetail"
defaultMessage="Check the {overviewTabButton} for other Stack deprecations."
values={{
overviewTabButton: (
<EuiLink onClick={navigateToOverviewPage}>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.noIssues.nextStepsDetail.overviewTabButtonLabel"
defaultMessage="Overview page"
/>
</EuiLink>
),
}}
/>
</p>
</>
}
/>
<div data-test-subj={`${checkupLabel}TabContent`}>
<NoDeprecationsPrompt
deprecationType={checkupLabel}
navigateToOverviewPage={navigateToOverviewPage}
/>
</div>
);
}
@ -100,28 +141,77 @@ export const DeprecationTabContent: FunctionComponent<CheckupTabProps> = ({
if (isLoading) {
content = <SectionLoading>{i18nTexts.isLoading}</SectionLoading>;
} else if (deprecations?.length) {
const levelGroups = groupBy(deprecations, 'level');
const levelToDeprecationCountMap = Object.keys(levelGroups).reduce((counts, level) => {
counts[level] = levelGroups[level].length;
return counts;
}, {} as Record<string, number>);
const filteredDeprecations = filterDeprecations(deprecations, currentFilter, search);
const groups = groupDeprecations(deprecations, currentFilter, search, currentGroupBy);
content = (
<div data-test-subj="deprecationsContainer">
<CheckupControls
<SearchBar
allDeprecations={deprecations}
isLoading={isLoading}
loadData={refreshCheckupData}
currentFilter={currentFilter}
onFilterChange={setCurrentFilter}
onSearchChange={setSearch}
availableGroupByOptions={availableGroupByOptions()}
currentGroupBy={currentGroupBy}
onGroupByChange={setCurrentGroupBy}
totalDeprecationsCount={deprecations.length}
levelToDeprecationCountMap={levelToDeprecationCountMap}
groupByFilterProps={{
availableGroupByOptions: getAvailableGroupByOptions(),
currentGroupBy,
onGroupByChange: setCurrentGroupBy,
}}
/>
<EuiSpacer />
<GroupedDeprecations
currentGroupBy={currentGroupBy}
currentFilter={currentFilter}
search={search}
allDeprecations={deprecations}
<DeprecationListBar
allDeprecationsCount={deprecations.length}
filteredDeprecationsCount={filteredDeprecations.length}
setExpandAll={setExpandAll}
/>
<EuiHorizontalRule margin="m" />
<>
{Object.keys(groups)
.sort()
// Apply pagination
.slice(currentPage * DEPRECATIONS_PER_PAGE, (currentPage + 1) * DEPRECATIONS_PER_PAGE)
.map((groupName, index) => [
<div key={`es-deprecation-${index}`}>
<EsDeprecationAccordion
{...{
key: expandState.expandNumber,
id: `depgroup-${groupName}`,
dataTestSubj: `depgroup_${groupName.split(' ').join('_')}`,
title: groupName,
deprecations: groups[groupName],
currentGroupBy,
forceExpand: expandState.forceExpand,
}}
/>
<EuiHorizontalRule margin="s" />
</div>,
])}
{/* Only show pagination if we have more than DEPRECATIONS_PER_PAGE. */}
{Object.keys(groups).length > DEPRECATIONS_PER_PAGE && (
<>
<EuiSpacer />
<DeprecationPagination
pageCount={getPageCount(deprecations, currentFilter, search, currentGroupBy)}
activePage={currentPage}
setPage={setCurrentPage}
/>
</>
)}
</>
</div>
);
} else if (error) {

View file

@ -1,18 +0,0 @@
.upgDeprecations {
// Pull the container through the padding of EuiPageContent
margin-left: -$euiSizeL;
margin-right: -$euiSizeL;
}
.upgDeprecations__item {
padding: $euiSize $euiSizeL;
border-top: $euiBorderThin;
&:last-of-type {
margin-bottom: -$euiSizeL;
}
}
.upgDeprecations__itemName {
font-weight: $euiFontWeightMedium;
}

View file

@ -1,3 +1,2 @@
@import 'cell';
@import 'deprecations';
@import 'reindex/index';

View file

@ -63,26 +63,25 @@ export const DeprecationCell: FunctionComponent<DeprecationCellProps> = ({
</EuiTitle>
)}
{items.map((item, index) => (
<EuiText key={`deprecation-item-${index}`}>
{item.title && <h6>{item.title}</h6>}
<p>{item.body}</p>
</EuiText>
))}
{docUrl && (
<div>
<>
<EuiSpacer size="s" />
<EuiLink href={docUrl} target="_blank">
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.deprecations.documentationButtonLabel"
defaultMessage="Documentation"
/>
</EuiLink>
<EuiSpacer size="s" />
</div>
</>
)}
{items.map((item) => (
<div key={item.title || item.body}>
<EuiText>
{item.title && <h6>{item.title}</h6>}
<p>{item.body}</p>
</EuiText>
</div>
))}
</EuiFlexItem>
{reindex && (

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent } from 'react';
import { EuiAccordion, EuiBadge } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EnrichedDeprecationInfo } from '../../../../../common/types';
import { DeprecationHealth } from '../../shared';
import { GroupByOption } from '../../types';
import { EsDeprecationList } from './list';
import { LEVEL_MAP } from '../../constants';
export interface Props {
id: string;
deprecations: EnrichedDeprecationInfo[];
title: string;
currentGroupBy: GroupByOption;
forceExpand: boolean;
dataTestSubj: string;
}
/**
* A single accordion item for a grouped deprecation item.
*/
export const EsDeprecationAccordion: FunctionComponent<Props> = ({
id,
deprecations,
title,
currentGroupBy,
forceExpand,
dataTestSubj,
}) => {
const hasIndices = Boolean(
currentGroupBy === GroupByOption.message &&
(deprecations as EnrichedDeprecationInfo[]).filter((d) => d.index).length
);
const numIndices = hasIndices ? deprecations.length : null;
return (
<EuiAccordion
id={id}
key={id}
data-test-subj={dataTestSubj}
initialIsOpen={forceExpand}
buttonContent={title}
extraAction={
<div>
{hasIndices && (
<>
<EuiBadge color="hollow">
<span data-test-subj="indexCount">{numIndices}</span>{' '}
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.indicesBadgeLabel"
defaultMessage="{numIndices, plural, one {index} other {indices}}"
values={{ numIndices }}
/>
</EuiBadge>
&emsp;
</>
)}
<DeprecationHealth
single={currentGroupBy === GroupByOption.message}
deprecationLevels={deprecations.map((d) => LEVEL_MAP[d.level])}
/>
</div>
}
>
<EsDeprecationList deprecations={deprecations} currentGroupBy={currentGroupBy} />
</EuiAccordion>
);
};

View file

@ -1,215 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { range } from 'lodash';
import React from 'react';
import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest';
import { EuiBadge, EuiPagination } from '@elastic/eui';
import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../common/types';
import { GroupByOption, LevelFilterOption } from '../../types';
import { DeprecationAccordion, filterDeps, GroupedDeprecations } from './grouped';
describe('filterDeps', () => {
test('filters on levels', () => {
const fd = filterDeps(LevelFilterOption.critical);
expect(fd({ level: 'critical' } as DeprecationInfo)).toBe(true);
expect(fd({ level: 'warning' } as DeprecationInfo)).toBe(false);
});
test('filters on title search', () => {
const fd = filterDeps(LevelFilterOption.critical, 'wow');
expect(fd({ level: 'critical', message: 'the wow error' } as DeprecationInfo)).toBe(true);
expect(fd({ level: 'critical', message: 'other error' } as DeprecationInfo)).toBe(false);
});
test('filters on index search', () => {
const fd = filterDeps(LevelFilterOption.critical, 'myIndex');
expect(
fd({
level: 'critical',
message: 'the wow error',
index: 'myIndex-2',
} as EnrichedDeprecationInfo)
).toBe(true);
expect(
fd({
level: 'critical',
message: 'other error',
index: 'notIndex',
} as EnrichedDeprecationInfo)
).toBe(false);
});
test('filters on node search', () => {
const fd = filterDeps(LevelFilterOption.critical, 'myNode');
expect(
fd({
level: 'critical',
message: 'the wow error',
index: 'myNode-123',
} as EnrichedDeprecationInfo)
).toBe(true);
expect(
fd({
level: 'critical',
message: 'other error',
index: 'notNode',
} as EnrichedDeprecationInfo)
).toBe(false);
});
});
describe('GroupedDeprecations', () => {
const defaultProps = {
currentFilter: LevelFilterOption.all,
search: '',
currentGroupBy: GroupByOption.message,
allDeprecations: [
{ message: 'Cluster error 1', url: '', level: 'warning' },
{ message: 'Cluster error 2', url: '', level: 'critical' },
] as EnrichedDeprecationInfo[],
};
describe('expand + collapse all', () => {
const expectNumOpen = (wrapper: any, numExpected: number) =>
expect(wrapper.find('div.euiAccordion-isOpen')).toHaveLength(numExpected);
test('clicking opens and closes panels', () => {
const wrapper = mountWithIntl(<GroupedDeprecations {...defaultProps} />);
expectNumOpen(wrapper, 0);
// Test expand all
wrapper.find('button[data-test-subj="expandAll"]').simulate('click');
expectNumOpen(wrapper, 2);
// Test collapse all
wrapper.find('button[data-test-subj="collapseAll"]').simulate('click');
expectNumOpen(wrapper, 0);
});
test('clicking overrides current state when some are open', () => {
const wrapper = mountWithIntl(<GroupedDeprecations {...defaultProps} />);
// Open a single deprecation
wrapper.find('button.euiAccordion__button').first().simulate('click');
expectNumOpen(wrapper, 1);
// Test expand all
wrapper.find('button[data-test-subj="expandAll"]').simulate('click');
expectNumOpen(wrapper, 2);
// Close a single deprecation
wrapper.find('button.euiAccordion__button').first().simulate('click');
expectNumOpen(wrapper, 1);
// Test collapse all
wrapper.find('button[data-test-subj="collapseAll"]').simulate('click');
expectNumOpen(wrapper, 0);
});
});
describe('pagination', () => {
const paginationProps = {
...defaultProps,
allDeprecations: range(0, 40).map((i) => ({
message: `Message ${i}`,
level: 'warning',
})) as DeprecationInfo[],
};
test('it only displays 25 items', () => {
const wrapper = shallowWithIntl(<GroupedDeprecations {...paginationProps} />);
expect(wrapper.find(DeprecationAccordion)).toHaveLength(25);
});
test('it displays pagination', () => {
const wrapper = shallowWithIntl(<GroupedDeprecations {...paginationProps} />);
expect(wrapper.find(EuiPagination).exists()).toBe(true);
});
test('shows next page on click', () => {
const wrapper = mountWithIntl(<GroupedDeprecations {...paginationProps} />);
wrapper.find('button[data-test-subj="pagination-button-next"]').simulate('click');
expect(wrapper.find(DeprecationAccordion)).toHaveLength(15); // 40 total - 25 first page = 15 second page
});
});
describe('grouping', () => {
test('group by message', () => {
const wrapper = shallowWithIntl(
<GroupedDeprecations
{...defaultProps}
currentGroupBy={GroupByOption.message}
allDeprecations={[
{ message: 'Cluster error 1', url: '', level: 'warning' },
{ message: 'Cluster error 2', url: '', level: 'warning' },
{ message: 'Cluster error 2', url: '', level: 'warning' },
{ message: 'Cluster error 2', url: '', level: 'warning' },
]}
/>
);
// Only 2 groups should exist b/c there are only 2 unique messages
expect(wrapper.find(DeprecationAccordion)).toHaveLength(2);
});
test('group by index', () => {
const wrapper = shallowWithIntl(
<GroupedDeprecations
{...defaultProps}
currentGroupBy={GroupByOption.index}
allDeprecations={[
{
message: 'Cluster error 1',
url: '',
level: 'warning',
index: 'index1',
},
{
message: 'Cluster error 2',
url: '',
level: 'warning',
index: 'index1',
},
{
message: 'Cluster error 2',
url: '',
level: 'warning',
index: 'index2',
},
{
message: 'Cluster error 2',
url: '',
level: 'warning',
index: 'index3',
},
]}
/>
);
// Only 3 groups should exist b/c there are only 3 unique indexes
expect(wrapper.find(DeprecationAccordion)).toHaveLength(3);
});
});
});
describe('DeprecationAccordion', () => {
const defaultProps = {
id: 'x',
dataTestSubj: 'data-test-subj',
title: 'Issue 1',
currentGroupBy: GroupByOption.message,
forceExpand: false,
deprecations: [{ index: 'index1' }, { index: 'index2' }] as EnrichedDeprecationInfo[],
};
test('shows indices count badge', () => {
const wrapper = mountWithIntl(<DeprecationAccordion {...defaultProps} />);
expect(wrapper.find(EuiBadge).find('[data-test-subj="indexCount"]').text()).toEqual('2');
});
});

View file

@ -1,261 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { groupBy } from 'lodash';
import React, { Fragment, FunctionComponent } from 'react';
import {
EuiAccordion,
EuiBadge,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiPagination,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../common/types';
import { GroupByOption, LevelFilterOption } from '../../types';
import { DeprecationCountSummary } from './count_summary';
import { DeprecationHealth } from './health';
import { DeprecationList } from './list';
// exported only for testing
export const filterDeps = (level: LevelFilterOption, search: string = '') => {
const conditions: Array<(dep: EnrichedDeprecationInfo) => boolean> = [];
if (level !== LevelFilterOption.all) {
conditions.push((dep: DeprecationInfo) => dep.level === level);
}
if (search.length > 0) {
// Change everything to lower case for a case-insensitive comparison
conditions.push((dep) => {
try {
const searchReg = new RegExp(search.toLowerCase());
return Boolean(
dep.message.toLowerCase().match(searchReg) ||
(dep.details && dep.details.toLowerCase().match(searchReg)) ||
(dep.index && dep.index.toLowerCase().match(searchReg)) ||
(dep.node && dep.node.toLowerCase().match(searchReg))
);
} catch (e) {
// ignore any regexp errors.
return true;
}
});
}
// Return true if every condition function returns true (boolean AND)
return (dep: EnrichedDeprecationInfo) => conditions.map((c) => c(dep)).every((t) => t);
};
/**
* A single accordion item for a grouped deprecation item.
*/
export const DeprecationAccordion: FunctionComponent<{
id: string;
deprecations: EnrichedDeprecationInfo[];
title: string;
currentGroupBy: GroupByOption;
forceExpand: boolean;
dataTestSubj: string;
}> = ({ id, deprecations, title, currentGroupBy, forceExpand, dataTestSubj }) => {
const hasIndices = Boolean(
currentGroupBy === GroupByOption.message && deprecations.filter((d) => d.index).length
);
const numIndices = hasIndices ? deprecations.length : null;
return (
<EuiAccordion
id={id}
data-test-subj={dataTestSubj}
className="upgDeprecations__item"
initialIsOpen={forceExpand}
buttonContent={<span className="upgDeprecations__itemName">{title}</span>}
extraAction={
<div>
{hasIndices && (
<Fragment>
<EuiBadge color="hollow">
<span data-test-subj="indexCount">{numIndices}</span>{' '}
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.indicesBadgeLabel"
defaultMessage="{numIndices, plural, one {index} other {indices}}"
values={{ numIndices }}
/>
</EuiBadge>
&emsp;
</Fragment>
)}
<DeprecationHealth
single={currentGroupBy === GroupByOption.message}
deprecations={deprecations}
/>
</div>
}
>
<DeprecationList deprecations={deprecations} currentGroupBy={currentGroupBy} />
</EuiAccordion>
);
};
interface GroupedDeprecationsProps {
currentFilter: LevelFilterOption;
search: string;
currentGroupBy: GroupByOption;
allDeprecations?: EnrichedDeprecationInfo[];
}
interface GroupedDeprecationsState {
forceExpand: true | false | null;
expandNumber: number;
currentPage: number;
}
const PER_PAGE = 25;
/**
* Collection of calculated fields based on props, extracted for reuse in
* `render` and `getDerivedStateFromProps`.
*/
const CalcFields = {
filteredDeprecations(props: GroupedDeprecationsProps) {
const { allDeprecations = [], currentFilter, search } = props;
return allDeprecations.filter(filterDeps(currentFilter, search));
},
groups(props: GroupedDeprecationsProps) {
const { currentGroupBy } = props;
return groupBy(CalcFields.filteredDeprecations(props), currentGroupBy);
},
numPages(props: GroupedDeprecationsProps) {
return Math.ceil(Object.keys(CalcFields.groups(props)).length / PER_PAGE);
},
};
/**
* Displays groups of deprecation messages in an accordion.
*/
export class GroupedDeprecations extends React.Component<
GroupedDeprecationsProps,
GroupedDeprecationsState
> {
public static getDerivedStateFromProps(
nextProps: GroupedDeprecationsProps,
{ currentPage }: GroupedDeprecationsState
) {
// If filters change and the currentPage is now bigger than the num of pages we're going to show,
// reset the current page to 0.
if (currentPage >= CalcFields.numPages(nextProps)) {
return { currentPage: 0 };
} else {
return null;
}
}
public state = {
forceExpand: false,
// `expandNumber` is used as workaround to force EuiAccordion to re-render by
// incrementing this number (used as a key) when expand all or collapse all is clicked.
expandNumber: 0,
currentPage: 0,
};
public render() {
const { currentGroupBy, allDeprecations = [] } = this.props;
const { forceExpand, expandNumber, currentPage } = this.state;
const filteredDeprecations = CalcFields.filteredDeprecations(this.props);
const groups = CalcFields.groups(this.props);
return (
<Fragment>
<EuiFlexGroup responsive={false} alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
flush="left"
size="s"
onClick={() => this.setExpand(true)}
data-test-subj="expandAll"
>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.controls.expandAllButtonLabel"
defaultMessage="Expand all"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
flush="left"
size="s"
onClick={() => this.setExpand(false)}
data-test-subj="collapseAll"
>
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.controls.collapseAllButtonLabel"
defaultMessage="Collapse all"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem />
<EuiFlexItem grow={false}>
<DeprecationCountSummary
allDeprecations={allDeprecations}
deprecations={filteredDeprecations}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<div className="upgDeprecations">
{Object.keys(groups)
.sort()
// Apply pagination
.slice(currentPage * PER_PAGE, (currentPage + 1) * PER_PAGE)
.map((groupName) => [
<DeprecationAccordion
key={expandNumber}
id={`depgroup-${groupName}`}
dataTestSubj={`depgroup_${groupName.split(' ').join('_')}`}
title={groupName}
deprecations={groups[groupName]}
{...{ currentGroupBy, forceExpand }}
/>,
])}
{/* Only show pagination if we have more than PER_PAGE. */}
{Object.keys(groups).length > PER_PAGE && (
<Fragment>
<EuiSpacer />
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiPagination
pageCount={CalcFields.numPages(this.props)}
activePage={currentPage}
onPageClick={this.setPage}
/>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
)}
</div>
</Fragment>
);
}
private setExpand = (forceExpand: boolean) => {
this.setState({ forceExpand, expandNumber: this.state.expandNumber + 1 });
};
private setPage = (currentPage: number) => this.setState({ currentPage });
}

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { GroupedDeprecations } from './grouped';
export { EsDeprecationAccordion } from './deprecation_group_item';

View file

@ -10,9 +10,9 @@ import React from 'react';
import { EnrichedDeprecationInfo } from '../../../../../common/types';
import { GroupByOption } from '../../types';
import { DeprecationList } from './list';
import { EsDeprecationList } from './list';
describe('DeprecationList', () => {
describe('EsDeprecationList', () => {
describe('group by message', () => {
const defaultProps = {
deprecations: [
@ -23,7 +23,7 @@ describe('DeprecationList', () => {
};
test('shows simple messages when index field is not present', () => {
expect(shallow(<DeprecationList {...defaultProps} />)).toMatchInlineSnapshot(`
expect(shallow(<EsDeprecationList {...defaultProps} />)).toMatchInlineSnapshot(`
<div>
<SimpleMessageDeprecation
deprecation={
@ -33,7 +33,7 @@ describe('DeprecationList', () => {
"url": "",
}
}
key="Issue 1"
key="Issue 1-0"
/>
<SimpleMessageDeprecation
deprecation={
@ -43,7 +43,7 @@ describe('DeprecationList', () => {
"url": "",
}
}
key="Issue 1"
key="Issue 1-1"
/>
</div>
`);
@ -58,7 +58,7 @@ describe('DeprecationList', () => {
index: index.toString(),
})),
};
const wrapper = shallow(<DeprecationList {...props} />);
const wrapper = shallow(<EsDeprecationList {...props} />);
expect(wrapper).toMatchInlineSnapshot(`
<IndexDeprecation
deprecation={
@ -102,7 +102,7 @@ describe('DeprecationList', () => {
};
test('shows detailed messages', () => {
expect(shallow(<DeprecationList {...defaultProps} />)).toMatchInlineSnapshot(`
expect(shallow(<EsDeprecationList {...defaultProps} />)).toMatchInlineSnapshot(`
<div>
<MessageDeprecation
deprecation={
@ -113,7 +113,7 @@ describe('DeprecationList', () => {
"url": "",
}
}
key="Issue 1"
key="Issue 1-0"
/>
<MessageDeprecation
deprecation={
@ -124,7 +124,7 @@ describe('DeprecationList', () => {
"url": "",
}
}
key="Issue 2"
key="Issue 2-1"
/>
</div>
`);

View file

@ -10,7 +10,7 @@ import React, { FunctionComponent } from 'react';
import { DeprecationInfo, EnrichedDeprecationInfo } from '../../../../../common/types';
import { GroupByOption } from '../../types';
import { COLOR_MAP, LEVEL_MAP } from '../constants';
import { COLOR_MAP, LEVEL_MAP } from '../../constants';
import { DeprecationCell } from './cell';
import { IndexDeprecationDetails, IndexDeprecationTable } from './index_table';
@ -86,7 +86,7 @@ const IndexDeprecation: FunctionComponent<IndexDeprecationProps> = ({ deprecatio
* A list of deprecations that is either shown as individual deprecation cells or as a
* deprecation summary for a list of indices.
*/
export const DeprecationList: FunctionComponent<{
export const EsDeprecationList: FunctionComponent<{
deprecations: EnrichedDeprecationInfo[];
currentGroupBy: GroupByOption;
}> = ({ deprecations, currentGroupBy }) => {
@ -106,16 +106,16 @@ export const DeprecationList: FunctionComponent<{
} else if (currentGroupBy === GroupByOption.index) {
return (
<div>
{deprecations.sort(sortByLevelDesc).map((dep) => (
<MessageDeprecation deprecation={dep} key={dep.message} />
{deprecations.sort(sortByLevelDesc).map((dep, index) => (
<MessageDeprecation deprecation={dep} key={`${dep.message}-${index}`} />
))}
</div>
);
} else {
return (
<div>
{deprecations.sort(sortByLevelDesc).map((dep) => (
<SimpleMessageDeprecation deprecation={dep} key={dep.message} />
{deprecations.sort(sortByLevelDesc).map((dep, index) => (
<SimpleMessageDeprecation deprecation={dep} key={`${dep.message}-${index}`} />
))}
</div>
);

View file

@ -1,84 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { groupBy } from 'lodash';
import React from 'react';
import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DeprecationInfo } from '../../../../common/types';
import { LevelFilterOption } from '../types';
const LocalizedOptions: { [option: string]: string } = {
warning: i18n.translate(
'xpack.upgradeAssistant.checkupTab.controls.filterBar.warningButtonLabel',
{
defaultMessage: 'warning',
}
),
critical: i18n.translate(
'xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel',
{ defaultMessage: 'critical' }
),
};
interface FilterBarProps {
allDeprecations?: DeprecationInfo[];
currentFilter: LevelFilterOption;
onFilterChange(level: LevelFilterOption): void;
}
export const FilterBar: React.FunctionComponent<FilterBarProps> = ({
allDeprecations = [],
currentFilter,
onFilterChange,
}) => {
const levelGroups = groupBy(allDeprecations, 'level');
const levelCounts = Object.keys(levelGroups).reduce((counts, level) => {
counts[level] = levelGroups[level].length;
return counts;
}, {} as { [level: string]: number });
return (
<EuiFlexItem grow={false}>
<EuiFilterGroup>
<EuiFilterButton
withNext
key={LevelFilterOption.critical}
onClick={() => {
onFilterChange(
currentFilter !== LevelFilterOption.critical
? LevelFilterOption.critical
: LevelFilterOption.all
);
}}
hasActiveFilters={currentFilter === LevelFilterOption.critical}
numFilters={levelCounts[LevelFilterOption.critical] || undefined}
data-test-subj="criticalLevelFilter"
>
{LocalizedOptions[LevelFilterOption.critical]}
</EuiFilterButton>
<EuiFilterButton
key={LevelFilterOption.warning}
onClick={() => {
onFilterChange(
currentFilter !== LevelFilterOption.warning
? LevelFilterOption.warning
: LevelFilterOption.all
);
}}
hasActiveFilters={currentFilter === LevelFilterOption.warning}
numFilters={levelCounts[LevelFilterOption.warning] || undefined}
data-test-subj="warningLevelFilter"
>
{LocalizedOptions[LevelFilterOption.warning]}
</EuiFilterButton>
</EuiFilterGroup>
</EuiFlexItem>
);
};

View file

@ -0,0 +1,145 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent } from 'react';
import {
EuiAccordion,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiText,
EuiCallOut,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { DomainDeprecationDetails } from 'kibana/public';
import { DeprecationHealth } from '../shared';
import { LEVEL_MAP } from '../constants';
import { StepsModalContent } from './steps_modal';
const i18nTexts = {
getDeprecationTitle: (domainId: string) => {
return i18n.translate('xpack.upgradeAssistant.deprecationGroupItemTitle', {
defaultMessage: "'{domainId}' is using a deprecated feature",
values: {
domainId,
},
});
},
docLinkText: i18n.translate('xpack.upgradeAssistant.deprecationGroupItem.docLinkText', {
defaultMessage: 'View documentation',
}),
manualFixButtonLabel: i18n.translate(
'xpack.upgradeAssistant.deprecationGroupItem.fixButtonLabel',
{
defaultMessage: 'Show steps to fix',
}
),
resolveButtonLabel: i18n.translate(
'xpack.upgradeAssistant.deprecationGroupItem.resolveButtonLabel',
{
defaultMessage: 'Quick resolve',
}
),
};
export interface Props {
deprecation: DomainDeprecationDetails;
index: number;
forceExpand: boolean;
showStepsModal: (modalContent: StepsModalContent) => void;
showResolveModal: (deprecation: DomainDeprecationDetails) => void;
}
export const KibanaDeprecationAccordion: FunctionComponent<Props> = ({
deprecation,
forceExpand,
index,
showStepsModal,
showResolveModal,
}) => {
const { domainId, level, message, documentationUrl, correctiveActions } = deprecation;
return (
<EuiAccordion
id={`${domainId}-${index}`}
data-test-subj={`${domainId}Deprecation`}
initialIsOpen={forceExpand}
buttonContent={i18nTexts.getDeprecationTitle(domainId)}
paddingSize="m"
extraAction={<DeprecationHealth single deprecationLevels={[LEVEL_MAP[level]]} />}
>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiText size="s">
{level === 'fetch_error' ? (
<EuiCallOut
title={message}
color="warning"
iconType="alert"
data-test-subj={`${domainId}Error`}
size="s"
/>
) : (
<>
<p>{message}</p>
{(documentationUrl || correctiveActions?.manualSteps) && (
<EuiFlexGroup>
{correctiveActions?.api && (
<EuiFlexItem grow={false}>
<EuiButton
fill
size="s"
data-test-subj="resolveButton"
onClick={() => showResolveModal(deprecation)}
>
{i18nTexts.resolveButtonLabel}
</EuiButton>
</EuiFlexItem>
)}
{correctiveActions?.manualSteps && (
<EuiFlexItem grow={false}>
<EuiButton
size="s"
data-test-subj="stepsButton"
onClick={() =>
showStepsModal({
domainId,
steps: correctiveActions.manualSteps!,
documentationUrl,
})
}
>
{i18nTexts.manualFixButtonLabel}
</EuiButton>
</EuiFlexItem>
)}
{documentationUrl && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
href={documentationUrl}
iconType="help"
target="_blank"
>
{i18nTexts.docLinkText}
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
</>
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
);
};

View file

@ -0,0 +1,150 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent, useState, useEffect } from 'react';
import { groupBy } from 'lodash';
import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import type { DomainDeprecationDetails } from 'kibana/public';
import { LevelFilterOption } from '../types';
import { SearchBar, DeprecationListBar, DeprecationPagination } from '../shared';
import { DEPRECATIONS_PER_PAGE } from '../constants';
import { KibanaDeprecationAccordion } from './deprecation_item';
import { StepsModalContent } from './steps_modal';
import { KibanaDeprecationErrors } from './kibana_deprecation_errors';
interface Props {
deprecations: DomainDeprecationDetails[];
showStepsModal: (newStepsModalContent: StepsModalContent) => void;
showResolveModal: (deprecation: DomainDeprecationDetails) => void;
reloadDeprecations: () => Promise<void>;
isLoading: boolean;
}
const getFilteredDeprecations = (
deprecations: DomainDeprecationDetails[],
level: LevelFilterOption,
search: string
) => {
return deprecations
.filter((deprecation) => {
return level === 'all' || deprecation.level === level;
})
.filter((filteredDep) => {
if (search.length > 0) {
try {
// 'i' is used for case-insensitive matching
const searchReg = new RegExp(search, 'i');
return searchReg.test(filteredDep.message);
} catch (e) {
// ignore any regexp errors
return true;
}
}
return true;
});
};
export const KibanaDeprecationList: FunctionComponent<Props> = ({
deprecations,
showStepsModal,
showResolveModal,
reloadDeprecations,
isLoading,
}) => {
const [currentFilter, setCurrentFilter] = useState<LevelFilterOption>('all');
const [search, setSearch] = useState('');
const [expandState, setExpandState] = useState({
forceExpand: false,
expandNumber: 0,
});
const [currentPage, setCurrentPage] = useState(0);
const setExpandAll = (expandAll: boolean) => {
setExpandState({ forceExpand: expandAll, expandNumber: expandState.expandNumber + 1 });
};
const levelGroups = groupBy(deprecations, 'level');
const levelToDeprecationCountMap = Object.keys(levelGroups).reduce((counts, level) => {
counts[level] = levelGroups[level].length;
return counts;
}, {} as { [level: string]: number });
const filteredDeprecations = getFilteredDeprecations(deprecations, currentFilter, search);
const deprecationsWithErrors = deprecations.filter((dep) => dep.level === 'fetch_error');
useEffect(() => {
const pageCount = Math.ceil(filteredDeprecations.length / DEPRECATIONS_PER_PAGE);
if (currentPage >= pageCount) {
setCurrentPage(0);
}
}, [filteredDeprecations, currentPage]);
return (
<>
<SearchBar
isLoading={isLoading}
loadData={reloadDeprecations}
currentFilter={currentFilter}
onFilterChange={setCurrentFilter}
onSearchChange={setSearch}
totalDeprecationsCount={deprecations.length}
levelToDeprecationCountMap={levelToDeprecationCountMap}
/>
{deprecationsWithErrors.length > 0 && (
<>
<KibanaDeprecationErrors errorType="pluginError" />
<EuiSpacer />
</>
)}
<DeprecationListBar
allDeprecationsCount={deprecations.length}
filteredDeprecationsCount={filteredDeprecations.length}
setExpandAll={setExpandAll}
/>
<EuiHorizontalRule margin="m" />
<>
{filteredDeprecations
.slice(currentPage * DEPRECATIONS_PER_PAGE, (currentPage + 1) * DEPRECATIONS_PER_PAGE)
.map((deprecation, index) => [
<div key={`kibana-deprecation-${index}`} data-test-subj="kibanaDeprecationItem">
<KibanaDeprecationAccordion
{...{
key: expandState.expandNumber,
index,
deprecation,
forceExpand: expandState.forceExpand,
showStepsModal,
showResolveModal,
}}
/>
<EuiHorizontalRule margin="s" />
</div>,
])}
{/* Only show pagination if we have more than DEPRECATIONS_PER_PAGE */}
{filteredDeprecations.length > DEPRECATIONS_PER_PAGE && (
<>
<EuiSpacer />
<DeprecationPagination
pageCount={Math.ceil(filteredDeprecations.length / DEPRECATIONS_PER_PAGE)}
activePage={currentPage}
setPage={setCurrentPage}
/>
</>
)}
</>
</>
);
};

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { KibanaDeprecationsContent } from './kibana_deprecations';

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCallOut } from '@elastic/eui';
interface Props {
errorType: 'pluginError' | 'requestError';
}
const i18nTexts = {
pluginError: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationErrors.pluginErrorMessage', {
defaultMessage:
'Not all Kibana deprecations were retrieved successfully. This list may be incomplete. Check the Kibana server logs for errors.',
}),
loadingError: i18n.translate(
'xpack.upgradeAssistant.kibanaDeprecationErrors.loadingErrorMessage',
{
defaultMessage:
'Could not retrieve Kibana deprecations. Check the Kibana server logs for errors.',
}
),
};
export const KibanaDeprecationErrors: React.FunctionComponent<Props> = ({ errorType }) => {
if (errorType === 'pluginError') {
return (
<EuiCallOut
title={i18nTexts.pluginError}
color="warning"
iconType="alert"
data-test-subj="kibanaPluginError"
/>
);
}
return (
<EuiCallOut
title={i18nTexts.loadingError}
color="danger"
iconType="alert"
data-test-subj="kibanaRequestError"
/>
);
};

View file

@ -0,0 +1,210 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useState, useCallback } from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import {
EuiButtonEmpty,
EuiPageBody,
EuiPageHeader,
EuiPageContent,
EuiPageContentBody,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { DomainDeprecationDetails } from 'kibana/public';
import { SectionLoading } from '../../../shared_imports';
import { useAppContext } from '../../app_context';
import { NoDeprecationsPrompt } from '../shared';
import { KibanaDeprecationList } from './deprecation_list';
import { StepsModal, StepsModalContent } from './steps_modal';
import { KibanaDeprecationErrors } from './kibana_deprecation_errors';
import { ResolveDeprecationModal } from './resolve_deprecation_modal';
import { LEVEL_MAP } from '../constants';
const i18nTexts = {
pageTitle: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.pageTitle', {
defaultMessage: 'Kibana',
}),
pageDescription: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.pageDescription', {
defaultMessage: 'Some Kibana issues may require your attention. Resolve them before upgrading.',
}),
docLinkText: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.docLinkText', {
defaultMessage: 'Documentation',
}),
deprecationLabel: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.deprecationLabel', {
defaultMessage: 'Kibana',
}),
isLoading: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.loadingText', {
defaultMessage: 'Loading deprecations…',
}),
successMessage: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.successMessage', {
defaultMessage: 'Deprecation resolved',
}),
errorMessage: i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.errorMessage', {
defaultMessage: 'Error resolving deprecation',
}),
};
const sortByLevelDesc = (a: DomainDeprecationDetails, b: DomainDeprecationDetails) => {
return -1 * (LEVEL_MAP[a.level] - LEVEL_MAP[b.level]);
};
export const KibanaDeprecationsContent = withRouter(({ history }: RouteComponentProps) => {
const [kibanaDeprecations, setKibanaDeprecations] = useState<
DomainDeprecationDetails[] | undefined
>(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | undefined>(undefined);
const [stepsModalContent, setStepsModalContent] = useState<StepsModalContent | undefined>(
undefined
);
const [resolveModalContent, setResolveModalContent] = useState<
undefined | DomainDeprecationDetails
>(undefined);
const [isResolvingDeprecation, setIsResolvingDeprecation] = useState(false);
const { deprecations, breadcrumbs, docLinks, api, notifications } = useAppContext();
const getAllDeprecations = useCallback(async () => {
setIsLoading(true);
try {
const response = await deprecations.getAllDeprecations();
const sortedDeprecations = response.sort(sortByLevelDesc);
setKibanaDeprecations(sortedDeprecations);
} catch (e) {
setError(e);
}
setIsLoading(false);
}, [deprecations]);
const toggleStepsModal = (newStepsModalContent?: StepsModalContent) => {
setStepsModalContent(newStepsModalContent);
};
const toggleResolveModal = (newResolveModalContent?: DomainDeprecationDetails) => {
setResolveModalContent(newResolveModalContent);
};
const resolveDeprecation = async (deprecationDetails: DomainDeprecationDetails) => {
setIsResolvingDeprecation(true);
const response = await deprecations.resolveDeprecation(deprecationDetails);
setIsResolvingDeprecation(false);
toggleResolveModal();
// Handle error case
if (response.status === 'fail') {
notifications.toasts.addError(new Error(response.reason), {
title: i18nTexts.errorMessage,
});
return;
}
notifications.toasts.addSuccess(i18nTexts.successMessage);
// Refetch deprecations
getAllDeprecations();
};
useEffect(() => {
async function sendTelemetryData() {
await api.sendTelemetryData({
kibana: true,
});
}
sendTelemetryData();
}, [api]);
useEffect(() => {
breadcrumbs.setBreadcrumbs('kibanaDeprecations');
}, [breadcrumbs]);
useEffect(() => {
getAllDeprecations();
}, [deprecations, getAllDeprecations]);
const getPageContent = () => {
if (kibanaDeprecations && kibanaDeprecations.length === 0) {
return (
<NoDeprecationsPrompt
deprecationType={i18nTexts.deprecationLabel}
navigateToOverviewPage={() => history.push('/overview')}
/>
);
}
let content: React.ReactNode;
if (isLoading) {
content = <SectionLoading>{i18nTexts.isLoading}</SectionLoading>;
} else if (kibanaDeprecations?.length) {
content = (
<KibanaDeprecationList
deprecations={kibanaDeprecations}
showStepsModal={toggleStepsModal}
showResolveModal={toggleResolveModal}
reloadDeprecations={getAllDeprecations}
isLoading={isLoading}
/>
);
} else if (error) {
content = <KibanaDeprecationErrors errorType="requestError" />;
}
return (
<div data-test-subj="kibanaDeprecationsContent">
<EuiSpacer />
{content}
</div>
);
};
return (
<EuiPageBody>
<EuiPageContent>
<EuiPageHeader
pageTitle={i18nTexts.pageTitle}
description={i18nTexts.pageDescription}
rightSideItems={[
<EuiButtonEmpty
href={docLinks.links.upgradeAssistant}
target="_blank"
iconType="help"
data-test-subj="documentationLink"
>
{i18nTexts.docLinkText}
</EuiButtonEmpty>,
]}
/>
<EuiPageContentBody>
{getPageContent()}
{stepsModalContent && (
<StepsModal closeModal={() => toggleStepsModal()} modalContent={stepsModalContent} />
)}
{resolveModalContent && (
<ResolveDeprecationModal
closeModal={() => toggleResolveModal()}
resolveDeprecation={resolveDeprecation}
isResolvingDeprecation={isResolvingDeprecation}
deprecation={resolveModalContent}
/>
)}
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
});

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiConfirmModal } from '@elastic/eui';
import type { DomainDeprecationDetails } from 'kibana/public';
interface Props {
closeModal: () => void;
deprecation: DomainDeprecationDetails;
isResolvingDeprecation: boolean;
resolveDeprecation: (deprecationDetails: DomainDeprecationDetails) => Promise<void>;
}
const i18nTexts = {
getModalTitle: (domainId: string) =>
i18n.translate(
'xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.modalTitle',
{
defaultMessage: "Resolve '{domainId}'?",
values: {
domainId,
},
}
),
cancelButtonLabel: i18n.translate(
'xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.cancelButtonLabel',
{
defaultMessage: 'Cancel',
}
),
resolveButtonLabel: i18n.translate(
'xpack.upgradeAssistant.kibanaDeprecations.resolveConfirmationModal.resolveButtonLabel',
{
defaultMessage: 'Resolve',
}
),
};
export const ResolveDeprecationModal: FunctionComponent<Props> = ({
closeModal,
deprecation,
isResolvingDeprecation,
resolveDeprecation,
}) => {
return (
<EuiConfirmModal
data-test-subj="resolveModal"
title={i18nTexts.getModalTitle(deprecation.domainId)}
onCancel={closeModal}
onConfirm={() => resolveDeprecation(deprecation)}
cancelButtonText={i18nTexts.cancelButtonLabel}
confirmButtonText={i18nTexts.resolveButtonLabel}
defaultFocusedButton="confirm"
isLoading={isResolvingDeprecation}
/>
);
};

View file

@ -0,0 +1,130 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiText,
EuiSteps,
EuiSpacer,
EuiButton,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiTitle,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
export interface StepsModalContent {
domainId: string;
steps: string[];
documentationUrl?: string;
}
interface Props {
closeModal: () => void;
modalContent: StepsModalContent;
}
const i18nTexts = {
getModalTitle: (domainId: string) =>
i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.stepsModal.modalTitle', {
defaultMessage: "Fix '{domainId}'",
values: {
domainId,
},
}),
getStepTitle: (step: number) =>
i18n.translate('xpack.upgradeAssistant.kibanaDeprecations.stepsModal.stepTitle', {
defaultMessage: 'Step {step}',
values: {
step,
},
}),
modalDescription: i18n.translate(
'xpack.upgradeAssistant.kibanaDeprecations.stepsModal.modalDescription',
{
defaultMessage: 'Follow the steps below to address this deprecation.',
}
),
docLinkLabel: i18n.translate(
'xpack.upgradeAssistant.kibanaDeprecations.stepsModal.docLinkLabel',
{
defaultMessage: 'View documentation',
}
),
closeButtonLabel: i18n.translate(
'xpack.upgradeAssistant.kibanaDeprecations.stepsModal.closeButtonLabel',
{
defaultMessage: 'Close',
}
),
};
export const StepsModal: FunctionComponent<Props> = ({ closeModal, modalContent }) => {
const { domainId, steps, documentationUrl } = modalContent;
return (
<EuiModal onClose={closeModal} data-test-subj="stepsModal">
<EuiModalHeader>
<EuiModalHeaderTitle>
<EuiTitle size="m">
<h2>{i18nTexts.getModalTitle(domainId)}</h2>
</EuiTitle>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<>
<EuiText>
<p>{i18nTexts.modalDescription}</p>
</EuiText>
<EuiSpacer />
<EuiSteps
titleSize="xs"
data-test-subj="fixDeprecationSteps"
steps={steps.map((step, index) => {
return {
title: i18nTexts.getStepTitle(index + 1),
children: (
<EuiText>
<p>{step}</p>
</EuiText>
),
};
})}
/>
</>
</EuiModalBody>
<EuiModalFooter>
<EuiFlexGroup justifyContent="flexEnd">
{documentationUrl && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="help" target="_blank" href={documentationUrl}>
{i18nTexts.docLinkLabel}
</EuiButtonEmpty>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton onClick={closeModal} fill data-test-subj="closeButton">
{i18nTexts.closeButtonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalFooter>
</EuiModal>
);
};

View file

@ -16,6 +16,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
EuiScreenReaderOnly,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -46,6 +47,16 @@ const i18nTexts = {
defaultMessage: 'View deprecations',
}
),
loadingText: i18n.translate('xpack.upgradeAssistant.esDeprecationStats.loadingText', {
defaultMessage: 'Loading Elasticsearch deprecation stats…',
}),
getCriticalDeprecationsMessage: (criticalDeprecations: number) =>
i18n.translate('xpack.upgradeAssistant.esDeprecationStats.criticalDeprecationsLabel', {
defaultMessage: 'This cluster has {criticalDeprecations} critical deprecations',
values: {
criticalDeprecations,
},
}),
getTotalDeprecationsTooltip: (clusterCount: number, indexCount: number) =>
i18n.translate('xpack.upgradeAssistant.esDeprecationStats.totalDeprecationsTooltip', {
defaultMessage:
@ -105,11 +116,27 @@ export const ESDeprecationStats: FunctionComponent<Props> = ({ history }) => {
esDeprecations?.indices.length ?? 0
)}
position="right"
iconProps={{
tabIndex: -1,
}}
/>
</>
}
isLoading={isLoading}
/>
>
{error === null && (
<EuiScreenReaderOnly>
<p>
{isLoading
? i18nTexts.loadingText
: i18nTexts.getTotalDeprecationsTooltip(
esDeprecations?.cluster.length ?? 0,
esDeprecations?.indices.length ?? 0
)}
</p>
</EuiScreenReaderOnly>
)}
</EuiStat>
</EuiFlexItem>
<EuiFlexItem>
@ -120,6 +147,16 @@ export const ESDeprecationStats: FunctionComponent<Props> = ({ history }) => {
titleColor="danger"
isLoading={isLoading}
>
{error === null && (
<EuiScreenReaderOnly>
<p>
{isLoading
? i18nTexts.loadingText
: i18nTexts.getCriticalDeprecationsMessage(criticalDeprecations.length)}
</p>
</EuiScreenReaderOnly>
)}
{error && <EsStatsErrors error={error} />}
</EuiStat>
</EuiFlexItem>

View file

@ -0,0 +1,194 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent, useEffect, useState } from 'react';
import {
EuiLink,
EuiPanel,
EuiStat,
EuiTitle,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
EuiScreenReaderOnly,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RouteComponentProps } from 'react-router-dom';
import type { DomainDeprecationDetails } from 'kibana/public';
import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
import { useAppContext } from '../../app_context';
const i18nTexts = {
statsTitle: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.statsTitle', {
defaultMessage: 'Kibana',
}),
totalDeprecationsTitle: i18n.translate(
'xpack.upgradeAssistant.kibanaDeprecationStats.totalDeprecationsTitle',
{
defaultMessage: 'Deprecations',
}
),
criticalDeprecationsTitle: i18n.translate(
'xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsTitle',
{
defaultMessage: 'Critical',
}
),
viewDeprecationsLink: i18n.translate(
'xpack.upgradeAssistant.kibanaDeprecationStats.viewDeprecationsLinkText',
{
defaultMessage: 'View deprecations',
}
),
loadingError: i18n.translate(
'xpack.upgradeAssistant.kibanaDeprecationStats.loadingErrorMessage',
{
defaultMessage: 'An error occurred while retrieving Kibana deprecations.',
}
),
loadingText: i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.loadingText', {
defaultMessage: 'Loading Kibana deprecation stats…',
}),
getCriticalDeprecationsMessage: (criticalDeprecations: number) =>
i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.criticalDeprecationsLabel', {
defaultMessage: 'Kibana has {criticalDeprecations} critical deprecations',
values: {
criticalDeprecations,
},
}),
getTotalDeprecationsMessage: (totalDeprecations: number) =>
i18n.translate('xpack.upgradeAssistant.kibanaDeprecationStats.totalDeprecationsLabel', {
defaultMessage: 'Kibana has {totalDeprecations} total deprecations',
values: {
totalDeprecations,
},
}),
};
interface Props {
history: RouteComponentProps['history'];
}
export const KibanaDeprecationStats: FunctionComponent<Props> = ({ history }) => {
const { deprecations } = useAppContext();
const [kibanaDeprecations, setKibanaDeprecations] = useState<
DomainDeprecationDetails[] | undefined
>(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | undefined>(undefined);
useEffect(() => {
async function getAllDeprecations() {
setIsLoading(true);
try {
const response = await deprecations.getAllDeprecations();
setKibanaDeprecations(response);
} catch (e) {
setError(e);
}
setIsLoading(false);
}
getAllDeprecations();
}, [deprecations]);
return (
<EuiPanel data-test-subj="kibanaStatsPanel" hasShadow={false} hasBorder={true}>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="baseline">
<EuiFlexItem>
<EuiTitle size="s">
<h2>{i18nTexts.statsTitle}</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
{...reactRouterNavigate(history, '/kibana_deprecations')}
data-test-subj="kibanaDeprecationsLink"
>
{i18nTexts.viewDeprecationsLink}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem>
<EuiStat
data-test-subj="totalDeprecations"
title={error ? '--' : kibanaDeprecations?.length ?? '0'}
description={i18nTexts.totalDeprecationsTitle}
isLoading={isLoading}
>
{error === undefined && (
<EuiScreenReaderOnly>
<p>
{isLoading
? i18nTexts.loadingText
: i18nTexts.getTotalDeprecationsMessage(kibanaDeprecations?.length ?? 0)}
</p>
</EuiScreenReaderOnly>
)}
</EuiStat>
</EuiFlexItem>
<EuiFlexItem>
<EuiStat
data-test-subj="criticalDeprecations"
title={
kibanaDeprecations
? kibanaDeprecations.filter((deprecation) => deprecation.level === 'critical')
?.length ?? '0'
: '--'
}
description={i18nTexts.criticalDeprecationsTitle}
titleColor="danger"
isLoading={isLoading}
>
{error === undefined && (
<EuiScreenReaderOnly>
<p>
{isLoading
? i18nTexts.loadingText
: i18nTexts.getCriticalDeprecationsMessage(
kibanaDeprecations
? kibanaDeprecations.filter(
(deprecation) => deprecation.level === 'critical'
)?.length ?? 0
: 0
)}
</p>
</EuiScreenReaderOnly>
)}
{error && (
<>
<EuiSpacer size="s" />
<EuiIconTip
type="alert"
color="danger"
size="l"
content={i18nTexts.loadingError}
iconProps={{
'data-test-subj': 'requestErrorIconTip',
}}
/>
</>
)}
</EuiStat>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};

View file

@ -27,6 +27,7 @@ import { RouteComponentProps } from 'react-router-dom';
import { useAppContext } from '../../app_context';
import { LatestMinorBanner } from '../latest_minor_banner';
import { ESDeprecationStats } from './es_stats';
import { KibanaDeprecationStats } from './kibana_stats';
import { DeprecationLoggingToggle } from './deprecation_logging_toggle';
const i18nTexts = {
@ -114,21 +115,25 @@ export const DeprecationsOverview: FunctionComponent<Props> = ({ history }) => {
<EuiSpacer size="xl" />
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ minWidth: 400 }}>
<EuiFlexItem>
<ESDeprecationStats history={history} />
</EuiFlexItem>
<EuiSpacer />
<EuiFormRow
helpText={i18nTexts.getDeprecationLoggingLabel(
docLinks.links.elasticsearch.deprecationLogging
)}
data-test-subj="deprecationLoggingFormRow"
>
<DeprecationLoggingToggle />
</EuiFormRow>
<EuiFlexItem>
<KibanaDeprecationStats history={history} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFormRow
helpText={i18nTexts.getDeprecationLoggingLabel(
docLinks.links.elasticsearch.deprecationLogging
)}
data-test-subj="deprecationLoggingFormRow"
>
<DeprecationLoggingToggle />
</EuiFormRow>
</>
</EuiPageContentBody>
</EuiPageContent>

View file

@ -5,23 +5,21 @@
* 2.0.
*/
import React, { Fragment, FunctionComponent } from 'react';
import React, { FunctionComponent } from 'react';
import { EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EnrichedDeprecationInfo } from '../../../../../common/types';
export const DeprecationCountSummary: FunctionComponent<{
deprecations: EnrichedDeprecationInfo[];
allDeprecations: EnrichedDeprecationInfo[];
}> = ({ deprecations, allDeprecations }) => (
allDeprecationsCount: number;
filteredDeprecationsCount: number;
}> = ({ filteredDeprecationsCount, allDeprecationsCount }) => (
<EuiText size="s">
{allDeprecations.length ? (
{allDeprecationsCount > 0 ? (
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.numDeprecationsShownLabel"
defaultMessage="Showing {numShown} of {total}"
values={{ numShown: deprecations.length, total: allDeprecations.length }}
values={{ numShown: filteredDeprecationsCount, total: allDeprecationsCount }}
/>
) : (
<FormattedMessage
@ -29,15 +27,15 @@ export const DeprecationCountSummary: FunctionComponent<{
defaultMessage="No deprecations"
/>
)}
{deprecations.length !== allDeprecations.length && (
<Fragment>
{filteredDeprecationsCount !== allDeprecationsCount && (
<>
{'. '}
<FormattedMessage
id="xpack.upgradeAssistant.checkupTab.changeFiltersShowMoreLabel"
description="Explains how to show all deprecations if there are more available."
defaultMessage="Change filter to show more."
/>
</Fragment>
</>
)}
</EuiText>
);

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { DeprecationCountSummary } from './count_summary';
const i18nTexts = {
expandAllButton: i18n.translate(
'xpack.upgradeAssistant.deprecationListBar.expandAllButtonLabel',
{
defaultMessage: 'Expand all',
}
),
collapseAllButton: i18n.translate(
'xpack.upgradeAssistant.deprecationListBar.collapseAllButtonLabel',
{
defaultMessage: 'Collapse all',
}
),
};
export const DeprecationListBar: FunctionComponent<{
allDeprecationsCount: number;
filteredDeprecationsCount: number;
setExpandAll: (shouldExpandAll: boolean) => void;
}> = ({ allDeprecationsCount, filteredDeprecationsCount, setExpandAll }) => {
return (
<EuiFlexGroup responsive={false} justifyContent="spaceBetween" alignItems="baseline">
<EuiFlexItem>
<DeprecationCountSummary
allDeprecationsCount={allDeprecationsCount}
filteredDeprecationsCount={filteredDeprecationsCount}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
flush="left"
size="s"
onClick={() => setExpandAll(true)}
data-test-subj="expandAll"
>
{i18nTexts.expandAllButton}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
flush="left"
size="s"
onClick={() => setExpandAll(false)}
data-test-subj="collapseAll"
>
{i18nTexts.collapseAllButton}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { DeprecationListBar } from './deprecation_list_bar';

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPagination } from '@elastic/eui';
export const DeprecationPagination: FunctionComponent<{
pageCount: number;
activePage: number;
setPage: (page: number) => void;
}> = ({ pageCount, activePage, setPage }) => {
return (
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiPagination pageCount={pageCount} activePage={activePage} onPageClick={setPage} />
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -11,8 +11,8 @@ import React, { FunctionComponent } from 'react';
import { EuiBadge, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DeprecationInfo } from '../../../../../common/types';
import { COLOR_MAP, LEVEL_MAP, REVERSE_LEVEL_MAP } from '../constants';
import { DeprecationInfo } from '../../../../common/types';
import { COLOR_MAP, REVERSE_LEVEL_MAP } from '../constants';
const LocalizedLevels: { [level: string]: string } = {
warning: i18n.translate('xpack.upgradeAssistant.checkupTab.deprecations.warningLabel', {
@ -33,7 +33,7 @@ export const LocalizedActions: { [level: string]: string } = {
};
interface DeprecationHealthProps {
deprecations: DeprecationInfo[];
deprecationLevels: number[];
single?: boolean;
}
@ -54,23 +54,21 @@ const SingleHealth: FunctionComponent<{ level: DeprecationInfo['level']; label:
* deprecations in the list.
*/
export const DeprecationHealth: FunctionComponent<DeprecationHealthProps> = ({
deprecations,
deprecationLevels,
single = false,
}) => {
if (deprecations.length === 0) {
if (deprecationLevels.length === 0) {
return <span />;
}
const levels = deprecations.map((d) => LEVEL_MAP[d.level]);
if (single) {
const highest = Math.max(...levels);
const highest = Math.max(...deprecationLevels);
const highestLevel = REVERSE_LEVEL_MAP[highest];
return <SingleHealth level={highestLevel} label={LocalizedLevels[highestLevel]} />;
}
const countByLevel = countBy(levels);
const countByLevel = countBy(deprecationLevels);
return (
<React.Fragment>

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { NoDeprecationsPrompt } from './no_deprecations';
export { DeprecationHealth } from './health';
export { SearchBar } from './search_bar';
export { DeprecationPagination } from './deprecation_pagination';
export { DeprecationListBar } from './deprecation_list_bar';

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent } from 'react';
import { EuiLink, EuiEmptyPrompt } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
const i18nTexts = {
emptyPromptTitle: i18n.translate('xpack.upgradeAssistant.noDeprecationsPrompt.title', {
defaultMessage: 'Ready to upgrade!',
}),
getEmptyPromptDescription: (deprecationType: string) =>
i18n.translate('xpack.upgradeAssistant.noDeprecationsPrompt.description', {
defaultMessage: 'Your configuration is up to date.',
}),
getEmptyPromptNextStepsDescription: (navigateToOverviewPage: () => void) => (
<FormattedMessage
id="xpack.upgradeAssistant.noDeprecationsPrompt.nextStepsDescription"
defaultMessage="Check the {overviewButton} for other Stack deprecations."
values={{
overviewButton: (
<EuiLink onClick={navigateToOverviewPage}>
{i18n.translate('xpack.upgradeAssistant.noDeprecationsPrompt.overviewLinkText', {
defaultMessage: 'Overview page',
})}
</EuiLink>
),
}}
/>
),
};
interface Props {
deprecationType: string;
navigateToOverviewPage: () => void;
}
export const NoDeprecationsPrompt: FunctionComponent<Props> = ({
deprecationType,
navigateToOverviewPage,
}) => {
return (
<EuiEmptyPrompt
iconType="faceHappy"
data-test-subj="noDeprecationsPrompt"
title={<h2>{i18nTexts.emptyPromptTitle}</h2>}
body={
<>
<p data-test-subj="upgradeAssistantIssueSummary">
{i18nTexts.getEmptyPromptDescription(deprecationType)}
</p>
<p>{i18nTexts.getEmptyPromptNextStepsDescription(navigateToOverviewPage)}</p>
</>
}
/>
);
};

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GroupByBar renders 1`] = `
exports[`GroupByFilter renders 1`] = `
<EuiFlexItem
grow={false}
>

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterBar renders 1`] = `
exports[`DeprecationLevelFilter renders 1`] = `
<EuiFlexItem
grow={false}
>
@ -9,20 +9,11 @@ exports[`FilterBar renders 1`] = `
data-test-subj="criticalLevelFilter"
hasActiveFilters={false}
key="critical"
numFilters={2}
numFilters={1}
onClick={[Function]}
withNext={true}
>
critical
</EuiFilterButton>
<EuiFilterButton
data-test-subj="warningLevelFilter"
hasActiveFilters={false}
key="warning"
onClick={[Function]}
>
warning
</EuiFilterButton>
</EuiFilterGroup>
</EuiFlexItem>
`;

View file

@ -8,8 +8,8 @@
import { mount, shallow } from 'enzyme';
import React from 'react';
import { GroupByOption } from '../types';
import { GroupByBar } from './group_by_bar';
import { GroupByOption } from '../../types';
import { GroupByFilter } from './group_by_filter';
const defaultProps = {
availableGroupByOptions: [GroupByOption.message, GroupByOption.index],
@ -17,13 +17,13 @@ const defaultProps = {
onGroupByChange: jest.fn(),
};
describe('GroupByBar', () => {
describe('GroupByFilter', () => {
test('renders', () => {
expect(shallow(<GroupByBar {...defaultProps} />)).toMatchSnapshot();
expect(shallow(<GroupByFilter {...defaultProps} />)).toMatchSnapshot();
});
test('clicking button calls onGroupByChange', () => {
const wrapper = mount(<GroupByBar {...defaultProps} />);
const wrapper = mount(<GroupByFilter {...defaultProps} />);
wrapper.find('button.euiFilterButton-hasActiveFilters').simulate('click');
expect(defaultProps.onGroupByChange).toHaveBeenCalledTimes(1);
expect(defaultProps.onGroupByChange.mock.calls[0][0]).toEqual(GroupByOption.message);

View file

@ -10,7 +10,7 @@ import React from 'react';
import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { GroupByOption } from '../types';
import { GroupByOption } from '../../types';
const LocalizedOptions: { [option: string]: string } = {
message: i18n.translate('xpack.upgradeAssistant.checkupTab.controls.groupByBar.byIssueLabel', {
@ -21,13 +21,13 @@ const LocalizedOptions: { [option: string]: string } = {
}),
};
interface GroupByBarProps {
interface GroupByFilterProps {
availableGroupByOptions: GroupByOption[];
currentGroupBy: GroupByOption;
onGroupByChange: (groupBy: GroupByOption) => void;
}
export const GroupByBar: React.FunctionComponent<GroupByBarProps> = ({
export const GroupByFilter: React.FunctionComponent<GroupByFilterProps> = ({
availableGroupByOptions,
currentGroupBy,
onGroupByChange,

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { SearchBar } from './search_bar';

View file

@ -7,29 +7,28 @@
import { mount, shallow } from 'enzyme';
import React from 'react';
import { DeprecationInfo } from '../../../../common/types';
import { LevelFilterOption } from '../../types';
import { LevelFilterOption } from '../types';
import { FilterBar } from './filter_bar';
import { DeprecationLevelFilter } from './level_filter';
const defaultProps = {
allDeprecations: [
{ level: LevelFilterOption.critical },
{ level: LevelFilterOption.critical },
] as DeprecationInfo[],
currentFilter: LevelFilterOption.all,
levelsCount: {
warning: 4,
critical: 1,
},
currentFilter: 'all' as LevelFilterOption,
onFilterChange: jest.fn(),
};
describe('FilterBar', () => {
describe('DeprecationLevelFilter', () => {
test('renders', () => {
expect(shallow(<FilterBar {...defaultProps} />)).toMatchSnapshot();
expect(shallow(<DeprecationLevelFilter {...defaultProps} />)).toMatchSnapshot();
});
test('clicking button calls onFilterChange', () => {
const wrapper = mount(<FilterBar {...defaultProps} />);
const wrapper = mount(<DeprecationLevelFilter {...defaultProps} />);
wrapper.find('button[data-test-subj="criticalLevelFilter"]').simulate('click');
expect(defaultProps.onFilterChange).toHaveBeenCalledTimes(1);
expect(defaultProps.onFilterChange.mock.calls[0][0]).toEqual(LevelFilterOption.critical);
expect(defaultProps.onFilterChange.mock.calls[0][0]).toEqual('critical');
});
});

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiFilterButton, EuiFilterGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { LevelFilterOption } from '../../types';
const LocalizedOptions: { [option: string]: string } = {
warning: i18n.translate(
'xpack.upgradeAssistant.checkupTab.controls.filterBar.warningButtonLabel',
{
defaultMessage: 'warning',
}
),
critical: i18n.translate(
'xpack.upgradeAssistant.checkupTab.controls.filterBar.criticalButtonLabel',
{ defaultMessage: 'critical' }
),
};
interface DeprecationLevelProps {
levelsCount: {
[key: string]: number;
};
currentFilter: LevelFilterOption;
onFilterChange(level: LevelFilterOption): void;
}
export const DeprecationLevelFilter: React.FunctionComponent<DeprecationLevelProps> = ({
levelsCount,
currentFilter,
onFilterChange,
}) => {
return (
<EuiFlexItem grow={false}>
<EuiFilterGroup>
<EuiFilterButton
key="critical"
onClick={() => {
onFilterChange(currentFilter !== 'critical' ? 'critical' : 'all');
}}
hasActiveFilters={currentFilter === 'critical'}
numFilters={levelsCount.critical || undefined}
data-test-subj="criticalLevelFilter"
>
{LocalizedOptions.critical}
</EuiFilterButton>
</EuiFilterGroup>
</EuiFlexItem>
);
};

View file

@ -0,0 +1,141 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { FunctionComponent, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButton,
EuiFieldSearch,
EuiFlexGroup,
EuiFlexItem,
EuiCallOut,
EuiSpacer,
} from '@elastic/eui';
import type { DomainDeprecationDetails } from 'kibana/public';
import { DeprecationInfo } from '../../../../../common/types';
import { validateRegExpString } from '../../../lib/utils';
import { GroupByOption, LevelFilterOption } from '../../types';
import { DeprecationLevelFilter } from './level_filter';
import { GroupByFilter } from './group_by_filter';
interface SearchBarProps {
allDeprecations?: DeprecationInfo[] | DomainDeprecationDetails;
isLoading: boolean;
loadData: () => void;
currentFilter: LevelFilterOption;
onFilterChange: (filter: LevelFilterOption) => void;
onSearchChange: (filter: string) => void;
totalDeprecationsCount: number;
levelToDeprecationCountMap: {
[key: string]: number;
};
groupByFilterProps?: {
availableGroupByOptions: GroupByOption[];
currentGroupBy: GroupByOption;
onGroupByChange: (groupBy: GroupByOption) => void;
};
}
const i18nTexts = {
searchAriaLabel: i18n.translate(
'xpack.upgradeAssistant.deprecationListSearchBar.placeholderAriaLabel',
{ defaultMessage: 'Filter' }
),
searchPlaceholderLabel: i18n.translate(
'xpack.upgradeAssistant.deprecationListSearchBar.placeholderLabel',
{
defaultMessage: 'Filter',
}
),
reloadButtonLabel: i18n.translate(
'xpack.upgradeAssistant.deprecationListSearchBar.reloadButtonLabel',
{
defaultMessage: 'Reload',
}
),
getInvalidSearchMessage: (searchTermError: string) =>
i18n.translate('xpack.upgradeAssistant.deprecationListSearchBar.filterErrorMessageLabel', {
defaultMessage: 'Filter invalid: {searchTermError}',
values: { searchTermError },
}),
};
export const SearchBar: FunctionComponent<SearchBarProps> = ({
totalDeprecationsCount,
levelToDeprecationCountMap,
isLoading,
loadData,
currentFilter,
onFilterChange,
onSearchChange,
groupByFilterProps,
}) => {
const [searchTermError, setSearchTermError] = useState<null | string>(null);
const filterInvalid = Boolean(searchTermError);
return (
<>
<EuiFlexGroup responsive={false}>
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiFieldSearch
isInvalid={filterInvalid}
aria-label={i18nTexts.searchAriaLabel}
placeholder={i18nTexts.searchPlaceholderLabel}
onChange={(e) => {
const string = e.target.value;
const errorMessage = validateRegExpString(string);
if (errorMessage) {
// Emit an empty search term to listeners if search term is invalid.
onSearchChange('');
setSearchTermError(errorMessage);
} else {
onSearchChange(e.target.value);
if (searchTermError) {
setSearchTermError(null);
}
}
}}
/>
</EuiFlexItem>
{/* These two components provide their own EuiFlexItem wrappers */}
<DeprecationLevelFilter
{...{
totalDeprecationsCount,
levelsCount: levelToDeprecationCountMap,
currentFilter,
onFilterChange,
}}
/>
{groupByFilterProps && <GroupByFilter {...groupByFilterProps} />}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={loadData} iconType="refresh" isLoading={isLoading}>
{i18nTexts.reloadButtonLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
{filterInvalid && (
<>
<EuiSpacer />
<EuiCallOut
color="danger"
title={i18nTexts.getInvalidSearchMessage(searchTermError!)}
iconType="faceSad"
/>
</>
)}
<EuiSpacer />
</>
);
};

View file

@ -32,11 +32,7 @@ export enum LoadingState {
Error,
}
export enum LevelFilterOption {
all = 'all',
critical = 'critical',
warning = 'warning',
}
export type LevelFilterOption = 'all' | 'critical';
export enum GroupByOption {
message = 'message',

View file

@ -18,6 +18,12 @@ const i18nTexts = {
esDeprecations: i18n.translate('xpack.upgradeAssistant.breadcrumb.esDeprecationsLabel', {
defaultMessage: 'Elasticsearch deprecations',
}),
kibanaDeprecations: i18n.translate(
'xpack.upgradeAssistant.breadcrumb.kibanaDeprecationsLabel',
{
defaultMessage: 'Kibana deprecations',
}
),
},
};
@ -42,6 +48,15 @@ export class BreadcrumbService {
text: i18nTexts.breadcrumbs.esDeprecations,
},
],
kibanaDeprecations: [
{
text: i18nTexts.breadcrumbs.overview,
href: '/',
},
{
text: i18nTexts.breadcrumbs.kibanaDeprecations,
},
],
};
private setBreadcrumbsHandler?: SetBreadcrumbs;
@ -50,7 +65,7 @@ export class BreadcrumbService {
this.setBreadcrumbsHandler = setBreadcrumbsHandler;
}
public setBreadcrumbs(type: 'overview' | 'esDeprecations'): void {
public setBreadcrumbs(type: 'overview' | 'esDeprecations' | 'kibanaDeprecations'): void {
if (!this.setBreadcrumbsHandler) {
throw new Error('Breadcrumb service has not been initialized');
}

View file

@ -19,7 +19,10 @@ export async function mountManagementSection(
params: ManagementAppMountParams,
kibanaVersionInfo: KibanaVersionContext
) {
const [{ i18n, docLinks, notifications, application }] = await coreSetup.getStartServices();
const [
{ i18n, docLinks, notifications, application, deprecations },
] = await coreSetup.getStartServices();
const { element, history, setBreadcrumbs } = params;
const { http } = coreSetup;
@ -39,5 +42,6 @@ export async function mountManagementSection(
api: apiService,
breadcrumbs: breadcrumbService,
getUrlForApp: application.getUrlForApp,
deprecations,
});
}

View file

@ -24,24 +24,30 @@ describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => {
overview: true,
cluster: true,
indices: true,
kibana: true,
savedObjects: { createInternalRepository: () => internalRepo } as any,
});
expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(3);
expect(internalRepo.incrementCounter).toHaveBeenCalledTimes(4);
expect(internalRepo.incrementCounter).toHaveBeenCalledWith(
UPGRADE_ASSISTANT_TYPE,
UPGRADE_ASSISTANT_DOC_ID,
[`ui_open.overview`]
['ui_open.overview']
);
expect(internalRepo.incrementCounter).toHaveBeenCalledWith(
UPGRADE_ASSISTANT_TYPE,
UPGRADE_ASSISTANT_DOC_ID,
[`ui_open.cluster`]
['ui_open.cluster']
);
expect(internalRepo.incrementCounter).toHaveBeenCalledWith(
UPGRADE_ASSISTANT_TYPE,
UPGRADE_ASSISTANT_DOC_ID,
[`ui_open.indices`]
['ui_open.indices']
);
expect(internalRepo.incrementCounter).toHaveBeenCalledWith(
UPGRADE_ASSISTANT_TYPE,
UPGRADE_ASSISTANT_DOC_ID,
['ui_open.kibana']
);
});
});

View file

@ -36,6 +36,7 @@ export async function upsertUIOpenOption({
cluster,
indices,
savedObjects,
kibana,
}: UpsertUIOpenOptionDependencies): Promise<UIOpen> {
if (overview) {
await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'overview' });
@ -49,9 +50,14 @@ export async function upsertUIOpenOption({
await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'indices' });
}
if (kibana) {
await incrementUIOpenOptionCounter({ savedObjects, uiOpenOptionCounter: 'kibana' });
}
return {
overview,
cluster,
indices,
kibana,
};
}

View file

@ -51,6 +51,7 @@ describe('Upgrade Assistant Usage Collector', () => {
'ui_open.overview': 10,
'ui_open.cluster': 20,
'ui_open.indices': 30,
'ui_open.kibana': 15,
'ui_reindex.close': 1,
'ui_reindex.open': 4,
'ui_reindex.start': 2,
@ -90,6 +91,7 @@ describe('Upgrade Assistant Usage Collector', () => {
overview: 10,
cluster: 20,
indices: 30,
kibana: 15,
},
ui_reindex: {
close: 1,

View file

@ -73,6 +73,7 @@ export async function fetchUpgradeAssistantMetrics(
overview: 0,
cluster: 0,
indices: 0,
kibana: 0,
},
ui_reindex: {
close: 0,
@ -91,6 +92,7 @@ export async function fetchUpgradeAssistantMetrics(
overview: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.overview', 0),
cluster: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.cluster', 0),
indices: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.indices', 0),
kibana: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.kibana', 0),
},
ui_reindex: {
close: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.close', 0),
@ -129,13 +131,41 @@ export function registerUpgradeAssistantUsageCollector({
schema: {
features: {
deprecation_logging: {
enabled: { type: 'boolean' },
enabled: {
type: 'boolean',
_meta: {
description: 'Whether user has enabled Elasticsearch deprecation logging',
},
},
},
},
ui_open: {
cluster: { type: 'long' },
indices: { type: 'long' },
overview: { type: 'long' },
cluster: {
type: 'long',
_meta: {
description:
'Number of times a user viewed the list of Elasticsearch cluster deprecations.',
},
},
indices: {
type: 'long',
_meta: {
description:
'Number of times a user viewed the list of Elasticsearch index deprecations.',
},
},
overview: {
type: 'long',
_meta: {
description: 'Number of times a user viewed the Overview page.',
},
},
kibana: {
type: 'long',
_meta: {
description: 'Number of times a user viewed the list of Kibana deprecations',
},
},
},
ui_reindex: {
close: { type: 'long' },

View file

@ -20,17 +20,19 @@ export function registerTelemetryRoutes({ router, getSavedObjectsService }: Rout
overview: schema.boolean({ defaultValue: false }),
cluster: schema.boolean({ defaultValue: false }),
indices: schema.boolean({ defaultValue: false }),
kibana: schema.boolean({ defaultValue: false }),
}),
},
},
async (ctx, request, response) => {
const { cluster, indices, overview } = request.body;
const { cluster, indices, overview, kibana } = request.body;
return response.ok({
body: await upsertUIOpenOption({
savedObjects: getSavedObjectsService(),
cluster,
indices,
overview,
kibana,
}),
});
}

View file

@ -29,6 +29,10 @@ export const telemetrySavedObjectType: SavedObjectsType = {
type: 'long',
null_value: 0,
},
kibana: {
type: 'long',
null_value: 0,
},
},
},
ui_reindex: {

View file

@ -12,7 +12,10 @@ import { ResponseError } from '../../public/application/lib/api';
// Register helpers to mock HTTP Requests
const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
const setLoadStatusResponse = (response?: UpgradeAssistantStatus, error?: ResponseError) => {
const setLoadEsDeprecationsResponse = (
response?: UpgradeAssistantStatus,
error?: ResponseError
) => {
const status = error ? error.statusCode || 400 : 200;
const body = error ? error : response;
@ -60,7 +63,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
};
return {
setLoadStatusResponse,
setLoadEsDeprecationsResponse,
setLoadDeprecationLoggingResponse,
setUpdateDeprecationLoggingResponse,
setUpdateIndexSettingsResponse,

View file

@ -7,5 +7,6 @@
export { setup as setupOverviewPage, OverviewTestBed } from './overview.helpers';
export { setup as setupIndicesPage, IndicesTestBed } from './indices.helpers';
export { setup as setupKibanaPage, KibanaTestBed } from './kibana.helpers';
export { setupEnvironment } from './setup_environment';

View file

@ -0,0 +1,59 @@
/*
* 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 { registerTestBed, TestBed, TestBedConfig } from '@kbn/test/jest';
import { KibanaDeprecationsContent } from '../../public/application/components/kibana_deprecations';
import { WithAppDependencies } from './setup_environment';
const testBedConfig: TestBedConfig = {
memoryRouter: {
initialEntries: ['/kibana_deprecations'],
componentRoutePath: '/kibana_deprecations',
},
doMountAsync: true,
};
export type KibanaTestBed = TestBed<KibanaTestSubjects> & {
actions: ReturnType<typeof createActions>;
};
const createActions = (testBed: TestBed) => {
/**
* User Actions
*/
const clickExpandAll = () => {
const { find } = testBed;
find('expandAll').simulate('click');
};
return {
clickExpandAll,
};
};
export const setup = async (overrides?: Record<string, unknown>): Promise<KibanaTestBed> => {
const initTestBed = registerTestBed(
WithAppDependencies(KibanaDeprecationsContent, overrides),
testBedConfig
);
const testBed = await initTestBed();
return {
...testBed,
actions: createActions(testBed),
};
};
export type KibanaTestSubjects =
| 'expandAll'
| 'noDeprecationsPrompt'
| 'kibanaPluginError'
| 'kibanaDeprecationsContent'
| 'kibanaDeprecationItem'
| 'kibanaRequestError'
| string;

View file

@ -34,6 +34,9 @@ export type OverviewTestSubjects =
| 'esStatsPanel'
| 'esStatsPanel.totalDeprecations'
| 'esStatsPanel.criticalDeprecations'
| 'kibanaStatsPanel'
| 'kibanaStatsPanel.totalDeprecations'
| 'kibanaStatsPanel.criticalDeprecations'
| 'deprecationLoggingFormRow'
| 'requestErrorIconTip'
| 'partiallyUpgradedErrorIconTip'

View file

@ -10,7 +10,11 @@ import axios from 'axios';
// @ts-ignore
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import { docLinksServiceMock, notificationServiceMock } from '../../../../../src/core/public/mocks';
import {
deprecationsServiceMock,
docLinksServiceMock,
notificationServiceMock,
} from '../../../../../src/core/public/mocks';
import { HttpSetup } from '../../../../../src/core/public';
import { mockKibanaSemverVersion, UA_READONLY_MODE } from '../../common/constants';
@ -41,6 +45,7 @@ export const WithAppDependencies = (Comp: any, overrides: Record<string, unknown
api: apiService,
breadcrumbs: breadcrumbService,
getUrlForApp: () => '',
deprecations: deprecationsServiceMock.createStartContract(),
};
return (

View file

@ -35,7 +35,7 @@ describe('Indices tab', () => {
};
beforeEach(async () => {
httpRequestsMockHelpers.setLoadStatusResponse(upgradeStatusMockResponse);
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(upgradeStatusMockResponse);
httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ isEnabled: true });
await act(async () => {
@ -118,7 +118,7 @@ describe('Indices tab', () => {
indices: [],
};
httpRequestsMockHelpers.setLoadStatusResponse(noDeprecationsResponse);
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(noDeprecationsResponse);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
@ -144,7 +144,7 @@ describe('Indices tab', () => {
message: 'Forbidden',
};
httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
@ -170,7 +170,7 @@ describe('Indices tab', () => {
},
};
httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
@ -196,7 +196,7 @@ describe('Indices tab', () => {
},
};
httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });
@ -219,7 +219,7 @@ describe('Indices tab', () => {
message: 'Internal server error',
};
httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
await act(async () => {
testBed = await setupIndicesPage({ isReadOnlyMode: false });

View file

@ -0,0 +1,230 @@
/*
* 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 type { DomainDeprecationDetails } from 'kibana/public';
import { act } from 'react-dom/test-utils';
import { deprecationsServiceMock } from 'src/core/public/mocks';
import { KibanaTestBed, setupKibanaPage, setupEnvironment } from './helpers';
describe('Kibana deprecations', () => {
let testBed: KibanaTestBed;
const { server } = setupEnvironment();
afterAll(() => {
server.restore();
});
describe('With deprecations', () => {
const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [
{
correctiveActions: {
manualSteps: ['Step 1', 'Step 2', 'Step 3'],
api: {
method: 'POST',
path: '/test',
},
},
domainId: 'test_domain',
level: 'critical',
message: 'Test deprecation message',
},
];
beforeEach(async () => {
await act(async () => {
const deprecationService = deprecationsServiceMock.createStartContract();
deprecationService.getAllDeprecations = jest
.fn()
.mockReturnValue(kibanaDeprecationsMockResponse);
testBed = await setupKibanaPage({
deprecations: deprecationService,
});
});
testBed.component.update();
});
test('renders deprecations', () => {
const { exists, find } = testBed;
expect(exists('kibanaDeprecationsContent')).toBe(true);
expect(find('kibanaDeprecationItem').length).toEqual(1);
});
describe('manual steps modal', () => {
test('renders modal with a list of steps to fix a deprecation', async () => {
const { component, actions, exists, find } = testBed;
const deprecation = kibanaDeprecationsMockResponse[0];
expect(exists('kibanaDeprecationsContent')).toBe(true);
// Open all deprecations
actions.clickExpandAll();
const accordionTestSubj = `${deprecation.domainId}Deprecation`;
await act(async () => {
find(`${accordionTestSubj}.stepsButton`).simulate('click');
});
component.update();
// We need to read the document "body" as the modal is added there and not inside
// the component DOM tree.
let modal = document.body.querySelector('[data-test-subj="stepsModal"]');
expect(modal).not.toBe(null);
expect(modal!.textContent).toContain(`Fix '${deprecation.domainId}'`);
const steps: NodeListOf<Element> | null = modal!.querySelectorAll(
'[data-test-subj="fixDeprecationSteps"] .euiStep'
);
expect(steps).not.toBe(null);
expect(steps.length).toEqual(deprecation!.correctiveActions!.manualSteps!.length);
await act(async () => {
const closeButton: HTMLButtonElement | null = modal!.querySelector(
'[data-test-subj="closeButton"]'
);
closeButton!.click();
});
component.update();
// Confirm modal closed and no longer appears in the DOM
modal = document.body.querySelector('[data-test-subj="stepsModal"]');
expect(modal).toBe(null);
});
});
describe('resolve modal', () => {
test('renders confirmation modal to resolve a deprecation', async () => {
const { component, actions, exists, find } = testBed;
const deprecation = kibanaDeprecationsMockResponse[0];
expect(exists('kibanaDeprecationsContent')).toBe(true);
// Open all deprecations
actions.clickExpandAll();
const accordionTestSubj = `${deprecation.domainId}Deprecation`;
await act(async () => {
find(`${accordionTestSubj}.resolveButton`).simulate('click');
});
component.update();
// We need to read the document "body" as the modal is added there and not inside
// the component DOM tree.
let modal = document.body.querySelector('[data-test-subj="resolveModal"]');
expect(modal).not.toBe(null);
expect(modal!.textContent).toContain(`Resolve '${deprecation.domainId}'`);
const confirmButton: HTMLButtonElement | null = modal!.querySelector(
'[data-test-subj="confirmModalConfirmButton"]'
);
await act(async () => {
confirmButton!.click();
});
component.update();
// Confirm modal should close and no longer appears in the DOM
modal = document.body.querySelector('[data-test-subj="resolveModal"]');
expect(modal).toBe(null);
});
});
});
describe('No deprecations', () => {
beforeEach(async () => {
await act(async () => {
testBed = await setupKibanaPage({ isReadOnlyMode: false });
});
const { component } = testBed;
component.update();
});
test('renders prompt', () => {
const { exists, find } = testBed;
expect(exists('noDeprecationsPrompt')).toBe(true);
expect(find('noDeprecationsPrompt').text()).toContain('Ready to upgrade!');
});
});
describe('Error handling', () => {
test('handles request error', async () => {
await act(async () => {
const deprecationService = deprecationsServiceMock.createStartContract();
deprecationService.getAllDeprecations = jest
.fn()
.mockRejectedValue(new Error('Internal Server Error'));
testBed = await setupKibanaPage({
deprecations: deprecationService,
});
});
const { component, exists, find } = testBed;
component.update();
expect(exists('kibanaRequestError')).toBe(true);
expect(find('kibanaRequestError').text()).toContain(
'Could not retrieve Kibana deprecations.'
);
});
test('handles deprecation service error', async () => {
const domainId = 'test';
const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [
{
domainId,
message: `Failed to get deprecations info for plugin "${domainId}".`,
level: 'fetch_error',
correctiveActions: {
manualSteps: ['Check Kibana server logs for error message.'],
},
},
];
await act(async () => {
const deprecationService = deprecationsServiceMock.createStartContract();
deprecationService.getAllDeprecations = jest
.fn()
.mockReturnValue(kibanaDeprecationsMockResponse);
testBed = await setupKibanaPage({
deprecations: deprecationService,
});
});
const { component, exists, find, actions } = testBed;
component.update();
// Verify top-level callout renders
expect(exists('kibanaPluginError')).toBe(true);
expect(find('kibanaPluginError').text()).toContain(
'Not all Kibana deprecations were retrieved successfully.'
);
// Open all deprecations
actions.clickExpandAll();
// Verify callout also displays for deprecation with error
expect(exists(`${domainId}Error`)).toBe(true);
});
});
});

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import type { DomainDeprecationDetails } from 'kibana/public';
import { act } from 'react-dom/test-utils';
import { deprecationsServiceMock } from 'src/core/public/mocks';
import { UpgradeAssistantStatus } from '../common/types';
import { OverviewTestBed, setupOverviewPage, setupEnvironment } from './helpers';
@ -14,17 +17,54 @@ describe('Overview page', () => {
const { server, httpRequestsMockHelpers } = setupEnvironment();
beforeEach(async () => {
const upgradeStatusMockResponse = {
const esDeprecationsMockResponse: UpgradeAssistantStatus = {
readyForUpgrade: false,
cluster: [],
indices: [],
cluster: [
{
level: 'critical',
message: 'Index Lifecycle Management poll interval is set too low',
url:
'https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html#ilm-poll-interval-limit',
details:
'The Index Lifecycle Management poll interval setting [indices.lifecycle.poll_interval] is currently set to [500ms], but must be 1s or greater',
},
],
indices: [
{
level: 'warning',
message: 'translog retention settings are ignored',
url:
'https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-translog.html',
details:
'translog retention settings [index.translog.retention.size] and [index.translog.retention.age] are ignored because translog is no longer used in peer recoveries with soft-deletes enabled (default in 7.0 or later)',
index: 'settings',
reindex: false,
},
],
};
httpRequestsMockHelpers.setLoadStatusResponse(upgradeStatusMockResponse);
const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [
{
correctiveActions: {},
domainId: 'xpack.spaces',
level: 'critical',
message:
'Disabling the spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)',
},
];
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(esDeprecationsMockResponse);
httpRequestsMockHelpers.setLoadDeprecationLoggingResponse({ isEnabled: true });
await act(async () => {
testBed = await setupOverviewPage();
const deprecationService = deprecationsServiceMock.createStartContract();
deprecationService.getAllDeprecations = jest
.fn()
.mockReturnValue(kibanaDeprecationsMockResponse);
testBed = await setupOverviewPage({
deprecations: deprecationService,
});
});
const { component } = testBed;
@ -39,10 +79,16 @@ describe('Overview page', () => {
const { exists, find } = testBed;
expect(exists('overviewPageContent')).toBe(true);
// Verify ES stats
expect(exists('esStatsPanel')).toBe(true);
expect(find('esStatsPanel.totalDeprecations').text()).toContain('0');
expect(find('esStatsPanel.criticalDeprecations').text()).toContain('0');
expect(find('esStatsPanel.totalDeprecations').text()).toContain('2');
expect(find('esStatsPanel.criticalDeprecations').text()).toContain('1');
// Verify Kibana stats
expect(exists('kibanaStatsPanel')).toBe(true);
expect(find('kibanaStatsPanel.totalDeprecations').text()).toContain('1');
expect(find('kibanaStatsPanel.criticalDeprecations').text()).toContain('1');
});
describe('Deprecation logging', () => {
@ -96,90 +142,113 @@ describe('Overview page', () => {
});
describe('Error handling', () => {
test('handles network failure', async () => {
const error = {
statusCode: 500,
error: 'Internal server error',
message: 'Internal server error',
};
describe('Kibana deprecations', () => {
test('handles network failure', async () => {
await act(async () => {
const deprecationService = deprecationsServiceMock.createStartContract();
deprecationService.getAllDeprecations = jest
.fn()
.mockRejectedValue(new Error('Internal Server Error'));
httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
testBed = await setupOverviewPage({
deprecations: deprecationService,
});
});
await act(async () => {
testBed = await setupOverviewPage();
const { component, exists } = testBed;
component.update();
expect(exists('requestErrorIconTip')).toBe(true);
});
const { component, exists } = testBed;
component.update();
expect(exists('requestErrorIconTip')).toBe(true);
});
test('handles unauthorized error', async () => {
const error = {
statusCode: 403,
error: 'Forbidden',
message: 'Forbidden',
};
describe('Elasticsearch deprecations', () => {
test('handles network failure', async () => {
const error = {
statusCode: 500,
error: 'Internal server error',
message: 'Internal server error',
};
httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
await act(async () => {
testBed = await setupOverviewPage();
await act(async () => {
testBed = await setupOverviewPage();
});
const { component, exists } = testBed;
component.update();
expect(exists('requestErrorIconTip')).toBe(true);
});
const { component, exists } = testBed;
test('handles unauthorized error', async () => {
const error = {
statusCode: 403,
error: 'Forbidden',
message: 'Forbidden',
};
component.update();
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
expect(exists('unauthorizedErrorIconTip')).toBe(true);
});
await act(async () => {
testBed = await setupOverviewPage();
});
test('handles partially upgraded error', async () => {
const error = {
statusCode: 426,
error: 'Upgrade required',
message: 'There are some nodes running a different version of Elasticsearch',
attributes: {
allNodesUpgraded: false,
},
};
const { component, exists } = testBed;
httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
component.update();
await act(async () => {
testBed = await setupOverviewPage({ isReadOnlyMode: false });
expect(exists('unauthorizedErrorIconTip')).toBe(true);
});
const { component, exists } = testBed;
test('handles partially upgraded error', async () => {
const error = {
statusCode: 426,
error: 'Upgrade required',
message: 'There are some nodes running a different version of Elasticsearch',
attributes: {
allNodesUpgraded: false,
},
};
component.update();
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
expect(exists('partiallyUpgradedErrorIconTip')).toBe(true);
});
await act(async () => {
testBed = await setupOverviewPage({ isReadOnlyMode: false });
});
test('handles upgrade error', async () => {
const error = {
statusCode: 426,
error: 'Upgrade required',
message: 'There are some nodes running a different version of Elasticsearch',
attributes: {
allNodesUpgraded: true,
},
};
const { component, exists } = testBed;
httpRequestsMockHelpers.setLoadStatusResponse(undefined, error);
component.update();
await act(async () => {
testBed = await setupOverviewPage({ isReadOnlyMode: false });
expect(exists('partiallyUpgradedErrorIconTip')).toBe(true);
});
const { component, exists } = testBed;
test('handles upgrade error', async () => {
const error = {
statusCode: 426,
error: 'Upgrade required',
message: 'There are some nodes running a different version of Elasticsearch',
attributes: {
allNodesUpgraded: true,
},
};
component.update();
httpRequestsMockHelpers.setLoadEsDeprecationsResponse(undefined, error);
expect(exists('upgradedErrorIconTip')).toBe(true);
await act(async () => {
testBed = await setupOverviewPage({ isReadOnlyMode: false });
});
const { component, exists } = testBed;
component.update();
expect(exists('upgradedErrorIconTip')).toBe(true);
});
});
});
});

View file

@ -34,19 +34,57 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await a11y.testAppSnapshot();
});
it('Elasticsearch cluster tab', async () => {
await testSubjects.click('esDeprecationsLink');
await retry.waitFor('Upgrade Assistant Cluster tab to be visible', async () => {
it('Elasticsearch cluster deprecations', async () => {
await PageObjects.common.navigateToUrl(
'management',
'stack/upgrade_assistant/es_deprecations/cluster',
{
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
shouldUseHashForSubUrl: false,
}
);
await retry.waitFor('Cluster tab to be visible', async () => {
return testSubjects.exists('clusterTabContent');
});
await a11y.testAppSnapshot();
});
it('Elasticsearch indices tab', async () => {
await testSubjects.click('upgradeAssistantIndicesTab');
await retry.waitFor('Upgrade Assistant Indices tab to be visible', async () => {
it('Elasticsearch index deprecations', async () => {
await PageObjects.common.navigateToUrl(
'management',
'stack/upgrade_assistant/es_deprecations/indices',
{
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
shouldUseHashForSubUrl: false,
}
);
await retry.waitFor('Indices tab to be visible', async () => {
return testSubjects.exists('indexTabContent');
});
await a11y.testAppSnapshot();
});
it('Kibana deprecations', async () => {
await PageObjects.common.navigateToUrl(
'management',
'stack/upgrade_assistant/kibana_deprecations',
{
ensureCurrentUrl: false,
shouldLoginIfPrompted: false,
shouldUseHashForSubUrl: false,
}
);
await retry.waitFor('Kibana deprecations to be visible', async () => {
return testSubjects.exists('kibanaDeprecationsContent');
});
await a11y.testAppSnapshot();
});
});