Skip to content

feat: Add workspace with vite project for v2 dashboard #2540

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 20 commits into
base: alpha
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.DS_Store
node_modules/
bundles/
PIG/bundles/
Parse-Dashboard/public/bundles/
Parse-Dashboard/public/v2/
Parse-Dashboard/v2/
Parse-Dashboard/parse-dashboard-config.json
npm-debug.log
.eslintcache

// vim .swp
*.swp
.env
.idea/

logs/
test_logs

# visual studio code
.vscode

.history
.turbo

v2
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@ node_modules/
bundles/
PIG/bundles/
Parse-Dashboard/public/bundles/
Parse-Dashboard/public/v2/
Parse-Dashboard/v2/
Parse-Dashboard/parse-dashboard-config.json
npm-debug.log
.eslintcache
@@ -17,3 +19,6 @@ test_logs

# visual studio code
.vscode

.history
.turbo
1 change: 1 addition & 0 deletions .lintstagedrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "{src,webpack}/{**/*,*}.js": ["prettier --write", "eslint --fix --cache"] }
3 changes: 0 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -21,9 +21,6 @@ COPY . /src
# Install remaining dev dependencies
RUN npm ci

# Run all webpack build steps
RUN npm run prepare && npm run build

############################################################
# Release stage
############################################################
67 changes: 42 additions & 25 deletions Parse-Dashboard/app.js
Original file line number Diff line number Diff line change
@@ -36,13 +36,13 @@ function checkIfIconsExistForApps(apps, iconsFolder) {
const iconName = currentApp.iconName;
const path = iconsFolder + '/' + iconName;

fs.stat(path, function(err) {
fs.stat(path, function (err) {
if (err) {
if ('ENOENT' == err.code) {// file does not exist
if ('ENOENT' == err.code) {
// file does not exist
console.warn('Icon with file name: ' + iconName + ' couldn\'t be found in icons folder!');
} else {
console.log(
'An error occurd while checking for icons, please check permission!');
console.log('An error occurd while checking for icons, please check permission!');
}
} else {
//every thing was ok so for example you can read it and send it to client
@@ -51,37 +51,42 @@ function checkIfIconsExistForApps(apps, iconsFolder) {
}
}

module.exports = function(config, options) {
module.exports = function (config, options) {
options = options || {};
const app = express();
// Serve public files.
app.use(express.static(path.join(__dirname,'public')));
app.use(express.static(path.join(__dirname, 'public')));

// Allow setting via middleware
if (config.trustProxy && app.disabled('trust proxy')) {
app.enable('trust proxy');
}

// wait for app to mount in order to get mountpath
app.on('mount', function() {
app.on('mount', function () {
const mountPath = getMount(app.mountpath);
const users = config.users;
const useEncryptedPasswords = config.useEncryptedPasswords ? true : false;
const authInstance = new Authentication(users, useEncryptedPasswords, mountPath);
authInstance.initialize(app, { cookieSessionSecret: options.cookieSessionSecret, cookieSessionMaxAge: options.cookieSessionMaxAge });
authInstance.initialize(app, {
cookieSessionSecret: options.cookieSessionSecret,
cookieSessionMaxAge: options.cookieSessionMaxAge,
});

// CSRF error handler
app.use(function (err, req, res, next) {
if (err.code !== 'EBADCSRFTOKEN') {return next(err)}
if (err.code !== 'EBADCSRFTOKEN') {
return next(err);
}

// handle CSRF token errors here
res.status(403)
res.send('form tampered with')
res.status(403);
res.send('form tampered with');
});

// Serve the configuration.
app.get('/parse-dashboard-config.json', function(req, res) {
const apps = config.apps.map((app) => Object.assign({}, app)); // make a copy
app.get('/parse-dashboard-config.json', function (req, res) {
const apps = config.apps.map(app => Object.assign({}, app)); // make a copy
const response = {
apps: apps,
newFeaturesInLatestVersion: newFeaturesInLatestVersion,
@@ -96,12 +101,18 @@ module.exports = function(config, options) {
if (!options.dev && !requestIsLocal) {
if (!req.secure && !options.allowInsecureHTTP) {
//Disallow HTTP requests except on localhost, to prevent the master key from being transmitted in cleartext
return res.send({ success: false, error: 'Parse Dashboard can only be remotely accessed via HTTPS' });
return res.send({
success: false,
error: 'Parse Dashboard can only be remotely accessed via HTTPS',
});
}

if (!users) {
//Accessing the dashboard over the internet can only be done with username and password
return res.send({ success: false, error: 'Configure a user to access Parse Dashboard remotely' });
return res.send({
success: false,
error: 'Configure a user to access Parse Dashboard remotely',
});
}
}
const authentication = req.user;
@@ -111,7 +122,7 @@ module.exports = function(config, options) {
const isReadOnly = authentication && authentication.isReadOnly;
// User is full read-only, replace the masterKey by the read-only one
if (isReadOnly) {
response.apps = response.apps.map((app) => {
response.apps = response.apps.map(app => {
app.masterKey = app.readOnlyMasterKey;
if (!app.masterKey) {
throw new Error('You need to provide a readOnlyMasterKey to use read-only features.');
@@ -131,7 +142,7 @@ module.exports = function(config, options) {
app.masterKey = app.readOnlyMasterKey;
}
return isSame;
})
});
});
}
// They provided correct auth
@@ -167,13 +178,15 @@ module.exports = function(config, options) {
}
} catch (e) {
// Directory doesn't exist or something.
console.warn('Iconsfolder at path: ' + config.iconsFolder +
' not found!');
console.warn('Iconsfolder at path: ' + config.iconsFolder + ' not found!');
}
}

app.get('/login', csrf(), function(req, res) {
const redirectURL = req.url.includes('?redirect=') && req.url.split('?redirect=')[1].length > 1 && req.url.split('?redirect=')[1];
app.get('/login', csrf(), function (req, res) {
const redirectURL =
req.url.includes('?redirect=') &&
req.url.split('?redirect=')[1].length > 1 &&
req.url.split('?redirect=')[1];
if (!users || (req.user && req.user.isAuthenticated)) {
return res.redirect(`${mountPath}${redirectURL || 'apps'}`);
}
@@ -182,7 +195,7 @@ module.exports = function(config, options) {
if (errors && errors.length) {
errors = `<div id="login_errors" style="display: none;">
${errors.join(' ')}
</div>`
</div>`;
}
res.send(`<!DOCTYPE html>
<html>
@@ -205,7 +218,7 @@ module.exports = function(config, options) {
});

// For every other request, go to index.html. Let client-side handle the rest.
app.get('/*', function(req, res) {
app.get('/*', function (req, res, next) {
if (users && (!req.user || !req.user.isAuthenticated)) {
const redirect = req.url.replace('/login', '');
if (redirect.length > 1) {
@@ -216,7 +229,8 @@ module.exports = function(config, options) {
if (users && req.user && req.user.matchingUsername) {
res.append('username', req.user.matchingUsername);
}
res.send(`<!DOCTYPE html>
if (!req.path.startsWith('/v2')) {
res.send(`<!DOCTYPE html>
<html>
<head>
<link rel="shortcut icon" type="image/x-icon" href="${mountPath}favicon.ico" />
@@ -232,8 +246,11 @@ module.exports = function(config, options) {
</body>
</html>
`);
} else {
next();
}
});
});

return app;
}
};
103 changes: 76 additions & 27 deletions Parse-Dashboard/server.js
Original file line number Diff line number Diff line change
@@ -6,32 +6,40 @@
* the root directory of this source tree.
*/
// Command line tool for npm start
'use strict'
'use strict';
const path = require('path');
const fs = require('fs');
const express = require('express');
const ViteExpress = require('vite-express');
const parseDashboard = require('./app');
const pc = require('picocolors');

module.exports = (options) => {
module.exports = options => {
const host = options.host || process.env.HOST || '0.0.0.0';
const port = options.port || process.env.PORT || 4040;
const mountPath = options.mountPath || process.env.MOUNT_PATH || '/';
const allowInsecureHTTP = options.allowInsecureHTTP || process.env.PARSE_DASHBOARD_ALLOW_INSECURE_HTTP;
const cookieSessionSecret = options.cookieSessionSecret || process.env.PARSE_DASHBOARD_COOKIE_SESSION_SECRET;
const allowInsecureHTTP =
options.allowInsecureHTTP || process.env.PARSE_DASHBOARD_ALLOW_INSECURE_HTTP;
const cookieSessionSecret =
options.cookieSessionSecret || process.env.PARSE_DASHBOARD_COOKIE_SESSION_SECRET;
const trustProxy = options.trustProxy || process.env.PARSE_DASHBOARD_TRUST_PROXY;
const cookieSessionMaxAge = options.cookieSessionMaxAge || process.env.PARSE_DASHBOARD_COOKIE_SESSION_MAX_AGE;
const cookieSessionMaxAge =
options.cookieSessionMaxAge || process.env.PARSE_DASHBOARD_COOKIE_SESSION_MAX_AGE;
const dev = options.dev;

if (trustProxy && allowInsecureHTTP) {
console.log('Set only trustProxy *or* allowInsecureHTTP, not both. Only one is needed to handle being behind a proxy.');
console.log(
'Set only trustProxy *or* allowInsecureHTTP, not both. Only one is needed to handle being behind a proxy.'
);
process.exit(-1);
}

const explicitConfigFileProvided = !!options.config;
let configFile = null;
let configFromCLI = null;
const configServerURL = options.serverURL || process.env.PARSE_DASHBOARD_SERVER_URL;
const configGraphQLServerURL = options.graphQLServerURL || process.env.PARSE_DASHBOARD_GRAPHQL_SERVER_URL;
const configGraphQLServerURL =
options.graphQLServerURL || process.env.PARSE_DASHBOARD_GRAPHQL_SERVER_URL;
const configMasterKey = options.masterKey || process.env.PARSE_DASHBOARD_MASTER_KEY;
const configAppId = options.appId || process.env.PARSE_DASHBOARD_APP_ID;
const configAppName = options.appName || process.env.PARSE_DASHBOARD_APP_NAME;
@@ -42,8 +50,8 @@ module.exports = (options) => {

function handleSIGs(server) {
const signals = {
'SIGINT': 2,
'SIGTERM': 15
SIGINT: 2,
SIGTERM: 15,
};
function shutdown(signal, value) {
server.close(function () {
@@ -69,8 +77,8 @@ module.exports = (options) => {
masterKey: configMasterKey,
appName: configAppName,
},
]
}
],
},
};
if (configGraphQLServerURL) {
configFromCLI.data.apps[0].graphQLServerURL = configGraphQLServerURL;
@@ -80,20 +88,28 @@ module.exports = (options) => {
{
user: configUserId,
pass: configUserPassword,
}
},
];
}
} else if (!configServerURL && !configMasterKey && !configAppName) {
configFile = path.join(__dirname, 'parse-dashboard-config.json');
}
} else if (!options.config && process.env.PARSE_DASHBOARD_CONFIG) {
configFromCLI = {
data: JSON.parse(process.env.PARSE_DASHBOARD_CONFIG)
data: JSON.parse(process.env.PARSE_DASHBOARD_CONFIG),
};
} else {
configFile = options.config;
if (options.appId || options.serverURL || options.masterKey || options.appName || options.graphQLServerURL) {
console.log('You must provide either a config file or other CLI options (appName, appId, masterKey, serverURL, and graphQLServerURL); not both.');
if (
options.appId ||
options.serverURL ||
options.masterKey ||
options.appName ||
options.graphQLServerURL
) {
console.log(
'You must provide either a config file or other CLI options (appName, appId, masterKey, serverURL, and graphQLServerURL); not both.'
);
process.exit(3);
}
}
@@ -103,7 +119,7 @@ module.exports = (options) => {
if (configFile) {
try {
config = {
data: JSON.parse(fs.readFileSync(configFile, 'utf8'))
data: JSON.parse(fs.readFileSync(configFile, 'utf8')),
};
configFilePath = path.dirname(configFile);
} catch (error) {
@@ -115,7 +131,9 @@ module.exports = (options) => {
console.log('Your config file is missing. Exiting.');
process.exit(2);
} else {
console.log('You must provide either a config file or required CLI options (app ID, Master Key, and server URL); not both.');
console.log(
'You must provide either a config file or required CLI options (app ID, Master Key, and server URL); not both.'
);
process.exit(3);
}
} else {
@@ -127,7 +145,9 @@ module.exports = (options) => {
config = configFromCLI;
} else {
//Failed to load default config file.
console.log('You must provide either a config file or an app ID, Master Key, and server URL. See parse-dashboard --help for details.');
console.log(
'You must provide either a config file or an app ID, Master Key, and server URL. See parse-dashboard --help for details.'
);
process.exit(4);
}

@@ -143,28 +163,57 @@ module.exports = (options) => {

const app = express();

if (allowInsecureHTTP || trustProxy || dev) {app.enable('trust proxy');}
if (allowInsecureHTTP || trustProxy || dev) {
app.enable('trust proxy');
}

config.data.trustProxy = trustProxy;
const dashboardOptions = { allowInsecureHTTP, cookieSessionSecret, dev, cookieSessionMaxAge };
app.use(mountPath, parseDashboard(config.data, dashboardOptions));
let server;
if(!configSSLKey || !configSSLCert){
if (!configSSLKey || !configSSLCert) {
// Start the server.
server = app.listen(port, host, function () {
console.log(`The dashboard is now available at http://${server.address().address}:${server.address().port}${mountPath}`);
const timestamp = new Date().toLocaleString('en-US').split(',')[1].trim();
console.log(
`${pc.dim(timestamp)} ${pc.bold(pc.cyan('[express]'))} ${pc.yellow(
'The dashboard'
)} ${pc.green('is now available at')} ${pc.cyan(
`http://${server.address().address}:${server.address().port}${mountPath}`
)}`
);
});
ViteExpress.config({
inlineViteConfig: {
base: `${mountPath}/v2/`,
root: path.join(__dirname, '../v2'),
build: {
outDir: path.join(__dirname, './v2'),
},
},
mode: dev && fs.existsSync(path.join(__dirname, '../v2')) ? 'development' : 'production',
});
ViteExpress.bind(app, server);
} else {
// Start the server using SSL.
const privateKey = fs.readFileSync(configSSLKey);
const certificate = fs.readFileSync(configSSLCert);

server = require('https').createServer({
key: privateKey,
cert: certificate
}, app).listen(port, host, function () {
console.log(`The dashboard is now available at https://${server.address().address}:${server.address().port}${mountPath}`);
});
server = require('https')
.createServer(
{
key: privateKey,
cert: certificate,
},
app
)
.listen(port, host, function () {
console.log(
`The dashboard is now available at https://${server.address().address}:${
server.address().port
}${mountPath}`
);
});
}
handleSIGs(server);
};
24,176 changes: 14,171 additions & 10,005 deletions package-lock.json

Large diffs are not rendered by default.

26 changes: 12 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
@@ -57,6 +57,7 @@
"parse": "3.4.2",
"passport": "0.5.3",
"passport-local": "1.0.0",
"picocolors": "^1.0.0",
"prismjs": "1.29.0",
"prop-types": "15.8.1",
"qrcode": "1.5.1",
@@ -71,7 +72,8 @@
"react-router-dom": "6.4.1",
"regenerator-runtime": "0.13.11",
"semver": "7.5.2",
"typescript": "4.8.3"
"typescript": "4.8.3",
"vite-express": "^0.16.0"
},
"devDependencies": {
"@actions/core": "1.9.1",
@@ -119,16 +121,16 @@
"scripts": {
"ci:check": "node ./ci/ciCheck.js",
"ci:checkNodeEngine": "node ./ci/nodeEngineCheck.js",
"dev": "node ./Parse-Dashboard/index.js & webpack --config webpack/build.config.js --devtool eval-source-map --progress --watch",
"dev": "node ./Parse-Dashboard/index.js --dev & webpack --config webpack/build.config.js --devtool eval-source-map --progress --watch",
"dashboard": "node ./Parse-Dashboard/index.js & webpack --config webpack/build.config.js --progress --watch",
"pig": "http-server ./PIG -p 4041 -s & webpack --config webpack/PIG.config.js --progress --watch",
"build": "webpack --node-env=production --config webpack/production.config.js && webpack --config webpack/PIG.config.js",
"build": "webpack --node-env=production --config webpack/production.config.js && webpack --config webpack/PIG.config.js && npm run build --workspace=v2",
"test": "jest",
"lint": "eslint --ignore-path .gitignore --cache ./",
"lint:fix": "DEBUG=eslint:cli-engine eslint --ignore-path .gitignore --fix --cache ./",
"prettier": "prettier --write '{src,webpack}/**/*.js'",
"lint": "eslint --cache ./",
"lint:fix": "DEBUG=eslint:cli-engine eslint --fix --cache ./",
"prettier": "prettier --write {src,webpack}/**/*.js",
"generate": "node scripts/generate.js",
"prepare": "webpack --config webpack/publish.config.js --progress",
"prepare": "webpack --config webpack/publish.config.js --progress && npm run build --workspace=v2",
"start": "node ./Parse-Dashboard/index.js",
"madge:circular": "node_modules/.bin/madge ./src --circular",
"semantic-release": "semantic-release"
@@ -162,11 +164,7 @@
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"{src,webpack}/{**/*,*}.js": [
"prettier --write",
"eslint --fix --cache",
"git add"
]
}
"workspaces": [
"v2"
]
}
11 changes: 8 additions & 3 deletions src/lib/tests/e2e/dashboard.e2e.test.js
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ const path = require('path');
const spawn = require('child_process').spawn;
const ParseDashboard = require('../../../../Parse-Dashboard/app');
const puppeteer = require('puppeteer');
const pc = require('picocolors');

const dashboardSettings = {
apps: [
@@ -51,10 +52,14 @@ describe('dashboard e2e', () => {
});
});

describe('Config options', () => {
it('should start with port option', async () => {
describe.only('Config options', () => {
it.only('should start with port option', async () => {
const result = await startParseDashboardAndGetOutput(['--port', '4041']);
expect(result).toContain('The dashboard is now available at http://0.0.0.0:4041/');
expect(result).toContain(
`${pc.yellow('The dashboard')} ${pc.green('is now available at')} ${pc.cyan(
'http://0.0.0.0:4041/'
)}`
);
});

it('should reject to start if config and other options are combined', async () => {
12 changes: 12 additions & 0 deletions v2/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
18 changes: 18 additions & 0 deletions v2/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-env node */

module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
},
};
23 changes: 23 additions & 0 deletions v2/.gitignore
Original file line number Diff line number Diff line change
@@ -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*
1 change: 1 addition & 0 deletions v2/.lintstagedrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix --cache"] }
2 changes: 2 additions & 0 deletions v2/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
legacy-peer-deps=true
strict-peer-dependencies=false
48 changes: 48 additions & 0 deletions v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# pretty-mugs-pick

<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>
</div>
<br/>

This [Refine](https://github.com/refinedev/refine) project was generated with [create refine-app](https://github.com/refinedev/refine/tree/master/packages/create-refine-app).

## Getting Started

A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility ✨

Refine's hooks and components simplifies the development process and eliminates the repetitive tasks by providing industry-standard solutions for crucial aspects of a project, including authentication, access control, routing, networking, state management, and i18n.

## Available Scripts

### Running the development server.

```bash
npm run dev
```

### Building for production.

```bash
npm run build
```

### Running the production server.

```bash
npm run start
```

## Learn More

To learn more about **Refine**, please check out the [Documentation](https://refine.dev/docs)

- **REST Data Provider** [Docs](https://refine.dev/docs/core/providers/data-provider/#overview)
- **Material UI** [Docs](https://refine.dev/docs/ui-frameworks/mui/tutorial/)
- **React Router** [Docs](https://refine.dev/docs/core/providers/router-provider/)

## License

MIT
41 changes: 41 additions & 0 deletions v2/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!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="refine | Build your React-based CRUD applications, without constraints."
/>
<meta
data-rh="true"
property="og:image"
content="https://refine.dev/img/refine_social.png"
/>
<meta
data-rh="true"
name="twitter:image"
content="https://refine.dev/img/refine_social.png"
/>
<title>
refine - Build your React-based CRUD applications, without constraints.
</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 dev` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
63 changes: 63 additions & 0 deletions v2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "pretty-mugs-pick",
"version": "0.1.0",
"private": true,
"type": "module",
"dependencies": {
"@refinedev/cli": "^2.16.21",
"@refinedev/core": "^4.47.1",
"@refinedev/devtools": "^1.1.32",
"@refinedev/kbar": "^1.3.6",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-router-dom": "^6.8.1",
"@refinedev/simple-rest": "^5.0.1",
"@refinedev/mui": "^5.14.4",
"@refinedev/react-hook-form": "^4.8.14",
"@mui/icons-material": "^5.8.3",
"@emotion/react": "^11.8.2",
"@emotion/styled": "^11.8.1",
"@mui/lab": "^5.0.0-alpha.85",
"@mui/material": "^5.8.6",
"@mui/x-data-grid": "^6.6.0",
"react-hook-form": "^7.30.0",
"@refinedev/react-router-v6": "^4.5.5"
},
"devDependencies": {
"@types/node": "^18.16.2",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"typescript": "^4.7.4",
"vite": "^4.3.1"
},
"scripts": {
"dev": "refine dev",
"build": "tsc && refine build",
"preview": "refine start",
"refine": "refine",
"lint": "eslint --ignore-path .gitignore --cache ./",
"lint:fix": "DEBUG=eslint:cli-engine eslint --ignore-path .gitignore --fix --cache ./",
"prettier": "prettier --write **/*.{js,jsx,ts,tsx,css,json,md}"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"refine": {
"projectId": "p9wUtw-8ArjkU-n0YdzH"
}
}
10 changes: 10 additions & 0 deletions v2/prettier.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import("prettier").Config} */
const config = {
semi: true,
trailingComma: 'es5',
singleQuote: true,
tabWidth: 4,
endOfLine: 'lf',
};

export default config;
Binary file added v2/public/favicon.ico
Binary file not shown.
111 changes: 111 additions & 0 deletions v2/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { GitHubBanner, Refine } from '@refinedev/core';
import { DevtoolsPanel, DevtoolsProvider } from '@refinedev/devtools';
import { RefineKbar, RefineKbarProvider } from '@refinedev/kbar';

import {
ErrorComponent,
notificationProvider,
RefineSnackbarProvider,
ThemedLayoutV2,
} from '@refinedev/mui';

import CssBaseline from '@mui/material/CssBaseline';
import GlobalStyles from '@mui/material/GlobalStyles';
import routerBindings, {
DocumentTitleHandler,
NavigateToResource,
UnsavedChangesNotifier,
} from '@refinedev/react-router-v6';
import dataProvider from '@refinedev/simple-rest';
import { BrowserRouter, Outlet, Route, Routes } from 'react-router-dom';
import { Header } from './components/header';
import { ColorModeContextProvider } from './contexts/color-mode';
import { BlogPostCreate, BlogPostEdit, BlogPostList, BlogPostShow } from './pages/blog-posts';
import { CategoryCreate, CategoryEdit, CategoryList, CategoryShow } from './pages/categories';
import { useRef } from 'react';

function App() {
const pathname = location.pathname;
const first = useRef(pathname.substring(0, pathname.indexOf('/v2') + 3));

return (
<BrowserRouter basename={first.current}>
<GitHubBanner />
<RefineKbarProvider>
<ColorModeContextProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: 'auto' } }} />
<RefineSnackbarProvider>
<DevtoolsProvider>
<Refine
dataProvider={dataProvider('https://api.fake-rest.refine.dev')}
notificationProvider={notificationProvider}
routerProvider={routerBindings}
resources={[
{
name: 'blog_posts',
list: '/blog-posts',
create: '/blog-posts/create',
edit: '/blog-posts/edit/:id',
show: '/blog-posts/show/:id',
meta: {
canDelete: true,
},
},
{
name: 'categories',
list: '/categories',
create: '/categories/create',
edit: '/categories/edit/:id',
show: '/categories/show/:id',
meta: {
canDelete: true,
},
},
]}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
useNewQueryKeys: true,
projectId: 'p9wUtw-8ArjkU-n0YdzH',
}}
>
<Routes>
<Route
element={
<ThemedLayoutV2 Header={() => <Header sticky />}>
<Outlet />
</ThemedLayoutV2>
}
>
<Route index element={<NavigateToResource resource="blog_posts" />} />
<Route path="/blog-posts">
<Route index element={<BlogPostList />} />
<Route path="create" element={<BlogPostCreate />} />
<Route path="edit/:id" element={<BlogPostEdit />} />
<Route path="show/:id" element={<BlogPostShow />} />
</Route>
<Route path="/categories">
<Route index element={<CategoryList />} />
<Route path="create" element={<CategoryCreate />} />
<Route path="edit/:id" element={<CategoryEdit />} />
<Route path="show/:id" element={<CategoryShow />} />
</Route>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>

<RefineKbar />
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
<DevtoolsPanel />
</DevtoolsProvider>
</RefineSnackbarProvider>
</ColorModeContextProvider>
</RefineKbarProvider>
</BrowserRouter>
);
}

export default App;
63 changes: 63 additions & 0 deletions v2/src/components/header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined';
import LightModeOutlined from '@mui/icons-material/LightModeOutlined';
import AppBar from '@mui/material/AppBar';
import Avatar from '@mui/material/Avatar';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import { useGetIdentity } from '@refinedev/core';
import { HamburgerMenu, RefineThemedLayoutV2HeaderProps } from '@refinedev/mui';
import React, { useContext } from 'react';
import { ColorModeContext } from '../../contexts/color-mode';

type IUser = {
id: number;
name: string;
avatar: string;
};

export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({ sticky = true }) => {
const { mode, setMode } = useContext(ColorModeContext);

const { data: user } = useGetIdentity<IUser>();

return (
<AppBar position={sticky ? 'sticky' : 'relative'}>
<Toolbar>
<Stack direction="row" width="100%" justifyContent="flex-end" alignItems="center">
<HamburgerMenu />
<Stack direction="row" width="100%" justifyContent="flex-end" alignItems="center">
<IconButton
color="inherit"
onClick={() => {
setMode();
}}
>
{mode === 'dark' ? <LightModeOutlined /> : <DarkModeOutlined />}
</IconButton>

{(user?.avatar || user?.name) && (
<Stack direction="row" gap="16px" alignItems="center" justifyContent="center">
{user?.name && (
<Typography
sx={{
display: {
xs: 'none',
sm: 'inline-block',
},
}}
variant="subtitle2"
>
{user?.name}
</Typography>
)}
<Avatar src={user?.avatar} alt={user?.name} />
</Stack>
)}
</Stack>
</Stack>
</Toolbar>
</AppBar>
);
};
1 change: 1 addition & 0 deletions v2/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Header } from './header';
46 changes: 46 additions & 0 deletions v2/src/contexts/color-mode/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ThemeProvider } from '@mui/material/styles';
import { RefineThemes } from '@refinedev/mui';
import React, { PropsWithChildren, createContext, useEffect, useState } from 'react';

type ColorModeContextType = {
mode: string;
setMode: () => void;
};

export const ColorModeContext = createContext<ColorModeContextType>({} as ColorModeContextType);

export const ColorModeContextProvider: React.FC<PropsWithChildren> = ({ children }) => {
const colorModeFromLocalStorage = localStorage.getItem('colorMode');
const isSystemPreferenceDark = window?.matchMedia('(prefers-color-scheme: dark)').matches;

const systemPreference = isSystemPreferenceDark ? 'dark' : 'light';
const [mode, setMode] = useState(colorModeFromLocalStorage || systemPreference);

useEffect(() => {
window.localStorage.setItem('colorMode', mode);
}, [mode]);

const setColorMode = () => {
if (mode === 'light') {
setMode('dark');
} else {
setMode('light');
}
};

return (
<ColorModeContext.Provider
value={{
setMode: setColorMode,
mode,
}}
>
<ThemeProvider
// you can change the theme colors here. example: mode === "light" ? RefineThemes.Magenta : RefineThemes.MagentaDark
theme={mode === 'light' ? RefineThemes.Blue : RefineThemes.BlueDark}
>
{children}
</ThemeProvider>
</ColorModeContext.Provider>
);
};
13 changes: 13 additions & 0 deletions v2/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import { createRoot } from 'react-dom/client';

import App from './App';

const container = document.getElementById('root') as HTMLElement;
const root = createRoot(container);

root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
107 changes: 107 additions & 0 deletions v2/src/pages/blog-posts/create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Autocomplete, Box, MenuItem, Select, TextField } from '@mui/material';
import { Create, useAutocomplete } from '@refinedev/mui';
import { useForm } from '@refinedev/react-hook-form';
import { Controller } from 'react-hook-form';

export const BlogPostCreate = () => {
const {
saveButtonProps,
refineCore: { formLoading },
register,
control,
formState: { errors },
} = useForm({});

const { autocompleteProps: categoryAutocompleteProps } = useAutocomplete({
resource: 'categories',
});

return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Box component="form" sx={{ display: 'flex', flexDirection: 'column' }} autoComplete="off">
<TextField
{...register('title', {
required: 'This field is required',
})}
error={!!(errors as any)?.title}
helperText={(errors as any)?.title?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={'Title'}
name="title"
/>
<TextField
{...register('content', {
required: 'This field is required',
})}
error={!!(errors as any)?.content}
helperText={(errors as any)?.content?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
multiline
label={'Content'}
name="content"
/>
<Controller
control={control}
name={'category.id'}
rules={{ required: 'This field is required' }}
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<Autocomplete
{...categoryAutocompleteProps}
{...field}
onChange={(_, value) => {
field.onChange(value.id);
}}
getOptionLabel={item => {
return (
categoryAutocompleteProps?.options?.find(p => {
const itemId =
typeof item === 'object' ? item?.id?.toString() : item?.toString();
const pId = p?.id?.toString();
return itemId === pId;
})?.title ?? ''
);
}}
isOptionEqualToValue={(option, value) => {
const optionId = option?.id?.toString();
const valueId =
typeof value === 'object' ? value?.id?.toString() : value?.toString();
return value === undefined || optionId === valueId;
}}
renderInput={params => (
<TextField
{...params}
label={'Category'}
margin="normal"
variant="outlined"
error={!!(errors as any)?.category?.id}
helperText={(errors as any)?.category?.id?.message}
required
/>
)}
/>
)}
/>
<Controller
name="status"
control={control}
render={({ field }) => {
return (
<Select {...field} value={field?.value || 'draft'} label={'Status'}>
<MenuItem value="draft">Draft</MenuItem>
<MenuItem value="published">Published</MenuItem>
<MenuItem value="rejected">Rejected</MenuItem>
</Select>
);
}}
/>
</Box>
</Create>
);
};
112 changes: 112 additions & 0 deletions v2/src/pages/blog-posts/edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Autocomplete, Box, Select, TextField } from '@mui/material';
import MenuItem from '@mui/material/MenuItem';
import { Edit, useAutocomplete } from '@refinedev/mui';
import { useForm } from '@refinedev/react-hook-form';
import { Controller } from 'react-hook-form';

export const BlogPostEdit = () => {
const {
saveButtonProps,
refineCore: { queryResult, formLoading },
register,
control,
formState: { errors },
} = useForm({});

const blogPostsData = queryResult?.data?.data;

const { autocompleteProps: categoryAutocompleteProps } = useAutocomplete({
resource: 'categories',
defaultValue: blogPostsData?.category?.id,
});

return (
<Edit isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Box component="form" sx={{ display: 'flex', flexDirection: 'column' }} autoComplete="off">
<TextField
{...register('title', {
required: 'This field is required',
})}
error={!!(errors as any)?.title}
helperText={(errors as any)?.title?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={'Title'}
name="title"
/>
<TextField
{...register('content', {
required: 'This field is required',
})}
error={!!(errors as any)?.content}
helperText={(errors as any)?.content?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
multiline
label={'Content'}
name="content"
rows={4}
/>
<Controller
control={control}
name={'category.id'}
rules={{ required: 'This field is required' }}
// eslint-disable-next-line
defaultValue={null as any}
render={({ field }) => (
<Autocomplete
{...categoryAutocompleteProps}
{...field}
onChange={(_, value) => {
field.onChange(value.id);
}}
getOptionLabel={item => {
return (
categoryAutocompleteProps?.options?.find(p => {
const itemId =
typeof item === 'object' ? item?.id?.toString() : item?.toString();
const pId = p?.id?.toString();
return itemId === pId;
})?.title ?? ''
);
}}
isOptionEqualToValue={(option, value) => {
const optionId = option?.id?.toString();
const valueId =
typeof value === 'object' ? value?.id?.toString() : value?.toString();
return value === undefined || optionId === valueId;
}}
renderInput={params => (
<TextField
{...params}
label={'Category'}
margin="normal"
variant="outlined"
error={!!(errors as any)?.category?.id}
helperText={(errors as any)?.category?.id?.message}
required
/>
)}
/>
)}
/>
<Controller
name="status"
control={control}
render={({ field }) => {
return (
<Select {...field} value={field?.value || 'draft'} label={'Status'}>
<MenuItem value="draft">Draft</MenuItem>
<MenuItem value="published">Published</MenuItem>
<MenuItem value="rejected">Rejected</MenuItem>
</Select>
);
}}
/>
</Box>
</Edit>
);
};
4 changes: 4 additions & 0 deletions v2/src/pages/blog-posts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './create';
export * from './edit';
export * from './list';
export * from './show';
109 changes: 109 additions & 0 deletions v2/src/pages/blog-posts/list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { useMany } from '@refinedev/core';
import {
DateField,
DeleteButton,
EditButton,
List,
MarkdownField,
ShowButton,
useDataGrid,
} from '@refinedev/mui';
import React from 'react';

export const BlogPostList = () => {
const { dataGridProps } = useDataGrid({
syncWithLocation: true,
});

const { data: categoryData, isLoading: categoryIsLoading } = useMany({
resource: 'categories',
ids: dataGridProps?.rows?.map((item: any) => item?.category?.id).filter(Boolean) ?? [],
queryOptions: {
enabled: !!dataGridProps?.rows,
},
});

const columns = React.useMemo<GridColDef[]>(
() => [
{
field: 'id',
headerName: 'ID',
type: 'number',
minWidth: 50,
},
{
field: 'title',
flex: 1,
headerName: 'Title',
minWidth: 200,
},
{
field: 'content',
flex: 1,
headerName: 'content',
minWidth: 250,
renderCell: function render({ value }) {
if (!value) return '-';
return <MarkdownField value={value?.slice(0, 80) + '...' || ''} />;
},
},
{
field: 'category',
flex: 1,
headerName: 'Category',
minWidth: 300,
valueGetter: ({ row }) => {
const value = row?.category;
return value;
},
renderCell: function render({ value }) {
return categoryIsLoading ? (
<>Loading...</>
) : (
categoryData?.data?.find(item => item.id === value?.id)?.title
);
},
},
{
field: 'status',
flex: 1,
headerName: 'Status',
minWidth: 200,
},
{
field: 'createdAt',
flex: 1,
headerName: 'Created at',
minWidth: 250,
renderCell: function render({ value }) {
return <DateField value={value} />;
},
},
{
field: 'actions',
headerName: 'Actions',
sortable: false,
renderCell: function render({ row }) {
return (
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} />
</>
);
},
align: 'center',
headerAlign: 'center',
minWidth: 80,
},
],
[categoryData]
);

return (
<List>
<DataGrid {...dataGridProps} columns={columns} autoHeight />
</List>
);
};
59 changes: 59 additions & 0 deletions v2/src/pages/blog-posts/show.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Stack, Typography } from '@mui/material';
import { useOne, useShow } from '@refinedev/core';
import {
DateField,
MarkdownField,
NumberField,
Show,
TextFieldComponent as TextField,
} from '@refinedev/mui';

export const BlogPostShow = () => {
const { queryResult } = useShow({});

const { data, isLoading } = queryResult;

const record = data?.data;

const { data: categoryData, isLoading: categoryIsLoading } = useOne({
resource: 'categories',
id: record?.category?.id || '',
queryOptions: {
enabled: !!record,
},
});

return (
<Show isLoading={isLoading}>
<Stack gap={1}>
<Typography variant="body1" fontWeight="bold">
{'ID'}
</Typography>
<NumberField value={record?.id ?? ''} />

<Typography variant="body1" fontWeight="bold">
{'Title'}
</Typography>
<TextField value={record?.title} />

<Typography variant="body1" fontWeight="bold">
{'Content'}
</Typography>
<MarkdownField value={record?.content} />

<Typography variant="body1" fontWeight="bold">
{'Category'}
</Typography>
{categoryIsLoading ? <>Loading...</> : <>{categoryData?.data?.title}</>}
<Typography variant="body1" fontWeight="bold">
{'Status'}
</Typography>
<TextField value={record?.status} />
<Typography variant="body1" fontWeight="bold">
{'CreatedAt'}
</Typography>
<DateField value={record?.createdAt} />
</Stack>
</Show>
);
};
32 changes: 32 additions & 0 deletions v2/src/pages/categories/create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Box, TextField } from '@mui/material';
import { Create } from '@refinedev/mui';
import { useForm } from '@refinedev/react-hook-form';

export const CategoryCreate = () => {
const {
saveButtonProps,
refineCore: { formLoading },
register,
formState: { errors },
} = useForm({});

return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
<Box component="form" sx={{ display: 'flex', flexDirection: 'column' }} autoComplete="off">
<TextField
{...register('title', {
required: 'This field is required',
})}
error={!!(errors as any)?.title}
helperText={(errors as any)?.title?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={'Title'}
name="title"
/>
</Box>
</Create>
);
};
31 changes: 31 additions & 0 deletions v2/src/pages/categories/edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Box, TextField } from '@mui/material';
import { Edit } from '@refinedev/mui';
import { useForm } from '@refinedev/react-hook-form';

export const CategoryEdit = () => {
const {
saveButtonProps,
register,
formState: { errors },
} = useForm({});

return (
<Edit saveButtonProps={saveButtonProps}>
<Box component="form" sx={{ display: 'flex', flexDirection: 'column' }} autoComplete="off">
<TextField
{...register('title', {
required: 'This field is required',
})}
error={!!(errors as any)?.title}
helperText={(errors as any)?.title?.message}
margin="normal"
fullWidth
InputLabelProps={{ shrink: true }}
type="text"
label={'Title'}
name="title"
/>
</Box>
</Edit>
);
};
4 changes: 4 additions & 0 deletions v2/src/pages/categories/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './create';
export * from './edit';
export * from './list';
export * from './show';
48 changes: 48 additions & 0 deletions v2/src/pages/categories/list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { DeleteButton, EditButton, List, ShowButton, useDataGrid } from '@refinedev/mui';
import React from 'react';

export const CategoryList = () => {
const { dataGridProps } = useDataGrid({});

const columns = React.useMemo<GridColDef[]>(
() => [
{
field: 'id',
headerName: 'ID',
type: 'number',
minWidth: 50,
},
{
field: 'title',
flex: 1,
headerName: 'Title',
minWidth: 200,
},
{
field: 'actions',
headerName: 'Actions',
sortable: false,
renderCell: function render({ row }) {
return (
<>
<EditButton hideText recordItemId={row.id} />
<ShowButton hideText recordItemId={row.id} />
<DeleteButton hideText recordItemId={row.id} />
</>
);
},
align: 'center',
headerAlign: 'center',
minWidth: 80,
},
],
[]
);

return (
<List>
<DataGrid {...dataGridProps} columns={columns} autoHeight />
</List>
);
};
25 changes: 25 additions & 0 deletions v2/src/pages/categories/show.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Stack, Typography } from '@mui/material'
import { useShow } from '@refinedev/core';
import { NumberField, Show, TextFieldComponent as TextField } from '@refinedev/mui';

export const CategoryShow = () => {
const { queryResult } = useShow({});
const { data, isLoading } = queryResult;

const record = data?.data;

return (
<Show isLoading={isLoading}>
<Stack gap={1}>
<Typography variant="body1" fontWeight="bold">
{'ID'}
</Typography>
<NumberField value={record?.id ?? ''} />
<Typography variant="body1" fontWeight="bold">
{'Title'}
</Typography>
<TextField value={record?.title} />
</Stack>
</Show>
);
};
1 change: 1 addition & 0 deletions v2/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
25 changes: 25 additions & 0 deletions v2/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"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": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src", "vite.config.ts"],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}
8 changes: 8 additions & 0 deletions v2/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}
11 changes: 11 additions & 0 deletions v2/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [react()],
build: {
outDir: '../Parse-Dashboard/v2',
emptyOutDir: true,
},
base: '',
});