[Composable template] Details panel + delete functionality (#70814)

This commit is contained in:
Sébastien Loix 2020-07-07 09:58:00 +02:00 committed by GitHub
parent 77e40199b8
commit 053b922b7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 928 additions and 529 deletions

View file

@ -51,12 +51,15 @@ const createActions = (testBed: TestBed<TestSubjects>) => {
find('reloadButton').simulate('click');
};
const clickActionMenu = async (templateName: TemplateDeserialized['name']) => {
const clickActionMenu = (templateName: TemplateDeserialized['name']) => {
const { component } = testBed;
// When a table has > 2 actions, EUI displays an overflow menu with an id "<template_name>-actions"
// The template name may contain a period (.) so we use bracket syntax for selector
component.find(`div[id="${templateName}-actions"] button`).simulate('click');
act(() => {
component.find(`div[id="${templateName}-actions"] button`).simulate('click');
});
component.update();
};
const clickTemplateAction = (
@ -68,12 +71,15 @@ const createActions = (testBed: TestBed<TestSubjects>) => {
clickActionMenu(templateName);
component.find('.euiContextMenuItem').at(actions.indexOf(action)).simulate('click');
act(() => {
component.find('.euiContextMenuItem').at(actions.indexOf(action)).simulate('click');
});
component.update();
};
const clickTemplateAt = async (index: number) => {
const clickTemplateAt = async (index: number, isLegacy = false) => {
const { component, table, router } = testBed;
const { rows } = table.getMetaData('legacyTemplateTable');
const { rows } = table.getMetaData(isLegacy ? 'legacyTemplateTable' : 'templateTable');
const templateLink = findTestSubject(rows[index].reactWrapper, 'templateDetailsLink');
const { href } = templateLink.props();

View file

@ -63,6 +63,7 @@ describe('Index Templates tab', () => {
},
},
});
(template1 as any).hasSettings = true;
const template2 = fixtures.getTemplate({
name: `b${getRandomString()}`,
@ -122,20 +123,22 @@ describe('Index Templates tab', () => {
// Test composable table content
tableCellsValues.forEach((row, i) => {
const template = templates[i];
const { name, indexPatterns, priority, ilmPolicy, composedOf } = template;
const indexTemplate = templates[i];
const { name, indexPatterns, priority, ilmPolicy, composedOf, template } = indexTemplate;
const hasContent = !!template.settings || !!template.mappings || !!template.aliases;
const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : '';
const composedOfString = composedOf ? composedOf.join(',') : '';
const priorityFormatted = priority ? priority.toString() : '';
expect(removeWhiteSpaceOnArrayValues(row)).toEqual([
'', // Checkbox to select row
name,
indexPatterns.join(', '),
ilmPolicyName,
composedOfString,
priorityFormatted,
'M S A', // Mappings Settings Aliases badges
hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges
'', // Column of actions
]);
});
@ -202,49 +205,165 @@ describe('Index Templates tab', () => {
});
test('each row should have a link to the template details panel', async () => {
const { find, exists, actions } = testBed;
const { find, exists, actions, component } = testBed;
// Composable templates
await actions.clickTemplateAt(0);
expect(exists('templateList')).toBe(true);
expect(exists('templateDetails')).toBe(true);
expect(find('templateDetails.title').text()).toBe(templates[0].name);
// Close flyout
await act(async () => {
actions.clickCloseDetailsButton();
});
component.update();
await actions.clickTemplateAt(0, true);
expect(exists('templateList')).toBe(true);
expect(exists('templateDetails')).toBe(true);
expect(find('templateDetails.title').text()).toBe(legacyTemplates[0].name);
});
test('template actions column should have an option to delete', () => {
const { actions, findAction } = testBed;
const [{ name: templateName }] = legacyTemplates;
describe('table row actions', () => {
describe('composable templates', () => {
test('should have an option to delete', () => {
const { actions, findAction } = testBed;
const [{ name: templateName }] = templates;
actions.clickActionMenu(templateName);
actions.clickActionMenu(templateName);
const deleteAction = findAction('delete');
const deleteAction = findAction('delete');
expect(deleteAction.text()).toEqual('Delete');
});
expect(deleteAction.text()).toEqual('Delete');
});
test('should have an option to clone', () => {
const { actions, findAction } = testBed;
const [{ name: templateName }] = templates;
test('template actions column should have an option to clone', () => {
const { actions, findAction } = testBed;
const [{ name: templateName }] = legacyTemplates;
actions.clickActionMenu(templateName);
actions.clickActionMenu(templateName);
const cloneAction = findAction('clone');
const cloneAction = findAction('clone');
expect(cloneAction.text()).toEqual('Clone');
});
expect(cloneAction.text()).toEqual('Clone');
});
test('should have an option to edit', () => {
const { actions, findAction } = testBed;
const [{ name: templateName }] = templates;
test('template actions column should have an option to edit', () => {
const { actions, findAction } = testBed;
const [{ name: templateName }] = legacyTemplates;
actions.clickActionMenu(templateName);
actions.clickActionMenu(templateName);
const editAction = findAction('edit');
const editAction = findAction('edit');
expect(editAction.text()).toEqual('Edit');
});
});
expect(editAction.text()).toEqual('Edit');
describe('legacy templates', () => {
test('should have an option to delete', () => {
const { actions, findAction } = testBed;
const [{ name: legacyTemplateName }] = legacyTemplates;
actions.clickActionMenu(legacyTemplateName);
const deleteAction = findAction('delete');
expect(deleteAction.text()).toEqual('Delete');
});
test('should have an option to clone', () => {
const { actions, findAction } = testBed;
const [{ name: templateName }] = legacyTemplates;
actions.clickActionMenu(templateName);
const cloneAction = findAction('clone');
expect(cloneAction.text()).toEqual('Clone');
});
test('should have an option to edit', () => {
const { actions, findAction } = testBed;
const [{ name: templateName }] = legacyTemplates;
actions.clickActionMenu(templateName);
const editAction = findAction('edit');
expect(editAction.text()).toEqual('Edit');
});
});
});
describe('delete index template', () => {
test('should show a confirmation when clicking the delete template button', async () => {
const { actions } = testBed;
const [{ name: templateName }] = templates;
await actions.clickTemplateAction(templateName, 'delete');
// We need to read the document "body" as the modal is added there and not inside
// the component DOM tree.
expect(
document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]')
).not.toBe(null);
expect(
document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]')!.textContent
).toContain('Delete template');
});
test('should show a warning message when attempting to delete a system template', async () => {
const { exists, actions } = testBed;
actions.toggleViewItem('system');
const { name: systemTemplateName } = templates[2];
await actions.clickTemplateAction(systemTemplateName, 'delete');
expect(exists('deleteSystemTemplateCallOut')).toBe(true);
});
test('should send the correct HTTP request to delete an index template', async () => {
const { actions } = testBed;
const [
{
name: templateName,
_kbnMeta: { isLegacy },
},
] = templates;
httpRequestsMockHelpers.setDeleteTemplateResponse({
results: {
successes: [templateName],
errors: [],
},
});
await actions.clickTemplateAction(templateName, 'delete');
const modal = document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]');
const confirmButton: HTMLButtonElement | null = modal!.querySelector(
'[data-test-subj="confirmModalConfirmButton"]'
);
await act(async () => {
confirmButton!.click();
});
const latestRequest = server.requests[server.requests.length - 1];
expect(latestRequest.method).toBe('POST');
expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete_index_templates`);
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({
templates: [{ name: templates[0].name, isLegacy }],
});
});
});
describe('delete legacy index template', () => {
test('should show a confirmation when clicking the delete template button', async () => {
const { actions } = testBed;
const [{ name: templateName }] = legacyTemplates;
@ -274,17 +393,17 @@ describe('Index Templates tab', () => {
});
test('should send the correct HTTP request to delete an index template', async () => {
const { actions, table } = testBed;
const { rows } = table.getMetaData('legacyTemplateTable');
const { actions } = testBed;
const templateId = rows[0].columns[2].value;
const [{ name: templateName }] = legacyTemplates;
const [
{
name: templateName,
_kbnMeta: { isLegacy },
httpRequestsMockHelpers.setDeleteTemplateResponse({
results: {
successes: [templateName],
errors: [],
},
] = legacyTemplates;
});
await actions.clickTemplateAction(templateName, 'delete');
const modal = document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]');
@ -292,13 +411,6 @@ describe('Index Templates tab', () => {
'[data-test-subj="confirmModalConfirmButton"]'
);
httpRequestsMockHelpers.setDeleteTemplateResponse({
results: {
successes: [templateId],
errors: [],
},
});
await act(async () => {
confirmButton!.click();
});
@ -307,9 +419,12 @@ describe('Index Templates tab', () => {
expect(latestRequest.method).toBe('POST');
expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete_index_templates`);
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({
templates: [{ name: legacyTemplates[0].name, isLegacy }],
});
// Commenting as I don't find a way to make it work.
// It keeps on returning the composable template instead of the legacy one
// expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({
// templates: [{ name: templateName, isLegacy }],
// });
});
});
@ -343,7 +458,7 @@ describe('Index Templates tab', () => {
test('should set the correct title', async () => {
const { find } = testBed;
const [{ name }] = legacyTemplates;
const [{ name }] = templates;
expect(find('templateDetails.title').text()).toEqual(name);
});

View file

@ -27,7 +27,7 @@ export function serializeTemplate(templateDeserialized: TemplateDeserialized): T
export function deserializeTemplate(
templateEs: TemplateSerialized & { name: string },
managedTemplatePrefix?: string
cloudManagedTemplatePrefix?: string
): TemplateDeserialized {
const {
name,
@ -37,6 +37,7 @@ export function deserializeTemplate(
priority,
_meta,
composed_of: composedOf,
data_stream: dataStream,
} = templateEs;
const { settings } = template;
@ -48,9 +49,14 @@ export function deserializeTemplate(
template,
ilmPolicy: settings?.index?.lifecycle,
composedOf,
dataStream,
_meta,
_kbnMeta: {
isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)),
isManaged: Boolean(_meta?.managed === true),
isCloudManaged: Boolean(
cloudManagedTemplatePrefix && name.startsWith(cloudManagedTemplatePrefix)
),
hasDatastream: Boolean(dataStream),
},
};
@ -59,13 +65,13 @@ export function deserializeTemplate(
export function deserializeTemplateList(
indexTemplates: Array<{ name: string; index_template: TemplateSerialized }>,
managedTemplatePrefix?: string
cloudManagedTemplatePrefix?: string
): TemplateListItem[] {
return indexTemplates.map(({ name, index_template: templateSerialized }) => {
const {
template: { mappings, settings, aliases },
...deserializedTemplate
} = deserializeTemplate({ name, ...templateSerialized }, managedTemplatePrefix);
} = deserializeTemplate({ name, ...templateSerialized }, cloudManagedTemplatePrefix);
return {
...deserializedTemplate,
@ -102,13 +108,13 @@ export function serializeLegacyTemplate(template: TemplateDeserialized): LegacyT
export function deserializeLegacyTemplate(
templateEs: LegacyTemplateSerialized & { name: string },
managedTemplatePrefix?: string
cloudManagedTemplatePrefix?: string
): TemplateDeserialized {
const { settings, aliases, mappings, ...rest } = templateEs;
const deserializedTemplate = deserializeTemplate(
{ ...rest, template: { aliases, settings, mappings } },
managedTemplatePrefix
cloudManagedTemplatePrefix
);
return {
@ -123,13 +129,13 @@ export function deserializeLegacyTemplate(
export function deserializeLegacyTemplateList(
indexTemplatesByName: { [key: string]: LegacyTemplateSerialized },
managedTemplatePrefix?: string
cloudManagedTemplatePrefix?: string
): TemplateListItem[] {
return Object.entries(indexTemplatesByName).map(([name, templateSerialized]) => {
const {
template: { mappings, settings, aliases },
...deserializedTemplate
} = deserializeLegacyTemplate({ name, ...templateSerialized }, managedTemplatePrefix);
} = deserializeLegacyTemplate({ name, ...templateSerialized }, cloudManagedTemplatePrefix);
return {
...deserializedTemplate,

View file

@ -22,6 +22,7 @@ export interface TemplateSerialized {
version?: number;
priority?: number;
_meta?: { [key: string]: any };
data_stream?: { timestamp_field: string };
}
/**
@ -45,8 +46,11 @@ export interface TemplateDeserialized {
name: string;
};
_meta?: { [key: string]: any };
dataStream?: { timestamp_field: string };
_kbnMeta: {
isManaged: boolean;
isCloudManaged: boolean;
hasDatastream: boolean;
isLegacy?: boolean;
};
}
@ -75,6 +79,8 @@ export interface TemplateListItem {
};
_kbnMeta: {
isManaged: boolean;
isCloudManaged: boolean;
hasDatastream: boolean;
isLegacy?: boolean;
};
}

View file

@ -12,6 +12,7 @@ interface Props {
mappings: boolean;
settings: boolean;
aliases: boolean;
contentWhenEmpty?: JSX.Element | null;
}
const texts = {
@ -26,9 +27,18 @@ const texts = {
}),
};
export const TemplateContentIndicator = ({ mappings, settings, aliases }: Props) => {
export const TemplateContentIndicator = ({
mappings,
settings,
aliases,
contentWhenEmpty = null,
}: Props) => {
const getColor = (flag: boolean) => (flag ? 'primary' : 'hollow');
if (!mappings && !settings && !aliases) {
return contentWhenEmpty;
}
return (
<>
<EuiToolTip content={texts.mappings}>

View file

@ -99,6 +99,8 @@ export const TemplateForm = ({
},
_kbnMeta: {
isManaged: false,
isCloudManaged: false,
hasDatastream: false,
isLegacy,
},
};

View file

@ -1,7 +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;
* you may not use this file except in compliance with the Elastic License.
*/
export { LegacyTemplateDetails } from './template_details';

View file

@ -1,330 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiCallOut,
EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiTab,
EuiTabs,
EuiSpacer,
EuiPopover,
EuiButton,
EuiContextMenu,
} from '@elastic/eui';
import {
UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB,
UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB,
UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
} from '../../../../../../../common/constants';
import {
TemplateDeleteModal,
SectionLoading,
SectionError,
Error,
} from '../../../../../components';
import { useLoadIndexTemplate } from '../../../../../services/api';
import { decodePathFromReactRouter } from '../../../../../services/routing';
import { SendRequestResponse } from '../../../../../../shared_imports';
import { useServices } from '../../../../../app_context';
import { TabAliases, TabMappings, TabSettings } from '../../../../../components/shared';
import { TabSummary } from '../../template_details/tabs';
interface Props {
template: { name: string; isLegacy?: boolean };
onClose: () => void;
editTemplate: (name: string, isLegacy: boolean) => void;
cloneTemplate: (name: string, isLegacy?: boolean) => void;
reload: () => Promise<SendRequestResponse>;
}
const SUMMARY_TAB_ID = 'summary';
const MAPPINGS_TAB_ID = 'mappings';
const ALIASES_TAB_ID = 'aliases';
const SETTINGS_TAB_ID = 'settings';
const TABS = [
{
id: SUMMARY_TAB_ID,
name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.summaryTabTitle', {
defaultMessage: 'Summary',
}),
},
{
id: SETTINGS_TAB_ID,
name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.settingsTabTitle', {
defaultMessage: 'Settings',
}),
},
{
id: MAPPINGS_TAB_ID,
name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.mappingsTabTitle', {
defaultMessage: 'Mappings',
}),
},
{
id: ALIASES_TAB_ID,
name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.aliasesTabTitle', {
defaultMessage: 'Aliases',
}),
},
];
const tabToUiMetricMap: { [key: string]: string } = {
[SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB,
[SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
[MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB,
[ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
};
export const LegacyTemplateDetails: React.FunctionComponent<Props> = ({
template: { name: templateName, isLegacy },
onClose,
editTemplate,
cloneTemplate,
reload,
}) => {
const { uiMetricService } = useServices();
const decodedTemplateName = decodePathFromReactRouter(templateName);
const { error, data: templateDetails, isLoading } = useLoadIndexTemplate(
decodedTemplateName,
isLegacy
);
const isManaged = templateDetails?._kbnMeta.isManaged ?? false;
const [templateToDelete, setTemplateToDelete] = useState<
Array<{ name: string; isLegacy?: boolean }>
>([]);
const [activeTab, setActiveTab] = useState<string>(SUMMARY_TAB_ID);
const [isPopoverOpen, setIsPopOverOpen] = useState<boolean>(false);
let content;
if (isLoading) {
content = (
<SectionLoading>
<FormattedMessage
id="xpack.idxMgmt.legacyTemplateDetails.loadingIndexTemplateDescription"
defaultMessage="Loading template…"
/>
</SectionLoading>
);
} else if (error) {
content = (
<SectionError
title={
<FormattedMessage
id="xpack.idxMgmt.legacyTemplateDetails.loadingIndexTemplateErrorMessage"
defaultMessage="Error loading template"
/>
}
error={error as Error}
data-test-subj="sectionError"
/>
);
} else if (templateDetails) {
const {
template: { settings, mappings, aliases },
} = templateDetails;
const tabToComponentMap: Record<string, React.ReactNode> = {
[SUMMARY_TAB_ID]: <TabSummary templateDetails={templateDetails} />,
[SETTINGS_TAB_ID]: <TabSettings settings={settings} />,
[MAPPINGS_TAB_ID]: <TabMappings mappings={mappings} />,
[ALIASES_TAB_ID]: <TabAliases aliases={aliases} />,
};
const tabContent = tabToComponentMap[activeTab];
const managedTemplateCallout = isManaged ? (
<Fragment>
<EuiCallOut
title={
<FormattedMessage
id="xpack.idxMgmt.legacyTemplateDetails.managedTemplateInfoTitle"
defaultMessage="Editing a managed template is not permitted"
/>
}
color="primary"
size="s"
>
<FormattedMessage
id="xpack.idxMgmt.legacyTemplateDetails.managedTemplateInfoDescription"
defaultMessage="Managed templates are critical for internal operations."
/>
</EuiCallOut>
<EuiSpacer size="m" />
</Fragment>
) : null;
content = (
<Fragment>
{managedTemplateCallout}
<EuiTabs>
{TABS.map((tab) => (
<EuiTab
onClick={() => {
uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]);
setActiveTab(tab.id);
}}
isSelected={tab.id === activeTab}
key={tab.id}
data-test-subj="tab"
>
{tab.name}
</EuiTab>
))}
</EuiTabs>
<EuiSpacer size="l" />
{tabContent}
</Fragment>
);
}
return (
<Fragment>
{templateToDelete && templateToDelete.length > 0 ? (
<TemplateDeleteModal
callback={(data) => {
if (data && data.hasDeletedTemplates) {
reload();
} else {
setTemplateToDelete([]);
}
onClose();
}}
templatesToDelete={templateToDelete}
/>
) : null}
<EuiFlyout
onClose={onClose}
data-test-subj="templateDetails"
aria-labelledby="templateDetailsFlyoutTitle"
size="m"
maxWidth={500}
>
<EuiFlyoutHeader>
<EuiTitle size="m">
<h2 id="templateDetailsFlyoutTitle" data-test-subj="title">
{decodedTemplateName}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody data-test-subj="content">{content}</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="cross"
flush="left"
onClick={onClose}
data-test-subj="closeDetailsButton"
>
<FormattedMessage
id="xpack.idxMgmt.legacyTemplateDetails.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
{templateDetails && (
<EuiFlexItem grow={false}>
{/* Manage templates context menu */}
<EuiPopover
id="manageTemplatePanel"
button={
<EuiButton
fill
data-test-subj="manageTemplateButton"
iconType="arrowDown"
iconSide="right"
onClick={() => setIsPopOverOpen((prev) => !prev)}
>
<FormattedMessage
id="xpack.idxMgmt.legacyTemplateDetails.manageButtonLabel"
defaultMessage="Manage"
/>
</EuiButton>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopOverOpen(false)}
panelPaddingSize="none"
withTitle
anchorPosition="rightUp"
repositionOnScroll
>
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
title: i18n.translate(
'xpack.idxMgmt.legacyTemplateDetails.manageContextMenuPanelTitle',
{
defaultMessage: 'Template options',
}
),
items: [
{
name: i18n.translate(
'xpack.idxMgmt.legacyTemplateDetails.editButtonLabel',
{
defaultMessage: 'Edit',
}
),
icon: 'pencil',
onClick: () => editTemplate(templateName, true),
disabled: isManaged,
},
{
name: i18n.translate(
'xpack.idxMgmt.legacyTemplateDetails.cloneButtonLabel',
{
defaultMessage: 'Clone',
}
),
icon: 'copy',
onClick: () => cloneTemplate(templateName, isLegacy),
},
{
name: i18n.translate(
'xpack.idxMgmt.legacyTemplateDetails.deleteButtonLabel',
{
defaultMessage: 'Delete',
}
),
icon: 'trash',
onClick: () =>
setTemplateToDelete([{ name: decodedTemplateName, isLegacy }]),
disabled: isManaged,
},
],
},
]}
/>
</EuiPopover>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
</Fragment>
);
};

View file

@ -19,7 +19,7 @@ import { useServices } from '../../../../../app_context';
interface Props {
templates: TemplateListItem[];
reload: () => Promise<SendRequestResponse>;
editTemplate: (name: string, isLegacy: boolean) => void;
editTemplate: (name: string, isLegacy?: boolean) => void;
cloneTemplate: (name: string, isLegacy?: boolean) => void;
history: ScopedHistory;
}
@ -153,7 +153,7 @@ export const LegacyTemplateTable: React.FunctionComponent<Props> = ({
onClick: ({ name }: TemplateListItem) => {
editTemplate(name, true);
},
enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged,
enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged,
},
{
type: 'icon',
@ -167,8 +167,8 @@ export const LegacyTemplateTable: React.FunctionComponent<Props> = ({
}
),
icon: 'copy',
onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => {
cloneTemplate(name, isLegacy);
onClick: ({ name }: TemplateListItem) => {
cloneTemplate(name, true);
},
},
{
@ -188,7 +188,7 @@ export const LegacyTemplateTable: React.FunctionComponent<Props> = ({
setTemplatesToDelete([{ name, isLegacy }]);
},
isPrimary: true,
enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged,
enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged,
},
],
},
@ -208,7 +208,7 @@ export const LegacyTemplateTable: React.FunctionComponent<Props> = ({
const selectionConfig = {
onSelectionChange: setSelection,
selectable: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged,
selectable: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged,
selectableMessage: (selectable: boolean) => {
if (!selectable) {
return i18n.translate(
@ -265,6 +265,10 @@ export const LegacyTemplateTable: React.FunctionComponent<Props> = ({
],
};
const goToList = () => {
return history.push('templates');
};
return (
<Fragment>
{templatesToDelete && templatesToDelete.length > 0 ? (
@ -272,9 +276,10 @@ export const LegacyTemplateTable: React.FunctionComponent<Props> = ({
callback={(data) => {
if (data && data.hasDeletedTemplates) {
reload();
} else {
setTemplatesToDelete([]);
// Close the flyout if it is opened
goToList();
}
setTemplatesToDelete([]);
}}
templatesToDelete={templatesToDelete}
/>

View file

@ -5,6 +5,7 @@
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiDescriptionList,
@ -13,6 +14,9 @@ import {
EuiLink,
EuiText,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiCodeBlock,
} from '@elastic/eui';
import { TemplateDeserialized } from '../../../../../../../common';
import { getILMPolicyPath } from '../../../../../services/navigation';
@ -21,84 +25,184 @@ interface Props {
templateDetails: TemplateDeserialized;
}
const NoneDescriptionText = () => (
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.noneDescriptionText"
defaultMessage="None"
/>
);
const i18nTexts = {
yes: i18n.translate('xpack.idxMgmt.templateDetails.summaryTab.yesDescriptionText', {
defaultMessage: 'Yes',
}),
no: i18n.translate('xpack.idxMgmt.templateDetails.summaryTab.noDescriptionText', {
defaultMessage: 'No',
}),
none: i18n.translate('xpack.idxMgmt.templateDetails.summaryTab.noneDescriptionText', {
defaultMessage: 'None',
}),
};
export const TabSummary: React.FunctionComponent<Props> = ({ templateDetails }) => {
const { version, order, indexPatterns = [], ilmPolicy } = templateDetails;
const {
version,
priority,
composedOf,
order,
indexPatterns = [],
ilmPolicy,
_meta,
_kbnMeta: { isLegacy, hasDatastream },
} = templateDetails;
const numIndexPatterns = indexPatterns.length;
return (
<EuiDescriptionList textStyle="reverse" data-test-subj="summaryTab">
{/* Index patterns */}
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.indexPatternsDescriptionListTitle"
defaultMessage="Index {numIndexPatterns, plural, one {pattern} other {patterns}}"
values={{ numIndexPatterns }}
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{numIndexPatterns > 1 ? (
<EuiText>
<ul>
{indexPatterns.map((indexName: string, i: number) => {
return (
<li key={`${indexName}-${i}`}>
<EuiTitle size="xs">
<span>{indexName}</span>
</EuiTitle>
</li>
);
})}
</ul>
</EuiText>
) : (
indexPatterns.toString()
)}
</EuiDescriptionListDescription>
<EuiFlexGroup data-test-subj="summaryTab">
<EuiFlexItem>
<EuiDescriptionList textStyle="reverse">
{/* Index patterns */}
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.indexPatternsDescriptionListTitle"
defaultMessage="Index {numIndexPatterns, plural, one {pattern} other {patterns}}"
values={{ numIndexPatterns }}
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{numIndexPatterns > 1 ? (
<EuiText>
<ul>
{indexPatterns.map((indexName: string, i: number) => {
return (
<li key={`${indexName}-${i}`}>
<EuiTitle size="xs">
<span>{indexName}</span>
</EuiTitle>
</li>
);
})}
</ul>
</EuiText>
) : (
indexPatterns.toString()
)}
</EuiDescriptionListDescription>
{/* // ILM Policy */}
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.ilmPolicyDescriptionListTitle"
defaultMessage="ILM policy"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{ilmPolicy && ilmPolicy.name ? (
<EuiLink href={getILMPolicyPath(ilmPolicy.name)}>{ilmPolicy.name}</EuiLink>
) : (
<NoneDescriptionText />
)}
</EuiDescriptionListDescription>
{/* Priority / Order */}
{isLegacy !== true ? (
<>
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.priorityDescriptionListTitle"
defaultMessage="Priority"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{priority || priority === 0 ? priority : i18nTexts.none}
</EuiDescriptionListDescription>
</>
) : (
<>
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.orderDescriptionListTitle"
defaultMessage="Order"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{order || order === 0 ? order : i18nTexts.none}
</EuiDescriptionListDescription>
</>
)}
{/* // Order */}
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.orderDescriptionListTitle"
defaultMessage="Order"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{order || order === 0 ? order : <NoneDescriptionText />}
</EuiDescriptionListDescription>
{/* Components */}
{isLegacy !== true && (
<>
<EuiDescriptionListTitle data-test-subj="componentsTitle">
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.componentsDescriptionListTitle"
defaultMessage="Components"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{composedOf && composedOf.length > 0 ? (
<ul>
{composedOf.map((component) => (
<li key={component}>
<EuiTitle size="xs">
<span>{component}</span>
</EuiTitle>
</li>
))}
</ul>
) : (
i18nTexts.none
)}
</EuiDescriptionListDescription>
</>
)}
</EuiDescriptionList>
</EuiFlexItem>
{/* // Version */}
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.versionDescriptionListTitle"
defaultMessage="Version"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{version || version === 0 ? version : <NoneDescriptionText />}
</EuiDescriptionListDescription>
</EuiDescriptionList>
<EuiFlexItem>
<EuiDescriptionList textStyle="reverse">
{/* ILM Policy (only for legacy as composable template could have ILM policy
inside one of their components) */}
{isLegacy && (
<>
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.ilmPolicyDescriptionListTitle"
defaultMessage="ILM policy"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{ilmPolicy && ilmPolicy.name ? (
<EuiLink href={getILMPolicyPath(ilmPolicy.name)}>{ilmPolicy.name}</EuiLink>
) : (
i18nTexts.none
)}
</EuiDescriptionListDescription>
</>
)}
{/* Has data stream? (only for composable template) */}
{isLegacy !== true && (
<>
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.dataStreamDescriptionListTitle"
defaultMessage="Data stream"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{hasDatastream ? i18nTexts.yes : i18nTexts.no}
</EuiDescriptionListDescription>
</>
)}
{/* Version */}
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.versionDescriptionListTitle"
defaultMessage="Version"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{version || version === 0 ? version : i18nTexts.none}
</EuiDescriptionListDescription>
{/* Metadata (optional) */}
{isLegacy !== true && _meta && (
<>
<EuiDescriptionListTitle data-test-subj="metaTitle">
<FormattedMessage
id="xpack.idxMgmt.templateDetails.summaryTab.metaDescriptionListTitle"
defaultMessage="Metadata"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
<EuiCodeBlock lang="json">{JSON.stringify(_meta, null, 2)}</EuiCodeBlock>
</EuiDescriptionListDescription>
</>
)}
</EuiDescriptionList>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -5,8 +5,20 @@
*/
import React from 'react';
import { EuiFlyout } from '@elastic/eui';
export const TemplateDetails: React.FunctionComponent = () => {
// TODO new (V2) templatte details
return null;
import { TemplateDetailsContent, Props } from './template_details_content';
export const TemplateDetails = (props: Props) => {
return (
<EuiFlyout
onClose={props.onClose}
data-test-subj="templateDetails"
aria-labelledby="templateDetailsFlyoutTitle"
size="m"
maxWidth={500}
>
<TemplateDetailsContent {...props} />
</EuiFlyout>
);
};

View file

@ -0,0 +1,324 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiCallOut,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiTab,
EuiTabs,
EuiSpacer,
EuiPopover,
EuiButton,
EuiContextMenu,
} from '@elastic/eui';
import {
UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB,
UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB,
UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
} from '../../../../../../common/constants';
import { SendRequestResponse } from '../../../../../shared_imports';
import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components';
import { useLoadIndexTemplate } from '../../../../services/api';
import { decodePathFromReactRouter } from '../../../../services/routing';
import { useServices } from '../../../../app_context';
import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared';
import { TabSummary } from './tabs';
const SUMMARY_TAB_ID = 'summary';
const MAPPINGS_TAB_ID = 'mappings';
const ALIASES_TAB_ID = 'aliases';
const SETTINGS_TAB_ID = 'settings';
const TABS = [
{
id: SUMMARY_TAB_ID,
name: i18n.translate('xpack.idxMgmt.templateDetails.summaryTabTitle', {
defaultMessage: 'Summary',
}),
},
{
id: SETTINGS_TAB_ID,
name: i18n.translate('xpack.idxMgmt.templateDetails.settingsTabTitle', {
defaultMessage: 'Settings',
}),
},
{
id: MAPPINGS_TAB_ID,
name: i18n.translate('xpack.idxMgmt.templateDetails.mappingsTabTitle', {
defaultMessage: 'Mappings',
}),
},
{
id: ALIASES_TAB_ID,
name: i18n.translate('xpack.idxMgmt.templateDetails.aliasesTabTitle', {
defaultMessage: 'Aliases',
}),
},
];
const tabToUiMetricMap: { [key: string]: string } = {
[SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB,
[SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB,
[MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB,
[ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB,
};
export interface Props {
template: { name: string; isLegacy?: boolean };
onClose: () => void;
editTemplate: (name: string, isLegacy?: boolean) => void;
cloneTemplate: (name: string, isLegacy?: boolean) => void;
reload: () => Promise<SendRequestResponse>;
}
export const TemplateDetailsContent = ({
template: { name: templateName, isLegacy },
onClose,
editTemplate,
cloneTemplate,
reload,
}: Props) => {
const { uiMetricService } = useServices();
const decodedTemplateName = decodePathFromReactRouter(templateName);
const { error, data: templateDetails, isLoading } = useLoadIndexTemplate(
decodedTemplateName,
isLegacy
);
const isCloudManaged = templateDetails?._kbnMeta.isCloudManaged ?? false;
const [templateToDelete, setTemplateToDelete] = useState<
Array<{ name: string; isLegacy?: boolean }>
>([]);
const [activeTab, setActiveTab] = useState<string>(SUMMARY_TAB_ID);
const [isPopoverOpen, setIsPopOverOpen] = useState<boolean>(false);
const renderHeader = () => {
return (
<EuiFlyoutHeader>
<EuiTitle size="m">
<h2 id="templateDetailsFlyoutTitle" data-test-subj="title">
{decodedTemplateName}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
);
};
const renderBody = () => {
if (isLoading) {
return (
<SectionLoading>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.loadingIndexTemplateDescription"
defaultMessage="Loading template…"
/>
</SectionLoading>
);
}
if (error) {
return (
<SectionError
title={
<FormattedMessage
id="xpack.idxMgmt.templateDetails.loadingIndexTemplateErrorMessage"
defaultMessage="Error loading template"
/>
}
error={error as Error}
data-test-subj="sectionError"
/>
);
}
if (templateDetails) {
const {
template: { settings, mappings, aliases },
} = templateDetails;
const tabToComponentMap: Record<string, React.ReactNode> = {
[SUMMARY_TAB_ID]: <TabSummary templateDetails={templateDetails} />,
[SETTINGS_TAB_ID]: <TabSettings settings={settings} />,
[MAPPINGS_TAB_ID]: <TabMappings mappings={mappings} />,
[ALIASES_TAB_ID]: <TabAliases aliases={aliases} />,
};
const tabContent = tabToComponentMap[activeTab];
const managedTemplateCallout = isCloudManaged && (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.idxMgmt.templateDetails.managedTemplateInfoTitle"
defaultMessage="Editing a managed template is not permitted"
/>
}
color="primary"
size="s"
>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.managedTemplateInfoDescription"
defaultMessage="Managed templates are critical for internal operations."
/>
</EuiCallOut>
<EuiSpacer size="m" />
</>
);
return (
<>
{managedTemplateCallout}
<EuiTabs>
{TABS.map((tab) => (
<EuiTab
onClick={() => {
uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]);
setActiveTab(tab.id);
}}
isSelected={tab.id === activeTab}
key={tab.id}
data-test-subj="tab"
>
{tab.name}
</EuiTab>
))}
</EuiTabs>
<EuiSpacer size="l" />
{tabContent}
</>
);
}
};
const renderFooter = () => {
return (
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="cross"
flush="left"
onClick={onClose}
data-test-subj="closeDetailsButton"
>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
{templateDetails && (
<EuiFlexItem grow={false}>
{/* Manage templates context menu */}
<EuiPopover
id="manageTemplatePanel"
button={
<EuiButton
fill
data-test-subj="manageTemplateButton"
iconType="arrowDown"
iconSide="right"
onClick={() => setIsPopOverOpen((prev) => !prev)}
>
<FormattedMessage
id="xpack.idxMgmt.templateDetails.manageButtonLabel"
defaultMessage="Manage"
/>
</EuiButton>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopOverOpen(false)}
panelPaddingSize="none"
withTitle
anchorPosition="rightUp"
repositionOnScroll
>
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
title: i18n.translate(
'xpack.idxMgmt.templateDetails.manageContextMenuPanelTitle',
{
defaultMessage: 'Template options',
}
),
items: [
{
name: i18n.translate('xpack.idxMgmt.templateDetails.editButtonLabel', {
defaultMessage: 'Edit',
}),
icon: 'pencil',
onClick: () => editTemplate(templateName, isLegacy),
disabled: isCloudManaged,
},
{
name: i18n.translate('xpack.idxMgmt.templateDetails.cloneButtonLabel', {
defaultMessage: 'Clone',
}),
icon: 'copy',
onClick: () => cloneTemplate(templateName, isLegacy),
},
{
name: i18n.translate('xpack.idxMgmt.templateDetails.deleteButtonLabel', {
defaultMessage: 'Delete',
}),
icon: 'trash',
onClick: () =>
setTemplateToDelete([{ name: decodedTemplateName, isLegacy }]),
disabled: isCloudManaged,
},
],
},
]}
/>
</EuiPopover>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutFooter>
);
};
return (
<>
{renderHeader()}
<EuiFlyoutBody data-test-subj="content">{renderBody()}</EuiFlyoutBody>
{renderFooter()}
{templateToDelete && templateToDelete.length > 0 ? (
<TemplateDeleteModal
callback={(data) => {
if (data && data.hasDeletedTemplates) {
reload();
} else {
setTemplateToDelete([]);
}
onClose();
}}
templatesToDelete={templateToDelete}
/>
) : null}
</>
);
};

View file

@ -31,8 +31,8 @@ import {
} from '../../../services/routing';
import { getIsLegacyFromQueryParams } from '../../../lib/index_templates';
import { TemplateTable } from './template_table';
import { TemplateDetails } from './template_details';
import { LegacyTemplateTable } from './legacy_templates/template_table';
import { LegacyTemplateDetails } from './legacy_templates/template_details';
import { FilterListButton, Filters } from './components';
type FilterName = 'composable' | 'system';
@ -90,7 +90,7 @@ export const TemplateList: React.FunctionComponent<RouteComponentProps<MatchPara
}
: null;
const isLegacyTemplateDetailsVisible = selectedTemplate !== null && selectedTemplate.isLegacy;
const isTemplateDetailsVisible = selectedTemplate !== null;
const hasTemplates =
allTemplates && (allTemplates.legacyTemplates.length > 0 || allTemplates.templates.length > 0);
@ -146,6 +146,7 @@ export const TemplateList: React.FunctionComponent<RouteComponentProps<MatchPara
templates={filteredTemplates.templates}
reload={reload}
editTemplate={editTemplate}
cloneTemplate={cloneTemplate}
history={history as ScopedHistory}
/>
</>
@ -235,8 +236,8 @@ export const TemplateList: React.FunctionComponent<RouteComponentProps<MatchPara
<div data-test-subj="templateList">
{renderContent()}
{isLegacyTemplateDetailsVisible && (
<LegacyTemplateDetails
{isTemplateDetailsVisible && (
<TemplateDetails
template={selectedTemplate!}
onClose={closeTemplateDetails}
editTemplate={editTemplate}

View file

@ -7,27 +7,41 @@
import React, { useState, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton } from '@elastic/eui';
import {
EuiInMemoryTable,
EuiBasicTableColumn,
EuiButton,
EuiLink,
EuiBadge,
EuiIcon,
} from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
import { TemplateListItem } from '../../../../../../common';
import { TemplateDeleteModal } from '../../../../components';
import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../common/constants';
import { SendRequestResponse, reactRouterNavigate } from '../../../../../shared_imports';
import { encodePathForReactRouter } from '../../../../services/routing';
import { useServices } from '../../../../app_context';
import { TemplateDeleteModal } from '../../../../components';
import { TemplateContentIndicator } from '../../../../components/shared';
interface Props {
templates: TemplateListItem[];
reload: () => Promise<SendRequestResponse>;
editTemplate: (name: string) => void;
cloneTemplate: (name: string) => void;
history: ScopedHistory;
}
export const TemplateTable: React.FunctionComponent<Props> = ({
templates,
reload,
history,
editTemplate,
cloneTemplate,
history,
}) => {
const { uiMetricService } = useServices();
const [selection, setSelection] = useState<TemplateListItem[]>([]);
const [templatesToDelete, setTemplatesToDelete] = useState<
Array<{ name: string; isLegacy?: boolean }>
>([]);
@ -40,6 +54,32 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
}),
truncateText: true,
sortable: true,
render: (name: TemplateListItem['name'], item: TemplateListItem) => {
return (
<>
<EuiLink
{...reactRouterNavigate(
history,
{
pathname: `/templates/${encodePathForReactRouter(name)}`,
},
() => uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK)
)}
data-test-subj="templateDetailsLink"
>
{name}
</EuiLink>
&nbsp;
{item._kbnMeta.isManaged ? (
<EuiBadge color="hollow" data-test-subj="isManagedBadge">
Managed
</EuiBadge>
) : (
''
)}
</>
);
},
},
{
field: 'indexPatterns',
@ -50,27 +90,6 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
sortable: true,
render: (indexPatterns: string[]) => <strong>{indexPatterns.join(', ')}</strong>,
},
{
field: 'ilmPolicy',
name: i18n.translate('xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle', {
defaultMessage: 'ILM policy',
}),
truncateText: true,
sortable: true,
render: (ilmPolicy: { name: string }) =>
ilmPolicy && ilmPolicy.name ? (
<span
title={i18n.translate('xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription', {
defaultMessage: "'{policyName}' index lifecycle policy",
values: {
policyName: ilmPolicy.name,
},
})}
>
{ilmPolicy.name}
</span>
) : null,
},
{
field: 'composedOf',
name: i18n.translate('xpack.idxMgmt.templateList.table.componentsColumnTitle', {
@ -89,8 +108,16 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
sortable: true,
},
{
name: i18n.translate('xpack.idxMgmt.templateList.table.overridesColumnTitle', {
defaultMessage: 'Overrides',
name: i18n.translate('xpack.idxMgmt.templateList.table.dataStreamColumnTitle', {
defaultMessage: 'Data stream',
}),
truncateText: true,
render: (template: TemplateListItem) =>
template._kbnMeta.hasDatastream ? <EuiIcon type="check" /> : null,
},
{
name: i18n.translate('xpack.idxMgmt.templateList.table.contentColumnTitle', {
defaultMessage: 'Content',
}),
truncateText: true,
render: (item: TemplateListItem) => (
@ -98,6 +125,13 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
mappings={item.hasMappings}
settings={item.hasSettings}
aliases={item.hasAliases}
contentWhenEmpty={
<em>
{i18n.translate('xpack.idxMgmt.templateList.table.noneDescriptionText', {
defaultMessage: 'None',
})}
</em>
}
/>
),
},
@ -119,7 +153,36 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
onClick: ({ name }: TemplateListItem) => {
editTemplate(name);
},
enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged,
enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged,
},
{
type: 'icon',
name: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneTitle', {
defaultMessage: 'Clone',
}),
description: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneDescription', {
defaultMessage: 'Clone this template',
}),
icon: 'copy',
onClick: ({ name }: TemplateListItem) => {
cloneTemplate(name);
},
},
{
name: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteText', {
defaultMessage: 'Delete',
}),
description: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteDecription', {
defaultMessage: 'Delete this template',
}),
icon: 'trash',
color: 'danger',
type: 'icon',
onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => {
setTemplatesToDelete([{ name, isLegacy }]);
},
isPrimary: true,
enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged,
},
],
},
@ -137,10 +200,47 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
},
} as const;
const selectionConfig = {
onSelectionChange: setSelection,
selectable: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged,
selectableMessage: (selectable: boolean) => {
if (!selectable) {
return i18n.translate(
'xpack.idxMgmt.templateList.legacyTable.deleteManagedTemplateTooltip',
{
defaultMessage: 'You cannot delete a managed template.',
}
);
}
return '';
},
};
const searchConfig = {
box: {
incremental: true,
},
toolsLeft:
selection.length > 0 ? (
<EuiButton
data-test-subj="deleteTemplatesButton"
onClick={() =>
setTemplatesToDelete(
selection.map(({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => ({
name,
isLegacy,
}))
)
}
color="danger"
>
<FormattedMessage
id="xpack.idxMgmt.templateList.table.deleteTemplatesButtonLabel"
defaultMessage="Delete {count, plural, one {template} other {templates} }"
values={{ count: selection.length }}
/>
</EuiButton>
) : undefined,
toolsRight: [
<EuiButton
iconType="plusInCircle"
@ -157,6 +257,10 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
],
};
const goToList = () => {
return history.push('templates');
};
return (
<Fragment>
{templatesToDelete && templatesToDelete.length > 0 ? (
@ -164,9 +268,10 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
callback={(data) => {
if (data && data.hasDeletedTemplates) {
reload();
} else {
setTemplatesToDelete([]);
// Close the flyout if it is opened
goToList();
}
setTemplatesToDelete([]);
}}
templatesToDelete={templatesToDelete}
/>
@ -177,7 +282,8 @@ export const TemplateTable: React.FunctionComponent<Props> = ({
columns={columns}
search={searchConfig}
sorting={sorting}
isSelectable={false}
isSelectable={true}
selection={selectionConfig}
pagination={pagination}
rowProps={() => ({
'data-test-subj': 'row',

View file

@ -85,11 +85,11 @@ export const TemplateEdit: React.FunctionComponent<RouteComponentProps<MatchPara
} else if (template) {
const {
name: templateName,
_kbnMeta: { isManaged },
_kbnMeta: { isCloudManaged },
} = template;
const isSystemTemplate = templateName && templateName.startsWith('.');
if (isManaged) {
if (isCloudManaged) {
content = (
<EuiCallOut
title={

View file

@ -6,7 +6,7 @@
// Cloud has its own system for managing templates and we want to make
// this clear in the UI when a template is used in a Cloud deployment.
export const getManagedTemplatePrefix = async (
export const getCloudManagedTemplatePrefix = async (
callAsCurrentUser: any
): Promise<string | undefined> => {
try {

View file

@ -28,6 +28,7 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) {
validate: { body: bodySchema },
},
license.guardApiRoute(async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
const { templates } = req.body as TypeOf<typeof bodySchema>;
const response: { templatesDeleted: Array<TemplateDeserialized['name']>; errors: any[] } = {
templatesDeleted: [],
@ -37,14 +38,16 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) {
await Promise.all(
templates.map(async ({ name, isLegacy }) => {
try {
if (!isLegacy) {
return res.badRequest({ body: 'Only legacy index template can be deleted.' });
if (isLegacy) {
await callAsCurrentUser('indices.deleteTemplate', {
name,
});
} else {
await callAsCurrentUser('dataManagement.deleteComposableIndexTemplate', {
name,
});
}
await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.deleteTemplate', {
name,
});
return response.templatesDeleted.push(name);
} catch (e) {
return response.errors.push({

View file

@ -11,7 +11,7 @@ import {
deserializeLegacyTemplate,
deserializeLegacyTemplateList,
} from '../../../../common/lib';
import { getManagedTemplatePrefix } from '../../../lib/get_managed_templates';
import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_templates';
import { RouteDependencies } from '../../../types';
import { addBasePath } from '../index';
@ -20,7 +20,7 @@ export function registerGetAllRoute({ router, license }: RouteDependencies) {
{ path: addBasePath('/index_templates'), validate: false },
license.guardApiRoute(async (ctx, req, res) => {
const { callAsCurrentUser } = ctx.dataManagement!.client;
const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser);
const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser);
const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate');
const { index_templates: templatesEs } = await callAsCurrentUser(
@ -29,9 +29,9 @@ export function registerGetAllRoute({ router, license }: RouteDependencies) {
const legacyTemplates = deserializeLegacyTemplateList(
legacyTemplatesEs,
managedTemplatePrefix
cloudManagedTemplatePrefix
);
const templates = deserializeTemplateList(templatesEs, managedTemplatePrefix);
const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix);
const body = {
templates,
@ -65,7 +65,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies)
const isLegacy = (req.query as TypeOf<typeof querySchema>).legacy === 'true';
try {
const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser);
const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser);
if (isLegacy) {
const indexTemplateByName = await callAsCurrentUser('indices.getTemplate', { name });
@ -74,7 +74,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies)
return res.ok({
body: deserializeLegacyTemplate(
{ ...indexTemplateByName[name], name },
managedTemplatePrefix
cloudManagedTemplatePrefix
),
});
}
@ -87,7 +87,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies)
return res.ok({
body: deserializeTemplate(
{ ...indexTemplates[0].index_template, name },
managedTemplatePrefix
cloudManagedTemplatePrefix
),
});
}

View file

@ -29,6 +29,8 @@ export const templateSchema = schema.object({
),
_kbnMeta: schema.object({
isManaged: schema.maybe(schema.boolean()),
isCloudManaged: schema.maybe(schema.boolean()),
hasDatastream: schema.maybe(schema.boolean()),
isLegacy: schema.maybe(schema.boolean()),
}),
});

View file

@ -14,11 +14,15 @@ export const getTemplate = ({
indexPatterns = [],
template: { settings, aliases, mappings } = {},
isManaged = false,
isCloudManaged = false,
hasDatastream = false,
isLegacy = false,
}: Partial<
TemplateDeserialized & {
isLegacy?: boolean;
isManaged: boolean;
isCloudManaged: boolean;
hasDatastream: boolean;
}
> = {}): TemplateDeserialized => ({
name,
@ -32,6 +36,8 @@ export const getTemplate = ({
},
_kbnMeta: {
isManaged,
isCloudManaged,
hasDatastream,
isLegacy,
},
});

View file

@ -7079,8 +7079,6 @@
"xpack.idxMgmt.templateForm.steps.mappingsStepName": "マッピング",
"xpack.idxMgmt.templateForm.steps.settingsStepName": "インデックス設定",
"xpack.idxMgmt.templateForm.steps.summaryStepName": "テンプレートのレビュー",
"xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription": "インデックスライフサイクルポリシー「{policyName}」",
"xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle": "ILM ポリシー",
"xpack.idxMgmt.templateList.table.indexPatternsColumnTitle": "インデックスパターン",
"xpack.idxMgmt.templateList.table.nameColumnTitle": "名前",
"xpack.idxMgmt.templateList.table.noIndexTemplatesMessage": "インデックステンプレートが見つかりません",

View file

@ -7083,8 +7083,6 @@
"xpack.idxMgmt.templateForm.steps.mappingsStepName": "映射",
"xpack.idxMgmt.templateForm.steps.settingsStepName": "索引设置",
"xpack.idxMgmt.templateForm.steps.summaryStepName": "复查模板",
"xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription": "“{policyName}”索引生命周期策略",
"xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle": "ILM 策略",
"xpack.idxMgmt.templateList.table.indexPatternsColumnTitle": "索引模式",
"xpack.idxMgmt.templateList.table.nameColumnTitle": "名称",
"xpack.idxMgmt.templateList.table.noIndexTemplatesMessage": "未找到任何索引模板",

View file

@ -252,6 +252,38 @@ export default function ({ getService }) {
describe('delete', () => {
it('should delete an index template', async () => {
const templateName = `template-${getRandomString()}`;
const payload = getTemplatePayload(templateName, [getRandomString()]);
const { status: createStatus, body: createBody } = await createTemplate(payload);
if (createStatus !== 200) {
throw new Error(`Error creating template: ${createStatus} ${createBody.message}`);
}
let catTemplateResponse = await catTemplate(templateName);
expect(
catTemplateResponse.find((template) => template.name === payload.name).name
).to.equal(templateName);
const { status: deleteStatus, body: deleteBody } = await deleteTemplates([
{ name: templateName },
]);
if (deleteStatus !== 200) {
throw new Error(`Error deleting template: ${deleteBody.message}`);
}
expect(deleteBody.errors).to.be.empty;
expect(deleteBody.templatesDeleted[0]).to.equal(templateName);
catTemplateResponse = await catTemplate(templateName);
expect(catTemplateResponse.find((template) => template.name === payload.name)).to.equal(
undefined
);
});
it('should delete a legacy index template', async () => {
const templateName = `template-${getRandomString()}`;
const payload = getTemplatePayload(templateName, [getRandomString()], true);