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",
|
"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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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']
|
|
@ -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 {
|
||||||
|
|
|
@ -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 = () => {
|
||||||
|
try {
|
||||||
ws = new WebSocket(`ws://${backendAddressAndPort}`)
|
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.' }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = await response.json()
|
|
||||||
topics.sort()
|
topics.sort()
|
||||||
console.log('topics', topics)
|
console.log('topics', topics)
|
||||||
querySettings.topic = topics[0]
|
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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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