From 64a43d63b8a086ebb0dacb90d5bc7a93873a8245 Mon Sep 17 00:00:00 2001 From: Ben Coleman Date: Thu, 11 Nov 2021 23:28:32 +0000 Subject: [PATCH] Version 0.1.0 (#7) * Clean up script * chat look and feel update * Bicep tweaks --- .vscode/settings.json | 4 ---- api/package-lock.json | 16 ++++++------- api/package.json | 4 ++-- api/state.js | 13 +++++++---- client/css/main.css | 23 +++++++++++++++++++ client/index.html | 11 +++++---- client/js/app.js | 14 +++++++++++ client/js/components/chat.js | 24 +++++++++++++------ client/js/utils.js | 4 ++-- client/login.html | 2 +- deploy/modules/pubsub.bicep | 2 +- deploy/modules/static-webapp.bicep | 2 +- deploy/modules/storage.bicep | 1 - makefile | 32 +++++++++++++------------- scripts/cleanup.js | 37 ++++++++++++++++++++++++++++++ 15 files changed, 136 insertions(+), 53 deletions(-) delete mode 100644 .vscode/settings.json create mode 100755 scripts/cleanup.js diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 227029c..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - //Fix for bicep extension - "editor.semanticHighlighting.enabled": true -} diff --git a/api/package-lock.json b/api/package-lock.json index a3d28c3..f8c4e18 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,12 +1,12 @@ { "name": "chatr-serverless-api", - "version": "0.0.9", + "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "chatr-serverless-api", - "version": "0.0.9", + "version": "0.1.0", "dependencies": { "@azure/data-tables": "^12.1.2", "@azure/web-pubsub": "^1.0.0-beta.4" @@ -146,9 +146,9 @@ } }, "node_modules/@azure/web-pubsub": { - "version": "1.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@azure/web-pubsub/-/web-pubsub-1.0.0-beta.4.tgz", - "integrity": "sha512-Zmz3hKq2TLTYazUi4zR+SxONs3C9eu5qKfGbY/uF+ZtgC/pbKH0x501DuwHdL7arQj8E9u2EaGEJx+OdgAMeMA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/web-pubsub/-/web-pubsub-1.0.0.tgz", + "integrity": "sha512-/2/xNC/h75Kfuo9sy6ST2RYq6KXe2gl/MkdcnRkO9gNQWbLaGATc7gmBKQ5YX2O8q9mPP7MWLLbhc/lH3zW1Bg==", "dependencies": { "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.0.0", @@ -1497,9 +1497,9 @@ } }, "@azure/web-pubsub": { - "version": "1.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@azure/web-pubsub/-/web-pubsub-1.0.0-beta.4.tgz", - "integrity": "sha512-Zmz3hKq2TLTYazUi4zR+SxONs3C9eu5qKfGbY/uF+ZtgC/pbKH0x501DuwHdL7arQj8E9u2EaGEJx+OdgAMeMA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/web-pubsub/-/web-pubsub-1.0.0.tgz", + "integrity": "sha512-/2/xNC/h75Kfuo9sy6ST2RYq6KXe2gl/MkdcnRkO9gNQWbLaGATc7gmBKQ5YX2O8q9mPP7MWLLbhc/lH3zW1Bg==", "requires": { "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.0.0", diff --git a/api/package.json b/api/package.json index 8128d90..63e8a92 100644 --- a/api/package.json +++ b/api/package.json @@ -1,9 +1,9 @@ { "name": "chatr-serverless-api", - "version": "0.0.9", + "version": "0.1.0", "description": "Serverless API and event handler for Chatr", "scripts": { - "start": "swa start ../client --api . --swa-config-location ../client", + "start": "swa start ../client --api-location . --swa-config-location ../client", "lint": "eslint . && prettier --check *.js", "lint-fix": "eslint . --fix && prettier --write *.js" }, diff --git a/api/state.js b/api/state.js index c012685..1cac275 100644 --- a/api/state.js +++ b/api/state.js @@ -12,15 +12,15 @@ const chatsTable = 'chats' const usersTable = 'users' const partitionKey = 'chatr' +if (!account || !accountKey) { + console.log('### ๐Ÿ’ฅ Fatal! STORAGE_ACCOUNT_NAME and/or STORAGE_ACCOUNT_KEY is not set') +} + const credential = new AzureNamedKeyCredential(account, accountKey) const serviceClient = new TableServiceClient(`https://${account}.table.core.windows.net`, credential) const userTableClient = new TableClient(`https://${account}.table.core.windows.net`, usersTable, credential) const chatTableClient = new TableClient(`https://${account}.table.core.windows.net`, chatsTable, credential) -if (!account || !accountKey) { - console.log('### ๐Ÿ’ฅ Fatal! STORAGE_ACCOUNT_NAME and/or STORAGE_ACCOUNT_KEY is not set') -} - // ============================================================== // Create tables and absorb errors if they already exist // ============================================================== @@ -79,7 +79,10 @@ async function listChats() { let chatList = chatTableClient.listEntities() for await (const chat of chatList) { - chatsResp[chat.rowKey] = JSON.parse(chat.data) + let chatObj = JSON.parse(chat.data) + // Timestamp only used by cleanup script + chatObj.timestamp = chat.timestamp + chatsResp[chat.rowKey] = chatObj } return chatsResp } diff --git a/client/css/main.css b/client/css/main.css index 4ef84c9..e4061a5 100644 --- a/client/css/main.css +++ b/client/css/main.css @@ -37,6 +37,29 @@ footer { color: #c0bdba !important; } +.chatMsgRow { + width: 100%; + display: flex; +} + +.chatMsg { + max-width: 70%; +} + +.chatMsgTitle { + font-size: 0.6rem; + font-weight: bold; + color: hsl(204, 86%, 53%); +} + +.chatMsgBody { + font-size: 0.9rem; +} + +.chatRight { + justify-content: flex-end; +} + .loaderOverlay { position: absolute; top: 0; diff --git a/client/index.html b/client/index.html index 594f064..06d5af5 100644 --- a/client/index.html +++ b/client/index.html @@ -6,7 +6,7 @@ Chatr App - + @@ -25,12 +25,13 @@
-
+
Loading...
{{ error }}
-
+ +
@@ -38,7 +39,7 @@
-
- + diff --git a/client/js/app.js b/client/js/app.js index 09c6498..c6a447a 100644 --- a/client/js/app.js +++ b/client/js/app.js @@ -93,6 +93,15 @@ new Vue({ // Now connect to Azure Web PubSub using the URL we got this.ws = new WebSocket(token.url, 'json.webpubsub.azure.v1') + + // Both of these handle error situations + this.ws.onerror = (evt) => { + this.error = `WebSocket error ${evt.message}` + } + this.ws.onclose = (evt) => { + this.error = `WebSocket closed, code: ${evt.code}` + } + // Custom notification event, rather that relying on the system connected event this.ws.onopen = () => { this.ws.send( @@ -123,6 +132,11 @@ new Vue({ if (msg.from === 'server' && msg.data.chatEvent === 'chatCreated') { let chat = JSON.parse(msg.data.data) this.$set(this.allChats, chat.id, chat) + + this.$nextTick(() => { + const chatList = this.$refs.chatList + chatList.scrollTop = chatList.scrollHeight + }) } if (msg.from === 'server' && msg.data.chatEvent === 'chatDeleted') { diff --git a/client/js/components/chat.js b/client/js/components/chat.js index bbb532f..f1d6264 100644 --- a/client/js/components/chat.js +++ b/client/js/components/chat.js @@ -4,9 +4,9 @@ import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.esm.browser.js export default Vue.component('chat', { data() { return { - chatText: '', message: '', connected: false, + chats: [], } }, @@ -15,8 +15,7 @@ export default Vue.component('chat', { id: String, active: Boolean, user: Object, - // This is shared with the parent app component - ws: WebSocket, + ws: WebSocket, // This is shared with the parent app component }, async mounted() { @@ -31,7 +30,7 @@ export default Vue.component('chat', { // User sent messages, i.e. from sendMessage() below if (msg.data.message && msg.data.fromUserName) { - this.appendChat(`${msg.data.fromUserName}: ${msg.data.message}`) + this.appendChat(msg.data.message, msg.data.fromUserName) break } @@ -50,8 +49,12 @@ export default Vue.component('chat', { }, methods: { - appendChat(text) { - this.chatText += `${text}
` + appendChat(text, from = null) { + this.chats.push({ + text, + from, + time: new Date(), + }) // eslint-disable-next-line no-undef Vue.nextTick(() => { @@ -91,6 +94,13 @@ export default Vue.component('chat', { -
+
+
+
+
{{ chat.from }}
+
+
+
+
`, }) diff --git a/client/js/utils.js b/client/js/utils.js index 55cd140..3666165 100644 --- a/client/js/utils.js +++ b/client/js/utils.js @@ -1,4 +1,4 @@ -import { toast } from 'https://cdn.jsdelivr.net/npm/bulma-toast@2.3.0/dist/bulma-toast.esm.js' +import { toast } from 'https://cdn.jsdelivr.net/npm/bulma-toast@2.4.1/dist/bulma-toast.esm.js' export default { uuidv4() { @@ -14,7 +14,7 @@ export default { message: msg, type: `is-${type}`, duration: 1500, - position: 'top-center', + position: 'bottom-center', animate: { in: 'fadeIn', out: 'fadeOut' }, }) }, diff --git a/client/login.html b/client/login.html index 2f55f73..5142e98 100644 --- a/client/login.html +++ b/client/login.html @@ -47,7 +47,7 @@

Azure Web PubSub Demo

- + diff --git a/deploy/modules/pubsub.bicep b/deploy/modules/pubsub.bicep index 59ddc77..a452882 100644 --- a/deploy/modules/pubsub.bicep +++ b/deploy/modules/pubsub.bicep @@ -3,7 +3,7 @@ param name string = 'chatr' param sku string = 'Free_F1' param eventHandlerUrl string -resource pubsub 'Microsoft.SignalRService/WebPubSub@2021-04-01-preview' = { +resource pubsub 'Microsoft.SignalRService/webPubSub@2021-10-01' = { name: name location: location diff --git a/deploy/modules/static-webapp.bicep b/deploy/modules/static-webapp.bicep index 0948f6a..996c55c 100644 --- a/deploy/modules/static-webapp.bicep +++ b/deploy/modules/static-webapp.bicep @@ -6,7 +6,7 @@ param repoUrl string @secure() param repoToken string -resource staticApp 'Microsoft.Web/staticSites@2020-12-01' = { +resource staticApp 'Microsoft.Web/staticSites@2021-02-01' = { name: name location: location diff --git a/deploy/modules/storage.bicep b/deploy/modules/storage.bicep index 2914e4f..338a0d3 100644 --- a/deploy/modules/storage.bicep +++ b/deploy/modules/storage.bicep @@ -8,7 +8,6 @@ resource storageAcct 'Microsoft.Storage/storageAccounts@2021-02-01' = { sku: { name: 'Standard_LRS' - tier: 'Standard' } } diff --git a/makefile b/makefile index ab1090b..c39f3e7 100644 --- a/makefile +++ b/makefile @@ -6,41 +6,41 @@ GITHUB_REPO ?= $(shell git remote get-url origin) GITHUB_TOKEN ?= # Don't change -SRC_DIR := api +API_DIR := api CLIENT_DIR := client .PHONY: help run deploy lint lint-fix .DEFAULT_GOAL := help .EXPORT_ALL_VARIABLES: -help: ## ๐Ÿ’ฌ This help message +help: ## ๐Ÿ’ฌ This help message @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' -lint: $(SRC_DIR)/node_modules ## ๐Ÿ”Ž Lint & format, will not fix but sets exit code on error - cd $(SRC_DIR); npm run lint +lint: $(API_DIR)/node_modules ## ๐Ÿ”Ž Lint & format, will not fix but sets exit code on error + cd $(API_DIR); npm run lint eslint $(CLIENT_DIR) -lint-fix: $(SRC_DIR)/node_modules ## ๐Ÿ“œ Lint & format, will try to fix errors and modify code - cd $(SRC_DIR); npm run lint-fix +lint-fix: $(API_DIR)/node_modules ## ๐Ÿ“œ Lint & format, will try to fix errors and modify code + cd $(API_DIR); npm run lint-fix -run: $(SRC_DIR)/node_modules ## ๐Ÿƒ Run server locally using node +run: $(API_DIR)/node_modules ## ๐Ÿƒ Run server locally using node @which swa > /dev/null || { echo "๐Ÿ‘‹ Must install the SWA CLI https://aka.ms/swa-cli"; exit 1; } - swa start ./client --api ./api --swa-config-location ./client + swa start ./client --api-location ./api --swa-config-location ./client -clean: ## ๐Ÿงน Clean up project - rm -rf $(SRC_DIR)/node_modules +clean: ## ๐Ÿงน Clean up project + rm -rf $(API_DIR)/node_modules -deploy: ## ๐Ÿš€ Deploy everything to Azure using Bicep +deploy: ## ๐Ÿš€ Deploy everything to Azure using Bicep @./deploy/deploy.sh -tunnel: ## ๐Ÿš‡ Start loophole tunnel to expose localhost +tunnel: ## ๐Ÿš‡ Start loophole tunnel to expose localhost loophole http 7071 --hostname chatr # ============================================================================ -$(SRC_DIR)/node_modules: $(SRC_DIR)/package.json - cd $(SRC_DIR); npm install --silent - touch -m $(SRC_DIR)/node_modules +$(API_DIR)/node_modules: $(API_DIR)/package.json + cd $(API_DIR); npm install --silent + touch -m $(API_DIR)/node_modules -$(SRC_DIR)/package.json: +$(API_DIR)/package.json: @echo "package.json was modified" diff --git a/scripts/cleanup.js b/scripts/cleanup.js new file mode 100755 index 0000000..9980dfe --- /dev/null +++ b/scripts/cleanup.js @@ -0,0 +1,37 @@ +#!node +const state = require('../api/state') + +const DELETE_AGE = process.argv[2] !== undefined ? parseInt(process.argv[2]) : 24 +console.log(`### Deleting data older than ${DELETE_AGE} hours`) + +console.log('### Cleaning up old users') +state.listUsers().then((users) => { + for (let userId in users) { + const user = users[userId] + + const timestamp = new Date(user.timestamp) + const now = new Date() + const ageInHours = (now.getTime() - timestamp.getTime()) / (1000 * 60 * 60) + + if (ageInHours > DELETE_AGE) { + console.log(`### Deleting user ${user.userName} with age of ${ageInHours} hours`) + state.removeUser(userId) + } + } +}) + +console.log('### Cleaning up old chats') +state.listChats().then((chats) => { + for (let chatId in chats) { + const chat = chats[chatId] + + const timestamp = new Date(chat.timestamp) + const now = new Date() + const ageInHours = (now.getTime() - timestamp.getTime()) / (1000 * 60 * 60) + + if (ageInHours > DELETE_AGE) { + console.log(`### Deleting chat ${chat.name} with age of ${ageInHours} hours`) + state.removeChat(chatId) + } + } +})