Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Datanode List Page UI #17161

Merged
merged 53 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
2a32d57
cherry picked new datanode page
gally47 Nov 2, 2023
788a620
initial setup for datanode page navigation
gally47 Nov 2, 2023
83080af
added DataNodesPageNavigation to pages
gally47 Nov 2, 2023
5016d55
DataNodeList component initial setup
gally47 Nov 9, 2023
38383e7
fix build
gally47 Nov 9, 2023
ce76557
datanode list initial prototype
gally47 Nov 14, 2023
bf0c092
fix eslint issues
gally47 Nov 14, 2023
32ddf34
add datanode list components
gally47 Nov 16, 2023
22a87a3
fix eslint issues
gally47 Nov 16, 2023
aebb541
add new data node button
gally47 Nov 20, 2023
6bab214
prepare column components
gally47 Nov 20, 2023
591e89d
DataNodeStatusCell component
gally47 Nov 20, 2023
32c91b8
useDataNodes return type
gally47 Nov 20, 2023
579755f
fix build
gally47 Nov 20, 2023
bc0bd69
fix build errors
gally47 Nov 20, 2023
ad266d9
fix eslint issues
gally47 Nov 21, 2023
9aa0efb
initial api integration
gally47 Nov 23, 2023
8a89d7e
refetch on change
gally47 Nov 23, 2023
d56b1bc
added removeDataNode & rejoinDataNode endpoints
gally47 Nov 23, 2023
1a67ea0
datanode actions integration progress
gally47 Nov 24, 2023
fb41b14
cleanup DataNodeStatusCell
gally47 Nov 27, 2023
13ab565
DataNodeStatusCell using data_node_status
gally47 Nov 27, 2023
bb16ec0
refetch list every 5s
gally47 Nov 27, 2023
0593add
cleanup
gally47 Nov 27, 2023
2cdc7a3
add datanode details page
ousmaneo Nov 28, 2023
493a46d
update datanode page
ousmaneo Nov 28, 2023
2987d3b
add todo
ousmaneo Nov 28, 2023
5bc3b1a
add license header
ousmaneo Nov 28, 2023
fad89c4
fix linter
ousmaneo Nov 29, 2023
48ce2b7
fix linter
ousmaneo Nov 30, 2023
fb2f804
Updating yarn lockfile (#17528)
github-actions[bot] Nov 30, 2023
e5a618e
replace quotes
ousmaneo Nov 30, 2023
539b157
add confirm dialog for actions
ousmaneo Dec 1, 2023
2399825
cleanup status cell
ousmaneo Dec 1, 2023
f48d388
handle certificate renewal
ousmaneo Dec 1, 2023
993b8c1
remove bulk actions
ousmaneo Dec 1, 2023
ef3f62d
add start/stop datanode
ousmaneo Dec 1, 2023
eb8192b
show leader and add cert info to details
ousmaneo Dec 1, 2023
7e0cc7d
remove is_master
ousmaneo Dec 1, 2023
0bb2c18
use correct action on start button
moesterheld Dec 7, 2023
5645dec
fix spelling
ousmaneo Dec 19, 2023
d3b7c1b
fix review
ousmaneo Dec 19, 2023
c75106c
update Datatable QueryHelper example to use ReactNode
ousmaneo Dec 19, 2023
ae9ae62
remove DataNodeBulkActions
ousmaneo Dec 19, 2023
93cb1a0
fix query by name
moesterheld Dec 19, 2023
683ffd6
add check for removal of running nodes
moesterheld Dec 19, 2023
c33df84
remove done todo
moesterheld Dec 19, 2023
43aad76
fix reset to do a complete restart
moesterheld Dec 19, 2023
71a905a
make status and cert_valid_until not sortable
ousmaneo Dec 20, 2023
c33239d
update spelling
ousmaneo Dec 20, 2023
b8f203c
Update graylog2-web-interface/src/components/datanode/DataNodeList/Da…
grotlue Dec 20, 2023
237facd
Update graylog2-web-interface/src/components/datanode/hooks/useDataNo…
grotlue Dec 20, 2023
c9db05f
show remove and rejoin for relevant state
ousmaneo Dec 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading