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' && ( renewDatanodeCertificate(dataNode.node_id)}>Renew certificate - {!isDatanodeRunning && startDataNode(dataNode.node_id)}>Start} + {!isDatanodeRunning && Start} {isDatanodeRunning && handleAction(DIALOG_TYPES.STOP)}>Stop} {isDatanodeRemoved && handleAction(DIALOG_TYPES.REJOIN)}>Rejoin} {(!isDatanodeRemoved || isRemovingDatanode) && handleAction(DIALOG_TYPES.REMOVE)}>Remove} @@ -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 ( <> - bulkStartDataNode(selectedEntities, setSelectedEntities)}>Start + Start setShowDialogType('STOP')}>Stop setShowDialogType('REMOVE')}>Remove 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;