diff --git a/x-pack/plugins/infra/types/eui.d.ts b/x-pack/plugins/infra/types/eui.d.ts index d8272455d2c21..b4b415ab8a379 100644 --- a/x-pack/plugins/infra/types/eui.d.ts +++ b/x-pack/plugins/infra/types/eui.d.ts @@ -116,6 +116,8 @@ declare module '@elastic/eui' { loading?: any; hasActions?: any; message?: any; + rowProps?: any; + cellProps?: any; }; export const EuiInMemoryTable: React.SFC; } diff --git a/x-pack/plugins/snapshot_restore/common/constants.ts b/x-pack/plugins/snapshot_restore/common/constants.ts index 2b27fea98ef28..a13c57da16932 100644 --- a/x-pack/plugins/snapshot_restore/common/constants.ts +++ b/x-pack/plugins/snapshot_restore/common/constants.ts @@ -17,3 +17,5 @@ export const PLUGIN = { }); }, }; + +export const API_BASE_PATH = '/api/snapshot_restore/'; diff --git a/x-pack/plugins/snapshot_restore/common/types/repository_types.ts b/x-pack/plugins/snapshot_restore/common/types/repository_types.ts new file mode 100644 index 0000000000000..2eefe2fd67c85 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/common/types/repository_types.ts @@ -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. + */ + +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 enum RepositoryTypes { + fs = 'fs', + url = 'url', + source = 'source', + s3 = 's3', + hdfs = 'hdfs', + azure = 'azure', + gcs = 'gcs', +} + +export type RepositoryType = + | FSRepositoryType + | ReadonlyRepositoryType + | SourceRepositoryType + | S3RepositoryType + | HDFSRepositoryType + | AzureRepositoryType + | GCSRepositoryType; + +export enum RepositoryDocPaths { + 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', +} + +export interface FSRepository { + name: string; + type: FSRepositoryType; + settings: { + location: string; + compress?: boolean; + chunk_size?: string | null; + max_restore_bytes_per_sec?: string; + max_snapshot_bytes_per_sec?: 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; + base_path?: string; + compress?: boolean; + chunk_size?: string | null; + server_side_encryption?: boolean; + buffer_size?: string; + canned_acl?: string; + storage_class?: string; + }; +} + +export interface HDFSRepository { + name: string; + type: HDFSRepositoryType; + settings: { + uri: string; + path: string; + load_defaults?: boolean; + compress?: boolean; + chunk_size?: string | null; + ['security.principal']?: string; + [key: string]: any; // For conf.* settings + }; +} + +export interface AzureRepository { + name: string; + type: AzureRepositoryType; + settings: { + client?: string; + container?: string; + base_path?: string; + compress?: boolean; + chunk_size?: string | null; + readonly?: boolean; + location_mode?: string; + }; +} + +export interface GCSRepository { + name: string; + type: GCSRepositoryType; + settings: { + bucket: string; + client?: string; + base_path?: string; + compress?: boolean; + chunk_size?: string | null; + }; +} + +export interface SourceRepository { + name: string; + type: SourceRepositoryType; + settings: SourceRepositorySettings; +} + +export type SourceRepositorySettings = 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 & { + delegate_type: T; + }; + +export type Repository = + | FSRepository + | ReadonlyRepository + | S3Repository + | HDFSRepository + | AzureRepository + | GCSRepository + | SourceRepository; diff --git a/x-pack/plugins/snapshot_restore/plugin.ts b/x-pack/plugins/snapshot_restore/plugin.ts index 7668eb5cee9dd..74adb0c7d0ae1 100644 --- a/x-pack/plugins/snapshot_restore/plugin.ts +++ b/x-pack/plugins/snapshot_restore/plugin.ts @@ -3,12 +3,13 @@ * 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/snapshot_restore/'); + const router = core.http.createRouter(API_BASE_PATH); // Register routes registerRoutes(router); diff --git a/x-pack/plugins/snapshot_restore/public/app/app.tsx b/x-pack/plugins/snapshot_restore/public/app/app.tsx index 28c40d15be1d8..3fcc8205b782f 100644 --- a/x-pack/plugins/snapshot_restore/public/app/app.tsx +++ b/x-pack/plugins/snapshot_restore/public/app/app.tsx @@ -4,25 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component } from 'react'; +import React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import { BASE_PATH } from './constants'; -import { AppContext } from './services/app_context'; - import { SnapshotRestoreHome } from './sections'; -export class App extends Component { - public static contextType = AppContext; - - public render() { - return ( -
- - - - -
- ); - } -} +export const App = () => { + return ( +
+ + + + +
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/components/index.ts b/x-pack/plugins/snapshot_restore/public/app/components/index.ts new file mode 100644 index 0000000000000..4b5223f872110 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/components/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { RepositoryDeleteProvider } from './repository_delete_provider'; +export { RepositoryTypeName } from './repository_type_name'; +export { SectionError } from './section_error'; +export { SectionLoading } from './section_loading'; diff --git a/x-pack/plugins/snapshot_restore/public/app/components/repository_delete_provider.tsx b/x-pack/plugins/snapshot_restore/public/app/components/repository_delete_provider.tsx new file mode 100644 index 0000000000000..a3f549446be3b --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/components/repository_delete_provider.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, useState } from 'react'; +import { Repository } from '../../../common/types/repository_types'; + +interface Props { + children: (deleteRepository: DeleteRepository) => React.ReactNode; +} + +type DeleteRepository = (names: Array) => void; + +export const RepositoryDeleteProvider = ({ children }: Props) => { + const [repositoryNames, setRepositoryNames] = useState>([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const deleteRepository: DeleteRepository = names => { + setIsModalOpen(true); + setRepositoryNames(names); + }; + + if (isModalOpen && repositoryNames.length) { + /* placeholder */ + } + + return {children(deleteRepository)}; +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/components/repository_type_name.tsx b/x-pack/plugins/snapshot_restore/public/app/components/repository_type_name.tsx new file mode 100644 index 0000000000000..0ddc7718b4320 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/components/repository_type_name.tsx @@ -0,0 +1,89 @@ +/* + * 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 { RepositoryType, RepositoryTypes } from '../../../common/types/repository_types'; +import { AppStateInterface, useAppState } from '../services/app_context'; + +interface Props { + type: RepositoryType; + delegateType?: RepositoryType; +} + +export const RepositoryTypeName = ({ type, delegateType }: Props) => { + const [ + { + core: { + i18n: { FormattedMessage }, + }, + }, + ] = useAppState() as [AppStateInterface]; + + const typeNameMap: { [key in RepositoryType]: JSX.Element } = { + [RepositoryTypes.fs]: ( + + ), + [RepositoryTypes.url]: ( + + ), + [RepositoryTypes.s3]: ( + + ), + [RepositoryTypes.hdfs]: ( + + ), + [RepositoryTypes.azure]: ( + + ), + [RepositoryTypes.gcs]: ( + + ), + [RepositoryTypes.source]: ( + + ), + }; + + const getTypeName = (repositoryType: RepositoryType): JSX.Element => { + return typeNameMap[repositoryType] || {type}; + }; + + if (type === RepositoryTypes.source && delegateType) { + return ( + + + + ); + } + + return getTypeName(type); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/components/section_error.tsx b/x-pack/plugins/snapshot_restore/public/app/components/section_error.tsx new file mode 100644 index 0000000000000..89d4215dafbcd --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/components/section_error.tsx @@ -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 function SectionError({ title, error }: Props) { + const { + error: errorString, + cause, // wrapEsError() on the server adds a "cause" array + message, + } = error.data; + + return ( + +
{message || errorString}
+ {cause && ( + + +
    + {cause.map((causeMsg, i) => ( +
  • {causeMsg}
  • + ))} +
+
+ )} +
+ ); +} diff --git a/x-pack/plugins/snapshot_restore/public/app/components/section_loading.tsx b/x-pack/plugins/snapshot_restore/public/app/components/section_loading.tsx new file mode 100644 index 0000000000000..d07a8c6836fad --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/components/section_loading.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText, EuiTextColor } from '@elastic/eui'; + +interface Props { + children: React.ReactNode; +} + +export function SectionLoading({ children }: Props) { + return ( + + + + + + + + {children} + + + + ); +} diff --git a/x-pack/plugins/snapshot_restore/public/app/constants/index.ts b/x-pack/plugins/snapshot_restore/public/app/constants/index.ts index 23c935b60a4b7..c993c76b4b639 100644 --- a/x-pack/plugins/snapshot_restore/public/app/constants/index.ts +++ b/x-pack/plugins/snapshot_restore/public/app/constants/index.ts @@ -5,3 +5,4 @@ */ export const BASE_PATH = '/management/elasticsearch/snapshot_restore'; +export type Section = 'repositories' | 'snapshots'; diff --git a/x-pack/plugins/snapshot_restore/public/app/index.tsx b/x-pack/plugins/snapshot_restore/public/app/index.tsx index a66d925cb9766..6d46af98e6f07 100644 --- a/x-pack/plugins/snapshot_restore/public/app/index.tsx +++ b/x-pack/plugins/snapshot_restore/public/app/index.tsx @@ -3,38 +3,51 @@ * 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 React, { useReducer } from 'react'; import { render } from 'react-dom'; import { HashRouter } from 'react-router-dom'; import { AppCore, AppPlugins } from '../shim'; import { App } from './app'; -import { AppContext, AppContextInterface } from './services/app_context'; +import { AppStateInterface, AppStateProvider } from './services/app_context'; export { BASE_PATH as CLIENT_BASE_PATH } from './constants'; -export const renderReact = async ( - elem: Element, - core: AppCore, - plugins: AppPlugins -): Promise => { +// Placeholder reducer in case we need it for any app state data +const appStateReducer = (state: any, action: any) => { + switch (action.type) { + default: + return state; + } +}; + +const ReactApp = ({ appState }: { appState: AppStateInterface }) => { const { i18n: { Context: I18nContext }, - } = core; - - const appContext: AppContextInterface = { - core, - plugins, - }; - - render( + } = appState.core; + return ( - + - + - , + + ); +}; + +export const renderReact = async ( + elem: Element, + core: AppCore, + plugins: AppPlugins +): Promise => { + render( + , elem ); }; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/home.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/home.tsx index 452b661a1caa1..072e9e6b80eb5 100644 --- a/x-pack/plugins/snapshot_restore/public/app/sections/home/home.tsx +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/home.tsx @@ -4,54 +4,68 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent } from 'react'; +import React, { useEffect, useState } from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui'; -import { BASE_PATH } from '../../constants'; -import { AppContext, AppContextInterface } from '../../services/app_context'; +import { BASE_PATH, Section } from '../../constants'; +import { AppStateInterface, useAppState } from '../../services/app_context'; import { RepositoryList } from './repository_list'; import { SnapshotList } from './snapshot_list'; -type Section = 'repositories' | 'snapshots'; - interface MatchParams { section: Section; } interface Props extends RouteComponentProps {} -interface State { - activeSection: Section; -} - -export class SnapshotRestoreHome extends PureComponent { - public static contextType = AppContext; +export const SnapshotRestoreHome = ({ + match: { + params: { section }, + }, + history, +}: Props) => { + const [activeSection, setActiveSection] = useState
(section); - public static getDerivedStateFromProps(nextProps: Props) { - const { - match: { - params: { section }, - }, - } = nextProps; - return { - activeSection: section, - }; - } - public context!: React.ContextType; - - public readonly state: Readonly = { - activeSection: 'repositories' as Section, - }; - - public componentDidMount() { - const { + const [ + { core: { i18n, chrome }, plugins: { management }, - } = this.context as AppContextInterface; + }, + ] = useAppState() as [AppStateInterface]; + const { FormattedMessage } = i18n; + + const tabs = [ + { + id: 'snapshots' as Section, + name: ( + + ), + testSubj: 'srSnapshotsTab', + }, + { + id: 'repositories' as Section, + name: ( + + ), + testSubj: 'srRepositoriesTab', + }, + ]; + + const onSectionChange = (newSection: Section) => { + setActiveSection(newSection); + history.push(`${BASE_PATH}/${newSection}`); + }; + useEffect(() => { chrome.breadcrumbs.set([ management.constants.BREADCRUMB, { @@ -61,78 +75,42 @@ export class SnapshotRestoreHome extends PureComponent { href: `#${BASE_PATH}`, }, ]); - } - - public onSectionChange = (section: Section): void => { - const { history } = this.props; - history.push(`${BASE_PATH}/${section}`); - }; - - public render() { - const { - core: { - i18n: { FormattedMessage }, - }, - } = this.context as AppContextInterface; - - const tabs = [ - { - id: 'snapshots' as Section, - name: ( - - ), - testSubj: 'srSnapshotsTab', - }, - { - id: 'repositories' as Section, - name: ( - - ), - testSubj: 'srRepositoriesTab', - }, - ]; - - return ( - - - -

- -

-
- - - - - {tabs.map(tab => ( - this.onSectionChange(tab.id)} - isSelected={tab.id === this.state.activeSection} - key={tab.id} - data-test-subject={tab.testSubj} - > - {tab.name} - - ))} - - - - - - - - -
-
- ); - } -} + }, []); + + return ( + + + +

+ +

+
+ + + + + {tabs.map(tab => ( + onSectionChange(tab.id)} + isSelected={tab.id === activeSection} + key={tab.id} + data-test-subject={tab.testSubj} + > + {tab.name} + + ))} + + + + + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/index.ts b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/index.ts new file mode 100644 index 0000000000000..21b03ee969ec4 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/index.ts @@ -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'; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx new file mode 100644 index 0000000000000..8ae6d80b764f5 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx @@ -0,0 +1,224 @@ +/* + * 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 { AppStateInterface, useAppState } from '../../../../services/app_context'; +import { getRepositoryTypeDocUrl } from '../../../../services/documentation_links'; +import { useRequest } from '../../../../services/use_request'; + +import { Repository, RepositoryTypes } from '../../../../../../common/types/repository_types'; +import { + RepositoryDeleteProvider, + RepositoryTypeName, + SectionError, + SectionLoading, +} from '../../../../components'; +import { TypeDetails } from './type_details'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +interface Props { + repositoryName: Repository['name']; + onClose: () => void; +} + +export const RepositoryDetails = ({ repositoryName, onClose }: Props) => { + const [ + { + core: { + i18n, + documentation: { esDocBasePath, esPluginDocBasePath }, + }, + }, + ] = useAppState() as [AppStateInterface]; + const { FormattedMessage } = i18n; + const { error, loading, data: repository } = useRequest({ + path: `repositories/${repositoryName}`, + method: 'get', + }); + + const renderBody = () => { + if (loading) { + return renderLoading(); + } + if (error) { + return renderError(); + } + return renderRepository(); + }; + + const renderLoading = () => { + return ( + + + + ); + }; + + const renderError = () => { + const notFound = error.status === 404; + const errorObject = notFound + ? { + data: { + error: i18n.translate( + 'xpack.snapshotRestore.repositoryDetails.errorRepositoryNotFound', + { + defaultMessage: `The repository '{name}' does not exist.`, + values: { + name: repositoryName, + }, + } + ), + }, + } + : error; + return ( + + } + error={errorObject} + /> + ); + }; + + const renderRepository = () => { + const { type } = repository as Repository; + return ( + + + + +

+ +

+
+ + {type === RepositoryTypes.source ? ( + + ) : ( + + )} +
+ + + Type docs + + +
+ + +
+ ); + }; + + const renderFooter = () => { + return ( + + + + + + + {!error && !loading && repository ? ( + + + + + {(deleteRepository: (names: Array) => void) => { + return ( + deleteRepository([repositoryName])} + > + + + ); + }} + + + + + { + /* placeholder */ + }} + fill + color="primary" + > + + + + + + ) : null} + + ); + }; + + return ( + + + +

+ {repositoryName} +

+
+
+ + {renderBody()} + + {renderFooter()} +
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/azure_details.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/azure_details.tsx new file mode 100644 index 0000000000000..bcab9cc876a84 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/azure_details.tsx @@ -0,0 +1,136 @@ +/* + * 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 { AzureRepository } from '../../../../../../../common/types/repository_types'; +import { AppStateInterface, useAppState } from '../../../../../services/app_context'; + +import { EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui'; + +interface Props { + repository: AzureRepository; +} + +export const AzureDetails = ({ repository }: Props) => { + const [ + { + core: { + i18n: { FormattedMessage }, + }, + }, + ] = useAppState() as [AppStateInterface]; + const { + settings: { client, container, base_path, compress, chunk_size, readonly, location_mode }, + } = repository; + + const listItems = []; + + if (client !== undefined) { + listItems.push({ + title: ( + + ), + description: client, + }); + } + + if (container !== undefined) { + listItems.push({ + title: ( + + ), + description: container, + }); + } + + if (base_path !== undefined) { + listItems.push({ + title: ( + + ), + description: base_path, + }); + } + + if (compress !== undefined) { + listItems.push({ + title: ( + + ), + description: String(compress), + }); + } + + if (chunk_size !== undefined) { + listItems.push({ + title: ( + + ), + description: String(chunk_size), + }); + } + + if (readonly !== undefined) { + listItems.push({ + title: ( + + ), + description: String(readonly), + }); + } + + if (location_mode !== undefined) { + listItems.push({ + title: ( + + ), + description: location_mode, + }); + } + + if (!listItems.length) { + return null; + } + + return ( + + +

+ +

+
+ + + + +
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/default_details.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/default_details.tsx new file mode 100644 index 0000000000000..539ce2842f60f --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/default_details.tsx @@ -0,0 +1,74 @@ +/* + * 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. + */ + +// TODO: Remove once typescript definitions are in EUI +declare module '@elastic/eui' { + export const EuiCodeEditor: React.SFC; +} + +import React, { Fragment } from 'react'; + +import { Repository } from '../../../../../../../common/types/repository_types'; +import { AppStateInterface, useAppState } from '../../../../../services/app_context'; + +import 'brace/theme/textmate'; + +import { EuiCodeEditor, EuiSpacer, EuiTitle } from '@elastic/eui'; + +interface Props { + repository: Repository; +} + +export const DefaultDetails = ({ repository: { name, settings } }: Props) => { + const [ + { + core: { + i18n: { FormattedMessage }, + }, + }, + ] = useAppState() as [AppStateInterface]; + + return ( + + +

+ +

+
+ + + + + } + /> +
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/fs_details.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/fs_details.tsx new file mode 100644 index 0000000000000..64586461b270a --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/fs_details.tsx @@ -0,0 +1,125 @@ +/* + * 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 { FSRepository } from '../../../../../../../common/types/repository_types'; +import { AppStateInterface, useAppState } from '../../../../../services/app_context'; + +import { EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui'; + +interface Props { + repository: FSRepository; +} + +export const FSDetails = ({ repository }: Props) => { + const [ + { + core: { + i18n: { FormattedMessage }, + }, + }, + ] = useAppState() as [AppStateInterface]; + const { + settings: { + location, + compress, + chunk_size, + max_restore_bytes_per_sec, + max_snapshot_bytes_per_sec, + readonly, + }, + } = repository; + + const listItems = [ + { + title: ( + + ), + description: location, + }, + ]; + + if (readonly !== undefined) { + listItems.push({ + title: ( + + ), + description: String(readonly), + }); + } + + if (compress !== undefined) { + listItems.push({ + title: ( + + ), + description: String(compress), + }); + } + + if (chunk_size !== undefined) { + listItems.push({ + title: ( + + ), + description: String(chunk_size), + }); + } + + if (max_restore_bytes_per_sec !== undefined) { + listItems.push({ + title: ( + + ), + description: max_restore_bytes_per_sec, + }); + } + + if (max_snapshot_bytes_per_sec !== undefined) { + listItems.push({ + title: ( + + ), + description: max_snapshot_bytes_per_sec, + }); + } + + return ( + + +

+ +

+
+ + + + +
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/gcs_details.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/gcs_details.tsx new file mode 100644 index 0000000000000..a934da36d0850 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/gcs_details.tsx @@ -0,0 +1,106 @@ +/* + * 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 { GCSRepository } from '../../../../../../../common/types/repository_types'; +import { AppStateInterface, useAppState } from '../../../../../services/app_context'; + +import { EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui'; + +interface Props { + repository: GCSRepository; +} + +export const GCSDetails = ({ repository }: Props) => { + const [ + { + core: { + i18n: { FormattedMessage }, + }, + }, + ] = useAppState() as [AppStateInterface]; + const { + settings: { bucket, client, base_path, compress, chunk_size }, + } = repository; + + const listItems = [ + { + title: ( + + ), + description: bucket, + }, + ]; + + if (client !== undefined) { + listItems.push({ + title: ( + + ), + description: client, + }); + } + + if (base_path !== undefined) { + listItems.push({ + title: ( + + ), + description: base_path, + }); + } + + if (compress !== undefined) { + listItems.push({ + title: ( + + ), + description: String(compress), + }); + } + + if (chunk_size !== undefined) { + listItems.push({ + title: ( + + ), + description: String(chunk_size), + }); + } + + return ( + + +

+ +

+
+ + + + +
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/hdfs_details.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/hdfs_details.tsx new file mode 100644 index 0000000000000..da1a6c98a11c5 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/hdfs_details.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; + +import { HDFSRepository } from '../../../../../../../common/types/repository_types'; +import { AppStateInterface, useAppState } from '../../../../../services/app_context'; + +import { EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui'; + +interface Props { + repository: HDFSRepository; +} + +export const HDFSDetails = ({ repository }: Props) => { + const [ + { + core: { + i18n: { FormattedMessage }, + }, + }, + ] = useAppState() as [AppStateInterface]; + const { settings } = repository; + const { + uri, + path, + load_defaults, + compress, + chunk_size, + 'security.principal': securityPrincipal, + ...rest + } = settings; + + const listItems = [ + { + title: ( + + ), + description: uri, + }, + { + title: ( + + ), + description: path, + }, + ]; + + if (load_defaults !== undefined) { + listItems.push({ + title: ( + + ), + description: String(load_defaults), + }); + } + + if (compress !== undefined) { + listItems.push({ + title: ( + + ), + description: String(compress), + }); + } + + if (chunk_size !== undefined) { + listItems.push({ + title: ( + + ), + description: String(chunk_size), + }); + } + + if (securityPrincipal !== undefined) { + listItems.push({ + title: ( + + ), + description: securityPrincipal, + }); + } + + Object.keys(rest).forEach(key => { + listItems.push({ + title: {key}, + description: String(settings[key]), + }); + }); + + return ( + + +

+ +

+
+ + + + +
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/index.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/index.tsx new file mode 100644 index 0000000000000..5eaf6dd32f92c --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/index.tsx @@ -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 React from 'react'; +import { + AzureRepository, + FSRepository, + GCSRepository, + HDFSRepository, + ReadonlyRepository, + Repository, + RepositoryTypes, + S3Repository, +} from '../../../../../../../common/types/repository_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 = ({ repository }: Props) => { + const { type, settings } = repository; + switch (type) { + case RepositoryTypes.fs: + return ; + break; + case RepositoryTypes.url: + return ; + break; + case RepositoryTypes.source: + const { delegate_type } = settings; + const delegatedRepository = { + ...repository, + type: delegate_type, + }; + return ; + break; + case RepositoryTypes.azure: + return ; + break; + case RepositoryTypes.gcs: + return ; + break; + case RepositoryTypes.hdfs: + return ; + break; + case RepositoryTypes.s3: + return ; + break; + default: + return ; + break; + } +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/readonly_details.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/readonly_details.tsx new file mode 100644 index 0000000000000..9301d5994c303 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/readonly_details.tsx @@ -0,0 +1,58 @@ +/* + * 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 { ReadonlyRepository } from '../../../../../../../common/types/repository_types'; +import { AppStateInterface, useAppState } from '../../../../../services/app_context'; + +import { EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui'; + +interface Props { + repository: ReadonlyRepository; +} + +export const ReadonlyDetails = ({ repository }: Props) => { + const [ + { + core: { + i18n: { FormattedMessage }, + }, + }, + ] = useAppState() as [AppStateInterface]; + const { + settings: { url }, + } = repository; + + const listItems = [ + { + title: ( + + ), + description: url, + }, + ]; + + return ( + + +

+ +

+
+ + + + +
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/s3_details.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/s3_details.tsx new file mode 100644 index 0000000000000..7f3f2d1a0ff17 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/type_details/s3_details.tsx @@ -0,0 +1,164 @@ +/* + * 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 { S3Repository } from '../../../../../../../common/types/repository_types'; +import { AppStateInterface, useAppState } from '../../../../../services/app_context'; + +import { EuiDescriptionList, EuiSpacer, EuiTitle } from '@elastic/eui'; + +interface Props { + repository: S3Repository; +} + +export const S3Details = ({ repository }: Props) => { + const [ + { + core: { + i18n: { FormattedMessage }, + }, + }, + ] = useAppState() as [AppStateInterface]; + const { + settings: { + bucket, + client, + base_path, + compress, + chunk_size, + server_side_encryption, + buffer_size, + canned_acl, + storage_class, + }, + } = repository; + + const listItems = [ + { + title: ( + + ), + description: bucket, + }, + ]; + + if (client !== undefined) { + listItems.push({ + title: ( + + ), + description: client, + }); + } + + if (base_path !== undefined) { + listItems.push({ + title: ( + + ), + description: base_path, + }); + } + + if (compress !== undefined) { + listItems.push({ + title: ( + + ), + description: String(compress), + }); + } + + if (chunk_size !== undefined) { + listItems.push({ + title: ( + + ), + description: String(chunk_size), + }); + } + + if (server_side_encryption !== undefined) { + listItems.push({ + title: ( + + ), + description: String(server_side_encryption), + }); + } + + if (buffer_size !== undefined) { + listItems.push({ + title: ( + + ), + description: buffer_size, + }); + } + + if (canned_acl !== undefined) { + listItems.push({ + title: ( + + ), + description: canned_acl, + }); + } + + if (storage_class !== undefined) { + listItems.push({ + title: ( + + ), + description: storage_class, + }); + } + + return ( + + +

+ +

+
+ + + + +
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx index 0d4bdd44d7cb0..5e67dc22cf48c 100644 --- a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx @@ -4,15 +4,137 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent } from 'react'; +import React, { Fragment, useEffect, useState } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; -import { AppContext } from '../../../services/app_context'; +import { Repository } from '../../../../../common/types/repository_types'; +import { BASE_PATH, Section } from '../../../constants'; +import { AppStateInterface, useAppState } from '../../../services/app_context'; +import { useRequest } from '../../../services/use_request'; -export class RepositoryList extends PureComponent { - public static contextType = AppContext; - public context!: React.ContextType; +import { SectionError, SectionLoading } from '../../../components'; +import { RepositoryDetails } from './repository_details'; +import { RepositoryTable } from './repository_table'; - public render() { - return
List of repositories
; - } +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; + +interface MatchParams { + name?: Repository['name']; } +interface Props extends RouteComponentProps {} + +export const RepositoryList = ({ + match: { + params: { name }, + }, + history, +}: Props) => { + const section = 'repositories' as Section; + const [ + { + core: { + i18n: { FormattedMessage }, + }, + }, + ] = useAppState() as [AppStateInterface]; + const { error, loading, data: repositories, request: reload } = useRequest({ + path: 'repositories', + method: 'get', + }); + const [currentRepository, setCurrentRepository] = useState( + undefined + ); + const openRepositoryDetails = (repositoryName: Repository['name']) => { + setCurrentRepository(repositoryName); + history.push(`${BASE_PATH}/${section}/${repositoryName}`); + }; + const closeRepositoryDetails = () => { + setCurrentRepository(undefined); + history.push(`${BASE_PATH}/${section}`); + }; + useEffect( + () => { + setCurrentRepository(name); + }, + [name] + ); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + } + error={error} + /> + ); + } + + if (repositories.length === 0) { + return ( + + + + } + body={ + +

+ +

+
+ } + actions={ + { + /* placeholder */ + }} + fill + iconType="plusInCircle" + data-test-subj="srRepositoriesEmptyPromptCreateButton" + > + + + } + /> + ); + } + + return ( + + {currentRepository ? ( + + ) : null} + + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/index.ts b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/index.ts new file mode 100644 index 0000000000000..d709a338a6e85 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/index.ts @@ -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'; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/repository_table.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/repository_table.tsx new file mode 100644 index 0000000000000..ecf65ca73eaa8 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_table/repository_table.tsx @@ -0,0 +1,205 @@ +/* + * 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 { EuiButton, EuiButtonIcon, EuiInMemoryTable, EuiLink } from '@elastic/eui'; +import React, { useState } from 'react'; +import { + Repository, + RepositoryType, + RepositoryTypes, +} from '../../../../../../common/types/repository_types'; +import { RepositoryDeleteProvider, RepositoryTypeName } from '../../../../components'; +import { AppStateInterface, useAppState } from '../../../../services/app_context'; + +interface Props { + repositories: Repository[]; + reload: () => Promise; + openRepositoryDetails: (name: Repository['name']) => void; +} + +export const RepositoryTable = ({ repositories, reload, openRepositoryDetails }: Props) => { + const [ + { + core: { i18n }, + }, + ] = useAppState() as [AppStateInterface]; + const { FormattedMessage } = i18n; + const [selectedItems, setSelectedItems] = useState([]); + + 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 openRepositoryDetails(name)}>{name}; + }, + }, + { + field: 'type', + name: i18n.translate('xpack.snapshotRestore.repositoryList.table.typeColumnTitle', { + defaultMessage: 'Type', + }), + truncateText: true, + sortable: true, + render: (type: RepositoryType, repository: Repository) => { + if (type === RepositoryTypes.source) { + return ( + + ); + } else { + return ; + } + }, + }, + { + name: i18n.translate('xpack.snapshotRestore.repositoryList.table.actionsColumnTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate('xpack.snapshotRestore.repositoryList.table.actionEditButton', { + defaultMessage: 'Edit', + }), + description: i18n.translate( + 'xpack.snapshotRestore.repositoryList.table.actionEditDescription', + { + defaultMessage: 'Edit repository', + } + ), + icon: 'pencil', + type: 'icon', + onClick: () => { + /* placeholder */ + }, + }, + { + render: ({ name }: Repository) => { + return ( + + {(deleteRepository: (names: Array) => void) => { + return ( + deleteRepository([name])} + /> + ); + }} + + ); + }, + }, + ], + 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 ? ( + + {(deleteRepository: (names: Array) => void) => { + return ( + deleteRepository(selectedItems.map(repository => repository.name))} + color="danger" + data-test-subj="srRepositoryListBulkDeleteActionButton" + > + {selectedItems.length === 1 ? ( + + ) : ( + + )} + + ); + }} + + ) : ( + undefined + ), + toolsRight: ( + + + + ), + 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: , + }; + }), + }, + ], + }; + + return ( + ({ + 'data-test-subj': 'srRepositoryListTableRow', + })} + cellProps={(item: any, column: any) => ({ + 'data-test-subj': `srRepositoryListTableCell-${column.field}`, + })} + /> + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx index 7281b9599d725..b485207e8edc3 100644 --- a/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx @@ -4,15 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent } from 'react'; +import React from 'react'; -import { AppContext } from '../../../services/app_context'; - -export class SnapshotList extends PureComponent { - public static contextType = AppContext; - public context!: React.ContextType; - - public render() { - return
List of snapshots
; - } -} +export const SnapshotList = () => { + return
List of snapshots
; +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/services/app_context.ts b/x-pack/plugins/snapshot_restore/public/app/services/app_context.ts index 7d31c2744ab8b..1be52a8c69050 100644 --- a/x-pack/plugins/snapshot_restore/public/app/services/app_context.ts +++ b/x-pack/plugins/snapshot_restore/public/app/services/app_context.ts @@ -4,12 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import { createContext, useContext } from 'react'; import { AppCore, AppPlugins } from '../../shim'; -export interface AppContextInterface { +export interface AppStateInterface { core: AppCore; plugins: AppPlugins; } -export const AppContext = React.createContext(null); +const AppState = createContext({}); + +export const AppStateProvider = AppState.Provider; + +export const useAppState = () => useContext(AppState); diff --git a/x-pack/plugins/snapshot_restore/public/app/services/documentation_links.ts b/x-pack/plugins/snapshot_restore/public/app/services/documentation_links.ts new file mode 100644 index 0000000000000..17e4c9c331981 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/services/documentation_links.ts @@ -0,0 +1,35 @@ +/* + * 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 { + RepositoryDocPaths, + RepositoryType, + RepositoryTypes, +} from '../../../common/types/repository_types'; + +export const getRepositoryTypeDocUrl = ( + type: RepositoryType, + esDocBasePath: string, + esPluginDocBasePath: string +): string => { + switch (type) { + case RepositoryTypes.fs: + return `${esDocBasePath}${RepositoryDocPaths.fs}`; + case RepositoryTypes.url: + return `${esDocBasePath}${RepositoryDocPaths.url}`; + case RepositoryTypes.source: + return `${esDocBasePath}${RepositoryDocPaths.source}`; + case RepositoryTypes.s3: + return `${esPluginDocBasePath}${RepositoryDocPaths.s3}`; + case RepositoryTypes.hdfs: + return `${esPluginDocBasePath}${RepositoryDocPaths.hdfs}`; + case RepositoryTypes.azure: + return `${esPluginDocBasePath}${RepositoryDocPaths.azure}`; + case RepositoryTypes.gcs: + return `${esPluginDocBasePath}${RepositoryDocPaths.gcs}`; + default: + return `${esDocBasePath}${RepositoryDocPaths.default}`; + } +}; diff --git a/x-pack/plugins/snapshot_restore/public/app/services/use_request.ts b/x-pack/plugins/snapshot_restore/public/app/services/use_request.ts new file mode 100644 index 0000000000000..fbc05d2b8e267 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/app/services/use_request.ts @@ -0,0 +1,67 @@ +/* + * 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 { API_BASE_PATH } from '../../../common/constants'; +import { AppStateInterface, useAppState } from './app_context'; + +export const useRequest = ({ + path, + method, + body, + interval, +}: { + path: string; + method: string; + body?: any; + interval?: number; +}) => { + const [ + { + core: { http, chrome }, + }, + ] = useAppState() as [AppStateInterface]; + + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + + const request = async () => { + setError(null); + setLoading(true); + try { + const response = await http + .getClient() + [method](chrome.addBasePath(`${API_BASE_PATH}${path}`), body); + if (!response.data) { + throw new Error(response.statusText); + } + setData(response.data); + } catch (e) { + setError(e); + } + setLoading(false); + }; + + useEffect( + () => { + request(); + if (interval) { + const intervalRequest = setInterval(request, interval); + return () => { + clearInterval(intervalRequest); + }; + } + }, + [path] + ); + + return { + error, + loading, + data, + request, + }; +}; diff --git a/x-pack/plugins/snapshot_restore/public/plugin.ts b/x-pack/plugins/snapshot_restore/public/plugin.ts index 0b6bbc198485d..cc0f006603a9c 100644 --- a/x-pack/plugins/snapshot_restore/public/plugin.ts +++ b/x-pack/plugins/snapshot_restore/public/plugin.ts @@ -15,7 +15,7 @@ const REACT_ROOT_ID = 'snapshotRestoreReactRoot'; export class Plugin { public start(core: Core, plugins: Plugins): void { - const { i18n, routing, http, chrome, notification } = core; + const { i18n, routing, http, chrome, notification, documentation } = core; const { management } = plugins; // Register management section @@ -56,7 +56,7 @@ export class Plugin { if (elem) { renderReact( elem, - { i18n, chrome, notification } as AppCore, + { i18n, chrome, notification, http, documentation } as AppCore, { management } as AppPlugins ); } diff --git a/x-pack/plugins/snapshot_restore/public/shim.ts b/x-pack/plugins/snapshot_restore/public/shim.ts index 86de665c39bc9..5389b26939399 100644 --- a/x-pack/plugins/snapshot_restore/public/shim.ts +++ b/x-pack/plugins/snapshot_restore/public/shim.ts @@ -8,6 +8,7 @@ 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 } from 'ui/notify'; import routes from 'ui/routes'; @@ -24,6 +25,14 @@ export interface AppCore { notification: { fatalError: typeof fatalError; }; + http: { + getClient(): any; + setClient(client: any): void; + }; + documentation: { + esDocBasePath: string; + esPluginDocBasePath: string; + }; } export interface AppPlugins { @@ -41,10 +50,6 @@ export interface Core extends AppCore { registerRouter(router: HashRouter): void; getRouter(): HashRouter | undefined; }; - http: { - setClient(client: any): void; - getClient(): any; - }; } export interface Plugins extends AppPlugins {} // tslint:disable-line no-empty-interface @@ -84,6 +89,10 @@ export function createShim(): { core: Core; plugins: Plugins } { notification: { fatalError, }, + 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: { diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts new file mode 100644 index 0000000000000..e77821e4ef9cc --- /dev/null +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { getAllHandler, getOneHandler } 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().mockReturnValue(mockEsResponse); + const expectedResponse = [{ name: 'fooRepository' }, { name: 'barRepository' }]; + 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().mockReturnValue(mockEsResponse); + const expectedResponse: any[] = []; + await expect( + getAllHandler(mockRequest, callWithRequest, mockResponseToolkit) + ).resolves.toEqual(expectedResponse); + }); + + it('should throw if ES error', async () => { + const callWithRequest = jest.fn().mockImplementation(() => { + throw 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]: { abc: 123 }, + }; + const callWithRequest = jest.fn().mockReturnValue(mockEsResponse); + const expectedResponse = { name, ...mockEsResponse[name] }; + 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().mockReturnValue(mockEsResponse); + const expectedResponse = {}; + await expect( + getOneHandler(mockOneRequest, callWithRequest, mockResponseToolkit) + ).resolves.toEqual(expectedResponse); + }); + + it('should throw if ES error', async () => { + const callWithRequest = jest.fn().mockImplementation(() => { + throw new Error(); + }); + await expect( + getOneHandler(mockOneRequest, callWithRequest, mockResponseToolkit) + ).rejects.toThrow(); + }); + }); +}); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts index ee1a260ecba4e..9a53ac628a593 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts @@ -10,24 +10,27 @@ export function registerRepositoriesRoutes(router: Router) { router.get('repositories/{name}', getOneHandler); } -const getAllHandler: RouterRouteHandler = async (req, callWithRequest) => { +export const getAllHandler: RouterRouteHandler = async (req, callWithRequest) => { const repositoriesByName = await callWithRequest('snapshot.getRepository', { repository: '_all', }); const repositories = Object.keys(repositoriesByName).map(name => { return { name, - test: repositoriesByName[name], + ...repositoriesByName[name], }; }); return repositories; }; -const getOneHandler: RouterRouteHandler = async (req, callWithRequest) => { +export const getOneHandler: RouterRouteHandler = async (req, callWithRequest) => { const { name } = req.params; const repositoryByName = await callWithRequest('snapshot.getRepository', { repository: name }); if (repositoryByName[name]) { - return repositoryByName[name]; + return { + name, + ...repositoryByName[name], + }; } else { return {}; }