diff --git a/.github/workflows/ui_notebooks_test.yaml b/.github/workflows/ui_notebooks_test.yaml
index 864330b9c..9dff9fafd 100644
--- a/.github/workflows/ui_notebooks_test.yaml
+++ b/.github/workflows/ui_notebooks_test.yaml
@@ -86,7 +86,8 @@ jobs:
jq -r 'del(.cells[] | select(.source[] | contains("Create authentication object for user permissions")))' 3_widget_example.ipynb > 3_widget_example.ipynb.tmp && mv 3_widget_example.ipynb.tmp 3_widget_example.ipynb
jq -r 'del(.cells[] | select(.source[] | contains("auth.logout()")))' 3_widget_example.ipynb > 3_widget_example.ipynb.tmp && mv 3_widget_example.ipynb.tmp 3_widget_example.ipynb
# Set explicit namespace as SDK need it (currently) to resolve local queues
- sed -i "s/head_memory_limits=2,/head_memory_limits=2, namespace='default',/" 3_widget_example.ipynb
+ sed -i "s/head_memory_limits=2,/head_memory_limits=2, namespace='default', image='quay.io/modh/ray:2.35.0-py39-cu121',/" 3_widget_example.ipynb
+ sed -i "s/view_clusters()/view_clusters('default')/" 3_widget_example.ipynb
working-directory: demo-notebooks/guided-demos
- name: Run UI notebook tests
diff --git a/demo-notebooks/guided-demos/3_widget_example.ipynb b/demo-notebooks/guided-demos/3_widget_example.ipynb
index 4d3d6ea70..c3bb6b862 100644
--- a/demo-notebooks/guided-demos/3_widget_example.ipynb
+++ b/demo-notebooks/guided-demos/3_widget_example.ipynb
@@ -19,7 +19,7 @@
"outputs": [],
"source": [
"# Import pieces from codeflare-sdk\n",
- "from codeflare_sdk import Cluster, ClusterConfiguration, TokenAuthentication"
+ "from codeflare_sdk import Cluster, ClusterConfiguration, TokenAuthentication, view_clusters"
]
},
{
@@ -61,7 +61,7 @@
"# Create and configure our cluster object\n",
"# The SDK will try to find the name of your default local queue based on the annotation \"kueue.x-k8s.io/default-queue\": \"true\" unless you specify the local queue manually below\n",
"cluster = Cluster(ClusterConfiguration(\n",
- " name='raytest', \n",
+ " name='raytest',\n",
" head_cpu_requests='500m',\n",
" head_cpu_limits='500m',\n",
" head_memory_requests=2,\n",
@@ -73,12 +73,22 @@
" worker_cpu_limits=1,\n",
" worker_memory_requests=2,\n",
" worker_memory_limits=2,\n",
- " # image=\"\", # Optional Field \n",
- " write_to_file=False, # When enabled Ray Cluster yaml files are written to /HOME/.codeflare/resources \n",
+ " # image=\"\", # Optional Field\n",
+ " write_to_file=False, # When enabled Ray Cluster yaml files are written to /HOME/.codeflare/resources\n",
" # local_queue=\"local-queue-name\" # Specify the local queue manually\n",
"))"
]
},
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3de6403c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "view_clusters()"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
diff --git a/src/codeflare_sdk/cluster/widgets.py b/src/codeflare_sdk/cluster/widgets.py
index e68c52c42..ae2390fe1 100644
--- a/src/codeflare_sdk/cluster/widgets.py
+++ b/src/codeflare_sdk/cluster/widgets.py
@@ -253,7 +253,7 @@ def _delete_cluster(
plural = "rayclusters"
# Wait for the resource to be deleted
- while True:
+ while timeout > 0:
try:
api_instance.get_namespaced_custom_object(
group=group,
@@ -321,17 +321,13 @@ def _fetch_cluster_data(namespace):
}
return pd.DataFrame(data)
-# format_status takes a RayCluster status and applies colors and icons based on the status.
+
def _format_status(status):
- if status == RayClusterStatus.READY:
- return 'Ready ✓'
- elif status == RayClusterStatus.SUSPENDED:
- return 'Suspended ❄️'
- elif status == RayClusterStatus.FAILED:
- return 'Failed ✗'
- elif status == RayClusterStatus.UNHEALTHY:
- return 'Unhealthy'
- elif status == RayClusterStatus.UNKNOWN:
- return 'Unknown'
- else:
- return status
+ status_map = {
+ RayClusterStatus.READY: 'Ready ✓',
+ RayClusterStatus.SUSPENDED: 'Suspended ❄️',
+ RayClusterStatus.FAILED: 'Failed ✗',
+ RayClusterStatus.UNHEALTHY: 'Unhealthy',
+ RayClusterStatus.UNKNOWN: 'Unknown'
+ }
+ return status_map.get(status, status)
diff --git a/tests/unit_test.py b/tests/unit_test.py
index c3f59f63c..b9682996a 100644
--- a/tests/unit_test.py
+++ b/tests/unit_test.py
@@ -77,16 +77,7 @@
is_openshift_cluster,
)
-from codeflare_sdk.cluster.widgets import (
- cluster_up_down_buttons,
- view_clusters,
- _on_cluster_click,
- _on_delete_button_click,
- _on_list_jobs_button_click,
- _on_ray_dashboard_button_click,
- _format_status,
- _fetch_cluster_data,
-)
+import codeflare_sdk.cluster.widgets as cf_widgets
import pandas as pd
import openshift
@@ -2933,7 +2924,7 @@ def test_cluster_up_down_buttons(mocker):
MockButton.side_effect = [mock_up_button, mock_down_button]
# Call the method under test
- cluster_up_down_buttons(cluster)
+ cf_widgets.cluster_up_down_buttons(cluster)
# Simulate checkbox being checked or unchecked
mock_wait_ready_check_box.value = True # Simulate checkbox being checked
@@ -2975,7 +2966,7 @@ def test_view_clusters(mocker):
# Return empty dataframe when no clusters are found
mocker.patch("codeflare_sdk.cluster.cluster.list_all_clusters", return_value=[])
- df = _fetch_cluster_data(namespace="default")
+ df = cf_widgets._fetch_cluster_data(namespace="default")
assert df.empty
test_df=pd.DataFrame({
@@ -3022,33 +3013,33 @@ def test_view_clusters(mocker):
MockOutput.return_value = mock_output
# Call the function under test
- view_clusters(namespace="default")
+ cf_widgets.view_clusters(namespace="default")
# Simulate selecting a cluster
mock_toggle.value = "test-cluster"
selection_change = {"new": "test-cluster"}
- _on_cluster_click(selection_change, mock_output, "default", mock_toggle)
+ cf_widgets._on_cluster_click(selection_change, mock_output, "default", mock_toggle)
# Assert that the toggle options are set correctly
mock_toggle.observe.assert_called()
# Simulate clicking the list jobs button
- _on_list_jobs_button_click(None, mock_toggle, test_df, mock_output, mock_output)
+ cf_widgets._on_list_jobs_button_click(None, mock_toggle, test_df, mock_output, mock_output)
mock_javascript.assert_called()
# Simulate clicking the Ray dashboard button
- _on_ray_dashboard_button_click(None, mock_toggle, test_df, mock_output, mock_output)
+ cf_widgets._on_ray_dashboard_button_click(None, mock_toggle, test_df, mock_output, mock_output)
mock_javascript.assert_called()
# Simulate clicking the delete button
- _on_delete_button_click(None, mock_toggle, test_df, mock_output, mock_output,
+ cf_widgets._on_delete_button_click(None, mock_toggle, test_df, mock_output, mock_output,
mock_delete_button, mock_list_jobs_button, mock_ray_dashboard_button)
def test_fetch_cluster_data(mocker):
# Return empty dataframe when no clusters are found
mocker.patch("codeflare_sdk.cluster.cluster.list_all_clusters", return_value=[])
- df = _fetch_cluster_data(namespace="default")
+ df = cf_widgets._fetch_cluster_data(namespace="default")
assert df.empty
# Create mock RayCluster objects
@@ -3088,7 +3079,7 @@ def test_fetch_cluster_data(mocker):
with patch('codeflare_sdk.cluster.cluster.list_all_clusters', return_value=[mock_raycluster1, mock_raycluster2]):
# Call the function under test
- df = _fetch_cluster_data(namespace='default')
+ df = cf_widgets._fetch_cluster_data(namespace='default')
# Expected DataFrame
expected_data = {
@@ -3123,11 +3114,11 @@ def test_format_status():
]
for status, expected_output in test_cases:
- assert _format_status(status) == expected_output, f"Failed for status: {status}"
+ assert cf_widgets._format_status(status) == expected_output, f"Failed for status: {status}"
# Test an unrecognized status
unrecognized_status = 'NotAStatus'
- assert _format_status(unrecognized_status) == 'NotAStatus', "Failed for unrecognized status"
+ assert cf_widgets._format_status(unrecognized_status) == 'NotAStatus', "Failed for unrecognized status"
# Make sure to always keep this function last
diff --git a/ui-tests/tests/widget_notebook_example.test.ts b/ui-tests/tests/widget_notebook_example.test.ts
index 798c2eb60..95e84d66f 100644
--- a/ui-tests/tests/widget_notebook_example.test.ts
+++ b/ui-tests/tests/widget_notebook_example.test.ts
@@ -30,11 +30,13 @@ test.describe("Visual Regression", () => {
tmpPath,
}) => {
const notebook = "3_widget_example.ipynb";
+ const namespace = 'default';
await page.notebook.openByPath(`${tmpPath}/${notebook}`);
await page.notebook.activate(notebook);
const captures: (Buffer | null)[] = []; // Array to store cell screenshots
const cellCount = await page.notebook.getCellCount();
+ console.log(`Cell count: ${cellCount}`);
// Run all cells and capture their screenshots
await page.notebook.runCellByCell({
@@ -59,25 +61,27 @@ test.describe("Visual Regression", () => {
}
}
- const widgetCellIndex = 3;
+ // At this point, all cells have been ran, and their screenshots have been captured.
+ // We now interact with the widgets in the notebook.
+ const upDownWidgetCellIndex = 3; // 4 on OpenShift
- await waitForWidget(page, widgetCellIndex, 'input[type="checkbox"]');
- await waitForWidget(page, widgetCellIndex, 'button:has-text("Cluster Down")');
- await waitForWidget(page, widgetCellIndex, 'button:has-text("Cluster Up")');
+ await waitForWidget(page, upDownWidgetCellIndex, 'input[type="checkbox"]');
+ await waitForWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Down")');
+ await waitForWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Up")');
- await interactWithWidget(page, widgetCellIndex, 'input[type="checkbox"]', async (checkbox) => {
+ await interactWithWidget(page, upDownWidgetCellIndex, 'input[type="checkbox"]', async (checkbox) => {
await checkbox.click();
const isChecked = await checkbox.isChecked();
expect(isChecked).toBe(true);
});
- await interactWithWidget(page, widgetCellIndex, 'button:has-text("Cluster Down")', async (button) => {
+ await interactWithWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Down")', async (button) => {
await button.click();
const clusterDownMessage = await page.waitForSelector('text=No instances found, nothing to be done.', { timeout: 5000 });
expect(clusterDownMessage).not.toBeNull();
});
- await interactWithWidget(page, widgetCellIndex, 'button:has-text("Cluster Up")', async (button) => {
+ await interactWithWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Up")', async (button) => {
await button.click();
const successMessage = await page.waitForSelector('text=Ray Cluster: \'raytest\' has successfully been created', { timeout: 10000 });
@@ -95,13 +99,51 @@ test.describe("Visual Regression", () => {
await runPreviousCell(page, cellCount, '(, True)');
- await interactWithWidget(page, widgetCellIndex, 'button:has-text("Cluster Down")', async (button) => {
+ await interactWithWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Down")', async (button) => {
await button.click();
const clusterDownMessage = await page.waitForSelector('text=Ray Cluster: \'raytest\' has successfully been deleted', { timeout: 5000 });
expect(clusterDownMessage).not.toBeNull();
});
await runPreviousCell(page, cellCount, '(, False)');
+
+ // view_clusters table with buttons
+ await interactWithWidget(page, upDownWidgetCellIndex, 'input[type="checkbox"]', async (checkbox) => {
+ await checkbox.click();
+ const isChecked = await checkbox.isChecked();
+ expect(isChecked).toBe(false);
+ });
+
+ await interactWithWidget(page, upDownWidgetCellIndex, 'button:has-text("Cluster Up")', async (button) => {
+ await button.click();
+ const successMessage = await page.waitForSelector('text=Ray Cluster: \'raytest\' has successfully been created', { timeout: 10000 });
+ expect(successMessage).not.toBeNull();
+ });
+
+ const viewClustersCellIndex = 4; // 5 on OpenShift
+ await page.notebook.runCell(cellCount - 2, true);
+ await interactWithWidget(page, viewClustersCellIndex, 'button:has-text("Open Ray Dashboard")', async (button) => {
+ await button.click();
+ const successMessage = await page.waitForSelector('text=Opening Ray Dashboard for raytest cluster', { timeout: 5000 });
+ expect(successMessage).not.toBeNull();
+ });
+
+ await interactWithWidget(page, viewClustersCellIndex, 'button:has-text("View Jobs")', async (button) => {
+ await button.click();
+ const successMessage = await page.waitForSelector('text=Opening Ray Jobs Dashboard for raytest cluster', { timeout: 5000 });
+ expect(successMessage).not.toBeNull();
+ });
+
+ await interactWithWidget(page, viewClustersCellIndex, 'button:has-text("Delete Cluster")', async (button) => {
+ await button.click();
+
+ const noClustersMessage = await page.waitForSelector(`text=No clusters found in the ${namespace} namespace.`, { timeout: 5000 });
+ expect(noClustersMessage).not.toBeNull();
+ const successMessage = await page.waitForSelector(`text=Cluster raytest in the ${namespace} namespace was deleted successfully.`, { timeout: 5000 });
+ expect(successMessage).not.toBeNull();
+ });
+
+ await runPreviousCell(page, cellCount, '(, False)');
});
});