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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
<script>
import NavBar from "./NavBar.svelte";
import logo from './kd-full-logo.png';
import { state, testMode } from '../state.js'
</script>
<header>
@ -13,7 +14,16 @@
<NavBar />
<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>
</header>
@ -28,6 +38,7 @@
font-size: 18px;
color: black;
height: 3em;
padding: 1em;
}
.corner a {

View File

@ -17,15 +17,15 @@ const mockItems = [
{
"eventType": "Movie",
"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() }))
const testMode = false
export const testMode = true
export const state = writable({
items: [],
itemCount: undefined,
error: null,
error: 'Connecting to WebSocket...',
})
const updateClearError = updater => state.update(s => {
s.error = null
@ -38,6 +38,7 @@ let disconnected = false
const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)]
const testTimeout = 200
const testQuery = (mode, jsFilter, queryCode) => {
try {
const f = new Function('message', 'value', queryCode)
@ -46,16 +47,17 @@ const testQuery = (mode, jsFilter, queryCode) => {
const item = getRandomFromArray(mockItems)
if (!jsFilter || f(item, item.value)) {
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 {
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) {
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 = () => {
try {
ws = new WebSocket(`ws://${backendAddressAndPort}`)
} catch (e) {
console.error(e.toString())
}
if (!ws) {
updateClearError(s => ({ ...s, error: 'Unable to connect to websocket.' }))
return
}
ws.addEventListener('close', () => {
disconnected = true
})
ws.addEventListener('open', () => {
console.log('WebSocket opened')
})
@ -101,7 +103,6 @@ export const connect = () => {
let data;
try {
data = JSON.parse(message.data)
console.log('WebSocket message received', data)
switch (data?.type.toLowerCase()) {
case 'complete':
updateClearError(s => ({
@ -128,6 +129,15 @@ export const connect = () => {
})
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 { JsonView } from '@zerodevx/svelte-json-view'
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 showMetadata = true
@ -24,28 +25,22 @@ return message.value.eventType === "Television";`
let topics = []
let clusters = []
const updateTopics = async () => {
topics = []
try {
const response = await fetch(`${backendUrl}/topics/${querySettings.cluster}`)
topics = await response.json()
topics = await apiFetch(`/topics/${querySettings.cluster}`)
topics ??= []
topics.sort()
console.log('topics', topics)
querySettings.topic = topics[0]
} catch (e) {
console.log('fetch error:', e.toString())
}
}
onMount(async () => {
connect();
const response = await fetch(`${backendUrl}/clusters`)
clusters = Object.keys(await response.json())
clusters = Object.keys((await apiFetch('/clusters')) || {})
querySettings.cluster = clusters[0]
await updateTopics()
})
</script>
<svelte:head>
<title>{querySettings.topic} - Kafka Dance</title>
<title>{querySettings.topic ? `${querySettings.topic} - ` : ''}Kafka Dance</title>
<meta name="description" content="Kafka Dance" />
</svelte:head>

View File

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