From 251fb8861b462baec84b5204d645c2f24206e81c Mon Sep 17 00:00:00 2001
From: Mohamed OULD HOCINE <106236152+gally47@users.noreply.github.com>
Date: Fri, 20 Sep 2024 17:05:37 +0200
Subject: [PATCH] Fix new Datanode EntityDataTable issues (#20495)
* add DataNodeBulkActions back
* changelog file
* unselect after bulk action success
* add back refetch interval
* refetch datanodes after row action
* refetch datanodes after bulk action
* add clear timeout to sleep function
---
changelog/unreleased/issue-20481.toml | 5 +++
.../datanode/DataNodeList/DataNodeActions.tsx | 31 +++++++++++++++---
.../DataNodeList/DataNodeBulkActions.tsx | 32 ++++++++++++++++---
.../datanode/DataNodeList/DataNodeList.tsx | 3 ++
.../components/datanode/hooks/useDataNodes.ts | 6 ++++
graylog2-web-interface/src/logic/sleep.ts | 21 ++++++++++++
6 files changed, 90 insertions(+), 8 deletions(-)
create mode 100644 changelog/unreleased/issue-20481.toml
create mode 100644 graylog2-web-interface/src/logic/sleep.ts
diff --git a/changelog/unreleased/issue-20481.toml b/changelog/unreleased/issue-20481.toml
new file mode 100644
index 000000000000..b5e9ed5849fb
--- /dev/null
+++ b/changelog/unreleased/issue-20481.toml
@@ -0,0 +1,5 @@
+type="f"
+message="Fix new Datanode EntityDataTable issues"
+
+issues=["20481"]
+pulls=["20495"]
diff --git a/graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeActions.tsx b/graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeActions.tsx
index 05131eafa8a5..e1dc6304c607 100644
--- a/graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeActions.tsx
+++ b/graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeActions.tsx
@@ -15,13 +15,15 @@
* .
*/
import * as React from 'react';
-import { useState } from 'react';
+import { useState, useRef } from 'react';
import styled from 'styled-components';
import { ConfirmDialog } from 'components/common';
import { Button, MenuItem } from 'components/bootstrap';
import type { DataNode } from 'preflight/types';
import { MoreActions } from 'components/common/EntityDataTable';
+import { useTableFetchContext } from 'components/common/PaginatedEntityTable';
+import sleep from 'logic/sleep';
import DataNodeLogsDialog from './DataNodeLogsDialog';
@@ -68,6 +70,26 @@ const DataNodeActions = ({ dataNode, displayAs }: Props) => {
const [showLogsDialog, setShowLogsDialog] = useState(false);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [dialogType, setDialogType] = useState(null);
+ const { refetch } = useTableFetchContext();
+ const statusTimeout = useRef>();
+
+ const sleepAndClearTimer = async () => {
+ if (statusTimeout.current) {
+ clearTimeout(statusTimeout.current);
+ }
+
+ statusTimeout.current = await sleep(1000);
+ };
+
+ const refetchDatanodes = async () => {
+ await sleepAndClearTimer();
+ await refetch();
+ };
+
+ const handleStartDatanode = async () => {
+ await startDataNode(dataNode.node_id);
+ await refetchDatanodes();
+ };
const updateState = ({ show, type }) => {
setShowConfirmDialog(show);
@@ -93,8 +115,9 @@ const DataNodeActions = ({ dataNode, displayAs }: Props) => {
}
};
- const handleClearState = () => {
+ const handleClearState = async () => {
updateState({ show: false, type: null });
+ await refetchDatanodes();
};
const handleConfirm = () => {
@@ -131,7 +154,7 @@ const DataNodeActions = ({ dataNode, displayAs }: Props) => {
{displayAs === 'dropdown' && (
- {!isDatanodeRunning && }
+ {!isDatanodeRunning && }
{isDatanodeRunning && }
{isDatanodeRemoved && }
{(!isDatanodeRemoved || isRemovingDatanode) && }
@@ -140,7 +163,7 @@ const DataNodeActions = ({ dataNode, displayAs }: Props) => {
)}
{displayAs === 'buttons' && (
<>
- {!isDatanodeRunning && startDataNode(dataNode.node_id)} bsSize="small">Start}
+ {!isDatanodeRunning && Start}
{isDatanodeRunning && handleAction(DIALOG_TYPES.STOP)} bsSize="small">Stop}
{isDatanodeRemoved && handleAction(DIALOG_TYPES.REJOIN)} bsSize="small">Rejoin}
{(!isDatanodeRemoved || isRemovingDatanode) && handleAction(DIALOG_TYPES.REMOVE)} bsSize="small">Remove}
diff --git a/graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeBulkActions.tsx b/graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeBulkActions.tsx
index 7e513f8fd884..d2e42a3325a7 100644
--- a/graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeBulkActions.tsx
+++ b/graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeBulkActions.tsx
@@ -15,34 +15,58 @@
* .
*/
import * as React from 'react';
-import { useState } from 'react';
+import { useState, useRef } from 'react';
import MenuItem from 'components/bootstrap/MenuItem';
import BulkActionsDropdown from 'components/common/EntityDataTable/BulkActionsDropdown';
import useSelectedEntities from 'components/common/EntityDataTable/hooks/useSelectedEntities';
import ConfirmDialog from 'components/common/ConfirmDialog';
+import { useTableFetchContext } from 'components/common/PaginatedEntityTable';
+import sleep from 'logic/sleep';
import { bulkRemoveDataNode, bulkStartDataNode, bulkStopDataNode } from '../hooks/useDataNodes';
const DataNodeBulkActions = () => {
const { selectedEntities, setSelectedEntities } = useSelectedEntities();
const [showDialogType, setShowDialogType] = useState<'REMOVE'|'STOP'|null>(null);
+ const { refetch } = useTableFetchContext();
+ const statusTimeout = useRef>();
+
+ const sleepAndClearTimer = async () => {
+ if (statusTimeout.current) {
+ clearTimeout(statusTimeout.current);
+ }
+
+ statusTimeout.current = await sleep(1000);
+ };
+
+ const refetchDatanodes = async () => {
+ await sleepAndClearTimer();
+ await refetch();
+ };
+
+ const handleBulkStartDatanode = async () => {
+ await bulkStartDataNode(selectedEntities, setSelectedEntities);
+ await refetchDatanodes();
+ };
const CONFIRM_DIALOG = {
REMOVE: {
dialogTitle: 'Remove Data Nodes',
dialogBody: `Are you sure you want to remove the selected ${selectedEntities.length > 1 ? `${selectedEntities.length} Data Nodes` : 'Data Node'}?`,
- handleConfirm: () => {
+ handleConfirm: async () => {
bulkRemoveDataNode(selectedEntities, setSelectedEntities);
setShowDialogType(null);
+ await refetchDatanodes();
},
},
STOP: {
dialogTitle: 'Stop Data Nodes',
dialogBody: `Are you sure you want to stop the selected ${selectedEntities.length > 1 ? `${selectedEntities.length} Data Nodes` : 'Data Node'}?`,
- handleConfirm: () => {
+ handleConfirm: async () => {
bulkStopDataNode(selectedEntities, setSelectedEntities);
setShowDialogType(null);
+ await refetchDatanodes();
},
},
};
@@ -50,7 +74,7 @@ const DataNodeBulkActions = () => {
return (
<>
-
+
diff --git a/graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeList.tsx b/graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeList.tsx
index 9faf7800fdb0..1497c5e69995 100644
--- a/graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeList.tsx
+++ b/graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeList.tsx
@@ -26,6 +26,7 @@ import Routes from 'routing/Routes';
import DataNodeActions from './DataNodeActions';
import DataNodeStatusCell from './DataNodeStatusCell';
+import DataNodeBulkActions from './DataNodeBulkActions';
import { fetchDataNodes, keyFn } from '../hooks/useDataNodes';
@@ -76,9 +77,11 @@ const DataNodeList = () => (
)} />
)}
+ bulkSelection={{ actions: }}
entityActions={entityActions}
tableLayout={DEFAULT_LAYOUT}
fetchEntities={fetchDataNodes}
+ fetchOptions={{ refetchInterval: 5000 }}
keyFn={keyFn}
entityAttributesAreCamelCase={false}
columnRenderers={columnRenderers} />
diff --git a/graylog2-web-interface/src/components/datanode/hooks/useDataNodes.ts b/graylog2-web-interface/src/components/datanode/hooks/useDataNodes.ts
index 8f0315d53736..5860aa322664 100644
--- a/graylog2-web-interface/src/components/datanode/hooks/useDataNodes.ts
+++ b/graylog2-web-interface/src/components/datanode/hooks/useDataNodes.ts
@@ -28,6 +28,8 @@ export const bulkRemoveDataNode = async (entity_ids: string[], selectBackFailedE
try {
const { failures, successfully_performed } = await fetch('POST', qualifyUrl('/datanode/bulk_remove'), { entity_ids });
+ selectBackFailedEntities([]);
+
if (failures?.length) {
selectBackFailedEntities(failures.map(({ entity_id }) => entity_id));
}
@@ -48,6 +50,8 @@ export const bulkStartDataNode = async (entity_ids: string[], selectBackFailedEn
try {
const { failures, successfully_performed } = await fetch('POST', qualifyUrl('/datanode/bulk_start'), { entity_ids });
+ selectBackFailedEntities([]);
+
if (failures?.length) {
selectBackFailedEntities(failures.map(({ entity_id }) => entity_id));
}
@@ -68,6 +72,8 @@ export const bulkStopDataNode = async (entity_ids: string[], selectBackFailedEnt
try {
const { failures, successfully_performed } = await fetch('POST', qualifyUrl('/datanode/bulk_stop'), { entity_ids });
+ selectBackFailedEntities([]);
+
if (failures?.length) {
selectBackFailedEntities(failures.map(({ entity_id }) => entity_id));
}
diff --git a/graylog2-web-interface/src/logic/sleep.ts b/graylog2-web-interface/src/logic/sleep.ts
new file mode 100644
index 000000000000..6349c901d77e
--- /dev/null
+++ b/graylog2-web-interface/src/logic/sleep.ts
@@ -0,0 +1,21 @@
+/*
+ * 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
+ * .
+ */
+const sleep = async (milliseconds: number): Promise> => new Promise((resolve) => {
+ const timerID = setTimeout(() => resolve(timerID), milliseconds);
+});
+
+export default sleep;