Compare commits
8 Commits
9fd201c10a
...
9aac1d3e34
| Author | SHA1 | Date | |
|---|---|---|---|
| 9aac1d3e34 | |||
| 7aced9477c | |||
| 5a0327c930 | |||
| 18e4fcea28 | |||
| 239ab2e241 | |||
| 9fcead8c95 | |||
| 73871ae04e | |||
| c35c63b421 |
@@ -214,11 +214,11 @@ class ContractDraftFilters(FilterSchema):
|
||||
|
||||
class Constants(Filter.Constants):
|
||||
model = ContractDraft
|
||||
search_model_fields = ["label", "status"]
|
||||
search_model_fields = ["label"]
|
||||
|
||||
class ContractFilters(FilterSchema):
|
||||
status: Optional[str] = None
|
||||
|
||||
class Constants(Filter.Constants):
|
||||
model = Contract
|
||||
search_model_fields = ["label", "status"]
|
||||
search_model_fields = ["label"]
|
||||
|
||||
@@ -155,4 +155,11 @@ class Filter(BaseFilterModel):
|
||||
|
||||
class FilterSchema(Filter):
|
||||
label__ilike: Optional[str] = None
|
||||
search: Optional[str] = None
|
||||
order_by: Optional[list[str]] = None
|
||||
created_at__lte: Optional[str] = None
|
||||
created_at__gte: Optional[str] = None
|
||||
created_by__in: Optional[str] = None
|
||||
updated_at__lte: Optional[str] = None
|
||||
updated_at__gte: Optional[str] = None
|
||||
updated_by__in: Optional[str] = None
|
||||
|
||||
1305
gui/rpk-gui/package-lock.json
generated
1305
gui/rpk-gui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
"@mui/lab": "^6.0.0-beta.14",
|
||||
"@mui/material": "^6.1.7",
|
||||
"@mui/x-data-grid": "^7.22.2",
|
||||
"@mui/x-date-pickers": "^8.3.0",
|
||||
"@refinedev/cli": "^2.16.21",
|
||||
"@refinedev/core": "^4.47.1",
|
||||
"@refinedev/devtools": "^1.1.32",
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useTranslation } from "@refinedev/core";
|
||||
import { useTheme } from "@mui/material";
|
||||
import * as locales from '@mui/material/locale';
|
||||
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
|
||||
type SupportedLocales = keyof typeof locales;
|
||||
|
||||
@@ -10,11 +12,13 @@ export const I18nTheme: React.FC<PropsWithChildren> = ({ children }: PropsWithCh
|
||||
const { getLocale } = useTranslation();
|
||||
const theme = useTheme()
|
||||
|
||||
const themeWithLocale = createTheme(theme, locales[getLocale() as SupportedLocales])
|
||||
|
||||
const locale = getLocale() || "en"
|
||||
const themeWithLocale = createTheme(theme, locales[locale as SupportedLocales])
|
||||
return (
|
||||
<ThemeProvider theme={themeWithLocale}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={locale.slice(0,2)} >
|
||||
{ children }
|
||||
</LocalizationProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
95
gui/rpk-gui/src/lib/crud/components/crud-filters.tsx
Normal file
95
gui/rpk-gui/src/lib/crud/components/crud-filters.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { jsonschemaProvider } from "../providers/jsonschema-provider";
|
||||
import { Accordion, AccordionDetails, AccordionSummary, CircularProgress } from "@mui/material";
|
||||
import FilterForm from "../../filter-form/components/filter-form";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { GridExpandMoreIcon } from "@mui/x-data-grid";
|
||||
|
||||
export type OnChangeValue = {
|
||||
search: string | null
|
||||
filters: {[filter: string]: {[op: string]: string}}
|
||||
}
|
||||
|
||||
type CrudFiltersProps = {
|
||||
resourceName: string
|
||||
resourcePath: string
|
||||
onChange: (value: OnChangeValue) => void
|
||||
}
|
||||
|
||||
const CrudFilters = (props: CrudFiltersProps) => {
|
||||
const { resourceName, resourcePath, onChange } = props
|
||||
|
||||
const [hasSearch, setHasSearch] = useState(false)
|
||||
const [filtersSchema, setFiltersSchema] = useState<any[]>([])
|
||||
const [filtersLoading, setFiltersLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
const fetchSchema = async () => {
|
||||
try {
|
||||
setHasSearch(await jsonschemaProvider.hasSearch(resourcePath))
|
||||
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();
|
||||
}, []);
|
||||
|
||||
if (filtersLoading) {
|
||||
return <CircularProgress />
|
||||
}
|
||||
|
||||
let currentValue = {
|
||||
search: "",
|
||||
filters: {}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasSearch &&
|
||||
<SearchFilter value="" onChange={(value) => {
|
||||
currentValue.search = value;
|
||||
onChange(currentValue);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
<Accordion>
|
||||
<AccordionSummary
|
||||
expandIcon={<>Advanced filters<GridExpandMoreIcon /></>}
|
||||
/>
|
||||
<AccordionDetails>
|
||||
<FilterForm fields={filtersSchema} values={{}} onChange={(value) => {
|
||||
currentValue.filters = value;
|
||||
onChange(currentValue);
|
||||
}}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CrudFilters;
|
||||
|
||||
type SearchFilter = {
|
||||
value: string
|
||||
onChange: (value: any) => void
|
||||
}
|
||||
|
||||
const SearchFilter = (props: SearchFilter) => {
|
||||
const {value, onChange} = props;
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label="schemas.search"
|
||||
fullWidth={true}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
defaultValue={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -40,7 +40,6 @@ const CrudList = <T extends GridValidRowModel>(props: CrudListProps) => {
|
||||
const resourceColumns = await jsonschemaProvider.getReadResourceColumns(schemaName)
|
||||
const definedColumns = computeColumnSchema<T>(columnDefinitions, resourceColumns)
|
||||
setColumnSchema(definedColumns);
|
||||
console.log(resourceColumns);
|
||||
setSchemaLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
|
||||
@@ -68,19 +68,130 @@ export const jsonschemaProvider = {
|
||||
getReadResourceColumns: async (resourceName: string): Promise<GridColDef[]> => {
|
||||
return getColumns(`${resourceName}Read`)
|
||||
},
|
||||
|
||||
getListFilters: async (resourceName: string, resourcePath: string): Promise<FilterField[]> => {
|
||||
return getFilters(resourceName, resourcePath);
|
||||
},
|
||||
|
||||
hasSearch: async (resourcePath: string): Promise<boolean> => {
|
||||
return hasSearch(resourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
const getResourceSchema = async (resourceName: string): Promise<CrudRJSFSchema> => {
|
||||
return buildResource(await getJsonschema(), resourceName)
|
||||
}
|
||||
|
||||
const getColumns = async (resourceName: string): Promise<GridColDef[]> => {
|
||||
return buildColumns(await getJsonschema(), resourceName)
|
||||
}
|
||||
|
||||
function buildColumns (rawSchemas: RJSFSchema, resourceName: string, prefix: string|undefined = undefined): GridColDef[] {
|
||||
if (rawSchemas.components.schemas[resourceName] === undefined) {
|
||||
throw new Error(`Resource "${resourceName}" not found in schema.`);
|
||||
const getFilters = async (resourceName: string, resourcePath: string): Promise<FilterField[]> => {
|
||||
return buildFilters(await getJsonschema(), resourceName, resourcePath);
|
||||
}
|
||||
|
||||
const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, ""));
|
||||
let resource = rawSchemas.components.schemas[resourceName];
|
||||
type PathParameter = {
|
||||
in: string,
|
||||
name:string
|
||||
}
|
||||
|
||||
type PathSchema = {
|
||||
parameters: PathParameter[]
|
||||
}
|
||||
|
||||
type FilterField = {
|
||||
name: string,
|
||||
label: string,
|
||||
type: string,
|
||||
operators: { name: string, label: string }[]
|
||||
}
|
||||
|
||||
type Operator = {
|
||||
operator: string,
|
||||
fieldName: string,
|
||||
}
|
||||
|
||||
async function hasSearch(resourcePath: string): Promise<boolean> {
|
||||
const jst = new JsonSchemaTraverser(await getJsonschema());
|
||||
const pathSchema = jst.getPath(resourcePath);
|
||||
for (const param of pathSchema.parameters) {
|
||||
if (param.name == "search") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function buildFilters(rawSchema: RJSFSchema, resourceName: string, resourcePath: string): FilterField[] {
|
||||
const shortResourceName = shortenResourceName(resourceName);
|
||||
const jst = new JsonSchemaTraverser(rawSchema);
|
||||
const pathSchema = jst.getPath(resourcePath);
|
||||
|
||||
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: getPropertyI18nLabel(shortResourceName, fieldName),
|
||||
type: getFieldFilterType(fieldName, field),
|
||||
operators: [{ name: operator, label: operator }]
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
filters[seen[fieldName]].operators?.push({ name: operator, label: operator });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
function processParamName(param: PathParameter): Operator {
|
||||
const nameParts = param.name.split("__")
|
||||
|
||||
return {
|
||||
operator: nameParts.pop() as string,
|
||||
fieldName: nameParts.join("."),
|
||||
}
|
||||
}
|
||||
|
||||
const getFieldFilterType = (fieldName: string, field: RJSFSchema): string => {
|
||||
if (fieldName == "created_by" || fieldName == "updated_by") {
|
||||
return "author";
|
||||
} else if (Array.isArray(field)) {
|
||||
let enumValues = [];
|
||||
for (const f of field) {
|
||||
enumValues.push(f.const)
|
||||
}
|
||||
return `enum(${enumValues.join("|")})`
|
||||
} else if (field.hasOwnProperty('type')) {
|
||||
if (field.type == "string" && field.format == "date-time") {
|
||||
return "dateTime";
|
||||
}
|
||||
return field.type as string;
|
||||
} else if (field.hasOwnProperty('anyOf') && field.anyOf) {
|
||||
for (const prop of field.anyOf) {
|
||||
if (typeof prop != "boolean" && prop.type != "null") {
|
||||
return prop.type as string;
|
||||
}
|
||||
|
||||
}
|
||||
return "null";
|
||||
}
|
||||
throw "Unimplemented field type"
|
||||
}
|
||||
|
||||
function buildColumns (rawSchemas: RJSFSchema, resourceName: string, prefix: string|undefined = undefined): GridColDef[] {
|
||||
const shortResourceName = shortenResourceName(resourceName);
|
||||
const jst = new JsonSchemaTraverser(rawSchemas);
|
||||
let resource = structuredClone(jst.getResource(resourceName));
|
||||
|
||||
let result: GridColDef[] = [];
|
||||
for (const prop_name in resource.properties) {
|
||||
let prop = resource.properties[prop_name];
|
||||
@@ -141,9 +252,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,
|
||||
@@ -156,42 +265,32 @@ function buildColumns (rawSchemas: RJSFSchema, resourceName: string, prefix: str
|
||||
return result;
|
||||
}
|
||||
|
||||
const getResourceSchema = async (resourceName: string): Promise<CrudRJSFSchema> => {
|
||||
return buildResource(await getJsonschema(), resourceName)
|
||||
}
|
||||
|
||||
function convertCamelToSnake(str: string): string {
|
||||
return str.replace(/([a-zA-Z])(?=[A-Z])/g,'$1_').toLowerCase()
|
||||
}
|
||||
|
||||
function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
|
||||
if (rawSchemas.components.schemas[resourceName] === undefined) {
|
||||
throw new Error(`Resource "${resourceName}" not found in schema.`);
|
||||
}
|
||||
const shortResourceName = shortenResourceName(resourceName);
|
||||
const jst = new JsonSchemaTraverser(rawSchemas);
|
||||
let resource = structuredClone(jst.getResource(resourceName));
|
||||
|
||||
const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, ""));
|
||||
let resource = structuredClone(rawSchemas.components.schemas[resourceName]);
|
||||
resource.components = { schemas: {} };
|
||||
for (let prop_name in resource.properties) {
|
||||
let prop = resource.properties[prop_name];
|
||||
|
||||
if (is_reference(prop)) {
|
||||
resolveReference(rawSchemas, resource, prop);
|
||||
buildReference(rawSchemas, resource, prop);
|
||||
} else if (is_union(prop)) {
|
||||
const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf;
|
||||
for (let i in union) {
|
||||
if (is_reference(union[i])) {
|
||||
resolveReference(rawSchemas, resource, union[i]);
|
||||
buildReference(rawSchemas, resource, union[i]);
|
||||
}
|
||||
}
|
||||
} else if (is_enum(prop)) {
|
||||
for (let i in prop.allOf) {
|
||||
if (is_reference(prop.allOf[i])) {
|
||||
resolveReference(rawSchemas, resource, prop.allOf[i]);
|
||||
buildReference(rawSchemas, resource, prop.allOf[i]);
|
||||
}
|
||||
}
|
||||
} else if (is_array(prop) && is_reference(prop.items)) {
|
||||
resolveReference(rawSchemas, resource, prop.items);
|
||||
buildReference(rawSchemas, resource, prop.items);
|
||||
}
|
||||
|
||||
if (prop.hasOwnProperty("title")) {
|
||||
@@ -205,16 +304,7 @@ function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
let rawSchema: RJSFSchema;
|
||||
const getJsonschema = async (): Promise<RJSFSchema> => {
|
||||
if (rawSchema === undefined) {
|
||||
const response = await fetch(`${API_URL}/openapi.json`,);
|
||||
rawSchema = await response.json();
|
||||
}
|
||||
return rawSchema;
|
||||
}
|
||||
|
||||
function resolveReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
|
||||
function buildReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
|
||||
const subresourceName = get_reference_name(prop_reference);
|
||||
const subresource = buildResource(rawSchemas, subresourceName);
|
||||
resource.components.schemas[subresourceName] = subresource;
|
||||
@@ -283,81 +373,152 @@ 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 {
|
||||
function convertCamelToSnake(str: string): string {
|
||||
return str.replace(/([a-zA-Z])(?=[A-Z])/g,'$1_').toLowerCase()
|
||||
}
|
||||
|
||||
function shortenResourceName(resourceName: string) {
|
||||
return convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, ""));
|
||||
}
|
||||
|
||||
function getPropertyI18nLabel(shortResourceName: string, fieldName: string): string {
|
||||
if (meta_fields.indexOf(fieldName) > -1) {
|
||||
return i18n.t(`schemas.${convertCamelToSnake(fieldName)}`);
|
||||
}
|
||||
const path = `schemas.${shortResourceName}.${convertCamelToSnake(fieldName)}`
|
||||
return i18n.t(path)
|
||||
}
|
||||
|
||||
let rawSchema: RJSFSchema;
|
||||
const getJsonschema = async (): Promise<RJSFSchema> => {
|
||||
if (rawSchema === undefined) {
|
||||
const response = await fetch(`${API_URL}/openapi.json`,);
|
||||
rawSchema = await response.json();
|
||||
}
|
||||
return rawSchema;
|
||||
}
|
||||
|
||||
const JsonSchemaTraverser = class {
|
||||
private rawSchemas: RJSFSchema;
|
||||
|
||||
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.`);
|
||||
}
|
||||
|
||||
return this.rawSchemas.components.schemas[resourceName]
|
||||
}
|
||||
|
||||
public getPath = (resourcePath: string) => {
|
||||
const resourceParts = `/${resourcePath}/`.split("/");
|
||||
let pathSchema: PathSchema|undefined;
|
||||
for (const path in this.rawSchemas.paths) {
|
||||
if (this.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 = this.rawSchemas.paths[path].get
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pathSchema !== undefined) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pathSchema === undefined) {
|
||||
throw ("Path not found in schema");
|
||||
}
|
||||
return pathSchema
|
||||
}
|
||||
|
||||
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 has_descendant(rawSchemas, buildResource(rawSchemas, subresourceName), property_name);
|
||||
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 has_descendant(rawSchemas, ref as RJSFSchema, property_name)
|
||||
return this.hasDescendant(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);
|
||||
return this.hasDescendant(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 {
|
||||
public getDescendant = (resource: RJSFSchema, property_name: string): RJSFSchema | 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);
|
||||
let subresource = this.getResource(subresourceName);
|
||||
return this.getDescendant(subresource, property_name);
|
||||
} else if (is_union(resource)) {
|
||||
let descendants: RJSFSchema[] = [];
|
||||
for (const ref of resource.oneOf!) {
|
||||
if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) {
|
||||
return get_descendant(rawSchemas, ref as RJSFSchema, property_name);
|
||||
if (this.hasDescendant(ref as RJSFSchema, property_name)) {
|
||||
descendants.push(this.getDescendant(ref as RJSFSchema, property_name));
|
||||
}
|
||||
}
|
||||
if (descendants.length > 0) {
|
||||
return descendants;
|
||||
}
|
||||
} 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);
|
||||
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");
|
||||
}
|
||||
|
||||
function path_exists(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): boolean{
|
||||
public pathExists = (resource: RJSFSchema, path: string): boolean => {
|
||||
const pointFirstPosition = path.indexOf('.')
|
||||
if (pointFirstPosition == -1) {
|
||||
return has_descendant(rawSchemas, resource, path);
|
||||
return this.hasDescendant(resource, path);
|
||||
}
|
||||
|
||||
return has_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition))
|
||||
&& path_exists(
|
||||
rawSchemas,
|
||||
get_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition)),
|
||||
return this.hasDescendant(resource, path.substring(0, pointFirstPosition))
|
||||
&& this.pathExists(
|
||||
this.getDescendant(resource, path.substring(0, pointFirstPosition)),
|
||||
path.substring(pointFirstPosition + 1)
|
||||
);
|
||||
}
|
||||
|
||||
function get_property_by_path(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): RJSFSchema {
|
||||
public getPropertyByPath = (resource: RJSFSchema, path: string): RJSFSchema => {
|
||||
const pointFirstPosition = path.indexOf('.')
|
||||
if (pointFirstPosition == -1) {
|
||||
return get_descendant(rawSchemas, resource, path);
|
||||
return this.getDescendant(resource, path);
|
||||
}
|
||||
|
||||
return get_property_by_path(
|
||||
rawSchemas,
|
||||
get_descendant(
|
||||
rawSchemas,
|
||||
return this.getPropertyByPath(
|
||||
this.getDescendant(
|
||||
resource,
|
||||
path.substring(0, pointFirstPosition)
|
||||
),
|
||||
path.substring(pointFirstPosition + 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
184
gui/rpk-gui/src/lib/filter-form/components/filter-form.tsx
Normal file
184
gui/rpk-gui/src/lib/filter-form/components/filter-form.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import { FirmContext } from "../../../contexts/FirmContext";
|
||||
import { useContext } from "react";
|
||||
import { Box, Grid2, styled } from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export type FilterField = {
|
||||
name: string
|
||||
label: string
|
||||
type: string
|
||||
}
|
||||
|
||||
type FilterFormProps = {
|
||||
values: {[field_name: string]: { [operator: string]: string }}
|
||||
fields: FilterField[]
|
||||
onChange: (value: any) => void
|
||||
}
|
||||
|
||||
const FilterForm = (props: FilterFormProps) => {
|
||||
const { fields, values, onChange } = props;
|
||||
|
||||
let currentValue = values
|
||||
|
||||
const formField = fields.filter(f => f.name != "search").map((f, index) =>
|
||||
<>
|
||||
{ f.name == "created_at" && <Box width="100%" key={`created_at_break-${index}`} /> }
|
||||
<Grid2 size={6} key={`${f.name}-${index}`} >
|
||||
<FilterFormField
|
||||
field={f}
|
||||
value={values.hasOwnProperty(f.name) ? values[f.name] : {}}
|
||||
onChange={(value) => {
|
||||
for (const op in value) {
|
||||
if (value[op] == null || value[op] == "") {
|
||||
if (currentValue.hasOwnProperty(f.name)) {
|
||||
if (currentValue[f.name].hasOwnProperty(op)) {
|
||||
delete currentValue[f.name][op];
|
||||
}
|
||||
if (Object.entries(currentValue[f.name]).length == 0) {
|
||||
delete currentValue[f.name];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (! currentValue.hasOwnProperty(f.name)) {
|
||||
currentValue[f.name] = {};
|
||||
}
|
||||
currentValue[f.name][op] = value[op];
|
||||
}
|
||||
}
|
||||
onChange(currentValue);
|
||||
}}
|
||||
/>
|
||||
</Grid2>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<Grid2 container spacing={2}>
|
||||
{formField}
|
||||
</Grid2>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
type OperatorValue = { [operator: string]: string }
|
||||
|
||||
type FilterFormFieldProps = {
|
||||
field: FilterField
|
||||
value: OperatorValue
|
||||
onChange: (value: { [operator: string]: string | null }) => void
|
||||
}
|
||||
|
||||
const FilterFormField = (props: FilterFormFieldProps) => {
|
||||
const { field, value, onChange } = props;
|
||||
|
||||
if (field.type == "string") {
|
||||
const defaultValue = value.hasOwnProperty('ilike') ? value['ilike'] : undefined
|
||||
return (
|
||||
<TextField
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
defaultValue={defaultValue}
|
||||
fullWidth={true}
|
||||
onChange={(event) => onChange({"ilike": event.target.value})}
|
||||
/>
|
||||
)
|
||||
} else if (field.type == "dateTime") {
|
||||
return (
|
||||
<FilterFieldDateRange field={field} value={value} onChange={onChange} />
|
||||
)
|
||||
} else if (field.type == "author") {
|
||||
return (
|
||||
<FilterFieldAuthor field={field} value={value} onChange={onChange} />
|
||||
);
|
||||
} else if (field.type.slice(0, 4) == "enum") {
|
||||
const values = field.type.slice(5,-1).split("|");
|
||||
const defaultValue = value.hasOwnProperty("in") ? [{
|
||||
value: value["in"],
|
||||
label: value["in"]
|
||||
}] : undefined;
|
||||
return (
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={values.map(opt => {
|
||||
return {
|
||||
value: opt,
|
||||
label: opt
|
||||
}
|
||||
})}
|
||||
onChange={(event, value) => onChange({ "in": value.map(v => v.value).join(",") })}
|
||||
getOptionLabel={(option) => option.label}
|
||||
defaultValue={defaultValue}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={field.label}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
throw("Unsupported field filter type");
|
||||
}
|
||||
|
||||
|
||||
type FilterFieldAuthorProp = FilterFormFieldProps
|
||||
|
||||
const FilterFieldAuthor = (props: FilterFieldAuthorProp) => {
|
||||
const { field, onChange } = props;
|
||||
const { partnerMap } = useContext(FirmContext)
|
||||
|
||||
if (partnerMap == undefined) {
|
||||
throw "Can't use author filter outside of the context of a firm";
|
||||
}
|
||||
|
||||
let options = []
|
||||
for(let key of Array.from(partnerMap.keys()) ) {
|
||||
options.push({
|
||||
id: key,
|
||||
label: partnerMap.get(key)
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
multiple
|
||||
renderInput={(params) => <TextField {...params} label={field.label} />}
|
||||
options={options}
|
||||
onChange={(event, value) => onChange({ "in": value.length == 0 ? null : value.map(v => v.id).join(",") })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const FilterFieldDateRange = (props: FilterFormFieldProps) => {
|
||||
const { field, value, onChange } = props;
|
||||
|
||||
const defaultAfterValue = value.hasOwnProperty('gte') ? dayjs(value['gte']) : undefined;
|
||||
const defaultBeforeValue = value.hasOwnProperty('lte') ? dayjs(value['lte']) : undefined;
|
||||
|
||||
return (
|
||||
<Stack direction="row">
|
||||
<DateTimePicker
|
||||
name={field.name}
|
||||
label={`${field.label} After:`}
|
||||
slotProps={{ textField: { fullWidth: true }, field: { clearable: true }}}
|
||||
defaultValue={defaultAfterValue}
|
||||
onChange={(value) => onChange({'gte': value === null ? null : value.toJSON()})}
|
||||
/>
|
||||
<DateTimePicker
|
||||
name={field.name}
|
||||
label={`${field.label} Before:`}
|
||||
slotProps={{ textField: { fullWidth: true }, field: { clearable: true } }}
|
||||
defaultValue={defaultBeforeValue}
|
||||
onChange={(value) => onChange({'lte': value === null ? null : value.toJSON()})}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterForm;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useContext } from "react";
|
||||
import { Link, useNavigate } from "react-router"
|
||||
import { UiSchema } from "@rjsf/utils";
|
||||
import { useTranslation } from "@refinedev/core";
|
||||
import { CrudFilter, useTranslation } from "@refinedev/core";
|
||||
import { List as RefineList, useDataGrid } from "@refinedev/mui";
|
||||
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, { OnChangeValue } from "../../../lib/crud/components/crud-filters";
|
||||
import { CrudOperators } from "@refinedev/core/src/contexts/data/types";
|
||||
|
||||
type ListProps = {
|
||||
resource: string,
|
||||
@@ -27,13 +29,34 @@ const List = <T extends GridValidRowModel>(props: ListProps) => {
|
||||
const { currentFirm } = useContext(FirmContext);
|
||||
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
|
||||
|
||||
const { dataGridProps, tableQueryResult } = useDataGrid<T>({
|
||||
const { dataGridProps, tableQuery, setFilters } = useDataGrid<T>({
|
||||
resource: `${resourceBasePath}/${resource}`,
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (tableQueryResult.error?.status == 404) {
|
||||
throw tableQueryResult.error
|
||||
if (tableQuery.error?.status == 404) {
|
||||
throw tableQuery.error
|
||||
}
|
||||
|
||||
const onFilterChange = (value: OnChangeValue) => {
|
||||
let newFilters: CrudFilter[] = []
|
||||
if (value.search != null) {
|
||||
newFilters.push({
|
||||
field: "search",
|
||||
operator: "eq",
|
||||
value: value.search
|
||||
})
|
||||
}
|
||||
for (const filterName in value.filters) {
|
||||
for (const operator in value.filters[filterName]) {
|
||||
newFilters.push({
|
||||
field: filterName,
|
||||
operator: operator as Exclude<CrudOperators, "or" | "and">,
|
||||
value: value.filters[filterName][operator]
|
||||
})
|
||||
}
|
||||
}
|
||||
setFilters(newFilters);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -41,6 +64,11 @@ const List = <T extends GridValidRowModel>(props: ListProps) => {
|
||||
<Link to={"create"} >
|
||||
<Button>{t("buttons.create")}</Button>
|
||||
</Link>
|
||||
<CrudFilters
|
||||
resourceName={schemaName}
|
||||
resourcePath={`${resourceBasePath}/${resource}`}
|
||||
onChange={onFilterChange}
|
||||
/>
|
||||
<CrudList
|
||||
dataGridProps={dataGridProps}
|
||||
onRowClick={(params: any) => { navigate(`edit/${params.id}`) }}
|
||||
|
||||
@@ -61,8 +61,12 @@ const dataProvider: DataProvider = {
|
||||
|
||||
if (filters && filters.length > 0) {
|
||||
filters.forEach((filter) => {
|
||||
if ("field" in filter && filter.value && filter.operator === "contains") {
|
||||
params.append(filter.field + "__ilike", filter.value);
|
||||
if ("field" in filter) {
|
||||
if (filter.field == "search") {
|
||||
params.append("search", filter.value);
|
||||
} else {
|
||||
params.append(`${filter.field.replace(".", "__")}__${filter.operator}`, filter.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user