Adding Crud filters form
This commit is contained in:
2
gui/rpk-gui/package-lock.json
generated
2
gui/rpk-gui/package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"@mui/lab": "^6.0.0-beta.14",
|
"@mui/lab": "^6.0.0-beta.14",
|
||||||
"@mui/material": "^6.1.7",
|
"@mui/material": "^6.1.7",
|
||||||
"@mui/x-data-grid": "^7.22.2",
|
"@mui/x-data-grid": "^7.22.2",
|
||||||
|
"@react-querybuilder/material": "^8.6.1",
|
||||||
"@refinedev/cli": "^2.16.21",
|
"@refinedev/cli": "^2.16.21",
|
||||||
"@refinedev/core": "^4.47.1",
|
"@refinedev/core": "^4.47.1",
|
||||||
"@refinedev/devtools": "^1.1.32",
|
"@refinedev/devtools": "^1.1.32",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hook-form": "^7.30.0",
|
"react-hook-form": "^7.30.0",
|
||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
|
"react-querybuilder": "^8.6.1",
|
||||||
"react-router": "^7.0.2"
|
"react-router": "^7.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"@mui/lab": "^6.0.0-beta.14",
|
"@mui/lab": "^6.0.0-beta.14",
|
||||||
"@mui/material": "^6.1.7",
|
"@mui/material": "^6.1.7",
|
||||||
"@mui/x-data-grid": "^7.22.2",
|
"@mui/x-data-grid": "^7.22.2",
|
||||||
|
"@react-querybuilder/material": "^8.6.1",
|
||||||
"@refinedev/cli": "^2.16.21",
|
"@refinedev/cli": "^2.16.21",
|
||||||
"@refinedev/core": "^4.47.1",
|
"@refinedev/core": "^4.47.1",
|
||||||
"@refinedev/devtools": "^1.1.32",
|
"@refinedev/devtools": "^1.1.32",
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hook-form": "^7.30.0",
|
"react-hook-form": "^7.30.0",
|
||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
|
"react-querybuilder": "^8.6.1",
|
||||||
"react-router": "^7.0.2"
|
"react-router": "^7.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
48
gui/rpk-gui/src/lib/crud/components/crud-filters.tsx
Normal file
48
gui/rpk-gui/src/lib/crud/components/crud-filters.tsx
Normal file
@@ -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<any[]>([])
|
||||||
|
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<RuleGroupType>({ combinator: 'and', rules: [] });
|
||||||
|
|
||||||
|
if (filtersLoading) {
|
||||||
|
return <CircularProgress />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryBuilderMaterial>
|
||||||
|
<QueryBuilder
|
||||||
|
fields={filtersSchema}
|
||||||
|
defaultQuery={query}
|
||||||
|
onQueryChange={setQuery} />
|
||||||
|
</QueryBuilderMaterial>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CrudFilters;
|
||||||
@@ -3,6 +3,7 @@ import i18n from '../../../i18n'
|
|||||||
import { JSONSchema7Definition } from "json-schema";
|
import { JSONSchema7Definition } from "json-schema";
|
||||||
import { GridColDef } from "@mui/x-data-grid";
|
import { GridColDef } from "@mui/x-data-grid";
|
||||||
import { GridColType } from "@mui/x-data-grid/models/colDef/gridColType";
|
import { GridColType } from "@mui/x-data-grid/models/colDef/gridColType";
|
||||||
|
import { Field as FilterField } from "react-querybuilder";
|
||||||
|
|
||||||
const API_URL = "/api/v1";
|
const API_URL = "/api/v1";
|
||||||
|
|
||||||
@@ -68,6 +69,92 @@ export const jsonschemaProvider = {
|
|||||||
getReadResourceColumns: async (resourceName: string): Promise<GridColDef[]> => {
|
getReadResourceColumns: async (resourceName: string): Promise<GridColDef[]> => {
|
||||||
return getColumns(`${resourceName}Read`)
|
return getColumns(`${resourceName}Read`)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getListFilters: async (resourceName: string, resourcePath: string): Promise<FilterField[]> => {
|
||||||
|
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<FilterField[]> {
|
||||||
|
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<GridColDef[]> => {
|
const getColumns = async (resourceName: string): Promise<GridColDef[]> => {
|
||||||
@@ -141,9 +228,7 @@ function buildColumns (rawSchemas: RJSFSchema, resourceName: string, prefix: str
|
|||||||
valueGetter = (value: string) => new Date(value)
|
valueGetter = (value: string) => new Date(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (prop.type == "string" && prop.format == "date-time") {
|
|
||||||
|
|
||||||
}
|
|
||||||
const column: GridColDef = {
|
const column: GridColDef = {
|
||||||
field: prefix ? `${prefix}.${prop_name}` : prop_name,
|
field: prefix ? `${prefix}.${prop_name}` : prop_name,
|
||||||
headerName: i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title) as string,
|
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);
|
return prop['$ref'].substring(prop['$ref'].lastIndexOf('/')+1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function has_descendant(rawSchemas:RJSFSchema, resource: RJSFSchema, property_name: string): boolean {
|
const JsonSchemaTraverser = class {
|
||||||
if (is_array(resource)) {
|
private rawSchemas: RJSFSchema;
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_descendant(rawSchemas: RJSFSchema, resource: RJSFSchema, property_name: string): RJSFSchema {
|
constructor(rawSchemas: RJSFSchema) {
|
||||||
if (is_array(resource) && property_name == 'items') {
|
this.rawSchemas = rawSchemas
|
||||||
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;
|
public getResource = (resourceName: string) => {
|
||||||
} else if (is_reference(resource)) {
|
if (this.rawSchemas.components.schemas[resourceName] === undefined) {
|
||||||
let subresourceName = get_reference_name(resource);
|
throw new Error(`Resource "${resourceName}" not found in schema.`);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} 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!) {
|
for (const ref of resource.allOf!) {
|
||||||
if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) {
|
return this.hasDescendant(ref as RJSFSchema, property_name);
|
||||||
return get_descendant(rawSchemas, ref as RJSFSchema, property_name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error("property not found or Jsonschema format not implemented");
|
throw new Error("Jsonschema format not implemented in property finder");
|
||||||
}
|
|
||||||
|
|
||||||
function path_exists(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): boolean{
|
|
||||||
const pointFirstPosition = path.indexOf('.')
|
|
||||||
if (pointFirstPosition == -1) {
|
|
||||||
return has_descendant(rawSchemas, resource, path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return has_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition))
|
public getDescendant = (resource: RJSFSchema, property_name: string): RJSFSchema => {
|
||||||
&& path_exists(
|
if (is_array(resource) && property_name == 'items') {
|
||||||
rawSchemas,
|
return resource.items as RJSFSchema;
|
||||||
get_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition)),
|
} 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)
|
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)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Button } from "@mui/material";
|
|||||||
import { GridColDef, GridValidRowModel } from "@mui/x-data-grid";
|
import { GridColDef, GridValidRowModel } from "@mui/x-data-grid";
|
||||||
import { FirmContext } from "../../../contexts/FirmContext";
|
import { FirmContext } from "../../../contexts/FirmContext";
|
||||||
import CrudList from "../../../lib/crud/components/crud-list";
|
import CrudList from "../../../lib/crud/components/crud-list";
|
||||||
|
import CrudFilters from "../../../lib/crud/components/crud-filters";
|
||||||
|
|
||||||
type ListProps = {
|
type ListProps = {
|
||||||
resource: string,
|
resource: string,
|
||||||
@@ -41,6 +42,10 @@ const List = <T extends GridValidRowModel>(props: ListProps) => {
|
|||||||
<Link to={"create"} >
|
<Link to={"create"} >
|
||||||
<Button>{t("buttons.create")}</Button>
|
<Button>{t("buttons.create")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
<CrudFilters
|
||||||
|
resourceName={schemaName}
|
||||||
|
resourcePath={`${resourceBasePath}/${resource}`}
|
||||||
|
/>
|
||||||
<CrudList
|
<CrudList
|
||||||
dataGridProps={dataGridProps}
|
dataGridProps={dataGridProps}
|
||||||
onRowClick={(params: any) => { navigate(`edit/${params.id}`) }}
|
onRowClick={(params: any) => { navigate(`edit/${params.id}`) }}
|
||||||
|
|||||||
Reference in New Issue
Block a user