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 ad45c2b..3fc4e0c 100644 --- a/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx +++ b/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx @@ -1,6 +1,7 @@ import { RJSFSchema } from '@rjsf/utils'; import i18n from '../../../i18n' import { JSONSchema7Definition } from "json-schema"; +import { GridColDef } from "@mui/x-data-grid"; const API_URL = "/api/v1"; @@ -51,6 +52,10 @@ export const jsonschemaProvider = { return readSchema }, + getReadResourceSchema: async (resourceName: string): Promise => { + return getResourceSchema(`${resourceName}Read`) + }, + getUpdateResourceSchema: async (resourceName: string): Promise => { return getResourceSchema(`${resourceName}Update`) }, @@ -58,21 +63,88 @@ export const jsonschemaProvider = { getCreateResourceSchema: async (resourceName: string): Promise => { return getResourceSchema(`${resourceName}Create`) }, + + getReadResourceColumns: async (resourceName: string): Promise => { + return getColumns(`${resourceName}Read`) + }, +} + +const getColumns = async (resourceName: string): Promise => { + 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 shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, "")); + let resource = rawSchemas.components.schemas[resourceName]; + let result: GridColDef[] = []; + for (const prop_name in resource.properties) { + let prop = resource.properties[prop_name]; + + if (is_reference(prop)) { + const subresourceName = get_reference_name(prop); + result = result.concat(buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name)) + } else if (is_union(prop)) { + const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf; + let seen = new Set(); + for (let i in union) { + if (is_reference(union[i])) { + const subresourceName = get_reference_name(union[i]); + const subcolumns = buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name); + for (const s of subcolumns) { + if (! seen.has(s.field)) { + result.push(s); + seen.add(s.field); + } + } + } + } + } else if (is_enum(prop)) { + let seen = new Set(); + for (let i in prop.allOf) { + if (is_reference(prop.allOf[i])) { + const subresourceName = get_reference_name(prop.allOf[i]); + const subcolumns = buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name); + for (const s of subcolumns) { + if (! seen.has(s.field)) { + result.push(s); + seen.add(s.field); + } + } + } + } + } else if (is_array(prop) && is_reference(prop.items)) { + const subresourceName = get_reference_name(prop.items); + result = result.concat(buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name)) + } else { + let column: GridColDef = { + field: prefix ? `${prefix}.${prop_name}` : prop_name, + headerName: i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title) as string, + valueGetter: (value: any, row: any ) => { + if (prefix === undefined) { + return value; + } + + let parent = row; + for (const column of prefix.split(".")) { + parent = parent[column]; + } + return parent ? parent[prop_name] : ""; + } + } + result.push(column); + } + } + return result; } const getResourceSchema = async (resourceName: string): Promise => { return buildResource(await getJsonschema(), resourceName) } -let rawSchema: RJSFSchema; -const getJsonschema = async (): Promise => { - if (rawSchema === undefined) { - const response = await fetch(`${API_URL}/openapi.json`,); - rawSchema = await response.json(); - } - return rawSchema; -} - function convertCamelToSnake(str: string): string { return str.replace(/([a-zA-Z])(?=[A-Z])/g,'$1_').toLowerCase() } @@ -82,7 +154,7 @@ function buildResource(rawSchemas: RJSFSchema, resourceName: string) { throw new Error(`Resource "${resourceName}" not found in schema.`); } - const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|Create|Update)$/g, "")); + 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) { @@ -118,6 +190,15 @@ function buildResource(rawSchemas: RJSFSchema, resourceName: string) { return resource; } +let rawSchema: RJSFSchema; +const getJsonschema = async (): Promise => { + 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) { const subresourceName = get_reference_name(prop_reference); const subresource = buildResource(rawSchemas, subresourceName); diff --git a/gui/rpk-gui/src/pages/firm/ContractRoutes.tsx b/gui/rpk-gui/src/pages/firm/ContractRoutes.tsx index 25ccf66..d7ec645 100644 --- a/gui/rpk-gui/src/pages/firm/ContractRoutes.tsx +++ b/gui/rpk-gui/src/pages/firm/ContractRoutes.tsx @@ -24,9 +24,9 @@ export const ContractRoutes = () => { const ListContract = () => { const columns = [ - { field: "label", headerName: "Label", flex: 1 }, + { field: "label", column: { flex: 1 }}, ]; - return resource={`contracts`} columns={columns} /> + return resource={`contracts`} schemaName={"Contract"} columns={columns} /> } const EditContract = () => { diff --git a/gui/rpk-gui/src/pages/firm/DraftRoutes.tsx b/gui/rpk-gui/src/pages/firm/DraftRoutes.tsx index 58ae011..b157a51 100644 --- a/gui/rpk-gui/src/pages/firm/DraftRoutes.tsx +++ b/gui/rpk-gui/src/pages/firm/DraftRoutes.tsx @@ -29,9 +29,9 @@ export const DraftRoutes = () => { const ListDraft = () => { const columns = [ - { field: "label", headerName: "Label", flex: 1 }, + { field: "label", column: { flex: 1 }}, ]; - return resource={`contracts/drafts`} columns={columns} /> + return resource={`contracts/drafts`} columns={columns} schemaName={"Contract"} /> } const EditDraft = () => { diff --git a/gui/rpk-gui/src/pages/firm/EntityRoutes.tsx b/gui/rpk-gui/src/pages/firm/EntityRoutes.tsx index e39e2bd..97f9521 100644 --- a/gui/rpk-gui/src/pages/firm/EntityRoutes.tsx +++ b/gui/rpk-gui/src/pages/firm/EntityRoutes.tsx @@ -1,5 +1,4 @@ import { Route, Routes } from "react-router"; -import { useTranslation } from "@refinedev/core"; import List from "./base-page/List"; import Edit from "./base-page/Edit"; import New from "./base-page/New"; @@ -22,12 +21,12 @@ export const EntityRoutes = () => { } const ListEntity = () => { - const { translate: t } = useTranslation(); const columns = [ - { field: "entity_data", headerName: t("schemas.type"), width: 110, valueFormatter: ({ type }: {type: string}) => type }, - { field: "label", headerName: t("schemas.label"), flex: 1 }, + { field: "entity_data.type", column: { width: 110 }}, + { field: "label", column: { flex: 1 }}, + { field: "updated_at", column: { flex: 1 }}, ]; - return resource={`entities`} columns={columns} /> + return resource={`entities`} schemaName={"Entity"} columns={columns} /> } const EditEntity = () => { diff --git a/gui/rpk-gui/src/pages/firm/ProvisionRoutes.tsx b/gui/rpk-gui/src/pages/firm/ProvisionRoutes.tsx index 5c136d5..5272b47 100644 --- a/gui/rpk-gui/src/pages/firm/ProvisionRoutes.tsx +++ b/gui/rpk-gui/src/pages/firm/ProvisionRoutes.tsx @@ -21,9 +21,9 @@ export const ProvisionRoutes = () => { const ListProvision = () => { const columns = [ - { field: "label", headerName: "Label", flex: 1 }, + { field: "label", column: { flex: 1 }}, ]; - return resource={`templates/provisions`} columns={columns} /> + return resource={`templates/provisions`} schemaName={"ProvisionTemplate"} columns={columns} /> } const EditProvision = () => { diff --git a/gui/rpk-gui/src/pages/firm/TemplateRoutes.tsx b/gui/rpk-gui/src/pages/firm/TemplateRoutes.tsx index 3a1f42d..c4066db 100644 --- a/gui/rpk-gui/src/pages/firm/TemplateRoutes.tsx +++ b/gui/rpk-gui/src/pages/firm/TemplateRoutes.tsx @@ -20,9 +20,9 @@ export const TemplateRoutes = () => { const ListTemplate = () => { const columns = [ - { field: "label", headerName: "Label", flex: 1 }, + { field: "label", column: { flex: 1 }}, ]; - return resource={`templates/contracts`} columns={columns} /> + return resource={`templates/contracts`} schemaName={"ContractTemplate"} columns={columns} /> } const EditTemplate = () => { 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 665f004..531cbc9 100644 --- a/gui/rpk-gui/src/pages/firm/base-page/List.tsx +++ b/gui/rpk-gui/src/pages/firm/base-page/List.tsx @@ -1,21 +1,55 @@ +import { Link, useNavigate } from "react-router" import { UiSchema } from "@rjsf/utils"; import { useTranslation } from "@refinedev/core"; import { List as RefineList, useDataGrid } from "@refinedev/mui"; -import { DataGrid, GridColDef, GridValidRowModel } from "@mui/x-data-grid"; -import { Link, useNavigate } from "react-router" -import React, { useContext } from "react"; -import { Button } from "@mui/material"; +import { DataGrid, GridColDef, GridValidRowModel, GridColumnVisibilityModel } from "@mui/x-data-grid"; +import React, { useContext, useEffect, useState } from "react"; +import { Button, CircularProgress } from "@mui/material"; import { FirmContext } from "../../../contexts/FirmContext"; +import { jsonschemaProvider } from "../../../lib/crud/providers/jsonschema-provider"; -type ListProps = { +type ListProps = { resource: string, - columns: GridColDef[], - schemaName?: string, + columns: ColumnDefinition[], + schemaName: string, uiSchema?: UiSchema, } -const List = (props: ListProps) => { - const { resource, columns } = props; +type ColumnSchema = { + columns: GridColDef[], + columnVisibilityModel: GridColumnVisibilityModel +} + +type ColumnDefinition = { + field: string, + column: Partial, + hide?: boolean +} + +function computeColumnSchema(definitionColumns: ColumnDefinition[], resourceColumns: GridColDef[]): ColumnSchema { + //reorder resourceColumns as in definition + definitionColumns.slice().reverse().forEach(first => { + resourceColumns.sort(function(x,y){ return x.field == first.field ? -1 : y.field == first.field ? 1 : 0; }); + }) + + let visibilityModel: GridColumnVisibilityModel = {} + resourceColumns.forEach((resource, index) =>{ + visibilityModel[resource.field] = definitionColumns.some(col => col.field == resource.field && !col.hide) + definitionColumns.forEach((def) => { + if (def.field == resource.field) { + resourceColumns[index] = {...resource, ...def.column}; + } + }) + }) + + return { + columns: resourceColumns, + columnVisibilityModel: visibilityModel + } +} + +const List = (props: ListProps) => { + const { resource, columns, schemaName } = props; const { translate: t } = useTranslation(); const { currentFirm } = useContext(FirmContext); const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}` @@ -25,15 +59,32 @@ const List = (props: ListProps) => { }); const navigate = useNavigate(); - const cols = React.useMemo[]>( - () => columns, - [], - ); + const [columnSchema, setColumnSchema] = useState>() + const [schemaLoading, setSchemaLoading] = useState(true); + useEffect(() => { + const fetchSchema = async () => { + try { + const resourceColumns = await jsonschemaProvider.getReadResourceColumns(schemaName) + const definedColumns = computeColumnSchema(columns, resourceColumns) + setColumnSchema(definedColumns); + console.log(resourceColumns); + setSchemaLoading(false); + } catch (error) { + console.error('Error fetching data:', error); + setSchemaLoading(false); + } + }; + fetchSchema(); + }, []); const handleRowClick = (params: any, event: any) => { navigate(`edit/${params.id}`) } + if (schemaLoading || columnSchema === undefined) { + return + } + return ( @@ -41,9 +92,14 @@ const List = (props: ListProps) => { )