dispatch<any>(initAdvancedTabDialog(advanceDataUser));
break;
case ResourceKind.NODE:
- const dataComputeNode = getState().computeNodes.find(node => node.uuid === uuid);
+ const computeNodeResources = getState().resources;
+ const dataComputeNode = getResource<NodeResource>(uuid)(computeNodeResources);
const advanceDataComputeNode = advancedTabData({
uuid,
metadata: '',
// SPDX-License-Identifier: AGPL-3.0
import { Dispatch } from "redux";
-import { unionize, ofType, UnionOf } from "~/common/unionize";
import { RootState } from '~/store/store';
import { setBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
-import { ServiceRepository } from "~/services/services";
-import { NodeResource } from '~/models/node';
import { dialogActions } from '~/store/dialog/dialog-actions';
import { snackbarActions } from '~/store/snackbar/snackbar-actions';
import { navigateToRootProject } from '~/store/navigation/navigation-action';
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+import { getResource } from '~/store/resources/resources';
+import { ServiceRepository } from "~/services/services";
+import { NodeResource } from '~/models/node';
-export const computeNodesActions = unionize({
- SET_COMPUTE_NODES: ofType<NodeResource[]>(),
- REMOVE_COMPUTE_NODE: ofType<string>()
-});
-
-export type ComputeNodesActions = UnionOf<typeof computeNodesActions>;
+export const COMPUTE_NODE_PANEL_ID = "computeNodeId";
+export const computeNodesActions = bindDataExplorerActions(COMPUTE_NODE_PANEL_ID);
export const COMPUTE_NODE_REMOVE_DIALOG = 'computeNodeRemoveDialog';
export const COMPUTE_NODE_ATTRIBUTES_DIALOG = 'computeNodeAttributesDialog';
if (user && user.isAdmin) {
try {
dispatch(setBreadcrumbs([{ label: 'Compute Nodes' }]));
- const response = await services.nodeService.list();
- dispatch(computeNodesActions.SET_COMPUTE_NODES(response.items));
+ dispatch(computeNodesActions.REQUEST_ITEMS());
} catch (e) {
return;
}
export const openComputeNodeAttributesDialog = (uuid: string) =>
(dispatch: Dispatch, getState: () => RootState) => {
- const computeNode = getState().computeNodes.find(node => node.uuid === uuid);
+ const { resources } = getState();
+ const computeNode = getResource<NodeResource>(uuid)(resources);
dispatch(dialogActions.OPEN_DIALOG({ id: COMPUTE_NODE_ATTRIBUTES_DIALOG, data: { computeNode } }));
};
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
try {
await services.nodeService.delete(uuid);
- dispatch(computeNodesActions.REMOVE_COMPUTE_NODE(uuid));
+ dispatch(computeNodesActions.REQUEST_ITEMS());
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Compute node has been successfully removed.', hideDuration: 2000 }));
} catch (e) {
return;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { DataExplorerMiddlewareService, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from '~/store/data-explorer/data-explorer-middleware-service';
+import { RootState } from '~/store/store';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { updateResources } from '~/store/resources/resources-actions';
+import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { computeNodesActions } from '~/store/compute-nodes/compute-nodes-actions';
+import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
+import { ListResults } from '~/services/common-service/common-service';
+import { NodeResource } from '~/models/node';
+import { SortDirection } from '~/components/data-table/data-column';
+import { ComputeNodePanelColumnNames } from '~/views/compute-node-panel/compute-node-panel-root';
+
+export class ComputeNodeMiddlewareService extends DataExplorerMiddlewareService {
+ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ const state = api.getState();
+ const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+ try {
+ const response = await this.services.nodeService.list(getParams(dataExplorer));
+ api.dispatch(updateResources(response.items));
+ api.dispatch(setItems(response));
+ } catch {
+ api.dispatch(couldNotFetchLinks());
+ }
+ }
+}
+
+export const getParams = (dataExplorer: DataExplorer) => ({
+ ...dataExplorerToListParams(dataExplorer),
+ order: getOrder(dataExplorer)
+});
+
+const getOrder = (dataExplorer: DataExplorer) => {
+ const sortColumn = getSortColumn(dataExplorer);
+ const order = new OrderBuilder<NodeResource>();
+ if (sortColumn) {
+ const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+ ? OrderDirection.ASC
+ : OrderDirection.DESC;
+
+ const columnName = sortColumn && sortColumn.name === ComputeNodePanelColumnNames.UUID ? "uuid" : "modifiedAt";
+ return order
+ .addOrder(sortDirection, columnName)
+ .getOrder();
+ } else {
+ return order.getOrder();
+ }
+};
+
+export const setItems = (listResults: ListResults<NodeResource>) =>
+ computeNodesActions.SET_ITEMS({
+ ...listResultsToDataExplorerItemsMeta(listResults),
+ items: listResults.items.map(resource => resource.uuid),
+ });
+
+const couldNotFetchLinks = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch compute nodes.',
+ kind: SnackbarKind.ERROR
+ });
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { computeNodesActions, ComputeNodesActions } from '~/store/compute-nodes/compute-nodes-actions';
-import { NodeResource } from '~/models/node';
-
-export type ComputeNodesState = NodeResource[];
-
-const initialState: ComputeNodesState = [];
-
-export const computeNodesReducer = (state: ComputeNodesState = initialState, action: ComputeNodesActions): ComputeNodesState =>
- computeNodesActions.match(action, {
- SET_COMPUTE_NODES: nodes => nodes,
- REMOVE_COMPUTE_NODE: (uuid: string) => state.filter((computeNode) => computeNode.uuid !== uuid),
- default: () => state
- });
\ No newline at end of file
import { SshKeyResource } from '~/models/ssh-key';
import { VirtualMachinesResource } from '~/models/virtual-machines';
import { KeepServiceResource } from '~/models/keep-services';
-import { NodeResource } from '~/models/node';
import { ApiClientAuthorization } from '~/models/api-client-authorization';
export const contextMenuActions = unionize({
}));
};
-export const openComputeNodeContextMenu = (event: React.MouseEvent<HTMLElement>, computeNode: NodeResource) =>
+export const openComputeNodeContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
(dispatch: Dispatch) => {
dispatch<any>(openContextMenu(event, {
name: '',
- uuid: computeNode.uuid,
- ownerUuid: computeNode.ownerUuid,
+ uuid: resourceUuid,
+ ownerUuid: '',
kind: ResourceKind.NODE,
menuKind: ContextMenuKind.NODE
}));
export const LINK_REMOVE_DIALOG = 'linkRemoveDialog';
export const LINK_ATTRIBUTES_DIALOG = 'linkAttributesDialog';
+export const loadLinkPanel = () =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(setBreadcrumbs([{ label: 'Links' }]));
+ dispatch(linkPanelActions.REQUEST_ITEMS());
+ };
+
export const openLinkAttributesDialog = (uuid: string) =>
(dispatch: Dispatch, getState: () => RootState) => {
const { resources } = getState();
}));
};
-export const loadLinkPanel = () =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(setBreadcrumbs([{ label: 'Links' }]));
- dispatch(linkPanelActions.REQUEST_ITEMS());
- };
-
export const removeLink = (uuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
import { keepServicesReducer } from '~/store/keep-services/keep-services-reducer';
import { UserMiddlewareService } from '~/store/users/user-panel-middleware-service';
import { USERS_PANEL_ID } from '~/store/users/users-actions';
-import { computeNodesReducer } from '~/store/compute-nodes/compute-nodes-reducer';
import { apiClientAuthorizationsReducer } from '~/store/api-client-authorizations/api-client-authorizations-reducer';
import { LINK_PANEL_ID } from '~/store/link-panel/link-panel-actions';
import { LinkMiddlewareService } from '~/store/link-panel/link-panel-middleware-service';
+import { COMPUTE_NODE_PANEL_ID } from '~/store/compute-nodes/compute-nodes-actions';
+import { ComputeNodeMiddlewareService } from '~/store/compute-nodes/compute-nodes-middleware-service';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
const linkPanelMiddleware = dataExplorerMiddleware(
new LinkMiddlewareService(services, LINK_PANEL_ID)
);
+ const computeNodeMiddleware = dataExplorerMiddleware(
+ new ComputeNodeMiddlewareService(services, COMPUTE_NODE_PANEL_ID)
+ );
const middlewares: Middleware[] = [
routerMiddleware(history),
thunkMiddleware.withExtraArgument(services),
sharedWithMePanelMiddleware,
workflowPanelMiddleware,
userPanelMiddleware,
- linkPanelMiddleware
+ linkPanelMiddleware,
+ computeNodeMiddleware
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
virtualMachines: virtualMachinesReducer,
repositories: repositoriesReducer,
keepServices: keepServicesReducer,
- computeNodes: computeNodesReducer,
apiClientAuthorizations: apiClientAuthorizationsReducer
});
import { loadKeepServicesPanel } from '~/store/keep-services/keep-services-actions';
import { loadUsersPanel, userBindedActions } from '~/store/users/users-actions';
import { loadLinkPanel, linkPanelActions } from '~/store/link-panel/link-panel-actions';
+import { loadComputeNodesPanel, computeNodesActions } from '~/store/compute-nodes/compute-nodes-actions';
import { linkPanelColumns } from '~/views/link-panel/link-panel-root';
import { userPanelColumns } from '~/views/user-panel/user-panel';
-import { loadComputeNodesPanel } from '~/store/compute-nodes/compute-nodes-actions';
+import { computeNodePanelColumns } from '~/views/compute-node-panel/compute-node-panel-root';
import { loadApiClientAuthorizationsPanel } from '~/store/api-client-authorizations/api-client-authorizations-actions';
export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
dispatch(searchResultsPanelActions.SET_COLUMNS({ columns: searchResultsPanelColumns }));
dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
+ dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
dispatch<any>(initSidePanelTree());
if (router.location) {
const match = matchRootRoute(router.location.pathname);
import { LinkResource } from '~/models/link';
import { navigateTo } from '~/store/navigation/navigation-action';
import { Link } from 'react-router-dom';
+import { NodeResource } from '../../models/node';
+import { NodeInfo } from '~/models/node';
const renderName = (item: { name: string; uuid: string, kind: string }) =>
<Grid container alignItems="center" wrap="nowrap" spacing={16}>
return resource || { username: '' };
})(renderUsername);
+// Compute Node Resources
+const renderNodeDate = (date?: string) =>
+ <Typography noWrap>{formatDate(date) || '(none)'}</Typography>;
+
+const renderNodeData = (property?: string) =>
+ <Typography noWrap>{property || '(none)'}</Typography>;
+
+const renderNodeInfo = (item: { info: NodeInfo }) =>
+ <Typography>
+ {JSON.stringify(item.info, null, 4)}
+ </Typography>;
+
+export const ResourceNodeInfo = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<NodeResource>(props.uuid)(state.resources);
+ return resource || { info: {} };
+ })(renderNodeInfo);
+
+export const ResourceNodeDomain = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<NodeResource>(props.uuid)(state.resources);
+ return { property: resource ? resource.domain : '' };
+ })((props: { property: string }) => renderNodeData(props.property));
+
+export const ResourceNodeFirstPingAt = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<NodeResource>(props.uuid)(state.resources);
+ return { date: resource ? resource.firstPingAt : '' };
+ })((props: { date: string }) => renderNodeDate(props.date));
+
+export const ResourceNodeHostname = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<NodeResource>(props.uuid)(state.resources);
+ return { property: resource ? resource.hostname : '' };
+ })((props: { property: string }) => renderNodeData(props.property));
+
+export const ResourceNodeIpAddress = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<NodeResource>(props.uuid)(state.resources);
+ return { property: resource ? resource.ipAddress : '' };
+ })((props: { property: string }) => renderNodeData(props.property));
+
+export const ResourceNodeJobUuid = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<NodeResource>(props.uuid)(state.resources);
+ return { property: resource ? resource.jobUuid : '' };
+ })((props: { property: string }) => renderNodeData(props.property));
+
+export const ResourceNodeLastPingAt = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<NodeResource>(props.uuid)(state.resources);
+ return { date: resource ? resource.lastPingAt : '' };
+ })((props: { date: string }) => renderNodeDate(props.date));
+
// Links Resources
const renderLinkName = (item: { name: string }) =>
<Typography noWrap>{item.name || '(none)'}</Typography>;
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
+import { ShareMeIcon } from '~/components/icon/icon';
+import { DataExplorer } from '~/views-components/data-explorer/data-explorer';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { COMPUTE_NODE_PANEL_ID } from '~/store/compute-nodes/compute-nodes-actions';
+import { DataColumns } from '~/components/data-table/data-table';
+import { SortDirection } from '~/components/data-table/data-column';
+import { createTree } from '~/models/tree';
import {
- StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, Table,
- TableHead, TableRow, TableCell, TableBody, Tooltip, IconButton
-} from '@material-ui/core';
-import { ArvadosTheme } from '~/common/custom-theme';
-import { MoreOptionsIcon } from '~/components/icon/icon';
-import { NodeResource } from '~/models/node';
-import { formatDate } from '~/common/formatters';
+ ResourceUuid, ResourceNodeInfo, ResourceNodeDomain, ResourceNodeHostname, ResourceNodeJobUuid,
+ ResourceNodeFirstPingAt, ResourceNodeLastPingAt, ResourceNodeIpAddress
+} from '~/views-components/data-explorer/renderers';
+import { ResourcesState } from '~/store/resources/resources';
-type CssRules = 'root' | 'tableRow';
+export enum ComputeNodePanelColumnNames {
+ INFO = 'Info',
+ UUID = 'UUID',
+ DOMAIN = 'Domain',
+ FIRST_PING_AT = 'First ping at',
+ HOSTNAME = 'Hostname',
+ IP_ADDRESS = 'IP Address',
+ JOB = 'Job',
+ LAST_PING_AT = 'Last ping at'
+}
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- root: {
- width: '100%',
- overflow: 'auto'
+export const computeNodePanelColumns: DataColumns<string> = [
+ {
+ name: ComputeNodePanelColumnNames.INFO,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceNodeInfo uuid={uuid} />
+ },
+ {
+ name: ComputeNodePanelColumnNames.UUID,
+ selected: true,
+ configurable: true,
+ sortDirection: SortDirection.NONE,
+ filters: createTree(),
+ render: uuid => <ResourceUuid uuid={uuid} />
+ },
+ {
+ name: ComputeNodePanelColumnNames.DOMAIN,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceNodeDomain uuid={uuid} />
+ },
+ {
+ name: ComputeNodePanelColumnNames.FIRST_PING_AT,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceNodeFirstPingAt uuid={uuid} />
+ },
+ {
+ name: ComputeNodePanelColumnNames.HOSTNAME,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceNodeHostname uuid={uuid} />
+ },
+ {
+ name: ComputeNodePanelColumnNames.IP_ADDRESS,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceNodeIpAddress uuid={uuid} />
+ },
+ {
+ name: ComputeNodePanelColumnNames.JOB,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceNodeJobUuid uuid={uuid} />
},
- tableRow: {
- '& th': {
- whiteSpace: 'nowrap'
- }
+ {
+ name: ComputeNodePanelColumnNames.LAST_PING_AT,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceNodeLastPingAt uuid={uuid} />
}
-});
+];
export interface ComputeNodePanelRootActionProps {
- openRowOptions: (event: React.MouseEvent<HTMLElement>, computeNode: NodeResource) => void;
+ onItemClick: (item: string) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
+ onItemDoubleClick: (item: string) => void;
}
export interface ComputeNodePanelRootDataProps {
- computeNodes: NodeResource[];
- hasComputeNodes: boolean;
+ resources: ResourcesState;
}
-type ComputeNodePanelRootProps = ComputeNodePanelRootActionProps & ComputeNodePanelRootDataProps & WithStyles<CssRules>;
+type ComputeNodePanelRootProps = ComputeNodePanelRootActionProps & ComputeNodePanelRootDataProps;
-export const ComputeNodePanelRoot = withStyles(styles)(
- ({ classes, hasComputeNodes, computeNodes, openRowOptions }: ComputeNodePanelRootProps) =>
- <Card className={classes.root}>
- <CardContent>
- {hasComputeNodes && <Grid container direction="row">
- <Grid item xs={12}>
- <Table>
- <TableHead>
- <TableRow className={classes.tableRow}>
- <TableCell>Info</TableCell>
- <TableCell>UUID</TableCell>
- <TableCell>Domain</TableCell>
- <TableCell>First ping at</TableCell>
- <TableCell>Hostname</TableCell>
- <TableCell>IP Address</TableCell>
- <TableCell>Job</TableCell>
- <TableCell>Last ping at</TableCell>
- <TableCell />
- </TableRow>
- </TableHead>
- <TableBody>
- {computeNodes.map((computeNode, index) =>
- <TableRow key={index} className={classes.tableRow}>
- <TableCell>{JSON.stringify(computeNode.info, null, 4)}</TableCell>
- <TableCell>{computeNode.uuid}</TableCell>
- <TableCell>{computeNode.domain}</TableCell>
- <TableCell>{formatDate(computeNode.firstPingAt) || '(none)'}</TableCell>
- <TableCell>{computeNode.hostname || '(none)'}</TableCell>
- <TableCell>{computeNode.ipAddress || '(none)'}</TableCell>
- <TableCell>{computeNode.jobUuid || '(none)'}</TableCell>
- <TableCell>{formatDate(computeNode.lastPingAt) || '(none)'}</TableCell>
- <TableCell>
- <Tooltip title="More options" disableFocusListener>
- <IconButton onClick={event => openRowOptions(event, computeNode)}>
- <MoreOptionsIcon />
- </IconButton>
- </Tooltip>
- </TableCell>
- </TableRow>)}
- </TableBody>
- </Table>
- </Grid>
- </Grid>}
- </CardContent>
- </Card>
-);
\ No newline at end of file
+export const ComputeNodePanelRoot = (props: ComputeNodePanelRootProps) => {
+ return <DataExplorer
+ id={COMPUTE_NODE_PANEL_ID}
+ onRowClick={props.onItemClick}
+ onRowDoubleClick={props.onItemDoubleClick}
+ onContextMenu={props.onContextMenu}
+ contextMenuColumn={true}
+ hideColumnSelector
+ hideSearchInput
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={ShareMeIcon}
+ messages={['Your compute node list is empty.']} />
+ } />;
+};
\ No newline at end of file
import { RootState } from '~/store/store';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
-import { } from '~/store/compute-nodes/compute-nodes-actions';
import {
ComputeNodePanelRoot,
ComputeNodePanelRootDataProps,
ComputeNodePanelRootActionProps
} from '~/views/compute-node-panel/compute-node-panel-root';
-import { openComputeNodeContextMenu } from '~/store/context-menu/context-menu-actions';
+import { openComputeNodeContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
const mapStateToProps = (state: RootState): ComputeNodePanelRootDataProps => {
return {
- computeNodes: state.computeNodes,
- hasComputeNodes: state.computeNodes.length > 0
+ resources: state.resources
};
};
const mapDispatchToProps = (dispatch: Dispatch): ComputeNodePanelRootActionProps => ({
- openRowOptions: (event, computeNode) => {
- dispatch<any>(openComputeNodeContextMenu(event, computeNode));
- }
+ onContextMenu: (event, resourceUuid) => {
+ dispatch<any>(openComputeNodeContextMenu(event, resourceUuid));
+ },
+ onItemClick: (resourceUuid: string) => { return; },
+ onItemDoubleClick: uuid => { return; }
});
export const ComputeNodePanel = connect(mapStateToProps, mapDispatchToProps)(ComputeNodePanelRoot);
\ No newline at end of file