Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/concurrent queries #16

Merged
merged 2 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading