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
-1
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.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+938
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
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+14
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>
@@ -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
v-if="this.subMenuOpened" :Peer="Peer"
ref="target"> v-if="this.subMenuOpened"
<li> ref="target"
<a class="dropdown-item d-flex" role="button" ></a></li> ></PeerSettingsDropdown>
</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>
@@ -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>
@@ -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>
@@ -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>
+14 -3
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,9 +50,18 @@ 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
}
]
},
] ]
}, },
{ {
+4 -413
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>
+15
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;
}