Added containers.

This commit is contained in:
retoor 2025-05-18 19:47:15 +02:00
parent e1727caa5f
commit 527b010b24
12 changed files with 363 additions and 0 deletions

View File

@ -10,10 +10,12 @@ from snek.mapper.user import UserMapper
from snek.mapper.user_property import UserPropertyMapper
from snek.mapper.repository import RepositoryMapper
from snek.mapper.channel_attachment import ChannelAttachmentMapper
from snek.mapper.container import ContainerMapper
from snek.system.object import Object
@functools.cache
def get_mappers(app=None):
return Object(
**{
@ -27,6 +29,7 @@ def get_mappers(app=None):
"user_property": UserPropertyMapper(app=app),
"repository": RepositoryMapper(app=app),
"channel_attachment": ChannelAttachmentMapper(app=app),
"container": ContainerMapper(app=app),
}
)

View File

@ -0,0 +1,6 @@
from snek.model.container import Container
from snek.system.mapper import BaseMapper
class ContainerMapper(BaseMapper):
model_class = Container
table_name = "container"

View File

@ -12,6 +12,7 @@ from snek.model.user import UserModel
from snek.model.user_property import UserPropertyModel
from snek.model.repository import RepositoryModel
from snek.model.channel_attachment import ChannelAttachmentModel
from snek.model.container import Container
from snek.system.object import Object
@ -29,6 +30,7 @@ def get_models():
"user_property": UserPropertyModel,
"repository": RepositoryModel,
"channel_attachment": ChannelAttachmentModel,
"container": Container,
}
)

View File

@ -0,0 +1,10 @@
from snek.system.model import BaseModel, ModelField
class Container(BaseModel):
id = ModelField(name="id", required=True, kind=str)
name = ModelField(name="name", required=True, kind=str)
status = ModelField(name="status", required=True, kind=str)
resources = ModelField(name="resources", required=False, kind=str)
user_uid = ModelField(name="user_uid", required=False, kind=str)
path = ModelField(name="path", required=False, kind=str)
readonly = ModelField(name="readonly", required=False, kind=bool, default=False)

View File

@ -13,6 +13,7 @@ from snek.service.user_property import UserPropertyService
from snek.service.util import UtilService
from snek.service.repository import RepositoryService
from snek.service.channel_attachment import ChannelAttachmentService
from snek.service.container import ContainerService
from snek.system.object import Object
from snek.service.db import DBService
@ -34,6 +35,7 @@ def get_services(app):
"repository": RepositoryService(app=app),
"db": DBService(app=app),
"channel_attachment": ChannelAttachmentService(app=app),
"container": ContainerService(app=app),
}
)

View File

@ -0,0 +1,29 @@
from snek.system.service import BaseService
class ContainerService(BaseService):
mapper_name = "container"
async def create(self, id, name, status, resources=None, user_uid=None, path=None, readonly=False):
model = await self.new()
model["id"] = id
model["name"] = name
model["status"] = status
if resources:
model["resources"] = resources
if user_uid:
model["user_uid"] = user_uid
if path:
model["path"] = path
model["readonly"] = readonly
if await super().save(model):
return model
raise Exception(f"Failed to create container: {model.errors}")
async def get(self, id):
return await self.mapper.get(id)
async def update(self, model):
return await self.mapper.update(model)
async def delete(self, id):
return await self.mapper.delete(id)

View File

@ -0,0 +1,39 @@
{% extends 'settings/index.html' %}
{% block header_text %}<h1><i class="fa-solid fa-plus"></i> Create Container</h1>{% endblock %}
{% block main %}
{% include 'settings/containers/form.html' %}
<div class="container">
<form action="/settings/containers/create.html" method="post">
<div>
<label for="name"><i class="fa-solid fa-box"></i> Name</label>
<input type="text" id="name" name="name" required placeholder="Container name">
</div>
<div>
<label for="status"><i class="fa-solid fa-info-circle"></i> Status</label>
<input type="text" id="status" name="status" required placeholder="Container status">
</div>
<div>
<label for="resources"><i class="fa-solid fa-memory"></i> Resources</label>
<input type="text" id="resources" name="resources" placeholder="Resource details">
</div>
<div>
<label for="user_uid"><i class="fa-solid fa-user"></i> User UID</label>
<input type="text" id="user_uid" name="user_uid" placeholder="User UID">
</div>
<div>
<label for="path"><i class="fa-solid fa-folder"></i> Path</label>
<input type="text" id="path" name="path" placeholder="Container path">
</div>
<div>
<label>
<input type="checkbox" name="readonly" value="1">
<i class="fa-solid fa-lock"></i> Readonly
</label>
</div>
<button type="submit"><i class="fa-solid fa-plus"></i> Create</button>
<button onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i>Cancel</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends 'settings/index.html' %}
{% block header_text %}<h1><i class="fa-solid fa-trash-can"></i> Delete Container</h1>{% endblock %}
{% block main %}
<div class="container">
<p>Are you sure you want to <strong>delete</strong> the following container?</p>
<div class="container-name"><i class="fa-solid fa-box"></i> {{ container.name }}</div>
<form method="post" style="margin-top:1.5rem;">
<input type="hidden" name="id" value="{{ container.id }}">
<div class="actions">
<button type="submit"><i class="fa-solid fa-trash"></i> Yes, delete</button>
<button type="button" onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i> Cancel</button>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,28 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<style>
form {
padding: 2rem;
border-radius: 10px;
div {
padding: 10px;
padding-bottom: 15px
}
}
label { font-weight: bold; display: flex; align-items: center; gap: 0.5rem;}
button {
background: #0d6efd; color: #fff;
border: none; border-radius: 5px; padding: 0.6rem 1rem;
cursor: pointer;
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
}
.cancel {
background: #6c757d;
}
@media (max-width: 600px) {
.container { max-width: 98vw; }
form { padding: 1rem; }
}
</style>

View File

@ -0,0 +1,96 @@
{% extends 'settings/index.html' %}
{% block header_text %}<h1><i class="fa-solid fa-database"></i> Containers</h1>{% endblock %}
{% block main %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Containers - List</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<style>
.actions {
display: flex;
gap: 0.5rem;
justify-content: center;
flex-wrap: wrap;
}
.container-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.container-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-radius: 8px;
flex-wrap: wrap;
}
.container-info {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
min-width: 220px;
}
.container-name {
font-size: 1.1rem;
font-weight: 600;
}
@media (max-width: 600px) {
.container-row { flex-direction: column; align-items: stretch; }
.actions { justify-content: flex-start; }
}
.topbar {
display: flex;
margin-bottom: 1rem;
}
button, a.button {
background: #198754; color: #fff; border: none; border-radius: 5px;
padding: 0.4rem 0.8rem; text-decoration: none; cursor: pointer;
transition: background 0.2s;
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
}
.button.delete { background: #dc3545; }
.button.edit { background: #0d6efd; }
.button.clone { background: #6c757d; }
.button.browse { background: #ffc107; color: #212529; }
.button.create { background: #20c997; margin-left: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="topbar">
<a class="button create" href="/settings/containers/create.html">
<i class="fa-solid fa-plus"></i> New Container
</a>
</div>
<section class="container-list">
{% for container in containers %}
<div class="container-row">
<div class="container-info">
<span class="container-name"><i class="fa-solid fa-box"></i> {{ container.name }}</span>
<span title="Status"><i class="fa-solid fa-info-circle"></i> {{ container.status }}</span>
<span title="Readonly">
<i class="fa-solid {% if container.readonly %}fa-lock{% else %}fa-lock-open{% endif %}"></i>
{% if container.readonly %}Readonly{% else %}Writable{% endif %}
</span>
</div>
<div class="actions">
<a class="button edit" href="/settings/containers/container/{{ container.id }}/update.html">
<i class="fa-solid fa-pen"></i> Edit
</a>
<a class="button delete" href="/settings/containers/container/{{ container.id }}/delete.html">
<i class="fa-solid fa-trash"></i> Delete
</a>
</div>
</div>
{% endfor %}
</section>
</div>
</body>
</html>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends "settings/index.html" %}
{% block header_text %}<h1><i class="fa-solid fa-pen"></i> Update Container</h1>{% endblock %}
{% block main %}
{% include "settings/containers/form.html" %}
<div class="container">
<form method="post">
<input type="hidden" name="id" value="{{ container.id }}">
<div>
<label for="name"><i class="fa-solid fa-box"></i> Name</label>
<input type="text" id="name" name="name" value="{{ container.name }}" required>
</div>
<div>
<label for="status"><i class="fa-solid fa-info-circle"></i> Status</label>
<input type="text" id="status" name="status" value="{{ container.status }}" required>
</div>
<div>
<label for="resources"><i class="fa-solid fa-memory"></i> Resources</label>
<input type="text" id="resources" name="resources" value="{{ container.resources }}">
</div>
<div>
<label for="user_uid"><i class="fa-solid fa-user"></i> User UID</label>
<input type="text" id="user_uid" name="user_uid" value="{{ container.user_uid }}">
</div>
<div>
<label for="path"><i class="fa-solid fa-folder"></i> Path</label>
<input type="text" id="path" name="path" value="{{ container.path }}">
</div>
<div>
<label>
<input type="checkbox" name="readonly" value="1" {% if container.readonly %}checked{% endif %}>
<i class="fa-solid fa-lock"></i> Readonly
</label>
</div>
<button type="submit"><i class="fa-solid fa-pen"></i> Update</button>
<button onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i>Cancel</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,91 @@
import asyncio
from aiohttp import web
from snek.system.view import BaseFormView
import pathlib
class ContainersIndexView(BaseFormView):
login_required = True
async def get(self):
user_uid = self.session.get("uid")
containers = []
async for container in self.services.container.find(user_uid=user_uid):
containers.append(container.record)
user = await self.services.user.get(uid=self.session.get("uid"))
return await self.render_template("settings/containers/index.html", {"containers": containers, "user": user})
class ContainersCreateView(BaseFormView):
login_required = True
async def get(self):
return await self.render_template("settings/containers/create.html")
async def post(self):
data = await self.request.post()
container = await self.services.container.create(
user_uid=self.session.get("uid"),
name=data['name'],
status=data['status'],
resources=data.get('resources', ''),
path=data.get('path', ''),
readonly=bool(data.get('readonly', False))
)
return web.HTTPFound("/settings/containers/index.html")
class ContainersUpdateView(BaseFormView):
login_required = True
async def get(self):
container = await self.services.container.get(
user_uid=self.session.get("uid"), uid=self.request.match_info["uid"]
)
if not container:
return web.HTTPNotFound()
return await self.render_template("settings/containers/update.html", {"container": container.record})
async def post(self):
data = await self.request.post()
container = await self.services.container.get(
user_uid=self.session.get("uid"), uid=self.request.match_info["uid"]
)
container['status'] = data['status']
container['resources'] = data.get('resources', '')
container['path'] = data.get('path', '')
container['readonly'] = bool(data.get('readonly', False))
await self.services.container.save(container)
return web.HTTPFound("/settings/containers/index.html")
class ContainersDeleteView(BaseFormView):
login_required = True
async def get(self):
container = await self.services.container.get(
user_uid=self.session.get("uid"), uid=self.request.match_info["uid"]
)
if not container:
return web.HTTPNotFound()
return await self.render_template("settings/containers/delete.html", {"container": container.record})
async def post(self):
user_uid = self.session.get("uid")
uid = self.request.match_info["uid"]
container = await self.services.container.get(
user_uid=user_uid, uid=uid
)
if not container:
return web.HTTPNotFound()
await self.services.container.delete(user_uid=user_uid, uid=uid)
return web.HTTPFound("/settings/containers/index.html")