Browse Source

Update

Split main layout into separate components
Set up linting & formatting
pull/937/head
Sergei Birukov 2 years ago
parent
commit
d6a90c8f10
  1. 1
      .prettierignore
  2. 6
      .prettierrc.json
  3. 2551
      package-lock.json
  4. 3
      package.json
  5. 2
      webui/.editorconfig
  6. 21
      webui/.eslintrc.cjs
  7. 1
      webui/.gitignore
  8. 7
      webui/.prettierrc.cjs
  9. 1542
      webui/package-lock.json
  10. 6
      webui/package.json
  11. 966
      webui/src/App.vue
  12. 46
      webui/src/components/Auth.vue
  13. 34
      webui/src/components/Client.vue
  14. 80
      webui/src/components/ClientAddress.vue
  15. 34
      webui/src/components/ClientCharts.vue
  16. 73
      webui/src/components/ClientControls.vue
  17. 67
      webui/src/components/ClientInfo.vue
  18. 130
      webui/src/components/ClientName.vue
  19. 12
      webui/src/components/Footer.vue
  20. 6
      webui/src/main.js
  21. 8
      webui/src/services/api.js
  22. 69
      webui/src/utils/chartOptions.js

1
.prettierignore

@ -1 +0,0 @@
**

6
.prettierrc.json

@ -1,6 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}

2551
package-lock.json

File diff suppressed because it is too large

3
package.json

@ -4,5 +4,8 @@
"build": "DOCKER_BUILDKIT=1 docker build --tag wg-easy .",
"serve": "docker compose -f docker-compose.yml -f docker-compose.dev.yml up",
"start": "docker run --env WG_HOST=0.0.0.0 --name wg-easy --cap-add=NET_ADMIN --cap-add=SYS_MODULE --sysctl=\"net.ipv4.conf.all.src_valid_mark=1\" --mount type=bind,source=\"$(pwd)\"/config,target=/etc/wireguard -p 51820:51820/udp -p 51821:51821/tcp wg-easy"
},
"devDependencies": {
"eslint-plugin-import": "^2.29.1"
}
}

2
webui/.editorconfig

@ -0,0 +1,2 @@
[*]
end_of_line= lf

21
webui/.eslintrc.cjs

@ -0,0 +1,21 @@
module.exports = {
env: {
browser: true,
node: true,
es2021: true,
},
extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'prettier'],
parser: 'vue-eslint-parser',
// parserOptions: {
// parser: '@typescript-eslint/parser',
// // sourceType: 'module',
// },
plugins: ['vue', 'prettier'],
rules: {
'vue/no-v-html': 'off',
'vue/multi-word-component-names': 'off',
'prettier/prettier': ['error'],
'vue/no-undef-components': ['error'],
'vue/no-undef-properties': ['error'],
},
};

1
webui/.gitignore

@ -19,6 +19,7 @@ coverage
# Editor directories and files
.vscode/*
*.code-workspace
!.vscode/extensions.json
.idea
*.suo

7
webui/.prettierrc.cjs

@ -0,0 +1,7 @@
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: true,
singleQuote: true,
printWidth: 120,
};

1542
webui/package-lock.json

File diff suppressed because it is too large

6
webui/package.json

@ -12,12 +12,18 @@
"dependencies": {
"apexcharts": "^3.45.1",
"crypto-js": "^4.2.0",
"pinia": "^2.1.7",
"vue": "^3.3.11",
"vue3-apexcharts": "^1.4.4"
},
"devDependencies": {
"@typescript-eslint/parser": "^7.3.1",
"@vitejs/plugin-vue": "^4.5.2",
"autoprefixer": "^10.4.16",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.23.0",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"vite": "^5.0.10"

966
webui/src/App.vue

File diff suppressed because it is too large

46
webui/src/components/Auth.vue

@ -0,0 +1,46 @@
<template>
<div>
<h1 class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center">WireGuard</h1>
<form class="shadow rounded-md bg-white dark:bg-neutral-700 mx-auto w-64 p-5 overflow-hidden mt-10" @submit="login">
<!-- Avatar -->
<div class="h-20 w-20 mb-10 mt-5 mx-auto rounded-full bg-red-800 dark:bg-red-800 relative overflow-hidden">
<IconAvatarDefault class="w-10 h-10 m-5 text-white dark:text-white" />
</div>
<input
v-model="password"
type="password"
name="password"
placeholder="Password"
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none"
/>
<button
v-if="authenticating"
class="bg-red-800 dark:bg-red-800 w-full rounded shadow py-2 text-sm text-white dark:text-white cursor-not-allowed"
>
<LoadingSpinner />
</button>
<input
v-if="!authenticating && password"
type="submit"
class="bg-red-800 dark:bg-red-800 w-full rounded shadow py-2 text-sm text-white dark:text-white hover:bg-red-700 dark:hover:bg-red-700 transition cursor-pointer"
value="Sign In"
/>
<input
v-if="!authenticating && !password"
type="submit"
class="bg-gray-200 dark:bg-neutral-800 w-full rounded shadow py-2 text-sm text-white dark:text-white cursor-not-allowed"
value="Sign In"
/>
</form>
</div>
</template>
<script setup>
import IconAvatarDefault from '@/components/icons/IconAvatarDefault.vue';
import LoadingSpinner from './LoadingSpinner.vue';
</script>

34
webui/src/components/Client.vue

@ -0,0 +1,34 @@
<template>
<div
v-if="client"
class="relative overflow-hidden border-b last:border-b-0 border-gray-100 dark:border-neutral-600 border-solid"
>
<!-- Chart -->
<ClientCharts :client="client" />
<div class="relative p-5 z-10 flex flex-col md:flex-row justify-between">
<div class="flex items-center pb-2 md:pb-0">
<ClientAvatar :client="client" />
<ClientInfo :client="client" />
</div>
<div class="flex items-center justify-end">
<ClientControls :client="client" />
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import ClientCharts from '@/components/ClientCharts.vue';
import ClientInfo from '@/components/ClientInfo.vue';
import ClientAvatar from '@/components/ClientAvatar.vue';
import ClientControls from '@/components/ClientControls.vue';
defineProps({
client: {},
});
</script>

80
webui/src/components/ClientAddress.vue

@ -2,61 +2,51 @@
<template>
<span class="group block md:inline-block pb-1 md:pb-0">
<!-- Show -->
<input
v-show="editAddressId === client.id"
v-model="editAddress"
@keyup.enter="updateAddress"
@keyup.escape="cancelEdit"
:test="'client-' + client.id + '-address'"
:ref="'client-' + client.id + '-address'"
class="rounded border-2 dark:bg-neutral-700 border-gray-100 dark:border-neutral-600 focus:border-gray-200 dark:focus:border-neutral-500 outline-none w-20 text-black dark:text-neutral-300 dark:placeholder:text-neutral-500"
/>
<span
v-show="editAddressId !== client.id"
class="inline-block border-t-2 border-b-2 border-transparent"
>{{ client.address }}</span
>
<input v-show="editAddressId === client.id" v-model="editAddress" @keyup.enter="updateAddress" @keyup.escape="cancelEdit" :test="'client-' + client.id + '-address'" :ref="'client-' + client.id + '-address'" class="rounded border-2 dark:bg-neutral-700 border-gray-100 dark:border-neutral-600 focus:border-gray-200 dark:focus:border-neutral-500 outline-none w-20 text-black dark:text-neutral-300 dark:placeholder:text-neutral-500" />
<span v-show="editAddressId !== client.id" class="inline-block border-t-2 border-b-2 border-transparent">{{ client.address }}</span>
<!-- Edit -->
<span
v-show="editAddressId !== client.id"
@click="edit"
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity"
>
<span v-show="editAddressId !== client.id" @click="edit" class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity">
<IconEdit />
</span>
</span>
</template>
<script>
<script setup>
import { ref, nextTick } from 'vue';
import IconEdit from './icons/IconEdit.vue';
export default {
props: ['client'],
data() {
return {
editAddress: null,
editAddressId: null,
const clientEditAddress = ref(null);
const clientEditAddressId = ref(null);
const props = defineProps(['client']);
const editAddress = ref(null);
const editAddressId = ref(null);
const emit = defineEmits(['inFocus', 'submit', 'update-address']);
const updateAddress = () => {
console.log('emit')
emit('update-address', props.client, editAddress.value);
editAddress.value = null;
editAddressId.value = null;
};
},
methods: {
updateAddress() {
this.$emit('update-address', this.client, this.editAddress);
this.editAddress = null;
this.editAddressId = null;
},
cancelEdit() {
this.editAddress = null;
this.editAddressId = null;
},
edit() {
this.editAddress = this.client.address;
this.editAddressId = this.client.id;
this.$nextTick(() => {
this.$refs['client-' + this.client.id + '-address'].select();
const cancelEdit = () => {
editAddress.value = null;
editAddressId.value = null;
};
const edit = () => {
editAddress.value = props.client.address;
editAddressId.value = props.client.id;
nextTick(() => {
const clientAddressRef = ['client-' + props.client.id + '-address'];
if (clientAddressRef.value) {
clientAddressRef.value.select();
}
});
},
},
components: { IconEdit },
};
</script>

34
webui/src/components/ClientCharts.vue

@ -0,0 +1,34 @@
<template>
<div v-if="client">
<!-- TODO: Individual bars are too wide -->
<div class="absolute z-0 bottom-0 left-0 right-0" style="top: 60%">
<VueApexCharts
width="100%"
height="100%"
type="bar"
:options="chartOptions"
:series="[{ name: 'Upload (TX)', data: client?.transferTxHistory, },]"
/>
</div>
<div class="absolute z-0 top-0 left-0 right-0" style="bottom: 60%">
<VueApexCharts
width="100%"
height="100%"
type="bar"
:options="chartOptions"
:series="[{ name: 'Download (RX)', data: client?.transferRxHistory, },]"
style="transform: scaleY(-1)"
/>
</div>
</div>
</template>
<script setup>
import VueApexCharts from 'vue3-apexcharts';
import chartOptions from '@/utils/chartOptions';
defineProps({
client: {},
});
</script>

73
webui/src/components/ClientControls.vue

@ -0,0 +1,73 @@
<template>
<div class="text-gray-400 dark:text-neutral-400 flex gap-1 items-center justify-between">
<!-- Enable/Disable -->
<div
@click="disableClient(client)"
v-if="client.enabled === true"
title="Disable Client"
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-red-800 cursor-pointer hover:bg-red-700 transition-all"
>
<div class="rounded-full w-4 h-4 m-1 ml-5 bg-white"></div>
</div>
<div
@click="enableClient(client)"
v-if="client.enabled === false"
title="Enable Client"
class="inline-block align-middle rounded-full w-10 h-6 mr-1 bg-gray-200 dark:bg-neutral-400 cursor-pointer hover:bg-gray-300 dark:hover:bg-neutral-500 transition-all"
>
<div class="rounded-full w-4 h-4 m-1 bg-white"></div>
</div>
<!-- Show QR-->
<button
class="align-middle bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition"
title="Show QR Code"
@click="qrcode = `./api/wireguard/client/${client.id}/qrcode.svg`"
>
<IconQRCode />
</button>
<!-- Download Config -->
<a
:href="'./api/wireguard/client/' + client.id + '/configuration'"
download
class="align-middle inline-block bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition"
title="Download Configuration"
>
<IconDownload />
</a>
<!-- Delete -->
<button
class="align-middle bg-gray-100 dark:bg-neutral-600 dark:text-neutral-300 hover:bg-red-800 dark:hover:bg-red-800 hover:text-white dark:hover:text-white p-2 rounded transition"
title="Delete Client"
@click="clientDelete = client"
>
<IconDelete />
</button>
</div>
</template>
<script setup>
import IconQRCode from '@/components/icons/IconQRCode.vue';
import IconDownload from '@/components/icons/IconDownload.vue';
import IconDelete from '@/components/icons/IconDelete.vue';
import API from '@/services/api';
const api = new API();
defineProps({
client: {},
});
function enableClient(client) {
api.enableClient({ clientId: client.id }).catch((err) => alert(err.message || err.toString()));
// .finally(() => refresh().catch(console.error));
}
function disableClient(client) {
api.disableClient({ clientId: client.id }).catch((err) => alert(err.message || err.toString()));
// .finally(() => refresh().catch(console.error));
}
</script>

67
webui/src/components/ClientInfo.vue

@ -0,0 +1,67 @@
<template>
<div class="flex-grow">
<!-- Name -->
<ClientName
:client="client"
@update-client-name="updateClientName"
/>
<!-- Info -->
<div class="text-gray-400 dark:text-neutral-400 text-xs">
<!-- Address -->
<ClientAddress :client="client" @update-address="updateClientAddress" />
<!-- Transfer TX -->
<ClientTransfer
:transferData="client.transferTx"
:transferDataCurrent="client.transferTxCurrent"
:title="'Total Download: ' + bytes(client.transferTx)"
><IconDownArrow
/></ClientTransfer>
<!-- Transfer RX -->
<ClientTransfer
:transferData="client.transferRx"
:transferDataCurrent="client.transferRxCurrent"
:title="'Total Upload: ' + bytes(client.transferRx)"
>
<IconUpArrow />
</ClientTransfer>
<!-- Last seen -->
<span
v-if="client.latestHandshakeAt"
:title="'Last seen on ' + dateTime(new Date(client.latestHandshakeAt))"
>
<!-- FIXME: add "timeago" -->
<!-- · {{ new Date(client.latestHandshakeAt) | timeago }} -->
· {{ timeago(client.latestHandshakeAt) }}
</span>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import ClientName from '@/components/ClientName.vue';
import ClientAddress from '@/components/ClientAddress.vue';
import IconUpArrow from '@/components/icons/IconUpArrow.vue';
import IconDownArrow from '@/components/icons/IconDownArrow.vue';
import ClientTransfer from '@/components/ClientTransfer.vue';
import { useDateTime } from '@/composables/useDateTime';
import { useTimeAgo } from '@/composables/useTimeAgo';
import { useBytes } from '@/composables/useBytes';
import API from '@/services/api';
defineProps({
client: {},
});
const { dateTime } = useDateTime();
const { timeago } = useTimeAgo();
const { bytes } = useBytes();
const api = new API();
</script>

130
webui/src/components/ClientName.vue

@ -1,37 +1,3 @@
<script>
import { useDateTime } from '../composables/useDateTime';
import IconEdit from './icons/IconEdit.vue';
export default {
props: ['client', 'clientEditName', 'clientEditNameId'],
setup() {
const { dateTime } = useDateTime();
return {
dateTime,
// other reactive properties
};
},
methods: {
updateClientName() {
this.$emit('update-client-name', this.client, this.clientEditName);
this.clientEditName = null;
this.clientEditNameId = null;
},
cancelEdit() {
this.clientEditName = null;
this.clientEditNameId = null;
},
editName() {
this.clientEditName = this.client.name;
this.clientEditNameId = this.client.id;
this.$nextTick(() => {
this.$refs['client-' + this.client.id + '-name'][0].select();
});
},
},
components: { IconEdit },
};
</script>
<template>
<div
class="text-gray-700 dark:text-neutral-200 group"
@ -42,7 +8,7 @@ export default {
v-show="clientEditNameId === client.id"
:value="clientEditName"
@input="clientEditName = $event.target.value"
@keyup.enter="updateClientName"
@keyup.enter="updateName(client)"
@keyup.escape="cancelEdit"
:ref="'client-' + client.id + '-name'"
class="rounded px-1 border-2 dark:bg-neutral-700 border-gray-100 dark:border-neutral-600 focus:border-gray-200 dark:focus:border-neutral-500 dark:placeholder:text-neutral-500 outline-none w-30"
@ -56,10 +22,102 @@ export default {
<!-- Edit -->
<span
v-show="clientEditNameId !== client.id"
@click="editName"
@click="editName(client)"
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity"
>
<IconEdit />
</span>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue';
import { useDateTime } from '../composables/useDateTime';
import IconEdit from './icons/IconEdit.vue';
import API from '@/services/api';
defineProps({
client: {},
})
const { dateTime } = useDateTime();
const clientEditName = ref(null);
const clientEditNameId = ref(null);
const api = new API();
const updateName = (client) => {
// emit('update-client-name', client, clientEditName);
updateClientName(client, clientEditName.value);
clientEditName.value = null;
clientEditNameId.value = null;
};
const cancelEdit = () => {
clientEditName.value = null;
clientEditNameId.value = null;
};
const editName = (client) => {
console.log(client);
clientEditName.value = client.name;
clientEditNameId.value = client.id;
nextTick(() => {
const clientNameRef = ['client-' + client.id + '-name'];
if (clientNameRef.value) {
clientNameRef.value[0].select()
}
});
};
function updateClientName(client, name) {
api
.updateClientName({ clientId: client.id, name })
.catch((err) => alert(err.message || err.toString()));
// .finally(() => refresh().catch(console.error));
}
function updateClientAddress(client, address) {
api
.updateClientAddress({ clientId: client.id, address })
.catch((err) => alert(err.message || err.toString()));
// .finally(() => refresh().catch(console.error));
}
// import { useDateTime } from '../composables/useDateTime';
// import IconEdit from './icons/IconEdit.vue';
// export default {
// props: ['client', 'clientEditName', 'clientEditNameId'],
// setup() {
// const { dateTime } = useDateTime();
// return {
// dateTime,
// // other reactive properties
// };
// },
// methods: {
// updateClientName() {
// this.$emit('update-client-name', this.client, this.clientEditName);
// this.clientEditName = null;
// this.clientEditNameId = null;
// },
// cancelEdit() {
// this.clientEditName = null;
// this.clientEditNameId = null;
// },
// editName() {
// this.clientEditName = this.client.name;
// this.clientEditNameId = this.client.id;
// this.$nextTick(() => {
// this.$refs['client-' + this.client.id + '-name'][0].select();
// });
// },
// },
// components: { IconEdit },
// };
</script>

12
webui/src/components/Footer.vue

@ -0,0 +1,12 @@
<template>
<p v-cloak class="text-center m-10 text-gray-300 dark:text-neutral-600 text-xs">
Made by
<a target="_blank" class="hover:underline" href="https://emilenijssen.nl/?ref=wg-easy">Emile Nijssen</a>
·
<a class="hover:underline" href="https://github.com/sponsors/WeeJeWel" target="_blank">Donate</a>
·
<a class="hover:underline" href="https://github.com/wg-easy/wg-easy" target="_blank">GitHub</a>
</p>
</template>
<script setup></script>

6
webui/src/main.js

@ -2,5 +2,9 @@ import './assets/style.css';
import App from './App.vue';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
createApp(App).mount('#app');
const pinia = createPinia();
const app = createApp(App).mount('#app');
app.use(pinia);

8
webui/src/services/api.js

@ -1,4 +1,4 @@
const SERVER = 'http://127.0.0.1:51821'
const SERVER = 'http://127.0.0.1:51821';
export default class API {
async call({ method, path, body }) {
@ -13,7 +13,6 @@ export default class API {
if (res.status === 204) {
return undefined;
}
console.log(`${SERVER}/api${path}`);
const json = await res.json();
@ -62,10 +61,7 @@ export default class API {
...client,
createdAt: new Date(client.createdAt),
updatedAt: new Date(client.updatedAt),
latestHandshakeAt:
client.latestHandshakeAt !== null
? new Date(client.latestHandshakeAt)
: null,
latestHandshakeAt: client.latestHandshakeAt !== null ? new Date(client.latestHandshakeAt) : null,
}))
);
}

69
webui/src/utils/chartOptions.js

@ -0,0 +1,69 @@
import { reactive } from 'vue';
const chartOptions = reactive({
chart: {
background: 'transparent',
type: 'bar',
stacked: false,
toolbar: {
show: false,
},
animations: {
enabled: false,
},
},
colors: [
'#DDDDDD', // rx
'#EEEEEE', // tx
],
dataLabels: {
enabled: false,
},
plotOptions: {
bar: {
horizontal: false,
},
},
xaxis: {
labels: {
show: false,
},
axisTicks: {
show: true,
},
axisBorder: {
show: true,
},
},
yaxis: {
labels: {
show: false,
},
min: 0,
},
tooltip: {
enabled: false,
},
legend: {
show: false,
},
grid: {
show: false,
padding: {
left: -10,
right: 0,
bottom: -15,
top: -15,
},
column: {
opacity: 0,
},
xaxis: {
lines: {
show: false,
},
},
},
});
export default chartOptions;
Loading…
Cancel
Save