From 239ab2e2410c507b8f81c51d8e305e32645c0719 Mon Sep 17 00:00:00 2001 From: ewandor Date: Sun, 11 May 2025 17:04:35 +0200 Subject: [PATCH] Adding Crud filters form --- gui/rpk-gui/package-lock.json | 2 + gui/rpk-gui/package.json | 2 + .../src/lib/crud/components/crud-filters.tsx | 48 ++++ .../crud/providers/jsonschema-provider.tsx | 238 ++++++++++++------ gui/rpk-gui/src/pages/firm/base-page/List.tsx | 5 + 5 files changed, 225 insertions(+), 70 deletions(-) create mode 100644 gui/rpk-gui/src/lib/crud/components/crud-filters.tsx diff --git a/gui/rpk-gui/package-lock.json b/gui/rpk-gui/package-lock.json index c53753f..ee09154 100644 --- a/gui/rpk-gui/package-lock.json +++ b/gui/rpk-gui/package-lock.json @@ -14,6 +14,7 @@ "@mui/lab": "^6.0.0-beta.14", "@mui/material": "^6.1.7", "@mui/x-data-grid": "^7.22.2", + "@react-querybuilder/material": "^8.6.1", "@refinedev/cli": "^2.16.21", "@refinedev/core": "^4.47.1", "@refinedev/devtools": "^1.1.32", @@ -46,6 +47,7 @@ "react-error-boundary": "^6.0.0", "react-hook-form": "^7.30.0", "react-i18next": "^15.5.1", + "react-querybuilder": "^8.6.1", "react-router": "^7.0.2" }, "devDependencies": { diff --git a/gui/rpk-gui/package.json b/gui/rpk-gui/package.json index 747a0b4..eb9ab05 100644 --- a/gui/rpk-gui/package.json +++ b/gui/rpk-gui/package.json @@ -10,6 +10,7 @@ "@mui/lab": "^6.0.0-beta.14", "@mui/material": "^6.1.7", "@mui/x-data-grid": "^7.22.2", + "@react-querybuilder/material": "^8.6.1", "@refinedev/cli": "^2.16.21", "@refinedev/core": "^4.47.1", "@refinedev/devtools": "^1.1.32", @@ -42,6 +43,7 @@ "react-error-boundary": "^6.0.0", "react-hook-form": "^7.30.0", "react-i18next": "^15.5.1", + "react-querybuilder": "^8.6.1", "react-router": "^7.0.2" }, "devDependencies": { diff --git a/gui/rpk-gui/src/lib/crud/components/crud-filters.tsx b/gui/rpk-gui/src/lib/crud/components/crud-filters.tsx new file mode 100644 index 0000000..b229c44 --- /dev/null +++ b/gui/rpk-gui/src/lib/crud/components/crud-filters.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from "react"; +import { jsonschemaProvider } from "../providers/jsonschema-provider"; +import { CircularProgress } from "@mui/material"; +import { QueryBuilderMaterial } from "@react-querybuilder/material"; +import { QueryBuilder, type RuleGroupType } from 'react-querybuilder'; + +type CrudFiltersProps = { + resourceName: string + resourcePath: string +} + +const CrudFilters = (props: CrudFiltersProps) => { + const { resourceName, resourcePath } = props + + const [filtersSchema, setFiltersSchema] = useState([]) + const [filtersLoading, setFiltersLoading] = useState(true); + useEffect(() => { + const fetchSchema = async () => { + try { + const resourceFilters = await jsonschemaProvider.getListFilters(resourceName, resourcePath) + setFiltersSchema(resourceFilters); + console.log(resourceFilters); + setFiltersLoading(false); + } catch (error) { + console.error('Error fetching data:', error); + setFiltersLoading(false); + } + }; + fetchSchema(); + }, []); + + const [query, setQuery] = useState({ combinator: 'and', rules: [] }); + + if (filtersLoading) { + return + } + + return ( + + + + ) +} + +export default CrudFilters; diff --git a/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx b/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx index 8210531..06a1bdc 100644 --- a/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx +++ b/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx @@ -3,6 +3,7 @@ import i18n from '../../../i18n' import { JSONSchema7Definition } from "json-schema"; import { GridColDef } from "@mui/x-data-grid"; import { GridColType } from "@mui/x-data-grid/models/colDef/gridColType"; +import { Field as FilterField } from "react-querybuilder"; const API_URL = "/api/v1"; @@ -68,6 +69,92 @@ export const jsonschemaProvider = { getReadResourceColumns: async (resourceName: string): Promise => { return getColumns(`${resourceName}Read`) }, + + getListFilters: async (resourceName: string, resourcePath: string): Promise => { + return getFilters(resourceName, resourcePath); + } +} + +type PathParameter = { + in: string, + name:string +} + +type PathSchema = { + parameters: PathParameter[] +} + +function shortenResourceName(resourceName: string) { + return convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, "")); +} + +async function getFilters(resourceName: string, resourcePath: string): Promise { + const shortResourceName = shortenResourceName(resourceName); + const rawSchemas = await getJsonschema() + const resourceParts = `/${resourcePath}/`.split("/") + let pathSchema: PathSchema|undefined; + for (const path in rawSchemas.paths) { + if (rawSchemas.paths[path].hasOwnProperty("get")) { + const pathParts = path.split("/") + if (pathParts.length == resourceParts.length) { + for (let i = 0; i < pathParts.length; i++) { + const isVariable = pathParts[i].slice(0,1) == "{" && pathParts[i].slice(-1) == "}"; + if (! isVariable && pathParts[i] != resourceParts[i] ) { + break; + } + + if (i == pathParts.length - 1) { + pathSchema = rawSchemas.paths[path].get + } + } + } + if (pathSchema !== undefined) { + break + } + } + } + if (pathSchema === undefined) { + throw ("Path not found in schema"); + } + + const jst = new JsonSchemaTraverser(rawSchemas) + const seen: { [k: string]: number } = {}; + let filters: FilterField[] = [] + for (const param of pathSchema.parameters) { + if (param.name.indexOf("__") > -1) { + const { operator, fieldName } = processParamName(param) + if (! seen.hasOwnProperty(fieldName)) { + seen[fieldName] = filters.length; + const field = jst.getPropertyByPath(jst.getResource(`${resourceName}Read`), fieldName) + + filters.push({ + name: fieldName, + label: i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(fieldName)}`), + operators: [{ name: operator, label: operator }] + }); + } else { + // @ts-ignore + filters[seen[fieldName]].operators?.push({ name: operator, label: operator }); + } + } + } + + return filters; +} + + +type Operator = { + operator: string, + fieldName: string, +} + +function processParamName(param: PathParameter): Operator { + const nameParts = param.name.split("__") + + return { + operator: nameParts.pop() as string, + fieldName: nameParts.join("."), + } } const getColumns = async (resourceName: string): Promise => { @@ -141,9 +228,7 @@ function buildColumns (rawSchemas: RJSFSchema, resourceName: string, prefix: str valueGetter = (value: string) => new Date(value) } } - if (prop.type == "string" && prop.format == "date-time") { - } const column: GridColDef = { field: prefix ? `${prefix}.${prop_name}` : prop_name, headerName: i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title) as string, @@ -283,81 +368,94 @@ function get_reference_name(prop: any) { return prop['$ref'].substring(prop['$ref'].lastIndexOf('/')+1); } -function has_descendant(rawSchemas:RJSFSchema, resource: RJSFSchema, property_name: string): boolean { - if (is_array(resource)) { - return property_name == 'items'; - } else if (is_object(resource)) { - return property_name in resource.properties!; - } else if (is_reference(resource)) { - let subresourceName = get_reference_name(resource); - return has_descendant(rawSchemas, buildResource(rawSchemas, subresourceName), property_name); - } else if (is_union(resource)) { - const union = resource.hasOwnProperty("oneOf") ? resource.oneOf : resource.anyOf; - if (union !== undefined) { - for (const ref of union) { - return has_descendant(rawSchemas, ref as RJSFSchema, property_name) - } - } - } else if (is_enum(resource)) { - for (const ref of resource.allOf!) { - return has_descendant(rawSchemas, ref as RJSFSchema, property_name); - } - } - throw new Error("Jsonschema format not implemented in property finder"); -} +const JsonSchemaTraverser = class { + private rawSchemas: RJSFSchema; -function get_descendant(rawSchemas: RJSFSchema, resource: RJSFSchema, property_name: string): RJSFSchema { - if (is_array(resource) && property_name == 'items') { - return resource.items as RJSFSchema; - } else if (is_object(resource) && resource.properties !== undefined && property_name in resource.properties!) { - return resource.properties[property_name] as RJSFSchema; - } else if (is_reference(resource)) { - let subresourceName = get_reference_name(resource); - let subresource = buildResource(rawSchemas, subresourceName); - return get_descendant(rawSchemas, subresource, property_name); - } else if (is_union(resource)) { - for (const ref of resource.oneOf!) { - if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) { - return get_descendant(rawSchemas, ref as RJSFSchema, property_name); - } + constructor(rawSchemas: RJSFSchema) { + this.rawSchemas = rawSchemas + } + + public getResource = (resourceName: string) => { + if (this.rawSchemas.components.schemas[resourceName] === undefined) { + throw new Error(`Resource "${resourceName}" not found in schema.`); } - } else if (is_enum(resource)) { + + return this.rawSchemas.components.schemas[resourceName] + } + + public hasDescendant = (resource: RJSFSchema, property_name: string): boolean => { + if (is_array(resource)) { + return property_name == 'items'; + } else if (is_object(resource)) { + return property_name in resource.properties!; + } else if (is_reference(resource)) { + let subresourceName = get_reference_name(resource); + return this.hasDescendant(this.getResource(subresourceName), property_name); + } else if (is_union(resource)) { + const union = resource.hasOwnProperty("oneOf") ? resource.oneOf : resource.anyOf; + if (union !== undefined) { + for (const ref of union) { + return this.hasDescendant(ref as RJSFSchema, property_name) + } + } + } else if (is_enum(resource)) { for (const ref of resource.allOf!) { - if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) { - return get_descendant(rawSchemas, ref as RJSFSchema, property_name); - } + return this.hasDescendant(ref as RJSFSchema, property_name); } - } - throw new Error("property not found or Jsonschema format not implemented"); -} - -function path_exists(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): boolean{ - const pointFirstPosition = path.indexOf('.') - if (pointFirstPosition == -1) { - return has_descendant(rawSchemas, resource, path); + } + throw new Error("Jsonschema format not implemented in property finder"); } - return has_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition)) - && path_exists( - rawSchemas, - get_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition)), + public getDescendant = (resource: RJSFSchema, property_name: string): RJSFSchema => { + if (is_array(resource) && property_name == 'items') { + return resource.items as RJSFSchema; + } else if (is_object(resource) && resource.properties !== undefined && property_name in resource.properties!) { + return resource.properties[property_name] as RJSFSchema; + } else if (is_reference(resource)) { + let subresourceName = get_reference_name(resource); + let subresource = this.getResource(subresourceName); + return this.getDescendant(subresource, property_name); + } else if (is_union(resource)) { + for (const ref of resource.oneOf!) { + if (this.hasDescendant(ref as RJSFSchema, property_name)) { + return this.getDescendant(ref as RJSFSchema, property_name); + } + } + } else if (is_enum(resource)) { + for (const ref of resource.allOf!) { + if (this.hasDescendant(ref as RJSFSchema, property_name)) { + return this.getDescendant(ref as RJSFSchema, property_name); + } + } + } + throw new Error("property not found or Jsonschema format not implemented"); + } + + public pathExists = (resource: RJSFSchema, path: string): boolean => { + const pointFirstPosition = path.indexOf('.') + if (pointFirstPosition == -1) { + return this.hasDescendant(resource, path); + } + + return this.hasDescendant(resource, path.substring(0, pointFirstPosition)) + && this.pathExists( + this.getDescendant(resource, path.substring(0, pointFirstPosition)), + path.substring(pointFirstPosition + 1) + ); + } + + public getPropertyByPath = (resource: RJSFSchema, path: string): RJSFSchema => { + const pointFirstPosition = path.indexOf('.') + if (pointFirstPosition == -1) { + return this.getDescendant(resource, path); + } + + return this.getPropertyByPath( + this.getDescendant( + resource, + path.substring(0, pointFirstPosition) + ), path.substring(pointFirstPosition + 1) ); -} - -function get_property_by_path(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): RJSFSchema { - const pointFirstPosition = path.indexOf('.') - if (pointFirstPosition == -1) { - return get_descendant(rawSchemas, resource, path); } - - return get_property_by_path( - rawSchemas, - get_descendant( - rawSchemas, - resource, - path.substring(0, pointFirstPosition) - ), - path.substring(pointFirstPosition + 1) - ); } diff --git a/gui/rpk-gui/src/pages/firm/base-page/List.tsx b/gui/rpk-gui/src/pages/firm/base-page/List.tsx index 5fa1c3c..2eb69a3 100644 --- a/gui/rpk-gui/src/pages/firm/base-page/List.tsx +++ b/gui/rpk-gui/src/pages/firm/base-page/List.tsx @@ -7,6 +7,7 @@ import { Button } from "@mui/material"; import { GridColDef, GridValidRowModel } from "@mui/x-data-grid"; import { FirmContext } from "../../../contexts/FirmContext"; import CrudList from "../../../lib/crud/components/crud-list"; +import CrudFilters from "../../../lib/crud/components/crud-filters"; type ListProps = { resource: string, @@ -41,6 +42,10 @@ const List = (props: ListProps) => { + { navigate(`edit/${params.id}`) }}