Some appearance cleanup.
A bit of refactoring, prepping for increased flexibility, and less hard-coding.
This commit is contained in:
parent
d926a9cd36
commit
454f12003a
44
src/app.css
44
src/app.css
|
@ -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;
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
export const queryMode = {
|
||||
REAL_TIME: 'realTime',
|
||||
ONE_SHOT: 'oneShot'
|
||||
}
|
||||
}
|
||||
|
||||
export const backendAddressAndPort = 'localhost:3000'
|
||||
|
||||
export const backendUrl = `http://${backendAddressAndPort}`
|
|
@ -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>
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue