initialization

This commit is contained in:
2025-01-10 12:40:50 +01:00
commit 5f0ffc703a
28 changed files with 4683 additions and 0 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
VITE_SIMPLE_REST_URL=http://localhost:8083/core/api/admin

21
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,21 @@
module.exports = {
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"prettier",
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
env: {
browser: true,
es2021: true,
},
settings: {
react: {
version: "detect",
},
},
};

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

47
README.md Normal file
View File

@@ -0,0 +1,47 @@
# tfp-frontend-poc
## Installation
Install the application dependencies by running:
```sh
npm install
# or
yarn install
```
## Development
Start the application in development mode by running:
```sh
npm run dev
# or
yarn dev
```
## Production
Build the application in production mode by running:
```sh
npm run build
# or
yarn build
```
## DataProvider
The included data provider use [ra-data-simple-rest](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-simple-rest). It fits REST APIs using simple GET parameters for filters and sorting. This is the dialect used for instance in [FakeRest](https://github.com/marmelab/FakeRest).
You'll find an `.env` file at the project root that includes a `VITE_JSON_SERVER_URL` variable. Set it to the URL of your backend.
## Authentication
The included auth provider should only be used for development and test purposes.
You'll find a `users.json` file in the `src` directory that includes the users you can use.
You can sign in to the application with the following usernames and password:
- janedoe / password
- johndoe / password

119
index.html Normal file
View File

@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="./manifest.json" />
<link rel="shortcut icon" href="./favicon.ico" />
<title>tfp-frontend-poc</title>
<style>
.loader-container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #fafafa;
}
/* CSS Spinner from https://projects.lukehaas.me/css-loaders/ */
.loader,
.loader:before,
.loader:after {
border-radius: 50%;
}
.loader {
color: #283593;
font-size: 11px;
text-indent: -99999em;
margin: 55px auto;
position: relative;
width: 10em;
height: 10em;
box-shadow: inset 0 0 0 1em;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.loader:before,
.loader:after {
position: absolute;
content: '';
}
.loader:before {
width: 5.2em;
height: 10.2em;
background: #fafafa;
border-radius: 10.2em 0 0 10.2em;
top: -0.1em;
left: -0.1em;
-webkit-transform-origin: 5.2em 5.1em;
transform-origin: 5.2em 5.1em;
-webkit-animation: load2 2s infinite ease 1.5s;
animation: load2 2s infinite ease 1.5s;
}
.loader:after {
width: 5.2em;
height: 10.2em;
background: #fafafa;
border-radius: 0 10.2em 10.2em 0;
top: -0.1em;
left: 5.1em;
-webkit-transform-origin: 0px 5.1em;
transform-origin: 0px 5.1em;
-webkit-animation: load2 2s infinite ease;
animation: load2 2s infinite ease;
}
@-webkit-keyframes load2 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load2 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
</style>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root">
<div class="loader-container">
<div class="loader">Loading...</div>
</div>
</div>
</body>
<script type="module" src="/src/index.tsx"></script>
</html>

39
package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"type-check": "tsc --noEmit",
"lint": "eslint --fix --ext .js,.jsx,.ts,.tsx ./src",
"format": "prettier --write ./src"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^5.16.14",
"@mui/material": "^5.16.14",
"@tanstack/react-query": "^5.63.0",
"ra-data-simple-rest": "^5.4.0",
"react": "^18.3.0",
"react-admin": "^5.4.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"@types/node": "^20.10.7",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"@vitejs/plugin-react": "^4.0.1",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^3.3.3",
"typescript": "^5.1.6",
"vite": "^5.3.5"
},
"name": "tfp-frontend-poc"
}

4084
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

1
prettier.config.js Normal file
View File

@@ -0,0 +1 @@
export default {}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

15
public/manifest.json Normal file
View File

@@ -0,0 +1,15 @@
{
"short_name": "tfp-frontend-poc",
"name": "{{name}}",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

27
src/App.tsx Normal file
View File

@@ -0,0 +1,27 @@
import {
Admin,
EditGuesser,
ListGuesser,
Resource,
ShowGuesser,
} from "react-admin";
import { Layout } from "./Layout";
import { dataProvider } from "./dataProvider";
import { authProvider } from "./authProvider";
import { AdminDashboard } from '@admin';
export const App = () => (
<Admin
layout={Layout}
dashboard={AdminDashboard}
dataProvider={dataProvider}
authProvider={authProvider}
>
<Resource
name="person"
list={ListGuesser}
edit={EditGuesser}
show={ShowGuesser}
/>
</Admin>
);

10
src/Layout.tsx Normal file
View File

@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
import { CheckForApplicationUpdate, Layout as RALayout } from "react-admin";
import { AdminMenu } from "@admin";
export const Layout = ({ children }: { children: ReactNode }) => (
<RALayout menu={AdminMenu}>
{children}
<CheckForApplicationUpdate />
</RALayout>
);

View File

@@ -0,0 +1,20 @@
import { Card, CardContent } from "@mui/material";
import { Title, useDataProvider } from "react-admin";
import { useQuery } from "@tanstack/react-query";
export const AdminDashboard = () => {
const dataProvider = useDataProvider();
const { data } = useQuery({
queryKey: ["dashboard"],
queryFn: () => dataProvider.person(),
});
console.log(data);
return (
<Card>
<Title title="Welcome to the administration" />
<CardContent>Lorem ipsum sic dolor amet...</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,7 @@
import { Menu } from "react-admin";
export const AdminMenu = () => (
<Menu>
<Menu.DashboardItem />
</Menu>
);

View File

@@ -0,0 +1,2 @@
export { AdminMenu } from './admin-menu';
export { AdminDashboard } from './admin-dashboard';

View File

@@ -0,0 +1,19 @@
import { useQueryEngine } from "@core";
export const dataProviderExtension = () => {
const { fetchCommon } = useQueryEngine();
return {
person: () =>
fetchCommon(
"http://localhost:8081/core/api/admin/person",
{
headers: new Headers({
Accept: "application/json",
'X-XSRF-TOKEN': 'c3aKjTfLsPSdmiMtTaj933RbsQMR6IWmf9C5ZImMh9pCmDGkEETougKpiZew-UcZe4XJ5xE4nDpwirWLR-SOXbm94-Im-QLB',
'Cookie': 'LAST_LOGIN_LOCATION=http://localhost:3000; SESSION=652f7c23-fe94-4727-a980-59052e75aab2; XSRF-TOKEN=c2b75b9c-cd46-48ec-9ab0-847901d8da3e',
}),
},
),
};
};

View File

@@ -0,0 +1 @@
export { useQueryEngine } from './useQueryEngine';

View File

@@ -0,0 +1,77 @@
export const useQueryEngine = () => {
const defaultHeaders = {
Accept: 'application/ld+json',
'Content-Type': 'application/ld+json',
};
const response = async (request) => {
let response;
try {
response = await fetch(request);
} catch (e) {
throw new Error('Network error');
}
if (200 > response?.status || 300 <= response?.status) {
throw new Error(response.statusText);
}
return response.json();
};
return ({
defaultHeaders,
fetchCommon: async (url, options = {}) => {
return await response(
new Request(url, {
...(options?.method ? { method: options.method } : { method: 'GET' }),
...(options.replaceHeaders
? { headers: options.replaceHeaders }
: {
headers: {
...defaultHeaders,
Authorization: `Bearer ${localStorage.getItem('token')}`,
...(options?.headers ? { ...options.headers } : {}),
},
}),
...(options?.body ? { body: options.body } : {}),
}),
);
},
fetchMultipart: async (url, options) => {
return await response(
new Request(url, {
method: 'POST',
headers: {
Accept: 'application/ld+json',
/**
* DO NOT SPECIFY THIS - Because the boundary data in it
* @see https://stackoverflow.com/a/71392989/3111514
*/
// 'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
...options,
}),
);
},
fetchPlain: async (url, options = {}, withAuth = false) => {
return await response(
new Request(url, {
...(options?.method ? { method: options.method } : { method: 'GET' }),
...(options.replaceHeaders
? { headers: options.replaceHeaders }
: {
headers: {
'Content-Type': 'application/json',
...(options?.headers ? { headers: options.headers } : {}),
...(withAuth ? { Authorization: `Bearer ${localStorage.getItem('token')}` } : {}),
},
}),
...(options?.body ? { body: options.body } : {}),
}),
);
},
});
};

2
src/admin-core/index.js Normal file
View File

@@ -0,0 +1,2 @@
export { dataProviderExtension } from './data-provider-extension';
export * from './hooks';

63
src/authProvider.ts Normal file
View File

@@ -0,0 +1,63 @@
import { AuthProvider, HttpError } from "react-admin";
export const authProvider: AuthProvider = {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
login: async ({ username, password }) => {
let response;
try {
response = await fetch(
new Request("http://localhost:8080/atsp-idp/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "authorization_code",
code: "code",
client_id: "client_id",
}),
headers: new Headers({
"Content-Type": "application/x-www-form-urlencoded",
}),
}),
);
} catch (_error) {
return Promise.reject(
new HttpError("Unauthorized", 401, {
message: "Invalid username or password",
}),
);
}
if (response.status < 200 || response.status >= 300) {
return Promise.reject(
new HttpError("Unauthorized", 401, {
message: "Invalid username or password",
}),
);
}
const { access_token } = await response.json();
localStorage.setItem("token", access_token);
localStorage.setItem("user", access_token);
return Promise.resolve();
},
logout: () => {
localStorage.removeItem("user");
return Promise.resolve();
},
checkError: () => Promise.resolve(),
checkAuth: () =>
localStorage.getItem("user") ? Promise.resolve() : Promise.reject(),
getPermissions: () => {
return Promise.resolve(undefined);
},
getIdentity: () => {
const persistedUser = localStorage.getItem("user");
const user = persistedUser ? JSON.parse(persistedUser) : null;
return Promise.resolve(user);
},
};
export default authProvider;

7
src/dataProvider.ts Normal file
View File

@@ -0,0 +1,7 @@
import simpleRestProvider from "ra-data-simple-rest";
import { dataProviderExtension } from "@core";
export const dataProvider = {
...simpleRestProvider(import.meta.env.VITE_SIMPLE_REST_URL),
...dataProviderExtension(),
};

9
src/index.tsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

18
src/users.json Normal file

File diff suppressed because one or more lines are too long

1
src/vite-env.d.ts vendored Normal file
View File

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

27
tsconfig.app.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

11
tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}

13
tsconfig.node.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
}

18
vite.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: true,
},
base: "./",
resolve: {
alias: {
"@admin": path.resolve(__dirname, "./src/admin-components"),
"@core": path.resolve(__dirname, "./src/admin-core"),
},
},
});