forked from Hostea/dashboard
feat: views for listing, viewing and deleting specific VMs
parent
2fb1a3d0d1
commit
d5dc06be18
|
@ -4,7 +4,21 @@
|
|||
<p class="secondary-nav__option-link">Hello, {{ username }}!</p>
|
||||
</div>
|
||||
<div class="secondary-nav__options">
|
||||
<a href="{% url 'dash.instances.new' %}" class="secondary-nav__option-link">Instances</a>
|
||||
<details class="secondary-nav__options-group" {{ open_instances }}>
|
||||
<summary><a href="{% url 'dash.instances.new' %}">Manage Instances</a></summary>
|
||||
<div class="secondary-nav__options-group-options">
|
||||
<a href="{% url 'dash.instances.new' %}" class="secondary-nav__option-link">
|
||||
Create Instance
|
||||
</a>
|
||||
</div>
|
||||
<div class="secondary-nav__options-group-options">
|
||||
<a href="{% url 'dash.instances.list' %}" class="secondary-nav__option-link">
|
||||
My Instances
|
||||
</a>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
<details class="secondary-nav__options-group" {{ open_support }}>
|
||||
<summary><a href="{% url 'support.home' %}">Support</a></summary>
|
||||
<div class="secondary-nav__options-group-options">
|
||||
|
@ -16,6 +30,7 @@
|
|||
</a>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<a href="/foo" class="secondary-nav__option-link">Billing</a>
|
||||
</div>
|
||||
<div class="secondary-nav__options">
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
{% extends 'dash/common/base.html' %} {% block dash %}
|
||||
|
||||
<h2>Are you sure you wan't to delete VM {{ instance.name }}</h2>
|
||||
<form
|
||||
action="{% url 'dash.instances.delete' name=instance.name %}"
|
||||
method="POST"
|
||||
class="dash__form"
|
||||
accept-charset="utf-8"
|
||||
>
|
||||
{% include "common/components/error.html" %} {% csrf_token %}
|
||||
<div class="form__action-container">
|
||||
<button class="form__submit--danger" type="submit">Delete Instance</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
|||
{% extends 'dash/common/base.html' %} {% block dash %}
|
||||
<h2>{{ title }}</h2>
|
||||
|
||||
<ul class="list-instance__container">
|
||||
{% for instance in instances %}
|
||||
<li class="list-instance__item">
|
||||
<a
|
||||
href="{% url 'dash.instances.view' name=instance.name %}"
|
||||
class="href">{{ instance.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
|
@ -0,0 +1,17 @@
|
|||
{% extends 'dash/common/base.html' %} {% block dash %}
|
||||
<h2>{{ title }}</h2>
|
||||
<p>
|
||||
Name: {{ instance.name }}
|
||||
</p>
|
||||
<p>
|
||||
Configuration: {{ instance.configuration }}
|
||||
</p>
|
||||
<p>
|
||||
Created On: {{ instance.created_at }}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Click <a href="{% url 'dash.instances.delete' name=instance.name %}">here</a> to delete instance
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
|
@ -174,6 +174,7 @@ class CreateInstance(TestCase):
|
|||
self.assertEqual(b"Logout" in resp.content, True)
|
||||
self.assertEqual(str.encode(test) in resp.content, True)
|
||||
|
||||
# create instance
|
||||
payload = {
|
||||
"name": "test_create_instance_renders",
|
||||
"configuration": self.instance_config[0].name,
|
||||
|
@ -193,11 +194,14 @@ class CreateInstance(TestCase):
|
|||
).exists(),
|
||||
True,
|
||||
)
|
||||
instance = Instance.objects.get(name=payload["name"], owned_by=self.user)
|
||||
|
||||
# try to create instance with duplicate name
|
||||
resp = c.post(reverse("dash.instances.new"), payload)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(b"Instance name exists" in resp.content, True)
|
||||
|
||||
# try to create instance with a VM configuration that doesn't exist
|
||||
payload = {
|
||||
"name": f"2{payload['name']}",
|
||||
"configuration": f"2{payload['name']}",
|
||||
|
@ -205,3 +209,48 @@ class CreateInstance(TestCase):
|
|||
resp = c.post(reverse("dash.instances.new"), payload)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(b"Configuration doesn" in resp.content, True)
|
||||
|
||||
# list instances
|
||||
resp = c.get(reverse("dash.instances.list"))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(str.encode(instance.name) in resp.content, True)
|
||||
|
||||
# view instance details
|
||||
resp = c.get(reverse("dash.instances.view", args=(instance.name,)))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(str.encode(instance.name) in resp.content, True)
|
||||
|
||||
# delete instance details
|
||||
delete_uri = reverse("dash.instances.delete", args=(instance.name,))
|
||||
|
||||
## will ask for sudo confirmation
|
||||
resp = c.get(delete_uri)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
ctx = {"next": delete_uri}
|
||||
self.assertEqual(
|
||||
resp.headers["location"], f"{reverse('accounts.sudo')}?{urlencode(ctx)}"
|
||||
)
|
||||
|
||||
## give sudo confirmation
|
||||
payload = {"password": self.password, "next": ctx["next"]}
|
||||
resp = c.post(reverse("accounts.sudo"), payload)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], ctx["next"])
|
||||
|
||||
resp = c.get(delete_uri)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(
|
||||
str.encode(f"to delete VM {instance.name}") in resp.content, True
|
||||
)
|
||||
|
||||
resp = c.post(delete_uri)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("dash.home"))
|
||||
self.assertEqual(
|
||||
Instance.objects.filter(
|
||||
name=instance.name,
|
||||
owned_by=self.user,
|
||||
configuration_id=self.instance_config[0],
|
||||
).exists(),
|
||||
False,
|
||||
)
|
||||
|
|
|
@ -15,9 +15,12 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
from .views import home, create_instance
|
||||
from .views import home, create_instance, delete_instance, list_instances, view_instance
|
||||
|
||||
urlpatterns = [
|
||||
path("instance/new/", create_instance, name="dash.instances.new"),
|
||||
path("instance/list/", list_instances, name="dash.instances.list"),
|
||||
path("instance/view/<str:name>/", view_instance, name="dash.instances.view"),
|
||||
path("instance/delete/<str:name>/", delete_instance, name="dash.instances.delete"),
|
||||
path("", home, name="dash.home"),
|
||||
]
|
||||
|
|
|
@ -23,6 +23,8 @@ from django.urls import reverse
|
|||
|
||||
from .models import Instance, InstanceConfiguration
|
||||
|
||||
from accounts.decorators import confirm_access
|
||||
|
||||
|
||||
def default_ctx(title: str, username: str):
|
||||
"""
|
||||
|
@ -31,6 +33,7 @@ def default_ctx(title: str, username: str):
|
|||
return {
|
||||
"title": title,
|
||||
"username": username,
|
||||
"open_instances": "open",
|
||||
}
|
||||
|
||||
|
||||
|
@ -95,3 +98,42 @@ def create_instance(request):
|
|||
)
|
||||
instance.save()
|
||||
return redirect(reverse("dash.home"))
|
||||
|
||||
|
||||
@login_required
|
||||
def list_instances(request):
|
||||
PAGE_TITLE = "My Instances"
|
||||
user = request.user
|
||||
|
||||
instances = Instance.objects.filter(owned_by=user)
|
||||
ctx = default_ctx(title=PAGE_TITLE, username=user.username)
|
||||
ctx["instances"] = instances
|
||||
return render(request, "dash/instances/list/index.html", context=ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def view_instance(request, name: str):
|
||||
PAGE_TITLE = f"Hostea Instance: {name}"
|
||||
user = request.user
|
||||
|
||||
instance = get_object_or_404(Instance, owned_by=user, name=name)
|
||||
ctx = default_ctx(title=PAGE_TITLE, username=user.username)
|
||||
ctx["instance"] = instance
|
||||
return render(request, "dash/instances/view/index.html", context=ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
@confirm_access
|
||||
def delete_instance(request, name):
|
||||
PAGE_TITLE = f"Delete Instance {name}"
|
||||
user = request.user
|
||||
|
||||
instance = get_object_or_404(Instance, name=name, owned_by=user)
|
||||
if request.method == "GET":
|
||||
ctx = default_ctx(title=PAGE_TITLE, username=user.username)
|
||||
ctx["instance"] = instance
|
||||
return render(request, "dash/instances/delete/index.html", context=ctx)
|
||||
|
||||
instance.delete()
|
||||
return redirect(reverse("dash.home"))
|
||||
|
|
|
@ -595,6 +595,20 @@ fieldset {
|
|||
margin: 10px;
|
||||
}
|
||||
|
||||
.form__submit--danger {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-weight: 400;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
background-color: #e11d21;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
.form__label {
|
||||
margin: 5px 0;
|
||||
|
|
Loading…
Reference in New Issue