diff --git a/api/rpk-api/hub/__init__.py b/api/rpk-api/hub/__init__.py index 571b642..f58646b 100644 --- a/api/rpk-api/hub/__init__.py +++ b/api/rpk-api/hub/__init__.py @@ -1,19 +1,19 @@ -import os +from datetime import datetime -from beanie import init_beanie -from motor.motor_asyncio import AsyncIOMotorClient +from beanie import Document, PydanticObjectId +from pydantic import Field, computed_field -from hub.user import User -from hub.auth import AccessToken -MONGO_USERNAME = os.getenv("MONGO_INITDB_ROOT_USERNAME") -MONGO_PASSWORD = os.getenv("MONGO_INITDB_ROOT_PASSWORD") +class CrudDocument(Document): + _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(): - client = AsyncIOMotorClient(DATABASE_URL, uuidRepresentation="standard") - - await init_beanie(database=client.hub, - document_models=[User, AccessToken], - allow_index_dropping=True) + def compute_label(self) -> str: + return "" diff --git a/api/rpk-api/hub/auth/__init__.py b/api/rpk-api/hub/auth/__init__.py index e0642a0..5b86dd2 100644 --- a/api/rpk-api/hub/auth/__init__.py +++ b/api/rpk-api/hub/auth/__init__.py @@ -90,3 +90,6 @@ cookie_transport = CookieTransportOauth(cookie_name="rpkapiusersauth") 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) 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) diff --git a/api/rpk-api/hub/db.py b/api/rpk-api/hub/db.py new file mode 100644 index 0000000..d889cfe --- /dev/null +++ b/api/rpk-api/hub/db.py @@ -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) \ No newline at end of file diff --git a/api/rpk-api/hub/firm/__init__.py b/api/rpk-api/hub/firm/__init__.py new file mode 100644 index 0000000..ffe6340 --- /dev/null +++ b/api/rpk-api/hub/firm/__init__.py @@ -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 diff --git a/api/rpk-api/hub/firm/routes.py b/api/rpk-api/hub/firm/routes.py new file mode 100644 index 0000000..7afa19b --- /dev/null +++ b/api/rpk-api/hub/firm/routes.py @@ -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__) + } diff --git a/api/rpk-api/main.py b/api/rpk-api/main.py index 0c99c16..5a0db1a 100644 --- a/api/rpk-api/main.py +++ b/api/rpk-api/main.py @@ -1,10 +1,10 @@ from contextlib import asynccontextmanager - 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, \ google_oauth_router, discord_oauth_router +from hub.firm.routes import router as firm_router if __name__ == '__main__': 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(users_router, prefix="/users", tags=["Users"], ) app.include_router(password_router, prefix="/users", tags=["Users"], ) +app.include_router(firm_router, prefix="/firms", tags=["Firms"], ) diff --git a/gui/rpk-gui/package-lock.json b/gui/rpk-gui/package-lock.json index 861bc46..7f64978 100644 --- a/gui/rpk-gui/package-lock.json +++ b/gui/rpk-gui/package-lock.json @@ -26,12 +26,14 @@ "@rjsf/mui": "^5.24.1", "@rjsf/utils": "^5.24.1", "@rjsf/validator-ajv8": "^5.24.1", + "lodash": "^4.17.21", "react": "^18.0.0", "react-dom": "^18.0.0", "react-hook-form": "^7.30.0", "react-router": "^7.0.2" }, "devDependencies": { + "@types/lodash": "^4.17.16", "@types/node": "^18.16.2", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", @@ -3011,6 +3013,13 @@ "dev": true, "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": { "version": "3.0.15", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", diff --git a/gui/rpk-gui/package.json b/gui/rpk-gui/package.json index ac32cce..6e615dc 100644 --- a/gui/rpk-gui/package.json +++ b/gui/rpk-gui/package.json @@ -22,12 +22,14 @@ "@rjsf/mui": "^5.24.1", "@rjsf/utils": "^5.24.1", "@rjsf/validator-ajv8": "^5.24.1", + "lodash": "^4.17.21", "react": "^18.0.0", "react-dom": "^18.0.0", "react-hook-form": "^7.30.0", "react-router": "^7.0.2" }, "devDependencies": { + "@types/lodash": "^4.17.16", "@types/node": "^18.16.2", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", diff --git a/gui/rpk-gui/src/App.tsx b/gui/rpk-gui/src/App.tsx index 97775e5..6c61247 100644 --- a/gui/rpk-gui/src/App.tsx +++ b/gui/rpk-gui/src/App.tsx @@ -23,7 +23,8 @@ import { ForgotPassword } from "./components/auth/ForgotPassword"; import { UpdatePassword } from "./components/auth/UpdatePassword"; import { Header } from "./components"; -import {Hub} from "./pages/hub"; +import { Hub } from "./pages/hub"; +import { CreateFirm } from "./pages/hub/CreateFirm"; function App() { return ( @@ -55,6 +56,7 @@ function App() { )} > } /> + } /> HOME} /> } /> diff --git a/gui/rpk-gui/src/lib/crud/components/crud-card.tsx b/gui/rpk-gui/src/lib/crud/components/crud-card.tsx new file mode 100644 index 0000000..e69de29 diff --git a/gui/rpk-gui/src/lib/crud/components/crud-form.tsx b/gui/rpk-gui/src/lib/crud/components/crud-form.tsx new file mode 100644 index 0000000..6bd804c --- /dev/null +++ b/gui/rpk-gui/src/lib/crud/components/crud-form.tsx @@ -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) => void +} + +const customWidgets: RegistryWidgetsType = { + TextWidget: CrudTextWidget +}; + +const customFields: RegistryFieldsType = { + AnyOfField: UnionEnumField +} + +export const CrudForm: React.FC = ({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 ( + setFormData(e.formData)} + onSubmit={(e) => onFinish(e.formData)} + validator={validator} + omitExtraData={true} + widgets={customWidgets} + fields={customFields} + /> + ) +} diff --git a/gui/rpk-gui/src/lib/crud/components/crud-list.tsx b/gui/rpk-gui/src/lib/crud/components/crud-list.tsx new file mode 100644 index 0000000..e69de29 diff --git a/gui/rpk-gui/src/lib/crud/components/fields/union-enum.tsx b/gui/rpk-gui/src/lib/crud/components/fields/union-enum.tsx new file mode 100644 index 0000000..617cf95 --- /dev/null +++ b/gui/rpk-gui/src/lib/crud/components/fields/union-enum.tsx @@ -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( + props: FieldProps +) { + const { + name, + disabled = false, + errorSchema = {}, + formContext, + formData, + onChange, + onBlur, + onFocus, + registry, + schema, + uiSchema, + options, + idSchema, + } = props; + + getDefaultRegistry().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 () + } + 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 ( + + ); +} diff --git a/gui/rpk-gui/src/lib/crud/components/widgets/crud-text-widget.tsx b/gui/rpk-gui/src/lib/crud/components/widgets/crud-text-widget.tsx new file mode 100644 index 0000000..c0f6f4a --- /dev/null +++ b/gui/rpk-gui/src/lib/crud/components/widgets/crud-text-widget.tsx @@ -0,0 +1,15 @@ +import {FormContextType, getTemplate, RJSFSchema, StrictRJSFSchema, WidgetProps} from "@rjsf/utils"; + +import ForeignKeyWidget from "./foreign-key"; + +export default function CrudTextWidget( + props: WidgetProps +) { + if (props.schema.hasOwnProperty("foreign_key")) { + return (); + } else { + const { options, registry } = props; + const BaseInputTemplate = getTemplate<'BaseInputTemplate', T, S, F>('BaseInputTemplate', registry, options); + return ; + } +} diff --git a/gui/rpk-gui/src/lib/crud/components/widgets/foreign-key.tsx b/gui/rpk-gui/src/lib/crud/components/widgets/foreign-key.tsx new file mode 100644 index 0000000..b9580f4 --- /dev/null +++ b/gui/rpk-gui/src/lib/crud/components/widgets/foreign-key.tsx @@ -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( + props: WidgetProps +) { + 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(""); + const [selectedValue, setSelectedValue] = useState(valueResult.data?.data || null); + const [debouncedInputValue, setDebouncedInputValue] = useState(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 ( + { + 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) => ( + + )} + /> + ); +}; diff --git a/gui/rpk-gui/src/lib/crud/components/widgets/union-enum.tsx b/gui/rpk-gui/src/lib/crud/components/widgets/union-enum.tsx new file mode 100644 index 0000000..e4a9790 --- /dev/null +++ b/gui/rpk-gui/src/lib/crud/components/widgets/union-enum.tsx @@ -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 = EnumOptionsType & { + type: string, + title: string +} + +interface CrudEnumWidgetProps + extends WidgetProps { + options: NonNullable> & { + /** The enum options list for a type that supports them */ + enumOptions?: CrudEnumOptionsType[]; + }; +} + +export default function UnionEnumWidget( + props: CrudEnumWidgetProps +) { + const { + options, + value, + } = props; + const [selectedValue, setSelectedValue] = useState(null); + + if (! selectedValue && value && options.enumOptions) { + for (const opt of options.enumOptions){ + if (opt.value == value) { + setSelectedValue(opt); + break; + } + } + } + + if (options.enumOptions !== undefined) { + return ( + { + setSelectedValue(newValue); + props.onChange(newValue?.value); + }} + options={options.enumOptions} + groupBy={(option) => option.type} + getOptionLabel={(option) => option.title} + renderInput={(params) => ( + + )} + /> + ); + } +} diff --git a/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx b/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx new file mode 100644 index 0000000..0f4ddcc --- /dev/null +++ b/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx @@ -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 => { + 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 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) + ); +} diff --git a/gui/rpk-gui/src/pages/hub/CreateFirm.tsx b/gui/rpk-gui/src/pages/hub/CreateFirm.tsx index 5b964b0..42e95e1 100644 --- a/gui/rpk-gui/src/pages/hub/CreateFirm.tsx +++ b/gui/rpk-gui/src/pages/hub/CreateFirm.tsx @@ -1,9 +1,7 @@ +import { CrudForm } from "../../lib/crud/components/crud-form"; export const CreateFirm = () => { - return ( - - Create Firm - + ) } diff --git a/gui/rpk-gui/src/pages/hub/index.tsx b/gui/rpk-gui/src/pages/hub/index.tsx index ea6db91..2c9ed7c 100644 --- a/gui/rpk-gui/src/pages/hub/index.tsx +++ b/gui/rpk-gui/src/pages/hub/index.tsx @@ -1,13 +1,13 @@ -import {Button} from "@mui/material"; +import { Button } from "@mui/material"; +import { Link } from "react-router"; export const Hub = () => { - return ( HUB List of managed firms List of firm you're working atx - Create a new firm + Create a new firm ); };
List of managed firms
List of firm you're working atx
Create a new firm