Skip to content

Commit

Permalink
Datanode List Page UI (#17161)
Browse files Browse the repository at this point in the history
* cherry picked new datanode page

* initial setup for datanode page navigation

* added DataNodesPageNavigation to pages

* DataNodeList component initial setup

* fix build

* datanode list initial prototype

* fix eslint issues

* add datanode list components

* fix eslint issues

* add new data node button

* prepare column components

* DataNodeStatusCell component

* useDataNodes return type

* fix build

* fix build errors

* fix eslint issues

* initial api integration

* refetch on change

* added removeDataNode & rejoinDataNode endpoints

* datanode actions integration progress

* cleanup DataNodeStatusCell

* DataNodeStatusCell using data_node_status

* refetch list every 5s

* cleanup

* add datanode details page

* update datanode page

* add todo

* add license header

* fix linter

* fix linter

* Updating yarn lockfile (#17528)

Co-authored-by: Gary Bot <[email protected]>

* replace quotes

* add confirm dialog for actions

* cleanup status cell

* handle certificate renewal

* remove bulk actions

* add start/stop datanode

* show leader and add cert info to details

* remove is_master

* use correct action on start button

* fix spelling

* fix review

* update Datatable QueryHelper example to use ReactNode

* remove DataNodeBulkActions

* fix query by name

* add check for removal of running nodes

* remove done todo

* fix reset to do a complete restart

* make status and cert_valid_until not sortable

* update spelling

* Update graylog2-web-interface/src/components/datanode/DataNodeList/DataNodeActions.tsx

* Update graylog2-web-interface/src/components/datanode/hooks/useDataNodes.ts

* show remove and rejoin for relevant state

---------

Co-authored-by: Ousmane Samba <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Gary Bot <[email protected]>
Co-authored-by: Matthias Oesterheld <[email protected]>
Co-authored-by: Laura <[email protected]>
  • Loading branch information
6 people authored Dec 21, 2023
1 parent 16f3d66 commit f0a9ca2
Show file tree
Hide file tree
Showing 25 changed files with 775 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ public void onRemove() {
@Override
public void onReset() {
onEvent(ProcessEvent.RESET);
restart();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,6 @@ void checkRemovalStatus() {
if (health.getRelocatingShards() == 0) {
process.stop();
executorService.shutdown();
// TODO: set the state for this datanode to removed in nodes
// and include state in OpensearchProcessImpl.writeSeedHostsList
}
} catch (IOException | OpenSearchStatusException e) {
process.onEvent(ProcessEvent.HEALTH_CHECK_FAILED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public DataNodeDto removeNode(String nodeId) throws NodeNotFoundException {
if (nodeService.allActive().values().stream().anyMatch(n -> n.getDataNodeStatus() == DataNodeStatus.REMOVING)) {
throw new IllegalArgumentException("Only one data node can be removed at a time.");
}
if (node.getDataNodeStatus() != DataNodeStatus.AVAILABLE) {
throw new IllegalArgumentException("Only running data nodes can be removed from the cluster.");
}
DataNodeLifecycleEvent e = DataNodeLifecycleEvent.create(node.getNodeId(), DataNodeLifecycleTrigger.REMOVE);
clusterEventBus.post(e);
return node;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.graylog.events.notifications.NotificationDto;
import org.graylog.security.certutil.CertRenewalService;
import org.graylog2.cluster.Node;
import org.graylog2.cluster.NodeNotFoundException;
Expand Down Expand Up @@ -77,16 +76,13 @@ public class ClusterResource extends RestResource {

private static final ImmutableMap<String, SearchQueryField> SEARCH_FIELD_MAPPING = ImmutableMap.<String, SearchQueryField>builder()
.put("id", SearchQueryField.create("_id", SearchQueryField.Type.OBJECT_ID))
.put("title", SearchQueryField.create(NotificationDto.FIELD_TITLE))
.put("description", SearchQueryField.create(NotificationDto.FIELD_DESCRIPTION))
.put("hostname", SearchQueryField.create("hostname"))
.build();

private static final String DEFAULT_SORT_FIELD = "title";
private static final String DEFAULT_SORT_DIRECTION = "asc";
private static final List<EntityAttribute> attributes = List.of(
EntityAttribute.builder().id("title").title("Title").build(),
EntityAttribute.builder().id("description").title("Description").build(),
EntityAttribute.builder().id("type").title("Type").build()
EntityAttribute.builder().id("hostname").title("Name").build()
);
private static final EntityDefaults settings = EntityDefaults.builder()
.sort(Sorting.create(DEFAULT_SORT_FIELD, Sorting.Direction.valueOf(DEFAULT_SORT_DIRECTION.toUpperCase(Locale.ROOT))))
Expand All @@ -100,7 +96,7 @@ public ClusterResource(final NodeService nodeService,
this.nodeService = nodeService;
this.dataNodePaginatedService = dataNodePaginatedService;
this.certRenewalService = certRenewalService;
this.searchQueryParser = new SearchQueryParser(NotificationDto.FIELD_TITLE, SEARCH_FIELD_MAPPING);
this.searchQueryParser = new SearchQueryParser("hostname", SEARCH_FIELD_MAPPING);
this.nodeId = nodeId;
this.clusterId = clusterConfigService.getOrDefault(ClusterId.class, ClusterId.create(UUID.nilUUID().toString()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ type Props = Omit<BootstrapCheckboxProps, 'onChange'> & {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
}
const Checkbox = ({ onChange, ...props }: Props) => <BootstrapCheckbox onChange={onChange as unknown as BootstrapCheckboxProps['onChange']} {...props} />;

export default Checkbox;
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type CommonFields = keyof typeof COMMON_FIELD_MAP;
type Props = {
commonFields?: Array<CommonFields>,
fieldMap?: { [field: string]: string },
example?: string,
example?: React.ReactNode,
entityName?: string,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ import { Spinner } from 'components/common';
import { Alert, ListGroup, ListGroupItem, Button } from 'components/bootstrap';
import { defaultCompare } from 'logic/DefaultCompare';
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
import DataNodeBadge from 'components/datanode/DataNodeBadge';
import { Badge } from 'preflight/components/common';
import useLocation from 'routing/useLocation';
import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';

import DataNodeBadge from '../DataNodeList/DataNodeBadge';

const StyledList = styled(ListGroup)`
max-width: fit-content;
Expand Down Expand Up @@ -98,7 +99,7 @@ const provisioningWording = {
buttonStyle: 'success',
} as const;

const CertRenewalButton = ({ nodeId, status }: { nodeId: string, status: DataNode['status'] }) => {
export const CertRenewalButton = ({ nodeId, status }: { nodeId: string, status: DataNode['status'] }) => {
const sendTelemetry = useSendTelemetry();
const { pathname } = useLocation();
const [isRenewing, setIsRenewing] = useState(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* 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
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import { useState } from 'react';

import type { DataNode } from 'preflight/types';
import { ConfirmDialog } from 'components/common';
import { MenuItem } from 'components/bootstrap';
import OverlayDropdownButton from 'components/common/OverlayDropdownButton';
import { MORE_ACTIONS_TITLE, MORE_ACTIONS_HOVER_TITLE } from 'components/common/EntityDataTable/Constants';

import {
rejoinDataNode,
removeDataNode,
renewDatanodeCertificate,
stopDataNode,
startDataNode,
} from '../hooks/useDataNodes';

type Props = {
dataNode: DataNode,
};

const DIALOG_TYPES = {
STOP: 'stop',
REJOIN: 'rejoin',
REMOVE: 'remove',
RENEW_CERT: 'renew',
};

const DIALOG_TEXT = {
[DIALOG_TYPES.REJOIN]: {
dialogTitle: 'Rejoin Data Node',
dialogBody: (datanode: string) => `Are you sure you want to rejoin Data Node "${datanode}"?`,
},
[DIALOG_TYPES.REMOVE]: {
dialogTitle: 'Remove Data Node',
dialogBody: (datanode: string) => `Are you sure you want to remove Data Node "${datanode}"?`,
},
[DIALOG_TYPES.STOP]: {
dialogTitle: 'Stop Data Node',
dialogBody: (datanode: string) => `Are you sure you want to stop Data Node "${datanode}"?`,
},
};

const DataNodeActions = ({ dataNode }: Props) => {
const [showDialog, setShowDialog] = useState(false);
const [dialogType, setDialogType] = useState(null);

const updateState = ({ show, type }) => {
setShowDialog(show);
setDialogType(type);
};

const handleAction = (action) => {
switch (action) {
case DIALOG_TYPES.REJOIN:
updateState({ show: true, type: DIALOG_TYPES.REJOIN });

break;
case DIALOG_TYPES.REMOVE:
updateState({ show: true, type: DIALOG_TYPES.REMOVE });

break;
case DIALOG_TYPES.STOP:
updateState({ show: true, type: DIALOG_TYPES.STOP });

break;
default:
break;
}
};

const handleClearState = () => {
updateState({ show: false, type: null });
};

const handleConfirm = () => {
switch (dialogType) {
case 'rejoin':
rejoinDataNode(dataNode.node_id).then(() => {
handleClearState();
});

break;
case 'remove':
removeDataNode(dataNode.node_id).then(() => {
handleClearState();
});

break;
case 'stop':
stopDataNode(dataNode.node_id).then(() => {
handleClearState();
});

break;
default:
break;
}
};

const isDatanodeRunning = dataNode.data_node_status === 'AVAILABLE';
const isDatanodeRemoved = dataNode.data_node_status === 'REMOVED';
const isRemovingDatanode = dataNode.data_node_status === 'REMOVING';

return (
<>
<OverlayDropdownButton title={MORE_ACTIONS_TITLE}
bsSize="xsmall"
buttonTitle={MORE_ACTIONS_HOVER_TITLE}
disabled={false}
dropdownZIndex={1000}>
<MenuItem onSelect={() => renewDatanodeCertificate(dataNode.node_id)}>Renew certificate</MenuItem>
{!isDatanodeRunning && <MenuItem onSelect={() => startDataNode(dataNode.node_id)}>Start</MenuItem>}
{isDatanodeRunning && <MenuItem onSelect={() => handleAction(DIALOG_TYPES.STOP)}>Stop</MenuItem>}
{isDatanodeRemoved && <MenuItem onSelect={() => handleAction(DIALOG_TYPES.REJOIN)}>Rejoin</MenuItem>}
{(!isDatanodeRemoved || isRemovingDatanode) && <MenuItem onSelect={() => handleAction(DIALOG_TYPES.REMOVE)}>Remove</MenuItem>}
</OverlayDropdownButton>
{showDialog && (
<ConfirmDialog title={DIALOG_TEXT[dialogType].dialogTitle}
show
onConfirm={handleConfirm}
onCancel={handleClearState}>
{DIALOG_TEXT[dialogType].dialogBody(dataNode.hostname)}
</ConfirmDialog>
)}
</>
);
};

export default DataNodeActions;
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type { DataNodeStatus } from 'preflight/types';
import { Badge } from 'preflight/components/common';
import Icon from 'preflight/components/common/Icon';

import Spinner from '../common/Spinner';
import Spinner from '../../common/Spinner';

const NodeId = styled(Badge)`
margin-right: 3px;
Expand Down
Loading

0 comments on commit f0a9ca2

Please sign in to comment.