Skip to content
Open
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
14 changes: 14 additions & 0 deletions packages/graph-explorer/src/components/Tabular/useTabular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ export interface TabularOptions<T extends object> {
*/
disableSortRemove?: boolean;

/**
* Enables sorting detection functionality, but does not automatically perform row sorting.
*/
manualSorting?: boolean;

/**
* Must be memoized. An array of filters.
*/
Expand All @@ -220,6 +225,11 @@ export interface TabularOptions<T extends object> {
*/
autoResetFilters?: boolean;

/**
* Enables filter detection functionality, but does not automatically perform row filtering.
*/
manualFilters?: boolean;

/**
* Disables the pagination.
*/
Expand Down Expand Up @@ -309,6 +319,8 @@ export const useTabular = <T extends object>(options: TabularOptions<T>) => {
toggleAllRowsSelected,
initialColumnOrder,
initialHiddenColumns,
manualFilters,
manualSorting,
...restOptions
} = options;

Expand Down Expand Up @@ -394,6 +406,8 @@ export const useTabular = <T extends object>(options: TabularOptions<T>) => {
defaultColumn,
disableSortBy: disableSorting,
disableMultiSort: disableMultiSorting,
manualSortBy: manualSorting,
manualFilters,
columns: useDeepMemo(
() => columns.map(column => columnDefinitionToColumn(column)),
[columns],
Expand Down
1 change: 1 addition & 0 deletions packages/graph-explorer/src/connector/emptyExplorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const emptyExplorer: Explorer = {
fetchNeighbors: async () => ({ vertices: [], edges: [] }),
neighborCounts: async () => ({ counts: [] }),
keywordSearch: async () => ({ vertices: [] }),
filterAndSortSearch: async () => ({ vertices: [] }),
vertexDetails: async () => ({ vertices: [] }),
edgeDetails: async () => ({ edges: [] }),
rawQuery: async () => ({ results: [], rawResponse: null }),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type {
Criterion,
FilterAndSortRequest,
} from "@/connector/useGEFetchTypes";

import { escapeString } from "@/utils";

function escapeRegexLiteral(s: string): string {
return s.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
}

function criterionNumberTemplate({ name, value }: Criterion): string {
const num = Number(value);
if (!Number.isFinite(num)) {
return `has("${name}",eq(0))`;
}
return `has("${name}",eq(${num}))`;
}

function criterionStringTemplate(
{ name, value }: Criterion,
exactMatch: boolean,
): string {
const escaped = escapeString(String(value));
const str = String(value);
const numericValue = Number(value);
const isNumeric =
str.trim() !== "" &&
Number.isFinite(numericValue) &&
String(numericValue) === str.trim();

if (isNumeric) {
return `has("${name}",eq(${numericValue}))`;
}
if (exactMatch) {
return `has("${name}","${escaped}")`;
}
const regexEscaped = escapeRegexLiteral(String(value));
const pattern = `(?i).*${regexEscaped}.*`;
return `has("${name}",regex("${escapeString(pattern)}"))`;
}

function criterionDateTemplate({ name, value }: Criterion): string {
return `has("${name}",eq(datetime(${value})))`;
}

function criterionTemplate(criterion: Criterion, exactMatch: boolean): string {
switch (criterion.dataType) {
case "Number":
return criterionNumberTemplate(criterion);
case "Date":
return criterionDateTemplate(criterion);
case "String":
case undefined:
default:
return criterionStringTemplate(criterion, exactMatch);
}
}

/**
* Builds a Gremlin traversal for g.V() with optional vertexTypes, filterCriteria,
* sortingCriteria, and range.
*
* @example
* vertexTypes = ["airport"]
* filterCriteria = [{ name: "country", value: "US" }]
* sortingCriteria = [{ name: "code", direction: "asc" }]
* limit = 20, offset = 0
*
* g.V().hasLabel("airport").and(has("country",containing("US"))).order().by("code",asc).range(0,20)
*/
export default function filterAndSortTemplate({
vertexTypes = [],
filterCriteria = [],
sortingCriteria = [],
limit,
offset = 0,
exactMatch = false,
}: FilterAndSortRequest): string {
let template = "g.V()";

if (vertexTypes.length > 0) {
const hasLabelContent = vertexTypes
.flatMap(type => type.split("::"))
.map(type => `"${type}"`)
.join(",");
template += `.hasLabel(${hasLabelContent})`;
}

if (filterCriteria.length > 0) {
const andContent = filterCriteria
.map(c => criterionTemplate(c, exactMatch))
.join(", ");
template += `.and(${andContent})`;
}

if (sortingCriteria.length > 0) {
const byClauses = sortingCriteria
.map(s => `.by("${s.name}",${s.direction === "desc" ? "desc" : "asc"})`)
.join("");
template += `.order()${byClauses}`;
}

if (limit != null && limit > 0) {
template += `.range(${offset},${offset + limit})`;
}

return template;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type {
ErrorResponse,
FilterAndSortRequest,
FilterAndSortResponse,
} from "@/connector/useGEFetchTypes";

import isErrorResponse from "@/connector/utils/isErrorResponse";
import { createVertex } from "@/core";

import type { GVertexList } from "../types";
import type { GremlinFetch } from "../types";

import mapApiVertex from "../mappers/mapApiVertex";
import filterAndSortTemplate from "./filterAndSortTemplate";

type RawFilterAndSortResponse = {
requestId: string;
status: {
message: string;
code: number;
};
result: {
data: GVertexList;
};
};

async function filterAndSortSearch(
gremlinFetch: GremlinFetch,
req: FilterAndSortRequest,
): Promise<FilterAndSortResponse> {
const gremlinTemplate = filterAndSortTemplate(req);
const data = await gremlinFetch<RawFilterAndSortResponse | ErrorResponse>(
gremlinTemplate,
);

if (isErrorResponse(data)) {
throw new Error(data.detailedMessage);
}

const vertices = data.result.data["@value"]
.map(value => mapApiVertex(value))
.map(createVertex);

return { vertices };
}

export default filterAndSortSearch;
11 changes: 11 additions & 0 deletions packages/graph-explorer/src/connector/gremlin/gremlinExplorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import fetchEdgeConnections from "./fetchEdgeConnections";
import fetchNeighbors from "./fetchNeighbors";
import fetchSchema from "./fetchSchema";
import fetchVertexTypeCounts from "./fetchVertexTypeCounts";
import filterAndSortSearch from "./filterAndSort";
import keywordSearch from "./keywordSearch";
import { neighborCounts } from "./neighborCounts";
import { rawQuery } from "./rawQuery";
Expand Down Expand Up @@ -120,6 +121,16 @@ export function createGremlinExplorer(
req,
);
},
async filterAndSortSearch(req, options) {
options ??= {};
options.queryId = v4();

remoteLogger.info("[Gremlin Explorer] Fetching filter and sort...");
return filterAndSortSearch(
_gremlinFetch(connection, featureFlags, options),
req,
);
},
async vertexDetails(req, options) {
options ??= {};
options.queryId = v4();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { queryOptions } from "@tanstack/react-query";

import type { UpdateSchemaHandler } from "@/core/StateProvider/schema";

import type { FilterAndSortRequest } from "../useGEFetchTypes";

import {
getExplorer,
getStore,
setVertexDetailsQueryCache,
updateVertexGraphCanvasState,
} from "./helpers";

/**
* Performs a filter-and-sort query with the provided parameters.
* @param request The filter/sort parameters to use for the query.
* @param updateSchema Handler to update schema from results.
* @returns Query options for vertices matching the filter/sort criteria.
*/
export function filterAndSortQuery(
request: FilterAndSortRequest,
updateSchema: UpdateSchemaHandler,
) {
return queryOptions({
queryKey: ["filter-and-sort", request],
queryFn: async ({ signal, meta, client }) => {
const explorer = getExplorer(meta);
const store = getStore(meta);

const results = await explorer.filterAndSortSearch(request, { signal });

results.vertices.forEach(vertex => {
setVertexDetailsQueryCache(client, vertex);
});
updateVertexGraphCanvasState(store, results.vertices);
updateSchema(results);

return results;
},
});
}
1 change: 1 addition & 0 deletions packages/graph-explorer/src/connector/queries/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./filterAndSortQuery";
export * from "./schemaSyncQuery";
export * from "./searchQuery";
export * from "./bulkNeighborCountsQuery";
Expand Down
46 changes: 46 additions & 0 deletions packages/graph-explorer/src/connector/useGEFetchTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ export type Criterion = {
dataType?: "String" | "Number" | "Date";
};

export type SortingCriterion = {
/**
* Attribute name.
*/
name: string;
/**
* Sorting direction.
* By default, "asc".
*/
direction: "asc" | "desc";
};

/**
* A request for the neighbors and relationships for the given vertex, filtered
* by the provided paramters.
Expand Down Expand Up @@ -171,8 +183,38 @@ export type KeywordSearchRequest = {
exactMatch?: boolean;
};

export type FilterAndSortRequest = {
/**
* Filter by vertex types.
*/
vertexTypes?: Array<string>;
/**
* Filter criteria to apply to the request.
*/
filterCriteria?: Array<Criterion>;
/**
* Sorting criteria to apply to the request.
*/
sortingCriteria?: Array<SortingCriterion>;
/**
* Limit the number of results.
* 0 = No limit.
*/
limit?: number;
/**
* Skip the given number of results.
*/
offset?: number;
/**
* Only return exact attribute value matches.
*/
exactMatch?: boolean;
};

export type KeywordSearchResponse = { vertices: Vertex[] };

export type FilterAndSortResponse = { vertices: Vertex[] };

export type ErrorResponse = {
code: string;
detailedMessage: string;
Expand Down Expand Up @@ -249,6 +291,10 @@ export type Explorer = {
req: KeywordSearchRequest,
options?: ExplorerRequestOptions,
) => Promise<KeywordSearchResponse>;
filterAndSortSearch: (
req: FilterAndSortRequest,
options?: ExplorerRequestOptions,
) => Promise<FilterAndSortResponse>;
vertexDetails: (
req: VertexDetailsRequest,
options?: ExplorerRequestOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type DisplayConfigAttribute = {
name: string;
displayLabel: string;
isSearchable: boolean;
dataType?: "String" | "Number" | "Date";
};

/** Gets the matching vertex type config or a generated default value. */
Expand Down Expand Up @@ -166,6 +167,7 @@ export function mapToDisplayVertexTypeConfig(
name: attr.name,
displayLabel: textTransform(attr.name),
isSearchable: isAttributeSearchable(attr),
dataType: toDisplayDataType(attr.dataType),
}))
.toSorted(sortAttributeByName);

Expand Down Expand Up @@ -193,6 +195,7 @@ export function mapToDisplayEdgeTypeConfig(
name: attr.name,
displayLabel: textTransform(attr.name),
isSearchable: isAttributeSearchable(attr),
dataType: toDisplayDataType(attr.dataType),
}))
.toSorted(sortAttributeByName);

Expand All @@ -207,3 +210,12 @@ export function mapToDisplayEdgeTypeConfig(
function isAttributeSearchable(attribute: AttributeConfig) {
return attribute.dataType === "String";
}

function toDisplayDataType(
dataType: string | undefined,
): DisplayConfigAttribute["dataType"] {
if (dataType === "Number" || dataType === "Date" || dataType === "String") {
return dataType;
}
return undefined;
}
Loading