Initial commit
This commit is contained in:
23
gui/app/.gitignore
vendored
Normal file
23
gui/app/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
43
gui/app/README.md
Normal file
43
gui/app/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
<div align="center" style="margin: 30px;">
|
||||
<a href="https://refine.dev/">
|
||||
<img alt="refine logo" src="https://refine.ams3.cdn.digitaloceanspaces.com/readme/refine-readme-banner.png">
|
||||
</a>
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://refine.dev">Home Page</a> |
|
||||
<a href="https://discord.gg/refine">Discord</a> |
|
||||
<a href="https://refine.dev/examples/">Examples</a> |
|
||||
<a href="https://refine.dev/blog/">Blog</a> |
|
||||
<a href="https://refine.dev/docs/">Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</br>
|
||||
</br>
|
||||
|
||||
<div align="center"><strong>Build your <a href="https://reactjs.org/">React</a>-based CRUD applications, without constraints.</strong><br>An open source, headless web application framework developed with flexibility in mind.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
[](https://discord.gg/refine)
|
||||
[](https://twitter.com/refine_dev)
|
||||
|
||||
<a href="https://www.producthunt.com/posts/refine-3?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-refine-3" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=362220&theme=light&period=daily" alt="refine - 100% open source React framework to build web apps 3x faster | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
</div>
|
||||
|
||||
## Try this example on your local
|
||||
|
||||
```bash
|
||||
npm create refine-app@latest -- --example auth-material-ui
|
||||
```
|
||||
|
||||
## Try this example on CodeSandbox
|
||||
|
||||
<br/>
|
||||
|
||||
[](https://codesandbox.io/embed/github/refinedev/refine/tree/main/examples/auth-material-ui?view=preview&theme=dark&codemirror=1)
|
||||
36
gui/app/index.html
Normal file
36
gui/app/index.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created using refine" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Material UI Authentication example</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm run dev` or `yarn dev`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
--></body>
|
||||
</html>
|
||||
8755
gui/app/package-lock.json
generated
Normal file
8755
gui/app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
gui/app/package.json
Normal file
53
gui/app/package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "budget-forecast-gui",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc && refine build",
|
||||
"dev": "refine dev --devtools=false",
|
||||
"refine": "refine",
|
||||
"start": "refine start"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.8.2",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/lab": "^6.0.0-beta.14",
|
||||
"@mui/material": "^6.1.7",
|
||||
"@mui/x-data-grid": "^7.23.5",
|
||||
"@refinedev/cli": "^2.16.42",
|
||||
"@refinedev/core": "^4.57.5",
|
||||
"@refinedev/devtools": "^1.2.12",
|
||||
"@refinedev/mui": "^6.0.3",
|
||||
"@refinedev/react-hook-form": "^4.9.3",
|
||||
"@refinedev/react-router": "^1.0.1",
|
||||
"@refinedev/simple-rest": "^5.0.10",
|
||||
"@rjsf/core": "^5.24.1",
|
||||
"@rjsf/mui": "^5.24.1",
|
||||
"@rjsf/utils": "^5.24.1",
|
||||
"@rjsf/validator-ajv8": "^5.24.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-router": "^7.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.16.2",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"typescript": "^5.4.2",
|
||||
"vite": "^5.1.6"
|
||||
}
|
||||
}
|
||||
BIN
gui/app/public/favicon.ico
Normal file
BIN
gui/app/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
45
gui/app/public/index.html
Normal file
45
gui/app/public/index.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created using refine" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Material UI Authentication example</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm run dev` or `yarn dev`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
--></body>
|
||||
</html>
|
||||
15
gui/app/public/manifest.json
Normal file
15
gui/app/public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "Material UI Authentication example",
|
||||
"name": "Material UI Authentication example",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
185
gui/app/src/App.tsx
Normal file
185
gui/app/src/App.tsx
Normal 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;
|
||||
0
gui/app/src/common/crud/crud-card.tsx
Normal file
0
gui/app/src/common/crud/crud-card.tsx
Normal file
52
gui/app/src/common/crud/crud-form.tsx
Normal file
52
gui/app/src/common/crud/crud-form.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
0
gui/app/src/common/crud/crud-list.tsx
Normal file
0
gui/app/src/common/crud/crud-list.tsx
Normal file
13
gui/app/src/index.tsx
Normal file
13
gui/app/src/index.tsx
Normal 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
26
gui/app/src/interfaces/index.d.ts
vendored
Normal 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;
|
||||
};
|
||||
11
gui/app/src/pages/accounts/create.tsx
Normal file
11
gui/app/src/pages/accounts/create.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import {CrudForm} from "../../common/crud/crud-form";
|
||||
|
||||
|
||||
export const AccountCreate: React.FC = () => {
|
||||
return (
|
||||
<CrudForm
|
||||
schemaName={"AccountCreate"}
|
||||
resource={"accounts"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
15
gui/app/src/pages/accounts/edit.tsx
Normal file
15
gui/app/src/pages/accounts/edit.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
3
gui/app/src/pages/accounts/index.tsx
Normal file
3
gui/app/src/pages/accounts/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./list";
|
||||
export * from "./create";
|
||||
export * from "./edit";
|
||||
51
gui/app/src/pages/accounts/list.tsx
Normal file
51
gui/app/src/pages/accounts/list.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
11
gui/app/src/pages/categories/create.tsx
Normal file
11
gui/app/src/pages/categories/create.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import {CrudForm} from "../../common/crud/crud-form";
|
||||
|
||||
|
||||
export const CategoryCreate: React.FC = () => {
|
||||
return (
|
||||
<CrudForm
|
||||
schemaName={"CategoryCreate"}
|
||||
resource={"categories"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
15
gui/app/src/pages/categories/edit.tsx
Normal file
15
gui/app/src/pages/categories/edit.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
3
gui/app/src/pages/categories/index.tsx
Normal file
3
gui/app/src/pages/categories/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./list";
|
||||
export * from "./create";
|
||||
export * from "./edit";
|
||||
48
gui/app/src/pages/categories/list.tsx
Normal file
48
gui/app/src/pages/categories/list.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
122
gui/app/src/pages/posts/create.tsx
Normal file
122
gui/app/src/pages/posts/create.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
124
gui/app/src/pages/posts/edit.tsx
Normal file
124
gui/app/src/pages/posts/edit.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
3
gui/app/src/pages/posts/index.tsx
Normal file
3
gui/app/src/pages/posts/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./list";
|
||||
export * from "./create";
|
||||
export * from "./edit";
|
||||
79
gui/app/src/pages/posts/list.tsx
Normal file
79
gui/app/src/pages/posts/list.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
82
gui/app/src/providers/auth-provider.tsx
Normal file
82
gui/app/src/providers/auth-provider.tsx
Normal 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"); },
|
||||
};
|
||||
111
gui/app/src/providers/data-provider.tsx
Normal file
111
gui/app/src/providers/data-provider.tsx
Normal 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: () => { /* ... */ },
|
||||
};
|
||||
186
gui/app/src/providers/jsonschema-provider.tsx
Normal file
186
gui/app/src/providers/jsonschema-provider.tsx
Normal 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
1
gui/app/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
21
gui/app/tsconfig.json
Normal file
21
gui/app/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
8
gui/app/tsconfig.node.json
Normal file
8
gui/app/tsconfig.node.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
6
gui/app/vite.config.ts
Normal file
6
gui/app/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
Reference in New Issue
Block a user