mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
parent
717c7f2c48
commit
21b22c5b59
56 changed files with 2808 additions and 347 deletions
|
@ -4,10 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Chance from 'chance';
|
||||
import { getRepository } from '../../../test/fixtures';
|
||||
export const REPOSITORY_NAME = 'my-test-repository';
|
||||
|
||||
const chance = new Chance();
|
||||
const CHARS_POOL = 'abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
export const getRandomString = (options = {}) =>
|
||||
`${chance.string({ pool: CHARS_POOL, ...options })}-${Date.now()}`;
|
||||
export const REPOSITORY_EDIT = getRepository({ name: REPOSITORY_NAME });
|
|
@ -0,0 +1,392 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import {
|
||||
registerTestBed,
|
||||
findTestSubject,
|
||||
TestBed,
|
||||
TestBedConfig,
|
||||
nextTick,
|
||||
} from '../../../../../test_utils';
|
||||
import { SnapshotRestoreHome } from '../../../public/app/sections/home/home';
|
||||
import { BASE_PATH } from '../../../public/app/constants';
|
||||
import { WithProviders } from './providers';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
initialEntries: [`${BASE_PATH}/repositories`],
|
||||
componentRoutePath: `${BASE_PATH}/:section(repositories|snapshots)/:repositoryName?/:snapshotId*`,
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
const initTestBed = registerTestBed(WithProviders(SnapshotRestoreHome), testBedConfig);
|
||||
|
||||
export interface HomeTestBed extends TestBed<HomeTestSubjects> {
|
||||
actions: {
|
||||
clickReloadButton: () => void;
|
||||
selectRepositoryAt: (index: number) => void;
|
||||
clickRepositoryAt: (index: number) => void;
|
||||
clickSnapshotAt: (index: number) => void;
|
||||
clickRepositoryActionAt: (index: number, action: 'delete' | 'edit') => void;
|
||||
selectTab: (tab: 'snapshots' | 'repositories') => void;
|
||||
selectSnapshotDetailTab: (tab: 'summary' | 'failedIndices') => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (): Promise<HomeTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
const REPOSITORY_TABLE = 'repositoryTable';
|
||||
const SNAPSHOT_TABLE = 'snapshotTable';
|
||||
const { find, table, router, component } = testBed;
|
||||
|
||||
/**
|
||||
* User Actions
|
||||
*/
|
||||
const clickReloadButton = () => {
|
||||
find('reloadButton').simulate('click');
|
||||
};
|
||||
|
||||
const selectRepositoryAt = (index: number) => {
|
||||
const { rows } = table.getMetaData(REPOSITORY_TABLE);
|
||||
const row = rows[index];
|
||||
const checkBox = row.reactWrapper.find('input').hostNodes();
|
||||
checkBox.simulate('change', { target: { checked: true } });
|
||||
};
|
||||
|
||||
const clickRepositoryAt = async (index: number) => {
|
||||
const { rows } = table.getMetaData(REPOSITORY_TABLE);
|
||||
const repositoryLink = findTestSubject(rows[index].reactWrapper, 'repositoryLink');
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
const { href } = repositoryLink.props();
|
||||
router.navigateTo(href!);
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
};
|
||||
|
||||
const clickRepositoryActionAt = async (index: number, action: 'delete' | 'edit') => {
|
||||
const { rows } = table.getMetaData('repositoryTable');
|
||||
const currentRow = rows[index];
|
||||
const lastColumn = currentRow.columns[currentRow.columns.length - 1].reactWrapper;
|
||||
const button = findTestSubject(lastColumn, `${action}RepositoryButton`);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
button.simulate('click');
|
||||
component.update();
|
||||
});
|
||||
};
|
||||
|
||||
const clickSnapshotAt = async (index: number) => {
|
||||
const { rows } = table.getMetaData(SNAPSHOT_TABLE);
|
||||
const snapshotLink = findTestSubject(rows[index].reactWrapper, 'snapshotLink');
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
const { href } = snapshotLink.props();
|
||||
router.navigateTo(href!);
|
||||
await nextTick(100);
|
||||
component.update();
|
||||
});
|
||||
};
|
||||
|
||||
const selectTab = (tab: 'repositories' | 'snapshots') => {
|
||||
const tabs = ['snapshots', 'repositories'];
|
||||
|
||||
testBed
|
||||
.find('tab')
|
||||
.at(tabs.indexOf(tab))
|
||||
.simulate('click');
|
||||
};
|
||||
|
||||
const selectSnapshotDetailTab = (tab: 'summary' | 'failedIndices') => {
|
||||
const tabs = ['summary', 'failedIndices'];
|
||||
|
||||
testBed
|
||||
.find('snapshotDetail.tab')
|
||||
.at(tabs.indexOf(tab))
|
||||
.simulate('click');
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
clickReloadButton,
|
||||
selectRepositoryAt,
|
||||
clickRepositoryAt,
|
||||
clickRepositoryActionAt,
|
||||
clickSnapshotAt,
|
||||
selectTab,
|
||||
selectSnapshotDetailTab,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type HomeTestSubjects = TestSubjects | ThreeLevelDepth | NonVisibleTestSubjects;
|
||||
|
||||
type NonVisibleTestSubjects =
|
||||
| 'snapshotDetail.sectionLoading'
|
||||
| 'sectionLoading'
|
||||
| 'emptyPrompt'
|
||||
| 'emptyPrompt.documentationLink'
|
||||
| 'emptyPrompt.title'
|
||||
| 'emptyPrompt.registerRepositoryButton'
|
||||
| 'repositoryDetail.sectionLoading'
|
||||
| 'snapshotDetail.indexFailure';
|
||||
|
||||
type ThreeLevelDepth =
|
||||
| 'snapshotDetail.uuid.value'
|
||||
| 'snapshotDetail.state.value'
|
||||
| 'snapshotDetail.version.value'
|
||||
| 'snapshotDetail.includeGlobalState.value'
|
||||
| 'snapshotDetail.indices.title'
|
||||
| 'snapshotDetail.startTime.value'
|
||||
| 'snapshotDetail.endTime.value'
|
||||
| 'snapshotDetail.indexFailure.index'
|
||||
| 'snapshotDetail.indices.value';
|
||||
|
||||
export type TestSubjects =
|
||||
| 'appTitle'
|
||||
| 'cell'
|
||||
| 'cell.repositoryLink'
|
||||
| 'cell.snapshotLink'
|
||||
| 'checkboxSelectAll'
|
||||
| 'checkboxSelectRow-my-repo'
|
||||
| 'closeButton'
|
||||
| 'content'
|
||||
| 'content.documentationLink'
|
||||
| 'content.duration'
|
||||
| 'content.endTime'
|
||||
| 'content.includeGlobalState'
|
||||
| 'content.indices'
|
||||
| 'content.repositoryType'
|
||||
| 'content.snapshotCount'
|
||||
| 'content.startTime'
|
||||
| 'content.state'
|
||||
| 'content.title'
|
||||
| 'content.uuid'
|
||||
| 'content.value'
|
||||
| 'content.verifyRepositoryButton'
|
||||
| 'content.version'
|
||||
| 'deleteRepositoryButton'
|
||||
| 'detailTitle'
|
||||
| 'documentationLink'
|
||||
| 'duration'
|
||||
| 'duration.title'
|
||||
| 'duration.value'
|
||||
| 'editRepositoryButton'
|
||||
| 'endTime'
|
||||
| 'endTime.title'
|
||||
| 'endTime.value'
|
||||
| 'euiFlyoutCloseButton'
|
||||
| 'includeGlobalState'
|
||||
| 'includeGlobalState.title'
|
||||
| 'includeGlobalState.value'
|
||||
| 'indices'
|
||||
| 'indices.title'
|
||||
| 'indices.value'
|
||||
| 'registerRepositoryButton'
|
||||
| 'reloadButton'
|
||||
| 'repositoryDetail'
|
||||
| 'repositoryDetail.content'
|
||||
| 'repositoryDetail.documentationLink'
|
||||
| 'repositoryDetail.euiFlyoutCloseButton'
|
||||
| 'repositoryDetail.repositoryType'
|
||||
| 'repositoryDetail.snapshotCount'
|
||||
| 'repositoryDetail.srRepositoryDetailsDeleteActionButton'
|
||||
| 'repositoryDetail.srRepositoryDetailsFlyoutCloseButton'
|
||||
| 'repositoryDetail.title'
|
||||
| 'repositoryDetail.verifyRepositoryButton'
|
||||
| 'repositoryLink'
|
||||
| 'repositoryList'
|
||||
| 'repositoryList.cell'
|
||||
| 'repositoryList.checkboxSelectAll'
|
||||
| 'repositoryList.checkboxSelectRow-my-repo'
|
||||
| 'repositoryList.content'
|
||||
| 'repositoryList.deleteRepositoryButton'
|
||||
| 'repositoryList.documentationLink'
|
||||
| 'repositoryList.editRepositoryButton'
|
||||
| 'repositoryList.euiFlyoutCloseButton'
|
||||
| 'repositoryList.registerRepositoryButton'
|
||||
| 'repositoryList.reloadButton'
|
||||
| 'repositoryList.repositoryDetail'
|
||||
| 'repositoryList.repositoryLink'
|
||||
| 'repositoryList.repositoryTable'
|
||||
| 'repositoryList.repositoryType'
|
||||
| 'repositoryList.row'
|
||||
| 'repositoryList.snapshotCount'
|
||||
| 'repositoryList.srRepositoryDetailsDeleteActionButton'
|
||||
| 'repositoryList.srRepositoryDetailsFlyoutCloseButton'
|
||||
| 'repositoryList.tableHeaderCell_name_0'
|
||||
| 'repositoryList.tableHeaderCell_type_1'
|
||||
| 'repositoryList.tableHeaderSortButton'
|
||||
| 'repositoryList.title'
|
||||
| 'repositoryList.verifyRepositoryButton'
|
||||
| 'repositoryTable'
|
||||
| 'repositoryTable.cell'
|
||||
| 'repositoryTable.checkboxSelectAll'
|
||||
| 'repositoryTable.checkboxSelectRow-my-repo'
|
||||
| 'repositoryTable.deleteRepositoryButton'
|
||||
| 'repositoryTable.editRepositoryButton'
|
||||
| 'repositoryTable.repositoryLink'
|
||||
| 'repositoryTable.row'
|
||||
| 'repositoryTable.tableHeaderCell_name_0'
|
||||
| 'repositoryTable.tableHeaderCell_type_1'
|
||||
| 'repositoryTable.tableHeaderSortButton'
|
||||
| 'repositoryType'
|
||||
| 'row'
|
||||
| 'row.cell'
|
||||
| 'row.checkboxSelectRow-my-repo'
|
||||
| 'row.deleteRepositoryButton'
|
||||
| 'row.editRepositoryButton'
|
||||
| 'row.repositoryLink'
|
||||
| 'row.snapshotLink'
|
||||
| 'snapshotCount'
|
||||
| 'snapshotDetail'
|
||||
| 'snapshotDetail.closeButton'
|
||||
| 'snapshotDetail.content'
|
||||
| 'snapshotDetail.detailTitle'
|
||||
| 'snapshotDetail.duration'
|
||||
| 'snapshotDetail.endTime'
|
||||
| 'snapshotDetail.euiFlyoutCloseButton'
|
||||
| 'snapshotDetail.includeGlobalState'
|
||||
| 'snapshotDetail.indices'
|
||||
| 'snapshotDetail.repositoryLink'
|
||||
| 'snapshotDetail.startTime'
|
||||
| 'snapshotDetail.state'
|
||||
| 'snapshotDetail.tab'
|
||||
| 'snapshotDetail.title'
|
||||
| 'snapshotDetail.uuid'
|
||||
| 'snapshotDetail.value'
|
||||
| 'snapshotDetail.version'
|
||||
| 'snapshotLink'
|
||||
| 'snapshotList'
|
||||
| 'snapshotList.cell'
|
||||
| 'snapshotList.closeButton'
|
||||
| 'snapshotList.content'
|
||||
| 'snapshotList.detailTitle'
|
||||
| 'snapshotList.duration'
|
||||
| 'snapshotList.endTime'
|
||||
| 'snapshotList.euiFlyoutCloseButton'
|
||||
| 'snapshotList.includeGlobalState'
|
||||
| 'snapshotList.indices'
|
||||
| 'snapshotList.reloadButton'
|
||||
| 'snapshotList.repositoryLink'
|
||||
| 'snapshotList.row'
|
||||
| 'snapshotList.snapshotDetail'
|
||||
| 'snapshotList.snapshotLink'
|
||||
| 'snapshotList.snapshotTable'
|
||||
| 'snapshotList.startTime'
|
||||
| 'snapshotList.state'
|
||||
| 'snapshotList.tab'
|
||||
| 'snapshotList.tableHeaderCell_durationInMillis_3'
|
||||
| 'snapshotList.tableHeaderCell_indices_4'
|
||||
| 'snapshotList.tableHeaderCell_repository_1'
|
||||
| 'snapshotList.tableHeaderCell_snapshot_0'
|
||||
| 'snapshotList.tableHeaderCell_startTimeInMillis_2'
|
||||
| 'snapshotList.tableHeaderSortButton'
|
||||
| 'snapshotList.title'
|
||||
| 'snapshotList.uuid'
|
||||
| 'snapshotList.value'
|
||||
| 'snapshotList.version'
|
||||
| 'snapshotRestoreApp'
|
||||
| 'snapshotRestoreApp.appTitle'
|
||||
| 'snapshotRestoreApp.cell'
|
||||
| 'snapshotRestoreApp.checkboxSelectAll'
|
||||
| 'snapshotRestoreApp.checkboxSelectRow-my-repo'
|
||||
| 'snapshotRestoreApp.closeButton'
|
||||
| 'snapshotRestoreApp.content'
|
||||
| 'snapshotRestoreApp.deleteRepositoryButton'
|
||||
| 'snapshotRestoreApp.detailTitle'
|
||||
| 'snapshotRestoreApp.documentationLink'
|
||||
| 'snapshotRestoreApp.duration'
|
||||
| 'snapshotRestoreApp.editRepositoryButton'
|
||||
| 'snapshotRestoreApp.endTime'
|
||||
| 'snapshotRestoreApp.euiFlyoutCloseButton'
|
||||
| 'snapshotRestoreApp.includeGlobalState'
|
||||
| 'snapshotRestoreApp.indices'
|
||||
| 'snapshotRestoreApp.registerRepositoryButton'
|
||||
| 'snapshotRestoreApp.reloadButton'
|
||||
| 'snapshotRestoreApp.repositoryDetail'
|
||||
| 'snapshotRestoreApp.repositoryLink'
|
||||
| 'snapshotRestoreApp.repositoryList'
|
||||
| 'snapshotRestoreApp.repositoryTable'
|
||||
| 'snapshotRestoreApp.repositoryType'
|
||||
| 'snapshotRestoreApp.row'
|
||||
| 'snapshotRestoreApp.snapshotCount'
|
||||
| 'snapshotRestoreApp.snapshotDetail'
|
||||
| 'snapshotRestoreApp.snapshotLink'
|
||||
| 'snapshotRestoreApp.snapshotList'
|
||||
| 'snapshotRestoreApp.snapshotTable'
|
||||
| 'snapshotRestoreApp.srRepositoryDetailsDeleteActionButton'
|
||||
| 'snapshotRestoreApp.srRepositoryDetailsFlyoutCloseButton'
|
||||
| 'snapshotRestoreApp.startTime'
|
||||
| 'snapshotRestoreApp.state'
|
||||
| 'snapshotRestoreApp.tab'
|
||||
| 'snapshotRestoreApp.tableHeaderCell_durationInMillis_3'
|
||||
| 'snapshotRestoreApp.tableHeaderCell_indices_4'
|
||||
| 'snapshotRestoreApp.tableHeaderCell_name_0'
|
||||
| 'snapshotRestoreApp.tableHeaderCell_repository_1'
|
||||
| 'snapshotRestoreApp.tableHeaderCell_snapshot_0'
|
||||
| 'snapshotRestoreApp.tableHeaderCell_startTimeInMillis_2'
|
||||
| 'snapshotRestoreApp.tableHeaderCell_type_1'
|
||||
| 'snapshotRestoreApp.tableHeaderSortButton'
|
||||
| 'snapshotRestoreApp.title'
|
||||
| 'snapshotRestoreApp.uuid'
|
||||
| 'snapshotRestoreApp.value'
|
||||
| 'snapshotRestoreApp.verifyRepositoryButton'
|
||||
| 'snapshotRestoreApp.version'
|
||||
| 'snapshotTable'
|
||||
| 'snapshotTable.cell'
|
||||
| 'snapshotTable.repositoryLink'
|
||||
| 'snapshotTable.row'
|
||||
| 'snapshotTable.snapshotLink'
|
||||
| 'snapshotTable.tableHeaderCell_durationInMillis_3'
|
||||
| 'snapshotTable.tableHeaderCell_indices_4'
|
||||
| 'snapshotTable.tableHeaderCell_repository_1'
|
||||
| 'snapshotTable.tableHeaderCell_snapshot_0'
|
||||
| 'snapshotTable.tableHeaderCell_startTimeInMillis_2'
|
||||
| 'snapshotTable.tableHeaderSortButton'
|
||||
| 'srRepositoryDetailsDeleteActionButton'
|
||||
| 'srRepositoryDetailsFlyoutCloseButton'
|
||||
| 'startTime'
|
||||
| 'startTime.title'
|
||||
| 'startTime.value'
|
||||
| 'state'
|
||||
| 'state.title'
|
||||
| 'state.value'
|
||||
| 'tab'
|
||||
| 'tableHeaderCell_durationInMillis_3'
|
||||
| 'tableHeaderCell_durationInMillis_3.tableHeaderSortButton'
|
||||
| 'tableHeaderCell_indices_4'
|
||||
| 'tableHeaderCell_indices_4.tableHeaderSortButton'
|
||||
| 'tableHeaderCell_name_0'
|
||||
| 'tableHeaderCell_name_0.tableHeaderSortButton'
|
||||
| 'tableHeaderCell_repository_1'
|
||||
| 'tableHeaderCell_repository_1.tableHeaderSortButton'
|
||||
| 'tableHeaderCell_shards.failed_6'
|
||||
| 'tableHeaderCell_shards.total_5'
|
||||
| 'tableHeaderCell_snapshot_0'
|
||||
| 'tableHeaderCell_snapshot_0.tableHeaderSortButton'
|
||||
| 'tableHeaderCell_startTimeInMillis_2'
|
||||
| 'tableHeaderCell_startTimeInMillis_2.tableHeaderSortButton'
|
||||
| 'tableHeaderCell_type_1'
|
||||
| 'tableHeaderCell_type_1.tableHeaderSortButton'
|
||||
| 'tableHeaderSortButton'
|
||||
| 'title'
|
||||
| 'uuid'
|
||||
| 'uuid.title'
|
||||
| 'uuid.value'
|
||||
| 'value'
|
||||
| 'verifyRepositoryButton'
|
||||
| 'version'
|
||||
| 'version.title'
|
||||
| 'version.value';
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import sinon, { SinonFakeServer } from 'sinon';
|
||||
import { API_BASE_PATH } from '../../../common/constants';
|
||||
|
||||
type HttpResponse = Record<string, any> | any[];
|
||||
|
||||
const mockResponse = (defaultResponse: HttpResponse, response: HttpResponse) => [
|
||||
200,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify({ ...defaultResponse, ...response }),
|
||||
];
|
||||
|
||||
// Register helpers to mock HTTP Requests
|
||||
const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
|
||||
const setLoadRepositoriesResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { repositories: [] };
|
||||
|
||||
server.respondWith(
|
||||
'GET',
|
||||
`${API_BASE_PATH}repositories`,
|
||||
mockResponse(defaultResponse, response)
|
||||
);
|
||||
};
|
||||
|
||||
const setLoadRepositoryTypesResponse = (response: HttpResponse = []) => {
|
||||
server.respondWith('GET', `${API_BASE_PATH}repository_types`, JSON.stringify(response));
|
||||
};
|
||||
|
||||
const setGetRepositoryResponse = (response?: HttpResponse) => {
|
||||
const defaultResponse = {};
|
||||
|
||||
server.respondWith(
|
||||
'GET',
|
||||
/api\/snapshot_restore\/repositories\/.+/,
|
||||
response
|
||||
? mockResponse(defaultResponse, response)
|
||||
: [200, { 'Content-Type': 'application/json' }, '']
|
||||
);
|
||||
};
|
||||
|
||||
const setSaveRepositoryResponse = (response?: HttpResponse, error?: any) => {
|
||||
const status = error ? error.status || 400 : 200;
|
||||
const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
|
||||
|
||||
server.respondWith('PUT', `${API_BASE_PATH}repositories`, [
|
||||
status,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
body,
|
||||
]);
|
||||
};
|
||||
|
||||
const setLoadSnapshotsResponse = (response: HttpResponse = {}) => {
|
||||
const defaultResponse = { errors: {}, snapshots: [], repositories: [] };
|
||||
|
||||
server.respondWith('GET', `${API_BASE_PATH}snapshots`, mockResponse(defaultResponse, response));
|
||||
};
|
||||
|
||||
const setGetSnapshotResponse = (response?: HttpResponse) => {
|
||||
const defaultResponse = {};
|
||||
|
||||
server.respondWith(
|
||||
'GET',
|
||||
/\/api\/snapshot_restore\/snapshots\/.+/,
|
||||
response
|
||||
? mockResponse(defaultResponse, response)
|
||||
: [200, { 'Content-Type': 'application/json' }, '']
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
setLoadRepositoriesResponse,
|
||||
setLoadRepositoryTypesResponse,
|
||||
setGetRepositoryResponse,
|
||||
setSaveRepositoryResponse,
|
||||
setLoadSnapshotsResponse,
|
||||
setGetSnapshotResponse,
|
||||
};
|
||||
};
|
||||
|
||||
export const init = () => {
|
||||
const server = sinon.fakeServer.create();
|
||||
server.respondImmediately = true;
|
||||
|
||||
// Define default response for unhandled requests.
|
||||
// We make requests to APIs which don't impact the component under test, e.g. UI metric telemetry,
|
||||
// and we can mock them all with a 200 instead of mocking each one individually.
|
||||
server.respondWith([200, {}, 'DefaultResponse']);
|
||||
|
||||
const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server);
|
||||
|
||||
return {
|
||||
server,
|
||||
httpRequestsMockHelpers,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { setup as homeSetup } from './home.helpers';
|
||||
import { setup as repositoryAddSetup } from './repository_add.helpers';
|
||||
import { setup as repositoryEditSetup } from './repository_edit.helpers';
|
||||
|
||||
export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../test_utils';
|
||||
|
||||
export { setupEnvironment } from './setup_environment';
|
||||
|
||||
export const pageHelpers = {
|
||||
home: { setup: homeSetup },
|
||||
repositoryAdd: { setup: repositoryAddSetup },
|
||||
repositoryEdit: { setup: repositoryEditSetup },
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { ComponentClass, FunctionComponent } from 'react';
|
||||
import { createShim } from '../../../public/shim';
|
||||
import { setAppDependencies } from '../../../public/app/index';
|
||||
|
||||
const { core, plugins } = createShim();
|
||||
const appDependencies = {
|
||||
core,
|
||||
plugins,
|
||||
};
|
||||
|
||||
type ComponentType = ComponentClass<any> | FunctionComponent<any>;
|
||||
|
||||
export const WithProviders = (Comp: ComponentType) => {
|
||||
const AppDependenciesProvider = setAppDependencies(appDependencies);
|
||||
|
||||
return (props: any) => {
|
||||
return (
|
||||
<AppDependenciesProvider value={appDependencies}>
|
||||
<Comp {...props} />
|
||||
</AppDependenciesProvider>
|
||||
);
|
||||
};
|
||||
};
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { registerTestBed, TestBed } from '../../../../../test_utils';
|
||||
import { RepositoryType } from '../../../common/types';
|
||||
import { RepositoryAdd } from '../../../public/app/sections/repository_add';
|
||||
import { WithProviders } from './providers';
|
||||
|
||||
const initTestBed = registerTestBed<RepositoryAddTestSubjects>(WithProviders(RepositoryAdd), {
|
||||
doMountAsync: true,
|
||||
});
|
||||
|
||||
export interface RepositoryAddTestBed extends TestBed<RepositoryAddTestSubjects> {
|
||||
actions: {
|
||||
clickNextButton: () => void;
|
||||
clickBackButton: () => void;
|
||||
clickSubmitButton: () => void;
|
||||
selectRepositoryType: (type: RepositoryType) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const setup = async (): Promise<RepositoryAddTestBed> => {
|
||||
const testBed = await initTestBed();
|
||||
|
||||
// User actions
|
||||
const clickNextButton = () => {
|
||||
testBed.find('nextButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickBackButton = () => {
|
||||
testBed.find('backButton').simulate('click');
|
||||
};
|
||||
|
||||
const clickSubmitButton = () => {
|
||||
testBed.find('submitButton').simulate('click');
|
||||
};
|
||||
|
||||
const selectRepositoryType = (type: RepositoryType) => {
|
||||
const button = testBed.find(`${type}RepositoryType` as 'fsRepositoryType').find('button');
|
||||
if (!button.length) {
|
||||
throw new Error(`Repository type "${type}" button not found.`);
|
||||
}
|
||||
button.simulate('click');
|
||||
};
|
||||
|
||||
return {
|
||||
...testBed,
|
||||
actions: {
|
||||
clickNextButton,
|
||||
clickBackButton,
|
||||
clickSubmitButton,
|
||||
selectRepositoryType,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export type RepositoryAddTestSubjects = TestSubjects | NonVisibleTestSubjects;
|
||||
|
||||
type NonVisibleTestSubjects =
|
||||
| 'noRepositoryTypesError'
|
||||
| 'sectionLoading'
|
||||
| 'saveRepositoryApiError';
|
||||
|
||||
type TestSubjects =
|
||||
| 'backButton'
|
||||
| 'chunkSizeInput'
|
||||
| 'compressToggle'
|
||||
| 'fsRepositoryType'
|
||||
| 'locationInput'
|
||||
| 'maxRestoreBytesInput'
|
||||
| 'maxSnapshotBytesInput'
|
||||
| 'nameInput'
|
||||
| 'nextButton'
|
||||
| 'pageTitle'
|
||||
| 'readOnlyToggle'
|
||||
| 'repositoryForm'
|
||||
| 'repositoryForm.backButton'
|
||||
| 'repositoryForm.chunkSizeInput'
|
||||
| 'repositoryForm.compressToggle'
|
||||
| 'repositoryForm.fsRepositoryType'
|
||||
| 'repositoryForm.locationInput'
|
||||
| 'repositoryForm.maxRestoreBytesInput'
|
||||
| 'repositoryForm.maxSnapshotBytesInput'
|
||||
| 'repositoryForm.nameInput'
|
||||
| 'repositoryForm.nextButton'
|
||||
| 'repositoryForm.readOnlyToggle'
|
||||
| 'repositoryForm.repositoryFormError'
|
||||
| 'repositoryForm.sourceOnlyToggle'
|
||||
| 'repositoryForm.stepTwo'
|
||||
| 'repositoryForm.submitButton'
|
||||
| 'repositoryForm.title'
|
||||
| 'repositoryForm.urlRepositoryType'
|
||||
| 'repositoryFormError'
|
||||
| 'snapshotRestoreApp'
|
||||
| 'snapshotRestoreApp.backButton'
|
||||
| 'snapshotRestoreApp.chunkSizeInput'
|
||||
| 'snapshotRestoreApp.compressToggle'
|
||||
| 'snapshotRestoreApp.fsRepositoryType'
|
||||
| 'snapshotRestoreApp.locationInput'
|
||||
| 'snapshotRestoreApp.maxRestoreBytesInput'
|
||||
| 'snapshotRestoreApp.maxSnapshotBytesInput'
|
||||
| 'snapshotRestoreApp.nameInput'
|
||||
| 'snapshotRestoreApp.nextButton'
|
||||
| 'snapshotRestoreApp.pageTitle'
|
||||
| 'snapshotRestoreApp.readOnlyToggle'
|
||||
| 'snapshotRestoreApp.repositoryForm'
|
||||
| 'snapshotRestoreApp.repositoryFormError'
|
||||
| 'snapshotRestoreApp.sourceOnlyToggle'
|
||||
| 'snapshotRestoreApp.stepTwo'
|
||||
| 'snapshotRestoreApp.submitButton'
|
||||
| 'snapshotRestoreApp.title'
|
||||
| 'snapshotRestoreApp.urlRepositoryType'
|
||||
| 'sourceOnlyToggle'
|
||||
| 'stepTwo'
|
||||
| 'stepTwo.backButton'
|
||||
| 'stepTwo.chunkSizeInput'
|
||||
| 'stepTwo.compressToggle'
|
||||
| 'stepTwo.locationInput'
|
||||
| 'stepTwo.maxRestoreBytesInput'
|
||||
| 'stepTwo.maxSnapshotBytesInput'
|
||||
| 'stepTwo.readOnlyToggle'
|
||||
| 'stepTwo.submitButton'
|
||||
| 'stepTwo.title'
|
||||
| 'submitButton'
|
||||
| 'title'
|
||||
| 'urlRepositoryType';
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { registerTestBed, TestBedConfig } from '../../../../../test_utils';
|
||||
import { RepositoryEdit } from '../../../public/app/sections/repository_edit';
|
||||
import { WithProviders } from './providers';
|
||||
import { REPOSITORY_NAME } from './constant';
|
||||
|
||||
const testBedConfig: TestBedConfig = {
|
||||
memoryRouter: {
|
||||
initialEntries: [`/${REPOSITORY_NAME}`],
|
||||
componentRoutePath: '/:name',
|
||||
},
|
||||
doMountAsync: true,
|
||||
};
|
||||
|
||||
export const setup = registerTestBed<RepositoryEditTestSubjects>(
|
||||
WithProviders(RepositoryEdit),
|
||||
testBedConfig
|
||||
);
|
||||
|
||||
export type RepositoryEditTestSubjects = TestSubjects | ThreeLevelDepth | NonVisibleTestSubjects;
|
||||
|
||||
type NonVisibleTestSubjects =
|
||||
| 'uriInput'
|
||||
| 'schemeSelect'
|
||||
| 'clientInput'
|
||||
| 'containerInput'
|
||||
| 'basePathInput'
|
||||
| 'maxSnapshotBytesInput'
|
||||
| 'locationModeSelect'
|
||||
| 'bucketInput'
|
||||
| 'urlInput'
|
||||
| 'pathInput'
|
||||
| 'loadDefaultsToggle'
|
||||
| 'securityPrincipalInput'
|
||||
| 'serverSideEncryptionToggle'
|
||||
| 'bufferSizeInput'
|
||||
| 'cannedAclSelect'
|
||||
| 'storageClassSelect';
|
||||
|
||||
type ThreeLevelDepth = 'repositoryForm.stepTwo.title';
|
||||
|
||||
type TestSubjects =
|
||||
| 'chunkSizeInput'
|
||||
| 'compressToggle'
|
||||
| 'locationInput'
|
||||
| 'maxRestoreBytesInput'
|
||||
| 'maxSnapshotBytesInput'
|
||||
| 'readOnlyToggle'
|
||||
| 'repositoryForm'
|
||||
| 'repositoryForm.chunkSizeInput'
|
||||
| 'repositoryForm.compressToggle'
|
||||
| 'repositoryForm.locationInput'
|
||||
| 'repositoryForm.maxRestoreBytesInput'
|
||||
| 'repositoryForm.maxSnapshotBytesInput'
|
||||
| 'repositoryForm.readOnlyToggle'
|
||||
| 'repositoryForm.stepTwo'
|
||||
| 'repositoryForm.submitButton'
|
||||
| 'repositoryForm.title'
|
||||
| 'snapshotRestoreApp'
|
||||
| 'snapshotRestoreApp.chunkSizeInput'
|
||||
| 'snapshotRestoreApp.compressToggle'
|
||||
| 'snapshotRestoreApp.locationInput'
|
||||
| 'snapshotRestoreApp.maxRestoreBytesInput'
|
||||
| 'snapshotRestoreApp.maxSnapshotBytesInput'
|
||||
| 'snapshotRestoreApp.readOnlyToggle'
|
||||
| 'snapshotRestoreApp.repositoryForm'
|
||||
| 'snapshotRestoreApp.stepTwo'
|
||||
| 'snapshotRestoreApp.submitButton'
|
||||
| 'snapshotRestoreApp.title'
|
||||
| 'stepTwo'
|
||||
| 'stepTwo.chunkSizeInput'
|
||||
| 'stepTwo.compressToggle'
|
||||
| 'stepTwo.locationInput'
|
||||
| 'stepTwo.maxRestoreBytesInput'
|
||||
| 'stepTwo.maxSnapshotBytesInput'
|
||||
| 'stepTwo.readOnlyToggle'
|
||||
| 'stepTwo.submitButton'
|
||||
| 'stepTwo.title'
|
||||
| 'submitButton'
|
||||
| 'title';
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import axiosXhrAdapter from 'axios/lib/adapters/xhr';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { httpService } from '../../../public/app/services/http';
|
||||
import { breadcrumbService } from '../../../public/app/services/navigation';
|
||||
import { textService } from '../../../public/app/services/text';
|
||||
import { chrome } from '../../../public/test/mocks';
|
||||
import { init as initHttpRequests } from './http_requests';
|
||||
|
||||
export const setupEnvironment = () => {
|
||||
httpService.init(axios.create({ adapter: axiosXhrAdapter }), {
|
||||
addBasePath: (path: string) => path,
|
||||
});
|
||||
breadcrumbService.init(chrome, {});
|
||||
textService.init(i18n);
|
||||
|
||||
const { server, httpRequestsMockHelpers } = initHttpRequests();
|
||||
|
||||
return {
|
||||
server,
|
||||
httpRequestsMockHelpers,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,736 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import * as fixtures from '../../test/fixtures';
|
||||
import { SNAPSHOT_STATE } from '../../public/app/constants';
|
||||
import { API_BASE_PATH } from '../../common/constants';
|
||||
import { formatDate } from '../../public/app/services/text';
|
||||
import {
|
||||
setupEnvironment,
|
||||
pageHelpers,
|
||||
nextTick,
|
||||
getRandomString,
|
||||
findTestSubject,
|
||||
} from './helpers';
|
||||
import { HomeTestBed } from './helpers/home.helpers';
|
||||
import { REPOSITORY_NAME } from './helpers/constant';
|
||||
|
||||
const { setup } = pageHelpers.home;
|
||||
|
||||
jest.mock('ui/i18n', () => {
|
||||
const I18nContext = ({ children }: any) => children;
|
||||
return { I18nContext };
|
||||
});
|
||||
|
||||
const removeWhiteSpaceOnArrayValues = (array: any[]) =>
|
||||
array.map(value => {
|
||||
if (!value.trim) {
|
||||
return value;
|
||||
}
|
||||
return value.trim();
|
||||
});
|
||||
|
||||
// We need to skip the tests until react 16.9.0 is released
|
||||
// which supports asynchronous code inside act()
|
||||
describe.skip('<SnapshotRestoreHome />', () => {
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: HomeTestBed;
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('on component mount', () => {
|
||||
beforeEach(async () => {
|
||||
testBed = await setup();
|
||||
});
|
||||
|
||||
test('should set the correct app title', () => {
|
||||
const { exists, find } = testBed;
|
||||
expect(exists('appTitle')).toBe(true);
|
||||
expect(find('appTitle').text()).toEqual('Snapshot Repositories');
|
||||
});
|
||||
|
||||
test('should display a loading while fetching the repositories', () => {
|
||||
const { exists, find } = testBed;
|
||||
expect(exists('sectionLoading')).toBe(true);
|
||||
expect(find('sectionLoading').text()).toEqual('Loading repositories…');
|
||||
});
|
||||
|
||||
test('should have a link to the documentation', () => {
|
||||
const { exists, find } = testBed;
|
||||
expect(exists('documentationLink')).toBe(true);
|
||||
expect(find('documentationLink').text()).toBe('Snapshot docs');
|
||||
});
|
||||
|
||||
describe('tabs', () => {
|
||||
beforeEach(async () => {
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
testBed.component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should have 2 tabs', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('tab').length).toBe(2);
|
||||
expect(find('tab').map(t => t.text())).toEqual(['Snapshots', 'Repositories']);
|
||||
});
|
||||
|
||||
test('should navigate to snapshot list tab', () => {
|
||||
const { exists, actions } = testBed;
|
||||
|
||||
expect(exists('repositoryList')).toBe(true);
|
||||
expect(exists('snapshotList')).toBe(false);
|
||||
|
||||
actions.selectTab('snapshots');
|
||||
|
||||
expect(exists('repositoryList')).toBe(false);
|
||||
expect(exists('snapshotList')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('repositories', () => {
|
||||
describe('when there are no repositories', () => {
|
||||
beforeEach(() => {
|
||||
httpRequestsMockHelpers.setLoadRepositoriesResponse({ repositories: [] });
|
||||
});
|
||||
|
||||
test('should display an empty prompt', async () => {
|
||||
const { component, exists } = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(exists('sectionLoading')).toBe(false);
|
||||
expect(exists('emptyPrompt')).toBe(true);
|
||||
expect(exists('emptyPrompt.registerRepositoryButton')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are repositories', () => {
|
||||
const repo1 = fixtures.getRepository({ name: `a${getRandomString()}`, type: 'fs' });
|
||||
const repo2 = fixtures.getRepository({ name: `b${getRandomString()}`, type: 'url' });
|
||||
const repo3 = fixtures.getRepository({ name: `c${getRandomString()}`, type: 's3' });
|
||||
const repo4 = fixtures.getRepository({ name: `d${getRandomString()}`, type: 'hdfs' });
|
||||
const repo5 = fixtures.getRepository({ name: `e${getRandomString()}`, type: 'azure' });
|
||||
const repo6 = fixtures.getRepository({ name: `f${getRandomString()}`, type: 'gcs' });
|
||||
const repo7 = fixtures.getRepository({ name: `g${getRandomString()}`, type: 'source' });
|
||||
const repo8 = fixtures.getRepository({
|
||||
name: `h${getRandomString()}`,
|
||||
type: 'source',
|
||||
settings: { delegateType: 'gcs' },
|
||||
});
|
||||
|
||||
const repositories = [repo1, repo2, repo3, repo4, repo5, repo6, repo7, repo8];
|
||||
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadRepositoriesResponse({ repositories });
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
testBed.component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should list them in the table', async () => {
|
||||
const { table } = testBed;
|
||||
const mapTypeToText: Record<string, string> = {
|
||||
fs: 'Shared file system',
|
||||
url: 'Read-only URL',
|
||||
s3: 'AWS S3',
|
||||
hdfs: 'Hadoop HDFS',
|
||||
azure: 'Azure',
|
||||
gcs: 'Google Cloud Storage',
|
||||
source: 'Source-only',
|
||||
};
|
||||
|
||||
const { tableCellsValues } = table.getMetaData('repositoryTable');
|
||||
tableCellsValues.forEach((row, i) => {
|
||||
const repository = repositories[i];
|
||||
if (repository === repo8) {
|
||||
// The "repo8" is source with a delegate type
|
||||
expect(removeWhiteSpaceOnArrayValues(row)).toEqual([
|
||||
'',
|
||||
repository.name,
|
||||
`${mapTypeToText[repository.settings.delegateType]} (Source-only)`,
|
||||
'',
|
||||
]);
|
||||
} else {
|
||||
expect(removeWhiteSpaceOnArrayValues(row)).toEqual([
|
||||
'',
|
||||
repository.name,
|
||||
mapTypeToText[repository.type],
|
||||
'',
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should have a button to reload the repositories', async () => {
|
||||
const { component, exists, actions } = testBed;
|
||||
const totalRequests = server.requests.length;
|
||||
expect(exists('reloadButton')).toBe(true);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickReloadButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(server.requests.length).toBe(totalRequests + 1);
|
||||
expect(server.requests[server.requests.length - 1].url).toBe(
|
||||
`${API_BASE_PATH}repositories`
|
||||
);
|
||||
});
|
||||
|
||||
test('should have a button to register a new repository', () => {
|
||||
const { exists } = testBed;
|
||||
expect(exists('registerRepositoryButton')).toBe(true);
|
||||
});
|
||||
|
||||
test('should have action buttons on each row to edit and delete a repository', () => {
|
||||
const { table } = testBed;
|
||||
const { rows } = table.getMetaData('repositoryTable');
|
||||
const lastColumn = rows[0].columns[rows[0].columns.length - 1].reactWrapper;
|
||||
|
||||
expect(findTestSubject(lastColumn, 'editRepositoryButton').length).toBe(1);
|
||||
expect(findTestSubject(lastColumn, 'deleteRepositoryButton').length).toBe(1);
|
||||
});
|
||||
|
||||
describe('delete repository', () => {
|
||||
test('should show a confirmation when clicking the delete repository button', async () => {
|
||||
const { actions } = testBed;
|
||||
|
||||
await actions.clickRepositoryActionAt(0, 'delete');
|
||||
|
||||
// We need to read the document "body" as the modal is added there and not inside
|
||||
// the component DOM tree.
|
||||
expect(
|
||||
document.body.querySelector('[data-test-subj="deleteRepositoryConfirmation"]')
|
||||
).not.toBe(null);
|
||||
|
||||
expect(
|
||||
document.body.querySelector('[data-test-subj="deleteRepositoryConfirmation"]')!
|
||||
.textContent
|
||||
).toContain(`Remove repository '${repo1.name}'?`);
|
||||
});
|
||||
|
||||
test('should send the correct HTTP request to delete repository', async () => {
|
||||
const { component, actions } = testBed;
|
||||
|
||||
await actions.clickRepositoryActionAt(0, 'delete');
|
||||
|
||||
const modal = document.body.querySelector(
|
||||
'[data-test-subj="deleteRepositoryConfirmation"]'
|
||||
);
|
||||
const confirmButton: HTMLButtonElement | null = modal!.querySelector(
|
||||
'[data-test-subj="confirmModalConfirmButton"]'
|
||||
);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
confirmButton!.click();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.method).toBe('DELETE');
|
||||
expect(latestRequest.url).toBe(`${API_BASE_PATH}repositories/${repo1.name}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detail panel', () => {
|
||||
test('should show the detail when clicking on a repository', async () => {
|
||||
const { exists, actions } = testBed;
|
||||
|
||||
expect(exists('repositoryDetail')).toBe(false);
|
||||
|
||||
await actions.clickRepositoryAt(0);
|
||||
|
||||
expect(exists('repositoryDetail')).toBe(true);
|
||||
});
|
||||
|
||||
test('should set the correct title', async () => {
|
||||
const { find, actions } = testBed;
|
||||
|
||||
await actions.clickRepositoryAt(0);
|
||||
|
||||
expect(find('repositoryDetail.title').text()).toEqual(repo1.name);
|
||||
});
|
||||
|
||||
test('should show a loading state while fetching the repository', async () => {
|
||||
const { find, exists, actions } = testBed;
|
||||
|
||||
// By providing undefined, the "loading section" will be displayed
|
||||
httpRequestsMockHelpers.setGetRepositoryResponse(undefined);
|
||||
|
||||
await actions.clickRepositoryAt(0);
|
||||
|
||||
expect(exists('repositoryDetail.sectionLoading')).toBe(true);
|
||||
expect(find('repositoryDetail.sectionLoading').text()).toEqual('Loading repository…');
|
||||
});
|
||||
|
||||
describe('when the repository has been fetched', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setGetRepositoryResponse({
|
||||
repository: {
|
||||
name: 'my-repo',
|
||||
type: 'fs',
|
||||
settings: { location: '/tmp/es-backups' },
|
||||
},
|
||||
snapshots: { count: 0 },
|
||||
});
|
||||
|
||||
await testBed.actions.clickRepositoryAt(0);
|
||||
});
|
||||
|
||||
test('should have a link to the documentation', async () => {
|
||||
const { exists } = testBed;
|
||||
|
||||
expect(exists('repositoryDetail.documentationLink')).toBe(true);
|
||||
});
|
||||
|
||||
test('should set the correct repository settings', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('repositoryDetail.repositoryType').text()).toEqual('Shared file system');
|
||||
expect(find('repositoryDetail.snapshotCount').text()).toEqual(
|
||||
'Repository has no snapshots'
|
||||
);
|
||||
});
|
||||
|
||||
test('should have a button to verify the status of the repository', async () => {
|
||||
const { exists, find, component } = testBed;
|
||||
expect(exists('repositoryDetail.verifyRepositoryButton')).toBe(true);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
find('repositoryDetail.verifyRepositoryButton').simulate('click');
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.method).toBe('GET');
|
||||
expect(latestRequest.url).toBe(`${API_BASE_PATH}repositories/${repo1.name}/verify`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the repository has been fetched (and has snapshots)', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setGetRepositoryResponse({
|
||||
repository: {
|
||||
name: 'my-repo',
|
||||
type: 'fs',
|
||||
settings: { location: '/tmp/es-backups' },
|
||||
},
|
||||
snapshots: { count: 2 },
|
||||
});
|
||||
|
||||
await testBed.actions.clickRepositoryAt(0);
|
||||
});
|
||||
|
||||
test('should indicate the number of snapshots found', () => {
|
||||
const { find } = testBed;
|
||||
expect(find('repositoryDetail.snapshotCount').text()).toEqual('2 snapshots found');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('snapshots', () => {
|
||||
describe('when there are no snapshots nor repositories', () => {
|
||||
beforeAll(() => {
|
||||
httpRequestsMockHelpers.setLoadSnapshotsResponse({ snapshots: [], repositories: [] });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
testBed.actions.selectTab('snapshots');
|
||||
await nextTick(100);
|
||||
testBed.component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should display an empty prompt', () => {
|
||||
const { exists } = testBed;
|
||||
|
||||
expect(exists('emptyPrompt')).toBe(true);
|
||||
});
|
||||
|
||||
test('should invite the user to first register a repository', () => {
|
||||
const { find, exists } = testBed;
|
||||
expect(find('emptyPrompt.title').text()).toBe(
|
||||
`You don't have any snapshots or repositories yet`
|
||||
);
|
||||
expect(exists('emptyPrompt.registerRepositoryButton')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no snapshots but has some repository', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadSnapshotsResponse({
|
||||
snapshots: [],
|
||||
repositories: ['my-repo'],
|
||||
});
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
testBed.actions.selectTab('snapshots');
|
||||
await nextTick(2000);
|
||||
testBed.component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should display an empty prompt', () => {
|
||||
const { find, exists } = testBed;
|
||||
|
||||
expect(exists('emptyPrompt')).toBe(true);
|
||||
expect(find('emptyPrompt.title').text()).toBe(`You don't have any snapshots yet`);
|
||||
});
|
||||
|
||||
test('should have a link to the snapshot documentation', () => {
|
||||
const { exists } = testBed;
|
||||
expect(exists('emptyPrompt.documentationLink')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are snapshots and repositories', () => {
|
||||
const snapshot1 = fixtures.getSnapshot({
|
||||
repository: REPOSITORY_NAME,
|
||||
snapshot: `a${getRandomString()}`,
|
||||
});
|
||||
const snapshot2 = fixtures.getSnapshot({
|
||||
repository: REPOSITORY_NAME,
|
||||
snapshot: `b${getRandomString()}`,
|
||||
});
|
||||
const snapshots = [snapshot1, snapshot2];
|
||||
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadSnapshotsResponse({
|
||||
snapshots,
|
||||
repositories: [REPOSITORY_NAME],
|
||||
errors: {},
|
||||
});
|
||||
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
testBed.actions.selectTab('snapshots');
|
||||
await nextTick(2000);
|
||||
testBed.component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should list them in the table', async () => {
|
||||
const { table } = testBed;
|
||||
|
||||
const { tableCellsValues } = table.getMetaData('snapshotTable');
|
||||
tableCellsValues.forEach((row, i) => {
|
||||
const snapshot = snapshots[i];
|
||||
expect(row).toEqual([
|
||||
snapshot.snapshot, // Snapshot
|
||||
REPOSITORY_NAME, // Repository
|
||||
formatDate(snapshot.startTimeInMillis), // Date created
|
||||
`${Math.ceil(snapshot.durationInMillis / 1000).toString()}s`, // Duration
|
||||
snapshot.indices.length.toString(), // Indices
|
||||
snapshot.shards.total.toString(), // Shards
|
||||
snapshot.shards.failed.toString(), // Failed shards
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('each row should have a link to the repository', async () => {
|
||||
const { component, find, exists, table, router } = testBed;
|
||||
|
||||
const { rows } = table.getMetaData('snapshotTable');
|
||||
const repositoryLink = findTestSubject(rows[0].reactWrapper, 'repositoryLink');
|
||||
const { href } = repositoryLink.props();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
router.navigateTo(href!);
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
// Make sure that we navigated to the repository list
|
||||
// and opened the detail panel for the repository
|
||||
expect(exists('snapshotList')).toBe(false);
|
||||
expect(exists('repositoryList')).toBe(true);
|
||||
expect(exists('repositoryDetail')).toBe(true);
|
||||
expect(find('repositoryDetail.title').text()).toBe(REPOSITORY_NAME);
|
||||
});
|
||||
|
||||
test('should have a button to reload the snapshots', async () => {
|
||||
const { component, exists, actions } = testBed;
|
||||
const totalRequests = server.requests.length;
|
||||
expect(exists('reloadButton')).toBe(true);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickReloadButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(server.requests.length).toBe(totalRequests + 1);
|
||||
expect(server.requests[server.requests.length - 1].url).toBe(`${API_BASE_PATH}snapshots`);
|
||||
});
|
||||
|
||||
describe('detail panel', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setGetSnapshotResponse(snapshot1);
|
||||
});
|
||||
|
||||
test('should show the detail when clicking on a snapshot', async () => {
|
||||
const { exists, actions } = testBed;
|
||||
expect(exists('snapshotDetail')).toBe(false);
|
||||
|
||||
await actions.clickSnapshotAt(0);
|
||||
|
||||
expect(exists('snapshotDetail')).toBe(true);
|
||||
});
|
||||
|
||||
test('should show a loading while fetching the snapshot', async () => {
|
||||
const { find, exists, actions } = testBed;
|
||||
// By providing undefined, the "loading section" will be displayed
|
||||
httpRequestsMockHelpers.setGetSnapshotResponse(undefined);
|
||||
|
||||
await actions.clickSnapshotAt(0);
|
||||
|
||||
expect(exists('snapshotDetail.sectionLoading')).toBe(true);
|
||||
expect(find('snapshotDetail.sectionLoading').text()).toEqual('Loading snapshot…');
|
||||
});
|
||||
|
||||
describe('on mount', () => {
|
||||
beforeEach(async () => {
|
||||
await testBed.actions.clickSnapshotAt(0);
|
||||
});
|
||||
|
||||
test('should set the correct title', async () => {
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('snapshotDetail.detailTitle').text()).toEqual(snapshot1.snapshot);
|
||||
});
|
||||
|
||||
test('should have a link to show the repository detail', async () => {
|
||||
const { component, exists, find, router } = testBed;
|
||||
expect(exists('snapshotDetail.repositoryLink')).toBe(true);
|
||||
|
||||
const { href } = find('snapshotDetail.repositoryLink').props();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
router.navigateTo(href);
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
// Make sure that we navigated to the repository list
|
||||
// and opened the detail panel for the repository
|
||||
expect(exists('snapshotList')).toBe(false);
|
||||
expect(exists('repositoryList')).toBe(true);
|
||||
expect(exists('repositoryDetail')).toBe(true);
|
||||
expect(find('repositoryDetail.title').text()).toBe(REPOSITORY_NAME);
|
||||
});
|
||||
|
||||
test('should have a button to close the detail panel', () => {
|
||||
const { find, exists } = testBed;
|
||||
expect(exists('snapshotDetail.closeButton')).toBe(true);
|
||||
|
||||
find('snapshotDetail.closeButton').simulate('click');
|
||||
|
||||
expect(exists('snapshotDetail')).toBe(false);
|
||||
});
|
||||
|
||||
describe('tabs', () => {
|
||||
test('should have 2 tabs', () => {
|
||||
const { find } = testBed;
|
||||
const tabs = find('snapshotDetail.tab');
|
||||
|
||||
expect(tabs.length).toBe(2);
|
||||
expect(tabs.map(t => t.text())).toEqual(['Summary', 'Failed indices (0)']);
|
||||
});
|
||||
|
||||
test('should have the default tab set on "Summary"', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
const tabs = find('snapshotDetail.tab');
|
||||
const selectedTab = find('snapshotDetail').find('.euiTab-isSelected');
|
||||
|
||||
expect(selectedTab.instance()).toBe(tabs.at(0).instance());
|
||||
});
|
||||
|
||||
describe('summary tab', () => {
|
||||
test('should set the correct summary values', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('snapshotDetail.version.value').text()).toBe(
|
||||
`${snapshot1.version} / ${snapshot1.versionId}`
|
||||
);
|
||||
expect(find('snapshotDetail.uuid.value').text()).toBe(snapshot1.uuid);
|
||||
expect(find('snapshotDetail.state.value').text()).toBe('Snapshot complete');
|
||||
expect(find('snapshotDetail.includeGlobalState.value').text()).toBe('Yes');
|
||||
expect(find('snapshotDetail.indices.title').text()).toBe(
|
||||
`Indices (${snapshot1.indices.length})`
|
||||
);
|
||||
expect(find('snapshotDetail.indices.value').text()).toBe(
|
||||
snapshot1.indices.join('')
|
||||
);
|
||||
expect(find('snapshotDetail.startTime.value').text()).toBe(
|
||||
formatDate(snapshot1.startTimeInMillis)
|
||||
);
|
||||
expect(find('snapshotDetail.endTime.value').text()).toBe(
|
||||
formatDate(snapshot1.endTimeInMillis)
|
||||
);
|
||||
});
|
||||
|
||||
test('should indicate the different snapshot states', async () => {
|
||||
const { find, actions } = testBed;
|
||||
|
||||
// We need to click back and forth between the first table row (0) and the second row (1)
|
||||
// in order to trigger the HTTP request that loads the snapshot with the new state.
|
||||
// This varible keeps track of it.
|
||||
let itemIndexToClickOn = 1;
|
||||
|
||||
const setSnapshotStateAndUpdateDetail = async (state: string) => {
|
||||
const updatedSnapshot = { ...snapshot1, state };
|
||||
httpRequestsMockHelpers.setGetSnapshotResponse(updatedSnapshot);
|
||||
await actions.clickSnapshotAt(itemIndexToClickOn); // click another snapshot to trigger the HTTP call
|
||||
};
|
||||
|
||||
const expectMessageForSnapshotState = async (
|
||||
state: string,
|
||||
expectedMessage: string
|
||||
) => {
|
||||
await setSnapshotStateAndUpdateDetail(state);
|
||||
|
||||
const stateMessage = find('snapshotDetail.state.value').text();
|
||||
try {
|
||||
expect(stateMessage).toBe(expectedMessage);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Expected snapshot state message "${expectedMessage}" for state "${state}, but got "${stateMessage}".`
|
||||
);
|
||||
}
|
||||
|
||||
itemIndexToClickOn = itemIndexToClickOn ? 0 : 1;
|
||||
};
|
||||
|
||||
const mapStateToMessage = {
|
||||
[SNAPSHOT_STATE.IN_PROGRESS]: 'Taking snapshot…',
|
||||
[SNAPSHOT_STATE.FAILED]: 'Snapshot failed',
|
||||
[SNAPSHOT_STATE.PARTIAL]: 'Partial failure ',
|
||||
[SNAPSHOT_STATE.INCOMPATIBLE]: 'Incompatible version ',
|
||||
};
|
||||
|
||||
// Call sequencially each state and verify that the message is ok
|
||||
return Object.entries(mapStateToMessage).reduce((promise, [state, message]) => {
|
||||
return promise.then(async () => expectMessageForSnapshotState(state, message));
|
||||
}, Promise.resolve());
|
||||
});
|
||||
});
|
||||
|
||||
describe('failed indices tab', () => {
|
||||
test('should display a message when snapshot created successfully', () => {
|
||||
const { find, actions } = testBed;
|
||||
actions.selectSnapshotDetailTab('failedIndices');
|
||||
|
||||
expect(find('snapshotDetail.content').text()).toBe(
|
||||
'All indices were stored successfully.'
|
||||
);
|
||||
});
|
||||
|
||||
test('should display a message when snapshot in progress ', async () => {
|
||||
const { find, actions } = testBed;
|
||||
const updatedSnapshot = { ...snapshot1, state: 'IN_PROGRESS' };
|
||||
httpRequestsMockHelpers.setGetSnapshotResponse(updatedSnapshot);
|
||||
|
||||
await actions.clickSnapshotAt(1); // click another snapshot to trigger the HTTP call
|
||||
actions.selectSnapshotDetailTab('failedIndices');
|
||||
|
||||
expect(find('snapshotDetail.content').text()).toBe('Snapshot is being created.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are failed indices', () => {
|
||||
const failure1 = fixtures.getIndexFailure();
|
||||
const failure2 = fixtures.getIndexFailure();
|
||||
const indexFailures = [failure1, failure2];
|
||||
|
||||
beforeEach(async () => {
|
||||
const updatedSnapshot = { ...snapshot1, indexFailures };
|
||||
httpRequestsMockHelpers.setGetSnapshotResponse(updatedSnapshot);
|
||||
await testBed.actions.clickSnapshotAt(0);
|
||||
testBed.actions.selectSnapshotDetailTab('failedIndices');
|
||||
});
|
||||
|
||||
test('should update the tab label', () => {
|
||||
const { find } = testBed;
|
||||
expect(
|
||||
find('snapshotDetail.tab')
|
||||
.at(1)
|
||||
.text()
|
||||
).toBe(`Failed indices (${indexFailures.length})`);
|
||||
});
|
||||
|
||||
test('should display the failed indices', () => {
|
||||
const { find } = testBed;
|
||||
|
||||
const expected = indexFailures.map(failure => failure.index);
|
||||
const found = find('snapshotDetail.indexFailure.index').map(wrapper => wrapper.text());
|
||||
|
||||
expect(find('snapshotDetail.indexFailure').length).toBe(2);
|
||||
expect(found).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should detail the failure for each index', () => {
|
||||
const { find } = testBed;
|
||||
const index0Failure = find('snapshotDetail.indexFailure').at(0);
|
||||
const failuresFound = findTestSubject(index0Failure, 'failure');
|
||||
|
||||
expect(failuresFound.length).toBe(failure1.failures.length);
|
||||
|
||||
const failure0 = failuresFound.at(0);
|
||||
const shardText = findTestSubject(failure0, 'shard').text();
|
||||
const reasonText = findTestSubject(failure0, 'reason').text();
|
||||
const [mockedFailure] = failure1.failures;
|
||||
|
||||
expect(shardText).toBe(`Shard ${mockedFailure.shard_id}`);
|
||||
expect(reasonText).toBe(`${mockedFailure.status}: ${mockedFailure.reason}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,302 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { INVALID_NAME_CHARS } from '../../public/app/services/validation/validate_repository';
|
||||
import { getRepository } from '../../test/fixtures';
|
||||
import { RepositoryType } from '../../common/types';
|
||||
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
|
||||
import { RepositoryAddTestBed } from './helpers/repository_add.helpers';
|
||||
|
||||
const { setup } = pageHelpers.repositoryAdd;
|
||||
const repositoryTypes = ['fs', 'url', 'source', 'azure', 'gcs', 's3', 'hdfs'];
|
||||
|
||||
jest.mock('ui/i18n', () => {
|
||||
const I18nContext = ({ children }: any) => children;
|
||||
return { I18nContext };
|
||||
});
|
||||
|
||||
// We need to skip the tests until react 16.9.0 is released
|
||||
// which supports asynchronous code inside act()
|
||||
describe.skip('<RepositoryAdd />', () => {
|
||||
let testBed: RepositoryAddTestBed;
|
||||
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('on component mount', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadRepositoryTypesResponse(repositoryTypes);
|
||||
|
||||
testBed = await setup();
|
||||
});
|
||||
|
||||
test('should set the correct page title', () => {
|
||||
const { exists, find } = testBed;
|
||||
expect(exists('pageTitle')).toBe(true);
|
||||
expect(find('pageTitle').text()).toEqual('Register repository');
|
||||
});
|
||||
|
||||
test('should indicate that the repository types are loading', () => {
|
||||
const { exists, find } = testBed;
|
||||
expect(exists('sectionLoading')).toBe(true);
|
||||
expect(find('sectionLoading').text()).toBe('Loading repository types…');
|
||||
});
|
||||
|
||||
test('should not let the user go to the next step if some fields are missing', () => {
|
||||
const { form, actions } = testBed;
|
||||
|
||||
actions.clickNextButton();
|
||||
|
||||
expect(form.getErrorsMessages()).toEqual([
|
||||
'Repository name is required.',
|
||||
'Type is required.',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no repository types are not found', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadRepositoryTypesResponse([]);
|
||||
testBed = await setup();
|
||||
await nextTick();
|
||||
testBed.component.update();
|
||||
});
|
||||
|
||||
test('should show an error callout ', async () => {
|
||||
const { find, exists } = testBed;
|
||||
|
||||
expect(exists('noRepositoryTypesError')).toBe(true);
|
||||
expect(find('noRepositoryTypesError').text()).toContain('No repository types available');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when repository types are found', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadRepositoryTypesResponse(repositoryTypes);
|
||||
testBed = await setup();
|
||||
await nextTick();
|
||||
testBed.component.update();
|
||||
});
|
||||
|
||||
test('should have 1 card for each repository type', () => {
|
||||
const { exists } = testBed;
|
||||
|
||||
repositoryTypes.forEach(type => {
|
||||
const testSubject: any = `${type}RepositoryType`;
|
||||
try {
|
||||
expect(exists(testSubject)).toBe(true);
|
||||
} catch {
|
||||
throw new Error(`Repository type "${type}" was not found.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('form validations', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadRepositoryTypesResponse(repositoryTypes);
|
||||
|
||||
testBed = await setup();
|
||||
await nextTick();
|
||||
testBed.component.update();
|
||||
});
|
||||
|
||||
describe('name (step 1)', () => {
|
||||
it('should not allow spaces in the name', () => {
|
||||
const { form, actions } = testBed;
|
||||
form.setInputValue('nameInput', 'with space');
|
||||
|
||||
actions.clickNextButton();
|
||||
|
||||
expect(form.getErrorsMessages()).toContain('Spaces are not allowed in the name.');
|
||||
});
|
||||
|
||||
it('should not allow invalid characters', () => {
|
||||
const { form, actions } = testBed;
|
||||
|
||||
const expectErrorForChar = (char: string) => {
|
||||
form.setInputValue('nameInput', `with${char}`);
|
||||
actions.clickNextButton();
|
||||
|
||||
try {
|
||||
expect(form.getErrorsMessages()).toContain(
|
||||
`Character "${char}" is not allowed in the name.`
|
||||
);
|
||||
} catch {
|
||||
throw new Error(`Invalid character ${char} did not display an error.`);
|
||||
}
|
||||
};
|
||||
|
||||
INVALID_NAME_CHARS.forEach(expectErrorForChar);
|
||||
});
|
||||
});
|
||||
|
||||
describe('settings (step 2)', () => {
|
||||
const typeToErrorMessagesMap: Record<string, string[]> = {
|
||||
fs: ['Location is required.'],
|
||||
url: ['URL is required.'],
|
||||
s3: ['Bucket is required.'],
|
||||
gcs: ['Bucket is required.'],
|
||||
hdfs: ['URI is required.'],
|
||||
};
|
||||
|
||||
test('should validate required repository settings', async () => {
|
||||
const { component, actions, form } = testBed;
|
||||
|
||||
form.setInputValue('nameInput', 'my-repo');
|
||||
|
||||
const selectRepoTypeAndExpectErrors = async (type: RepositoryType) => {
|
||||
actions.selectRepositoryType(type);
|
||||
actions.clickNextButton();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSubmitButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
const expectedErrors = typeToErrorMessagesMap[type];
|
||||
const errorsFound = form.getErrorsMessages();
|
||||
|
||||
expectedErrors.forEach(error => {
|
||||
try {
|
||||
expect(errorsFound).toContain(error);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Expected "${error}" not found in form. Got "${JSON.stringify(errorsFound)}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickBackButton();
|
||||
await nextTick(100);
|
||||
component.update();
|
||||
});
|
||||
};
|
||||
|
||||
await selectRepoTypeAndExpectErrors('fs');
|
||||
await selectRepoTypeAndExpectErrors('url');
|
||||
await selectRepoTypeAndExpectErrors('s3');
|
||||
await selectRepoTypeAndExpectErrors('gcs');
|
||||
await selectRepoTypeAndExpectErrors('hdfs');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('form payload & api errors', () => {
|
||||
const repository = getRepository();
|
||||
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setLoadRepositoryTypesResponse(repositoryTypes);
|
||||
|
||||
testBed = await setup();
|
||||
});
|
||||
|
||||
describe('not source only', () => {
|
||||
beforeEach(() => {
|
||||
// Fill step 1 required fields and go to step 2
|
||||
testBed.form.setInputValue('nameInput', repository.name);
|
||||
testBed.actions.selectRepositoryType(repository.type);
|
||||
testBed.actions.clickNextButton();
|
||||
});
|
||||
|
||||
test('should send the correct payload', async () => {
|
||||
const { form, actions } = testBed;
|
||||
|
||||
// Fill step 2
|
||||
form.setInputValue('locationInput', repository.settings.location);
|
||||
form.selectCheckBox('compressToggle');
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSubmitButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
name: repository.name,
|
||||
type: repository.type,
|
||||
settings: {
|
||||
location: repository.settings.location,
|
||||
compress: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should surface the API errors from the "save" HTTP request', async () => {
|
||||
const { component, form, actions, find, exists } = testBed;
|
||||
|
||||
form.setInputValue('locationInput', repository.settings.location);
|
||||
form.selectCheckBox('compressToggle');
|
||||
|
||||
const error = {
|
||||
status: 400,
|
||||
error: 'Bad request',
|
||||
message: 'Repository payload is invalid',
|
||||
};
|
||||
|
||||
httpRequestsMockHelpers.setSaveRepositoryResponse(undefined, { body: error });
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSubmitButton();
|
||||
await nextTick();
|
||||
component.update();
|
||||
});
|
||||
|
||||
expect(exists('saveRepositoryApiError')).toBe(true);
|
||||
expect(find('saveRepositoryApiError').text()).toContain(error.message);
|
||||
});
|
||||
});
|
||||
|
||||
describe('source only', () => {
|
||||
beforeEach(() => {
|
||||
// Fill step 1 required fields and go to step 2
|
||||
testBed.form.setInputValue('nameInput', repository.name);
|
||||
testBed.actions.selectRepositoryType(repository.type);
|
||||
testBed.form.selectCheckBox('sourceOnlyToggle'); // toggle source
|
||||
testBed.actions.clickNextButton();
|
||||
});
|
||||
|
||||
test('should send the correct payload', async () => {
|
||||
const { form, actions } = testBed;
|
||||
|
||||
// Fill step 2
|
||||
form.setInputValue('locationInput', repository.settings.location);
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
actions.clickSubmitButton();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
const latestRequest = server.requests[server.requests.length - 1];
|
||||
|
||||
expect(latestRequest.requestBody).toEqual(
|
||||
JSON.stringify({
|
||||
name: repository.name,
|
||||
type: 'source',
|
||||
settings: {
|
||||
delegateType: repository.type,
|
||||
location: repository.settings.location,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { setupEnvironment, pageHelpers, nextTick, TestBed, getRandomString } from './helpers';
|
||||
import { RepositoryForm } from '../../public/app/components/repository_form';
|
||||
import { RepositoryEditTestSubjects } from './helpers/repository_edit.helpers';
|
||||
import { RepositoryAddTestSubjects } from './helpers/repository_add.helpers';
|
||||
import { REPOSITORY_EDIT } from './helpers/constant';
|
||||
|
||||
const { setup } = pageHelpers.repositoryEdit;
|
||||
const { setup: setupRepositoryAdd } = pageHelpers.repositoryAdd;
|
||||
|
||||
jest.mock('ui/i18n', () => {
|
||||
const I18nContext = ({ children }: any) => children;
|
||||
return { I18nContext };
|
||||
});
|
||||
|
||||
// We need to skip the tests until react 16.9.0 is released
|
||||
// which supports asynchronous code inside act()
|
||||
describe.skip('<RepositoryEdit />', () => {
|
||||
let testBed: TestBed<RepositoryEditTestSubjects>;
|
||||
let testBedRepositoryAdd: TestBed<RepositoryAddTestSubjects>;
|
||||
const { server, httpRequestsMockHelpers } = setupEnvironment();
|
||||
|
||||
afterAll(() => {
|
||||
server.restore();
|
||||
});
|
||||
|
||||
describe('on component mount', () => {
|
||||
beforeEach(async () => {
|
||||
httpRequestsMockHelpers.setGetRepositoryResponse({
|
||||
repository: REPOSITORY_EDIT,
|
||||
snapshots: { count: 0 },
|
||||
});
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
testBed.component.update();
|
||||
});
|
||||
});
|
||||
|
||||
test('should set the correct page title', () => {
|
||||
const { find } = testBed;
|
||||
expect(find('repositoryForm.stepTwo.title').text()).toBe(
|
||||
`'${REPOSITORY_EDIT.name}' settings`
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* As the "edit" repository component uses the same form underneath that
|
||||
* the "create" repository, we won't test it again but simply make sure that
|
||||
* the same form component is indeed shared between the 2 app sections.
|
||||
*/
|
||||
test('should use the same Form component as the "<RepositoryAdd />" section', async () => {
|
||||
httpRequestsMockHelpers.setLoadRepositoryTypesResponse(['fs', 'url']);
|
||||
|
||||
testBedRepositoryAdd = await setupRepositoryAdd();
|
||||
|
||||
const formEdit = testBed.component.find(RepositoryForm);
|
||||
const formAdd = testBedRepositoryAdd.component.find(RepositoryForm);
|
||||
|
||||
expect(formEdit.length).toBe(1);
|
||||
expect(formAdd.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should populate the correct values', () => {
|
||||
const mountComponentWithMock = async (repository: any) => {
|
||||
httpRequestsMockHelpers.setGetRepositoryResponse({
|
||||
repository: { name: getRandomString(), ...repository },
|
||||
snapshots: { count: 0 },
|
||||
});
|
||||
testBed = await setup();
|
||||
|
||||
// @ts-ignore (remove when react 16.9.0 is released)
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
testBed.component.update();
|
||||
});
|
||||
};
|
||||
|
||||
it('fs repository', async () => {
|
||||
const settings = {
|
||||
location: getRandomString(),
|
||||
compress: true,
|
||||
chunkSize: getRandomString(),
|
||||
maxSnapshotBytesPerSec: getRandomString(),
|
||||
maxRestoreBytesPerSec: getRandomString(),
|
||||
readonly: true,
|
||||
};
|
||||
|
||||
await mountComponentWithMock({ type: 'fs', settings });
|
||||
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('locationInput').props().defaultValue).toBe(settings.location);
|
||||
expect(find('compressToggle').props().checked).toBe(settings.compress);
|
||||
expect(find('chunkSizeInput').props().defaultValue).toBe(settings.chunkSize);
|
||||
expect(find('maxSnapshotBytesInput').props().defaultValue).toBe(
|
||||
settings.maxSnapshotBytesPerSec
|
||||
);
|
||||
expect(find('maxRestoreBytesInput').props().defaultValue).toBe(
|
||||
settings.maxRestoreBytesPerSec
|
||||
);
|
||||
expect(find('readOnlyToggle').props().checked).toBe(settings.readonly);
|
||||
});
|
||||
|
||||
it('readonly repository', async () => {
|
||||
const settings = {
|
||||
url: 'https://elastic.co',
|
||||
};
|
||||
|
||||
await mountComponentWithMock({ type: 'url', settings });
|
||||
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('schemeSelect').props().value).toBe('https');
|
||||
expect(find('urlInput').props().defaultValue).toBe('elastic.co');
|
||||
});
|
||||
|
||||
it('azure repository', async () => {
|
||||
const settings = {
|
||||
client: getRandomString(),
|
||||
container: getRandomString(),
|
||||
basePath: getRandomString(),
|
||||
compress: true,
|
||||
chunkSize: getRandomString(),
|
||||
readonly: true,
|
||||
locationMode: getRandomString(),
|
||||
maxRestoreBytesPerSec: getRandomString(),
|
||||
maxSnapshotBytesPerSec: getRandomString(),
|
||||
};
|
||||
|
||||
await mountComponentWithMock({ type: 'azure', settings });
|
||||
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('clientInput').props().defaultValue).toBe(settings.client);
|
||||
expect(find('containerInput').props().defaultValue).toBe(settings.container);
|
||||
expect(find('basePathInput').props().defaultValue).toBe(settings.basePath);
|
||||
expect(find('compressToggle').props().checked).toBe(settings.compress);
|
||||
expect(find('chunkSizeInput').props().defaultValue).toBe(settings.chunkSize);
|
||||
expect(find('maxSnapshotBytesInput').props().defaultValue).toBe(
|
||||
settings.maxSnapshotBytesPerSec
|
||||
);
|
||||
expect(find('maxRestoreBytesInput').props().defaultValue).toBe(
|
||||
settings.maxRestoreBytesPerSec
|
||||
);
|
||||
expect(find('locationModeSelect').props().value).toBe(settings.locationMode);
|
||||
expect(find('readOnlyToggle').props().checked).toBe(settings.readonly);
|
||||
});
|
||||
|
||||
it('gcs repository', async () => {
|
||||
const settings = {
|
||||
bucket: getRandomString(),
|
||||
client: getRandomString(),
|
||||
basePath: getRandomString(),
|
||||
compress: true,
|
||||
chunkSize: getRandomString(),
|
||||
readonly: true,
|
||||
maxRestoreBytesPerSec: getRandomString(),
|
||||
maxSnapshotBytesPerSec: getRandomString(),
|
||||
};
|
||||
|
||||
await mountComponentWithMock({ type: 'gcs', settings });
|
||||
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('clientInput').props().defaultValue).toBe(settings.client);
|
||||
expect(find('bucketInput').props().defaultValue).toBe(settings.bucket);
|
||||
expect(find('basePathInput').props().defaultValue).toBe(settings.basePath);
|
||||
expect(find('compressToggle').props().checked).toBe(settings.compress);
|
||||
expect(find('chunkSizeInput').props().defaultValue).toBe(settings.chunkSize);
|
||||
expect(find('maxSnapshotBytesInput').props().defaultValue).toBe(
|
||||
settings.maxSnapshotBytesPerSec
|
||||
);
|
||||
expect(find('maxRestoreBytesInput').props().defaultValue).toBe(
|
||||
settings.maxRestoreBytesPerSec
|
||||
);
|
||||
expect(find('readOnlyToggle').props().checked).toBe(settings.readonly);
|
||||
});
|
||||
|
||||
it('hdfs repository', async () => {
|
||||
const settings = {
|
||||
delegateType: 'gcs',
|
||||
uri: 'hdfs://elastic.co',
|
||||
path: getRandomString(),
|
||||
loadDefault: true,
|
||||
compress: true,
|
||||
chunkSize: getRandomString(),
|
||||
readonly: true,
|
||||
'security.principal': getRandomString(),
|
||||
maxRestoreBytesPerSec: getRandomString(),
|
||||
maxSnapshotBytesPerSec: getRandomString(),
|
||||
conf1: 'foo',
|
||||
conf2: 'bar',
|
||||
};
|
||||
|
||||
await mountComponentWithMock({ type: 'hdfs', settings });
|
||||
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('uriInput').props().defaultValue).toBe('elastic.co');
|
||||
expect(find('pathInput').props().defaultValue).toBe(settings.path);
|
||||
expect(find('loadDefaultsToggle').props().checked).toBe(settings.loadDefault);
|
||||
expect(find('compressToggle').props().checked).toBe(settings.compress);
|
||||
expect(find('chunkSizeInput').props().defaultValue).toBe(settings.chunkSize);
|
||||
expect(find('securityPrincipalInput').props().defaultValue).toBe(
|
||||
settings['security.principal']
|
||||
);
|
||||
expect(find('maxSnapshotBytesInput').props().defaultValue).toBe(
|
||||
settings.maxSnapshotBytesPerSec
|
||||
);
|
||||
expect(find('maxRestoreBytesInput').props().defaultValue).toBe(
|
||||
settings.maxRestoreBytesPerSec
|
||||
);
|
||||
expect(find('readOnlyToggle').props().checked).toBe(settings.readonly);
|
||||
|
||||
const codeEditor = testBed.component.find('EuiCodeEditor');
|
||||
expect(JSON.parse(codeEditor.props().value as string)).toEqual({
|
||||
loadDefault: true,
|
||||
conf1: 'foo',
|
||||
conf2: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
it('s3 repository', async () => {
|
||||
const settings = {
|
||||
bucket: getRandomString(),
|
||||
client: getRandomString(),
|
||||
basePath: getRandomString(),
|
||||
compress: true,
|
||||
chunkSize: getRandomString(),
|
||||
serverSideEncryption: true,
|
||||
bufferSize: getRandomString(),
|
||||
cannedAcl: getRandomString(),
|
||||
storageClass: getRandomString(),
|
||||
readonly: true,
|
||||
maxRestoreBytesPerSec: getRandomString(),
|
||||
maxSnapshotBytesPerSec: getRandomString(),
|
||||
};
|
||||
|
||||
await mountComponentWithMock({ type: 's3', settings });
|
||||
|
||||
const { find } = testBed;
|
||||
|
||||
expect(find('clientInput').props().defaultValue).toBe(settings.client);
|
||||
expect(find('bucketInput').props().defaultValue).toBe(settings.bucket);
|
||||
expect(find('basePathInput').props().defaultValue).toBe(settings.basePath);
|
||||
expect(find('compressToggle').props().checked).toBe(settings.compress);
|
||||
expect(find('chunkSizeInput').props().defaultValue).toBe(settings.chunkSize);
|
||||
expect(find('serverSideEncryptionToggle').props().checked).toBe(
|
||||
settings.serverSideEncryption
|
||||
);
|
||||
expect(find('bufferSizeInput').props().defaultValue).toBe(settings.bufferSize);
|
||||
expect(find('cannedAclSelect').props().value).toBe(settings.cannedAcl);
|
||||
expect(find('storageClassSelect').props().value).toBe(settings.storageClass);
|
||||
expect(find('maxSnapshotBytesInput').props().defaultValue).toBe(
|
||||
settings.maxSnapshotBytesPerSec
|
||||
);
|
||||
expect(find('maxRestoreBytesInput').props().defaultValue).toBe(
|
||||
settings.maxRestoreBytesPerSec
|
||||
);
|
||||
expect(find('readOnlyToggle').props().checked).toBe(settings.readonly);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -91,7 +91,7 @@ export const App: React.FunctionComponent = () => {
|
|||
const sectionsRegex = sections.join('|');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-test-subj="snapshotRestoreApp">
|
||||
<Switch>
|
||||
<Route exact path={`${BASE_PATH}/add_repository`} component={RepositoryAdd} />
|
||||
<Route exact path={`${BASE_PATH}/edit_repository/:name*`} component={RepositoryEdit} />
|
||||
|
|
|
@ -147,7 +147,7 @@ export const RepositoryDeleteProvider: React.FunctionComponent<Props> = ({ child
|
|||
)
|
||||
}
|
||||
buttonColor="danger"
|
||||
data-test-subj="srDeleteRepositoryConfirmationModal"
|
||||
data-test-subj="deleteRepositoryConfirmation"
|
||||
>
|
||||
{isSingle ? (
|
||||
<p>
|
||||
|
|
|
@ -115,7 +115,11 @@ export const RepositoryForm: React.FunctionComponent<Props> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiForm isInvalid={hasValidationErrors} error={validationErrors}>
|
||||
<EuiForm
|
||||
isInvalid={hasValidationErrors}
|
||||
error={validationErrors}
|
||||
data-test-subj="repositoryForm"
|
||||
>
|
||||
{currentStep === 1 && !isEditing ? renderStepOne() : renderStepTwo()}
|
||||
</EuiForm>
|
||||
);
|
||||
|
|
|
@ -125,6 +125,7 @@ export const RepositoryFormStepOne: React.FunctionComponent<Props> = ({
|
|||
name: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="nameInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -159,6 +160,7 @@ export const RepositoryFormStepOne: React.FunctionComponent<Props> = ({
|
|||
onClick: () => onTypeChange(type),
|
||||
isSelected: isSelectedType,
|
||||
}}
|
||||
data-test-subj={`${type}RepositoryType`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
@ -331,6 +333,7 @@ export const RepositoryFormStepOne: React.FunctionComponent<Props> = ({
|
|||
});
|
||||
}
|
||||
}}
|
||||
data-test-subj="sourceOnlyToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -343,7 +346,7 @@ export const RepositoryFormStepOne: React.FunctionComponent<Props> = ({
|
|||
fill
|
||||
iconType="arrowRight"
|
||||
iconSide="right"
|
||||
data-test-subj="srRepositoryFormNextButton"
|
||||
data-test-subj="nextButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.nextButtonLabel"
|
||||
|
|
|
@ -66,7 +66,7 @@ export const RepositoryFormStepTwo: React.FunctionComponent<Props> = ({
|
|||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<h2 data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.settingsTitle"
|
||||
defaultMessage="{repositoryName} settings"
|
||||
|
@ -135,7 +135,7 @@ export const RepositoryFormStepTwo: React.FunctionComponent<Props> = ({
|
|||
color="primary"
|
||||
iconType="arrowLeft"
|
||||
onClick={onBack}
|
||||
data-test-subj="srRepositoryFormSubmitButton"
|
||||
data-test-subj="backButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.backButtonLabel"
|
||||
|
@ -150,7 +150,7 @@ export const RepositoryFormStepTwo: React.FunctionComponent<Props> = ({
|
|||
iconType="check"
|
||||
onClick={onSave}
|
||||
fill={isManagedRepository ? false : true}
|
||||
data-test-subj="srRepositoryFormSubmitButton"
|
||||
data-test-subj="submitButton"
|
||||
isLoading={isSaving}
|
||||
>
|
||||
{isSaving ? savingLabel : saveLabel}
|
||||
|
@ -195,11 +195,11 @@ export const RepositoryFormStepTwo: React.FunctionComponent<Props> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div data-test-subj="stepTwo">
|
||||
{renderSettings()}
|
||||
{renderFormValidationError()}
|
||||
{renderSaveError()}
|
||||
{renderActions()}
|
||||
</Fragment>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -100,6 +100,7 @@ export const AzureSettings: React.FunctionComponent<Props> = ({
|
|||
client: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="clientInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -145,6 +146,7 @@ export const AzureSettings: React.FunctionComponent<Props> = ({
|
|||
container: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="containerInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -190,6 +192,7 @@ export const AzureSettings: React.FunctionComponent<Props> = ({
|
|||
basePath: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="basePathInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -235,6 +238,7 @@ export const AzureSettings: React.FunctionComponent<Props> = ({
|
|||
compress: e.target.checked,
|
||||
});
|
||||
}}
|
||||
data-test-subj="compressToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -281,6 +285,7 @@ export const AzureSettings: React.FunctionComponent<Props> = ({
|
|||
chunkSize: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="chunkSizeInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -327,6 +332,7 @@ export const AzureSettings: React.FunctionComponent<Props> = ({
|
|||
maxSnapshotBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="maxSnapshotBytesInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -373,6 +379,7 @@ export const AzureSettings: React.FunctionComponent<Props> = ({
|
|||
maxRestoreBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="maxRestoreBytesInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -420,6 +427,7 @@ export const AzureSettings: React.FunctionComponent<Props> = ({
|
|||
});
|
||||
}}
|
||||
fullWidth
|
||||
data-test-subj="locationModeSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -466,6 +474,7 @@ export const AzureSettings: React.FunctionComponent<Props> = ({
|
|||
readonly: locationMode === locationModeOptions[1].value ? true : e.target.checked,
|
||||
});
|
||||
}}
|
||||
data-test-subj="readOnlyToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
|
|
@ -96,6 +96,7 @@ export const FSSettings: React.FunctionComponent<Props> = ({
|
|||
location: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="locationInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -141,6 +142,7 @@ export const FSSettings: React.FunctionComponent<Props> = ({
|
|||
compress: e.target.checked,
|
||||
});
|
||||
}}
|
||||
data-test-subj="compressToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -187,6 +189,7 @@ export const FSSettings: React.FunctionComponent<Props> = ({
|
|||
chunkSize: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="chunkSizeInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -233,6 +236,7 @@ export const FSSettings: React.FunctionComponent<Props> = ({
|
|||
maxSnapshotBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="maxSnapshotBytesInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -279,6 +283,7 @@ export const FSSettings: React.FunctionComponent<Props> = ({
|
|||
maxRestoreBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="maxRestoreBytesInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -324,6 +329,7 @@ export const FSSettings: React.FunctionComponent<Props> = ({
|
|||
readonly: e.target.checked,
|
||||
});
|
||||
}}
|
||||
data-test-subj="readOnlyToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
|
|
@ -87,6 +87,7 @@ export const GCSSettings: React.FunctionComponent<Props> = ({
|
|||
client: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="clientInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -132,6 +133,7 @@ export const GCSSettings: React.FunctionComponent<Props> = ({
|
|||
bucket: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="bucketInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -177,6 +179,7 @@ export const GCSSettings: React.FunctionComponent<Props> = ({
|
|||
basePath: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="basePathInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -222,6 +225,7 @@ export const GCSSettings: React.FunctionComponent<Props> = ({
|
|||
compress: e.target.checked,
|
||||
});
|
||||
}}
|
||||
data-test-subj="compressToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -268,6 +272,7 @@ export const GCSSettings: React.FunctionComponent<Props> = ({
|
|||
chunkSize: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="chunkSizeInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -314,6 +319,7 @@ export const GCSSettings: React.FunctionComponent<Props> = ({
|
|||
maxSnapshotBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="maxSnapshotBytesInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -360,6 +366,7 @@ export const GCSSettings: React.FunctionComponent<Props> = ({
|
|||
maxRestoreBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="maxRestoreBytesInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -405,6 +412,7 @@ export const GCSSettings: React.FunctionComponent<Props> = ({
|
|||
readonly: e.target.checked,
|
||||
});
|
||||
}}
|
||||
data-test-subj="readOnlyToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
|
|
@ -109,6 +109,7 @@ export const HDFSSettings: React.FunctionComponent<Props> = ({
|
|||
});
|
||||
}}
|
||||
aria-describedby="hdfsRepositoryUriDescription hdfsRepositoryUriProtocolDescription"
|
||||
data-test-subj="uriInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -154,6 +155,7 @@ export const HDFSSettings: React.FunctionComponent<Props> = ({
|
|||
path: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="pathInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -199,6 +201,7 @@ export const HDFSSettings: React.FunctionComponent<Props> = ({
|
|||
loadDefaults: e.target.checked,
|
||||
});
|
||||
}}
|
||||
data-test-subj="loadDefaultsToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -244,6 +247,7 @@ export const HDFSSettings: React.FunctionComponent<Props> = ({
|
|||
compress: e.target.checked,
|
||||
});
|
||||
}}
|
||||
data-test-subj="compressToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -290,6 +294,7 @@ export const HDFSSettings: React.FunctionComponent<Props> = ({
|
|||
chunkSize: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="chunkSizeInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -335,6 +340,7 @@ export const HDFSSettings: React.FunctionComponent<Props> = ({
|
|||
'security.principal': e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="securityPrincipalInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -434,6 +440,7 @@ export const HDFSSettings: React.FunctionComponent<Props> = ({
|
|||
setIsConfInvalid(true);
|
||||
}
|
||||
}}
|
||||
data-test-subj="codeEditor"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -480,6 +487,7 @@ export const HDFSSettings: React.FunctionComponent<Props> = ({
|
|||
maxSnapshotBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="maxSnapshotBytesInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -526,6 +534,7 @@ export const HDFSSettings: React.FunctionComponent<Props> = ({
|
|||
maxRestoreBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="maxRestoreBytesInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -571,6 +580,7 @@ export const HDFSSettings: React.FunctionComponent<Props> = ({
|
|||
readonly: e.target.checked,
|
||||
});
|
||||
}}
|
||||
data-test-subj="readOnlyToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
|
|
@ -137,6 +137,7 @@ export const ReadonlySettings: React.FunctionComponent<Props> = ({
|
|||
value={selectedScheme}
|
||||
onChange={e => selectScheme(e.target.value)}
|
||||
aria-controls="readonlyRepositoryUrlHelp"
|
||||
data-test-subj="schemeSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
@ -162,6 +163,7 @@ export const ReadonlySettings: React.FunctionComponent<Props> = ({
|
|||
url: `${selectedScheme}://${e.target.value}`,
|
||||
});
|
||||
}}
|
||||
data-test-subj="urlInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -116,6 +116,7 @@ export const S3Settings: React.FunctionComponent<Props> = ({
|
|||
client: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="clientInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -161,6 +162,7 @@ export const S3Settings: React.FunctionComponent<Props> = ({
|
|||
bucket: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="bucketInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -206,6 +208,7 @@ export const S3Settings: React.FunctionComponent<Props> = ({
|
|||
basePath: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="basePathInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -251,6 +254,7 @@ export const S3Settings: React.FunctionComponent<Props> = ({
|
|||
compress: e.target.checked,
|
||||
});
|
||||
}}
|
||||
data-test-subj="compressToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -297,6 +301,7 @@ export const S3Settings: React.FunctionComponent<Props> = ({
|
|||
chunkSize: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="chunkSizeInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -342,6 +347,7 @@ export const S3Settings: React.FunctionComponent<Props> = ({
|
|||
serverSideEncryption: e.target.checked,
|
||||
});
|
||||
}}
|
||||
data-test-subj="serverSideEncryptionToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -389,6 +395,7 @@ export const S3Settings: React.FunctionComponent<Props> = ({
|
|||
bufferSize: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="bufferSizeInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -435,6 +442,7 @@ export const S3Settings: React.FunctionComponent<Props> = ({
|
|||
});
|
||||
}}
|
||||
fullWidth
|
||||
data-test-subj="cannedAclSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -481,6 +489,7 @@ export const S3Settings: React.FunctionComponent<Props> = ({
|
|||
});
|
||||
}}
|
||||
fullWidth
|
||||
data-test-subj="storageClassSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -527,6 +536,7 @@ export const S3Settings: React.FunctionComponent<Props> = ({
|
|||
maxSnapshotBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="maxSnapshotBytesInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -573,6 +583,7 @@ export const S3Settings: React.FunctionComponent<Props> = ({
|
|||
maxRestoreBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
data-test-subj="maxRestoreBytesInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
@ -618,6 +629,7 @@ export const S3Settings: React.FunctionComponent<Props> = ({
|
|||
readonly: e.target.checked,
|
||||
});
|
||||
}}
|
||||
data-test-subj="readOnlyToggle"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
|
|
@ -18,7 +18,7 @@ interface Props {
|
|||
};
|
||||
}
|
||||
|
||||
export const SectionError: React.FunctionComponent<Props> = ({ title, error }) => {
|
||||
export const SectionError: React.FunctionComponent<Props> = ({ title, error, ...rest }) => {
|
||||
const {
|
||||
error: errorString,
|
||||
cause, // wrapEsError() on the server adds a "cause" array
|
||||
|
@ -26,7 +26,7 @@ export const SectionError: React.FunctionComponent<Props> = ({ title, error }) =
|
|||
} = error.data;
|
||||
|
||||
return (
|
||||
<EuiCallOut title={title} color="danger" iconType="alert">
|
||||
<EuiCallOut title={title} color="danger" iconType="alert" {...rest}>
|
||||
<div>{message || errorString}</div>
|
||||
{cause && (
|
||||
<Fragment>
|
||||
|
|
|
@ -17,6 +17,7 @@ export const SectionLoading: React.FunctionComponent<Props> = ({ children }) =>
|
|||
<EuiEmptyPrompt
|
||||
title={<EuiLoadingSpinner size="xl" />}
|
||||
body={<EuiText color="subdued">{children}</EuiText>}
|
||||
data-test-subj="sectionLoading"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, useReducer } from 'react';
|
||||
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
|
@ -19,37 +19,45 @@ export { BASE_PATH as CLIENT_BASE_PATH } from './constants';
|
|||
*/
|
||||
let DependenciesContext: React.Context<AppDependencies>;
|
||||
|
||||
export const useAppDependencies = () => useContext<AppDependencies>(DependenciesContext);
|
||||
export const setAppDependencies = (deps: AppDependencies) => {
|
||||
DependenciesContext = createContext<AppDependencies>(deps);
|
||||
return DependenciesContext.Provider;
|
||||
};
|
||||
|
||||
const ReactApp: React.FunctionComponent<AppDependencies> = ({ core, plugins }) => {
|
||||
export const useAppDependencies = () => {
|
||||
if (!DependenciesContext) {
|
||||
throw new Error(`The app dependencies Context hasn't been set.
|
||||
Use the "setAppDependencies()" method when bootstrapping the app.`);
|
||||
}
|
||||
return useContext<AppDependencies>(DependenciesContext);
|
||||
};
|
||||
|
||||
const getAppProviders = (deps: AppDependencies) => {
|
||||
const {
|
||||
i18n: { Context: I18nContext },
|
||||
} = core;
|
||||
} = deps.core;
|
||||
|
||||
const appDependencies: AppDependencies = {
|
||||
core,
|
||||
plugins,
|
||||
};
|
||||
// Create App dependencies context and get its provider
|
||||
const AppDependenciesProvider = setAppDependencies(deps);
|
||||
|
||||
DependenciesContext = createContext<AppDependencies>(appDependencies);
|
||||
|
||||
return (
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<I18nContext>
|
||||
<HashRouter>
|
||||
<DependenciesContext.Provider value={appDependencies}>
|
||||
<AppStateProvider value={useReducer(reducer, initialState)}>
|
||||
<App />
|
||||
</AppStateProvider>
|
||||
</DependenciesContext.Provider>
|
||||
<AppDependenciesProvider value={deps}>
|
||||
<AppStateProvider value={useReducer(reducer, initialState)}>{children}</AppStateProvider>
|
||||
</AppDependenciesProvider>
|
||||
</HashRouter>
|
||||
</I18nContext>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderReact = async (
|
||||
elem: Element,
|
||||
core: AppCore,
|
||||
plugins: AppPlugins
|
||||
): Promise<void> => {
|
||||
render(<ReactApp core={core} plugins={plugins} />, elem);
|
||||
export const renderReact = async (elem: Element, core: AppCore, plugins: AppPlugins) => {
|
||||
const Providers = getAppProviders({ core, plugins });
|
||||
|
||||
render(
|
||||
<Providers>
|
||||
<App />
|
||||
</Providers>,
|
||||
elem
|
||||
);
|
||||
};
|
||||
|
|
|
@ -53,7 +53,6 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
defaultMessage="Snapshots"
|
||||
/>
|
||||
),
|
||||
testSubj: 'srSnapshotsTab',
|
||||
},
|
||||
{
|
||||
id: 'repositories' as Section,
|
||||
|
@ -63,7 +62,6 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
defaultMessage="Repositories"
|
||||
/>
|
||||
),
|
||||
testSubj: 'srRepositoriesTab',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -82,7 +80,7 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
<EuiTitle size="l">
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={true}>
|
||||
<h1>
|
||||
<h1 data-test-subj="appTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.snapshotRestoreTitle"
|
||||
defaultMessage="Snapshot Repositories"
|
||||
|
@ -94,6 +92,7 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
href={documentationLinksService.getRepositoryTypeDocUrl()}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
data-test-subj="documentationLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.snapshotRestoreDocsLinkText"
|
||||
|
@ -121,7 +120,7 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
onClick={() => onSectionChange(tab.id)}
|
||||
isSelected={tab.id === section}
|
||||
key={tab.id}
|
||||
data-test-subject={tab.testSubj}
|
||||
data-test-subj="tab"
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
|
|
|
@ -200,9 +200,11 @@ const RepositoryDetailsUi: React.FunctionComponent<Props> = ({
|
|||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
{type === REPOSITORY_TYPES.source
|
||||
? textService.getRepositoryTypeName(type, repository.settings.delegateType)
|
||||
: textService.getRepositoryTypeName(type)}
|
||||
<span data-test-subj="repositoryType">
|
||||
{type === REPOSITORY_TYPES.source
|
||||
? textService.getRepositoryTypeName(type, repository.settings.delegateType)
|
||||
: textService.getRepositoryTypeName(type)}
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
|
@ -211,6 +213,7 @@ const RepositoryDetailsUi: React.FunctionComponent<Props> = ({
|
|||
href={documentationLinksService.getRepositoryTypeDocUrl(type)}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
data-test-subj="documentationLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.repositoryTypeDocLink"
|
||||
|
@ -229,7 +232,7 @@ const RepositoryDetailsUi: React.FunctionComponent<Props> = ({
|
|||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
{renderSnapshotCount()}
|
||||
<span data-test-subj="snapshotCount">{renderSnapshotCount()}</span>
|
||||
<EuiSpacer size="l" />
|
||||
<TypeDetails repository={repository} />
|
||||
<EuiHorizontalRule />
|
||||
|
@ -305,7 +308,12 @@ const RepositoryDetailsUi: React.FunctionComponent<Props> = ({
|
|||
) : (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiButton onClick={verifyRepository} color="primary" isLoading={isLoadingVerification}>
|
||||
<EuiButton
|
||||
onClick={verifyRepository}
|
||||
color="primary"
|
||||
isLoading={isLoadingVerification}
|
||||
data-test-subj="verifyRepositoryButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.verifyButtonLabel"
|
||||
defaultMessage="Verify repository"
|
||||
|
@ -392,20 +400,20 @@ const RepositoryDetailsUi: React.FunctionComponent<Props> = ({
|
|||
return (
|
||||
<EuiFlyout
|
||||
onClose={onClose}
|
||||
data-test-subj="srRepositoryDetailsFlyout"
|
||||
data-test-subj="repositoryDetail"
|
||||
aria-labelledby="srRepositoryDetailsFlyoutTitle"
|
||||
size="m"
|
||||
maxWidth={400}
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2 id="srRepositoryDetailsFlyoutTitle" data-test-subj="srRepositoryDetailsFlyoutTitle">
|
||||
<h2 id="srRepositoryDetailsFlyoutTitle" data-test-subj="title">
|
||||
{repositoryName}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody data-test-subj="srRepositoryDetailsContent">{renderBody()}</EuiFlyoutBody>
|
||||
<EuiFlyoutBody data-test-subj="content">{renderBody()}</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>{renderFooter()}</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
|
|
|
@ -121,7 +121,7 @@ export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
})}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="srRepositoriesEmptyPromptAddButton"
|
||||
data-test-subj="registerRepositoryButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.addRepositoryButtonLabel"
|
||||
|
@ -129,6 +129,7 @@ export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
data-test-subj="emptyPrompt"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
@ -144,7 +145,7 @@ export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<section data-test-subj="repositoryList">
|
||||
{repositoryName ? (
|
||||
<RepositoryDetails
|
||||
repositoryName={repositoryName}
|
||||
|
@ -153,6 +154,6 @@ export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
/>
|
||||
) : null}
|
||||
{content}
|
||||
</Fragment>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -63,6 +63,7 @@ const RepositoryTableUi: React.FunctionComponent<Props> = ({
|
|||
<EuiLink
|
||||
onClick={() => trackUiMetric(UIM_REPOSITORY_SHOW_DETAILS_CLICK)}
|
||||
href={openRepositoryDetailsUrl(name)}
|
||||
data-test-subj="repositoryLink"
|
||||
>
|
||||
{name}
|
||||
</EuiLink>
|
||||
|
@ -118,6 +119,7 @@ const RepositoryTableUi: React.FunctionComponent<Props> = ({
|
|||
iconType="pencil"
|
||||
color="primary"
|
||||
href={`#${BASE_PATH}/edit_repository/${name}`}
|
||||
data-test-subj="editRepositoryButton"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
@ -152,7 +154,7 @@ const RepositoryTableUi: React.FunctionComponent<Props> = ({
|
|||
)}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj="srRepositoryListDeleteActionButton"
|
||||
data-test-subj="deleteRepositoryButton"
|
||||
onClick={() => deleteRepositoryPrompt([name], onRepositoryDeleted)}
|
||||
isDisabled={Boolean(name === managedRepository)}
|
||||
/>
|
||||
|
@ -236,7 +238,12 @@ const RepositoryTableUi: React.FunctionComponent<Props> = ({
|
|||
toolsRight: (
|
||||
<EuiFlexGroup gutterSize="m" justifyContent="spaceAround">
|
||||
<EuiFlexItem>
|
||||
<EuiButton color="secondary" iconType="refresh" onClick={reload}>
|
||||
<EuiButton
|
||||
color="secondary"
|
||||
iconType="refresh"
|
||||
onClick={reload}
|
||||
data-test-subj="reloadButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryList.table.reloadRepositoriesButton"
|
||||
defaultMessage="Reload"
|
||||
|
@ -250,7 +257,7 @@ const RepositoryTableUi: React.FunctionComponent<Props> = ({
|
|||
})}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="srRepositoriesAddButton"
|
||||
data-test-subj="registerRepositoryButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryList.addRepositoryButtonLabel"
|
||||
|
@ -296,11 +303,12 @@ const RepositoryTableUi: React.FunctionComponent<Props> = ({
|
|||
pagination={pagination}
|
||||
isSelectable={true}
|
||||
rowProps={() => ({
|
||||
'data-test-subj': 'srRepositoryListTableRow',
|
||||
'data-test-subj': 'row',
|
||||
})}
|
||||
cellProps={(item: any, column: any) => ({
|
||||
'data-test-subj': `srRepositoryListTableCell-${column.field}`,
|
||||
'data-test-subj': `cell`,
|
||||
})}
|
||||
data-test-subj="repositoryTable"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -84,7 +84,6 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
|
|||
defaultMessage="Summary"
|
||||
/>
|
||||
),
|
||||
testSubj: 'srSnapshotDetailsSummaryTab',
|
||||
},
|
||||
{
|
||||
id: TAB_FAILURES,
|
||||
|
@ -95,7 +94,6 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
|
|||
values={{ failuresCount: indexFailures.length }}
|
||||
/>
|
||||
),
|
||||
testSubj: 'srSnapshotDetailsFailuresTab',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -111,7 +109,7 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
|
|||
}}
|
||||
isSelected={tab.id === activeTab}
|
||||
key={tab.id}
|
||||
data-test-subject={tab.testSubj}
|
||||
data-test-subj="tab"
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
|
@ -172,7 +170,7 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
|
|||
iconType="cross"
|
||||
flush="left"
|
||||
onClick={onClose}
|
||||
data-test-subj="srSnapshotDetailsFlyoutCloseButton"
|
||||
data-test-subj="closeButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.closeButtonLabel"
|
||||
|
@ -187,7 +185,7 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
|
|||
return (
|
||||
<EuiFlyout
|
||||
onClose={onClose}
|
||||
data-test-subj="srSnapshotDetailsFlyout"
|
||||
data-test-subj="snapshotDetail"
|
||||
aria-labelledby="srSnapshotDetailsFlyoutTitle"
|
||||
size="m"
|
||||
maxWidth={400}
|
||||
|
@ -196,7 +194,7 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
|
|||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="m">
|
||||
<h2 id="srSnapshotDetailsFlyoutTitle" data-test-subj="srSnapshotDetailsFlyoutTitle">
|
||||
<h2 id="srSnapshotDetailsFlyoutTitle" data-test-subj="detailTitle">
|
||||
{snapshotId}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
@ -205,7 +203,7 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
|
|||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<EuiLink href={linkToRepository(repositoryName)}>
|
||||
<EuiLink href={linkToRepository(repositoryName)} data-test-subj="repositoryLink">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.repositoryTitle"
|
||||
defaultMessage="'{repositoryName}' repository"
|
||||
|
@ -220,7 +218,7 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
|
|||
{tabs}
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody data-test-subj="srSnapshotDetailsContent">{content}</EuiFlyoutBody>
|
||||
<EuiFlyoutBody data-test-subj="content">{content}</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>{renderFooter()}</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
|
|
|
@ -46,8 +46,8 @@ export const TabFailures: React.SFC<Props> = ({ indexFailures, snapshotState })
|
|||
const { index, failures } = indexObject;
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
<EuiTitle size="xs">
|
||||
<div key={index} data-test-subj="indexFailure">
|
||||
<EuiTitle size="xs" data-test-subj="index">
|
||||
<h3>{index}</h3>
|
||||
</EuiTitle>
|
||||
|
||||
|
@ -57,8 +57,8 @@ export const TabFailures: React.SFC<Props> = ({ indexFailures, snapshotState })
|
|||
const { status, reason, shard_id: shardId } = failure;
|
||||
|
||||
return (
|
||||
<div key={`${shardId}${reason}`}>
|
||||
<EuiText size="xs">
|
||||
<div key={`${shardId}${reason}`} data-test-subj="failure">
|
||||
<EuiText size="xs" data-test-subj="shard">
|
||||
<p>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
|
@ -70,7 +70,7 @@ export const TabFailures: React.SFC<Props> = ({ indexFailures, snapshotState })
|
|||
</p>
|
||||
</EuiText>
|
||||
|
||||
<EuiCodeBlock paddingSize="s">
|
||||
<EuiCodeBlock paddingSize="s" data-test-subj="reason">
|
||||
{status}: {reason}
|
||||
</EuiCodeBlock>
|
||||
|
||||
|
|
|
@ -76,7 +76,6 @@ export const TabSummary: React.SFC<Props> = ({ snapshotDetails }) => {
|
|||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemIndicesNoneLabel"
|
||||
data-test-subj="srSnapshotDetailsIndicesNoneTitle"
|
||||
defaultMessage="-"
|
||||
/>
|
||||
);
|
||||
|
@ -84,130 +83,102 @@ export const TabSummary: React.SFC<Props> = ({ snapshotDetails }) => {
|
|||
return (
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsVersionItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiFlexItem data-test-subj="version">
|
||||
<EuiDescriptionListTitle data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemVersionLabel"
|
||||
data-test-subj="srSnapshotDetailsVersionTitle"
|
||||
defaultMessage="Version / Version ID"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailsVersionDescription"
|
||||
>
|
||||
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
|
||||
{version} / {versionId}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsIncludeGlobalUuidItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiFlexItem data-test-subj="uuid">
|
||||
<EuiDescriptionListTitle data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemUuidLabel"
|
||||
data-test-subj="srSnapshotDetailsUuidTitle"
|
||||
defaultMessage="UUID"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailUuidDescription"
|
||||
>
|
||||
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
|
||||
{uuid}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsStateItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiFlexItem data-test-subj="state">
|
||||
<EuiDescriptionListTitle data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemStateLabel"
|
||||
data-test-subj="srSnapshotDetailsStateTitle"
|
||||
defaultMessage="State"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailStateDescription"
|
||||
>
|
||||
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
|
||||
<SnapshotState state={state} />
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsIncludeGlobalStateItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiFlexItem data-test-subj="includeGlobalState">
|
||||
<EuiDescriptionListTitle data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemIncludeGlobalStateLabel"
|
||||
data-test-subj="srSnapshotDetailsIncludeGlobalStateTitle"
|
||||
defaultMessage="Includes global state"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailIncludeGlobalStateDescription"
|
||||
>
|
||||
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
|
||||
{includeGlobalStateToHumanizedMap[includeGlobalState]}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsIndicesItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiFlexItem data-test-subj="indices">
|
||||
<EuiDescriptionListTitle data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemIndicesLabel"
|
||||
data-test-subj="srSnapshotDetailsIndicesTitle"
|
||||
defaultMessage="Indices ({indicesCount})"
|
||||
values={{ indicesCount: indices.length }}
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailIndicesDescription"
|
||||
>
|
||||
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
|
||||
<EuiText>{indicesList}</EuiText>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsStartTimeItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiFlexItem data-test-subj="startTime">
|
||||
<EuiDescriptionListTitle data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemStartTimeLabel"
|
||||
data-test-subj="srSnapshotDetailsStartTimeTitle"
|
||||
defaultMessage="Start time"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailStartTimeDescription"
|
||||
>
|
||||
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
|
||||
<DataPlaceholder data={startTimeInMillis}>
|
||||
{formatDate(startTimeInMillis)}
|
||||
</DataPlaceholder>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsEndTimeItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiFlexItem data-test-subj="endTime">
|
||||
<EuiDescriptionListTitle data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemEndTimeLabel"
|
||||
data-test-subj="srSnapshotDetailsEndTimeTitle"
|
||||
defaultMessage="End time"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailEndTimeDescription"
|
||||
>
|
||||
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
|
||||
{state === SNAPSHOT_STATE.IN_PROGRESS ? (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
) : (
|
||||
|
@ -220,19 +191,15 @@ export const TabSummary: React.SFC<Props> = ({ snapshotDetails }) => {
|
|||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsDurationItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<EuiFlexItem data-test-subj="duration">
|
||||
<EuiDescriptionListTitle data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemDurationLabel"
|
||||
data-test-subj="srSnapshotDetailsDurationTitle"
|
||||
defaultMessage="Duration"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailDurationDescription"
|
||||
>
|
||||
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
|
||||
{state === SNAPSHOT_STATE.IN_PROGRESS ? (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
) : (
|
||||
|
|
|
@ -107,7 +107,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
<h1 data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.errorRepositoriesTitle"
|
||||
defaultMessage="Some repositories contain errors"
|
||||
|
@ -154,7 +154,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
<h1 data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noRepositoriesTitle"
|
||||
defaultMessage="You don't have any snapshots or repositories yet"
|
||||
|
@ -176,7 +176,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
})}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="srSnapshotsEmptyPromptAddRepositoryButton"
|
||||
data-test-subj="registerRepositoryButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noRepositoriesAddButtonLabel"
|
||||
|
@ -186,6 +186,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
data-test-subj="emptyPrompt"
|
||||
/>
|
||||
);
|
||||
} else if (snapshots.length === 0) {
|
||||
|
@ -193,7 +194,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
<h1 data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noSnapshotsTitle"
|
||||
defaultMessage="You don't have any snapshots yet"
|
||||
|
@ -212,7 +213,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
<EuiLink
|
||||
href={documentationLinksService.getSnapshotDocUrl()}
|
||||
target="_blank"
|
||||
data-test-subj="srSnapshotsEmptyPromptDocLink"
|
||||
data-test-subj="documentationLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.emptyPrompt.noSnapshotsDocLinkText"
|
||||
|
@ -223,6 +224,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
data-test-subj="emptyPrompt"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
@ -272,7 +274,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<section data-test-subj="snapshotList">
|
||||
{repositoryName && snapshotId ? (
|
||||
<SnapshotDetails
|
||||
repositoryName={repositoryName}
|
||||
|
@ -281,6 +283,6 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
/>
|
||||
) : null}
|
||||
{content}
|
||||
</Fragment>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -48,6 +48,7 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
|
|||
<EuiLink
|
||||
onClick={() => trackUiMetric(UIM_SNAPSHOT_SHOW_DETAILS_CLICK)}
|
||||
href={openSnapshotDetailsUrl(snapshot.repository, snapshotId)}
|
||||
data-test-subj="snapshotLink"
|
||||
>
|
||||
{snapshotId}
|
||||
</EuiLink>
|
||||
|
@ -61,7 +62,9 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
|
|||
truncateText: true,
|
||||
sortable: true,
|
||||
render: (repositoryName: string) => (
|
||||
<EuiLink href={linkToRepository(repositoryName)}>{repositoryName}</EuiLink>
|
||||
<EuiLink href={linkToRepository(repositoryName)} data-test-subj="repositoryLink">
|
||||
{repositoryName}
|
||||
</EuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
@ -153,7 +156,12 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
|
|||
|
||||
const search = {
|
||||
toolsRight: (
|
||||
<EuiButton color="secondary" iconType="refresh" onClick={reload}>
|
||||
<EuiButton
|
||||
color="secondary"
|
||||
iconType="refresh"
|
||||
onClick={reload}
|
||||
data-test-subj="reloadButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.table.reloadSnapshotsButton"
|
||||
defaultMessage="Reload"
|
||||
|
@ -195,11 +203,12 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
|
|||
sorting={sorting}
|
||||
pagination={pagination}
|
||||
rowProps={() => ({
|
||||
'data-test-subj': 'srSnapshotListTableRow',
|
||||
'data-test-subj': 'row',
|
||||
})}
|
||||
cellProps={(item: any, column: any) => ({
|
||||
'data-test-subj': `srSnapshotListTableCell-${column.field}`,
|
||||
'data-test-subj': 'cell',
|
||||
})}
|
||||
data-test-subj="snapshotTable"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -59,6 +59,7 @@ export const RepositoryAdd: React.FunctionComponent<RouteComponentProps> = ({ hi
|
|||
/>
|
||||
}
|
||||
error={saveError}
|
||||
data-test-subj="saveRepositoryApiError"
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
@ -71,7 +72,7 @@ export const RepositoryAdd: React.FunctionComponent<RouteComponentProps> = ({ hi
|
|||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<h1 data-test-subj="pageTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.addRepositoryTitle"
|
||||
defaultMessage="Register repository"
|
||||
|
|
|
@ -30,7 +30,7 @@ export const sendRequest = async ({
|
|||
try {
|
||||
const response = await httpService.httpClient[method](path, body);
|
||||
|
||||
if (!response.data) {
|
||||
if (typeof response.data === 'undefined') {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ export const sendRequest = async ({
|
|||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e,
|
||||
error: e.response ? e.response : e,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -83,16 +83,7 @@ export const useRequest = ({
|
|||
uimActionType,
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
if (timeout) {
|
||||
[response] = await Promise.all([
|
||||
sendRequest(requestBody),
|
||||
new Promise(resolve => setTimeout(resolve, timeout)),
|
||||
]);
|
||||
} else {
|
||||
response = await sendRequest(requestBody);
|
||||
}
|
||||
const response = await sendRequest(requestBody);
|
||||
|
||||
// Don't update state if an outdated request has resolved.
|
||||
if (isOutdatedRequest) {
|
||||
|
|
|
@ -29,6 +29,29 @@ export interface RepositorySettingsValidation {
|
|||
[key: string]: string[];
|
||||
}
|
||||
|
||||
export const INVALID_NAME_CHARS = ['"', '*', '\\', '<', '|', ',', '>', '/', '?'];
|
||||
|
||||
const isStringEmpty = (str: string | null): boolean => {
|
||||
return str ? !Boolean(str.trim()) : true;
|
||||
};
|
||||
|
||||
const doesStringContainChar = (string: string, char: string | string[]) => {
|
||||
const chars = Array.isArray(char) ? char : [char];
|
||||
const total = chars.length;
|
||||
let containsChar = false;
|
||||
let charFound: string | null = null;
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
if (string.includes(chars[i])) {
|
||||
containsChar = true;
|
||||
charFound = chars[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { containsChar, charFound };
|
||||
};
|
||||
|
||||
export const validateRepository = (
|
||||
repository: Repository | EmptyRepository,
|
||||
validateSettings: boolean = true
|
||||
|
@ -56,6 +79,25 @@ export const validateRepository = (
|
|||
];
|
||||
}
|
||||
|
||||
if (name.includes(' ')) {
|
||||
validation.errors.name = [
|
||||
i18n.translate('xpack.snapshotRestore.repositoryValidation.nameValidation.errorSpace', {
|
||||
defaultMessage: 'Spaces are not allowed in the name.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
const nameCharValidation = doesStringContainChar(name, INVALID_NAME_CHARS);
|
||||
|
||||
if (nameCharValidation.containsChar) {
|
||||
validation.errors.name = [
|
||||
i18n.translate('xpack.snapshotRestore.repositoryValidation.nameValidation.invalidCharacter', {
|
||||
defaultMessage: 'Character "{char}" is not allowed in the name.',
|
||||
values: { char: nameCharValidation.charFound },
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
isStringEmpty(type) ||
|
||||
(type === REPOSITORY_TYPES.source && isStringEmpty(settings.delegateType))
|
||||
|
@ -74,10 +116,6 @@ export const validateRepository = (
|
|||
return validation;
|
||||
};
|
||||
|
||||
const isStringEmpty = (str: string | null): boolean => {
|
||||
return str ? !Boolean(str.trim()) : true;
|
||||
};
|
||||
|
||||
const validateRepositorySettings = (
|
||||
type: RepositoryType | null,
|
||||
settings: Repository['settings']
|
||||
|
|
11
x-pack/plugins/snapshot_restore/public/test/mocks/chrome.ts
Normal file
11
x-pack/plugins/snapshot_restore/public/test/mocks/chrome.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const chrome = {
|
||||
breadcrumbs: {
|
||||
set() {},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { chrome } from './chrome';
|
8
x-pack/plugins/snapshot_restore/test/fixtures/index.ts
vendored
Normal file
8
x-pack/plugins/snapshot_restore/test/fixtures/index.ts
vendored
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './repository';
|
||||
export * from './snapshot';
|
25
x-pack/plugins/snapshot_restore/test/fixtures/repository.ts
vendored
Normal file
25
x-pack/plugins/snapshot_restore/test/fixtures/repository.ts
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getRandomString } from '../../../../test_utils';
|
||||
import { RepositoryType } from '../../common/types';
|
||||
const defaultSettings: any = { chunkSize: '10mb', location: '/tmp/es-backups' };
|
||||
|
||||
interface Repository {
|
||||
name: string;
|
||||
type: RepositoryType;
|
||||
settings: any;
|
||||
}
|
||||
|
||||
export const getRepository = ({
|
||||
name = getRandomString(),
|
||||
type = 'fs' as 'fs',
|
||||
settings = defaultSettings,
|
||||
}: Partial<Repository> = {}): Repository => ({
|
||||
name,
|
||||
type,
|
||||
settings,
|
||||
});
|
41
x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts
vendored
Normal file
41
x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getRandomString, getRandomNumber } from '../../../../test_utils';
|
||||
|
||||
export const getSnapshot = ({
|
||||
repository = 'my-repo',
|
||||
snapshot = getRandomString(),
|
||||
uuid = getRandomString(),
|
||||
state = 'SUCCESS',
|
||||
indexFailures = [],
|
||||
totalIndices = getRandomNumber(),
|
||||
} = {}) => ({
|
||||
repository,
|
||||
snapshot,
|
||||
uuid,
|
||||
versionId: 8000099,
|
||||
version: '8.0.0',
|
||||
indices: new Array(totalIndices).fill('').map(getRandomString),
|
||||
includeGlobalState: 1,
|
||||
state,
|
||||
startTime: '2019-05-23T06:25:15.896Z',
|
||||
startTimeInMillis: 1558592715896,
|
||||
endTime: '2019-05-23T06:25:16.603Z',
|
||||
endTimeInMillis: 1558592716603,
|
||||
durationInMillis: 707,
|
||||
indexFailures,
|
||||
shards: { total: 3, failed: 0, successful: 3 },
|
||||
});
|
||||
|
||||
export const getIndexFailure = (index = getRandomString()) => ({
|
||||
index,
|
||||
failures: new Array(getRandomNumber({ min: 1, max: 5 })).fill('').map(() => ({
|
||||
status: 400,
|
||||
reason: getRandomString(),
|
||||
shard_id: getRandomString(),
|
||||
})),
|
||||
});
|
|
@ -281,3 +281,12 @@ in order to register the value provided. This helper takes care of that.
|
|||
##### `getErrorsMessages()`
|
||||
|
||||
Find all the DOM nodes with the `.euiFormErrorText` css class from EUI and return an Array with its text content.
|
||||
|
||||
#### `router`
|
||||
|
||||
An object with the following methods:
|
||||
|
||||
##### `navigateTo(url)`
|
||||
|
||||
If you need to navigate to a different route inside your test and you are not using the `<Link />` component from `react-router`
|
||||
in your component, you need to use the `router.navigateTo()` method from the testBed in order to trigger the route change on the `MemoryRouter`.
|
||||
|
|
|
@ -27,6 +27,15 @@
|
|||
If you chose "typescript" you will get a Union Type ready to copy and paste in your test file.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="form-control__label" for="depthInput">Max parent - child depth</label>
|
||||
<input class="form-control__input" type="text" id="depthInput" value="2" />
|
||||
<div class="form-control__helper-text">
|
||||
The dom traversal "depth" to be returned. In most cases 2 level depth is enough to access all your test subjects.
|
||||
You can always add manually later other depths in your Typescript union string type.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
|
|
|
@ -33,9 +33,10 @@ const onStopTracking = () => {
|
|||
document.body.classList.remove('is-tracking');
|
||||
};
|
||||
|
||||
chrome.storage.sync.get(['outputType', 'domTreeRoot'], async ({ outputType, domTreeRoot }) => {
|
||||
chrome.storage.sync.get(['outputType', 'domTreeRoot', 'depth'], async ({ outputType, domTreeRoot, depth }) => {
|
||||
const domRootInput = document.getElementById('domRootInput');
|
||||
const outputTypeSelect = document.getElementById('outputTypeSelect');
|
||||
const depthInput = document.getElementById('depthInput');
|
||||
const startTrackButton = document.getElementById('startTrackingButton');
|
||||
const stopTrackButton = document.getElementById('stopTrackingButton');
|
||||
|
||||
|
@ -53,6 +54,10 @@ chrome.storage.sync.get(['outputType', 'domTreeRoot'], async ({ outputType, domT
|
|||
domRootInput.value = domTreeRoot;
|
||||
}
|
||||
|
||||
if (depth) {
|
||||
depthInput.value = depth;
|
||||
}
|
||||
|
||||
document.querySelectorAll('#outputTypeSelect option').forEach((node) => {
|
||||
if (node.value === outputType) {
|
||||
node.setAttribute('selected', 'selected');
|
||||
|
@ -65,6 +70,13 @@ chrome.storage.sync.get(['outputType', 'domTreeRoot'], async ({ outputType, domT
|
|||
chrome.storage.sync.set({ domTreeRoot: value });
|
||||
});
|
||||
|
||||
depthInput.addEventListener('change', (e) => {
|
||||
const { value } = e.target;
|
||||
if (value) {
|
||||
chrome.storage.sync.set({ depth: value });
|
||||
}
|
||||
});
|
||||
|
||||
outputTypeSelect.addEventListener('change', (e) => {
|
||||
const { value } = e.target;
|
||||
chrome.storage.sync.set({ outputType: value });
|
||||
|
|
|
@ -7,7 +7,51 @@
|
|||
/* eslint-disable no-undef */
|
||||
|
||||
(function () {
|
||||
chrome.storage.sync.get(['domTreeRoot', 'outputType'], ({ domTreeRoot, outputType }) => {
|
||||
/**
|
||||
* Go from ['a', 'b', 'c', 'd', 'e']
|
||||
* To ['a.b.c.d.e', 'a.b.c.d', 'a.b.c', 'a.b']
|
||||
* @param arr The array to outpu
|
||||
*/
|
||||
const outputArray = (arr) => {
|
||||
const output = [];
|
||||
let i = 0;
|
||||
while(i < arr.length - 1) {
|
||||
const end = i ? i * -1 : undefined;
|
||||
output.push(arr.slice(0, end).join('.'));
|
||||
i++;
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const getAllNestedPathsFromArray = (arr, computedArray = []) => {
|
||||
// Output the array without skipping any item
|
||||
let output = [...computedArray, ...outputArray(arr)];
|
||||
|
||||
// We remove the "head" and the "tail" of the array (pos 0 and arr.length -1)
|
||||
// We go from ['a', 'b', 'c', 'd', 'e'] (5 items)
|
||||
// To 3 modified arrays
|
||||
// ['a', 'c', 'd', 'e'] => outputArray()
|
||||
// ['a', 'd', 'e'] => outputArray()
|
||||
// ['a', 'e'] => outputArray()
|
||||
let itemsToSkip = arr.length - 2;
|
||||
if (itemsToSkip > 0) {
|
||||
while(itemsToSkip) {
|
||||
const newArray = [...arr];
|
||||
newArray.splice(1, itemsToSkip);
|
||||
output = [...output, ...outputArray(newArray)];
|
||||
itemsToSkip--;
|
||||
}
|
||||
}
|
||||
|
||||
if (arr.length > 2) {
|
||||
// Recursively call the function skipping the first array item
|
||||
return getAllNestedPathsFromArray(arr.slice(1), output);
|
||||
}
|
||||
|
||||
return output.sort();
|
||||
};
|
||||
|
||||
chrome.storage.sync.get(['domTreeRoot', 'outputType', 'depth'], ({ domTreeRoot, outputType, depth = 2 }) => {
|
||||
const datasetKey = 'testSubj';
|
||||
|
||||
if (domTreeRoot && !document.querySelector(domTreeRoot)) {
|
||||
|
@ -16,8 +60,6 @@
|
|||
throw new Error(`DOM node "${domTreeRoot}" not found.`);
|
||||
}
|
||||
|
||||
const dataTestSubjects = new Set();
|
||||
|
||||
const arrayToType = array => (
|
||||
array.reduce((string, subject) => {
|
||||
return string === '' ? `'${subject}'` : `${string}\n | '${subject}'`;
|
||||
|
@ -30,6 +72,13 @@
|
|||
}, '')
|
||||
);
|
||||
|
||||
const addTestSubject = (testSubject) => {
|
||||
const subjectDepth = testSubject.split('.').length;
|
||||
if (subjectDepth <= parseInt(depth, 10)) {
|
||||
window.__test_utils__.dataTestSubjects.add(testSubject);
|
||||
}
|
||||
};
|
||||
|
||||
const findTestSubjects = (
|
||||
node = domTreeRoot ? document.querySelector(domTreeRoot) : document.querySelector('body'),
|
||||
path = []
|
||||
|
@ -38,13 +87,8 @@
|
|||
// We probably navigated outside the initial DOM root
|
||||
return;
|
||||
}
|
||||
|
||||
const testSubjectOnNode = node.dataset[datasetKey];
|
||||
|
||||
if (testSubjectOnNode) {
|
||||
dataTestSubjects.add(testSubjectOnNode);
|
||||
}
|
||||
|
||||
const updatedPath = testSubjectOnNode
|
||||
? [...path, testSubjectOnNode]
|
||||
: path;
|
||||
|
@ -53,7 +97,13 @@
|
|||
const pathToString = updatedPath.join('.');
|
||||
|
||||
if (pathToString) {
|
||||
dataTestSubjects.add(pathToString);
|
||||
// Add the complete nested path ('a.b.c.d')
|
||||
addTestSubject(pathToString);
|
||||
// Add each item separately
|
||||
updatedPath.forEach(addTestSubject);
|
||||
// Add all the combination ('a.b', 'a.c', 'a.e', ...)
|
||||
const nestedPaths = getAllNestedPathsFromArray(updatedPath);
|
||||
nestedPaths.forEach(addTestSubject);
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -65,6 +115,7 @@
|
|||
};
|
||||
|
||||
const output = () => {
|
||||
const { dataTestSubjects } = window.__test_utils__;
|
||||
const allTestSubjects = Array.from(dataTestSubjects).sort();
|
||||
|
||||
console.log(`------------- TEST SUBJECTS (${allTestSubjects.length}) ------------- `);
|
||||
|
@ -79,20 +130,24 @@
|
|||
// Handler for the clicks on the document to keep tracking
|
||||
// new test subjects
|
||||
const documentClicksHandler = () => {
|
||||
const { dataTestSubjects } = window.__test_utils__;
|
||||
const total = dataTestSubjects.size;
|
||||
|
||||
findTestSubjects();
|
||||
// Wait to be sure that the DOM has updated
|
||||
setTimeout(() => {
|
||||
findTestSubjects();
|
||||
if (dataTestSubjects.size === total) {
|
||||
// No new test subject, nothing to output
|
||||
return;
|
||||
}
|
||||
|
||||
if (dataTestSubjects.size === total) {
|
||||
// No new test subject, nothing to output
|
||||
return;
|
||||
}
|
||||
output();
|
||||
}, 500);
|
||||
|
||||
output();
|
||||
};
|
||||
|
||||
// Add meta data on the window object
|
||||
window.__test_utils__ = window.__test_utils__ || { documentClicksHandler, isTracking: false };
|
||||
window.__test_utils__ = window.__test_utils__ || { documentClicksHandler, isTracking: false, dataTestSubjects: new Set() };
|
||||
|
||||
// Handle "click" event on the document to update our test subjects
|
||||
if (!window.__test_utils__.isTracking) {
|
||||
|
|
|
@ -7,4 +7,5 @@
|
|||
if (window.__test_utils__ && window.__test_utils__.isTracking) {
|
||||
document.removeEventListener('click', window.__test_utils__.documentClicksHandler);
|
||||
window.__test_utils__.isTracking = false;
|
||||
window.__test_utils__.dataTestSubjects = new Set();
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { registerTestBed } from './testbed';
|
||||
export { getRandomString, nextTick } from './lib';
|
||||
export * from './testbed';
|
||||
export * from './lib';
|
||||
export { findTestSubject } from './find_test_subject';
|
||||
export { getConfigSchema } from './get_config_schema';
|
||||
|
|
|
@ -4,5 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { getRandomString } from './strings';
|
||||
export { nextTick } from './utils';
|
||||
export { nextTick, getRandomString, getRandomNumber } from './utils';
|
||||
|
|
|
@ -4,4 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Chance from 'chance';
|
||||
|
||||
const chance = new Chance();
|
||||
const CHARS_POOL = 'abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
export const nextTick = (time = 0) => new Promise(resolve => setTimeout(resolve, time));
|
||||
|
||||
export const getRandomNumber = (range: { min: number; max: number } = { min: 1, max: 20 }) =>
|
||||
chance.integer(range);
|
||||
|
||||
export const getRandomString = (options = {}) =>
|
||||
`${chance.string({ pool: CHARS_POOL, ...options })}-${Date.now()}`;
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { Component, ComponentType } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { MemoryRouter, Route, withRouter } from 'react-router-dom';
|
||||
import * as H from 'history';
|
||||
|
||||
export const WithMemoryRouter = (initialEntries: string[] = ['/'], initialIndex: number = 0) => (
|
||||
|
@ -17,28 +16,31 @@ export const WithMemoryRouter = (initialEntries: string[] = ['/'], initialIndex:
|
|||
</MemoryRouter>
|
||||
);
|
||||
|
||||
export const WithRoute = (componentRoutePath = '/', onRouter = (router: MemoryRouter) => {}) => (
|
||||
export const WithRoute = (componentRoutePath = '/', onRouter = (router: any) => {}) => (
|
||||
WrappedComponent: ComponentType
|
||||
) => {
|
||||
return class extends Component {
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
// Create a class component that will catch the router
|
||||
// and forward it to our "onRouter()" handler.
|
||||
const CatchRouter = withRouter(
|
||||
class extends Component<any> {
|
||||
componentDidMount() {
|
||||
const { match, location, history } = this.props;
|
||||
const router = { route: { match, location }, history };
|
||||
onRouter(router);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { router } = this.context;
|
||||
onRouter(router);
|
||||
render() {
|
||||
return <WrappedComponent {...this.props} />;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Route
|
||||
path={componentRoutePath}
|
||||
render={props => <WrappedComponent {...props} {...this.props} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
return (props: any) => (
|
||||
<Route
|
||||
path={componentRoutePath}
|
||||
render={routerProps => <CatchRouter {...routerProps} {...props} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface Router {
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
*/
|
||||
|
||||
export { registerTestBed } from './testbed';
|
||||
export { TestBed, TestBedConfig } from './types';
|
||||
|
|
|
@ -7,39 +7,63 @@
|
|||
import React, { ComponentType } from 'react';
|
||||
import { Store } from 'redux';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { mountWithIntl } from '../enzyme_helpers';
|
||||
import { WithMemoryRouter, WithRoute } from '../router_helpers';
|
||||
import { WithStore } from '../redux_helpers';
|
||||
import { MemoryRouterConfig } from './types';
|
||||
|
||||
export const mountComponent = (
|
||||
Component: ComponentType,
|
||||
memoryRouter: MemoryRouterConfig,
|
||||
store: Store | null,
|
||||
props: any
|
||||
): ReactWrapper => {
|
||||
interface Config {
|
||||
Component: ComponentType;
|
||||
memoryRouter: MemoryRouterConfig;
|
||||
store: Store | null;
|
||||
props: any;
|
||||
onRouter: (router: any) => void;
|
||||
}
|
||||
|
||||
const getCompFromConfig = ({ Component, memoryRouter, store, onRouter }: Config): ComponentType => {
|
||||
const wrapWithRouter = memoryRouter.wrapComponent !== false;
|
||||
|
||||
let Comp;
|
||||
let Comp: ComponentType = store !== null ? WithStore(store)(Component) : Component;
|
||||
|
||||
if (wrapWithRouter) {
|
||||
const { componentRoutePath, onRouter, initialEntries, initialIndex } = memoryRouter!;
|
||||
const { componentRoutePath, initialEntries, initialIndex } = memoryRouter!;
|
||||
|
||||
// Wrap the componenet with a MemoryRouter and attach it to a react-router <Route />
|
||||
Comp = WithMemoryRouter(initialEntries, initialIndex)(
|
||||
WithRoute(componentRoutePath, onRouter)(Component)
|
||||
WithRoute(componentRoutePath, onRouter)(Comp)
|
||||
);
|
||||
|
||||
// Add the Redux Provider
|
||||
if (store !== null) {
|
||||
Comp = WithStore(store)(Comp);
|
||||
}
|
||||
} else {
|
||||
Comp = store !== null ? WithStore(store)(Component) : Component;
|
||||
}
|
||||
|
||||
return mountWithIntl(<Comp {...props} />);
|
||||
return Comp;
|
||||
};
|
||||
|
||||
export const mountComponentSync = (config: Config): ReactWrapper => {
|
||||
const Comp = getCompFromConfig(config);
|
||||
return mountWithIntl(<Comp {...config.props} />);
|
||||
};
|
||||
|
||||
export const mountComponentAsync = async (config: Config): Promise<ReactWrapper> => {
|
||||
const Comp = getCompFromConfig(config);
|
||||
|
||||
/**
|
||||
* In order for hooks with effects to work in our tests
|
||||
* we need to wrap the mounting under the new act "async"
|
||||
* that ships with React 16.9.0
|
||||
*
|
||||
* https://github.com/facebook/react/pull/14853
|
||||
* https://github.com/threepointone/react-act-examples/blob/master/sync.md
|
||||
*/
|
||||
let component: ReactWrapper;
|
||||
|
||||
// @ts-ignore
|
||||
await act(async () => {
|
||||
component = mountWithIntl(<Comp {...config.props} />);
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return component;
|
||||
};
|
||||
|
||||
export const getJSXComponentWithProps = (Component: ComponentType, props: any) => (
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
import { ComponentType, ReactWrapper } from 'enzyme';
|
||||
import { findTestSubject } from '../find_test_subject';
|
||||
import { reactRouterMock } from '../router_helpers';
|
||||
import { mountComponent, getJSXComponentWithProps } from './mount_component';
|
||||
import {
|
||||
mountComponentSync,
|
||||
mountComponentAsync,
|
||||
getJSXComponentWithProps,
|
||||
} from './mount_component';
|
||||
import { TestBedConfig, TestBed, SetupFunc } from './types';
|
||||
|
||||
const defaultConfig: TestBedConfig = {
|
||||
|
@ -48,7 +52,20 @@ export const registerTestBed = <T extends string = string>(
|
|||
defaultProps = defaultConfig.defaultProps,
|
||||
memoryRouter = defaultConfig.memoryRouter!,
|
||||
store = defaultConfig.store,
|
||||
doMountAsync = false,
|
||||
} = config || {};
|
||||
|
||||
// Keep a reference to the React Router
|
||||
let router: any;
|
||||
|
||||
const onRouter = (_router: any) => {
|
||||
router = _router;
|
||||
|
||||
if (memoryRouter.onRouter) {
|
||||
memoryRouter.onRouter(_router);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* In some cases, component have some logic that interacts with the react router
|
||||
* _before_ the component is mounted.(Class constructor() I'm looking at you :)
|
||||
|
@ -56,156 +73,196 @@ export const registerTestBed = <T extends string = string>(
|
|||
* By adding the following lines, we make sure there is always a router available
|
||||
* when instantiating the Component.
|
||||
*/
|
||||
if (memoryRouter.onRouter) {
|
||||
memoryRouter.onRouter(reactRouterMock);
|
||||
}
|
||||
onRouter(reactRouterMock);
|
||||
|
||||
const setup: SetupFunc<T> = props => {
|
||||
// If a function is provided we execute it
|
||||
const storeToMount = typeof store === 'function' ? store() : store!;
|
||||
const mountConfig = {
|
||||
Component,
|
||||
memoryRouter,
|
||||
store: storeToMount,
|
||||
props: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
onRouter,
|
||||
};
|
||||
|
||||
const component = mountComponent(Component, memoryRouter, storeToMount, {
|
||||
...defaultProps,
|
||||
...props,
|
||||
});
|
||||
if (doMountAsync) {
|
||||
return mountComponentAsync(mountConfig).then(onComponentMounted);
|
||||
}
|
||||
|
||||
/**
|
||||
* ----------------------------------------------------------------
|
||||
* Utils
|
||||
* ----------------------------------------------------------------
|
||||
*/
|
||||
return onComponentMounted(mountComponentSync(mountConfig));
|
||||
|
||||
const find: TestBed<T>['find'] = (testSubject: T) => {
|
||||
const testSubjectToArray = testSubject.split('.');
|
||||
// ---------------------
|
||||
|
||||
return testSubjectToArray.reduce((reactWrapper, subject, i) => {
|
||||
const target = findTestSubject(reactWrapper, subject);
|
||||
if (!target.length && i < testSubjectToArray.length - 1) {
|
||||
function onComponentMounted(component: ReactWrapper) {
|
||||
/**
|
||||
* ----------------------------------------------------------------
|
||||
* Utils
|
||||
* ----------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const find: TestBed<T>['find'] = (testSubject: T) => {
|
||||
const testSubjectToArray = testSubject.split('.');
|
||||
|
||||
return testSubjectToArray.reduce((reactWrapper, subject, i) => {
|
||||
const target = findTestSubject(reactWrapper, subject);
|
||||
if (!target.length && i < testSubjectToArray.length - 1) {
|
||||
throw new Error(
|
||||
`Can't access nested test subject "${
|
||||
testSubjectToArray[i + 1]
|
||||
}" of unknown node "${subject}"`
|
||||
);
|
||||
}
|
||||
return target;
|
||||
}, component);
|
||||
};
|
||||
|
||||
const exists: TestBed<T>['exists'] = (testSubject, count = 1) =>
|
||||
find(testSubject).length === count;
|
||||
|
||||
const setProps: TestBed<T>['setProps'] = updatedProps => {
|
||||
if (memoryRouter.wrapComponent !== false) {
|
||||
throw new Error(
|
||||
`Can't access nested test subject "${
|
||||
testSubjectToArray[i + 1]
|
||||
}" of unknown node "${subject}"`
|
||||
'setProps() can only be called on a component **not** wrapped by a router route.'
|
||||
);
|
||||
}
|
||||
return target;
|
||||
}, component);
|
||||
};
|
||||
if (store === null) {
|
||||
return component.setProps({ ...defaultProps, ...updatedProps });
|
||||
}
|
||||
// Update the props on the Redux Provider children
|
||||
return component.setProps({
|
||||
children: getJSXComponentWithProps(Component, { ...defaultProps, ...updatedProps }),
|
||||
});
|
||||
};
|
||||
|
||||
const exists: TestBed<T>['exists'] = (testSubject, count = 1) =>
|
||||
find(testSubject).length === count;
|
||||
/**
|
||||
* ----------------------------------------------------------------
|
||||
* Forms
|
||||
* ----------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const setProps: TestBed<T>['setProps'] = updatedProps => {
|
||||
if (memoryRouter.wrapComponent !== false) {
|
||||
throw new Error(
|
||||
'setProps() can only be called on a component **not** wrapped by a router route.'
|
||||
);
|
||||
}
|
||||
if (store === null) {
|
||||
return component.setProps({ ...defaultProps, ...updatedProps });
|
||||
}
|
||||
// Update the props on the Redux Provider children
|
||||
return component.setProps({
|
||||
children: getJSXComponentWithProps(Component, { ...defaultProps, ...updatedProps }),
|
||||
});
|
||||
};
|
||||
const setInputValue: TestBed<T>['form']['setInputValue'] = (
|
||||
input,
|
||||
value,
|
||||
isAsync = false
|
||||
) => {
|
||||
const formInput = typeof input === 'string' ? find(input) : (input as ReactWrapper);
|
||||
|
||||
/**
|
||||
* ----------------------------------------------------------------
|
||||
* Forms
|
||||
* ----------------------------------------------------------------
|
||||
*/
|
||||
if (!formInput.length) {
|
||||
throw new Error(`Input "${input}" was not found.`);
|
||||
}
|
||||
formInput.simulate('change', { target: { value } });
|
||||
component.update();
|
||||
|
||||
const setInputValue: TestBed<T>['form']['setInputValue'] = (input, value, isAsync = false) => {
|
||||
const formInput = typeof input === 'string' ? find(input) : (input as ReactWrapper);
|
||||
if (!isAsync) {
|
||||
return;
|
||||
}
|
||||
return new Promise(resolve => setTimeout(resolve));
|
||||
};
|
||||
|
||||
formInput.simulate('change', { target: { value } });
|
||||
component.update();
|
||||
const selectCheckBox: TestBed<T>['form']['selectCheckBox'] = (
|
||||
testSubject,
|
||||
isChecked = true
|
||||
) => {
|
||||
const checkBox = find(testSubject);
|
||||
if (!checkBox.length) {
|
||||
throw new Error(`"${testSubject}" was not found.`);
|
||||
}
|
||||
checkBox.simulate('change', { target: { checked: isChecked } });
|
||||
};
|
||||
|
||||
if (!isAsync) {
|
||||
return;
|
||||
}
|
||||
return new Promise(resolve => setTimeout(resolve));
|
||||
};
|
||||
const toggleEuiSwitch: TestBed<T>['form']['toggleEuiSwitch'] = selectCheckBox; // Same API as "selectCheckBox"
|
||||
|
||||
const selectCheckBox: TestBed<T>['form']['selectCheckBox'] = (
|
||||
dataTestSubject,
|
||||
isChecked = true
|
||||
) => {
|
||||
find(dataTestSubject).simulate('change', { target: { checked: isChecked } });
|
||||
};
|
||||
const setComboBoxValue: TestBed<T>['form']['setComboBoxValue'] = (
|
||||
comboBoxTestSubject,
|
||||
value
|
||||
) => {
|
||||
const comboBox = find(comboBoxTestSubject);
|
||||
const formInput = findTestSubject(comboBox, 'comboBoxSearchInput');
|
||||
setInputValue(formInput, value);
|
||||
|
||||
const toggleEuiSwitch: TestBed<T>['form']['toggleEuiSwitch'] = selectCheckBox; // Same API as "selectCheckBox"
|
||||
// keyCode 13 === ENTER
|
||||
comboBox.simulate('keydown', { keyCode: 13 });
|
||||
component.update();
|
||||
};
|
||||
|
||||
const setComboBoxValue: TestBed<T>['form']['setComboBoxValue'] = (
|
||||
comboBoxTestSubject,
|
||||
value
|
||||
) => {
|
||||
const comboBox = find(comboBoxTestSubject);
|
||||
const formInput = findTestSubject(comboBox, 'comboBoxSearchInput');
|
||||
setInputValue(formInput, value);
|
||||
const getErrorsMessages: TestBed<T>['form']['getErrorsMessages'] = () => {
|
||||
const errorMessagesWrappers = component.find('.euiFormErrorText');
|
||||
return errorMessagesWrappers.map(err => err.text());
|
||||
};
|
||||
|
||||
// keyCode 13 === ENTER
|
||||
comboBox.simulate('keydown', { keyCode: 13 });
|
||||
component.update();
|
||||
};
|
||||
/**
|
||||
* ----------------------------------------------------------------
|
||||
* Tables
|
||||
* ----------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const getErrorsMessages: TestBed<T>['form']['getErrorsMessages'] = () => {
|
||||
const errorMessagesWrappers = component.find('.euiFormErrorText');
|
||||
return errorMessagesWrappers.map(err => err.text());
|
||||
};
|
||||
/**
|
||||
* Parse an EUI table and return meta data information about its rows and colum content.
|
||||
*
|
||||
* @param tableTestSubject The data test subject of the EUI table
|
||||
*/
|
||||
const getMetaData: TestBed<T>['table']['getMetaData'] = tableTestSubject => {
|
||||
const table = find(tableTestSubject);
|
||||
|
||||
/**
|
||||
* ----------------------------------------------------------------
|
||||
* Tables
|
||||
* ----------------------------------------------------------------
|
||||
*/
|
||||
if (!table.length) {
|
||||
throw new Error(`Eui Table "${tableTestSubject}" not found.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an EUI table and return meta data information about its rows and colum content.
|
||||
*
|
||||
* @param tableTestSubject The data test subject of the EUI table
|
||||
*/
|
||||
const getMetaData: TestBed<T>['table']['getMetaData'] = tableTestSubject => {
|
||||
const table = find(tableTestSubject);
|
||||
const rows = table
|
||||
.find('tr')
|
||||
.slice(1) // we remove the first row as it is the table header
|
||||
.map(row => ({
|
||||
reactWrapper: row,
|
||||
columns: row.find('td').map(col => ({
|
||||
reactWrapper: col,
|
||||
// We can't access the td value with col.text() because
|
||||
// eui adds an extra div in td on mobile => (.euiTableRowCell__mobileHeader)
|
||||
value: col.find('.euiTableCellContent').text(),
|
||||
})),
|
||||
}));
|
||||
|
||||
if (!table.length) {
|
||||
throw new Error(`Eui Table "${tableTestSubject}" not found.`);
|
||||
}
|
||||
// Also output the raw cell values, in the following format: [[td0, td1, td2], [td0, td1, td2]]
|
||||
const tableCellsValues = rows.map(({ columns }) => columns.map(col => col.value));
|
||||
return { rows, tableCellsValues };
|
||||
};
|
||||
|
||||
const rows = table
|
||||
.find('tr')
|
||||
.slice(1) // we remove the first row as it is the table header
|
||||
.map(row => ({
|
||||
reactWrapper: row,
|
||||
columns: row.find('td').map(col => ({
|
||||
reactWrapper: col,
|
||||
// We can't access the td value with col.text() because
|
||||
// eui adds an extra div in td on mobile => (.euiTableRowCell__mobileHeader)
|
||||
value: col.find('.euiTableCellContent').text(),
|
||||
})),
|
||||
}));
|
||||
/**
|
||||
* ----------------------------------------------------------------
|
||||
* Router
|
||||
* ----------------------------------------------------------------
|
||||
*/
|
||||
const navigateTo = (_url: string) => {
|
||||
const url =
|
||||
_url[0] === '#'
|
||||
? _url.replace('#', '') // remove the beginning hash as the memory router does not understand them
|
||||
: _url;
|
||||
router.history.push(url);
|
||||
};
|
||||
|
||||
// Also output the raw cell values, in the following format: [[td0, td1, td2], [td0, td1, td2]]
|
||||
const tableCellsValues = rows.map(({ columns }) => columns.map(col => col.value));
|
||||
return { rows, tableCellsValues };
|
||||
};
|
||||
|
||||
return {
|
||||
component,
|
||||
exists,
|
||||
find,
|
||||
setProps,
|
||||
table: {
|
||||
getMetaData,
|
||||
},
|
||||
form: {
|
||||
setInputValue,
|
||||
selectCheckBox,
|
||||
toggleEuiSwitch,
|
||||
setComboBoxValue,
|
||||
getErrorsMessages,
|
||||
},
|
||||
};
|
||||
return {
|
||||
component,
|
||||
exists,
|
||||
find,
|
||||
setProps,
|
||||
table: {
|
||||
getMetaData,
|
||||
},
|
||||
form: {
|
||||
setInputValue,
|
||||
selectCheckBox,
|
||||
toggleEuiSwitch,
|
||||
setComboBoxValue,
|
||||
getErrorsMessages,
|
||||
},
|
||||
router: {
|
||||
navigateTo,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return setup;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { Store } from 'redux';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
|
||||
export type SetupFunc<T> = (props?: any) => TestBed<T>;
|
||||
export type SetupFunc<T> = (props?: any) => TestBed<T> | Promise<TestBed<T>>;
|
||||
|
||||
export interface EuiTableMetaData {
|
||||
/** Array of rows of the table. Each row exposes its reactWrapper and its columns */
|
||||
|
@ -23,7 +23,7 @@ export interface EuiTableMetaData {
|
|||
tableCellsValues: string[][];
|
||||
}
|
||||
|
||||
export interface TestBed<T> {
|
||||
export interface TestBed<T = string> {
|
||||
/** The comonent under test */
|
||||
component: ReactWrapper;
|
||||
/**
|
||||
|
@ -41,14 +41,14 @@ export interface TestBed<T> {
|
|||
*
|
||||
* @example
|
||||
*
|
||||
```ts
|
||||
find('nameInput');
|
||||
// or more specific,
|
||||
// "nameInput" is a child of "myForm"
|
||||
find('myForm.nameInput');
|
||||
```
|
||||
```ts
|
||||
find('nameInput');
|
||||
// or more specific,
|
||||
// "nameInput" is a child of "myForm"
|
||||
find('myForm.nameInput');
|
||||
```
|
||||
*/
|
||||
find: (testSubject: T) => ReactWrapper;
|
||||
find: (testSubject: T) => ReactWrapper<any>;
|
||||
/**
|
||||
* Update the props of the mounted component
|
||||
*
|
||||
|
@ -102,6 +102,12 @@ find('myForm.nameInput');
|
|||
table: {
|
||||
getMetaData: (tableTestSubject: T) => EuiTableMetaData;
|
||||
};
|
||||
router: {
|
||||
/**
|
||||
* Navigate to another React router <Route />
|
||||
*/
|
||||
navigateTo: (url: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TestBedConfig {
|
||||
|
@ -111,6 +117,8 @@ export interface TestBedConfig {
|
|||
memoryRouter?: MemoryRouterConfig;
|
||||
/** An optional redux store. You can also provide a function that returns a store. */
|
||||
store?: (() => Store) | Store | null;
|
||||
/* Mount the component asynchronously. When using "hooked" components with _useEffect()_ calls, you need to set this to "true". */
|
||||
doMountAsync?: boolean;
|
||||
}
|
||||
|
||||
export interface MemoryRouterConfig {
|
||||
|
|
2
x-pack/typings/index.d.ts
vendored
2
x-pack/typings/index.d.ts
vendored
|
@ -20,3 +20,5 @@ type MethodKeysOf<T> = {
|
|||
}[keyof T];
|
||||
|
||||
type PublicMethodsOf<T> = Pick<T, MethodKeysOf<T>>;
|
||||
|
||||
declare module 'axios/lib/adapters/xhr';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue