diff --git a/frontend/src/JsonTable.svelte b/frontend/src/JsonTable.svelte index 3c1d319..fc55048 100644 --- a/frontend/src/JsonTable.svelte +++ b/frontend/src/JsonTable.svelte @@ -1,15 +1,15 @@ -{#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}
- {$searchTerm.charAt(0).toUpperCase() + $searchTerm.slice(1)}s + {$searchTerm.charAt(0).toUpperCase() + $searchTerm.slice(1) + "s" + "(" + displayedData.length + ")"}
- {#each Object.keys(data[0]) as header, i} - {#if i == 0 } - - {:else } + {#each Object.keys(displayedData[0]) as header} - {/if} {/each} - {#each Object.entries(data) as [id, obj] } + {#each Object.entries(displayedData) as [id, obj] } {#if id == activeRowIndex} {#each Object.values(obj) as val } diff --git a/frontend/src/Legend.svelte b/frontend/src/Legend.svelte index 8554014..645836e 100644 --- a/frontend/src/Legend.svelte +++ b/frontend/src/Legend.svelte @@ -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 = { @@ -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 = 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 } @@ -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 @@ -85,7 +81,7 @@
Local Contexts - {#if $clusterQStore.data} + {#if $queryStore.data}
{#each clusterArr as {id, context, checked}}
diff --git a/frontend/src/SearchBar.svelte b/frontend/src/SearchBar.svelte index 5c64841..b437f2d 100644 --- a/frontend/src/SearchBar.svelte +++ b/frontend/src/SearchBar.svelte @@ -1,54 +1,27 @@
@@ -130,7 +88,6 @@ type="text" autocomplete="off" /> - {numResults} results
diff --git a/frontend/src/activeContextStore.ts b/frontend/src/activeContextStore.ts index 3becef7..e38c26e 100644 --- a/frontend/src/activeContextStore.ts +++ b/frontend/src/activeContextStore.ts @@ -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> = writable(new Map()); -export const allContextStore: Writable<[]> = writable([]); \ No newline at end of file +export const activeContextStore: Writable> = 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 = writable() +export const removeContextStore: Writable = 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) +}) \ No newline at end of file diff --git a/frontend/src/gqlQuery.ts b/frontend/src/gqlQuery.ts index ab37042..07bbedb 100644 --- a/frontend/src/gqlQuery.ts +++ b/frontend/src/gqlQuery.ts @@ -1,4 +1,4 @@ -import type {AnyVariables, Client, OperationResult, TypedDocumentNode} from "@urql/svelte"; +import type {AnyVariables, Client, OperationResult, OperationResultStore, TypedDocumentNode} from "@urql/svelte"; import {getContextClient, gql, queryStore} from "@urql/svelte"; import type {tableObject} from "./jsonTable"; @@ -8,12 +8,14 @@ export class BaseQuery { readonly rootQueryString: string readonly bodyQueryString: string readonly footerQueryString: string - readonly client: Client - contexts: string[] + readonly contextName: string + client: Client + queryIssued: boolean = false + queryStore: OperationResultStore enableTemplating: boolean - constructor(debug?: boolean) { - this.client = getContextClient() + constructor(contextName: string, debug?: boolean) { + this.contextName = contextName if (debug) { this.client.subscribeToDebugTarget(event => { if (event.source === 'cacheExchange') @@ -23,27 +25,27 @@ export class BaseQuery { } } - templateContexts(): TypedDocumentNode { + templateContext(): TypedDocumentNode { // body query templating let templatedBody: string = `` - for (let context of this.contexts) { // convert dashes to underscore for graphql compliance - let paramName = context.replaceAll("-", "_") + let paramName = this.contextName.replaceAll("-", "_") let firstTemplate = this.bodyQueryString.replaceAll("PARAM-PLACEHOLDER", paramName) - let secondTemplate = firstTemplate.replaceAll("CONTEXT-PLACEHOLDER", context) + let secondTemplate = firstTemplate.replaceAll("CONTEXT-PLACEHOLDER", this.contextName) templatedBody += secondTemplate - } - // TODO: why does this execute twice on load? // console.log(this.rootQueryString + templatedBody + this.footerQueryString) return gql(this.rootQueryString + templatedBody + this.footerQueryString) } - executeQuery(contexts?: string[], variables?: any): any { - this.contexts = contexts - return queryStore({ + executeQuery(variables?: any) { + if (!this.client){ + // Note: getContextClient() must be called from within a svelte component! + this.client = getContextClient() + } + this.queryStore = queryStore({ client: this.client, - query: this.enableTemplating ? this.templateContexts() : this.query, + query: this.enableTemplating ? this.templateContext() : this.query, variables }) } diff --git a/frontend/src/jsonTable.ts b/frontend/src/jsonTable.ts index 00f5e1f..cd9ae00 100644 --- a/frontend/src/jsonTable.ts +++ b/frontend/src/jsonTable.ts @@ -1,8 +1,22 @@ +import type {Writable} from "svelte/store"; +import {writable} from "svelte/store"; +import Fuse from "fuse.js"; + +const filterOptions = { + keys: ['name', 'Name', 'namespace', 'Namespace'], + threshold: 0.40 // 0 = perfect match, 1 = indiscriminate +} + +export const tableDataStore: Writable> = writable(new Map()) +export const searchTerm: Writable = writable("") +export const filterTerm: Writable = writable() + export interface tableObject { // TODO } -import {writable} from "svelte/store" - -export const tableDataStore = writable({}) -export const searchTerm = writable("") \ No newline at end of file +export function filter(data: any, str: string): any { + const fuse = new Fuse(data, filterOptions) + let filteredTableObjectMap = fuse.search(str) + return filteredTableObjectMap.map((idx) => idx.item) +} \ No newline at end of file
- {header} - {header}