Some appearance cleanup.

A bit of refactoring, prepping for increased flexibility, and less
hard-coding.
This commit is contained in:
Sage Vaillancourt 2022-08-20 19:14:20 -04:00 committed by Sage Vaillancourt
parent d926a9cd36
commit 454f12003a
6 changed files with 221 additions and 88 deletions

View File

@ -1,7 +1,8 @@
@import '@fontsource/fira-mono';
@import url('https://fonts.googleapis.com/css2?family=Fira+Sans&family=Montserrat:wght@500&display=swap');
:root {
font-family: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
font-family: 'Fira Sans', Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--font-mono: 'Fira Mono', monospace;
--pure-white: #ffffff;
@ -9,13 +10,31 @@
--secondary-color: #d0dde9;
--tertiary-color: #edf0f8;
--accent-color: #ff0df8;
--heading-color: rgba(0, 0, 0, 0.7);
--heading-color: rgba(0, 0, 0, 0.8);
--text-color: #444444;
--background-without-opacity: rgba(255, 255, 255, 0.7);
--column-width: 42rem;
--column-margin-top: 4rem;
}
select, button {
font-family: 'Fira Sans', Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 90%;
border-style: solid;
border-radius: 0;
border-width: 1px;
border-color: #888888;
padding: 0.2em;
}
button {
padding: 0.2em 0.8em;
}
nav {
font-family: 'Liberation Sans', sans-serif;
}
body {
min-height: 100vh;
margin: 0;
@ -54,14 +73,21 @@ body::before {
flex-direction: column;
}
h1 {
font-weight: bold;
color: #111111;
}
h1,
h2,
p {
font-family: 'Montserrat', sans-serif;
font-weight: 400;
color: var(--heading-color);
}
h3 {
font-family: 'Montserrat', sans-serif;
margin-top: 0;
}
@ -108,6 +134,20 @@ button {
font-family: inherit;
}
.data-view {
display: flex;
flex-direction: column;
border-style: solid;
border-width: 1px;
border-color: #95c1d3;
flex-grow: 2;
padding: 1em;
background-color: #e2eaff;
height: 86vh;
max-width: 70vw;
overflow-y: scroll;
}
.settings-option {
padding: 1em;
margin-bottom: 1em;

View File

@ -1,4 +1,8 @@
export const queryMode = {
REAL_TIME: 'realTime',
ONE_SHOT: 'oneShot'
}
}
export const backendAddressAndPort = 'localhost:3000'
export const backendUrl = `http://${backendAddressAndPort}`

View File

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

View File

@ -1,7 +1,7 @@
// noinspection JSCheckFunctionSignatures
import { writable } from 'svelte/store'
import {queryMode} from "./constants.js";
import { backendAddressAndPort, queryMode } from "./constants.js";
const mockItems = [
{
@ -19,9 +19,9 @@ const mockItems = [
"broughtToYouBy": "20th Century Fox",
"title": "Star Wars 2"
},
].map(o => ({value: o, timestamp: new Date().getTime()}))
].map(o => ({ value: o, timestamp: new Date().getTime() }))
const testMode = true
const testMode = false
export const state = writable({
items: [],
itemCount: undefined,
@ -38,34 +38,39 @@ let disconnected = false
const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)]
const testQuery = (mode, jsFilter, queryCode) => {
try {
const f = new Function('message', 'value', queryCode)
if (mode === queryMode.REAL_TIME) {
const addItem = () => {
const item = getRandomFromArray(mockItems)
if (!jsFilter || f(item, item.value)) {
item.timestamp = new Date().getTime()
updateClearError(s => ({ ...s, items: [item, ...s.items].slice(0, itemLimit), itemCount: 0 }))
}
setTimeout(addItem, 2000)
}
setTimeout(addItem, 2000)
} else {
updateClearError(s => ({ ...s, items: mockItems.filter(item => !jsFilter || f(item, item.value)), itemCount: 0 }))
}
} catch (e) {
updateClearError(s => ({ ...s, error: e.toString() }))
}
}
// noinspection JSUnusedGlobalSymbols
export const query = async ({ cluster, topic, mode, jsFilter, queryCode, maxItems, mutableObjects }) => {
updateClearError(s => ({...s, items: [], itemCount: 0}))
updateClearError(s => ({ ...s, items: [], itemCount: 0 }))
if (testMode) {
testQuery(mode, jsFilter, queryCode)
return
}
if (disconnected) {
connect()
disconnected = false
}
if (testMode) {
try {
const f = new Function('message', 'value', queryCode)
if (mode === queryMode.REAL_TIME) {
const addItem = () => {
const item = getRandomFromArray(mockItems)
if (!jsFilter || f(item, item.value)) {
item.timestamp = new Date().getTime()
updateClearError(s => ({...s, items: [item, ...s.items].slice(0, itemLimit), itemCount: 0}))
}
setTimeout(addItem, 2000)
}
setTimeout(addItem, 2000)
} else {
updateClearError(s => ({...s, items: mockItems.filter(item => !jsFilter || f(item, item.value)), itemCount: 0}))
}
} catch (e) {
updateClearError(s => ({ ...s, error: e.toString() }))
}
} else {
updateClearError(s => ({ ...s, items: [], itemCount: 0 }))
}
updateClearError(s => ({ ...s, items: [], itemCount: 0 }))
itemLimit = maxItems
ws.send(JSON.stringify({
cluster,
@ -78,27 +83,26 @@ export const query = async ({ cluster, topic, mode, jsFilter, queryCode, maxItem
}
export const connect = () => {
ws = new WebSocket(`ws://localhost:3000`)
ws = new WebSocket(`ws://${backendAddressAndPort}`)
if (!ws) {
updateClearError(s => ({...s, error: 'Unable to connect to websocket.'}))
updateClearError(s => ({ ...s, error: 'Unable to connect to websocket.' }))
return
}
ws.addEventListener('close', () => { disconnected = true })
ws.addEventListener('close', () => {
disconnected = true
})
ws.addEventListener('open', () => {
console.log('WebSocket opened')
// ws.send(JSON.stringify({
// searchCode: 'return message.value?.UserId === "MINTERCI"'
// }))
})
ws.addEventListener('message', message => {
//console.log('WebSocket message received', message)
let data;
try {
data = JSON.parse(message.data)
switch (data.type) {
console.log('WebSocket message received', data)
switch (data?.type.toLowerCase()) {
case 'complete':
updateClearError(s => ({
...s,
@ -114,7 +118,7 @@ export const connect = () => {
case 'message':
updateClearError(s => ({
...s,
items: [data.message, ...s.items].slice(0, itemLimit)
items: console.log('new item', data.message) || [data.message, ...s.items].slice(0, itemLimit)
}))
break;
}
@ -126,4 +130,4 @@ export const connect = () => {
ws.addEventListener('close', () => {
console.log('WebSocket closed')
})
}
}

View File

@ -2,16 +2,16 @@
import { onMount } from 'svelte'
import { JsonView } from '@zerodevx/svelte-json-view'
import { state, connect, query } from '$lib/state';
import { queryMode } from "../lib/constants.js";
import { queryMode, backendUrl } from "../lib/constants.js";
let expandAll = false
let showMetadata = false
let expandAll = true
let showMetadata = true
const querySettings = {
topic: null,
cluster: null,
mode: queryMode.REAL_TIME,
jsFilter: true,
jsFilter: false,
maxItems: 20,
mutableObjects: false,
queryCode:
@ -21,21 +21,23 @@
return message.value.eventType === "Television";`
}
const makeRequest = async () => query(querySettings)
let topics = []
let clusters = []
const updateTopics = async () => {
topics = []
const response = await fetch(`http://localhost:3000/topics/${querySettings.cluster}`)
topics = await response.json()
topics.sort()
console.log('topics', topics)
querySettings.topic = topics[0]
try {
const response = await fetch(`${backendUrl}/topics/${querySettings.cluster}`)
topics = await response.json()
topics.sort()
console.log('topics', topics)
querySettings.topic = topics[0]
} catch (e) {
console.log('fetch error:', e.toString())
}
}
onMount(async () => {
connect();
const response = await fetch(`http://localhost:3000/clusters`)
const response = await fetch(`${backendUrl}/clusters`)
clusters = Object.keys(await response.json())
querySettings.cluster = clusters[0]
await updateTopics()
@ -86,7 +88,7 @@ return message.value.eventType === "Television";`
<br/>
<div class="settings-option">
<div class="query-input-header">Use JavaScript to filter messages <input type=checkbox bind:checked={querySettings.jsFilter}></div>
<div class="query-input-header"><div on:click={() => querySettings.jsFilter = !querySettings.jsFilter}>Use JavaScript to filter messages</div> <input type=checkbox bind:checked={querySettings.jsFilter}></div>
{#if querySettings.jsFilter}
<div class="query-input-display">
<div title="If enabled, mutations made by the below code will be displayed in the result data.">
@ -113,7 +115,7 @@ return message.value.eventType === "Television";`
</div>
<div class="query-button">
<button on:click={makeRequest}>Start Query</button>
<button on:click={() => query(querySettings)}>Start Query</button>
</div>
<br>
@ -127,12 +129,12 @@ return message.value.eventType === "Television";`
</div>
{#if $state.items?.length > 0}
<div class="json-view">
<JsonView json={showMetadata ? $state.items : $state.items.map(item => item.value)}
<div class="data-view">
<JsonView json={console.log('state.items', $state.items) || showMetadata ? $state.items : $state.items.map(item => item.value)}
depth={expandAll ? Infinity : 1}/>
</div>
{:else }
<div class="json-view no-query-data">
<div class="data-view no-query-data">
<h2>No query data</h2>
</div>
{/if}
@ -144,6 +146,7 @@ return message.value.eventType === "Television";`
flex-direction: row;
flex: 1;
justify-content: space-between;
overflow: hidden;
}
h1 {
@ -171,8 +174,16 @@ return message.value.eventType === "Television";`
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: auto;
padding: 1em;
height: 86vh;
}
.query-input-header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.query-input-display {
margin-top: 1em;
display: flex;
@ -180,6 +191,7 @@ return message.value.eventType === "Television";`
}
.query-input {
min-height: 4vw;
resize: vertical;
}
.query-type {
@ -220,15 +232,6 @@ return message.value.eventType === "Television";`
}
*/
.json-view {
border-radius: 4px;
flex-grow: 6;
padding: 1em;
background-color: #e2eaff;
height: 86vh;
overflow-y: scroll;
}
.no-query-data {
display: flex;
align-items: center;

View File

@ -1,7 +1,8 @@
<script>
import {onMount} from "svelte";
import { onMount } from "svelte";
import { backendUrl } from "../../lib/constants.js";
const jsonRequest = type => async (path, object) => fetch('http://localhost:3000' + path, {
const jsonRequest = type => async (path, object) => fetch(backendUrl + path, {
method: type,
body: JSON.stringify(object),
headers: {
@ -9,23 +10,48 @@
}
})
const findLast = array => {
if (!array || array.length === 0) {
return undefined
}
return array[array.length - 1]
}
const deepCopy = object => JSON.parse(JSON.stringify(object))
const post = jsonRequest('POST')
const put = jsonRequest('PUT')
const addCluster = async cluster => post('/clusters', cluster)
const updateCluster = async cluster => put('/clusters', cluster)
const addCluster = async cluster => fetchClusters(await post('/clusters', cluster))
const updateCluster = async cluster => fetchClusters(await put('/clusters', cluster))
const deleteCluster = async clusterName => fetchClusters(await post('/clusters/delete', { clusterName }))
let clusters = []
onMount(async () => {
const response = await fetch(`http://localhost:3000/clusters`)
const fetchClusters = async (response) => {
response ??= await fetch(`${backendUrl}/clusters`)
clusters = await response.json()
if (Object.keys(clusters).length > 0) {
config = Object.entries(clusters).map(([key, value]) => ({...value, clusterName: key}))[0]
}
})
clusters = Object.fromEntries(
Object.entries(clusters).map(([name, cluster]) =>
[name, ({ ...cluster, clusterName: name, originalName: name })]))
let config = {
clusterName: '',
const values = Object.values(clusters)
if (values.length > 0) {
config = deepCopy(values[0])
}
}
onMount(async () => {
await fetchClusters()
console.log('onMount', clusters)
})
console.log('instant', clusters)
const startNewCluster = () => {
config = emptyConfig()
}
const emptyConfig = () => ({
originalName: null, // A `null` originalName indicates that the config is new,
clusterName: '', // and will be POSTed as a new cluster, not PUTted, updating an existing one
clientId: '',
brokers: [''],
ssl: true,
@ -34,11 +60,12 @@
username: '',
password: ''
}
};
})
let config = emptyConfig()
</script>
<svelte:head>
<meta name="description" content="Kafka Dance Settings" />
<meta name="description" content="Kafka Dance Settings"/>
</svelte:head>
<section>
@ -58,7 +85,9 @@
{#each config.brokers as broker}
<input bind:value={broker}>
{/each}
<button on:click={() => {config.brokers = [...config.brokers, '']}}>+</button>
{#if findLast(config.brokers) !== ''}
<button on:click={() => {config.brokers = [...config.brokers, '']}}>+</button>
{/if}
</div>
<div class="settings-option">
@ -69,22 +98,63 @@
<div class="settings-option">
<div>SASL:</div>
<div>
Mechanism: <input bind:value={config.sasl.mechanism}>
<div class="settings-sub-option">
<div>Mechanism:</div>
<input bind:value={config.sasl.mechanism}>
</div>
<div>
Username: <input bind:value={config.sasl.username}>
<div class="settings-sub-option">
<div>Username:</div>
<input bind:value={config.sasl.username}>
</div>
<div>
Password: <input type="password" bind:value={config.sasl.password}>
<div class="settings-sub-option">
<div>Password:</div>
<input type="password" bind:value={config.sasl.password}>
</div>
</div>
<button style="float: right;" on:click={() => updateCluster(config)}>Save</button>
<button style="float: right;"
on:click={() => (config.originalName ? updateCluster : addCluster)(config)}>{config.originalName ? 'Update Cluster Config' : 'Add Cluster Config'}</button>
</div>
<div class="data-view">
<button style="margin: 1em;" on:click={startNewCluster}>Add New Cluster</button>
{#each Object.entries(clusters) as [clusterName, cluster]}
<div class={"cluster-listing" + (clusterName === config.clusterName ? " selected" : "")}>
<h2 class="cluster-title">{cluster.clusterName}</h2>
<div class="cluster-buttons">
<button on:click={() => { config = deepCopy(clusters[clusterName]); }}>View & Edit</button>
<button on:click={() => deleteCluster(clusterName)}>Delete</button>
</div>
</div>
{/each}
</div>
</section>
<style>
.cluster-listing {
display: flex;
flex-direction: row;
border-style: solid;
border-width: 1px;
padding: 1em;
margin: 1em;
justify-content: space-between;
align-items: center;
}
.selected {
background-color: rgba(0, 0, 0, 0.1);
}
.cluster-buttons button {
min-width: 5em;
margin-left: 1em;
}
.cluster-title {
}
section {
display: flex;
flex-direction: row;
@ -114,6 +184,18 @@
.query-settings {
margin-right: 1rem;
padding: 1em;
height: 86vh;
overflow: auto;
display: flex;
flex-direction: column;
}
.settings-sub-option {
margin-top: 0.2em;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.no-query-data h2 {