Skip to content

Commit

Permalink
Added Indicators (Warning labels) in the left navigation bar and also…
Browse files Browse the repository at this point in the history
… in clusters table to communicate cluster reachability issues with the users. Added button to test connectivity and display current reachability information in the table along with last reachable date time and also error message from cluster if the cluster is currently unreachable
  • Loading branch information
ydahal1 committed Dec 9, 2024
1 parent 7d134a5 commit fd229ea
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { Table, Space, message, Popconfirm } from 'antd';
import { EyeOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import React, { useState } from 'react';
import { Table, Space, message, Popconfirm, Button, Tooltip } from 'antd';
import { EyeOutlined, EditOutlined, DeleteOutlined, CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';

import { deleteCluster } from './clusterUtils';
import { deleteCluster, pingExistingCluster, getCluster } from './clusterUtils';
import { formatDateTime } from '../../common/CommonUtil';

function ClustersTable({
clusters,
Expand All @@ -11,12 +12,60 @@ function ClustersTable({
setSelectedCluster,
setDisplayEditClusterModal,
}) {
// States
const [testingConnection, setTestingConnection] = useState(null);

//Columns
const columns = [
{
title: 'Name',
dataIndex: 'name',
},
{
status: 'Status',
dataIndex: 'status',
render: (text, record) => {
if (testingConnection === record.id) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<div>{text}</div>
<span className="Clusters__statusText">Testing ...</span>
</div>
);
} else if (record.reachabilityInfo && record.reachabilityInfo.reachable === false) {
return (
<Tooltip
overlayClassName="clustersTable__toolTip_reachabilityStatus"
title={
<div>
<div>Last Reachable : {formatDateTime(record.reachabilityInfo?.lastReachableAt)}</div>
<div>Issue: {record.reachabilityInfo?.unReachableMessage}</div>
</div>
}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<div>{text}</div>
<CloseCircleFilled style={{ color: 'red', fontSize: '1.2rem' }} />
<span className="Clusters__statusText">Unreachable</span>
</div>
</Tooltip>
);
} else {
return (
<Tooltip
title={<div>Last Reachable : {formatDateTime(record.reachabilityInfo?.lastReachableAt)}</div>}
overlayClassName="clustersTable__toolTip_reachabilityStatus">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<div>{text}</div>
<>
<CheckCircleFilled style={{ color: 'green', fontSize: '1.2rem' }} />
<span className="Clusters__statusText">Reachable</span>
</>
</div>
</Tooltip>
);
}
},
},
{
title: 'Thor host',
dataIndex: 'thor_host',
Expand Down Expand Up @@ -49,6 +98,13 @@ function ClustersTable({
title="Are you sure you want to delete this cluster? This action will permanently remove all cluster details and any recorded usage history.">
<DeleteOutlined />
</Popconfirm>
<Button
type="primary"
size="small"
onClick={() => handleTestConnection(record)}
disabled={testingConnection === record.id ? true : false}>
Test Connection
</Button>
</Space>
),
},
Expand Down Expand Up @@ -76,6 +132,27 @@ function ClustersTable({
setSelectedCluster(record);
setDisplayEditClusterModal(true);
};

// Handle test connection
const handleTestConnection = async (record) => {
try {
setTestingConnection(record.id);
await pingExistingCluster({ clusterId: record.id });
} catch (e) {
message.error('Failed to establish connection with cluster');
} finally {
setTestingConnection(false);
}

// Get updated cluster and set it
try {
const updatedCluster = await getCluster(record.id);
setClusters((clusters) => clusters.map((cluster) => (cluster.id === record.id ? updatedCluster : cluster)));
} catch (err) {
message.error('Failed to get updated cluster');
}
};

return <Table columns={columns} dataSource={clusters} size="small" rowKey={(record) => record.id} />;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ export const pingCluster = async ({ clusterInfo, abortController }) => {
return response.status;
};

//Ping existing cluster
export const pingExistingCluster = async ({ clusterId }) => {
const payload = {
method: 'Get',
headers: authHeader(),
};

const response = await fetch(`/api/cluster/pingExistingCluster/${clusterId}`, payload);

if (response.status !== 200 && response.status !== 401) {
throw new Error('Failed to establish connection with cluster');
}
return response.status;
};

//Add cluster
export const addCluster = async ({ clusterInfo, abortController }) => {
const payload = {
Expand Down Expand Up @@ -113,6 +128,23 @@ export const getConfigurationDetails = async () => {
return responseJson.data; // {instanceName: 'Tombolo', environment: 'production'}
};

// Get cluster details by ID
export const getCluster = async (id) => {
const payload = {
method: 'GET',
headers: authHeader(),
};

const response = await fetch(`/api/cluster/${id}`, payload);

if (!response.ok) {
throw new Error('Failed to fetch cluster details');
}

const responseJson = await response.json();
return responseJson.data;
};

export const allStepsToAddCluster = [
{ step: 1, message: 'Authenticate cluster' },
{ step: 2, message: 'Select default engine' },
Expand Down
13 changes: 13 additions & 0 deletions Tombolo/client-reactjs/src/components/admin/clusters/clusters.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,16 @@
color: var(--primary);
font-weight: 600;
}

.Clusters__statusText {
color: var(--primary);
}

.Clusters__statusText:hover {
cursor: pointer;
}

.clustersTable__toolTip_reachabilityStatus {
min-width: 320px;
max-width: 400px;
}
15 changes: 12 additions & 3 deletions Tombolo/client-reactjs/src/components/layout/LeftNav.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { Layout, Menu, Typography } from 'antd';
import { Layout, Menu, Tooltip, Typography } from 'antd';
import {
DashboardOutlined,
FileSearchOutlined,
Expand All @@ -14,6 +14,7 @@ import {
ApiOutlined,
BellOutlined,
FolderOutlined,
WarningFilled,
} from '@ant-design/icons';

import { hasEditPermission } from '../common/AuthUtil.js';
Expand Down Expand Up @@ -106,8 +107,8 @@ class LeftNav extends Component {
const integrations = this.props?.integrations || [];
const disabled = applicationId === '' ? true : false;
const clusterDisabled = this.props?.clusters?.length === 0 ? true : false;

const asrActive = integrations.some((i) => i.name === 'ASR' && i.application_id === applicationId);
const clusterConnectionIssue = this.props?.clusters?.some((c) => c.reachabilityInfo?.reachable === false);

if (!this.props.loggedIn || !this.props.user || Object.getOwnPropertyNames(this.props.user).length == 0) {
return null;
Expand Down Expand Up @@ -331,8 +332,16 @@ class LeftNav extends Component {
</span>
) : (
<Link ref={this.props.clusterLinkRef} style={{ color: 'rgba(255, 255, 255, .65)' }} to={'/admin/clusters'}>
<ClusterOutlined style={{ color: 'rgba(255, 255, 255, .65)' }} />
<Tooltip
placement="right"
arrow={false}
overlayStyle={{ left: 35 }}
open={this.props.collapsed && clusterConnectionIssue ? true : false}
title={<WarningFilled style={{ color: 'yellow', marginLeft: '1rem' }} />}>
<ClusterOutlined style={{ color: 'rgba(255, 255, 255, .65)' }} />
</Tooltip>
<span style={{ marginLeft: '1rem', color: 'rgb(255, 255, 255, .65)' }}>Clusters</span>
{clusterConnectionIssue && <WarningFilled style={{ color: 'yellow', marginLeft: '1rem' }} />}
</Link>
)}
</>,
Expand Down
5 changes: 2 additions & 3 deletions Tombolo/client-reactjs/src/redux/actions/Application.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,12 @@ function getClusters() {
.then((response) => (response.ok ? response.json() : handleError(response)))
.then((clusters) => {
//if there are no clusters, set this to null for later checks

if (clusters.length === 0) {
if (clusters.data.length === 0) {
dispatch({ type: Constants.NO_CLUSTERS_FOUND, noClusters: true });
return;
}

dispatch({ type: Constants.CLUSTERS_FOUND, clusters });
dispatch({ type: Constants.CLUSTERS_FOUND, clusters: clusters.data });
})
.catch(console.log);
};
Expand Down
57 changes: 55 additions & 2 deletions Tombolo/server/controllers/clusterController.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,34 @@ const getCluster = async (req, res) => {
// Get one cluster by id
const cluster = await Cluster.findOne({
where: { id: req.params.id },
attributes: { exclude: ["hash"] },
attributes: {
exclude: ["hash", "metaData"],
include: [
[
Sequelize.literal(`
CASE
WHEN metaData IS NOT NULL AND JSON_EXTRACT(metaData, '$.reachabilityInfo') IS NOT NULL
THEN JSON_EXTRACT(metaData, '$.reachabilityInfo')
ELSE '{}'
END
`),
"reachabilityInfoString",
],
],
},
});

if (!cluster) throw new CustomError("Cluster not found", 404);
res.status(200).json({ success: true, data: cluster });

// Plain cluster data
const clusterPlainData = cluster.get({ plain: true });
if (clusterPlainData.reachabilityInfoString) {
clusterPlainData.reachabilityInfo = JSON.parse(
clusterPlainData.reachabilityInfoString
);
}

res.status(200).json({ success: true, data: clusterPlainData });
} catch (err) {
logger.error(`Get cluster: ${err.message}`);
res
Expand Down Expand Up @@ -415,6 +439,34 @@ const pingCluster = async (req, res) => {
}
};

// Ping HPCC cluster that is already saved in the database
const pingExistingCluster = async (req, res) => {
const { id } = req.params;
try{
await hpccUtil.getCluster(id);
// Update the reachability info for the cluster
await Cluster.update(
{
metaData: Sequelize.literal(
`JSON_SET(metaData, '$.reachabilityInfo.reachable', true, '$.reachabilityInfo.lastMonitored', NOW(), '$.reachabilityInfo.lastReachableAt', NOW())`
),
},
{ where: { id } }
);
res.status(200).json({ success: true, message: "Reachable" }); // Success Response
}catch(err){
await Cluster.update(
{
metaData: Sequelize.literal(
`JSON_SET(metaData, '$.reachabilityInfo.reachable', false, '$.reachabilityInfo.lastMonitored', NOW())`
),
},
{ where: { id } }
);
res.status(503).json({ success: false, message: err });
}
};

const clusterUsage = async (req, res) => {
try {
const { id } = req.params;
Expand Down Expand Up @@ -502,6 +554,7 @@ module.exports = {
updateCluster,
getClusterWhiteList,
pingCluster,
pingExistingCluster,
clusterUsage,
clusterStorageHistory,
};
4 changes: 3 additions & 1 deletion Tombolo/server/routes/clusterRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ const {
addClusterWithProgress,
getClusters,
getCluster,
deleteCluster,
updateCluster,
deleteCluster,
getClusterWhiteList,
pingCluster,
pingExistingCluster,
clusterUsage,
clusterStorageHistory,
} = require("../controllers/clusterController");

router.post("/ping", validateClusterPingPayload, pingCluster); // GET - Ping cluster
router.get("/pingExistingCluster/:id", validateClusterId, pingExistingCluster); // GET - Ping existing cluster
router.get("/whiteList", getClusterWhiteList); // GET - cluster white list
router.post("/", validateAddClusterInputs, addCluster); // CREATE - one cluster
router.post(
Expand Down

0 comments on commit fd229ea

Please sign in to comment.