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:
parent
8719371bd2
commit
bc1ad5e45a
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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";`
|
||||||
|
|
|
@ -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()
|
querySettings.topic = topics[0]
|
||||||
console.log('topics', topics)
|
}
|
||||||
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 () => {
|
||||||
connect()
|
const loadSave = ({ checked }) => {
|
||||||
clusters = Object.keys((await apiFetch('/clusters')) || {})
|
if (localStorage) {
|
||||||
querySettings.cluster = clusters[0]
|
mapFromLocalStorage({
|
||||||
await updateTopics()
|
checked,
|
||||||
|
jsonDisplay,
|
||||||
if (localStorage) {
|
querySettings,
|
||||||
dataViewFontSize = localStorage.dataViewFontSize || dataViewFontSize
|
clusterNames,
|
||||||
if (localStorage.querySettings) {
|
topics,
|
||||||
let storedSettings = {}
|
})
|
||||||
try {
|
jsonDisplay = jsonDisplay
|
||||||
storedSettings = JSON.parse(localStorage.querySettings)
|
querySettings = 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
|
// Instantly load saved data as if cluster and topic names are accurate
|
||||||
const startQuery = () => {
|
// This should usually be the case
|
||||||
queryRunning = true
|
loadSave({ checked: false })
|
||||||
localStorage.setItem('querySettings', JSON.stringify(querySettings))
|
|
||||||
query(querySettings)
|
connect()
|
||||||
}
|
clusterNames = await getClusterNames()
|
||||||
const stopQuery = () => {
|
await updateTopics()
|
||||||
queryRunning = false
|
|
||||||
killQuery(querySettings)
|
// Reload with confirmed truthful values
|
||||||
}
|
// Will only flicker if topic or cluster names have changed
|
||||||
|
loadSave({ checked: true })
|
||||||
|
})
|
||||||
</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,28 +164,23 @@ 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' : ''}
|
||||||
Expand All Objects
|
on:click={() => jsonDisplay.expandAll = !jsonDisplay.expandAll}>
|
||||||
|
Expand All Objects
|
||||||
</button>
|
</button>
|
||||||
<button class={showMetadata ? 'selected' : ''} on:click={() => showMetadata = !showMetadata}>
|
<button class={jsonDisplay.showMetadata ? 'selected' : ''}
|
||||||
Show Metadata
|
on:click={() => jsonDisplay.showMetadata = !jsonDisplay.showMetadata}>
|
||||||
|
Show Metadata
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center">
|
<div style="display: flex; align-items: center">
|
||||||
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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' }))
|
||||||
|
|
Loading…
Reference in New Issue