Add organization and global dashboards
All checks were successful
Tests / test (push) Successful in 24s
All checks were successful
Tests / test (push) Successful in 24s
This commit is contained in:
parent
1ca3658a5a
commit
ddbc97721c
8 changed files with 530 additions and 4 deletions
|
@ -10,6 +10,8 @@
|
|||
<link rel="stylesheet" href="{% static 'mazer/compiled/css/iconly.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/servala.css' %}">
|
||||
<script src="{% static "js/htmx.min.js" %}" defer></script>
|
||||
{% block extra_css %}
|
||||
{% endblock extra_css %}
|
||||
</head>
|
||||
<title>
|
||||
{% block html_title %}
|
||||
|
|
|
@ -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,260 @@
|
|||
{% 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>
|
||||
{% comment %}
|
||||
{# TODO: use once support form is merged #}
|
||||
<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>
|
||||
{% endcomment %}
|
||||
</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 = [
|
||||
path("accounts/profile/", views.ProfileView.as_view(), name="profile"),
|
||||
path("accounts/logout/", views.LogoutView.as_view(), name="logout"),
|
||||
path(
|
||||
"organizations/",
|
||||
views.OrganizationSelectionView.as_view(),
|
||||
name="organization.selection",
|
||||
),
|
||||
path(
|
||||
"organizations/create",
|
||||
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 .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 (
|
||||
OrganizationCreateView,
|
||||
OrganizationDashboardView,
|
||||
|
@ -21,6 +28,7 @@ __all__ = [
|
|||
"LogoutView",
|
||||
"OrganizationCreateView",
|
||||
"OrganizationDashboardView",
|
||||
"OrganizationSelectionView",
|
||||
"OrganizationUpdateView",
|
||||
"ServiceDetailView",
|
||||
"ServiceInstanceDeleteView",
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
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.utils.functional import cached_property
|
||||
from django.views.generic import TemplateView
|
||||
|
@ -13,6 +15,30 @@ class IndexView(TemplateView):
|
|||
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):
|
||||
template_name = "frontend/profile.html"
|
||||
form_class = UserProfileForm
|
||||
|
|
|
@ -3,7 +3,12 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django.views.generic import CreateView, DetailView
|
||||
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.views.mixins import HtmxUpdateView, OrganizationViewMixin
|
||||
|
||||
|
@ -61,6 +66,37 @@ class OrganizationDashboardView(
|
|||
):
|
||||
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):
|
||||
template_name = "frontend/organizations/update.html"
|
||||
|
|
|
@ -99,3 +99,78 @@ a.btn-keycloak {
|
|||
.hide-form-errors .alert.form-errors {
|
||||
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