|
{% extends "base.html" %}
|
|
|
|
{% block extra_head %}
|
|
<link rel="stylesheet" href="{{ static_url('/static/css/containers.css') }}">
|
|
<link rel="stylesheet" href="{{ static_url('/static/css/workspace.css') }}">
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="workspace-page" data-workspace-root
|
|
data-slug="{{ project.slug or project.uid }}"
|
|
data-workspace-uid="{{ workspace.uid if has_workspace else '' }}"
|
|
data-has-workspace="{{ 1 if has_workspace else 0 }}">
|
|
|
|
<h1 class="workspace-title">Workspace: {{ project.title }}</h1>
|
|
|
|
{% if not has_workspace %}
|
|
<div class="card workspace-empty">
|
|
<p>No workspace yet for this project. Opening one starts a container with your
|
|
project files, a browser editor, and a terminal.</p>
|
|
<p class="workspace-muted">You are using {{ workspace_count }} of {{ max_workspaces }} workspaces.</p>
|
|
<form method="post" action="/projects/{{ project.slug or project.uid }}/workspace">
|
|
<button type="submit" class="btn btn-primary">Open workspace</button>
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
|
|
{% if workspace.suspended %}
|
|
<div class="card workspace-suspended">
|
|
<strong>This workspace is suspended.</strong>
|
|
<p>{{ workspace.flag_reason or "Contact an administrator." }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% for flag in workspace.flags %}
|
|
<div class="card workspace-flag workspace-flag-{{ flag.severity }}">
|
|
<strong>{{ flag.kind }}</strong>
|
|
<span>{{ flag.detail }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
<div class="card workspace-summary">
|
|
<div class="workspace-state" data-workspace-status>{{ workspace.status }}</div>
|
|
<div class="workspace-meters">
|
|
<div class="workspace-meter">
|
|
<span>Disk</span>
|
|
<progress max="100" value="{{ workspace.disk_percent }}"></progress>
|
|
<span>{{ workspace.disk_percent }}% of {{ workspace.disk_quota_mb }} MB</span>
|
|
</div>
|
|
<div class="workspace-meter">
|
|
<span>Egress</span>
|
|
<progress max="100" value="{{ workspace.egress_percent }}"></progress>
|
|
<span>{{ workspace.egress_percent }}% of {{ workspace.egress_quota_mb }} MB</span>
|
|
</div>
|
|
</div>
|
|
<p class="workspace-muted">
|
|
Stops after {{ workspace.idle_stop_minutes }} minutes idle. Removed after
|
|
{{ workspace.retention_days }} days idle.
|
|
{% if workspace.last_active_at %}
|
|
Last active {{ dt_ago(workspace.last_active_at) }}.
|
|
{% endif %}
|
|
</p>
|
|
<div class="workspace-actions">
|
|
{% if workspace.status == "running" %}
|
|
{% set _url = editor_url %}
|
|
{% set _uid = workspace.uid %}
|
|
{% set _class = "btn btn-primary" %}
|
|
{% set _mode = editor.window_mode %}
|
|
{% set _width = editor.window_width %}
|
|
{% set _height = editor.window_height %}
|
|
{% set _label = "Open editor" %}
|
|
{% include "_editor_open.html" %}
|
|
<form method="post" action="/projects/{{ project.slug or project.uid }}/workspace/stop">
|
|
<button type="submit" class="btn">Stop</button>
|
|
</form>
|
|
{% else %}
|
|
<form method="post" action="/projects/{{ project.slug or project.uid }}/workspace">
|
|
<button type="submit" class="btn btn-primary">Start</button>
|
|
</form>
|
|
{% endif %}
|
|
{% if editor_password %}
|
|
<code class="secret-value" id="editor-password">{{ editor_password }}</code>
|
|
<button type="button" class="btn btn-sm" data-copy="editor-password">Copy password</button>
|
|
{% endif %}
|
|
<form method="post" action="/projects/{{ project.slug or project.uid }}/workspace/delete">
|
|
<button type="submit" data-confirm="Delete this workspace?" data-confirm-danger class="btn btn-danger">Delete</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card workspace-editor" data-editor-card>
|
|
<h2>Editor</h2>
|
|
<p class="workspace-muted">
|
|
Your workspace opens a branded DevPlace editor with the
|
|
<code>dpc</code> coding agent already running. These preferences are yours and
|
|
follow you into every workspace you open.
|
|
</p>
|
|
|
|
{% if restart_required %}
|
|
<div class="workspace-editor-restart">
|
|
<span>Your editor settings changed. Restart the workspace to apply them.</span>
|
|
<form method="post" action="/projects/{{ project.slug or project.uid }}/workspace/stop">
|
|
<button type="submit" class="btn btn-sm">Stop</button>
|
|
</form>
|
|
<form method="post" action="/projects/{{ project.slug or project.uid }}/workspace">
|
|
<button type="submit" class="btn btn-sm btn-primary">Start</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<ul class="workspace-editor-summary">
|
|
<li><span>Theme</span><strong>{{ editor.theme }}</strong><em>{{ editor.sources.theme }}</em></li>
|
|
<li><span>Layout</span><strong>{{ editor.layout }}</strong><em>{{ editor.sources.layout }}</em></li>
|
|
<li><span>Panel</span><strong>{{ editor.panel_preset }}</strong><em>{{ editor.sources.panel_preset }}</em></li>
|
|
<li><span>Editor font</span><strong>{{ editor.font_size }} px</strong><em>{{ editor.sources.font_size }}</em></li>
|
|
<li><span>Terminal font</span><strong>{{ editor.terminal_font_size }} px</strong><em>{{ editor.sources.terminal_font_size }}</em></li>
|
|
<li><span>Zoom</span><strong>{{ editor.zoom_level }}</strong><em>{{ editor.sources.zoom_level }}</em></li>
|
|
<li><span>Agent on boot</span><strong>{{ editor.boot_agent }}</strong><em>{{ editor.sources.boot_agent }}</em></li>
|
|
<li><span>Shell on boot</span><strong>{{ "yes" if editor.boot_shell else "no" }}</strong><em>{{ editor.sources.boot_shell }}</em></li>
|
|
<li><span>Opens in</span><strong>{{ editor.window_mode }}</strong><em>{{ editor.sources.window_mode }}</em></li>
|
|
<li><span>Trusts every folder</span><strong>{{ "yes" if editor.trust_all else "no" }}</strong><em>site</em></li>
|
|
</ul>
|
|
|
|
<h3 class="workspace-editor-heading">Container size</h3>
|
|
<p class="workspace-muted">Set by an administrator through your workspace quota.</p>
|
|
<ul class="workspace-editor-summary workspace-editor-resources">
|
|
<li><span>CPU</span><strong>{{ editor.cpu_cores }} cores</strong><em>quota</em></li>
|
|
<li><span>Memory</span><strong>{{ editor.memory_mb }} MB</strong><em>quota</em></li>
|
|
<li><span>Disk</span><strong>{{ editor.disk_quota_mb }} MB</strong><em>quota</em></li>
|
|
</ul>
|
|
|
|
<form class="workspace-editor-form" method="post" data-native
|
|
action="/projects/{{ project.slug or project.uid }}/workspace/editor">
|
|
<label>Theme
|
|
<select name="theme">
|
|
<option value="">Site default</option>
|
|
<option value="devplace-dark" {{ "selected" if editor.sources.theme == "user" and editor.theme == "devplace-dark" }}>DevPlace Dark</option>
|
|
<option value="devplace-light" {{ "selected" if editor.sources.theme == "user" and editor.theme == "devplace-light" }}>DevPlace Light</option>
|
|
<option value="system" {{ "selected" if editor.sources.theme == "user" and editor.theme == "system" }}>Leave to me</option>
|
|
</select>
|
|
</label>
|
|
<label>Layout
|
|
<select name="layout">
|
|
<option value="">Site default</option>
|
|
<option value="standard" {{ "selected" if editor.sources.layout == "user" and editor.layout == "standard" }}>Standard</option>
|
|
<option value="terminal-focus" {{ "selected" if editor.sources.layout == "user" and editor.layout == "terminal-focus" }}>Terminal focus</option>
|
|
<option value="zen" {{ "selected" if editor.sources.layout == "user" and editor.layout == "zen" }}>Zen</option>
|
|
</select>
|
|
</label>
|
|
<label>Terminal panel
|
|
<select name="panel_preset">
|
|
<option value="">Site default</option>
|
|
<option value="short" {{ "selected" if editor.sources.panel_preset == "user" and editor.panel_preset == "short" }}>Short</option>
|
|
<option value="normal" {{ "selected" if editor.sources.panel_preset == "user" and editor.panel_preset == "normal" }}>Normal</option>
|
|
<option value="tall" {{ "selected" if editor.sources.panel_preset == "user" and editor.panel_preset == "tall" }}>Tall</option>
|
|
<option value="maximized" {{ "selected" if editor.sources.panel_preset == "user" and editor.panel_preset == "maximized" }}>Maximized</option>
|
|
</select>
|
|
</label>
|
|
<label>Editor font size
|
|
<input type="number" name="font_size" min="0" max="48"
|
|
value="{{ editor.font_size if editor.sources.font_size == 'user' else 0 }}">
|
|
</label>
|
|
<label>Terminal font size
|
|
<input type="number" name="terminal_font_size" min="0" max="48"
|
|
value="{{ editor.terminal_font_size if editor.sources.terminal_font_size == 'user' else 0 }}">
|
|
</label>
|
|
<label>Zoom level
|
|
<input type="number" name="zoom_level" min="-99" max="5"
|
|
value="{{ editor.zoom_level if editor.sources.zoom_level == 'user' else -99 }}">
|
|
</label>
|
|
<label>Agent on boot
|
|
<select name="boot_agent">
|
|
<option value="">Site default</option>
|
|
<option value="dpc" {{ "selected" if editor.sources.boot_agent == "user" and editor.boot_agent == "dpc" }}>DevPlace Code (dpc)</option>
|
|
<option value="none" {{ "selected" if editor.sources.boot_agent == "user" and editor.boot_agent == "none" }}>None</option>
|
|
</select>
|
|
</label>
|
|
<label>Shell on boot
|
|
<select name="boot_shell">
|
|
<option value="-1">Site default</option>
|
|
<option value="1" {{ "selected" if editor.sources.boot_shell == "user" and editor.boot_shell }}>Yes</option>
|
|
<option value="0" {{ "selected" if editor.sources.boot_shell == "user" and not editor.boot_shell }}>No</option>
|
|
</select>
|
|
</label>
|
|
<label>Open editor in
|
|
<select name="window_mode">
|
|
<option value="">Site default</option>
|
|
<option value="tab" {{ "selected" if editor.sources.window_mode == "user" and editor.window_mode == "tab" }}>A new tab</option>
|
|
<option value="window" {{ "selected" if editor.sources.window_mode == "user" and editor.window_mode == "window" }}>A sized window</option>
|
|
<option value="fullscreen" {{ "selected" if editor.sources.window_mode == "user" and editor.window_mode == "fullscreen" }}>A fullscreen window</option>
|
|
</select>
|
|
</label>
|
|
<label>Window width
|
|
<input type="number" name="window_width" min="0" max="7680"
|
|
value="{{ editor.window_width if editor.sources.window_width == 'user' else 0 }}">
|
|
</label>
|
|
<label>Window height
|
|
<input type="number" name="window_height" min="0" max="4320"
|
|
value="{{ editor.window_height if editor.sources.window_height == 'user' else 0 }}">
|
|
</label>
|
|
<div class="workspace-editor-actions">
|
|
<button type="submit" class="btn btn-primary">Save preferences</button>
|
|
</div>
|
|
</form>
|
|
|
|
<form class="workspace-editor-reset" method="post" data-native
|
|
action="/projects/{{ project.slug or project.uid }}/workspace/editor">
|
|
<input type="hidden" name="reset" value="true">
|
|
<button type="submit" class="btn btn-sm">Reset to site defaults</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card workspace-tunnels">
|
|
<h2>Public tunnels</h2>
|
|
<p class="workspace-muted">
|
|
A tunnel publishes a port from inside your container on a public HTTPS address.
|
|
Anyone with the link can reach it. You may have up to {{ workspace.max_tunnels }}.
|
|
</p>
|
|
<ul class="workspace-tunnel-list" data-tunnel-list>
|
|
{% for tunnel in workspace.tunnels %}
|
|
<li class="workspace-tunnel">
|
|
<a href="https://{{ tunnel.hostname }}" rel="noopener" target="_blank">{{ tunnel.hostname }}</a>
|
|
<span class="workspace-badge">port {{ tunnel.container_port }}</span>
|
|
<span class="workspace-badge workspace-badge-{{ tunnel.status }}">{{ tunnel.status }}</span>
|
|
{% if tunnel.last_error %}<span class="workspace-error">{{ tunnel.last_error }}</span>{% endif %}
|
|
<form method="post"
|
|
action="/projects/{{ project.slug or project.uid }}/workspace/tunnels/{{ tunnel.uid }}/delete">
|
|
<button type="submit" data-confirm="Remove this tunnel?" data-confirm-danger class="btn btn-sm">Remove</button>
|
|
</form>
|
|
</li>
|
|
{% else %}
|
|
<li class="workspace-muted">No tunnels yet.</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<form class="workspace-tunnel-form" method="post"
|
|
action="/projects/{{ project.slug or project.uid }}/workspace/tunnels">
|
|
<input type="text" name="label" placeholder="Label (optional)" maxlength="64">
|
|
<input type="number" name="container_port" placeholder="Port" min="1" max="65535" required>
|
|
<button type="submit" class="btn">Add tunnel</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card workspace-help">
|
|
<h2>Inside the container</h2>
|
|
<ul>
|
|
<li>A <strong>DevPlace Code</strong> terminal running <code>dpc</code> opens for you on boot.</li>
|
|
<li>Every folder is trusted, so nothing opens in Restricted Mode and project tasks run.</li>
|
|
<li><code>sudo</code> and <code>apt install</code> work with no extra setup.</li>
|
|
<li>Python, Rust, Nim and Swift toolchains are preinstalled.</li>
|
|
<li>Ports below 1024 cannot bind. Use a high port and a tunnel.</li>
|
|
<li>Forwarding a port in the editor's <strong>Ports</strong> view publishes it here automatically.</li>
|
|
<li>Your public URLs are also in <code>/app/.devplace/tunnels.json</code>.</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script type="module" src="{{ static_url('/static/js/WorkspaceManager.js') }}"></script>
|
|
{% endblock %}
|