Add button bar to data view.
Add confirmation modal. Better button styling Save some of querySettings.
This commit is contained in:
parent
d7fe000d8b
commit
bd0967df0e
62
src/app.css
62
src/app.css
|
@ -1,5 +1,5 @@
|
|||
@import '@fontsource/fira-mono';
|
||||
@import url('https://fonts.googleapis.com/css2?family=Fira+Sans&family=Montserrat:wght@500&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Fira+Sans&family=Montserrat:wght@500&family=Roboto&display=swap');
|
||||
|
||||
:root {
|
||||
font-family: 'Fira Sans', Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
|
@ -9,7 +9,8 @@
|
|||
--primary-color: #e7f4ff;
|
||||
--secondary-color: #d0dde9;
|
||||
--tertiary-color: #edf0f8;
|
||||
--accent-color: #ff0df8;
|
||||
--accent-color: #de09fa;
|
||||
/*--accent-color: #ff0df8;*/
|
||||
--heading-color: rgba(0, 0, 0, 0.8);
|
||||
--text-color: #444444;
|
||||
--background-without-opacity: rgba(255, 255, 255, 0.7);
|
||||
|
@ -18,7 +19,7 @@
|
|||
}
|
||||
|
||||
select, button {
|
||||
font-family: 'Fira Sans', Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-family: Roboto, Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: 90%;
|
||||
border-style: solid;
|
||||
border-radius: 0;
|
||||
|
@ -28,7 +29,51 @@ select, button {
|
|||
}
|
||||
|
||||
button {
|
||||
padding: 0.2em 0.8em;
|
||||
padding: 0.4em 1.0em;
|
||||
border-radius: 5px;
|
||||
border-style: solid;
|
||||
background-color: white;
|
||||
border-color: #999999;
|
||||
width: auto;;
|
||||
transition: 100ms linear;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
|
||||
button.colored {
|
||||
background-color: var(--accent-color);
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
button.colored:hover {
|
||||
background-color: white;
|
||||
font-weight: bold;
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.danger {
|
||||
background-color: #ee0000;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: #ee0000;
|
||||
}
|
||||
|
||||
button.danger:hover {
|
||||
background-color: black;
|
||||
border-color: black;
|
||||
}
|
||||
|
||||
nav {
|
||||
|
@ -125,16 +170,17 @@ pre {
|
|||
}
|
||||
|
||||
.bracket {
|
||||
color: #d0dde9;
|
||||
color: #a0adb9;
|
||||
}
|
||||
|
||||
/*
|
||||
input,
|
||||
button {
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
*/
|
||||
|
||||
.data-view {
|
||||
font-family: monospace;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-style: solid;
|
||||
|
@ -143,7 +189,7 @@ button {
|
|||
flex-grow: 2;
|
||||
padding: 1em;
|
||||
background-color: #e2eaff;
|
||||
height: 86vh;
|
||||
height: 84vh;
|
||||
max-width: 65vw;
|
||||
min-width: 65vw;
|
||||
overflow-y: scroll;
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
<script>
|
||||
export let level = ''
|
||||
export let title
|
||||
export let onCancel = () => {}
|
||||
export let onConfirm = () => {}
|
||||
export let onClickOut = () => {}
|
||||
</script>
|
||||
|
||||
<div class="modal" on:click|self={() => onClickOut && onClickOut()}>
|
||||
<div class="modal-content" on:click={() => {}}>
|
||||
{#if title}
|
||||
<h2 class="modal-title">{title}</h2>
|
||||
{/if}
|
||||
<div class="modal-details">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-button-row">
|
||||
<button on:click={onCancel}>Cancel</button>
|
||||
<button class={level} on:click={onConfirm}>Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* The Modal (background) */
|
||||
.modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 1; /* Sit on top */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw; /* Full width */
|
||||
height: 100vh; /* Full height */
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgb(0,0,0); /* Fallback color */
|
||||
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
|
||||
}
|
||||
|
||||
/* Modal Content/Box */
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 15% auto; /* 15% from the top and centered */
|
||||
padding: 2em;
|
||||
border: 1px solid #888;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 140%;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.modal-details {
|
||||
margin-bottom: 2em;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.modal-button-row {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* The Close Button */
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -14,10 +14,10 @@
|
|||
<NavBar />
|
||||
|
||||
<div class="corner">
|
||||
{#if testMode}
|
||||
<div class="error-display">TEST MODE ENABLED</div>
|
||||
{/if}
|
||||
<!--
|
||||
{#if testMode}
|
||||
<div class="error-display">TEST MODE ENABLED</div>
|
||||
{/if}
|
||||
{#if $state.error}
|
||||
<div class="error-display">{$state.error}</div>
|
||||
{:else}
|
||||
|
@ -38,7 +38,6 @@
|
|||
font-size: 18px;
|
||||
color: black;
|
||||
height: 3em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.corner a {
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
// noinspection JSCheckFunctionSignatures
|
||||
|
||||
import { writable } from 'svelte/store'
|
||||
import { backendAddressAndPort, queryMode } from "./constants.js";
|
||||
import { backendAddressAndPort, queryMode } from './constants.js'
|
||||
|
||||
const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)]
|
||||
|
||||
let mockOffset = 1;
|
||||
|
||||
const addMetadata = mockItem => ({
|
||||
value: mockItem,
|
||||
magicByte: getRandomFromArray([0, 1, 2]),
|
||||
attributes: 0,
|
||||
timestamp: new Date().getTime(),
|
||||
offset: mockOffset++,
|
||||
key: null,
|
||||
})
|
||||
|
||||
const mockItems = [
|
||||
{
|
||||
|
@ -19,41 +32,45 @@ const mockItems = [
|
|||
"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(o => ({ value: o, timestamp: new Date().getTime() }))
|
||||
].map(addMetadata)
|
||||
|
||||
export const testMode = true
|
||||
|
||||
export const state = writable({
|
||||
items: [],
|
||||
itemCount: undefined,
|
||||
error: 'Connecting to WebSocket...',
|
||||
})
|
||||
|
||||
const updateClearError = updater => state.update(s => {
|
||||
s.error = null
|
||||
return updater(s)
|
||||
})
|
||||
|
||||
let itemLimit = Infinity
|
||||
let ws;
|
||||
let itemLimit = Infinity
|
||||
let disconnected = false
|
||||
let filterFunc = (_message, _value) => true
|
||||
|
||||
const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)]
|
||||
|
||||
let runTestQuery = true
|
||||
const testTimeout = 200
|
||||
const testQuery = (mode, jsFilter, queryCode) => {
|
||||
const testQuery = (mode) => {
|
||||
runTestQuery = true
|
||||
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)) {
|
||||
if (filterFunc(item, item.value)) {
|
||||
item.timestamp = new Date().getTime()
|
||||
state.update(s => ({ ...s, items: [item, ...s.items].slice(0, itemLimit), itemCount: 0 }))
|
||||
state.update(s => ({ ...s, items: [item, ...s.items].slice(0, itemLimit), itemCount: (s.itemCount || 0) + 1 }))
|
||||
}
|
||||
if (runTestQuery) {
|
||||
setTimeout(addItem, testTimeout)
|
||||
}
|
||||
setTimeout(addItem, testTimeout)
|
||||
}
|
||||
setTimeout(addItem, testTimeout)
|
||||
} else {
|
||||
state.update(s => ({ ...s, items: mockItems.filter(item => !jsFilter || f(item, item.value)), itemCount: 0 }))
|
||||
state.update(s => ({ ...s, items: mockItems.filter(item => filterFunc(item, item.value)), itemCount: (s.itemCount || 0) + 1 }))
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Caught an error:', e.toString())
|
||||
|
@ -61,30 +78,49 @@ const testQuery = (mode, jsFilter, queryCode) => {
|
|||
}
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export const query = async ({ cluster, topic, mode, jsFilter, queryCode, maxItems, mutableObjects }) => {
|
||||
updateClearError(s => ({ ...s, items: [], itemCount: 0 }))
|
||||
export const killQuery = async ({ }) => {
|
||||
// TODO
|
||||
if (testMode) {
|
||||
testQuery(mode, jsFilter, queryCode)
|
||||
runTestQuery = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export const query = async ({ cluster, topic, mode, jsFilter, queryCode, maxItems, mutableObjects }) => {
|
||||
if (jsFilter) {
|
||||
filterFunc = new Function('message', 'value', queryCode)
|
||||
} else {
|
||||
filterFunc = () => true
|
||||
}
|
||||
itemLimit = maxItems
|
||||
|
||||
updateClearError(s => ({ ...s, items: [], itemCount: 0 }))
|
||||
if (testMode) {
|
||||
testQuery(mode)
|
||||
return
|
||||
}
|
||||
|
||||
if (disconnected) {
|
||||
connect()
|
||||
disconnected = false
|
||||
}
|
||||
updateClearError(s => ({ ...s, items: [], itemCount: 0 }))
|
||||
itemLimit = maxItems
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
cluster,
|
||||
mode,
|
||||
topic,
|
||||
maxItems,
|
||||
immutable: !mutableObjects,
|
||||
searchCode: jsFilter && queryCode
|
||||
searchCode: false //jsFilter && queryCode
|
||||
}))
|
||||
}
|
||||
|
||||
export const connect = () => {
|
||||
if (testMode) {
|
||||
updateClearError(s => ({ ...s, error: 'TEST MODE ENABLED' }))
|
||||
return
|
||||
}
|
||||
try {
|
||||
ws = new WebSocket(`ws://${backendAddressAndPort}`)
|
||||
} catch (e) {
|
||||
|
@ -117,10 +153,13 @@ export const connect = () => {
|
|||
}))
|
||||
break;
|
||||
case 'message':
|
||||
updateClearError(s => ({
|
||||
...s,
|
||||
items: console.log('new item', data.message) || [data.message, ...s.items].slice(0, itemLimit)
|
||||
}))
|
||||
if (filterFunc(data.message)) {
|
||||
updateClearError(s => ({
|
||||
...s,
|
||||
items: console.log('new item', data.message) || [data.message, ...s.items].slice(0, itemLimit),
|
||||
itemCount: (s.itemCount || 0) + 1
|
||||
}))
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
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 { apiFetch } from "../utils.js";
|
||||
import { state, connect, query, killQuery } from '$lib/state'
|
||||
import { queryMode } from "../lib/constants.js"
|
||||
import { apiFetch } from "../utils.js"
|
||||
|
||||
let expandAll = true
|
||||
let showMetadata = true
|
||||
|
@ -31,12 +31,52 @@ return message.value.eventType === "Television";`
|
|||
console.log('topics', topics)
|
||||
querySettings.topic = topics[0]
|
||||
}
|
||||
const fontSizes = [30, 50, 67, 80, 90, 100, 110, 120, 133, 150]
|
||||
let dataViewFontSize = 5
|
||||
const setFontSize = size => {
|
||||
dataViewFontSize = size
|
||||
localStorage.dataViewFontSize = size
|
||||
}
|
||||
const fontUp = () => setFontSize(Math.min(dataViewFontSize + 1, fontSizes.length - 1))
|
||||
const fontDown = () => setFontSize(Math.max(dataViewFontSize - 1, 0))
|
||||
|
||||
onMount(async () => {
|
||||
connect();
|
||||
connect()
|
||||
clusters = Object.keys((await apiFetch('/clusters')) || {})
|
||||
querySettings.cluster = clusters[0]
|
||||
await updateTopics()
|
||||
|
||||
if (localStorage) {
|
||||
dataViewFontSize = localStorage.dataViewFontSize || dataViewFontSize
|
||||
if (localStorage.querySettings) {
|
||||
let storedSettings = {}
|
||||
try {
|
||||
storedSettings = JSON.parse(localStorage.querySettings)
|
||||
} catch (e) {}
|
||||
querySettings.maxItems = storedSettings?.maxItems || querySettings.maxItems
|
||||
querySettings.jsFilter = storedSettings?.jsFilter || querySettings.jsFilter
|
||||
querySettings.queryCode = storedSettings?.queryCode || querySettings.queryCode
|
||||
querySettings.mode = storedSettings?.mode || querySettings.mode
|
||||
if (clusters.includes(storedSettings?.cluster)) {
|
||||
querySettings.cluster = storedSettings.cluster
|
||||
}
|
||||
if (topics.includes(storedSettings?.topic)) {
|
||||
querySettings.topic = storedSettings.topic
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let queryRunning = false
|
||||
const startQuery = () => {
|
||||
queryRunning = true
|
||||
localStorage.setItem('querySettings', JSON.stringify(querySettings))
|
||||
query(querySettings)
|
||||
}
|
||||
const stopQuery = () => {
|
||||
queryRunning = false
|
||||
killQuery(querySettings)
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -110,32 +150,87 @@ return message.value.eventType === "Television";`
|
|||
</div>
|
||||
|
||||
<div class="query-button">
|
||||
<button on:click={() => query(querySettings)}>Start Query</button>
|
||||
{#if queryRunning}
|
||||
<button class="danger" on:click={() => stopQuery()}>Stop Query</button>
|
||||
{:else}
|
||||
<button class="colored" on:click={() => startQuery()}>Start Query</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="live-settings">
|
||||
<div>Expand All Objects <input type=checkbox bind:checked={expandAll}></div>
|
||||
<div>Show Metadata <input type=checkbox bind:checked={showMetadata}></div>
|
||||
<!--{#if $state.itemCount >= 0}
|
||||
<h2>{$state.itemCount} Matches Found...</h2>
|
||||
{/if}-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $state.items?.length > 0}
|
||||
<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 class="data-view-container">
|
||||
<div class="data-view-bar">
|
||||
<div>
|
||||
<button class={expandAll ? 'selected' : ''} on:click={() => expandAll = !expandAll}>
|
||||
Expand All Objects
|
||||
</button>
|
||||
<button class={showMetadata ? 'selected' : ''} on:click={() => showMetadata = !showMetadata}>
|
||||
Show Metadata
|
||||
</button>
|
||||
<button on:click={fontUp}>+</button>
|
||||
<button on:click={fontDown}>-</button>
|
||||
</div>
|
||||
{#if $state.itemCount >= 0}
|
||||
<div class="data-view-results">
|
||||
{$state.itemCount} Messages
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else }
|
||||
<div class="data-view no-query-data">
|
||||
<h2>No query data</h2>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $state.items?.length > 0}
|
||||
<div class="data-view" style={`font-size: ${fontSizes[dataViewFontSize]}%`}>
|
||||
<JsonView json={showMetadata ? $state.items : $state.items.map(item => item.value)}
|
||||
depth={expandAll ? Infinity : 1}/>
|
||||
</div>
|
||||
{:else }
|
||||
<div class="data-view no-query-data">
|
||||
<h2>No query data</h2>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.data-view {
|
||||
border-top: none;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.data-view-bar button {
|
||||
border-style: none;
|
||||
border-radius: 0;
|
||||
margin-right: 1px;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -171,7 +266,7 @@ return message.value.eventType === "Television";`
|
|||
flex-direction: column;
|
||||
overflow: auto;
|
||||
padding: 1em;
|
||||
height: 86vh;
|
||||
height: 83vh;
|
||||
}
|
||||
|
||||
.query-input-header {
|
||||
|
|
|
@ -1,31 +1,12 @@
|
|||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { backendUrl, saslAuthMethods } from "../../lib/constants.js";
|
||||
import { apiFetch } from "../../utils.js";
|
||||
import { saslAuthMethods } from "../../lib/constants.js";
|
||||
import { apiFetch, findLast, deepCopy, postJson, putJson } from "../../utils.js";
|
||||
import Modal from "../../lib/Modal.svelte";
|
||||
|
||||
const jsonRequest = type => async (path, object) => fetch(backendUrl + path, {
|
||||
method: type,
|
||||
body: JSON.stringify(object),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
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 => fetchClusters(await post('/clusters', cluster))
|
||||
const updateCluster = async cluster => fetchClusters(await put('/clusters', cluster))
|
||||
const deleteCluster = async clusterName => fetchClusters(await post('/clusters/delete', { clusterName }))
|
||||
const addCluster = async cluster => fetchClusters(await postJson('/clusters', cluster))
|
||||
const updateCluster = async cluster => fetchClusters(await putJson('/clusters', cluster))
|
||||
const deleteCluster = async clusterName => fetchClusters(await postJson('/clusters/delete', { clusterName }))
|
||||
|
||||
let clusters = []
|
||||
const fetchClusters = async (response) => {
|
||||
|
@ -45,9 +26,7 @@
|
|||
}
|
||||
onMount(async () => {
|
||||
await fetchClusters()
|
||||
console.log('onMount', clusters)
|
||||
})
|
||||
console.log('instant', clusters)
|
||||
|
||||
const startNewCluster = () => {
|
||||
config = emptyConfig()
|
||||
|
@ -66,6 +45,18 @@
|
|||
}
|
||||
})
|
||||
let config = emptyConfig()
|
||||
|
||||
let showConfirmModal = false
|
||||
const closeModal = () => {showConfirmModal = false}
|
||||
let modalConfirmAction = closeModal
|
||||
|
||||
let modalTitle = ''
|
||||
const buildDeleteModal = clusterName => {
|
||||
showConfirmModal = true
|
||||
modalTitle = `Are you sure you want to delete ${clusterName}?`
|
||||
modalConfirmAction = () => deleteCluster(clusterName)
|
||||
closeModal()
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -143,7 +134,18 @@
|
|||
<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>
|
||||
<button class="danger" on:click={() => buildDeleteModal(clusterName)}>Delete</button>
|
||||
{#if showConfirmModal}
|
||||
<Modal level='danger'
|
||||
onClickOut={closeModal}
|
||||
onCancel={closeModal}
|
||||
onConfirm={modalConfirmAction}
|
||||
title={modalTitle}>
|
||||
This will remove it from this instance of Kafka Dance for all users.<br>
|
||||
<br>
|
||||
This action cannot be undone.
|
||||
</Modal>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -205,7 +207,7 @@
|
|||
.query-settings {
|
||||
margin-right: 1rem;
|
||||
padding: 1em;
|
||||
height: 86vh;
|
||||
height: 83vh;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
33
src/utils.js
33
src/utils.js
|
@ -1,6 +1,34 @@
|
|||
import { backendUrl } from "$lib/constants.js";
|
||||
import { state, testMode } from "$lib/state.js";
|
||||
|
||||
export const deepCopy = object => JSON.parse(JSON.stringify(object))
|
||||
|
||||
export const getRandomFromArray = array => array[Math.floor(Math.random() * array.length)]
|
||||
|
||||
export const findLast = array => {
|
||||
if (!array || array.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return array[array.length - 1]
|
||||
}
|
||||
|
||||
const jsonRequest = type => async (path, object) => fetch(path, {
|
||||
method: type,
|
||||
body: JSON.stringify(object),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
* @param path
|
||||
* @param object
|
||||
* @returns {Promise<*|undefined>}
|
||||
*/
|
||||
export const postJson = (path, object) => jsonRequest('POST')(path, object)
|
||||
export const putJson = (path, object) => jsonRequest('PUT')(path, object)
|
||||
|
||||
const mockApi = {
|
||||
'/clusters': {
|
||||
'TestCluster': {
|
||||
|
@ -21,15 +49,16 @@ const mockApi = {
|
|||
* Returns the JSON response from the given path, or undefined if an error occurred
|
||||
* Sets `state.error` if something goes wrong.
|
||||
* @param path
|
||||
* @param options Options object to pass to `fetch()`
|
||||
* @returns {Promise<any|undefined>}
|
||||
*/
|
||||
export const apiFetch = async path => {
|
||||
export const apiFetch = async (path, options = undefined) => {
|
||||
if (testMode) {
|
||||
return mockApi[path]
|
||||
}
|
||||
let response
|
||||
try {
|
||||
response = await fetch(`${backendUrl}${path}`)
|
||||
response = await fetch(`${backendUrl}${path}`, options)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
state.update(s => ({ ...s, error: 'Could not connect to the backend' }))
|
||||
|
|
Loading…
Reference in New Issue