Skip to content

Add refresh button to widgets UI #741

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Binary file modified docs/sphinx/user-docs/images/ui-view-clusters.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions docs/sphinx/user-docs/ui-widgets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ requests and limits along with the clusters status.

Above is a list of two Ray Clusters ``raytest`` and ``raytest2`` each of
those headings is clickable and will update the table to view the
selected Cluster's information. There are three buttons under the table
``Cluster Down``, ``View Jobs`` and ``Open Ray Dashboard``. \* The
selected Cluster's information. There are four buttons under the table
``Cluster Down``, ``View Jobs``, ``Open Ray Dashboard``, and ``Refresh Data``. \* The
``Cluster Down`` button will delete the selected Cluster. \* The
``View Jobs`` button will try to open the Ray Dashboard's Jobs view in a
Web Browser. The link will also be printed to the console. \* The
``Open Ray Dashboard`` button will try to open the Ray Dashboard view in
a Web Browser. The link will also be printed to the console.
a Web Browser. The link will also be printed to the console. \* The
``Refresh Data`` button will refresh the list of RayClusters, the spec, and
the status of the Ray Cluster.

The UI Table can be viewed by calling the following function.

Expand Down
10 changes: 10 additions & 0 deletions src/codeflare_sdk/common/widgets/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@ def test_ray_cluster_manager_widgets_init(mocker, capsys):
assert (
ray_cluster_manager_instance.ray_dashboard_button == mock_button.return_value
), "ray_dashboard_button is not set correctly"
assert (
ray_cluster_manager_instance.refresh_data_button == mock_button.return_value
), "refresh_data_button is not set correctly"
assert (
ray_cluster_manager_instance.raycluster_data_output == mock_output.return_value
), "raycluster_data_output is not set correctly"
Expand All @@ -310,6 +313,7 @@ def test_ray_cluster_manager_widgets_init(mocker, capsys):
mock_delete_button = MagicMock()
mock_list_jobs_button = MagicMock()
mock_ray_dashboard_button = MagicMock()
mock_refresh_dataframe_button = MagicMock()

mock_javascript = mocker.patch("codeflare_sdk.common.widgets.widgets.Javascript")
ray_cluster_manager_instance.url_output = MagicMock()
Expand All @@ -332,6 +336,12 @@ def test_ray_cluster_manager_widgets_init(mocker, capsys):
f'window.open("{mock_dashboard_uri.return_value}/#/jobs", "_blank");'
)

# Simulate clicking the refresh data button
ray_cluster_manager_instance._on_refresh_data_button_click(
mock_refresh_dataframe_button
)
mock_fetch_cluster_data.assert_called_with(namespace)

# Simulate clicking the Ray dashboard button
ray_cluster_manager_instance.classification_widget.value = "test-cluster-1"
ray_cluster_manager_instance._on_ray_dashboard_button_click(
Expand Down
142 changes: 94 additions & 48 deletions src/codeflare_sdk/common/widgets/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ def __init__(self, ray_clusters_df: pd.DataFrame, namespace: str = None):
tooltip="Open the Ray Dashboard in a new tab",
layout=widgets.Layout(width="auto"),
)
self.refresh_data_button = widgets.Button(
description="Refresh Data",
icon="refresh",
tooltip="Refresh the list of Ray Clusters",
layout=widgets.Layout(width="auto", left="1em"),
)

# Set up interactions
self._initialize_callbacks()
Expand All @@ -95,6 +101,9 @@ def _initialize_callbacks(self):
self.ray_dashboard_button.on_click(
lambda b: self._on_ray_dashboard_button_click(b)
)
self.refresh_data_button.on_click(
lambda b: self._on_refresh_data_button_click(b)
)

def _trigger_initial_display(self):
"""
Expand All @@ -110,59 +119,25 @@ def _on_cluster_click(self, selection_change):
_on_cluster_click handles the event when a cluster is selected from the toggle buttons, updating the output with cluster details.
"""
new_value = selection_change["new"]
self.raycluster_data_output.clear_output()
ray_clusters_df = _fetch_cluster_data(self.namespace)
self.classification_widget.options = ray_clusters_df["Name"].tolist()
with self.raycluster_data_output:
display(
HTML(
ray_clusters_df[ray_clusters_df["Name"] == new_value][
[
"Name",
"Namespace",
"Num Workers",
"Head GPUs",
"Head CPU Req~Lim",
"Head Memory Req~Lim",
"Worker GPUs",
"Worker CPU Req~Lim",
"Worker Memory Req~Lim",
"status",
]
].to_html(escape=False, index=False, border=2)
)
)
self.classification_widget.value = new_value
self._refresh_dataframe()

def _on_delete_button_click(self, b):
"""
_on_delete_button_click handles the event when the Delete Button is clicked, deleting the selected cluster.
"""
cluster_name = self.classification_widget.value
namespace = self.ray_clusters_df[
self.ray_clusters_df["Name"] == self.classification_widget.value
]["Namespace"].values[0]

_delete_cluster(cluster_name, namespace)
_delete_cluster(cluster_name, self.namespace)

with self.user_output:
self.user_output.clear_output()
print(
f"Cluster {cluster_name} in the {namespace} namespace was deleted successfully."
f"Cluster {cluster_name} in the {self.namespace} namespace was deleted successfully."
)

# Refresh the dataframe
new_df = _fetch_cluster_data(namespace)
self.ray_clusters_df = new_df
if new_df.empty:
self.classification_widget.close()
self.delete_button.close()
self.list_jobs_button.close()
self.ray_dashboard_button.close()
with self.raycluster_data_output:
self.raycluster_data_output.clear_output()
print(f"No clusters found in the {namespace} namespace.")
else:
self.classification_widget.options = new_df["Name"].tolist()
self._refresh_dataframe()

def _on_list_jobs_button_click(self, b):
"""
Expand All @@ -171,15 +146,12 @@ def _on_list_jobs_button_click(self, b):
from codeflare_sdk import Cluster

cluster_name = self.classification_widget.value
namespace = self.ray_clusters_df[
self.ray_clusters_df["Name"] == self.classification_widget.value
]["Namespace"].values[0]

# Suppress from Cluster Object initialisation widgets and outputs
with widgets.Output(), contextlib.redirect_stdout(
io.StringIO()
), contextlib.redirect_stderr(io.StringIO()):
cluster = Cluster(ClusterConfiguration(cluster_name, namespace))
cluster = Cluster(ClusterConfiguration(cluster_name, self.namespace))
dashboard_url = cluster.cluster_dashboard_uri()

with self.user_output:
Expand All @@ -197,15 +169,12 @@ def _on_ray_dashboard_button_click(self, b):
from codeflare_sdk import Cluster

cluster_name = self.classification_widget.value
namespace = self.ray_clusters_df[
self.ray_clusters_df["Name"] == self.classification_widget.value
]["Namespace"].values[0]

# Suppress from Cluster Object initialisation widgets and outputs
with widgets.Output(), contextlib.redirect_stdout(
io.StringIO()
), contextlib.redirect_stderr(io.StringIO()):
cluster = Cluster(ClusterConfiguration(cluster_name, namespace))
cluster = Cluster(ClusterConfiguration(cluster_name, self.namespace))
dashboard_url = cluster.cluster_dashboard_uri()

with self.user_output:
Expand All @@ -214,11 +183,88 @@ def _on_ray_dashboard_button_click(self, b):
with self.url_output:
display(Javascript(f'window.open("{dashboard_url}", "_blank");'))

def _on_refresh_data_button_click(self, b):
"""
_on_refresh_button_click handles the event when the Refresh Data button is clicked, refreshing the list of Ray Clusters.
"""
self.refresh_data_button.disabled = True
self._refresh_dataframe()
self.refresh_data_button.disabled = False

def _refresh_dataframe(self):
"""
_refresh_data function refreshes the list of Ray Clusters.
"""
self.ray_clusters_df = _fetch_cluster_data(self.namespace)
if self.ray_clusters_df.empty:
self.classification_widget.close()
self.delete_button.close()
self.list_jobs_button.close()
self.ray_dashboard_button.close()
self.refresh_data_button.close()
with self.raycluster_data_output:
self.raycluster_data_output.clear_output()
print(f"No clusters found in the {self.namespace} namespace.")
else:
# Store the current selection if it still exists (Was not previously deleted).
selected_cluster = (
self.classification_widget.value
if self.classification_widget.value
in self.ray_clusters_df["Name"].tolist()
else None
)

# Update list of Ray Clusters.
self.classification_widget.options = self.ray_clusters_df["Name"].tolist()

# If the selected cluster exists, preserve the selection to remain viewing the currently selected cluster.
# If it does not exist, default to the first available cluster.
if selected_cluster:
self.classification_widget.value = selected_cluster
else:
self.classification_widget.value = self.ray_clusters_df["Name"].iloc[0]

# Update the output with the current Ray Cluster details.
self._display_cluster_details()

def _display_cluster_details(self):
"""
_display_cluster_details function displays the selected cluster details in the output widget.
"""
self.raycluster_data_output.clear_output()
selected_cluster = self.ray_clusters_df[
self.ray_clusters_df["Name"] == self.classification_widget.value
]
with self.raycluster_data_output:
display(
HTML(
selected_cluster[
[
"Name",
"Namespace",
"Num Workers",
"Head GPUs",
"Head CPU Req~Lim",
"Head Memory Req~Lim",
"Worker GPUs",
"Worker CPU Req~Lim",
"Worker Memory Req~Lim",
"status",
]
].to_html(escape=False, index=False, border=2)
)
)

def display_widgets(self):
display(widgets.VBox([self.classification_widget, self.raycluster_data_output]))
display(
widgets.HBox(
[self.delete_button, self.list_jobs_button, self.ray_dashboard_button]
[
self.delete_button,
self.list_jobs_button,
self.ray_dashboard_button,
self.refresh_data_button,
]
),
self.url_output,
self.user_output,
Expand Down
40 changes: 30 additions & 10 deletions ui-tests/tests/widget_notebook_example.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ test.describe("Visual Regression", () => {

// Hide the cell toolbar before capturing the screenshots
await page.addStyleTag({ content: '.jp-cell-toolbar { display: none !important; }' });
// Hide the file explorer
await page.keyboard.press('Control+Shift+F');

const captures: (Buffer | null)[] = []; // Array to store cell screenshots
const cellCount = await page.notebook.getCellCount();
Expand Down Expand Up @@ -109,30 +111,48 @@ test.describe("Visual Regression", () => {

await runPreviousCell(page, cellCount, '(<CodeFlareClusterStatus.UNKNOWN: 6>, 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);
});
// Replace text in ClusterConfiguration to run a new RayCluster
const cell = page.getByText('raytest').first();
await cell.fill('"raytest-1"');
await page.notebook.runCell(cellCount - 3, true); // Run ClusterConfiguration cell

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 });
const successMessage = await page.waitForSelector('text=Ray Cluster: \'raytest-1\' has successfully been created', { timeout: 10000 });
expect(successMessage).not.toBeNull();
});

const viewClustersCellIndex = 4; // 5 on OpenShift
await page.notebook.runCell(cellCount - 2, true);

// Wait until the RayCluster status in the table updates to "Ready"
await interactWithWidget(page, viewClustersCellIndex, 'button:has-text("Refresh Data")', async (button) => {
let clusterReady = false;
const maxRefreshRetries = 24; // 24 retries * 5 seconds = 120 seconds
let numRefreshRetries = 0;
while (!clusterReady && numRefreshRetries < maxRefreshRetries) {
await button.click();
try {
await page.waitForSelector('text=Ready ✓', { timeout: 5000 });
clusterReady = true;
}
catch (e) {
console.log(`Cluster not ready yet. Retrying...`);
numRefreshRetries++;
}
}
expect(clusterReady).toBe(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 });
const successMessage = await page.waitForSelector('text=Opening Ray Dashboard for raytest-1 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 });
const successMessage = await page.waitForSelector('text=Opening Ray Jobs Dashboard for raytest-1 cluster', { timeout: 5000 });
expect(successMessage).not.toBeNull();
});

Expand All @@ -141,7 +161,7 @@ test.describe("Visual Regression", () => {

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 });
const successMessage = await page.waitForSelector(`text=Cluster raytest-1 in the ${namespace} namespace was deleted successfully.`, { timeout: 5000 });
expect(successMessage).not.toBeNull();
});

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading