Skip to content

Commit

Permalink
Add workspaces panel
Browse files Browse the repository at this point in the history
  • Loading branch information
oeway committed Sep 4, 2024
1 parent 052cceb commit ef6b96f
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 25 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Hypha Change Log

### 0.20.33

- Add `delete_workspace` to the workspace api.
- Add workspaces panel to the web ui.

### 0.20.31

- Upgrade hypha-rpc to fix ssl issue with the hypha-rpc client.
Expand Down
2 changes: 1 addition & 1 deletion hypha/VERSION
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "0.20.33"
"version": "0.20.33.post1"
}
37 changes: 32 additions & 5 deletions hypha/core/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,18 @@ async def get_summary(self, context: Optional[dict] = None) -> dict:
)
return summary

def _validate_workspace_name(self, name):
def _validate_workspace_name(self, name, with_hyphen=True):
"""Validate the workspace name."""
if not name:
raise ValueError("Workspace name must not be empty.")

if "-" not in name:
if with_hyphen and "-" not in name:
raise ValueError(
"Workspace name must contain at least one hyphen (e.g. my-workspace)."
)
# only allow numbers, letters in lower case and hyphens (no underscore)
# use a regex to validate the workspace name
pattern = re.compile(r"^[a-z0-9-]*$")
pattern = re.compile(r"^[a-z0-9-|]*$")
if not pattern.match(name):
raise ValueError(
f"Invalid workspace name: {name}, only lowercase letters, numbers and hyphens are allowed."
Expand Down Expand Up @@ -200,8 +200,9 @@ async def create_workspace(
workspace = WorkspaceInfo.model_validate(config)
if user_info.id not in workspace.owners:
workspace.owners.append(user_info.id)
if user_info.id != "root":
self._validate_workspace_name(workspace.name)
self._validate_workspace_name(
workspace.name, with_hyphen=user_info.id != "root"
)
# make sure we add the user's email to owners
_id = user_info.email or user_info.id
if _id not in workspace.owners:
Expand All @@ -226,6 +227,31 @@ async def create_workspace(
await self._bookmark_workspace(workspace, user_info, context=context)
return workspace.model_dump()

@schema_method
async def delete_workspace(
self,
workspace: str = Field(..., description="workspace name"),
context: Optional[dict] = None,
):
"""Remove a workspace."""
assert context is not None
ws = context["ws"]
user_info = UserInfo.model_validate(context["user"])
if not user_info.check_permission(ws, UserPermission.admin):
raise PermissionError(f"Permission denied for workspace {ws}")
workspace_info = await self.load_workspace_info(workspace)
await self._redis.hdel("workspaces", workspace)
await self._event_bus.emit("workspace_deleted", workspace_info.model_dump())
# remove the workspace from the user's bookmarks
user_workspace = await self.load_workspace_info(user_info.get_workspace())
user_workspace.config = user_workspace.config or {}
if "bookmarks" in user_workspace.config:
user_workspace.config["bookmarks"] = [
b for b in user_workspace.config["bookmarks"] if b["name"] != workspace
]
await self._update_workspace(user_workspace, user_info)
logger.info("Workspace %s removed by %s", workspace, user_info.id)

@schema_method
async def install_application(
self,
Expand Down Expand Up @@ -1295,6 +1321,7 @@ def create_service(self, service_id, service_name=None):
"generate_token": self.generate_token,
"revoke_token": self.revoke_token,
"create_workspace": self.create_workspace,
"delete_workspace": self.delete_workspace,
"get_workspace_info": self.get_workspace_info,
"install_application": self.install_application,
"uninstall_application": self.uninstall_application,
Expand Down
10 changes: 7 additions & 3 deletions hypha/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ def __init__(
"workspace_unloaded",
lambda w: asyncio.ensure_future(self._cleanup_workspace(w)),
)
event_bus.on_local(
"workspace_deleted",
lambda w: asyncio.ensure_future(self._cleanup_workspace(w, force=True)),
)

router = APIRouter()

Expand Down Expand Up @@ -505,13 +509,13 @@ async def list_users(
items = await list_objects_async(s3_client, self.workspace_bucket, path)
return items

async def _cleanup_workspace(self, workspace: dict):
async def _cleanup_workspace(self, workspace: dict, force=False):
"""Clean up workspace."""
workspace = WorkspaceInfo.model_validate(workspace)
if workspace.read_only:
if workspace.read_only and not force:
return

if not workspace.persistent:
if not workspace.persistent or force:
# remove workspace etc files
remove_objects_sync(
self.s3client,
Expand Down
Loading

0 comments on commit ef6b96f

Please sign in to comment.