mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* Change app name and change default tab to snapshots * Fix i18n issues * UI placeholder for deleting snapshots * Add bulk snapshot delete endpoint and test, adjust UI delete components * Add restore action buttons * Set up restore snapshot form entry * Add RestoreSettings type * Restore step general * Combobox for ignore settings * Code editor for modifying index settings * Truncate list of indices in snapshot details and provide link to show all * Disable include global state option for snapshots that don't have global state * Add step titles, rename General to Logistics * Committing deleted file * Change repository detail settings to reverse list style * Review summary tab and placeholder json tab * Add restore wizard validation * Add restore serialization * Create restore endpoint and integration * Move new files to /legacy * Fix bugs, add search filter bar to indices list * Allow de/select all of indices * Create new recovery status tab * Prefix hook methods with `use` * Remove unnecessary RouteComponentProps * Add get all snapshot recoveries endpoint, deserialization, types, and tests * Remove unused timeout variable; enhance to allow polling (interval'd requests) without resetting state like loading * Add recovery table * Use shim'd i18n * Adjust disabled restore state * Fix invariant error * Fix partial restore label * Fix misc bugs * Address copywriting feedback * Address i18n feedback * Address PR feedback * Rename recovery to restore * Add toggle show/hide link to restore wizard summary * Fix snapshot tests due to changes in includeGlobalState deserialization format * Fix EuiCard warning
This commit is contained in:
parent
860be24124
commit
8d7b34dd40
69 changed files with 3952 additions and 179 deletions
|
@ -11,7 +11,7 @@ export const PLUGIN = {
|
|||
MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType,
|
||||
getI18nName: (i18n: any): string => {
|
||||
return i18n.translate('xpack.snapshotRestore.appName', {
|
||||
defaultMessage: 'Snapshot Repositories',
|
||||
defaultMessage: 'Snapshot and Restore',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,3 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
export { flatten } from './flatten';
|
||||
export { serializeRestoreSettings } from './restore_settings_serialization';
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { serializeRestoreSettings } from './restore_settings_serialization';
|
||||
|
||||
describe('restore_settings_serialization()', () => {
|
||||
it('should serialize blank restore settings', () => {
|
||||
expect(serializeRestoreSettings({})).toEqual({});
|
||||
});
|
||||
|
||||
it('should serialize partial restore settings', () => {
|
||||
expect(serializeRestoreSettings({})).toEqual({});
|
||||
expect(
|
||||
serializeRestoreSettings({
|
||||
indices: ['foo', 'bar'],
|
||||
ignoreIndexSettings: ['setting1'],
|
||||
partial: true,
|
||||
})
|
||||
).toEqual({
|
||||
indices: ['foo', 'bar'],
|
||||
ignore_index_settings: ['setting1'],
|
||||
partial: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize full restore settings', () => {
|
||||
expect(
|
||||
serializeRestoreSettings({
|
||||
indices: ['foo', 'bar'],
|
||||
renamePattern: 'capture_pattern',
|
||||
renameReplacement: 'replacement_pattern',
|
||||
includeGlobalState: true,
|
||||
partial: true,
|
||||
indexSettings: '{"modified_setting":123}',
|
||||
ignoreIndexSettings: ['setting1'],
|
||||
})
|
||||
).toEqual({
|
||||
indices: ['foo', 'bar'],
|
||||
rename_pattern: 'capture_pattern',
|
||||
rename_replacement: 'replacement_pattern',
|
||||
include_global_state: true,
|
||||
partial: true,
|
||||
index_settings: { modified_setting: 123 },
|
||||
ignore_index_settings: ['setting1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip serialization of invalid json index settings', () => {
|
||||
expect(
|
||||
serializeRestoreSettings({
|
||||
indexSettings: '{"invalid_setting:123,}',
|
||||
})
|
||||
).toEqual({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { RestoreSettings, RestoreSettingsEs } from '../types';
|
||||
|
||||
const removeUndefinedSettings = (settings: RestoreSettingsEs): RestoreSettingsEs => {
|
||||
return Object.entries(settings).reduce((sts: RestoreSettingsEs, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
sts[key as keyof RestoreSettingsEs] = value;
|
||||
}
|
||||
return sts;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export function serializeRestoreSettings(restoreSettings: RestoreSettings): RestoreSettingsEs {
|
||||
const {
|
||||
indices,
|
||||
renamePattern,
|
||||
renameReplacement,
|
||||
includeGlobalState,
|
||||
partial,
|
||||
indexSettings,
|
||||
ignoreIndexSettings,
|
||||
} = restoreSettings;
|
||||
|
||||
let parsedIndexSettings: RestoreSettingsEs['index_settings'] | undefined;
|
||||
if (indexSettings) {
|
||||
try {
|
||||
parsedIndexSettings = JSON.parse(indexSettings);
|
||||
} catch (e) {
|
||||
// Silently swallow parsing errors since parsing validation is done on client
|
||||
// so we should never reach this point
|
||||
}
|
||||
}
|
||||
|
||||
const settings: RestoreSettingsEs = {
|
||||
indices,
|
||||
rename_pattern: renamePattern,
|
||||
rename_replacement: renameReplacement,
|
||||
include_global_state: includeGlobalState,
|
||||
partial,
|
||||
index_settings: parsedIndexSettings,
|
||||
ignore_index_settings: ignoreIndexSettings,
|
||||
};
|
||||
|
||||
return removeUndefinedSettings(settings);
|
||||
}
|
|
@ -6,3 +6,4 @@
|
|||
|
||||
export * from './repository';
|
||||
export * from './snapshot';
|
||||
export * from './restore';
|
||||
|
|
127
x-pack/legacy/plugins/snapshot_restore/common/types/restore.ts
Normal file
127
x-pack/legacy/plugins/snapshot_restore/common/types/restore.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 interface RestoreSettings {
|
||||
indices?: string[];
|
||||
renamePattern?: string;
|
||||
renameReplacement?: string;
|
||||
includeGlobalState?: boolean;
|
||||
partial?: boolean;
|
||||
indexSettings?: string;
|
||||
ignoreIndexSettings?: string[];
|
||||
}
|
||||
|
||||
export interface RestoreSettingsEs {
|
||||
indices?: string[];
|
||||
rename_pattern?: string;
|
||||
rename_replacement?: string;
|
||||
include_global_state?: boolean;
|
||||
partial?: boolean;
|
||||
index_settings?: { [key: string]: any };
|
||||
ignore_index_settings?: string[];
|
||||
}
|
||||
|
||||
export interface SnapshotRestore {
|
||||
index: string;
|
||||
latestActivityTimeInMillis: number;
|
||||
shards: Array<Partial<SnapshotRestoreShard>>;
|
||||
isComplete: boolean;
|
||||
}
|
||||
|
||||
export interface SnapshotRestoreShard {
|
||||
id: number;
|
||||
primary: boolean;
|
||||
stage: string;
|
||||
snapshot: string;
|
||||
repository: string;
|
||||
version: string;
|
||||
|
||||
targetHost: string;
|
||||
targetNode: string;
|
||||
|
||||
/** e.g. '2019-04-05T21:56:40.438Z' */
|
||||
startTime: string;
|
||||
startTimeInMillis: number;
|
||||
/** e.g. '2019-04-05T21:56:40.438Z' */
|
||||
stopTime: string;
|
||||
stopTimeInMillis: number;
|
||||
totalTime: string;
|
||||
totalTimeInMillis: number;
|
||||
|
||||
bytesTotal: number;
|
||||
bytesRecovered: number;
|
||||
bytesPercent: string;
|
||||
|
||||
filesTotal: number;
|
||||
filesRecovered: number;
|
||||
filesPercent: string;
|
||||
}
|
||||
|
||||
export interface SnapshotRestoreShardEs {
|
||||
id: number;
|
||||
type: string;
|
||||
stage: string;
|
||||
primary: boolean;
|
||||
/** e.g. '2019-04-05T21:56:40.438Z' */
|
||||
start_time: string;
|
||||
start_time_in_millis: number;
|
||||
/** e.g. '2019-04-05T21:56:40.438Z' */
|
||||
stop_time: string;
|
||||
stop_time_in_millis: number;
|
||||
total_time: string;
|
||||
total_time_in_millis: number;
|
||||
source: {
|
||||
repository: string;
|
||||
snapshot: string;
|
||||
version: string;
|
||||
index: string;
|
||||
restoreUUID: string;
|
||||
};
|
||||
target: {
|
||||
id: string;
|
||||
host: string;
|
||||
transport_address: string;
|
||||
ip: string;
|
||||
name: string;
|
||||
};
|
||||
index: {
|
||||
size: {
|
||||
total: string;
|
||||
total_in_bytes: number;
|
||||
reused: string;
|
||||
reused_in_bytes: number;
|
||||
recovered: string;
|
||||
recovered_in_bytes: number;
|
||||
percent: string;
|
||||
};
|
||||
files: {
|
||||
total: number;
|
||||
reused: number;
|
||||
recovered: number;
|
||||
percent: string;
|
||||
};
|
||||
total_time: string;
|
||||
total_time_in_millis: number;
|
||||
source_throttle_time: string;
|
||||
source_throttle_time_in_millis: number;
|
||||
target_throttle_time: string;
|
||||
target_throttle_time_in_millis: number;
|
||||
};
|
||||
translog: {
|
||||
recovered: number;
|
||||
total: number;
|
||||
percent: string;
|
||||
total_on_start: number;
|
||||
total_time: string;
|
||||
total_time_in_millis: number;
|
||||
};
|
||||
verify_index: {
|
||||
check_index_time: string;
|
||||
check_index_time_in_millis: number;
|
||||
total_time: string;
|
||||
total_time_in_millis: number;
|
||||
};
|
||||
}
|
|
@ -11,7 +11,7 @@ export interface SnapshotDetails {
|
|||
versionId: number;
|
||||
version: string;
|
||||
indices: string[];
|
||||
includeGlobalState: number;
|
||||
includeGlobalState: boolean;
|
||||
state: string;
|
||||
/** e.g. '2019-04-05T21:56:40.438Z' */
|
||||
startTime: string;
|
||||
|
|
|
@ -17,6 +17,7 @@ export function snapshotRestore(kibana: any) {
|
|||
publicDir: resolve(__dirname, 'public'),
|
||||
require: ['kibana', 'elasticsearch', 'xpack_main'],
|
||||
uiExports: {
|
||||
styleSheetPaths: resolve(__dirname, 'public/app/index.scss'),
|
||||
managementSections: ['plugins/snapshot_restore'],
|
||||
},
|
||||
init(server: Legacy.Server) {
|
||||
|
|
|
@ -10,8 +10,8 @@ import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui';
|
|||
|
||||
import { SectionLoading, SectionError } from './components';
|
||||
import { BASE_PATH, DEFAULT_SECTION, Section } from './constants';
|
||||
import { RepositoryAdd, RepositoryEdit, SnapshotRestoreHome } from './sections';
|
||||
import { loadPermissions } from './services/http';
|
||||
import { RepositoryAdd, RepositoryEdit, RestoreSnapshot, SnapshotRestoreHome } from './sections';
|
||||
import { useLoadPermissions } from './services/http';
|
||||
import { useAppDependencies } from './index';
|
||||
|
||||
export const App: React.FunctionComponent = () => {
|
||||
|
@ -29,7 +29,7 @@ export const App: React.FunctionComponent = () => {
|
|||
hasPermission: true,
|
||||
missingClusterPrivileges: [],
|
||||
},
|
||||
} = loadPermissions();
|
||||
} = useLoadPermissions();
|
||||
|
||||
if (loadingPermissions) {
|
||||
return (
|
||||
|
@ -73,7 +73,7 @@ export const App: React.FunctionComponent = () => {
|
|||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.app.deniedPermissionDescription"
|
||||
defaultMessage="To use Snapshot Repositories, you must have {clusterPrivilegesCount,
|
||||
defaultMessage="To use Snapshot and Restore, you must have {clusterPrivilegesCount,
|
||||
plural, one {this cluster privilege} other {these cluster privileges}}: {clusterPrivileges}."
|
||||
values={{
|
||||
clusterPrivileges: missingClusterPrivileges.join(', '),
|
||||
|
@ -87,7 +87,7 @@ export const App: React.FunctionComponent = () => {
|
|||
);
|
||||
}
|
||||
|
||||
const sections: Section[] = ['repositories', 'snapshots'];
|
||||
const sections: Section[] = ['repositories', 'snapshots', 'restore_status'];
|
||||
const sectionsRegex = sections.join('|');
|
||||
|
||||
return (
|
||||
|
@ -100,6 +100,16 @@ export const App: React.FunctionComponent = () => {
|
|||
path={`${BASE_PATH}/:section(${sectionsRegex})/:repositoryName?/:snapshotId*`}
|
||||
component={SnapshotRestoreHome}
|
||||
/>
|
||||
<Redirect
|
||||
exact
|
||||
from={`${BASE_PATH}/restore/:repositoryName`}
|
||||
to={`${BASE_PATH}/snapshots`}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${BASE_PATH}/restore/:repositoryName/:snapshotId*`}
|
||||
component={RestoreSnapshot}
|
||||
/>
|
||||
<Redirect from={`${BASE_PATH}`} to={`${BASE_PATH}/${DEFAULT_SECTION}`} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
|
|
@ -11,3 +11,5 @@ export { RepositoryVerificationBadge } from './repository_verification_badge';
|
|||
export { RepositoryTypeLogo } from './repository_type_logo';
|
||||
export { SectionError } from './section_error';
|
||||
export { SectionLoading } from './section_loading';
|
||||
export { SnapshotDeleteProvider } from './snapshot_delete_provider';
|
||||
export { RestoreSnapshotForm } from './restore_snapshot_form';
|
||||
|
|
|
@ -27,7 +27,7 @@ import { REPOSITORY_TYPES } from '../../../../common/constants';
|
|||
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { documentationLinksService } from '../../services/documentation';
|
||||
import { loadRepositoryTypes } from '../../services/http';
|
||||
import { useLoadRepositoryTypes } from '../../services/http';
|
||||
import { textService } from '../../services/text';
|
||||
import { RepositoryValidation } from '../../services/validation';
|
||||
import { SectionError, SectionLoading, RepositoryTypeLogo } from '../';
|
||||
|
@ -56,7 +56,7 @@ export const RepositoryFormStepOne: React.FunctionComponent<Props> = ({
|
|||
error: repositoryTypesError,
|
||||
loading: repositoryTypesLoading,
|
||||
data: repositoryTypes = [],
|
||||
} = loadRepositoryTypes();
|
||||
} = useLoadRepositoryTypes();
|
||||
|
||||
const hasValidationErrors: boolean = !validation.isValid;
|
||||
|
||||
|
@ -143,6 +143,7 @@ export const RepositoryFormStepOne: React.FunctionComponent<Props> = ({
|
|||
<EuiCard
|
||||
title={displayName}
|
||||
icon={<RepositoryTypeLogo type={type} size="l" />}
|
||||
description={<Fragment />} /* EuiCard requires `description` */
|
||||
footer={
|
||||
<EuiButtonEmpty
|
||||
href={documentationLinksService.getRepositoryTypeDocUrl(type)}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Prevent switch controls from moving around when toggling content
|
||||
*/
|
||||
.snapshotRestore__restoreForm__stepLogistics,
|
||||
.snapshotRestore__restoreForm__stepSettings {
|
||||
.euiFormRow--hasEmptyLabelSpace {
|
||||
min-height: auto;
|
||||
margin-top: $euiFontSizeXS + $euiSizeS + ($euiSizeXXL / 4);
|
||||
}
|
||||
}
|
|
@ -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 { RestoreSnapshotForm } from './restore_snapshot_form';
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { EuiStepsHorizontal } from '@elastic/eui';
|
||||
import { useAppDependencies } from '../../index';
|
||||
|
||||
interface Props {
|
||||
currentStep: number;
|
||||
maxCompletedStep: number;
|
||||
updateCurrentStep: (step: number) => void;
|
||||
}
|
||||
|
||||
export const RestoreSnapshotNavigation: React.FunctionComponent<Props> = ({
|
||||
currentStep,
|
||||
maxCompletedStep,
|
||||
updateCurrentStep,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: i18n.translate('xpack.snapshotRestore.restoreForm.navigation.stepLogisticsName', {
|
||||
defaultMessage: 'Logistics',
|
||||
}),
|
||||
isComplete: maxCompletedStep >= 1,
|
||||
isSelected: currentStep === 1,
|
||||
onClick: () => updateCurrentStep(1),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.snapshotRestore.restoreForm.navigation.stepSettingsName', {
|
||||
defaultMessage: 'Index settings',
|
||||
}),
|
||||
isComplete: maxCompletedStep >= 2,
|
||||
isSelected: currentStep === 2,
|
||||
disabled: maxCompletedStep < 1,
|
||||
onClick: () => updateCurrentStep(2),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.snapshotRestore.restoreForm.navigation.stepReviewName', {
|
||||
defaultMessage: 'Review',
|
||||
}),
|
||||
isComplete: maxCompletedStep >= 2,
|
||||
isSelected: currentStep === 3,
|
||||
disabled: maxCompletedStep < 2,
|
||||
onClick: () => updateCurrentStep(3),
|
||||
},
|
||||
];
|
||||
|
||||
return <EuiStepsHorizontal steps={steps} />;
|
||||
};
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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, { Fragment, useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { SnapshotDetails, RestoreSettings } from '../../../../common/types';
|
||||
import { RestoreValidation, validateRestore } from '../../services/validation';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import {
|
||||
RestoreSnapshotStepLogistics,
|
||||
RestoreSnapshotStepSettings,
|
||||
RestoreSnapshotStepReview,
|
||||
} from './steps';
|
||||
import { RestoreSnapshotNavigation } from './navigation';
|
||||
|
||||
interface Props {
|
||||
snapshotDetails: SnapshotDetails;
|
||||
isSaving: boolean;
|
||||
saveError?: React.ReactNode;
|
||||
clearSaveError: () => void;
|
||||
onSave: (repository: RestoreSettings) => void;
|
||||
}
|
||||
|
||||
export const RestoreSnapshotForm: React.FunctionComponent<Props> = ({
|
||||
snapshotDetails,
|
||||
isSaving,
|
||||
saveError,
|
||||
clearSaveError,
|
||||
onSave,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
// Step state
|
||||
const [currentStep, setCurrentStep] = useState<number>(1);
|
||||
const [maxCompletedStep, setMaxCompletedStep] = useState<number>(0);
|
||||
const stepMap: { [key: number]: any } = {
|
||||
1: RestoreSnapshotStepLogistics,
|
||||
2: RestoreSnapshotStepSettings,
|
||||
3: RestoreSnapshotStepReview,
|
||||
};
|
||||
const CurrentStepForm = stepMap[currentStep];
|
||||
|
||||
// Restore details state
|
||||
const [restoreSettings, setRestoreSettings] = useState<RestoreSettings>({});
|
||||
|
||||
// Restore validation state
|
||||
const [validation, setValidation] = useState<RestoreValidation>({
|
||||
isValid: true,
|
||||
errors: {},
|
||||
});
|
||||
|
||||
const updateRestoreSettings = (updatedSettings: Partial<RestoreSettings>): void => {
|
||||
const newRestoreSettings = { ...restoreSettings, ...updatedSettings };
|
||||
const newValidation = validateRestore(newRestoreSettings);
|
||||
setRestoreSettings(newRestoreSettings);
|
||||
setValidation(newValidation);
|
||||
};
|
||||
|
||||
const updateCurrentStep = (step: number) => {
|
||||
if (maxCompletedStep < step - 1) {
|
||||
return;
|
||||
}
|
||||
setCurrentStep(step);
|
||||
setMaxCompletedStep(step - 1);
|
||||
clearSaveError();
|
||||
};
|
||||
|
||||
const onBack = () => {
|
||||
const previousStep = currentStep - 1;
|
||||
setCurrentStep(previousStep);
|
||||
setMaxCompletedStep(previousStep - 1);
|
||||
clearSaveError();
|
||||
};
|
||||
|
||||
const onNext = () => {
|
||||
if (!validation.isValid) {
|
||||
return;
|
||||
}
|
||||
const nextStep = currentStep + 1;
|
||||
setMaxCompletedStep(Math.max(currentStep, maxCompletedStep));
|
||||
setCurrentStep(nextStep);
|
||||
};
|
||||
|
||||
const executeRestore = () => {
|
||||
if (validation.isValid) {
|
||||
onSave(restoreSettings);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<RestoreSnapshotNavigation
|
||||
currentStep={currentStep}
|
||||
maxCompletedStep={maxCompletedStep}
|
||||
updateCurrentStep={updateCurrentStep}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiForm>
|
||||
<CurrentStepForm
|
||||
snapshotDetails={snapshotDetails}
|
||||
restoreSettings={restoreSettings}
|
||||
updateRestoreSettings={updateRestoreSettings}
|
||||
errors={validation.errors}
|
||||
updateCurrentStep={updateCurrentStep}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
{saveError ? (
|
||||
<Fragment>
|
||||
{saveError}
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
) : null}
|
||||
|
||||
<EuiFlexGroup>
|
||||
{currentStep > 1 ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty iconType="arrowLeft" onClick={() => onBack()}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.backButtonLabel"
|
||||
defaultMessage="Back"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
{currentStep < 3 ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="arrowRight"
|
||||
onClick={() => onNext()}
|
||||
disabled={!validation.isValid}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.nextButtonLabel"
|
||||
defaultMessage="Next"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
{currentStep === 3 ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="secondary"
|
||||
iconType="check"
|
||||
onClick={() => executeRestore()}
|
||||
isLoading={isSaving}
|
||||
>
|
||||
{isSaving ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.savingButtonLabel"
|
||||
defaultMessage="Restoring…"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.submitButtonLabel"
|
||||
defaultMessage="Restore snapshot"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -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 { SnapshotDetails, RestoreSettings } from '../../../../../common/types';
|
||||
import { RestoreValidation } from '../../../services/validation';
|
||||
|
||||
export interface StepProps {
|
||||
snapshotDetails: SnapshotDetails;
|
||||
restoreSettings: RestoreSettings;
|
||||
updateRestoreSettings: (updatedSettings: Partial<RestoreSettings>) => void;
|
||||
errors: RestoreValidation['errors'];
|
||||
updateCurrentStep: (step: number) => void;
|
||||
}
|
||||
|
||||
export { RestoreSnapshotStepLogistics } from './step_logistics';
|
||||
export { RestoreSnapshotStepSettings } from './step_settings';
|
||||
export { RestoreSnapshotStepReview } from './step_review';
|
|
@ -0,0 +1,460 @@
|
|||
/*
|
||||
* 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, { Fragment, useState } from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiSelectable,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { Option } from '@elastic/eui/src/components/selectable/types';
|
||||
import { RestoreSettings } from '../../../../../common/types';
|
||||
import { documentationLinksService } from '../../../services/documentation';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { StepProps } from './';
|
||||
|
||||
export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> = ({
|
||||
snapshotDetails,
|
||||
restoreSettings,
|
||||
updateRestoreSettings,
|
||||
errors,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
const {
|
||||
indices: snapshotIndices,
|
||||
includeGlobalState: snapshotIncludeGlobalState,
|
||||
} = snapshotDetails;
|
||||
|
||||
const {
|
||||
indices: restoreIndices,
|
||||
renamePattern,
|
||||
renameReplacement,
|
||||
partial,
|
||||
includeGlobalState,
|
||||
} = restoreSettings;
|
||||
|
||||
// States for choosing all indices, or a subset, including caching previously chosen subset list
|
||||
const [isAllIndices, setIsAllIndices] = useState<boolean>(!Boolean(restoreIndices));
|
||||
const [indicesOptions, setIndicesOptions] = useState<Option[]>(
|
||||
snapshotIndices.map(
|
||||
(index): Option => ({
|
||||
label: index,
|
||||
checked:
|
||||
isAllIndices || (restoreIndices && restoreIndices.includes(index)) ? 'on' : undefined,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// State for setting renaming indices patterns
|
||||
const [isRenamingIndices, setIsRenamingIndices] = useState<boolean>(
|
||||
Boolean(renamePattern || renameReplacement)
|
||||
);
|
||||
|
||||
// Caching state for togglable settings
|
||||
const [cachedRestoreSettings, setCachedRestoreSettings] = useState<RestoreSettings>({
|
||||
indices: [...snapshotIndices],
|
||||
renamePattern: '',
|
||||
renameReplacement: '',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="snapshotRestore__restoreForm__stepLogistics">
|
||||
{/* Step title and doc link */}
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogisticsTitle"
|
||||
defaultMessage="Logistics"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
flush="right"
|
||||
href={documentationLinksService.getRestoreDocUrl()}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel"
|
||||
defaultMessage="Logistics docs"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
{/* Indices */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.indicesTitle"
|
||||
defaultMessage="Indices"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.indicesDescription"
|
||||
defaultMessage="Creates new indices if they don’t exist. Restores existing indices
|
||||
if they are closed and have the same number of shards as the snapshot index."
|
||||
/>
|
||||
}
|
||||
idAria="stepLogisticsIndicesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace
|
||||
fullWidth
|
||||
describedByIds={['stepLogisticsIndicesDescription']}
|
||||
>
|
||||
{/* Fragment needed because EuiFormRow can only have one child: https://github.com/elastic/eui/issues/1931 */}
|
||||
<Fragment>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.allIndicesLabel"
|
||||
defaultMessage="All indices, including system indices"
|
||||
/>
|
||||
}
|
||||
checked={isAllIndices}
|
||||
onChange={e => {
|
||||
const isChecked = e.target.checked;
|
||||
setIsAllIndices(isChecked);
|
||||
if (isChecked) {
|
||||
updateRestoreSettings({ indices: undefined });
|
||||
} else {
|
||||
updateRestoreSettings({
|
||||
indices: [...(cachedRestoreSettings.indices || [])],
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isAllIndices ? null : (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesLabel"
|
||||
defaultMessage="Select indices"
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesHelpText"
|
||||
defaultMessage="{count} {count, plural, one {index} other {indices}} will be restored. {selectOrDeselectAllLink}"
|
||||
values={{
|
||||
count: restoreIndices && restoreIndices.length,
|
||||
selectOrDeselectAllLink:
|
||||
restoreIndices && restoreIndices.length > 0 ? (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
indicesOptions.forEach((option: Option) => {
|
||||
option.checked = undefined;
|
||||
});
|
||||
updateRestoreSettings({ indices: [] });
|
||||
setCachedRestoreSettings({
|
||||
...cachedRestoreSettings,
|
||||
indices: [],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink"
|
||||
defaultMessage="Deselect all"
|
||||
/>
|
||||
</EuiLink>
|
||||
) : (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
indicesOptions.forEach((option: Option) => {
|
||||
option.checked = 'on';
|
||||
});
|
||||
updateRestoreSettings({ indices: [...snapshotIndices] });
|
||||
setCachedRestoreSettings({
|
||||
...cachedRestoreSettings,
|
||||
indices: [...snapshotIndices],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink"
|
||||
defaultMessage="Select all"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isInvalid={Boolean(errors.indices)}
|
||||
error={errors.indices}
|
||||
>
|
||||
<EuiSelectable
|
||||
allowExclusions={false}
|
||||
options={indicesOptions}
|
||||
onChange={options => {
|
||||
const newSelectedIndices: string[] = [];
|
||||
options.forEach(({ label, checked }) => {
|
||||
if (checked === 'on') {
|
||||
newSelectedIndices.push(label);
|
||||
}
|
||||
});
|
||||
setIndicesOptions(options);
|
||||
updateRestoreSettings({ indices: [...newSelectedIndices] });
|
||||
setCachedRestoreSettings({
|
||||
...cachedRestoreSettings,
|
||||
indices: [...newSelectedIndices],
|
||||
});
|
||||
}}
|
||||
searchable
|
||||
height={300}
|
||||
>
|
||||
{(list, search) => (
|
||||
<EuiPanel paddingSize="s" hasShadow={false}>
|
||||
{search}
|
||||
{list}
|
||||
</EuiPanel>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Rename indices */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesTitle"
|
||||
defaultMessage="Rename indices"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesDescription"
|
||||
defaultMessage="Renames indices on restore."
|
||||
/>
|
||||
}
|
||||
idAria="stepLogisticsRenameIndicesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace
|
||||
fullWidth
|
||||
describedByIds={['stepLogisticsRenameIndicesDescription']}
|
||||
>
|
||||
{/* Fragment needed because EuiFormRow can only have one child: https://github.com/elastic/eui/issues/1931 */}
|
||||
<Fragment>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesLabel"
|
||||
defaultMessage="Rename indices"
|
||||
/>
|
||||
}
|
||||
checked={isRenamingIndices}
|
||||
onChange={e => {
|
||||
const isChecked = e.target.checked;
|
||||
setIsRenamingIndices(isChecked);
|
||||
if (isChecked) {
|
||||
updateRestoreSettings({
|
||||
renamePattern: cachedRestoreSettings.renamePattern,
|
||||
renameReplacement: cachedRestoreSettings.renameReplacement,
|
||||
});
|
||||
} else {
|
||||
updateRestoreSettings({
|
||||
renamePattern: undefined,
|
||||
renameReplacement: undefined,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{!isRenamingIndices ? null : (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternLabel"
|
||||
defaultMessage="Capture pattern"
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternHelpText"
|
||||
defaultMessage="Use regular expressions"
|
||||
/>
|
||||
}
|
||||
isInvalid={Boolean(errors.renamePattern)}
|
||||
error={errors.renamePattern}
|
||||
>
|
||||
<EuiFieldText
|
||||
value={renamePattern}
|
||||
placeholder="index_(.+)"
|
||||
onChange={e => {
|
||||
setCachedRestoreSettings({
|
||||
...cachedRestoreSettings,
|
||||
renamePattern: e.target.value,
|
||||
});
|
||||
updateRestoreSettings({
|
||||
renamePattern: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.renameReplacementLabel"
|
||||
defaultMessage="Replacement pattern"
|
||||
/>
|
||||
}
|
||||
isInvalid={Boolean(errors.renameReplacement)}
|
||||
error={errors.renameReplacement}
|
||||
>
|
||||
<EuiFieldText
|
||||
value={renameReplacement}
|
||||
placeholder="restored_index_$1"
|
||||
onChange={e => {
|
||||
setCachedRestoreSettings({
|
||||
...cachedRestoreSettings,
|
||||
renameReplacement: e.target.value,
|
||||
});
|
||||
updateRestoreSettings({
|
||||
renameReplacement: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Partial restore */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.partialTitle"
|
||||
defaultMessage="Partial restore"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.partialDescription"
|
||||
defaultMessage="Allows restore of indices that don’t have snapshots of all shards."
|
||||
/>
|
||||
}
|
||||
idAria="stepLogisticsPartialDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['stepLogisticsPartialDescription']}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.partialLabel"
|
||||
defaultMessage="Partial restore"
|
||||
/>
|
||||
}
|
||||
checked={partial === undefined ? false : partial}
|
||||
onChange={e => updateRestoreSettings({ partial: e.target.checked })}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Include global state */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle"
|
||||
defaultMessage="Restore global state"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription"
|
||||
defaultMessage="Restores templates that don’t currently exist in the cluster and overrides
|
||||
templates with the same name. Also restores persistent settings."
|
||||
/>
|
||||
}
|
||||
idAria="stepLogisticsIncludeGlobalStateDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['stepLogisticsIncludeGlobalStateDescription']}
|
||||
helpText={
|
||||
snapshotIncludeGlobalState ? null : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription"
|
||||
defaultMessage="Not available for this snapshot."
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel"
|
||||
defaultMessage="Restore global state"
|
||||
/>
|
||||
}
|
||||
checked={includeGlobalState === undefined ? false : includeGlobalState}
|
||||
onChange={e => updateRestoreSettings({ includeGlobalState: e.target.checked })}
|
||||
disabled={!snapshotIncludeGlobalState}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,374 @@
|
|||
/*
|
||||
* 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, { useState, Fragment } from 'react';
|
||||
import {
|
||||
EuiCodeEditor,
|
||||
EuiFlexGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiSpacer,
|
||||
EuiTabbedContent,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiLink,
|
||||
EuiIcon,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { serializeRestoreSettings } from '../../../../../common/lib';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { StepProps } from './';
|
||||
|
||||
export const RestoreSnapshotStepReview: React.FunctionComponent<StepProps> = ({
|
||||
restoreSettings,
|
||||
updateCurrentStep,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const {
|
||||
indices,
|
||||
renamePattern,
|
||||
renameReplacement,
|
||||
partial,
|
||||
includeGlobalState,
|
||||
ignoreIndexSettings,
|
||||
} = restoreSettings;
|
||||
|
||||
const serializedRestoreSettings = serializeRestoreSettings(restoreSettings);
|
||||
const { index_settings: serializedIndexSettings } = serializedRestoreSettings;
|
||||
|
||||
const [isShowingFullIndicesList, setIsShowingFullIndicesList] = useState<boolean>(false);
|
||||
const hiddenIndicesCount = indices && indices.length > 10 ? indices.length - 10 : 0;
|
||||
|
||||
const renderSummaryTab = () => (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.sectionLogisticsTitle"
|
||||
defaultMessage="Logistics"
|
||||
/>{' '}
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.editStepTooltip"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiLink onClick={() => updateCurrentStep(1)}>
|
||||
<EuiIcon type="pencil" />
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indicesLabel"
|
||||
defaultMessage="Indices"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{indices ? (
|
||||
<EuiText>
|
||||
<ul>
|
||||
{(isShowingFullIndicesList ? indices : [...indices].splice(0, 10)).map(
|
||||
index => (
|
||||
<li key={index}>
|
||||
<EuiTitle size="xs">
|
||||
<span>{index}</span>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
{hiddenIndicesCount ? (
|
||||
<li key="hiddenIndicesCount">
|
||||
<EuiTitle size="xs">
|
||||
{isShowingFullIndicesList ? (
|
||||
<EuiLink onClick={() => setIsShowingFullIndicesList(false)}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indicesCollapseAllLink"
|
||||
defaultMessage="Hide {count, plural, one {# index} other {# indices}}"
|
||||
values={{ count: hiddenIndicesCount }}
|
||||
/>{' '}
|
||||
<EuiIcon type="arrowUp" />
|
||||
</EuiLink>
|
||||
) : (
|
||||
<EuiLink onClick={() => setIsShowingFullIndicesList(true)}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indicesShowAllLink"
|
||||
defaultMessage="Show {count} more {count, plural, one {index} other {indices}}"
|
||||
values={{ count: hiddenIndicesCount }}
|
||||
/>{' '}
|
||||
<EuiIcon type="arrowDown" />
|
||||
</EuiLink>
|
||||
)}
|
||||
</EuiTitle>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</EuiText>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.allIndicesValue"
|
||||
defaultMessage="All indices"
|
||||
/>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{renamePattern || renameReplacement ? (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="xs">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.sectionRenameTitle"
|
||||
defaultMessage="Rename indices"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup>
|
||||
{renamePattern ? (
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.renamePatternLabel"
|
||||
defaultMessage="Capture pattern"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{renamePattern}</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
{renameReplacement ? (
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.renameReplacementLabel"
|
||||
defaultMessage="Replacement pattern"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{renameReplacement}</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
) : null}
|
||||
|
||||
{partial !== undefined || includeGlobalState !== undefined ? (
|
||||
<EuiFlexGroup>
|
||||
{partial !== undefined ? (
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialLabel"
|
||||
defaultMessage="Partial restore"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{partial ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialTrueValue"
|
||||
defaultMessage="Yes"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialFalseValue"
|
||||
defaultMessage="No"
|
||||
/>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
{includeGlobalState !== undefined ? (
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateLabel"
|
||||
defaultMessage="Restore global state"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{includeGlobalState ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateTrueValue"
|
||||
defaultMessage="Yes"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateFalseValue"
|
||||
defaultMessage="No"
|
||||
/>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.sectionSettingsTitle"
|
||||
defaultMessage="Index settings"
|
||||
/>{' '}
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.editStepTooltip"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiLink onClick={() => updateCurrentStep(2)}>
|
||||
<EuiIcon type="pencil" />
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{serializedIndexSettings || ignoreIndexSettings ? (
|
||||
<EuiFlexGroup>
|
||||
{serializedIndexSettings ? (
|
||||
<EuiFlexItem style={{ maxWidth: '50%' }}>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indexSettingsLabel"
|
||||
defaultMessage="Modify"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<EuiFlexGrid columns={2} gutterSize="none">
|
||||
{Object.entries(serializedIndexSettings).map(([setting, value]) => (
|
||||
<Fragment key={setting}>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<strong>{setting}</strong>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<span> {value}</span>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
))}
|
||||
</EuiFlexGrid>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
{ignoreIndexSettings ? (
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.ignoreIndexSettingsLabel"
|
||||
defaultMessage="Reset"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<EuiText>
|
||||
<ul>
|
||||
{ignoreIndexSettings.map(setting => (
|
||||
<li key={setting}>
|
||||
<EuiTitle size="xs">
|
||||
<span>{setting}</span>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</EuiText>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.noSettingsValue"
|
||||
defaultMessage="No index setting modifications"
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const renderJsonTab = () => (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCodeEditor
|
||||
mode="json"
|
||||
theme="textmate"
|
||||
isReadOnly
|
||||
setOptions={{ maxLines: Infinity }}
|
||||
value={JSON.stringify(serializedRestoreSettings, null, 2)}
|
||||
editorProps={{ $blockScrolling: Infinity }}
|
||||
aria-label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel"
|
||||
defaultMessage="Restore settings to be executed"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepReviewTitle"
|
||||
defaultMessage="Review restore details"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTabbedContent
|
||||
tabs={[
|
||||
{
|
||||
id: 'summary',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreForm.stepReview.summaryTabTitle', {
|
||||
defaultMessage: 'Summary',
|
||||
}),
|
||||
content: renderSummaryTab(),
|
||||
},
|
||||
{
|
||||
id: 'json',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreForm.stepReview.jsonTabTitle', {
|
||||
defaultMessage: 'JSON',
|
||||
}),
|
||||
content: renderJsonTab(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,330 @@
|
|||
/*
|
||||
* 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, { useState, Fragment } from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiCode,
|
||||
EuiCodeEditor,
|
||||
EuiComboBox,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { RestoreSettings } from '../../../../../common/types';
|
||||
import { REMOVE_INDEX_SETTINGS_SUGGESTIONS } from '../../../constants';
|
||||
import { documentationLinksService } from '../../../services/documentation';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { StepProps } from './';
|
||||
|
||||
export const RestoreSnapshotStepSettings: React.FunctionComponent<StepProps> = ({
|
||||
restoreSettings,
|
||||
updateRestoreSettings,
|
||||
errors,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const { indexSettings, ignoreIndexSettings } = restoreSettings;
|
||||
|
||||
// State for index setting toggles
|
||||
const [isUsingIndexSettings, setIsUsingIndexSettings] = useState<boolean>(Boolean(indexSettings));
|
||||
const [isUsingIgnoreIndexSettings, setIsUsingIgnoreIndexSettings] = useState<boolean>(
|
||||
Boolean(ignoreIndexSettings)
|
||||
);
|
||||
|
||||
// Caching state for togglable settings
|
||||
const [cachedRestoreSettings, setCachedRestoreSettings] = useState<RestoreSettings>({
|
||||
indexSettings: indexSettings || '{}',
|
||||
ignoreIndexSettings: ignoreIndexSettings ? [...ignoreIndexSettings] : [],
|
||||
});
|
||||
|
||||
// List of settings for ignore settings combobox suggestions, using a state because users can add custom settings
|
||||
const [ignoreIndexSettingsOptions, setIgnoreIndexSettingsOptions] = useState<
|
||||
Array<{ label: string }>
|
||||
>(
|
||||
[
|
||||
...new Set((ignoreIndexSettings || []).concat([...REMOVE_INDEX_SETTINGS_SUGGESTIONS].sort())),
|
||||
].map(setting => ({
|
||||
label: setting,
|
||||
}))
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="snapshotRestore__restoreForm__stepSettings">
|
||||
{/* Step title and doc link */}
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepSettingsTitle"
|
||||
defaultMessage="Index settings"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
flush="right"
|
||||
href={documentationLinksService.getRestoreIndexSettingsUrl()}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepSettings.docsButtonLabel"
|
||||
defaultMessage="Index settings docs"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
{/* Modify index settings */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepSettings.indexSettingsTitle"
|
||||
defaultMessage="Modify index settings"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepSettings.indexSettingsDescription"
|
||||
defaultMessage="Overrides index settings during restore."
|
||||
/>
|
||||
}
|
||||
idAria="stepSettingsIndexSettingsDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace
|
||||
fullWidth
|
||||
describedByIds={['stepSettingsIndexSettingsDescription']}
|
||||
>
|
||||
{/* Fragment needed because EuiFormRow can only have one child: https://github.com/elastic/eui/issues/1931 */}
|
||||
<Fragment>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepSettings.indexSettingsLabel"
|
||||
defaultMessage="Modify index settings"
|
||||
/>
|
||||
}
|
||||
checked={isUsingIndexSettings}
|
||||
onChange={e => {
|
||||
const isChecked = e.target.checked;
|
||||
if (isChecked) {
|
||||
setIsUsingIndexSettings(true);
|
||||
updateRestoreSettings({
|
||||
indexSettings: cachedRestoreSettings.indexSettings,
|
||||
});
|
||||
} else {
|
||||
setIsUsingIndexSettings(false);
|
||||
updateRestoreSettings({ indexSettings: undefined });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{!isUsingIndexSettings ? null : (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepSettings.indexSettingsEditorLabel"
|
||||
defaultMessage="Index settings"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['stepSettingsIndexSettingsDescription']}
|
||||
isInvalid={Boolean(errors.indexSettings)}
|
||||
error={errors.indexSettings}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepSettings.indexSettingsEditorDescription"
|
||||
defaultMessage="Use JSON format: {format}"
|
||||
values={{
|
||||
format: <EuiCode>{'{ "index.number_of_replicas": 0 }'}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiCodeEditor
|
||||
mode="json"
|
||||
theme="textmate"
|
||||
width="100%"
|
||||
value={indexSettings}
|
||||
setOptions={{
|
||||
showLineNumbers: false,
|
||||
tabSize: 2,
|
||||
maxLines: Infinity,
|
||||
}}
|
||||
editorProps={{
|
||||
$blockScrolling: Infinity,
|
||||
}}
|
||||
showGutter={false}
|
||||
minLines={6}
|
||||
maxLines={15}
|
||||
aria-label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepSettings.indexSettingsAriaLabel"
|
||||
defaultMessage="Index settings to modify"
|
||||
/>
|
||||
}
|
||||
onChange={(value: string) => {
|
||||
updateRestoreSettings({
|
||||
indexSettings: value,
|
||||
});
|
||||
setCachedRestoreSettings({
|
||||
...cachedRestoreSettings,
|
||||
indexSettings: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Ignore index settings */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepSettings.ignoreIndexSettingsTitle"
|
||||
defaultMessage="Reset index settings"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepSettings.ignoreIndexSettingsDescription"
|
||||
defaultMessage="Resets selected settings to default during restore. "
|
||||
/>
|
||||
}
|
||||
idAria="stepSettingsIgnoreIndexSettingsDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace
|
||||
fullWidth
|
||||
describedByIds={['stepSettingsIgnoreIndexSettingsDescription']}
|
||||
>
|
||||
{/* Fragment needed because EuiFormRow can only have one child: https://github.com/elastic/eui/issues/1931 */}
|
||||
<Fragment>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepSettings.ignoreIndexSettingsLabel"
|
||||
defaultMessage="Reset index settings"
|
||||
/>
|
||||
}
|
||||
checked={isUsingIgnoreIndexSettings}
|
||||
onChange={e => {
|
||||
const isChecked = e.target.checked;
|
||||
if (isChecked) {
|
||||
setIsUsingIgnoreIndexSettings(true);
|
||||
updateRestoreSettings({
|
||||
ignoreIndexSettings: [...(cachedRestoreSettings.ignoreIndexSettings || [])],
|
||||
});
|
||||
} else {
|
||||
setIsUsingIgnoreIndexSettings(false);
|
||||
updateRestoreSettings({ ignoreIndexSettings: undefined });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{!isUsingIgnoreIndexSettings ? null : (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepSettings.selectIgnoreIndexSettingsLabel"
|
||||
defaultMessage="Select settings"
|
||||
/>
|
||||
}
|
||||
isInvalid={Boolean(errors.ignoreIndexSettings)}
|
||||
error={errors.ignoreIndexSettings}
|
||||
>
|
||||
<EuiComboBox
|
||||
placeholder={i18n.translate(
|
||||
'xpack.snapshotRestore.restoreForm.stepSettings.ignoreIndexSettingsPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select or type index settings',
|
||||
}
|
||||
)}
|
||||
options={ignoreIndexSettingsOptions}
|
||||
selectedOptions={
|
||||
ignoreIndexSettings
|
||||
? ignoreIndexSettingsOptions.filter(({ label }) =>
|
||||
ignoreIndexSettings.includes(label)
|
||||
)
|
||||
: []
|
||||
}
|
||||
onChange={selectedOptions => {
|
||||
const newIgnoreIndexSettings = selectedOptions.map(({ label }) => label);
|
||||
updateRestoreSettings({ ignoreIndexSettings: newIgnoreIndexSettings });
|
||||
setCachedRestoreSettings({
|
||||
...cachedRestoreSettings,
|
||||
ignoreIndexSettings: newIgnoreIndexSettings,
|
||||
});
|
||||
}}
|
||||
onCreateOption={(
|
||||
newIndexSetting: string,
|
||||
flattenedOptions: Array<{ label: string }>
|
||||
) => {
|
||||
const normalizedSettingName = newIndexSetting.trim().toLowerCase();
|
||||
if (!normalizedSettingName) {
|
||||
return;
|
||||
}
|
||||
const isCustomSetting = !Boolean(
|
||||
flattenedOptions.find(({ label }) => label === normalizedSettingName)
|
||||
);
|
||||
if (isCustomSetting) {
|
||||
setIgnoreIndexSettingsOptions([
|
||||
{ label: normalizedSettingName },
|
||||
...ignoreIndexSettingsOptions,
|
||||
]);
|
||||
}
|
||||
updateRestoreSettings({
|
||||
ignoreIndexSettings: [
|
||||
...(ignoreIndexSettings || []),
|
||||
normalizedSettingName,
|
||||
],
|
||||
});
|
||||
setCachedRestoreSettings({
|
||||
...cachedRestoreSettings,
|
||||
ignoreIndexSettings: [
|
||||
...(ignoreIndexSettings || []),
|
||||
normalizedSettingName,
|
||||
],
|
||||
});
|
||||
}}
|
||||
isClearable={true}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* 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, { Fragment, useRef, useState } from 'react';
|
||||
import {
|
||||
EuiConfirmModal,
|
||||
EuiOverlayMask,
|
||||
EuiCallOut,
|
||||
EuiLoadingSpinner,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { useAppDependencies } from '../index';
|
||||
import { deleteSnapshots } from '../services/http';
|
||||
|
||||
interface Props {
|
||||
children: (deleteSnapshot: DeleteSnapshot) => React.ReactElement;
|
||||
}
|
||||
|
||||
export type DeleteSnapshot = (
|
||||
ids: Array<{ snapshot: string; repository: string }>,
|
||||
onSuccess?: OnSuccessCallback
|
||||
) => void;
|
||||
|
||||
type OnSuccessCallback = (
|
||||
snapshotsDeleted: Array<{ snapshot: string; repository: string }>
|
||||
) => void;
|
||||
|
||||
export const SnapshotDeleteProvider: React.FunctionComponent<Props> = ({ children }) => {
|
||||
const {
|
||||
core: {
|
||||
i18n,
|
||||
notification: { toastNotifications },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const [snapshotIds, setSnapshotIds] = useState<Array<{ snapshot: string; repository: string }>>(
|
||||
[]
|
||||
);
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||
const onSuccessCallback = useRef<OnSuccessCallback | null>(null);
|
||||
|
||||
const deleteSnapshotPrompt: DeleteSnapshot = (ids, onSuccess = () => undefined) => {
|
||||
if (!ids || !ids.length) {
|
||||
throw new Error('No snapshot IDs specified for deletion');
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
setSnapshotIds(ids);
|
||||
onSuccessCallback.current = onSuccess;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setSnapshotIds([]);
|
||||
};
|
||||
|
||||
const deleteSnapshot = () => {
|
||||
const snapshotsToDelete = [...snapshotIds];
|
||||
setIsDeleting(true);
|
||||
deleteSnapshots(snapshotsToDelete).then(({ data: { itemsDeleted, errors }, error }) => {
|
||||
// Wait until request is done to close modal; deleting snapshots take longer due to their sequential nature
|
||||
closeModal();
|
||||
setIsDeleting(false);
|
||||
|
||||
// Surface success notifications
|
||||
if (itemsDeleted && itemsDeleted.length) {
|
||||
const hasMultipleSuccesses = itemsDeleted.length > 1;
|
||||
const successMessage = hasMultipleSuccesses
|
||||
? i18n.translate(
|
||||
'xpack.snapshotRestore.deleteSnapshot.successMultipleNotificationTitle',
|
||||
{
|
||||
defaultMessage: 'Deleted {count} snapshots',
|
||||
values: { count: itemsDeleted.length },
|
||||
}
|
||||
)
|
||||
: i18n.translate('xpack.snapshotRestore.deleteSnapshot.successSingleNotificationTitle', {
|
||||
defaultMessage: "Deleted snapshot '{name}'",
|
||||
values: { name: itemsDeleted[0].snapshot },
|
||||
});
|
||||
toastNotifications.addSuccess(successMessage);
|
||||
if (onSuccessCallback.current) {
|
||||
onSuccessCallback.current([...itemsDeleted]);
|
||||
}
|
||||
}
|
||||
|
||||
// Surface error notifications
|
||||
// `error` is generic server error
|
||||
// `data.errors` are specific errors with removing particular snapshot(s)
|
||||
if (error || (errors && errors.length)) {
|
||||
const hasMultipleErrors =
|
||||
(errors && errors.length > 1) || (error && snapshotsToDelete.length > 1);
|
||||
const errorMessage = hasMultipleErrors
|
||||
? i18n.translate('xpack.snapshotRestore.deleteSnapshot.errorMultipleNotificationTitle', {
|
||||
defaultMessage: 'Error deleting {count} snapshots',
|
||||
values: {
|
||||
count: (errors && errors.length) || snapshotsToDelete.length,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.snapshotRestore.deleteSnapshot.errorSingleNotificationTitle', {
|
||||
defaultMessage: "Error deleting snapshot '{name}'",
|
||||
values: { name: (errors && errors[0].id.snapshot) || snapshotsToDelete[0].snapshot },
|
||||
});
|
||||
toastNotifications.addDanger(errorMessage);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renderModal = () => {
|
||||
if (!isModalOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isSingle = snapshotIds.length === 1;
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={
|
||||
isSingle ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteSnapshot.confirmModal.deleteSingleTitle"
|
||||
defaultMessage="Delete snapshot '{name}'?"
|
||||
values={{ name: snapshotIds[0].snapshot }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteSnapshot.confirmModal.deleteMultipleTitle"
|
||||
defaultMessage="Delete {count} snapshots?"
|
||||
values={{ count: snapshotIds.length }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onCancel={closeModal}
|
||||
onConfirm={deleteSnapshot}
|
||||
cancelButtonText={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteSnapshot.confirmModal.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
}
|
||||
confirmButtonText={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteSnapshot.confirmModal.confirmButtonLabel"
|
||||
defaultMessage="Delete {count, plural, one {snapshot} other {snapshots}}"
|
||||
values={{ count: snapshotIds.length }}
|
||||
/>
|
||||
}
|
||||
confirmButtonDisabled={isDeleting}
|
||||
buttonColor="danger"
|
||||
data-test-subj="srdeleteSnapshotConfirmationModal"
|
||||
>
|
||||
{!isSingle ? (
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteSnapshot.confirmModal.deleteMultipleListDescription"
|
||||
defaultMessage="You are about to delete these snapshots:"
|
||||
/>
|
||||
</p>
|
||||
<ul>
|
||||
{snapshotIds.map(({ snapshot, repository }) => (
|
||||
<li key={`${repository}/${snapshot}`}>{snapshot}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Fragment>
|
||||
) : null}
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteSnapshot.confirmModal.deleteMultipleDescription"
|
||||
defaultMessage="Restore operations associated with {count, plural, one {this snapshot} other {these snapshots}} will stop."
|
||||
values={{ count: snapshotIds.length }}
|
||||
/>
|
||||
</p>
|
||||
{!isSingle && isDeleting ? (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
title={
|
||||
<Fragment>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteSnapshot.confirmModal.deletingCalloutTitle"
|
||||
defaultMessage="Deleting snapshots"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteSnapshot.confirmModal.deletingCalloutDescription"
|
||||
defaultMessage="This may take a few minutes."
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</Fragment>
|
||||
) : null}
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{children(deleteSnapshotPrompt)}
|
||||
{renderModal()}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
export const BASE_PATH = '/management/elasticsearch/snapshot_restore';
|
||||
export const DEFAULT_SECTION: Section = 'repositories';
|
||||
export type Section = 'repositories' | 'snapshots';
|
||||
export const DEFAULT_SECTION: Section = 'snapshots';
|
||||
export type Section = 'repositories' | 'snapshots' | 'restore_status';
|
||||
|
||||
// Set a minimum request duration to avoid strange UI flickers
|
||||
export const MINIMUM_TIMEOUT_MS = 300;
|
||||
|
@ -31,6 +31,61 @@ export enum SNAPSHOT_STATE {
|
|||
INCOMPATIBLE = 'INCOMPATIBLE',
|
||||
}
|
||||
|
||||
const INDEX_SETTING_SUGGESTIONS: string[] = [
|
||||
'index.number_of_shards',
|
||||
'index.shard.check_on_startup',
|
||||
'index.codec',
|
||||
'index.routing_partition_size',
|
||||
'index.load_fixed_bitset_filters_eagerly',
|
||||
'index.number_of_replicas',
|
||||
'index.auto_expand_replicas',
|
||||
'index.search.idle.after',
|
||||
'index.refresh_interval',
|
||||
'index.max_result_window',
|
||||
'index.max_inner_result_window',
|
||||
'index.max_rescore_window',
|
||||
'index.max_docvalue_fields_search',
|
||||
'index.max_script_fields',
|
||||
'index.max_ngram_diff',
|
||||
'index.max_shingle_diff',
|
||||
'index.blocks.read_only',
|
||||
'index.blocks.read_only_allow_delete',
|
||||
'index.blocks.read',
|
||||
'index.blocks.write',
|
||||
'index.blocks.metadata',
|
||||
'index.max_refresh_listeners',
|
||||
'index.analyze.max_token_count',
|
||||
'index.highlight.max_analyzed_offset',
|
||||
'index.max_terms_count',
|
||||
'index.max_regex_length',
|
||||
'index.routing.allocation.enable',
|
||||
'index.routing.rebalance.enable',
|
||||
'index.gc_deletes',
|
||||
'index.default_pipeline',
|
||||
];
|
||||
|
||||
export const UNMODIFIABLE_INDEX_SETTINGS: string[] = [
|
||||
'index.number_of_shards',
|
||||
'index.version.created',
|
||||
'index.uuid',
|
||||
'index.creation_date',
|
||||
];
|
||||
|
||||
export const UNREMOVABLE_INDEX_SETTINGS: string[] = [
|
||||
...UNMODIFIABLE_INDEX_SETTINGS,
|
||||
'index.number_of_replicas',
|
||||
'index.auto_expand_replicas',
|
||||
'index.version.upgraded',
|
||||
];
|
||||
|
||||
export const MODIFY_INDEX_SETTINGS_SUGGESTIONS: string[] = INDEX_SETTING_SUGGESTIONS.filter(
|
||||
setting => !UNMODIFIABLE_INDEX_SETTINGS.includes(setting)
|
||||
);
|
||||
|
||||
export const REMOVE_INDEX_SETTINGS_SUGGESTIONS: string[] = INDEX_SETTING_SUGGESTIONS.filter(
|
||||
setting => !UNREMOVABLE_INDEX_SETTINGS.includes(setting)
|
||||
);
|
||||
|
||||
// UI Metric constants
|
||||
export const UIM_APP_NAME = 'snapshot_restore';
|
||||
export const UIM_REPOSITORY_LIST_LOAD = 'repository_list_load';
|
||||
|
@ -45,3 +100,8 @@ export const UIM_SNAPSHOT_SHOW_DETAILS_CLICK = 'snapshot_show_details_click';
|
|||
export const UIM_SNAPSHOT_DETAIL_PANEL_SUMMARY_TAB = 'snapshot_detail_panel_summary_tab';
|
||||
export const UIM_SNAPSHOT_DETAIL_PANEL_FAILED_INDICES_TAB =
|
||||
'snapshot_detail_panel_failed_indices_tab';
|
||||
export const UIM_SNAPSHOT_DELETE = 'snapshot_delete';
|
||||
export const UIM_SNAPSHOT_DELETE_MANY = 'snapshot_delete_many';
|
||||
export const UIM_RESTORE_CREATE = 'restore_create';
|
||||
export const UIM_RESTORE_LIST_LOAD = 'restore_list_load';
|
||||
export const UIM_RESTORE_LIST_EXPAND_INDEX = 'restore_list_expand_index';
|
||||
|
|
14
x-pack/legacy/plugins/snapshot_restore/public/app/index.scss
Normal file
14
x-pack/legacy/plugins/snapshot_restore/public/app/index.scss
Normal file
|
@ -0,0 +1,14 @@
|
|||
// Import the EUI global scope so we can use EUI constants
|
||||
@import 'src/legacy/ui/public/styles/_styling_constants';
|
||||
|
||||
// Snapshot and Restore plugin styles
|
||||
|
||||
// Prefix all styles with "snapshotRestore" to avoid conflicts.
|
||||
// Examples
|
||||
// snapshotRestore
|
||||
// snapshotRestore__legend
|
||||
// snapshotRestore__legend--small
|
||||
// snapshotRestore__legend-isLoading
|
||||
|
||||
@import 'components/restore_snapshot_form/restore_snapshot_form';
|
||||
@import 'sections/home/home';
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 1. Allow child progress bar to expand to container size
|
||||
* 2. Adjust table styling to differentiate from parent table
|
||||
*/
|
||||
.snapshotRestore__shardsTable {
|
||||
.euiToolTipAnchor {
|
||||
width: 100%; /* 1 */
|
||||
}
|
||||
|
||||
margin: $euiSizeS 0 $euiSizeS $euiSizeL; /* 2 */
|
||||
.euiTable {
|
||||
background: none;
|
||||
.euiTableRow:last-child > .euiTableRowCell {
|
||||
border-bottom: none;
|
||||
}
|
||||
.euiTableRow:hover,
|
||||
.euiTableRow:hover > .euiTableRowCell {
|
||||
background: $euiColorLightestShade;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import { breadcrumbService } from '../../services/navigation';
|
|||
|
||||
import { RepositoryList } from './repository_list';
|
||||
import { SnapshotList } from './snapshot_list';
|
||||
import { RestoreList } from './restore_list';
|
||||
import { documentationLinksService } from '../../services/documentation';
|
||||
|
||||
interface MatchParams {
|
||||
|
@ -44,9 +45,12 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const tabs = [
|
||||
const tabs: Array<{
|
||||
id: Section;
|
||||
name: React.ReactNode;
|
||||
}> = [
|
||||
{
|
||||
id: 'snapshots' as Section,
|
||||
id: 'snapshots',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.snapshotsTabTitle"
|
||||
|
@ -55,7 +59,7 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
),
|
||||
},
|
||||
{
|
||||
id: 'repositories' as Section,
|
||||
id: 'repositories',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.repositoriesTabTitle"
|
||||
|
@ -63,6 +67,15 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'restore_status',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.restoreTabTitle"
|
||||
defaultMessage="Restore Status"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const onSectionChange = (newSection: Section) => {
|
||||
|
@ -83,7 +96,7 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
<h1 data-test-subj="appTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.snapshotRestoreTitle"
|
||||
defaultMessage="Snapshot Repositories"
|
||||
defaultMessage="Snapshot and Restore"
|
||||
/>
|
||||
</h1>
|
||||
</EuiFlexItem>
|
||||
|
@ -96,7 +109,7 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.snapshotRestoreDocsLinkText"
|
||||
defaultMessage="Snapshot docs"
|
||||
defaultMessage="Snapshot and Restore docs"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
@ -107,7 +120,7 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.snapshotRestoreDescription"
|
||||
defaultMessage="Use repositories to store backups of your Elasticsearch indices and clusters."
|
||||
defaultMessage="Use repositories to store and recover backups of your Elasticsearch indices and clusters."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiTitle>
|
||||
|
@ -144,6 +157,7 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
path={`${BASE_PATH}/snapshots/:repositoryName*/:snapshotId`}
|
||||
component={SnapshotList}
|
||||
/>
|
||||
<Route exact path={`${BASE_PATH}/restore_status`} component={RestoreList} />
|
||||
</Switch>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
|
@ -28,7 +26,7 @@ import 'brace/theme/textmate';
|
|||
import { useAppDependencies } from '../../../../index';
|
||||
import { documentationLinksService } from '../../../../services/documentation';
|
||||
import {
|
||||
loadRepository,
|
||||
useLoadRepository,
|
||||
verifyRepository as verifyRepositoryRequest,
|
||||
} from '../../../../services/http';
|
||||
import { textService } from '../../../../services/text';
|
||||
|
@ -45,24 +43,23 @@ import {
|
|||
import { BASE_PATH } from '../../../../constants';
|
||||
import { TypeDetails } from './type_details';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
interface Props {
|
||||
repositoryName: Repository['name'];
|
||||
onClose: () => void;
|
||||
onRepositoryDeleted: (repositoriesDeleted: Array<Repository['name']>) => void;
|
||||
}
|
||||
|
||||
const RepositoryDetailsUi: React.FunctionComponent<Props> = ({
|
||||
export const RepositoryDetails: React.FunctionComponent<Props> = ({
|
||||
repositoryName,
|
||||
onClose,
|
||||
onRepositoryDeleted,
|
||||
history,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
|
||||
const { FormattedMessage } = i18n;
|
||||
const { error, data: repositoryDetails } = loadRepository(repositoryName);
|
||||
const { error, data: repositoryDetails } = useLoadRepository(repositoryName);
|
||||
const [verification, setVerification] = useState<RepositoryVerification | undefined>(undefined);
|
||||
const [isLoadingVerification, setIsLoadingVerification] = useState<boolean>(false);
|
||||
|
||||
|
@ -378,9 +375,7 @@ const RepositoryDetailsUi: React.FunctionComponent<Props> = ({
|
|||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
href={history.createHref({
|
||||
pathname: `${BASE_PATH}/edit_repository/${repositoryName}`,
|
||||
})}
|
||||
href={`#${BASE_PATH}/edit_repository/${repositoryName}`}
|
||||
fill
|
||||
color="primary"
|
||||
>
|
||||
|
@ -419,5 +414,3 @@ const RepositoryDetailsUi: React.FunctionComponent<Props> = ({
|
|||
</EuiFlyout>
|
||||
);
|
||||
};
|
||||
|
||||
export const RepositoryDetails = withRouter(RepositoryDetailsUi);
|
||||
|
|
|
@ -162,7 +162,7 @@ export const AzureDetails: React.FunctionComponent<Props> = ({ repository }) =>
|
|||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
<EuiDescriptionList textStyle="reverse" listItems={listItems} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -117,7 +117,7 @@ export const FSDetails: React.FunctionComponent<Props> = ({ repository }) => {
|
|||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
<EuiDescriptionList textStyle="reverse" listItems={listItems} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -143,7 +143,7 @@ export const GCSDetails: React.FunctionComponent<Props> = ({ repository }) => {
|
|||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
<EuiDescriptionList textStyle="reverse" listItems={listItems} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -160,7 +160,7 @@ export const HDFSDetails: React.FunctionComponent<Props> = ({ repository }) => {
|
|||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
<EuiDescriptionList textStyle="reverse" listItems={listItems} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -49,7 +49,7 @@ export const ReadonlyDetails: React.FunctionComponent<Props> = ({ repository })
|
|||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
<EuiDescriptionList textStyle="reverse" listItems={listItems} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -195,7 +195,7 @@ export const S3Details: React.FunctionComponent<Props> = ({ repository }) => {
|
|||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
<EuiDescriptionList textStyle="reverse" listItems={listItems} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Repository } from '../../../../../common/types';
|
|||
import { SectionError, SectionLoading } from '../../../components';
|
||||
import { BASE_PATH, UIM_REPOSITORY_LIST_LOAD } from '../../../constants';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { loadRepositories } from '../../../services/http';
|
||||
import { useLoadRepositories } from '../../../services/http';
|
||||
import { uiMetricService } from '../../../services/ui_metric';
|
||||
|
||||
import { RepositoryDetails } from './repository_details';
|
||||
|
@ -42,7 +42,7 @@ export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
managedRepository: undefined,
|
||||
},
|
||||
request: reload,
|
||||
} = loadRepositories();
|
||||
} = useLoadRepositories();
|
||||
|
||||
const openRepositoryDetailsUrl = (newRepositoryName: Repository['name']): string => {
|
||||
return history.createHref({
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
*/
|
||||
|
||||
import React, { useState, Fragment } from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButton,
|
||||
|
@ -26,7 +24,7 @@ import { useAppDependencies } from '../../../../index';
|
|||
import { textService } from '../../../../services/text';
|
||||
import { uiMetricService } from '../../../../services/ui_metric';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
interface Props {
|
||||
repositories: Repository[];
|
||||
managedRepository?: string;
|
||||
reload: () => Promise<void>;
|
||||
|
@ -34,13 +32,12 @@ interface Props extends RouteComponentProps {
|
|||
onRepositoryDeleted: (repositoriesDeleted: Array<Repository['name']>) => void;
|
||||
}
|
||||
|
||||
const RepositoryTableUi: React.FunctionComponent<Props> = ({
|
||||
export const RepositoryTable: React.FunctionComponent<Props> = ({
|
||||
repositories,
|
||||
managedRepository,
|
||||
reload,
|
||||
openRepositoryDetailsUrl,
|
||||
onRepositoryDeleted,
|
||||
history,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
|
@ -107,7 +104,7 @@ const RepositoryTableUi: React.FunctionComponent<Props> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip content={label} delay="long">
|
||||
<EuiToolTip content={label}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryList.table.actionEditAriaLabel',
|
||||
|
@ -143,7 +140,7 @@ const RepositoryTableUi: React.FunctionComponent<Props> = ({
|
|||
}
|
||||
);
|
||||
return (
|
||||
<EuiToolTip content={label} delay="long">
|
||||
<EuiToolTip content={label}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryList.table.actionRemoveAriaLabel',
|
||||
|
@ -252,9 +249,7 @@ const RepositoryTableUi: React.FunctionComponent<Props> = ({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
href={history.createHref({
|
||||
pathname: `${BASE_PATH}/add_repository`,
|
||||
})}
|
||||
href={`#${BASE_PATH}/add_repository`}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="registerRepositoryButton"
|
||||
|
@ -312,5 +307,3 @@ const RepositoryTableUi: React.FunctionComponent<Props> = ({
|
|||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RepositoryTable = withRouter(RepositoryTableUi);
|
||||
|
|
|
@ -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 { RestoreList } from './restore_list';
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* 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, { useEffect, useState, Fragment } from 'react';
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiPopover,
|
||||
EuiButtonEmpty,
|
||||
EuiContextMenuPanel,
|
||||
EuiContextMenuItem,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import { SectionError, SectionLoading } from '../../../components';
|
||||
import { UIM_RESTORE_LIST_LOAD } from '../../../constants';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { useLoadRestores } from '../../../services/http';
|
||||
import { uiMetricService } from '../../../services/ui_metric';
|
||||
import { RestoreTable } from './restore_table';
|
||||
|
||||
const ONE_SECOND_MS = 1000;
|
||||
const TEN_SECONDS_MS = 10 * 1000;
|
||||
const THIRTY_SECONDS_MS = 30 * 1000;
|
||||
const ONE_MINUTE_MS = 60 * 1000;
|
||||
const FIVE_MINUTES_MS = 5 * 60 * 1000;
|
||||
|
||||
const INTERVAL_OPTIONS: number[] = [
|
||||
TEN_SECONDS_MS,
|
||||
THIRTY_SECONDS_MS,
|
||||
ONE_MINUTE_MS,
|
||||
FIVE_MINUTES_MS,
|
||||
];
|
||||
export const RestoreList: React.FunctionComponent = () => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
// State for tracking interval picker
|
||||
const [isIntervalMenuOpen, setIsIntervalMenuOpen] = useState<boolean>(false);
|
||||
const [currentInterval, setCurrentInterval] = useState<number>(INTERVAL_OPTIONS[1]);
|
||||
|
||||
// Load restores
|
||||
const { error, loading, data: restores = [], polling, changeInterval } = useLoadRestores(
|
||||
currentInterval
|
||||
);
|
||||
|
||||
// Track component loaded
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
useEffect(() => {
|
||||
trackUiMetric(UIM_RESTORE_LIST_LOAD);
|
||||
}, []);
|
||||
|
||||
let content;
|
||||
|
||||
if (loading) {
|
||||
content = (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.loadingRestoresDescription"
|
||||
defaultMessage="Loading restores…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
} else if (error) {
|
||||
content = (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.LoadingRestoresErrorMessage"
|
||||
defaultMessage="Error loading restores"
|
||||
/>
|
||||
}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
} else if (restores && restores.length === 0) {
|
||||
content = (
|
||||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.emptyPromptTitle"
|
||||
defaultMessage="You don't have any snapshot restores"
|
||||
/>
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.emptyPromptDescription"
|
||||
defaultMessage="Track progress of indices that are restored from snapshots."
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
data-test-subj="emptyPrompt"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<Fragment>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexStart" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
id="srRestoreListIntervalMenu"
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
type="text"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={() => setIsIntervalMenuOpen(!isIntervalMenuOpen)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenuButtonText"
|
||||
defaultMessage="Refresh data every {interval}"
|
||||
values={{
|
||||
interval:
|
||||
currentInterval >= ONE_MINUTE_MS ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenu.minutesIntervalValue"
|
||||
defaultMessage="{minutes} {minutes, plural, one {minute} other {minutes}}"
|
||||
values={{ minutes: Math.ceil(currentInterval / ONE_MINUTE_MS) }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenu.secondsIntervalValue"
|
||||
defaultMessage="{seconds} {seconds, plural, one {second} other {seconds}}"
|
||||
values={{ seconds: Math.ceil(currentInterval / ONE_SECOND_MS) }}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
isOpen={isIntervalMenuOpen}
|
||||
closePopover={() => setIsIntervalMenuOpen(false)}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
items={INTERVAL_OPTIONS.map(interval => (
|
||||
<EuiContextMenuItem
|
||||
key={interval}
|
||||
icon="empty"
|
||||
onClick={() => {
|
||||
changeInterval(interval);
|
||||
setCurrentInterval(interval);
|
||||
setIsIntervalMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
{interval >= ONE_MINUTE_MS ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenu.minutesIntervalValue"
|
||||
defaultMessage="{minutes} {minutes, plural, one {minute} other {minutes}}"
|
||||
values={{ minutes: Math.ceil(interval / ONE_MINUTE_MS) }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.intervalMenu.secondsIntervalValue"
|
||||
defaultMessage="{seconds} {seconds, plural, one {second} other {seconds}}"
|
||||
values={{ seconds: Math.ceil(interval / ONE_SECOND_MS) }}
|
||||
/>
|
||||
)}
|
||||
</EuiContextMenuItem>
|
||||
))}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{polling ? <EuiLoadingSpinner size="m" /> : null}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<RestoreTable restores={restores || []} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return <section data-test-subj="restoreList">{content}</section>;
|
||||
};
|
|
@ -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 { RestoreTable } from './restore_table';
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { sortByOrder } from 'lodash';
|
||||
import { EuiBasicTable, EuiButtonIcon, EuiHealth } from '@elastic/eui';
|
||||
import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
|
||||
import { SnapshotRestore } from '../../../../../../common/types';
|
||||
import { UIM_RESTORE_LIST_EXPAND_INDEX } from '../../../../constants';
|
||||
import { useAppDependencies } from '../../../../index';
|
||||
import { uiMetricService } from '../../../../services/ui_metric';
|
||||
import { formatDate } from '../../../../services/text';
|
||||
import { ShardsTable } from './shards_table';
|
||||
|
||||
interface Props {
|
||||
restores: SnapshotRestore[];
|
||||
}
|
||||
|
||||
export const RestoreTable: React.FunctionComponent<Props> = ({ restores }) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
|
||||
// Track restores to show based on sort and pagination state
|
||||
const [currentRestores, setCurrentRestores] = useState<SnapshotRestore[]>([]);
|
||||
|
||||
// Sort state
|
||||
const [sorting, setSorting] = useState<{
|
||||
sort: {
|
||||
field: keyof SnapshotRestore;
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
}>({
|
||||
sort: {
|
||||
field: 'isComplete',
|
||||
direction: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
// Pagination state
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 20,
|
||||
totalItemCount: restores.length,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
});
|
||||
|
||||
// Track expanded indices
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{
|
||||
[key: string]: React.ReactNode;
|
||||
}>({});
|
||||
|
||||
// On sorting and pagination change
|
||||
const onTableChange = ({ page = {}, sort = {} }: any) => {
|
||||
const { index: pageIndex, size: pageSize } = page;
|
||||
const { field: sortField, direction: sortDirection } = sort;
|
||||
setSorting({
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
});
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
});
|
||||
};
|
||||
|
||||
// Expand or collapse index details
|
||||
const toggleIndexRestoreDetails = (restore: SnapshotRestore) => {
|
||||
const { index, shards } = restore;
|
||||
const newItemIdToExpandedRowMap = { ...itemIdToExpandedRowMap };
|
||||
|
||||
if (newItemIdToExpandedRowMap[index]) {
|
||||
delete newItemIdToExpandedRowMap[index];
|
||||
} else {
|
||||
trackUiMetric(UIM_RESTORE_LIST_EXPAND_INDEX);
|
||||
newItemIdToExpandedRowMap[index] = <ShardsTable shards={shards} />;
|
||||
}
|
||||
setItemIdToExpandedRowMap(newItemIdToExpandedRowMap);
|
||||
};
|
||||
|
||||
// Refresh expanded index details
|
||||
const refreshIndexRestoreDetails = () => {
|
||||
const newItemIdToExpandedRowMap: typeof itemIdToExpandedRowMap = {};
|
||||
restores.forEach(restore => {
|
||||
const { index, shards } = restore;
|
||||
if (!itemIdToExpandedRowMap[index]) {
|
||||
return;
|
||||
}
|
||||
newItemIdToExpandedRowMap[index] = <ShardsTable shards={shards} />;
|
||||
setItemIdToExpandedRowMap(newItemIdToExpandedRowMap);
|
||||
});
|
||||
};
|
||||
|
||||
// Get restores to show based on sort and pagination state
|
||||
const getCurrentRestores = (): SnapshotRestore[] => {
|
||||
const newRestoresList = [...restores];
|
||||
const {
|
||||
sort: { field, direction },
|
||||
} = sorting;
|
||||
const { pageIndex, pageSize } = pagination;
|
||||
const sortedRestores = sortByOrder(newRestoresList, [field], [direction]);
|
||||
return sortedRestores.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
|
||||
};
|
||||
|
||||
// Update current restores to show if table changes
|
||||
useEffect(
|
||||
() => {
|
||||
setCurrentRestores(getCurrentRestores());
|
||||
},
|
||||
[sorting, pagination]
|
||||
);
|
||||
|
||||
// Update current restores to show if data changes
|
||||
// as well as any expanded index details
|
||||
useEffect(
|
||||
() => {
|
||||
setPagination({
|
||||
...pagination,
|
||||
totalItemCount: restores.length,
|
||||
});
|
||||
setCurrentRestores(getCurrentRestores());
|
||||
refreshIndexRestoreDetails();
|
||||
},
|
||||
[restores]
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'index',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.table.indexColumnTitle', {
|
||||
defaultMessage: 'Index',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'isComplete',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.table.statusColumnTitle', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
render: (isComplete: SnapshotRestore['isComplete']) =>
|
||||
isComplete ? (
|
||||
<EuiHealth color="success">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.table.statusColumn.completeLabel"
|
||||
defaultMessage="Complete"
|
||||
/>
|
||||
</EuiHealth>
|
||||
) : (
|
||||
<EuiHealth color="warning">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.table.statusColumn.inProgressLabel"
|
||||
defaultMessage="In progress"
|
||||
/>
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'latestActivityTimeInMillis',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.table.lastActivityTitle', {
|
||||
defaultMessage: 'Last activity',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: (
|
||||
latestActivityTimeInMillis: SnapshotRestore['latestActivityTimeInMillis'],
|
||||
{ isComplete }: SnapshotRestore
|
||||
) => {
|
||||
return isComplete ? (
|
||||
formatDate(latestActivityTimeInMillis)
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.table.lastActivityColumn.nowLabel"
|
||||
defaultMessage="now"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'shards',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.table.shardsCompletedTitle', {
|
||||
defaultMessage: 'Shards completed',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: (shards: SnapshotRestore['shards']) => {
|
||||
return shards.filter(shard => Boolean(shard.stopTimeInMillis)).length;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'shards',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.table.shardsInProgressTitle', {
|
||||
defaultMessage: 'Shards in progress',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: (shards: SnapshotRestore['shards']) => {
|
||||
return shards.filter(shard => !Boolean(shard.stopTimeInMillis)).length;
|
||||
},
|
||||
},
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
render: (item: SnapshotRestore) => (
|
||||
<EuiButtonIcon
|
||||
onClick={() => toggleIndexRestoreDetails(item)}
|
||||
aria-label={itemIdToExpandedRowMap[item.index] ? 'Collapse' : 'Expand'}
|
||||
iconType={itemIdToExpandedRowMap[item.index] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
items={currentRestores}
|
||||
itemId="index"
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isExpandable={true}
|
||||
columns={columns}
|
||||
sorting={sorting}
|
||||
pagination={pagination}
|
||||
onChange={onTableChange}
|
||||
rowProps={(restore: SnapshotRestore) => ({
|
||||
'data-test-subj': 'row',
|
||||
onClick: () => toggleIndexRestoreDetails(restore),
|
||||
})}
|
||||
cellProps={(item: any, column: any) => ({
|
||||
'data-test-subj': `cell`,
|
||||
})}
|
||||
data-test-subj="restoresTable"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiProgress,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { SnapshotRestore, SnapshotRestoreShard } from '../../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../../index';
|
||||
import { formatDate } from '../../../../services/text';
|
||||
|
||||
interface Props {
|
||||
shards: SnapshotRestore['shards'];
|
||||
}
|
||||
|
||||
export const ShardsTable: React.FunctionComponent<Props> = ({ shards }) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
|
||||
const Progress = ({
|
||||
total,
|
||||
restored,
|
||||
percent,
|
||||
}: {
|
||||
total: number;
|
||||
restored: number;
|
||||
percent: string;
|
||||
}) => (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={i18n.translate('xpack.snapshotRestore.restoreList.shardTable.progressTooltipLabel', {
|
||||
defaultMessage: '{restored} of {total} restored',
|
||||
values: {
|
||||
restored,
|
||||
total,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<EuiText size="xs" textAlign="center" style={{ width: '100%' }}>
|
||||
<EuiProgress value={total === 0 ? 1 : restored} max={total === 0 ? 1 : total} size="xs" />
|
||||
<EuiSpacer size="xs" />
|
||||
{percent}
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'id',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.shardTable.indexColumnTitle', {
|
||||
defaultMessage: 'ID',
|
||||
}),
|
||||
width: '40px',
|
||||
render: (id: SnapshotRestoreShard['id'], shard: SnapshotRestoreShard) =>
|
||||
shard.primary ? (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>{id}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
position="right"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.shardTable.primaryTooltipLabel"
|
||||
defaultMessage="Primary"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.shardTable.primaryAbbreviationText"
|
||||
defaultMessage="P"
|
||||
/>
|
||||
</strong>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
id
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'stage',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.shardTable.stageColumnTitle', {
|
||||
defaultMessage: 'Stage',
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'startTimeInMillis',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.shardTable.startTimeColumnTitle', {
|
||||
defaultMessage: 'Start time',
|
||||
}),
|
||||
render: (startTimeInMillis: SnapshotRestoreShard['startTimeInMillis']) =>
|
||||
startTimeInMillis ? formatDate(startTimeInMillis) : <EuiLoadingSpinner size="m" />,
|
||||
},
|
||||
{
|
||||
field: 'stopTimeInMillis',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.shardTable.endTimeColumnTitle', {
|
||||
defaultMessage: 'End time',
|
||||
}),
|
||||
render: (stopTimeInMillis: SnapshotRestoreShard['stopTimeInMillis']) =>
|
||||
stopTimeInMillis ? formatDate(stopTimeInMillis) : <EuiLoadingSpinner size="m" />,
|
||||
},
|
||||
{
|
||||
field: 'totalTimeInMillis',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.shardTable.durationColumnTitle', {
|
||||
defaultMessage: 'Duration',
|
||||
}),
|
||||
render: (totalTimeInMillis: SnapshotRestoreShard['totalTimeInMillis']) =>
|
||||
totalTimeInMillis ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreList.shardTable.durationValue"
|
||||
defaultMessage="{seconds} {seconds, plural, one {second} other {seconds}}"
|
||||
values={{ seconds: Math.ceil(totalTimeInMillis / 1000) }}
|
||||
/>
|
||||
) : (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'repository',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.shardTable.repositoryColumnTitle', {
|
||||
defaultMessage: 'Repository',
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'snapshot',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.shardTable.snapshotColumnTitle', {
|
||||
defaultMessage: 'Snapshot',
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'version',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.shardTable.versionColumnTitle', {
|
||||
defaultMessage: 'Version',
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'targetHost',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.shardTable.targetHostColumnTitle', {
|
||||
defaultMessage: 'Target host',
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'targetNode',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.shardTable.targetNodeColumnTitle', {
|
||||
defaultMessage: 'Target node',
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'bytesTotal',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.shardTable.bytesColumnTitle', {
|
||||
defaultMessage: 'Bytes',
|
||||
}),
|
||||
render: (
|
||||
bytesTotal: SnapshotRestoreShard['bytesTotal'],
|
||||
{ bytesRecovered, bytesPercent }: SnapshotRestoreShard
|
||||
) => <Progress total={bytesTotal} restored={bytesRecovered} percent={bytesPercent} />,
|
||||
},
|
||||
{
|
||||
field: 'filesTotal',
|
||||
name: i18n.translate('xpack.snapshotRestore.restoreList.shardTable.filesColumnTitle', {
|
||||
defaultMessage: 'Files',
|
||||
}),
|
||||
render: (
|
||||
filesTotal: SnapshotRestoreShard['filesTotal'],
|
||||
{ filesRecovered, filesPercent }: SnapshotRestoreShard
|
||||
) => <Progress total={filesTotal} restored={filesRecovered} percent={filesPercent} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
className="snapshotRestore__shardsTable"
|
||||
compressed={true}
|
||||
items={shards}
|
||||
columns={columns}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -20,23 +21,25 @@ import {
|
|||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import { SectionError, SectionLoading } from '../../../../components';
|
||||
import { SectionError, SectionLoading, SnapshotDeleteProvider } from '../../../../components';
|
||||
import { useAppDependencies } from '../../../../index';
|
||||
import {
|
||||
BASE_PATH,
|
||||
UIM_SNAPSHOT_DETAIL_PANEL_SUMMARY_TAB,
|
||||
UIM_SNAPSHOT_DETAIL_PANEL_FAILED_INDICES_TAB,
|
||||
SNAPSHOT_STATE,
|
||||
} from '../../../../constants';
|
||||
import { loadSnapshot } from '../../../../services/http';
|
||||
import { useLoadSnapshot } from '../../../../services/http';
|
||||
import { linkToRepository } from '../../../../services/navigation';
|
||||
import { uiMetricService } from '../../../../services/ui_metric';
|
||||
import { TabSummary, TabFailures } from './tabs';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
interface Props {
|
||||
repositoryName: string;
|
||||
snapshotId: string;
|
||||
onClose: () => void;
|
||||
onSnapshotDeleted: (snapshotsDeleted: Array<{ snapshot: string; repository: string }>) => void;
|
||||
}
|
||||
|
||||
const TAB_SUMMARY = 'summary';
|
||||
|
@ -47,17 +50,18 @@ const panelTypeToUiMetricMap: { [key: string]: string } = {
|
|||
[TAB_FAILURES]: UIM_SNAPSHOT_DETAIL_PANEL_FAILED_INDICES_TAB,
|
||||
};
|
||||
|
||||
const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
|
||||
export const SnapshotDetails: React.FunctionComponent<Props> = ({
|
||||
repositoryName,
|
||||
snapshotId,
|
||||
onClose,
|
||||
onSnapshotDeleted,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
const { error, data: snapshotDetails } = loadSnapshot(repositoryName, snapshotId);
|
||||
const { error, data: snapshotDetails } = useLoadSnapshot(repositoryName, snapshotId);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<string>(TAB_SUMMARY);
|
||||
|
||||
|
@ -178,6 +182,53 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
|
|||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
{snapshotDetails ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<SnapshotDeleteProvider>
|
||||
{deleteSnapshotPrompt => {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
data-test-subj="srSnapshotDetailsDeleteActionButton"
|
||||
onClick={() =>
|
||||
deleteSnapshotPrompt(
|
||||
[{ repository: repositoryName, snapshot: snapshotId }],
|
||||
onSnapshotDeleted
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.deleteButtonLabel"
|
||||
defaultMessage="Delete"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}}
|
||||
</SnapshotDeleteProvider>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
href={`#${BASE_PATH}/restore/${repositoryName}/${snapshotId}`}
|
||||
fill
|
||||
color="primary"
|
||||
isDisabled={
|
||||
snapshotDetails.state !== SNAPSHOT_STATE.SUCCESS &&
|
||||
snapshotDetails.state !== SNAPSHOT_STATE.PARTIAL
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.restoreButtonLabel"
|
||||
defaultMessage="Restore"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
@ -223,5 +274,3 @@ const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
|
|||
</EuiFlyout>
|
||||
);
|
||||
};
|
||||
|
||||
export const SnapshotDetails = withRouter(SnapshotDetailsUi);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiDescriptionList,
|
||||
|
@ -12,9 +12,11 @@ import {
|
|||
EuiDescriptionListTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiLoadingSpinner,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { SNAPSHOT_STATE } from '../../../../../constants';
|
||||
|
@ -34,21 +36,6 @@ export const TabSummary: React.SFC<Props> = ({ snapshotDetails }) => {
|
|||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const includeGlobalStateToHumanizedMap: Record<string, any> = {
|
||||
0: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemIncludeGlobalStateNoLabel"
|
||||
defaultMessage="No"
|
||||
/>
|
||||
),
|
||||
1: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemIncludeGlobalStateYesLabel"
|
||||
defaultMessage="Yes"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const {
|
||||
versionId,
|
||||
version,
|
||||
|
@ -63,15 +50,32 @@ export const TabSummary: React.SFC<Props> = ({ snapshotDetails }) => {
|
|||
uuid,
|
||||
} = snapshotDetails;
|
||||
|
||||
const indicesList = indices.length ? (
|
||||
// Only show 10 indices initially
|
||||
const [isShowingFullIndicesList, setIsShowingFullIndicesList] = useState<boolean>(false);
|
||||
const hiddenIndicesCount = indices.length > 10 ? indices.length - 10 : 0;
|
||||
const shortIndicesList = indices.length ? (
|
||||
<ul>
|
||||
{indices.map((index: string) => (
|
||||
{[...indices].splice(0, 10).map((index: string) => (
|
||||
<li key={index}>
|
||||
<EuiTitle size="xs">
|
||||
<span>{index}</span>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
))}
|
||||
{hiddenIndicesCount ? (
|
||||
<li key="hiddenIndicesCount">
|
||||
<EuiTitle size="xs">
|
||||
<EuiLink onClick={() => setIsShowingFullIndicesList(true)}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemIndicesShowAllLink"
|
||||
defaultMessage="Show {count} more {count, plural, one {index} other {indices}}"
|
||||
values={{ count: hiddenIndicesCount }}
|
||||
/>{' '}
|
||||
<EuiIcon type="arrowDown" />
|
||||
</EuiLink>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
|
@ -79,6 +83,32 @@ export const TabSummary: React.SFC<Props> = ({ snapshotDetails }) => {
|
|||
defaultMessage="-"
|
||||
/>
|
||||
);
|
||||
const fullIndicesList =
|
||||
indices.length && indices.length > 10 ? (
|
||||
<ul>
|
||||
{indices.map((index: string) => (
|
||||
<li key={index}>
|
||||
<EuiTitle size="xs">
|
||||
<span>{index}</span>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
))}
|
||||
{hiddenIndicesCount ? (
|
||||
<li key="hiddenIndicesCount">
|
||||
<EuiTitle size="xs">
|
||||
<EuiLink onClick={() => setIsShowingFullIndicesList(false)}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemIndicesCollapseAllLink"
|
||||
defaultMessage="Hide {count, plural, one {# index} other {# indices}}"
|
||||
values={{ count: hiddenIndicesCount }}
|
||||
/>{' '}
|
||||
<EuiIcon type="arrowUp" />
|
||||
</EuiLink>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
|
@ -133,7 +163,17 @@ export const TabSummary: React.SFC<Props> = ({ snapshotDetails }) => {
|
|||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
|
||||
{includeGlobalStateToHumanizedMap[includeGlobalState]}
|
||||
{includeGlobalState ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemIncludeGlobalStateYesLabel"
|
||||
defaultMessage="Yes"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemIncludeGlobalStateNoLabel"
|
||||
defaultMessage="No"
|
||||
/>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -149,7 +189,7 @@ export const TabSummary: React.SFC<Props> = ({ snapshotDetails }) => {
|
|||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
|
||||
<EuiText>{indicesList}</EuiText>
|
||||
<EuiText>{isShowingFullIndicesList ? fullIndicesList : shortIndicesList}</EuiText>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -14,7 +14,7 @@ import { SectionError, SectionLoading } from '../../../components';
|
|||
import { BASE_PATH, UIM_SNAPSHOT_LIST_LOAD } from '../../../constants';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { documentationLinksService } from '../../../services/documentation';
|
||||
import { loadSnapshots } from '../../../services/http';
|
||||
import { useLoadSnapshots } from '../../../services/http';
|
||||
import { linkToRepositories } from '../../../services/navigation';
|
||||
import { uiMetricService } from '../../../services/ui_metric';
|
||||
|
||||
|
@ -44,7 +44,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
loading,
|
||||
data: { snapshots = [], repositories = [], errors = {} },
|
||||
request: reload,
|
||||
} = loadSnapshots();
|
||||
} = useLoadSnapshots();
|
||||
|
||||
const openSnapshotDetailsUrl = (
|
||||
repositoryNameToOpen: string,
|
||||
|
@ -61,6 +61,23 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
history.push(`${BASE_PATH}/snapshots`);
|
||||
};
|
||||
|
||||
const onSnapshotDeleted = (
|
||||
snapshotsDeleted: Array<{ snapshot: string; repository: string }>
|
||||
): void => {
|
||||
if (
|
||||
repositoryName &&
|
||||
snapshotId &&
|
||||
snapshotsDeleted.find(
|
||||
({ snapshot, repository }) => snapshot === snapshotId && repository === repositoryName
|
||||
)
|
||||
) {
|
||||
closeSnapshotDetails();
|
||||
}
|
||||
if (snapshotsDeleted.length) {
|
||||
reload();
|
||||
}
|
||||
};
|
||||
|
||||
// Allow deeplinking to list pre-filtered by repository name
|
||||
const [filteredRepository, setFilteredRepository] = useState<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
|
@ -229,44 +246,46 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
);
|
||||
} else {
|
||||
const repositoryErrorsWarning = Object.keys(errors).length ? (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryWarningTitle"
|
||||
defaultMessage="Some repositories contain errors"
|
||||
/>
|
||||
}
|
||||
color="warning"
|
||||
iconType="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryWarningTitle"
|
||||
defaultMessage="Some repositories contain errors"
|
||||
id="xpack.snapshotRestore.repositoryWarningDescription"
|
||||
defaultMessage="Snapshots might load slowly. Go to {repositoryLink} to fix the errors."
|
||||
values={{
|
||||
repositoryLink: (
|
||||
<EuiLink href={linkToRepositories()}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryWarningLinkText"
|
||||
defaultMessage="Repositories"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
color="warning"
|
||||
iconType="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryWarningDescription"
|
||||
defaultMessage="Snapshots might load slowly. Go to {repositoryLink} to fix the errors."
|
||||
values={{
|
||||
repositoryLink: (
|
||||
<EuiLink href={linkToRepositories()}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryWarningLinkText"
|
||||
defaultMessage="Repositories"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
) : null;
|
||||
|
||||
content = (
|
||||
<Fragment>
|
||||
{repositoryErrorsWarning}
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<SnapshotTable
|
||||
snapshots={snapshots}
|
||||
repositories={repositories}
|
||||
reload={reload}
|
||||
openSnapshotDetailsUrl={openSnapshotDetailsUrl}
|
||||
onSnapshotDeleted={onSnapshotDeleted}
|
||||
repositoryFilter={filteredRepository}
|
||||
/>
|
||||
</Fragment>
|
||||
|
@ -280,6 +299,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
repositoryName={repositoryName}
|
||||
snapshotId={snapshotId}
|
||||
onClose={closeSnapshotDetails}
|
||||
onSnapshotDeleted={onSnapshotDeleted}
|
||||
/>
|
||||
) : null}
|
||||
{content}
|
||||
|
|
|
@ -4,16 +4,24 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButton, EuiInMemoryTable, EuiLink, Query, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiInMemoryTable,
|
||||
EuiLink,
|
||||
Query,
|
||||
EuiLoadingSpinner,
|
||||
EuiToolTip,
|
||||
EuiButtonIcon,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { SnapshotDetails } from '../../../../../../common/types';
|
||||
import { SNAPSHOT_STATE, UIM_SNAPSHOT_SHOW_DETAILS_CLICK } from '../../../../constants';
|
||||
import { BASE_PATH, SNAPSHOT_STATE, UIM_SNAPSHOT_SHOW_DETAILS_CLICK } from '../../../../constants';
|
||||
import { useAppDependencies } from '../../../../index';
|
||||
import { formatDate } from '../../../../services/text';
|
||||
import { linkToRepository } from '../../../../services/navigation';
|
||||
import { uiMetricService } from '../../../../services/ui_metric';
|
||||
import { DataPlaceholder } from '../../../../components';
|
||||
import { DataPlaceholder, SnapshotDeleteProvider } from '../../../../components';
|
||||
|
||||
interface Props {
|
||||
snapshots: SnapshotDetails[];
|
||||
|
@ -21,6 +29,7 @@ interface Props {
|
|||
reload: () => Promise<void>;
|
||||
openSnapshotDetailsUrl: (repositoryName: string, snapshotId: string) => string;
|
||||
repositoryFilter?: string;
|
||||
onSnapshotDeleted: (snapshotsDeleted: Array<{ snapshot: string; repository: string }>) => void;
|
||||
}
|
||||
|
||||
export const SnapshotTable: React.FunctionComponent<Props> = ({
|
||||
|
@ -28,6 +37,7 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
|
|||
repositories,
|
||||
reload,
|
||||
openSnapshotDetailsUrl,
|
||||
onSnapshotDeleted,
|
||||
repositoryFilter,
|
||||
}) => {
|
||||
const {
|
||||
|
@ -35,6 +45,7 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
|
|||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
const [selectedItems, setSelectedItems] = useState<SnapshotDetails[]>([]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
@ -67,6 +78,36 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
|
|||
</EuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'indices',
|
||||
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.indicesColumnTitle', {
|
||||
defaultMessage: 'Indices',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (indices: string[]) => indices.length,
|
||||
},
|
||||
{
|
||||
field: 'shards.total',
|
||||
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.shardsColumnTitle', {
|
||||
defaultMessage: 'Shards',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (totalShards: number) => totalShards,
|
||||
},
|
||||
{
|
||||
field: 'shards.failed',
|
||||
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.failedShardsColumnTitle', {
|
||||
defaultMessage: 'Failed shards',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (failedShards: number) => failedShards,
|
||||
},
|
||||
{
|
||||
field: 'startTimeInMillis',
|
||||
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.startTimeColumnTitle', {
|
||||
|
@ -102,34 +143,85 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
|
|||
},
|
||||
},
|
||||
{
|
||||
field: 'indices',
|
||||
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.indicesColumnTitle', {
|
||||
defaultMessage: 'Indices',
|
||||
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.actionsColumnTitle', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
actions: [
|
||||
{
|
||||
render: ({ snapshot, repository, state }: SnapshotDetails) => {
|
||||
const canRestore = state === SNAPSHOT_STATE.SUCCESS || state === SNAPSHOT_STATE.PARTIAL;
|
||||
const label = canRestore
|
||||
? i18n.translate('xpack.snapshotRestore.snapshotList.table.actionRestoreTooltip', {
|
||||
defaultMessage: 'Restore',
|
||||
})
|
||||
: state === SNAPSHOT_STATE.IN_PROGRESS
|
||||
? i18n.translate(
|
||||
'xpack.snapshotRestore.snapshotList.table.actionRestoreDisabledInProgressTooltip',
|
||||
{
|
||||
defaultMessage: `Can't restore in-progress snapshot`,
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.snapshotRestore.snapshotList.table.actionRestoreDisabledInvalidTooltip',
|
||||
{
|
||||
defaultMessage: `Can't restore invalid snapshot`,
|
||||
}
|
||||
);
|
||||
return (
|
||||
<EuiToolTip content={label}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.snapshotRestore.snapshotList.table.actionRestoreAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Store snapshot `{name}`',
|
||||
values: { name: snapshot },
|
||||
}
|
||||
)}
|
||||
iconType="importAction"
|
||||
color="primary"
|
||||
data-test-subj="srsnapshotListRestoreActionButton"
|
||||
href={`#${BASE_PATH}/restore/${repository}/${snapshot}`}
|
||||
isDisabled={!canRestore}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
render: ({ snapshot, repository }: SnapshotDetails) => {
|
||||
return (
|
||||
<SnapshotDeleteProvider>
|
||||
{deleteSnapshotPrompt => {
|
||||
const label = i18n.translate(
|
||||
'xpack.snapshotRestore.snapshotList.table.actionDeleteTooltip',
|
||||
{ defaultMessage: 'Delete' }
|
||||
);
|
||||
return (
|
||||
<EuiToolTip content={label}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.snapshotRestore.snapshotList.table.actionDeleteAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Delete snapshot `{name}`',
|
||||
values: { name: snapshot },
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj="srsnapshotListDeleteActionButton"
|
||||
onClick={() =>
|
||||
deleteSnapshotPrompt([{ snapshot, repository }], onSnapshotDeleted)
|
||||
}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}}
|
||||
</SnapshotDeleteProvider>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
width: '100px',
|
||||
render: (indices: string[]) => indices.length,
|
||||
},
|
||||
{
|
||||
field: 'shards.total',
|
||||
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.shardsColumnTitle', {
|
||||
defaultMessage: 'Shards',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (totalShards: number) => totalShards,
|
||||
},
|
||||
{
|
||||
field: 'shards.failed',
|
||||
name: i18n.translate('xpack.snapshotRestore.snapshotList.table.failedShardsColumnTitle', {
|
||||
defaultMessage: 'Failed shards',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (failedShards: number) => failedShards,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -154,7 +246,44 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
|
|||
},
|
||||
};
|
||||
|
||||
const selection = {
|
||||
onSelectionChange: (newSelectedItems: SnapshotDetails[]) => setSelectedItems(newSelectedItems),
|
||||
};
|
||||
|
||||
const search = {
|
||||
toolsLeft: selectedItems.length ? (
|
||||
<SnapshotDeleteProvider>
|
||||
{(
|
||||
deleteSnapshotPrompt: (
|
||||
ids: Array<{ snapshot: string; repository: string }>,
|
||||
onSuccess?: (snapshotsDeleted: Array<{ snapshot: string; repository: string }>) => void
|
||||
) => void
|
||||
) => {
|
||||
return (
|
||||
<EuiButton
|
||||
onClick={() =>
|
||||
deleteSnapshotPrompt(
|
||||
selectedItems.map(({ snapshot, repository }) => ({ snapshot, repository })),
|
||||
onSnapshotDeleted
|
||||
)
|
||||
}
|
||||
color="danger"
|
||||
data-test-subj="srSnapshotListBulkDeleteActionButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.table.deleteSnapshotButton"
|
||||
defaultMessage="Delete {count, plural, one {snapshot} other {snapshots}}"
|
||||
values={{
|
||||
count: selectedItems.length,
|
||||
}}
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
}}
|
||||
</SnapshotDeleteProvider>
|
||||
) : (
|
||||
undefined
|
||||
),
|
||||
toolsRight: (
|
||||
<EuiButton
|
||||
color="secondary"
|
||||
|
@ -197,10 +326,12 @@ export const SnapshotTable: React.FunctionComponent<Props> = ({
|
|||
return (
|
||||
<EuiInMemoryTable
|
||||
items={snapshots}
|
||||
itemId="name"
|
||||
itemId="uuid"
|
||||
columns={columns}
|
||||
search={search}
|
||||
sorting={sorting}
|
||||
isSelectable={true}
|
||||
selection={selection}
|
||||
pagination={pagination}
|
||||
rowProps={() => ({
|
||||
'data-test-subj': 'row',
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
export { SnapshotRestoreHome } from './home';
|
||||
export { RepositoryAdd } from './repository_add';
|
||||
export { RepositoryEdit } from './repository_edit';
|
||||
export { RestoreSnapshot } from './restore_snapshot';
|
||||
|
|
|
@ -13,7 +13,7 @@ import { RepositoryForm, SectionError, SectionLoading } from '../../components';
|
|||
import { BASE_PATH, Section } from '../../constants';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { breadcrumbService } from '../../services/navigation';
|
||||
import { editRepository, loadRepository } from '../../services/http';
|
||||
import { editRepository, useLoadRepository } from '../../services/http';
|
||||
|
||||
interface MatchParams {
|
||||
name: string;
|
||||
|
@ -48,7 +48,7 @@ export const RepositoryEdit: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
error: repositoryError,
|
||||
loading: loadingRepository,
|
||||
data: repositoryData,
|
||||
} = loadRepository(name);
|
||||
} = useLoadRepository(name);
|
||||
|
||||
// Update repository state when data is loaded
|
||||
useEffect(
|
||||
|
|
|
@ -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 { RestoreSnapshot } from './restore_snapshot';
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* 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, { useEffect, useState } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
|
||||
import { SnapshotDetails, RestoreSettings } from '../../../../common/types';
|
||||
import { BASE_PATH } from '../../constants';
|
||||
import { SectionError, SectionLoading, RestoreSnapshotForm } from '../../components';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { breadcrumbService } from '../../services/navigation';
|
||||
import { useLoadSnapshot, executeRestore } from '../../services/http';
|
||||
|
||||
interface MatchParams {
|
||||
repositoryName: string;
|
||||
snapshotId: string;
|
||||
}
|
||||
|
||||
export const RestoreSnapshot: React.FunctionComponent<RouteComponentProps<MatchParams>> = ({
|
||||
match: {
|
||||
params: { repositoryName, snapshotId },
|
||||
},
|
||||
history,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
|
||||
// Set breadcrumb
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs('restoreSnapshot');
|
||||
}, []);
|
||||
|
||||
// Snapshot details state with default empty snapshot
|
||||
const [snapshotDetails, setSnapshotDetails] = useState<SnapshotDetails | {}>({});
|
||||
|
||||
// Load snapshot
|
||||
const { error: snapshotError, loading: loadingSnapshot, data: snapshotData } = useLoadSnapshot(
|
||||
repositoryName,
|
||||
snapshotId
|
||||
);
|
||||
|
||||
// Update repository state when data is loaded
|
||||
useEffect(
|
||||
() => {
|
||||
if (snapshotData) {
|
||||
setSnapshotDetails(snapshotData);
|
||||
}
|
||||
},
|
||||
[snapshotData]
|
||||
);
|
||||
|
||||
// Saving repository states
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [saveError, setSaveError] = useState<any>(null);
|
||||
|
||||
// Execute restore
|
||||
const onSave = async (restoreSettings: RestoreSettings) => {
|
||||
setIsSaving(true);
|
||||
setSaveError(null);
|
||||
const { error } = await executeRestore(repositoryName, snapshotId, restoreSettings);
|
||||
if (error) {
|
||||
setIsSaving(false);
|
||||
setSaveError(error);
|
||||
} else {
|
||||
// Wait a few seconds before redirecting so that restore information has time to
|
||||
// populate into master node
|
||||
setTimeout(() => {
|
||||
setIsSaving(false);
|
||||
history.push(`${BASE_PATH}/restore_status`);
|
||||
}, 5 * 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const renderLoading = () => {
|
||||
return (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreSnapshot.loadingSnapshotDescription"
|
||||
defaultMessage="Loading snapshot details…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
};
|
||||
|
||||
const renderError = () => {
|
||||
const notFound = snapshotError.status === 404;
|
||||
const errorObject = notFound
|
||||
? {
|
||||
data: {
|
||||
error: i18n.translate(
|
||||
'xpack.snapshotRestore.restoreSnapshot.snapshotNotFoundErrorMessage',
|
||||
{
|
||||
defaultMessage: `The snapshot '{snapshot}' does not exist in repository '{repository}'.`,
|
||||
values: {
|
||||
snapshot: snapshotId,
|
||||
repository: repositoryName,
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
: snapshotError;
|
||||
return (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreSnapshot.loadingSnapshotErrorTitle"
|
||||
defaultMessage="Error loading snapshot details"
|
||||
/>
|
||||
}
|
||||
error={errorObject}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSaveError = () => {
|
||||
return saveError ? (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreSnapshot.executeRestoreErrorTitle"
|
||||
defaultMessage="Unable to execute restore"
|
||||
/>
|
||||
}
|
||||
error={saveError}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const clearSaveError = () => {
|
||||
setSaveError(null);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (loadingSnapshot) {
|
||||
return renderLoading();
|
||||
}
|
||||
if (snapshotError) {
|
||||
return renderError();
|
||||
}
|
||||
|
||||
return (
|
||||
<RestoreSnapshotForm
|
||||
snapshotDetails={snapshotDetails as SnapshotDetails}
|
||||
isSaving={isSaving}
|
||||
saveError={renderSaveError()}
|
||||
clearSaveError={clearSaveError}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="m">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreSnapshotTitle"
|
||||
defaultMessage="Restore '{snapshot}'"
|
||||
values={{ snapshot: snapshotId }}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
{renderContent()}
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
|
@ -44,6 +44,14 @@ class DocumentationLinksService {
|
|||
public getSnapshotDocUrl() {
|
||||
return `${this.esDocBasePath}/modules-snapshots.html#_snapshot`;
|
||||
}
|
||||
|
||||
public getRestoreDocUrl() {
|
||||
return `${this.esDocBasePath}/modules-snapshots.html#restore-snapshot`;
|
||||
}
|
||||
|
||||
public getRestoreIndexSettingsUrl() {
|
||||
return `${this.esDocBasePath}/modules-snapshots.html#_changing_index_settings_during_restore`;
|
||||
}
|
||||
}
|
||||
|
||||
export const documentationLinksService = new DocumentationLinksService();
|
||||
|
|
|
@ -7,7 +7,7 @@ import { API_BASE_PATH } from '../../../../common/constants';
|
|||
import { httpService } from './http';
|
||||
import { useRequest } from './use_request';
|
||||
|
||||
export const loadPermissions = () => {
|
||||
export const useLoadPermissions = () => {
|
||||
return useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}permissions`),
|
||||
method: 'get',
|
||||
|
|
|
@ -7,3 +7,4 @@ export { httpService } from './http';
|
|||
export * from './app_requests';
|
||||
export * from './repository_requests';
|
||||
export * from './snapshot_requests';
|
||||
export * from './restore_requests';
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
import { API_BASE_PATH } from '../../../../common/constants';
|
||||
import { Repository, EmptyRepository } from '../../../../common/types';
|
||||
import {
|
||||
MINIMUM_TIMEOUT_MS,
|
||||
UIM_REPOSITORY_CREATE,
|
||||
UIM_REPOSITORY_UPDATE,
|
||||
UIM_REPOSITORY_DELETE,
|
||||
|
@ -16,16 +15,15 @@ import {
|
|||
import { httpService } from './http';
|
||||
import { sendRequest, useRequest } from './use_request';
|
||||
|
||||
export const loadRepositories = () => {
|
||||
export const useLoadRepositories = () => {
|
||||
return useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}repositories`),
|
||||
method: 'get',
|
||||
initialData: [],
|
||||
timeout: MINIMUM_TIMEOUT_MS,
|
||||
});
|
||||
};
|
||||
|
||||
export const loadRepository = (name: Repository['name']) => {
|
||||
export const useLoadRepository = (name: Repository['name']) => {
|
||||
return useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}repositories/${encodeURIComponent(name)}`),
|
||||
method: 'get',
|
||||
|
@ -42,7 +40,7 @@ export const verifyRepository = (name: Repository['name']) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const loadRepositoryTypes = () => {
|
||||
export const useLoadRepositoryTypes = () => {
|
||||
return useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}repository_types`),
|
||||
method: 'get',
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { API_BASE_PATH } from '../../../../common/constants';
|
||||
import { RestoreSettings } from '../../../../common/types';
|
||||
import { UIM_RESTORE_CREATE } from '../../constants';
|
||||
import { httpService } from './http';
|
||||
import { sendRequest, useRequest } from './use_request';
|
||||
|
||||
export const executeRestore = async (
|
||||
repository: string,
|
||||
snapshot: string,
|
||||
restoreSettings: RestoreSettings
|
||||
) => {
|
||||
return sendRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}restore/${encodeURIComponent(repository)}/${encodeURIComponent(snapshot)}`
|
||||
),
|
||||
method: 'post',
|
||||
body: restoreSettings,
|
||||
uimActionType: UIM_RESTORE_CREATE,
|
||||
});
|
||||
};
|
||||
|
||||
export const useLoadRestores = (interval?: number) => {
|
||||
return useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}restores`),
|
||||
method: 'get',
|
||||
initialData: [],
|
||||
interval,
|
||||
});
|
||||
};
|
|
@ -4,19 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { API_BASE_PATH } from '../../../../common/constants';
|
||||
import { MINIMUM_TIMEOUT_MS } from '../../constants';
|
||||
import { UIM_SNAPSHOT_DELETE, UIM_SNAPSHOT_DELETE_MANY } from '../../constants';
|
||||
import { httpService } from './http';
|
||||
import { useRequest } from './use_request';
|
||||
import { sendRequest, useRequest } from './use_request';
|
||||
|
||||
export const loadSnapshots = () =>
|
||||
export const useLoadSnapshots = () =>
|
||||
useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}snapshots`),
|
||||
method: 'get',
|
||||
initialData: [],
|
||||
timeout: MINIMUM_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
export const loadSnapshot = (repositoryName: string, snapshotId: string) =>
|
||||
export const useLoadSnapshot = (repositoryName: string, snapshotId: string) =>
|
||||
useRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}snapshots/${encodeURIComponent(repositoryName)}/${encodeURIComponent(
|
||||
|
@ -25,3 +24,17 @@ export const loadSnapshot = (repositoryName: string, snapshotId: string) =>
|
|||
),
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
export const deleteSnapshots = async (
|
||||
snapshotIds: Array<{ snapshot: string; repository: string }>
|
||||
) => {
|
||||
return sendRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}snapshots/${snapshotIds
|
||||
.map(({ snapshot, repository }) => encodeURIComponent(`${repository}/${snapshot}`))
|
||||
.join(',')}`
|
||||
),
|
||||
method: 'delete',
|
||||
uimActionType: snapshotIds.length > 1 ? UIM_SNAPSHOT_DELETE_MANY : UIM_SNAPSHOT_DELETE,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { httpService } from './index';
|
||||
import { uiMetricService } from '../ui_metric';
|
||||
|
||||
|
@ -52,7 +52,6 @@ export const sendRequest = async ({
|
|||
interface UseRequest extends SendRequest {
|
||||
interval?: number;
|
||||
initialData?: any;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export const useRequest = ({
|
||||
|
@ -61,20 +60,34 @@ export const useRequest = ({
|
|||
body,
|
||||
interval,
|
||||
initialData,
|
||||
timeout,
|
||||
uimActionType,
|
||||
}: UseRequest) => {
|
||||
// Main states for tracking request status and data
|
||||
const [error, setError] = useState<null | any>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [data, setData] = useState<any>(initialData);
|
||||
|
||||
// States for tracking polling
|
||||
const [polling, setPolling] = useState<boolean>(false);
|
||||
const [currentInterval, setCurrentInterval] = useState<UseRequest['interval']>(interval);
|
||||
const intervalRequest = useRef<any>(null);
|
||||
const isFirstRequest = useRef<boolean>(true);
|
||||
|
||||
// Tied to every render and bound to each request.
|
||||
let isOutdatedRequest = false;
|
||||
|
||||
const request = async () => {
|
||||
setError(null);
|
||||
setData(initialData);
|
||||
setLoading(true);
|
||||
const isPollRequest = currentInterval && !isFirstRequest.current;
|
||||
|
||||
// Don't reset main error/loading states if we are doing polling
|
||||
if (isPollRequest) {
|
||||
setPolling(true);
|
||||
} else {
|
||||
setError(null);
|
||||
setData(initialData);
|
||||
setLoading(true);
|
||||
setPolling(false);
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
path,
|
||||
|
@ -90,31 +103,49 @@ export const useRequest = ({
|
|||
return;
|
||||
}
|
||||
|
||||
setError(response.error);
|
||||
setData(response.data);
|
||||
setLoading(false);
|
||||
// Only set data if we are doing polling
|
||||
if (isPollRequest) {
|
||||
setPolling(false);
|
||||
if (response.data) {
|
||||
setData(response.data);
|
||||
}
|
||||
} else {
|
||||
setError(response.error);
|
||||
setData(response.data);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
isFirstRequest.current = false;
|
||||
};
|
||||
|
||||
const cancelOutdatedRequest = () => {
|
||||
isOutdatedRequest = true;
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
function cancelOutdatedRequest() {
|
||||
isOutdatedRequest = true;
|
||||
}
|
||||
|
||||
// Perform request
|
||||
request();
|
||||
|
||||
if (interval) {
|
||||
const intervalRequest = setInterval(request, interval);
|
||||
return () => {
|
||||
cancelOutdatedRequest();
|
||||
clearInterval(intervalRequest);
|
||||
};
|
||||
// Clear current interval
|
||||
if (intervalRequest.current) {
|
||||
clearInterval(intervalRequest.current);
|
||||
}
|
||||
|
||||
// Called when a new render will trigger this effect.
|
||||
return cancelOutdatedRequest;
|
||||
// Set new interval
|
||||
if (currentInterval) {
|
||||
intervalRequest.current = setInterval(request, currentInterval);
|
||||
}
|
||||
|
||||
// Cleanup intervals and inflight requests and corresponding state changes
|
||||
return () => {
|
||||
cancelOutdatedRequest();
|
||||
if (intervalRequest.current) {
|
||||
clearInterval(intervalRequest.current);
|
||||
}
|
||||
};
|
||||
},
|
||||
[path]
|
||||
[path, currentInterval]
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -122,5 +153,13 @@ export const useRequest = ({
|
|||
loading,
|
||||
data,
|
||||
request,
|
||||
polling,
|
||||
changeInterval: (newInterval: UseRequest['interval']) => {
|
||||
// Allow changing polling interval if there was one set
|
||||
if (!interval) {
|
||||
return;
|
||||
}
|
||||
setCurrentInterval(newInterval);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ class BreadcrumbService {
|
|||
home: {},
|
||||
repositoryAdd: {},
|
||||
repositoryEdit: {},
|
||||
restoreSnapshot: {},
|
||||
};
|
||||
|
||||
public init(chrome: any, managementBreadcrumb: any): void {
|
||||
|
@ -25,11 +26,13 @@ class BreadcrumbService {
|
|||
};
|
||||
this.breadcrumbs.repositoryAdd = {
|
||||
text: textService.breadcrumbs.repositoryAdd,
|
||||
href: `#${BASE_PATH}/add_repository`,
|
||||
};
|
||||
this.breadcrumbs.repositoryEdit = {
|
||||
text: textService.breadcrumbs.repositoryEdit,
|
||||
};
|
||||
this.breadcrumbs.restoreSnapshot = {
|
||||
text: textService.breadcrumbs.restoreSnapshot,
|
||||
};
|
||||
}
|
||||
|
||||
public setBreadcrumbs(type: string): void {
|
||||
|
|
|
@ -49,7 +49,7 @@ class TextService {
|
|||
};
|
||||
this.breadcrumbs = {
|
||||
home: i18n.translate('xpack.snapshotRestore.home.breadcrumbTitle', {
|
||||
defaultMessage: 'Snapshot Repositories',
|
||||
defaultMessage: 'Snapshot and Restore',
|
||||
}),
|
||||
repositoryAdd: i18n.translate('xpack.snapshotRestore.addRepository.breadcrumbTitle', {
|
||||
defaultMessage: 'Add repository',
|
||||
|
@ -57,6 +57,9 @@ class TextService {
|
|||
repositoryEdit: i18n.translate('xpack.snapshotRestore.editRepository.breadcrumbTitle', {
|
||||
defaultMessage: 'Edit repository',
|
||||
}),
|
||||
restoreSnapshot: i18n.translate('xpack.snapshotRestore.restoreSnapshot.breadcrumbTitle', {
|
||||
defaultMessage: 'Restore snapshot',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -9,3 +9,5 @@ export {
|
|||
RepositorySettingsValidation,
|
||||
validateRepository,
|
||||
} from './validate_repository';
|
||||
|
||||
export { RestoreValidation, validateRestore } from './validate_restore';
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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 { RestoreSettings } from '../../../../common/types';
|
||||
import { UNMODIFIABLE_INDEX_SETTINGS, UNREMOVABLE_INDEX_SETTINGS } from '../../../app/constants';
|
||||
import { textService } from '../text';
|
||||
|
||||
export interface RestoreValidation {
|
||||
isValid: boolean;
|
||||
errors: { [key: string]: React.ReactNode[] };
|
||||
}
|
||||
|
||||
const isStringEmpty = (str: string | null): boolean => {
|
||||
return str ? !Boolean(str.trim()) : true;
|
||||
};
|
||||
|
||||
export const validateRestore = (restoreSettings: RestoreSettings): RestoreValidation => {
|
||||
const i18n = textService.i18n;
|
||||
const {
|
||||
indices,
|
||||
renamePattern,
|
||||
renameReplacement,
|
||||
indexSettings,
|
||||
ignoreIndexSettings,
|
||||
} = restoreSettings;
|
||||
|
||||
const validation: RestoreValidation = {
|
||||
isValid: true,
|
||||
errors: {
|
||||
indices: [],
|
||||
renamePattern: [],
|
||||
renameReplacement: [],
|
||||
indexSettings: [],
|
||||
ignoreIndexSettings: [],
|
||||
},
|
||||
};
|
||||
|
||||
if (Array.isArray(indices) && indices.length === 0) {
|
||||
validation.errors.indices.push(
|
||||
i18n.translate('xpack.snapshotRestore.restoreValidation.indicesRequiredError', {
|
||||
defaultMessage: 'You must select at least one index.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (renamePattern !== undefined && isStringEmpty(renamePattern)) {
|
||||
validation.errors.renamePattern.push(
|
||||
i18n.translate('xpack.snapshotRestore.restoreValidation.renamePatternRequiredError', {
|
||||
defaultMessage: 'Capture pattern is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (renameReplacement !== undefined && isStringEmpty(renameReplacement)) {
|
||||
validation.errors.renameReplacement.push(
|
||||
i18n.translate('xpack.snapshotRestore.restoreValidation.renameReplacementRequiredError', {
|
||||
defaultMessage: 'Replacement pattern is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof indexSettings === 'string') {
|
||||
try {
|
||||
const parsedIndexSettings = JSON.parse(indexSettings);
|
||||
const modifiedSettings = Object.keys(parsedIndexSettings);
|
||||
const modifiedSettingsCount = modifiedSettings.length;
|
||||
const unmodifiableSettings =
|
||||
modifiedSettingsCount > 0
|
||||
? modifiedSettings.filter(setting => UNMODIFIABLE_INDEX_SETTINGS.includes(setting))
|
||||
: null;
|
||||
|
||||
if (modifiedSettingsCount === 0) {
|
||||
validation.errors.indexSettings.push(
|
||||
i18n.translate('xpack.snapshotRestore.restoreValidation.indexSettingsRequiredError', {
|
||||
defaultMessage: 'At least one setting is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (unmodifiableSettings && unmodifiableSettings.length > 0) {
|
||||
validation.errors.indexSettings.push(
|
||||
i18n.translate(
|
||||
'xpack.snapshotRestore.restoreValidation.indexSettingsNotModifiableError',
|
||||
{
|
||||
defaultMessage: 'You can’t modify: {settings}',
|
||||
// @ts-ignore Bug filed: https://github.com/elastic/kibana/issues/39299
|
||||
values: {
|
||||
settings: unmodifiableSettings.map((setting: string, index: number) =>
|
||||
index === 0 ? `${setting} ` : setting
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
validation.errors.indexSettings.push(
|
||||
i18n.translate('xpack.snapshotRestore.restoreValidation.indexSettingsInvalidError', {
|
||||
defaultMessage: 'Invalid JSON format',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(ignoreIndexSettings)) {
|
||||
const ignoredSettingsCount = ignoreIndexSettings.length;
|
||||
const unremovableSettings =
|
||||
ignoredSettingsCount > 0
|
||||
? ignoreIndexSettings.filter(setting => UNREMOVABLE_INDEX_SETTINGS.includes(setting))
|
||||
: null;
|
||||
|
||||
if (ignoredSettingsCount === 0) {
|
||||
validation.errors.ignoreIndexSettings.push(
|
||||
i18n.translate('xpack.snapshotRestore.restoreValidation.ignoreIndexSettingsRequiredError', {
|
||||
defaultMessage: 'At least one setting is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (unremovableSettings && unremovableSettings.length > 0) {
|
||||
validation.errors.ignoreIndexSettings.push(
|
||||
i18n.translate('xpack.snapshotRestore.restoreValidation.indexSettingsNotRemovableError', {
|
||||
defaultMessage: 'You can’t reset: {settings}',
|
||||
// @ts-ignore Bug filed: https://github.com/elastic/kibana/issues/39299
|
||||
values: {
|
||||
settings: unremovableSettings.map((setting: string, index: number) =>
|
||||
index === 0 ? `${setting} ` : setting
|
||||
),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove fields with no errors
|
||||
validation.errors = Object.entries(validation.errors)
|
||||
.filter(([key, value]) => value.length > 0)
|
||||
.reduce((errs: RestoreValidation['errors'], [key, value]) => {
|
||||
errs[key] = value;
|
||||
return errs;
|
||||
}, {});
|
||||
|
||||
// Set overall validations status
|
||||
if (Object.keys(validation.errors).length > 0) {
|
||||
validation.isValid = false;
|
||||
}
|
||||
|
||||
return validation;
|
||||
};
|
|
@ -29,7 +29,7 @@ export class Plugin {
|
|||
esSection.register(PLUGIN.ID, {
|
||||
visible: true,
|
||||
display: i18n.translate('xpack.snapshotRestore.appName', {
|
||||
defaultMessage: 'Snapshot Repositories',
|
||||
defaultMessage: 'Snapshot and Restore',
|
||||
}),
|
||||
order: 7,
|
||||
url: `#${CLIENT_BASE_PATH}`,
|
||||
|
|
|
@ -8,5 +8,6 @@ export {
|
|||
deserializeRepositorySettings,
|
||||
serializeRepositorySettings,
|
||||
} from './repository_serialization';
|
||||
export { deserializeSnapshotDetails } from './snapshot_serialization';
|
||||
export { cleanSettings } from './clean_settings';
|
||||
export { deserializeSnapshotDetails } from './snapshot_serialization';
|
||||
export { deserializeRestoreShard } from './restore_serialization';
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 { deserializeRestoreShard } from './restore_serialization';
|
||||
import { SnapshotRestoreShardEs } from '../../common/types';
|
||||
|
||||
describe('restore_serialization', () => {
|
||||
describe('deserializeRestoreShard()', () => {
|
||||
it('should deserialize a snapshot restore shard', () => {
|
||||
expect(
|
||||
deserializeRestoreShard({
|
||||
id: 0,
|
||||
type: 'SNAPSHOT',
|
||||
stage: 'DONE',
|
||||
primary: true,
|
||||
start_time: '2019-06-24T20:40:10.583Z',
|
||||
start_time_in_millis: 1561408810583,
|
||||
stop_time: '2019-06-24T20:40:11.324Z',
|
||||
stop_time_in_millis: 1561408811324,
|
||||
total_time: '740ms',
|
||||
total_time_in_millis: 740,
|
||||
source: {
|
||||
repository: 'my-backups',
|
||||
snapshot: 'snapshot_1',
|
||||
version: '8.0.0',
|
||||
index: 'test_index',
|
||||
restoreUUID: 'some_uuid',
|
||||
},
|
||||
target: {
|
||||
id: 'some_node_id',
|
||||
host: '127.0.0.1',
|
||||
transport_address: '127.0.0.1:9300',
|
||||
ip: '127.0.0.1',
|
||||
name: 'some_node_name',
|
||||
},
|
||||
index: {
|
||||
size: {
|
||||
total: '4.7mb',
|
||||
total_in_bytes: 4986706,
|
||||
reused: '0b',
|
||||
reused_in_bytes: 0,
|
||||
recovered: '4.7mb',
|
||||
recovered_in_bytes: 4986706,
|
||||
percent: '100.0%',
|
||||
},
|
||||
files: {
|
||||
total: 10,
|
||||
reused: 0,
|
||||
recovered: 10,
|
||||
percent: '100.0%',
|
||||
},
|
||||
total_time: '624ms',
|
||||
total_time_in_millis: 624,
|
||||
source_throttle_time: '-1',
|
||||
source_throttle_time_in_millis: 0,
|
||||
target_throttle_time: '-1',
|
||||
target_throttle_time_in_millis: 0,
|
||||
},
|
||||
translog: {
|
||||
recovered: 0,
|
||||
total: 0,
|
||||
percent: '100.0%',
|
||||
total_on_start: 0,
|
||||
total_time: '87ms',
|
||||
total_time_in_millis: 87,
|
||||
},
|
||||
verify_index: {
|
||||
check_index_time: '0s',
|
||||
check_index_time_in_millis: 0,
|
||||
total_time: '0s',
|
||||
total_time_in_millis: 0,
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
bytesPercent: '100.0%',
|
||||
bytesRecovered: 4986706,
|
||||
bytesTotal: 4986706,
|
||||
filesPercent: '100.0%',
|
||||
filesRecovered: 10,
|
||||
filesTotal: 10,
|
||||
id: 0,
|
||||
primary: true,
|
||||
repository: 'my-backups',
|
||||
snapshot: 'snapshot_1',
|
||||
stage: 'DONE',
|
||||
startTime: '2019-06-24T20:40:10.583Z',
|
||||
startTimeInMillis: 1561408810583,
|
||||
stopTime: '2019-06-24T20:40:11.324Z',
|
||||
stopTimeInMillis: 1561408811324,
|
||||
targetHost: '127.0.0.1',
|
||||
targetNode: 'some_node_name',
|
||||
totalTime: '740ms',
|
||||
totalTimeInMillis: 740,
|
||||
version: '8.0.0',
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove undefined properties', () => {
|
||||
expect(
|
||||
deserializeRestoreShard({
|
||||
type: 'SNAPSHOT',
|
||||
source: {},
|
||||
target: {},
|
||||
index: { size: {}, files: {} },
|
||||
} as SnapshotRestoreShardEs)
|
||||
).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SnapshotRestoreShard, SnapshotRestoreShardEs } from '../../common/types';
|
||||
|
||||
export const deserializeRestoreShard = (
|
||||
esSnapshotRestoreShard: SnapshotRestoreShardEs
|
||||
): Partial<SnapshotRestoreShard> => {
|
||||
const {
|
||||
id,
|
||||
primary,
|
||||
stage,
|
||||
source: { repository, snapshot, version },
|
||||
target: { host: targetHost, name: targetNode },
|
||||
start_time: startTime,
|
||||
start_time_in_millis: startTimeInMillis,
|
||||
stop_time: stopTime,
|
||||
stop_time_in_millis: stopTimeInMillis,
|
||||
total_time: totalTime,
|
||||
total_time_in_millis: totalTimeInMillis,
|
||||
index: {
|
||||
size: {
|
||||
total_in_bytes: bytesTotal,
|
||||
recovered_in_bytes: bytesRecovered,
|
||||
percent: bytesPercent,
|
||||
},
|
||||
files: { total: filesTotal, recovered: filesRecovered, percent: filesPercent },
|
||||
},
|
||||
} = esSnapshotRestoreShard;
|
||||
|
||||
const snapshotRestoreShard = {
|
||||
id,
|
||||
primary,
|
||||
stage,
|
||||
snapshot,
|
||||
repository,
|
||||
version,
|
||||
targetHost,
|
||||
targetNode,
|
||||
startTime,
|
||||
startTimeInMillis,
|
||||
stopTime,
|
||||
stopTimeInMillis,
|
||||
totalTime,
|
||||
totalTimeInMillis,
|
||||
bytesTotal,
|
||||
bytesRecovered,
|
||||
bytesPercent,
|
||||
filesTotal,
|
||||
filesRecovered,
|
||||
filesPercent,
|
||||
};
|
||||
|
||||
return Object.entries(snapshotRestoreShard).reduce((shard: any, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
shard[key] = value;
|
||||
}
|
||||
return shard;
|
||||
}, {});
|
||||
};
|
|
@ -54,8 +54,7 @@ describe('deserializeSnapshotDetails', () => {
|
|||
version: 'version',
|
||||
// Indices are sorted.
|
||||
indices: ['index1', 'index2', 'index3'],
|
||||
// Converted from a boolean into 0 or 1.
|
||||
includeGlobalState: 0,
|
||||
includeGlobalState: false,
|
||||
// Failures are grouped and sorted by index, and the failures themselves are sorted by shard.
|
||||
indexFailures: [
|
||||
{
|
||||
|
|
|
@ -66,7 +66,7 @@ export function deserializeSnapshotDetails(
|
|||
versionId,
|
||||
version,
|
||||
indices: [...indices].sort(),
|
||||
includeGlobalState: Boolean(includeGlobalState) ? 1 : 0,
|
||||
includeGlobalState,
|
||||
state,
|
||||
startTime,
|
||||
startTimeInMillis,
|
||||
|
|
|
@ -8,9 +8,11 @@ import { Plugins } from '../../../shim';
|
|||
import { registerAppRoutes } from './app';
|
||||
import { registerRepositoriesRoutes } from './repositories';
|
||||
import { registerSnapshotsRoutes } from './snapshots';
|
||||
import { registerRestoreRoutes } from './restore';
|
||||
|
||||
export const registerRoutes = (router: Router, plugins: Plugins): void => {
|
||||
registerAppRoutes(router, plugins);
|
||||
registerRepositoriesRoutes(router, plugins);
|
||||
registerSnapshotsRoutes(router);
|
||||
registerRestoreRoutes(router);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { Request, ResponseToolkit } from 'hapi';
|
||||
import { createHandler, getAllHandler } from './restore';
|
||||
|
||||
describe('[Snapshot and Restore API Routes] Restore', () => {
|
||||
const mockRequest = {} as Request;
|
||||
const mockResponseToolkit = {} as ResponseToolkit;
|
||||
const mockEsShard = {
|
||||
type: 'SNAPSHOT',
|
||||
source: {},
|
||||
target: {},
|
||||
index: { size: {}, files: {} },
|
||||
};
|
||||
|
||||
describe('createHandler()', () => {
|
||||
const mockCreateRequest = ({
|
||||
params: {
|
||||
repository: 'foo',
|
||||
snapshot: 'snapshot-1',
|
||||
},
|
||||
payload: {},
|
||||
} as unknown) as Request;
|
||||
|
||||
it('should return successful response from ES', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
const callWithRequest = jest.fn().mockReturnValueOnce(mockEsResponse);
|
||||
await expect(
|
||||
createHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(mockEsResponse);
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
const callWithRequest = jest.fn().mockRejectedValueOnce(new Error());
|
||||
await expect(
|
||||
createHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllHandler()', () => {
|
||||
it('should arrify and filter restore shards returned from ES', async () => {
|
||||
const mockEsResponse = {
|
||||
fooIndex: {
|
||||
shards: [mockEsShard],
|
||||
},
|
||||
barIndex: {
|
||||
shards: [mockEsShard, mockEsShard],
|
||||
},
|
||||
testIndex: {
|
||||
shards: [
|
||||
{
|
||||
...mockEsShard,
|
||||
type: 'EMPTY_STORE',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const callWithRequest = jest.fn().mockReturnValueOnce(mockEsResponse);
|
||||
const expectedResponse = [
|
||||
{
|
||||
index: 'fooIndex',
|
||||
shards: [{}],
|
||||
isComplete: false,
|
||||
latestActivityTimeInMillis: 0,
|
||||
},
|
||||
{
|
||||
index: 'barIndex',
|
||||
shards: [{}, {}],
|
||||
isComplete: false,
|
||||
latestActivityTimeInMillis: 0,
|
||||
},
|
||||
];
|
||||
await expect(
|
||||
getAllHandler(mockRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return empty array if no repositories returned from ES', async () => {
|
||||
const mockEsResponse = {};
|
||||
const callWithRequest = jest.fn().mockReturnValueOnce(mockEsResponse);
|
||||
const expectedResponse: any[] = [];
|
||||
await expect(
|
||||
getAllHandler(mockRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
const callWithRequest = jest.fn().mockRejectedValueOnce(new Error());
|
||||
await expect(
|
||||
getAllHandler(mockRequest, callWithRequest, mockResponseToolkit)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { Router, RouterRouteHandler } from '../../../../../server/lib/create_router';
|
||||
import { RestoreSettings, SnapshotRestore, SnapshotRestoreShardEs } from '../../../common/types';
|
||||
import { serializeRestoreSettings } from '../../../common/lib';
|
||||
import { deserializeRestoreShard } from '../../lib';
|
||||
|
||||
export function registerRestoreRoutes(router: Router) {
|
||||
router.post('restore/{repository}/{snapshot}', createHandler);
|
||||
router.get('restores', getAllHandler);
|
||||
}
|
||||
|
||||
export const createHandler: RouterRouteHandler = async (req, callWithRequest) => {
|
||||
const { repository, snapshot } = req.params;
|
||||
const restoreSettings = req.payload as RestoreSettings;
|
||||
|
||||
return await callWithRequest('snapshot.restore', {
|
||||
repository,
|
||||
snapshot,
|
||||
body: serializeRestoreSettings(restoreSettings),
|
||||
});
|
||||
};
|
||||
|
||||
export const getAllHandler: RouterRouteHandler = async (req, callWithRequest) => {
|
||||
const snapshotRestores: SnapshotRestore[] = [];
|
||||
const recoveryByIndexName: {
|
||||
[key: string]: {
|
||||
shards: SnapshotRestoreShardEs[];
|
||||
};
|
||||
} = await callWithRequest('indices.recovery', {
|
||||
human: true,
|
||||
});
|
||||
|
||||
// Filter to snapshot-recovered shards only
|
||||
Object.keys(recoveryByIndexName).forEach(index => {
|
||||
const recovery = recoveryByIndexName[index];
|
||||
let latestActivityTimeInMillis: number = 0;
|
||||
let latestEndTimeInMillis: number | null = null;
|
||||
const snapshotShards = (recovery.shards || [])
|
||||
.filter(shard => shard.type === 'SNAPSHOT')
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map(shard => {
|
||||
const deserializedShard = deserializeRestoreShard(shard);
|
||||
const { startTimeInMillis, stopTimeInMillis } = deserializedShard;
|
||||
|
||||
// Set overall latest activity time
|
||||
latestActivityTimeInMillis = Math.max(
|
||||
startTimeInMillis || 0,
|
||||
stopTimeInMillis || 0,
|
||||
latestActivityTimeInMillis
|
||||
);
|
||||
|
||||
// Set overall end time
|
||||
if (stopTimeInMillis === undefined) {
|
||||
latestEndTimeInMillis = null;
|
||||
} else if (latestEndTimeInMillis === null || stopTimeInMillis > latestEndTimeInMillis) {
|
||||
latestEndTimeInMillis = stopTimeInMillis;
|
||||
}
|
||||
|
||||
return deserializedShard;
|
||||
});
|
||||
|
||||
if (snapshotShards.length > 0) {
|
||||
snapshotRestores.push({
|
||||
index,
|
||||
latestActivityTimeInMillis,
|
||||
shards: snapshotShards,
|
||||
isComplete: latestEndTimeInMillis !== null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by latest activity
|
||||
snapshotRestores.sort((a, b) => b.latestActivityTimeInMillis - a.latestActivityTimeInMillis);
|
||||
|
||||
return snapshotRestores;
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Request, ResponseToolkit } from 'hapi';
|
||||
import { getAllHandler, getOneHandler } from './snapshots';
|
||||
import { getAllHandler, getOneHandler, deleteHandler } from './snapshots';
|
||||
|
||||
const defaultSnapshot = {
|
||||
repository: undefined,
|
||||
|
@ -14,7 +14,7 @@ const defaultSnapshot = {
|
|||
versionId: undefined,
|
||||
version: undefined,
|
||||
indices: [],
|
||||
includeGlobalState: 0,
|
||||
includeGlobalState: undefined,
|
||||
state: undefined,
|
||||
startTime: undefined,
|
||||
startTimeInMillis: undefined,
|
||||
|
@ -128,4 +128,74 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteHandler()', () => {
|
||||
const ids = ['fooRepository/snapshot-1', 'barRepository/snapshot-2'];
|
||||
const mockCreateRequest = ({
|
||||
params: {
|
||||
ids: ids.join(','),
|
||||
},
|
||||
} as unknown) as Request;
|
||||
|
||||
it('should return successful ES responses', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(mockEsResponse)
|
||||
.mockResolvedValueOnce(mockEsResponse);
|
||||
const expectedResponse = {
|
||||
itemsDeleted: [
|
||||
{ snapshot: 'snapshot-1', repository: 'fooRepository' },
|
||||
{ snapshot: 'snapshot-2', repository: 'barRepository' },
|
||||
],
|
||||
errors: [],
|
||||
};
|
||||
await expect(
|
||||
deleteHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return error ES responses', async () => {
|
||||
const mockEsError = new Error('Test error') as any;
|
||||
mockEsError.response = '{}';
|
||||
mockEsError.statusCode = 500;
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(mockEsError)
|
||||
.mockRejectedValueOnce(mockEsError);
|
||||
const expectedResponse = {
|
||||
itemsDeleted: [],
|
||||
errors: [
|
||||
{ id: { snapshot: 'snapshot-1', repository: 'fooRepository' }, error: mockEsError },
|
||||
{ id: { snapshot: 'snapshot-2', repository: 'barRepository' }, error: mockEsError },
|
||||
],
|
||||
};
|
||||
await expect(
|
||||
deleteHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return combination of ES successes and errors', async () => {
|
||||
const mockEsError = new Error('Test error') as any;
|
||||
mockEsError.response = '{}';
|
||||
mockEsError.statusCode = 500;
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(mockEsError)
|
||||
.mockResolvedValueOnce(mockEsResponse);
|
||||
const expectedResponse = {
|
||||
itemsDeleted: [{ snapshot: 'snapshot-2', repository: 'barRepository' }],
|
||||
errors: [
|
||||
{
|
||||
id: { snapshot: 'snapshot-1', repository: 'fooRepository' },
|
||||
error: mockEsError,
|
||||
},
|
||||
],
|
||||
};
|
||||
await expect(
|
||||
deleteHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { Router, RouterRouteHandler } from '../../../../../server/lib/create_router';
|
||||
import { wrapEsError } from '../../../../../server/lib/create_router/error_wrappers';
|
||||
import { SnapshotDetails } from '../../../common/types';
|
||||
import { deserializeSnapshotDetails } from '../../lib';
|
||||
import { SnapshotDetailsEs } from '../../types';
|
||||
|
@ -11,6 +12,7 @@ import { SnapshotDetailsEs } from '../../types';
|
|||
export function registerSnapshotsRoutes(router: Router) {
|
||||
router.get('snapshots', getAllHandler);
|
||||
router.get('snapshots/{repository}/{snapshot}', getOneHandler);
|
||||
router.delete('snapshots/{ids}', deleteHandler);
|
||||
}
|
||||
|
||||
export const getAllHandler: RouterRouteHandler = async (
|
||||
|
@ -81,3 +83,37 @@ export const getOneHandler: RouterRouteHandler = async (
|
|||
// If the snapshot is missing the endpoint will return a 404, so we'll never get to this point.
|
||||
return deserializeSnapshotDetails(repository, snapshots[0]);
|
||||
};
|
||||
|
||||
export const deleteHandler: RouterRouteHandler = async (req, callWithRequest) => {
|
||||
const { ids } = req.params;
|
||||
const snapshotIds = ids.split(',');
|
||||
const response: {
|
||||
itemsDeleted: Array<{ snapshot: string; repository: string }>;
|
||||
errors: any[];
|
||||
} = {
|
||||
itemsDeleted: [],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
// We intentially perform deletion requests sequentially (blocking) instead of in parallel (non-blocking)
|
||||
// because there can only be one snapshot deletion task performed at a time (ES restriction).
|
||||
for (let i = 0; i < snapshotIds.length; i++) {
|
||||
// IDs come in the format of `repository-name/snapshot-name`
|
||||
// Extract the two parts by splitting at last occurrence of `/` in case
|
||||
// repository name contains '/` (from older versions)
|
||||
const id = snapshotIds[i];
|
||||
const indexOfDivider = id.lastIndexOf('/');
|
||||
const snapshot = id.substring(indexOfDivider + 1);
|
||||
const repository = id.substring(0, indexOfDivider);
|
||||
await callWithRequest('snapshot.delete', { snapshot, repository })
|
||||
.then(() => response.itemsDeleted.push({ snapshot, repository }))
|
||||
.catch(e =>
|
||||
response.errors.push({
|
||||
id: { snapshot, repository },
|
||||
error: wrapEsError(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
|
4
x-pack/typings/@elastic/eui/index.d.ts
vendored
4
x-pack/typings/@elastic/eui/index.d.ts
vendored
|
@ -13,6 +13,10 @@ declare module '@elastic/eui' {
|
|||
export const EuiCard: any;
|
||||
}
|
||||
|
||||
declare module '@elastic/eui/lib/services' {
|
||||
export const RIGHT_ALIGNMENT: any;
|
||||
}
|
||||
|
||||
declare module '@elastic/eui/lib/services/format' {
|
||||
export const dateFormatAliases: any;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue