@@ -8,16 +8,24 @@ from fastapi.responses import JSONResponse
|
||||
from devplacepy.database import db, get_table, get_users_by_uids
|
||||
from devplacepy.dependencies import json_or_form
|
||||
from devplacepy.models import (
|
||||
EditorPrefsForm,
|
||||
WorkspaceFlagForm,
|
||||
WorkspaceQuotaForm,
|
||||
WorkspaceSuspendForm,
|
||||
)
|
||||
from devplacepy.responses import action_result, json_error, respond
|
||||
from devplacepy.routers.admin._shared import deny_senior, is_senior_admin
|
||||
from devplacepy.schemas import AdminWorkspacesOut
|
||||
from devplacepy.seo import base_seo_context
|
||||
from devplacepy.services.audit import record as audit
|
||||
from devplacepy.services.containers import store
|
||||
from devplacepy.services.containers.workspace import flags, provision, quota, tunnels
|
||||
from devplacepy.services.containers.workspace import (
|
||||
editor,
|
||||
flags,
|
||||
provision,
|
||||
quota,
|
||||
tunnels,
|
||||
)
|
||||
from devplacepy.utils import create_notification, generate_uid, not_found, require_admin
|
||||
|
||||
router = APIRouter()
|
||||
@@ -237,6 +245,43 @@ async def admin_flag_resolve(request: Request, flag_uid: str, status: str = "res
|
||||
return action_result(request, "/admin/workspaces")
|
||||
|
||||
|
||||
@router.post("/workspaces/{uid}/editor")
|
||||
async def admin_workspace_editor(
|
||||
request: Request,
|
||||
uid: str,
|
||||
data: Annotated[EditorPrefsForm, Depends(json_or_form(EditorPrefsForm))],
|
||||
):
|
||||
admin = require_admin(request)
|
||||
if not isinstance(admin, dict):
|
||||
return admin
|
||||
instance = _instance_or_404(uid)
|
||||
owner_uid = instance.get("workspace_owner_uid", "")
|
||||
owner = get_users_by_uids([owner_uid]).get(owner_uid) if owner_uid else None
|
||||
if is_senior_admin(admin, owner):
|
||||
return deny_senior(
|
||||
request,
|
||||
admin,
|
||||
owner_uid,
|
||||
owner,
|
||||
"container.workspace.editor.update",
|
||||
"/admin/workspaces",
|
||||
)
|
||||
if not owner_uid:
|
||||
return json_error(400, "this workspace has no owner")
|
||||
if data.reset:
|
||||
editor.reset_prefs(owner_uid, admin["uid"])
|
||||
else:
|
||||
editor.save_prefs(
|
||||
owner_uid, data.model_dump(exclude={"reset"}, exclude_unset=True)
|
||||
)
|
||||
_audit(request, admin, "container.workspace.editor.update", instance)
|
||||
return action_result(
|
||||
request,
|
||||
"/admin/workspaces",
|
||||
data={"editor": editor.view(owner_uid, instance)},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/workspaces/quota")
|
||||
async def admin_workspace_quota(
|
||||
request: Request,
|
||||
|
||||
@@ -58,6 +58,12 @@ DOCS_PAGES = [
|
||||
"kind": "prose",
|
||||
"section": SECTION_GENERAL,
|
||||
},
|
||||
{
|
||||
"slug": "workspace-editor",
|
||||
"title": "The workspace editor",
|
||||
"kind": "prose",
|
||||
"section": SECTION_GENERAL,
|
||||
},
|
||||
{
|
||||
"slug": "feed",
|
||||
"title": "The feed",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Form, Request, WebSocket
|
||||
from fastapi import APIRouter, Depends, Form, Request, WebSocket
|
||||
from starlette.responses import Response
|
||||
|
||||
from devplacepy.content import (
|
||||
@@ -10,13 +10,14 @@ from devplacepy.content import (
|
||||
can_open_workspace,
|
||||
)
|
||||
from devplacepy.database import get_table, resolve_by_slug
|
||||
from devplacepy.models import TunnelForm
|
||||
from devplacepy.dependencies import json_or_form
|
||||
from devplacepy.models import EditorPrefsForm, TunnelForm
|
||||
from devplacepy.responses import action_result, json_error, respond
|
||||
from devplacepy.schemas import WorkspaceOut
|
||||
from devplacepy.services.audit import record as audit
|
||||
from devplacepy.services.containers import activity, api, forward, store
|
||||
from devplacepy.services.containers.api import ContainerError
|
||||
from devplacepy.services.containers.workspace import provision, quota, tunnels
|
||||
from devplacepy.services.containers.workspace import editor, provision, quota, tunnels
|
||||
from devplacepy.services.containers.workspace.provision import WorkspaceError
|
||||
from devplacepy.utils import not_found, require_user
|
||||
|
||||
@@ -25,6 +26,12 @@ from ._shared import audit_instance, fail
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _restart_required(instance: dict, profile: editor.EditorProfile) -> bool:
|
||||
if instance.get("status") != store.ST_RUNNING:
|
||||
return False
|
||||
return editor.restart_required(instance, profile)
|
||||
|
||||
|
||||
def _project_or_404(slug: str) -> dict:
|
||||
project = resolve_by_slug(get_table("projects"), slug)
|
||||
if not project:
|
||||
@@ -64,6 +71,7 @@ async def workspace_page(request: Request, slug: str):
|
||||
_guard(request, project, user, "container.workspace.open")
|
||||
instance = provision.find_for_project(project["uid"], user["uid"])
|
||||
limits = quota.resolve(user["uid"])
|
||||
profile = editor.resolve(user["uid"], instance)
|
||||
context = {
|
||||
"project": project,
|
||||
"workspace": provision.view(instance) if instance else None,
|
||||
@@ -79,6 +87,10 @@ async def workspace_page(request: Request, slug: str):
|
||||
"editor_password": (
|
||||
api.ensure_editor_password(instance) if instance else ""
|
||||
),
|
||||
"editor": editor.view(user["uid"], instance),
|
||||
"restart_required": (
|
||||
_restart_required(instance, profile) if instance else False
|
||||
),
|
||||
"user": user,
|
||||
}
|
||||
return respond(request, "workspace.html", context, model=WorkspaceOut)
|
||||
@@ -151,6 +163,63 @@ async def workspace_delete(request: Request, slug: str):
|
||||
return action_result(request, f"/projects/{slug}/workspace")
|
||||
|
||||
|
||||
@router.get("/{slug}/workspace/editor")
|
||||
async def editor_prefs_read(request: Request, slug: str):
|
||||
user = require_user(request)
|
||||
if isinstance(user, Response):
|
||||
return user
|
||||
project = _project_or_404(slug)
|
||||
instance = _workspace_or_404(project, user)
|
||||
if not can_manage_workspace(instance, project, user):
|
||||
return json_error(403, "Not allowed to manage this workspace")
|
||||
profile = editor.resolve(user["uid"], instance)
|
||||
return {
|
||||
"editor": editor.view(user["uid"], instance),
|
||||
"restart_required": _restart_required(instance, profile),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{slug}/workspace/editor")
|
||||
async def editor_prefs_write(
|
||||
request: Request,
|
||||
slug: str,
|
||||
data: Annotated[EditorPrefsForm, Depends(json_or_form(EditorPrefsForm))],
|
||||
):
|
||||
user = require_user(request)
|
||||
if isinstance(user, Response):
|
||||
return user
|
||||
project = _project_or_404(slug)
|
||||
instance = _workspace_or_404(project, user)
|
||||
if not can_manage_workspace(instance, project, user):
|
||||
return json_error(403, "Not allowed to manage this workspace")
|
||||
owner_uid = instance.get("workspace_owner_uid") or user["uid"]
|
||||
if data.reset:
|
||||
editor.reset_prefs(owner_uid, user["uid"])
|
||||
summary = f"{user['username']} reset their workspace editor preferences"
|
||||
else:
|
||||
editor.save_prefs(
|
||||
owner_uid, data.model_dump(exclude={"reset"}, exclude_unset=True)
|
||||
)
|
||||
summary = f"{user['username']} updated their workspace editor preferences"
|
||||
audit_instance(
|
||||
request,
|
||||
user,
|
||||
"container.workspace.editor.update",
|
||||
instance,
|
||||
project,
|
||||
summary=summary,
|
||||
)
|
||||
profile = editor.resolve(owner_uid, instance)
|
||||
return action_result(
|
||||
request,
|
||||
f"/projects/{slug}/workspace",
|
||||
data={
|
||||
"editor": editor.view(owner_uid, instance),
|
||||
"restart_required": _restart_required(instance, profile),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{slug}/workspace/tunnels")
|
||||
async def tunnel_list(request: Request, slug: str):
|
||||
user = require_user(request)
|
||||
|
||||
@@ -176,17 +176,24 @@ async def projects_page(
|
||||
model=ProjectsOut,
|
||||
)
|
||||
|
||||
def _editor_url(project: dict, user: dict) -> str:
|
||||
def _editor_launch(project: dict, user: dict) -> dict:
|
||||
from devplacepy.services.containers import store
|
||||
from devplacepy.services.containers.workspace import provision
|
||||
from devplacepy.services.containers.workspace import editor, provision
|
||||
|
||||
blank = {"url": "", "mode": "tab", "width": 0, "height": 0}
|
||||
instance = provision.find_for_project(project["uid"], user["uid"])
|
||||
if not instance or instance.get("suspended_at"):
|
||||
return ""
|
||||
return blank
|
||||
if instance.get("status") != store.ST_RUNNING:
|
||||
return ""
|
||||
return blank
|
||||
slug = project["slug"] or project["uid"]
|
||||
return f"/projects/{slug}/containers/instances/{instance['uid']}/code/"
|
||||
profile = editor.resolve(user["uid"], instance)
|
||||
return {
|
||||
"url": f"/projects/{slug}/containers/instances/{instance['uid']}/code/",
|
||||
"mode": profile.window_mode,
|
||||
"width": profile.window_width,
|
||||
"height": profile.window_height,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{project_slug}", response_class=HTMLResponse)
|
||||
@@ -226,9 +233,12 @@ async def project_detail(request: Request, project_slug: str, before: str = None
|
||||
schemas=[website_schema(base), software_application_schema(project, base)],
|
||||
)
|
||||
viewer_can_workspace = can_open_workspace(project, user)
|
||||
workspace_editor_url = (
|
||||
_editor_url(project, user) if viewer_can_workspace else ""
|
||||
editor_launch = (
|
||||
_editor_launch(project, user)
|
||||
if viewer_can_workspace
|
||||
else {"url": "", "mode": "tab", "width": 0, "height": 0}
|
||||
)
|
||||
workspace_editor_url = editor_launch["url"]
|
||||
parent = get_fork_parent(project["uid"])
|
||||
forked_from = (
|
||||
{
|
||||
@@ -276,6 +286,9 @@ async def project_detail(request: Request, project_slug: str, before: str = None
|
||||
"viewer_can_containers": can_view_project_containers(project, user),
|
||||
"viewer_can_workspace": viewer_can_workspace,
|
||||
"workspace_editor_url": workspace_editor_url,
|
||||
"workspace_editor_mode": editor_launch["mode"],
|
||||
"workspace_editor_width": editor_launch["width"],
|
||||
"workspace_editor_height": editor_launch["height"],
|
||||
"forked_from": forked_from,
|
||||
"fork_count": count_forks(project["uid"]),
|
||||
"file_count": count_files(project["uid"]),
|
||||
|
||||
Reference in New Issue
Block a user