From f376a1e516856491cdc971a6d2cea5c8425730ef Mon Sep 17 00:00:00 2001 From: Daniil Isakov <12859907+oplexz@users.noreply.github.com> Date: Sat, 13 Jan 2024 00:28:51 +0300 Subject: [PATCH] Icons and components; move functions to methods; dateTime and timeago --- webui/src/App.vue | 839 +++++------------- webui/src/components/ClientAddress.vue | 60 ++ webui/src/components/ClientAvatar.vue | 22 + webui/src/components/ClientName.vue | 64 ++ webui/src/components/ClientNewButton.vue | 30 + webui/src/components/ClientTransfer.vue | 13 + webui/src/components/LoadingSpinner.vue | 11 + webui/src/components/UpdateNotification.vue | 28 + .../components/icons/IconAvatarDefault.vue | 5 + webui/src/components/icons/IconClose.vue | 5 + webui/src/components/icons/IconDelete.vue | 9 + webui/src/components/icons/IconDownArrow.vue | 9 + webui/src/components/icons/IconDownload.vue | 10 + webui/src/components/icons/IconEdit.vue | 16 + webui/src/components/icons/IconLogout.vue | 10 + webui/src/components/icons/IconNew.vue | 11 + webui/src/components/icons/IconQRCode.vue | 10 + webui/src/components/icons/IconSpinner.vue | 10 + webui/src/components/icons/IconUpArrow.vue | 9 + webui/src/components/icons/IconWarning.vue | 17 + webui/src/composables/useDateTime.js | 13 + webui/src/composables/useTimeAgo.js | 29 + 22 files changed, 632 insertions(+), 598 deletions(-) create mode 100644 webui/src/components/ClientAddress.vue create mode 100644 webui/src/components/ClientAvatar.vue create mode 100644 webui/src/components/ClientName.vue create mode 100644 webui/src/components/ClientNewButton.vue create mode 100644 webui/src/components/ClientTransfer.vue create mode 100644 webui/src/components/LoadingSpinner.vue create mode 100644 webui/src/components/UpdateNotification.vue create mode 100644 webui/src/components/icons/IconAvatarDefault.vue create mode 100644 webui/src/components/icons/IconClose.vue create mode 100644 webui/src/components/icons/IconDelete.vue create mode 100644 webui/src/components/icons/IconDownArrow.vue create mode 100644 webui/src/components/icons/IconDownload.vue create mode 100644 webui/src/components/icons/IconEdit.vue create mode 100644 webui/src/components/icons/IconLogout.vue create mode 100644 webui/src/components/icons/IconNew.vue create mode 100644 webui/src/components/icons/IconQRCode.vue create mode 100644 webui/src/components/icons/IconSpinner.vue create mode 100644 webui/src/components/icons/IconUpArrow.vue create mode 100644 webui/src/components/icons/IconWarning.vue create mode 100644 webui/src/composables/useDateTime.js create mode 100644 webui/src/composables/useTimeAgo.js diff --git a/webui/src/App.vue b/webui/src/App.vue index 5b9480f6..27bd083d 100644 --- a/webui/src/App.vue +++ b/webui/src/App.vue @@ -2,6 +2,27 @@ import { ref, reactive } from 'vue'; import sha256 from 'crypto-js/sha256'; +import { useDateTime } from './composables/useDateTime'; +import { useTimeAgo } from './composables/useTimeAgo'; + +import UpdateNotification from './components/UpdateNotification.vue'; +import ClientNewButton from './components/ClientNewButton.vue'; +import ClientName from './components/ClientName.vue'; +import ClientAvatar from './components/ClientAvatar.vue'; +import ClientAddress from './components/ClientAddress.vue'; +import IconUpArrow from './components/icons/IconUpArrow.vue'; +import IconDownArrow from './components/icons/IconDownArrow.vue'; +import IconLogout from './components/icons/IconLogout.vue'; +import IconQRCode from './components/icons/IconQRCode.vue'; +import IconDownload from './components/icons/IconDownload.vue'; +import IconDelete from './components/icons/IconDelete.vue'; +import IconNew from './components/icons/IconNew.vue'; +import LoadingSpinner from './components/LoadingSpinner.vue'; +import IconClose from './components/icons/IconClose.vue'; +import IconWarning from './components/icons/IconWarning.vue'; +import IconAvatarDefault from './components/icons/IconAvatarDefault.vue'; +import ClientTransfer from './components/ClientTransfer.vue' + class API { async call({ method, path, body }) { const res = await fetch(`./api${path}`, { @@ -208,168 +229,8 @@ export default { const api = new API(); - const dateTime = (value) => { - return new Intl.DateTimeFormat(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - }).format(value); - }; - - const refresh = async ({ updateCharts = false } = {}) => { - if (!authenticated.value) return; - - const clientsData = await api.getClients(); - clients.value = clientsData.map((client) => { - if (client.name.includes('@') && client.name.includes('.')) { - client.avatar = `https://www.gravatar.com/avatar/${sha256(client.name)}?d=blank`; - } - - if (!clientsPersist[client.id]) { - clientsPersist[client.id] = {}; - clientsPersist[client.id].transferRxHistory = Array(50).fill(0); - clientsPersist[client.id].transferRxPrevious = client.transferRx; - clientsPersist[client.id].transferTxHistory = Array(50).fill(0); - clientsPersist[client.id].transferTxPrevious = client.transferTx; - } - - // Debug - // client.transferRx = clientsPersist[client.id].transferRxPrevious + Math.random() * 1000; - // client.transferTx = clientsPersist[client.id].transferTxPrevious + Math.random() * 1000; - - if (updateCharts) { - clientsPersist[client.id].transferRxCurrent = - client.transferRx - clientsPersist[client.id].transferRxPrevious; - clientsPersist[client.id].transferRxPrevious = client.transferRx; - clientsPersist[client.id].transferTxCurrent = - client.transferTx - clientsPersist[client.id].transferTxPrevious; - clientsPersist[client.id].transferTxPrevious = client.transferTx; - - clientsPersist[client.id].transferRxHistory.push( - clientsPersist[client.id].transferRxCurrent - ); - clientsPersist[client.id].transferRxHistory.shift(); - - clientsPersist[client.id].transferTxHistory.push( - clientsPersist[client.id].transferTxCurrent - ); - clientsPersist[client.id].transferTxHistory.shift(); - } - - client.transferTxCurrent = clientsPersist[client.id].transferTxCurrent; - client.transferRxCurrent = clientsPersist[client.id].transferRxCurrent; - - client.transferTxHistory = clientsPersist[client.id].transferTxHistory; - client.transferRxHistory = clientsPersist[client.id].transferRxHistory; - client.transferMax = Math.max( - ...client.transferTxHistory, - ...client.transferRxHistory - ); - - client.hoverTx = clientsPersist[client.id].hoverTx; - client.hoverRx = clientsPersist[client.id].hoverRx; - - return client; - }); - }; - - const login = (e) => { - e.preventDefault(); - - if (!password.value) return; - if (authenticating.value) return; - - authenticating.value = true; - api - .createSession({ - password: password.value, - }) - .then(async () => { - const session = await api.getSession(); - authenticated.value = session.authenticated; - requiresPassword.value = session.requiresPassword; - return refresh(); - }) - .catch((err) => { - alert(err.message || err.toString()); - }) - .finally(() => { - authenticating.value = false; - password.value = null; - }); - }; - - const logout = (e) => { - e.preventDefault(); - - api - .deleteSession() - .then(() => { - authenticated.value = false; - clients.value = null; - }) - .catch((err) => { - alert(err.message || err.toString()); - }); - }; - - const createClient = () => { - const name = clientCreateName.value; - if (!name) return; - - api - .createClient({ name }) - .catch((err) => alert(err.message || err.toString())) - .finally(() => refresh().catch(console.error)); - }; - - const deleteClient = (client) => { - api - .deleteClient({ clientId: client.id }) - .catch((err) => alert(err.message || err.toString())) - .finally(() => refresh().catch(console.error)); - }; - - const enableClient = (client) => { - api - .enableClient({ clientId: client.id }) - .catch((err) => alert(err.message || err.toString())) - .finally(() => refresh().catch(console.error)); - }; - - const disableClient = (client) => { - api - .disableClient({ clientId: client.id }) - .catch((err) => alert(err.message || err.toString())) - .finally(() => refresh().catch(console.error)); - }; - - const updateClientName = (client, name) => { - api - .updateClientName({ clientId: client.id, name }) - .catch((err) => alert(err.message || err.toString())) - .finally(() => refresh().catch(console.error)); - }; - - const updateClientAddress = (client, address) => { - api - .updateClientAddress({ clientId: client.id, address }) - .catch((err) => alert(err.message || err.toString())) - .finally(() => refresh().catch(console.error)); - }; - - const toggleTheme = () => { - if (isDark.value) { - localStorage.theme = 'light'; - document.documentElement.classList.remove('dark'); - } else { - localStorage.theme = 'dark'; - document.documentElement.classList.add('dark'); - } - isDark.value = !isDark.value; - }; + const { dateTime } = useDateTime(); + const { timeago } = useTimeAgo(); return { authenticated, @@ -396,16 +257,7 @@ export default { chartOptions, dateTime, - refresh, - login, - logout, - createClient, - deleteClient, - enableClient, - disableClient, - updateClientName, - updateClientAddress, - toggleTheme, + timeago, api, }; @@ -469,6 +321,184 @@ export default { }) .catch(console.error); }, + components: { + UpdateNotification, + ClientNewButton, + ClientName, + ClientAvatar, + ClientAddress, + IconUpArrow, + IconDownArrow, + IconLogout, + IconQRCode, + IconDownload, + IconDelete, + IconNew, + LoadingSpinner, + IconClose, + IconWarning, + IconAvatarDefault, + ClientTransfer, + }, + methods: { + handleNewClient() { + this.clientCreate = true; + this.clientCreateName = ''; + }, + + async refresh ({ updateCharts = false } = {}) { + if (!this.authenticated) return; + + const clientsData = await this.api.getClients(); + this.clients = clientsData.map((client) => { + if (client.name.includes('@') && client.name.includes('.')) { + client.avatar = `https://www.gravatar.com/avatar/${sha256(client.name)}?d=blank`; + } + + if (!this.clientsPersist[client.id]) { + this.clientsPersist[client.id] = {}; + this.clientsPersist[client.id].transferRxHistory = Array(50).fill(0); + this.clientsPersist[client.id].transferRxPrevious = client.transferRx; + this.clientsPersist[client.id].transferTxHistory = Array(50).fill(0); + this.clientsPersist[client.id].transferTxPrevious = client.transferTx; + } + + // Debug + // client.transferRx = this.clientsPersist[client.id].transferRxPrevious + Math.random() * 1000; + // client.transferTx = this.clientsPersist[client.id].transferTxPrevious + Math.random() * 1000; + + if (updateCharts) { + this.clientsPersist[client.id].transferRxCurrent = + client.transferRx - this.clientsPersist[client.id].transferRxPrevious; + this.clientsPersist[client.id].transferRxPrevious = client.transferRx; + this.clientsPersist[client.id].transferTxCurrent = + client.transferTx - this.clientsPersist[client.id].transferTxPrevious; + this.clientsPersist[client.id].transferTxPrevious = client.transferTx; + + this.clientsPersist[client.id].transferRxHistory.push( + this.clientsPersist[client.id].transferRxCurrent + ); + this.clientsPersist[client.id].transferRxHistory.shift(); + + this.clientsPersist[client.id].transferTxHistory.push( + this.clientsPersist[client.id].transferTxCurrent + ); + this.clientsPersist[client.id].transferTxHistory.shift(); + } + + client.transferTxCurrent = this.clientsPersist[client.id].transferTxCurrent; + client.transferRxCurrent = this.clientsPersist[client.id].transferRxCurrent; + + client.transferTxHistory = this.clientsPersist[client.id].transferTxHistory; + client.transferRxHistory = this.clientsPersist[client.id].transferRxHistory; + client.transferMax = Math.max( + ...client.transferTxHistory, + ...client.transferRxHistory + ); + + client.hoverTx = this.clientsPersist[client.id].hoverTx; + client.hoverRx = this.clientsPersist[client.id].hoverRx; + + return client; + }); + }, + + login (e) { + e.preventDefault(); + + if (!this.password) return; + if (this.authenticating) return; + + this.authenticating = true; + this.api + .createSession({ + password: this.password, + }) + .then(async () => { + const session = await this.api.getSession(); + this.authenticated = session.authenticated; + this.requiresPassword = session.requiresPassword; + return this.refresh(); + }) + .catch((err) => { + alert(err.message || err.toString()); + }) + .finally(() => { + this.authenticating = false; + this.password = null; + }); + }, + + logout (e) { + e.preventDefault(); + + this.api + .deleteSession() + .then(() => { + this.authenticated = false; + this.clients = null; + }) + .catch((err) => { + alert(err.message || err.toString()); + }); + }, + + createClient () { + const name = this.clientCreateName; + if (!name) return; + + this.api + .createClient({ name }) + .catch((err) => alert(err.message || err.toString())) + .finally(() => this.refresh().catch(console.error)); + }, + + deleteClient (client) { + this.api + .deleteClient({ clientId: client.id }) + .catch((err) => alert(err.message || err.toString())) + .finally(() => this.refresh().catch(console.error)); + }, + + enableClient (client) { + this.api + .enableClient({ clientId: client.id }) + .catch((err) => alert(err.message || err.toString())) + .finally(() => this.refresh().catch(console.error)); + }, + + disableClient (client) { + this.api + .disableClient({ clientId: client.id }) + .catch((err) => alert(err.message || err.toString())) + .finally(() => this.refresh().catch(console.error)); + }, + + updateClientName (client, name) { + this.api + .updateClientName({ clientId: client.id, name }) + .catch((err) => alert(err.message || err.toString())) + .finally(() => this.refresh().catch(console.error)); + }, + + updateClientAddress (client, address) { + this.api + .updateClientAddress({ clientId: client.id, address }) + .catch((err) => alert(err.message || err.toString())) + .finally(() => this.refresh().catch(console.error)); + }, + + toggleTheme () { + if (this.isDark) { + localStorage.theme = 'light'; + document.documentElement.classList.remove('dark'); + } else { + localStorage.theme = 'dark'; + document.documentElement.classList.add('dark'); + } + this.isDark = !this.isDark; + }, + }, }; @@ -482,20 +512,7 @@ export default { @click="logout" > Logout - - - +

-
-
-
-

There is an update available!

-

{{ latestRelease.changelog }}

-
- - - Update → - -
-
+
-
-
-

Clients

-
-
- -
-
+
@@ -599,208 +565,44 @@ export default { class="relative p-5 z-10 flex flex-col md:flex-row justify-between" >
-
- - - - - -
-
-
-
-
+
-
- - - {{ client.name }} - - - - - - - -
+
- - - - {{ client.address }} - - - - - - - - + - - · - - - - - {{ client.transferTxCurrent | bytes }}/s - + + - - · - - - - - {{ client.transferRxCurrent | bytes }}/s - + + - · {{ new Date(client.latestHandshakeAt) }} + · {{ timeago(client.latestHandshakeAt) }}
@@ -848,20 +650,7 @@ export default { qrcode = `./api/wireguard/client/${client.id}/qrcode.svg` " > - - - + @@ -873,20 +662,7 @@ export default { 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" > - - - + @@ -895,18 +671,7 @@ export default { title="Delete Client" @click="clientDelete = client" > - - - +
@@ -924,21 +689,7 @@ export default { " class="bg-red-800 hover:bg-red-700 text-white border-2 border-none py-2 px-4 rounded inline-flex items-center transition" > - - - + New Client

@@ -947,26 +698,7 @@ export default { v-if="clients === null" class="text-gray-200 dark:text-red-300 p-5" > - - - - +
@@ -981,20 +713,7 @@ export default { @click="qrcode = null" class="absolute right-4 top-4 text-gray-600 dark:text-neutral-500 hover:text-gray-800 dark:hover:text-neutral-700" > - - - + @@ -1051,21 +770,7 @@ export default {
- - - +
+

- - - +

- - - - + - - - - + diff --git a/webui/src/components/ClientAddress.vue b/webui/src/components/ClientAddress.vue new file mode 100644 index 00000000..893c21b2 --- /dev/null +++ b/webui/src/components/ClientAddress.vue @@ -0,0 +1,60 @@ + + + + diff --git a/webui/src/components/ClientAvatar.vue b/webui/src/components/ClientAvatar.vue new file mode 100644 index 00000000..df1a01ff --- /dev/null +++ b/webui/src/components/ClientAvatar.vue @@ -0,0 +1,22 @@ + + + diff --git a/webui/src/components/ClientName.vue b/webui/src/components/ClientName.vue new file mode 100644 index 00000000..7cf8056a --- /dev/null +++ b/webui/src/components/ClientName.vue @@ -0,0 +1,64 @@ + + + diff --git a/webui/src/components/ClientNewButton.vue b/webui/src/components/ClientNewButton.vue new file mode 100644 index 00000000..07f712c3 --- /dev/null +++ b/webui/src/components/ClientNewButton.vue @@ -0,0 +1,30 @@ + + + + diff --git a/webui/src/components/ClientTransfer.vue b/webui/src/components/ClientTransfer.vue new file mode 100644 index 00000000..7d7c8a98 --- /dev/null +++ b/webui/src/components/ClientTransfer.vue @@ -0,0 +1,13 @@ + + + diff --git a/webui/src/components/LoadingSpinner.vue b/webui/src/components/LoadingSpinner.vue new file mode 100644 index 00000000..75e76380 --- /dev/null +++ b/webui/src/components/LoadingSpinner.vue @@ -0,0 +1,11 @@ + + + diff --git a/webui/src/components/UpdateNotification.vue b/webui/src/components/UpdateNotification.vue new file mode 100644 index 00000000..5c087e4d --- /dev/null +++ b/webui/src/components/UpdateNotification.vue @@ -0,0 +1,28 @@ + + + diff --git a/webui/src/components/icons/IconAvatarDefault.vue b/webui/src/components/icons/IconAvatarDefault.vue new file mode 100644 index 00000000..407d2cbb --- /dev/null +++ b/webui/src/components/icons/IconAvatarDefault.vue @@ -0,0 +1,5 @@ + diff --git a/webui/src/components/icons/IconClose.vue b/webui/src/components/icons/IconClose.vue new file mode 100644 index 00000000..38ab7571 --- /dev/null +++ b/webui/src/components/icons/IconClose.vue @@ -0,0 +1,5 @@ + diff --git a/webui/src/components/icons/IconDelete.vue b/webui/src/components/icons/IconDelete.vue new file mode 100644 index 00000000..ae8811c7 --- /dev/null +++ b/webui/src/components/icons/IconDelete.vue @@ -0,0 +1,9 @@ + diff --git a/webui/src/components/icons/IconDownArrow.vue b/webui/src/components/icons/IconDownArrow.vue new file mode 100644 index 00000000..21b218e2 --- /dev/null +++ b/webui/src/components/icons/IconDownArrow.vue @@ -0,0 +1,9 @@ + diff --git a/webui/src/components/icons/IconDownload.vue b/webui/src/components/icons/IconDownload.vue new file mode 100644 index 00000000..e8783494 --- /dev/null +++ b/webui/src/components/icons/IconDownload.vue @@ -0,0 +1,10 @@ + diff --git a/webui/src/components/icons/IconEdit.vue b/webui/src/components/icons/IconEdit.vue new file mode 100644 index 00000000..36f0be72 --- /dev/null +++ b/webui/src/components/icons/IconEdit.vue @@ -0,0 +1,16 @@ + diff --git a/webui/src/components/icons/IconLogout.vue b/webui/src/components/icons/IconLogout.vue new file mode 100644 index 00000000..a97cbcd1 --- /dev/null +++ b/webui/src/components/icons/IconLogout.vue @@ -0,0 +1,10 @@ + diff --git a/webui/src/components/icons/IconNew.vue b/webui/src/components/icons/IconNew.vue new file mode 100644 index 00000000..01d878c4 --- /dev/null +++ b/webui/src/components/icons/IconNew.vue @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/webui/src/components/icons/IconQRCode.vue b/webui/src/components/icons/IconQRCode.vue new file mode 100644 index 00000000..a6cbdce3 --- /dev/null +++ b/webui/src/components/icons/IconQRCode.vue @@ -0,0 +1,10 @@ + diff --git a/webui/src/components/icons/IconSpinner.vue b/webui/src/components/icons/IconSpinner.vue new file mode 100644 index 00000000..fc86cdb2 --- /dev/null +++ b/webui/src/components/icons/IconSpinner.vue @@ -0,0 +1,10 @@ + diff --git a/webui/src/components/icons/IconUpArrow.vue b/webui/src/components/icons/IconUpArrow.vue new file mode 100644 index 00000000..1948e55e --- /dev/null +++ b/webui/src/components/icons/IconUpArrow.vue @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/webui/src/components/icons/IconWarning.vue b/webui/src/components/icons/IconWarning.vue new file mode 100644 index 00000000..9d5d9095 --- /dev/null +++ b/webui/src/components/icons/IconWarning.vue @@ -0,0 +1,17 @@ + diff --git a/webui/src/composables/useDateTime.js b/webui/src/composables/useDateTime.js new file mode 100644 index 00000000..19820e19 --- /dev/null +++ b/webui/src/composables/useDateTime.js @@ -0,0 +1,13 @@ +export function useDateTime() { + const dateTime = (value) => { + return new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + }).format(value); + }; + + return { dateTime }; +} diff --git a/webui/src/composables/useTimeAgo.js b/webui/src/composables/useTimeAgo.js new file mode 100644 index 00000000..fec74a27 --- /dev/null +++ b/webui/src/composables/useTimeAgo.js @@ -0,0 +1,29 @@ +export function useTimeAgo() { + const timeago = (date) => { + const now = new Date(); + const diff = now - date; + + if (diff < 1000) { + return "just now"; + } else if (diff < 60000) { + const seconds = Math.floor(diff / 1000); + return `${seconds} ${plural("second", seconds)} ago`; + } else if (diff < 3600000) { + const minutes = Math.floor(diff / 60000); + return `${minutes} ${plural("minute", minutes)} ago`; + } else if (diff < 86400000) { + const hours = Math.floor(diff / 3600000); + return `${hours} ${plural("hour", hours)} ago`; + } else { + const days = Math.floor(diff / 86400000); + return `${days} ${plural("day", days)} ago`; + } + }; + + // #278 -- this function might need to be rewritten to support l18n + const plural = (word, count) => { + return count !== 1 ? `${word}s` : word; + }; + + return { timeago }; +}