Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,22 @@
from evo.files import FileAPIClient
from evo.files.data import FileMetadata
from evo.notebooks._consts import DEFAULT_DISCOVERY_URL
from evo.notebooks._helpers import build_img_widget
from evo.notebooks.assets import get as get_asset
from evo.objects import ObjectAPIClient
from evo.objects.data import ObjectMetadata
from evo.workspaces import User, Workspace, WorkspaceAPIClient
from evo.workspaces import parse as _parse
from evo.workspaces.endpoints.api import AdminApi, LicenseAccessApi


def build_img_widget(filename: str) -> widgets.Image:
return widgets.Image(
value=get_asset(filename).read_bytes(),
format="png",
layout=widgets.Layout(max_height="26px", margin="3px", align_self="center"),
)


class WorkspaceAdminHelper:
"""Aggregates administrative information across workspaces in an Evo instance.

Expand Down
4 changes: 2 additions & 2 deletions packages/evo-sdk-common/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ Documentation = "https://developer.seequent.com/"
aiohttp = ["aiohttp"]
notebooks = [
"evo-sdk-common[aiohttp]",
"anywidget",
"ipykernel",
"ipython",
"ipywidgets",
"python-dotenv",
"Pillow",
]
Expand All @@ -39,7 +39,7 @@ jmespath = [
[dependency-groups]
# Dev dependencies. The version is left unspecified so the latest is installed.
test = [
"evo-sdk-common[aiohttp]",
"evo-sdk-common[aiohttp,notebooks]",
"freezegun",
"parameterized==0.9.0",
"pytest",
Expand Down
37 changes: 20 additions & 17 deletions packages/evo-sdk-common/src/evo/notebooks/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import base64
from pathlib import Path
from typing import TypeAlias

import ipywidgets as widgets

from evo.common.utils import Cache

from . import assets
from ._consts import DEFAULT_CACHE_LOCATION

FileName: TypeAlias = str | Path

_MEDIA_TYPES = {".gif": "image/gif", ".png": "image/png"}


def init_cache(cache_location: FileName = DEFAULT_CACHE_LOCATION) -> Cache:
"""Initialise the storage location for the notebook environment.
Expand All @@ -37,20 +38,22 @@ def init_cache(cache_location: FileName = DEFAULT_CACHE_LOCATION) -> Cache:
return cache


def build_img_widget(filename: str) -> widgets.Image:
image = assets.get(filename).read_bytes()
return widgets.Image(
value=image,
format="png",
layout=widgets.Layout(max_height="26px", margin="3px", align_self="center"),
)
def read_asset_text(filename: str) -> str:
"""Read a bundled text asset, such as the widget ESM or stylesheet.

:param filename: The name of the file in the assets directory.

:returns: The contents of the file.
"""
return assets.get(filename).read_text(encoding="utf-8")


def asset_data_uri(filename: str) -> str:
"""Encode a bundled image asset as a data URI so it can be embedded directly in widget markup.

def build_button_widget(text: str) -> widgets.Button:
widget = widgets.Button(
description=text,
button_style="info",
layout=widgets.Layout(margin="5px 5px 5px 5px", align_self="center"),
)
widget.style.button_color = "#265C7F"
return widget
:param filename: The name of the file in the assets directory.

:returns: The data URI for the image.
"""
encoded = base64.b64encode(assets.get(filename).read_bytes()).decode("ascii")
return f"data:{_MEDIA_TYPES[Path(filename).suffix.lower()]};base64,{encoded}"
55 changes: 55 additions & 0 deletions packages/evo-sdk-common/src/evo/notebooks/assets/feedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
function render({ model, el }) {
const root = document.createElement("div");
root.className = "evo-row";

const label = document.createElement("span");
label.className = "evo-label";

const percent = document.createElement("span");
percent.className = "evo-percent";

const progress = document.createElement("div");
progress.className = "evo-progress";
progress.role = "progressbar";

const bar = document.createElement("div");
bar.className = "evo-progress-bar";
progress.appendChild(bar);

const message = document.createElement("span");
message.className = "evo-message";

root.append(label, percent, progress, message);
el.appendChild(root);

const renderLabel = () => {
label.textContent = model.get("label");
};

const renderValue = () => {
const value = Math.min(Math.max(model.get("value"), 0), 1);
bar.style.width = `${value * 100}%`;
percent.textContent = `${(value * 100).toFixed(1).padStart(5)}%`;
progress.ariaValueNow = String(Math.round(value * 100));
};

const renderMessage = () => {
message.textContent = model.get("message");
};

model.on("change:label", renderLabel);
model.on("change:value", renderValue);
model.on("change:message", renderMessage);

renderLabel();
renderValue();
renderMessage();

return () => {
model.off("change:label", renderLabel);
model.off("change:value", renderValue);
model.off("change:message", renderMessage);
};
}

export default { render };
76 changes: 76 additions & 0 deletions packages/evo-sdk-common/src/evo/notebooks/assets/selector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
function render({ model, el }) {
const root = document.createElement("div");
root.className = "evo-row";

const select = document.createElement("select");
select.className = "evo-select";
select.id = `evo-select-${Math.random().toString(36).slice(2)}`;

const label = document.createElement("label");
label.className = "evo-label";
label.htmlFor = select.id;

const spinner = document.createElement("img");
spinner.className = "evo-spinner";
spinner.alt = "";

root.append(label, select, spinner);
el.appendChild(root);

const renderLabel = () => {
label.textContent = model.get("label");
};

const renderOptions = () => {
select.replaceChildren();
for (const [text, value] of model.get("options") ?? []) {
const option = document.createElement("option");
option.value = value;
option.textContent = text;
select.appendChild(option);
}
select.value = model.get("value");
};

const renderValue = () => {
select.value = model.get("value");
};

const renderDisabled = () => {
select.disabled = model.get("disabled");
};

const renderLoading = () => {
const loading = model.get("loading");
spinner.src = loading ? model.get("spinner") : "";
spinner.style.display = loading ? "inline-block" : "none";
};

const onChange = () => {
model.set("value", select.value);
model.save_changes();
};

select.addEventListener("change", onChange);
model.on("change:label", renderLabel);
model.on("change:options", renderOptions);
model.on("change:value", renderValue);
model.on("change:disabled", renderDisabled);
model.on("change:loading", renderLoading);

renderLabel();
renderOptions();
renderDisabled();
renderLoading();

return () => {
select.removeEventListener("change", onChange);
model.off("change:label", renderLabel);
model.off("change:options", renderOptions);
model.off("change:value", renderValue);
model.off("change:disabled", renderDisabled);
model.off("change:loading", renderLoading);
};
}

export default { render };
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
function render({ model, el }) {
const root = document.createElement("div");
root.className = "evo-row";

const logo = document.createElement("img");
logo.className = "evo-logo";
logo.alt = "Evo";
logo.src = model.get("logo");

const button = document.createElement("button");
button.className = "evo-button";
button.type = "button";

const spinner = document.createElement("img");
spinner.className = "evo-spinner";
spinner.alt = "";

const message = document.createElement("span");
message.className = "evo-message";

root.append(logo, button, spinner, message);
el.appendChild(root);

const renderButtonText = () => {
button.textContent = model.get("button_text");
};

const renderDisabled = () => {
button.disabled = model.get("disabled");
};

const renderLoading = () => {
const loading = model.get("loading");
spinner.src = loading ? model.get("spinner") : "";
spinner.style.display = loading ? "inline-block" : "none";
};

const renderMessage = () => {
message.textContent = model.get("message");
};

const onClick = () => {
model.send({ type: "click" });
};

button.addEventListener("click", onClick);
model.on("change:button_text", renderButtonText);
model.on("change:disabled", renderDisabled);
model.on("change:loading", renderLoading);
model.on("change:message", renderMessage);

renderButtonText();
renderDisabled();
renderLoading();
renderMessage();

return () => {
button.removeEventListener("click", onClick);
model.off("change:button_text", renderButtonText);
model.off("change:disabled", renderDisabled);
model.off("change:loading", renderLoading);
model.off("change:message", renderMessage);
};
}

export default { render };
85 changes: 85 additions & 0 deletions packages/evo-sdk-common/src/evo/notebooks/assets/widgets.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
.evo-row {
display: flex;
flex-flow: row wrap;
align-items: center;
gap: 8px;
margin: 5px;
}

.evo-label {
font-family: var(--jp-ui-font-family, sans-serif);
font-size: var(--jp-ui-font-size1, 13px);
color: var(--jp-ui-font-color1, #212121);
min-width: var(--jp-widgets-label-width, 80px);
}

.evo-select {
font-family: var(--jp-ui-font-family, sans-serif);
font-size: var(--jp-ui-font-size1, 13px);
padding: 3px 6px;
min-width: var(--jp-widgets-inline-width, 300px);
color: var(--jp-ui-font-color1, #212121);
background-color: var(--jp-layout-color1, #ffffff);
border: 1px solid var(--jp-border-color1, #bdbdbd);
border-radius: 2px;
}

.evo-select:disabled {
opacity: 0.6;
cursor: not-allowed;
}

.evo-button {
font-family: var(--jp-ui-font-family, sans-serif);
font-size: var(--jp-ui-font-size1, 13px);
color: #ffffff;
background-color: #265c7f;
border: none;
border-radius: 2px;
padding: 5px 12px;
cursor: pointer;
}

.evo-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}

.evo-logo {
max-height: 26px;
margin: 3px;
}

.evo-spinner {
max-height: 26px;
margin: 3px;
}

.evo-message {
font-family: var(--jp-ui-font-family, sans-serif);
font-size: var(--jp-ui-font-size1, 13px);
font-style: italic;
color: var(--jp-ui-font-color2, #616161);
}

.evo-percent {
font-family: var(--jp-code-font-family, monospace);
font-size: var(--jp-ui-font-size1, 13px);
color: var(--jp-ui-font-color1, #212121);
white-space: pre;
}

.evo-progress {
width: 400px;
height: 14px;
background-color: var(--jp-layout-color2, #eeeeee);
border-radius: 2px;
overflow: hidden;
}

.evo-progress-bar {
height: 100%;
width: 0;
background-color: #265c7f;
transition: width 0.1s linear;
}
Loading