1
0
mirror of https://github.com/donaldzou/WGDashboard.git synced 2024-11-06 16:00:28 +01:00

20240323 Commit

Finished implementing peer settings dropdown and planned how peer settings will be link
This commit is contained in:
Donald Zou 2024-03-21 02:03:08 -04:00
parent 0aa4c8af6f
commit f1e71ecb78
15 changed files with 1644 additions and 429 deletions

View File

@ -9,7 +9,6 @@ lerna-debug.log*
node_modules node_modules
.DS_Store .DS_Store
dist
dist-ssr dist-ssr
coverage coverage
*.local *.local

Binary file not shown.

Binary file not shown.

9
src/static/app/dist/assets/index.css vendored Normal file

File diff suppressed because one or more lines are too long

40
src/static/app/dist/assets/index.js vendored Normal file

File diff suppressed because one or more lines are too long

938
src/static/app/dist/dashboard.css vendored Normal file
View File

@ -0,0 +1,938 @@
body {
font-size: .875rem;
/*font-family: 'Poppins', sans-serif;*/
}
.codeFont{
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.feather {
width: 16px;
height: 16px;
vertical-align: text-bottom;
}
.btn-primary {
font-weight: bold;
}
.dashboardLogo{
background: -webkit-linear-gradient(#178bff, #ff4a00);
}
/*
* Sidebar
*/
/*.sidebar {*/
/* position: fixed;*/
/* top: 0;*/
/* bottom: 0;*/
/* left: 0;*/
/* z-index: 100;*/
/* !* Behind the navbar *!*/
/* padding: 48px 0 0;*/
/* !* Height of navbar *!*/
/* box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);*/
/*}*/
/*.sidebar-sticky {*/
/* position: relative;*/
/* top: 0;*/
/* height: calc(100vh - 48px);*/
/* padding-top: .5rem;*/
/* overflow-x: hidden;*/
/* overflow-y: auto;*/
/* !* Scrollable contents if viewport is shorter than content. *!*/
/*}*/
/*@supports ((position: -webkit-sticky) or (position: sticky)) {*/
/* .sidebar-sticky {*/
/* position: -webkit-sticky;*/
/* position: sticky;*/
/* }*/
/*}*/
.sidebar .nav-link, .bottomNavContainer .nav-link{
font-weight: 500;
color: #333;
transition: 0.2s cubic-bezier(0.82, -0.07, 0, 1.01);
}
.nav-link:hover {
padding-left: 30px;
background-color: #dfdfdf;
}
.sidebar .nav-link .feather {
margin-right: 4px;
color: #999;
}
.sidebar .nav-link.active, .bottomNavContainer .nav-link.active {
color: #007bff;
}
.sidebar .nav-link:hover .feather,
.sidebar .nav-link.active .feather {
color: inherit;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
/*
* Navbar
*/
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
font-size: 1rem;
/*background-color: rgba(0, 0, 0, .25);*/
/*box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);*/
}
.navbar .navbar-toggler {
top: .25rem;
right: 1rem;
}
.form-control {
transition: all 0.2s ease-in-out;
}
.form-control:disabled {
cursor: not-allowed;
}
.navbar .form-control {
padding: .75rem 1rem;
border-width: 0;
border-radius: 0;
}
.form-control-dark {
color: #fff;
background-color: rgba(255, 255, 255, .1);
border-color: rgba(255, 255, 255, .1);
}
.form-control-dark:focus {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
}
.dot {
width: 10px;
height: 10px;
border-radius: 50px;
display: inline-block;
margin-left: auto !important;
}
.dot-running {
background-color: #28a745!important;
box-shadow: 0 0 0 0.2rem #28a74545;
}
.h6-dot-running {
margin-left: 0.3rem;
}
.dot-stopped {
background-color: #6c757d!important;
}
.card-running {
border-color: #28a745;
}
.info h6 {
line-break: anywhere;
transition: all 0.4s cubic-bezier(0.96, -0.07, 0.34, 1.01);
opacity: 1;
}
.info .row .col-sm {
display: flex;
flex-direction: column;
}
.info .row .col-sm small {
display: flex;
}
.info .row .col-sm small strong:last-child(1) {
margin-left: auto !important;
}
.btn-control {
border: none !important;
padding: 0;
margin: 0 1rem 0 0;
}
.btn-control:hover{
background-color: transparent !important;
}
.btn-control:active,
.btn-control:focus {
background-color: transparent !important;
border: none !important;
box-shadow: none;
}
.btn-qrcode-peer {
padding: 0 !important;
}
.btn-qrcode-peer:active,
.btn-qrcode-peer:hover {
transform: scale(0.9) rotate(180deg);
border: 0 !important;
}
.btn-download-peer:active,
.btn-download-peer:hover {
color: #17a2b8 !important;
transform: translateY(5px);
}
.share_peer_btn_group .btn-control {
margin: 0 0 0 1rem;
padding: 0 !important;
transition: all 0.4s cubic-bezier(1, -0.43, 0, 1.37);
}
.btn-control:hover {
background: white;
}
.btn-delete-peer:hover {
color: #dc3545;
}
.btn-lock-peer:hover {
color: #28a745;
}
.btn-lock-peer.lock{
color: #6c757d
}
.btn-lock-peer.lock:hover{
color: #6c757d
}
.btn-control.btn-outline-primary:hover{
color: #007bff
}
/* .btn-setting-peer:hover {
color: #007bff
} */
.btn-download-peer:hover {
color: #17a2b8;
}
.login-container {
padding: 2rem;
}
@media (max-width: 992px) {
.card-col {
margin-bottom: 1rem;
}
}
.switch {
font-size: 2rem;
}
.switch:hover {
text-decoration: none
}
.btn-group-label:hover {
color: #007bff;
border-color: #007bff;
background: white;
}
.peer_data_group {
text-align: right;
display: flex;
margin-bottom: 0.5rem
}
.peer_data_group p {
text-transform: uppercase;
margin-bottom: 0;
margin-right: 1rem
}
@media (max-width: 768px) {
.peer_data_group {
text-align: left;
}
}
.index-switch {
display: flex;
align-items: center;
justify-content: flex-end;
}
main {
margin-bottom: 3rem;
}
.peer_list {
margin-bottom: 7rem
}
@media (max-width: 768px) {
.add_btn {
bottom: 1.5rem !important;
}
.peer_list {
margin-bottom: 7rem !important;
}
}
.btn-manage-group {
z-index: 99;
position: fixed;
bottom: 3rem;
right: 2rem;
display: flex;
}
.btn-manage-group .setting_btn_menu {
position: absolute;
top: -124px;
background-color: white;
padding: 1rem 0;
right: 0;
box-shadow: 0 10px 20px rgb(0 0 0 / 19%), 0 6px 6px rgb(0 0 0 / 23%);
border-radius: 10px;
min-width: 250px;
display: none;
transform: translateY(-30px);
opacity: 0;
transition: all 0.3s cubic-bezier(0.58, 0.03, 0.05, 1.28);
}
.btn-manage-group .setting_btn_menu.show {
display: block;
}
.setting_btn_menu.showing {
transform: translateY(0px);
opacity: 1;
}
.setting_btn_menu a {
display: flex;
padding: 0.5rem 1rem;
transition: all 0.1s ease-in-out;
font-size: 1rem;
align-items: center;
cursor: pointer;
}
.setting_btn_menu a:hover {
background-color: #efefef;
text-decoration: none;
}
.setting_btn_menu a i {
margin-right: auto !important;
}
.add_btn {
height: 54px;
z-index: 99;
border-radius: 100px !important;
padding: 0 14px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
margin-right: 1rem;
font-size: 1.5rem;
}
.setting_btn {
height: 54px;
z-index: 99;
border-radius: 100px !important;
padding: 0 14px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
font-size: 1.5rem;
}
@-webkit-keyframes rotating
/* Safari and Chrome */
{
from {
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes rotating {
from {
-ms-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
-o-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-ms-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.rotating::before {
-webkit-animation: rotating 0.75s linear infinite;
-moz-animation: rotating 0.75s linear infinite;
-ms-animation: rotating 0.75s linear infinite;
-o-animation: rotating 0.75s linear infinite;
animation: rotating 0.75s linear infinite;
}
.peer_private_key_textbox_switch {
position: absolute;
right: 2rem;
transform: translateY(-28px);
font-size: 1.2rem;
cursor: pointer;
}
#peer_private_key_textbox,
#private_key,
#public_key,
#peer_preshared_key_textbox {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.progress-bar {
transition: 0.3s ease-in-out;
}
.key {
transition: 0.2s ease-in-out;
cursor: pointer;
}
.key:hover {
color: #007bff;
}
.card {
border-radius: 10px;
}
.peer_list .card .button-group {
height: 22px;
}
.form-control {
border-radius: 10px;
}
.btn {
border-radius: 8px;
/*padding: 0.6rem 0.9em;*/
}
.login-box #username,
.login-box #password {
padding: 0.6rem calc( 0.9rem + 32px);
height: inherit;
}
.login-box label[for="username"],
.login-box label[for="password"] {
font-size: 1rem;
margin: 0 !important;
transform: translateY(2.1rem) translateX(1rem);
padding: 0;
}
/*label[for="password"]{*/
/* transform: translateY(32px) translateX(16px);*/
/*}*/
.modal-content {
border-radius: 10px;
}
.tooltip-inner {
font-size: 0.8rem;
}
@-webkit-keyframes loading {
0% {
background-color: #dfdfdf;
}
50% {
background-color: #adadad;
}
100% {
background-color: #dfdfdf;
}
}
@-moz-keyframes loading {
0% {
background-color: #dfdfdf;
}
50% {
background-color: #adadad;
}
100% {
background-color: #dfdfdf;
}
}
.conf_card {
transition: 0.2s ease-in-out;
}
.conf_card:hover {
border-color: #007bff;
cursor: pointer;
}
.info_loading {
/* animation: loading 2s infinite ease-in-out;
/* border-radius: 5px; */
height: 19.19px;
/* transition: 0.3s ease-in-out; */
/* transform: translateX(40px); */
opacity: 0 !important;
}
#conf_status_btn {
transition: 0.2s ease-in-out;
}
#conf_status_btn.info_loading {
height: 38px;
border-radius: 5px;
animation: loading 3s infinite ease-in-out;
}
#qrcode_img img {
width: 100%;
}
#selected_ip_list .badge,
#selected_peer_list .badge {
margin: 0.1rem
}
#add_modal.ip_modal_open {
transition: filter 0.2s ease-in-out;
filter: brightness(0.5);
}
#delete_bulk_modal .list-group a.active {
background-color: #dc3545;
border-color: #dc3545;
}
#selected_peer_list {
max-height: 80px;
overflow-y: scroll;
overflow-x: hidden;
}
.no-response {
width: 100%;
height: 100%;
position: fixed;
background: #000000ba;
z-index: 10000;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 1s ease-in-out;
}
.no-response.active {
display: flex;
}
.no-response.active.show {
opacity: 100;
}
.no-response .container>* {
text-align: center;
}
.no-responding {
transition: all 1s ease-in-out;
filter: blur(10px);
}
pre.index-alert {
margin-bottom: 0;
padding: 1rem;
background-color: #343a40;
border: 1px solid rgba(0, 0, 0, .125);
border-radius: .25rem;
margin-top: 1rem;
color: white;
}
.peerNameCol {
display: flex;
align-items: center;
margin-bottom: 0.2rem
}
.peerName {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.peerLightContainer {
text-transform: uppercase;
margin: 0;
margin-left: auto !important;
}
.conf_card .dot,
.info .dot {
transform: translateX(10px);
}
#config_body {
transition: 0.3s ease-in-out;
}
#config_body.firstLoading {
opacity: 0.2;
}
.chartTitle {
display: flex;
}
.chartControl {
margin-bottom: 1rem;
display: flex;
align-items: center;
}
.chartTitle h6 {
margin-bottom: 0;
line-height: 1;
margin-right: 0.5rem;
}
.chartContainer.fullScreen {
position: fixed;
z-index: 9999;
background-color: white;
top: 0;
left: 0;
width: calc( 100% + 15px);
height: 100%;
padding: 32px;
}
.chartContainer.fullScreen .col-sm {
padding-right: 0;
height: 100%;
}
.chartContainer.fullScreen .chartCanvasContainer {
width: 100%;
height: calc( 100% - 47px) !important;
max-height: calc( 100% - 47px) !important;
}
#switch{
transition: all 200ms ease-in;
}
.toggle--switch{
display: none;
}
.toggleLabel{
width: 64px;
height: 32px;
background-color: #6c757d17;
display: flex;
position: relative;
border: 2px solid #6c757d8c;
border-radius: 100px;
transition: all 200ms ease-in;
cursor: pointer;
margin: 0;
}
.toggle--switch.waiting + .toggleLabel{
opacity: 0.5;
}
.toggleLabel::before{
background-color: #6c757d;
height: 26px;
width: 26px;
content: "";
border-radius: 100px;
margin: 1px;
position: absolute;
animation-name: off;
animation-duration: 350ms;
animation-fill-mode: forwards;
transition: all 200ms ease-in;
cursor: pointer;
}
.toggleLabel:hover::before{
filter: brightness(1.2);
}
.toggle--switch:checked + .toggleLabel{
background-color: #007bff17 !important;
border: 2px solid #007bff8c;
}
.toggle--switch:checked + .toggleLabel::before{
background-color: #007bff;
animation-name: on;
animation-duration: 350ms;
animation-fill-mode: forwards;
}
@keyframes on {
0%{
left: 0px;
}
60%{
left: 0px;
width: 40px;
}
100%{
left: 32px;
width: 26px;
}
}
@keyframes off {
0%{
left: 32px;
}
60%{
left: 18px;
width: 40px;
}
100%{
left: 0px;
width: 26px;
}
}
.toastContainer{
z-index: 99999 !important;
}
.toast{
min-width: 300px;
background-color: rgba(255,255,255,1);
z-index: 99999;
}
.toast-header{
background-color: rgba(255,255,255);
}
.toast-progressbar{
width: 100%;
height: 4px;
background-color: #007bff;
border-bottom-left-radius: .25rem;
}
.addConfigurationAvailableIPs{
margin-bottom: 0;
}
.input-feedback{
display: none;
}
#addConfigurationModal label{
display: flex;
width: 100%;
align-items: center;
}
#addConfigurationModal label a{
margin-left: auto !important;
}
#reGeneratePrivateKey{
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
.addConfigurationToggleStatus.waiting{
opacity: 0.5;
}
/*.conf_card .card-body .row .card-col{*/
/* margin-bottom: 0.5rem;*/
/*}*/
.peerDataUsageChartContainer{
min-height: 50vh;
width: 100%;
}
.peerDataUsageChartControl{
display: block !important;
margin: 0;
}
.peerDataUsageChartControl .switchUnit{
width: 33.3%;
}
.peerDataUsageChartControl .switchTimePeriod{
width: 25%;
}
@media (min-width: 1200px){
#peerDataUsage .modal-xl {
max-width: 95vw;
}
}
.bottom{
display: none;
}
@media (max-width: 768px){
.bottom{
display: block;
}
.btn-manage-group{
bottom: calc( 3rem + 40px + env(safe-area-inset-bottom, 5px));
}
main{
padding-bottom: calc( 3rem + 40px + env(safe-area-inset-bottom, 5px));
}
}
.bottomNavContainer{
display: flex;
color: #333;
padding-bottom: env(safe-area-inset-bottom, 5px);
box-shadow: inset 0 1px 0 rgb(0 0 0 / 10%);
}
.bottomNavButton{
width: 25vw;
display: flex;
flex-direction: column;
align-items: center;
margin: 0.7rem 0;
color: rgba(51, 51, 51, 0.5);
cursor: pointer;
transition: all ease-in 0.2s;
}
.bottomNavButton.active{
color: #333;
}
.bottomNavButton i{
font-size: 1.2rem;
}
.bottomNavButton .subNav{
width: 100vw;
position: absolute;
z-index: 10000;
bottom: 0;
left: 0;
background-color: #272b30;
display: none;
animation-duration: 400ms;
padding-bottom: env(safe-area-inset-bottom, 5px);
}
.bottomNavButton .subNav.active{
display: block;
}
.bottomNavButton .subNav .nav .nav-item .nav-link{
padding: 0.7rem 1rem;
}
.bottomNavWrapper{
height: 100%;
width: 100%;
background-color: #000000a1;
position: fixed;
z-index: 1030;
display: none;
left: 0;
}
.bottomNavWrapper.active{
display: block;
}
.sb-update-url .dot-running{
transform: translateX(10px);
}
.list-group-item{
transition: all 0.1s ease-in;
}
.theme-switch-btn{
width: 100%;
}

BIN
src/static/app/dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

14
src/static/app/dist/index.html vendored Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/static/app/dist/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<script type="module" crossorigin src="/static/app/dist/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="/static/app/dist/assets/index.css">
</head>
<body>
<div id="app" class="w-100 vh-100"></div>
</body>
</html>

View File

@ -1,8 +1,10 @@
<script> <script>
import { ref } from 'vue' import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
import PeerSettingsDropdown from "@/components/configurationComponents/peerSettingsDropdown.vue";
export default { export default {
name: "peer", name: "peer",
components: {PeerSettingsDropdown},
props: { props: {
Peer: Object Peer: Object
}, },
@ -19,8 +21,13 @@ export default {
}); });
return {target, subMenuOpened} return {target, subMenuOpened}
}, },
mounted() { computed: {
getLatestHandshake(){
if (this.Peer.latest_handshake.includes(",")){
return this.Peer.latest_handshake.split(",")[0]
}
return this.Peer.latest_handshake;
}
} }
} }
</script> </script>
@ -40,7 +47,7 @@ export default {
</span> </span>
<span class="text-secondary" v-if="Peer.latest_handshake !== 'No Handshake'"> <span class="text-secondary" v-if="Peer.latest_handshake !== 'No Handshake'">
<i class="bi bi-arrows-angle-contract"></i> <i class="bi bi-arrows-angle-contract"></i>
{{Peer.latest_handshake}} ago {{getLatestHandshake}} ago
</span> </span>
</div> </div>
</div> </div>
@ -57,17 +64,20 @@ export default {
<small class="text-muted">Allowed IP</small> <small class="text-muted">Allowed IP</small>
<p class="mb-0"><samp>{{Peer.allowed_ip}}</samp></p> <p class="mb-0"><samp>{{Peer.allowed_ip}}</samp></p>
</div> </div>
<div class="ms-auto px-2"> <div class="ms-auto px-2 rounded-3 subMenuBtn"
<a role="button" class="text-body" @click="this.subMenuOpened = true"> :class="{active: this.subMenuOpened}"
>
<a role="button" class="text-body"
@click="this.subMenuOpened = true">
<h5 class="mb-0"><i class="bi bi-three-dots"></i></h5> <h5 class="mb-0"><i class="bi bi-three-dots"></i></h5>
</a> </a>
<Transition name="slide-fade"> <Transition name="slide-fade">
<ul class="dropdown-menu mt-2 shadow-lg dropdown-menu-left d-block" <PeerSettingsDropdown
:Peer="Peer"
v-if="this.subMenuOpened" v-if="this.subMenuOpened"
ref="target"> ref="target"
<li> ></PeerSettingsDropdown>
<a class="dropdown-item d-flex" role="button" ></a></li>
</ul>
</Transition> </Transition>
</div> </div>
</div> </div>
@ -86,4 +96,8 @@ export default {
transform: translateY(20px); transform: translateY(20px);
opacity: 0; opacity: 0;
} }
.subMenuBtn.active{
background-color: #ffffff20;
}
</style> </style>

View File

@ -0,0 +1,489 @@
<script>
import PeerSearch from "@/components/configurationComponents/peerSearch.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {fetchGet} from "@/utilities/fetch.js";
import Peer from "@/components/configurationComponents/peer.vue";
import { Line, Bar } from 'vue-chartjs'
import Fuse from "fuse.js";
import {
Chart,
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
BubbleController,
DoughnutController,
LineController,
PieController,
PolarAreaController,
RadarController,
ScatterController,
CategoryScale,
LinearScale,
LogarithmicScale,
RadialLinearScale,
TimeScale,
TimeSeriesScale,
Decimation,
Filler,
Legend,
Title,
Tooltip
} from 'chart.js';
import dayjs from "dayjs";
Chart.register(
ArcElement,
LineElement,
BarElement,
PointElement,
BarController,
BubbleController,
DoughnutController,
LineController,
PieController,
PolarAreaController,
RadarController,
ScatterController,
CategoryScale,
LinearScale,
LogarithmicScale,
RadialLinearScale,
TimeScale,
TimeSeriesScale,
Decimation,
Filler,
Legend,
Title,
Tooltip
);
export default {
name: "peerList",
components: {PeerSearch, Peer, Line, Bar},
setup(){
const dashboardConfigurationStore = DashboardConfigurationStore();
const wireguardConfigurationStore = WireguardConfigurationsStore();
return {dashboardConfigurationStore, wireguardConfigurationStore}
},
data(){
return {
loading: false,
error: null,
configurationInfo: [],
configurationPeers: [],
historyDataSentDifference: [],
historyDataReceivedDifference: [],
historySentData: {
labels: [],
datasets: [
{
label: 'Data Sent',
data: [],
fill: false,
borderColor: '#198754',
tension: 0
},
],
},
historyReceiveData: {
labels: [],
datasets: [
{
label: 'Data Received',
data: [],
fill: false,
borderColor: '#0d6efd',
tension: 0
},
],
},
}
},
watch: {
'$route.params.id': {
immediate: true,
handler(){
clearInterval(this.interval)
this.loading = true;
let id = this.$route.params.id;
this.configurationInfo = [];
this.configurationPeers = [];
if (id){
this.getPeers(id)
this.setInterval();
}
}
},
'dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval'(){
clearInterval(this.interval);
this.setInterval();
}
},
beforeRouteLeave(){
clearInterval(this.interval)
},
methods:{
getPeers(id){
fetchGet("/api/getWireguardConfigurationInfo",
{
configurationName: id
}, (res) => {
this.configurationInfo = res.data.configurationInfo;
this.configurationPeers = res.data.configurationPeers;
this.loading = false;
if (this.configurationPeers.length > 0){
const sent = this.configurationPeers.map(x => x.total_sent + x.cumu_sent).reduce((x,y) => x + y).toFixed(4);
const receive = this.configurationPeers.map(x => x.total_receive + x.cumu_receive).reduce((x,y) => x + y).toFixed(4);
if (
this.historyDataSentDifference[this.historyDataSentDifference.length - 1] !== sent
){
if (this.historyDataSentDifference.length > 0){
this.historySentData = {
labels: [...this.historySentData.labels, dayjs().format("HH:mm:ss A")],
datasets: [
{
label: 'Data Sent',
data: [...this.historySentData.datasets[0].data,
((sent - this.historyDataSentDifference[this.historyDataSentDifference.length - 1])*1000).toFixed(4)],
fill: false,
borderColor: ' #198754',
tension: 0
}
],
}
}
this.historyDataSentDifference.push(sent)
}
if (
this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1] !== receive
){
if (this.historyDataReceivedDifference.length > 0){
this.historyReceiveData = {
labels: [...this.historyReceiveData.labels, dayjs().format("HH:mm:ss A")],
datasets: [
{
label: 'Data Received',
data: [...this.historyReceiveData.datasets[0].data,
((receive - this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1])*1000).toFixed(4)],
fill: false,
borderColor: '#0d6efd',
tension: 0
}
],
}
}
this.historyDataReceivedDifference.push(receive)
}
}
});
},
setInterval(){
this.interval = setInterval(() => {
this.getPeers(this.$route.params.id)
}, parseInt(this.dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval))
}
},
computed: {
configurationSummary(){
return {
connectedPeers: this.configurationPeers.filter(x => x.status === "running").length,
totalUsage: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_data + x.cumu_data).reduce((a, b) => a + b) : 0,
totalReceive: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_receive + x.cumu_receive).reduce((a, b) => a + b) : 0,
totalSent: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_sent + x.cumu_sent).reduce((a, b) => a + b) : 0
}
},
receiveData(){
return this.historyReceiveData
},
sentData(){
return this.historySentData
},
individualDataUsage(){
return {
labels: this.configurationPeers.map(x => {
if (x.name) return x.name
return `Untitled Peer - ${x.id}`
}),
datasets: [{
label: 'Total Data Usage',
data: this.configurationPeers.map(x => x.cumu_data + x.total_data),
backgroundColor: this.configurationPeers.map(x => `#0dcaf0`),
tooltip: {
callbacks: {
label: (tooltipItem) => {
return `${tooltipItem.formattedValue} GB`
}
}
}
}]
}
},
individualDataUsageChartOption(){
return {
responsive: true,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
ticks: {
display: false,
},
grid: {
display: false
},
},
y:{
ticks: {
callback: (val, index) => {
return `${val} GB`
}
},
grid: {
display: false
},
}
}
}
},
chartOptions() {
return {
responsive: true,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (tooltipItem) => {
return `${tooltipItem.formattedValue} MB/s`
}
}
}
},
scales: {
x: {
ticks: {
display: false,
},
grid: {
display: false
},
},
y:{
ticks: {
callback: (val, index) => {
return `${val} MB/s`
}
},
grid: {
display: false
},
}
}
}
},
searchPeers(){
const fuse = new Fuse(this.configurationPeers, {
keys: ["name", "id", "allowed_ip"]
});
const result = this.wireguardConfigurationStore.searchString ? fuse.search(this.wireguardConfigurationStore.searchString).map(x => x.item) : this.configurationPeers;
return result.slice().sort((a, b) => {
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
< b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort] ){
return -1;
}
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
> b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]){
return 1;
}
return 0;
});
}
}
}
</script>
<template>
<div v-if="!this.loading">
<div>
<small CLASS="text-muted">CONFIGURATION</small>
<div class="d-flex align-items-center gap-3">
<h1 class="mb-0"><samp>{{this.configurationInfo.Name}}</samp></h1>
<div class="dot active ms-0"></div>
</div>
</div>
<div class="row mt-3 gy-2 gx-2 mb-2">
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body py-2">
<p class="mb-0 text-muted"><small>Address</small></p>
{{this.configurationInfo.Address}}
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body py-2">
<p class="mb-0 text-muted"><small>Listen Port</small></p>
{{this.configurationInfo.ListenPort}}
</div>
</div>
</div>
<div style="word-break: break-all" class="col-12 col-lg-6">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body py-2">
<p class="mb-0 text-muted"><small>Public Key</small></p>
<samp>{{this.configurationInfo.PublicKey}}</samp>
</div>
</div>
</div>
</div>
<div class="row gx-2 gy-2 mb-2">
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>Connected Peers</small></p>
<strong class="h4">{{configurationSummary.connectedPeers}}</strong>
</div>
<i class="bi bi-ethernet ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>Total Usage</small></p>
<strong class="h4">{{configurationSummary.totalUsage.toFixed(4)}} GB</strong>
</div>
<i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>Total Received</small></p>
<strong class="h4 text-primary">{{configurationSummary.totalReceive.toFixed(4)}} GB</strong>
</div>
<i class="bi bi-arrow-down ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>Total Sent</small></p>
<strong class="h4 text-success">{{configurationSummary.totalSent.toFixed(4)}} GB</strong>
</div>
<i class="bi bi-arrow-up ms-auto h2 text-muted"></i>
</div>
</div>
</div>
</div>
<div class="row gx-2 gy-2 mb-5">
<div class="col-12 col-lg-6">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-header bg-transparent border-0"><small class="text-muted">Peers Total Data Usage</small></div>
<div class="card-body pt-1">
<Bar
:data="individualDataUsage"
:options="individualDataUsageChartOption"
style="height: 200px; width: 100%"></Bar>
</div>
</div>
</div>
<div class="col-sm col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-header bg-transparent border-0"><small class="text-muted">Real Time Received Data Usage</small></div>
<div class="card-body pt-1">
<Line
:options="chartOptions"
:data="receiveData"
style="width: 100%; height: 200px"
></Line>
</div>
</div>
</div>
<div class="col-sm col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-header bg-transparent border-0"><small class="text-muted">Real Time Sent Data Usage</small></div>
<div class="card-body pt-1">
<Line
:options="chartOptions"
:data="sentData"
style="width: 100%; height: 200px"
></Line>
</div>
</div>
</div>
</div>
<div class="mb-4">
<RouterView v-slot="{Component}">
<Transition name="fade3" mode="out-in">
<Component :is="Component"></Component>
</Transition>
</RouterView>
<div class="d-flex align-items-center gap-3 mb-2 ">
<h3>Peers</h3>
<RouterLink
to="./peer_settings"
class="ms-auto text-secondary text-decoration-none"><i class="bi bi-sliders2 me-2"></i>Peer Settings</RouterLink>
<a href="#" class="text-decoration-none"><i class="bi bi-plus-circle-fill me-2"></i>Add Peer</a>
</div>
<PeerSearch></PeerSearch>
<TransitionGroup name="list" tag="div" class="row gx-2 gy-2 z-0">
<div class="col-12 col-lg-6 col-xl-4"
:key="peer.id"
v-for="peer in this.searchPeers">
<Peer :Peer="peer"></Peer>
</div>
</TransitionGroup>
</div>
</div>
</template>
<style scoped>
.peerNav .nav-link{
&.active{
//background: linear-gradient(var(--degree), var(--brandColor1) var(--distance2), var(--brandColor2) 100%);
//color: white;
background-color: #efefef;
}
}
.list-move, /* apply transition to moving elements */
.list-enter-active,
.list-leave-active {
transition: all 0.4s cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
.list-leave-active{
position: absolute;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(30px);
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.list-leave-active {
position: absolute;
}
</style>

View File

@ -0,0 +1,32 @@
<script>
export default {
name: "peerSettings"
}
</script>
<template>
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0">
<div class="container d-flex h-100 w-100">
<div class="card m-auto rounded-3 w-100">
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4">
<h4 class="mb-0">Peer Settings</h4>
<router-link to="./" class="ms-auto btn">
<i class="bi bi-x-lg"></i>
</router-link>
</div>
<div class="card-body p-4">
</div>
</div>
</div>
</div>
</template>
<style scoped>
.peerSettingContainer{
background-color: #00000060;
z-index: 1000;
}
</style>

View File

@ -0,0 +1,63 @@
<script>
export default {
name: "peerSettingsDropdown",
props: {
Peer: Object
}
}
</script>
<template>
<ul class="dropdown-menu mt-2 shadow-lg d-block rounded-3" style="max-width: 200px">
<template v-if="!this.Peer.private_key">
<li>
<small class="w-100 dropdown-item text-muted"
style="white-space: break-spaces; font-size: 0.7rem"
>Download & QR Code is not available due to no <code>private key</code>
set for this peer
</small>
</li>
<li><hr class="dropdown-divider"></li>
</template>
<li>
<a class="dropdown-item d-flex" role="button">
<i class="me-auto bi bi-pen"></i> Edit
</a>
</li>
<template v-if="this.Peer.private_key">
<li>
<a class="dropdown-item d-flex" role="button">
<i class="me-auto bi bi-download"></i> Download
</a>
</li>
<li>
<a class="dropdown-item d-flex" role="button">
<i class="me-auto bi bi-qr-code"></i> QR Code
</a>
</li>
</template>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item d-flex text-warning"
role="button">
<i class="me-auto bi bi-lock"></i> Lock
</a>
</li>
<li>
<a class="dropdown-item d-flex fw-bold text-danger"
role="button">
<i class="me-auto bi bi-trash"></i> Delete
</a>
</li>
</ul>
</template>
<style scoped>
.dropdown-menu{
right: 1rem;
}
</style>

View File

@ -11,6 +11,8 @@ import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.
import Setup from "@/views/setup.vue"; import Setup from "@/views/setup.vue";
import NewConfiguration from "@/views/newConfiguration.vue"; import NewConfiguration from "@/views/newConfiguration.vue";
import Configuration from "@/views/configuration.vue"; import Configuration from "@/views/configuration.vue";
import PeerSettings from "@/components/configurationComponents/peerSettings.vue";
import PeerList from "@/components/configurationComponents/peerList.vue";
const checkAuth = async () => { const checkAuth = async () => {
let result = false let result = false
@ -48,11 +50,20 @@ const router = createRouter({
}, },
{ {
name: "Configuration", name: "Configuration",
path: '/configuration/:id', path: '/configuration/:id/',
component: Configuration component: Configuration,
children: [
{
name: "Peer Settings",
path: 'peer_settings',
component: PeerSettings
} }
] ]
}, },
]
},
{ {
path: '/signin', component: Signin path: '/signin', component: Signin
}, },

View File

@ -61,429 +61,20 @@ import PeerSearch from "@/components/configurationComponents/peerSearch.vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js"; import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {ref} from "vue"; import {ref} from "vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"; import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import PeerList from "@/components/configurationComponents/peerList.vue";
export default { export default {
name: "configuration", name: "configuration",
setup(){ components: {PeerList},
const dashboardConfigurationStore = DashboardConfigurationStore();
const wireguardConfigurationStore = WireguardConfigurationsStore();
return {dashboardConfigurationStore, wireguardConfigurationStore}
},
components: {PeerSearch, Peer, Line, Bar},
data(){
return {
loading: false,
error: null,
configurationInfo: [],
configurationPeers: [],
historyDataSentDifference: [],
historyDataReceivedDifference: [],
historySentData: {
labels: [],
datasets: [
{
label: 'Data Sent',
data: [],
fill: false,
borderColor: '#198754',
tension: 0
},
],
},
historyReceiveData: {
labels: [],
datasets: [
{
label: 'Data Received',
data: [],
fill: false,
borderColor: '#0d6efd',
tension: 0
},
],
},
}
},
watch: {
'$route.params': {
immediate: true,
handler(){
clearInterval(this.interval)
this.loading = true;
let id = this.$route.params.id;
this.configurationInfo = [];
this.configurationPeers = [];
if (id){
this.getPeers(id)
this.setInterval();
}
}
},
'dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval'(){
clearInterval(this.interval);
this.setInterval();
}
},
beforeRouteLeave(){
clearInterval(this.interval)
},
methods:{
getPeers(id){
fetchGet("/api/getWireguardConfigurationInfo",
{
configurationName: id
}, (res) => {
this.configurationInfo = res.data.configurationInfo;
this.configurationPeers = res.data.configurationPeers;
this.loading = false;
if (this.configurationPeers.length > 0){
const sent = this.configurationPeers.map(x => x.total_sent + x.cumu_sent).reduce((x,y) => x + y).toFixed(4);
const receive = this.configurationPeers.map(x => x.total_receive + x.cumu_receive).reduce((x,y) => x + y).toFixed(4);
if (
this.historyDataSentDifference[this.historyDataSentDifference.length - 1] !== sent
){
if (this.historyDataSentDifference.length > 0){
this.historySentData = {
labels: [...this.historySentData.labels, dayjs().format("HH:mm:ss A")],
datasets: [
{
label: 'Data Sent',
data: [...this.historySentData.datasets[0].data,
((sent - this.historyDataSentDifference[this.historyDataSentDifference.length - 1])*1000).toFixed(4)],
fill: false,
borderColor: ' #198754',
tension: 0
}
],
}
}
this.historyDataSentDifference.push(sent)
}
if (
this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1] !== receive
){
if (this.historyDataReceivedDifference.length > 0){
this.historyReceiveData = {
labels: [...this.historyReceiveData.labels, dayjs().format("HH:mm:ss A")],
datasets: [
{
label: 'Data Received',
data: [...this.historyReceiveData.datasets[0].data,
((receive - this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1])*1000).toFixed(4)],
fill: false,
borderColor: '#0d6efd',
tension: 0
}
],
}
}
this.historyDataReceivedDifference.push(receive)
}
}
});
},
setInterval(){
console.log('Set Interval');
this.interval = setInterval(() => {
this.getPeers(this.$route.params.id)
}, parseInt(this.dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval))
}
},
computed: {
configurationSummary(){
return {
connectedPeers: this.configurationPeers.filter(x => x.status === "running").length,
totalUsage: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_data + x.cumu_data).reduce((a, b) => a + b) : 0,
totalReceive: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_receive + x.cumu_receive).reduce((a, b) => a + b) : 0,
totalSent: this.configurationPeers.length > 0 ? this.configurationPeers.map(x => x.total_sent + x.cumu_sent).reduce((a, b) => a + b) : 0
}
},
receiveData(){
return this.historyReceiveData
},
sentData(){
return this.historySentData
},
individualDataUsage(){
return {
labels: this.configurationPeers.map(x => {
if (x.name) return x.name
return `Untitled Peer - ${x.id}`
}),
datasets: [{
label: 'Total Data Usage',
data: this.configurationPeers.map(x => x.cumu_data + x.total_data),
backgroundColor: this.configurationPeers.map(x => `#0dcaf0`),
tooltip: {
callbacks: {
label: (tooltipItem) => {
console.log(tooltipItem)
return `${tooltipItem.formattedValue} GB`
}
}
}
}]
}
},
individualDataUsageChartOption(){
return {
responsive: true,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
ticks: {
display: false,
},
grid: {
display: false
},
},
y:{
ticks: {
callback: (val, index) => {
return `${val} GB`
}
},
grid: {
display: false
},
}
}
}
},
chartOptions() {
return {
responsive: true,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (tooltipItem) => {
console.log(tooltipItem)
return `${tooltipItem.formattedValue} MB/s`
}
}
}
},
scales: {
x: {
ticks: {
display: false,
},
grid: {
display: false
},
},
y:{
ticks: {
callback: (val, index) => {
return `${val} MB/s`
}
},
grid: {
display: false
},
}
}
}
},
searchPeers(){
const fuse = new Fuse(this.configurationPeers, {
keys: ["name", "id", "allowed_ip"]
});
const result = this.wireguardConfigurationStore.searchString ? fuse.search(this.wireguardConfigurationStore.searchString).map(x => x.item) : this.configurationPeers;
return result.slice().sort((a, b) => {
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
< b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort] ){
return -1;
}
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
> b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]){
return 1;
}
return 0;
});
}
}
} }
</script> </script>
<template> <template>
<div class="mt-5 text-body" v-if="!loading"> <div class="mt-5 text-body">
<div> <PeerList></PeerList>
<small CLASS="text-muted">CONFIGURATION</small>
<div class="d-flex align-items-center gap-3">
<h1 class="mb-0"><samp>{{this.configurationInfo.Name}}</samp></h1>
<div class="dot active ms-0"></div>
</div>
</div>
<div class="row mt-3 gy-2 gx-2 mb-2">
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body py-2">
<p class="mb-0 text-muted"><small>Address</small></p>
{{this.configurationInfo.Address}}
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body py-2">
<p class="mb-0 text-muted"><small>Listen Port</small></p>
{{this.configurationInfo.ListenPort}}
</div>
</div>
</div>
<div style="word-break: break-all" class="col-12 col-lg-6">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body py-2">
<p class="mb-0 text-muted"><small>Public Key</small></p>
<samp>{{this.configurationInfo.PublicKey}}</samp>
</div>
</div>
</div>
</div>
<div class="row gx-2 gy-2 mb-2">
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>Connected Peers</small></p>
<strong class="h4">{{configurationSummary.connectedPeers}}</strong>
</div>
<i class="bi bi-ethernet ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>Total Usage</small></p>
<strong class="h4">{{configurationSummary.totalUsage.toFixed(4)}} GB</strong>
</div>
<i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>Total Received</small></p>
<strong class="h4 text-primary">{{configurationSummary.totalReceive.toFixed(4)}} GB</strong>
</div>
<i class="bi bi-arrow-down ms-auto h2 text-muted"></i>
</div>
</div>
</div>
<div class="col-6 col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-body d-flex">
<div>
<p class="mb-0 text-muted"><small>Total Sent</small></p>
<strong class="h4 text-success">{{configurationSummary.totalSent.toFixed(4)}} GB</strong>
</div>
<i class="bi bi-arrow-up ms-auto h2 text-muted"></i>
</div>
</div>
</div>
</div>
<div class="row gx-2 gy-2 mb-5">
<div class="col-12 col-lg-6">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-header bg-transparent border-0"><small class="text-muted">Peers Total Data Usage</small></div>
<div class="card-body pt-1">
<Bar
:data="individualDataUsage"
:options="individualDataUsageChartOption"
style="height: 200px; width: 100%"></Bar>
</div>
</div>
</div>
<div class="col-sm col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-header bg-transparent border-0"><small class="text-muted">Real Time Received Data Usage</small></div>
<div class="card-body pt-1">
<Line
:options="chartOptions"
:data="receiveData"
style="width: 100%; height: 200px"
></Line>
</div>
</div>
</div>
<div class="col-sm col-lg-3">
<div class="card rounded-3 bg-transparent shadow-sm">
<div class="card-header bg-transparent border-0"><small class="text-muted">Real Time Sent Data Usage</small></div>
<div class="card-body pt-1">
<Line
:options="chartOptions"
:data="sentData"
style="width: 100%; height: 200px"
></Line>
</div>
</div>
</div>
</div>
<div>
<div class="d-flex align-items-center gap-3 mb-2 ">
<h3>Peers</h3>
<a href="#"
class="ms-auto text-secondary text-decoration-none"><i class="bi bi-sliders2 me-2"></i>Peer Settings</a>
<a href="#" class="text-decoration-none"><i class="bi bi-plus-circle-fill me-2"></i>Add Peer</a>
</div>
<PeerSearch></PeerSearch>
<TransitionGroup name="list" tag="div" class="row gx-2 gy-2 z-0">
<div class="col-12 col-lg-6 col-xl-4"
:key="peer.id"
v-for="peer in this.searchPeers">
<Peer :Peer="peer"></Peer>
</div>
</TransitionGroup>
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.peerNav .nav-link{
&.active{
//background: linear-gradient(var(--degree), var(--brandColor1) var(--distance2), var(--brandColor2) 100%);
//color: white;
background-color: #efefef;
}
}
.list-move, /* apply transition to moving elements */
.list-enter-active,
.list-leave-active {
transition: all 0.4s cubic-bezier(0.82, 0.58, 0.17, 0.9);
}
.list-leave-active{
position: absolute;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(30px);
}
/* ensure leaving items are taken out of layout flow so that moving
animations can be calculated correctly. */
.list-leave-active {
position: absolute;
}
</style> </style>

View File

@ -1078,3 +1078,18 @@ pre.index-alert {
.message-leave-active { .message-leave-active {
position: absolute; position: absolute;
} }
.fade3-enter-active,
.fade3-leave-active {
transition: all 0.15s ease-in-out;
}
.fade3-enter-from{
transform: scale(1);
opacity: 0;
}
.fade3-leave-to {
transform: scale(0.8);
opacity: 0;
}