yaml-admin-front 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,40 +1,30 @@
1
1
  {
2
2
  "name": "yaml-admin-front",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
- "license": "MIT",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "vite build",
9
- "lint": "eslint .",
10
- "preview": "vite preview"
11
- },
5
+ "description": "React components for yaml-admin front (library)",
6
+ "main": "src/index.js",
7
+ "scripts": {},
12
8
  "dependencies": {
9
+ "@iconify/react": "^6.0.0",
10
+ "axios": "^1.11.0",
11
+ "js-yaml": "^4.1.0",
13
12
  "ra-data-json-server": "^5.10.1",
14
13
  "react": "^19.1.1",
15
14
  "react-admin": "^5.10.1",
16
- "react-dom": "^19.1.1"
15
+ "react-dom": "^19.1.1",
16
+ "yaml": "^2.8.1"
17
17
  },
18
18
  "devDependencies": {
19
- "@eslint/js": "^9.32.0",
20
19
  "@types/react": "^19.1.9",
21
- "@types/react-dom": "^19.1.7",
22
- "@vitejs/plugin-react": "^4.7.0",
23
- "eslint": "^9.32.0",
24
- "eslint-plugin-react-hooks": "^5.2.0",
25
- "eslint-plugin-react-refresh": "^0.4.20",
26
- "globals": "^16.3.0",
27
- "vite": "^7.1.0"
20
+ "@types/react-dom": "^19.1.7"
28
21
  },
29
22
  "repository": {
30
23
  "type": "git",
31
24
  "url": "git+https://github.com/muyoungko/yaml_admin.git"
32
25
  },
33
26
  "files": [
34
- "dist",
35
27
  "src",
36
- "index.html",
37
- "vite.config.js",
38
28
  "README.md"
39
29
  ],
40
30
  "publishConfig": {
@@ -0,0 +1,14 @@
1
+ import React, { createContext, useContext } from 'react';
2
+
3
+ // Context to share parsed admin YAML JSON across the app
4
+ export const AdminContext = createContext({ yml: null, setYml: () => {} });
5
+
6
+ export const AdminProvider = ({ initialYml = null, children }) => {
7
+ return <AdminContext.Provider value={initialYml}>{children}</AdminContext.Provider>;
8
+ };
9
+
10
+ export const useAdminContext = () => {
11
+ return useContext(AdminContext);
12
+ };
13
+
14
+
@@ -0,0 +1,55 @@
1
+ import { Admin, Resource, ListGuesser } from "react-admin";
2
+ import jsonServerProvider from "ra-data-json-server";
3
+ import YAML from 'yaml';
4
+ import LoginPage from './login/LoginPage';
5
+ import MyLayout from './layout/MyLayout'
6
+ import { useState, useEffect } from 'react';
7
+ import { Icon } from '@iconify/react';
8
+ import { AdminProvider } from './AdminContext';
9
+ import authProvider from './login/authProvider';
10
+ import { setApiHost} from './common/axios';
11
+
12
+ const API_HOST = import.meta.env.VITE_HOST_API || 'http://localhost:6911'
13
+ const dataProvider = jsonServerProvider(API_HOST);
14
+
15
+ const YMLAdmin = ({ adminYaml }) => {
16
+ const [yml, setYml] = useState(null);
17
+ useEffect(() => {
18
+ const loadYamlFile = async () => {
19
+ try {
20
+ const json = YAML.parse(adminYaml);
21
+ setYml(json);
22
+ setApiHost(json['api-host'].uri);
23
+ } catch (error) {
24
+ console.error('YAML 파일을 읽는 중 오류가 발생했습니다:', error);
25
+ }
26
+ };
27
+
28
+ loadYamlFile();
29
+ }, []);
30
+
31
+ return (
32
+ <AdminProvider initialYml={yml} width="1250px">
33
+ <Admin
34
+ layout={MyLayout}
35
+ authProvider={authProvider}
36
+ dataProvider={dataProvider}>
37
+ {yml?.entity && Object.keys(yml.entity).map(name => {
38
+ const entity = yml.entity[name];
39
+ const IconComponent = entity?.icon
40
+ ? () => <Icon icon={entity.icon} width="1.25rem" height="1.25rem" />
41
+ : undefined;
42
+ return (
43
+ <Resource key={name} name={name}
44
+ options={{ label: entity.label }}
45
+ icon={IconComponent}
46
+ list={ListGuesser} />
47
+ )
48
+ })}
49
+
50
+ </Admin>
51
+ </AdminProvider>
52
+ )
53
+ };
54
+
55
+ export default YMLAdmin;
@@ -0,0 +1,78 @@
1
+ import axios from 'axios';
2
+
3
+ // ----------------------------------------------------------------------
4
+
5
+ const axiosInstance = axios.create({});
6
+
7
+ export const setApiHost = (host) => {
8
+ let base = host ?? import.meta.env.VITE_HOST_API ?? 'http://localhost:6911';
9
+ if (base && !base.startsWith('http')) {
10
+ base = `http://${base}`;
11
+ }
12
+ axiosInstance.defaults.baseURL = base;
13
+ };
14
+
15
+ // initialize with defaults so it's usable before YAML is loaded
16
+ setApiHost();
17
+
18
+ axiosInstance.interceptors.response.use(
19
+ (res) => res,
20
+ (error) => Promise.reject((error.response && error.response.data) || 'Something went wrong')
21
+ );
22
+
23
+ export default axiosInstance;
24
+
25
+ // ----------------------------------------------------------------------
26
+
27
+ export const fetcher = async (args) => {
28
+ const [url, config] = Array.isArray(args) ? args : [args];
29
+
30
+ const res = await axiosInstance.get(url, { ...config });
31
+
32
+ return res.data;
33
+ };
34
+
35
+ export const postFetcher = async (url, config, param) => {
36
+ const res = await axiosInstance.post(url, param, { ...config });
37
+
38
+ return res.data;
39
+ };
40
+
41
+ export const putFetcher = async (url, config, param) => {
42
+ const res = await axiosInstance.put(url, param, { ...config });
43
+
44
+ return res.data;
45
+ };
46
+
47
+ export const uploadFile = (blob_path, file_name) => new Promise((resolve, reject) => {
48
+ if(blob_path){
49
+ const reader = new FileReader();
50
+ reader.readAsArrayBuffer(blob_path);
51
+ reader.onload = async () => {
52
+ const ext = file_name.substring(file_name.lastIndexOf('.')+1, file_name.length);
53
+ const res = await fetcher(`/api/media/url/put/${ext}`)
54
+ fetch(res.upload_url, { method: "PUT",
55
+ headers: {
56
+ 'Content-Type': res.contentType
57
+ },
58
+ body:reader.result })
59
+ .then(res2=>{
60
+ resolve(res.key)
61
+ });
62
+ };
63
+ reader.onerror = reject;
64
+ } else {
65
+ resolve(null);
66
+ }
67
+ });
68
+
69
+ export const convertFileToBase64 = file => new Promise((resolve, reject) => {
70
+ if(file){
71
+ const reader = new FileReader();
72
+ reader.readAsDataURL(file);
73
+ reader.onload = () => resolve(reader.result);
74
+ reader.onerror = reject;
75
+ } else {
76
+ reject(new Error('no file'))
77
+ }
78
+ });
@@ -0,0 +1,78 @@
1
+ import axios from 'axios';
2
+
3
+ // ----------------------------------------------------------------------
4
+
5
+ const axiosInstance = axios.create({});
6
+
7
+ export const setUpApiHost = (host) => {
8
+ let base = host ?? import.meta.env.VITE_HOST_API ?? 'http://localhost:6911';
9
+ if (base && !base.startsWith('http')) {
10
+ base = `http://${base}`;
11
+ }
12
+ axiosInstance.defaults.baseURL = base;
13
+ };
14
+
15
+ // initialize with defaults so it's usable before YAML is loaded
16
+ setUpApiHost();
17
+
18
+ axiosInstance.interceptors.response.use(
19
+ (res) => res,
20
+ (error) => Promise.reject((error.response && error.response.data) || 'Something went wrong')
21
+ );
22
+
23
+ export default axiosInstance;
24
+
25
+ // ----------------------------------------------------------------------
26
+
27
+ export const fetcher = async (args) => {
28
+ const [url, config] = Array.isArray(args) ? args : [args];
29
+
30
+ const res = await axiosInstance.get(url, { ...config });
31
+
32
+ return res.data;
33
+ };
34
+
35
+ export const postFetcher = async (url, config, param) => {
36
+ const res = await axiosInstance.post(url, param, { ...config });
37
+
38
+ return res.data;
39
+ };
40
+
41
+ export const putFetcher = async (url, config, param) => {
42
+ const res = await axiosInstance.put(url, param, { ...config });
43
+
44
+ return res.data;
45
+ };
46
+
47
+ export const uploadFile = (blob_path, file_name) => new Promise((resolve, reject) => {
48
+ if(blob_path){
49
+ const reader = new FileReader();
50
+ reader.readAsArrayBuffer(blob_path);
51
+ reader.onload = async () => {
52
+ const ext = file_name.substring(file_name.lastIndexOf('.')+1, file_name.length);
53
+ const res = await fetcher(`/api/media/url/put/${ext}`)
54
+ fetch(res.upload_url, { method: "PUT",
55
+ headers: {
56
+ 'Content-Type': res.contentType
57
+ },
58
+ body:reader.result })
59
+ .then(res2=>{
60
+ resolve(res.key)
61
+ });
62
+ };
63
+ reader.onerror = reject;
64
+ } else {
65
+ resolve(null);
66
+ }
67
+ });
68
+
69
+ export const convertFileToBase64 = file => new Promise((resolve, reject) => {
70
+ if(file){
71
+ const reader = new FileReader();
72
+ reader.readAsDataURL(file);
73
+ reader.onload = () => resolve(reader.result);
74
+ reader.onerror = reject;
75
+ } else {
76
+ reject(new Error('no file'))
77
+ }
78
+ });
package/src/index.js ADDED
@@ -0,0 +1 @@
1
+ export { default as YMLAdmin } from './YMLAdmin.jsx';
@@ -0,0 +1,60 @@
1
+ import React, { forwardRef, useState, useEffect } from 'react'
2
+ import { AppBar, UserMenu, Logout, MenuItemLink, useRefresh, useUserMenu, LoadingIndicator } from 'react-admin';
3
+ import Avatar from '@mui/material/Avatar';
4
+ // import client from '../common/client'
5
+ import { Badge, Typography, Toolbar, Tabs, Tab, Button, Box, List as CoreList, Chip, CircularProgress } from '@mui/material';
6
+
7
+
8
+ const ProjectMenu = forwardRef((props, ref) => {
9
+ const { onClose } = useUserMenu();
10
+ const refresh = useRefresh();
11
+
12
+ useEffect(() => {
13
+ }, [])
14
+
15
+ return (
16
+ <MenuItemLink
17
+ ref={ref}
18
+ to="/screen"
19
+ primaryText={<Typography color={selected ? 'primary' : ''}>{props.project.name}</Typography>}
20
+ leftIcon={<MyCustomIcon url={props.project.img && props.project.img.src} />}
21
+ onClick={handleClick} // close the menu on click
22
+ />
23
+ )
24
+ });
25
+
26
+ const MyUserMenu = props => {
27
+ const [projectList, setProjectList] = useState([]);
28
+ const [url, setUrl] = useState({});
29
+
30
+ useEffect(() => {
31
+
32
+ }, [])
33
+
34
+ const handleChange = () => {
35
+ let selected_project = getSelectedProject()
36
+ projectList.map(m => {
37
+ if (selected_project == m.id)
38
+ if (m.img)
39
+ setUrl(m.img.src)
40
+ else
41
+ setUrl(null)
42
+ })
43
+ }
44
+ return (
45
+ <UserMenu {...props}>
46
+ {projectList.map(m => (
47
+ <ProjectMenu key={m.id} project={m} onChange={handleChange} />
48
+ ))}
49
+ <Logout />
50
+ </UserMenu>
51
+ )
52
+ }
53
+
54
+
55
+ const MyAppBar = props => <AppBar {...props} userMenu={<MyUserMenu />} >
56
+
57
+ </AppBar>;
58
+
59
+
60
+ export default MyAppBar;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { Layout, AppBar } from 'react-admin';
3
+ import MyAppBar from './MyAppBar'
4
+ import MyMenu from './MyMenu'
5
+
6
+ const MyLayout = (props) => <Layout {...props} appBar={MyAppBar} menu={MyMenu} />;
7
+
8
+ export default MyLayout;
@@ -0,0 +1,62 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { Menu } from 'react-admin';
3
+ import SubMenu from './SubMenu';
4
+ import { Icon } from '@iconify/react';
5
+ import { useAdminContext } from '../AdminContext';
6
+
7
+ const MyMenu = () => {
8
+ const yml = useAdminContext();
9
+ const [state, setState] = useState({
10
+
11
+ });
12
+ const handleToggle = (menu) => {
13
+ setState(state => ({ ...state, [menu]: !state[menu] }));
14
+ };
15
+
16
+ const categoryList = useMemo(() => {
17
+ const list = yml?.front?.category || []
18
+ list.forEach(m=>{
19
+ state[m.name] = true
20
+ m.menuList = yml?.entity && Object.keys(yml.entity).map(m=>{
21
+ let r = yml.entity[m]
22
+ r.name = m
23
+ return r
24
+ }).filter(f=>f.category == m.name)
25
+ })
26
+ return list
27
+ }, [yml]);
28
+
29
+ const noCartegoryList = useMemo(() => {
30
+ const list = yml?.entity && Object.keys(yml.entity).map(m=>{
31
+ let r = yml.entity[m]
32
+ r.name = m
33
+ return r
34
+ }).filter(f=>!f.category)
35
+ return list || [];
36
+ }, [yml]);
37
+
38
+ return (
39
+ <Menu>
40
+ <Menu.DashboardItem />
41
+
42
+ {/* <Menu.ResourceItem name='member' /> */}
43
+ {categoryList.map(c => {
44
+ return <SubMenu
45
+ key={c.name}
46
+ handleToggle={() => handleToggle(c.name)}
47
+ isOpen={state[c.name]}
48
+ name={c.name}
49
+ icon={<Icon icon={c.icon} />}
50
+ dense={true}
51
+ >
52
+ {c.menuList.map(m => <Menu.ResourceItem key={m.name} name={m.name} />)}
53
+ </SubMenu>
54
+ })}
55
+
56
+ {noCartegoryList.map(m => <Menu.ResourceItem key={m.name} name={m.name} />)}
57
+
58
+ </Menu>
59
+ )
60
+ };
61
+
62
+ export default MyMenu;
@@ -0,0 +1,65 @@
1
+ import * as React from 'react';
2
+ import { ReactElement, ReactNode } from 'react';
3
+ import {
4
+ List,
5
+ MenuItem,
6
+ ListItemIcon,
7
+ Typography,
8
+ Collapse,
9
+ Tooltip,
10
+ } from '@mui/material';
11
+ import ExpandMore from '@mui/icons-material/ExpandMore';
12
+ import { useTranslate, useSidebarState } from 'react-admin';
13
+
14
+
15
+ const SubMenu = (props) => {
16
+ const { handleToggle, isOpen, name, icon, children, dense } = props;
17
+ const translate = useTranslate();
18
+
19
+ const [sidebarIsOpen] = useSidebarState();
20
+
21
+ const header = (
22
+ <MenuItem dense={dense} onClick={handleToggle}>
23
+ <ListItemIcon sx={{ minWidth: 5 }}>
24
+ {isOpen ? <ExpandMore /> : icon}
25
+ </ListItemIcon>
26
+ <Typography variant="inherit" color="textSecondary">
27
+ {translate(name)}
28
+ </Typography>
29
+ </MenuItem>
30
+ );
31
+
32
+ return (
33
+ <div>
34
+ {sidebarIsOpen || isOpen ? (
35
+ header
36
+ ) : (
37
+ <Tooltip title={translate(name)} placement="right">
38
+ {header}
39
+ </Tooltip>
40
+ )}
41
+ <Collapse in={isOpen} timeout="auto" unmountOnExit>
42
+ <List
43
+ dense={dense}
44
+ component="div"
45
+ disablePadding
46
+ className="SubMenu"
47
+ sx={{
48
+ '& .MuiMenuItem-root': {
49
+ transition:
50
+ 'padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms',
51
+ paddingLeft: theme =>
52
+ sidebarIsOpen
53
+ ? theme.spacing(4)
54
+ : theme.spacing(2),
55
+ },
56
+ }}
57
+ >
58
+ {children}
59
+ </List>
60
+ </Collapse>
61
+ </div>
62
+ );
63
+ };
64
+
65
+ export default SubMenu;
@@ -0,0 +1,155 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import {
3
+ useLogin,
4
+ useNotify
5
+ } from 'react-admin';
6
+
7
+ import {
8
+ Box,
9
+ Typography
10
+ } from '@mui/material';
11
+ import client from '../common/axios.jsx';
12
+
13
+ const LoginPage = ({ theme }) => {
14
+ const login = useLogin();
15
+ const divRef = useRef(null);
16
+
17
+ const [loading, setLoading] = React.useState(false)
18
+ const [email, setEmail] = React.useState('')
19
+ const [password, setPassword] = React.useState('')
20
+
21
+ useEffect(() => {
22
+
23
+ }, [])
24
+
25
+ useEffect(() => {
26
+ if (divRef.current && window.google) {
27
+ window.google.accounts.id.initialize({
28
+ client_id: '886217236574-52ccfmm2nj12dc63lcfb0ksms2akkimm.apps.googleusercontent.com',
29
+ context:'signin',
30
+ itp_support: true,
31
+ ux_mode: "redirect",
32
+ login_uri:`${api_host}/google/login/callback`,
33
+ callback: (res, error) => {
34
+ // This is the function that will be executed once the authentication with google is finished
35
+ },
36
+ });
37
+ window.google.accounts.id.renderButton(divRef.current, {
38
+ // theme: 'filled_blue',
39
+ // size: 'medium',
40
+ // type: 'standard',
41
+ // text: 'continue_with',
42
+ });
43
+ }
44
+ }, [divRef.current, window.google]);
45
+
46
+ const handleSubmit = e => {
47
+ e.preventDefault();
48
+ }
49
+
50
+ const handleLogin = (e) => {
51
+ setLoading(true)
52
+ client.request_post('/member/login', {
53
+ type: 'force',
54
+ email: email,
55
+ pass: password,
56
+ }).then((res) => {
57
+ setLoading(false)
58
+ if (res && res.r) {
59
+ localStorage.setItem('token', res.token);
60
+ window.location.href = '/'
61
+ } else if (res) {
62
+ alert(res.msg)
63
+ }
64
+ })
65
+ }
66
+
67
+ return (
68
+ <Box>
69
+ <form onSubmit={handleSubmit}>
70
+ <Box
71
+ display="flex"
72
+ justifyContent="center"
73
+ alignItems="center"
74
+ minHeight="100vh"
75
+ >
76
+ <Box>
77
+ <Box>
78
+ <img
79
+ width='200'
80
+ height='200'
81
+ alt='testA' />
82
+ </Box>
83
+ <Box mt={2}>
84
+ <Typography variant="h4" gutterBottom>
85
+ Devil App Builder
86
+ </Typography>
87
+ </Box>
88
+ <Box>
89
+ <Typography variant="subtitle1" gutterBottom>
90
+ Build your app by yourself
91
+ </Typography>
92
+ </Box>
93
+ {/* <Box mt={2}>
94
+ <TextField
95
+ fullWidth
96
+ variant="filled"
97
+ label="Google Email"
98
+ onChange={(e) => {setEmail(e.target.value)}}
99
+ defaultValue=""
100
+ />
101
+ </Box>
102
+ <Box>
103
+ <TextField
104
+ fullWidth
105
+ variant="filled"
106
+ label="Password"
107
+ type="password"
108
+ onChange={(e) => {setPassword(e.target.value)}}
109
+ autoComplete="current-password"
110
+ />
111
+ </Box>
112
+ <Box mt={1} sx={{ display: 'flex', justifyContent: 'flex-start' }}>
113
+ <Box>
114
+ <Button
115
+ disabled={loading}
116
+ startIcon={loading && <CircularProgress color="success" size={24} />}
117
+ onClick={handleLogin}
118
+ sx={{ width: '280px' }} variant="contained">Sign In</Button>
119
+ </Box>
120
+ <Box ml={1}>
121
+ <Button
122
+ onClick={handleJoin}
123
+ variant="outlined">Sign Up</Button>
124
+ </Box>
125
+ </Box> */}
126
+ <Box>
127
+ <div ref={divRef} />
128
+ {/* <div id="g_id_onload"
129
+
130
+ data-client_id="886217236574-52ccfmm2nj12dc63lcfb0ksms2akkimm.apps.googleusercontent.com"
131
+ data-context="signin"
132
+ data-ux_mode="redirect"
133
+ data-login_uri={`${api_host}/google/login/callback`}
134
+ data-itp_support="true">
135
+ </div>
136
+
137
+ <div class="g_id_signin"
138
+ data-type="standard"
139
+ data-shape="pill"
140
+ data-theme="filled_black"
141
+ data-text="signin_with"
142
+ data-size="large"
143
+ data-logo_alignment="left">
144
+ </div> */}
145
+ </Box>
146
+ </Box>
147
+
148
+ </Box>
149
+ </form>
150
+ </Box>
151
+ );
152
+
153
+ };
154
+
155
+ export default LoginPage;