mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Search][Onboarding] Elasticsearch start page (#192034)
## Summary This PR introduces the elasticsearch start page with the UI user flow. This page allows you to create an index and then redirects you to the index details page on creation. It also polls for new indices so if you create an index in the dev console or outside the browser it will also redirect you to the index details page for the new index. For now we are redirecting to the existing index details page, but we'll update this once the new page is in place. This PR also introduces FTR tests for the above UX. ### Screenshots  Viewer  ### Testing The `search_indices` plugin is disabled by default so to test this you must enable it in your `kibana.dev.yml` with: ```yaml xpack.searchIndices.enabled: true ``` Then run serverless search with `yarn serverless-es` Then navigate to the new start page manually: `http://localhost:5601/app/elasticsearch/start` This page will redirect you if you have any indices, so to test the UX you need to start with 0 user indices. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
e02e8fc1ff
commit
31b7c0b0a4
27 changed files with 933 additions and 12 deletions
11
x-pack/plugins/search_indices/common/routes.ts
Normal file
11
x-pack/plugins/search_indices/common/routes.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
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const GET_STATUS_ROUTE = '/internal/search_indices/status';
|
||||||
|
export const GET_USER_PRIVILEGES_ROUTE = '/internal/search_indices/start_privileges';
|
||||||
|
|
||||||
|
export const POST_CREATE_INDEX_ROUTE = '/internal/search_indices/indices/create';
|
|
@ -15,3 +15,11 @@ export interface UserStartPrivilegesResponse {
|
||||||
canCreateIndex: boolean;
|
canCreateIndex: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateIndexRequest {
|
||||||
|
indexName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateIndexResponse {
|
||||||
|
index: string;
|
||||||
|
}
|
||||||
|
|
11
x-pack/plugins/search_indices/public/analytics/constants.ts
Normal file
11
x-pack/plugins/search_indices/public/analytics/constants.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
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum AnalyticsEvents {
|
||||||
|
startPageOpened = 'start_page_opened',
|
||||||
|
startCreateIndexClick = 'start_create_index',
|
||||||
|
}
|
|
@ -11,20 +11,19 @@ import { CoreStart } from '@kbn/core/public';
|
||||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
import { I18nProvider } from '@kbn/i18n-react';
|
import { I18nProvider } from '@kbn/i18n-react';
|
||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { Router } from '@kbn/shared-ux-router';
|
import { Router } from '@kbn/shared-ux-router';
|
||||||
import { UsageTrackerContextProvider } from './contexts/usage_tracker_context';
|
import { UsageTrackerContextProvider } from './contexts/usage_tracker_context';
|
||||||
import { initQueryClient } from './services/query_client';
|
|
||||||
import { SearchIndicesServicesContextDeps } from './types';
|
import { SearchIndicesServicesContextDeps } from './types';
|
||||||
|
|
||||||
export const renderApp = async (
|
export const renderApp = async (
|
||||||
App: React.FC<{}>,
|
App: React.FC<{}>,
|
||||||
core: CoreStart,
|
core: CoreStart,
|
||||||
services: SearchIndicesServicesContextDeps,
|
services: SearchIndicesServicesContextDeps,
|
||||||
element: HTMLElement
|
element: HTMLElement,
|
||||||
|
queryClient: QueryClient
|
||||||
) => {
|
) => {
|
||||||
const queryClient = initQueryClient(core.notifications.toasts);
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<KibanaRenderContextProvider {...core}>
|
<KibanaRenderContextProvider {...core}>
|
||||||
<KibanaContextProvider services={{ ...core, ...services }}>
|
<KibanaContextProvider services={{ ...core, ...services }}>
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
EuiButton,
|
||||||
|
EuiFieldText,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiForm,
|
||||||
|
EuiFormRow,
|
||||||
|
EuiIcon,
|
||||||
|
EuiText,
|
||||||
|
EuiTitle,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
|
import type { UserStartPrivilegesResponse } from '../../../common';
|
||||||
|
import { AnalyticsEvents } from '../../analytics/constants';
|
||||||
|
import { useUsageTracker } from '../../hooks/use_usage_tracker';
|
||||||
|
import { isValidIndexName, generateRandomIndexName } from '../../utils/indices';
|
||||||
|
|
||||||
|
import { useCreateIndex } from './hooks/use_create_index';
|
||||||
|
|
||||||
|
interface CreateIndexFormState {
|
||||||
|
indexName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCreateIndexState(): CreateIndexFormState {
|
||||||
|
return {
|
||||||
|
indexName: generateRandomIndexName(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateIndexFormProps {
|
||||||
|
userPrivileges?: UserStartPrivilegesResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateIndexForm = ({ userPrivileges }: CreateIndexFormProps) => {
|
||||||
|
const [formState, setFormState] = useState<CreateIndexFormState>(initCreateIndexState());
|
||||||
|
const [indexNameHasError, setIndexNameHasError] = useState<boolean>(false);
|
||||||
|
const usageTracker = useUsageTracker();
|
||||||
|
const { createIndex, isLoading } = useCreateIndex();
|
||||||
|
const onCreateIndex = useCallback(() => {
|
||||||
|
if (!isValidIndexName(formState.indexName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
usageTracker.click(AnalyticsEvents.startCreateIndexClick);
|
||||||
|
createIndex({ indexName: formState.indexName });
|
||||||
|
}, [usageTracker, createIndex, formState.indexName]);
|
||||||
|
const onIndexNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newIndexName = e.target.value;
|
||||||
|
setFormState({ ...formState, indexName: e.target.value });
|
||||||
|
const invalidIndexName = !isValidIndexName(newIndexName);
|
||||||
|
if (indexNameHasError !== invalidIndexName) {
|
||||||
|
setIndexNameHasError(invalidIndexName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiForm component="form" fullWidth>
|
||||||
|
<EuiFlexGroup direction="column" gutterSize="m">
|
||||||
|
<EuiFlexGroup>
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiTitle size="xs">
|
||||||
|
<h4>
|
||||||
|
{i18n.translate('xpack.searchIndices.startPage.createIndex.title', {
|
||||||
|
defaultMessage: 'Create your first index',
|
||||||
|
})}
|
||||||
|
</h4>
|
||||||
|
</EuiTitle>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<></>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
<EuiText color="subdued">
|
||||||
|
<p>
|
||||||
|
{i18n.translate('xpack.searchIndices.startPage.createIndex.description', {
|
||||||
|
defaultMessage:
|
||||||
|
'An index stores your data and defines the schema, or field mappings, for your searches',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</EuiText>
|
||||||
|
<EuiFormRow
|
||||||
|
label={i18n.translate('xpack.searchIndices.startPage.createIndex.name.label', {
|
||||||
|
defaultMessage: 'Name your index',
|
||||||
|
})}
|
||||||
|
helpText={i18n.translate('xpack.searchIndices.startPage.createIndex.name.helpText', {
|
||||||
|
defaultMessage:
|
||||||
|
'Index names must be lowercase and can only contain hyphens and numbers',
|
||||||
|
})}
|
||||||
|
fullWidth
|
||||||
|
isInvalid={indexNameHasError}
|
||||||
|
>
|
||||||
|
<EuiFieldText
|
||||||
|
fullWidth
|
||||||
|
data-test-subj="indexNameField"
|
||||||
|
name="indexName"
|
||||||
|
value={formState.indexName}
|
||||||
|
isInvalid={indexNameHasError}
|
||||||
|
onChange={onIndexNameChange}
|
||||||
|
placeholder={i18n.translate(
|
||||||
|
'xpack.searchIndices.startPage.createIndex.name.placeholder',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Enter a name for your index',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</EuiFormRow>
|
||||||
|
<EuiFlexGroup alignItems="center">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiButton
|
||||||
|
color="primary"
|
||||||
|
iconSide="left"
|
||||||
|
iconType="sparkles"
|
||||||
|
data-test-subj="createIndexBtn"
|
||||||
|
fill
|
||||||
|
disabled={
|
||||||
|
indexNameHasError ||
|
||||||
|
isLoading ||
|
||||||
|
userPrivileges?.privileges?.canCreateIndex === false
|
||||||
|
}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={onCreateIndex}
|
||||||
|
>
|
||||||
|
{i18n.translate('xpack.searchIndices.startPage.createIndex.action.text', {
|
||||||
|
defaultMessage: 'Create my index',
|
||||||
|
})}
|
||||||
|
</EuiButton>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem>
|
||||||
|
{userPrivileges?.privileges?.canCreateApiKeys && (
|
||||||
|
<EuiFlexGroup gutterSize="s">
|
||||||
|
<EuiIcon size="l" type="key" color="subdued" />
|
||||||
|
<EuiText size="s" data-test-subj="apiKeyLabel">
|
||||||
|
<p>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.searchIndices.startPage.createIndex.apiKeyCreation.description',
|
||||||
|
{
|
||||||
|
defaultMessage: "We'll create an API key for this index",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</EuiText>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
)}
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</EuiForm>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
|
import type { IndicesStatusResponse, UserStartPrivilegesResponse } from '../../../common';
|
||||||
|
|
||||||
|
import { AnalyticsEvents } from '../../analytics/constants';
|
||||||
|
import { useUsageTracker } from '../../hooks/use_usage_tracker';
|
||||||
|
|
||||||
|
import { CreateIndexForm } from './create_index';
|
||||||
|
|
||||||
|
const MAX_WIDTH = '600px';
|
||||||
|
|
||||||
|
export interface ElasticsearchStartProps {
|
||||||
|
indicesData?: IndicesStatusResponse;
|
||||||
|
userPrivileges?: UserStartPrivilegesResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ElasticsearchStart = ({ userPrivileges }: ElasticsearchStartProps) => {
|
||||||
|
const usageTracker = useUsageTracker();
|
||||||
|
useEffect(() => {
|
||||||
|
usageTracker.load(AnalyticsEvents.startPageOpened);
|
||||||
|
}, [usageTracker]);
|
||||||
|
return (
|
||||||
|
<EuiPanel
|
||||||
|
color="subdued"
|
||||||
|
hasShadow={false}
|
||||||
|
hasBorder
|
||||||
|
paddingSize="l"
|
||||||
|
style={{ maxWidth: MAX_WIDTH, margin: '0 auto' }}
|
||||||
|
>
|
||||||
|
<EuiFlexGroup alignItems="flexStart">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiIcon type="logoElasticsearch" size="xl" />
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiPanel paddingSize="none" color="transparent">
|
||||||
|
<EuiTitle size="xs">
|
||||||
|
<h1>
|
||||||
|
{i18n.translate('xpack.searchIndices.startPage.pageTitle', {
|
||||||
|
defaultMessage: 'Elasticsearch',
|
||||||
|
})}
|
||||||
|
</h1>
|
||||||
|
</EuiTitle>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
<EuiTitle size="l">
|
||||||
|
<h2>
|
||||||
|
{i18n.translate('xpack.searchIndices.startPage.pageDescription', {
|
||||||
|
defaultMessage: 'Vectorize, search, and visualize your data',
|
||||||
|
})}
|
||||||
|
</h2>
|
||||||
|
</EuiTitle>
|
||||||
|
</EuiPanel>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
<EuiSpacer />
|
||||||
|
<EuiPanel>
|
||||||
|
<CreateIndexForm userPrivileges={userPrivileges} />
|
||||||
|
</EuiPanel>
|
||||||
|
</EuiPanel>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useCreateIndex as useCreateIndexApi } from '../../../hooks/api/use_create_index';
|
||||||
|
|
||||||
|
import { useKibana } from '../../../hooks/use_kibana';
|
||||||
|
|
||||||
|
import { navigateToIndexDetails } from './utils';
|
||||||
|
|
||||||
|
export const useCreateIndex = () => {
|
||||||
|
const { application, http } = useKibana().services;
|
||||||
|
const { createIndex, isSuccess, isLoading, data: createIndexResponse } = useCreateIndexApi();
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSuccess && createIndexResponse !== undefined) {
|
||||||
|
navigateToIndexDetails(application, http, createIndexResponse.index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [application, http, isSuccess, createIndexResponse]);
|
||||||
|
|
||||||
|
return { createIndex, isLoading };
|
||||||
|
};
|
|
@ -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
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import type { IndicesStatusResponse } from '../../../../common';
|
||||||
|
|
||||||
|
import { useKibana } from '../../../hooks/use_kibana';
|
||||||
|
|
||||||
|
import { navigateToIndexDetails } from './utils';
|
||||||
|
|
||||||
|
export const useIndicesRedirect = (indicesStatus?: IndicesStatusResponse) => {
|
||||||
|
const { application, http } = useKibana().services;
|
||||||
|
return useEffect(() => {
|
||||||
|
if (!indicesStatus) return;
|
||||||
|
if (indicesStatus.indexNames.length === 0) return;
|
||||||
|
if (indicesStatus.indexNames.length === 1) {
|
||||||
|
navigateToIndexDetails(application, http, indicesStatus.indexNames[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
application.navigateToApp('management', { deepLinkId: 'index_management' });
|
||||||
|
}, [application, http, indicesStatus]);
|
||||||
|
};
|
|
@ -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
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ApplicationStart, HttpSetup } from '@kbn/core/public';
|
||||||
|
|
||||||
|
// TODO: we should define a locator for this and use that instead
|
||||||
|
const INDEX_DETAILS_PATH = '/app/management/data/index_management/indices/index_details';
|
||||||
|
|
||||||
|
function getIndexDetailsPath(http: HttpSetup, indexName: string) {
|
||||||
|
return http.basePath.prepend(`${INDEX_DETAILS_PATH}?indexName=${indexName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const navigateToIndexDetails = (
|
||||||
|
application: ApplicationStart,
|
||||||
|
http: HttpSetup,
|
||||||
|
indexName: string
|
||||||
|
) => {
|
||||||
|
application.navigateToUrl(getIndexDetailsPath(http, indexName));
|
||||||
|
};
|
|
@ -11,24 +11,40 @@ import { EuiLoadingLogo, EuiPageTemplate } from '@elastic/eui';
|
||||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||||
|
|
||||||
import { useKibana } from '../../hooks/use_kibana';
|
import { useKibana } from '../../hooks/use_kibana';
|
||||||
|
import { useIndicesStatusQuery } from '../../hooks/api/use_indices_status';
|
||||||
|
import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions';
|
||||||
|
|
||||||
|
import { useIndicesRedirect } from './hooks/use_indices_redirect';
|
||||||
|
import { ElasticsearchStart } from './elasticsearch_start';
|
||||||
|
import { StartPageError } from './status_error';
|
||||||
|
|
||||||
export const ElasticsearchStartPage = () => {
|
export const ElasticsearchStartPage = () => {
|
||||||
const { console: consolePlugin } = useKibana().services;
|
const { console: consolePlugin } = useKibana().services;
|
||||||
|
const {
|
||||||
|
data: indicesData,
|
||||||
|
isInitialLoading,
|
||||||
|
isError: hasIndicesStatusFetchError,
|
||||||
|
error: indicesFetchError,
|
||||||
|
} = useIndicesStatusQuery();
|
||||||
|
const { data: userPrivileges } = useUserPrivilegesQuery();
|
||||||
|
|
||||||
const embeddableConsole = useMemo(
|
const embeddableConsole = useMemo(
|
||||||
() => (consolePlugin?.EmbeddableConsole ? <consolePlugin.EmbeddableConsole /> : null),
|
() => (consolePlugin?.EmbeddableConsole ? <consolePlugin.EmbeddableConsole /> : null),
|
||||||
[consolePlugin]
|
[consolePlugin]
|
||||||
);
|
);
|
||||||
|
useIndicesRedirect(indicesData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiPageTemplate
|
<EuiPageTemplate
|
||||||
offset={0}
|
offset={0}
|
||||||
restrictWidth={false}
|
restrictWidth={false}
|
||||||
data-test-subj="search-startpage"
|
data-test-subj="elasticsearchStartPage"
|
||||||
grow={false}
|
grow={false}
|
||||||
>
|
>
|
||||||
<KibanaPageTemplate.Section alignment="center" restrictWidth={false} grow>
|
<KibanaPageTemplate.Section alignment="center" restrictWidth={false} grow>
|
||||||
<EuiLoadingLogo />
|
{isInitialLoading && <EuiLoadingLogo />}
|
||||||
|
{hasIndicesStatusFetchError && <StartPageError error={indicesFetchError} />}
|
||||||
|
<ElasticsearchStart indicesData={indicesData} userPrivileges={userPrivileges} />
|
||||||
</KibanaPageTemplate.Section>
|
</KibanaPageTemplate.Section>
|
||||||
{embeddableConsole}
|
{embeddableConsole}
|
||||||
</EuiPageTemplate>
|
</EuiPageTemplate>
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
|
import { getErrorMessage } from '../../utils/errors';
|
||||||
|
|
||||||
|
export interface StartPageErrorProps {
|
||||||
|
error: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StartPageError = ({ error }: StartPageErrorProps) => {
|
||||||
|
return (
|
||||||
|
<EuiEmptyPrompt
|
||||||
|
iconType="error"
|
||||||
|
color="danger"
|
||||||
|
title={
|
||||||
|
<h2>
|
||||||
|
{i18n.translate('xpack.searchIndices.startPage.statusFetchError.title', {
|
||||||
|
defaultMessage: 'Error loading indices',
|
||||||
|
})}
|
||||||
|
</h2>
|
||||||
|
}
|
||||||
|
body={
|
||||||
|
<EuiCodeBlock css={{ textAlign: 'left' }}>
|
||||||
|
{getErrorMessage(
|
||||||
|
error,
|
||||||
|
i18n.translate('xpack.searchIndices.startPage.statusFetchError.unknownError', {
|
||||||
|
defaultMessage: 'Unknown error fetching indices.',
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</EuiCodeBlock>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { POST_CREATE_INDEX_ROUTE } from '../../../common/routes';
|
||||||
|
import { CreateIndexRequest, CreateIndexResponse } from '../../../common/types';
|
||||||
|
|
||||||
|
import { useKibana } from '../use_kibana';
|
||||||
|
|
||||||
|
export const useCreateIndex = () => {
|
||||||
|
const { http } = useKibana().services;
|
||||||
|
|
||||||
|
const { mutate: createIndex, ...rest } = useMutation({
|
||||||
|
mutationKey: ['searchIndicesCreateIndex'],
|
||||||
|
mutationFn: async (input: CreateIndexRequest) =>
|
||||||
|
http.post<CreateIndexResponse>(POST_CREATE_INDEX_ROUTE, {
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { createIndex, ...rest };
|
||||||
|
};
|
|
@ -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
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { GET_STATUS_ROUTE } from '../../../common/routes';
|
||||||
|
import type { IndicesStatusResponse } from '../../../common/types';
|
||||||
|
|
||||||
|
import { useKibana } from '../use_kibana';
|
||||||
|
|
||||||
|
const DEFAULT_INDICES_POLLING_INTERVAL = 15 * 1000;
|
||||||
|
|
||||||
|
export const useIndicesStatusQuery = (pollingInterval = DEFAULT_INDICES_POLLING_INTERVAL) => {
|
||||||
|
const { http } = useKibana().services;
|
||||||
|
return useQuery({
|
||||||
|
refetchInterval: pollingInterval,
|
||||||
|
refetchIntervalInBackground: true,
|
||||||
|
refetchOnWindowFocus: 'always',
|
||||||
|
retry: true,
|
||||||
|
queryKey: ['fetchSearchIndicesStatus'],
|
||||||
|
queryFn: () => http.get<IndicesStatusResponse>(GET_STATUS_ROUTE),
|
||||||
|
});
|
||||||
|
};
|
|
@ -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
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { GET_USER_PRIVILEGES_ROUTE } from '../../../common/routes';
|
||||||
|
import type { UserStartPrivilegesResponse } from '../../../common/types';
|
||||||
|
|
||||||
|
import { useKibana } from '../use_kibana';
|
||||||
|
|
||||||
|
export const useUserPrivilegesQuery = () => {
|
||||||
|
const { http } = useKibana().services;
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['fetchUserStartPrivileges'],
|
||||||
|
queryFn: () => http.get<UserStartPrivilegesResponse>(GET_USER_PRIVILEGES_ROUTE),
|
||||||
|
});
|
||||||
|
};
|
|
@ -13,6 +13,7 @@ import type {
|
||||||
SearchIndicesPluginStart,
|
SearchIndicesPluginStart,
|
||||||
SearchIndicesServicesContextDeps,
|
SearchIndicesServicesContextDeps,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { initQueryClient } from './services/query_client';
|
||||||
|
|
||||||
export class SearchIndicesPlugin
|
export class SearchIndicesPlugin
|
||||||
implements Plugin<SearchIndicesPluginSetup, SearchIndicesPluginStart>
|
implements Plugin<SearchIndicesPluginSetup, SearchIndicesPluginStart>
|
||||||
|
@ -20,6 +21,8 @@ export class SearchIndicesPlugin
|
||||||
public setup(
|
public setup(
|
||||||
core: CoreSetup<SearchIndicesAppPluginStartDependencies, SearchIndicesPluginStart>
|
core: CoreSetup<SearchIndicesAppPluginStartDependencies, SearchIndicesPluginStart>
|
||||||
): SearchIndicesPluginSetup {
|
): SearchIndicesPluginSetup {
|
||||||
|
const queryClient = initQueryClient(core.notifications.toasts);
|
||||||
|
|
||||||
core.application.register({
|
core.application.register({
|
||||||
id: 'elasticsearchStart',
|
id: 'elasticsearchStart',
|
||||||
appRoute: '/app/elasticsearch/start',
|
appRoute: '/app/elasticsearch/start',
|
||||||
|
@ -34,7 +37,7 @@ export class SearchIndicesPlugin
|
||||||
...depsStart,
|
...depsStart,
|
||||||
history,
|
history,
|
||||||
};
|
};
|
||||||
return renderApp(ElasticsearchStartPage, coreStart, startDeps, element);
|
return renderApp(ElasticsearchStartPage, coreStart, startDeps, element, queryClient);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
core.application.register({
|
core.application.register({
|
||||||
|
@ -51,7 +54,7 @@ export class SearchIndicesPlugin
|
||||||
...depsStart,
|
...depsStart,
|
||||||
history,
|
history,
|
||||||
};
|
};
|
||||||
return renderApp(SearchIndicesRouter, coreStart, startDeps, element);
|
return renderApp(SearchIndicesRouter, coreStart, startDeps, element, queryClient);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
49
x-pack/plugins/search_indices/public/utils/indices.test.ts
Normal file
49
x-pack/plugins/search_indices/public/utils/indices.test.ts
Normal file
|
@ -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
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { generateRandomIndexName, isValidIndexName } from './indices';
|
||||||
|
|
||||||
|
describe('indices utils', function () {
|
||||||
|
describe('generateRandomIndexName', function () {
|
||||||
|
const DEFAULT_PREFIX = 'search-';
|
||||||
|
const DEFAULT_SUFFIX_LENGTH = 4;
|
||||||
|
it('defaults to search- with a 4 character suffix', () => {
|
||||||
|
const indexName = generateRandomIndexName();
|
||||||
|
|
||||||
|
expect(indexName.startsWith(DEFAULT_PREFIX)).toBe(true);
|
||||||
|
expect(indexName.length).toBe(DEFAULT_PREFIX.length + DEFAULT_SUFFIX_LENGTH);
|
||||||
|
expect(isValidIndexName(indexName)).toBe(true);
|
||||||
|
});
|
||||||
|
it('supports changing the prefix', () => {
|
||||||
|
const otherPrefix = 'foo-';
|
||||||
|
const indexName = generateRandomIndexName(otherPrefix);
|
||||||
|
|
||||||
|
expect(indexName.startsWith(otherPrefix)).toBe(true);
|
||||||
|
expect(indexName.length).toBe(otherPrefix.length + DEFAULT_SUFFIX_LENGTH);
|
||||||
|
expect(isValidIndexName(indexName)).toBe(true);
|
||||||
|
});
|
||||||
|
it('supports changing the suffix length', () => {
|
||||||
|
const indexName = generateRandomIndexName(undefined, 6);
|
||||||
|
|
||||||
|
expect(indexName.startsWith(DEFAULT_PREFIX)).toBe(true);
|
||||||
|
expect(indexName.length).toBe(DEFAULT_PREFIX.length + 6);
|
||||||
|
expect(isValidIndexName(indexName)).toBe(true);
|
||||||
|
});
|
||||||
|
it('fallsback to single character suffix for invalid lengths', () => {
|
||||||
|
let indexName = generateRandomIndexName(undefined, 0);
|
||||||
|
|
||||||
|
expect(indexName.startsWith(DEFAULT_PREFIX)).toBe(true);
|
||||||
|
expect(indexName.length).toBe(DEFAULT_PREFIX.length + 1);
|
||||||
|
expect(isValidIndexName(indexName)).toBe(true);
|
||||||
|
|
||||||
|
indexName = generateRandomIndexName(undefined, -5);
|
||||||
|
expect(indexName.startsWith(DEFAULT_PREFIX)).toBe(true);
|
||||||
|
expect(indexName.length).toBe(DEFAULT_PREFIX.length + 1);
|
||||||
|
expect(isValidIndexName(indexName)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
37
x-pack/plugins/search_indices/public/utils/indices.ts
Normal file
37
x-pack/plugins/search_indices/public/utils/indices.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html for the current rules
|
||||||
|
|
||||||
|
export function isValidIndexName(name: string) {
|
||||||
|
const byteLength = encodeURI(name).split(/%(?:u[0-9A-F]{2})?[0-9A-F]{2}|./).length - 1;
|
||||||
|
const reg = new RegExp('[\\\\/:*?"<>|\\s,#]+');
|
||||||
|
const indexPatternInvalid =
|
||||||
|
byteLength > 255 || // name can't be greater than 255 bytes
|
||||||
|
name !== name.toLowerCase() || // name should be lowercase
|
||||||
|
name.match(/^[-_+.]/) !== null || // name can't start with these chars
|
||||||
|
name.match(reg) !== null; // name can't contain these chars
|
||||||
|
|
||||||
|
return !indexPatternInvalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateRandomIndexName(
|
||||||
|
prefix: string = 'search-',
|
||||||
|
randomSuffixLength: number = 4
|
||||||
|
) {
|
||||||
|
const suffixCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
const charsLength = suffixCharacters.length;
|
||||||
|
let result = prefix;
|
||||||
|
|
||||||
|
let counter = 0;
|
||||||
|
do {
|
||||||
|
result += suffixCharacters.charAt(Math.random() * charsLength);
|
||||||
|
counter++;
|
||||||
|
} while (counter < randomSuffixLength);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
50
x-pack/plugins/search_indices/server/lib/indices.test.ts
Normal file
50
x-pack/plugins/search_indices/server/lib/indices.test.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||||
|
import type { Logger } from '@kbn/logging';
|
||||||
|
|
||||||
|
import { createIndex } from './indices';
|
||||||
|
|
||||||
|
const mockLogger = {
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
};
|
||||||
|
const logger: Logger = mockLogger as unknown as Logger;
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
indices: {
|
||||||
|
create: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const client = mockClient as unknown as ElasticsearchClient;
|
||||||
|
|
||||||
|
describe('indices lib', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createIndex', () => {
|
||||||
|
it('should create index with req', async () => {
|
||||||
|
mockClient.indices.create.mockResolvedValue({});
|
||||||
|
|
||||||
|
await expect(createIndex(client, logger, { indexName: 'test-index' })).resolves.toEqual({
|
||||||
|
index: 'test-index',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockClient.indices.create).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockClient.indices.create).toHaveBeenCalledWith({ index: 'test-index' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should raise errors from client', async () => {
|
||||||
|
const error = new Error('Boom!!');
|
||||||
|
mockClient.indices.create.mockRejectedValue(error);
|
||||||
|
|
||||||
|
await expect(createIndex(client, logger, { indexName: 'test-index' })).rejects.toEqual(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
24
x-pack/plugins/search_indices/server/lib/indices.ts
Normal file
24
x-pack/plugins/search_indices/server/lib/indices.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||||
|
import type { Logger } from '@kbn/logging';
|
||||||
|
|
||||||
|
import { CreateIndexRequest, CreateIndexResponse } from '../../common/types';
|
||||||
|
|
||||||
|
export async function createIndex(
|
||||||
|
client: ElasticsearchClient,
|
||||||
|
logger: Logger,
|
||||||
|
data: CreateIndexRequest
|
||||||
|
): Promise<CreateIndexResponse> {
|
||||||
|
await client.indices.create({
|
||||||
|
index: data.indexName,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
index: data.indexName,
|
||||||
|
};
|
||||||
|
}
|
|
@ -8,8 +8,10 @@
|
||||||
import type { IRouter } from '@kbn/core/server';
|
import type { IRouter } from '@kbn/core/server';
|
||||||
import type { Logger } from '@kbn/logging';
|
import type { Logger } from '@kbn/logging';
|
||||||
|
|
||||||
|
import { registerIndicesRoutes } from './indices';
|
||||||
import { registerStatusRoutes } from './status';
|
import { registerStatusRoutes } from './status';
|
||||||
|
|
||||||
export function defineRoutes(router: IRouter, logger: Logger) {
|
export function defineRoutes(router: IRouter, logger: Logger) {
|
||||||
|
registerIndicesRoutes(router, logger);
|
||||||
registerStatusRoutes(router, logger);
|
registerStatusRoutes(router, logger);
|
||||||
}
|
}
|
||||||
|
|
65
x-pack/plugins/search_indices/server/routes/indices.ts
Normal file
65
x-pack/plugins/search_indices/server/routes/indices.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { schema } from '@kbn/config-schema';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import type { IRouter } from '@kbn/core/server';
|
||||||
|
import type { Logger } from '@kbn/logging';
|
||||||
|
|
||||||
|
import { POST_CREATE_INDEX_ROUTE } from '../../common/routes';
|
||||||
|
import { CreateIndexRequest } from '../../common/types';
|
||||||
|
import { createIndex } from '../lib/indices';
|
||||||
|
|
||||||
|
export function registerIndicesRoutes(router: IRouter, logger: Logger) {
|
||||||
|
router.post(
|
||||||
|
{
|
||||||
|
path: POST_CREATE_INDEX_ROUTE,
|
||||||
|
validate: {
|
||||||
|
body: schema.object({
|
||||||
|
indexName: schema.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
access: 'internal',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (context, request, response) => {
|
||||||
|
const core = await context.core;
|
||||||
|
const client = core.elasticsearch.client.asCurrentUser;
|
||||||
|
const data: CreateIndexRequest = request.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await createIndex(client, logger, data);
|
||||||
|
return response.ok({
|
||||||
|
body,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
switch (e?.meta?.body?.error?.type) {
|
||||||
|
case 'resource_already_exists_exception':
|
||||||
|
return response.conflict({
|
||||||
|
body: {
|
||||||
|
message: e.message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.customError({
|
||||||
|
statusCode: e?.meta && e.meta?.statusCode ? e.meta?.statusCode : 500,
|
||||||
|
body: {
|
||||||
|
message: i18n.translate('xpack.searchIndices.server.createIndex.errorMessage', {
|
||||||
|
defaultMessage: 'Failed to create index due to an exception.\n{errorMessage}',
|
||||||
|
values: {
|
||||||
|
errorMessage: e.message,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,12 +8,13 @@
|
||||||
import type { IRouter } from '@kbn/core/server';
|
import type { IRouter } from '@kbn/core/server';
|
||||||
import type { Logger } from '@kbn/logging';
|
import type { Logger } from '@kbn/logging';
|
||||||
|
|
||||||
|
import { GET_STATUS_ROUTE, GET_USER_PRIVILEGES_ROUTE } from '../../common/routes';
|
||||||
import { fetchIndicesStatus, fetchUserStartPrivileges } from '../lib/status';
|
import { fetchIndicesStatus, fetchUserStartPrivileges } from '../lib/status';
|
||||||
|
|
||||||
export function registerStatusRoutes(router: IRouter, logger: Logger) {
|
export function registerStatusRoutes(router: IRouter, logger: Logger) {
|
||||||
router.get(
|
router.get(
|
||||||
{
|
{
|
||||||
path: '/internal/search_indices/status',
|
path: GET_STATUS_ROUTE,
|
||||||
validate: {},
|
validate: {},
|
||||||
options: {
|
options: {
|
||||||
access: 'internal',
|
access: 'internal',
|
||||||
|
@ -33,7 +34,7 @@ export function registerStatusRoutes(router: IRouter, logger: Logger) {
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
{
|
{
|
||||||
path: '/internal/search_indices/start_privileges',
|
path: GET_USER_PRIVILEGES_ROUTE,
|
||||||
validate: {},
|
validate: {},
|
||||||
options: {
|
options: {
|
||||||
access: 'internal',
|
access: 'internal',
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from 'expect';
|
||||||
|
import { InternalRequestHeader, RoleCredentials } from '../../../../shared/services';
|
||||||
|
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||||
|
|
||||||
|
const INTERNAL_API_BASE_PATH = '/internal/search_indices';
|
||||||
|
|
||||||
|
export default function ({ getService }: FtrProviderContext) {
|
||||||
|
const log = getService('log');
|
||||||
|
const svlCommonApi = getService('svlCommonApi');
|
||||||
|
const svlUserManager = getService('svlUserManager');
|
||||||
|
const esDeleteAllIndices = getService('esDeleteAllIndices');
|
||||||
|
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||||
|
let roleAuthc: RoleCredentials;
|
||||||
|
let internalReqHeader: InternalRequestHeader;
|
||||||
|
|
||||||
|
describe('search_indices Indices APIs', function () {
|
||||||
|
before(function () {
|
||||||
|
internalReqHeader = svlCommonApi.getInternalRequestHeader();
|
||||||
|
});
|
||||||
|
describe('create index', function () {
|
||||||
|
const createIndexName = 'a-test-index';
|
||||||
|
describe('developer', function () {
|
||||||
|
before(async () => {
|
||||||
|
// get auth header for Viewer role
|
||||||
|
roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('developer');
|
||||||
|
});
|
||||||
|
after(async () => {
|
||||||
|
// Cleanup index created for testing purposes
|
||||||
|
try {
|
||||||
|
await esDeleteAllIndices(createIndexName);
|
||||||
|
} catch (err) {
|
||||||
|
log.debug('[Cleanup error] Error deleting index');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can create a new index', async () => {
|
||||||
|
const { body } = await supertestWithoutAuth
|
||||||
|
.post(`${INTERNAL_API_BASE_PATH}/indices/create`)
|
||||||
|
.set(internalReqHeader)
|
||||||
|
.set(roleAuthc.apiKeyHeader)
|
||||||
|
.send({
|
||||||
|
indexName: createIndexName,
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(body?.index).toBe(createIndexName);
|
||||||
|
});
|
||||||
|
it('gives a conflict error if the index exists already', async () => {
|
||||||
|
await supertestWithoutAuth
|
||||||
|
.post(`${INTERNAL_API_BASE_PATH}/indices/create`)
|
||||||
|
.set(internalReqHeader)
|
||||||
|
.set(roleAuthc.apiKeyHeader)
|
||||||
|
.send({
|
||||||
|
indexName: createIndexName,
|
||||||
|
})
|
||||||
|
.expect(409);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('viewer', function () {
|
||||||
|
before(async () => {
|
||||||
|
// get auth header for Viewer role
|
||||||
|
roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('viewer');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot create a new index', async () => {
|
||||||
|
await supertestWithoutAuth
|
||||||
|
.post(`${INTERNAL_API_BASE_PATH}/indices/create`)
|
||||||
|
.set(internalReqHeader)
|
||||||
|
.set(roleAuthc.apiKeyHeader)
|
||||||
|
.send({
|
||||||
|
indexName: 'a-new-index',
|
||||||
|
})
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import { SvlManagementPageProvider } from './svl_management_page';
|
||||||
import { SvlIngestPipelines } from './svl_ingest_pipelines';
|
import { SvlIngestPipelines } from './svl_ingest_pipelines';
|
||||||
import { SvlSearchHomePageProvider } from './svl_search_homepage';
|
import { SvlSearchHomePageProvider } from './svl_search_homepage';
|
||||||
import { SvlSearchIndexDetailPageProvider } from './svl_search_index_detail_page';
|
import { SvlSearchIndexDetailPageProvider } from './svl_search_index_detail_page';
|
||||||
|
import { SvlSearchElasticsearchStartPageProvider } from './svl_search_elasticsearch_start_page';
|
||||||
|
|
||||||
export const pageObjects = {
|
export const pageObjects = {
|
||||||
...xpackFunctionalPageObjects,
|
...xpackFunctionalPageObjects,
|
||||||
|
@ -41,4 +42,5 @@ export const pageObjects = {
|
||||||
svlIngestPipelines: SvlIngestPipelines,
|
svlIngestPipelines: SvlIngestPipelines,
|
||||||
svlSearchHomePage: SvlSearchHomePageProvider,
|
svlSearchHomePage: SvlSearchHomePageProvider,
|
||||||
svlSearchIndexDetailPage: SvlSearchIndexDetailPageProvider,
|
svlSearchIndexDetailPage: SvlSearchIndexDetailPageProvider,
|
||||||
|
svlSearchElasticsearchStartPage: SvlSearchElasticsearchStartPageProvider,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from '@kbn/expect';
|
||||||
|
import { FtrProviderContext } from '../ftr_provider_context';
|
||||||
|
|
||||||
|
export function SvlSearchElasticsearchStartPageProvider({ getService }: FtrProviderContext) {
|
||||||
|
const testSubjects = getService('testSubjects');
|
||||||
|
const browser = getService('browser');
|
||||||
|
const retry = getService('retry');
|
||||||
|
|
||||||
|
return {
|
||||||
|
async expectToBeOnStartPage() {
|
||||||
|
expect(await browser.getCurrentUrl()).contain('/app/elasticsearch/start');
|
||||||
|
await testSubjects.existOrFail('elasticsearchStartPage', { timeout: 2000 });
|
||||||
|
},
|
||||||
|
async expectToBeOnIndexDetailsPage() {
|
||||||
|
await retry.tryForTime(60 * 1000, async () => {
|
||||||
|
expect(await browser.getCurrentUrl()).contain(
|
||||||
|
'/app/management/data/index_management/indices/index_details'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async expectIndexNameToExist() {
|
||||||
|
await testSubjects.existOrFail('indexNameField');
|
||||||
|
},
|
||||||
|
async setIndexNameValue(value: string) {
|
||||||
|
await testSubjects.existOrFail('indexNameField');
|
||||||
|
await testSubjects.setValue('indexNameField', value);
|
||||||
|
},
|
||||||
|
async expectCreateIndexButtonToExist() {
|
||||||
|
await testSubjects.existOrFail('createIndexBtn');
|
||||||
|
},
|
||||||
|
async expectCreateIndexButtonToBeEnabled() {
|
||||||
|
await testSubjects.existOrFail('createIndexBtn');
|
||||||
|
expect(await testSubjects.isEnabled('createIndexBtn')).equal(true);
|
||||||
|
},
|
||||||
|
async expectCreateIndexButtonToBeDisabled() {
|
||||||
|
await testSubjects.existOrFail('createIndexBtn');
|
||||||
|
expect(await testSubjects.isEnabled('createIndexBtn')).equal(false);
|
||||||
|
},
|
||||||
|
async clickCreateIndexButton() {
|
||||||
|
await testSubjects.existOrFail('createIndexBtn');
|
||||||
|
expect(await testSubjects.isEnabled('createIndexBtn')).equal(true);
|
||||||
|
await testSubjects.click('createIndexBtn');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -22,5 +22,13 @@ export function SvlSearchNavigationServiceProvider({
|
||||||
await testSubjects.existOrFail('svlSearchOverviewPage', { timeout: 2000 });
|
await testSubjects.existOrFail('svlSearchOverviewPage', { timeout: 2000 });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async navigateToElasticsearchStartPage() {
|
||||||
|
await retry.tryForTime(60 * 1000, async () => {
|
||||||
|
await PageObjects.common.navigateToApp('elasticsearch/start', {
|
||||||
|
shouldLoginIfPrompted: false,
|
||||||
|
});
|
||||||
|
await testSubjects.existOrFail('elasticsearchStartPage', { timeout: 2000 });
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,82 @@
|
||||||
|
|
||||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||||
|
|
||||||
export default function ({}: FtrProviderContext) {
|
import { testHasEmbeddedConsole } from './embedded_console';
|
||||||
describe('Elasticsearch Start [Onboarding Empty State]', function () {});
|
|
||||||
|
export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
|
const pageObjects = getPageObjects([
|
||||||
|
'svlCommonPage',
|
||||||
|
'embeddedConsole',
|
||||||
|
'svlSearchElasticsearchStartPage',
|
||||||
|
]);
|
||||||
|
const svlSearchNavigation = getService('svlSearchNavigation');
|
||||||
|
const esDeleteAllIndices = getService('esDeleteAllIndices');
|
||||||
|
const es = getService('es');
|
||||||
|
|
||||||
|
const deleteAllTestIndices = async () => {
|
||||||
|
await esDeleteAllIndices(['search-*', 'test-*']);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Elasticsearch Start [Onboarding Empty State]', function () {
|
||||||
|
describe('developer', function () {
|
||||||
|
before(async () => {
|
||||||
|
await pageObjects.svlCommonPage.loginWithRole('developer');
|
||||||
|
});
|
||||||
|
after(async () => {
|
||||||
|
await deleteAllTestIndices();
|
||||||
|
});
|
||||||
|
beforeEach(async () => {
|
||||||
|
await deleteAllTestIndices();
|
||||||
|
await svlSearchNavigation.navigateToElasticsearchStartPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have embedded dev console', async () => {
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||||
|
await testHasEmbeddedConsole(pageObjects);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support index creation flow with UI', async () => {
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexButtonToBeEnabled();
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.clickCreateIndexButton();
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexDetailsPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support setting index name', async () => {
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectIndexNameToExist();
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.setIndexNameValue('INVALID_INDEX');
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexButtonToBeDisabled();
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.setIndexNameValue('test-index-name');
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexButtonToBeEnabled();
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.clickCreateIndexButton();
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexDetailsPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect to index details when index is created via API', async () => {
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||||
|
await es.indices.create({ index: 'test-my-index' });
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexDetailsPage();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('viewer', function () {
|
||||||
|
before(async () => {
|
||||||
|
await pageObjects.svlCommonPage.loginAsViewer();
|
||||||
|
await deleteAllTestIndices();
|
||||||
|
});
|
||||||
|
beforeEach(async () => {
|
||||||
|
await svlSearchNavigation.navigateToElasticsearchStartPage();
|
||||||
|
});
|
||||||
|
after(async () => {
|
||||||
|
await deleteAllTestIndices();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect to index details when index is created via API', async () => {
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage();
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexButtonToBeDisabled();
|
||||||
|
await es.indices.create({ index: 'test-my-api-index' });
|
||||||
|
await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexDetailsPage();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue