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:
parent
454f12003a
commit
d03d1fbfcc
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -144,7 +144,8 @@ button {
|
|||
padding: 1em;
|
||||
background-color: #e2eaff;
|
||||
height: 86vh;
|
||||
max-width: 70vw;
|
||||
max-width: 65vw;
|
||||
min-width: 65vw;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,4 +5,6 @@ export const queryMode = {
|
|||
|
||||
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']
|
|
@ -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 {
|
||||
|
|
|
@ -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 = () => {
|
||||
ws = new WebSocket(`ws://${backendAddressAndPort}`)
|
||||
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.' }))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.sort()
|
||||
console.log('topics', topics)
|
||||
querySettings.topic = topics[0]
|
||||
} catch (e) {
|
||||
console.log('fetch error:', e.toString())
|
||||
}
|
||||
topics = await apiFetch(`/topics/${querySettings.cluster}`)
|
||||
topics ??= []
|
||||
topics.sort()
|
||||
console.log('topics', topics)
|
||||
querySettings.topic = topics[0]
|
||||
}
|
||||
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>
|
||||
|
||||
|
|
|
@ -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`)
|
||||
clusters = await response.json()
|
||||
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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue