Merge pull request #141 from simple-login/dashboard-ui
Better dashboard ui, global stats
This commit is contained in:
commit
f8ba0d954f
|
@ -20,6 +20,61 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block default_content %}
|
{% block default_content %}
|
||||||
|
<!-- Global Stats -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 col-sm-4 col-lg-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-3 text-center">
|
||||||
|
<div class="h1 m-0 mt-3">{{ stats.nb_alias }}</div>
|
||||||
|
<div class="text-muted mb-4">Aliases</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-4 col-lg-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-3 text-center">
|
||||||
|
<div class="h1 m-0 mt-3">{{ stats.nb_active_alias }}</div>
|
||||||
|
<div class="text-muted mb-4">Active Aliases</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-4 col-lg-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-3 text-center">
|
||||||
|
<div class="h1 m-0 mt-3">{{ stats.nb_forward }}</div>
|
||||||
|
<div class="text-muted mb-4">Forwards</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-4 col-lg-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-3 text-center">
|
||||||
|
<div class="h1 m-0 mt-3">{{ stats.nb_reply }}</div>
|
||||||
|
<div class="text-muted mb-4">Replies</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-4 col-lg-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-3 text-center">
|
||||||
|
<div class="h1 m-0 mt-3">{{ stats.nb_directory }}</div>
|
||||||
|
<div class="text-muted mb-4">Directories</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-4 col-lg-2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-3 text-center">
|
||||||
|
<div class="h1 m-0 mt-3">{{ stats.nb_domain }}</div>
|
||||||
|
<div class="text-muted mb-4">Domains</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- END Global Stats -->
|
||||||
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|
||||||
<div class="col-lg-6 pt-1" style="max-width: 25em">
|
<div class="col-lg-6 pt-1" style="max-width: 25em">
|
||||||
|
@ -62,53 +117,65 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-auto pt-1 flex-grow-1">
|
<div id="filter-app" class="col-lg-auto pt-1 flex-grow-1">
|
||||||
<div class="float-right">
|
<div class="float-right d-flex">
|
||||||
<form method="get" class="form-inline">
|
|
||||||
<select name="sort"
|
|
||||||
onchange="this.form.submit()"
|
|
||||||
class="form-control custom-select mr-3 shadow">
|
|
||||||
<option value="" {% if sort == "" %} selected {% endif %}>
|
|
||||||
Sort by most recent activity
|
|
||||||
</option>
|
|
||||||
<option value="old2new" {% if sort == "old2new" %} selected {% endif %}>
|
|
||||||
Alias Old-Recent
|
|
||||||
</option>
|
|
||||||
<option value="old2new" {% if sort == "new2old" %} selected {% endif %}>
|
|
||||||
Alias Recent-Old
|
|
||||||
</option>
|
|
||||||
<option value="a2z" {% if sort == "a2z" %} selected {% endif %}>
|
|
||||||
Alias A-Z
|
|
||||||
</option>
|
|
||||||
<option value="z2a" {% if sort == "z2a" %} selected {% endif %}>
|
|
||||||
Alias Z-A
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select name="filter"
|
<!-- Filter Control -->
|
||||||
onchange="this.form.submit()"
|
<div v-if="showFilter" id="filter-control">
|
||||||
class="form-control custom-select mr-3 shadow">
|
<form method="get" class="form-inline">
|
||||||
<option value="" {% if filter == "" %} selected {% endif %}>
|
<select name="sort"
|
||||||
All Aliases
|
onchange="this.form.submit()"
|
||||||
</option>
|
class="form-control custom-select mr-3 shadow">
|
||||||
<option value="enabled" {% if filter == "enabled" %} selected {% endif %}>
|
<option value="" {% if sort == "" %} selected {% endif %}>
|
||||||
Only Enabled Aliases
|
Sort by most recent activity
|
||||||
</option>
|
</option>
|
||||||
<option value="disabled" {% if filter == "disabled" %} selected {% endif %}>
|
<option value="old2new" {% if sort == "old2new" %} selected {% endif %}>
|
||||||
Only Disabled Aliases
|
Alias Old-Recent
|
||||||
</option>
|
</option>
|
||||||
</select>
|
<option value="old2new" {% if sort == "new2old" %} selected {% endif %}>
|
||||||
|
Alias Recent-Old
|
||||||
|
</option>
|
||||||
|
<option value="a2z" {% if sort == "a2z" %} selected {% endif %}>
|
||||||
|
Alias A-Z
|
||||||
|
</option>
|
||||||
|
<option value="z2a" {% if sort == "z2a" %} selected {% endif %}>
|
||||||
|
Alias Z-A
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<input type="search" name="query" placeholder="Enter to search for alias"
|
<select name="filter"
|
||||||
class="form-control shadow mr-2"
|
onchange="this.form.submit()"
|
||||||
style="max-width: 15em"
|
class="form-control custom-select mr-3 shadow">
|
||||||
value="{{ query }}">
|
<option value="" {% if filter == "" %} selected {% endif %}>
|
||||||
|
All Aliases
|
||||||
|
</option>
|
||||||
|
<option value="enabled" {% if filter == "enabled" %} selected {% endif %}>
|
||||||
|
Only Enabled Aliases
|
||||||
|
</option>
|
||||||
|
<option value="disabled" {% if filter == "disabled" %} selected {% endif %}>
|
||||||
|
Only Disabled Aliases
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
{% if query or sort or filter %}
|
<input type="search" name="query" placeholder="Enter to search for alias"
|
||||||
<a href="{{ url_for('dashboard.index') }}"
|
class="form-control shadow mr-2"
|
||||||
class="btn btn-light">Reset</a>
|
style="max-width: 15em"
|
||||||
{% endif %}
|
value="{{ query }}">
|
||||||
</form>
|
|
||||||
|
{% if query or sort or filter %}
|
||||||
|
<a href="{{ url_for('dashboard.index') }}"
|
||||||
|
class="btn btn-light">Reset</a>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a v-if="!showFilter" @click="toggleFilter()" class="btn btn-secondary">
|
||||||
|
<i class="fe fe-chevrons-left"></i> Filters
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a v-if="showFilter" @click="toggleFilter()" class="btn btn-outline-secondary">
|
||||||
|
<i class="fe fe-chevrons-right"></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -169,7 +236,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email Activity -->
|
<!-- Email Activity -->
|
||||||
<div class="row">
|
<div class="row mb-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div style="font-size: 12px">
|
<div style="font-size: 12px">
|
||||||
{% if alias_info.latest_email_log != None %}
|
{% if alias_info.latest_email_log != None %}
|
||||||
|
@ -199,15 +266,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
No Activity. Alias created {{ alias.created_at | dt }}
|
No Activity. Alias created {{ alias.created_at | dt }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br>
|
|
||||||
|
|
||||||
<span class="alias-activity">{{ alias_info.nb_forward }}</span> forwards,
|
|
||||||
<span class="alias-activity">{{ alias_info.nb_blocked }}</span> blocks,
|
|
||||||
<span class="alias-activity">{{ alias_info.nb_reply }}</span> replies
|
|
||||||
<a href="{{ url_for('dashboard.alias_log', alias_id=alias.id) }}"
|
|
||||||
class="btn btn-sm btn-link">
|
|
||||||
See All →
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -250,6 +309,14 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<span class="alias-activity">{{ alias_info.nb_forward }}</span> forwards,
|
||||||
|
<span class="alias-activity">{{ alias_info.nb_blocked }}</span> blocks,
|
||||||
|
<span class="alias-activity">{{ alias_info.nb_reply }}</span> replies
|
||||||
|
<a href="{{ url_for('dashboard.alias_log', alias_id=alias.id) }}"
|
||||||
|
class="btn btn-sm btn-link">
|
||||||
|
See All →
|
||||||
|
</a>
|
||||||
|
|
||||||
{% if mailboxes|length > 1 %}
|
{% if mailboxes|length > 1 %}
|
||||||
<div class="small-text">Current mailbox</div>
|
<div class="small-text">Current mailbox</div>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
|
@ -336,7 +403,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- END Collapse section -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -582,4 +649,26 @@
|
||||||
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='node_modules/vue/dist/vue.min.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
var app = new Vue({
|
||||||
|
el: '#filter-app',
|
||||||
|
delimiters: ["[[", "]]"], // necessary to avoid conflict with jinja
|
||||||
|
data: {
|
||||||
|
showFilter: false
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async toggleFilter() {
|
||||||
|
let that = this;
|
||||||
|
that.showFilter = !that.showFilter;
|
||||||
|
store.set('showFilter', that.showFilter);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
if (store.get("showFilter"))
|
||||||
|
this.showFilter = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from flask import render_template, request, redirect, url_for, flash
|
from flask import render_template, request, redirect, url_for, flash
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
@ -12,9 +14,45 @@ from app.models import (
|
||||||
ClientUser,
|
ClientUser,
|
||||||
DeletedAlias,
|
DeletedAlias,
|
||||||
AliasGeneratorEnum,
|
AliasGeneratorEnum,
|
||||||
|
User,
|
||||||
|
EmailLog,
|
||||||
|
CustomDomain,
|
||||||
|
Directory,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Stats:
|
||||||
|
nb_alias: int
|
||||||
|
nb_active_alias: int
|
||||||
|
nb_forward: int
|
||||||
|
nb_reply: int
|
||||||
|
nb_domain: int
|
||||||
|
nb_directory: int
|
||||||
|
|
||||||
|
|
||||||
|
def get_stats(user: User) -> Stats:
|
||||||
|
nb_alias = Alias.query.filter_by(user_id=user.id).count()
|
||||||
|
nb_active_alias = Alias.query.filter_by(user_id=user.id, enabled=True).count()
|
||||||
|
nb_forward = EmailLog.query.filter_by(
|
||||||
|
user_id=user.id, is_reply=False, blocked=False, bounced=False
|
||||||
|
).count()
|
||||||
|
nb_reply = EmailLog.query.filter_by(
|
||||||
|
user_id=user.id, is_reply=True, blocked=False, bounced=False
|
||||||
|
).count()
|
||||||
|
nb_domain = CustomDomain.query.filter_by(user_id=user.id).count()
|
||||||
|
nb_directory = Directory.query.filter_by(user_id=user.id).count()
|
||||||
|
|
||||||
|
data = locals()
|
||||||
|
# to keep only Stats field
|
||||||
|
data = {
|
||||||
|
k: v
|
||||||
|
for (k, v) in data.items()
|
||||||
|
if k in vars(Stats)["__dataclass_fields__"].keys()
|
||||||
|
}
|
||||||
|
return Stats(**data)
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route("/", methods=["GET", "POST"])
|
@dashboard_bp.route("/", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
|
@ -120,6 +158,8 @@ def index():
|
||||||
current_user.intro_shown = True
|
current_user.intro_shown = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
stats = get_stats(current_user)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"dashboard/index.html",
|
"dashboard/index.html",
|
||||||
client_users=client_users,
|
client_users=client_users,
|
||||||
|
@ -134,4 +174,5 @@ def index():
|
||||||
page=page,
|
page=page,
|
||||||
sort=sort,
|
sort=sort,
|
||||||
filter=alias_filter,
|
filter=alias_filter,
|
||||||
|
stats=stats,
|
||||||
)
|
)
|
||||||
|
|
5
static/package-lock.json
generated
5
static/package-lock.json
generated
|
@ -98,6 +98,11 @@
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
|
||||||
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
|
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
|
||||||
|
},
|
||||||
|
"vue": {
|
||||||
|
"version": "2.6.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.11.tgz",
|
||||||
|
"integrity": "sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"intro.js": "^2.9.3",
|
"intro.js": "^2.9.3",
|
||||||
"notie": "^4.3.1",
|
"notie": "^4.3.1",
|
||||||
"qrious": "^4.0.2",
|
"qrious": "^4.0.2",
|
||||||
"toastr": "^2.1.4"
|
"toastr": "^2.1.4",
|
||||||
|
"vue": "^2.6.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<a href="{{ url_for('dashboard.index') }}"
|
<a href="{{ url_for('dashboard.index') }}"
|
||||||
class="nav-link {{ 'active' if active_page == 'dashboard' }}">
|
class="nav-link {{ 'active' if active_page == 'dashboard' }}">
|
||||||
<i class="fe fe-home"></i>
|
<i class="fe fe-home"></i>
|
||||||
Alias
|
Aliases
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@ -11,21 +11,21 @@
|
||||||
<a href="{{ url_for('dashboard.api_key') }}"
|
<a href="{{ url_for('dashboard.api_key') }}"
|
||||||
class="nav-link {{ 'active' if active_page == 'api_key' }}">
|
class="nav-link {{ 'active' if active_page == 'api_key' }}">
|
||||||
<i><img src="/static/key.svg"></i>
|
<i><img src="/static/key.svg"></i>
|
||||||
API Key
|
API Keys
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="{{ url_for('dashboard.custom_domain') }}"
|
<a href="{{ url_for('dashboard.custom_domain') }}"
|
||||||
class="nav-link {{ 'active' if active_page == 'custom_domain' }}">
|
class="nav-link {{ 'active' if active_page == 'custom_domain' }}">
|
||||||
<i class="fe fe-server"></i> Custom Domains
|
<i class="fe fe-server"></i> Domains
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="{{ url_for('dashboard.directory') }}"
|
<a href="{{ url_for('dashboard.directory') }}"
|
||||||
class="nav-link {{ 'active' if active_page == 'directory' }}">
|
class="nav-link {{ 'active' if active_page == 'directory' }}">
|
||||||
<i class="fe fe-folder"></i> Alias Directory
|
<i class="fe fe-folder"></i> Directories
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue