Initial commit

This commit is contained in:
2025-01-16 00:24:42 +01:00
parent c804af2a92
commit a2a42437a5
58 changed files with 10919 additions and 0 deletions

185
gui/app/src/App.tsx Normal file
View File

@@ -0,0 +1,185 @@
import { DevtoolsProvider, DevtoolsPanel } from "@refinedev/devtools";
import { Refine, type AuthProvider, Authenticated } from "@refinedev/core";
import {
ThemedLayoutV2,
ErrorComponent,
RefineThemes,
useNotificationProvider,
RefineSnackbarProvider,
AuthPage,
} from "@refinedev/mui";
import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles";
import FormControlLabel from "@mui/material/FormControlLabel";
import Checkbox from "@mui/material/Checkbox";
import { ThemeProvider } from "@mui/material/styles";
import { authProvider } from "./providers/auth-provider";
//import dataProvider from "@refinedev/simple-rest";
import { dataProvider } from "./providers/data-provider";
import routerProvider, {
NavigateToResource,
CatchAllNavigate,
UnsavedChangesNotifier,
DocumentTitleHandler,
} from "@refinedev/react-router";
import { BrowserRouter, Routes, Route, Outlet } from "react-router";
import { useFormContext } from "react-hook-form";
import { PostList, PostCreate, PostEdit } from "../src/pages/posts";
import { AccountList, AccountCreate, AccountEdit } from "../src/pages/accounts";
import { CategoryList, CategoryCreate, CategoryEdit } from "../src/pages/categories";
/**
* mock auth credentials to simulate authentication
*/
const authCredentials = {
email: "demo@refine.dev",
password: "demodemo",
};
const App: React.FC = () => {
const RememeberMe = () => {
const { register } = useFormContext();
return (
<FormControlLabel
sx={{
span: {
fontSize: "12px",
color: "text.secondary",
},
}}
color="secondary"
control={
<Checkbox size="small" id="rememberMe" {...register("rememberMe")} />
}
label="Remember me"
/>
);
};
return (
<BrowserRouter>
<ThemeProvider theme={RefineThemes.Blue}>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
<RefineSnackbarProvider>
<DevtoolsProvider>
<Refine
authProvider={authProvider}
dataProvider={dataProvider}
routerProvider={routerProvider}
notificationProvider={useNotificationProvider}
resources={[
{
name: "posts",
list: "/posts",
edit: "/posts/edit/:id",
create: "/posts/create",
},
{
name: "accounts",
list: "/accounts",
edit: "/accounts/edit/:id",
create: "/accounts/create",
},
{
name: "categories",
list: "/categories",
edit: "/categories/edit/:id",
create: "/categories/create",
},
]}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
disableTelemetry: true,
}}
>
<Routes>
<Route
element={
<Authenticated
key="authenticated-routes"
fallback={<CatchAllNavigate to="/login" />}
>
<ThemedLayoutV2>
<Outlet />
</ThemedLayoutV2>
</Authenticated>
}
>
<Route
index
element={<NavigateToResource resource="posts" />}
/>
<Route path="/posts">
<Route index element={<PostList />} />
<Route path="create" element={<PostCreate />} />
<Route path="edit/:id" element={<PostEdit />} />
<Route path="delete/:id" element={<PostEdit />} />
</Route>
<Route path="/accounts">
<Route index element={<AccountList />} />
<Route path="create" element={<AccountCreate />} />
<Route path="edit/:id" element={<AccountEdit />} />
</Route>
<Route path="/categories">
<Route index element={<CategoryList />} />
<Route path="create" element={<CategoryCreate />} />
<Route path="edit/:id" element={<CategoryEdit />} />
</Route>
</Route>
<Route
element={
<Authenticated key="auth-pages" fallback={<Outlet />}>
<NavigateToResource resource="posts" />
</Authenticated>
}
>
<Route
path="/login"
element={
<AuthPage type="login" rememberMe={<RememeberMe />} />
}
/>
<Route
path="/register"
element={<AuthPage type="register" />}
/>
<Route
path="/forgot-password"
element={<AuthPage type="forgotPassword" />}
/>
<Route
path="/update-password"
element={<AuthPage type="updatePassword" />}
/>
</Route>
<Route
element={
<Authenticated key="catch-all">
<ThemedLayoutV2>
<Outlet />
</ThemedLayoutV2>
</Authenticated>
}
>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
<DevtoolsPanel />
</DevtoolsProvider>
</RefineSnackbarProvider>
</ThemeProvider>
</BrowserRouter>
);
};
export default App;

View File

View File

@@ -0,0 +1,52 @@
import validator from "@rjsf/validator-ajv8";
import Form from "@rjsf/mui";
import { useEffect, useState } from "react";
import { jsonschemaProvider } from "../../providers/jsonschema-provider";
import { useForm } from "@refinedev/core";
type Props = {
schemaName: string,
resource: string,
id?: string,
//onSubmit: (data: IChangeEvent, event: FormEvent<any>) => void
}
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}
/>
)
}

View File

13
gui/app/src/index.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const container = document.getElementById("root");
// eslint-disable-next-line
const root = createRoot(container!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

26
gui/app/src/interfaces/index.d.ts vendored Normal file
View File

@@ -0,0 +1,26 @@
export interface ICategory {
id: number;
title: string;
}
export type IStatus = "published" | "draft" | "rejected";
export interface IPost {
id: number;
title: string;
content: string;
status: IStatus;
category: ICategory;
}
export type IType = "published" | "draft" | "rejected";
export interface IAccount {
id: number;
name: string;
type: IType;
}
export type Nullable<T> = {
[P in keyof T]: T[P] | null;
};

View File

@@ -0,0 +1,11 @@
import {CrudForm} from "../../common/crud/crud-form";
export const AccountCreate: React.FC = () => {
return (
<CrudForm
schemaName={"AccountCreate"}
resource={"accounts"}
/>
);
};

View File

@@ -0,0 +1,15 @@
import { CrudForm } from "../../common/crud/crud-form";
import { useParams } from "react-router"
export const AccountEdit: React.FC = () => {
const { id } = useParams()
return (
<CrudForm
schemaName={"AccountUpdate"}
resource={"accounts"}
id={id}
/>
);
};

View File

@@ -0,0 +1,3 @@
export * from "./list";
export * from "./create";
export * from "./edit";

View File

@@ -0,0 +1,51 @@
import { useMany } from "@refinedev/core";
import {DeleteButton, EditButton, List, useDataGrid} from "@refinedev/mui";
import React from "react";
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
import type { IAccount } from "../../interfaces";
import {AccountCreate} from "./create";
import {ButtonGroup} from "@mui/material";
export const AccountList: React.FC = () => {
const { dataGridProps } = useDataGrid<IAccount>();
const columns = React.useMemo<GridColDef<IAccount>[]>(
() => [
{ field: "id", headerName: "ID" },
{ field: "name", headerName: "Name", flex: 1 },
{ field: "type", headerName: "Type", flex: 0.3 },
{
field: "actions",
headerName: "Actions",
display: "flex",
renderCell: function render({ row }) {
return (
<ButtonGroup>
<EditButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} />
</ButtonGroup>
);
},
align: "center",
headerAlign: "center",
},
],
[],
);
return (
<List>
<div
style={{
display: "flex",
flexDirection: "column",
maxHeight: "calc(100vh - 320px)",
}}
>
<DataGrid {...dataGridProps} columns={columns} />
</div>
</List>
);
};

View File

@@ -0,0 +1,11 @@
import {CrudForm} from "../../common/crud/crud-form";
export const CategoryCreate: React.FC = () => {
return (
<CrudForm
schemaName={"CategoryCreate"}
resource={"categories"}
/>
);
};

View File

@@ -0,0 +1,15 @@
import { CrudForm } from "../../common/crud/crud-form";
import { useParams } from "react-router"
export const CategoryEdit: React.FC = () => {
const { id } = useParams()
return (
<CrudForm
schemaName={"CategoryUpdate"}
resource={"categories"}
id={id}
/>
);
};

View File

@@ -0,0 +1,3 @@
export * from "./list";
export * from "./create";
export * from "./edit";

View File

@@ -0,0 +1,48 @@
import {DeleteButton, EditButton, List, useDataGrid} from "@refinedev/mui";
import React from "react";
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
import type { IAccount } from "../../interfaces";
import {ButtonGroup} from "@mui/material";
export const CategoryList: React.FC = () => {
const { dataGridProps } = useDataGrid<IAccount>();
const columns = React.useMemo<GridColDef<IAccount>[]>(
() => [
{ field: "id", headerName: "ID" },
{ field: "name", headerName: "Name", flex: 1 },
{
field: "actions",
headerName: "Actions",
display: "flex",
renderCell: function render({ row }) {
return (
<ButtonGroup>
<EditButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} />
</ButtonGroup>
);
},
align: "center",
headerAlign: "center",
},
],
[],
);
return (
<List>
<div
style={{
display: "flex",
flexDirection: "column",
maxHeight: "calc(100vh - 320px)",
}}
>
<DataGrid {...dataGridProps} columns={columns} />
</div>
</List>
);
};

View File

@@ -0,0 +1,122 @@
import type { HttpError } from "@refinedev/core";
import { Create, useAutocomplete } from "@refinedev/mui";
import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import type { IPost, ICategory, IStatus, Nullable } from "../../interfaces";
export const PostCreate: React.FC = () => {
const {
saveButtonProps,
register,
control,
formState: { errors },
} = useForm<IPost, HttpError, Nullable<IPost>>();
const { autocompleteProps } = useAutocomplete<ICategory>({
resource: "categories",
});
return (
<Create saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("title", {
required: "This field is required",
})}
error={!!errors.title}
helperText={errors.title?.message}
margin="normal"
fullWidth
label="Title"
name="title"
autoFocus
/>
<Controller
control={control}
name="status"
rules={{ required: "This field is required" }}
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<Autocomplete<IStatus>
options={["published", "draft", "rejected"]}
{...field}
onChange={(_, value) => {
field.onChange(value);
}}
renderInput={(params) => (
<TextField
{...params}
label="Status"
margin="normal"
variant="outlined"
error={!!errors.status}
helperText={errors.status?.message}
required
/>
)}
/>
)}
/>
<Controller
control={control}
name="category"
rules={{ required: "This field is required" }}
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<Autocomplete
{...autocompleteProps}
{...field}
onChange={(_, value) => {
field.onChange(value);
}}
getOptionLabel={(item) => {
return (
autocompleteProps?.options?.find(
(p) => p?.id?.toString() === item?.id?.toString(),
)?.title ?? ""
);
}}
isOptionEqualToValue={(option, value) =>
value === undefined ||
option?.id?.toString() === (value?.id ?? value)?.toString()
}
renderInput={(params) => (
<TextField
{...params}
label="Category"
margin="normal"
variant="outlined"
error={!!errors.category}
helperText={errors.category?.message}
required
/>
)}
/>
)}
/>
<TextField
{...register("content", {
required: "This field is required",
})}
error={!!errors.content}
helperText={errors.content?.message}
margin="normal"
label="Content"
multiline
rows={4}
/>
</Box>
</Create>
);
};

View File

@@ -0,0 +1,124 @@
import type { HttpError } from "@refinedev/core";
import { Edit, useAutocomplete } from "@refinedev/mui";
import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import type { IPost, ICategory, IStatus, Nullable } from "../../interfaces";
export const PostEdit: React.FC = () => {
const {
saveButtonProps,
refineCore: { query: queryResult },
register,
control,
formState: { errors },
} = useForm<IPost, HttpError, Nullable<IPost>>();
const { autocompleteProps } = useAutocomplete<ICategory>({
resource: "categories",
defaultValue: queryResult?.data?.data.category.id,
});
return (
<Edit saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
{...register("title", {
required: "This field is required",
})}
error={!!errors.title}
helperText={errors.title?.message}
margin="normal"
fullWidth
label="Title"
name="title"
autoFocus
/>
<Controller
control={control}
name="status"
rules={{ required: "This field is required" }}
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<Autocomplete<IStatus>
options={["published", "draft", "rejected"]}
{...field}
onChange={(_, value) => {
field.onChange(value);
}}
renderInput={(params) => (
<TextField
{...params}
label="Status"
margin="normal"
variant="outlined"
error={!!errors.status}
helperText={errors.status?.message}
required
/>
)}
/>
)}
/>
<Controller
control={control}
name="category"
rules={{ required: "This field is required" }}
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<Autocomplete
{...autocompleteProps}
{...field}
onChange={(_, value) => {
field.onChange(value);
}}
getOptionLabel={(item) => {
return (
autocompleteProps?.options?.find(
(p) => p?.id?.toString() === item?.id?.toString(),
)?.title ?? ""
);
}}
isOptionEqualToValue={(option, value) =>
value === undefined ||
option?.id?.toString() === (value?.id ?? value)?.toString()
}
renderInput={(params) => (
<TextField
{...params}
label="Category"
margin="normal"
variant="outlined"
error={!!errors.category}
helperText={errors.category?.message}
required
/>
)}
/>
)}
/>
<TextField
{...register("content", {
required: "This field is required",
})}
error={!!errors.content}
helperText={errors.content?.message}
margin="normal"
label="Content"
multiline
rows={4}
/>
</Box>
</Edit>
);
};

View File

@@ -0,0 +1,3 @@
export * from "./list";
export * from "./create";
export * from "./edit";

View File

@@ -0,0 +1,79 @@
import { useMany } from "@refinedev/core";
import { EditButton, List, useDataGrid } from "@refinedev/mui";
import React from "react";
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
import type { ICategory, IPost } from "../../interfaces";
export const PostList: React.FC = () => {
const { dataGridProps } = useDataGrid<IPost>();
const categoryIds = dataGridProps.rows.map((item) => item.category.id);
const { data: categoriesData, isLoading } = useMany<ICategory>({
resource: "categories",
ids: categoryIds,
queryOptions: {
enabled: categoryIds.length > 0,
},
});
const columns = React.useMemo<GridColDef<IPost>[]>(
() => [
{
field: "id",
headerName: "ID",
type: "number",
width: 50,
},
{ field: "title", headerName: "Title", minWidth: 400, flex: 1 },
{
field: "category.id",
headerName: "Category",
type: "number",
headerAlign: "left",
align: "left",
minWidth: 250,
flex: 0.5,
display: "flex",
renderCell: function render({ row }) {
if (isLoading) {
return "Loading...";
}
const category = categoriesData?.data.find(
(item) => item.id === row.category.id,
);
return category?.title;
},
},
{ field: "status", headerName: "Status", minWidth: 120, flex: 0.3 },
{
field: "actions",
headerName: "Actions",
display: "flex",
renderCell: function render({ row }) {
return <EditButton hideText recordItemId={row.id} />;
},
align: "center",
headerAlign: "center",
minWidth: 80,
},
],
[categoriesData, isLoading],
);
return (
<List>
<div
style={{
display: "flex",
flexDirection: "column",
maxHeight: "calc(100vh - 320px)",
}}
>
<DataGrid {...dataGridProps} columns={columns} />
</div>
</List>
);
};

View File

@@ -0,0 +1,82 @@
import { AuthProvider } from "@refinedev/core";
const API_URL = "http://localhost:8000";
export const authProvider: AuthProvider = {
login: async ({ email, password }) => {
const response = await fetch(
API_URL + "/auth/login",
{
method: "POST",
body: new URLSearchParams({
"grant_type": "password",
"username": email,
password
}).toString(),
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
},
);
const data = await response.json();
if (data.access_token) {
localStorage.setItem("access_token", data.access_token);
return { success: true };
}
return { success: false };
},
logout: async () => {
const response = await fetch(
API_URL + "/auth/logout",
{
method: "POST",
headers: {
Authorization: "Bearer " + localStorage.getItem("access_token"),
},
},
);
if (response.status == 204 || response.status == 401) {
localStorage.removeItem("access_token");
return { success: true };
}
return { success: false };
},
check: async () => {
const token = localStorage.getItem("access_token");
return { authenticated: Boolean(token) };
},
getIdentity: async () => {
const response = await fetch(API_URL + "/users/me", {
headers: {
Authorization: "Bearer " + localStorage.getItem("access_token"),
},
});
if (response.status < 200 || response.status > 299) {
return null;
}
const data = await response.json();
return data;
},
onError: async (error) => {
if (error?.status === 401) {
localStorage.removeItem("access_token");
return Promise<{
redirectTo: "/login",
logout: true,
error: { message: "Unauthorized" },
}>;
}
return {};
},
register: async (params) => { throw new Error("Not implemented"); },
forgotPassword: async (params) => { throw new Error("Not implemented"); },
updatePassword: async (params) => { throw new Error("Not implemented"); },
getPermissions: async () => { throw new Error("Not implemented"); },
};

View File

@@ -0,0 +1,111 @@
import type { DataProvider } from "@refinedev/core";
//const API_URL = "https://api.fake-rest.refine.dev";
const API_URL = "http://localhost:8000";
const fetcher = async (url: string, options?: RequestInit) => {
return fetch(url, {
...options,
headers: {
...options?.headers,
Authorization: "Bearer " + localStorage.getItem("access_token"),
},
});
};
export const dataProvider: DataProvider = {
getOne: async ({ resource, id, meta }) => {
const response = await fetcher(`${API_URL}/${resource}/${id}`);
if (response.status < 200 || response.status > 299) throw response;
const data = await response.json();
return {
data
};
},
update: async ({ resource, id, variables }) => {
const response = await fetcher(`${API_URL}/${resource}/${id}`, {
method: "PUT",
body: JSON.stringify(variables),
headers: {
"Content-Type": "application/json",
},
});
if (response.status < 200 || response.status > 299) throw response;
const data = await response.json();
return { data };
},
getList: async ({ resource, pagination, filters, sorters, meta }) => {
const params = new URLSearchParams();
if (pagination) {
params.append("page", String(pagination.current));
params.append("size", String(pagination.pageSize));
}
if (sorters && sorters.length > 0) {
params.append("_sort", sorters.map((sorter) => sorter.field).join(","));
params.append("_order", sorters.map((sorter) => sorter.order).join(","));
}
if (filters && filters.length > 0) {
filters.forEach((filter) => {
if ("field" in filter && filter.operator === "eq") {
// Our fake API supports "eq" operator by simply appending the field name and value to the query string.
params.append(filter.field, filter.value);
}
});
}
const response = await fetcher(`${API_URL}/${resource}?${params.toString()}`);
if (response.status < 200 || response.status > 299) throw response;
const data = await response.json();
return {
data: data.items,
total: data.total, // We'll cover this in the next steps.
};
},
create: async ({ resource, variables }) => {
const response = await fetcher(`${API_URL}/${resource}`, {
method: "POST",
body: JSON.stringify(variables),
headers: {
"Content-Type": "application/json",
},
});
if (response.status < 200 || response.status > 299) throw response;
const data = await response.json();
return { data };
},
deleteOne: async ({ resource, id, variables, meta }) => {
const response = await fetcher(`${API_URL}/${resource}/${id}`,{
method: "DELETE",
});
if (response.status < 200 || response.status > 299) throw response;
const data = await response.json();
return {
data
};
},
getApiUrl: () => API_URL,
// Optional methods:
// getMany: () => { /* ... */ },
// createMany: () => { /* ... */ },
// deleteMany: () => { /* ... */ },
// updateMany: () => { /* ... */ },
// custom: () => { /* ... */ },
};

View File

@@ -0,0 +1,186 @@
import { RJSFSchema } from '@rjsf/utils';
const API_URL = "http://localhost:8000";
export const jsonschemaProvider = {
getResourceSchema: async (resourceName: string): RJSFSchema => {
return buildResource(await getJsonschema(), resourceName)
}
};
let rawSchema: RJSFSchema;
const getJsonschema = async (): 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)) {
for (let i in prop.oneOf) {
resolveReference(rawSchemas, resource, prop.oneOf[i]);
}
} else if (is_enum(prop)) {
for (let i in prop.allOf) {
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');
}
function is_enum(prop: any) {
return prop.hasOwnProperty('allOf');
}
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)) {
for (const ref of resource.oneOf!) {
return has_descendant(rawSchemas, ref, property_name)
}
} else if (is_enum(resource)) {
for (const ref of resource.allOf!) {
return has_descendant(rawSchemas, ref, 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;
} else if (is_object(resource) && property_name in resource.properties!) {
return resource.properties[property_name];
} 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, property_name)) {
return get_descendant(rawSchemas, ref, property_name);
}
}
} else if (is_enum(resource)) {
for (const ref of resource.allOf!) {
if (has_descendant(rawSchemas, ref, property_name)) {
return get_descendant(rawSchemas, ref, 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
gui/app/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />