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:
parent
91cca57992
commit
e50c76664d
19
src/app.css
19
src/app.css
|
@ -70,6 +70,25 @@ button.colored {
|
|||
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 {
|
||||
background-color: white;
|
||||
font-weight: bold;
|
||||
|
|
|
@ -7,19 +7,20 @@
|
|||
</script>
|
||||
|
||||
<div class="query-input-header">
|
||||
<div on:click={() => jsFilter = !jsFilter}>
|
||||
<label>
|
||||
Use JavaScript to filter messages
|
||||
<input type=checkbox bind:checked={jsFilter}>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class={"query-input-display" + (big ? ' big' : '')}>
|
||||
{#if jsFilter}
|
||||
<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.">
|
||||
Allow JavaScript to mutate objects
|
||||
<label title="If enabled, mutations made by the below code will be displayed in the result data."
|
||||
on:click={() => mutableObjects = !mutableObjects} >
|
||||
Allow mutation of message data
|
||||
<input type=checkbox bind:checked={mutableObjects}>
|
||||
</div>
|
||||
</label>
|
||||
<textarea class='query-input' bind:value={queryCode}></textarea>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -42,7 +43,7 @@
|
|||
margin-top: 1em;
|
||||
display: flex;
|
||||
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%;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,12 @@ export const queryMode = {
|
|||
KILL: 'kill'
|
||||
}
|
||||
|
||||
export const queryState = {
|
||||
LOADING: 'loading',
|
||||
DONE: 'done',
|
||||
IDLE: 'idle',
|
||||
}
|
||||
|
||||
export const backendAddressAndPort = 'localhost:3000'
|
||||
|
||||
export const backendUrl = `http://${backendAddressAndPort}`
|
||||
|
|
|
@ -8,8 +8,13 @@
|
|||
</svg>
|
||||
<ul>
|
||||
<li class:active={$page.url.pathname === '/'}>
|
||||
<a sveltekit:prefetch href="/">Message Search</a>
|
||||
<a sveltekit:prefetch href="/">Topic Search</a>
|
||||
</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'}>
|
||||
<a sveltekit:prefetch href="/settings">Settings</a>
|
||||
</li>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// noinspection JSCheckFunctionSignatures
|
||||
|
||||
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)]
|
||||
|
||||
|
@ -16,21 +16,23 @@ const addMetadata = mockItem => ({
|
|||
key: null,
|
||||
})
|
||||
|
||||
const mockItems = [
|
||||
const mockItems = () => [
|
||||
{
|
||||
"eventType": "Television",
|
||||
"broughtToYouBy": "AMC",
|
||||
"title": "Breaking Bad"
|
||||
eventType: "purchase",
|
||||
userName: "sagev",
|
||||
itemName: "Bean Enchilada",
|
||||
quantity: getRandomFromArray([1, 2, 3])
|
||||
},
|
||||
{
|
||||
"eventType": "Television",
|
||||
"broughtToYouBy": "CBS",
|
||||
"title": "Breaking Bad Bang Theory"
|
||||
eventType: "purchase",
|
||||
userName: "cameronl",
|
||||
itemName: "Tacos",
|
||||
quantity: getRandomFromArray([1, 2, 3])
|
||||
},
|
||||
{
|
||||
"eventType": "Movie",
|
||||
"broughtToYouBy": "20th Century Fox",
|
||||
"title": "Star Wars 2: The Star Warsening, brought to you by the Big Stink Corporation, a division of PepsiCo and somehow also Disney."
|
||||
eventType: "refund",
|
||||
userName: "sagev",
|
||||
purchaseId: crypto.randomUUID(),
|
||||
},
|
||||
].map(addMetadata)
|
||||
|
||||
|
@ -40,8 +42,11 @@ export const state = writable({
|
|||
items: [],
|
||||
itemCount: undefined,
|
||||
matchCount: undefined,
|
||||
queryState: queryState.IDLE,
|
||||
error: 'Connecting to WebSocket...',
|
||||
errorDetails: undefined,
|
||||
queryStartTime: undefined,
|
||||
queryEndTime: undefined,
|
||||
})
|
||||
|
||||
const updateClearError = updater => state.update(s => {
|
||||
|
@ -61,8 +66,12 @@ const testQuery = (mode) => {
|
|||
runTestQuery = true
|
||||
try {
|
||||
if (mode === queryMode.REAL_TIME) {
|
||||
state.update(s => ({
|
||||
...s,
|
||||
queryStartTime: new Date()
|
||||
}))
|
||||
const addItem = () => {
|
||||
const item = getRandomFromArray(mockItems)
|
||||
const item = getRandomFromArray(mockItems())
|
||||
if (filterFunc(item, item.value, JSON.stringify(item))) {
|
||||
item.timestamp = new Date().getTime()
|
||||
state.update(s => ({
|
||||
|
@ -79,9 +88,16 @@ const testQuery = (mode) => {
|
|||
} else {
|
||||
state.update(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) {
|
||||
console.log('Caught an error:', e.toString())
|
||||
|
@ -90,9 +106,12 @@ const testQuery = (mode) => {
|
|||
}
|
||||
|
||||
export const killQuery = async ({ }) => {
|
||||
// TODO
|
||||
if (testMode) {
|
||||
runTestQuery = false
|
||||
updateClearError(s => ({
|
||||
...s,
|
||||
queryState: queryState.DONE
|
||||
}))
|
||||
return
|
||||
}
|
||||
ws.send(JSON.stringify({
|
||||
|
@ -109,7 +128,13 @@ export const query = async ({ cluster, topic, mode, jsFilter, queryCode, maxItem
|
|||
}
|
||||
itemLimit = maxItems
|
||||
|
||||
updateClearError(s => ({ ...s, items: [], itemCount: 0 }))
|
||||
updateClearError(s => ({
|
||||
...s,
|
||||
items: [],
|
||||
itemCount: 0,
|
||||
queryState: queryState.LOADING,
|
||||
queryStartTime: new Date()
|
||||
}))
|
||||
if (testMode) {
|
||||
testQuery(mode)
|
||||
return
|
||||
|
@ -171,7 +196,8 @@ export const connect = () => {
|
|||
case 'complete':
|
||||
updateClearError(s => ({
|
||||
...s,
|
||||
items: data.message
|
||||
items: data.message,
|
||||
queryState: queryState.DONE
|
||||
}))
|
||||
break;
|
||||
case 'item_count':
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { apiFetch } from "../utils.js";
|
||||
import { apiFetch, setStateError } from "../utils.js";
|
||||
import { killQuery, query } from "../lib/state.js";
|
||||
|
||||
export const getTopics = async cluster =>
|
||||
|
@ -48,12 +48,12 @@ export const getClusterNames = async () => Object.keys((await apiFetch('/cluster
|
|||
|
||||
export const startQuery = querySettings => {
|
||||
localStorage.setItem('querySettings', JSON.stringify(querySettings))
|
||||
query(querySettings)
|
||||
return true
|
||||
query(querySettings).catch(setStateError)
|
||||
return querySettings.mode
|
||||
}
|
||||
|
||||
export const stopQuery = querySettings => {
|
||||
killQuery(querySettings)
|
||||
killQuery(querySettings).catch(setStateError)
|
||||
return false
|
||||
}
|
||||
export const defaultQueryCode =
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
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 QueryInput from '$lib/QueryInput.svelte'
|
||||
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 {
|
||||
defaultQueryCode,
|
||||
|
@ -17,7 +17,7 @@
|
|||
export let errors, data
|
||||
|
||||
let showQueryModal = false
|
||||
let queryRunning = false
|
||||
let queryRunning = ''
|
||||
|
||||
const fontSizes = [30, 50, 67, 80, 90, 100, 110, 120, 133, 150]
|
||||
let jsonDisplay = {
|
||||
|
@ -44,9 +44,9 @@
|
|||
|
||||
let topics = []
|
||||
let clusterNames = []
|
||||
const updateTopics = async (updateTopic = true) => {
|
||||
const updateTopics = async () => {
|
||||
topics = await getTopics(querySettings.cluster)
|
||||
if (updateTopic) {
|
||||
if (!querySettings.topic || !topics.includes(querySettings.topic)) {
|
||||
querySettings.topic = topics[0]
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,9 @@
|
|||
|
||||
connect()
|
||||
clusterNames = await getClusterNames()
|
||||
if (!clusterNames.includes(querySettings.cluster)) {
|
||||
querySettings.cluster = clusterNames[0]
|
||||
}
|
||||
await updateTopics()
|
||||
|
||||
// Reload with confirmed truthful values
|
||||
|
@ -93,12 +96,16 @@
|
|||
<NavBar />
|
||||
-->
|
||||
<h1>Topic Search</h1>
|
||||
{#if $state.error}
|
||||
<div class="state-error">{$state.error}</div>
|
||||
{#if $state.errorDetails}
|
||||
<div class="state-error-details">{$state.errorDetails}</div>
|
||||
<div class="state-error">
|
||||
{#if $state.error}
|
||||
<span transition:fade={{duration: 100}}>{$state.error}</span>
|
||||
{#if $state.errorDetails}
|
||||
<div class="state-error-details">{$state.errorDetails}</div>
|
||||
{/if}
|
||||
{:else}
|
||||
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="settings-option">
|
||||
<h3>Cluster</h3>
|
||||
|
@ -132,7 +139,7 @@
|
|||
bind:mutableObjects={querySettings.mutableObjects}
|
||||
bind:queryCode={querySettings.queryCode} />
|
||||
{#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 showQueryModal}
|
||||
<Modal onClickOut={() => showQueryModal = false}
|
||||
|
@ -174,18 +181,22 @@
|
|||
-->
|
||||
|
||||
<div class="query-button">
|
||||
{#if queryRunning}
|
||||
<button transition:slide class="danger" on:click={() => queryRunning = stopQuery(querySettings)}>Stop Query</button>
|
||||
{#if queryRunning && $state.queryState !== queryState.DONE}
|
||||
<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}
|
||||
<button transition:scale class="colored"
|
||||
on:click={() => queryRunning = startQuery({...querySettings, mode: queryMode.ONE_SHOT})}>
|
||||
Start One-Shot
|
||||
</button>
|
||||
<div class="separator"></div>
|
||||
<button transition:slide class="colored"
|
||||
on:click={() => queryRunning = startQuery({...querySettings, mode: queryMode.REAL_TIME})}>
|
||||
Start Real-Time
|
||||
</button>
|
||||
<div transition:slide|local class="start-buttons colored">
|
||||
<button class="colored" on:click={() => queryRunning = startQuery({...querySettings, mode: queryMode.ONE_SHOT})}>
|
||||
Start One-Shot
|
||||
</button>
|
||||
<div class="separator"></div>
|
||||
<button class="colored" on:click={() => queryRunning = startQuery({...querySettings, mode: queryMode.REAL_TIME})}>
|
||||
Start Real-Time
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -206,6 +217,12 @@
|
|||
{#if $state.itemCount >= 0}
|
||||
<div class="data-view-results">
|
||||
{$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>
|
||||
{/if}
|
||||
<div class="border-between zoom-buttons">
|
||||
|
@ -329,6 +346,11 @@
|
|||
border-style: none;
|
||||
/* New button CSS */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.start-buttons, .stop-button {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.separator {
|
||||
|
|
|
@ -76,3 +76,5 @@ export const apiFetch = async (path, options = undefined) => {
|
|||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const setStateError = e => state.update(s => ({ ...s, error: e.toString() }))
|
||||
|
|
Loading…
Reference in New Issue