More re-org.

Try to move message searching implementation into separate .js file.
Have apiFetch() put errors into state, as appropriate.
Add errorDetails display.
This commit is contained in:
Sage Vaillancourt 2022-08-26 18:46:21 -04:00
parent 8719371bd2
commit bc1ad5e45a
6 changed files with 163 additions and 93 deletions

View File

@ -18,10 +18,11 @@
Allow JavaScript to mutate objects Allow JavaScript to mutate objects
<input type=checkbox bind:checked={mutableObjects}> <input type=checkbox bind:checked={mutableObjects}>
</div> </div>
<textarea class={"query-input" + (big ? ' big' : '')} bind:value={queryCode}></textarea> <textarea class={'query-input' + (big ? ' big' : '')} bind:value={queryCode}></textarea>
</div> </div>
{/if} {/if}
<!--suppress CssUnusedSymbol -->
<style> <style>
.query-input-header { .query-input-header {
display: flex; display: flex;

View File

@ -41,10 +41,12 @@ export const state = writable({
itemCount: undefined, itemCount: undefined,
matchCount: undefined, matchCount: undefined,
error: 'Connecting to WebSocket...', error: 'Connecting to WebSocket...',
errorDetails: undefined,
}) })
const updateClearError = updater => state.update(s => { const updateClearError = updater => state.update(s => {
s.error = null s.error = null
s.errorDetails = null
return updater(s) return updater(s)
}) })
@ -129,6 +131,9 @@ export const query = async ({ cluster, topic, mode, jsFilter, queryCode, maxItem
} }
export const connect = () => { export const connect = () => {
if (ws?.readyState === WebSocket.OPEN) {
return;
}
if (testMode) { if (testMode) {
updateClearError(s => ({ ...s, error: 'TEST MODE ENABLED' })) updateClearError(s => ({ ...s, error: 'TEST MODE ENABLED' }))
return return

View File

@ -1 +1,65 @@
export const prerender = true; import { apiFetch } from "../utils.js";
import { killQuery, query } from "../lib/state.js";
export const getTopics = async cluster =>
(await apiFetch(`/topics/${cluster}`) || []).sort()
export const mapFromLocalStorage = ({ checked, querySettings, jsonDisplay, clusterNames, topics }) => {
if (localStorage.querySettings) {
let storedSettings
try {
storedSettings = JSON.parse(localStorage.querySettings)
} catch (e) {
storedSettings = {}
}
querySettings.maxItems = storedSettings?.maxItems ?? querySettings.maxItems
querySettings.jsFilter = storedSettings?.jsFilter ?? querySettings.jsFilter
querySettings.queryCode = storedSettings?.queryCode ?? querySettings.queryCode
querySettings.mode = storedSettings?.mode ?? querySettings.mode
if (!checked || clusterNames.includes(storedSettings?.cluster)) {
querySettings.cluster = storedSettings.cluster
} else {
querySettings.cluster = clusterNames[0]
}
if (!checked || topics.includes(storedSettings?.topic)) {
querySettings.topic = storedSettings.topic
} else {
querySettings.topic = topics[0]
}
}
if (localStorage.jsonDisplay) {
try {
const savedJsonDisplay = JSON.parse(localStorage.jsonDisplay)
Object.entries(savedJsonDisplay).forEach(([key, value]) => {
if (savedJsonDisplay[key] !== undefined) {
jsonDisplay[key] = value
}
})
} catch (e) {
}
}
}
export const getClusterNames = async () => Object.keys((await apiFetch('/clusters')) || {})
export const startQuery = querySettings => {
localStorage.setItem('querySettings', JSON.stringify(querySettings))
query(querySettings)
return true
}
export const stopQuery = querySettings => {
killQuery(querySettings)
return false
}
export const defaultQueryCode =
`// \`message\` contains all metadata
// \`value\` is shorthand for message.value
// it contains the actual produced data
// \`json\` is a JSON representation of \`message\`
return message.value.eventType === "Television";`

View File

@ -2,92 +2,86 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { JsonView } from '@zerodevx/svelte-json-view' import { JsonView } from '@zerodevx/svelte-json-view'
import QueryInput from '$lib/QueryInput.svelte' import QueryInput from '$lib/QueryInput.svelte'
import { state, connect, query, killQuery } from '$lib/state' import { state, connect } from '$lib/state'
import { queryMode } from "../lib/constants.js" import { queryMode } from '../lib/constants.js'
import { apiFetch } from "../utils.js" import Modal from '../lib/Modal.svelte';
import Modal from "../lib/Modal.svelte"; import {
defaultQueryCode,
getClusterNames,
getTopics,
mapFromLocalStorage,
startQuery,
stopQuery
} from './+page.js';
let showQueryModal = false let showQueryModal = false
let queryRunning = false
let expandAll = true const fontSizes = [30, 50, 67, 80, 90, 100, 110, 120, 133, 150]
let showMetadata = true let jsonDisplay = {
expandAll: true,
showMetadata: true,
fontSize: fontSizes.indexOf(100),
fontUp: () => setFontSize(Math.min(jsonDisplay.fontSize + 1, fontSizes.length - 1)),
fontDown: () => setFontSize(Math.max(jsonDisplay.fontSize - 1, 0))
}
const setFontSize = size => {
jsonDisplay.fontSize = size
localStorage.jsonDisplay = JSON.stringify(jsonDisplay)
}
const querySettings = { let querySettings = {
topic: null, topic: null,
cluster: null, cluster: null,
mode: queryMode.REAL_TIME, mode: queryMode.REAL_TIME,
jsFilter: false, jsFilter: false,
maxItems: 20, maxItems: 20,
mutableObjects: false, mutableObjects: false,
queryCode: queryCode: defaultQueryCode
`// \`message\` contains all metadata
// \`value\` is shorthand for message.value
// it contains the actual produced data
// \`json\` is a JSON representation of \`message\`
return message.value.eventType === "Television";`
} }
let topics = [] let topics = []
let clusters = [] let clusterNames = []
const updateTopics = async () => { const updateTopics = async (updateTopic = true) => {
topics = await apiFetch(`/topics/${querySettings.cluster}`) topics = await getTopics(querySettings.cluster)
topics ??= [] if (updateTopic) {
topics.sort()
console.log('topics', topics)
querySettings.topic = topics[0] querySettings.topic = topics[0]
} }
const fontSizes = [30, 50, 67, 80, 90, 100, 110, 120, 133, 150]
let dataViewFontSize = 5
const setFontSize = size => {
dataViewFontSize = size
localStorage.dataViewFontSize = size
} }
const fontUp = () => setFontSize(Math.min(dataViewFontSize + 1, fontSizes.length - 1))
const fontDown = () => setFontSize(Math.max(dataViewFontSize - 1, 0))
onMount(async () => { onMount(async () => {
const loadSave = ({ checked }) => {
if (localStorage) {
mapFromLocalStorage({
checked,
jsonDisplay,
querySettings,
clusterNames,
topics,
})
jsonDisplay = jsonDisplay
querySettings = querySettings
}
}
// Instantly load saved data as if cluster and topic names are accurate
// This should usually be the case
loadSave({ checked: false })
connect() connect()
clusters = Object.keys((await apiFetch('/clusters')) || {}) clusterNames = await getClusterNames()
querySettings.cluster = clusters[0]
await updateTopics() await updateTopics()
if (localStorage) { // Reload with confirmed truthful values
dataViewFontSize = localStorage.dataViewFontSize || dataViewFontSize // Will only flicker if topic or cluster names have changed
if (localStorage.querySettings) { loadSave({ checked: true })
let storedSettings = {}
try {
storedSettings = JSON.parse(localStorage.querySettings)
} catch (e) {}
querySettings.maxItems = storedSettings?.maxItems || querySettings.maxItems
querySettings.jsFilter = storedSettings?.jsFilter || querySettings.jsFilter
querySettings.queryCode = storedSettings?.queryCode || querySettings.queryCode
querySettings.mode = storedSettings?.mode || querySettings.mode
if (clusters.includes(storedSettings?.cluster)) {
querySettings.cluster = storedSettings.cluster
}
if (topics.includes(storedSettings?.topic)) {
querySettings.topic = storedSettings.topic
}
}
}
}) })
let queryRunning = false
const startQuery = () => {
queryRunning = true
localStorage.setItem('querySettings', JSON.stringify(querySettings))
query(querySettings)
}
const stopQuery = () => {
queryRunning = false
killQuery(querySettings)
}
</script> </script>
<svelte:head> <svelte:head>
<title>{querySettings.topic ? `${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 message search" />
</svelte:head> </svelte:head>
<section> <section>
@ -96,16 +90,19 @@ return message.value.eventType === "Television";`
<Header /> <Header />
<NavBar /> <NavBar />
--> -->
<h1>Message Search</h1> <h1>Topic Search</h1>
{#if $state.error} {#if $state.error}
<div class="state-error">{$state.error}</div> <div class="state-error">{$state.error}</div>
{#if $state.errorDetails}
<div class="state-error-details">{$state.errorDetails}</div>
{/if}
{/if} {/if}
<div class="settings-option"> <div class="settings-option">
<h3>Cluster</h3> <h3>Cluster</h3>
<select disabled={clusters.length === 0} bind:value={querySettings.cluster} name="cluster" id="cluster" on:change={updateTopics}> <select disabled={clusterNames.length === 0} bind:value={querySettings.cluster} name="cluster" id="cluster" on:change={updateTopics}>
{#each clusters as cluster} {#each clusterNames as clusterName}
<option value={cluster}>{cluster}</option> <option value={clusterName}>{clusterName}</option>
{/each} {/each}
</select> </select>
</div> </div>
@ -167,27 +164,22 @@ return message.value.eventType === "Television";`
<div class="query-button"> <div class="query-button">
{#if queryRunning} {#if queryRunning}
<button class="danger" on:click={() => stopQuery()}>Stop Query</button> <button class="danger" on:click={() => queryRunning = stopQuery(querySettings)}>Stop Query</button>
{:else} {:else}
<button class="colored" on:click={() => startQuery()}>Start Query</button> <button class="colored" on:click={() => queryRunning = startQuery(querySettings)}>Start Query</button>
{/if} {/if}
</div> </div>
<br>
<div class="live-settings">
<!--{#if $state.itemCount >= 0}
<h2>{$state.itemCount} Matches Found...</h2>
{/if}-->
</div>
</div> </div>
<div class="data-view-container"> <div class="data-view-container">
<div class="data-view-bar"> <div class="data-view-bar">
<div class="border-between"> <div class="border-between">
<button class={expandAll ? 'selected' : ''} on:click={() => expandAll = !expandAll}> <button class={jsonDisplay.expandAll ? 'selected' : ''}
on:click={() => jsonDisplay.expandAll = !jsonDisplay.expandAll}>
Expand All Objects Expand All Objects
</button> </button>
<button class={showMetadata ? 'selected' : ''} on:click={() => showMetadata = !showMetadata}> <button class={jsonDisplay.showMetadata ? 'selected' : ''}
on:click={() => jsonDisplay.showMetadata = !jsonDisplay.showMetadata}>
Show Metadata Show Metadata
</button> </button>
</div> </div>
@ -198,15 +190,15 @@ return message.value.eventType === "Television";`
</div> </div>
{/if} {/if}
<div class="border-between zoom-buttons"> <div class="border-between zoom-buttons">
<button on:click={fontDown} style="border-color: #aaa;">-</button> <button on:click={jsonDisplay.fontDown} style="border-color: #aaa;">-</button>
<button on:click={fontUp}>+</button> <button on:click={jsonDisplay.fontUp}>+</button>
</div> </div>
</div> </div>
</div> </div>
{#if $state.items?.length > 0} {#if $state.items?.length > 0}
<div class="data-view" style={`font-size: ${fontSizes[dataViewFontSize]}%`}> <div class="data-view" style={`font-size: ${fontSizes[jsonDisplay.fontSize]}%`}>
<JsonView json={showMetadata ? $state.items : $state.items.map(item => item.value)} <JsonView json={jsonDisplay.showMetadata ? $state.items : $state.items.map(item => item.value)}
depth={expandAll ? Infinity : 1}/> depth={jsonDisplay.expandAll ? Infinity : 1}/>
</div> </div>
{:else } {:else }
<div class="data-view no-query-data"> <div class="data-view no-query-data">
@ -322,6 +314,11 @@ return message.value.eventType === "Television";`
color: #ff2222; color: #ff2222;
font-weight: bold; font-weight: bold;
} }
.state-error-details {
color: #ff2222;
font-weight: normal;
font-size: 80%;
}
/* JsonView config /* JsonView config
:root { :root {

View File

@ -41,8 +41,7 @@
clusterName: '', // and will be POSTed as a new cluster, not PUTted, updating an existing one clusterName: '', // and will be POSTed as a new cluster, not PUTted, updating an existing one
clientId: '', clientId: '',
brokers: [''], brokers: [''],
useSasl: true, ssl: false,
ssl: true,
sasl: { sasl: {
mechanism: '', mechanism: '',
username: '', username: '',
@ -69,7 +68,7 @@
</svelte:head> </svelte:head>
<section> <section>
<div class="query-settings"> <form class="query-settings" method="post">
<h1>Cluster Configuration</h1> <h1>Cluster Configuration</h1>
<div class="settings-option"> <div class="settings-option">
@ -109,7 +108,7 @@
<!-- TODO: Add options for SSL config --> <!-- TODO: Add options for SSL config -->
<div class="settings-option"> <div class="settings-option">
<div>SASL: <input type=checkbox bind:checked={useSasl} /></div> <div>Use SASL: <input type=checkbox bind:checked={useSasl} /></div>
{#if useSasl} {#if useSasl}
<div class="settings-sub-option"> <div class="settings-sub-option">
<div>Auth Mechanism:</div> <div>Auth Mechanism:</div>
@ -132,7 +131,7 @@
<button style="float: right;" <button style="float: right;"
on:click={() => (config.originalName ? updateCluster() : addCluster())}>{config.originalName ? 'Update Cluster Config' : 'Add Cluster Config'}</button> on:click={() => (config.originalName ? updateCluster() : addCluster())}>{config.originalName ? 'Update Cluster Config' : 'Add Cluster Config'}</button>
</div> </form>
<div class="data-view"> <div class="data-view">
<button style="margin: 1em;" on:click={startNewCluster}>Add New Cluster</button> <button style="margin: 1em;" on:click={startNewCluster}>Add New Cluster</button>

View File

@ -65,7 +65,11 @@ export const apiFetch = async (path, options = undefined) => {
return undefined return undefined
} }
try { try {
return response.json() const json = await response.json()
if (json.error) {
state.update(s => ({ ...s, error: json.error, errorDetails: json.errorDetails }))
}
return json
} catch (e) { } catch (e) {
console.error(e) console.error(e)
state.update(s => ({ ...s, error: 'Received non-JSON response from backend' })) state.update(s => ({ ...s, error: 'Received non-JSON response from backend' }))