├── public ├── favicon.ico ├── express-admin.css └── express-admin.js ├── lib ├── template │ ├── index.js │ └── table.js ├── format │ ├── index.js │ ├── list.js │ └── form.js ├── qb │ ├── index.js │ ├── otm.js │ ├── mtm.js │ ├── tbl.js │ ├── partials.js │ └── lst.js ├── data │ ├── index.js │ ├── pagination.js │ ├── tbl.js │ ├── otm.js │ ├── stc.js │ ├── list.js │ └── mtm.js ├── editview │ ├── validate.js │ ├── index.js │ └── upload.js ├── app │ ├── routes.js │ └── settings.js ├── db │ ├── schema.js │ ├── update.js │ └── client.js └── listview │ └── filter.js ├── views ├── 404.html ├── breadcrumbs.html ├── js │ ├── theme.html │ └── layout.html ├── pagination.html ├── editview │ ├── view.html │ ├── inline.html │ └── column.html ├── base.html ├── editview.html ├── listview.html ├── login.html ├── header.html ├── listview │ ├── filter.html │ └── column.html └── mainview.html ├── routes ├── 404.js ├── login.js ├── index.js ├── render.js ├── mainview.js ├── auth.js ├── listview.js └── editview.js ├── .editorconfig ├── .gitignore ├── config ├── libs.json ├── themes.json └── lang │ ├── cn.json │ ├── ko.json │ ├── en.json │ ├── tr.json │ ├── bg.json │ ├── es.json │ ├── ru.json │ └── de.json ├── CHANGELOG.md ├── LICENSE ├── package.json ├── app.js └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simov/express-admin/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /lib/template/index.js: -------------------------------------------------------------------------------- 1 | 2 | exports = module.exports = { 3 | table: require('./table') 4 | } 5 | -------------------------------------------------------------------------------- /views/404.html: -------------------------------------------------------------------------------- 1 | 2 |
{{string.notfound}}
5 || {{.}} | 34 | {{/columns}} 35 ||
|---|---|
| {{text}} | 42 | {{/pk}} 43 | {{#values}} 44 |45 | {{^mtm}}{{.}}{{/mtm}} 46 | {{#mtm}}{{.}}{{/mtm}} 47 | | 48 | {{/values}} 49 |
| {{string.tables}} | 32 |
|---|
| 38 | {{name}} 39 | 40 | | 41 |
| {{string.views}} | 52 |
|---|
| 58 | {{name}} 59 | | 60 |
| {{string.others}} | 71 |
|---|
| 77 | {{name}} 78 | | 79 |
{{hello}}, how are you?
860 | ``` 861 | 862 | The `res.locals` object contains all of the template variables that will be used across various partials to render the entirety of the admin UI. 863 | 864 | One additional variable called `res.locals._admin` exposes the admin internals to your route. The contents of this variable are not meant to be rendered and are there for internal use by your custom routes. 865 | 866 | For example, the `res.locals._admin.db.client` holds a reference to the underlying database client wrapper that the admin uses internally: 867 | 868 | ```js 869 | app.get('/hi', (req, res, next) => { 870 | var client = res.locals._admin.db.client 871 | // do some queries 872 | client.query('... sql ...', (err, result) => { 873 | // do something with result data 874 | }) 875 | } 876 | ``` 877 | 878 | To find more about the available data there you can put a breakpoint inside your custom route handler and inspect it with a debugger. 879 | 880 | Also have a look at the [examples] repository. 881 | 882 | **[Custom View Example][example-custom-views]** 883 | 884 | --- 885 | 886 | ## Custom: Event Hooks 887 | 888 | The supported event hooks are: 889 | 890 | - **[preSave](#custom-presave)** - before a record is saved 891 | - **[postSave](#custom-postsave)** - after a record was saved 892 | - **[preList](#custom-prelist)** - before the listview is rendered 893 | 894 | The event hooks config is configured by the `events` key: 895 | 896 | ```json 897 | { 898 | "My Awesome Event Hooks": { 899 | "events": "/absolute/path/to/event/handlers.js" 900 | } 901 | } 902 | ``` 903 | 904 | The event handlers are similar to Express.js middlewares, but they have one additional `args` parameter: 905 | 906 | ```js 907 | exports.preSave = (req, res, args, next) => { 908 | // do something 909 | next() 910 | } 911 | exports.postSave = (req, res, args, next) => { 912 | // do something 913 | next() 914 | } 915 | exports.postList = (req, res, args, next) => { 916 | // do something 917 | next() 918 | } 919 | ``` 920 | 921 | You can put a breakpoint inside any of your event hook handlers and inspect the available handler parameters with a debugger. 922 | 923 | Also have a look at the [examples] repository. 924 | 925 | ## Custom: `preSave` 926 | 927 | The `args` parameter contains: 928 | 929 | - **action** - query operation: `insert`, `update` or `remove` 930 | - **name** - the table name for which this operation was initiated for 931 | - **slug** - the slug of that table 932 | - **data** - data submitted via POST request or returned from the database 933 | - **view** - this table's data (the one currently shown inside the _editview_) 934 | - **oneToOne | manyToOne** - inline tables data 935 | ```js 936 | "table's name": { 937 | "records": [ 938 | "columns": {"column's name": "column's value", ...}, 939 | "insert|update|remove": "true" // only for inline records 940 | ] 941 | } 942 | ``` 943 | - **upath** - absolute path to the upload folder location 944 | - **upload** - list of files to be uploaded submitted via POST request 945 | - **db** - database connection instance 946 | 947 | ### `preSave` - set created_at and updated_at fields 948 | 949 | In this example we are updating the `created_at` and the `updated_at` fileds for a table called `user`: 950 | 951 | ```js 952 | var moment = require('moment') 953 | 954 | exports.preSave = (req, res, args, next) => { 955 | if (args.name === 'user') { 956 | var now = moment(new Date()).format('YYYY-MM-DD hh:mm:ss') 957 | var record = args.data.view.user.records[0].columns 958 | if (args.action === 'insert') { 959 | record.created_at = now 960 | record.updated_at = now 961 | } 962 | else if (args.action === 'update') { 963 | record.updated_at = now 964 | } 965 | } 966 | next() 967 | } 968 | ``` 969 | 970 | The `created_at` and the `updated_at` columns have to be hidden inside the editview because they will be updated internally by your event hook. Set `show: false` for the `editview` key for those columns inside the `settings.json` file. 971 | 972 | ### `preSave` - generate hash identifier 973 | 974 | In this example we are generating a hash `id` for a table called `cars`. That table view also contains `manyToOne` inline tables to be edited along with it that also needs their `id` generated: 975 | 976 | ```js 977 | var shortid = require('shortid') 978 | 979 | exports.preSave = (req, res, args, next) => { 980 | if (args.name == 'car') { 981 | if (args.action == 'insert') { 982 | var table = args.name 983 | var record = args.data.view[table].records[0].columns 984 | record.id = shortid.generate() 985 | } 986 | for (var table in args.data.manyToOne) { 987 | var inline = args.data.manyToOne[table] 988 | if (!inline.records) continue 989 | for (var i=0; i < inline.records.length; i++) { 990 | if (inline.records[i].insert != 'true') continue 991 | inline.records[i].columns.id = shortid.generate() 992 | } 993 | } 994 | } 995 | next() 996 | } 997 | ``` 998 | 999 | All of the `id` columns have to be hidden inside the editview because they will be generated internally by your event hook. Set `show: false` for the `editview` key for those columns inside the `settings.json` file. 1000 | 1001 | ### `preSave` - soft delete records 1002 | 1003 | In this example we are soft deleting records for a table called `purchase`. That table view also contains `manyToOne` inline tables to be edited along with it that also requires their records to be soft deleted: 1004 | 1005 | ```js 1006 | var moment = require('moment') 1007 | 1008 | exports.preSave = (req, res, args, next) => { 1009 | if (args.name === 'purchase') { 1010 | var now = moment(new Date()).format('YYYY-MM-DD hh:mm:ss') 1011 | // all inline oneToOne and manyToOne records should be marked as deleted 1012 | for (var table in args.data.manyToOne) { 1013 | var inline = args.data.manyToOne[table] 1014 | if (!inline.records) continue 1015 | for (var i=0; i < inline.records.length; i++) { 1016 | if (args.action !== 'remove' && !inline.records[i].remove) continue 1017 | // instead of deleting the record 1018 | delete inline.records[i].remove 1019 | // update it 1020 | inline.records[i].columns.deleted = true 1021 | inline.records[i].columns.deleted_at = now 1022 | } 1023 | } 1024 | // parent record 1025 | if (args.action == 'remove') { 1026 | // instead of deleting the record 1027 | args.action = 'update' 1028 | // update it 1029 | var record = args.data.view.purchase.records[0].columns 1030 | record.deleted = true 1031 | record.deleted_at = now 1032 | } 1033 | } 1034 | next() 1035 | } 1036 | ``` 1037 | 1038 | All of the `deleted` and `deleted_at` columns have to be hidden inside the editview because they will be managed by your event hook. Set `show: false` for the `editview` key for those columns inside the `settings.json` file. 1039 | 1040 | --- 1041 | 1042 | ## Custom: `postSave` 1043 | 1044 | The `args` parameter contains: 1045 | 1046 | - **action** - query operation: `insert`, `update` or `remove` 1047 | - **name** - the table name for which this operation was initiated for 1048 | - **slug** - the slug of that table 1049 | - **data** - data submitted via POST request or returned from the database 1050 | - **view** - this table's data (the one currently shown inside the _editview_) 1051 | - **oneToOne | manyToOne** - inline tables data 1052 | ```js 1053 | "table's name": { 1054 | "records": [ 1055 | "columns": {"column's name": "column's value", ...}, 1056 | "insert|update|remove": "true" // only for inline records 1057 | ] 1058 | } 1059 | ``` 1060 | - **upath** - absolute path to the upload folder location 1061 | - **upload** - list of files to be uploaded submitted via POST request 1062 | - **db** - database connection instance 1063 | 1064 | ### `postSave` - upload files to a third party server 1065 | 1066 | - in this example our table will be called `item` 1067 | - the item's table `image`'s column control type should be set to `file:true` in `settings.json` 1068 | - use the code below to upload the image, after the record is saved 1069 | 1070 | In this example we are uploading an image to a third-party service for a table called `item`: 1071 | 1072 | ```js 1073 | var cloudinary = require('cloudinary') 1074 | var fs = require('fs') 1075 | var path = require('path') 1076 | cloudinary.config({cloud_name: '...', api_key: '...', api_secret: '...'}) 1077 | 1078 | exports.postSave = (req, res, args, next) => { 1079 | if (args.name === 'item') { 1080 | // file upload control data 1081 | var image = args.upload.view.item.records[0].columns.image 1082 | // in case file is chosen through the file input control 1083 | if (image.name) { 1084 | // file name of the image already uploaded to the upload folder 1085 | var fname = args.data.view.item.records[0].columns.image 1086 | // upload 1087 | var fpath = path.join(args.upath, fname) 1088 | cloudinary.uploader.upload(fpath, (result) => { 1089 | console.log(result) 1090 | next() 1091 | }) 1092 | } 1093 | else next() 1094 | } 1095 | else next() 1096 | } 1097 | ``` 1098 | 1099 | The `image` column needs to have its control type set to `file: true` inside the `settings.json` file. 1100 | 1101 | --- 1102 | 1103 | ## Custom: `preList` 1104 | 1105 | The `args` parameter contains: 1106 | 1107 | - **name** - the table name for which this operation was initiated for 1108 | - **slug** - the slug of that table 1109 | - **filter** - filter data submitted via POST request 1110 | - **columns** - list of columns (and their values) to filter by 1111 | - **direction** - sort order direction 1112 | - **order** - column names to order by 1113 | - **or** - `true|false` whether to use logical _or_ or not 1114 | - **statements** - sql query strings partials 1115 | - **columns** - columns to select 1116 | - **table** - table to select from 1117 | - **join** - join statements 1118 | - **where** - where statements 1119 | - **group** - group by statements 1120 | - **order** - order by statements 1121 | - **from** - limit from number 1122 | - **to** - limit to number 1123 | - **db** - database connection instance 1124 | 1125 | ### `preList` - hide soft deleted records by default 1126 | 1127 | Have a look at the `preSave` hook example about soft deleted records. 1128 | 1129 | ```js 1130 | exports.preList = (req, res, args, next) => { 1131 | if (args.name === 'purchase') { 1132 | // check if we are using a listview filter 1133 | // and we want to see soft deleted records 1134 | var filter = args.filter.columns 1135 | if (filter && (filter.deleted == '1' || filter.deleted_at && filter.deleted_at[0])) { 1136 | return next() 1137 | } 1138 | // otherwise hide the soft deleted records by default 1139 | var filter = 1140 | ' `purchase`.`deleted` IS NULL OR `purchase`.`deleted` = 0' + 1141 | ' OR `purchase`.`deleted_at` IS NULL '; 1142 | args.statements.where 1143 | ? args.statements.where += ' AND ' + filter 1144 | : args.statements.where = ' WHERE ' + filter 1145 | } 1146 | next() 1147 | } 1148 | ``` 1149 | 1150 | --- 1151 | 1152 | ## Hosting 1153 | 1154 | By default all of the static assets needed by the admin will be served by the admin middleware itself. A good way to improve the performance of your admin instance is to serve only the dynamic routes with Node.js and leave the static files to be served by a reverse proxy instead. 1155 | 1156 | ## Hosting: Nginx 1157 | 1158 | ```nginx 1159 | # redirect HTTP to HTTPS 1160 | server { 1161 | listen 80; 1162 | server_name mywebsite.com; 1163 | return 301 https://$host$request_uri; 1164 | } 1165 | # HTTPS only 1166 | server { 1167 | listen 443 ssl; 1168 | server_name mywebsite.com; 1169 | 1170 | # (optional) you can put an additional basic auth in front of your admin 1171 | auth_basic 'Restricted'; 1172 | auth_basic_user_file /absolute/path/to/.htpasswd; 1173 | 1174 | access_log /var/log/nginx/mywebsite.com-access.log; 1175 | error_log /var/log/nginx/mywebsite.com-error.log debug; 1176 | 1177 | # certificates for HTTPS 1178 | ssl_certificate /etc/letsencrypt/live/mywebsite.com/fullchain.pem; 1179 | ssl_certificate_key /etc/letsencrypt/live/mywebsite.com/privkey.pem; 1180 | 1181 | # forward all requests to Node.js except for the static files below 1182 | location / { 1183 | # this is where your admin instance is listening to 1184 | proxy_pass http://127.0.0.1:3000/$uri$is_args$args; 1185 | # (optional) hide the fact that your app was built with Express.js 1186 | proxy_hide_header X-Powered-By; 1187 | } 1188 | 1189 | # express-admin - static files bundled with the admin 1190 | location /express-admin.css { 1191 | root /absolute/path/to/express-admin/node_modules/express-admin/public; 1192 | try_files $uri =404; 1193 | } 1194 | location /express-admin.js { 1195 | root /absolute/path/to/express-admin/node_modules/express-admin/public; 1196 | try_files $uri =404; 1197 | } 1198 | location /favicon.ico { 1199 | root /absolute/path/to/express-admin/node_modules/express-admin/public; 1200 | try_files $uri =404; 1201 | } 1202 | 1203 | # express-admin-static - third-party static files bundled with the admin 1204 | location /jslib/ { 1205 | root /absolute/path/to/express-admin/node_modules/express-admin-static; 1206 | try_files $uri =404; 1207 | } 1208 | location /csslib/ { 1209 | root /absolute/path/to/express-admin/node_modules/express-admin-static; 1210 | try_files $uri =404; 1211 | } 1212 | location /font/ { 1213 | root /absolute/path/to/express-admin/node_modules/express-admin-static; 1214 | try_files /csslib/fonts/$uri =404; 1215 | } 1216 | location /bootswatch/ { 1217 | root /absolute/path/to/express-admin/node_modules/express-admin-static; 1218 | try_files $uri =404; 1219 | } 1220 | 1221 | # (optional) any custom static file that you may have 1222 | location /custom.css { 1223 | root /absolute/path/to/custom/static/files; 1224 | try_files $uri =404; 1225 | } 1226 | location /custom.js { 1227 | root /absolute/path/to/custom/static/files; 1228 | try_files $uri =404; 1229 | } 1230 | } 1231 | ``` 1232 | 1233 | ## Hosting: Multiple Admins 1234 | 1235 | Multiple admin instances can be served with a single Node.js server: 1236 | 1237 | ```js 1238 | var express = require('express') 1239 | var admin = require('express-admin') 1240 | 1241 | express() 1242 | .use('/admin1', admin({ 1243 | config: require('/path1/config.json'), 1244 | settings: require('/path1/settings.json'), 1245 | users: require('/path1/users.json'), 1246 | custom: require('/path1/custom.json'), 1247 | })) 1248 | .use('/admin2', admin({ 1249 | config: require('/path2/config.json'), 1250 | settings: require('/path2/settings.json'), 1251 | users: require('/path2/users.json'), 1252 | custom: require('/path2/custom.json'), 1253 | })) 1254 | .use('/admin3', admin({ 1255 | config: require('/path3/config.json'), 1256 | settings: require('/path3/settings.json'), 1257 | users: require('/path3/users.json'), 1258 | custom: require('/path3/custom.json'), 1259 | })) 1260 | .listen(3000) 1261 | ``` 1262 | 1263 | In case you are serving them directly with Node.js then you have to set the `root` prefix for each admin instance inside the `config.json` file. 1264 | 1265 | However, in case you are using Nginx on top of Node.js to route the traffic, you can setup different sub domains for each admin instance and route the traffic to the correct path prefix: 1266 | 1267 | ```nginx 1268 | # map the sub domain being used to the path prefix for that admin instance 1269 | map $http_host $admin_prefix { 1270 | admin1.mywebsite.com admin1; 1271 | admin2.mywebsite.com admin2; 1272 | admin3.mywebsite.com admin3; 1273 | } 1274 | ``` 1275 | 1276 | and then update the above Nginx configuration by prepending the `$admin_prefix` variable to the path: 1277 | 1278 | ```nginx 1279 | location / { 1280 | # route to the appropriate admin prefix based on the sub domain being used 1281 | proxy_pass http://127.0.0.1:3000/$admin_prefix$uri$is_args$args; 1282 | # (optional) hide the fact that your app was built with Express.js 1283 | proxy_hide_header X-Powered-By; 1284 | } 1285 | ``` 1286 | 1287 | In that case there is no need to set the `root` configuration for the admin inside the `config.json` file because the routing will be done in Nginx, and for the Node.js (Express.js) server it will look like as if that was served on the default root `/` path. 1288 | 1289 | --- 1290 | 1291 | [npm-version]: https://img.shields.io/npm/v/express-admin.svg?style=flat-square (NPM Version) 1292 | [snyk-vulnerabilities]: https://img.shields.io/snyk/vulnerabilities/npm/express-admin.svg?style=flat-square (Vulnerabilities) 1293 | [screenshot]: https://i.imgur.com/6wFggqg.png (Express Admin) 1294 | 1295 | [npm]: https://www.npmjs.com/package/express-admin 1296 | [snyk]: https://snyk.io/test/npm/express-admin 1297 | 1298 | [tests]: https://github.com/simov/express-admin-tests 1299 | [examples]: https://github.com/simov/express-admin-examples 1300 | 1301 | [mysql]: https://www.npmjs.com/package/mysql 1302 | [pg]: https://www.npmjs.com/package/pg 1303 | [sqlite3]: https://www.npmjs.com/package/sqlite3 1304 | [mysql-connection]: https://github.com/mysqljs/mysql#connection-options 1305 | [pg-connection]: https://node-postgres.com/apis/client 1306 | 1307 | [bootstrap]: https://getbootstrap.com/docs/3.4/ 1308 | [bootswatch]: https://bootswatch.com/ 1309 | [express.js]: https://expressjs.com/ 1310 | [hogan.js]: https://twitter.github.io/hogan.js/ 1311 | [mustache.js]: https://github.com/janl/mustache.js/ 1312 | [jquery]: https://jquery.com/ 1313 | [chosen]: https://harvesthq.github.io/chosen/ 1314 | [bootstrap datepicker]: https://github.com/uxsolutions/bootstrap-datepicker 1315 | 1316 | [example-one-to-many]: https://simov.github.io/express-admin/examples/one-to-many.html 1317 | [example-many-to-many]: https://simov.github.io/express-admin/examples/many-to-many.html 1318 | [example-many-to-one]: https://simov.github.io/express-admin/examples/many-to-one.html 1319 | [example-one-to-one]: https://simov.github.io/express-admin/examples/one-to-one.html 1320 | [example-control-types]: https://simov.github.io/express-admin/examples/column.html 1321 | [example-complex-inline]: https://simov.github.io/express-admin/examples/controls.html 1322 | [example-listview-filter]: https://simov.github.io/express-admin/examples/filter.html 1323 | [example-custom-views]: https://simov.github.io/express-admin/examples/custom-views-apps.html 1324 | 1325 | [img-one-to-many]: https://simov.github.io/express-admin/images/one-to-many.png 1326 | [img-many-to-many]: https://simov.github.io/express-admin/images/many-to-many.png 1327 | [img-many-to-one]: https://simov.github.io/express-admin/images/many-to-one.png 1328 | [img-one-to-one]: https://simov.github.io/express-admin/images/one-to-one.png 1329 | [img-compound-primary-key]: https://simov.github.io/express-admin/images/compound-primary-key.png 1330 | [img-compound-one-to-many]: https://simov.github.io/express-admin/images/compound-one-to-many.png 1331 | [img-compound-many-to-many]: https://simov.github.io/express-admin/images/compound-many-to-many.png 1332 | [img-compound-many-to-one]: https://simov.github.io/express-admin/images/compound-many-to-one.png 1333 | [img-compound-one-to-one]: https://simov.github.io/express-admin/images/compound-one-to-one.png 1334 | 1335 | [morgan]: https://www.npmjs.com/package/morgan 1336 | [session]: https://www.npmjs.com/package/express-session 1337 | 1338 | [locale]: https://github.com/simov/express-admin/tree/master/config/lang 1339 | --------------------------------------------------------------------------------