Importing RJSF lib with first application
This commit is contained in:
@@ -1,19 +1,19 @@
|
|||||||
import os
|
from datetime import datetime
|
||||||
|
|
||||||
from beanie import init_beanie
|
from beanie import Document, PydanticObjectId
|
||||||
from motor.motor_asyncio import AsyncIOMotorClient
|
from pydantic import Field, computed_field
|
||||||
|
|
||||||
from hub.user import User
|
|
||||||
from hub.auth import AccessToken
|
|
||||||
|
|
||||||
MONGO_USERNAME = os.getenv("MONGO_INITDB_ROOT_USERNAME")
|
class CrudDocument(Document):
|
||||||
MONGO_PASSWORD = os.getenv("MONGO_INITDB_ROOT_PASSWORD")
|
_id: str
|
||||||
|
created_at: datetime = Field(default=datetime.utcnow(), nullable=False, title="Créé le")
|
||||||
|
created_by: PydanticObjectId = Field()
|
||||||
|
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False, title="Modifié le")
|
||||||
|
updated_by: PydanticObjectId = Field()
|
||||||
|
|
||||||
DATABASE_URL = f"mongodb://{MONGO_USERNAME}:{MONGO_PASSWORD}@mongo:27017"
|
@computed_field
|
||||||
|
def label(self) -> str:
|
||||||
|
return self.compute_label()
|
||||||
|
|
||||||
async def init_db():
|
def compute_label(self) -> str:
|
||||||
client = AsyncIOMotorClient(DATABASE_URL, uuidRepresentation="standard")
|
return ""
|
||||||
|
|
||||||
await init_beanie(database=client.hub,
|
|
||||||
document_models=[User, AccessToken],
|
|
||||||
allow_index_dropping=True)
|
|
||||||
|
|||||||
@@ -90,3 +90,6 @@ cookie_transport = CookieTransportOauth(cookie_name="rpkapiusersauth")
|
|||||||
auth_backend = AuthenticationBackend(name="db", transport=cookie_transport, get_strategy=get_database_strategy, )
|
auth_backend = AuthenticationBackend(name="db", transport=cookie_transport, get_strategy=get_database_strategy, )
|
||||||
google_oauth_router = fastapi_users.get_oauth_router(google_oauth_client, auth_backend, SECRET, is_verified_by_default=True)
|
google_oauth_router = fastapi_users.get_oauth_router(google_oauth_client, auth_backend, SECRET, is_verified_by_default=True)
|
||||||
discord_oauth_router = fastapi_users.get_oauth_router(discord_oauth_client, auth_backend, SECRET, is_verified_by_default=True)
|
discord_oauth_router = fastapi_users.get_oauth_router(discord_oauth_client, auth_backend, SECRET, is_verified_by_default=True)
|
||||||
|
|
||||||
|
get_current_user = fastapi_users.current_user(active=True)
|
||||||
|
get_current_superuser = fastapi_users.current_user(active=True, superuser=True)
|
||||||
|
|||||||
21
api/rpk-api/hub/db.py
Normal file
21
api/rpk-api/hub/db.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from beanie import init_beanie
|
||||||
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
|
||||||
|
from hub.user import User
|
||||||
|
from hub.auth import AccessToken
|
||||||
|
from hub.firm import Firm
|
||||||
|
|
||||||
|
|
||||||
|
MONGO_USERNAME = os.getenv("MONGO_INITDB_ROOT_USERNAME")
|
||||||
|
MONGO_PASSWORD = os.getenv("MONGO_INITDB_ROOT_PASSWORD")
|
||||||
|
|
||||||
|
DATABASE_URL = f"mongodb://{MONGO_USERNAME}:{MONGO_PASSWORD}@mongo:27017"
|
||||||
|
|
||||||
|
async def init_db():
|
||||||
|
client = AsyncIOMotorClient(DATABASE_URL, uuidRepresentation="standard")
|
||||||
|
|
||||||
|
await init_beanie(database=client.hub,
|
||||||
|
document_models=[User, AccessToken, Firm],
|
||||||
|
allow_index_dropping=True)
|
||||||
28
api/rpk-api/hub/firm/__init__.py
Normal file
28
api/rpk-api/hub/firm/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from beanie import PydanticObjectId
|
||||||
|
from pydantic import Field, BaseModel
|
||||||
|
from pymongo import IndexModel
|
||||||
|
|
||||||
|
from hub import CrudDocument
|
||||||
|
|
||||||
|
class Firm(CrudDocument):
|
||||||
|
name: str = Field()
|
||||||
|
instance: str = Field()
|
||||||
|
owner: PydanticObjectId = Field()
|
||||||
|
|
||||||
|
def compute_label(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
indexes = [
|
||||||
|
IndexModel(["name", "instance"], unique=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
class FirmRead(BaseModel):
|
||||||
|
instance: str = Field()
|
||||||
|
name: str = Field()
|
||||||
|
|
||||||
|
class FirmCreate(FirmRead):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FirmUpdate(FirmRead):
|
||||||
|
pass
|
||||||
72
api/rpk-api/hub/firm/routes.py
Normal file
72
api/rpk-api/hub/firm/routes.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from beanie import PydanticObjectId
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
|
from hub.auth import get_current_user
|
||||||
|
from hub.firm import Firm, FirmRead, FirmCreate, FirmUpdate
|
||||||
|
|
||||||
|
model = Firm
|
||||||
|
model_read = FirmRead
|
||||||
|
model_create = FirmCreate
|
||||||
|
model_update = FirmUpdate
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/", response_description="{} added to the database".format(model.__name__))
|
||||||
|
async def create(item: model_create, user=Depends(get_current_user)) -> dict:
|
||||||
|
exists = await Firm.find_one({"name": item.name, "instance": item.instance})
|
||||||
|
if exists:
|
||||||
|
raise HTTPException(status_code=400, detail="Firm already exists")
|
||||||
|
|
||||||
|
item.created_by = user.id
|
||||||
|
item.updated_by = user.id
|
||||||
|
item.owner = user.id
|
||||||
|
o = await model(**item.model_dump()).create()
|
||||||
|
return model_read(**o.model_dump())
|
||||||
|
|
||||||
|
@router.get("/{id}", response_description="{} record retrieved".format(model.__name__))
|
||||||
|
async def read_id(id: PydanticObjectId, user=Depends(get_current_user)) -> model_read:
|
||||||
|
item = await model.get(id)
|
||||||
|
return model_read(**item.model_dump())
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{id}", response_description="{} record updated".format(model.__name__))
|
||||||
|
async def update(id: PydanticObjectId, req: model_update, user=Depends(get_current_user)) -> model_read:
|
||||||
|
item = await model.get(id)
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="{} record not found!".format(model.__name__)
|
||||||
|
)
|
||||||
|
|
||||||
|
if item.owner != user.id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Insufficient credentials to modify {} record".format(model.__name__)
|
||||||
|
)
|
||||||
|
|
||||||
|
req = {k: v for k, v in req.model_dump().items() if v is not None}
|
||||||
|
update_query = {"$set": {
|
||||||
|
field: value for field, value in req.items()
|
||||||
|
}}
|
||||||
|
|
||||||
|
await item.update(update_query)
|
||||||
|
return model_read(**item.dict())
|
||||||
|
|
||||||
|
@router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__))
|
||||||
|
async def delete(id: PydanticObjectId, user=Depends(get_current_user)) -> dict:
|
||||||
|
item = await model.get(id)
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="{} record not found!".format(model.__name__)
|
||||||
|
)
|
||||||
|
if item.owner != user.id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Insufficient credentials delete {} record".format(model.__name__)
|
||||||
|
)
|
||||||
|
|
||||||
|
await item.delete()
|
||||||
|
return {
|
||||||
|
"message": "{} deleted successfully".format(model.__name__)
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from hub import init_db as hub_init_db
|
from hub.db import init_db as hub_init_db
|
||||||
from hub.auth import auth_router, register_router, password_router, verification_router, users_router, \
|
from hub.auth import auth_router, register_router, password_router, verification_router, users_router, \
|
||||||
google_oauth_router, discord_oauth_router
|
google_oauth_router, discord_oauth_router
|
||||||
|
from hub.firm.routes import router as firm_router
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@@ -28,3 +28,4 @@ app.include_router(discord_oauth_router, prefix="/auth/discord", tags=["Auth"])
|
|||||||
app.include_router(verification_router, prefix="/auth/verification", tags=["Auth"], )
|
app.include_router(verification_router, prefix="/auth/verification", tags=["Auth"], )
|
||||||
app.include_router(users_router, prefix="/users", tags=["Users"], )
|
app.include_router(users_router, prefix="/users", tags=["Users"], )
|
||||||
app.include_router(password_router, prefix="/users", tags=["Users"], )
|
app.include_router(password_router, prefix="/users", tags=["Users"], )
|
||||||
|
app.include_router(firm_router, prefix="/firms", tags=["Firms"], )
|
||||||
|
|||||||
9
gui/rpk-gui/package-lock.json
generated
9
gui/rpk-gui/package-lock.json
generated
@@ -26,12 +26,14 @@
|
|||||||
"@rjsf/mui": "^5.24.1",
|
"@rjsf/mui": "^5.24.1",
|
||||||
"@rjsf/utils": "^5.24.1",
|
"@rjsf/utils": "^5.24.1",
|
||||||
"@rjsf/validator-ajv8": "^5.24.1",
|
"@rjsf/validator-ajv8": "^5.24.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-hook-form": "^7.30.0",
|
"react-hook-form": "^7.30.0",
|
||||||
"react-router": "^7.0.2"
|
"react-router": "^7.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/node": "^18.16.2",
|
"@types/node": "^18.16.2",
|
||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
@@ -3011,6 +3013,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
|
||||||
|
"integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/mdast": {
|
"node_modules/@types/mdast": {
|
||||||
"version": "3.0.15",
|
"version": "3.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
|
||||||
|
|||||||
@@ -22,12 +22,14 @@
|
|||||||
"@rjsf/mui": "^5.24.1",
|
"@rjsf/mui": "^5.24.1",
|
||||||
"@rjsf/utils": "^5.24.1",
|
"@rjsf/utils": "^5.24.1",
|
||||||
"@rjsf/validator-ajv8": "^5.24.1",
|
"@rjsf/validator-ajv8": "^5.24.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-hook-form": "^7.30.0",
|
"react-hook-form": "^7.30.0",
|
||||||
"react-router": "^7.0.2"
|
"react-router": "^7.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/node": "^18.16.2",
|
"@types/node": "^18.16.2",
|
||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0",
|
||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { UpdatePassword } from "./components/auth/UpdatePassword";
|
|||||||
|
|
||||||
import { Header } from "./components";
|
import { Header } from "./components";
|
||||||
import { Hub } from "./pages/hub";
|
import { Hub } from "./pages/hub";
|
||||||
|
import { CreateFirm } from "./pages/hub/CreateFirm";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -55,6 +56,7 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Route path="/hub" element={ <Hub /> } />
|
<Route path="/hub" element={ <Hub /> } />
|
||||||
|
<Route path="/hub/create-firm" element={ <CreateFirm /> } />
|
||||||
</Route>
|
</Route>
|
||||||
<Route index element={<h1>HOME</h1>} />
|
<Route index element={<h1>HOME</h1>} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
|||||||
0
gui/rpk-gui/src/lib/crud/components/crud-card.tsx
Normal file
0
gui/rpk-gui/src/lib/crud/components/crud-card.tsx
Normal file
65
gui/rpk-gui/src/lib/crud/components/crud-form.tsx
Normal file
65
gui/rpk-gui/src/lib/crud/components/crud-form.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import validator from "@rjsf/validator-ajv8";
|
||||||
|
import Form from "@rjsf/mui";
|
||||||
|
import { RegistryFieldsType, RegistryWidgetsType } from "@rjsf/utils";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { jsonschemaProvider } from "../providers/jsonschema-provider";
|
||||||
|
import { useForm } from "@refinedev/core";
|
||||||
|
import CrudTextWidget from "./widgets/crud-text-widget";
|
||||||
|
import UnionEnumField from "./fields/union-enum";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
schemaName: string,
|
||||||
|
resource: string,
|
||||||
|
id?: string,
|
||||||
|
//onSubmit: (data: IChangeEvent, event: FormEvent<any>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const customWidgets: RegistryWidgetsType = {
|
||||||
|
TextWidget: CrudTextWidget
|
||||||
|
};
|
||||||
|
|
||||||
|
const customFields: RegistryFieldsType = {
|
||||||
|
AnyOfField: UnionEnumField
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CrudForm: React.FC<Props> = ({schemaName, resource, id}) => {
|
||||||
|
const { onFinish, query, formLoading } = useForm({
|
||||||
|
resource: resource,
|
||||||
|
action: id === undefined ? "create" : "edit",
|
||||||
|
redirect: "show",
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const record = query?.data?.data;
|
||||||
|
const [formData, setFormData] = useState(record);
|
||||||
|
|
||||||
|
const [schema, setSchema] = useState({});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSchema = async () => {
|
||||||
|
try {
|
||||||
|
const resourceSchema = await jsonschemaProvider.getResourceSchema(schemaName);
|
||||||
|
setSchema(resourceSchema);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSchema();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
schema={schema}
|
||||||
|
formData={record}
|
||||||
|
onChange={(e) => setFormData(e.formData)}
|
||||||
|
onSubmit={(e) => onFinish(e.formData)}
|
||||||
|
validator={validator}
|
||||||
|
omitExtraData={true}
|
||||||
|
widgets={customWidgets}
|
||||||
|
fields={customFields}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
0
gui/rpk-gui/src/lib/crud/components/crud-list.tsx
Normal file
0
gui/rpk-gui/src/lib/crud/components/crud-list.tsx
Normal file
100
gui/rpk-gui/src/lib/crud/components/fields/union-enum.tsx
Normal file
100
gui/rpk-gui/src/lib/crud/components/fields/union-enum.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import {ERRORS_KEY, FieldProps, FormContextType, getUiOptions, RJSFSchema, StrictRJSFSchema} from "@rjsf/utils";
|
||||||
|
import { getDefaultRegistry } from "@rjsf/core";
|
||||||
|
import UnionEnumWidget from "../widgets/union-enum";
|
||||||
|
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import omit from 'lodash/omit';
|
||||||
|
|
||||||
|
const {
|
||||||
|
fields: { AnyOfField },
|
||||||
|
} = getDefaultRegistry();
|
||||||
|
|
||||||
|
export default function UnionEnumField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
|
||||||
|
props: FieldProps
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
disabled = false,
|
||||||
|
errorSchema = {},
|
||||||
|
formContext,
|
||||||
|
formData,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
onFocus,
|
||||||
|
registry,
|
||||||
|
schema,
|
||||||
|
uiSchema,
|
||||||
|
options,
|
||||||
|
idSchema,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
getDefaultRegistry<T,S,F>().widgets
|
||||||
|
|
||||||
|
const enumOptions: any[] = []
|
||||||
|
|
||||||
|
if (options.length == 2 && (options[0].type == "null" || options[1].type == "null")) {
|
||||||
|
const { SchemaField: _SchemaField } = registry.fields;
|
||||||
|
|
||||||
|
let opt_schema = {...schema}
|
||||||
|
delete(opt_schema.anyOf)
|
||||||
|
|
||||||
|
if (options[0].type == "null") {
|
||||||
|
opt_schema = {...opt_schema, ...options[1]}
|
||||||
|
} else if (options[1].type == "null") {
|
||||||
|
opt_schema = {...opt_schema, ...options[0]}
|
||||||
|
}
|
||||||
|
return <_SchemaField {...props} schema={opt_schema} uiSchema={uiSchema} />
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let opt of options) {
|
||||||
|
if (!opt.hasOwnProperty('enum')) {
|
||||||
|
return (<AnyOfField {...props} />)
|
||||||
|
}
|
||||||
|
for (let val of opt.enum) {
|
||||||
|
enumOptions.push({
|
||||||
|
title: val,
|
||||||
|
value: val,
|
||||||
|
type: opt.title
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { globalUiOptions, schemaUtils } = registry;
|
||||||
|
const {
|
||||||
|
placeholder,
|
||||||
|
autofocus,
|
||||||
|
autocomplete,
|
||||||
|
title = schema.title,
|
||||||
|
...uiOptions
|
||||||
|
} = getUiOptions(uiSchema, globalUiOptions);
|
||||||
|
|
||||||
|
const rawErrors = get(errorSchema, ERRORS_KEY, []);
|
||||||
|
const fieldErrorSchema = omit(errorSchema, [ERRORS_KEY]);
|
||||||
|
const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnionEnumWidget
|
||||||
|
id={`${idSchema.$id}${schema.oneOf ? '__oneof_select' : '__anyof_select'}`}
|
||||||
|
name={`${name}${schema.oneOf ? '__oneof_select' : '__anyof_select'}`}
|
||||||
|
schema={schema}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onFocus={onFocus}
|
||||||
|
disabled={disabled || isEmpty(options)}
|
||||||
|
multiple={false}
|
||||||
|
rawErrors={rawErrors}
|
||||||
|
errorSchema={fieldErrorSchema}
|
||||||
|
value={formData}
|
||||||
|
options={{enumOptions}}
|
||||||
|
registry={registry}
|
||||||
|
formContext={formContext}
|
||||||
|
placeholder={placeholder}
|
||||||
|
autocomplete={autocomplete}
|
||||||
|
autofocus={autofocus}
|
||||||
|
label={title ?? name}
|
||||||
|
hideLabel={!displayLabel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import {FormContextType, getTemplate, RJSFSchema, StrictRJSFSchema, WidgetProps} from "@rjsf/utils";
|
||||||
|
|
||||||
|
import ForeignKeyWidget from "./foreign-key";
|
||||||
|
|
||||||
|
export default function CrudTextWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
|
||||||
|
props: WidgetProps<T, S, F>
|
||||||
|
) {
|
||||||
|
if (props.schema.hasOwnProperty("foreign_key")) {
|
||||||
|
return (<ForeignKeyWidget {...props} />);
|
||||||
|
} else {
|
||||||
|
const { options, registry } = props;
|
||||||
|
const BaseInputTemplate = getTemplate<'BaseInputTemplate', T, S, F>('BaseInputTemplate', registry, options);
|
||||||
|
return <BaseInputTemplate {...props} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
79
gui/rpk-gui/src/lib/crud/components/widgets/foreign-key.tsx
Normal file
79
gui/rpk-gui/src/lib/crud/components/widgets/foreign-key.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import {FormContextType, RJSFSchema, StrictRJSFSchema, WidgetProps} from '@rjsf/utils';
|
||||||
|
import { Autocomplete } from "@mui/material";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import {BaseRecord, useList, useOne} from "@refinedev/core";
|
||||||
|
|
||||||
|
type ForeignKeySchema = RJSFSchema & {
|
||||||
|
foreign_key?: {
|
||||||
|
reference: {
|
||||||
|
resource: string,
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ForeignKeyWidget<T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
||||||
|
props: WidgetProps<T, S, F>
|
||||||
|
) {
|
||||||
|
if (props.schema.foreign_key === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resource = props.schema.foreign_key.reference.resource
|
||||||
|
const labelField = props.schema.foreign_key.reference.label
|
||||||
|
|
||||||
|
const valueResult = useOne({
|
||||||
|
resource: resource,
|
||||||
|
id: props.value != null ? props.value : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const empty_option: BaseRecord = {
|
||||||
|
id: undefined
|
||||||
|
}
|
||||||
|
empty_option[labelField] = "(None)"
|
||||||
|
|
||||||
|
const [inputValue, setInputValue] = useState<string>("");
|
||||||
|
const [selectedValue, setSelectedValue] = useState(valueResult.data?.data || null);
|
||||||
|
const [debouncedInputValue, setDebouncedInputValue] = useState<string>(inputValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => setDebouncedInputValue(inputValue), 300); // Adjust debounce delay as needed
|
||||||
|
return () => clearTimeout(handler);
|
||||||
|
}, [inputValue]);
|
||||||
|
|
||||||
|
const listResult = useList({
|
||||||
|
resource: resource,
|
||||||
|
pagination: { current: 1, pageSize: 10 },
|
||||||
|
filters: [{ field: "name", operator: "contains", value: debouncedInputValue }],
|
||||||
|
sorters: [{ field: "name", order: "asc" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = listResult.data?.data || [];
|
||||||
|
if (! props.required) {
|
||||||
|
options.unshift(empty_option);
|
||||||
|
}
|
||||||
|
const isLoading = listResult.isLoading || valueResult.isLoading;
|
||||||
|
|
||||||
|
if(! selectedValue && valueResult.data) {
|
||||||
|
setSelectedValue(valueResult.data?.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
value={selectedValue}
|
||||||
|
onChange={(event, newValue) => {
|
||||||
|
setSelectedValue(newValue ? newValue : empty_option);
|
||||||
|
props.onChange(newValue ? newValue.id : null);
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
//inputValue={inputValue}
|
||||||
|
onInputChange={(event, newInputValue) => setInputValue(newInputValue)}
|
||||||
|
options={options}
|
||||||
|
getOptionLabel={(option) => option ? option[labelField] : ""}
|
||||||
|
loading={isLoading}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} label={ props.label } variant="outlined" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
55
gui/rpk-gui/src/lib/crud/components/widgets/union-enum.tsx
Normal file
55
gui/rpk-gui/src/lib/crud/components/widgets/union-enum.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import {EnumOptionsType, FormContextType, RJSFSchema, StrictRJSFSchema, WidgetProps} from "@rjsf/utils";
|
||||||
|
import {useState} from "react";
|
||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import {UIOptionsType} from "@rjsf/utils/src/types";
|
||||||
|
|
||||||
|
type CrudEnumOptionsType<S extends StrictRJSFSchema = RJSFSchema> = EnumOptionsType<S> & {
|
||||||
|
type: string,
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CrudEnumWidgetProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>
|
||||||
|
extends WidgetProps<T, S, F> {
|
||||||
|
options: NonNullable<UIOptionsType<T, S, F>> & {
|
||||||
|
/** The enum options list for a type that supports them */
|
||||||
|
enumOptions?: CrudEnumOptionsType<S>[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UnionEnumWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
|
||||||
|
props: CrudEnumWidgetProps<T, S, F>
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
} = props;
|
||||||
|
const [selectedValue, setSelectedValue] = useState<CrudEnumOptionsType | null>(null);
|
||||||
|
|
||||||
|
if (! selectedValue && value && options.enumOptions) {
|
||||||
|
for (const opt of options.enumOptions){
|
||||||
|
if (opt.value == value) {
|
||||||
|
setSelectedValue(opt);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.enumOptions !== undefined) {
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
value={selectedValue}
|
||||||
|
onChange={(event, newValue) => {
|
||||||
|
setSelectedValue(newValue);
|
||||||
|
props.onChange(newValue?.value);
|
||||||
|
}}
|
||||||
|
options={options.enumOptions}
|
||||||
|
groupBy={(option) => option.type}
|
||||||
|
getOptionLabel={(option) => option.title}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} label={ props.label } variant="outlined" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
188
gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx
Normal file
188
gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import { JSONSchema7Definition } from "json-schema";
|
||||||
|
import { RJSFSchema } from '@rjsf/utils';
|
||||||
|
|
||||||
|
const API_URL = "/api/v1";
|
||||||
|
|
||||||
|
|
||||||
|
export const jsonschemaProvider = {
|
||||||
|
getResourceSchema: async (resourceName: string): Promise<RJSFSchema> => {
|
||||||
|
return buildResource(await getJsonschema(), resourceName)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 buildResource(rawSchemas: RJSFSchema, resourceName: string) {
|
||||||
|
let resource;
|
||||||
|
|
||||||
|
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);
|
||||||
|
} 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (is_enum(prop)) {
|
||||||
|
for (let i in prop.allOf) {
|
||||||
|
if (is_reference(prop.allOf[i])) {
|
||||||
|
resolveReference(rawSchemas, resource, prop.allOf[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (is_array(prop) && is_reference(prop.items)) {
|
||||||
|
resolveReference(rawSchemas, resource, prop.items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
|
||||||
|
const subresourceName = get_reference_name(prop_reference);
|
||||||
|
const subresource = buildResource(rawSchemas, subresourceName);
|
||||||
|
resource.components.schemas[subresourceName] = subresource;
|
||||||
|
for (let subsubresourceName in subresource.components.schemas) {
|
||||||
|
if (! resource.components.schemas.hasOwnProperty(subsubresourceName)) {
|
||||||
|
resource.components.schemas[subsubresourceName] = subresource.components.schemas[subsubresourceName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePropertiesOrder(resource: any) {
|
||||||
|
let created_at;
|
||||||
|
let updated_at;
|
||||||
|
let new_properties: any = {};
|
||||||
|
for (let prop_name in resource.properties) {
|
||||||
|
if (prop_name == 'created_at') {
|
||||||
|
created_at = resource.properties[prop_name];
|
||||||
|
} else if (prop_name == 'updated_at') {
|
||||||
|
updated_at = resource.properties[prop_name];
|
||||||
|
} else {
|
||||||
|
new_properties[prop_name] = resource.properties[prop_name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (created_at) {
|
||||||
|
new_properties['created_at'] = created_at;
|
||||||
|
}
|
||||||
|
if (updated_at) {
|
||||||
|
new_properties['updated_at'] = updated_at;
|
||||||
|
}
|
||||||
|
resource.properties = new_properties
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_object(prop: any) {
|
||||||
|
return prop.hasOwnProperty('properties')
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_reference(prop: any) {
|
||||||
|
return prop.hasOwnProperty('$ref');
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_array(prop: any) {
|
||||||
|
return prop.hasOwnProperty('items');
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_union(prop: any) {
|
||||||
|
return prop.hasOwnProperty('oneOf') || prop.hasOwnProperty('anyOf');
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_enum(prop: any) {
|
||||||
|
return prop.hasOwnProperty('enum');
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return has_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition))
|
||||||
|
&& path_exists(
|
||||||
|
rawSchemas,
|
||||||
|
get_descendant(rawSchemas, 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
|
import { CrudForm } from "../../lib/crud/components/crud-form";
|
||||||
|
|
||||||
export const CreateFirm = () => {
|
export const CreateFirm = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<CrudForm schemaName={"FirmCreate"} resource={"firms"} />
|
||||||
<h1>Create Firm</h1>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
|
||||||
export const Hub = () => {
|
export const Hub = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>HUB</h1>
|
<h1>HUB</h1>
|
||||||
<p>List of managed firms</p>
|
<p>List of managed firms</p>
|
||||||
<p>List of firm you're working atx</p>
|
<p>List of firm you're working atx</p>
|
||||||
<p><Button>Create a new firm</Button></p>
|
<Link to="/hub/create-firm" ><Button >Create a new firm</Button></Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user