Skip to content

Commit

Permalink
Feat/concurrent queries (#16)
Browse files Browse the repository at this point in the history
* feat(frontend): async queries
  • Loading branch information
corinz committed Mar 8, 2024
1 parent e500040 commit 4501e91
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 108 deletions.
42 changes: 23 additions & 19 deletions frontend/src/JsonTable.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<script lang="ts">
import {tableDataStore, searchTerm} from "./jsonTable"
import {searchTerm, filterTerm, tableDataStore, filter} from "./jsonTable"
let data, activeRowIndex
$: {
data = $tableDataStore
activeRowIndex = 0
}
let activeRowIndex = 0, filteredData, displayedData
$: data = Array.from($tableDataStore.values()).flatMap(arr => arr)
$: displayedData = filteredData ? filteredData : data
function handleKeyDown(event: CustomEvent | KeyboardEvent) {
document.getElementById('highlight').scrollIntoView({behavior: "auto", block: "center", inline: "nearest"});
let element = document.getElementById('highlight')
if (element) {
element.scrollIntoView({behavior: "auto", block: "center", inline: "nearest"})
}
event = event as KeyboardEvent;
if (event.key === 'ArrowUp' || event.key === 'Up') {
activeRowIndex = Math.max(0, activeRowIndex - 1);
Expand All @@ -18,38 +18,42 @@
}
}
filterTerm.subscribe( term => {
if (term == "" || term == null){
filteredData = null
return
}
filteredData = filter(data, term)
})
window.addEventListener("keydown", function (e) {
handleKeyDown(e)
});
</script>

{#if (!data)}
{#if (!displayedData)}
Dataset does not exist
{:else if displayedData.length === 0}
Dataset is empty
{:else if data.length > 0}
{:else if displayedData.length > 0}
<fieldset>
<legend>{$searchTerm.charAt(0).toUpperCase() + $searchTerm.slice(1)}s</legend>
<legend>{$searchTerm.charAt(0).toUpperCase() + $searchTerm.slice(1) + "s" + "(" + displayedData.length + ")"} </legend>
<div class="scrollable-content">
<table>
<!-- HEADER ROW -->
<thead>
<tr>
{#each Object.keys(data[0]) as header, i}
{#if i == 0 }
<th columnId={header}>
{header}
</th>
{:else }
{#each Object.keys(displayedData[0]) as header}
<th columnId={header}>
{header}
</th>
{/if}
{/each}
</tr>
</thead>
<!-- DATA ROWS -->
<tbody>
{#each Object.entries(data) as [id, obj] }
{#each Object.entries(displayedData) as [id, obj] }
{#if id == activeRowIndex}
<tr id="highlight">
{#each Object.values(obj) as val }
Expand Down
22 changes: 9 additions & 13 deletions frontend/src/Legend.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import {onMount} from 'svelte';
import {BaseQuery} from "./gqlQuery";
import {gql} from "@urql/svelte";
import {activeContextStore, allContextStore} from "./activeContextStore";
import {addContextStore, removeContextStore} from "./activeContextStore";
// Cluster is used to display legend items
type Cluster = {
Expand All @@ -18,17 +18,15 @@
}`
}
const contextQuery = new ContextResourceQuery
let clusterQStore = contextQuery.executeQuery()
const contextQuery = new ContextResourceQuery(null)
contextQuery.executeQuery()
let queryStore = contextQuery.queryStore
let clusters
let activeContextsLocal: Map<string, string> = new Map()
let clusterArr: Cluster[] = []
// execute cluster api query and set store for external use
$: if ($clusterQStore.data) {
clusters = $clusterQStore.data["contexts"]
allContextStore.set(clusters)
$: if ($queryStore.data) {
clusters = $queryStore.data["contexts"]
} else {
clusters = null
}
Expand Down Expand Up @@ -56,13 +54,11 @@
// update active contexts
if (toggle) {
activeContextsLocal.set(name, name)
addContextStore.set(name)
} else {
activeContextsLocal.delete(name)
removeContextStore.set(name)
}
}
// set activeContexts store so we can subscribe in other places
activeContextStore.set(activeContextsLocal)
}
// handleKeyPress toggles clusters based on numpad key presses
Expand All @@ -85,7 +81,7 @@
<div>
<fieldset>
<legend>Local Contexts</legend>
{#if $clusterQStore.data}
{#if $queryStore.data}
<div>
{#each clusterArr as {id, context, checked}}
<div>
Expand Down
63 changes: 10 additions & 53 deletions frontend/src/SearchBar.svelte
Original file line number Diff line number Diff line change
@@ -1,54 +1,27 @@
<script lang="ts">
import Fuse from 'fuse.js'
import {focusedElement} from "./focus"
import _ from 'underscore';
import {GqlResourceQuery} from "./resourceQuery.ts"
import {activeContextStore} from "./activeContextStore";
import {tableDataStore, searchTerm} from "./jsonTable";
import {activeContextStore, execActiveContexts} from "./activeContextStore";
import {tableDataStore, searchTerm, filterTerm} from "./jsonTable";
// search and filter and ui
// search and ui
export let searchEventKey: string
let searchBarInput: string = ""
let placeholder
const filterOptions = {
keys: ['name', 'Name', 'namespace', 'Namespace'],
threshold: 0.40 // 0 = perfect match, 1 = indiscriminate
}
$: debounceDelay = searchEventKey === '/' ? 600 : 850
$: debouncedHandleInput = _.debounce(handleInput, debounceDelay)
focusedElement.set(document.getElementById("search"))
// Graphql
let queryVars = {"name": "pod"}
searchTerm.set("pod") // sets the legend title
let getResources = new GqlResourceQuery
let filteredTableObject = null
// display logic
let resourceQStore, tableObject, queryError, queryFetching, activeContextsArr
$: numResults = tableObject == null ? 0 : tableObject.length
$: if ($activeContextStore) {
// Convert array (map is used by Legend.svelte for ease)
activeContextsArr = [...$activeContextStore.keys()]
if (activeContextsArr.length > 0) {
resourceQStore = getResources.executeQuery(activeContextsArr, queryVars)
searchTerm.set("pod")
// update the UI w/ proper messaging
queryFetching = $resourceQStore.fetching
queryError = $resourceQStore.error
// query all un-queried clusters
$: execActiveContexts($activeContextStore, queryVars, false)
if (queryError) {
console.log(queryError.message)
} else if (filteredTableObject) { // filter takes precedence
tableObject = filteredTableObject
} else {
// resourceQStore is not guaranteed to have completed here
tableObject = $resourceQStore.data ? getResources.transform($resourceQStore.data) : null
}
} else { // default is empty table
tableObject = []
}
}
// when search term changes, query all active contexts
$: $searchTerm && execActiveContexts($activeContextStore, queryVars, true)
function search(): void {
// Don't search blank input
Expand All @@ -63,22 +36,12 @@
searchBarInput = ""
}
function filter(): void {
if (searchBarInput == "") {
filteredTableObject = null
} else {
const fuse = new Fuse(tableObject, filterOptions)
let filteredTableObjectMap = fuse.search(searchBarInput)
filteredTableObject = filteredTableObjectMap.map((idx) => idx.item)
}
}
// Handles dyanmic input
function handleInput(): void {
if (searchEventKey === ':') {
search()
} else if (searchEventKey === '/') {
filter()
filterTerm.set(searchBarInput)
}
}
Expand Down Expand Up @@ -112,11 +75,6 @@
default:
placeholder = "Press : to search or / to filter"
}
$: if (!queryFetching && !queryError) {
// set store with fetched data
tableDataStore.set(tableObject)
}
</script>

<div>
Expand All @@ -130,7 +88,6 @@
type="text"
autocomplete="off"
/>
{numResults} results
</fieldset>
</div>

Expand Down
104 changes: 100 additions & 4 deletions frontend/src/activeContextStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,102 @@
import type {Writable} from "svelte/store";
import {writable} from "svelte/store";
import type {Writable} from "svelte/store"
import {writable} from "svelte/store"
import {GqlResourceQuery} from "./resourceQuery";
import {tableDataStore} from "./jsonTable";

// activeContextStore is a map type to take advantage of set() and delete()
export const activeContextStore: Writable<Map<string, string>> = writable(new Map());
export const allContextStore: Writable<[]> = writable([]);
export const activeContextStore: Writable<Map<string, GqlResourceQuery>> = writable(new Map())

// writeable removeContextStore and addContextStore provide operation level insight into
// the activeContextStore, allowing subs on the current state, add operations, and remove operations
export const addContextStore: Writable<string> = writable()
export const removeContextStore: Writable<string> = writable()

// addContextStore manages the state of activeContextStore
addContextStore.subscribe((context) => {
// TODO: it seems writable() calls subscribe with a null input

// add context to activeContextStore
activeContextStore.update((allContexts) => {
let resourceQuery = new GqlResourceQuery(context)
allContexts.set(context, resourceQuery)

// clear addContextStore after activeContextStore is updated
addContextStore.set(null)

return allContexts
})
})

// finds active contexts that have not been queried
export async function execActiveContexts(activeContextMap, queryVars, execAll) {
if (execAll) {
activeContextMap.forEach((queryObject, contextName) => {
if (contextName != "" && contextName != null) {
queryObject.executeQuery(queryVars)
fetchContextData(queryObject)
}
})
} else {
activeContextMap.forEach((queryObject, contextName) => {
if (!queryObject.queryIssued && contextName != "" && contextName != null) {
queryObject.executeQuery(queryVars)
fetchContextData(queryObject)
}
})
}
}

// fetches data from existing query store
async function fetchContextData(queryObject) {
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
let retries = 0
let queryStore = queryObject.queryStore
let fetching, error, data
queryStore.subscribe(store => {
fetching = store.fetching
error = store.error
data = store.data
})

while (retries < 40) {
if (fetching) {
console.log("INFO: GraphQL Query Store fetching: ", queryObject.contextName)
} else if (error) {
console.log("ERROR: GraphQL Query Store: ", queryObject.contextName)
throw new Error(error)
} else if (data) {
// update tableDataStore with fetched data
tableDataStore.update(m => {
m.set(queryObject.contextName, queryObject.transform(data))
return m
})
queryObject.queryIssued = true
return
}
if (retries >= 39) {
console.log("INFO: GraphQL Query Store retries exhausted: ", queryObject.contextName)
return
}
retries++
await delay(250)
}
}

// removeContextStore manages the state of activeContextStore
removeContextStore.subscribe((context) => {
// TODO: it seems writable() calls subscribe with a null input

// remove context from activeContextStore
activeContextStore.update((allContexts) => {
allContexts.delete(context)
return allContexts
})

// remove context data from tableDataStore
tableDataStore.update(tableData => {
tableData.delete(context)
return tableData
})

removeContextStore.set(null)
})
Loading

0 comments on commit 4501e91

Please sign in to comment.