Several visual and functional improvements.

Better error handling with setStateError().
Variable cancel button text, depending on query type.
Fix query button start/stop transitions.
Add loading/complete indicators to query list.
Use labels instead of plain divs in some spots.
Better mock data.
Add queryStartTime to state (at this time, unused).
This commit is contained in:
Sage Vaillancourt 2022-09-06 09:31:54 -04:00
parent 91cca57992
commit e50c76664d
8 changed files with 132 additions and 51 deletions

View File

@ -70,6 +70,25 @@ button.colored {
border-color: var(--accent-color); border-color: var(--accent-color);
} }
.lds-dual-ring {
display: inline-block;
}
.lds-dual-ring:after {
content: " ";
display: inline-block;
width: 6px;
height: 6px;
margin: 0 0 0 8px;
border-radius: 50%;
border: 2px solid var(--accent-color);
border-color: var(--accent-color) transparent var(--accent-color) transparent;
animation: spin 1.2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
button.colored:hover { button.colored:hover {
background-color: white; background-color: white;
font-weight: bold; font-weight: bold;

View File

@ -7,19 +7,20 @@
</script> </script>
<div class="query-input-header"> <div class="query-input-header">
<div on:click={() => jsFilter = !jsFilter}> <label>
Use JavaScript to filter messages Use JavaScript to filter messages
<input type=checkbox bind:checked={jsFilter}> <input type=checkbox bind:checked={jsFilter}>
</div> </label>
</div> </div>
<div class={"query-input-display" + (big ? ' big' : '')}> <div class={"query-input-display" + (big ? ' big' : '')}>
{#if jsFilter} {#if jsFilter}
<div class="query-input-hideable" transition:slide|local> <div class="query-input-hideable" transition:slide|local>
<div title="If enabled, mutations made by the below code will be displayed in the result data."> <label title="If enabled, mutations made by the below code will be displayed in the result data."
Allow JavaScript to mutate objects on:click={() => mutableObjects = !mutableObjects} >
Allow mutation of message data
<input type=checkbox bind:checked={mutableObjects}> <input type=checkbox bind:checked={mutableObjects}>
</div> </label>
<textarea class='query-input' bind:value={queryCode}></textarea> <textarea class='query-input' bind:value={queryCode}></textarea>
</div> </div>
{/if} {/if}
@ -42,7 +43,7 @@
margin-top: 1em; margin-top: 1em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
/* For some reason, using flex-grow: 1; broke the slide in transition */ /* For some reason, using flex-grow: 1; broke the slide-in transition */
height: 100%; height: 100%;
} }

View File

@ -6,6 +6,12 @@ export const queryMode = {
KILL: 'kill' KILL: 'kill'
} }
export const queryState = {
LOADING: 'loading',
DONE: 'done',
IDLE: 'idle',
}
export const backendAddressAndPort = 'localhost:3000' export const backendAddressAndPort = 'localhost:3000'
export const backendUrl = `http://${backendAddressAndPort}` export const backendUrl = `http://${backendAddressAndPort}`

View File

@ -8,8 +8,13 @@
</svg> </svg>
<ul> <ul>
<li class:active={$page.url.pathname === '/'}> <li class:active={$page.url.pathname === '/'}>
<a sveltekit:prefetch href="/">Message Search</a> <a sveltekit:prefetch href="/">Topic Search</a>
</li> </li>
<!--
<li class:active={$page.url.pathname === '/post-message'}>
<a sveltekit:prefetch href="/post-message">Post Message</a>
</li>
-->
<li class:active={$page.url.pathname === '/settings'}> <li class:active={$page.url.pathname === '/settings'}>
<a sveltekit:prefetch href="/settings">Settings</a> <a sveltekit:prefetch href="/settings">Settings</a>
</li> </li>

View File

@ -1,7 +1,7 @@
// noinspection JSCheckFunctionSignatures // noinspection JSCheckFunctionSignatures
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import { backendAddressAndPort, queryMode } from './constants.js' import { backendAddressAndPort, queryMode, queryState } from './constants.js'
const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)] const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)]
@ -16,21 +16,23 @@ const addMetadata = mockItem => ({
key: null, key: null,
}) })
const mockItems = [ const mockItems = () => [
{ {
"eventType": "Television", eventType: "purchase",
"broughtToYouBy": "AMC", userName: "sagev",
"title": "Breaking Bad" itemName: "Bean Enchilada",
quantity: getRandomFromArray([1, 2, 3])
}, },
{ {
"eventType": "Television", eventType: "purchase",
"broughtToYouBy": "CBS", userName: "cameronl",
"title": "Breaking Bad Bang Theory" itemName: "Tacos",
quantity: getRandomFromArray([1, 2, 3])
}, },
{ {
"eventType": "Movie", eventType: "refund",
"broughtToYouBy": "20th Century Fox", userName: "sagev",
"title": "Star Wars 2: The Star Warsening, brought to you by the Big Stink Corporation, a division of PepsiCo and somehow also Disney." purchaseId: crypto.randomUUID(),
}, },
].map(addMetadata) ].map(addMetadata)
@ -40,8 +42,11 @@ export const state = writable({
items: [], items: [],
itemCount: undefined, itemCount: undefined,
matchCount: undefined, matchCount: undefined,
queryState: queryState.IDLE,
error: 'Connecting to WebSocket...', error: 'Connecting to WebSocket...',
errorDetails: undefined, errorDetails: undefined,
queryStartTime: undefined,
queryEndTime: undefined,
}) })
const updateClearError = updater => state.update(s => { const updateClearError = updater => state.update(s => {
@ -61,8 +66,12 @@ const testQuery = (mode) => {
runTestQuery = true runTestQuery = true
try { try {
if (mode === queryMode.REAL_TIME) { if (mode === queryMode.REAL_TIME) {
state.update(s => ({
...s,
queryStartTime: new Date()
}))
const addItem = () => { const addItem = () => {
const item = getRandomFromArray(mockItems) const item = getRandomFromArray(mockItems())
if (filterFunc(item, item.value, JSON.stringify(item))) { if (filterFunc(item, item.value, JSON.stringify(item))) {
item.timestamp = new Date().getTime() item.timestamp = new Date().getTime()
state.update(s => ({ state.update(s => ({
@ -79,9 +88,16 @@ const testQuery = (mode) => {
} else { } else {
state.update(s => ({ state.update(s => ({
...s, ...s,
items: mockItems.filter(item => filterFunc(item, item.value, JSON.stringify(item))),
itemCount: (s.itemCount || 0) + 1
})) }))
setTimeout(() => {
const items = mockItems().filter(item => filterFunc(item, item.value, JSON.stringify(item)))
state.update(s => ({
...s,
items,
itemCount: items.length,
queryState: queryState.DONE
}))
}, testTimeout * 10)
} }
} catch (e) { } catch (e) {
console.log('Caught an error:', e.toString()) console.log('Caught an error:', e.toString())
@ -90,9 +106,12 @@ const testQuery = (mode) => {
} }
export const killQuery = async ({ }) => { export const killQuery = async ({ }) => {
// TODO
if (testMode) { if (testMode) {
runTestQuery = false runTestQuery = false
updateClearError(s => ({
...s,
queryState: queryState.DONE
}))
return return
} }
ws.send(JSON.stringify({ ws.send(JSON.stringify({
@ -109,7 +128,13 @@ export const query = async ({ cluster, topic, mode, jsFilter, queryCode, maxItem
} }
itemLimit = maxItems itemLimit = maxItems
updateClearError(s => ({ ...s, items: [], itemCount: 0 })) updateClearError(s => ({
...s,
items: [],
itemCount: 0,
queryState: queryState.LOADING,
queryStartTime: new Date()
}))
if (testMode) { if (testMode) {
testQuery(mode) testQuery(mode)
return return
@ -171,7 +196,8 @@ export const connect = () => {
case 'complete': case 'complete':
updateClearError(s => ({ updateClearError(s => ({
...s, ...s,
items: data.message items: data.message,
queryState: queryState.DONE
})) }))
break; break;
case 'item_count': case 'item_count':

View File

@ -1,4 +1,4 @@
import { apiFetch } from "../utils.js"; import { apiFetch, setStateError } from "../utils.js";
import { killQuery, query } from "../lib/state.js"; import { killQuery, query } from "../lib/state.js";
export const getTopics = async cluster => export const getTopics = async cluster =>
@ -48,12 +48,12 @@ export const getClusterNames = async () => Object.keys((await apiFetch('/cluster
export const startQuery = querySettings => { export const startQuery = querySettings => {
localStorage.setItem('querySettings', JSON.stringify(querySettings)) localStorage.setItem('querySettings', JSON.stringify(querySettings))
query(querySettings) query(querySettings).catch(setStateError)
return true return querySettings.mode
} }
export const stopQuery = querySettings => { export const stopQuery = querySettings => {
killQuery(querySettings) killQuery(querySettings).catch(setStateError)
return false return false
} }
export const defaultQueryCode = export const defaultQueryCode =

View File

@ -1,10 +1,10 @@
<script> <script>
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { slide, fly, scale } from 'svelte/transition' import { slide, fade } from 'svelte/transition'
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 } from '$lib/state' import { state, connect } from '$lib/state'
import { queryMode } from '../lib/constants.js' import { queryMode, queryState } from '../lib/constants.js'
import Modal from '../lib/Modal.svelte'; import Modal from '../lib/Modal.svelte';
import { import {
defaultQueryCode, defaultQueryCode,
@ -17,7 +17,7 @@
export let errors, data export let errors, data
let showQueryModal = false let showQueryModal = false
let queryRunning = false let queryRunning = ''
const fontSizes = [30, 50, 67, 80, 90, 100, 110, 120, 133, 150] const fontSizes = [30, 50, 67, 80, 90, 100, 110, 120, 133, 150]
let jsonDisplay = { let jsonDisplay = {
@ -44,9 +44,9 @@
let topics = [] let topics = []
let clusterNames = [] let clusterNames = []
const updateTopics = async (updateTopic = true) => { const updateTopics = async () => {
topics = await getTopics(querySettings.cluster) topics = await getTopics(querySettings.cluster)
if (updateTopic) { if (!querySettings.topic || !topics.includes(querySettings.topic)) {
querySettings.topic = topics[0] querySettings.topic = topics[0]
} }
} }
@ -73,6 +73,9 @@
connect() connect()
clusterNames = await getClusterNames() clusterNames = await getClusterNames()
if (!clusterNames.includes(querySettings.cluster)) {
querySettings.cluster = clusterNames[0]
}
await updateTopics() await updateTopics()
// Reload with confirmed truthful values // Reload with confirmed truthful values
@ -93,12 +96,16 @@
<NavBar /> <NavBar />
--> -->
<h1>Topic Search</h1> <h1>Topic Search</h1>
<div class="state-error">
{#if $state.error} {#if $state.error}
<div class="state-error">{$state.error}</div> <span transition:fade={{duration: 100}}>{$state.error}</span>
{#if $state.errorDetails} {#if $state.errorDetails}
<div class="state-error-details">{$state.errorDetails}</div> <div class="state-error-details">{$state.errorDetails}</div>
{/if} {/if}
{:else}
&nbsp;
{/if} {/if}
</div>
<div class="settings-option"> <div class="settings-option">
<h3>Cluster</h3> <h3>Cluster</h3>
@ -132,7 +139,7 @@
bind:mutableObjects={querySettings.mutableObjects} bind:mutableObjects={querySettings.mutableObjects}
bind:queryCode={querySettings.queryCode} /> bind:queryCode={querySettings.queryCode} />
{#if querySettings.jsFilter} {#if querySettings.jsFilter}
<button class="query-input-button" on:click={() => showQueryModal = true}>Open Large Editor</button> <button class="query-input-button" on:click={() => showQueryModal = true}>Open Editor Window</button>
{/if} {/if}
{#if showQueryModal} {#if showQueryModal}
<Modal onClickOut={() => showQueryModal = false} <Modal onClickOut={() => showQueryModal = false}
@ -174,18 +181,22 @@
--> -->
<div class="query-button"> <div class="query-button">
{#if queryRunning} {#if queryRunning && $state.queryState !== queryState.DONE}
<button transition:slide class="danger" on:click={() => queryRunning = stopQuery(querySettings)}>Stop Query</button> <div transition:slide|local class="stop-button">
<button class="danger" on:click={() => queryRunning = stopQuery(querySettings)}>
{queryRunning === queryMode.REAL_TIME ? 'End Real Time' : 'Cancel One-Shot'} Query
</button>
</div>
{:else} {:else}
<button transition:scale class="colored" <div transition:slide|local class="start-buttons colored">
on:click={() => queryRunning = startQuery({...querySettings, mode: queryMode.ONE_SHOT})}> <button class="colored" on:click={() => queryRunning = startQuery({...querySettings, mode: queryMode.ONE_SHOT})}>
Start One-Shot Start One-Shot
</button> </button>
<div class="separator"></div> <div class="separator"></div>
<button transition:slide class="colored" <button class="colored" on:click={() => queryRunning = startQuery({...querySettings, mode: queryMode.REAL_TIME})}>
on:click={() => queryRunning = startQuery({...querySettings, mode: queryMode.REAL_TIME})}>
Start Real-Time Start Real-Time
</button> </button>
</div>
{/if} {/if}
</div> </div>
</div> </div>
@ -206,6 +217,12 @@
{#if $state.itemCount >= 0} {#if $state.itemCount >= 0}
<div class="data-view-results"> <div class="data-view-results">
{$state.itemCount} Messages {$state.itemCount} Messages
<!-- TODO: Error indicator? -->
{#if $state.queryState === queryState.DONE}
<span style="color: green; font-weight: bold;"></span>
{:else if $state.queryState === queryState.LOADING}
<span class="lds-dual-ring"></span>
{/if}
</div> </div>
{/if} {/if}
<div class="border-between zoom-buttons"> <div class="border-between zoom-buttons">
@ -329,6 +346,11 @@
border-style: none; border-style: none;
/* New button CSS */ /* New button CSS */
display: flex; display: flex;
flex-direction: column;
}
.start-buttons, .stop-button {
display: flex;
flex-grow: 1;
} }
.separator { .separator {

View File

@ -76,3 +76,5 @@ export const apiFetch = async (path, options = undefined) => {
return undefined return undefined
} }
} }
export const setStateError = e => state.update(s => ({ ...s, error: e.toString() }))