mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* [SR] Snapshot and restore plugin boilerplate (#32276)
* Initial plugin set up
* Set up client shell
* Add initial repository list routes
* Fix merge issues and some typings
* Decouple server from plugin.ts files, tighten up typings
* Use exported constant for required license
* Translate plugin name, more typings
* Fix more types, move list components under /home
* Remove unused var
* Change scss prefix
* Uncouple unmount logic from routing shim, and some other PR feedback
* [SR] Repository list and details UI (#33367)
* Initial pass at repositories list UI
* Add detail panel for file system repositories, and a generic detail panel with json settings view
* Add detail components for other types
* Add detail panel footer, rename `useStateValue` to `useAppState`
* Fix detail panel footer
* Fix unused vars
* PR feedback
* PR feedback
* [SR] Refactor proposal (#33690)
* Move app dependencies to its own context provider
* Add index.ts barrel file for common types
* Move Enums to constants.ts file
* Refactor function component using `React.FunctionComponent<Props>`
* Refactor service folder structure
* Fix type import
* Move REPOSITORY_DOC_PATHS from common to public constants
* Move AppCore and AppPlugins interfaces back to shim and re-export them from app types
* [SR] Create and edit repositories UI (#34020)
* Add routing and placeholder form
* Fix typings
* Set up edit repository route, and basic form UI
* Add typings for wrapCustomError, and copy extractCausedByChain from CCR wrapEsError
* Throw errors that are already boomified
* Create and edit for basic repository types (fs, url, source)
* Add repository verification UI to table and details
* Create and edit for plugin repository types (hdfs, azure, s3, gcs)
* Fix linting
* Fix test
* Fix test
* Remove unused import
* Fix duplicate i18n key
* Fix details opening on cancel edit, remove unnecessary Fragments, definition file for some EUI components to x-pack, rename saveError
* Remove breaks
* Adjust add and edit repo routes so they don't conflict with list route
* Add repo plugin and types doc links to form
* Bootstrap documentation service
* Bootstrap text service and replace RepositoryTypeName component with it
* Bootstrap breadcrumb service and replace usages
* Bootstrap httpService, remove chrome and http from app dependencies(!)
* Add request creator and replace all instances of useRequest and sendRequest with it
* Fix typo
* Simplify update repository and update repository setting methods
* Adjust copy
* Lint
* Remove unused var
* Remove unused import
* [SR] Add API for retrieving snapshots. (#34598)
* [SR] Single and multiple repository delete (#34593)
* Add single/multi repository delete API and UI
* Address PR feedback
* [SR] Add SnapshotTable and SnapshotDetails. (#34837)
* Remove associations between multiple repositories with a single snapshot.
* Retrieve complete snapshot details in getAllHandler.
* Fix cleanup function bug in useRequest hook.
* Fix bug in useRequest which prevented old data from being cleared when subsequent requests returned errors.
* Add initialValue config option to useRequest.
* Add formatDate service to text module.
* [SR] Fix linting and add (de)serialization for repositories (#35031)
* Fix eslint issues and add (de)serialization for repositories
* Add comment about flattening settings
* [SR] Surface repository errors and index failures more prominently (#35042)
* Add links to repositories from Snapshot Table and Snapshot Details.
- Rename services/breadcrumbs to services/navigation and add linkToRepository function.
- Refactor home component to update active tab when URL was changed.
* Add warning callout to let user know when their repositories contain errors.
* Sort failures by shard and add test for snapshot serialization.
* Sort failures and indices.
* Add filter for filtering snapshots by their repository.
* Surface states with humanized text, icons, and tooltips where necessary.
* Fix pluralization of seconds.
* Surface failures tab even if there are none.
- Display a '-' for missing times and durations.
- Create DataPlaceholder component.
* [SR] Polish repositories UX (#35123)
* Refactor repository detail panel to load repository based directly on route param.
* Display repository detail panel while table is loading.
* Make 'Edit repository' table action a link instead of a button.
* Render disabled EuiSelect as a readonly EuiFieldText.
* Prepend HDFS URI with hdfs:// protocol.
* Present scheme options for Read-Only URL repository as a select.
* [SR] Add client-side validation to repository form and link to snapshots from details (#35238)
* Add client side repository form validation, extract `flatten` into common lib
* Add snapshot count to repository details and link to snapshot list
* Reset validation when changing repository type
* Fix snapshot list filter deep linking for repository names with slashes and spaces
* Fix imports
* PR feedback
* [SR] Design and copywriting fixes (#35591)
* Split repository form into two steps; move `clean_settings.ts` to server
* Default to snapshots tab, adjust snapshot empty prompt, add app description
* Add minimum timeout to list view requests to avoid flicker, use EuiEmptyPrompt for loading screen, add doc link to settings step
* Add information about snapshots to delete repository behavior, add doc link for source only toggle, add size notation help text
* Add main doc link
* Copywriting and i18n fixes, and add some common settings to third party repo types
* Add fields to third party repo detail panel
* More copywriting fixes
* Use spinner for duration and end time if snapshotting is still in progress
* Show all repository type options, mark missing plugins
* Revert "Show all repository type options, mark missing plugins"
This reverts commit e34ee47cec
.
* Fix space
* [SR] Add permissions UI and Cloud-specific repository type UI branch (#35833)
* Add missing permissions UI and cloud-specific repository type UI branch
* Add ES UI as owners of /snapshot_restore directory
* Add no repository types callout for Cloud edge case
* Redirect invalid section param to repositories
* Add warning empty prompt if all repositories have errrors
* Replace repository cards with EuiCard
* Add snapshot doc link to repository error empty prompt
* Remove auto-verification from list and get routes, add separate verification route, add manual verification to repository detail panel
* Update copy and remove obsolete test
* Remove unused scss files
* Final changes to repository cards
This commit is contained in:
parent
f16d70c75a
commit
bba50a8839
113 changed files with 10139 additions and 6 deletions
|
@ -45,6 +45,7 @@
|
|||
"xpack.siem": "x-pack/plugins/siem",
|
||||
"xpack.security": "x-pack/plugins/security",
|
||||
"xpack.server": "x-pack/server",
|
||||
"xpack.snapshotRestore": "x-pack/plugins/snapshot_restore",
|
||||
"xpack.spaces": "x-pack/plugins/spaces",
|
||||
"xpack.upgradeAssistant": "x-pack/plugins/upgrade_assistant",
|
||||
"xpack.uptime": "x-pack/plugins/uptime",
|
||||
|
|
4
src/legacy/ui/public/management/index.d.ts
vendored
4
src/legacy/ui/public/management/index.d.ts
vendored
|
@ -28,4 +28,8 @@ declare module 'ui/management' {
|
|||
allowOverride: boolean
|
||||
): void;
|
||||
export const management: any; // TODO - properly provide types
|
||||
export const MANAGEMENT_BREADCRUMB: {
|
||||
text: string;
|
||||
href: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ import { upgradeAssistant } from './plugins/upgrade_assistant';
|
|||
import { uptime } from './plugins/uptime';
|
||||
import { ossTelemetry } from './plugins/oss_telemetry';
|
||||
import { encryptedSavedObjects } from './plugins/encrypted_saved_objects';
|
||||
import { snapshotRestore } from './plugins/snapshot_restore';
|
||||
|
||||
module.exports = function (kibana) {
|
||||
return [
|
||||
|
@ -79,5 +80,6 @@ module.exports = function (kibana) {
|
|||
uptime(kibana),
|
||||
ossTelemetry(kibana),
|
||||
encryptedSavedObjects(kibana),
|
||||
snapshotRestore(kibana),
|
||||
];
|
||||
};
|
||||
|
|
|
@ -64,8 +64,8 @@ export const registerCcrRoutes = (server) => {
|
|||
}
|
||||
|
||||
const securityInfo = (xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'));
|
||||
if (!securityInfo || !securityInfo.isEnabled()) {
|
||||
// If security isn't enabled, let the user use CCR.
|
||||
if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) {
|
||||
// If security isn't enabled or available (in the case where security is enabled but license reverted to Basic) let the user use CCR.
|
||||
return {
|
||||
hasPermission: true,
|
||||
missingClusterPrivileges: [],
|
||||
|
|
2
x-pack/plugins/infra/types/eui.d.ts
vendored
2
x-pack/plugins/infra/types/eui.d.ts
vendored
|
@ -116,6 +116,8 @@ declare module '@elastic/eui' {
|
|||
loading?: any;
|
||||
hasActions?: any;
|
||||
message?: any;
|
||||
rowProps?: any;
|
||||
cellProps?: any;
|
||||
};
|
||||
export const EuiInMemoryTable: React.SFC<EuiInMemoryTableProps>;
|
||||
}
|
||||
|
|
53
x-pack/plugins/snapshot_restore/common/constants.ts
Normal file
53
x-pack/plugins/snapshot_restore/common/constants.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { LICENSE_TYPE_BASIC, LicenseType } from '../../../common/constants';
|
||||
import { RepositoryType } from './types';
|
||||
|
||||
const PLUGIN_NAME = 'Snapshot Repositories';
|
||||
|
||||
export const PLUGIN = {
|
||||
ID: 'snapshot_restore',
|
||||
MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType,
|
||||
getI18nName: (translate: (key: string, config: object) => string): string => {
|
||||
return translate('xpack.snapshotRestore.appName', {
|
||||
defaultMessage: PLUGIN_NAME,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const API_BASE_PATH = '/api/snapshot_restore/';
|
||||
|
||||
export enum REPOSITORY_TYPES {
|
||||
fs = 'fs',
|
||||
url = 'url',
|
||||
source = 'source',
|
||||
s3 = 's3',
|
||||
hdfs = 'hdfs',
|
||||
azure = 'azure',
|
||||
gcs = 'gcs',
|
||||
}
|
||||
|
||||
// Deliberately do not include `source` as a default repository since we treat it as a flag
|
||||
export const DEFAULT_REPOSITORY_TYPES: RepositoryType[] = [
|
||||
REPOSITORY_TYPES.fs,
|
||||
REPOSITORY_TYPES.url,
|
||||
];
|
||||
|
||||
export const PLUGIN_REPOSITORY_TYPES: RepositoryType[] = [
|
||||
REPOSITORY_TYPES.s3,
|
||||
REPOSITORY_TYPES.hdfs,
|
||||
REPOSITORY_TYPES.azure,
|
||||
REPOSITORY_TYPES.gcs,
|
||||
];
|
||||
|
||||
export const REPOSITORY_PLUGINS_MAP: { [key: string]: RepositoryType } = {
|
||||
'repository-s3': REPOSITORY_TYPES.s3,
|
||||
'repository-hdfs': REPOSITORY_TYPES.hdfs,
|
||||
'repository-azure': REPOSITORY_TYPES.azure,
|
||||
'repository-gcs': REPOSITORY_TYPES.gcs,
|
||||
};
|
||||
|
||||
export const APP_PERMISSIONS = ['monitor', 'create_snapshot', 'cluster:admin/repository'];
|
31
x-pack/plugins/snapshot_restore/common/lib/flatten.test.ts
Normal file
31
x-pack/plugins/snapshot_restore/common/lib/flatten.test.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { flatten } from './flatten';
|
||||
|
||||
describe('flatten()', () => {
|
||||
it('should return an empty object', () => {
|
||||
expect(flatten({})).toEqual({});
|
||||
});
|
||||
|
||||
it('should flatten a nested object', () => {
|
||||
expect(
|
||||
flatten({
|
||||
foo: 'bar',
|
||||
nested: {
|
||||
test: 'value',
|
||||
list: [{ fruit: 'apple' }, { fruit: 'banana' }],
|
||||
},
|
||||
'with.dot': 123,
|
||||
})
|
||||
).toEqual({
|
||||
foo: 'bar',
|
||||
'nested.test': 'value',
|
||||
'nested.list.0.fruit': 'apple',
|
||||
'nested.list.1.fruit': 'banana',
|
||||
'with.dot': 123,
|
||||
});
|
||||
});
|
||||
});
|
21
x-pack/plugins/snapshot_restore/common/lib/flatten.ts
Normal file
21
x-pack/plugins/snapshot_restore/common/lib/flatten.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const flatten = (source: any, path: any[] = []): { [key: string]: any } => {
|
||||
if (!(source instanceof Object)) {
|
||||
return {
|
||||
[path.join('.')]: source,
|
||||
};
|
||||
}
|
||||
|
||||
return Object.keys(source).reduce((result, key) => {
|
||||
const flattened: any = flatten(source[key], [...path, key]);
|
||||
return {
|
||||
...result,
|
||||
...flattened,
|
||||
};
|
||||
}, {});
|
||||
};
|
6
x-pack/plugins/snapshot_restore/common/lib/index.ts
Normal file
6
x-pack/plugins/snapshot_restore/common/lib/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
* 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 { flatten } from './flatten';
|
8
x-pack/plugins/snapshot_restore/common/types/index.ts
Normal file
8
x-pack/plugins/snapshot_restore/common/types/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './repository';
|
||||
export * from './snapshot';
|
159
x-pack/plugins/snapshot_restore/common/types/repository.ts
Normal file
159
x-pack/plugins/snapshot_restore/common/types/repository.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* 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 type FSRepositoryType = 'fs';
|
||||
export type ReadonlyRepositoryType = 'url';
|
||||
export type SourceRepositoryType = 'source';
|
||||
export type S3RepositoryType = 's3';
|
||||
export type HDFSRepositoryType = 'hdfs';
|
||||
export type AzureRepositoryType = 'azure';
|
||||
export type GCSRepositoryType = 'gcs';
|
||||
|
||||
export type RepositoryType =
|
||||
| FSRepositoryType
|
||||
| ReadonlyRepositoryType
|
||||
| SourceRepositoryType
|
||||
| S3RepositoryType
|
||||
| HDFSRepositoryType
|
||||
| AzureRepositoryType
|
||||
| GCSRepositoryType;
|
||||
|
||||
export interface FSRepository {
|
||||
name: string;
|
||||
type: FSRepositoryType;
|
||||
settings: {
|
||||
location: string;
|
||||
compress?: boolean;
|
||||
chunkSize?: string | null;
|
||||
maxRestoreBytesPerSec?: string;
|
||||
maxSnapshotBytesPerSec?: string;
|
||||
readonly?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReadonlyRepository {
|
||||
name: string;
|
||||
type: ReadonlyRepositoryType;
|
||||
settings: {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface S3Repository {
|
||||
name: string;
|
||||
type: S3RepositoryType;
|
||||
settings: {
|
||||
bucket: string;
|
||||
client?: string;
|
||||
basePath?: string;
|
||||
compress?: boolean;
|
||||
chunkSize?: string | null;
|
||||
serverSideEncryption?: boolean;
|
||||
bufferSize?: string;
|
||||
cannedAcl?: string;
|
||||
storageClass?: string;
|
||||
maxRestoreBytesPerSec?: string;
|
||||
maxSnapshotBytesPerSec?: string;
|
||||
readonly?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HDFSRepository {
|
||||
name: string;
|
||||
type: HDFSRepositoryType;
|
||||
settings: {
|
||||
uri: string;
|
||||
path: string;
|
||||
loadDefaults?: boolean;
|
||||
compress?: boolean;
|
||||
chunkSize?: string | null;
|
||||
maxRestoreBytesPerSec?: string;
|
||||
maxSnapshotBytesPerSec?: string;
|
||||
readonly?: boolean;
|
||||
['security.principal']?: string;
|
||||
[key: string]: any; // For conf.* settings
|
||||
};
|
||||
}
|
||||
|
||||
export interface AzureRepository {
|
||||
name: string;
|
||||
type: AzureRepositoryType;
|
||||
settings: {
|
||||
client?: string;
|
||||
container?: string;
|
||||
basePath?: string;
|
||||
locationMode?: string;
|
||||
compress?: boolean;
|
||||
chunkSize?: string | null;
|
||||
maxRestoreBytesPerSec?: string;
|
||||
maxSnapshotBytesPerSec?: string;
|
||||
readonly?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GCSRepository {
|
||||
name: string;
|
||||
type: GCSRepositoryType;
|
||||
settings: {
|
||||
bucket: string;
|
||||
client?: string;
|
||||
basePath?: string;
|
||||
compress?: boolean;
|
||||
chunkSize?: string | null;
|
||||
maxRestoreBytesPerSec?: string;
|
||||
maxSnapshotBytesPerSec?: string;
|
||||
readonly?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EmptyRepository {
|
||||
name: string;
|
||||
type: null;
|
||||
settings: {
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SourceRepository<T> {
|
||||
name: string;
|
||||
type: SourceRepositoryType;
|
||||
settings: SourceRepositorySettings<T>;
|
||||
}
|
||||
|
||||
export type SourceRepositorySettings<T> = T extends FSRepositoryType
|
||||
? FSRepository['settings']
|
||||
: T extends S3RepositoryType
|
||||
? S3Repository['settings']
|
||||
: T extends HDFSRepositoryType
|
||||
? HDFSRepository['settings']
|
||||
: T extends AzureRepositoryType
|
||||
? AzureRepository['settings']
|
||||
: T extends GCSRepositoryType
|
||||
? GCSRepository['settings']
|
||||
: any & {
|
||||
delegateType: T;
|
||||
};
|
||||
|
||||
export type Repository<T = null> =
|
||||
| FSRepository
|
||||
| ReadonlyRepository
|
||||
| S3Repository
|
||||
| HDFSRepository
|
||||
| AzureRepository
|
||||
| GCSRepository
|
||||
| SourceRepository<T>;
|
||||
|
||||
export interface ValidRepositoryVerification {
|
||||
valid: true;
|
||||
response: object;
|
||||
}
|
||||
|
||||
export interface InvalidRepositoryVerification {
|
||||
valid: false;
|
||||
error: object;
|
||||
}
|
||||
|
||||
export type RepositoryVerification = ValidRepositoryVerification | InvalidRepositoryVerification;
|
31
x-pack/plugins/snapshot_restore/common/types/snapshot.ts
Normal file
31
x-pack/plugins/snapshot_restore/common/types/snapshot.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface SnapshotDetails {
|
||||
repository: string;
|
||||
snapshot: string;
|
||||
uuid: string;
|
||||
versionId: number;
|
||||
version: string;
|
||||
indices: string[];
|
||||
includeGlobalState: number;
|
||||
state: string;
|
||||
/** e.g. '2019-04-05T21:56:40.438Z' */
|
||||
startTime: string;
|
||||
startTimeInMillis: number;
|
||||
/** e.g. '2019-04-05T21:56:45.210Z' */
|
||||
endTime: string;
|
||||
endTimeInMillis: number;
|
||||
durationInMillis: number;
|
||||
indexFailures: any[];
|
||||
shards: SnapshotDetailsShardsStatus;
|
||||
}
|
||||
|
||||
interface SnapshotDetailsShardsStatus {
|
||||
total: number;
|
||||
failed: number;
|
||||
successful: number;
|
||||
}
|
39
x-pack/plugins/snapshot_restore/index.ts
Normal file
39
x-pack/plugins/snapshot_restore/index.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { Legacy } from 'kibana';
|
||||
import { resolve } from 'path';
|
||||
import { PLUGIN } from './common/constants';
|
||||
import { Plugin as SnapshotRestorePlugin } from './plugin';
|
||||
import { createShim } from './shim';
|
||||
|
||||
export function snapshotRestore(kibana: any) {
|
||||
return new kibana.Plugin({
|
||||
id: PLUGIN.ID,
|
||||
configPrefix: 'xpack.snapshot_restore',
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
require: ['kibana', 'elasticsearch', 'xpack_main'],
|
||||
uiExports: {
|
||||
managementSections: ['plugins/snapshot_restore'],
|
||||
},
|
||||
init(server: Legacy.Server) {
|
||||
const { core, plugins } = createShim(server, PLUGIN.ID);
|
||||
const { i18n } = core;
|
||||
const snapshotRestorePlugin = new SnapshotRestorePlugin();
|
||||
|
||||
// Start plugin
|
||||
snapshotRestorePlugin.start(core, plugins);
|
||||
|
||||
// Register license checker
|
||||
plugins.license.registerLicenseChecker(
|
||||
server,
|
||||
PLUGIN.ID,
|
||||
PLUGIN.getI18nName(i18n.translate),
|
||||
PLUGIN.MINIMUM_LICENSE_REQUIRED
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
17
x-pack/plugins/snapshot_restore/plugin.ts
Normal file
17
x-pack/plugins/snapshot_restore/plugin.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { registerRoutes } from './server/routes/api/register_routes';
|
||||
import { Core, Plugins } from './shim';
|
||||
|
||||
export class Plugin {
|
||||
public start(core: Core, plugins: Plugins): void {
|
||||
const router = core.http.createRouter(API_BASE_PATH);
|
||||
|
||||
// Register routes
|
||||
registerRoutes(router, plugins);
|
||||
}
|
||||
}
|
107
x-pack/plugins/snapshot_restore/public/app/app.tsx
Normal file
107
x-pack/plugins/snapshot_restore/public/app/app.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { Redirect, Route, Switch } from 'react-router-dom';
|
||||
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 { useAppDependencies } from './index';
|
||||
|
||||
export const App: React.FunctionComponent = () => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
// Load permissions
|
||||
const {
|
||||
error: permissionsError,
|
||||
loading: loadingPermissions,
|
||||
data: { hasPermission, missingClusterPrivileges } = {
|
||||
hasPermission: true,
|
||||
missingClusterPrivileges: [],
|
||||
},
|
||||
} = loadPermissions();
|
||||
|
||||
if (loadingPermissions) {
|
||||
return (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.app.checkingPermissionsDescription"
|
||||
defaultMessage="Checking permissions…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
}
|
||||
|
||||
if (permissionsError) {
|
||||
return (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.app.checkingPermissionsErrorMessage"
|
||||
defaultMessage="Error checking permissions"
|
||||
/>
|
||||
}
|
||||
error={permissionsError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasPermission) {
|
||||
return (
|
||||
<EuiPageContent horizontalPosition="center">
|
||||
<EuiEmptyPrompt
|
||||
iconType="securityApp"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.app.deniedPermissionTitle"
|
||||
defaultMessage="You're missing cluster privileges"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.app.deniedPermissionDescription"
|
||||
defaultMessage="To use Snapshot Repositories, you must have {clusterPrivilegesCount,
|
||||
plural, one {this cluster privilege} other {these cluster privileges}}: {clusterPrivileges}."
|
||||
values={{
|
||||
clusterPrivileges: missingClusterPrivileges.join(', '),
|
||||
clusterPrivilegesCount: missingClusterPrivileges.length,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
);
|
||||
}
|
||||
|
||||
const sections: Section[] = ['repositories', 'snapshots'];
|
||||
const sectionsRegex = sections.join('|');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Switch>
|
||||
<Route exact path={`${BASE_PATH}/add_repository`} component={RepositoryAdd} />
|
||||
<Route exact path={`${BASE_PATH}/edit_repository/:name*`} component={RepositoryEdit} />
|
||||
<Route
|
||||
exact
|
||||
path={`${BASE_PATH}/:section(${sectionsRegex})/:repositoryName?/:snapshotId*`}
|
||||
component={SnapshotRestoreHome}
|
||||
/>
|
||||
<Redirect from={`${BASE_PATH}`} to={`${BASE_PATH}/${DEFAULT_SECTION}`} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { useAppDependencies } from '../index';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const DataPlaceholder: React.SFC<Props> = ({ data, children }) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
|
||||
if (data != null) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return i18n.translate('xpack.snapshotRestore.dataPlaceholderLabel', {
|
||||
defaultMessage: '-',
|
||||
});
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { DataPlaceholder } from './data_placeholder';
|
||||
export { RepositoryDeleteProvider } from './repository_delete_provider';
|
||||
export { RepositoryForm } from './repository_form';
|
||||
export { RepositoryVerificationBadge } from './repository_verification_badge';
|
||||
export { RepositoryTypeLogo } from './repository_type_logo';
|
||||
export { SectionError } from './section_error';
|
||||
export { SectionLoading } from './section_loading';
|
|
@ -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, { Fragment, useRef, useState } from 'react';
|
||||
import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
|
||||
import { Repository } from '../../../common/types';
|
||||
import { useAppDependencies } from '../index';
|
||||
import { deleteRepositories } from '../services/http';
|
||||
|
||||
interface Props {
|
||||
children: (deleteRepository: DeleteRepository) => React.ReactElement;
|
||||
}
|
||||
|
||||
export type DeleteRepository = (
|
||||
names: Array<Repository['name']>,
|
||||
onSuccess?: OnSuccessCallback
|
||||
) => void;
|
||||
|
||||
type OnSuccessCallback = (repositoriesDeleted: Array<Repository['name']>) => void;
|
||||
|
||||
export const RepositoryDeleteProvider: React.FunctionComponent<Props> = ({ children }) => {
|
||||
const {
|
||||
core: {
|
||||
i18n,
|
||||
notification: { toastNotifications },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const [repositoryNames, setRepositoryNames] = useState<Array<Repository['name']>>([]);
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const onSuccessCallback = useRef<OnSuccessCallback | null>(null);
|
||||
|
||||
const deleteRepositoryPrompt: DeleteRepository = (names, onSuccess = () => undefined) => {
|
||||
if (!names || !names.length) {
|
||||
throw new Error('No repository names specified for deletion');
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
setRepositoryNames(names);
|
||||
onSuccessCallback.current = onSuccess;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setRepositoryNames([]);
|
||||
};
|
||||
|
||||
const deleteRepository = () => {
|
||||
const repositoriesToDelete = [...repositoryNames];
|
||||
deleteRepositories(repositoriesToDelete).then(({ data: { itemsDeleted, errors }, error }) => {
|
||||
// Surface success notifications
|
||||
if (itemsDeleted && itemsDeleted.length) {
|
||||
const hasMultipleSuccesses = itemsDeleted.length > 1;
|
||||
const successMessage = hasMultipleSuccesses
|
||||
? i18n.translate(
|
||||
'xpack.snapshotRestore.deleteRepository.successMultipleNotificationTitle',
|
||||
{
|
||||
defaultMessage: 'Removed {count} repositories',
|
||||
values: { count: itemsDeleted.length },
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.snapshotRestore.deleteRepository.successSingleNotificationTitle',
|
||||
{
|
||||
defaultMessage: "Removed repository '{name}'",
|
||||
values: { name: itemsDeleted[0] },
|
||||
}
|
||||
);
|
||||
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 repository(ies)
|
||||
if (error || (errors && errors.length)) {
|
||||
const hasMultipleErrors =
|
||||
(errors && errors.length > 1) || (error && repositoriesToDelete.length > 1);
|
||||
const errorMessage = hasMultipleErrors
|
||||
? i18n.translate(
|
||||
'xpack.snapshotRestore.deleteRepository.errorMultipleNotificationTitle',
|
||||
{
|
||||
defaultMessage: 'Error removing {count} repositories',
|
||||
values: {
|
||||
count: (errors && errors.length) || repositoriesToDelete.length,
|
||||
},
|
||||
}
|
||||
)
|
||||
: i18n.translate('xpack.snapshotRestore.deleteRepository.errorSingleNotificationTitle', {
|
||||
defaultMessage: "Error removing repository '{name}'",
|
||||
values: { name: (errors && errors[0].name) || repositoriesToDelete[0] },
|
||||
});
|
||||
toastNotifications.addDanger(errorMessage);
|
||||
}
|
||||
});
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const renderModal = () => {
|
||||
if (!isModalOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isSingle = repositoryNames.length === 1;
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={
|
||||
isSingle ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteRepository.confirmModal.deleteSingleTitle"
|
||||
defaultMessage="Remove repository '{name}'?"
|
||||
values={{ name: repositoryNames[0] }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteRepository.confirmModal.deleteMultipleTitle"
|
||||
defaultMessage="Remove {count} repositories?"
|
||||
values={{ count: repositoryNames.length }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onCancel={closeModal}
|
||||
onConfirm={deleteRepository}
|
||||
cancelButtonText={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteRepository.confirmModal.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
}
|
||||
confirmButtonText={
|
||||
isSingle ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteRepository.confirmModal.confirmSingleButtonLabel"
|
||||
defaultMessage="Remove repository"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteRepository.confirmModal.confirmMultipleButtonLabel"
|
||||
defaultMessage="Remove repositories"
|
||||
/>
|
||||
)
|
||||
}
|
||||
buttonColor="danger"
|
||||
data-test-subj="srDeleteRepositoryConfirmationModal"
|
||||
>
|
||||
{isSingle ? (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteRepository.confirmModal.deleteSingleDescription"
|
||||
defaultMessage="The snapshots in this repository will still exist, but Elasticsearch won’t have access to them."
|
||||
/>
|
||||
</p>
|
||||
) : (
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteRepository.confirmModal.deleteMultipleListDescription"
|
||||
defaultMessage="You are about to remove these repositories:"
|
||||
/>
|
||||
</p>
|
||||
<ul>
|
||||
{repositoryNames.map(name => (
|
||||
<li key={name}>{name}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteRepository.confirmModal.deleteMultipleDescription"
|
||||
defaultMessage="The snapshots in these repositories will still exist, but Elasticsearch won't have access to them."
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{children(deleteRepositoryPrompt)}
|
||||
{renderModal()}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -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 { RepositoryForm } from './repository_form';
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { EuiForm } from '@elastic/eui';
|
||||
import { Repository, EmptyRepository } from '../../../../common/types';
|
||||
import { flatten } from '../../../../common/lib';
|
||||
|
||||
import { RepositoryValidation, validateRepository } from '../../services/validation';
|
||||
import { RepositoryFormStepOne } from './step_one';
|
||||
import { RepositoryFormStepTwo } from './step_two';
|
||||
|
||||
interface Props {
|
||||
repository: Repository | EmptyRepository;
|
||||
isEditing?: boolean;
|
||||
isSaving: boolean;
|
||||
saveError?: React.ReactNode;
|
||||
clearSaveError: () => void;
|
||||
onSave: (repository: Repository | EmptyRepository) => void;
|
||||
}
|
||||
|
||||
export const RepositoryForm: React.FunctionComponent<Props> = ({
|
||||
repository: originalRepository,
|
||||
isEditing,
|
||||
isSaving,
|
||||
saveError,
|
||||
clearSaveError,
|
||||
onSave,
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState<1 | 2>(isEditing ? 2 : 1);
|
||||
|
||||
// Repository state
|
||||
const [repository, setRepository] = useState<Repository | EmptyRepository>({
|
||||
...originalRepository,
|
||||
settings: {
|
||||
...originalRepository.settings,
|
||||
},
|
||||
});
|
||||
|
||||
// Repository validation state
|
||||
const [validation, setValidation] = useState<RepositoryValidation>({
|
||||
isValid: true,
|
||||
errors: {},
|
||||
});
|
||||
|
||||
const updateRepository = (updatedFields: any): void => {
|
||||
const newRepository = { ...repository, ...updatedFields };
|
||||
setRepository(newRepository);
|
||||
};
|
||||
|
||||
const saveRepository = () => {
|
||||
const newValidation = validateRepository(repository, true);
|
||||
const { isValid } = newValidation;
|
||||
setValidation(newValidation);
|
||||
if (isValid) {
|
||||
onSave(repository);
|
||||
}
|
||||
};
|
||||
|
||||
const goToNextStep = () => {
|
||||
const newValidation = validateRepository(repository, false);
|
||||
const { isValid } = newValidation;
|
||||
setValidation(newValidation);
|
||||
if (isValid) {
|
||||
setCurrentStep(2);
|
||||
}
|
||||
};
|
||||
|
||||
const goToPreviousStep = () => {
|
||||
if (isEditing) {
|
||||
return;
|
||||
}
|
||||
setValidation({
|
||||
isValid: true,
|
||||
errors: {},
|
||||
});
|
||||
setCurrentStep(1);
|
||||
clearSaveError();
|
||||
};
|
||||
|
||||
const hasValidationErrors: boolean = !validation.isValid;
|
||||
const validationErrors = Object.entries(flatten(validation.errors)).reduce(
|
||||
(acc: string[], [key, value]) => {
|
||||
return [...acc, value];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const renderStepOne = () => (
|
||||
<RepositoryFormStepOne
|
||||
repository={repository}
|
||||
onNext={() => goToNextStep()}
|
||||
updateRepository={updateRepository}
|
||||
validation={validation}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderStepTwo = () => (
|
||||
<RepositoryFormStepTwo
|
||||
repository={repository as Repository}
|
||||
isEditing={isEditing}
|
||||
isSaving={isSaving}
|
||||
onSave={saveRepository}
|
||||
updateRepository={updateRepository}
|
||||
validation={validation}
|
||||
saveError={saveError}
|
||||
onBack={() => goToPreviousStep()}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiForm isInvalid={hasValidationErrors} error={validationErrors}>
|
||||
{currentStep === 1 && !isEditing ? renderStepOne() : renderStepTwo()}
|
||||
</EuiForm>
|
||||
);
|
||||
};
|
||||
|
||||
RepositoryForm.defaultProps = {
|
||||
isEditing: false,
|
||||
isSaving: false,
|
||||
};
|
|
@ -0,0 +1,385 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiCard,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiFlexGrid,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { Repository, RepositoryType, EmptyRepository } from '../../../../common/types';
|
||||
import { REPOSITORY_TYPES } from '../../../../common/constants';
|
||||
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { documentationLinksService } from '../../services/documentation';
|
||||
import { loadRepositoryTypes } from '../../services/http';
|
||||
import { textService } from '../../services/text';
|
||||
import { RepositoryValidation } from '../../services/validation';
|
||||
import { SectionError, SectionLoading, RepositoryTypeLogo } from '../';
|
||||
|
||||
interface Props {
|
||||
repository: Repository | EmptyRepository;
|
||||
onNext: () => void;
|
||||
updateRepository: (updatedFields: any) => void;
|
||||
validation: RepositoryValidation;
|
||||
}
|
||||
|
||||
export const RepositoryFormStepOne: React.FunctionComponent<Props> = ({
|
||||
repository,
|
||||
onNext,
|
||||
updateRepository,
|
||||
validation,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
// Load repository types
|
||||
const {
|
||||
error: repositoryTypesError,
|
||||
loading: repositoryTypesLoading,
|
||||
data: repositoryTypes = [],
|
||||
} = loadRepositoryTypes();
|
||||
|
||||
const hasValidationErrors: boolean = !validation.isValid;
|
||||
|
||||
const onTypeChange = (newType: RepositoryType) => {
|
||||
if (repository.type === REPOSITORY_TYPES.source) {
|
||||
updateRepository({
|
||||
settings: {
|
||||
delegateType: newType,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
updateRepository({
|
||||
type: newType,
|
||||
settings: {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const pluginDocLink = (
|
||||
<EuiLink href={documentationLinksService.getRepositoryPluginDocUrl()} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.typePluginsDocLinkText"
|
||||
defaultMessage="Learn more about plugins."
|
||||
/>
|
||||
</EuiLink>
|
||||
);
|
||||
|
||||
const renderNameField = () => (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.nameDescriptionTitle"
|
||||
defaultMessage="Repository name"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.nameDescription"
|
||||
defaultMessage="A unique name for the repository."
|
||||
/>
|
||||
}
|
||||
idAria="repositoryNameDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.nameLabel"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
}
|
||||
describedByIds={['repositoryNameDescription']}
|
||||
isInvalid={Boolean(hasValidationErrors && validation.errors.name)}
|
||||
error={validation.errors.name}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={repository.name}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepository({
|
||||
name: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
|
||||
const renderTypeCard = (type: RepositoryType, index: number) => {
|
||||
const isSelectedType =
|
||||
(repository.type === REPOSITORY_TYPES.source
|
||||
? repository.settings.delegateType
|
||||
: repository.type) === type;
|
||||
const displayName = textService.getRepositoryTypeName(type);
|
||||
|
||||
return (
|
||||
<EuiFlexItem key={index}>
|
||||
<EuiCard
|
||||
title={displayName}
|
||||
icon={<RepositoryTypeLogo type={type} size="l" />}
|
||||
footer={
|
||||
<EuiButtonEmpty
|
||||
href={documentationLinksService.getRepositoryTypeDocUrl(type)}
|
||||
target="_blank"
|
||||
size="xs"
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.typeDocsLinkText"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
selectable={{
|
||||
onClick: () => onTypeChange(type),
|
||||
isSelected: isSelectedType,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTypes = () => {
|
||||
if (repositoryTypesError) {
|
||||
return (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.loadingRepositoryTypesErrorMessage"
|
||||
defaultMessage="Error loading repository types"
|
||||
/>
|
||||
}
|
||||
error={repositoryTypesError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (repositoryTypesLoading) {
|
||||
return (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.loadingRepositoryTypesDescription"
|
||||
defaultMessage="Loading repository types…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
}
|
||||
|
||||
if (!repositoryTypes.length) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.noRepositoryTypesErrorTitle"
|
||||
defaultMessage="No repository types available"
|
||||
/>
|
||||
}
|
||||
color="warning"
|
||||
data-test-subj="noRepositoryTypesError"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.noRepositoryTypesErrorMessage"
|
||||
defaultMessage="You can install plugins to enable different repository types. {docLink}"
|
||||
values={{
|
||||
docLink: pluginDocLink,
|
||||
}}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGrid columns={4}>
|
||||
{repositoryTypes.map((type: RepositoryType, index: number) => renderTypeCard(type, index))}
|
||||
</EuiFlexGrid>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTypeField = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.typeDescriptionTitle"
|
||||
defaultMessage="Repository type"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText id="repositoryTypeDescription" size="s" color="subdued">
|
||||
{repositoryTypes.includes(REPOSITORY_TYPES.fs) &&
|
||||
repositoryTypes.includes(REPOSITORY_TYPES.url) ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.defaultTypeDescription"
|
||||
defaultMessage="Elasticsearch supports file system and read-only URL repositories.
|
||||
Additional types require plugins. {docLink}"
|
||||
values={{
|
||||
docLink: pluginDocLink,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.cloudTypeDescription"
|
||||
defaultMessage="Elasticsearch provides core plugins for custom repositories. {docLink}"
|
||||
values={{
|
||||
docLink: pluginDocLink,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace
|
||||
describedByIds={['repositoryTypeDescription']}
|
||||
fullWidth
|
||||
isInvalid={Boolean(hasValidationErrors && validation.errors.type)}
|
||||
error={validation.errors.type}
|
||||
>
|
||||
{renderTypes()}
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSourceOnlyToggle = () => (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.sourceOnlyDescriptionTitle"
|
||||
defaultMessage="Source-only snapshots"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<Fragment>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.sourceOnlyDescription"
|
||||
defaultMessage="Creates source-only snapshots that take up to 50% less space. {docLink}"
|
||||
values={{
|
||||
docLink: (
|
||||
<EuiLink
|
||||
href={documentationLinksService.getRepositoryTypeDocUrl(REPOSITORY_TYPES.source)}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.sourceOnlyDocLinkText"
|
||||
defaultMessage="Learn more about source-only repositories."
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
idAria="sourceOnlyDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow hasEmptyLabelSpace={true} fullWidth describedByIds={['sourceOnlyDescription']}>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.sourceOnlyLabel"
|
||||
defaultMessage="Source-only snapshots"
|
||||
/>
|
||||
}
|
||||
checked={repository.type === REPOSITORY_TYPES.source}
|
||||
onChange={e => {
|
||||
if (e.target.checked) {
|
||||
updateRepository({
|
||||
type: REPOSITORY_TYPES.source,
|
||||
settings: {
|
||||
...repository.settings,
|
||||
delegateType: repository.type,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const {
|
||||
settings: { delegateType, ...rest },
|
||||
} = repository;
|
||||
updateRepository({
|
||||
type: delegateType || null,
|
||||
settings: rest,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
|
||||
const renderActions = () => (
|
||||
<EuiButton
|
||||
color="primary"
|
||||
onClick={onNext}
|
||||
fill
|
||||
iconType="arrowRight"
|
||||
iconSide="right"
|
||||
data-test-subj="srRepositoryFormNextButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.nextButtonLabel"
|
||||
defaultMessage="Next"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
const renderFormValidationError = () => {
|
||||
if (!hasValidationErrors) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.validationErrorTitle"
|
||||
defaultMessage="Fix errors before continuing."
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
data-test-subj="repositoryFormError"
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{renderNameField()}
|
||||
{renderTypeField()}
|
||||
{renderSourceOnlyToggle()}
|
||||
{renderFormValidationError()}
|
||||
{renderActions()}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { Repository } from '../../../../common/types';
|
||||
import { REPOSITORY_TYPES } from '../../../../common/constants';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { RepositoryValidation } from '../../services/validation';
|
||||
import { documentationLinksService } from '../../services/documentation';
|
||||
import { TypeSettings } from './type_settings';
|
||||
import { textService } from '../../services/text';
|
||||
|
||||
interface Props {
|
||||
repository: Repository;
|
||||
isEditing?: boolean;
|
||||
isSaving: boolean;
|
||||
onSave: () => void;
|
||||
updateRepository: (updatedFields: any) => void;
|
||||
validation: RepositoryValidation;
|
||||
saveError?: React.ReactNode;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const RepositoryFormStepTwo: React.FunctionComponent<Props> = ({
|
||||
repository,
|
||||
isEditing,
|
||||
isSaving,
|
||||
onSave,
|
||||
updateRepository,
|
||||
validation,
|
||||
saveError,
|
||||
onBack,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const hasValidationErrors: boolean = !validation.isValid;
|
||||
const {
|
||||
name,
|
||||
type,
|
||||
settings: { delegateType },
|
||||
} = repository;
|
||||
const typeForDocs = type === REPOSITORY_TYPES.source ? delegateType : type;
|
||||
|
||||
const renderSettings = () => (
|
||||
<Fragment>
|
||||
{/* Repository settings title */}
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.fields.settingsTitle"
|
||||
defaultMessage="{repositoryName} settings"
|
||||
values={{
|
||||
repositoryName: `'${name}'`,
|
||||
}}
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
flush="right"
|
||||
href={documentationLinksService.getRepositoryTypeDocUrl(typeForDocs)}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.repositoryTypeDocLink"
|
||||
defaultMessage="{repositoryType} repository docs"
|
||||
values={{
|
||||
repositoryType: textService.getRepositoryTypeName(typeForDocs),
|
||||
}}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
{/* Repository settings fields */}
|
||||
<TypeSettings
|
||||
repository={repository}
|
||||
updateRepository={updateRepository}
|
||||
settingErrors={
|
||||
hasValidationErrors && validation.errors.settings ? validation.errors.settings : {}
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const renderActions = () => {
|
||||
const saveLabel = isEditing ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.registerButtonLabel"
|
||||
defaultMessage="Register"
|
||||
/>
|
||||
);
|
||||
const savingLabel = (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.savingButtonLabel"
|
||||
defaultMessage="Saving…"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center">
|
||||
{isEditing ? null : (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
color="primary"
|
||||
iconType="arrowLeft"
|
||||
onClick={onBack}
|
||||
data-test-subj="srRepositoryFormSubmitButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.backButtonLabel"
|
||||
defaultMessage="Back"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="secondary"
|
||||
iconType="check"
|
||||
onClick={onSave}
|
||||
fill
|
||||
data-test-subj="srRepositoryFormSubmitButton"
|
||||
isLoading={isSaving}
|
||||
>
|
||||
{isSaving ? savingLabel : saveLabel}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFormValidationError = () => {
|
||||
if (!hasValidationErrors) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.validationErrorTitle"
|
||||
defaultMessage="Fix errors before continuing."
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
data-test-subj="repositoryFormError"
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSaveError = () => {
|
||||
if (!saveError) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
{saveError}
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{renderSettings()}
|
||||
{renderFormValidationError()}
|
||||
{renderSaveError()}
|
||||
{renderActions()}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,474 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import {
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiSwitch,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { AzureRepository, Repository } from '../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { RepositorySettingsValidation } from '../../../services/validation';
|
||||
import { textService } from '../../../services/text';
|
||||
|
||||
interface Props {
|
||||
repository: AzureRepository;
|
||||
updateRepositorySettings: (
|
||||
updatedSettings: Partial<Repository['settings']>,
|
||||
replaceSettings?: boolean
|
||||
) => void;
|
||||
settingErrors: RepositorySettingsValidation;
|
||||
}
|
||||
|
||||
export const AzureSettings: React.FunctionComponent<Props> = ({
|
||||
repository,
|
||||
updateRepositorySettings,
|
||||
settingErrors,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
const {
|
||||
settings: {
|
||||
client,
|
||||
container,
|
||||
basePath,
|
||||
compress,
|
||||
chunkSize,
|
||||
readonly,
|
||||
locationMode,
|
||||
maxRestoreBytesPerSec,
|
||||
maxSnapshotBytesPerSec,
|
||||
},
|
||||
} = repository;
|
||||
const hasErrors: boolean = Boolean(Object.keys(settingErrors).length);
|
||||
|
||||
const locationModeOptions = ['primary_only', 'secondary_only'].map(option => ({
|
||||
value: option,
|
||||
text: option,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* Client field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.clientTitle"
|
||||
defaultMessage="Client"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.clientDescription"
|
||||
defaultMessage="The name of the Azure client."
|
||||
/>
|
||||
}
|
||||
idAria="azureRepositoryClientDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.clientLabel"
|
||||
defaultMessage="Client"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['azureRepositoryClientDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.client)}
|
||||
error={settingErrors.client}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={client || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
client: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Container field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.containerTitle"
|
||||
defaultMessage="Container"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.containerDescription"
|
||||
defaultMessage="The name of the Azure container to use for snapshots."
|
||||
/>
|
||||
}
|
||||
idAria="azureRepositoryContainerDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.containerLabel"
|
||||
defaultMessage="Container"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['azureRepositoryContainerDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.container)}
|
||||
error={settingErrors.container}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={container || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
container: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Base path field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.basePathTitle"
|
||||
defaultMessage="Base path"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.basePathDescription"
|
||||
defaultMessage="The container path to the repository data."
|
||||
/>
|
||||
}
|
||||
idAria="azureRepositoryBasePathDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.basePathLabel"
|
||||
defaultMessage="Base path"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['azureRepositoryBasePathDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.basePath)}
|
||||
error={settingErrors.basePath}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={basePath || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
basePath: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Compress field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.compressTitle"
|
||||
defaultMessage="Snapshot compression"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.compressDescription"
|
||||
defaultMessage="Compresses the index mapping and setting files for snapshots. Data files are not compressed."
|
||||
/>
|
||||
}
|
||||
idAria="azureRepositoryCompressDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['azureRepositoryCompressDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.compress)}
|
||||
error={settingErrors.compress}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.compressLabel"
|
||||
defaultMessage="Compress snapshots"
|
||||
/>
|
||||
}
|
||||
checked={!(compress === false)}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
compress: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Chunk size field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.chunkSizeTitle"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.chunkSizeDescription"
|
||||
defaultMessage="Breaks files into smaller units when taking snapshots."
|
||||
/>
|
||||
}
|
||||
idAria="azureRepositoryChunkSizeDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.chunkSizeLabel"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['azureRepositoryChunkSizeDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.chunkSize)}
|
||||
error={settingErrors.chunkSize}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={chunkSize || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
chunkSize: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Max snapshot bytes field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.maxSnapshotBytesTitle"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.maxSnapshotBytesDescription"
|
||||
defaultMessage="The rate for creating snapshots for each node."
|
||||
/>
|
||||
}
|
||||
idAria="azureRepositoryMaxSnapshotBytesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.maxSnapshotBytesLabel"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['azureRepositoryMaxSnapshotBytesDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.maxSnapshotBytesPerSec)}
|
||||
error={settingErrors.maxSnapshotBytesPerSec}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={maxSnapshotBytesPerSec || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
maxSnapshotBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Max restore bytes field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.maxRestoreBytesTitle"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.maxRestoreBytesDescription"
|
||||
defaultMessage="The snapshot restore rate for each node."
|
||||
/>
|
||||
}
|
||||
idAria="azureRepositoryMaxRestoreBytesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.maxRestoreBytesLabel"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['azureRepositoryMaxRestoreBytesDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.maxRestoreBytesPerSec)}
|
||||
error={settingErrors.maxRestoreBytesPerSec}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={maxRestoreBytesPerSec || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
maxRestoreBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Location mode field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.locationModeTitle"
|
||||
defaultMessage="Location mode"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.locationModeDescription"
|
||||
defaultMessage="The primary or secondary location. If secondary, read-only is true."
|
||||
/>
|
||||
}
|
||||
idAria="azureRepositoryLocationModeDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.locationModeLabel"
|
||||
defaultMessage="Location mode"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['azureRepositoryLocationModeDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.locationMode)}
|
||||
error={settingErrors.locationMode}
|
||||
>
|
||||
<EuiSelect
|
||||
options={locationModeOptions}
|
||||
value={locationMode || locationModeOptions[0].value}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
locationMode: e.target.value,
|
||||
readonly: e.target.value === locationModeOptions[1].value ? true : readonly,
|
||||
});
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Readonly field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.readonlyTitle"
|
||||
defaultMessage="Read-only"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.readonlyDescription"
|
||||
defaultMessage="Only one cluster should have write access to this repository. All other clusters should be read-only."
|
||||
/>
|
||||
}
|
||||
idAria="azureRepositoryReadonlyDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['azureRepositoryReadonlyDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.readonly)}
|
||||
error={settingErrors.readonly}
|
||||
>
|
||||
<EuiSwitch
|
||||
disabled={locationMode === locationModeOptions[1].value}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeAzure.readonlyLabel"
|
||||
defaultMessage="Read-only repository"
|
||||
/>
|
||||
}
|
||||
checked={!!readonly}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
readonly: locationMode === locationModeOptions[1].value ? true : e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,332 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import {
|
||||
EuiCode,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiSwitch,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FSRepository, Repository } from '../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { RepositorySettingsValidation } from '../../../services/validation';
|
||||
import { textService } from '../../../services/text';
|
||||
|
||||
interface Props {
|
||||
repository: FSRepository;
|
||||
updateRepositorySettings: (
|
||||
updatedSettings: Partial<Repository['settings']>,
|
||||
replaceSettings?: boolean
|
||||
) => void;
|
||||
settingErrors: RepositorySettingsValidation;
|
||||
}
|
||||
|
||||
export const FSSettings: React.FunctionComponent<Props> = ({
|
||||
repository,
|
||||
updateRepositorySettings,
|
||||
settingErrors,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const {
|
||||
settings: {
|
||||
location,
|
||||
compress,
|
||||
chunkSize,
|
||||
maxRestoreBytesPerSec,
|
||||
maxSnapshotBytesPerSec,
|
||||
readonly,
|
||||
},
|
||||
} = repository;
|
||||
const hasErrors: boolean = Boolean(Object.keys(settingErrors).length);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* Location field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.locationTitle"
|
||||
defaultMessage="File system location"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<Fragment>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryFor.typeFS.locationDescription"
|
||||
defaultMessage="The location must be registered in the {settingKey} setting on all master and data nodes."
|
||||
values={{
|
||||
settingKey: <EuiCode>path.repo</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
idAria="fsRepositoryLocationDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.locationLabel"
|
||||
defaultMessage="Location (required)"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['fsRepositoryLocationDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.location)}
|
||||
error={settingErrors.location}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={location || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
location: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Compress field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.compressTitle"
|
||||
defaultMessage="Snapshot compression"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.compressDescription"
|
||||
defaultMessage="Compresses the index mapping and setting files for snapshots. Data files are not compressed."
|
||||
/>
|
||||
}
|
||||
idAria="fsRepositoryCompressDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['fsRepositoryCompressDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.compress)}
|
||||
error={settingErrors.compress}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.compressLabel"
|
||||
defaultMessage="Compress snapshots"
|
||||
/>
|
||||
}
|
||||
checked={!!compress}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
compress: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Chunk size field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.chunkSizeTitle"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.chunkSizeDescription"
|
||||
defaultMessage="Breaks files into smaller units when taking snapshots."
|
||||
/>
|
||||
}
|
||||
idAria="fsRepositoryChunkSizeDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.chunkSizeLabel"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['fsRepositoryChunkSizeDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.chunkSize)}
|
||||
error={settingErrors.chunkSize}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={chunkSize || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
chunkSize: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Max snapshot bytes field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.maxSnapshotBytesTitle"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.maxSnapshotBytesDescription"
|
||||
defaultMessage="The rate for creating snapshots for each node."
|
||||
/>
|
||||
}
|
||||
idAria="fsRepositoryMaxSnapshotBytesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.maxSnapshotBytesLabel"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['fsRepositoryMaxSnapshotBytesDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.maxSnapshotBytesPerSec)}
|
||||
error={settingErrors.maxSnapshotBytesPerSec}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={maxSnapshotBytesPerSec || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
maxSnapshotBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Max restore bytes field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.maxRestoreBytesTitle"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.maxRestoreBytesDescription"
|
||||
defaultMessage="The snapshot restore rate for each node."
|
||||
/>
|
||||
}
|
||||
idAria="fsRepositoryMaxRestoreBytesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.maxRestoreBytesLabel"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['fsRepositoryMaxRestoreBytesDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.maxRestoreBytesPerSec)}
|
||||
error={settingErrors.maxRestoreBytesPerSec}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={maxRestoreBytesPerSec || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
maxRestoreBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Readonly field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.readonlyTitle"
|
||||
defaultMessage="Read-only"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.readonlyDescription"
|
||||
defaultMessage="Only one cluster should have write access to this repository. All other clusters should be read-only."
|
||||
/>
|
||||
}
|
||||
idAria="fsRepositoryReadonlyDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['fsRepositoryReadonlyDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.readonly)}
|
||||
error={settingErrors.readonly}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeFS.readonlyLabel"
|
||||
defaultMessage="Read-only repository"
|
||||
/>
|
||||
}
|
||||
checked={!!readonly}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
readonly: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,413 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { EuiDescribedFormGroup, EuiFieldText, EuiFormRow, EuiSwitch, EuiTitle } from '@elastic/eui';
|
||||
import { GCSRepository, Repository } from '../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { RepositorySettingsValidation } from '../../../services/validation';
|
||||
import { textService } from '../../../services/text';
|
||||
|
||||
interface Props {
|
||||
repository: GCSRepository;
|
||||
updateRepositorySettings: (
|
||||
updatedSettings: Partial<Repository['settings']>,
|
||||
replaceSettings?: boolean
|
||||
) => void;
|
||||
settingErrors: RepositorySettingsValidation;
|
||||
}
|
||||
|
||||
export const GCSSettings: React.FunctionComponent<Props> = ({
|
||||
repository,
|
||||
updateRepositorySettings,
|
||||
settingErrors,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
const {
|
||||
settings: {
|
||||
bucket,
|
||||
client,
|
||||
basePath,
|
||||
compress,
|
||||
chunkSize,
|
||||
maxRestoreBytesPerSec,
|
||||
maxSnapshotBytesPerSec,
|
||||
readonly,
|
||||
},
|
||||
} = repository;
|
||||
const hasErrors: boolean = Boolean(Object.keys(settingErrors).length);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* Client field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.clientTitle"
|
||||
defaultMessage="Client"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.clientDescription"
|
||||
defaultMessage="The name of the Google Cloud Storage client."
|
||||
/>
|
||||
}
|
||||
idAria="gcsRepositoryClientDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.clientLabel"
|
||||
defaultMessage="Client"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['gcsRepositoryClientDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.client)}
|
||||
error={settingErrors.client}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={client || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
client: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Bucket field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.bucketTitle"
|
||||
defaultMessage="Bucket"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.bucketDescription"
|
||||
defaultMessage="The name of the Google Cloud Storage bucket to use for snapshots."
|
||||
/>
|
||||
}
|
||||
idAria="gcsRepositoryBucketDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.bucketLabel"
|
||||
defaultMessage="Bucket (required)"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['gcsRepositoryBucketDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.bucket)}
|
||||
error={settingErrors.bucket}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={bucket || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
bucket: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Base path field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.basePathTitle"
|
||||
defaultMessage="Base path"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.basePathDescription"
|
||||
defaultMessage="The bucket path to the repository data."
|
||||
/>
|
||||
}
|
||||
idAria="gcsRepositoryBasePathDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.basePathLabel"
|
||||
defaultMessage="Base path"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['gcsRepositoryBasePathDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.basePath)}
|
||||
error={settingErrors.basePath}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={basePath || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
basePath: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Compress field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.compressTitle"
|
||||
defaultMessage="Compress snapshots"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.compressDescription"
|
||||
defaultMessage="Compresses the index mapping and setting files for snapshots. Data files are not compressed."
|
||||
/>
|
||||
}
|
||||
idAria="gcsRepositoryCompressDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['gcsRepositoryCompressDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.compress)}
|
||||
error={settingErrors.compress}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.compressLabel"
|
||||
defaultMessage="Compress snapshots"
|
||||
/>
|
||||
}
|
||||
checked={!(compress === false)}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
compress: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Chunk size field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.chunkSizeTitle"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.chunkSizeDescription"
|
||||
defaultMessage="Breaks files into smaller units when taking snapshots."
|
||||
/>
|
||||
}
|
||||
idAria="gcsRepositoryChunkSizeDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.chunkSizeLabel"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['gcsRepositoryChunkSizeDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.chunkSize)}
|
||||
error={settingErrors.chunkSize}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={chunkSize || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
chunkSize: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Max snapshot bytes field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.maxSnapshotBytesTitle"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.maxSnapshotBytesDescription"
|
||||
defaultMessage="The rate for creating snapshots for each node."
|
||||
/>
|
||||
}
|
||||
idAria="gcsRepositoryMaxSnapshotBytesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.maxSnapshotBytesLabel"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['gcsRepositoryMaxSnapshotBytesDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.maxSnapshotBytesPerSec)}
|
||||
error={settingErrors.maxSnapshotBytesPerSec}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={maxSnapshotBytesPerSec || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
maxSnapshotBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Max restore bytes field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.maxRestoreBytesTitle"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.maxRestoreBytesDescription"
|
||||
defaultMessage="The snapshot restore rate for each node."
|
||||
/>
|
||||
}
|
||||
idAria="gcsRepositoryMaxRestoreBytesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.maxRestoreBytesLabel"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['gcsRepositoryMaxRestoreBytesDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.maxRestoreBytesPerSec)}
|
||||
error={settingErrors.maxRestoreBytesPerSec}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={maxRestoreBytesPerSec || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
maxRestoreBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Readonly field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.readonlyTitle"
|
||||
defaultMessage="Read-only"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.readonlyDescription"
|
||||
defaultMessage="Only one cluster should have write access to this repository. All other clusters should be read-only."
|
||||
/>
|
||||
}
|
||||
idAria="gcsRepositoryReadonlyDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['gcsRepositoryReadonlyDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.readonly)}
|
||||
error={settingErrors.readonly}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeGCS.readonlyLabel"
|
||||
defaultMessage="Read-only repository"
|
||||
/>
|
||||
}
|
||||
checked={!!readonly}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
readonly: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,579 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiCode,
|
||||
EuiCodeEditor,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { HDFSRepository, Repository, SourceRepository } from '../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { RepositorySettingsValidation } from '../../../services/validation';
|
||||
import { textService } from '../../../services/text';
|
||||
|
||||
interface Props {
|
||||
repository: HDFSRepository | SourceRepository<HDFSRepository>;
|
||||
updateRepositorySettings: (
|
||||
updatedSettings: Partial<Repository['settings']>,
|
||||
replaceSettings?: boolean
|
||||
) => void;
|
||||
settingErrors: RepositorySettingsValidation;
|
||||
}
|
||||
|
||||
export const HDFSSettings: React.FunctionComponent<Props> = ({
|
||||
repository,
|
||||
updateRepositorySettings,
|
||||
settingErrors,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
const {
|
||||
settings: {
|
||||
delegateType,
|
||||
uri,
|
||||
path,
|
||||
loadDefaults,
|
||||
compress,
|
||||
chunkSize,
|
||||
maxRestoreBytesPerSec,
|
||||
maxSnapshotBytesPerSec,
|
||||
readonly,
|
||||
'security.principal': securityPrincipal,
|
||||
...rest // For conf.* settings
|
||||
},
|
||||
} = repository;
|
||||
const hasErrors: boolean = Boolean(Object.keys(settingErrors).length);
|
||||
|
||||
const [additionalConf, setAdditionalConf] = useState<string>(JSON.stringify(rest, null, 2));
|
||||
const [isConfInvalid, setIsConfInvalid] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* URI field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.uriTitle"
|
||||
defaultMessage="URI"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.uriDescription"
|
||||
defaultMessage="The URI address for HDFS."
|
||||
/>
|
||||
}
|
||||
idAria="hdfsRepositoryUriDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.uriLabel"
|
||||
defaultMessage="URI (required)"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['hdfsRepositoryUriDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.uri)}
|
||||
error={settingErrors.uri}
|
||||
>
|
||||
<EuiFieldText
|
||||
prepend={
|
||||
<EuiText size="s" id="hdfsRepositoryUriProtocolDescription">
|
||||
{/* Wrap as string due to prettier not parsing `//` inside JSX correctly (prettier/prettier#2347) */}
|
||||
{'hdfs://'}
|
||||
</EuiText>
|
||||
}
|
||||
defaultValue={uri ? uri.split('hdfs://')[1] : ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
uri: e.target.value ? `hdfs://${e.target.value}` : '',
|
||||
});
|
||||
}}
|
||||
aria-describedby="hdfsRepositoryUriDescription hdfsRepositoryUriProtocolDescription"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Path field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.pathTitle"
|
||||
defaultMessage="Path"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.pathDescription"
|
||||
defaultMessage="The file path where data is stored."
|
||||
/>
|
||||
}
|
||||
idAria="hdfsRepositoryPathDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.pathLabel"
|
||||
defaultMessage="Path (required)"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['hdfsRepositoryPathDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.path)}
|
||||
error={settingErrors.path}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={path || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
path: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Load defaults field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.loadDefaultsTitle"
|
||||
defaultMessage="Load defaults"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.loadDefaultsDescription"
|
||||
defaultMessage="Loads the default Hadoop configuration."
|
||||
/>
|
||||
}
|
||||
idAria="hdfsRepositoryLoadDefaultsDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['hdfsRepositoryLoadDefaultsDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.loadDefaults)}
|
||||
error={settingErrors.loadDefaults}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.loadDefaultsLabel"
|
||||
defaultMessage="Load defaults"
|
||||
/>
|
||||
}
|
||||
checked={!(loadDefaults === false)}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
loadDefaults: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Compress field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.compressTitle"
|
||||
defaultMessage="Snapshot compression"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.compressDescription"
|
||||
defaultMessage="Compresses the index mapping and setting files for snapshots. Data files are not compressed."
|
||||
/>
|
||||
}
|
||||
idAria="hdfsRepositoryCompressDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['hdfsRepositoryCompressDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.compress)}
|
||||
error={settingErrors.compress}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.compressLabel"
|
||||
defaultMessage="Compress snapshots"
|
||||
/>
|
||||
}
|
||||
checked={!(compress === false)}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
compress: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Chunk size field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.chunkSizeTitle"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.chunkSizeDescription"
|
||||
defaultMessage="Breaks files into smaller units when taking snapshots."
|
||||
/>
|
||||
}
|
||||
idAria="hdfsRepositoryChunkSizeDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.chunkSizeLabel"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['hdfsRepositoryChunkSizeDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.chunkSize)}
|
||||
error={settingErrors.chunkSize}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={chunkSize || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
chunkSize: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Security principal field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.securityPrincipalTitle"
|
||||
defaultMessage="Security principal"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.securityPrincipalDescription"
|
||||
defaultMessage="The Kerberos principal to use when connecting to a secured HDFS cluster."
|
||||
/>
|
||||
}
|
||||
idAria="hdfsRepositorySecurityPrincipalDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.securityPrincipalLabel"
|
||||
defaultMessage="Security principal"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['hdfsRepositorySecurityPrincipalDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.securityPrincipal)}
|
||||
error={settingErrors.securityPrincipal}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={securityPrincipal || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
'security.principal': e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Additional HDFS parameters field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.configurationTitle"
|
||||
defaultMessage="Configuration"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<Fragment>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.configurationDescription"
|
||||
defaultMessage="Additional JSON format configuration parameters to add to the Hadoop configuration. Only client-oriented properties from the Hadoop core and HDFS files are recognized."
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
idAria="hdfsRepositoryConfigurationDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.configurationLabel"
|
||||
defaultMessage="Configuration"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['hdfsRepositoryConfigurationDescription']}
|
||||
isInvalid={isConfInvalid}
|
||||
error={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.configurationFormatError"
|
||||
defaultMessage="Invalid JSON format"
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.configurationKeyDescription"
|
||||
defaultMessage="Keys should be in the format {confKeyFormat}."
|
||||
values={{
|
||||
confKeyFormat: <EuiCode>{'conf.<key>'}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiCodeEditor
|
||||
mode="json"
|
||||
theme="textmate"
|
||||
width="100%"
|
||||
value={additionalConf}
|
||||
setOptions={{
|
||||
showLineNumbers: false,
|
||||
tabSize: 2,
|
||||
maxLines: Infinity,
|
||||
}}
|
||||
editorProps={{
|
||||
$blockScrolling: Infinity,
|
||||
}}
|
||||
showGutter={false}
|
||||
minLines={6}
|
||||
aria-label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.configurationAriaLabel"
|
||||
defaultMessage="Additional configuration for HDFS repository '{name}'"
|
||||
values={{
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onChange={(value: string) => {
|
||||
setAdditionalConf(value);
|
||||
try {
|
||||
const parsedConf = JSON.parse(value);
|
||||
setIsConfInvalid(false);
|
||||
updateRepositorySettings(
|
||||
{
|
||||
delegateType,
|
||||
uri,
|
||||
path,
|
||||
loadDefaults,
|
||||
compress,
|
||||
chunkSize,
|
||||
'security.principal': securityPrincipal,
|
||||
...parsedConf,
|
||||
},
|
||||
true
|
||||
);
|
||||
} catch (e) {
|
||||
setIsConfInvalid(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Max snapshot bytes field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.maxSnapshotBytesTitle"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.maxSnapshotBytesDescription"
|
||||
defaultMessage="The rate for creating snapshots for each node."
|
||||
/>
|
||||
}
|
||||
idAria="hdfsRepositoryMaxSnapshotBytesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.maxSnapshotBytesLabel"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['hdfsRepositoryMaxSnapshotBytesDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.maxSnapshotBytesPerSec)}
|
||||
error={settingErrors.maxSnapshotBytesPerSec}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={maxSnapshotBytesPerSec || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
maxSnapshotBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Max restore bytes field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.maxRestoreBytesTitle"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.maxRestoreBytesDescription"
|
||||
defaultMessage="The snapshot restore rate for each node."
|
||||
/>
|
||||
}
|
||||
idAria="hdfsRepositoryMaxRestoreBytesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.maxRestoreBytesLabel"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['hdfsRepositoryMaxRestoreBytesDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.maxRestoreBytesPerSec)}
|
||||
error={settingErrors.maxRestoreBytesPerSec}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={maxRestoreBytesPerSec || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
maxRestoreBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Readonly field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.readonlyTitle"
|
||||
defaultMessage="Read-only"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.readonlyDescription"
|
||||
defaultMessage="Only one cluster should have write access to this repository. All other clusters should be read-only."
|
||||
/>
|
||||
}
|
||||
idAria="hdfsRepositoryReadonlyDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['hdfsRepositoryReadonlyDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.readonly)}
|
||||
error={settingErrors.readonly}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeHDFS.readonlyLabel"
|
||||
defaultMessage="Read-only repository"
|
||||
/>
|
||||
}
|
||||
checked={!!readonly}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
readonly: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { REPOSITORY_TYPES } from '../../../../../common/constants';
|
||||
import { Repository, RepositoryType, EmptyRepository } from '../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { RepositorySettingsValidation } from '../../../services/validation';
|
||||
import { SectionError } from '../../index';
|
||||
|
||||
import { AzureSettings } from './azure_settings';
|
||||
import { FSSettings } from './fs_settings';
|
||||
import { GCSSettings } from './gcs_settings';
|
||||
import { HDFSSettings } from './hdfs_settings';
|
||||
import { ReadonlySettings } from './readonly_settings';
|
||||
import { S3Settings } from './s3_settings';
|
||||
|
||||
interface Props {
|
||||
repository: Repository | EmptyRepository;
|
||||
updateRepository: (updatedFields: Partial<Repository>) => void;
|
||||
settingErrors: RepositorySettingsValidation;
|
||||
}
|
||||
|
||||
export const TypeSettings: React.FunctionComponent<Props> = ({
|
||||
repository,
|
||||
updateRepository,
|
||||
settingErrors,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const { type, settings } = repository;
|
||||
const updateRepositorySettings = (
|
||||
updatedSettings: Partial<Repository['settings']>,
|
||||
replaceSettings?: boolean
|
||||
): void => {
|
||||
if (replaceSettings) {
|
||||
updateRepository({
|
||||
settings: updatedSettings,
|
||||
});
|
||||
} else {
|
||||
updateRepository({
|
||||
settings: {
|
||||
...settings,
|
||||
...updatedSettings,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const typeSettingsMap: { [key: string]: any } = {
|
||||
[REPOSITORY_TYPES.fs]: FSSettings,
|
||||
[REPOSITORY_TYPES.url]: ReadonlySettings,
|
||||
[REPOSITORY_TYPES.azure]: AzureSettings,
|
||||
[REPOSITORY_TYPES.gcs]: GCSSettings,
|
||||
[REPOSITORY_TYPES.hdfs]: HDFSSettings,
|
||||
[REPOSITORY_TYPES.s3]: S3Settings,
|
||||
};
|
||||
|
||||
const renderTypeSettings = (repositoryType: RepositoryType | null) => {
|
||||
if (!repositoryType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const RepositorySettings = typeSettingsMap[repositoryType];
|
||||
if (RepositorySettings) {
|
||||
return (
|
||||
<RepositorySettings
|
||||
repository={repository}
|
||||
updateRepositorySettings={updateRepositorySettings}
|
||||
settingErrors={settingErrors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.errorUnknownRepositoryTypesTitle"
|
||||
defaultMessage="Unknown repository type"
|
||||
/>
|
||||
}
|
||||
error={{
|
||||
data: {
|
||||
error: i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryForm.errorUnknownRepositoryTypesMessage',
|
||||
{
|
||||
defaultMessage: `The repository type '{type}' is not supported.`,
|
||||
values: {
|
||||
type: repositoryType,
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return type === REPOSITORY_TYPES.source
|
||||
? renderTypeSettings(settings.delegateType)
|
||||
: renderTypeSettings(type);
|
||||
};
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiCode,
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormHelpText,
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { ReadonlyRepository, Repository } from '../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { RepositorySettingsValidation } from '../../../services/validation';
|
||||
|
||||
interface Props {
|
||||
repository: ReadonlyRepository;
|
||||
updateRepositorySettings: (
|
||||
updatedSettings: Partial<Repository['settings']>,
|
||||
replaceSettings?: boolean
|
||||
) => void;
|
||||
settingErrors: RepositorySettingsValidation;
|
||||
}
|
||||
|
||||
export const ReadonlySettings: React.FunctionComponent<Props> = ({
|
||||
repository,
|
||||
updateRepositorySettings,
|
||||
settingErrors,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
const {
|
||||
settings: { url },
|
||||
} = repository;
|
||||
const hasErrors: boolean = Boolean(Object.keys(settingErrors).length);
|
||||
|
||||
function getSchemeHelpText(scheme: string): React.ReactNode {
|
||||
switch (scheme) {
|
||||
case 'http':
|
||||
case 'https':
|
||||
case 'ftp':
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeReadonly.urlWhitelistDescription"
|
||||
defaultMessage="This URL must be registered in the {settingKey} setting."
|
||||
values={{
|
||||
settingKey: <EuiCode>repositories.url.allowed_urls</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case 'file':
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeReadonly.urlFilePathDescription"
|
||||
defaultMessage="This file location must be registered in the {settingKey} setting."
|
||||
values={{
|
||||
settingKey: <EuiCode>path.repo</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const schemeOptions = [
|
||||
{
|
||||
value: 'http',
|
||||
text: 'http://',
|
||||
},
|
||||
{
|
||||
value: 'https',
|
||||
text: 'https://',
|
||||
},
|
||||
{
|
||||
value: 'ftp',
|
||||
text: 'ftp://',
|
||||
},
|
||||
{
|
||||
value: 'file',
|
||||
text: 'file://',
|
||||
},
|
||||
];
|
||||
|
||||
const [selectedScheme, selectScheme] = useState(url ? url.split('://')[0] : 'http');
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* URL field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeReadonly.urlTitle"
|
||||
defaultMessage="URL"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<Fragment>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeReadonly.urlDescription"
|
||||
defaultMessage="The location of the snapshots."
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
idAria="readonlyRepositoryUrlDescription"
|
||||
fullWidth
|
||||
>
|
||||
<div>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeReadonly.urlSchemeLabel"
|
||||
defaultMessage="Scheme"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['readonlyRepositoryUrlDescription']}
|
||||
>
|
||||
<EuiSelect
|
||||
options={schemeOptions}
|
||||
value={selectedScheme}
|
||||
onChange={e => selectScheme(e.target.value)}
|
||||
aria-controls="readonlyRepositoryUrlHelp"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeReadonly.urlLabel"
|
||||
defaultMessage="Path (required)"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['readonlyRepositoryUrlDescription readonlyRepositoryUrlHelp']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.url)}
|
||||
error={settingErrors.url}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={url ? url.split('://')[1] : ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
url: `${selectedScheme}://${e.target.value}`,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFormHelpText id="readonlyRepositoryUrlHelp" aria-live="polite">
|
||||
{getSchemeHelpText(selectedScheme)}
|
||||
</EuiFormHelpText>
|
||||
</div>
|
||||
</EuiDescribedFormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,626 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import {
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiSwitch,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { Repository, S3Repository } from '../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { RepositorySettingsValidation } from '../../../services/validation';
|
||||
import { textService } from '../../../services/text';
|
||||
|
||||
interface Props {
|
||||
repository: S3Repository;
|
||||
updateRepositorySettings: (
|
||||
updatedSettings: Partial<Repository['settings']>,
|
||||
replaceSettings?: boolean
|
||||
) => void;
|
||||
settingErrors: RepositorySettingsValidation;
|
||||
}
|
||||
|
||||
export const S3Settings: React.FunctionComponent<Props> = ({
|
||||
repository,
|
||||
updateRepositorySettings,
|
||||
settingErrors,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
const {
|
||||
settings: {
|
||||
bucket,
|
||||
client,
|
||||
basePath,
|
||||
compress,
|
||||
chunkSize,
|
||||
serverSideEncryption,
|
||||
bufferSize,
|
||||
cannedAcl,
|
||||
storageClass,
|
||||
maxRestoreBytesPerSec,
|
||||
maxSnapshotBytesPerSec,
|
||||
readonly,
|
||||
},
|
||||
} = repository;
|
||||
|
||||
const cannedAclOptions = [
|
||||
'private',
|
||||
'public-read',
|
||||
'public-read-write',
|
||||
'authenticated-read',
|
||||
'log-delivery-write',
|
||||
'bucket-owner-read',
|
||||
'bucket-owner-full-control',
|
||||
].map(option => ({
|
||||
value: option,
|
||||
text: option,
|
||||
}));
|
||||
const hasErrors: boolean = Boolean(Object.keys(settingErrors).length);
|
||||
|
||||
const storageClassOptions = ['standard', 'reduced_redundancy', 'standard_ia'].map(option => ({
|
||||
value: option,
|
||||
text: option,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* Client field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.clientTitle"
|
||||
defaultMessage="Client"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.clientDescription"
|
||||
defaultMessage="The name of the AWS S3 client."
|
||||
/>
|
||||
}
|
||||
idAria="s3RepositoryClientDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.clientLabel"
|
||||
defaultMessage="Client"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['s3RepositoryClientDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.client)}
|
||||
error={settingErrors.client}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={client || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
client: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Bucket field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.bucketTitle"
|
||||
defaultMessage="Bucket"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.bucketDescription"
|
||||
defaultMessage="The name of the AWS S3 bucket to use for snapshots."
|
||||
/>
|
||||
}
|
||||
idAria="s3RepositoryBucketDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.bucketLabel"
|
||||
defaultMessage="Bucket (required)"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['s3RepositoryBucketDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.bucket)}
|
||||
error={settingErrors.bucket}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={bucket || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
bucket: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Base path field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.basePathTitle"
|
||||
defaultMessage="Base path"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.basePathDescription"
|
||||
defaultMessage="The bucket path to the repository data."
|
||||
/>
|
||||
}
|
||||
idAria="s3RepositoryBasePathDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.basePathLabel"
|
||||
defaultMessage="Base path"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['s3RepositoryBasePathDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.basePath)}
|
||||
error={settingErrors.basePath}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={basePath || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
basePath: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Compress field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.compressTitle"
|
||||
defaultMessage="Snapshot compression"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.compressDescription"
|
||||
defaultMessage="Compresses the index mapping and setting files for snapshots. Data files are not compressed."
|
||||
/>
|
||||
}
|
||||
idAria="s3RepositoryCompressDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['s3RepositoryCompressDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.compress)}
|
||||
error={settingErrors.compress}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.compressLabel"
|
||||
defaultMessage="Compress snapshots"
|
||||
/>
|
||||
}
|
||||
checked={!(compress === false)}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
compress: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Chunk size field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.chunkSizeTitle"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.chunkSizeDescription"
|
||||
defaultMessage="Breaks files into smaller units when taking snapshots."
|
||||
/>
|
||||
}
|
||||
idAria="s3RepositoryChunkSizeDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.chunkSizeLabel"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['s3RepositoryChunkSizeDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.chunkSize)}
|
||||
error={settingErrors.chunkSize}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={chunkSize || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
chunkSize: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Server side encryption field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.serverSideEncryptionTitle"
|
||||
defaultMessage="Server-side encryption"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.serverSideEncryptionDescription"
|
||||
defaultMessage="Encrypts files on the server using AES256 algorithm."
|
||||
/>
|
||||
}
|
||||
idAria="s3RepositoryServerSideEncryptionDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['s3RepositoryServerSideEncryptionDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.serverSideEncryption)}
|
||||
error={settingErrors.serverSideEncryption}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.serverSideEncryptionLabel"
|
||||
defaultMessage="Server-side encryption"
|
||||
/>
|
||||
}
|
||||
checked={!!serverSideEncryption}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
serverSideEncryption: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Buffer size field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.bufferSizeTitle"
|
||||
defaultMessage="Buffer size"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.bufferSizeDescription"
|
||||
defaultMessage="Beyond this minimum threshold, the S3 repository will use the AWS Multipart Upload API
|
||||
to split the chunk into several parts and upload each in its own request."
|
||||
/>
|
||||
}
|
||||
idAria="s3RepositoryBufferSizeDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.bufferSizeLabel"
|
||||
defaultMessage="Buffer size"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['s3RepositoryBufferSizeDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.bufferSize)}
|
||||
error={settingErrors.bufferSize}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={bufferSize || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
bufferSize: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Canned ACL field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.cannedAclTitle"
|
||||
defaultMessage="Canned ACL"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.cannedAclDescription"
|
||||
defaultMessage="The canned ACL to add to new S3 buckets and objects."
|
||||
/>
|
||||
}
|
||||
idAria="s3RepositoryCannedAclDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.cannedAclLabel"
|
||||
defaultMessage="Canned ACL"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['s3RepositoryCannedAclDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.cannedAcl)}
|
||||
error={settingErrors.cannedAcl}
|
||||
>
|
||||
<EuiSelect
|
||||
options={cannedAclOptions}
|
||||
value={cannedAcl || cannedAclOptions[0].value}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
cannedAcl: e.target.value,
|
||||
});
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Storage class field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.storageClassTitle"
|
||||
defaultMessage="Storage class"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.storageClassDescription"
|
||||
defaultMessage="The storage class for new objects in the S3 repository."
|
||||
/>
|
||||
}
|
||||
idAria="s3RepositoryStorageClassDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.storageClassLabel"
|
||||
defaultMessage="Storage class"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['s3RepositoryStorageClassDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.storageClass)}
|
||||
error={settingErrors.storageClass}
|
||||
>
|
||||
<EuiSelect
|
||||
options={storageClassOptions}
|
||||
value={storageClass || storageClassOptions[0].value}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
storageClass: e.target.value,
|
||||
});
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Max snapshot bytes field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.maxSnapshotBytesTitle"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.maxSnapshotBytesDescription"
|
||||
defaultMessage="The rate for creating snapshots for each node."
|
||||
/>
|
||||
}
|
||||
idAria="s3RepositoryMaxSnapshotBytesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.maxSnapshotBytesLabel"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['s3RepositoryMaxSnapshotBytesDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.maxSnapshotBytesPerSec)}
|
||||
error={settingErrors.maxSnapshotBytesPerSec}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={maxSnapshotBytesPerSec || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
maxSnapshotBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Max restore bytes field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.maxRestoreBytesTitle"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.maxRestoreBytesDescription"
|
||||
defaultMessage="The snapshot restore rate for each node."
|
||||
/>
|
||||
}
|
||||
idAria="s3RepositoryMaxRestoreBytesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.maxRestoreBytesLabel"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
describedByIds={['s3RepositoryMaxRestoreBytesDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.maxRestoreBytesPerSec)}
|
||||
error={settingErrors.maxRestoreBytesPerSec}
|
||||
helpText={textService.getSizeNotationHelpText()}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={maxRestoreBytesPerSec || ''}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
maxRestoreBytesPerSec: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
|
||||
{/* Readonly field */}
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.readonlyTitle"
|
||||
defaultMessage="Read-only"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.readonlyDescription"
|
||||
defaultMessage="Only one cluster should have write access to this repository. All other clusters should be read-only."
|
||||
/>
|
||||
}
|
||||
idAria="s3RepositoryReadonlyDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace={true}
|
||||
fullWidth
|
||||
describedByIds={['s3RepositoryReadonlyDescription']}
|
||||
isInvalid={Boolean(hasErrors && settingErrors.readonly)}
|
||||
error={settingErrors.readonly}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryForm.typeS3.readonlyLabel"
|
||||
defaultMessage="Read-only repository"
|
||||
/>
|
||||
}
|
||||
checked={!!readonly}
|
||||
onChange={e => {
|
||||
updateRepositorySettings({
|
||||
readonly: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { EuiIcon } from '@elastic/eui';
|
||||
import { REPOSITORY_TYPES } from '../../../common/constants';
|
||||
import { RepositoryType } from '../../../common/types';
|
||||
|
||||
interface Props {
|
||||
type: RepositoryType;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const RepositoryTypeLogo: React.SFC<Props> = ({ type, ...rest }) => {
|
||||
const typeLogoMap: { [key: string]: any } = {
|
||||
[REPOSITORY_TYPES.fs]: 'storage',
|
||||
[REPOSITORY_TYPES.url]: 'eye',
|
||||
[REPOSITORY_TYPES.azure]: 'logoAzure',
|
||||
[REPOSITORY_TYPES.gcs]: 'logoGCP',
|
||||
[REPOSITORY_TYPES.hdfs]: 'logoApache',
|
||||
[REPOSITORY_TYPES.s3]: 'logoAWS',
|
||||
};
|
||||
|
||||
return <EuiIcon type={typeLogoMap[type] || 'folderOpen'} {...rest} />;
|
||||
};
|
|
@ -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 { EuiHealth } from '@elastic/eui';
|
||||
import { RepositoryVerification } from '../../../common/types';
|
||||
import { useAppDependencies } from '../index';
|
||||
|
||||
interface Props {
|
||||
verificationResults: RepositoryVerification | null;
|
||||
}
|
||||
|
||||
export const RepositoryVerificationBadge: React.FunctionComponent<Props> = ({
|
||||
verificationResults,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
if (!verificationResults) {
|
||||
return (
|
||||
<EuiHealth color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryVerification.verificationUnknownValue"
|
||||
defaultMessage="Unknown"
|
||||
/>
|
||||
</EuiHealth>
|
||||
);
|
||||
}
|
||||
|
||||
if (verificationResults.valid) {
|
||||
return (
|
||||
<EuiHealth color="success">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryVerification.verificationSuccessfulValue"
|
||||
defaultMessage="Connected"
|
||||
/>
|
||||
</EuiHealth>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiHealth color="warning">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryVerification.verificationErrorValue"
|
||||
defaultMessage="Not connected"
|
||||
/>
|
||||
</EuiHealth>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
interface Props {
|
||||
title: React.ReactNode;
|
||||
error: {
|
||||
data: {
|
||||
error: string;
|
||||
cause?: string[];
|
||||
message?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const SectionError: React.FunctionComponent<Props> = ({ title, error }) => {
|
||||
const {
|
||||
error: errorString,
|
||||
cause, // wrapEsError() on the server adds a "cause" array
|
||||
message,
|
||||
} = error.data;
|
||||
|
||||
return (
|
||||
<EuiCallOut title={title} color="danger" iconType="alert">
|
||||
<div>{message || errorString}</div>
|
||||
{cause && (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<ul>
|
||||
{cause.map((causeMsg, i) => (
|
||||
<li key={i}>{causeMsg}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SectionLoading: React.FunctionComponent<Props> = ({ children }) => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={<EuiLoadingSpinner size="xl" />}
|
||||
body={<EuiText color="subdued">{children}</EuiText>}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const BASE_PATH = '/management/elasticsearch/snapshot_restore';
|
||||
export const DEFAULT_SECTION: Section = 'repositories';
|
||||
export type Section = 'repositories' | 'snapshots';
|
||||
|
||||
// Set a minimum request duration to avoid strange UI flickers
|
||||
export const MINIMUM_TIMEOUT_MS = 300;
|
||||
|
||||
export enum REPOSITORY_DOC_PATHS {
|
||||
default = 'modules-snapshots.html',
|
||||
fs = 'modules-snapshots.html#_shared_file_system_repository',
|
||||
url = 'modules-snapshots.html#_read_only_url_repository',
|
||||
source = 'modules-snapshots.html#_source_only_repository',
|
||||
s3 = 'repository-s3.html',
|
||||
hdfs = 'repository-hdfs.html',
|
||||
azure = 'repository-azure.html',
|
||||
gcs = 'repository-gcs.html',
|
||||
plugins = 'repository.html',
|
||||
}
|
||||
|
||||
export enum SNAPSHOT_STATE {
|
||||
IN_PROGRESS = 'IN_PROGRESS',
|
||||
SUCCESS = 'SUCCESS',
|
||||
FAILED = 'FAILED',
|
||||
PARTIAL = 'PARTIAL',
|
||||
INCOMPATIBLE = 'INCOMPATIBLE',
|
||||
}
|
55
x-pack/plugins/snapshot_restore/public/app/index.tsx
Normal file
55
x-pack/plugins/snapshot_restore/public/app/index.tsx
Normal file
|
@ -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, { createContext, useContext, useReducer } from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
import { App } from './app';
|
||||
import { AppStateProvider, initialState, reducer } from './services/state';
|
||||
import { AppCore, AppDependencies, AppPlugins } from './types';
|
||||
|
||||
export { BASE_PATH as CLIENT_BASE_PATH } from './constants';
|
||||
|
||||
/**
|
||||
* App dependencies
|
||||
*/
|
||||
let DependenciesContext: React.Context<AppDependencies>;
|
||||
|
||||
export const useAppDependencies = () => useContext<AppDependencies>(DependenciesContext);
|
||||
|
||||
const ReactApp: React.FunctionComponent<AppDependencies> = ({ core, plugins }) => {
|
||||
const {
|
||||
i18n: { Context: I18nContext },
|
||||
} = core;
|
||||
|
||||
const appDependencies: AppDependencies = {
|
||||
core,
|
||||
plugins,
|
||||
};
|
||||
|
||||
DependenciesContext = createContext<AppDependencies>(appDependencies);
|
||||
|
||||
return (
|
||||
<I18nContext>
|
||||
<HashRouter>
|
||||
<DependenciesContext.Provider value={appDependencies}>
|
||||
<AppStateProvider value={useReducer(reducer, initialState)}>
|
||||
<App />
|
||||
</AppStateProvider>
|
||||
</DependenciesContext.Provider>
|
||||
</HashRouter>
|
||||
</I18nContext>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderReact = async (
|
||||
elem: Element,
|
||||
core: AppCore,
|
||||
plugins: AppPlugins
|
||||
): Promise<void> => {
|
||||
render(<ReactApp core={core} plugins={plugins} />, elem);
|
||||
};
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiSpacer,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { BASE_PATH, Section } from '../../constants';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { breadcrumbService } from '../../services/navigation';
|
||||
|
||||
import { RepositoryList } from './repository_list';
|
||||
import { SnapshotList } from './snapshot_list';
|
||||
import { documentationLinksService } from '../../services/documentation';
|
||||
|
||||
interface MatchParams {
|
||||
section: Section;
|
||||
}
|
||||
|
||||
export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<MatchParams>> = ({
|
||||
match: {
|
||||
params: { section },
|
||||
},
|
||||
history,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'snapshots' as Section,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.snapshotsTabTitle"
|
||||
defaultMessage="Snapshots"
|
||||
/>
|
||||
),
|
||||
testSubj: 'srSnapshotsTab',
|
||||
},
|
||||
{
|
||||
id: 'repositories' as Section,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.repositoriesTabTitle"
|
||||
defaultMessage="Repositories"
|
||||
/>
|
||||
),
|
||||
testSubj: 'srRepositoriesTab',
|
||||
},
|
||||
];
|
||||
|
||||
const onSectionChange = (newSection: Section) => {
|
||||
history.push(`${BASE_PATH}/${newSection}`);
|
||||
};
|
||||
|
||||
// Set breadcrumb
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs('home');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="l">
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={true}>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.snapshotRestoreTitle"
|
||||
defaultMessage="Snapshot Repositories"
|
||||
/>
|
||||
</h1>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
href={documentationLinksService.getRepositoryTypeDocUrl()}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.snapshotRestoreDocsLinkText"
|
||||
defaultMessage="Snapshot docs"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiTitle size="s">
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.home.snapshotRestoreDescription"
|
||||
defaultMessage="Use repositories to store backups of your Elasticsearch indices and clusters."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiTabs>
|
||||
{tabs.map(tab => (
|
||||
<EuiTab
|
||||
onClick={() => onSectionChange(tab.id)}
|
||||
isSelected={tab.id === section}
|
||||
key={tab.id}
|
||||
data-test-subject={tab.testSubj}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${BASE_PATH}/repositories/:repositoryName*`}
|
||||
component={RepositoryList}
|
||||
/>
|
||||
{/* We have two separate SnapshotList routes because repository names could have slashes in
|
||||
* them. This would break a route with a path like snapshots/:repositoryName?/:snapshotId*
|
||||
*/}
|
||||
<Route exact path={`${BASE_PATH}/snapshots`} component={SnapshotList} />
|
||||
<Route
|
||||
exact
|
||||
path={`${BASE_PATH}/snapshots/:repositoryName*/:snapshotId`}
|
||||
component={SnapshotList}
|
||||
/>
|
||||
</Switch>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
|
@ -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 { SnapshotRestoreHome } from './home';
|
|
@ -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 { RepositoryList } from './repository_list';
|
|
@ -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 { RepositoryDetails } from './repository_details';
|
|
@ -0,0 +1,387 @@
|
|||
/*
|
||||
* 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, useEffect } from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCodeEditor,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiHorizontalRule,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import 'brace/theme/textmate';
|
||||
|
||||
import { useAppDependencies } from '../../../../index';
|
||||
import { documentationLinksService } from '../../../../services/documentation';
|
||||
import {
|
||||
loadRepository,
|
||||
verifyRepository as verifyRepositoryRequest,
|
||||
} from '../../../../services/http';
|
||||
import { textService } from '../../../../services/text';
|
||||
import { linkToSnapshots } from '../../../../services/navigation';
|
||||
|
||||
import { REPOSITORY_TYPES } from '../../../../../../common/constants';
|
||||
import { Repository, RepositoryVerification } from '../../../../../../common/types';
|
||||
import {
|
||||
RepositoryDeleteProvider,
|
||||
SectionError,
|
||||
SectionLoading,
|
||||
RepositoryVerificationBadge,
|
||||
} from '../../../../components';
|
||||
import { BASE_PATH } from '../../../../constants';
|
||||
import { TypeDetails } from './type_details';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
repositoryName: Repository['name'];
|
||||
onClose: () => void;
|
||||
onRepositoryDeleted: (repositoriesDeleted: Array<Repository['name']>) => void;
|
||||
}
|
||||
|
||||
const RepositoryDetailsUi: React.FunctionComponent<Props> = ({
|
||||
repositoryName,
|
||||
onClose,
|
||||
onRepositoryDeleted,
|
||||
history,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
|
||||
const { FormattedMessage } = i18n;
|
||||
const { error, data: repositoryDetails } = loadRepository(repositoryName);
|
||||
const [verification, setVerification] = useState<RepositoryVerification | undefined>(undefined);
|
||||
const [isLoadingVerification, setIsLoadingVerification] = useState<boolean>(false);
|
||||
|
||||
const verifyRepository = async () => {
|
||||
setIsLoadingVerification(true);
|
||||
const { data } = await verifyRepositoryRequest(repositoryName);
|
||||
setVerification(data.verification);
|
||||
setIsLoadingVerification(false);
|
||||
};
|
||||
|
||||
// Reset verification state when repository name changes, either from adjust URL or clicking
|
||||
// into a different repository in table list.
|
||||
useEffect(
|
||||
() => {
|
||||
setVerification(undefined);
|
||||
setIsLoadingVerification(false);
|
||||
},
|
||||
[repositoryName]
|
||||
);
|
||||
|
||||
const renderBody = () => {
|
||||
if (repositoryDetails) {
|
||||
return renderRepository();
|
||||
}
|
||||
if (error) {
|
||||
return renderError();
|
||||
}
|
||||
return renderLoading();
|
||||
};
|
||||
|
||||
const renderLoading = () => {
|
||||
return (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.loadingRepositoryDescription"
|
||||
defaultMessage="Loading repository…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
};
|
||||
|
||||
const renderError = () => {
|
||||
const notFound = error.status === 404;
|
||||
const errorObject = notFound
|
||||
? {
|
||||
data: {
|
||||
error: i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryDetails.repositoryNotFoundErrorMessage',
|
||||
{
|
||||
defaultMessage: `The repository '{name}' does not exist.`,
|
||||
values: {
|
||||
name: repositoryName,
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
: error;
|
||||
return (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.loadingRepositoryErrorTitle"
|
||||
defaultMessage="Error loading repository"
|
||||
/>
|
||||
}
|
||||
error={errorObject}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSnapshotCount = () => {
|
||||
const { snapshots } = repositoryDetails;
|
||||
if (!Number.isInteger(snapshots.count)) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.noSnapshotInformationDescription"
|
||||
defaultMessage="No snapshot information"
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (snapshots.count === 0) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.zeroSnapshotsDescription"
|
||||
defaultMessage="Repository has no snapshots"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiLink href={linkToSnapshots(repositoryName)}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.snapshotsDescription"
|
||||
defaultMessage="{count} {count, plural, one {snapshot} other {snapshots}} found"
|
||||
values={{ count: snapshots.count }}
|
||||
/>
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
const renderRepository = () => {
|
||||
const { repository } = repositoryDetails;
|
||||
|
||||
if (!repository) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { type } = repository as Repository;
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexStart">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeTitle"
|
||||
defaultMessage="Repository type"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
{type === REPOSITORY_TYPES.source
|
||||
? textService.getRepositoryTypeName(type, repository.settings.delegateType)
|
||||
: textService.getRepositoryTypeName(type)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
flush="right"
|
||||
href={documentationLinksService.getRepositoryTypeDocUrl(type)}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.repositoryTypeDocLink"
|
||||
defaultMessage="Repository docs"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.snapshotsTitle"
|
||||
defaultMessage="Snapshots"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
{renderSnapshotCount()}
|
||||
<EuiSpacer size="l" />
|
||||
<TypeDetails repository={repository} />
|
||||
<EuiHorizontalRule />
|
||||
{renderVerification()}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const renderVerification = () => (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.verificationTitle"
|
||||
defaultMessage="Verification status"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
{verification ? (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<RepositoryVerificationBadge verificationResults={verification} />
|
||||
<EuiSpacer size="s" />
|
||||
<EuiTitle size="xs">
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.verificationDetailsTitle"
|
||||
defaultMessage="Details"
|
||||
/>
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
{verification ? (
|
||||
<EuiCodeEditor
|
||||
mode="json"
|
||||
theme="textmate"
|
||||
width="100%"
|
||||
isReadOnly
|
||||
value={JSON.stringify(
|
||||
verification.valid ? verification.response : verification.error,
|
||||
null,
|
||||
2
|
||||
)}
|
||||
setOptions={{
|
||||
showLineNumbers: false,
|
||||
tabSize: 2,
|
||||
maxLines: Infinity,
|
||||
}}
|
||||
editorProps={{
|
||||
$blockScrolling: Infinity,
|
||||
}}
|
||||
showGutter={false}
|
||||
minLines={6}
|
||||
aria-label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.verificationDetails"
|
||||
defaultMessage="Verification details repository '{name}'"
|
||||
values={{
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<EuiSpacer size="m" />
|
||||
<EuiButton onClick={verifyRepository} color="primary" isLoading={isLoadingVerification}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.reverifyButtonLabel"
|
||||
defaultMessage="Re-verify repository"
|
||||
/>
|
||||
</EuiButton>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiButton onClick={verifyRepository} color="primary" isLoading={isLoadingVerification}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.verifyButtonLabel"
|
||||
defaultMessage="Verify repository"
|
||||
/>
|
||||
</EuiButton>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const renderFooter = () => {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="cross"
|
||||
flush="left"
|
||||
onClick={onClose}
|
||||
data-test-subj="srRepositoryDetailsFlyoutCloseButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.closeButtonLabel"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
{repositoryDetails ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<RepositoryDeleteProvider>
|
||||
{deleteRepositoryPrompt => {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
data-test-subj="srRepositoryDetailsDeleteActionButton"
|
||||
onClick={() =>
|
||||
deleteRepositoryPrompt([repositoryName], onRepositoryDeleted)
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.removeButtonLabel"
|
||||
defaultMessage="Remove"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}}
|
||||
</RepositoryDeleteProvider>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
href={history.createHref({
|
||||
pathname: `${BASE_PATH}/edit_repository/${repositoryName}`,
|
||||
})}
|
||||
fill
|
||||
color="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.editButtonLabel"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
onClose={onClose}
|
||||
data-test-subj="srRepositoryDetailsFlyout"
|
||||
aria-labelledby="srRepositoryDetailsFlyoutTitle"
|
||||
size="m"
|
||||
maxWidth={400}
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2 id="srRepositoryDetailsFlyoutTitle" data-test-subj="srRepositoryDetailsFlyoutTitle">
|
||||
{repositoryName}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody data-test-subj="srRepositoryDetailsContent">{renderBody()}</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>{renderFooter()}</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
||||
|
||||
export const RepositoryDetails = withRouter(RepositoryDetailsUi);
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { AzureRepository } from '../../../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../../../index';
|
||||
|
||||
interface Props {
|
||||
repository: AzureRepository;
|
||||
}
|
||||
|
||||
export const AzureDetails: React.FunctionComponent<Props> = ({ repository }) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const {
|
||||
settings: {
|
||||
client,
|
||||
container,
|
||||
basePath,
|
||||
compress,
|
||||
chunkSize,
|
||||
readonly,
|
||||
locationMode,
|
||||
maxRestoreBytesPerSec,
|
||||
maxSnapshotBytesPerSec,
|
||||
},
|
||||
} = repository;
|
||||
|
||||
const listItems = [];
|
||||
|
||||
if (client !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeAzure.clientLabel"
|
||||
defaultMessage="Client"
|
||||
/>
|
||||
),
|
||||
description: client,
|
||||
});
|
||||
}
|
||||
|
||||
if (container !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeAzure.containerLabel"
|
||||
defaultMessage="Container"
|
||||
/>
|
||||
),
|
||||
description: container,
|
||||
});
|
||||
}
|
||||
|
||||
if (basePath !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeAzure.basePathLabel"
|
||||
defaultMessage="Base path"
|
||||
/>
|
||||
),
|
||||
description: basePath,
|
||||
});
|
||||
}
|
||||
|
||||
if (compress !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeAzure.compressLabel"
|
||||
defaultMessage="Snapshot compression"
|
||||
/>
|
||||
),
|
||||
description: String(compress),
|
||||
});
|
||||
}
|
||||
|
||||
if (chunkSize !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeAzure.chunkSizeLabel"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
),
|
||||
description: String(chunkSize),
|
||||
});
|
||||
}
|
||||
|
||||
if (maxSnapshotBytesPerSec !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeAzure.maxSnapshotBytesLabel"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
),
|
||||
description: maxSnapshotBytesPerSec,
|
||||
});
|
||||
}
|
||||
|
||||
if (maxRestoreBytesPerSec !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeAzure.maxRestoreBytesLabel"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
),
|
||||
description: maxRestoreBytesPerSec,
|
||||
});
|
||||
}
|
||||
|
||||
if (readonly !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeAzure.readonlyLabel"
|
||||
defaultMessage="Read-only"
|
||||
/>
|
||||
),
|
||||
description: String(readonly),
|
||||
});
|
||||
}
|
||||
|
||||
if (locationMode !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeAzure.locationModeLabel"
|
||||
defaultMessage="Location mode"
|
||||
/>
|
||||
),
|
||||
description: locationMode,
|
||||
});
|
||||
}
|
||||
|
||||
if (!listItems.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.settingsTitle"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import { EuiCodeEditor, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { Repository } from '../../../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../../../index';
|
||||
|
||||
import 'brace/theme/textmate';
|
||||
|
||||
interface Props {
|
||||
repository: Repository;
|
||||
}
|
||||
|
||||
export const DefaultDetails: React.FunctionComponent<Props> = ({
|
||||
repository: { name, settings },
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.settingsTitle"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiCodeEditor
|
||||
mode="json"
|
||||
theme="textmate"
|
||||
width="100%"
|
||||
isReadOnly
|
||||
value={JSON.stringify(settings, null, 2)}
|
||||
setOptions={{
|
||||
showLineNumbers: false,
|
||||
tabSize: 2,
|
||||
maxLines: Infinity,
|
||||
}}
|
||||
editorProps={{
|
||||
$blockScrolling: Infinity,
|
||||
}}
|
||||
showGutter={false}
|
||||
minLines={6}
|
||||
aria-label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.genericSettingsDescription"
|
||||
defaultMessage="Readonly settings for repository '{name}'"
|
||||
values={{
|
||||
name,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { FSRepository } from '../../../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../../../index';
|
||||
|
||||
interface Props {
|
||||
repository: FSRepository;
|
||||
}
|
||||
|
||||
export const FSDetails: React.FunctionComponent<Props> = ({ repository }) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const {
|
||||
settings: {
|
||||
location,
|
||||
compress,
|
||||
chunkSize,
|
||||
maxRestoreBytesPerSec,
|
||||
maxSnapshotBytesPerSec,
|
||||
readonly,
|
||||
},
|
||||
} = repository;
|
||||
|
||||
const listItems = [
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeFS.locationLabel"
|
||||
defaultMessage="Location"
|
||||
/>
|
||||
),
|
||||
description: location,
|
||||
},
|
||||
];
|
||||
|
||||
if (compress !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeFS.compressLabel"
|
||||
defaultMessage="Snapshot compression"
|
||||
/>
|
||||
),
|
||||
description: String(compress),
|
||||
});
|
||||
}
|
||||
|
||||
if (chunkSize !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeFS.chunkSizeLabel"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
),
|
||||
description: String(chunkSize),
|
||||
});
|
||||
}
|
||||
|
||||
if (maxSnapshotBytesPerSec !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeFS.maxSnapshotBytesLabel"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
),
|
||||
description: maxSnapshotBytesPerSec,
|
||||
});
|
||||
}
|
||||
|
||||
if (maxRestoreBytesPerSec !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeFS.maxRestoreBytesLabel"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
),
|
||||
description: maxRestoreBytesPerSec,
|
||||
});
|
||||
}
|
||||
|
||||
if (readonly !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeFS.readonlyLabel"
|
||||
defaultMessage="Read-only"
|
||||
/>
|
||||
),
|
||||
description: String(readonly),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.settingsTitle"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { GCSRepository } from '../../../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../../../index';
|
||||
|
||||
interface Props {
|
||||
repository: GCSRepository;
|
||||
}
|
||||
|
||||
export const GCSDetails: React.FunctionComponent<Props> = ({ repository }) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const {
|
||||
settings: {
|
||||
bucket,
|
||||
client,
|
||||
basePath,
|
||||
compress,
|
||||
chunkSize,
|
||||
readonly,
|
||||
maxRestoreBytesPerSec,
|
||||
maxSnapshotBytesPerSec,
|
||||
},
|
||||
} = repository;
|
||||
|
||||
const listItems = [
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeGCS.bucketLabel"
|
||||
defaultMessage="Bucket"
|
||||
/>
|
||||
),
|
||||
description: bucket || '',
|
||||
},
|
||||
];
|
||||
|
||||
if (client !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeGCS.clientLabel"
|
||||
defaultMessage="Client"
|
||||
/>
|
||||
),
|
||||
description: client,
|
||||
});
|
||||
}
|
||||
|
||||
if (basePath !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeGCS.basePathLabel"
|
||||
defaultMessage="Base path"
|
||||
/>
|
||||
),
|
||||
description: basePath,
|
||||
});
|
||||
}
|
||||
|
||||
if (compress !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeGCS.compressLabel"
|
||||
defaultMessage="Snapshot compression"
|
||||
/>
|
||||
),
|
||||
description: String(compress),
|
||||
});
|
||||
}
|
||||
|
||||
if (chunkSize !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeGCS.chunkSizeLabel"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
),
|
||||
description: String(chunkSize),
|
||||
});
|
||||
}
|
||||
|
||||
if (maxSnapshotBytesPerSec !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeGCS.maxSnapshotBytesLabel"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
),
|
||||
description: maxSnapshotBytesPerSec,
|
||||
});
|
||||
}
|
||||
|
||||
if (maxRestoreBytesPerSec !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeGCS.maxRestoreBytesLabel"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
),
|
||||
description: maxRestoreBytesPerSec,
|
||||
});
|
||||
}
|
||||
|
||||
if (readonly !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeGCS.readonlyLabel"
|
||||
defaultMessage="Read-only"
|
||||
/>
|
||||
),
|
||||
description: String(readonly),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.settingsTitle"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { HDFSRepository } from '../../../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../../../index';
|
||||
|
||||
interface Props {
|
||||
repository: HDFSRepository;
|
||||
}
|
||||
|
||||
export const HDFSDetails: React.FunctionComponent<Props> = ({ repository }) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const { settings } = repository;
|
||||
const {
|
||||
uri,
|
||||
path,
|
||||
loadDefaults,
|
||||
compress,
|
||||
chunkSize,
|
||||
readonly,
|
||||
maxSnapshotBytesPerSec,
|
||||
maxRestoreBytesPerSec,
|
||||
'security.principal': securityPrincipal,
|
||||
...rest
|
||||
} = settings;
|
||||
|
||||
const listItems = [
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeHDFS.uriLabel"
|
||||
defaultMessage="URI"
|
||||
/>
|
||||
),
|
||||
description: uri || '',
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeHDFS.pathLabel"
|
||||
defaultMessage="Path"
|
||||
/>
|
||||
),
|
||||
description: path || '',
|
||||
},
|
||||
];
|
||||
|
||||
if (loadDefaults !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeHDFS.loadDefaultsLabel"
|
||||
defaultMessage="Load defaults"
|
||||
/>
|
||||
),
|
||||
description: String(loadDefaults),
|
||||
});
|
||||
}
|
||||
|
||||
if (compress !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeHDFS.compressLabel"
|
||||
defaultMessage="Snapshot compression"
|
||||
/>
|
||||
),
|
||||
description: String(compress),
|
||||
});
|
||||
}
|
||||
|
||||
if (chunkSize !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeHDFS.chunkSizeLabel"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
),
|
||||
description: String(chunkSize),
|
||||
});
|
||||
}
|
||||
|
||||
if (maxSnapshotBytesPerSec !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeHDFS.maxSnapshotBytesLabel"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
),
|
||||
description: maxSnapshotBytesPerSec,
|
||||
});
|
||||
}
|
||||
|
||||
if (maxRestoreBytesPerSec !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeHDFS.maxRestoreBytesLabel"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
),
|
||||
description: maxRestoreBytesPerSec,
|
||||
});
|
||||
}
|
||||
|
||||
if (readonly !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeHDFS.readonlyLabel"
|
||||
defaultMessage="Read-only"
|
||||
/>
|
||||
),
|
||||
description: String(readonly),
|
||||
});
|
||||
}
|
||||
|
||||
if (securityPrincipal !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeHDFS.securityPrincipalLabel"
|
||||
defaultMessage="Security principal"
|
||||
/>
|
||||
),
|
||||
description: securityPrincipal,
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(rest).forEach(key => {
|
||||
listItems.push({
|
||||
title: <Fragment>{key}</Fragment>,
|
||||
description: String(settings[key]),
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.settingsTitle"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { REPOSITORY_TYPES } from '../../../../../../../common/constants';
|
||||
import {
|
||||
AzureRepository,
|
||||
FSRepository,
|
||||
GCSRepository,
|
||||
HDFSRepository,
|
||||
ReadonlyRepository,
|
||||
Repository,
|
||||
S3Repository,
|
||||
} from '../../../../../../../common/types';
|
||||
|
||||
import { AzureDetails } from './azure_details';
|
||||
import { DefaultDetails } from './default_details';
|
||||
import { FSDetails } from './fs_details';
|
||||
import { GCSDetails } from './gcs_details';
|
||||
import { HDFSDetails } from './hdfs_details';
|
||||
import { ReadonlyDetails } from './readonly_details';
|
||||
import { S3Details } from './s3_details';
|
||||
|
||||
interface Props {
|
||||
repository: Repository;
|
||||
}
|
||||
|
||||
export const TypeDetails: React.FunctionComponent<Props> = ({ repository }) => {
|
||||
const { type, settings } = repository;
|
||||
switch (type) {
|
||||
case REPOSITORY_TYPES.fs:
|
||||
return <FSDetails repository={repository as FSRepository} />;
|
||||
case REPOSITORY_TYPES.url:
|
||||
return <ReadonlyDetails repository={repository as ReadonlyRepository} />;
|
||||
case REPOSITORY_TYPES.source:
|
||||
const { delegateType } = settings;
|
||||
const delegatedRepository = {
|
||||
...repository,
|
||||
type: delegateType,
|
||||
};
|
||||
return <TypeDetails repository={delegatedRepository} />;
|
||||
case REPOSITORY_TYPES.azure:
|
||||
return <AzureDetails repository={repository as AzureRepository} />;
|
||||
case REPOSITORY_TYPES.gcs:
|
||||
return <GCSDetails repository={repository as GCSRepository} />;
|
||||
case REPOSITORY_TYPES.hdfs:
|
||||
return <HDFSDetails repository={repository as HDFSRepository} />;
|
||||
case REPOSITORY_TYPES.s3:
|
||||
return <S3Details repository={repository as S3Repository} />;
|
||||
default:
|
||||
return <DefaultDetails repository={repository} />;
|
||||
}
|
||||
};
|
|
@ -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, { Fragment } from 'react';
|
||||
|
||||
import { EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { ReadonlyRepository } from '../../../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../../../index';
|
||||
|
||||
interface Props {
|
||||
repository: ReadonlyRepository;
|
||||
}
|
||||
|
||||
export const ReadonlyDetails: React.FunctionComponent<Props> = ({ repository }) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
const {
|
||||
settings: { url },
|
||||
} = repository;
|
||||
|
||||
const listItems = [
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeReadonly.urlLabel"
|
||||
defaultMessage="URL"
|
||||
/>
|
||||
),
|
||||
description: url,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.settingsTitle"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { S3Repository } from '../../../../../../../common/types';
|
||||
import { useAppDependencies } from '../../../../../index';
|
||||
|
||||
interface Props {
|
||||
repository: S3Repository;
|
||||
}
|
||||
|
||||
export const S3Details: React.FunctionComponent<Props> = ({ repository }) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const {
|
||||
settings: {
|
||||
bucket,
|
||||
client,
|
||||
basePath,
|
||||
compress,
|
||||
chunkSize,
|
||||
serverSideEncryption,
|
||||
bufferSize,
|
||||
cannedAcl,
|
||||
storageClass,
|
||||
readonly,
|
||||
maxRestoreBytesPerSec,
|
||||
maxSnapshotBytesPerSec,
|
||||
},
|
||||
} = repository;
|
||||
|
||||
const listItems = [
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeS3.bucketLabel"
|
||||
defaultMessage="Bucket"
|
||||
/>
|
||||
),
|
||||
description: bucket || '',
|
||||
},
|
||||
];
|
||||
|
||||
if (client !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeS3.clientLabel"
|
||||
defaultMessage="Client"
|
||||
/>
|
||||
),
|
||||
description: client,
|
||||
});
|
||||
}
|
||||
|
||||
if (basePath !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeS3.basePathLabel"
|
||||
defaultMessage="Base path"
|
||||
/>
|
||||
),
|
||||
description: basePath,
|
||||
});
|
||||
}
|
||||
|
||||
if (compress !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeS3.compressLabel"
|
||||
defaultMessage="Snapshot compression"
|
||||
/>
|
||||
),
|
||||
description: String(compress),
|
||||
});
|
||||
}
|
||||
|
||||
if (chunkSize !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeS3.chunkSizeLabel"
|
||||
defaultMessage="Chunk size"
|
||||
/>
|
||||
),
|
||||
description: String(chunkSize),
|
||||
});
|
||||
}
|
||||
|
||||
if (serverSideEncryption !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeS3.serverSideEncryptionLabel"
|
||||
defaultMessage="Server-side encryption"
|
||||
/>
|
||||
),
|
||||
description: String(serverSideEncryption),
|
||||
});
|
||||
}
|
||||
|
||||
if (bufferSize !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeS3.bufferSizeLabel"
|
||||
defaultMessage="Buffer size"
|
||||
/>
|
||||
),
|
||||
description: bufferSize,
|
||||
});
|
||||
}
|
||||
|
||||
if (cannedAcl !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeS3.cannedAclLabel"
|
||||
defaultMessage="Canned ACL"
|
||||
/>
|
||||
),
|
||||
description: cannedAcl,
|
||||
});
|
||||
}
|
||||
|
||||
if (storageClass !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeS3.storageClassLabel"
|
||||
defaultMessage="Storage class"
|
||||
/>
|
||||
),
|
||||
description: storageClass,
|
||||
});
|
||||
}
|
||||
|
||||
if (maxSnapshotBytesPerSec !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeS3.maxSnapshotBytesLabel"
|
||||
defaultMessage="Max snapshot bytes per second"
|
||||
/>
|
||||
),
|
||||
description: maxSnapshotBytesPerSec,
|
||||
});
|
||||
}
|
||||
|
||||
if (maxRestoreBytesPerSec !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeS3.maxRestoreBytesLabel"
|
||||
defaultMessage="Max restore bytes per second"
|
||||
/>
|
||||
),
|
||||
description: maxRestoreBytesPerSec,
|
||||
});
|
||||
}
|
||||
|
||||
if (readonly !== undefined) {
|
||||
listItems.push({
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.typeS3.readonlyLabel"
|
||||
defaultMessage="Read-only"
|
||||
/>
|
||||
),
|
||||
description: String(readonly),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryDetails.settingsTitle"
|
||||
defaultMessage="Settings"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiDescriptionList listItems={listItems} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { Repository } from '../../../../../common/types';
|
||||
import { SectionError, SectionLoading } from '../../../components';
|
||||
import { BASE_PATH } from '../../../constants';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { loadRepositories } from '../../../services/http';
|
||||
|
||||
import { RepositoryDetails } from './repository_details';
|
||||
import { RepositoryTable } from './repository_table';
|
||||
|
||||
interface MatchParams {
|
||||
repositoryName?: Repository['name'];
|
||||
}
|
||||
|
||||
export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchParams>> = ({
|
||||
match: {
|
||||
params: { repositoryName },
|
||||
},
|
||||
history,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const {
|
||||
error,
|
||||
loading,
|
||||
data: { repositories } = { repositories: undefined },
|
||||
request: reload,
|
||||
} = loadRepositories();
|
||||
|
||||
const openRepositoryDetails = (newRepositoryName: Repository['name']) => {
|
||||
history.push(`${BASE_PATH}/repositories/${newRepositoryName}`);
|
||||
};
|
||||
|
||||
const closeRepositoryDetails = () => {
|
||||
history.push(`${BASE_PATH}/repositories`);
|
||||
};
|
||||
|
||||
const onRepositoryDeleted = (repositoriesDeleted: Array<Repository['name']>): void => {
|
||||
if (repositoryName && repositoriesDeleted.includes(repositoryName)) {
|
||||
closeRepositoryDetails();
|
||||
}
|
||||
if (repositoriesDeleted.length) {
|
||||
reload();
|
||||
}
|
||||
};
|
||||
|
||||
let content;
|
||||
|
||||
if (loading) {
|
||||
content = (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryList.loadingRepositoriesDescription"
|
||||
defaultMessage="Loading repositories…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
} else if (error) {
|
||||
content = (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryList.LoadingRepositoriesErrorMessage"
|
||||
defaultMessage="Error loading repositories"
|
||||
/>
|
||||
}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
} else if (repositories && repositories.length === 0) {
|
||||
content = (
|
||||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryList.emptyPromptTitle"
|
||||
defaultMessage="You don't have any repositories yet"
|
||||
/>
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryList.emptyPromptDescription"
|
||||
defaultMessage="You need a repository to store your snapshots."
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
href={history.createHref({
|
||||
pathname: `${BASE_PATH}/add_repository`,
|
||||
})}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="srRepositoriesEmptyPromptAddButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.addRepositoryButtonLabel"
|
||||
defaultMessage="Register a repository"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<RepositoryTable
|
||||
repositories={repositories || []}
|
||||
reload={reload}
|
||||
openRepositoryDetails={openRepositoryDetails}
|
||||
onRepositoryDeleted={onRepositoryDeleted}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{repositoryName ? (
|
||||
<RepositoryDetails
|
||||
repositoryName={repositoryName}
|
||||
onClose={closeRepositoryDetails}
|
||||
onRepositoryDeleted={onRepositoryDeleted}
|
||||
/>
|
||||
) : null}
|
||||
{content}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -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 { RepositoryTable } from './repository_table';
|
|
@ -0,0 +1,265 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiLink,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { REPOSITORY_TYPES } from '../../../../../../common/constants';
|
||||
import { Repository, RepositoryType } from '../../../../../../common/types';
|
||||
import { RepositoryDeleteProvider } from '../../../../components';
|
||||
import { BASE_PATH } from '../../../../constants';
|
||||
import { useAppDependencies } from '../../../../index';
|
||||
import { textService } from '../../../../services/text';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
repositories: Repository[];
|
||||
reload: () => Promise<void>;
|
||||
openRepositoryDetails: (name: Repository['name']) => void;
|
||||
onRepositoryDeleted: (repositoriesDeleted: Array<Repository['name']>) => void;
|
||||
}
|
||||
|
||||
const RepositoryTableUi: React.FunctionComponent<Props> = ({
|
||||
repositories,
|
||||
reload,
|
||||
openRepositoryDetails,
|
||||
onRepositoryDeleted,
|
||||
history,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const [selectedItems, setSelectedItems] = useState<Repository[]>([]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate('xpack.snapshotRestore.repositoryList.table.nameColumnTitle', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
render: (name: Repository['name'], repository: Repository) => {
|
||||
return <EuiLink onClick={() => openRepositoryDetails(name)}>{name}</EuiLink>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
name: i18n.translate('xpack.snapshotRestore.repositoryList.table.typeColumnTitle', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
render: (type: RepositoryType, repository: Repository) => {
|
||||
if (type === REPOSITORY_TYPES.source) {
|
||||
return textService.getRepositoryTypeName(type, repository.settings.delegateType);
|
||||
}
|
||||
return textService.getRepositoryTypeName(type);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.snapshotRestore.repositoryList.table.actionsColumnTitle', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
actions: [
|
||||
{
|
||||
render: ({ name }: { name: string }) => {
|
||||
const label = i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryList.table.actionEditTooltip',
|
||||
{ defaultMessage: 'Edit' }
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiToolTip content={label} delay="long">
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryList.table.actionEditAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Edit repository `{name}`',
|
||||
values: { name },
|
||||
}
|
||||
)}
|
||||
iconType="pencil"
|
||||
color="primary"
|
||||
href={`#${BASE_PATH}/edit_repository/${name}`}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
render: ({ name }: Repository) => {
|
||||
return (
|
||||
<RepositoryDeleteProvider>
|
||||
{deleteRepositoryPrompt => {
|
||||
const label = i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryList.table.actionRemoveTooltip',
|
||||
{ defaultMessage: 'Remove' }
|
||||
);
|
||||
return (
|
||||
<EuiToolTip content={label} delay="long">
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryList.table.actionRemoveAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Remove repository `{name}`',
|
||||
values: { name },
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj="srRepositoryListDeleteActionButton"
|
||||
onClick={() => deleteRepositoryPrompt([name], onRepositoryDeleted)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}}
|
||||
</RepositoryDeleteProvider>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
width: '100px',
|
||||
},
|
||||
];
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: 'name',
|
||||
direction: 'asc',
|
||||
},
|
||||
};
|
||||
|
||||
const pagination = {
|
||||
initialPageSize: 20,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
};
|
||||
|
||||
const selection = {
|
||||
onSelectionChange: (newSelectedItems: Repository[]) => setSelectedItems(newSelectedItems),
|
||||
};
|
||||
|
||||
const search = {
|
||||
toolsLeft: selectedItems.length ? (
|
||||
<RepositoryDeleteProvider>
|
||||
{(
|
||||
deleteRepositoryPrompt: (
|
||||
names: Array<Repository['name']>,
|
||||
onSuccess?: (repositoriesDeleted: Array<Repository['name']>) => void
|
||||
) => void
|
||||
) => {
|
||||
return (
|
||||
<EuiButton
|
||||
onClick={() =>
|
||||
deleteRepositoryPrompt(
|
||||
selectedItems.map(repository => repository.name),
|
||||
onRepositoryDeleted
|
||||
)
|
||||
}
|
||||
color="danger"
|
||||
data-test-subj="srRepositoryListBulkDeleteActionButton"
|
||||
>
|
||||
{selectedItems.length === 1 ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryList.table.deleteSingleRepositoryButton"
|
||||
defaultMessage="Remove repository"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryList.table.deleteMultipleRepositoriesButton"
|
||||
defaultMessage="Remove repositories"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
);
|
||||
}}
|
||||
</RepositoryDeleteProvider>
|
||||
) : (
|
||||
undefined
|
||||
),
|
||||
toolsRight: (
|
||||
<EuiFlexGroup gutterSize="m" justifyContent="spaceAround">
|
||||
<EuiFlexItem>
|
||||
<EuiButton color="secondary" iconType="refresh" onClick={reload}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryList.table.reloadRepositoriesButton"
|
||||
defaultMessage="Reload"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
href={history.createHref({
|
||||
pathname: `${BASE_PATH}/add_repository`,
|
||||
})}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="srRepositoriesAddButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryList.addRepositoryButtonLabel"
|
||||
defaultMessage="Register a repository"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
box: {
|
||||
incremental: true,
|
||||
schema: true,
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'type',
|
||||
name: 'Type',
|
||||
multiSelect: false,
|
||||
options: Object.keys(
|
||||
repositories.reduce((typeMap: any, repository) => {
|
||||
typeMap[repository.type] = true;
|
||||
return typeMap;
|
||||
}, {})
|
||||
).map(type => {
|
||||
return {
|
||||
value: type,
|
||||
view: textService.getRepositoryTypeName(type),
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
items={repositories}
|
||||
itemId="name"
|
||||
columns={columns}
|
||||
search={search}
|
||||
sorting={sorting}
|
||||
selection={selection}
|
||||
pagination={pagination}
|
||||
isSelectable={true}
|
||||
rowProps={() => ({
|
||||
'data-test-subj': 'srRepositoryListTableRow',
|
||||
})}
|
||||
cellProps={(item: any, column: any) => ({
|
||||
'data-test-subj': `srRepositoryListTableCell-${column.field}`,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 { SnapshotList } from './snapshot_list';
|
|
@ -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 { SnapshotDetails } from './snapshot_details';
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiTab,
|
||||
EuiTabs,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import { SectionError, SectionLoading } from '../../../../components';
|
||||
import { useAppDependencies } from '../../../../index';
|
||||
import { loadSnapshot } from '../../../../services/http';
|
||||
import { linkToRepository } from '../../../../services/navigation';
|
||||
import { TabSummary, TabFailures } from './tabs';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
repositoryName: string;
|
||||
snapshotId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TAB_SUMMARY = 'summary';
|
||||
const TAB_FAILURES = 'failures';
|
||||
|
||||
const SnapshotDetailsUi: React.FunctionComponent<Props> = ({
|
||||
repositoryName,
|
||||
snapshotId,
|
||||
onClose,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage, translate },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const { error, data: snapshotDetails } = loadSnapshot(repositoryName, snapshotId);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<string>(TAB_SUMMARY);
|
||||
|
||||
// Reset tab when we look at a different snapshot.
|
||||
useEffect(
|
||||
() => {
|
||||
setActiveTab(TAB_SUMMARY);
|
||||
},
|
||||
[repositoryName, snapshotId]
|
||||
);
|
||||
|
||||
let tabs;
|
||||
|
||||
let content;
|
||||
|
||||
if (snapshotDetails) {
|
||||
const { indexFailures, state: snapshotState } = snapshotDetails;
|
||||
const tabOptions = [
|
||||
{
|
||||
id: TAB_SUMMARY,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.summaryTabTitle"
|
||||
defaultMessage="Summary"
|
||||
/>
|
||||
),
|
||||
testSubj: 'srSnapshotDetailsSummaryTab',
|
||||
},
|
||||
{
|
||||
id: TAB_FAILURES,
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.failuresTabTitle"
|
||||
defaultMessage="Failed indices ({failuresCount})"
|
||||
values={{ failuresCount: indexFailures.length }}
|
||||
/>
|
||||
),
|
||||
testSubj: 'srSnapshotDetailsFailuresTab',
|
||||
},
|
||||
];
|
||||
|
||||
tabs = (
|
||||
<Fragment>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiTabs>
|
||||
{tabOptions.map(tab => (
|
||||
<EuiTab
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
isSelected={tab.id === activeTab}
|
||||
key={tab.id}
|
||||
data-test-subject={tab.testSubj}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
if (activeTab === TAB_SUMMARY) {
|
||||
content = <TabSummary snapshotDetails={snapshotDetails} />;
|
||||
} else if (activeTab === TAB_FAILURES) {
|
||||
content = <TabFailures snapshotState={snapshotState} indexFailures={indexFailures} />;
|
||||
}
|
||||
} else if (error) {
|
||||
const notFound = error.status === 404;
|
||||
const errorObject = notFound
|
||||
? {
|
||||
data: {
|
||||
error: translate('xpack.snapshotRestore.snapshotDetails.errorSnapshotNotFound', {
|
||||
defaultMessage: `Either the snapshot '{snapshotId}' doesn't exist in the repository '{repositoryName}' or the repository doesn't exist.`,
|
||||
values: {
|
||||
snapshotId,
|
||||
repositoryName,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
: error;
|
||||
|
||||
content = (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.errorLoadingRepositoryTitle"
|
||||
defaultMessage="Error loading repository"
|
||||
/>
|
||||
}
|
||||
error={errorObject}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Assume the content is loading.
|
||||
content = (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.loadingSnapshotDescription"
|
||||
defaultMessage="Loading snapshot…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlyout
|
||||
onClose={onClose}
|
||||
data-test-subj="srSnapshotDetailsFlyout"
|
||||
aria-labelledby="srSnapshotDetailsFlyoutTitle"
|
||||
size="m"
|
||||
maxWidth={400}
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="m">
|
||||
<h2 id="srSnapshotDetailsFlyoutTitle" data-test-subj="srSnapshotDetailsFlyoutTitle">
|
||||
{snapshotId}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<EuiLink href={linkToRepository(repositoryName)}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.repositoryTitle"
|
||||
defaultMessage="'{repositoryName}' repository"
|
||||
values={{ repositoryName }}
|
||||
/>
|
||||
</EuiLink>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{tabs}
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody data-test-subj="srSnapshotDetailsContent">{content}</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
||||
|
||||
export const SnapshotDetails = withRouter(SnapshotDetailsUi);
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { TabSummary } from './tab_summary';
|
||||
export { TabFailures } from './tab_failures';
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiIconTip, EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
import { SNAPSHOT_STATE } from '../../../../../constants';
|
||||
import { useAppDependencies } from '../../../../../index';
|
||||
|
||||
interface Props {
|
||||
state: any;
|
||||
}
|
||||
|
||||
export const SnapshotState: React.SFC<Props> = ({ state }) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { translate },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const stateMap: any = {
|
||||
[SNAPSHOT_STATE.IN_PROGRESS]: {
|
||||
icon: <EuiLoadingSpinner size="m" />,
|
||||
label: translate('xpack.snapshotRestore.snapshotState.inProgressLabel', {
|
||||
defaultMessage: 'Taking snapshot…',
|
||||
}),
|
||||
},
|
||||
[SNAPSHOT_STATE.SUCCESS]: {
|
||||
icon: <EuiIcon color="success" type="check" />,
|
||||
label: translate('xpack.snapshotRestore.snapshotState.inProgressLabel', {
|
||||
defaultMessage: 'Snapshot complete',
|
||||
}),
|
||||
},
|
||||
[SNAPSHOT_STATE.FAILED]: {
|
||||
icon: <EuiIcon color="danger" type="cross" />,
|
||||
label: translate('xpack.snapshotRestore.snapshotState.failedLabel', {
|
||||
defaultMessage: 'Snapshot failed',
|
||||
}),
|
||||
},
|
||||
[SNAPSHOT_STATE.PARTIAL]: {
|
||||
icon: <EuiIcon color="warning" type="alert" />,
|
||||
label: translate('xpack.snapshotRestore.snapshotState.partialLabel', {
|
||||
defaultMessage: 'Partial failure',
|
||||
}),
|
||||
tip: translate('xpack.snapshotRestore.snapshotState.partialTipDescription', {
|
||||
defaultMessage: `Global cluster state was stored, but at least one shard wasn't stored successfully. See the 'Failed indices' tab.`,
|
||||
}),
|
||||
},
|
||||
[SNAPSHOT_STATE.INCOMPATIBLE]: {
|
||||
icon: <EuiIcon color="warning" type="alert" />,
|
||||
label: translate('xpack.snapshotRestore.snapshotState.incompatibleLabel', {
|
||||
defaultMessage: 'Incompatible version',
|
||||
}),
|
||||
tip: translate('xpack.snapshotRestore.snapshotState.partialTipDescription', {
|
||||
defaultMessage: `Snapshot was created with a version of Elasticsearch incompatible with the cluster's version.`,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
if (!stateMap[state]) {
|
||||
// Help debug unexpected state.
|
||||
return state;
|
||||
}
|
||||
|
||||
const { icon, label, tip } = stateMap[state];
|
||||
|
||||
const iconTip = tip && (
|
||||
<Fragment>
|
||||
{' '}
|
||||
<EuiIconTip content={tip} />
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>{icon}</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
{/* Escape flex layout created by EuiFlexItem. */}
|
||||
<div>
|
||||
{label}
|
||||
{iconTip}
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 { EuiCodeBlock, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
|
||||
import { SNAPSHOT_STATE } from '../../../../../constants';
|
||||
import { useAppDependencies } from '../../../../../index';
|
||||
|
||||
interface Props {
|
||||
indexFailures: any;
|
||||
snapshotState: string;
|
||||
}
|
||||
|
||||
export const TabFailures: React.SFC<Props> = ({ indexFailures, snapshotState }) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
if (!indexFailures.length) {
|
||||
// If the snapshot is in progress then we still might encounter errors later.
|
||||
if (snapshotState === SNAPSHOT_STATE.IN_PROGRESS) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.snapshotIsBeingCreatedMessage"
|
||||
defaultMessage="Snapshot is being created."
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.noIndexFailuresMessage"
|
||||
defaultMessage="All indices were stored successfully."
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return indexFailures.map((indexObject: any, count: number) => {
|
||||
const { index, failures } = indexObject;
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{index}</h3>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{failures.map((failure: any, failuresCount: number) => {
|
||||
const { status, reason, shard_id: shardId } = failure;
|
||||
|
||||
return (
|
||||
<div key={`${shardId}${reason}`}>
|
||||
<EuiText size="xs">
|
||||
<p>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.failureShardTitle"
|
||||
defaultMessage="Shard {shardId}"
|
||||
values={{ shardId }}
|
||||
/>
|
||||
</strong>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
<EuiCodeBlock paddingSize="s">
|
||||
{status}: {reason}
|
||||
</EuiCodeBlock>
|
||||
|
||||
{failuresCount < failures.length - 1 ? <EuiSpacer size="s" /> : undefined}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{count < indexFailures.length - 1 ? <EuiSpacer size="l" /> : undefined}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,253 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListDescription,
|
||||
EuiDescriptionListTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { SNAPSHOT_STATE } from '../../../../../constants';
|
||||
import { useAppDependencies } from '../../../../../index';
|
||||
import { formatDate } from '../../../../../services/text';
|
||||
import { DataPlaceholder } from '../../../../../components';
|
||||
import { SnapshotState } from './snapshot_state';
|
||||
|
||||
interface Props {
|
||||
snapshotDetails: any;
|
||||
}
|
||||
|
||||
export const TabSummary: React.SFC<Props> = ({ snapshotDetails }) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = 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,
|
||||
// TODO: Add a tooltip explaining that: a false value means that the cluster global state
|
||||
// is not stored as part of the snapshot.
|
||||
includeGlobalState,
|
||||
indices,
|
||||
state,
|
||||
startTimeInMillis,
|
||||
endTimeInMillis,
|
||||
durationInMillis,
|
||||
uuid,
|
||||
} = snapshotDetails;
|
||||
|
||||
const indicesList = indices.length ? (
|
||||
<ul>
|
||||
{indices.map((index: string) => (
|
||||
<li key={index}>
|
||||
<EuiTitle size="xs">
|
||||
<span>{index}</span>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemIndicesNoneLabel"
|
||||
data-test-subj="srSnapshotDetailsIndicesNoneTitle"
|
||||
defaultMessage="-"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsVersionItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemVersionLabel"
|
||||
data-test-subj="srSnapshotDetailsVersionTitle"
|
||||
defaultMessage="Version / Version ID"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailsVersionDescription"
|
||||
>
|
||||
{version} / {versionId}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsIncludeGlobalUuidItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemUuidLabel"
|
||||
data-test-subj="srSnapshotDetailsUuidTitle"
|
||||
defaultMessage="UUID"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailUuidDescription"
|
||||
>
|
||||
{uuid}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsStateItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemStateLabel"
|
||||
data-test-subj="srSnapshotDetailsStateTitle"
|
||||
defaultMessage="State"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailStateDescription"
|
||||
>
|
||||
<SnapshotState state={state} />
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsIncludeGlobalStateItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemIncludeGlobalStateLabel"
|
||||
data-test-subj="srSnapshotDetailsIncludeGlobalStateTitle"
|
||||
defaultMessage="Includes global state"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailIncludeGlobalStateDescription"
|
||||
>
|
||||
{includeGlobalStateToHumanizedMap[includeGlobalState]}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsIndicesItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemIndicesLabel"
|
||||
data-test-subj="srSnapshotDetailsIndicesTitle"
|
||||
defaultMessage="Indices ({indicesCount})"
|
||||
values={{ indicesCount: indices.length }}
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailIndicesDescription"
|
||||
>
|
||||
<EuiText>{indicesList}</EuiText>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsStartTimeItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemStartTimeLabel"
|
||||
data-test-subj="srSnapshotDetailsStartTimeTitle"
|
||||
defaultMessage="Start time"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailStartTimeDescription"
|
||||
>
|
||||
<DataPlaceholder data={startTimeInMillis}>
|
||||
{formatDate(startTimeInMillis)}
|
||||
</DataPlaceholder>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsEndTimeItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemEndTimeLabel"
|
||||
data-test-subj="srSnapshotDetailsEndTimeTitle"
|
||||
defaultMessage="End time"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailEndTimeDescription"
|
||||
>
|
||||
{state === SNAPSHOT_STATE.IN_PROGRESS ? (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
) : (
|
||||
<DataPlaceholder data={endTimeInMillis}>
|
||||
{formatDate(endTimeInMillis)}
|
||||
</DataPlaceholder>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="srSnapshotDetailsDurationItem">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemDurationLabel"
|
||||
data-test-subj="srSnapshotDetailsDurationTitle"
|
||||
defaultMessage="Duration"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription
|
||||
className="eui-textBreakWord"
|
||||
data-test-subj="srSnapshotDetailDurationDescription"
|
||||
>
|
||||
{state === SNAPSHOT_STATE.IN_PROGRESS ? (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
) : (
|
||||
<DataPlaceholder data={durationInMillis}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotDetails.itemDurationValueLabel"
|
||||
data-test-subj="srSnapshotDetailsDurationValue"
|
||||
defaultMessage="{seconds} {seconds, plural, one {second} other {seconds}}"
|
||||
values={{ seconds: Math.ceil(durationInMillis / 1000) }}
|
||||
/>
|
||||
</DataPlaceholder>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiDescriptionList>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
* 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, useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { parse } from 'querystring';
|
||||
|
||||
import { EuiButton, EuiCallOut, EuiIcon, EuiLink, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { SectionError, SectionLoading } from '../../../components';
|
||||
import { BASE_PATH } from '../../../constants';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { documentationLinksService } from '../../../services/documentation';
|
||||
import { loadSnapshots } from '../../../services/http';
|
||||
import { linkToRepositories } from '../../../services/navigation';
|
||||
|
||||
import { SnapshotDetails } from './snapshot_details';
|
||||
import { SnapshotTable } from './snapshot_table';
|
||||
|
||||
interface MatchParams {
|
||||
repositoryName?: string;
|
||||
snapshotId?: string;
|
||||
}
|
||||
|
||||
export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchParams>> = ({
|
||||
match: {
|
||||
params: { repositoryName, snapshotId },
|
||||
},
|
||||
location: { search },
|
||||
history,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const {
|
||||
error,
|
||||
loading,
|
||||
data: { snapshots = [], repositories = [], errors = {} },
|
||||
request: reload,
|
||||
} = loadSnapshots();
|
||||
|
||||
const openSnapshotDetails = (repositoryNameToOpen: string, snapshotIdToOpen: string) => {
|
||||
history.push(
|
||||
`${BASE_PATH}/snapshots/${encodeURIComponent(repositoryNameToOpen)}/${encodeURIComponent(
|
||||
snapshotIdToOpen
|
||||
)}`
|
||||
);
|
||||
};
|
||||
|
||||
const closeSnapshotDetails = () => {
|
||||
history.push(`${BASE_PATH}/snapshots`);
|
||||
};
|
||||
|
||||
// Allow deeplinking to list pre-filtered by repository name
|
||||
const [filteredRepository, setFilteredRepository] = useState<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (search) {
|
||||
const parsedParams = parse(search.replace(/^\?/, ''));
|
||||
if (parsedParams.repository && parsedParams.repository !== filteredRepository) {
|
||||
setFilteredRepository(String(parsedParams.repository));
|
||||
history.replace(`${BASE_PATH}/snapshots`);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
let content;
|
||||
|
||||
if (loading) {
|
||||
content = (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.loadingSnapshotsDescription"
|
||||
defaultMessage="Loading snapshots…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
} else if (error) {
|
||||
content = (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.loadingSnapshotsErrorMessage"
|
||||
defaultMessage="Error loading snapshots"
|
||||
/>
|
||||
}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
} else if (Object.keys(errors).length && repositories.length === 0) {
|
||||
content = (
|
||||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.errorRepositoriesTitle"
|
||||
defaultMessage="Some repositories contain errors"
|
||||
/>
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.repositoryWarningDescription"
|
||||
defaultMessage="Go to {repositoryLink} to fix the errors."
|
||||
values={{
|
||||
repositoryLink: (
|
||||
<EuiLink href={linkToRepositories()}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryWarningLinkText"
|
||||
defaultMessage="Repositories"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<EuiLink
|
||||
href={documentationLinksService.getSnapshotDocUrl()}
|
||||
target="_blank"
|
||||
data-test-subj="srSnapshotsEmptyPromptDocLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.emptyPrompt.noSnapshotsDocLinkText"
|
||||
defaultMessage="Learn how to create a snapshot"
|
||||
/>{' '}
|
||||
<EuiIcon type="link" />
|
||||
</EuiLink>
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (repositories.length === 0) {
|
||||
content = (
|
||||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noRepositoriesTitle"
|
||||
defaultMessage="You don't have any snapshots or repositories yet"
|
||||
/>
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noRepositoriesDescription"
|
||||
defaultMessage="Start by registering a repository for your snapshots."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<EuiButton
|
||||
href={history.createHref({
|
||||
pathname: `${BASE_PATH}/add_repository`,
|
||||
})}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="srSnapshotsEmptyPromptAddRepositoryButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noRepositoriesAddButtonLabel"
|
||||
defaultMessage="Register a repository"
|
||||
/>
|
||||
</EuiButton>
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (snapshots.length === 0) {
|
||||
content = (
|
||||
<EuiEmptyPrompt
|
||||
iconType="managementApp"
|
||||
title={
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noSnapshotsTitle"
|
||||
defaultMessage="You don't have any snapshots yet"
|
||||
/>
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noSnapshotsDescription"
|
||||
defaultMessage="Create a snapshot using the Elasticsearch API."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<EuiLink
|
||||
href={documentationLinksService.getSnapshotDocUrl()}
|
||||
target="_blank"
|
||||
data-test-subj="srSnapshotsEmptyPromptDocLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.emptyPrompt.noSnapshotsDocLinkText"
|
||||
defaultMessage="Learn how to create a snapshot"
|
||||
/>{' '}
|
||||
<EuiIcon type="link" />
|
||||
</EuiLink>
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const repositoryErrorsWarning = Object.keys(errors).length ? (
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryWarningTitle"
|
||||
defaultMessage="Some repositories contain errors"
|
||||
/>
|
||||
}
|
||||
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>
|
||||
) : null;
|
||||
|
||||
content = (
|
||||
<Fragment>
|
||||
{repositoryErrorsWarning}
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<SnapshotTable
|
||||
snapshots={snapshots}
|
||||
repositories={repositories}
|
||||
reload={reload}
|
||||
openSnapshotDetails={openSnapshotDetails}
|
||||
repositoryFilter={filteredRepository}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{repositoryName && snapshotId ? (
|
||||
<SnapshotDetails
|
||||
repositoryName={repositoryName}
|
||||
snapshotId={snapshotId}
|
||||
onClose={closeSnapshotDetails}
|
||||
/>
|
||||
) : null}
|
||||
{content}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -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 { SnapshotTable } from './snapshot_table';
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiInMemoryTable, EuiLink, Query, EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
import { SnapshotDetails } from '../../../../../../common/types';
|
||||
import { SNAPSHOT_STATE } from '../../../../constants';
|
||||
import { useAppDependencies } from '../../../../index';
|
||||
import { formatDate } from '../../../../services/text';
|
||||
import { linkToRepository } from '../../../../services/navigation';
|
||||
import { DataPlaceholder } from '../../../../components';
|
||||
|
||||
interface Props {
|
||||
snapshots: SnapshotDetails[];
|
||||
repositories: string[];
|
||||
reload: () => Promise<void>;
|
||||
openSnapshotDetails: (repositoryName: string, snapshotId: string) => void;
|
||||
repositoryFilter?: string;
|
||||
}
|
||||
|
||||
export const SnapshotTable: React.FunctionComponent<Props> = ({
|
||||
snapshots,
|
||||
repositories,
|
||||
reload,
|
||||
openSnapshotDetails,
|
||||
repositoryFilter,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage, translate },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'snapshot',
|
||||
name: translate('xpack.snapshotRestore.snapshotList.table.snapshotColumnTitle', {
|
||||
defaultMessage: 'Snapshot',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
render: (snapshotId: string, snapshot: SnapshotDetails) => (
|
||||
<EuiLink onClick={() => openSnapshotDetails(snapshot.repository, snapshotId)}>
|
||||
{snapshotId}
|
||||
</EuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'repository',
|
||||
name: translate('xpack.snapshotRestore.snapshotList.table.repositoryColumnTitle', {
|
||||
defaultMessage: 'Repository',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
render: (repositoryName: string) => (
|
||||
<EuiLink href={linkToRepository(repositoryName)}>{repositoryName}</EuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'startTimeInMillis',
|
||||
name: translate('xpack.snapshotRestore.snapshotList.table.startTimeColumnTitle', {
|
||||
defaultMessage: 'Date created',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
render: (startTimeInMillis: number) => (
|
||||
<DataPlaceholder data={startTimeInMillis}>{formatDate(startTimeInMillis)}</DataPlaceholder>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'durationInMillis',
|
||||
name: translate('xpack.snapshotRestore.snapshotList.table.durationColumnTitle', {
|
||||
defaultMessage: 'Duration',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (durationInMillis: number, { state }: SnapshotDetails) => {
|
||||
if (state === SNAPSHOT_STATE.IN_PROGRESS) {
|
||||
return <EuiLoadingSpinner size="m" />;
|
||||
}
|
||||
return (
|
||||
<DataPlaceholder data={durationInMillis}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.table.durationColumnValueLabel"
|
||||
defaultMessage="{seconds}s"
|
||||
values={{ seconds: Math.ceil(durationInMillis / 1000) }}
|
||||
/>
|
||||
</DataPlaceholder>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'indices',
|
||||
name: translate('xpack.snapshotRestore.snapshotList.table.indicesColumnTitle', {
|
||||
defaultMessage: 'Indices',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (indices: string[]) => indices.length,
|
||||
},
|
||||
{
|
||||
field: 'shards.total',
|
||||
name: translate('xpack.snapshotRestore.snapshotList.table.shardsColumnTitle', {
|
||||
defaultMessage: 'Shards',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (totalShards: number) => totalShards,
|
||||
},
|
||||
{
|
||||
field: 'shards.failed',
|
||||
name: translate('xpack.snapshotRestore.snapshotList.table.failedShardsColumnTitle', {
|
||||
defaultMessage: 'Failed shards',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
width: '100px',
|
||||
render: (failedShards: number) => failedShards,
|
||||
},
|
||||
];
|
||||
|
||||
// By default, we'll display the most recent snapshots at the top of the table.
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: 'startTimeInMillis',
|
||||
direction: 'desc',
|
||||
},
|
||||
};
|
||||
|
||||
const pagination = {
|
||||
initialPageSize: 20,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
};
|
||||
|
||||
const searchSchema = {
|
||||
fields: {
|
||||
repository: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const search = {
|
||||
toolsRight: (
|
||||
<EuiButton color="secondary" iconType="refresh" onClick={reload}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.table.reloadSnapshotsButton"
|
||||
defaultMessage="Reload"
|
||||
/>
|
||||
</EuiButton>
|
||||
),
|
||||
box: {
|
||||
incremental: true,
|
||||
schema: searchSchema,
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'repository',
|
||||
name: 'Repository',
|
||||
multiSelect: false,
|
||||
options: repositories.map(repository => ({
|
||||
value: repository,
|
||||
view: repository,
|
||||
})),
|
||||
},
|
||||
],
|
||||
defaultQuery: repositoryFilter
|
||||
? Query.parse(`repository:'${repositoryFilter}'`, {
|
||||
schema: {
|
||||
...searchSchema,
|
||||
strict: true,
|
||||
},
|
||||
})
|
||||
: '',
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
items={snapshots}
|
||||
itemId="name"
|
||||
columns={columns}
|
||||
search={search}
|
||||
sorting={sorting}
|
||||
pagination={pagination}
|
||||
rowProps={() => ({
|
||||
'data-test-subj': 'srSnapshotListTableRow',
|
||||
})}
|
||||
cellProps={(item: any, column: any) => ({
|
||||
'data-test-subj': `srSnapshotListTableCell-${column.field}`,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { SnapshotRestoreHome } from './home';
|
||||
export { RepositoryAdd } from './repository_add';
|
||||
export { RepositoryEdit } from './repository_edit';
|
|
@ -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 { RepositoryAdd } from './repository_add';
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { Repository, EmptyRepository } from '../../../../common/types';
|
||||
|
||||
import { RepositoryForm, SectionError } from '../../components';
|
||||
import { BASE_PATH, Section } from '../../constants';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { breadcrumbService } from '../../services/navigation';
|
||||
import { addRepository } from '../../services/http';
|
||||
|
||||
export const RepositoryAdd: React.FunctionComponent<RouteComponentProps> = ({ history }) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
const section = 'repositories' as Section;
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [saveError, setSaveError] = useState<any>(null);
|
||||
|
||||
// Set breadcrumb
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs('repositoryAdd');
|
||||
}, []);
|
||||
|
||||
const onSave = async (newRepository: Repository | EmptyRepository) => {
|
||||
setIsSaving(true);
|
||||
setSaveError(null);
|
||||
const { name } = newRepository;
|
||||
const { error } = await addRepository(newRepository);
|
||||
setIsSaving(false);
|
||||
if (error) {
|
||||
setSaveError(error);
|
||||
} else {
|
||||
history.push(`${BASE_PATH}/${section}/${name}`);
|
||||
}
|
||||
};
|
||||
|
||||
const emptyRepository = {
|
||||
name: '',
|
||||
type: null,
|
||||
settings: {},
|
||||
};
|
||||
|
||||
const renderSaveError = () => {
|
||||
return saveError ? (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.addRepository.savingRepositoryErrorTitle"
|
||||
defaultMessage="Cannot register new repository"
|
||||
/>
|
||||
}
|
||||
error={saveError}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const clearSaveError = () => {
|
||||
setSaveError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.addRepositoryTitle"
|
||||
defaultMessage="Register repository"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
<RepositoryForm
|
||||
repository={emptyRepository}
|
||||
isSaving={isSaving}
|
||||
saveError={renderSaveError()}
|
||||
clearSaveError={clearSaveError}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
|
@ -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 { RepositoryEdit } from './repository_edit';
|
|
@ -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 { Repository, EmptyRepository } from '../../../../common/types';
|
||||
|
||||
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';
|
||||
|
||||
interface MatchParams {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const RepositoryEdit: React.FunctionComponent<RouteComponentProps<MatchParams>> = ({
|
||||
match: {
|
||||
params: { name },
|
||||
},
|
||||
history,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const section = 'repositories' as Section;
|
||||
|
||||
// Set breadcrumb
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs('repositoryEdit');
|
||||
}, []);
|
||||
|
||||
// Repository state with default empty repository
|
||||
const [repository, setRepository] = useState<Repository | EmptyRepository>({
|
||||
name: '',
|
||||
type: null,
|
||||
settings: {},
|
||||
});
|
||||
|
||||
// Load repository
|
||||
const {
|
||||
error: repositoryError,
|
||||
loading: loadingRepository,
|
||||
data: repositoryData,
|
||||
} = loadRepository(name);
|
||||
|
||||
// Update repository state when data is loaded
|
||||
useEffect(
|
||||
() => {
|
||||
if (repositoryData && repositoryData.repository) {
|
||||
setRepository(repositoryData.repository);
|
||||
}
|
||||
},
|
||||
[repositoryData]
|
||||
);
|
||||
|
||||
// Saving repository states
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [saveError, setSaveError] = useState<any>(null);
|
||||
|
||||
// Save repository
|
||||
const onSave = async (editedRepository: Repository | EmptyRepository) => {
|
||||
setIsSaving(true);
|
||||
setSaveError(null);
|
||||
const { error } = await editRepository(editedRepository);
|
||||
setIsSaving(false);
|
||||
if (error) {
|
||||
setSaveError(error);
|
||||
} else {
|
||||
history.push(`${BASE_PATH}/${section}/${name}`);
|
||||
}
|
||||
};
|
||||
|
||||
const renderLoading = () => {
|
||||
return (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.editRepository.loadingRepositoryDescription"
|
||||
defaultMessage="Loading repository details…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
};
|
||||
|
||||
const renderError = () => {
|
||||
const notFound = repositoryError.status === 404;
|
||||
const errorObject = notFound
|
||||
? {
|
||||
data: {
|
||||
error: i18n.translate(
|
||||
'xpack.snapshotRestore.editRepository.repositoryNotFoundErrorMessage',
|
||||
{
|
||||
defaultMessage: `The repository '{name}' does not exist.`,
|
||||
values: {
|
||||
name,
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
: repositoryError;
|
||||
return (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.editRepository.loadingRepositoryErrorTitle"
|
||||
defaultMessage="Error loading repository details"
|
||||
/>
|
||||
}
|
||||
error={errorObject}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSaveError = () => {
|
||||
return saveError ? (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.editRepository.avingRepositoryErrorTitle"
|
||||
defaultMessage="Cannot save repository"
|
||||
/>
|
||||
}
|
||||
error={saveError}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const clearSaveError = () => {
|
||||
setSaveError(null);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (loadingRepository) {
|
||||
return renderLoading();
|
||||
}
|
||||
if (repositoryError) {
|
||||
return renderError();
|
||||
}
|
||||
|
||||
return (
|
||||
<RepositoryForm
|
||||
repository={repository}
|
||||
isEditing={true}
|
||||
isSaving={isSaving}
|
||||
saveError={renderSaveError()}
|
||||
clearSaveError={clearSaveError}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.editRepositoryTitle"
|
||||
defaultMessage="Edit repository"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
{renderContent()}
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
|
@ -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 { REPOSITORY_TYPES } from '../../../../common/constants';
|
||||
import { RepositoryType } from '../../../../common/types';
|
||||
import { REPOSITORY_DOC_PATHS } from '../../constants';
|
||||
|
||||
class DocumentationLinksService {
|
||||
private esDocBasePath: string = '';
|
||||
private esPluginDocBasePath: string = '';
|
||||
|
||||
public init(esDocBasePath: string, esPluginDocBasePath: string): void {
|
||||
this.esDocBasePath = esDocBasePath;
|
||||
this.esPluginDocBasePath = esPluginDocBasePath;
|
||||
}
|
||||
|
||||
public getRepositoryPluginDocUrl() {
|
||||
return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.plugins}`;
|
||||
}
|
||||
|
||||
public getRepositoryTypeDocUrl(type?: RepositoryType) {
|
||||
switch (type) {
|
||||
case REPOSITORY_TYPES.fs:
|
||||
return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.fs}`;
|
||||
case REPOSITORY_TYPES.url:
|
||||
return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.url}`;
|
||||
case REPOSITORY_TYPES.source:
|
||||
return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.source}`;
|
||||
case REPOSITORY_TYPES.s3:
|
||||
return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.s3}`;
|
||||
case REPOSITORY_TYPES.hdfs:
|
||||
return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.hdfs}`;
|
||||
case REPOSITORY_TYPES.azure:
|
||||
return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.azure}`;
|
||||
case REPOSITORY_TYPES.gcs:
|
||||
return `${this.esPluginDocBasePath}${REPOSITORY_DOC_PATHS.gcs}`;
|
||||
default:
|
||||
return `${this.esDocBasePath}${REPOSITORY_DOC_PATHS.default}`;
|
||||
}
|
||||
}
|
||||
|
||||
public getSnapshotDocUrl() {
|
||||
return `${this.esDocBasePath}/modules-snapshots.html#_snapshot`;
|
||||
}
|
||||
}
|
||||
|
||||
export const documentationLinksService = new DocumentationLinksService();
|
|
@ -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 { documentationLinksService } from './documentation_links';
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { httpService } from './http';
|
||||
import { useRequest } from './use_request';
|
||||
|
||||
export const loadPermissions = () => {
|
||||
return useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}permissions`),
|
||||
method: 'get',
|
||||
});
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
class HttpService {
|
||||
private client: any;
|
||||
public addBasePath: (path: string) => string = () => '';
|
||||
|
||||
public init(httpClient: any, chrome: any): void {
|
||||
this.client = httpClient;
|
||||
this.addBasePath = chrome.addBasePath.bind(chrome);
|
||||
}
|
||||
|
||||
get httpClient(): any {
|
||||
return this.client;
|
||||
}
|
||||
}
|
||||
|
||||
export const httpService = new HttpService();
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { httpService } from './http';
|
||||
export * from './app_requests';
|
||||
export * from './repository_requests';
|
||||
export * from './snapshot_requests';
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { Repository, EmptyRepository } from '../../../../common/types';
|
||||
import { MINIMUM_TIMEOUT_MS } from '../../constants';
|
||||
import { httpService } from './http';
|
||||
import { sendRequest, useRequest } from './use_request';
|
||||
|
||||
export const loadRepositories = () => {
|
||||
return useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}repositories`),
|
||||
method: 'get',
|
||||
initialData: [],
|
||||
timeout: MINIMUM_TIMEOUT_MS,
|
||||
});
|
||||
};
|
||||
|
||||
export const loadRepository = (name: Repository['name']) => {
|
||||
return useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}repositories/${encodeURIComponent(name)}`),
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
export const verifyRepository = (name: Repository['name']) => {
|
||||
return sendRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}repositories/${encodeURIComponent(name)}/verify`
|
||||
),
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
export const loadRepositoryTypes = () => {
|
||||
return useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}repository_types`),
|
||||
method: 'get',
|
||||
initialData: [],
|
||||
});
|
||||
};
|
||||
|
||||
export const addRepository = async (newRepository: Repository | EmptyRepository) => {
|
||||
return sendRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}repositories`),
|
||||
method: 'put',
|
||||
body: newRepository,
|
||||
});
|
||||
};
|
||||
|
||||
export const editRepository = async (editedRepository: Repository | EmptyRepository) => {
|
||||
return sendRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}repositories/${encodeURIComponent(editedRepository.name)}`
|
||||
),
|
||||
method: 'put',
|
||||
body: editedRepository,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteRepositories = async (names: Array<Repository['name']>) => {
|
||||
return sendRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}repositories/${names.map(name => encodeURIComponent(name)).join(',')}`
|
||||
),
|
||||
method: 'delete',
|
||||
});
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { MINIMUM_TIMEOUT_MS } from '../../constants';
|
||||
import { httpService } from './http';
|
||||
import { useRequest } from './use_request';
|
||||
|
||||
export const loadSnapshots = () =>
|
||||
useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}snapshots`),
|
||||
method: 'get',
|
||||
initialData: [],
|
||||
timeout: MINIMUM_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
export const loadSnapshot = (repositoryName: string, snapshotId: string) =>
|
||||
useRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}snapshots/${encodeURIComponent(repositoryName)}/${encodeURIComponent(
|
||||
snapshotId
|
||||
)}`
|
||||
),
|
||||
method: 'get',
|
||||
});
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
import { httpService } from './index';
|
||||
|
||||
interface SendRequest {
|
||||
path: string;
|
||||
method: string;
|
||||
body?: any;
|
||||
}
|
||||
|
||||
interface SendRequestResponse {
|
||||
data: any;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
export const sendRequest = async ({
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
}: SendRequest): Promise<Partial<SendRequestResponse>> => {
|
||||
try {
|
||||
const response = await httpService.httpClient[method](path, body);
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
return {
|
||||
data: response.data,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
interface UseRequest extends SendRequest {
|
||||
interval?: number;
|
||||
initialData?: any;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export const useRequest = ({ path, method, body, interval, initialData, timeout }: UseRequest) => {
|
||||
const [error, setError] = useState<null | any>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [data, setData] = useState<any>(initialData);
|
||||
|
||||
// Tied to every render and bound to each request.
|
||||
let isOutdatedRequest = false;
|
||||
|
||||
const request = async () => {
|
||||
setError(null);
|
||||
setData(initialData);
|
||||
setLoading(true);
|
||||
|
||||
const requestBody = {
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
if (timeout) {
|
||||
[response] = await Promise.all([
|
||||
sendRequest(requestBody),
|
||||
new Promise(resolve => setTimeout(resolve, timeout)),
|
||||
]);
|
||||
} else {
|
||||
response = await sendRequest(requestBody);
|
||||
}
|
||||
|
||||
// Don't update state if an outdated request has resolved.
|
||||
if (isOutdatedRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError(response.error);
|
||||
setData(response.data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
function cancelOutdatedRequest() {
|
||||
isOutdatedRequest = true;
|
||||
}
|
||||
|
||||
request();
|
||||
|
||||
if (interval) {
|
||||
const intervalRequest = setInterval(request, interval);
|
||||
return () => {
|
||||
cancelOutdatedRequest();
|
||||
clearInterval(intervalRequest);
|
||||
};
|
||||
}
|
||||
|
||||
// Called when a new render will trigger this effect.
|
||||
return cancelOutdatedRequest;
|
||||
},
|
||||
[path]
|
||||
);
|
||||
|
||||
return {
|
||||
error,
|
||||
loading,
|
||||
data,
|
||||
request,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { BASE_PATH } from '../../constants';
|
||||
import { textService } from '../text';
|
||||
|
||||
class BreadcrumbService {
|
||||
private chrome: any;
|
||||
private breadcrumbs: any = {
|
||||
management: {},
|
||||
home: {},
|
||||
repositoryAdd: {},
|
||||
repositoryEdit: {},
|
||||
};
|
||||
|
||||
public init(chrome: any, managementBreadcrumb: any): void {
|
||||
this.chrome = chrome;
|
||||
this.breadcrumbs.management = managementBreadcrumb;
|
||||
this.breadcrumbs.home = {
|
||||
text: textService.breadcrumbs.home,
|
||||
href: `#${BASE_PATH}`,
|
||||
};
|
||||
this.breadcrumbs.repositoryAdd = {
|
||||
text: textService.breadcrumbs.repositoryAdd,
|
||||
href: `#${BASE_PATH}/add_repository`,
|
||||
};
|
||||
this.breadcrumbs.repositoryEdit = {
|
||||
text: textService.breadcrumbs.repositoryEdit,
|
||||
};
|
||||
}
|
||||
|
||||
public setBreadcrumbs(type: string): void {
|
||||
if (!this.breadcrumbs[type]) {
|
||||
return;
|
||||
}
|
||||
if (type === 'home') {
|
||||
this.chrome.breadcrumbs.set([this.breadcrumbs.management, this.breadcrumbs.home]);
|
||||
} else {
|
||||
this.chrome.breadcrumbs.set([
|
||||
this.breadcrumbs.management,
|
||||
this.breadcrumbs.home,
|
||||
this.breadcrumbs[type],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const breadcrumbService = new BreadcrumbService();
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { breadcrumbService } from './breadcrumb';
|
||||
export { linkToRepository, linkToRepositories, linkToSnapshots } from './links';
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { BASE_PATH } from '../../constants';
|
||||
|
||||
export function linkToRepositories() {
|
||||
return `#${BASE_PATH}/repositories`;
|
||||
}
|
||||
|
||||
export function linkToRepository(repositoryName: string) {
|
||||
return `#${BASE_PATH}/repositories/${encodeURIComponent(repositoryName)}`;
|
||||
}
|
||||
|
||||
export function linkToSnapshots(repositoryName?: string) {
|
||||
if (repositoryName) {
|
||||
return `#${BASE_PATH}/snapshots?repository=${repositoryName}`;
|
||||
}
|
||||
return `#${BASE_PATH}/snapshots`;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { createContext, useContext } from 'react';
|
||||
import { AppState } from '../../types';
|
||||
|
||||
const StateContext = createContext<AppState>({});
|
||||
|
||||
export const initialState = {};
|
||||
|
||||
export const reducer = (state: any, action: { type: string }) => {
|
||||
switch (action.type) {
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const AppStateProvider = StateContext.Provider;
|
||||
|
||||
export const useAppState = () => useContext<AppState>(StateContext);
|
|
@ -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 { initialState, reducer, AppStateProvider, useAppState } from './app_state';
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { dateFormatAliases } from '@elastic/eui/lib/services/format';
|
||||
import moment from 'moment';
|
||||
|
||||
export function formatDate(epochMs: number): string {
|
||||
return moment(Number(epochMs)).format(dateFormatAliases.longDateTime);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { formatDate } from './format_date';
|
||||
export { textService } from './text';
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 { REPOSITORY_TYPES } from '../../../../common/constants';
|
||||
|
||||
class TextService {
|
||||
public breadcrumbs: { [key: string]: string } = {};
|
||||
public i18n: any;
|
||||
private repositoryTypeNames: { [key: string]: string } = {};
|
||||
|
||||
public init(i18n: any): void {
|
||||
this.i18n = i18n;
|
||||
this.repositoryTypeNames = {
|
||||
[REPOSITORY_TYPES.fs]: i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryType.fileSystemTypeName',
|
||||
{
|
||||
defaultMessage: 'Shared file system',
|
||||
}
|
||||
),
|
||||
[REPOSITORY_TYPES.url]: i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryType.readonlyTypeName',
|
||||
{
|
||||
defaultMessage: 'Read-only URL',
|
||||
}
|
||||
),
|
||||
[REPOSITORY_TYPES.s3]: i18n.translate('xpack.snapshotRestore.repositoryType.s3TypeName', {
|
||||
defaultMessage: 'AWS S3',
|
||||
}),
|
||||
[REPOSITORY_TYPES.hdfs]: i18n.translate('xpack.snapshotRestore.repositoryType.hdfsTypeName', {
|
||||
defaultMessage: 'Hadoop HDFS',
|
||||
}),
|
||||
[REPOSITORY_TYPES.azure]: i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryType.azureTypeName',
|
||||
{
|
||||
defaultMessage: 'Azure',
|
||||
}
|
||||
),
|
||||
[REPOSITORY_TYPES.gcs]: i18n.translate('xpack.snapshotRestore.repositoryType.gcsTypeName', {
|
||||
defaultMessage: 'Google Cloud Storage',
|
||||
}),
|
||||
[REPOSITORY_TYPES.source]: i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryType.sourceTypeName',
|
||||
{
|
||||
defaultMessage: 'Source-only',
|
||||
}
|
||||
),
|
||||
};
|
||||
this.breadcrumbs = {
|
||||
home: i18n.translate('xpack.snapshotRestore.home.breadcrumbTitle', {
|
||||
defaultMessage: 'Snapshot Repositories',
|
||||
}),
|
||||
repositoryAdd: i18n.translate('xpack.snapshotRestore.addRepository.breadcrumbTitle', {
|
||||
defaultMessage: 'Add repository',
|
||||
}),
|
||||
repositoryEdit: i18n.translate('xpack.snapshotRestore.editRepository.breadcrumbTitle', {
|
||||
defaultMessage: 'Edit repository',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
public getRepositoryTypeName(type: string, delegateType?: string) {
|
||||
const getTypeName = (repositoryType: string): string => {
|
||||
return this.repositoryTypeNames[repositoryType] || type || '';
|
||||
};
|
||||
|
||||
if (type === REPOSITORY_TYPES.source && delegateType) {
|
||||
return this.i18n.translate(
|
||||
'xpack.snapshotRestore.repositoryType.sourceTypeWithDelegateName',
|
||||
{
|
||||
defaultMessage: '{delegateType} (Source-only)',
|
||||
values: {
|
||||
delegateType: getTypeName(delegateType),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return getTypeName(type);
|
||||
}
|
||||
|
||||
public getSizeNotationHelpText() {
|
||||
return this.i18n.translate('xpack.snapshotRestore.repositoryForm.sizeNotationPlaceholder', {
|
||||
defaultMessage: 'Examples: {example1}, {example2}, {example3}, {example4}',
|
||||
values: {
|
||||
example1: '1g',
|
||||
example2: '10mb',
|
||||
example3: '5k',
|
||||
example4: '1024B',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const textService = new TextService();
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export {
|
||||
RepositoryValidation,
|
||||
RepositorySettingsValidation,
|
||||
validateRepository,
|
||||
} from './validate_repository';
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* 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 { textService } from '../text';
|
||||
import {
|
||||
Repository,
|
||||
RepositoryType,
|
||||
FSRepository,
|
||||
ReadonlyRepository,
|
||||
S3Repository,
|
||||
GCSRepository,
|
||||
HDFSRepository,
|
||||
EmptyRepository,
|
||||
} from '../../../../common/types';
|
||||
import { REPOSITORY_TYPES } from '../../../../common/constants';
|
||||
|
||||
export interface RepositoryValidation {
|
||||
isValid: boolean;
|
||||
errors: {
|
||||
name?: string[];
|
||||
type?: string[];
|
||||
settings?: RepositorySettingsValidation;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RepositorySettingsValidation {
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
export const validateRepository = (
|
||||
repository: Repository | EmptyRepository,
|
||||
validateSettings: boolean = true
|
||||
): RepositoryValidation => {
|
||||
const { name, type, settings } = repository;
|
||||
const { i18n } = textService;
|
||||
const validation: RepositoryValidation = {
|
||||
isValid: true,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
if (validateSettings) {
|
||||
const settingsValidation = validateRepositorySettings(type, settings);
|
||||
|
||||
if (Object.keys(settingsValidation).length) {
|
||||
validation.errors.settings = settingsValidation;
|
||||
}
|
||||
}
|
||||
|
||||
if (isStringEmpty(name)) {
|
||||
validation.errors.name = [
|
||||
i18n.translate('xpack.snapshotRestore.repositoryValidation.nameRequired', {
|
||||
defaultMessage: 'Repository name is required.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
isStringEmpty(type) ||
|
||||
(type === REPOSITORY_TYPES.source && isStringEmpty(settings.delegateType))
|
||||
) {
|
||||
validation.errors.type = [
|
||||
i18n.translate('xpack.snapshotRestore.repositoryValidation.delegateTypeRequired', {
|
||||
defaultMessage: 'Type is required.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
if (Object.keys(validation.errors).length) {
|
||||
validation.isValid = false;
|
||||
}
|
||||
|
||||
return validation;
|
||||
};
|
||||
|
||||
const isStringEmpty = (str: string | null): boolean => {
|
||||
return str ? !Boolean(str.trim()) : true;
|
||||
};
|
||||
|
||||
const validateRepositorySettings = (
|
||||
type: RepositoryType | null,
|
||||
settings: Repository['settings']
|
||||
): RepositorySettingsValidation => {
|
||||
switch (type) {
|
||||
case REPOSITORY_TYPES.fs:
|
||||
return validateFSRepositorySettings(settings);
|
||||
case REPOSITORY_TYPES.url:
|
||||
return validateReadonlyRepositorySettings(settings);
|
||||
case REPOSITORY_TYPES.source:
|
||||
return validateRepositorySettings(settings.delegateType, settings);
|
||||
case REPOSITORY_TYPES.s3:
|
||||
return validateS3RepositorySettings(settings);
|
||||
case REPOSITORY_TYPES.gcs:
|
||||
return validateGCSRepositorySettings(settings);
|
||||
case REPOSITORY_TYPES.hdfs:
|
||||
return validateHDFSRepositorySettings(settings);
|
||||
// No validation on settings needed for azure (all settings are optional)
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const validateFSRepositorySettings = (
|
||||
settings: FSRepository['settings']
|
||||
): RepositorySettingsValidation => {
|
||||
const i18n = textService.i18n;
|
||||
const validation: RepositorySettingsValidation = {};
|
||||
const { location } = settings;
|
||||
if (isStringEmpty(location)) {
|
||||
validation.location = [
|
||||
i18n.translate('xpack.snapshotRestore.repositoryValidation.locationRequired', {
|
||||
defaultMessage: 'Location is required.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
return validation;
|
||||
};
|
||||
|
||||
const validateReadonlyRepositorySettings = (
|
||||
settings: ReadonlyRepository['settings']
|
||||
): RepositorySettingsValidation => {
|
||||
const i18n = textService.i18n;
|
||||
const validation: RepositorySettingsValidation = {};
|
||||
const { url } = settings;
|
||||
if (isStringEmpty(url)) {
|
||||
validation.url = [
|
||||
i18n.translate('xpack.snapshotRestore.repositoryValidation.urlRequired', {
|
||||
defaultMessage: 'URL is required.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
return validation;
|
||||
};
|
||||
|
||||
const validateS3RepositorySettings = (
|
||||
settings: S3Repository['settings']
|
||||
): RepositorySettingsValidation => {
|
||||
const i18n = textService.i18n;
|
||||
const validation: RepositorySettingsValidation = {};
|
||||
const { bucket } = settings;
|
||||
if (isStringEmpty(bucket)) {
|
||||
validation.bucket = [
|
||||
i18n.translate('xpack.snapshotRestore.repositoryValidation.bucketRequired', {
|
||||
defaultMessage: 'Bucket is required.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
return validation;
|
||||
};
|
||||
|
||||
const validateGCSRepositorySettings = (
|
||||
settings: GCSRepository['settings']
|
||||
): RepositorySettingsValidation => {
|
||||
const i18n = textService.i18n;
|
||||
const validation: RepositorySettingsValidation = {};
|
||||
const { bucket } = settings;
|
||||
if (isStringEmpty(bucket)) {
|
||||
validation.bucket = [
|
||||
i18n.translate('xpack.snapshotRestore.repositoryValidation.bucketRequired', {
|
||||
defaultMessage: 'Bucket is required.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
return validation;
|
||||
};
|
||||
|
||||
const validateHDFSRepositorySettings = (
|
||||
settings: HDFSRepository['settings']
|
||||
): RepositorySettingsValidation => {
|
||||
const i18n = textService.i18n;
|
||||
const validation: RepositorySettingsValidation = {};
|
||||
const { uri, path } = settings;
|
||||
if (isStringEmpty(uri)) {
|
||||
validation.uri = [
|
||||
i18n.translate('xpack.snapshotRestore.repositoryValidation.uriRequired', {
|
||||
defaultMessage: 'URI is required.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
if (isStringEmpty(path)) {
|
||||
validation.path = [
|
||||
i18n.translate('xpack.snapshotRestore.repositoryValidation.pathRequired', {
|
||||
defaultMessage: 'Path is required.',
|
||||
}),
|
||||
];
|
||||
}
|
||||
return validation;
|
||||
};
|
16
x-pack/plugins/snapshot_restore/public/app/types/app.ts
Normal file
16
x-pack/plugins/snapshot_restore/public/app/types/app.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { AppCore, AppPlugins } from '../../shim';
|
||||
export { AppCore, AppPlugins } from '../../shim';
|
||||
|
||||
export interface AppDependencies {
|
||||
core: AppCore;
|
||||
plugins: AppPlugins;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
[key: string]: any;
|
||||
}
|
|
@ -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 * from './app';
|
3
x-pack/plugins/snapshot_restore/public/index.html
Normal file
3
x-pack/plugins/snapshot_restore/public/index.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<kbn-management-app section="elasticsearch/snapshot_restore">
|
||||
<div id="snapshotRestoreReactRoot"></div>
|
||||
</kbn-management-app>
|
11
x-pack/plugins/snapshot_restore/public/index.ts
Normal file
11
x-pack/plugins/snapshot_restore/public/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { Plugin as SnapshotRestorePlugin } from './plugin';
|
||||
import { createShim } from './shim';
|
||||
|
||||
const { core, plugins } = createShim();
|
||||
const snapshotRestorePlugin = new SnapshotRestorePlugin();
|
||||
snapshotRestorePlugin.start(core, plugins);
|
94
x-pack/plugins/snapshot_restore/public/plugin.ts
Normal file
94
x-pack/plugins/snapshot_restore/public/plugin.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { unmountComponentAtNode } from 'react-dom';
|
||||
|
||||
import { PLUGIN } from '../common/constants';
|
||||
import { CLIENT_BASE_PATH, renderReact } from './app';
|
||||
import { AppCore, AppPlugins } from './app/types';
|
||||
import template from './index.html';
|
||||
import { Core, Plugins } from './shim';
|
||||
|
||||
import { breadcrumbService } from './app/services/navigation';
|
||||
import { documentationLinksService } from './app/services/documentation';
|
||||
import { httpService } from './app/services/http';
|
||||
import { textService } from './app/services/text';
|
||||
|
||||
const REACT_ROOT_ID = 'snapshotRestoreReactRoot';
|
||||
|
||||
export class Plugin {
|
||||
public start(core: Core, plugins: Plugins): void {
|
||||
const { i18n, routing, http, chrome, notification, documentation } = core;
|
||||
const { management } = plugins;
|
||||
|
||||
// Register management section
|
||||
const esSection = management.sections.getSection('elasticsearch');
|
||||
esSection.register(PLUGIN.ID, {
|
||||
visible: true,
|
||||
display: i18n.translate('xpack.snapshotRestore.appName', {
|
||||
defaultMessage: 'Snapshot Repositories',
|
||||
}),
|
||||
order: 7,
|
||||
url: `#${CLIENT_BASE_PATH}`,
|
||||
});
|
||||
|
||||
// Initialize services
|
||||
textService.init(i18n);
|
||||
breadcrumbService.init(chrome, management.constants.BREADCRUMB);
|
||||
documentationLinksService.init(documentation.esDocBasePath, documentation.esPluginDocBasePath);
|
||||
|
||||
const unmountReactApp = (): void => {
|
||||
const elem = document.getElementById(REACT_ROOT_ID);
|
||||
if (elem) {
|
||||
unmountComponentAtNode(elem);
|
||||
}
|
||||
};
|
||||
|
||||
// Register react root
|
||||
routing.registerAngularRoute(`${CLIENT_BASE_PATH}/:section?/:subsection?/:view?/:id?`, {
|
||||
template,
|
||||
controllerAs: 'snapshotRestoreController',
|
||||
controller: ($scope: any, $route: any, $http: ng.IHttpService, $q: any) => {
|
||||
// NOTE: We depend upon Angular's $http service because it's decorated with interceptors,
|
||||
// e.g. to check license status per request.
|
||||
http.setClient($http);
|
||||
httpService.init(http.getClient(), chrome);
|
||||
|
||||
// Angular Lifecycle
|
||||
const appRoute = $route.current;
|
||||
const stopListeningForLocationChange = $scope.$on('$locationChangeSuccess', () => {
|
||||
const currentRoute = $route.current;
|
||||
const isNavigationInApp = currentRoute.$$route.template === appRoute.$$route.template;
|
||||
|
||||
// When we navigate within SR, prevent Angular from re-matching the route and rebuild the app
|
||||
if (isNavigationInApp) {
|
||||
$route.current = appRoute;
|
||||
} else {
|
||||
// Any clean up when user leaves SR
|
||||
}
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
if (stopListeningForLocationChange) {
|
||||
stopListeningForLocationChange();
|
||||
}
|
||||
unmountReactApp();
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$$postDigest(() => {
|
||||
unmountReactApp();
|
||||
const elem = document.getElementById(REACT_ROOT_ID);
|
||||
if (elem) {
|
||||
renderReact(
|
||||
elem,
|
||||
{ i18n, notification } as AppCore,
|
||||
{ management: { sections: management.sections } } as AppPlugins
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
112
x-pack/plugins/snapshot_restore/public/shim.ts
Normal file
112
x-pack/plugins/snapshot_restore/public/shim.ts
Normal file
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } from 'ui/documentation_links';
|
||||
import { management, MANAGEMENT_BREADCRUMB } from 'ui/management';
|
||||
import { fatalError, toastNotifications } from 'ui/notify';
|
||||
import routes from 'ui/routes';
|
||||
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
export interface AppCore {
|
||||
i18n: {
|
||||
[i18nPackage: string]: any;
|
||||
Context: typeof I18nContext;
|
||||
FormattedMessage: typeof FormattedMessage;
|
||||
};
|
||||
notification: {
|
||||
fatalError: typeof fatalError;
|
||||
toastNotifications: typeof toastNotifications;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AppPlugins {
|
||||
management: {
|
||||
sections: typeof management;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Core extends AppCore {
|
||||
chrome: typeof chrome;
|
||||
http: {
|
||||
getClient(): any;
|
||||
setClient(client: any): void;
|
||||
};
|
||||
routing: {
|
||||
registerAngularRoute(path: string, config: object): void;
|
||||
registerRouter(router: HashRouter): void;
|
||||
getRouter(): HashRouter | undefined;
|
||||
};
|
||||
documentation: {
|
||||
esDocBasePath: string;
|
||||
esPluginDocBasePath: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Plugins extends AppPlugins {
|
||||
management: {
|
||||
sections: typeof management;
|
||||
constants: {
|
||||
BREADCRUMB: typeof MANAGEMENT_BREADCRUMB;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function createShim(): { core: Core; plugins: Plugins } {
|
||||
// This is an Angular service, which is why we use this provider pattern
|
||||
// to access it within our React app.
|
||||
let httpClient: ng.IHttpService;
|
||||
|
||||
let reactRouter: HashRouter | undefined;
|
||||
|
||||
return {
|
||||
core: {
|
||||
i18n: {
|
||||
...i18n,
|
||||
Context: I18nContext,
|
||||
FormattedMessage,
|
||||
},
|
||||
routing: {
|
||||
registerAngularRoute: (path: string, config: object): void => {
|
||||
routes.when(path, config);
|
||||
},
|
||||
registerRouter: (router: HashRouter): void => {
|
||||
reactRouter = router;
|
||||
},
|
||||
getRouter: (): HashRouter | undefined => {
|
||||
return reactRouter;
|
||||
},
|
||||
},
|
||||
http: {
|
||||
setClient: (client: any): void => {
|
||||
httpClient = client;
|
||||
},
|
||||
getClient: (): any => httpClient,
|
||||
},
|
||||
chrome,
|
||||
notification: {
|
||||
fatalError,
|
||||
toastNotifications,
|
||||
},
|
||||
documentation: {
|
||||
esDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`,
|
||||
esPluginDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
management: {
|
||||
sections: management,
|
||||
constants: {
|
||||
BREADCRUMB: MANAGEMENT_BREADCRUMB,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
17
x-pack/plugins/snapshot_restore/server/lib/clean_settings.ts
Normal file
17
x-pack/plugins/snapshot_restore/server/lib/clean_settings.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { Repository } from '../../common/types';
|
||||
|
||||
/**
|
||||
* Utility to remove empty fields ("") from repository settings
|
||||
*/
|
||||
export const cleanSettings = (settings: Repository['settings']) =>
|
||||
Object.entries(settings).reduce((acc: Repository['settings'], [key, value]) => {
|
||||
if (value !== '') {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
12
x-pack/plugins/snapshot_restore/server/lib/index.ts
Normal file
12
x-pack/plugins/snapshot_restore/server/lib/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export {
|
||||
deserializeRepositorySettings,
|
||||
serializeRepositorySettings,
|
||||
} from './repository_serialization';
|
||||
export { deserializeSnapshotDetails } from './snapshot_serialization';
|
||||
export { cleanSettings } from './clean_settings';
|
|
@ -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 {
|
||||
deserializeRepositorySettings,
|
||||
serializeRepositorySettings,
|
||||
} from './repository_serialization';
|
||||
|
||||
describe('repository_serialization', () => {
|
||||
describe('deserializeRepositorySettings()', () => {
|
||||
it('should deserialize repository settings', () => {
|
||||
expect(
|
||||
deserializeRepositorySettings({
|
||||
uri: 'test://uri',
|
||||
path: '/foo/bar',
|
||||
load_defaults: true,
|
||||
compress: false,
|
||||
chunk_size: null,
|
||||
security: {
|
||||
principal: 'fooBar',
|
||||
},
|
||||
conf: {
|
||||
some_setting: 'test',
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
uri: 'test://uri',
|
||||
path: '/foo/bar',
|
||||
loadDefaults: true,
|
||||
compress: false,
|
||||
chunkSize: null,
|
||||
'security.principal': 'fooBar',
|
||||
'conf.some_setting': 'test',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeRepositorySettings()', () => {
|
||||
it('should serialize repository settings', () => {
|
||||
expect(
|
||||
serializeRepositorySettings({
|
||||
uri: 'test://uri',
|
||||
path: '/foo/bar',
|
||||
loadDefaults: true,
|
||||
compress: false,
|
||||
chunkSize: null,
|
||||
'security.principal': 'fooBar',
|
||||
'conf.some_setting': 'test',
|
||||
})
|
||||
).toEqual({
|
||||
uri: 'test://uri',
|
||||
path: '/foo/bar',
|
||||
load_defaults: true,
|
||||
compress: false,
|
||||
chunk_size: null,
|
||||
'security.principal': 'fooBar',
|
||||
'conf.some_setting': 'test',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { camelCase, snakeCase } from 'lodash';
|
||||
import { flatten } from '../../common/lib';
|
||||
import { cleanSettings } from './clean_settings';
|
||||
|
||||
interface RepositorySettings {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const booleanizeValue = (value: any) => {
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
} else if (value === 'false') {
|
||||
return false;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const deserializeRepositorySettings = (settings: RepositorySettings): RepositorySettings => {
|
||||
// HDFS repositories return settings like:
|
||||
// `{ security: { principal: 'some_value'}, conf: { foo: { bar: 'another_value' }}}`
|
||||
// Flattening such settings makes it easier to consume in the UI, for both viewing and updating
|
||||
const flattenedSettings: RepositorySettings = flatten(settings);
|
||||
const deserializedSettings: RepositorySettings = {};
|
||||
|
||||
Object.entries(flattenedSettings).forEach(([key, value]) => {
|
||||
// Avoid camel casing keys that are the result of being flattened, such as `security.principal` and `conf.*`
|
||||
if (key.includes('.')) {
|
||||
deserializedSettings[key] = booleanizeValue(value);
|
||||
} else {
|
||||
deserializedSettings[camelCase(key)] = booleanizeValue(value);
|
||||
}
|
||||
});
|
||||
|
||||
return deserializedSettings;
|
||||
};
|
||||
|
||||
export const serializeRepositorySettings = (settings: RepositorySettings): RepositorySettings => {
|
||||
const serializedSettings: RepositorySettings = {};
|
||||
|
||||
Object.entries(settings).forEach(([key, value]) => {
|
||||
// Avoid snake casing keys that are the result of being flattened, such as `security.principal` and `conf.*`
|
||||
if (key.includes('.')) {
|
||||
serializedSettings[key] = value;
|
||||
} else {
|
||||
serializedSettings[snakeCase(key)] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return cleanSettings(serializedSettings);
|
||||
};
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 { deserializeSnapshotDetails } from './snapshot_serialization';
|
||||
|
||||
describe('deserializeSnapshotDetails', () => {
|
||||
test('deserializes a snapshot', () => {
|
||||
expect(
|
||||
deserializeSnapshotDetails('repositoryName', {
|
||||
snapshot: 'snapshot name',
|
||||
uuid: 'UUID',
|
||||
version_id: 5,
|
||||
version: 'version',
|
||||
indices: ['index2', 'index3', 'index1'],
|
||||
include_global_state: false,
|
||||
state: 'SUCCESS',
|
||||
start_time: '0',
|
||||
start_time_in_millis: 0,
|
||||
end_time: '1',
|
||||
end_time_in_millis: 1,
|
||||
duration_in_millis: 1,
|
||||
shards: {
|
||||
total: 3,
|
||||
failed: 1,
|
||||
successful: 2,
|
||||
},
|
||||
failures: [
|
||||
{
|
||||
index: 'z',
|
||||
shard: 1,
|
||||
},
|
||||
{
|
||||
index: 'a',
|
||||
shard: 3,
|
||||
},
|
||||
{
|
||||
index: 'a',
|
||||
shard: 1,
|
||||
},
|
||||
{
|
||||
index: 'a',
|
||||
shard: 2,
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual({
|
||||
repository: 'repositoryName',
|
||||
snapshot: 'snapshot name',
|
||||
uuid: 'UUID',
|
||||
versionId: 5,
|
||||
version: 'version',
|
||||
// Indices are sorted.
|
||||
indices: ['index1', 'index2', 'index3'],
|
||||
// Converted from a boolean into 0 or 1.
|
||||
includeGlobalState: 0,
|
||||
// Failures are grouped and sorted by index, and the failures themselves are sorted by shard.
|
||||
indexFailures: [
|
||||
{
|
||||
index: 'a',
|
||||
failures: [
|
||||
{
|
||||
shard: 1,
|
||||
},
|
||||
{
|
||||
shard: 2,
|
||||
},
|
||||
{
|
||||
shard: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
index: 'z',
|
||||
failures: [
|
||||
{
|
||||
shard: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
state: 'SUCCESS',
|
||||
startTime: '0',
|
||||
startTimeInMillis: 0,
|
||||
endTime: '1',
|
||||
endTimeInMillis: 1,
|
||||
durationInMillis: 1,
|
||||
shards: {
|
||||
total: 3,
|
||||
failed: 1,
|
||||
successful: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { sortBy } from 'lodash';
|
||||
|
||||
import { SnapshotDetails } from '../../common/types';
|
||||
import { SnapshotDetailsEs } from '../types';
|
||||
|
||||
export function deserializeSnapshotDetails(
|
||||
repository: string,
|
||||
snapshotDetailsEs: SnapshotDetailsEs
|
||||
): SnapshotDetails {
|
||||
if (!snapshotDetailsEs || typeof snapshotDetailsEs !== 'object') {
|
||||
throw new Error('Unable to deserialize snapshot details');
|
||||
}
|
||||
|
||||
const {
|
||||
snapshot,
|
||||
uuid,
|
||||
version_id: versionId,
|
||||
version,
|
||||
indices = [],
|
||||
include_global_state: includeGlobalState,
|
||||
state,
|
||||
start_time: startTime,
|
||||
start_time_in_millis: startTimeInMillis,
|
||||
end_time: endTime,
|
||||
end_time_in_millis: endTimeInMillis,
|
||||
duration_in_millis: durationInMillis,
|
||||
failures = [],
|
||||
shards,
|
||||
} = snapshotDetailsEs;
|
||||
|
||||
// If an index has multiple failures, we'll want to see them grouped together.
|
||||
const indexToFailuresMap = failures.reduce((map, failure) => {
|
||||
const { index, ...rest } = failure;
|
||||
if (!map[index]) {
|
||||
map[index] = {
|
||||
index,
|
||||
failures: [],
|
||||
};
|
||||
}
|
||||
|
||||
map[index].failures.push(rest);
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
// Sort all failures by their shard.
|
||||
Object.keys(indexToFailuresMap).forEach(index => {
|
||||
indexToFailuresMap[index].failures = sortBy(
|
||||
indexToFailuresMap[index].failures,
|
||||
({ shard }) => shard
|
||||
);
|
||||
});
|
||||
|
||||
// Sort by index name.
|
||||
const indexFailures = sortBy(Object.values(indexToFailuresMap), ({ index }) => index);
|
||||
|
||||
return {
|
||||
repository,
|
||||
snapshot,
|
||||
uuid,
|
||||
versionId,
|
||||
version,
|
||||
indices: [...indices].sort(),
|
||||
includeGlobalState: Boolean(includeGlobalState) ? 1 : 0,
|
||||
state,
|
||||
startTime,
|
||||
startTimeInMillis,
|
||||
endTime,
|
||||
endTimeInMillis,
|
||||
durationInMillis,
|
||||
indexFailures,
|
||||
shards,
|
||||
};
|
||||
}
|
61
x-pack/plugins/snapshot_restore/server/routes/api/app.ts
Normal file
61
x-pack/plugins/snapshot_restore/server/routes/api/app.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { wrapCustomError } from '../../../../../server/lib/create_router/error_wrappers';
|
||||
import { APP_PERMISSIONS } from '../../../common/constants';
|
||||
import { Plugins } from '../../../shim';
|
||||
|
||||
let xpackMainPlugin: any;
|
||||
|
||||
export function registerAppRoutes(router: Router, plugins: Plugins) {
|
||||
xpackMainPlugin = plugins.xpack_main;
|
||||
router.get('permissions', getPermissionsHandler);
|
||||
}
|
||||
|
||||
export function getXpackMainPlugin() {
|
||||
return xpackMainPlugin;
|
||||
}
|
||||
|
||||
export const getPermissionsHandler: RouterRouteHandler = async (req, callWithRequest) => {
|
||||
const xpackInfo = getXpackMainPlugin() && getXpackMainPlugin().info;
|
||||
if (!xpackInfo) {
|
||||
// xpackInfo is updated via poll, so it may not be available until polling has begun.
|
||||
// In this rare situation, tell the client the service is temporarily unavailable.
|
||||
throw wrapCustomError(new Error('Security info unavailable'), 503);
|
||||
}
|
||||
|
||||
const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security');
|
||||
if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) {
|
||||
// If security isn't enabled, let the user use app.
|
||||
return {
|
||||
hasPermission: true,
|
||||
missingClusterPrivileges: [],
|
||||
};
|
||||
}
|
||||
|
||||
const { has_all_requested: hasPermission, cluster } = await callWithRequest('transport.request', {
|
||||
path: '/_security/user/_has_privileges',
|
||||
method: 'POST',
|
||||
body: {
|
||||
cluster: APP_PERMISSIONS,
|
||||
},
|
||||
});
|
||||
|
||||
const missingClusterPrivileges = Object.keys(cluster).reduce(
|
||||
(permissions: string[], permissionName: string): string[] => {
|
||||
if (!cluster[permissionName]) {
|
||||
permissions.push(permissionName);
|
||||
}
|
||||
return permissions;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
hasPermission,
|
||||
missingClusterPrivileges,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 } from '../../../../../server/lib/create_router';
|
||||
import { Plugins } from '../../../shim';
|
||||
import { registerAppRoutes } from './app';
|
||||
import { registerRepositoriesRoutes } from './repositories';
|
||||
import { registerSnapshotsRoutes } from './snapshots';
|
||||
|
||||
export const registerRoutes = (router: Router, plugins: Plugins): void => {
|
||||
registerAppRoutes(router, plugins);
|
||||
registerRepositoriesRoutes(router, plugins);
|
||||
registerSnapshotsRoutes(router);
|
||||
};
|
|
@ -0,0 +1,360 @@
|
|||
/*
|
||||
* 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 { DEFAULT_REPOSITORY_TYPES, REPOSITORY_PLUGINS_MAP } from '../../../common/constants';
|
||||
import {
|
||||
createHandler,
|
||||
deleteHandler,
|
||||
getAllHandler,
|
||||
getOneHandler,
|
||||
getTypesHandler,
|
||||
getVerificationHandler,
|
||||
updateHandler,
|
||||
} from './repositories';
|
||||
|
||||
describe('[Snapshot and Restore API Routes] Repositories', () => {
|
||||
const mockRequest = {} as Request;
|
||||
const mockResponseToolkit = {} as ResponseToolkit;
|
||||
|
||||
describe('getAllHandler()', () => {
|
||||
it('should arrify repositories returned from ES', async () => {
|
||||
const mockEsResponse = {
|
||||
fooRepository: {},
|
||||
barRepository: {},
|
||||
};
|
||||
const callWithRequest = jest.fn().mockReturnValueOnce(mockEsResponse);
|
||||
const expectedResponse = {
|
||||
repositories: [
|
||||
{
|
||||
name: 'fooRepository',
|
||||
type: '',
|
||||
settings: {},
|
||||
},
|
||||
{
|
||||
name: 'barRepository',
|
||||
type: '',
|
||||
settings: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
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 = {
|
||||
repositories: [],
|
||||
};
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOneHandler()', () => {
|
||||
const name = 'fooRepository';
|
||||
const mockOneRequest = ({
|
||||
params: {
|
||||
name,
|
||||
},
|
||||
} as unknown) as Request;
|
||||
|
||||
it('should return repository object if returned from ES', async () => {
|
||||
const mockEsResponse = {
|
||||
[name]: { type: '', settings: {} },
|
||||
};
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(mockEsResponse)
|
||||
.mockResolvedValueOnce({});
|
||||
const expectedResponse = {
|
||||
repository: { name, ...mockEsResponse[name] },
|
||||
snapshots: { count: null },
|
||||
};
|
||||
await expect(
|
||||
getOneHandler(mockOneRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return empty repository object if not returned from ES', async () => {
|
||||
const mockEsResponse = {};
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(mockEsResponse)
|
||||
.mockResolvedValueOnce({});
|
||||
const expectedResponse = {
|
||||
repository: {},
|
||||
snapshots: {},
|
||||
};
|
||||
await expect(
|
||||
getOneHandler(mockOneRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return snapshot count from ES', async () => {
|
||||
const mockEsResponse = {
|
||||
[name]: { type: '', settings: {} },
|
||||
};
|
||||
const mockEsSnapshotResponse = {
|
||||
snapshots: [{}, {}],
|
||||
};
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(mockEsResponse)
|
||||
.mockResolvedValueOnce(mockEsSnapshotResponse);
|
||||
const expectedResponse = {
|
||||
repository: { name, ...mockEsResponse[name] },
|
||||
snapshots: {
|
||||
count: 2,
|
||||
},
|
||||
};
|
||||
await expect(
|
||||
getOneHandler(mockOneRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return null snapshot count if ES error', async () => {
|
||||
const mockEsResponse = {
|
||||
[name]: { type: '', settings: {} },
|
||||
};
|
||||
const mockEsSnapshotError = new Error('snapshot error');
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(mockEsResponse)
|
||||
.mockRejectedValueOnce(mockEsSnapshotError);
|
||||
const expectedResponse = {
|
||||
repository: { name, ...mockEsResponse[name] },
|
||||
snapshots: {
|
||||
count: null,
|
||||
},
|
||||
};
|
||||
await expect(
|
||||
getOneHandler(mockOneRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
const callWithRequest = jest.fn().mockRejectedValueOnce(new Error());
|
||||
await expect(
|
||||
getOneHandler(mockOneRequest, callWithRequest, mockResponseToolkit)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVerificationHandler', () => {
|
||||
const name = 'fooRepository';
|
||||
const mockVerificationRequest = ({
|
||||
params: {
|
||||
name,
|
||||
},
|
||||
} as unknown) as Request;
|
||||
|
||||
it('should return repository verification response if returned from ES', async () => {
|
||||
const mockEsResponse = { nodes: {} };
|
||||
const callWithRequest = jest.fn().mockResolvedValueOnce(mockEsResponse);
|
||||
const expectedResponse = {
|
||||
verification: { valid: true, response: mockEsResponse },
|
||||
};
|
||||
await expect(
|
||||
getVerificationHandler(mockVerificationRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return repository verification error if returned from ES', async () => {
|
||||
const mockEsResponse = { error: {}, status: 500 };
|
||||
const callWithRequest = jest.fn().mockRejectedValueOnce(mockEsResponse);
|
||||
const expectedResponse = {
|
||||
verification: { valid: false, error: mockEsResponse },
|
||||
};
|
||||
await expect(
|
||||
getVerificationHandler(mockVerificationRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTypesHandler()', () => {
|
||||
it('should return default types if no repository plugins returned from ES', async () => {
|
||||
const mockEsResponse = {};
|
||||
const callWithRequest = jest.fn().mockReturnValue(mockEsResponse);
|
||||
const expectedResponse = [...DEFAULT_REPOSITORY_TYPES];
|
||||
await expect(
|
||||
getTypesHandler(mockRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return default types with any repository plugins returned from ES', async () => {
|
||||
const pluginNames = Object.keys(REPOSITORY_PLUGINS_MAP);
|
||||
const pluginTypes = Object.entries(REPOSITORY_PLUGINS_MAP).map(([key, value]) => value);
|
||||
const mockEsResponse = [...pluginNames.map(key => ({ component: key }))];
|
||||
const callWithRequest = jest.fn().mockReturnValue(mockEsResponse);
|
||||
const expectedResponse = [...DEFAULT_REPOSITORY_TYPES, ...pluginTypes];
|
||||
await expect(
|
||||
getTypesHandler(mockRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should not return non-repository plugins returned from ES', async () => {
|
||||
const pluginNames = ['foo-plugin', 'bar-plugin'];
|
||||
const mockEsResponse = [...pluginNames.map(key => ({ component: key }))];
|
||||
const callWithRequest = jest.fn().mockReturnValue(mockEsResponse);
|
||||
const expectedResponse = [...DEFAULT_REPOSITORY_TYPES];
|
||||
await expect(
|
||||
getTypesHandler(mockRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
const callWithRequest = jest.fn().mockRejectedValueOnce(new Error());
|
||||
await expect(
|
||||
getOneHandler(mockRequest, callWithRequest, mockResponseToolkit)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createHandler()', () => {
|
||||
const name = 'fooRepository';
|
||||
const mockCreateRequest = ({
|
||||
payload: {
|
||||
name,
|
||||
},
|
||||
} as unknown) as Request;
|
||||
|
||||
it('should return successful ES response', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce({})
|
||||
.mockReturnValueOnce(mockEsResponse);
|
||||
const expectedResponse = { ...mockEsResponse };
|
||||
await expect(
|
||||
createHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return error if repository with the same name already exists', async () => {
|
||||
const mockEsResponse = { [name]: {} };
|
||||
const callWithRequest = jest.fn().mockReturnValue(mockEsResponse);
|
||||
await expect(
|
||||
createHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce({})
|
||||
.mockRejectedValueOnce(new Error());
|
||||
await expect(
|
||||
createHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateHandler()', () => {
|
||||
const name = 'fooRepository';
|
||||
const mockCreateRequest = ({
|
||||
params: {
|
||||
name,
|
||||
},
|
||||
payload: {
|
||||
name,
|
||||
},
|
||||
} as unknown) as Request;
|
||||
|
||||
it('should return successful ES response', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce({ [name]: {} })
|
||||
.mockReturnValueOnce(mockEsResponse);
|
||||
const expectedResponse = { ...mockEsResponse };
|
||||
await expect(
|
||||
updateHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
const callWithRequest = jest.fn().mockRejectedValueOnce(new Error());
|
||||
await expect(
|
||||
updateHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteHandler()', () => {
|
||||
const names = ['fooRepository', 'barRepository'];
|
||||
const mockCreateRequest = ({
|
||||
params: {
|
||||
names: names.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: names, 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: names.map(name => ({
|
||||
name,
|
||||
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: [names[1]],
|
||||
errors: [
|
||||
{
|
||||
name: names[0],
|
||||
error: mockEsError,
|
||||
},
|
||||
],
|
||||
};
|
||||
await expect(
|
||||
deleteHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* 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 {
|
||||
wrapCustomError,
|
||||
wrapEsError,
|
||||
} from '../../../../../server/lib/create_router/error_wrappers';
|
||||
|
||||
import { DEFAULT_REPOSITORY_TYPES, REPOSITORY_PLUGINS_MAP } from '../../../common/constants';
|
||||
import { Repository, RepositoryType, RepositoryVerification } from '../../../common/types';
|
||||
|
||||
import { Plugins } from '../../../shim';
|
||||
import { deserializeRepositorySettings, serializeRepositorySettings } from '../../lib';
|
||||
|
||||
let isCloudEnabled = false;
|
||||
|
||||
export function registerRepositoriesRoutes(router: Router, plugins: Plugins) {
|
||||
isCloudEnabled = plugins.cloud.config.isCloudEnabled;
|
||||
router.get('repository_types', getTypesHandler);
|
||||
router.get('repositories', getAllHandler);
|
||||
router.get('repositories/{name}', getOneHandler);
|
||||
router.get('repositories/{name}/verify', getVerificationHandler);
|
||||
router.put('repositories', createHandler);
|
||||
router.put('repositories/{name}', updateHandler);
|
||||
router.delete('repositories/{names}', deleteHandler);
|
||||
}
|
||||
|
||||
export const getAllHandler: RouterRouteHandler = async (
|
||||
req,
|
||||
callWithRequest
|
||||
): Promise<{
|
||||
repositories: Repository[];
|
||||
}> => {
|
||||
const repositoriesByName = await callWithRequest('snapshot.getRepository', {
|
||||
repository: '_all',
|
||||
});
|
||||
const repositoryNames = Object.keys(repositoriesByName);
|
||||
const repositories: Repository[] = repositoryNames.map(name => {
|
||||
const { type = '', settings = {} } = repositoriesByName[name];
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
settings: deserializeRepositorySettings(settings),
|
||||
};
|
||||
});
|
||||
return { repositories };
|
||||
};
|
||||
|
||||
export const getOneHandler: RouterRouteHandler = async (
|
||||
req,
|
||||
callWithRequest
|
||||
): Promise<{
|
||||
repository: Repository | {};
|
||||
snapshots: { count: number | undefined } | {};
|
||||
}> => {
|
||||
const { name } = req.params;
|
||||
const repositoryByName = await callWithRequest('snapshot.getRepository', { repository: name });
|
||||
const { snapshots } = await callWithRequest('snapshot.get', {
|
||||
repository: name,
|
||||
snapshot: '_all',
|
||||
}).catch(e => ({
|
||||
snapshots: null,
|
||||
}));
|
||||
|
||||
if (repositoryByName[name]) {
|
||||
const { type = '', settings = {} } = repositoryByName[name];
|
||||
return {
|
||||
repository: {
|
||||
name,
|
||||
type,
|
||||
settings: deserializeRepositorySettings(settings),
|
||||
},
|
||||
snapshots: {
|
||||
count: snapshots ? snapshots.length : null,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
repository: {},
|
||||
snapshots: {},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getVerificationHandler: RouterRouteHandler = async (
|
||||
req,
|
||||
callWithRequest
|
||||
): Promise<{
|
||||
verification: RepositoryVerification | {};
|
||||
}> => {
|
||||
const { name } = req.params;
|
||||
const verificationResults = await callWithRequest('snapshot.verifyRepository', {
|
||||
repository: name,
|
||||
}).catch(e => ({
|
||||
valid: false,
|
||||
error: e.response ? JSON.parse(e.response) : e,
|
||||
}));
|
||||
return {
|
||||
verification: verificationResults.error
|
||||
? verificationResults
|
||||
: {
|
||||
valid: true,
|
||||
response: verificationResults,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getTypesHandler: RouterRouteHandler = async (req, callWithRequest) => {
|
||||
// In ECE/ESS, do not enable the default types
|
||||
const types: RepositoryType[] = isCloudEnabled ? [] : [...DEFAULT_REPOSITORY_TYPES];
|
||||
const plugins: any[] = await callWithRequest('cat.plugins', { format: 'json' });
|
||||
if (plugins && plugins.length) {
|
||||
const pluginNames: string[] = [...new Set(plugins.map(plugin => plugin.component))];
|
||||
pluginNames.forEach(pluginName => {
|
||||
if (REPOSITORY_PLUGINS_MAP[pluginName]) {
|
||||
types.push(REPOSITORY_PLUGINS_MAP[pluginName]);
|
||||
}
|
||||
});
|
||||
}
|
||||
return types;
|
||||
};
|
||||
|
||||
export const createHandler: RouterRouteHandler = async (req, callWithRequest) => {
|
||||
const { name = '', type = '', settings = {} } = req.payload as Repository;
|
||||
const conflictError = wrapCustomError(
|
||||
new Error('There is already a repository with that name.'),
|
||||
409
|
||||
);
|
||||
|
||||
// Check that repository with the same name doesn't already exist
|
||||
try {
|
||||
const repositoryByName = await callWithRequest('snapshot.getRepository', { repository: name });
|
||||
if (repositoryByName[name]) {
|
||||
throw conflictError;
|
||||
}
|
||||
} catch (e) {
|
||||
// Rethrow conflict error but silently swallow all others
|
||||
if (e === conflictError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise create new repository
|
||||
return await callWithRequest('snapshot.createRepository', {
|
||||
repository: name,
|
||||
body: {
|
||||
type,
|
||||
settings: serializeRepositorySettings(settings),
|
||||
},
|
||||
verify: false,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateHandler: RouterRouteHandler = async (req, callWithRequest) => {
|
||||
const { name } = req.params;
|
||||
const { type = '', settings = {} } = req.payload as Repository;
|
||||
|
||||
// Check that repository with the given name exists
|
||||
// If it doesn't exist, 404 will be thrown by ES and will be returned
|
||||
await callWithRequest('snapshot.getRepository', { repository: name });
|
||||
|
||||
// Otherwise update repository
|
||||
return await callWithRequest('snapshot.createRepository', {
|
||||
repository: name,
|
||||
body: {
|
||||
type,
|
||||
settings: serializeRepositorySettings(settings),
|
||||
},
|
||||
verify: false,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteHandler: RouterRouteHandler = async (req, callWithRequest) => {
|
||||
const { names } = req.params;
|
||||
const repositoryNames = names.split(',');
|
||||
const response: { itemsDeleted: string[]; errors: any[] } = {
|
||||
itemsDeleted: [],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
repositoryNames.map(name => {
|
||||
return callWithRequest('snapshot.deleteRepository', { repository: name })
|
||||
.then(() => response.itemsDeleted.push(name))
|
||||
.catch(e =>
|
||||
response.errors.push({
|
||||
name,
|
||||
error: wrapEsError(e),
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
return response;
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue