From 8cb8adeb24652e0737c91fd38f2064be76c826e5 Mon Sep 17 00:00:00 2001 From: Bernd Ahlers Date: Tue, 14 Nov 2023 09:27:02 +0100 Subject: [PATCH 1/4] Update download-maven-plugin to our fork (#17245) The caching in the latest upstream version is completely broken since April 2023 and the maintainers are currently unresponsive. Our fork is based on version 1.6.8 and fixes the excessive download progress logging which is a problem with the log size limit on GitHub Actions. --- data-node/pom.xml | 2 +- pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data-node/pom.xml b/data-node/pom.xml index ecd2c1e94ba3..20453df4c057 100644 --- a/data-node/pom.xml +++ b/data-node/pom.xml @@ -498,7 +498,7 @@ - com.googlecode.maven-download-plugin + org.graylog.repackaged download-maven-plugin ${download-maven-plugin.version} diff --git a/pom.xml b/pom.xml index 5e94d0b87e32..b365c931066c 100644 --- a/pom.xml +++ b/pom.xml @@ -203,7 +203,7 @@ 4.0.rc2 - 1.6.8 + 1.6.8.1 @@ -894,7 +894,7 @@ - com.googlecode.maven-download-plugin + org.graylog.repackaged download-maven-plugin ${download-maven-plugin.version} From 98fb2c1872ef03c25f3b4179ddfb5c671665ff7f Mon Sep 17 00:00:00 2001 From: Dennis Oelkers Date: Tue, 14 Nov 2023 09:28:36 +0100 Subject: [PATCH 2/4] Include basic data node usage information in telemetry. (#17042) * Include basic data node usage information in telemetry. * Adding changelog snippet. --- changelog/unreleased/pr-17042.toml | 5 +++++ .../rest/TelemetryResponseFactory.java | 5 ++++- .../telemetry/rest/TelemetryService.java | 18 +++++++++++++++--- .../telemetry/rest/TelemetryServiceTest.java | 10 ++++++++-- .../rest/TelemetryServiceWithDbTest.java | 10 ++++++++-- .../telemetry/rest/TelemetryTestHelper.java | 2 ++ .../src/logic/telemetry/TelemetryProvider.tsx | 2 ++ .../src/logic/telemetry/useTelemetryData.tsx | 3 +++ 8 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 changelog/unreleased/pr-17042.toml diff --git a/changelog/unreleased/pr-17042.toml b/changelog/unreleased/pr-17042.toml new file mode 100644 index 000000000000..9f547d01e011 --- /dev/null +++ b/changelog/unreleased/pr-17042.toml @@ -0,0 +1,5 @@ +type = "a" +message = "Including basic data node usage information in telemetry." + +pulls = ["17042"] + diff --git a/graylog2-server/src/main/java/org/graylog2/telemetry/rest/TelemetryResponseFactory.java b/graylog2-server/src/main/java/org/graylog2/telemetry/rest/TelemetryResponseFactory.java index 1530300b6c73..c6ab97426ff4 100644 --- a/graylog2-server/src/main/java/org/graylog2/telemetry/rest/TelemetryResponseFactory.java +++ b/graylog2-server/src/main/java/org/graylog2/telemetry/rest/TelemetryResponseFactory.java @@ -39,6 +39,7 @@ class TelemetryResponseFactory { private static final String LICENSE = "license"; private static final String PLUGIN = "plugin"; private static final String SEARCH_CLUSTER = "search_cluster"; + private static final String DATA_NODES = "data_nodes"; private static boolean isLeader(Map n) { if (n.get(FIELD_IS_LEADER) instanceof Boolean isLeader) { @@ -52,7 +53,8 @@ Map createTelemetryResponse(Map clusterInfo, Map pluginInfo, Map searchClusterInfo, List licenseStatuses, - TelemetryUserSettings telemetryUserSettings) { + TelemetryUserSettings telemetryUserSettings, + Map dataNodeInfo) { Map telemetryResponse = new LinkedHashMap<>(); telemetryResponse.put(CURRENT_USER, userInfo); telemetryResponse.put(USER_TELEMETRY_SETTINGS, telemetryUserSettings); @@ -60,6 +62,7 @@ Map createTelemetryResponse(Map clusterInfo, telemetryResponse.put(LICENSE, createLicenseInfo(licenseStatuses)); telemetryResponse.put(PLUGIN, pluginInfo); telemetryResponse.put(SEARCH_CLUSTER, searchClusterInfo); + telemetryResponse.put(DATA_NODES, dataNodeInfo); return telemetryResponse; } diff --git a/graylog2-server/src/main/java/org/graylog2/telemetry/rest/TelemetryService.java b/graylog2-server/src/main/java/org/graylog2/telemetry/rest/TelemetryService.java index bdd0b7cf6301..7c80f3c5d902 100644 --- a/graylog2-server/src/main/java/org/graylog2/telemetry/rest/TelemetryService.java +++ b/graylog2-server/src/main/java/org/graylog2/telemetry/rest/TelemetryService.java @@ -19,6 +19,8 @@ import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import com.google.common.hash.HashCode; +import org.graylog2.cluster.Node; +import org.graylog2.cluster.NodeService; import org.graylog2.indexer.cluster.ClusterAdapter; import org.graylog2.plugin.PluginMetaData; import org.graylog2.plugin.database.users.User; @@ -65,7 +67,7 @@ public class TelemetryService { private final boolean isTelemetryEnabled; private final TelemetryClusterService telemetryClusterService; private final String installationSource; - + private final NodeService nodeService; @Inject public TelemetryService( @@ -80,7 +82,8 @@ public TelemetryService( DBTelemetryUserSettingsService dbTelemetryUserSettingsService, EventBus eventBus, TelemetryClusterService telemetryClusterService, - @Named("installation_source") String installationSource) { + @Named("installation_source") String installationSource, + NodeService nodeService) { this.isTelemetryEnabled = isTelemetryEnabled; this.trafficCounterService = trafficCounterService; this.enterpriseDataProvider = enterpriseDataProvider; @@ -92,6 +95,7 @@ public TelemetryService( this.dbTelemetryUserSettingsService = dbTelemetryUserSettingsService; this.telemetryClusterService = telemetryClusterService; this.installationSource = installationSource; + this.nodeService = nodeService; eventBus.register(this); } @@ -108,7 +112,8 @@ public Map getTelemetryResponse(User currentUser) { getPluginInfo(), getSearchClusterInfo(), licenseStatuses, - telemetryUserSettings); + telemetryUserSettings, + getDataNodeInfo()); } else { return telemetryResponseFactory.createTelemetryDisabledResponse(telemetryUserSettings); } @@ -206,4 +211,11 @@ private Map getSearchClusterInfo() { elasticsearchVersion.toString(), nodesInfo); } + + private Map getDataNodeInfo() { + final var dataNodes = nodeService.allActive(Node.Type.DATANODE); + return Map.of( + "data_nodes_count", dataNodes.size() + ); + } } diff --git a/graylog2-server/src/test/java/org/graylog2/telemetry/rest/TelemetryServiceTest.java b/graylog2-server/src/test/java/org/graylog2/telemetry/rest/TelemetryServiceTest.java index a76ca39a7325..9975c7790d0e 100644 --- a/graylog2-server/src/test/java/org/graylog2/telemetry/rest/TelemetryServiceTest.java +++ b/graylog2-server/src/test/java/org/graylog2/telemetry/rest/TelemetryServiceTest.java @@ -17,6 +17,7 @@ package org.graylog2.telemetry.rest; import com.google.common.eventbus.EventBus; +import org.graylog2.cluster.NodeService; import org.graylog2.indexer.cluster.ClusterAdapter; import org.graylog2.plugin.PluginMetaData; import org.graylog2.plugin.database.users.User; @@ -45,6 +46,7 @@ import static org.graylog2.shared.utilities.StringUtils.f; import static org.graylog2.telemetry.rest.TelemetryTestHelper.CLUSTER; import static org.graylog2.telemetry.rest.TelemetryTestHelper.CURRENT_USER; +import static org.graylog2.telemetry.rest.TelemetryTestHelper.DATA_NODES; import static org.graylog2.telemetry.rest.TelemetryTestHelper.LICENSE; import static org.graylog2.telemetry.rest.TelemetryTestHelper.PLUGIN; import static org.graylog2.telemetry.rest.TelemetryTestHelper.SEARCH_CLUSTER; @@ -77,6 +79,9 @@ public class TelemetryServiceTest { @Mock User user; + @Mock + NodeService nodeService; + @Test void test_telemetry_is_disabled_globally() { TelemetryService telemetryService = createTelemetryService(false); @@ -177,7 +182,7 @@ private TelemetryLicenseStatus createLicense(String subject) { } private void assertThatAllTelemetryDataIsPresent(Map response) { - assertThat(response).containsOnlyKeys(USER_TELEMETRY_SETTINGS, CURRENT_USER, CLUSTER, LICENSE, PLUGIN, SEARCH_CLUSTER); + assertThat(response).containsOnlyKeys(USER_TELEMETRY_SETTINGS, CURRENT_USER, CLUSTER, LICENSE, PLUGIN, SEARCH_CLUSTER, DATA_NODES); } private TelemetryService createTelemetryService(boolean isTelemetryEnabled) { @@ -193,7 +198,8 @@ private TelemetryService createTelemetryService(boolean isTelemetryEnabled) { dbTelemetryUserSettingsService, eventBus, telemetryClusterService, - "unknown"); + "unknown", + nodeService); } private void mockUserTelemetryEnabled(boolean isTelemetryEnabled) { diff --git a/graylog2-server/src/test/java/org/graylog2/telemetry/rest/TelemetryServiceWithDbTest.java b/graylog2-server/src/test/java/org/graylog2/telemetry/rest/TelemetryServiceWithDbTest.java index 43a1c11c0947..1eedd8d96e66 100644 --- a/graylog2-server/src/test/java/org/graylog2/telemetry/rest/TelemetryServiceWithDbTest.java +++ b/graylog2-server/src/test/java/org/graylog2/telemetry/rest/TelemetryServiceWithDbTest.java @@ -20,6 +20,7 @@ import com.google.common.eventbus.EventBus; import org.graylog.testing.mongodb.MongoDBInstance; import org.graylog2.bindings.providers.MongoJackObjectMapperProvider; +import org.graylog2.cluster.NodeService; import org.graylog2.cluster.leader.LeaderElectionService; import org.graylog2.indexer.cluster.ClusterAdapter; import org.graylog2.plugin.PluginMetaData; @@ -50,6 +51,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.graylog2.telemetry.rest.TelemetryTestHelper.CLUSTER; import static org.graylog2.telemetry.rest.TelemetryTestHelper.CURRENT_USER; +import static org.graylog2.telemetry.rest.TelemetryTestHelper.DATA_NODES; import static org.graylog2.telemetry.rest.TelemetryTestHelper.LICENSE; import static org.graylog2.telemetry.rest.TelemetryTestHelper.PLUGIN; import static org.graylog2.telemetry.rest.TelemetryTestHelper.SEARCH_CLUSTER; @@ -88,6 +90,9 @@ public class TelemetryServiceWithDbTest { @Mock LeaderElectionService leaderElectionService; + @Mock + NodeService nodeService; + TelemetryService telemetryService; @Before @@ -111,7 +116,8 @@ public void setUp() { new DBTelemetryUserSettingsService(mongodb.mongoConnection(), mongoJackObjectMapperProvider), eventBus, telemetryClusterService, - "unknown"); + "unknown", + nodeService); } @Test @@ -153,7 +159,7 @@ public void test_all_telemetry_data_is_present() { telemetryService.updateTelemetryClusterData(); Map telemetryResponse = telemetryService.getTelemetryResponse(saveUserSettings(true)); - assertThat(telemetryResponse).containsOnlyKeys(USER_TELEMETRY_SETTINGS, CURRENT_USER, CLUSTER, LICENSE, PLUGIN, SEARCH_CLUSTER); + assertThat(telemetryResponse).containsOnlyKeys(USER_TELEMETRY_SETTINGS, CURRENT_USER, CLUSTER, LICENSE, PLUGIN, SEARCH_CLUSTER, DATA_NODES); } diff --git a/graylog2-server/src/test/java/org/graylog2/telemetry/rest/TelemetryTestHelper.java b/graylog2-server/src/test/java/org/graylog2/telemetry/rest/TelemetryTestHelper.java index 35be3c63db65..f6e33fa9bada 100644 --- a/graylog2-server/src/test/java/org/graylog2/telemetry/rest/TelemetryTestHelper.java +++ b/graylog2-server/src/test/java/org/graylog2/telemetry/rest/TelemetryTestHelper.java @@ -35,6 +35,8 @@ public class TelemetryTestHelper { static final String PLUGIN = "plugin"; static final String SEARCH_CLUSTER = "search_cluster"; + static final String DATA_NODES = "data_nodes"; + public static void mockTrafficData(TrafficCounterService trafficCounterService1) { when(trafficCounterService1.clusterTrafficOfLastDays(any(), any())).thenReturn(TelemetryTestHelper.TRAFFIC_HISTOGRAM); } diff --git a/graylog2-web-interface/src/logic/telemetry/TelemetryProvider.tsx b/graylog2-web-interface/src/logic/telemetry/TelemetryProvider.tsx index cabf6660635f..5e270c2aab07 100644 --- a/graylog2-web-interface/src/logic/telemetry/TelemetryProvider.tsx +++ b/graylog2-web-interface/src/logic/telemetry/TelemetryProvider.tsx @@ -79,6 +79,7 @@ const TelemetryProvider = ({ children }: { children: React.ReactElement }) => { license, plugin, search_cluster: searchCluster, + data_nodes: dataNodes, user_telemetry_settings: { telemetry_permission_asked: isPermissionAsked }, } = telemetryData as TelemetryDataType; setGlobalProps(getGlobalProps(telemetryData)); @@ -90,6 +91,7 @@ const TelemetryProvider = ({ children }: { children: React.ReactElement }) => { ...license, ...plugin, ...searchCluster, + ...dataNodes, ...getGlobalProps(telemetryData), }); diff --git a/graylog2-web-interface/src/logic/telemetry/useTelemetryData.tsx b/graylog2-web-interface/src/logic/telemetry/useTelemetryData.tsx index c5d97b968750..7ef1bde257ed 100644 --- a/graylog2-web-interface/src/logic/telemetry/useTelemetryData.tsx +++ b/graylog2-web-interface/src/logic/telemetry/useTelemetryData.tsx @@ -39,6 +39,9 @@ export type TelemetryDataType = { search_cluster?: { [key: string]: string, }, + data_nodes?: { + data_nodes_count: number, + } } const useTelemetryData = () => useQuery([TELEMETRY_CLUSTER_INFO_QUERY_KEY], () => Telemetry.get() as Promise, { From 55a1a4cfb04815bdc558e1ef17e385b62bb74093 Mon Sep 17 00:00:00 2001 From: maxiadlovskii Date: Tue, 14 Nov 2023 09:45:50 +0100 Subject: [PATCH 3/4] Add index set field type management page (#17070) * Add page and hook * Add paginated table * Change modal, add modal to the list * fix test * add is custom and is reserved * add delete button * Fix merge. Fix tests * Add more tests to ChangeFieldTypeModal * Add useIndexSetFieldType.test * Add test to IndexSetFieldTypesList * Add changelog file * Add permission check * rename file * Fix label for rotation * add reserved column * Remove search field * fix tests * refactoring --- changelog/unreleased/pr-17070.toml | 5 + .../indices/IndexSetFieldTypesList.test.tsx | 220 ++++++++++++++++++ .../indices/IndexSetFieldTypesList.tsx | 187 +++++++++++++++ .../indices/IndicesMaintenanceDropdown.tsx | 11 + .../src/hooks/useIndexSetFieldType.test.ts | 108 +++++++++ .../src/hooks/useIndexSetFieldType.ts | 97 ++++++++ .../src/pages/IndexSetFieldTypesPage.tsx | 75 ++++++ graylog2-web-interface/src/pages/index.jsx | 3 + .../src/routing/AppRouter.tsx | 2 + graylog2-web-interface/src/routing/Routes.ts | 1 + .../ChangeFieldType/ChangeFieldType.tsx | 8 +- .../ChangeFieldTypeModal.test.tsx | 74 ++++-- .../ChangeFieldType/ChangeFieldTypeModal.tsx | 52 +++-- .../ChangeFieldType/IndexSetsTable.tsx | 6 +- 14 files changed, 807 insertions(+), 42 deletions(-) create mode 100644 changelog/unreleased/pr-17070.toml create mode 100644 graylog2-web-interface/src/components/indices/IndexSetFieldTypesList.test.tsx create mode 100644 graylog2-web-interface/src/components/indices/IndexSetFieldTypesList.tsx create mode 100644 graylog2-web-interface/src/hooks/useIndexSetFieldType.test.ts create mode 100644 graylog2-web-interface/src/hooks/useIndexSetFieldType.ts create mode 100644 graylog2-web-interface/src/pages/IndexSetFieldTypesPage.tsx diff --git a/changelog/unreleased/pr-17070.toml b/changelog/unreleased/pr-17070.toml new file mode 100644 index 000000000000..91ebac42a8e4 --- /dev/null +++ b/changelog/unreleased/pr-17070.toml @@ -0,0 +1,5 @@ +type = "added" +message = "Add index set field type management page" + +issues = ["17160"] +pulls = ["17070"] diff --git a/graylog2-web-interface/src/components/indices/IndexSetFieldTypesList.test.tsx b/graylog2-web-interface/src/components/indices/IndexSetFieldTypesList.test.tsx new file mode 100644 index 000000000000..d607443d9b8e --- /dev/null +++ b/graylog2-web-interface/src/components/indices/IndexSetFieldTypesList.test.tsx @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import * as React from 'react'; +import { render, screen, fireEvent, within } from 'wrappedTestingLibrary'; + +import asMock from 'helpers/mocking/AsMock'; +import useIndexSetFieldTypes from 'hooks/useIndexSetFieldType'; +import useUserLayoutPreferences from 'components/common/EntityDataTable/hooks/useUserLayoutPreferences'; +import { layoutPreferences } from 'fixtures/entityListLayoutPreferences'; +import TestStoreProvider from 'views/test/TestStoreProvider'; +import { loadViewsPlugin, unloadViewsPlugin } from 'views/test/testViewsPlugin'; +import type { Attributes } from 'stores/PaginationTypes'; +import IndexSetFieldTypesList from 'components/indices/IndexSetFieldTypesList'; +import useFiledTypes from 'views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypes'; + +const attributes: Attributes = [ + { + id: 'field_name', + title: 'Field Name', + type: 'STRING', + sortable: true, + }, + { + id: 'is_custom', + title: 'Custom', + type: 'STRING', + sortable: true, + }, + { + id: 'is_reserved', + title: 'Reserved', + type: 'STRING', + sortable: true, + }, + { + id: 'type', + title: 'Type', + type: 'STRING', + sortable: true, + }, +]; + +const getData = (list = [{ + id: 'field', + fieldName: 'field', + type: 'bool', + isCustom: false, + isReserved: false, +}]) => ( + { + list, + pagination: { + total: 1, + }, + attributes, + } +); +const renderIndexSetFieldTypesList = () => render( + + + , +); + +jest.mock('views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypes', () => jest.fn()); +jest.mock('hooks/useIndexSetFieldType', () => jest.fn()); + +jest.mock('components/common/EntityDataTable/hooks/useUserLayoutPreferences'); + +describe('IndexSetFieldTypesList', () => { + beforeAll(loadViewsPlugin); + + afterAll(unloadViewsPlugin); + + beforeEach(() => { + asMock(useUserLayoutPreferences).mockReturnValue({ + data: { + ...layoutPreferences, + displayedAttributes: ['field_name', + 'is_custom', + 'is_reserved', + 'type'], + }, + isInitialLoading: false, + }); + + asMock(useFiledTypes).mockReturnValue({ + data: { + fieldTypes: { + string: 'String type', + int: 'Number(int)', + bool: 'Boolean', + }, + }, + isLoading: false, + }); + }); + + describe('Shows list of set field types with correct data', () => { + it('for field with non custom type', async () => { + asMock(useIndexSetFieldTypes).mockReturnValue({ + isLoading: false, + refetch: () => {}, + data: getData(), + }); + + renderIndexSetFieldTypesList(); + const tableRow = await screen.findByTestId('table-row-field'); + + await within(tableRow).findByText('field'); + await within(tableRow).findByText('Boolean'); + await within(tableRow).findByText('Edit'); + + expect(within(tableRow).queryByTitle('Field has custom field type')).not.toBeInTheDocument(); + }); + + it('for field with custom type', async () => { + asMock(useIndexSetFieldTypes).mockReturnValue({ + isLoading: false, + refetch: () => {}, + data: getData([{ + id: 'field', + fieldName: 'field', + type: 'bool', + isCustom: true, + isReserved: false, + }]), + }); + + renderIndexSetFieldTypesList(); + const tableRow = await screen.findByTestId('table-row-field'); + + await within(tableRow).findByText('field'); + await within(tableRow).findByText('Boolean'); + await within(tableRow).findByText('Edit'); + await within(tableRow).findByTitle('Field has custom field type'); + }); + + it('for field with non reserved type', async () => { + asMock(useIndexSetFieldTypes).mockReturnValue({ + isLoading: false, + refetch: () => {}, + data: getData([{ + id: 'field', + fieldName: 'field', + type: 'bool', + isCustom: true, + isReserved: false, + }]), + }); + + renderIndexSetFieldTypesList(); + const tableRow = await screen.findByTestId('table-row-field'); + + const editButton = await within(tableRow).findByText('Edit'); + + expect(within(tableRow).queryByTitle('Field has reserved field type')).not.toBeInTheDocument(); + expect(editButton.hasAttribute('disabled')).toBe(false); + }); + + it('for field with reserved type', async () => { + asMock(useIndexSetFieldTypes).mockReturnValue({ + isLoading: false, + refetch: () => {}, + data: getData([{ + id: 'field', + fieldName: 'field', + type: 'bool', + isCustom: true, + isReserved: true, + }]), + }); + + renderIndexSetFieldTypesList(); + const tableRow = await screen.findByTestId('table-row-field'); + + const editButton = await within(tableRow).findByText('Edit'); + await within(tableRow).findByTitle('Field has reserved field type'); + + expect(editButton.hasAttribute('disabled')).toBe(true); + }); + }); + + it('Shows modal on action click', async () => { + asMock(useIndexSetFieldTypes).mockReturnValue({ + isLoading: false, + refetch: () => {}, + data: getData([{ + id: 'field', + fieldName: 'field', + type: 'bool', + isCustom: true, + isReserved: false, + }]), + }); + + renderIndexSetFieldTypesList(); + const tableRow = await screen.findByTestId('table-row-field'); + const editButton = await within(tableRow).findByText('Edit'); + fireEvent.click(editButton); + await screen.findByText(/change field field type/i); + const modal = await screen.findByTestId('modal-form'); + await within(modal).findByText('Boolean'); + + expect(within(modal).queryByText(/select targeted index sets/i)).not.toBeInTheDocument(); + }); +}); diff --git a/graylog2-web-interface/src/components/indices/IndexSetFieldTypesList.tsx b/graylog2-web-interface/src/components/indices/IndexSetFieldTypesList.tsx new file mode 100644 index 000000000000..a42468d17ea9 --- /dev/null +++ b/graylog2-web-interface/src/components/indices/IndexSetFieldTypesList.tsx @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import React, { useCallback, useMemo, useState } from 'react'; + +import type { IndexSetFieldType } from 'hooks/useIndexSetFieldType'; +import { Button } from 'components/bootstrap'; +import useIndexSetFieldTypes from 'hooks/useIndexSetFieldType'; +import useParams from 'routing/useParams'; +import { + HoverForHelp, + Icon, + NoEntitiesExist, + PaginatedList, + Spinner, +} from 'components/common'; +import EntityDataTable from 'components/common/EntityDataTable'; +import useTableLayout from 'components/common/EntityDataTable/hooks/useTableLayout'; +import type { Sort } from 'stores/PaginationTypes'; +import useUpdateUserLayoutPreferences from 'components/common/EntityDataTable/hooks/useUpdateUserLayoutPreferences'; +import ChangeFieldTypeModal from 'views/logic/fieldactions/ChangeFieldType/ChangeFieldTypeModal'; +import useFiledTypes from 'views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypes'; + +export const ENTITY_TABLE_ID = 'index-set-field-types'; +export const DEFAULT_LAYOUT = { + pageSize: 20, + sort: { attributeId: 'field_name', direction: 'asc' } as Sort, + displayedColumns: ['field_name', 'type', 'is_custom', 'is_reserved'], + columnsOrder: ['field_name', 'type', 'is_custom', 'is_reserved'], +}; + +const IndexSetFieldTypesList = () => { + const { indexSetId } = useParams(); + const [editingField, setEditingField] = useState(null); + + const handleOnClose = useCallback(() => { + setEditingField(null); + }, []); + + const handleOnOpen = useCallback((fieldType: IndexSetFieldType) => { + setEditingField(fieldType); + }, []); + + const initialSelection = useMemo(() => [indexSetId], [indexSetId]); + const [query] = useState(''); + const [activePage, setActivePage] = useState(1); + const { data: { fieldTypes }, isLoading: isOptionsLoading } = useFiledTypes(); + const { layoutConfig, isInitialLoading: isLoadingLayoutPreferences } = useTableLayout({ + entityTableId: ENTITY_TABLE_ID, + defaultPageSize: DEFAULT_LAYOUT.pageSize, + defaultDisplayedAttributes: DEFAULT_LAYOUT.displayedColumns, + defaultSort: DEFAULT_LAYOUT.sort, + }); + const searchParams = useMemo(() => ({ + query, + page: activePage, + pageSize: layoutConfig.pageSize, + sort: layoutConfig.sort, + }), [activePage, layoutConfig.pageSize, layoutConfig.sort, query]); + const { mutate: updateTableLayout } = useUpdateUserLayoutPreferences(ENTITY_TABLE_ID); + const onPageChange = useCallback( + (newPage: number, newPageSize: number) => { + if (newPage) { + setActivePage(newPage); + } + + if (newPageSize) { + updateTableLayout({ perPage: newPageSize }); + } + }, [updateTableLayout], + ); + + const onPageSizeChange = useCallback((newPageSize: number) => { + setActivePage(1); + updateTableLayout({ perPage: newPageSize }); + }, [updateTableLayout]); + + const onSortChange = useCallback((newSort: Sort) => { + setActivePage(1); + updateTableLayout({ sort: newSort }); + }, [updateTableLayout]); + + const onColumnsChange = useCallback((displayedAttributes: Array) => { + updateTableLayout({ displayedAttributes }); + }, [updateTableLayout]); + const { isLoading, data: { list, pagination, attributes }, refetch } = useIndexSetFieldTypes(indexSetId, searchParams, { enabled: !isLoadingLayoutPreferences }); + + const customColumnRenderers = useMemo(() => ({ + attributes: { + type: { + renderCell: (item: string) => {fieldTypes[item]}, + }, + is_custom: { + renderCell: (isCustom: boolean) => (isCustom ? : null), + staticWidth: 120, + }, + is_reserved: { + renderCell: (isReserved: boolean) => (isReserved ? : null), + staticWidth: 120, + }, + }, + }), [fieldTypes]); + + const openEditModal = useCallback((fieldType: IndexSetFieldType) => { + handleOnOpen(fieldType); + }, [handleOnOpen]); + + const renderActions = useCallback((fieldType: IndexSetFieldType) => ( + + ), [openEditModal]); + + if (isLoadingLayoutPreferences || isLoading) { + return ; + } + + return ( + <> + + {pagination?.total === 0 && !searchParams.query && ( + + No fields have been created yet. + + )} + {!!list?.length && ( + data={list} + visibleColumns={layoutConfig.displayedAttributes} + columnsOrder={DEFAULT_LAYOUT.columnsOrder} + onColumnsChange={onColumnsChange} + onSortChange={onSortChange} + activeSort={layoutConfig.sort} + pageSize={searchParams.pageSize} + onPageSizeChange={onPageSizeChange} + actionsCellWidth={120} + columnRenderers={customColumnRenderers} + columnDefinitions={attributes} + rowActions={renderActions} /> + )} + + { + editingField ? ( + + ) : null + } + + ); +}; + +export default IndexSetFieldTypesList; diff --git a/graylog2-web-interface/src/components/indices/IndicesMaintenanceDropdown.tsx b/graylog2-web-interface/src/components/indices/IndicesMaintenanceDropdown.tsx index 1df851fa4b66..e1792d32f4c6 100644 --- a/graylog2-web-interface/src/components/indices/IndicesMaintenanceDropdown.tsx +++ b/graylog2-web-interface/src/components/indices/IndicesMaintenanceDropdown.tsx @@ -22,6 +22,9 @@ import { ButtonGroup, DropdownButton, MenuItem } from 'components/bootstrap'; import { DeflectorActions } from 'stores/indices/DeflectorStore'; import { IndexRangesActions } from 'stores/indices/IndexRangesStore'; import type { IndexSet } from 'stores/indices/IndexSetsStore'; +import useHistory from 'routing/useHistory'; +import Routes from 'routing/Routes'; +import useCurrentUser from 'hooks/useCurrentUser'; const _onRecalculateIndexRange = (indexSetId: string) => { // eslint-disable-next-line no-alert @@ -45,14 +48,22 @@ type Props = { }; const IndicesMaintenanceDropdown = ({ indexSet, indexSetId }: Props) => { + const currentUser = useCurrentUser(); + const history = useHistory(); const onCycleDeflector = useCallback(() => _onCycleDeflector(indexSetId), [indexSetId]); const onRecalculateIndexRange = useCallback(() => _onRecalculateIndexRange(indexSetId), [indexSetId]); const cycleButton = useMemo(() => (indexSet?.writable ? Rotate active write index : null), [indexSet?.writable, onCycleDeflector]); + const onShowFieldTypes = useCallback(() => { + history.push(Routes.SYSTEM.INDEX_SETS.FIELD_TYPES(indexSetId)); + }, [history, indexSetId]); + + const hasMappingPermission = currentUser.permissions.includes('typemappings:edit') || currentUser.permissions.includes('*'); return ( Recalculate index ranges + {hasMappingPermission && Show index field types} {cycleButton} diff --git a/graylog2-web-interface/src/hooks/useIndexSetFieldType.test.ts b/graylog2-web-interface/src/hooks/useIndexSetFieldType.test.ts new file mode 100644 index 000000000000..f857235746e5 --- /dev/null +++ b/graylog2-web-interface/src/hooks/useIndexSetFieldType.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ + +import { renderHook } from 'wrappedTestingLibrary/hooks'; + +import asMock from 'helpers/mocking/AsMock'; +import UserNotification from 'util/UserNotification'; +import suppressConsole from 'helpers/suppressConsole'; +import useIndexSetFieldType from 'hooks/useIndexSetFieldType'; +import fetch from 'logic/rest/FetchProvider'; +import { qualifyUrl } from 'util/URLUtils'; + +const mockData = { + attributes: [], + query: '', + pagination: { + total: 1, + count: 1, + page: 1, + per_page: 10, + }, + defaults: { + sort: { + id: 'field_name', + direction: 'ASC', + } as { id: string, direction: 'ASC' | 'DESC'}, + }, + total: 1, + sort: 'field_name', + order: 'desc', + elements: [{ + field_name: 'field', + type: 'bool', + is_custom: false, + is_reserved: false, + }], +}; + +const expectedState = { + attributes: [], + list: [{ + id: 'field', + fieldName: 'field', + type: 'bool', + isCustom: false, + isReserved: false, + }], + pagination: { + total: 1, + }, +}; +jest.mock('util/UserNotification', () => ({ error: jest.fn() })); +jest.mock('logic/rest/FetchProvider', () => jest.fn(() => Promise.resolve())); + +jest.mock('@graylog/server-api', () => ({ + SystemFieldTypes: { + getAllFieldTypes: jest.fn(() => Promise.resolve()), + }, +})); + +const renderUseIndexSetFieldTypeHook = () => renderHook(() => useIndexSetFieldType('id-1', { page: 1, query: '', pageSize: 10, sort: { attributeId: 'field_name', direction: 'asc' } }, { enabled: true })); + +describe('useIndexSetFieldType custom hook', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Test return initial data and take from fetch', async () => { + asMock(fetch).mockImplementation(() => Promise.resolve(mockData)); + const { result, waitFor } = renderUseIndexSetFieldTypeHook(); + + await waitFor(() => result.current.isLoading); + await waitFor(() => !result.current.isLoading); + + expect(fetch).toHaveBeenCalledWith('GET', qualifyUrl('/system/indices/index_sets/types/id-1?page=1&per_page=10&sort=field_name&order=asc')); + + expect(result.current.data).toEqual(expectedState); + }); + + it('Test trigger notification on fail', async () => { + asMock(fetch).mockImplementation(() => Promise.reject(new Error('Error'))); + + const { result, waitFor } = renderUseIndexSetFieldTypeHook(); + + await suppressConsole(async () => { + await waitFor(() => result.current.isLoading); + await waitFor(() => !result.current.isLoading); + }); + + expect(UserNotification.error).toHaveBeenCalledWith( + 'Loading index field types failed with status: Error: Error', + 'Could not load index field types'); + }); +}); diff --git a/graylog2-web-interface/src/hooks/useIndexSetFieldType.ts b/graylog2-web-interface/src/hooks/useIndexSetFieldType.ts new file mode 100644 index 000000000000..49c7f67ee7d8 --- /dev/null +++ b/graylog2-web-interface/src/hooks/useIndexSetFieldType.ts @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import { useQuery } from '@tanstack/react-query'; + +import UserNotification from 'util/UserNotification'; +import fetch from 'logic/rest/FetchProvider'; +import { qualifyUrl } from 'util/URLUtils'; +import type { Attribute, SearchParams } from 'stores/PaginationTypes'; +import PaginationURL from 'util/PaginationURL'; + +const INITIAL_DATA = { + pagination: { total: 0 }, + list: [], + attributes: [], +}; +export type IndexSetFieldTypeJson = { + field_name: string, + type: string, + is_custom: boolean, + is_reserved: boolean, +} + +export type IndexSetFieldType = { + id: string, + fieldName: string, + isCustom: boolean, + isReserved: boolean, + type: string, +} + +const fetchIndexSetFieldTypes = async (indexSetId: string, searchParams: SearchParams) => { + const indexSetFiledTypeUrl = qualifyUrl(`/system/indices/index_sets/types/${indexSetId}`); + const url = PaginationURL( + indexSetFiledTypeUrl, + searchParams.page, + searchParams.pageSize, + searchParams.query, + { sort: searchParams.sort.attributeId, order: searchParams.sort.direction }); + + return fetch('GET', url).then( + ({ elements, total, attributes }) => ({ + list: elements.map((fieldType: IndexSetFieldTypeJson) => ({ + id: fieldType.field_name, + fieldName: fieldType.field_name, + type: fieldType.type, + isCustom: fieldType.is_custom, + isReserved: fieldType.is_reserved, + })), + pagination: { total }, + attributes, + })); +}; + +const useIndexSetFieldTypes = (indexSetId: string, searchParams: SearchParams, { enabled }): { + data: { + list: Readonly>, + pagination: { total: number }, + attributes: Array + }, + isLoading: boolean, + refetch: () => void, +} => { + const { data, isLoading, refetch } = useQuery( + ['indexSetFieldTypes', searchParams], + () => fetchIndexSetFieldTypes(indexSetId, searchParams), + { + onError: (errorThrown) => { + UserNotification.error(`Loading index field types failed with status: ${errorThrown}`, + 'Could not load index field types'); + }, + keepPreviousData: true, + enabled, + }, + ); + + return ({ + data: data ?? INITIAL_DATA, + isLoading, + refetch, + }); +}; + +export default useIndexSetFieldTypes; diff --git a/graylog2-web-interface/src/pages/IndexSetFieldTypesPage.tsx b/graylog2-web-interface/src/pages/IndexSetFieldTypesPage.tsx new file mode 100644 index 000000000000..ce89ffe84c18 --- /dev/null +++ b/graylog2-web-interface/src/pages/IndexSetFieldTypesPage.tsx @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +import React, { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { DocumentTitle, PageHeader } from 'components/common'; +import { Row, Col, Button } from 'components/bootstrap'; +import { useStore } from 'stores/connect'; +import { IndexSetsActions, IndexSetsStore } from 'stores/indices/IndexSetsStore'; +import useParams from 'routing/useParams'; +import DocsHelper from 'util/DocsHelper'; +import { LinkContainer } from 'components/common/router'; +import Routes from 'routing/Routes'; +import IndexSetFieldTypesList from 'components/indices/IndexSetFieldTypesList'; +import useCurrentUser from 'hooks/useCurrentUser'; + +const IndexSetFieldTypesPage = () => { + const { indexSetId } = useParams(); + const navigate = useNavigate(); + const { indexSet } = useStore(IndexSetsStore); + const currentUser = useCurrentUser(); + + useEffect(() => { + const hasMappingPermission = currentUser.permissions.includes('typemappings:edit') || currentUser.permissions.includes('*'); + + if (!hasMappingPermission) { + navigate(Routes.NOTFOUND); + } else { + IndexSetsActions.get(indexSetId); + } + }, [currentUser.permissions, indexSetId, navigate]); + + return ( + +
+ + + + )}> + + Modify the current field types configuration for this index set. + + + + + + + + +
+
+ ); +}; + +export default IndexSetFieldTypesPage; diff --git a/graylog2-web-interface/src/pages/index.jsx b/graylog2-web-interface/src/pages/index.jsx index 48a60938235e..43616bf331ef 100644 --- a/graylog2-web-interface/src/pages/index.jsx +++ b/graylog2-web-interface/src/pages/index.jsx @@ -99,6 +99,8 @@ const UserTokensEditPage = loadAsync(() => import('./UserTokensEditPage')); const UsersOverviewPage = loadAsync(() => import('./UsersOverviewPage')); const ViewEventDefinitionPage = loadAsync(() => import('./ViewEventDefinitionPage')); +const IndexSetFieldTypesPage = loadAsync(() => import('./IndexSetFieldTypesPage')); + export { AuthenticationCreatePage, AuthenticationPage, @@ -132,6 +134,7 @@ export { IndexerFailuresPage, IndexSetConfigurationPage, IndexSetCreationPage, + IndexSetFieldTypesPage, IndexSetPage, IndicesPage, InputsPage, diff --git a/graylog2-web-interface/src/routing/AppRouter.tsx b/graylog2-web-interface/src/routing/AppRouter.tsx index b9b94cc51501..0db546c1ed9b 100644 --- a/graylog2-web-interface/src/routing/AppRouter.tsx +++ b/graylog2-web-interface/src/routing/AppRouter.tsx @@ -99,6 +99,7 @@ import { UsersOverviewPage, ViewEventDefinitionPage, SidecarFailureTrackingPage, + IndexSetFieldTypesPage, } from 'pages'; import AppConfig from 'util/AppConfig'; import { appPrefixed } from 'util/URLUtils'; @@ -211,6 +212,7 @@ const AppRouter = () => { { path: RoutePaths.SYSTEM.INDEX_SETS.CREATE, element: }, { path: RoutePaths.SYSTEM.INDEX_SETS.SHOW(':indexSetId'), element: }, { path: RoutePaths.SYSTEM.INDEX_SETS.CONFIGURATION(':indexSetId'), element: }, + { path: RoutePaths.SYSTEM.INDEX_SETS.FIELD_TYPES(':indexSetId'), element: }, { path: RoutePaths.SYSTEM.INDICES.LIST, element: }, !isCloud && ( diff --git a/graylog2-web-interface/src/routing/Routes.ts b/graylog2-web-interface/src/routing/Routes.ts index 8e7de51b5ac2..43cd53afb726 100644 --- a/graylog2-web-interface/src/routing/Routes.ts +++ b/graylog2-web-interface/src/routing/Routes.ts @@ -93,6 +93,7 @@ const Routes = { return `/system/index_sets/${indexSetId}/configuration`; }, SHOW: (indexSetId: string) => `/system/index_sets/${indexSetId}`, + FIELD_TYPES: (indexSetId: string) => `/system/index_sets/${indexSetId}/field-types`, CREATE: '/system/index_sets/create', }, INPUTS: '/system/inputs', diff --git a/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/ChangeFieldType.tsx b/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/ChangeFieldType.tsx index 9621da24487a..bcbf3e4e61a1 100644 --- a/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/ChangeFieldType.tsx +++ b/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/ChangeFieldType.tsx @@ -22,19 +22,23 @@ import { isFunction } from 'views/logic/aggregationbuilder/Series'; import type User from 'logic/users/User'; import AppConfig from 'util/AppConfig'; import isReservedField from 'views/logic/IsReservedField'; +import useInitialSelection from 'views/logic/fieldactions/ChangeFieldType/hooks/useInitialSelection'; +import useFiledTypes from 'views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypes'; const ChangeFieldType = ({ field, onClose, }: ActionComponentProps) => { const [show, setShow] = useState(true); - + const { data: { fieldTypes }, isLoading: isOptionsLoading } = useFiledTypes(); const handleOnClose = useCallback(() => { setShow(false); onClose(); }, [onClose]); - return show ? : null; + const initialSelection = useInitialSelection(); + + return show ? : null; }; const hasMappingPermission = (currentUser: User) => currentUser.permissions.includes('typemappings:edit') || currentUser.permissions.includes('*'); diff --git a/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/ChangeFieldTypeModal.test.tsx b/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/ChangeFieldTypeModal.test.tsx index 7150be8598c7..f2375498a2f2 100644 --- a/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/ChangeFieldTypeModal.test.tsx +++ b/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/ChangeFieldTypeModal.test.tsx @@ -20,16 +20,13 @@ import selectEvent from 'react-select-event'; import asMock from 'helpers/mocking/AsMock'; import useFieldTypeMutation from 'views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypeMutation'; -import useFieldTypes from 'views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypes'; import useFieldTypeUsages from 'views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypeUsages'; -import type { FieldTypes } from 'views/logic/fieldactions/ChangeFieldType/types'; import useUserLayoutPreferences from 'components/common/EntityDataTable/hooks/useUserLayoutPreferences'; import { layoutPreferences } from 'fixtures/entityListLayoutPreferences'; import TestStoreProvider from 'views/test/TestStoreProvider'; import { loadViewsPlugin, unloadViewsPlugin } from 'views/test/testViewsPlugin'; import ChangeFieldTypeModal from 'views/logic/fieldactions/ChangeFieldType/ChangeFieldTypeModal'; import type { Attributes } from 'stores/PaginationTypes'; -import useInitialSelection from 'views/logic/fieldactions/ChangeFieldType/hooks/useInitialSelection'; import suppressConsole from 'helpers/suppressConsole'; const onCloseMock = jest.fn(); @@ -37,9 +34,24 @@ const renderChangeFieldTypeModal = ({ onClose = onCloseMock, field = 'field', show = true, + showSelectionTable = undefined, + initialFieldType = undefined, + fieldTypes = { + string: 'String type', + int: 'Number(int)', + bool: 'Boolean', + }, + initialSelectedIndexSets = ['id-1', 'id-2'], }) => render( - + , ); const attributes: Attributes = [ @@ -101,25 +113,10 @@ const paginatedFieldUsage = ({ isLoading: false, }); -const fieldTypes: { - data: { fieldTypes: FieldTypes }, - isLoading: boolean, -} = { - data: { - fieldTypes: { - string: 'String type', - int: 'Number(int)', - boolean: 'Boolean', - }, - }, - isLoading: false, -}; jest.mock('views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypeMutation', () => jest.fn()); -jest.mock('views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypes', () => jest.fn()); jest.mock('views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypeUsages', () => jest.fn()); jest.mock('views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypeMutation', () => jest.fn()); -jest.mock('views/logic/fieldactions/ChangeFieldType/hooks/useInitialSelection', () => jest.fn()); jest.mock('components/common/EntityDataTable/hooks/useUserLayoutPreferences'); describe('ChangeFieldTypeModal', () => { @@ -132,8 +129,6 @@ describe('ChangeFieldTypeModal', () => { beforeEach(() => { asMock(useFieldTypeMutation).mockReturnValue({ isLoading: false, putFiledTypeMutation: putFiledTypeMutationMock }); asMock(useFieldTypeUsages).mockReturnValue(paginatedFieldUsage); - asMock(useFieldTypes).mockReturnValue(fieldTypes); - asMock(useInitialSelection).mockReturnValue(['id-1', 'id-2']); asMock(useUserLayoutPreferences).mockReturnValue({ data: { @@ -194,4 +189,41 @@ describe('ChangeFieldTypeModal', () => { field: 'field', })); }); + + it('run putFiledTypeMutationMock with selected type and indexes when showSelectionTable false', async () => { + renderChangeFieldTypeModal({ initialSelectedIndexSets: ['id-2'] }); + + const typeSelect = await screen.findByLabelText(/select field type for field/i); + selectEvent.openMenu(typeSelect); + await selectEvent.select(typeSelect, 'Number(int)'); + + const submit = await screen.findByTitle(/change field type/i); + + fireEvent.click(submit); + + await waitFor(() => expect(putFiledTypeMutationMock).toHaveBeenCalledWith({ + indexSetSelection: ['id-2'], + newFieldType: 'int', + rotated: true, + field: 'field', + })); + }); + + it('Doesn\'t shows index sets data when showSelectionTable false', async () => { + renderChangeFieldTypeModal({ showSelectionTable: false }); + + expect(screen.queryByText('Stream Title 1')).not.toBeInTheDocument(); + expect(screen.queryByText('Stream Title 1')).not.toBeInTheDocument(); + expect(screen.queryByText('Index Title 1')).not.toBeInTheDocument(); + expect(screen.queryByText('String type')).not.toBeInTheDocument(); + expect(screen.queryByText('Stream Title 2')).not.toBeInTheDocument(); + expect(screen.queryByText('Index Title 2')).not.toBeInTheDocument(); + expect(screen.queryByText('Number(int)')).not.toBeInTheDocument(); + }); + + it('Use initial type', async () => { + renderChangeFieldTypeModal({ initialFieldType: 'bool' }); + + await screen.findByText('Boolean'); + }); }); diff --git a/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/ChangeFieldTypeModal.tsx b/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/ChangeFieldTypeModal.tsx index 22a0c9287b0f..7ea84023212e 100644 --- a/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/ChangeFieldTypeModal.tsx +++ b/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/ChangeFieldTypeModal.tsx @@ -20,7 +20,6 @@ import styled, { css } from 'styled-components'; import { Badge, BootstrapModalForm, Alert, Input } from 'components/bootstrap'; import { Select, Spinner } from 'components/common'; import StreamLink from 'components/streams/StreamLink'; -import useFiledTypes from 'views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypes'; import IndexSetsTable from 'views/logic/fieldactions/ChangeFieldType/IndexSetsTable'; import usePutFiledTypeMutation from 'views/logic/fieldactions/ChangeFieldType/hooks/useFieldTypeMutation'; import useStream from 'components/streams/hooks/useStream'; @@ -31,7 +30,7 @@ import useSendTelemetry from 'logic/telemetry/useSendTelemetry'; import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants'; import { getPathnameWithoutId } from 'util/URLUtils'; import useLocation from 'routing/useLocation'; -import useInitialSelection from 'views/logic/fieldactions/ChangeFieldType/hooks/useInitialSelection'; +import type { FieldTypes } from 'views/logic/fieldactions/ChangeFieldType/types'; const StyledSelect = styled(Select)` width: 400px; @@ -54,14 +53,19 @@ const failureStreamId = '000000000000000000000004'; type Props = { show: boolean, field: string, - onClose: () => void + onClose: () => void, + onSubmitCallback?: () => void, + initialSelectedIndexSets: Array, + showSelectionTable?: boolean, + fieldTypes: FieldTypes, + isOptionsLoading: boolean, + initialFieldType?: string, } -const ChangeFieldTypeModal = ({ show, onClose, field }: Props) => { +const ChangeFieldTypeModal = ({ show, onSubmitCallback, fieldTypes, isOptionsLoading, initialSelectedIndexSets, onClose, field, showSelectionTable, initialFieldType }: Props) => { const sendTelemetry = useSendTelemetry(); const [rotated, setRotated] = useState(true); const [newFieldType, setNewFieldType] = useState(null); - const { data: { fieldTypes }, isLoading: isOptionsLoading } = useFiledTypes(); const fieldTypeOptions = useMemo(() => Object.entries(fieldTypes) .sort(([, label1], [, label2]) => defaultCompare(label1, label2)) .map(([value, label]) => ({ @@ -73,7 +77,7 @@ const ChangeFieldTypeModal = ({ show, onClose, field }: Props) => { const [indexSetSelection, setIndexSetSelection] = useState>(); const { putFiledTypeMutation } = usePutFiledTypeMutation(); - const initialSelection = useInitialSelection(); + const { pathname } = useLocation(); const telemetryPathName = useMemo(() => getPathnameWithoutId(pathname), [pathname]); const onSubmit = useCallback((e: React.FormEvent) => { @@ -91,13 +95,13 @@ const ChangeFieldTypeModal = ({ show, onClose, field }: Props) => { { value: 'change-field-type', rotated, - isAllIndexesSelected: indexSetSelection.length === initialSelection.length, + isAllIndexesSelected: indexSetSelection.length === initialSelectedIndexSets.length, }, }); onClose(); - }); - }, [field, indexSetSelection, initialSelection.length, newFieldType, onClose, putFiledTypeMutation, rotated, sendTelemetry, telemetryPathName]); + }).then(() => onSubmitCallback && onSubmitCallback()); + }, [field, indexSetSelection, initialSelectedIndexSets.length, newFieldType, onClose, onSubmitCallback, putFiledTypeMutation, rotated, sendTelemetry, telemetryPathName]); const onChangeFieldType = useCallback((value: string) => { setNewFieldType(value); @@ -112,6 +116,14 @@ const ChangeFieldTypeModal = ({ show, onClose, field }: Props) => { onClose(); }, [onClose, sendTelemetry, telemetryPathName]); + useEffect(() => { + setIndexSetSelection(initialSelectedIndexSets); + }, [initialSelectedIndexSets, setIndexSetSelection]); + + useEffect(() => { + if (initialFieldType) setNewFieldType(initialFieldType); + }, [initialFieldType]); + return ( Change {field} Field Type } submitButtonText="Change field type" @@ -137,11 +149,17 @@ const ChangeFieldTypeModal = ({ show, onClose, field }: Props) => { inputProps={{ 'aria-label': `Select Field Type For ${field}` }} required /> - Select Targeted Index Sets -

- By default the {newFieldType ? {newFieldType} : 'selected'} field type will be set for the {field} field in all index sets of the current message/search. You can select for which index sets you would like to make the change. -

- + { + showSelectionTable && ( + <> + Select Targeted Index Sets +

+ By default the {newFieldType ? {newFieldType} : 'selected'} field type will be set for the {field} field in all index sets of the current message/search. You can select for which index sets you would like to make the change. +

+ + + ) + } Select Rotation Strategy

To see and use the {newFieldType ? {newFieldType} : 'selected field type'} as a field type for {field}, you have to rotate indices. You can automatically rotate affected indices after submitting this form or do that manually later. @@ -157,4 +175,10 @@ const ChangeFieldTypeModal = ({ show, onClose, field }: Props) => { ); }; +ChangeFieldTypeModal.defaultProps = { + showSelectionTable: true, + onSubmitCallback: undefined, + initialFieldType: null, +}; + export default ChangeFieldTypeModal; diff --git a/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/IndexSetsTable.tsx b/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/IndexSetsTable.tsx index c7f979fa52db..3b63c039af5a 100644 --- a/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/IndexSetsTable.tsx +++ b/graylog2-web-interface/src/views/logic/fieldactions/ChangeFieldType/IndexSetsTable.tsx @@ -14,7 +14,7 @@ * along with this program. If not, see * . */ -import React, { useMemo, useCallback, useState, useEffect } from 'react'; +import React, { useMemo, useCallback, useState } from 'react'; import styled from 'styled-components'; import { @@ -71,10 +71,6 @@ const IndexSetsTable = ({ field, setIndexSetSelection, fieldTypes, initialSelect const { mutate: updateTableLayout } = useUpdateUserLayoutPreferences(ENTITY_TABLE_ID); - useEffect(() => { - setIndexSetSelection(initialSelection); - }, [initialSelection, setIndexSetSelection]); - const onPageChange = useCallback( (newPage: number, newPageSize: number) => { if (newPage) { From a95c74cc6224c1e8f91baf1d49fc991de3b8bd03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:09:14 +0100 Subject: [PATCH 4/4] Bump io.netty:netty-bom from 4.1.100.Final to 4.1.101.Final (#17244) Bumps [io.netty:netty-bom](https://github.com/netty/netty) from 4.1.100.Final to 4.1.101.Final. - [Commits](https://github.com/netty/netty/compare/netty-4.1.100.Final...netty-4.1.101.Final) --- updated-dependencies: - dependency-name: io.netty:netty-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b365c931066c..c9ddc6ddc897 100644 --- a/pom.xml +++ b/pom.xml @@ -152,7 +152,7 @@ 2.10.1.4 4.8.1-1 0.13 - 4.1.100.Final + 4.1.101.Final 2.0.62.Final 4.12.0 2.3