mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Index Management] Add content to index details page via extensions service (#170054)
## Summary Fixes https://github.com/elastic/kibana/issues/168704 This PR adds a function to the extensions service that allows to render custom content on overview tab of the index details page. When custom content is set, it will be rendered instead of the code block describing adding documents to the index. This PR also moves the ILM content from the overview tab to a separate tab. We will work on the design of this tab in a follow up PR. ### How to test To test the custom content apply changes in this [commit](16769d6c39
). ### Screenshots #### Custom content (example) <img width="1357" alt="Screenshot 2023-11-01 at 19 03 32" src="71372458
-4cc2-413d-bf5f-bb29bff73095"> #### ILM tab <img width="1129" alt="Screenshot 2023-11-01 at 18 54 07" src="52c09a73
-7d75-4f5f-8d52-b704cd9e6859"> ### Checklist - [ ] 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 - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] 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)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] 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)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Alison Goryachev <alisonmllr20@gmail.com>
This commit is contained in:
parent
ba4f60645d
commit
fdec4bf474
25 changed files with 453 additions and 405 deletions
|
@ -598,7 +598,7 @@ Index Management by running this series of requests in Console:
|
|||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/index_management/README.md[indexManagement]
|
||||
|Create an index with special characters and verify it renders correctly:
|
||||
|This service is exposed from the Index Management setup contract and can be used to add content to the indices list and the index details page.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/infra/README.md[infra]
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { init } from '../integration_tests/helpers/http_requests';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks';
|
||||
import { Index } from '../common/types';
|
||||
import {
|
||||
retryLifecycleActionExtension,
|
||||
removeLifecyclePolicyActionExtension,
|
||||
|
@ -20,8 +20,8 @@ import {
|
|||
} from '../public/extend_index_management';
|
||||
import { init as initHttp } from '../public/application/services/http';
|
||||
import { init as initUiMetric } from '../public/application/services/ui_metric';
|
||||
import { IndexLifecycleSummary } from '../public/extend_index_management/components/index_lifecycle_summary';
|
||||
import React from 'react';
|
||||
import { indexLifecycleTab } from '../public/extend_index_management/components/index_lifecycle_summary';
|
||||
import { Index } from '@kbn/index-management-plugin/common';
|
||||
|
||||
const { httpSetup } = init();
|
||||
|
||||
|
@ -113,6 +113,7 @@ const indexWithLifecycleError: Index = {
|
|||
},
|
||||
phase_execution: {
|
||||
policy: 'testy',
|
||||
// @ts-expect-error ILM type is incorrect https://github.com/elastic/elasticsearch-specification/issues/2326
|
||||
phase_definition: { min_age: '0s', actions: { rollover: { max_size: '1gb' } } },
|
||||
version: 1,
|
||||
modified_date_in_millis: 1544031699844,
|
||||
|
@ -243,29 +244,31 @@ describe('extend index management', () => {
|
|||
});
|
||||
|
||||
describe('ilm summary extension', () => {
|
||||
test('should render null when index has no index lifecycle policy', () => {
|
||||
const extension = (
|
||||
<IndexLifecycleSummary index={indexWithoutLifecyclePolicy} getUrlForApp={getUrlForApp} />
|
||||
);
|
||||
const rendered = mountWithIntl(extension);
|
||||
expect(rendered.isEmptyRender()).toBeTruthy();
|
||||
const IlmComponent = indexLifecycleTab.renderTabContent;
|
||||
test('should not render the tab when index has no index lifecycle policy', () => {
|
||||
const shouldRenderTab =
|
||||
indexLifecycleTab.shouldRenderTab &&
|
||||
indexLifecycleTab.shouldRenderTab({
|
||||
index: indexWithoutLifecyclePolicy,
|
||||
});
|
||||
expect(shouldRenderTab).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should return extension when index has lifecycle policy', () => {
|
||||
const extension = (
|
||||
<IndexLifecycleSummary index={indexWithLifecyclePolicy} getUrlForApp={getUrlForApp} />
|
||||
const ilmContent = (
|
||||
<IlmComponent index={indexWithLifecyclePolicy} getUrlForApp={getUrlForApp} />
|
||||
);
|
||||
expect(extension).toBeDefined();
|
||||
const rendered = mountWithIntl(extension);
|
||||
expect(ilmContent).toBeDefined();
|
||||
const rendered = mountWithIntl(ilmContent);
|
||||
expect(rendered.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should return extension when index has lifecycle error', () => {
|
||||
const extension = (
|
||||
<IndexLifecycleSummary index={indexWithLifecycleError} getUrlForApp={getUrlForApp} />
|
||||
const ilmContent = (
|
||||
<IlmComponent index={indexWithLifecycleError} getUrlForApp={getUrlForApp} />
|
||||
);
|
||||
expect(extension).toBeDefined();
|
||||
const rendered = mountWithIntl(extension);
|
||||
expect(ilmContent).toBeDefined();
|
||||
const rendered = mountWithIntl(ilmContent);
|
||||
expect(rendered.render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Index as IndexInterface } from '@kbn/index-management-plugin/common/types';
|
||||
|
||||
export type Phase = keyof Phases;
|
||||
|
||||
export type PhaseWithAllocation = 'warm' | 'cold';
|
||||
|
@ -244,7 +242,3 @@ export interface IndexLifecyclePolicy {
|
|||
};
|
||||
step_time_millis?: number;
|
||||
}
|
||||
|
||||
export interface Index extends IndexInterface {
|
||||
ilm: IndexLifecyclePolicy;
|
||||
}
|
||||
|
|
|
@ -25,10 +25,11 @@ import {
|
|||
EuiModalHeaderTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { Index } from '@kbn/index-management-plugin/common';
|
||||
import { loadPolicies, addLifecyclePolicyToIndex } from '../../application/services/api';
|
||||
import { showApiError } from '../../application/services/api_errors';
|
||||
import { toasts } from '../../application/services/notification';
|
||||
import { Index, PolicyFromES } from '../../../common/types';
|
||||
import { PolicyFromES } from '../../../common/types';
|
||||
|
||||
interface Props {
|
||||
indexName: string;
|
||||
|
|
|
@ -25,10 +25,12 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { ApplicationStart } from '@kbn/core/public';
|
||||
import { Index } from '@kbn/index-management-plugin/common';
|
||||
import { IndexDetailsTab } from '@kbn/index-management-plugin/common/constants';
|
||||
import { IlmExplainLifecycleLifecycleExplainManaged } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { getPolicyEditPath } from '../../application/services/navigation';
|
||||
import { Index, IndexLifecyclePolicy } from '../../../common/types';
|
||||
|
||||
const getHeaders = (): Array<[keyof IndexLifecyclePolicy, string]> => {
|
||||
const getHeaders = (): Array<[keyof IlmExplainLifecycleLifecycleExplainManaged, string]> => {
|
||||
return [
|
||||
[
|
||||
'policy',
|
||||
|
@ -85,7 +87,9 @@ interface Props {
|
|||
|
||||
export const IndexLifecycleSummary: FunctionComponent<Props> = ({ index, getUrlForApp }) => {
|
||||
const [showPhaseExecutionPopover, setShowPhaseExecutionPopover] = useState<boolean>(false);
|
||||
const { ilm } = index;
|
||||
const { ilm: ilmData } = index;
|
||||
// only ILM managed indices render the ILM tab
|
||||
const ilm = ilmData as IlmExplainLifecycleLifecycleExplainManaged;
|
||||
|
||||
const togglePhaseExecutionPopover = () => {
|
||||
setShowPhaseExecutionPopover(!showPhaseExecutionPopover);
|
||||
|
@ -144,15 +148,15 @@ export const IndexLifecycleSummary: FunctionComponent<Props> = ({ index, getUrlF
|
|||
right: [],
|
||||
};
|
||||
headers.forEach(([fieldName, label], arrayIndex) => {
|
||||
const value: any = ilm[fieldName];
|
||||
const value = ilm[fieldName];
|
||||
let content;
|
||||
if (fieldName === 'action_time_millis') {
|
||||
content = moment(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
content = moment(value as string).format('YYYY-MM-DD HH:mm:ss');
|
||||
} else if (fieldName === 'policy') {
|
||||
content = (
|
||||
<EuiLink
|
||||
href={getUrlForApp('management', {
|
||||
path: `data/index_lifecycle_management/${getPolicyEditPath(value)}`,
|
||||
path: `data/index_lifecycle_management/${getPolicyEditPath(value as string)}`,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
|
@ -184,9 +188,6 @@ export const IndexLifecycleSummary: FunctionComponent<Props> = ({ index, getUrlF
|
|||
return rows;
|
||||
};
|
||||
|
||||
if (!ilm.managed) {
|
||||
return null;
|
||||
}
|
||||
const { left, right } = buildRows();
|
||||
return (
|
||||
<>
|
||||
|
@ -243,3 +244,18 @@ export const IndexLifecycleSummary: FunctionComponent<Props> = ({ index, getUrlF
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const indexLifecycleTab: IndexDetailsTab = {
|
||||
id: 'ilm',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Index lifecycle"
|
||||
id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.tabHeaderLabel"
|
||||
/>
|
||||
),
|
||||
order: 50,
|
||||
renderTabContent: IndexLifecycleSummary,
|
||||
shouldRenderTab: ({ index }) => {
|
||||
return !!index.ilm && index.ilm.managed;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -11,20 +11,19 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiSearchBar } from '@elastic/eui';
|
||||
import { ApplicationStart } from '@kbn/core/public';
|
||||
|
||||
import { IndexManagementPluginSetup } from '@kbn/index-management-plugin/public';
|
||||
import { Index, IndexManagementPluginSetup } from '@kbn/index-management-plugin/public';
|
||||
|
||||
import { retryLifecycleForIndex } from '../application/services/api';
|
||||
import { IndexLifecycleSummary } from './components/index_lifecycle_summary';
|
||||
import { indexLifecycleTab } from './components/index_lifecycle_summary';
|
||||
|
||||
import { AddLifecyclePolicyConfirmModal } from './components/add_lifecycle_confirm_modal';
|
||||
import { RemoveLifecyclePolicyConfirmModal } from './components/remove_lifecycle_confirm_modal';
|
||||
import { Index } from '../../common/types';
|
||||
|
||||
const stepPath = 'ilm.step';
|
||||
|
||||
export const retryLifecycleActionExtension = ({ indices }: { indices: Index[] }) => {
|
||||
const allHaveErrors = every(indices, (index) => {
|
||||
return index.ilm && index.ilm.failed_step;
|
||||
return index.ilm?.managed && index.ilm.failed_step;
|
||||
});
|
||||
if (!allHaveErrors) {
|
||||
return null;
|
||||
|
@ -224,6 +223,7 @@ export const addAllExtensions = (
|
|||
extensionsService.addAction(addLifecyclePolicyActionExtension);
|
||||
|
||||
extensionsService.addBanner(ilmBannerExtension);
|
||||
extensionsService.addSummary(IndexLifecycleSummary);
|
||||
extensionsService.addFilter(ilmFilterExtension);
|
||||
|
||||
extensionsService.addIndexDetailsTab(indexLifecycleTab);
|
||||
};
|
||||
|
|
|
@ -9,9 +9,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import { CoreSetup, Plugin, Logger, PluginInitializerContext } from '@kbn/core/server';
|
||||
import { IScopedClusterClient } from '@kbn/core/server';
|
||||
|
||||
import { Index as IndexWithoutIlm } from '@kbn/index-management-plugin/common/types';
|
||||
import { Index } from '@kbn/index-management-plugin/common/types';
|
||||
import { PLUGIN } from '../common/constants';
|
||||
import { Index } from '../common/types';
|
||||
import { Dependencies } from './types';
|
||||
import { registerApiRoutes } from './routes';
|
||||
import { License } from './services';
|
||||
|
@ -19,7 +18,7 @@ import { IndexLifecycleManagementConfig } from './config';
|
|||
import { handleEsError } from './shared_imports';
|
||||
|
||||
const indexLifecycleDataEnricher = async (
|
||||
indicesList: IndexWithoutIlm[],
|
||||
indicesList: Index[],
|
||||
client: IScopedClusterClient
|
||||
): Promise<Index[]> => {
|
||||
if (!indicesList || !indicesList.length) {
|
||||
|
@ -29,8 +28,7 @@ const indexLifecycleDataEnricher = async (
|
|||
const { indices: ilmIndicesData } = await client.asCurrentUser.ilm.explainLifecycle({
|
||||
index: '*',
|
||||
});
|
||||
// @ts-expect-error IndexLifecyclePolicy is not compatible with IlmExplainLifecycleResponse
|
||||
return indicesList.map((index: IndexWithoutIlm) => {
|
||||
return indicesList.map((index: Index) => {
|
||||
return {
|
||||
...index,
|
||||
ilm: { ...(ilmIndicesData[index.name] || {}) },
|
||||
|
|
|
@ -1,4 +1,47 @@
|
|||
# Index Management UI
|
||||
## Extensions service
|
||||
This service is exposed from the Index Management setup contract and can be used to add content to the indices list and the index details page.
|
||||
### Extensions to the indices list
|
||||
- `addBanner(banner: any)`: adds a banner on top of the indices list, for example when some indices run into an ILM issue
|
||||
- `addFilter(filter: any)`: adds a filter to the indices list, for example to filter indices managed by ILM
|
||||
- `addToggle(toggle: any)`: adds a toggle to the indices list, for example to display hidden indices
|
||||
|
||||
#### Extensions to the indices list and the index details page
|
||||
- `addAction(action: any)`: adds an option to the "manage index" menu, for example to add an ILM policy to the index
|
||||
- `addBadge(badge: any)`: adds a badge to the index name, for example to indicate frozen, rollup or follower indices
|
||||
|
||||
#### Extensions to the index details page
|
||||
- `addIndexDetailsTab(tab: IndexDetailsTab)`: adds a tab to the index details page. The tab has the following interface:
|
||||
|
||||
```ts
|
||||
interface IndexDetailsTab {
|
||||
// a unique key to identify the tab
|
||||
id: IndexDetailsTabId;
|
||||
// a text that is displayed on the tab label, usually a Formatted message component
|
||||
name: ReactNode;
|
||||
// a function that renders the content of the tab
|
||||
renderTabContent: (args: {
|
||||
index: Index;
|
||||
getUrlForApp: ApplicationStart['getUrlForApp'];
|
||||
}) => ReturnType<FunctionComponent>;
|
||||
// a number to specify the order of the tabs
|
||||
order: number;
|
||||
// an optional function to return a boolean for when to render the tab
|
||||
// if omitted, the tab is always rendered
|
||||
shouldRenderTab?: (args: { index: Index }) => boolean;
|
||||
}
|
||||
```
|
||||
|
||||
An example of adding an ILM tab can be found in [this file](https://github.com/elastic/kibana/blob/main/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx#L250).
|
||||
|
||||
- `setIndexOverviewContent(content: IndexOverviewContent)`: replaces the default content in the overview tab (code block describing adding documents to the index) with the custom content. The custom content has the following interface:
|
||||
```ts
|
||||
interface IndexOverviewContent {
|
||||
renderContent: (args: {
|
||||
index: Index;
|
||||
getUrlForApp: ApplicationStart['getUrlForApp'];
|
||||
}) => ReturnType<FunctionComponent>;
|
||||
```
|
||||
|
||||
## Indices tab
|
||||
|
||||
|
|
|
@ -10,15 +10,6 @@ import { act } from 'react-dom/test-utils';
|
|||
import { setupEnvironment, nextTick } from '../helpers';
|
||||
import { HomeTestBed, setup } from './home.helpers';
|
||||
|
||||
/**
|
||||
* The below import is required to avoid a console error warn from the "brace" package
|
||||
* console.warn ../node_modules/brace/index.js:3999
|
||||
Could not load worker ReferenceError: Worker is not defined
|
||||
at createWorker (/<path-to-repo>/node_modules/brace/index.js:17992:5)
|
||||
*/
|
||||
import { stubWebWorker } from '@kbn/test-jest-helpers';
|
||||
stubWebWorker();
|
||||
|
||||
describe('<IndexManagementHome />', () => {
|
||||
const { httpSetup, httpRequestsMockHelpers } = setupEnvironment();
|
||||
let testBed: HomeTestBed;
|
||||
|
|
|
@ -135,6 +135,10 @@ describe('<IndexManagementHome />', () => {
|
|||
it('navigates to the index details page when the index name is clicked', async () => {
|
||||
const indexName = 'testIndex';
|
||||
httpRequestsMockHelpers.setLoadIndicesResponse([createNonDataStreamIndex(indexName)]);
|
||||
httpRequestsMockHelpers.setLoadIndexDetailsResponse(
|
||||
indexName,
|
||||
createNonDataStreamIndex(indexName)
|
||||
);
|
||||
|
||||
testBed = await setup(httpSetup, {
|
||||
history: createMemoryHistory(),
|
||||
|
@ -150,6 +154,10 @@ describe('<IndexManagementHome />', () => {
|
|||
it('index page works with % character in index name', async () => {
|
||||
const indexName = 'test%';
|
||||
httpRequestsMockHelpers.setLoadIndicesResponse([createNonDataStreamIndex(indexName)]);
|
||||
httpRequestsMockHelpers.setLoadIndexDetailsResponse(
|
||||
encodeURIComponent(indexName),
|
||||
createNonDataStreamIndex(indexName)
|
||||
);
|
||||
|
||||
testBed = await setup(httpSetup);
|
||||
const { component, actions } = testBed;
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { IndexDetailsTabIds } from '../../../common/constants';
|
||||
import { IndexDetailsTabId } from '../../../common/constants';
|
||||
import { IndexDetailsPage } from '../../../public/application/sections/home/index_list/details_page';
|
||||
import { WithAppDependencies } from '../helpers';
|
||||
import { testIndexName } from './mocks';
|
||||
|
@ -35,7 +35,7 @@ export interface IndexDetailsPageTestBed extends TestBed {
|
|||
routerMock: typeof reactRouterMock;
|
||||
actions: {
|
||||
getHeader: () => string;
|
||||
clickIndexDetailsTab: (tab: IndexDetailsTabIds) => Promise<void>;
|
||||
clickIndexDetailsTab: (tab: IndexDetailsTabId) => Promise<void>;
|
||||
getIndexDetailsTabs: () => string[];
|
||||
getActiveTabContent: () => string;
|
||||
mappings: {
|
||||
|
@ -88,7 +88,6 @@ export interface IndexDetailsPageTestBed extends TestBed {
|
|||
getDataStreamDetailsContent: () => string;
|
||||
reloadDataStreamDetails: () => Promise<void>;
|
||||
addDocCodeBlockExists: () => boolean;
|
||||
extensionSummaryExists: (index: number) => boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -127,7 +126,7 @@ export const setup = async ({
|
|||
return component.find('[data-test-subj="indexDetailsHeader"] h1').text();
|
||||
};
|
||||
|
||||
const clickIndexDetailsTab = async (tab: IndexDetailsTabIds) => {
|
||||
const clickIndexDetailsTab = async (tab: IndexDetailsTabId) => {
|
||||
await act(async () => {
|
||||
find(`indexDetailsTab-${tab}`).simulate('click');
|
||||
});
|
||||
|
@ -178,9 +177,6 @@ export const setup = async ({
|
|||
addDocCodeBlockExists: () => {
|
||||
return exists('codeBlockControlsPanel');
|
||||
},
|
||||
extensionSummaryExists: (index: number) => {
|
||||
return exists(`extensionsSummary-${index}`);
|
||||
},
|
||||
};
|
||||
|
||||
const mappings = {
|
||||
|
|
|
@ -11,11 +11,7 @@ import { act } from 'react-dom/test-utils';
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
IndexDetailsSection,
|
||||
IndexDetailsTab,
|
||||
IndexDetailsTabIds,
|
||||
} from '../../../common/constants';
|
||||
import { IndexDetailsSection, IndexDetailsTab, IndexDetailsTabId } from '../../../common/constants';
|
||||
import { API_BASE_PATH, Index, INTERNAL_API_BASE_PATH } from '../../../common';
|
||||
|
||||
import {
|
||||
|
@ -399,57 +395,28 @@ describe('<IndexDetailsPage />', () => {
|
|||
expect(testBed.actions.overview.addDocCodeBlockExists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('extension service summary', () => {
|
||||
it('renders all summaries added to the extension service', async () => {
|
||||
describe('extension service overview content', () => {
|
||||
it('renders the content instead of the default code block', async () => {
|
||||
const extensionsServiceOverview = 'Test content via extensions service';
|
||||
await act(async () => {
|
||||
testBed = await setup({
|
||||
httpSetup,
|
||||
dependencies: {
|
||||
services: {
|
||||
extensionsService: {
|
||||
summaries: [() => <span>test</span>, () => <span>test2</span>],
|
||||
_indexOverviewContent: {
|
||||
renderContent: () => extensionsServiceOverview,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
testBed.component.update();
|
||||
expect(testBed.actions.overview.extensionSummaryExists(0)).toBe(true);
|
||||
expect(testBed.actions.overview.extensionSummaryExists(1)).toBe(true);
|
||||
});
|
||||
|
||||
it(`doesn't render empty panels if the summary renders null`, async () => {
|
||||
await act(async () => {
|
||||
testBed = await setup({
|
||||
httpSetup,
|
||||
dependencies: {
|
||||
services: {
|
||||
extensionsService: {
|
||||
summaries: [() => null],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
testBed.component.update();
|
||||
expect(testBed.actions.overview.extensionSummaryExists(0)).toBe(false);
|
||||
});
|
||||
|
||||
it(`doesn't render anything when no summaries added to the extension service`, async () => {
|
||||
await act(async () => {
|
||||
testBed = await setup({
|
||||
httpSetup,
|
||||
dependencies: {
|
||||
services: {
|
||||
extensionsService: {
|
||||
summaries: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
testBed.component.update();
|
||||
expect(testBed.actions.overview.extensionSummaryExists(0)).toBe(false);
|
||||
expect(testBed.actions.overview.addDocCodeBlockExists()).toBe(false);
|
||||
const content = testBed.actions.getActiveTabContent();
|
||||
expect(content).toContain(extensionsServiceOverview);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -851,8 +818,9 @@ describe('<IndexDetailsPage />', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extension service tabs', () => {
|
||||
const testTabId = 'testTab' as IndexDetailsTabIds;
|
||||
const testTabId = 'testTab' as IndexDetailsTabId;
|
||||
const testContent = 'Test content';
|
||||
const additionalTab: IndexDetailsTab = {
|
||||
id: testTabId,
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { FunctionComponent, ReactNode } from 'react';
|
||||
import { ApplicationStart } from '@kbn/core-application-browser';
|
||||
import { Index } from '../types';
|
||||
|
||||
export enum Section {
|
||||
|
@ -23,15 +24,21 @@ export enum IndexDetailsSection {
|
|||
Stats = 'stats',
|
||||
}
|
||||
|
||||
export type IndexDetailsTabIds = IndexDetailsSection | string;
|
||||
export type IndexDetailsTabId = IndexDetailsSection | string;
|
||||
|
||||
export interface IndexDetailsTab {
|
||||
// a unique key to identify the tab
|
||||
id: IndexDetailsTabIds;
|
||||
id: IndexDetailsTabId;
|
||||
// a text that is displayed on the tab label, usually a Formatted message component
|
||||
name: ReactNode;
|
||||
// a function that renders the content of the tab
|
||||
renderTabContent: (indexName: string, index: Index) => ReactNode;
|
||||
renderTabContent: (args: {
|
||||
index: Index;
|
||||
getUrlForApp: ApplicationStart['getUrlForApp'];
|
||||
}) => ReturnType<FunctionComponent>;
|
||||
// a number to specify the order of the tabs
|
||||
order: number;
|
||||
// an optional function to return a boolean for when to render the tab
|
||||
// if omitted, the tab is always rendered
|
||||
shouldRenderTab?: (args: { index: Index }) => boolean;
|
||||
}
|
||||
|
|
|
@ -49,4 +49,4 @@ export {
|
|||
export { MAJOR_VERSION } from './plugin';
|
||||
|
||||
export { Section, IndexDetailsSection } from './home_sections';
|
||||
export type { IndexDetailsTab, IndexDetailsTabIds } from './home_sections';
|
||||
export type { IndexDetailsTab, IndexDetailsTabId } from './home_sections';
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import {
|
||||
HealthStatus,
|
||||
IlmExplainLifecycleLifecycleExplain,
|
||||
IndicesStatsIndexMetadataState,
|
||||
Uuid,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
|
@ -56,6 +57,7 @@ export interface IndexSettings {
|
|||
analysis?: AnalysisModule;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface Index {
|
||||
name: string;
|
||||
primary?: number | string;
|
||||
|
@ -67,10 +69,7 @@ export interface Index {
|
|||
|
||||
// The types below are added by extension services if corresponding plugins are enabled (ILM, Rollup, CCR)
|
||||
isRollupIndex?: boolean;
|
||||
ilm?: {
|
||||
index: string;
|
||||
managed: boolean;
|
||||
};
|
||||
ilm?: IlmExplainLifecycleLifecycleExplain;
|
||||
isFollowerIndex?: boolean;
|
||||
|
||||
// The types from here below represent information returned from the index stats API;
|
||||
|
|
|
@ -6,142 +6,29 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState, FunctionComponent } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiPageHeader,
|
||||
EuiSpacer,
|
||||
EuiPageHeaderProps,
|
||||
EuiPageSection,
|
||||
EuiButton,
|
||||
EuiPageTemplate,
|
||||
EuiText,
|
||||
EuiCode,
|
||||
} from '@elastic/eui';
|
||||
import { EuiPageTemplate, EuiText, EuiCode } from '@elastic/eui';
|
||||
import { SectionLoading } from '@kbn/es-ui-shared-plugin/public';
|
||||
|
||||
import {
|
||||
Section,
|
||||
IndexDetailsSection,
|
||||
IndexDetailsTab,
|
||||
IndexDetailsTabIds,
|
||||
} from '../../../../../../common/constants';
|
||||
import { getIndexDetailsLink } from '../../../../services/routing';
|
||||
import { IndexDetailsSection, IndexDetailsTabId } from '../../../../../../common/constants';
|
||||
import { Index } from '../../../../../../common';
|
||||
import { INDEX_OPEN } from '../../../../../../common/constants';
|
||||
import { Error } from '../../../../../shared_imports';
|
||||
import { loadIndex } from '../../../../services';
|
||||
import { useAppContext } from '../../../../app_context';
|
||||
import { DiscoverLink } from '../../../../lib/discover_link';
|
||||
import { DetailsPageError } from './details_page_error';
|
||||
import { ManageIndexButton } from './manage_index_button';
|
||||
import { DetailsPageStats } from './details_page_stats';
|
||||
import { DetailsPageMappings } from './details_page_mappings';
|
||||
import { DetailsPageOverview } from './details_page_overview';
|
||||
import { DetailsPageSettings } from './details_page_settings';
|
||||
import { DetailsPageContent } from './details_page_content';
|
||||
|
||||
const defaultTabs: IndexDetailsTab[] = [
|
||||
{
|
||||
id: IndexDetailsSection.Overview,
|
||||
name: (
|
||||
<FormattedMessage id="xpack.idxMgmt.indexDetails.overviewTitle" defaultMessage="Overview" />
|
||||
),
|
||||
renderTabContent: (indexName: string, index: Index) => (
|
||||
<DetailsPageOverview indexDetails={index} />
|
||||
),
|
||||
order: 10,
|
||||
},
|
||||
{
|
||||
id: IndexDetailsSection.Mappings,
|
||||
name: (
|
||||
<FormattedMessage id="xpack.idxMgmt.indexDetails.mappingsTitle" defaultMessage="Mappings" />
|
||||
),
|
||||
renderTabContent: (indexName: string, index: Index) => (
|
||||
<DetailsPageMappings indexName={indexName} />
|
||||
),
|
||||
order: 20,
|
||||
},
|
||||
{
|
||||
id: IndexDetailsSection.Settings,
|
||||
name: (
|
||||
<FormattedMessage id="xpack.idxMgmt.indexDetails.settingsTitle" defaultMessage="Settings" />
|
||||
),
|
||||
renderTabContent: (indexName: string, index: Index) => (
|
||||
<DetailsPageSettings indexName={indexName} isIndexOpen={index.status === INDEX_OPEN} />
|
||||
),
|
||||
order: 30,
|
||||
},
|
||||
];
|
||||
|
||||
const statsTab: IndexDetailsTab = {
|
||||
id: IndexDetailsSection.Stats,
|
||||
name: <FormattedMessage id="xpack.idxMgmt.indexDetails.statsTitle" defaultMessage="Statistics" />,
|
||||
renderTabContent: (indexName: string, index: Index) => (
|
||||
<DetailsPageStats indexName={indexName} isIndexOpen={index.status === INDEX_OPEN} />
|
||||
),
|
||||
order: 40,
|
||||
};
|
||||
|
||||
const getSelectedTabContent = ({
|
||||
tabs,
|
||||
indexDetailsSection,
|
||||
index,
|
||||
indexName,
|
||||
}: {
|
||||
tabs: IndexDetailsTab[];
|
||||
indexDetailsSection: IndexDetailsTabIds;
|
||||
index?: Index | null;
|
||||
indexName: string;
|
||||
}) => {
|
||||
// if there is no index data, the tab content won't be rendered, so it's safe to return null here
|
||||
if (!index) {
|
||||
return null;
|
||||
}
|
||||
const selectedTab = tabs.find((tab) => tab.id === indexDetailsSection);
|
||||
return selectedTab ? (
|
||||
selectedTab.renderTabContent(indexName, index)
|
||||
) : (
|
||||
<DetailsPageOverview indexDetails={index} />
|
||||
);
|
||||
};
|
||||
export const DetailsPage: FunctionComponent<
|
||||
RouteComponentProps<{ indexName: string; indexDetailsSection: IndexDetailsSection }>
|
||||
> = ({ location: { search }, history }) => {
|
||||
const {
|
||||
config,
|
||||
services: { extensionsService },
|
||||
} = useAppContext();
|
||||
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
|
||||
const indexName = queryParams.get('indexName') ?? '';
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
const sortedTabs = [...defaultTabs];
|
||||
if (config.enableIndexStats) {
|
||||
sortedTabs.push(statsTab);
|
||||
}
|
||||
sortedTabs.push(...extensionsService.indexDetailsTabs);
|
||||
|
||||
sortedTabs.sort((tabA, tabB) => {
|
||||
return tabA.order - tabB.order;
|
||||
});
|
||||
return sortedTabs;
|
||||
}, [config.enableIndexStats, extensionsService.indexDetailsTabs]);
|
||||
|
||||
const tabQueryParam = queryParams.get('tab') ?? IndexDetailsSection.Overview;
|
||||
let indexDetailsSection = IndexDetailsSection.Overview;
|
||||
if (tabs.map((tab) => tab.id).includes(tabQueryParam as IndexDetailsTabIds)) {
|
||||
indexDetailsSection = tabQueryParam as IndexDetailsSection;
|
||||
}
|
||||
const tab: IndexDetailsTabId = queryParams.get('tab') ?? IndexDetailsSection.Overview;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [index, setIndex] = useState<Index | null>();
|
||||
|
||||
const selectedTabContent = useMemo(() => {
|
||||
return getSelectedTabContent({ tabs, indexDetailsSection, index, indexName });
|
||||
}, [index, indexDetailsSection, indexName, tabs]);
|
||||
|
||||
const fetchIndexDetails = useCallback(async () => {
|
||||
if (indexName) {
|
||||
setIsLoading(true);
|
||||
|
@ -161,27 +48,6 @@ export const DetailsPage: FunctionComponent<
|
|||
fetchIndexDetails();
|
||||
}, [fetchIndexDetails]);
|
||||
|
||||
const onSectionChange = useCallback(
|
||||
(newSection: IndexDetailsTabIds) => {
|
||||
return history.push(getIndexDetailsLink(indexName, newSection));
|
||||
},
|
||||
[history, indexName]
|
||||
);
|
||||
|
||||
const navigateToAllIndices = useCallback(() => {
|
||||
history.push(`/${Section.Indices}`);
|
||||
}, [history]);
|
||||
|
||||
const headerTabs = useMemo<EuiPageHeaderProps['tabs']>(() => {
|
||||
return tabs.map((tab) => ({
|
||||
onClick: () => onSectionChange(tab.id),
|
||||
isSelected: tab.id === indexDetailsSection,
|
||||
key: tab.id,
|
||||
'data-test-subj': `indexDetailsTab-${tab.id}`,
|
||||
label: tab.name,
|
||||
}));
|
||||
}, [tabs, indexDetailsSection, onSectionChange]);
|
||||
|
||||
if (!indexName) {
|
||||
return (
|
||||
<EuiPageTemplate.EmptyPrompt
|
||||
|
@ -224,53 +90,11 @@ export const DetailsPage: FunctionComponent<
|
|||
return <DetailsPageError indexName={indexName} resendRequest={fetchIndexDetails} />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<EuiPageSection paddingSize="none">
|
||||
<EuiButton
|
||||
data-test-subj="indexDetailsBackToIndicesButton"
|
||||
color="text"
|
||||
iconType="arrowLeft"
|
||||
onClick={navigateToAllIndices}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.backToIndicesButtonLabel"
|
||||
defaultMessage="Back to all indices"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiPageSection>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiPageHeader
|
||||
data-test-subj="indexDetailsHeader"
|
||||
pageTitle={indexName}
|
||||
bottomBorder
|
||||
rightSideItems={[
|
||||
<DiscoverLink indexName={indexName} asButton={true} />,
|
||||
<ManageIndexButton
|
||||
indexName={indexName}
|
||||
indexDetails={index}
|
||||
reloadIndexDetails={fetchIndexDetails}
|
||||
navigateToAllIndices={navigateToAllIndices}
|
||||
/>,
|
||||
]}
|
||||
rightSideGroupProps={{
|
||||
wrap: false,
|
||||
}}
|
||||
responsive="reverse"
|
||||
tabs={headerTabs}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<div
|
||||
data-test-subj={`indexDetailsContent`}
|
||||
css={css`
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
{selectedTabContent}
|
||||
</div>
|
||||
</>
|
||||
<DetailsPageContent
|
||||
index={index}
|
||||
tab={tab}
|
||||
fetchIndexDetails={fetchIndexDetails}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* 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, { FunctionComponent, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiPageHeader,
|
||||
EuiPageHeaderProps,
|
||||
EuiPageSection,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Index } from '../../../../../../common';
|
||||
import {
|
||||
INDEX_OPEN,
|
||||
IndexDetailsSection,
|
||||
IndexDetailsTab,
|
||||
IndexDetailsTabId,
|
||||
Section,
|
||||
} from '../../../../../../common/constants';
|
||||
import { getIndexDetailsLink } from '../../../../services/routing';
|
||||
import { useAppContext } from '../../../../app_context';
|
||||
import { DiscoverLink } from '../../../../lib/discover_link';
|
||||
import { ManageIndexButton } from './manage_index_button';
|
||||
import { DetailsPageOverview } from './details_page_overview';
|
||||
import { DetailsPageMappings } from './details_page_mappings';
|
||||
import { DetailsPageSettings } from './details_page_settings';
|
||||
import { DetailsPageStats } from './details_page_stats';
|
||||
import { DetailsPageTab } from './details_page_tab';
|
||||
|
||||
const defaultTabs: IndexDetailsTab[] = [
|
||||
{
|
||||
id: IndexDetailsSection.Overview,
|
||||
name: (
|
||||
<FormattedMessage id="xpack.idxMgmt.indexDetails.overviewTitle" defaultMessage="Overview" />
|
||||
),
|
||||
renderTabContent: ({ index }) => <DetailsPageOverview indexDetails={index} />,
|
||||
order: 10,
|
||||
},
|
||||
{
|
||||
id: IndexDetailsSection.Mappings,
|
||||
name: (
|
||||
<FormattedMessage id="xpack.idxMgmt.indexDetails.mappingsTitle" defaultMessage="Mappings" />
|
||||
),
|
||||
renderTabContent: ({ index }) => <DetailsPageMappings indexName={index.name} />,
|
||||
order: 20,
|
||||
},
|
||||
{
|
||||
id: IndexDetailsSection.Settings,
|
||||
name: (
|
||||
<FormattedMessage id="xpack.idxMgmt.indexDetails.settingsTitle" defaultMessage="Settings" />
|
||||
),
|
||||
renderTabContent: ({ index }) => (
|
||||
<DetailsPageSettings indexName={index.name} isIndexOpen={index.status === INDEX_OPEN} />
|
||||
),
|
||||
order: 30,
|
||||
},
|
||||
];
|
||||
|
||||
const statsTab: IndexDetailsTab = {
|
||||
id: IndexDetailsSection.Stats,
|
||||
name: <FormattedMessage id="xpack.idxMgmt.indexDetails.statsTitle" defaultMessage="Statistics" />,
|
||||
renderTabContent: ({ index }) => (
|
||||
<DetailsPageStats indexName={index.name} isIndexOpen={index.status === INDEX_OPEN} />
|
||||
),
|
||||
order: 40,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
index: Index;
|
||||
tab: IndexDetailsTabId;
|
||||
history: RouteComponentProps['history'];
|
||||
fetchIndexDetails: () => Promise<void>;
|
||||
}
|
||||
export const DetailsPageContent: FunctionComponent<Props> = ({
|
||||
index,
|
||||
tab,
|
||||
history,
|
||||
fetchIndexDetails,
|
||||
}) => {
|
||||
const {
|
||||
config: { enableIndexStats },
|
||||
services: { extensionsService },
|
||||
} = useAppContext();
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
const sortedTabs = [...defaultTabs];
|
||||
if (enableIndexStats) {
|
||||
sortedTabs.push(statsTab);
|
||||
}
|
||||
extensionsService.indexDetailsTabs.forEach((dynamicTab) => {
|
||||
if (!dynamicTab.shouldRenderTab || dynamicTab.shouldRenderTab({ index })) {
|
||||
sortedTabs.push(dynamicTab);
|
||||
}
|
||||
});
|
||||
|
||||
sortedTabs.sort((tabA, tabB) => {
|
||||
return tabA.order - tabB.order;
|
||||
});
|
||||
return sortedTabs;
|
||||
}, [enableIndexStats, extensionsService.indexDetailsTabs, index]);
|
||||
|
||||
const onSectionChange = useCallback(
|
||||
(newSection: IndexDetailsTabId) => {
|
||||
return history.push(getIndexDetailsLink(index.name, newSection));
|
||||
},
|
||||
[history, index]
|
||||
);
|
||||
|
||||
const navigateToAllIndices = useCallback(() => {
|
||||
history.push(`/${Section.Indices}`);
|
||||
}, [history]);
|
||||
|
||||
const headerTabs = useMemo<EuiPageHeaderProps['tabs']>(() => {
|
||||
return tabs.map((tabConfig) => ({
|
||||
onClick: () => onSectionChange(tabConfig.id),
|
||||
isSelected: tabConfig.id === tab,
|
||||
key: tabConfig.id,
|
||||
'data-test-subj': `indexDetailsTab-${tabConfig.id}`,
|
||||
label: tabConfig.name,
|
||||
}));
|
||||
}, [tabs, tab, onSectionChange]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPageSection paddingSize="none">
|
||||
<EuiButton
|
||||
data-test-subj="indexDetailsBackToIndicesButton"
|
||||
color="text"
|
||||
iconType="arrowLeft"
|
||||
onClick={navigateToAllIndices}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.backToIndicesButtonLabel"
|
||||
defaultMessage="Back to all indices"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiPageSection>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiPageHeader
|
||||
data-test-subj="indexDetailsHeader"
|
||||
pageTitle={index.name}
|
||||
bottomBorder
|
||||
rightSideItems={[
|
||||
<DiscoverLink indexName={index.name} asButton={true} />,
|
||||
<ManageIndexButton
|
||||
index={index}
|
||||
reloadIndexDetails={fetchIndexDetails}
|
||||
navigateToAllIndices={navigateToAllIndices}
|
||||
/>,
|
||||
]}
|
||||
rightSideGroupProps={{
|
||||
wrap: false,
|
||||
}}
|
||||
responsive="reverse"
|
||||
tabs={headerTabs}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<div
|
||||
data-test-subj={`indexDetailsContent`}
|
||||
css={css`
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
<DetailsPageTab tabs={tabs} tab={tab} index={index} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -32,7 +32,6 @@ import { useAppContext } from '../../../../../app_context';
|
|||
import { documentationService } from '../../../../../services';
|
||||
import { breadcrumbService, IndexManagementBreadcrumb } from '../../../../../services/breadcrumbs';
|
||||
import { languageDefinitions, curlDefinition } from './languages';
|
||||
import { ExtensionsSummary } from './extensions_summary';
|
||||
import { DataStreamDetails } from './data_stream_details';
|
||||
import { StorageDetails } from './storage_details';
|
||||
import { AliasesDetails } from './aliases_details';
|
||||
|
@ -55,7 +54,11 @@ export const DetailsPageOverview: React.FunctionComponent<Props> = ({ indexDetai
|
|||
size,
|
||||
primary_size: primarySize,
|
||||
} = indexDetails;
|
||||
const { core, plugins } = useAppContext();
|
||||
const {
|
||||
core,
|
||||
plugins,
|
||||
services: { extensionsService },
|
||||
} = useAppContext();
|
||||
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs(IndexManagementBreadcrumb.indexDetailsOverview);
|
||||
|
@ -94,59 +97,64 @@ export const DetailsPageOverview: React.FunctionComponent<Props> = ({ indexDetai
|
|||
|
||||
<EuiSpacer />
|
||||
|
||||
<ExtensionsSummary index={indexDetails} />
|
||||
{extensionsService.indexOverviewContent ? (
|
||||
extensionsService.indexOverviewContent.renderContent({
|
||||
index: indexDetails,
|
||||
getUrlForApp: core.getUrlForApp,
|
||||
})
|
||||
) : (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h2>
|
||||
{i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.addMoreDataTitle', {
|
||||
defaultMessage: 'Add data to this index',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h2>
|
||||
{i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.addMoreDataTitle', {
|
||||
defaultMessage: 'Add data to this index',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiTextColor color="subdued">
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.overviewTab.addMoreDataDescription"
|
||||
defaultMessage="Use the bulk API to add data to your index. {docsLink}"
|
||||
values={{
|
||||
docsLink: (
|
||||
<EuiLink href={documentationService.getBulkApi()} target="_blank" external>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.overviewTab.addDocsLink"
|
||||
defaultMessage="Learn more."
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiTextColor color="subdued">
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.overviewTab.addMoreDataDescription"
|
||||
defaultMessage="Use the bulk API to add data to your index. {docsLink}"
|
||||
values={{
|
||||
docsLink: (
|
||||
<EuiLink href={documentationService.getBulkApi()} target="_blank" external>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.overviewTab.addDocsLink"
|
||||
defaultMessage="Learn more."
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<CodeBox
|
||||
languages={languageDefinitions}
|
||||
codeSnippet={getLanguageDefinitionCodeSnippet(
|
||||
selectedLanguage,
|
||||
'ingestDataIndex',
|
||||
codeSnippetArguments
|
||||
)}
|
||||
selectedLanguage={selectedLanguage}
|
||||
setSelectedLanguage={setSelectedLanguage}
|
||||
assetBasePath={core.http.basePath.prepend(`/plugins/indexManagement/assets`)}
|
||||
sharePlugin={plugins.share}
|
||||
application={core.application}
|
||||
consoleRequest={getConsoleRequest('ingestDataIndex', codeSnippetArguments)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CodeBox
|
||||
languages={languageDefinitions}
|
||||
codeSnippet={getLanguageDefinitionCodeSnippet(
|
||||
selectedLanguage,
|
||||
'ingestDataIndex',
|
||||
codeSnippetArguments
|
||||
)}
|
||||
selectedLanguage={selectedLanguage}
|
||||
setSelectedLanguage={setSelectedLanguage}
|
||||
assetBasePath={core.http.basePath.prepend(`/plugins/indexManagement/assets`)}
|
||||
sharePlugin={plugins.share}
|
||||
application={core.application}
|
||||
consoleRequest={getConsoleRequest('ingestDataIndex', codeSnippetArguments)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* 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, { Fragment, FunctionComponent } from 'react';
|
||||
import { EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { Index } from '../../../../../../../common';
|
||||
import { useAppContext } from '../../../../../app_context';
|
||||
|
||||
export const ExtensionsSummary: FunctionComponent<{ index: Index }> = ({ index }) => {
|
||||
const {
|
||||
services: { extensionsService },
|
||||
core: { getUrlForApp },
|
||||
} = useAppContext();
|
||||
const summaries = extensionsService.summaries.map((summaryExtension, i) => {
|
||||
const summary = summaryExtension({ index, getUrlForApp });
|
||||
|
||||
if (!summary) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Fragment key={`extensionsSummary-${i}`}>
|
||||
<EuiPanel data-test-subj={`extensionsSummary-${i}`} hasBorder={true}>
|
||||
{summary}
|
||||
</EuiPanel>
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
return <>{summaries}</>;
|
||||
};
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { Index } from '../../../../../../common';
|
||||
import { IndexDetailsTab, IndexDetailsTabId } from '../../../../../../common/constants';
|
||||
import { useAppContext } from '../../../../app_context';
|
||||
import { DetailsPageOverview } from './details_page_overview';
|
||||
|
||||
interface Props {
|
||||
tabs: IndexDetailsTab[];
|
||||
tab: IndexDetailsTabId;
|
||||
index: Index;
|
||||
}
|
||||
export const DetailsPageTab: FunctionComponent<Props> = ({ tabs, tab, index }) => {
|
||||
const selectedTab = tabs.find((tabConfig) => tabConfig.id === tab);
|
||||
const {
|
||||
core: { getUrlForApp },
|
||||
} = useAppContext();
|
||||
return selectedTab ? (
|
||||
selectedTab.renderTabContent({ index, getUrlForApp })
|
||||
) : (
|
||||
<DetailsPageOverview indexDetails={index} />
|
||||
);
|
||||
};
|
|
@ -41,21 +41,26 @@ const getIndexStatusByName = (
|
|||
};
|
||||
|
||||
interface Props {
|
||||
indexName: string;
|
||||
indexDetails: Index;
|
||||
index: Index;
|
||||
reloadIndexDetails: () => Promise<void>;
|
||||
navigateToAllIndices: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is a wrapper for the underlying "index actions context menu" that is currently used
|
||||
* in the indices list and works with redux. That is why all request helpers from the services are expecting
|
||||
* an array of indices, for example "deleteIndices(indexNames)".
|
||||
*
|
||||
*/
|
||||
export const ManageIndexButton: FunctionComponent<Props> = ({
|
||||
indexName,
|
||||
indexDetails,
|
||||
index,
|
||||
reloadIndexDetails,
|
||||
navigateToAllIndices,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// the variables are created to write the index actions in a way to later re-use for indices list without redux
|
||||
const indexNames = useMemo(() => [indexName], [indexName]);
|
||||
// the "index actions context menu" component is expecting an array of indices, the same as on the indices list
|
||||
const indexNames = useMemo(() => [index.name], [index]);
|
||||
|
||||
const reloadIndices = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
|
@ -63,7 +68,8 @@ export const ManageIndexButton: FunctionComponent<Props> = ({
|
|||
setIsLoading(false);
|
||||
}, [reloadIndexDetails]);
|
||||
|
||||
const indices = [indexDetails];
|
||||
// the "index actions context menu" component is expecting an array of indices, the same as on the indices list
|
||||
const indices = [index];
|
||||
const indexStatusByName = getIndexStatusByName(indexNames, indices);
|
||||
|
||||
const closeIndices = useCallback(async () => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { Section } from '../../../common/constants';
|
||||
import type { IndexDetailsTabIds } from '../../../common/constants';
|
||||
import type { IndexDetailsTabId } from '../../../common/constants';
|
||||
|
||||
export const getTemplateListLink = () => `/templates`;
|
||||
|
||||
|
@ -58,7 +58,7 @@ export const getDataStreamDetailsLink = (name: string) => {
|
|||
return encodeURI(`/data_streams/${encodeURIComponent(name)}`);
|
||||
};
|
||||
|
||||
export const getIndexDetailsLink = (indexName: string, tab?: IndexDetailsTabIds) => {
|
||||
export const getIndexDetailsLink = (indexName: string, tab?: IndexDetailsTabId) => {
|
||||
let link = `/${Section.Indices}/index_details?indexName=${encodeURIComponent(indexName)}`;
|
||||
if (tab) {
|
||||
link = `${link}&tab=${tab}`;
|
||||
|
|
|
@ -15,9 +15,9 @@ const createServiceMock = (): ExtensionsSetupMock => ({
|
|||
addBadge: jest.fn(),
|
||||
addBanner: jest.fn(),
|
||||
addFilter: jest.fn(),
|
||||
addSummary: jest.fn(),
|
||||
addToggle: jest.fn(),
|
||||
addIndexDetailsTab: jest.fn(),
|
||||
setIndexOverviewContent: jest.fn(),
|
||||
});
|
||||
|
||||
const createMock = () => {
|
||||
|
|
|
@ -6,21 +6,29 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { ApplicationStart } from '@kbn/core-application-browser';
|
||||
import type { IndexDetailsTab } from '../../common/constants';
|
||||
import { Index } from '..';
|
||||
|
||||
export interface IndexOverviewContent {
|
||||
renderContent: (args: {
|
||||
index: Index;
|
||||
getUrlForApp: ApplicationStart['getUrlForApp'];
|
||||
}) => ReturnType<FunctionComponent>;
|
||||
}
|
||||
|
||||
export interface ExtensionsSetup {
|
||||
addSummary(summary: any): void;
|
||||
addAction(action: any): void;
|
||||
addBanner(banner: any): void;
|
||||
addFilter(filter: any): void;
|
||||
addBadge(badge: any): void;
|
||||
addToggle(toggle: any): void;
|
||||
addIndexDetailsTab(tab: IndexDetailsTab): void;
|
||||
setIndexOverviewContent(content: IndexOverviewContent): void;
|
||||
}
|
||||
|
||||
export class ExtensionsService {
|
||||
private _indexDetailsTabs: IndexDetailsTab[] = [];
|
||||
private _summaries: any[] = [];
|
||||
private _actions: any[] = [];
|
||||
private _banners: any[] = [];
|
||||
private _filters: any[] = [];
|
||||
|
@ -37,6 +45,8 @@ export class ExtensionsService {
|
|||
},
|
||||
];
|
||||
private _toggles: any[] = [];
|
||||
private _indexDetailsTabs: IndexDetailsTab[] = [];
|
||||
private _indexOverviewContent: IndexOverviewContent | null = null;
|
||||
private service?: ExtensionsSetup;
|
||||
|
||||
public setup(): ExtensionsSetup {
|
||||
|
@ -45,18 +55,14 @@ export class ExtensionsService {
|
|||
addBadge: this.addBadge.bind(this),
|
||||
addBanner: this.addBanner.bind(this),
|
||||
addFilter: this.addFilter.bind(this),
|
||||
addSummary: this.addSummary.bind(this),
|
||||
addToggle: this.addToggle.bind(this),
|
||||
addIndexDetailsTab: this.addIndexDetailsTab.bind(this),
|
||||
setIndexOverviewContent: this.setIndexOverviewMainContent.bind(this),
|
||||
};
|
||||
|
||||
return this.service;
|
||||
}
|
||||
|
||||
private addSummary(summary: any) {
|
||||
this._summaries.push(summary);
|
||||
}
|
||||
|
||||
private addAction(action: any) {
|
||||
this._actions.push(action);
|
||||
}
|
||||
|
@ -81,8 +87,12 @@ export class ExtensionsService {
|
|||
this._indexDetailsTabs.push(tab);
|
||||
}
|
||||
|
||||
public get summaries() {
|
||||
return this._summaries;
|
||||
private setIndexOverviewMainContent(content: IndexOverviewContent) {
|
||||
if (this._indexOverviewContent) {
|
||||
throw new Error(`The content for index overview has already been set.`);
|
||||
} else {
|
||||
this._indexOverviewContent = content;
|
||||
}
|
||||
}
|
||||
|
||||
public get actions() {
|
||||
|
@ -108,4 +118,8 @@ export class ExtensionsService {
|
|||
public get indexDetailsTabs() {
|
||||
return this._indexDetailsTabs;
|
||||
}
|
||||
|
||||
public get indexOverviewContent() {
|
||||
return this._indexOverviewContent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
"@kbn/search-api-panels",
|
||||
"@kbn/cloud-plugin",
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/core-application-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue