Merge pull request 'Add organization and global dashboards' (#116) from 51-login-dashboard into main
Reviewed-on: #116
This commit is contained in:
commit
778c1fb801
7 changed files with 526 additions and 4 deletions
|
@ -0,0 +1,111 @@
|
||||||
|
{% extends "frontend/base.html" %}
|
||||||
|
{% load i18n static %}
|
||||||
|
{% block html_title %}
|
||||||
|
{% block page_title %}
|
||||||
|
{% translate "Welcome to Servala" %}
|
||||||
|
{% endblock page_title %}
|
||||||
|
{% endblock html_title %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="section">
|
||||||
|
{% if not user_organizations %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="card-title">{% translate "Welcome to Servala!" %}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="mb-4">
|
||||||
|
<i class="bi bi-building fs-1 text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="mb-3">{% translate "Get Started with Your First Organization" %}</h5>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
{% blocktranslate trimmed %}
|
||||||
|
Organizations help you manage your services and resources.
|
||||||
|
Create your first organization to start using Servala's service catalog.
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</p>
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||||
|
<a href="{% url 'frontend:organization.create' %}"
|
||||||
|
class="btn btn-primary btn-lg">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>
|
||||||
|
{% translate "Create Organization" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 pt-4 border-top">
|
||||||
|
<h6 class="text-muted mb-3">{% translate "Next Steps" %}</h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="bi bi-1-circle fs-2 text-success mb-2"></i>
|
||||||
|
<h6>{% translate "Create Organization" %}</h6>
|
||||||
|
<small class="text-muted">{% translate "Set up your workspace" %}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="bi bi-2-circle fs-2 text-info mb-2"></i>
|
||||||
|
<h6>{% translate "Discover Services" %}</h6>
|
||||||
|
<small class="text-muted">{% translate "Browse available services" %}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="bi bi-3-circle fs-2 text-warning mb-2"></i>
|
||||||
|
<h6>{% translate "Create Instances" %}</h6>
|
||||||
|
<small class="text-muted">{% translate "Deploy your first service" %}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
{% for organization in user_organizations %}
|
||||||
|
<div class="col-md-6 col-lg-4 mb-4">
|
||||||
|
<div class="card h-100 border-2 organization-card">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<div class="text-center mb-3">
|
||||||
|
<i class="bi bi-building fs-1 text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title text-center mb-3">{{ organization.name }}</h5>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-server me-1"></i>
|
||||||
|
{{ organization.instance_count }}
|
||||||
|
{% blocktranslate trimmed count count=organization.instance_count %}
|
||||||
|
instance
|
||||||
|
{% plural %}
|
||||||
|
instances
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</small>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-people me-1"></i>
|
||||||
|
{{ organization.members.count }}
|
||||||
|
{% blocktranslate trimmed count count=organization.members.count %}
|
||||||
|
member
|
||||||
|
{% plural %}
|
||||||
|
members
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="{% url 'frontend:organization.dashboard' organization=organization.slug %}"
|
||||||
|
class="btn btn-primary w-100">{% translate "Enter Organization" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="text-center mt-4 pt-4 border-top"></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
{% endblock content %}
|
|
@ -1 +1,258 @@
|
||||||
{% extends "frontend/base.html" %}
|
{% extends "frontend/base.html" %}
|
||||||
|
{% load i18n static %}
|
||||||
|
{% block html_title %}
|
||||||
|
{{ object.name }} {% translate "Dashboard" %}
|
||||||
|
{% endblock html_title %}
|
||||||
|
{% block page_title %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="section">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-2">{% translate "Welcome to" %} {{ object.name }}</h4>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
{% if has_instances %}
|
||||||
|
{% translate "Here's an overview of your organization's services and resources." %}
|
||||||
|
{% else %}
|
||||||
|
{% translate "Ready to get started? Discover services and create your first instance." %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% if user_role %}
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="badge bg-light text-dark">
|
||||||
|
{% if user_role == "owner" %}
|
||||||
|
<i class="bi bi-star-fill me-1"></i>{% translate "Owner" %}
|
||||||
|
{% elif user_role == "admin" %}
|
||||||
|
<i class="bi bi-gear me-1"></i>{% translate "Administrator" %}
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-person me-1"></i>{% translate "Member" %}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if has_instances %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="stats-icon purple me-3">
|
||||||
|
<i class="bi bi-hdd-stack"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="text-muted font-semibold mb-0">{% translate "Service Instances" %}</h6>
|
||||||
|
<h4 class="font-extrabold mb-0 mt-1">{{ service_instances_count }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="stats-icon blue me-3">
|
||||||
|
<i class="bi bi-people"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="text-muted font-semibold mb-0">{% translate "Team Members" %}</h6>
|
||||||
|
<h4 class="font-extrabold mb-0 mt-1">{{ members_count }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="row">
|
||||||
|
{% if has_instances %}
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="card-title">{% translate "Recent Service Instances" %}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% translate "Name" %}</th>
|
||||||
|
<th>{% translate "Service" %}</th>
|
||||||
|
<th>{% translate "Status" %}</th>
|
||||||
|
<th>{% translate "Created" %}</th>
|
||||||
|
<th>{% translate "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for instance in service_instances %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'frontend:organization.instance' organization=object.slug slug=instance.name %}"
|
||||||
|
class="fw-semibold text-decoration-none">{{ instance.name }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
{% if instance.context.service_offering.service.logo %}
|
||||||
|
<img src="{{ instance.context.service_offering.service.logo.url }}"
|
||||||
|
alt="{{ instance.context.service_offering.service.name }}"
|
||||||
|
class="me-2"
|
||||||
|
style="width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
object-fit: contain">
|
||||||
|
{% endif %}
|
||||||
|
<span class="small text-muted">{{ instance.context.service_offering.service.name }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if instance.is_deleted %}
|
||||||
|
<span class="badge bg-danger">{% translate "Deleted" %}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success">{% translate "Active" %}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="small text-muted">{{ instance.created_at|date:"M d, Y" }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'frontend:organization.instance' organization=object.slug slug=instance.name %}"
|
||||||
|
class="btn btn-outline-primary btn-sm"
|
||||||
|
title="{% translate 'View Details' %}">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
{% if instance.has_change_permission %}
|
||||||
|
<a href="{% url 'frontend:organization.instance.update' organization=object.slug slug=instance.name %}"
|
||||||
|
class="btn btn-outline-secondary btn-sm"
|
||||||
|
title="{% translate 'Edit' %}">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="{% url 'frontend:organization.instances' organization=object.slug %}"
|
||||||
|
class="btn btn-outline-primary me-2">{% translate "View All Instances" %}</a>
|
||||||
|
<a href="{% url 'frontend:organization.services' organization=object.slug %}"
|
||||||
|
class="btn btn-primary">{% translate "Create New Instance" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="card-title">{% translate "Quick Actions" %}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="list-group list-group-flush quick-actions">
|
||||||
|
<a href="{{ object.urls.services }}"
|
||||||
|
class="list-group-item list-group-item-action d-flex align-items-center">
|
||||||
|
<i class="bi h2 bi-grid-3x3-gap me-3 text-primary"></i>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">{% translate "Browse Services" %}</h6>
|
||||||
|
<small class="text-muted">{% translate "Explore available services" %}</small>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="{{ object.urls.instances }}"
|
||||||
|
class="list-group-item list-group-item-action d-flex align-items-center">
|
||||||
|
<i class="bi h2 bi-server me-3 text-success"></i>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">{% translate "Manage Instances" %}</h6>
|
||||||
|
<small class="text-muted">{% translate "View and configure instances" %}</small>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="{{ object.urls.details }}"
|
||||||
|
class="list-group-item list-group-item-action d-flex align-items-center">
|
||||||
|
<i class="bi h2 bi-gear me-3 text-warning"></i>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">{% translate "Organization Settings" %}</h6>
|
||||||
|
<small class="text-muted">{% translate "Configure organization details" %}</small>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="{{ organization.urls.support }}"
|
||||||
|
class="list-group-item list-group-item-action d-flex align-items-center">
|
||||||
|
<i class="bi h2 bi-life-preserver me-3 text-info"></i>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">{% translate "Get Support" %}</h6>
|
||||||
|
<small class="text-muted">{% translate "Access help and documentation" %}</small>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{# if has_instances #}
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="card-title">{% translate "Get Started" %}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<i class="bi bi-rocket fs-1 text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="mb-3">{% translate "Ready to deploy your first service?" %}</h5>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
{% translate "Browse our service catalog and deploy databases, storage, and other services with just a few clicks." %}
|
||||||
|
</p>
|
||||||
|
<a href="{% url 'frontend:organization.services' organization=object.slug %}"
|
||||||
|
class="btn btn-primary btn-lg">
|
||||||
|
<i class="bi bi-search me-2"></i>
|
||||||
|
{% translate "Discover Services" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="card-title">{% translate "Next Steps" %}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="timeline">
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-marker bg-success"></div>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<h6 class="text-success">{% translate "Organization Created" %}</h6>
|
||||||
|
<small class="text-muted">{% translate "You're all set up!" %}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-marker bg-primary"></div>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<h6>{% translate "Discover Services" %}</h6>
|
||||||
|
<small class="text-muted">{% translate "Browse what's available" %}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-marker"></div>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<h6 class="text-muted">{% translate "Deploy Your First Service" %}</h6>
|
||||||
|
<small class="text-muted">{% translate "Create an instance" %}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock content %}
|
||||||
|
|
|
@ -6,6 +6,11 @@ from servala.frontend import views
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("accounts/profile/", views.ProfileView.as_view(), name="profile"),
|
path("accounts/profile/", views.ProfileView.as_view(), name="profile"),
|
||||||
path("accounts/logout/", views.LogoutView.as_view(), name="logout"),
|
path("accounts/logout/", views.LogoutView.as_view(), name="logout"),
|
||||||
|
path(
|
||||||
|
"organizations/",
|
||||||
|
views.OrganizationSelectionView.as_view(),
|
||||||
|
name="organization.selection",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"organizations/create",
|
"organizations/create",
|
||||||
views.OrganizationCreateView.as_view(),
|
views.OrganizationCreateView.as_view(),
|
||||||
|
@ -68,5 +73,9 @@ urlpatterns = [
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
path("", RedirectView.as_view(pattern_name="frontend:profile"), name="index"),
|
path(
|
||||||
|
"",
|
||||||
|
RedirectView.as_view(pattern_name="frontend:organization.selection"),
|
||||||
|
name="index",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
from .auth import LogoutView
|
from .auth import LogoutView
|
||||||
from .generic import IndexView, ProfileView, custom_403, custom_404, custom_500
|
from .generic import (
|
||||||
|
IndexView,
|
||||||
|
OrganizationSelectionView,
|
||||||
|
ProfileView,
|
||||||
|
custom_403,
|
||||||
|
custom_404,
|
||||||
|
custom_500,
|
||||||
|
)
|
||||||
from .organization import (
|
from .organization import (
|
||||||
OrganizationCreateView,
|
OrganizationCreateView,
|
||||||
OrganizationDashboardView,
|
OrganizationDashboardView,
|
||||||
|
@ -21,6 +28,7 @@ __all__ = [
|
||||||
"LogoutView",
|
"LogoutView",
|
||||||
"OrganizationCreateView",
|
"OrganizationCreateView",
|
||||||
"OrganizationDashboardView",
|
"OrganizationDashboardView",
|
||||||
|
"OrganizationSelectionView",
|
||||||
"OrganizationUpdateView",
|
"OrganizationUpdateView",
|
||||||
"ServiceDetailView",
|
"ServiceDetailView",
|
||||||
"ServiceInstanceDeleteView",
|
"ServiceInstanceDeleteView",
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.shortcuts import render
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.db.models import Count, Q
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
@ -13,6 +15,30 @@ class IndexView(TemplateView):
|
||||||
template_name = "frontend/index.html"
|
template_name = "frontend/index.html"
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationSelectionView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = "frontend/organization_selection.html"
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def user_organizations(self):
|
||||||
|
return self.request.user.organizations.all()
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
# Users with a single organization get redirected to the organization’s dashboard
|
||||||
|
if self.user_organizations.count() == 1:
|
||||||
|
organization = self.user_organizations.first()
|
||||||
|
return redirect(organization.urls.base)
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["user_organizations"] = self.user_organizations.annotate(
|
||||||
|
instance_count=Count(
|
||||||
|
"service_instances", filter=Q(service_instances__is_deleted=False)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProfileView(HtmxUpdateView):
|
class ProfileView(HtmxUpdateView):
|
||||||
template_name = "frontend/profile.html"
|
template_name = "frontend/profile.html"
|
||||||
form_class = UserProfileForm
|
form_class = UserProfileForm
|
||||||
|
|
|
@ -3,7 +3,12 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, DetailView
|
from django.views.generic import CreateView, DetailView
|
||||||
from rules.contrib.views import AutoPermissionRequiredMixin
|
from rules.contrib.views import AutoPermissionRequiredMixin
|
||||||
|
|
||||||
from servala.core.models import BillingEntity, Organization
|
from servala.core.models import (
|
||||||
|
BillingEntity,
|
||||||
|
Organization,
|
||||||
|
OrganizationMembership,
|
||||||
|
ServiceInstance,
|
||||||
|
)
|
||||||
from servala.frontend.forms.organization import OrganizationCreateForm, OrganizationForm
|
from servala.frontend.forms.organization import OrganizationCreateForm, OrganizationForm
|
||||||
from servala.frontend.views.mixins import HtmxUpdateView, OrganizationViewMixin
|
from servala.frontend.views.mixins import HtmxUpdateView, OrganizationViewMixin
|
||||||
|
|
||||||
|
@ -61,6 +66,37 @@ class OrganizationDashboardView(
|
||||||
):
|
):
|
||||||
template_name = "frontend/organizations/dashboard.html"
|
template_name = "frontend/organizations/dashboard.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
organization = self.get_object()
|
||||||
|
|
||||||
|
service_instances = ServiceInstance.objects.filter(
|
||||||
|
organization=organization, is_deleted=False
|
||||||
|
)
|
||||||
|
recent_instances = service_instances.order_by("-created_at")[:5]
|
||||||
|
|
||||||
|
for instance in recent_instances:
|
||||||
|
instance.has_change_permission = self.request.user.has_perm(
|
||||||
|
"core.change_serviceinstance", instance
|
||||||
|
)
|
||||||
|
|
||||||
|
user_membership = OrganizationMembership.objects.filter(
|
||||||
|
user=self.request.user, organization=organization
|
||||||
|
).first()
|
||||||
|
user_role = user_membership.role if user_membership else None
|
||||||
|
|
||||||
|
context.update(
|
||||||
|
{
|
||||||
|
"service_instances_count": service_instances.count(),
|
||||||
|
"service_instances": recent_instances,
|
||||||
|
"members_count": organization.members.count(),
|
||||||
|
"has_instances": service_instances.exists(),
|
||||||
|
"user_role": user_role,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
|
class OrganizationUpdateView(OrganizationViewMixin, HtmxUpdateView):
|
||||||
template_name = "frontend/organizations/update.html"
|
template_name = "frontend/organizations/update.html"
|
||||||
|
|
|
@ -99,3 +99,78 @@ a.btn-keycloak {
|
||||||
.hide-form-errors .alert.form-errors {
|
.hide-form-errors .alert.form-errors {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.organization-card {
|
||||||
|
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.stats-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: white;
|
||||||
|
i::before {
|
||||||
|
display: block;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-icon.purple {
|
||||||
|
background-color: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-icon.blue {
|
||||||
|
background-color: var(--bs-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-icon.green {
|
||||||
|
background-color: var(--brand-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-marker {
|
||||||
|
position: absolute;
|
||||||
|
left: -16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #d1d5db;
|
||||||
|
border: 3px solid white;
|
||||||
|
box-shadow: 0 0 0 3px #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.quick-actions i.bi {
|
||||||
|
margin-top: -16px;
|
||||||
|
padding-right: 28px;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue