|
{% extends "base.html" %}
|
|
|
|
{% block extra_head %}
|
|
<link rel="stylesheet" href="{{ static_url('css/containers.css') }}">
|
|
<link rel="stylesheet" href="{{ static_url('css/workspace.css') }}">
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="workspace-page" data-workspace-root
|
|
data-slug="{{ project.slug or project.uid }}"
|
|
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" %}
|
|
<a class="btn btn-primary" href="{{ editor_url }}" target="_blank" rel="noopener">Open editor</a>
|
|
<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-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><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>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('js/WorkspaceManager.js') }}"></script>
|
|
{% endblock %}
|