373 lines
9.1 KiB
Svelte
373 lines
9.1 KiB
Svelte
<script>
|
|
import { onMount } from 'svelte'
|
|
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, queryState } from '../lib/constants.js'
|
|
import Modal from '../lib/Modal.svelte';
|
|
import {
|
|
defaultQueryCode,
|
|
getClusterNames,
|
|
getTopics,
|
|
mapFromLocalStorage,
|
|
startQuery,
|
|
stopQuery
|
|
} from './+page.js';
|
|
export let errors, data
|
|
|
|
let showQuerySettings = true
|
|
let showQueryModal = false
|
|
let queryRunning = ''
|
|
|
|
const fontSizes = [30, 50, 67, 80, 90, 100, 110, 120, 133, 150]
|
|
let jsonDisplay = {
|
|
expandAll: true,
|
|
showMetadata: true,
|
|
fontSize: fontSizes.indexOf(100),
|
|
fontUp: () => setFontSize(Math.min(jsonDisplay.fontSize + 1, fontSizes.length - 1)),
|
|
fontDown: () => setFontSize(Math.max(jsonDisplay.fontSize - 1, 0))
|
|
}
|
|
const setFontSize = size => {
|
|
jsonDisplay.fontSize = size
|
|
localStorage.jsonDisplay = JSON.stringify(jsonDisplay)
|
|
}
|
|
|
|
let querySettings = {
|
|
topic: null,
|
|
cluster: null,
|
|
mode: queryMode.REAL_TIME,
|
|
jsFilter: false,
|
|
maxItems: 20,
|
|
mutableObjects: false,
|
|
queryCode: defaultQueryCode
|
|
}
|
|
|
|
let topics = []
|
|
let clusterNames = []
|
|
const updateTopics = async () => {
|
|
topics = await getTopics(querySettings.cluster)
|
|
if (!querySettings.topic || !topics.includes(querySettings.topic)) {
|
|
querySettings.topic = topics[0]
|
|
}
|
|
}
|
|
|
|
|
|
onMount(async () => {
|
|
const loadSave = ({ checked }) => {
|
|
if (localStorage) {
|
|
mapFromLocalStorage({
|
|
checked,
|
|
jsonDisplay,
|
|
querySettings,
|
|
clusterNames,
|
|
topics,
|
|
})
|
|
jsonDisplay = jsonDisplay
|
|
querySettings = querySettings
|
|
}
|
|
}
|
|
|
|
// Instantly load saved data as if cluster and topic names are accurate
|
|
// This should usually be the case
|
|
loadSave({ checked: false })
|
|
|
|
connect()
|
|
clusterNames = await getClusterNames()
|
|
if (!clusterNames.includes(querySettings.cluster)) {
|
|
querySettings.cluster = clusterNames[0]
|
|
}
|
|
await updateTopics()
|
|
|
|
// Reload with confirmed truthful values
|
|
// Will only flicker if topic or cluster names have changed
|
|
loadSave({ checked: true })
|
|
})
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>{querySettings.topic ? `${querySettings.topic} - ` : ''}Kafka Dance</title>
|
|
<meta name="description" content="Kafka Dance message search" />
|
|
</svelte:head>
|
|
|
|
<section>
|
|
{#if showQuerySettings}
|
|
<div class="query-settings">
|
|
<!--
|
|
<Header />
|
|
<NavBar />
|
|
-->
|
|
<h1>Topic Search</h1>
|
|
<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}
|
|
</div>
|
|
|
|
<div class="settings-option">
|
|
<h3>Cluster</h3>
|
|
<select disabled={clusterNames.length === 0} bind:value={querySettings.cluster} name="cluster" id="cluster" on:change={updateTopics}>
|
|
{#each clusterNames as clusterName}
|
|
<option value={clusterName}>{clusterName}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="settings-option">
|
|
<h3>Topic</h3>
|
|
<select disabled={topics.length === 0} bind:value={querySettings.topic} name="topic" id="topic">
|
|
{#each topics as topic}
|
|
<option value={topic}>{topic}</option>
|
|
{/each}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="settings-option">
|
|
<label>
|
|
Maximum Results
|
|
<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 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">
|
|
{#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}
|
|
<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>
|
|
<hr>
|
|
{/if}
|
|
|
|
<div class="data-view-container">
|
|
<div class="data-view-bar">
|
|
<div class="border-between">
|
|
<button class={showQuerySettings ? "query-settings-hider" : "query-settings-shower"}
|
|
on:click={() => showQuerySettings = !showQuerySettings}></button>
|
|
<button class={jsonDisplay.expandAll ? 'selected' : ''}
|
|
on:click={() => jsonDisplay.expandAll = !jsonDisplay.expandAll}>
|
|
Expand All Objects
|
|
</button>
|
|
<button class={jsonDisplay.showMetadata ? 'selected' : ''}
|
|
on:click={() => jsonDisplay.showMetadata = !jsonDisplay.showMetadata}>
|
|
Show Metadata
|
|
</button>
|
|
</div>
|
|
<div style="display: flex; align-items: center">
|
|
{#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">
|
|
<button on:click={jsonDisplay.fontDown} style="border-color: #aaa;">-</button>
|
|
<button on:click={jsonDisplay.fontUp}>+</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{#if $state.queryState !== queryState.IDLE}
|
|
<div class="data-view" style={`font-size: ${fontSizes[jsonDisplay.fontSize]}%`}>
|
|
<JsonView json={jsonDisplay.showMetadata ? $state.items : $state.items.map(item => item.value)}
|
|
depth={jsonDisplay.expandAll ? Infinity : 1}/>
|
|
</div>
|
|
{:else }
|
|
<div class="data-view no-query-data" style={`font-size: ${fontSizes[jsonDisplay.fontSize]}%`}>
|
|
<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>
|
|
{/if}
|
|
</div>
|
|
</section>
|
|
|
|
<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 {
|
|
border-top: none;
|
|
}
|
|
.data-view-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-grow: 1;
|
|
}
|
|
.data-view-bar {
|
|
background-color: #d7dbf3;
|
|
border-style: solid;
|
|
border-width: 1px;
|
|
border-color: #95c1d3;
|
|
border-bottom: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.border-between {
|
|
border-width: 1px;
|
|
border-style: none;
|
|
display: flex;
|
|
flex-grow: 1;
|
|
}
|
|
.border-between button:last-child {
|
|
border-right: none;
|
|
}
|
|
.zoom-buttons button {
|
|
width: 2.5em;
|
|
}
|
|
.data-view-bar button {
|
|
border-color: white;
|
|
border-style: none;
|
|
border-right: solid;
|
|
border-width: 1px;
|
|
border-radius: 0;
|
|
margin: 0;
|
|
}
|
|
button.selected {
|
|
background-color: #4732a5;
|
|
color: white;
|
|
}
|
|
button.selected:hover {
|
|
background-color: #53429d;
|
|
color: white;
|
|
}
|
|
.data-view-results {
|
|
color: #333;
|
|
font-size: 80%;
|
|
margin-right: 1em;
|
|
}
|
|
|
|
h1 {
|
|
width: 100%;
|
|
}
|
|
|
|
.query-input-button {
|
|
border-style: none;
|
|
border-radius: 0;
|
|
background-color: #313131;
|
|
color: #d0dde9;
|
|
}
|
|
.query-input-button:hover {
|
|
background-color: #575757;
|
|
}
|
|
|
|
.query-type {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.query-type label {
|
|
padding-left: 0.5em;
|
|
}
|
|
|
|
.query-button {
|
|
text-align: right;
|
|
border-radius: 0;
|
|
border-style: none;
|
|
/* New button CSS */
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.start-buttons, .stop-button {
|
|
display: flex;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.separator {
|
|
width: 1em;
|
|
}
|
|
.query-button button {
|
|
flex-grow: 1;
|
|
}
|
|
.query-button button:hover {
|
|
flex-grow: 2;
|
|
}
|
|
|
|
.state-error {
|
|
color: #ff2222;
|
|
font-weight: bold;
|
|
}
|
|
.state-error-details {
|
|
color: #ff2222;
|
|
font-weight: normal;
|
|
font-size: 80%;
|
|
}
|
|
|
|
/* JsonView config
|
|
:root {
|
|
--nodePaddingLeft: 1rem;
|
|
--nodeBorderLeft: 1px dotted #9ca3af;
|
|
--nodeColor: #6d81a4;
|
|
--bracketHoverBackground: #d1d5db;
|
|
--leafDefaultColor: #9ca3af;
|
|
--leafStringColor: #14cb8f;
|
|
--leafNumberColor: #d97706;
|
|
--leafBooleanColor: #2563eb;
|
|
}
|
|
|
|
.bracket {
|
|
color: #d0dde9;
|
|
}
|
|
*/
|
|
|
|
.no-query-data {
|
|
opacity: 65%;
|
|
}
|
|
</style>
|