Slightly more organized.

Centralize API fetches (don't need to always specify backend URL)
Add mock data to api fetches.
Add dropdown for SASL config.
Try to keep data view from rapidly resizing.
Toying with error display in top-right corner (tentative)
Rename package.
Slightly better WebSocket error handling.
This commit is contained in:
Sage Vaillancourt 2022-08-21 23:05:51 -04:00
parent 454f12003a
commit d03d1fbfcc
9 changed files with 115 additions and 45 deletions

10
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^4.5.0", "@fontsource/fira-mono": "^4.5.0",
"@zerodevx/svelte-json-view": "^0.2.1",
"cookie": "^0.4.1", "cookie": "^0.4.1",
"highlight.js": "^11.6.0", "highlight.js": "^11.6.0",
"svelte-highlight": "^6.2.1" "svelte-highlight": "^6.2.1"
@ -17,8 +18,7 @@
"@sveltejs/adapter-auto": "next", "@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next", "@sveltejs/kit": "next",
"@types/cookie": "^0.5.1", "@types/cookie": "^0.5.1",
"@zerodevx/svelte-json-view": "^0.2.1", "eslint": "^8.22.0",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.6.2", "prettier": "^2.6.2",
@ -395,8 +395,7 @@
"node_modules/@zerodevx/svelte-json-view": { "node_modules/@zerodevx/svelte-json-view": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@zerodevx/svelte-json-view/-/svelte-json-view-0.2.1.tgz", "resolved": "https://registry.npmjs.org/@zerodevx/svelte-json-view/-/svelte-json-view-0.2.1.tgz",
"integrity": "sha512-yaLojLYTi08vccUKRg/XSRCCPoyzCZqrG+W8mVhJEGiOfFKAmWqNH6b+/il1gG3V1UaEe7amj2mzmo1mo4q1iA==", "integrity": "sha512-yaLojLYTi08vccUKRg/XSRCCPoyzCZqrG+W8mVhJEGiOfFKAmWqNH6b+/il1gG3V1UaEe7amj2mzmo1mo4q1iA=="
"dev": true
}, },
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "1.1.1", "version": "1.1.1",
@ -3515,8 +3514,7 @@
"@zerodevx/svelte-json-view": { "@zerodevx/svelte-json-view": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@zerodevx/svelte-json-view/-/svelte-json-view-0.2.1.tgz", "resolved": "https://registry.npmjs.org/@zerodevx/svelte-json-view/-/svelte-json-view-0.2.1.tgz",
"integrity": "sha512-yaLojLYTi08vccUKRg/XSRCCPoyzCZqrG+W8mVhJEGiOfFKAmWqNH6b+/il1gG3V1UaEe7amj2mzmo1mo4q1iA==", "integrity": "sha512-yaLojLYTi08vccUKRg/XSRCCPoyzCZqrG+W8mVhJEGiOfFKAmWqNH6b+/il1gG3V1UaEe7amj2mzmo1mo4q1iA=="
"dev": true
}, },
"abbrev": { "abbrev": {
"version": "1.1.1", "version": "1.1.1",

View File

@ -1,5 +1,5 @@
{ {
"name": "my-app", "name": "kafka-dance-frontend",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@ -15,7 +15,7 @@
"@sveltejs/adapter-auto": "next", "@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next", "@sveltejs/kit": "next",
"@types/cookie": "^0.5.1", "@types/cookie": "^0.5.1",
"eslint": "^8.16.0", "eslint": "^8.22.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.6.2", "prettier": "^2.6.2",

View File

@ -144,7 +144,8 @@ button {
padding: 1em; padding: 1em;
background-color: #e2eaff; background-color: #e2eaff;
height: 86vh; height: 86vh;
max-width: 70vw; max-width: 65vw;
min-width: 65vw;
overflow-y: scroll; overflow-y: scroll;
} }

View File

@ -6,3 +6,5 @@ export const queryMode = {
export const backendAddressAndPort = 'localhost:3000' export const backendAddressAndPort = 'localhost:3000'
export const backendUrl = `http://${backendAddressAndPort}` export const backendUrl = `http://${backendAddressAndPort}`
export const saslAuthMethods = ['PLAIN', 'SCRAM-SHA-256', 'SCRAM-SHA-512', 'AWS']

View File

@ -1,6 +1,7 @@
<script> <script>
import NavBar from "./NavBar.svelte"; import NavBar from "./NavBar.svelte";
import logo from './kd-full-logo.png'; import logo from './kd-full-logo.png';
import { state, testMode } from '../state.js'
</script> </script>
<header> <header>
@ -13,7 +14,16 @@
<NavBar /> <NavBar />
<div class="corner"> <div class="corner">
<!-- TODO put something else here? github link? --> {#if testMode}
<div class="error-display">TEST MODE ENABLED</div>
{/if}
<!--
{#if $state.error}
<div class="error-display">{$state.error}</div>
{:else}
<div class="error-display">OK</div>
{/if}
-->
</div> </div>
</header> </header>
@ -28,6 +38,7 @@
font-size: 18px; font-size: 18px;
color: black; color: black;
height: 3em; height: 3em;
padding: 1em;
} }
.corner a { .corner a {

View File

@ -17,15 +17,15 @@ const mockItems = [
{ {
"eventType": "Movie", "eventType": "Movie",
"broughtToYouBy": "20th Century Fox", "broughtToYouBy": "20th Century Fox",
"title": "Star Wars 2" "title": "Star Wars 2: The Star Warsening, brought to you by the Big Stink Corporation, a division of PepsiCo and somehow also Disney."
}, },
].map(o => ({ value: o, timestamp: new Date().getTime() })) ].map(o => ({ value: o, timestamp: new Date().getTime() }))
const testMode = false export const testMode = true
export const state = writable({ export const state = writable({
items: [], items: [],
itemCount: undefined, itemCount: undefined,
error: null, error: 'Connecting to WebSocket...',
}) })
const updateClearError = updater => state.update(s => { const updateClearError = updater => state.update(s => {
s.error = null s.error = null
@ -38,6 +38,7 @@ let disconnected = false
const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)] const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)]
const testTimeout = 200
const testQuery = (mode, jsFilter, queryCode) => { const testQuery = (mode, jsFilter, queryCode) => {
try { try {
const f = new Function('message', 'value', queryCode) const f = new Function('message', 'value', queryCode)
@ -46,16 +47,17 @@ const testQuery = (mode, jsFilter, queryCode) => {
const item = getRandomFromArray(mockItems) const item = getRandomFromArray(mockItems)
if (!jsFilter || f(item, item.value)) { if (!jsFilter || f(item, item.value)) {
item.timestamp = new Date().getTime() item.timestamp = new Date().getTime()
updateClearError(s => ({ ...s, items: [item, ...s.items].slice(0, itemLimit), itemCount: 0 })) state.update(s => ({ ...s, items: [item, ...s.items].slice(0, itemLimit), itemCount: 0 }))
} }
setTimeout(addItem, 2000) setTimeout(addItem, testTimeout)
} }
setTimeout(addItem, 2000) setTimeout(addItem, testTimeout)
} else { } else {
updateClearError(s => ({ ...s, items: mockItems.filter(item => !jsFilter || f(item, item.value)), itemCount: 0 })) state.update(s => ({ ...s, items: mockItems.filter(item => !jsFilter || f(item, item.value)), itemCount: 0 }))
} }
} catch (e) { } catch (e) {
updateClearError(s => ({ ...s, error: e.toString() })) console.log('Caught an error:', e.toString())
state.update(s => ({ ...s, error: e.toString() }))
} }
} }
@ -83,16 +85,16 @@ export const query = async ({ cluster, topic, mode, jsFilter, queryCode, maxItem
} }
export const connect = () => { export const connect = () => {
ws = new WebSocket(`ws://${backendAddressAndPort}`) try {
ws = new WebSocket(`ws://${backendAddressAndPort}`)
} catch (e) {
console.error(e.toString())
}
if (!ws) { if (!ws) {
updateClearError(s => ({ ...s, error: 'Unable to connect to websocket.' })) updateClearError(s => ({ ...s, error: 'Unable to connect to websocket.' }))
return return
} }
ws.addEventListener('close', () => {
disconnected = true
})
ws.addEventListener('open', () => { ws.addEventListener('open', () => {
console.log('WebSocket opened') console.log('WebSocket opened')
}) })
@ -101,7 +103,6 @@ export const connect = () => {
let data; let data;
try { try {
data = JSON.parse(message.data) data = JSON.parse(message.data)
console.log('WebSocket message received', data)
switch (data?.type.toLowerCase()) { switch (data?.type.toLowerCase()) {
case 'complete': case 'complete':
updateClearError(s => ({ updateClearError(s => ({
@ -128,6 +129,15 @@ export const connect = () => {
}) })
ws.addEventListener('close', () => { ws.addEventListener('close', () => {
console.log('WebSocket closed') disconnected = true
state.update(s => ({ ...s, error: 'WebSocket connection closed.' }))
}) })
ws.addEventListener('error', () => {
state.update(s => ({ ...s, error: 'WebSocket connection error.' }))
})
if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
state.update(s => ({ ...s, error: 'WebSocket connection closed.' }))
}
} }

View File

@ -2,7 +2,8 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { JsonView } from '@zerodevx/svelte-json-view' import { JsonView } from '@zerodevx/svelte-json-view'
import { state, connect, query } from '$lib/state'; import { state, connect, query } from '$lib/state';
import { queryMode, backendUrl } from "../lib/constants.js"; import { queryMode } from "../lib/constants.js";
import { apiFetch } from "../utils.js";
let expandAll = true let expandAll = true
let showMetadata = true let showMetadata = true
@ -24,28 +25,22 @@ return message.value.eventType === "Television";`
let topics = [] let topics = []
let clusters = [] let clusters = []
const updateTopics = async () => { const updateTopics = async () => {
topics = [] topics = await apiFetch(`/topics/${querySettings.cluster}`)
try { topics ??= []
const response = await fetch(`${backendUrl}/topics/${querySettings.cluster}`) topics.sort()
topics = await response.json() console.log('topics', topics)
topics.sort() querySettings.topic = topics[0]
console.log('topics', topics)
querySettings.topic = topics[0]
} catch (e) {
console.log('fetch error:', e.toString())
}
} }
onMount(async () => { onMount(async () => {
connect(); connect();
const response = await fetch(`${backendUrl}/clusters`) clusters = Object.keys((await apiFetch('/clusters')) || {})
clusters = Object.keys(await response.json())
querySettings.cluster = clusters[0] querySettings.cluster = clusters[0]
await updateTopics() await updateTopics()
}) })
</script> </script>
<svelte:head> <svelte:head>
<title>{querySettings.topic} - Kafka Dance</title> <title>{querySettings.topic ? `${querySettings.topic} - ` : ''}Kafka Dance</title>
<meta name="description" content="Kafka Dance" /> <meta name="description" content="Kafka Dance" />
</svelte:head> </svelte:head>

View File

@ -1,6 +1,7 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { backendUrl } from "../../lib/constants.js"; import { backendUrl, saslAuthMethods } from "../../lib/constants.js";
import { apiFetch } from "../../utils.js";
const jsonRequest = type => async (path, object) => fetch(backendUrl + path, { const jsonRequest = type => async (path, object) => fetch(backendUrl + path, {
method: type, method: type,
@ -28,8 +29,11 @@
let clusters = [] let clusters = []
const fetchClusters = async (response) => { const fetchClusters = async (response) => {
response ??= await fetch(`${backendUrl}/clusters`) if (response) {
clusters = await response.json() clusters = await response.json()
} else {
clusters = await apiFetch('/clusters')
}
clusters = Object.fromEntries( clusters = Object.fromEntries(
Object.entries(clusters).map(([name, cluster]) => Object.entries(clusters).map(([name, cluster]) =>
[name, ({ ...cluster, clusterName: name, originalName: name })])) [name, ({ ...cluster, clusterName: name, originalName: name })]))
@ -96,11 +100,16 @@
</div> </div>
</div> </div>
<!-- TODO: Add options for SSL config -->
<div class="settings-option"> <div class="settings-option">
<div>SASL:</div> <div>SASL:</div>
<div class="settings-sub-option"> <div class="settings-sub-option">
<div>Mechanism:</div> <div>Auth Mechanism:</div>
<input bind:value={config.sasl.mechanism}> <select bind:value={config.sasl.mechanism}>
{#each saslAuthMethods as method}
<option value={method}>{method}</option>
{/each}
</select>
</div> </div>
<div class="settings-sub-option"> <div class="settings-sub-option">
<div>Username:</div> <div>Username:</div>

44
src/utils.js Normal file
View File

@ -0,0 +1,44 @@
import { backendUrl } from "$lib/constants.js";
import { state, testMode } from "$lib/state.js";
const mockApi = {
'/clusters': {
'TestCluster': {
clientId: 'TestClient',
brokers: ['testbroker.com:5000'],
sasl: {
mechanism: 'SCRAM-SHA-512',
username: 'testuser',
password: 'XXXXXXXXXXXXXXXXXXXXXX'
}
}
},
'/topics/TestCluster': ['NewReleases']
}
/**
* Returns the JSON response from the given path, or undefined if an error occurred
* Sets `state.error` if something goes wrong.
* @param path
* @returns {Promise<any|undefined>}
*/
export const apiFetch = async path => {
if (testMode) {
return mockApi[path]
}
let response
try {
response = await fetch(`${backendUrl}${path}`)
} catch (e) {
console.error(e)
state.update(s => ({ ...s, error: 'Could not connect to the backend' }))
return undefined
}
try {
return response.json()
} catch (e) {
console.error(e)
state.update(s => ({ ...s, error: 'Received non-JSON response from backend' }))
return undefined
}
}