├── js ├── .eslintignore ├── README.md ├── src │ ├── components │ │ ├── helpers │ │ │ └── loading.js │ │ ├── elements │ │ │ ├── index.js │ │ │ ├── table.js │ │ │ ├── select.js │ │ │ └── tableHeader.js │ │ └── app.js │ └── index.js ├── .eslintrc.json ├── webpack.config.dev.js ├── webpack.config.js ├── package.json └── dist │ └── helpers.js ├── .gitignore ├── react_admin.routing.yml ├── react_admin.info.yml ├── react_admin.libraries.yml ├── README.md ├── src └── Controller │ └── DblogController.php └── config └── install └── views.view.react_watchdog.yml /js/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/**/*.js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | js/dist/index.js 4 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | yarn 3 | 4 | yarn run start:dev 5 | 6 | // to build 7 | yarn run build:js 8 | ``` 9 | -------------------------------------------------------------------------------- /js/src/components/helpers/loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => (

Loading

); 4 | -------------------------------------------------------------------------------- /js/src/components/elements/index.js: -------------------------------------------------------------------------------- 1 | import Select from './select'; 2 | import Table from './table'; 3 | 4 | export { 5 | Select, 6 | Table, 7 | }; 8 | -------------------------------------------------------------------------------- /js/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import App from './components/app'; 5 | 6 | render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /react_admin.routing.yml: -------------------------------------------------------------------------------- 1 | react_admin.dblog_page: 2 | path: /admin/reports/dblog-react 3 | defaults: 4 | _controller: \Drupal\react_admin\Controller\DblogController::overview 5 | _title: 'Recent log messages' 6 | requirements: 7 | _permission: 'access site reports' 8 | -------------------------------------------------------------------------------- /react_admin.info.yml: -------------------------------------------------------------------------------- 1 | name: React admin 2 | description: Experimental React-based Drupal admin UI. 3 | # See discussion: https://www.drupal.org/project/ideas/issues/2913321 4 | core: 8.x 5 | type: module 6 | dependencies: 7 | - dblog 8 | - rest 9 | - serialization 10 | - views 11 | -------------------------------------------------------------------------------- /js/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "parser": "babel-eslint", 4 | "root": true, 5 | "env": { 6 | "browser": true, 7 | "es6": true 8 | }, 9 | "rules": { 10 | "brace-style": ["error", "stroustrup"], 11 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /react_admin.libraries.yml: -------------------------------------------------------------------------------- 1 | dblog: 2 | js: 3 | https://unpkg.com/react@16/umd/react.production.min.js: 4 | external: true 5 | https://unpkg.com/react-dom@16/umd/react-dom.production.min.js: 6 | external: true 7 | js/dist/helpers.js: 8 | preprocess: false 9 | js/dist/index.js: 10 | preprocess: false 11 | 12 | dependencies: 13 | - core/drupalSettings 14 | - dblog/drupal.dblog 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Admin UI 2 | 3 | ## Setup 4 | 5 | Ensure you have a moderately recent version of [Node.js](https://nodejs.org/en/) (8.9.1LTS) and [Yarn](https://yarnpkg.com/en/) installed. Currently the bundle is not included as this is under active development. 6 | 7 | `cd js && yarn install && yarn run build:js` 8 | 9 | ## Development 10 | 11 | The watcher will continuously build the bundle while you are working. 12 | 13 | `cd js && yarn install && yarn run watch:js` 14 | -------------------------------------------------------------------------------- /src/Controller/DblogController.php: -------------------------------------------------------------------------------- 1 | '; 22 | 23 | return $build; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /js/src/components/elements/table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { arrayOf, string, shape, func } from 'prop-types'; 3 | 4 | import TableHeader from './tableHeader'; 5 | 6 | const Table = ({ entries, header, order, sortBy }) => ( 7 | 8 | 9 | 10 | {entries.map(entry => ( 11 | 12 | 14 | 15 | 16 | 17 | 19 | ))} 20 | 21 |
13 | {entry.type}{entry.timestamp}{`${entry.message.substring(0, 54)} …`}{entry.user} 18 |
22 | ); 23 | 24 | Table.propTypes = { 25 | entries: arrayOf(shape({ 26 | type: string, 27 | timestamp: string, 28 | message: string, 29 | user: string, 30 | wid: string, 31 | })).isRequired, 32 | header: arrayOf(shape({ 33 | txt: string.isRequired, 34 | callback: func, 35 | })).isRequired, 36 | order: string.isRequired, 37 | sortBy: string.isRequired, 38 | }; 39 | 40 | export default Table; 41 | -------------------------------------------------------------------------------- /js/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = [ 5 | { 6 | entry: './src/index.js', 7 | output: { 8 | filename: 'index.js', 9 | path: path.resolve(__dirname, 'dist'), 10 | }, 11 | externals: { 12 | react: 'React', 13 | 'react-dom': 'ReactDOM', 14 | }, 15 | plugins: [ 16 | new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('development') } }), 17 | ], 18 | module: { 19 | loaders: [ 20 | { 21 | test: /\.js$/, 22 | loader: 'babel-loader', 23 | exclude: ['/node_modules/'], 24 | query: { 25 | plugins: [ 26 | 'external-helpers', 27 | 'transform-class-properties', 28 | 'transform-decorators-legacy', 29 | 'transform-object-rest-spread', 30 | ], 31 | presets: [ 32 | 'react', 33 | [ 34 | 'env', { 35 | modules: false, 36 | targets: { 37 | browsers: ['last 2 versions'], 38 | }, 39 | }, 40 | ], 41 | ], 42 | }, 43 | }, 44 | ], 45 | }, 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /js/src/components/elements/select.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { arrayOf, string, func, shape } from 'prop-types'; 3 | 4 | export default class Select extends Component { 5 | static propTypes = { 6 | data: arrayOf(shape({ 7 | item: string, 8 | value: string, 9 | })).isRequired, 10 | label: string, 11 | onChange: func.isRequired, 12 | } 13 | static defaultProps = { 14 | label: '', 15 | } 16 | constructor({ onChange }) { 17 | super(); 18 | // @fixme State should be stored in this.state 19 | this.onChange = onChange; 20 | } 21 | changeHandler = (e) => { 22 | const selected = new Set( 23 | Array.from(e.target.options) 24 | .filter(option => option.selected) 25 | .map(option => option.value) 26 | ); 27 | this.onChange(selected); 28 | } 29 | render() { 30 | const { label, data, selected } = this.props; 31 | return [ 32 | label !== '' ? : '', 33 | , 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /js/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const webpack = require('webpack'); 4 | const Minify = require('babel-minify-webpack-plugin'); 5 | 6 | module.exports = [ 7 | { 8 | entry: './src/index.js', 9 | output: { 10 | filename: 'index.js', 11 | path: path.resolve(__dirname, 'dist'), 12 | }, 13 | plugins: [ 14 | new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } }), 15 | new Minify(), 16 | ], 17 | externals: { 18 | react: 'React', 19 | 'react-dom': 'ReactDOM', 20 | }, 21 | module: { 22 | loaders: [ 23 | { 24 | test: /\.js$/, 25 | loader: 'babel-loader', 26 | exclude: ['/node_modules/'], 27 | query: { 28 | plugins: [ 29 | 'external-helpers', 30 | 'transform-class-properties', 31 | 'transform-decorators-legacy', 32 | 'transform-object-rest-spread', 33 | ], 34 | presets: [ 35 | 'react', 36 | [ 37 | 'env', { 38 | modules: false, 39 | targets: { 40 | browsers: ['last 2 versions'], 41 | }, 42 | }, 43 | ], 44 | ], 45 | }, 46 | }, 47 | ], 48 | }, 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /js/src/components/elements/tableHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { arrayOf, string, func, shape } from 'prop-types'; 3 | 4 | const onClickSort = (entry, order) => (e) => { 5 | e.preventDefault(); 6 | entry.callback(entry.sort, order); 7 | }; 8 | 9 | const tableSortIndicator = (entry, order, sortBy) => { 10 | if (sortBy && entry.sort === sortBy) { 11 | return ( 12 | 13 | 14 | Sort descending 15 | 16 | 17 | ) 18 | } 19 | }; 20 | 21 | const TableHeader = ({ headerEntries, order, sortBy }) => ( 22 | 23 | 24 | { 25 | headerEntries.map(entry => ( 26 | 27 | {(entry.callback ? ( 28 | 33 | {entry.txt} 34 | {tableSortIndicator(entry, order, sortBy)} 35 | 36 | ) : ( 37 | {entry.txt} 38 | ))} 39 | 40 | )) 41 | } 42 | 43 | 44 | ); 45 | 46 | TableHeader.propTypes = { 47 | headerEntries: arrayOf(shape({ 48 | txt: string.isRequired, 49 | callback: func, 50 | })).isRequired, 51 | order: string.isRequired, 52 | sortBy: string.isRequired, 53 | }; 54 | 55 | export default TableHeader; 56 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupal-react-window-global", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "author": "Matthew Grill ", 6 | "license": "MIT", 7 | "scripts": { 8 | "watch:js": "./node_modules/.bin/webpack --watch --config webpack.config.dev.js", 9 | "lint:js": "./node_modules/.bin/eslint -c .eslintrc.json ./src", 10 | "build:js": "yarn run build:js-helpers && BABEL_ENV=production ./node_modules/.bin/webpack --progress --display-reasons --display-modules --config webpack.config.js", 11 | "build:js-helpers": "./node_modules/.bin/babel-external-helpers | ./node_modules/.bin/minify --mangle false > dist/helpers.js" 12 | }, 13 | "dependencies": { 14 | "prop-types": "^15.6.0", 15 | "qs": "^6.5.1", 16 | "react": "^16.0.0", 17 | "react-dom": "^16.0.0", 18 | "superagent": "^3.8.1" 19 | }, 20 | "devDependencies": { 21 | "babel-cli": "^6.26.0", 22 | "babel-core": "^6.26.0", 23 | "babel-eslint": "^8.0.1", 24 | "babel-loader": "^7.1.2", 25 | "babel-minify": "^0.2.0", 26 | "babel-minify-webpack-plugin": "^0.2.0", 27 | "babel-plugin-external-helpers": "^6.22.0", 28 | "babel-plugin-transform-class-properties": "^6.24.1", 29 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 30 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 31 | "babel-preset-env": "^1.6.0", 32 | "babel-preset-react": "^6.24.1", 33 | "eslint": "^3.19.0 || ^4.3.0", 34 | "eslint-config-airbnb": "^15.1.0", 35 | "eslint-plugin-import": "^2.7.0", 36 | "eslint-plugin-jsx-a11y": "^5.1.1", 37 | "eslint-plugin-react": "^7.1.0", 38 | "serve": "^6.1.0", 39 | "webpack": "^3.6.0", 40 | "webpack-dev-server": "^2.9.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /js/src/components/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import qs from 'qs'; 3 | import request from 'superagent'; 4 | 5 | import Loading from './helpers/loading'; 6 | import { Select, Table } from './elements'; 7 | 8 | const dblogEndpointUrl = `${window.location.origin}${drupalSettings.path.baseUrl}/admin/reports/dblog/rest`; 9 | 10 | export default class App extends Component { 11 | state = { 12 | data: [], 13 | loaded: false, 14 | buttonDisabled: false, 15 | page: 0, 16 | order: 'desc', 17 | sortBy: 'wid', 18 | filterParams: { 19 | _format: 'json', 20 | sort_by: 'wid', 21 | type: [], 22 | severity: [], 23 | }, 24 | } 25 | componentDidMount() { 26 | this.fetchLogEntries(this.state.page); 27 | } 28 | fetchLogEntries = (page) => { 29 | const queryString = qs.stringify( 30 | { ...this.state.filterParams, page: this.state.page }, 31 | { arrayFormat: 'brackets', encode: false }, 32 | ); 33 | this.setState({ buttonDisabled: true }, () => { 34 | request 35 | .get(`${dblogEndpointUrl}?${queryString}`) 36 | .end((err, { body }) => this.setState({ 37 | data: body, 38 | page, 39 | loaded: true, 40 | buttonDisabled: false, 41 | })); 42 | }); 43 | } 44 | sortHandler = (sort, order) => { 45 | this.setState({ 46 | order: (order === 'desc' && 'asc') || 'desc', 47 | sortBy: sort, 48 | filterParams: Object.assign(this.state.filterParams, { sort_by: `${sort}_${order}` }), 49 | }, () => this.fetchLogEntries(this.state.page)); 50 | } 51 | typeFilterHandler = (typeFilters) => { 52 | this.setState({ 53 | filterParams: Object.assign(this.state.filterParams, { type: Array.from(typeFilters) }), 54 | }, () => { 55 | this.fetchLogEntries(this.state.page); 56 | }); 57 | } 58 | severityFilterHandler = (severity) => { 59 | this.setState({ 60 | filterParams: Object.assign(this.state.filterParams, { severity: Array.from(severity) }), 61 | }, () => { 62 | this.fetchLogEntries(this.state.page); 63 | }); 64 | } 65 | nextPageHandler = (e) => { 66 | e.preventDefault(); 67 | this.setState((prevState) => ({ 68 | page: prevState.page + 1 69 | }), () => { 70 | this.fetchLogEntries(this.state.page); 71 | }); 72 | } 73 | previousPageHandler = (e) => { 74 | e.preventDefault(); 75 | if (this.state.page === 0) { 76 | return; 77 | } 78 | this.setState((prevState) => ({ 79 | page: prevState.page - 1 80 | }), () => { 81 | this.fetchLogEntries(this.state.page); 82 | }); 83 | } 84 | render() { 85 | return ( 86 |
87 | {this.state.loaded ? [ 88 |

The Database Logging module logs system events in the Drupal database. Monitor your site or debug site problems on this page.

, 89 |
90 |
91 | 124 |
125 |
, 126 | ] : } 140 |

141 | 147 | 153 |

154 | 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /js/dist/helpers.js: -------------------------------------------------------------------------------- 1 | (function(global){var babelHelpers=global.babelHelpers={};babelHelpers.typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(obj){return typeof obj}:function(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj},babelHelpers.jsx=function(){var REACT_ELEMENT_TYPE="function"==typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103;return function(type,props,key,children){var defaultProps=type&&type.defaultProps,childrenLength=arguments.length-3;if(props||0==childrenLength||(props={}),props&&defaultProps)for(var propName in defaultProps)void 0===props[propName]&&(props[propName]=defaultProps[propName]);else props||(props=defaultProps||{});if(1==childrenLength)props.children=children;else if(1