GUI-Umbau Phase 3: Kunden-Workspace-Creator
Organisations-Admins bekommen erstmals Zugriff auf die Workspace-
Komposition (bisher exklusiv /admin/technik/workspaces vorbehalten,
ADR-0028 hatte das als "zunaechst admin-only" offen gelassen).
- routers/workspace.py (Neubau, ersetzt den Phase-1-Platzhalter):
/workspace (eigene Workspaces + bereitgestellte globale Standards,
ueber den bestehenden ADR-0028-Endpunkt), /workspace (POST, anlegen -
organization_id kommt nie aus dem Formular), /workspace/{id} (Detail:
eigener Workspace -> volle Kategorie-Checkbox-Komposition, Wiederver-
wendung von admin_technik.gruppiere_merkmale_nach_kategorie(); globaler
Standard -> reine Leseansicht, kein Formular).
- Webapps-Verwaltung zieht von der bisherigen eigenen Seite (/webapps)
hierher um (workspace_detail.html, nur fuer eigene Workspaces sichtbar)
- alte Route bleibt nur noch als 301-Redirect auf /workspace bestehen.
- Neue Besitz-/Sichtbarkeitspruefung fetch_sichtbaren_workspace() (eigene
Organisation ODER global sichtbar, sonst 404) analog zum bestehenden
find_oe_in_organization()-Muster.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLk93dhDMrkPXefVdknMV9
This commit is contained in:
1 parent
ae6a1c38c6
commit
6d9b2f5a0d
5 files changed
+368
-186
No files matched your search
+8
-93
@@ -1,99 +1,14 @@
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||
from fastapi.responses import RedirectResponse, StreamingResponse
|
||||
|
||||
from anode_client import anode_request, anode_stream
|
||||
from auth import get_current_user
|
||||
from templating import templates
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def fetch_organization_workspaces(organization_id: str) -> list[dict]:
|
||||
"""Eigene Workspaces des Kunden - abgeleitet aus den eigenen
|
||||
Bereitstellungsvorlagen (es gibt noch keine direkte Workspace-Liste im
|
||||
Kunden-Self-Service, siehe project_phase2_app_store_merkmale-Memory).
|
||||
Ein Workspace kann von mehreren Vorlagen geteilt werden - hier dedupliziert."""
|
||||
result = anode_request("GET", f"/api/v1/organizations/{organization_id}/bereitstellungsvorlagen")
|
||||
vorlagen = result.get("bereitstellungsvorlagen", []) if result.get("success") else []
|
||||
|
||||
gesehen = {}
|
||||
for v in vorlagen:
|
||||
workspace = v.get("workspace")
|
||||
if workspace and workspace["id"] not in gesehen:
|
||||
gesehen[workspace["id"]] = workspace
|
||||
|
||||
return list(gesehen.values())
|
||||
|
||||
|
||||
def find_workspace_in_organization(workspace_id: str, organization_id: str) -> dict | None:
|
||||
"""Besitz-Validierung analog zu find_oe_in_organization() (struktur.py,
|
||||
siehe ADR-0011): eine workspace_id aus der URL wird nur akzeptiert, wenn
|
||||
sie tatsächlich zu einer eigenen Bereitstellungsvorlage gehört."""
|
||||
|
||||
for workspace in fetch_organization_workspaces(organization_id):
|
||||
if workspace["id"] == workspace_id:
|
||||
return workspace
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# GUI-Umbau Phase 3 (11.09.2026): Webapps sind Teil des neuen
|
||||
# "Workspace"-Navigationspunkts geworden (routers/workspace.py) statt
|
||||
# einer eigenen Seite - dieser Router bleibt nur noch als Weiterleitung
|
||||
# für alte Lesezeichen/Links bestehen.
|
||||
@router.get("/webapps")
|
||||
def webapps_ansicht(request: Request, user=Depends(get_current_user)):
|
||||
organization_id = str(user.organization_id)
|
||||
workspaces = fetch_organization_workspaces(organization_id)
|
||||
|
||||
for workspace in workspaces:
|
||||
webapps_result = anode_request("GET", f"/api/v1/workspaces/{workspace['id']}/webapps")
|
||||
workspace["webapps"] = webapps_result.get("webapps", []) if webapps_result.get("success") else []
|
||||
|
||||
templates_result = anode_request("GET", f"/api/v1/workspaces/{workspace['id']}/webapp-templates/available")
|
||||
workspace["verfuegbare_vorlagen"] = templates_result.get("webapp_templates", []) if templates_result.get("success") else []
|
||||
|
||||
return templates.TemplateResponse(request, "webapps.html", {"user": user, "workspaces": workspaces})
|
||||
|
||||
|
||||
@router.post("/webapps/{workspace_id}/hinzufuegen")
|
||||
def webapp_hinzufuegen(
|
||||
request: Request, workspace_id: str, name: str = Form(...), url: str = Form(...),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
if find_workspace_in_organization(workspace_id, str(user.organization_id)) is None:
|
||||
raise HTTPException(status_code=404, detail="Workspace wurde nicht gefunden.")
|
||||
|
||||
anode_request("POST", f"/api/v1/workspaces/{workspace_id}/webapps", json={"name": name, "url": url})
|
||||
|
||||
return RedirectResponse("/webapps", status_code=303)
|
||||
|
||||
|
||||
@router.post("/webapps/{workspace_id}/{webapp_id}/entfernen")
|
||||
def webapp_entfernen(workspace_id: str, webapp_id: str, user=Depends(get_current_user)):
|
||||
if find_workspace_in_organization(workspace_id, str(user.organization_id)) is None:
|
||||
raise HTTPException(status_code=404, detail="Workspace wurde nicht gefunden.")
|
||||
|
||||
anode_request("DELETE", f"/api/v1/workspaces/{workspace_id}/webapps/{webapp_id}")
|
||||
|
||||
return RedirectResponse("/webapps", status_code=303)
|
||||
|
||||
|
||||
@router.post("/webapps/{workspace_id}/vorlagen/{template_id}/aktivieren")
|
||||
def webapp_vorlage_aktivieren(workspace_id: str, template_id: str, user=Depends(get_current_user)):
|
||||
if find_workspace_in_organization(workspace_id, str(user.organization_id)) is None:
|
||||
raise HTTPException(status_code=404, detail="Workspace wurde nicht gefunden.")
|
||||
|
||||
anode_request("POST", f"/api/v1/workspaces/{workspace_id}/webapp-templates/{template_id}/activate")
|
||||
|
||||
return RedirectResponse("/webapps", status_code=303)
|
||||
|
||||
|
||||
@router.get("/webapp-icons/{filename}")
|
||||
def webapp_icon_proxy(filename: str, user=Depends(get_current_user)):
|
||||
# Gleiches Proxy-Muster wie im Admin-Bereich (admin_technik.py) und beim
|
||||
# ISO-Download - kundenplattform kennt anodes oeffentliche Adresse nicht.
|
||||
def stream_datei():
|
||||
with anode_stream("GET", f"/webapp-icons/{filename}") as response:
|
||||
response.raise_for_status()
|
||||
for chunk in response.iter_bytes(chunk_size=65536):
|
||||
yield chunk
|
||||
|
||||
return StreamingResponse(stream_datei(), media_type="image/x-icon")
|
||||
def webapps_redirect():
|
||||
return RedirectResponse("/workspace", status_code=301)
|
||||
+175
-8
@@ -1,19 +1,186 @@
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||
from fastapi.responses import RedirectResponse, StreamingResponse
|
||||
|
||||
from anode_client import anode_request, anode_stream
|
||||
from auth import get_current_user
|
||||
from routers.admin_technik import gruppiere_merkmale_nach_kategorie
|
||||
from templating import templates
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/workspace")
|
||||
def workspace_liste(request: Request, user=Depends(get_current_user)):
|
||||
def fetch_verfuegbare_workspaces(organization_id: str) -> list[dict]:
|
||||
result = anode_request("GET", f"/api/v1/organizations/{organization_id}/workspaces")
|
||||
return result.get("workspaces", []) if result.get("success") else []
|
||||
|
||||
|
||||
def fetch_sichtbaren_workspace(workspace_id: str, organization_id: str) -> dict | None:
|
||||
"""
|
||||
Platzhalter (GUI-Umbau Phase 1, 11.09.2026) - der Navigationspunkt
|
||||
"Workspace" existiert schon, der eigentliche Kunden-Workspace-Creator
|
||||
(eigene Workspaces komponieren, globale Standards read-only ansehen,
|
||||
Webapps-Verwaltung zieht hierher um) kommt in Phase 3 des Plans.
|
||||
Besitz-/Sichtbarkeits-Validierung (analog find_oe_in_organization(),
|
||||
ADR-0011) - ein Workspace ist sichtbar, wenn er entweder der eigenen
|
||||
Organisation gehört (bearbeitbar) oder global ist (organization_id
|
||||
NULL, nur lesend). Ein privater Workspace einer fremden Organisation
|
||||
bleibt unsichtbar.
|
||||
"""
|
||||
|
||||
return templates.TemplateResponse(request, "workspace_liste.html", {"user": user})
|
||||
result = anode_request("GET", f"/api/v1/workspaces/{workspace_id}")
|
||||
workspace = result.get("workspace") if result.get("success") else None
|
||||
|
||||
if workspace is None:
|
||||
return None
|
||||
|
||||
ws_org = workspace.get("organization_id")
|
||||
if ws_org is not None and ws_org != organization_id:
|
||||
return None
|
||||
|
||||
return workspace
|
||||
|
||||
|
||||
def _eigenen_workspace_oder_404(workspace_id: str, organization_id: str) -> dict:
|
||||
"""Wie fetch_sichtbaren_workspace(), aber verlangt zusätzlich
|
||||
Bearbeitbarkeit (eigene Organisation, nicht global) - für alle
|
||||
schreibenden Endpunkte (Merkmale/Webapps ändern)."""
|
||||
|
||||
workspace = fetch_sichtbaren_workspace(workspace_id, organization_id)
|
||||
|
||||
if workspace is None or workspace.get("organization_id") != organization_id:
|
||||
raise HTTPException(status_code=404, detail="Workspace wurde nicht gefunden.")
|
||||
|
||||
return workspace
|
||||
|
||||
|
||||
@router.get("/workspace")
|
||||
def workspace_liste(request: Request, user=Depends(get_current_user)):
|
||||
alle = fetch_verfuegbare_workspaces(str(user.organization_id))
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"workspace_liste.html",
|
||||
{
|
||||
"user": user,
|
||||
"eigene_workspaces": [w for w in alle if not w["ist_global"]],
|
||||
"standard_workspaces": [w for w in alle if w["ist_global"]],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/workspace")
|
||||
def workspace_anlegen(
|
||||
key: str = Form(...), name: str = Form(...), description: str = Form(default=""),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
# organization_id kommt nie aus dem Formular, sondern immer aus der
|
||||
# Session - gleiches Muster wie /organisation/konten (verhindert einen
|
||||
# global sichtbaren Workspace per manipuliertem Request).
|
||||
result = anode_request(
|
||||
"POST", "/api/v1/workspaces",
|
||||
json={"key": key, "name": name, "description": description or None, "organization_id": str(user.organization_id)},
|
||||
)
|
||||
|
||||
if not result.get("success"):
|
||||
raise HTTPException(status_code=400, detail=result.get("message", "Workspace konnte nicht angelegt werden."))
|
||||
|
||||
return RedirectResponse(f"/workspace/{result['workspace']['id']}", status_code=303)
|
||||
|
||||
|
||||
@router.get("/workspace/{workspace_id}")
|
||||
def workspace_detail(request: Request, workspace_id: str, user=Depends(get_current_user)):
|
||||
organization_id = str(user.organization_id)
|
||||
workspace = fetch_sichtbaren_workspace(workspace_id, organization_id)
|
||||
|
||||
if workspace is None:
|
||||
raise HTTPException(status_code=404, detail="Workspace wurde nicht gefunden.")
|
||||
|
||||
bearbeitbar = workspace.get("organization_id") == organization_id
|
||||
|
||||
merkmale_result = anode_request("GET", "/api/v1/merkmale")
|
||||
alle_merkmale = merkmale_result.get("merkmale", []) if merkmale_result.get("success") else []
|
||||
komponierte_ids = {m["id"] for m in workspace["merkmale"]}
|
||||
kategorien = gruppiere_merkmale_nach_kategorie(alle_merkmale, komponierte_ids)
|
||||
|
||||
webapps = []
|
||||
verfuegbare_vorlagen = []
|
||||
if bearbeitbar:
|
||||
webapps_result = anode_request("GET", f"/api/v1/workspaces/{workspace_id}/webapps")
|
||||
webapps = webapps_result.get("webapps", []) if webapps_result.get("success") else []
|
||||
vorlagen_result = anode_request("GET", f"/api/v1/workspaces/{workspace_id}/webapp-templates/available")
|
||||
verfuegbare_vorlagen = vorlagen_result.get("webapp_templates", []) if vorlagen_result.get("success") else []
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"workspace_detail.html",
|
||||
{
|
||||
"user": user,
|
||||
"workspace": workspace,
|
||||
"workspace_id": workspace_id,
|
||||
"kategorien": kategorien,
|
||||
"bearbeitbar": bearbeitbar,
|
||||
"webapps": webapps,
|
||||
"verfuegbare_vorlagen": verfuegbare_vorlagen,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/workspace/{workspace_id}/merkmale")
|
||||
async def workspace_merkmale_speichern(request: Request, workspace_id: str, user=Depends(get_current_user)):
|
||||
"""Diff-basiertes Sammel-Speichern, gleiches Muster wie
|
||||
admin_technik.py:workspace_merkmale_speichern()."""
|
||||
|
||||
organization_id = str(user.organization_id)
|
||||
workspace = _eigenen_workspace_oder_404(workspace_id, organization_id)
|
||||
|
||||
form = await request.form()
|
||||
ausgewaehlt = set(form.getlist("merkmal_ids"))
|
||||
aktuell = {m["id"] for m in workspace["merkmale"]}
|
||||
|
||||
for merkmal_id in ausgewaehlt - aktuell:
|
||||
anode_request("POST", f"/api/v1/workspaces/{workspace_id}/merkmale/{merkmal_id}")
|
||||
|
||||
for merkmal_id in aktuell - ausgewaehlt:
|
||||
anode_request("DELETE", f"/api/v1/workspaces/{workspace_id}/merkmale/{merkmal_id}")
|
||||
|
||||
return RedirectResponse(f"/workspace/{workspace_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/workspace/{workspace_id}/webapps")
|
||||
def webapp_hinzufuegen(
|
||||
workspace_id: str, name: str = Form(...), url: str = Form(...), user=Depends(get_current_user)
|
||||
):
|
||||
_eigenen_workspace_oder_404(workspace_id, str(user.organization_id))
|
||||
|
||||
anode_request("POST", f"/api/v1/workspaces/{workspace_id}/webapps", json={"name": name, "url": url})
|
||||
|
||||
return RedirectResponse(f"/workspace/{workspace_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/workspace/{workspace_id}/webapps/{webapp_id}/entfernen")
|
||||
def webapp_entfernen(workspace_id: str, webapp_id: str, user=Depends(get_current_user)):
|
||||
_eigenen_workspace_oder_404(workspace_id, str(user.organization_id))
|
||||
|
||||
anode_request("DELETE", f"/api/v1/workspaces/{workspace_id}/webapps/{webapp_id}")
|
||||
|
||||
return RedirectResponse(f"/workspace/{workspace_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/workspace/{workspace_id}/vorlagen/{template_id}/aktivieren")
|
||||
def webapp_vorlage_aktivieren(workspace_id: str, template_id: str, user=Depends(get_current_user)):
|
||||
_eigenen_workspace_oder_404(workspace_id, str(user.organization_id))
|
||||
|
||||
anode_request("POST", f"/api/v1/workspaces/{workspace_id}/webapp-templates/{template_id}/activate")
|
||||
|
||||
return RedirectResponse(f"/workspace/{workspace_id}", status_code=303)
|
||||
|
||||
|
||||
@router.get("/webapp-icons/{filename}")
|
||||
def webapp_icon_proxy(filename: str, user=Depends(get_current_user)):
|
||||
# Gleiches Proxy-Muster wie im Admin-Bereich (admin_technik.py) und
|
||||
# beim ISO-Download - kundenplattform kennt anodes öffentliche Adresse
|
||||
# nicht.
|
||||
def stream_datei():
|
||||
with anode_stream("GET", f"/webapp-icons/{filename}") as response:
|
||||
response.raise_for_status()
|
||||
for chunk in response.iter_bytes(chunk_size=65536):
|
||||
yield chunk
|
||||
|
||||
return StreamingResponse(stream_datei(), media_type="image/x-icon")
|
||||
@@ -1,78 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Webapps — Tuxflotte{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Webapps</h2>
|
||||
<p><small>URLs, die auf den Geräten deiner Workspaces als Menü-/Desktop-Verknüpfung mit dem Favicon der Seite erscheinen.</small></p>
|
||||
|
||||
{% if not workspaces %}
|
||||
<p>Noch kein Workspace vorhanden - lege zuerst ein Installationsmedium mit einer Bereitstellungsvorlage an.</p>
|
||||
{% endif %}
|
||||
|
||||
{% for w in workspaces %}
|
||||
<article>
|
||||
<h3>{{ w.name }}</h3>
|
||||
|
||||
{% if w.webapps %}
|
||||
<figure>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for webapp in w.webapps %}
|
||||
<tr>
|
||||
<td style="width:2rem">
|
||||
{% if webapp.icon_path %}
|
||||
<img src="/webapp-icons/{{ webapp.icon_path.split('/')[-1] }}" alt="" width="24" height="24"
|
||||
title="Bild verlinkt zum Betreiber ({{ webapp.url }})">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ webapp.name }}<br><small><a href="{{ webapp.url }}" target="_blank" rel="noopener">{{ webapp.url }}</a></small></td>
|
||||
<td>
|
||||
<form method="post" action="/webapps/{{ w.id }}/{{ webapp.id }}/entfernen">
|
||||
<button type="submit" class="secondary outline">Entfernen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
{% else %}
|
||||
<p><small>Noch keine Webapps für diesen Workspace.</small></p>
|
||||
{% endif %}
|
||||
|
||||
{% if w.verfuegbare_vorlagen %}
|
||||
<details>
|
||||
<summary role="button" class="outline">Vorlage aktivieren</summary>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for vorlage in w.verfuegbare_vorlagen %}
|
||||
<tr>
|
||||
<td style="width:2rem">
|
||||
{% if vorlage.icon_path %}
|
||||
<img src="/webapp-icons/{{ vorlage.icon_path.split('/')[-1] }}" alt="" width="24" height="24"
|
||||
title="Bild verlinkt zum Betreiber ({{ vorlage.url }})">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ vorlage.name }}{% if vorlage.description %}<br><small>{{ vorlage.description }}</small>{% endif %}</td>
|
||||
<td>
|
||||
<form method="post" action="/webapps/{{ w.id }}/vorlagen/{{ vorlage.id }}/aktivieren">
|
||||
<button type="submit" class="secondary outline">Aktivieren</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<details>
|
||||
<summary role="button" class="secondary outline">Eigene Webapp hinzufügen</summary>
|
||||
<form method="post" action="/webapps/{{ w.id }}/hinzufuegen">
|
||||
<label>Name<input type="text" name="name" required placeholder="z.B. Schulportal"></label>
|
||||
<label>URL<input type="url" name="url" required placeholder="https://..."></label>
|
||||
<button type="submit">Hinzufügen</button>
|
||||
</form>
|
||||
</details>
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,122 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ workspace.name if workspace else workspace_id }} — Tuxflotte{% endblock %}
|
||||
{% block content %}
|
||||
<p><a href="/workspace">← Workspace</a></p>
|
||||
<h2>{{ workspace.name }}</h2>
|
||||
<p>
|
||||
{% if bearbeitbar %}Eigener Workspace{% else %}Bereitgestellter Standard <small>(schreibgeschützt)</small>{% endif %}
|
||||
{% if workspace.description %} — {{ workspace.description }}{% endif %}
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h3>Zusammensetzung</h3>
|
||||
{% if bearbeitbar %}
|
||||
<p><small>
|
||||
Angehakte Merkmale gehören zum Grundbestand dieses Workspace - gilt
|
||||
additiv für alle Geräte, die ihm zugewiesen sind (per
|
||||
Installationsmedium, oder nachträglich per Gerätegruppe mit
|
||||
eigenem Workspace).
|
||||
</small></p>
|
||||
{% if not kategorien %}
|
||||
<p>Keine katalogfähigen Merkmale vorhanden.</p>
|
||||
{% else %}
|
||||
<form method="post" action="/workspace/{{ workspace_id }}/merkmale">
|
||||
{% for kategorie_name, eintraege in kategorien.items() %}
|
||||
<fieldset>
|
||||
<legend><strong>{{ kategorie_name }}</strong></legend>
|
||||
{% for m in eintraege %}
|
||||
<label>
|
||||
<input type="checkbox" name="merkmal_ids" value="{{ m.id }}" {% if m.zugeordnet %}checked{% endif %}>
|
||||
{{ m.name }}
|
||||
{% if m.description %}<br><small>{{ m.description }}</small>{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
<button type="submit">Zusammensetzung speichern</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% for kategorie_name, eintraege in kategorien.items() %}
|
||||
{% set zugeordnete = eintraege | selectattr("zugeordnet") | list %}
|
||||
{% if zugeordnete %}
|
||||
<p><strong>{{ kategorie_name }}</strong></p>
|
||||
<ul>
|
||||
{% for m in zugeordnete %}
|
||||
<li>{{ m.name }}{% if m.description %} — <small>{{ m.description }}</small>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% if bearbeitbar %}
|
||||
<section>
|
||||
<h3>Webapps</h3>
|
||||
<p><small>URLs, die auf den Geräten dieses Workspace als Menü-/Desktop-Verknüpfung mit dem Favicon der Seite erscheinen.</small></p>
|
||||
|
||||
{% if webapps %}
|
||||
<figure>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for webapp in webapps %}
|
||||
<tr>
|
||||
<td style="width:2rem">
|
||||
{% if webapp.icon_path %}
|
||||
<img src="/webapp-icons/{{ webapp.icon_path.split('/')[-1] }}" alt="" width="24" height="24"
|
||||
title="Bild verlinkt zum Betreiber ({{ webapp.url }})">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ webapp.name }}<br><small><a href="{{ webapp.url }}" target="_blank" rel="noopener">{{ webapp.url }}</a></small></td>
|
||||
<td>
|
||||
<form method="post" action="/workspace/{{ workspace_id }}/webapps/{{ webapp.id }}/entfernen">
|
||||
<button type="submit" class="secondary outline">Entfernen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
{% else %}
|
||||
<p><small>Noch keine Webapps für diesen Workspace.</small></p>
|
||||
{% endif %}
|
||||
|
||||
{% if verfuegbare_vorlagen %}
|
||||
<details>
|
||||
<summary role="button" class="outline">Vorlage aktivieren</summary>
|
||||
<table>
|
||||
<tbody>
|
||||
{% for vorlage in verfuegbare_vorlagen %}
|
||||
<tr>
|
||||
<td style="width:2rem">
|
||||
{% if vorlage.icon_path %}
|
||||
<img src="/webapp-icons/{{ vorlage.icon_path.split('/')[-1] }}" alt="" width="24" height="24"
|
||||
title="Bild verlinkt zum Betreiber ({{ vorlage.url }})">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ vorlage.name }}{% if vorlage.description %}<br><small>{{ vorlage.description }}</small>{% endif %}</td>
|
||||
<td>
|
||||
<form method="post" action="/workspace/{{ workspace_id }}/vorlagen/{{ vorlage.id }}/aktivieren">
|
||||
<button type="submit" class="secondary outline">Aktivieren</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<details>
|
||||
<summary role="button" class="secondary outline">Eigene Webapp hinzufügen</summary>
|
||||
<form method="post" action="/workspace/{{ workspace_id }}/webapps">
|
||||
<label>Name<input type="text" name="name" required placeholder="z.B. Schulportal"></label>
|
||||
<label>URL<input type="url" name="url" required placeholder="https://..."></label>
|
||||
<button type="submit">Hinzufügen</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -2,11 +2,67 @@
|
||||
{% block title %}Workspace — Tuxflotte{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Workspace</h2>
|
||||
<p>
|
||||
Hier entsteht bald der Workspace-Creator: eigene Workspaces
|
||||
zusammenstellen oder einen bereitgestellten Standard übernehmen, dazu
|
||||
die Webapp-Verwaltung (zieht von der bisherigen Webapps-Seite hierher
|
||||
um).
|
||||
</p>
|
||||
<p><a href="/webapps">Webapps verwalten →</a> (vorübergehend noch hier, zieht in Kürze um)</p>
|
||||
<p><small>
|
||||
Ein Workspace ist der Merkmal-Grundbestand, den Sie einem
|
||||
Installationsmedium mitgeben - wählen Sie einen bereitgestellten
|
||||
Standard unverändert, oder stellen Sie einen eigenen zusammen.
|
||||
</small></p>
|
||||
|
||||
<section>
|
||||
<h3>Ihre Workspaces</h3>
|
||||
{% if not eigene_workspaces %}
|
||||
<p>Noch kein eigener Workspace angelegt.</p>
|
||||
{% else %}
|
||||
<figure>
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Beschreibung</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for w in eigene_workspaces %}
|
||||
<tr>
|
||||
<td>{{ w.name }}</td>
|
||||
<td>{{ w.description or "—" }}</td>
|
||||
<td><a href="/workspace/{{ w.id }}" role="button" class="outline">Bearbeiten →</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
{% endif %}
|
||||
|
||||
<details>
|
||||
<summary role="button" class="secondary outline">Neuen Workspace anlegen</summary>
|
||||
<form method="post" action="/workspace">
|
||||
<label for="key">Key <small>(technischer Kurzname, z.B. "schulcomputer-klasse5")</small></label>
|
||||
<input type="text" id="key" name="key" required>
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
<label for="description">Beschreibung</label>
|
||||
<input type="text" id="description" name="description">
|
||||
<button type="submit">Anlegen</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Bereitgestellte Standards</h3>
|
||||
<p><small>Schreibgeschützte Vorlagen - für ein Installationsmedium direkt unverändert auswählbar.</small></p>
|
||||
{% if not standard_workspaces %}
|
||||
<p>Aktuell keine Standards verfügbar.</p>
|
||||
{% else %}
|
||||
<figure>
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Beschreibung</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for w in standard_workspaces %}
|
||||
<tr>
|
||||
<td>{{ w.name }}</td>
|
||||
<td>{{ w.description or "—" }}</td>
|
||||
<td><a href="/workspace/{{ w.id }}" role="button" class="outline secondary">Ansehen →</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user