Compare commits

...

10 Commits

Author SHA1 Message Date
Sage Vaillancourt 2859f57681 Better hamburger menu. 2022-09-09 07:51:40 -04:00
Sage Vaillancourt c9ea301ac1 Use compiled-in environment variables. 2022-09-09 07:40:43 -04:00
Sage Vaillancourt 6b0d49cb02 Quick testMode log. 2022-09-09 07:35:49 -04:00
Sage Vaillancourt e09d9fc0a6 Read testMode from KAFKA_FRONTEND_TEST_MODE env var. 2022-09-09 07:31:26 -04:00
Sage Vaillancourt 233afb75eb More work on the mobile interface.
Begin toying with a hamburger menu.
Remove many redundant CSS definitions.
2022-09-08 23:30:38 -04:00
Sage Vaillancourt 0ee4b3df40 Fix TestCluster1 topic list. 2022-09-06 23:17:42 -04:00
Sage Vaillancourt ddc4576f94 Better mockItems. 2022-09-06 21:01:40 -04:00
Sage Vaillancourt 81e18d4e60 Collapsible query settings and better mobile support.
Also use a larger list of test clusters, for testing.
2022-09-06 16:48:25 -04:00
Sage Vaillancourt 4e75739b1d Darker headings.
Display JSON example when in IDLE queryState.
2022-09-06 11:14:46 -04:00
Sage Vaillancourt e50c76664d 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).
2022-09-06 09:31:54 -04:00
10 changed files with 382 additions and 295 deletions

View File

@ -11,7 +11,7 @@
--tertiary-color: #edf0f8; --tertiary-color: #edf0f8;
--accent-color: #de09fa; --accent-color: #de09fa;
/*--accent-color: #ff0df8;*/ /*--accent-color: #ff0df8;*/
--heading-color: rgba(0, 0, 0, 0.8); --heading-color: rgba(0, 0, 0, 0.9);
--text-color: #444444; --text-color: #444444;
--background-without-opacity: rgba(255, 255, 255, 0.7); --background-without-opacity: rgba(255, 255, 255, 0.7);
--column-width: 42rem; --column-width: 42rem;
@ -38,6 +38,11 @@ select, input {
padding: 0.3em; padding: 0.3em;
background-color: white; background-color: white;
border-radius: 4px; border-radius: 4px;
max-width: 90%;
}
main {
height: calc(100% - 4em);
} }
select option { select option {
@ -70,6 +75,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;
@ -211,13 +235,12 @@ button {
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
border-color: #95c1d3; border-color: #95c1d3;
flex-grow: 2; flex-grow: 1;
padding: 1em; padding: 1em;
background-color: #e2eaff; background-color: #e2eaff;
height: 84vh;
max-width: 65vw;
min-width: 65vw; min-width: 65vw;
overflow-y: scroll; overflow-y: scroll;
margin-bottom: 1em;
} }
.settings-option { .settings-option {
@ -235,8 +258,63 @@ button:focus:not(:focus-visible) {
outline: none; outline: none;
} }
.query-settings-hider {
color: rgba(0,0,0, 0.5)
}
.query-settings-shower {
color: rgba(0,0,0, 0.5)
}
.query-settings-hider:before {
content: "«";
}
.query-settings-shower:before {
content: "»";
}
section {
display: flex;
flex-direction: row;
flex: 1;
justify-content: space-between;
overflow: hidden;
}
.query-settings {
margin-right: 1rem;
display: flex;
flex-direction: column;
padding: 1em;
}
@media (min-width: 720px) { @media (min-width: 720px) {
h1 { h1 {
font-size: 2.4rem; font-size: 2.4rem;
} }
.query-settings {
overflow: scroll;
}
.mobile {
display: none !important;
}
}
@media (max-width: 719px) {
.desktop {
display: none !important;
}
section {
flex-direction: column;
overflow: scroll;
}
.query-settings {
flex-grow: 1;
overflow: visible;
}
.query-settings-hider:before {
content: '▲';
}
.query-settings-shower:before {
content: '▼';
}
} }

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

@ -13,7 +13,7 @@
<NavBar /> <NavBar />
<div class="corner"> <div class="desktop corner">
<!-- <!--
{#if testMode} {#if testMode}
<div class="error-display">TEST MODE ENABLED</div> <div class="error-display">TEST MODE ENABLED</div>
@ -59,66 +59,6 @@
object-fit: contain; object-fit: contain;
} }
nav {
display: flex;
justify-content: center;
--background: rgba(255, 255, 255, 0.7);
}
svg {
width: 2em;
height: 3em;
display: block;
}
path {
fill: var(--background);
}
ul {
position: relative;
padding: 0;
margin: 0;
height: 3em;
display: flex;
justify-content: center;
align-items: center;
list-style: none;
background: var(--background);
background-size: contain;
}
li {
position: relative;
height: 100%;
}
li.active::before {
--size: 6px;
content: '';
width: 0;
height: 0;
position: absolute;
top: 0;
left: calc(50% - var(--size));
border: var(--size) solid transparent;
border-top: var(--size) solid var(--accent-color);
}
nav a {
display: flex;
height: 100%;
align-items: center;
padding: 0 1em;
color: var(--heading-color);
font-weight: 700;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.1em;
text-decoration: none;
transition: color 0.2s linear;
}
a:hover { a:hover {
color: var(--accent-color); color: var(--accent-color);
} }

View File

@ -1,15 +1,23 @@
<script> <script>
import { page } from '$app/stores'; import { page } from '$app/stores';
import { fade } from 'svelte/transition'
let dropdownVisible = false
</script> </script>
<nav> <nav class="desktop">
<svg viewBox="0 0 2 3" aria-hidden="true"> <svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" /> <path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
</svg> </svg>
<ul> <ul class="desktop">
<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>
@ -19,44 +27,38 @@
</svg> </svg>
</nav> </nav>
<nav class="mobile">
<button style="font-size: 3rem;" on:click={() => dropdownVisible = !dropdownVisible}>{dropdownVisible ? 'x' : '≡'}</button>
{#if dropdownVisible}
<!-- TODO: Click-away layer -->
<ul class="mobile" transition:fade={{duration: 300}}>
<li class:active={$page.url.pathname === '/'}>
<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>
</ul>
{/if}
</nav>
<style> <style>
header {
display: flex;
justify-content: space-between;
}
.corner {
font-weight: bold;
font-size: 18px;
color: black;
height: 3em;
}
.corner a {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.corner span {
margin-top: 0.45em;
margin-left: 0.5em;
}
.corner img {
margin-left: 0.5em;
height: 2em;
object-fit: contain;
}
nav { nav {
display: flex; display: flex;
justify-content: center; justify-content: center;
--background: rgba(255, 255, 255, 0.7); --background: rgba(255, 255, 255, 0.7);
} }
nav.mobile {
flex-direction: column;
align-items: end;
}
svg { svg {
width: 2em; width: 2em;
height: 3em; height: 3em;
@ -67,7 +69,7 @@
fill: var(--background); fill: var(--background);
} }
ul { ul.desktop {
position: relative; position: relative;
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -80,6 +82,20 @@
background-size: contain; background-size: contain;
} }
ul.mobile {
flex-direction: column;
position: absolute;
padding: 256px 0 1em;
margin: 0;
width: 50vw;
display: flex;
justify-content: center;
align-items: center;
list-style: none;
background-color: rgba(255, 255, 255);
background-size: contain;
}
li { li {
position: relative; position: relative;
height: 100%; height: 100%;
@ -111,8 +127,31 @@
transition: color 0.2s linear; transition: color 0.2s linear;
} }
@media (max-width: 719px) {
nav a {
font-size: 1rem;
line-height: 200%;
}
li {
margin-top: 1em;
}
}
a:hover { a:hover {
color: var(--accent-color); color: var(--accent-color);
} }
button {
z-index: 1;
padding: 0;
height: 64px;
width: 64px;
font-size: 64px;
border-radius: 0;
border-style: none;
}
button:hover {
background-color: white;
}
</style> </style>

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,32 +16,46 @@ const addMetadata = mockItem => ({
key: null, key: null,
}) })
const mockItems = [ const mockUsernames = [
{ 'sagev', 'cameronl', 'westonm', 'connorl', 'cameronf', 'thomase'
"eventType": "Television", ]
"broughtToYouBy": "AMC",
"title": "Breaking Bad"
},
{
"eventType": "Television",
"broughtToYouBy": "CBS",
"title": "Breaking Bad Bang Theory"
},
{
"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."
},
].map(addMetadata)
export const testMode = true const mockEventTypes = [
'purchase', 'refund',
]
const mockItemTypes = [
'Taco', 'Enchilada', 'Soda', 'Chalupa', 'Tostada'
]
const buildMockItem = () => {
const purchasedBy = getRandomFromArray(mockUsernames)
let transactionHandledBy = getRandomFromArray(mockUsernames)
while (transactionHandledBy === purchasedBy) {
transactionHandledBy = getRandomFromArray(mockUsernames)
}
return addMetadata({
eventType: getRandomFromArray(mockEventTypes),
itemType: getRandomFromArray(mockItemTypes),
purchasedBy,
transactionHandledBy,
purchaseId: crypto.randomUUID()
});
}
const mockItems = Array(100).fill(buildMockItem, 0).map(mocker => mocker())
export const testMode = import.meta.env.VITE_KAFKA_FRONTEND_DEMO === 'true'
export const state = writable({ 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 +75,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 = buildMockItem()
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 => ({
@ -76,12 +94,16 @@ const testQuery = (mode) => {
} }
} }
setTimeout(addItem, testTimeout) setTimeout(addItem, testTimeout)
} else { } else { // ONE_SHOT
state.update(s => ({ setTimeout(() => {
...s, const filteredItems = mockItems.filter(item => filterFunc(item, item.value, JSON.stringify(item))).slice(0, itemLimit)
items: mockItems.filter(item => filterFunc(item, item.value, JSON.stringify(item))), state.update(s => ({
itemCount: (s.itemCount || 0) + 1 ...s,
})) items: filteredItems,
itemCount: filteredItems.length,
queryState: queryState.DONE
}))
}, 2000)
} }
} catch (e) { } catch (e) {
console.log('Caught an error:', e.toString()) console.log('Caught an error:', e.toString())
@ -90,9 +112,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 +134,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 +202,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,
@ -16,8 +16,9 @@
} from './+page.js'; } from './+page.js';
export let errors, data export let errors, data
let showQuerySettings = true
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 +45,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 +74,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
@ -87,112 +91,100 @@
</svelte:head> </svelte:head>
<section> <section>
<div class="query-settings"> {#if showQuerySettings}
<!-- <div class="query-settings">
<Header /> <!--
<NavBar /> <Header />
--> <NavBar />
<h1>Topic Search</h1> -->
{#if $state.error} <h1>Topic Search</h1>
<div class="state-error">{$state.error}</div> <div class="state-error">
{#if $state.errorDetails} {#if $state.error}
<div class="state-error-details">{$state.errorDetails}</div> <span transition:fade={{duration: 100}}>{$state.error}</span>
{/if} {#if $state.errorDetails}
{/if} <div class="state-error-details">{$state.errorDetails}</div>
{/if}
{:else}
&nbsp;
{/if}
</div>
<div class="settings-option"> <div class="settings-option">
<h3>Cluster</h3> <h3>Cluster</h3>
<select disabled={clusterNames.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 clusterNames as clusterName} {#each clusterNames as clusterName}
<option value={clusterName}>{clusterName}</option> <option value={clusterName}>{clusterName}</option>
{/each} {/each}
</select> </select>
</div> </div>
<div class="settings-option"> <div class="settings-option">
<h3>Topic</h3> <h3>Topic</h3>
<select disabled={topics.length === 0} bind:value={querySettings.topic} name="topic" id="topic"> <select disabled={topics.length === 0} bind:value={querySettings.topic} name="topic" id="topic">
{#each topics as topic} {#each topics as topic}
<option value={topic}>{topic}</option> <option value={topic}>{topic}</option>
{/each} {/each}
</select> </select>
</div> </div>
<div class="settings-option"> <div class="settings-option">
<label> <label>
Maximum Results Maximum Results
<input type=number bind:value={querySettings.maxItems} min=0 max=10000> <input type=number bind:value={querySettings.maxItems} min=0 max=10000>
</label>
</div>
<br/>
<div class="settings-option">
<QueryInput
bind:jsFilter={querySettings.jsFilter}
bind:mutableObjects={querySettings.mutableObjects}
bind:queryCode={querySettings.queryCode} />
{#if querySettings.jsFilter}
<button class="query-input-button" on:click={() => showQueryModal = true}>Open Large Editor</button>
{/if}
{#if showQueryModal}
<Modal onClickOut={() => showQueryModal = false}
onCancel={null}
onConfirm={() => showQueryModal = false}
confirm="Close"
title={'JS Filter'}>
<QueryInput big
bind:jsFilter={querySettings.jsFilter}
bind:mutableObjects={querySettings.mutableObjects}
bind:queryCode={querySettings.queryCode} />
</Modal>
{/if}
</div>
<!-- Previous 'Start' style
<div class="settings-option query-type">
<span>Query Type:</span>
<span>
<label title="Consume and display new events as they are produced.">
<input type=radio bind:group={querySettings.mode} name="mode" value={queryMode.REAL_TIME}>
Real Time
</label> </label>
</div>
<br/>
<label title="Query all messages that currently exist, then stop."> <div class="settings-option">
<input type=radio bind:group={querySettings.mode} name="mode" value={queryMode.ONE_SHOT}> <QueryInput
One Shot bind:jsFilter={querySettings.jsFilter}
</label> bind:mutableObjects={querySettings.mutableObjects}
</span> bind:queryCode={querySettings.queryCode} />
</div> {#if querySettings.jsFilter}
<button class="query-input-button" on:click={() => showQueryModal = true}>Open Editor Window</button>
{/if}
{#if showQueryModal}
<Modal onClickOut={() => showQueryModal = false}
onCancel={null}
onConfirm={() => showQueryModal = false}
confirm="Close"
title={'JS Filter'}>
<QueryInput big
bind:jsFilter={querySettings.jsFilter}
bind:mutableObjects={querySettings.mutableObjects}
bind:queryCode={querySettings.queryCode} />
</Modal>
{/if}
</div>
<div class="query-button"> <div class="query-button">
{#if queryRunning} {#if queryRunning && $state.queryState !== queryState.DONE}
<button class="danger" on:click={() => queryRunning = stopQuery(querySettings)}>Stop Query</button> <div transition:slide|local class="stop-button">
{:else} <button class="danger" on:click={() => queryRunning = stopQuery(querySettings)}>
<button class="colored" on:click={() => queryRunning = startQuery(querySettings)}>Start Query</button> {queryRunning === queryMode.REAL_TIME ? 'End Real Time' : 'Cancel One-Shot'} Query
{/if} </button>
</div>
{:else}
<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> </div>
--> <hr>
{/if}
<div class="query-button">
{#if queryRunning}
<button transition:slide class="danger" on:click={() => queryRunning = stopQuery(querySettings)}>Stop Query</button>
{: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>
{/if}
</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={showQuerySettings ? "query-settings-hider" : "query-settings-shower"}
on:click={() => showQuerySettings = !showQuerySettings}></button>
<button class={jsonDisplay.expandAll ? 'selected' : ''} <button class={jsonDisplay.expandAll ? 'selected' : ''}
on:click={() => jsonDisplay.expandAll = !jsonDisplay.expandAll}> on:click={() => jsonDisplay.expandAll = !jsonDisplay.expandAll}>
Expand All Objects Expand All Objects
@ -206,6 +198,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">
@ -214,26 +212,39 @@
</div> </div>
</div> </div>
</div> </div>
{#if $state.items?.length > 0} {#if $state.queryState !== queryState.IDLE}
<div class="data-view" style={`font-size: ${fontSizes[jsonDisplay.fontSize]}%`}> <div class="data-view" style={`font-size: ${fontSizes[jsonDisplay.fontSize]}%`}>
<JsonView json={jsonDisplay.showMetadata ? $state.items : $state.items.map(item => item.value)} <JsonView json={jsonDisplay.showMetadata ? $state.items : $state.items.map(item => item.value)}
depth={jsonDisplay.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" style={`font-size: ${fontSizes[jsonDisplay.fontSize]}%`}>
<h2>No query data</h2> <JsonView json={{value: {"Hey": "There's no data here yet!", "When you start a query": "your results will appear here!"}}}
depth={jsonDisplay.expandAll ? Infinity : -1}/>
</div> </div>
{/if} {/if}
</div> </div>
</section> </section>
<style> <style>
hr {
width: 60%;
border-style: solid;
margin-bottom: 1.5em;
border-color: rgba(0, 0, 0, 0.2);
}
@media (min-width: 720px) {
hr {
display: none;
}
}
.data-view { .data-view {
border-top: none; border-top: none;
} }
.data-view-container { .data-view-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-grow: 1;
} }
.data-view-bar { .data-view-bar {
background-color: #d7dbf3; background-color: #d7dbf3;
@ -281,14 +292,6 @@
margin-right: 1em; margin-right: 1em;
} }
section {
display: flex;
flex-direction: row;
flex: 1;
justify-content: space-between;
overflow: hidden;
}
h1 { h1 {
width: 100%; width: 100%;
} }
@ -303,16 +306,6 @@
background-color: #575757; background-color: #575757;
} }
.query-settings {
margin-right: 1rem;
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: auto;
padding: 1em;
height: 84.5vh;
}
.query-type { .query-type {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -329,6 +322,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 {
@ -369,13 +367,6 @@
*/ */
.no-query-data { .no-query-data {
display: flex; opacity: 65%;
align-items: center;
justify-content: center;
}
.no-query-data h2 {
font-size: 32px;
color: rgba(0, 0, 0, 0.3);
} }
</style> </style>

View File

@ -159,6 +159,9 @@
</section> </section>
<style> <style>
.data-view {
min-height: 40%;
}
.cluster-listing { .cluster-listing {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -183,13 +186,6 @@
} }
section {
display: flex;
flex-direction: row;
flex: 1;
justify-content: space-between;
}
h1 { h1 {
width: 100%; width: 100%;
} }
@ -210,14 +206,6 @@
min-width: 20vw; min-width: 20vw;
} }
.query-settings {
margin-right: 1rem;
padding: 1em;
overflow: auto;
display: flex;
flex-direction: column;
}
.settings-sub-option { .settings-sub-option {
margin-top: 0.2em; margin-top: 0.2em;
display: flex; display: flex;

View File

@ -29,20 +29,30 @@ const jsonRequest = type => async (path, object) => fetch(backendUrl + path, {
export const postJson = (path, object) => jsonRequest('POST')(path, object) export const postJson = (path, object) => jsonRequest('POST')(path, object)
export const putJson = (path, object) => jsonRequest('PUT')(path, object) export const putJson = (path, object) => jsonRequest('PUT')(path, object)
const mockCluster = {
clientId: 'TestClient',
brokers: ['testbroker.com:5000'],
ssl: true,
sasl: {
mechanism: 'SCRAM-SHA-512',
username: 'testuser',
password: 'XXXXXXXXXXXXXXXXXXXXXX'
}
}
const mockApi = { const mockApi = {
'/clusters': { '/clusters': {
'TestCluster': { 'TestCluster1': mockCluster,
clientId: 'TestClient', 'TestCluster2': mockCluster,
brokers: ['testbroker.com:5000'], 'TestCluster3': mockCluster,
ssl: true, 'TestCluster4': mockCluster,
sasl: { 'TestCluster5': mockCluster,
mechanism: 'SCRAM-SHA-512', 'TestCluster6': mockCluster,
username: 'testuser', 'TestCluster7': mockCluster,
password: 'XXXXXXXXXXXXXXXXXXXXXX' 'TestCluster8': mockCluster,
} 'TestCluster9': mockCluster,
} 'TestCluster10': mockCluster,
}, },
'/topics/TestCluster': ['NewReleases'] '/topics/TestCluster1': ['Purchases'],
} }
/** /**
@ -75,4 +85,6 @@ export const apiFetch = async (path, options = undefined) => {
state.update(s => ({ ...s, error: 'Received non-JSON response from backend' })) state.update(s => ({ ...s, error: 'Received non-JSON response from backend' }))
return undefined return undefined
} }
} }
export const setStateError = e => state.update(s => ({ ...s, error: e.toString() }))