├── .github └── stale.yml ├── .gitignore ├── README.md ├── composer.json ├── dist ├── audit-log-button.gif ├── css │ └── tool.css ├── js │ └── tool.js └── mix-manifest.json ├── package.json ├── resources ├── js │ ├── components │ │ ├── RestoreAuditModal.vue │ │ └── Tool.vue │ ├── fields.js │ └── tool.js └── sass │ └── tool.scss ├── routes └── api.php ├── src ├── AuditableLog.php ├── Http │ └── Controllers │ │ └── AuditController.php └── ToolServiceProvider.php ├── tailwind.config.js └── webpack.mix.js /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /node_modules 4 | package-lock.json 5 | composer.phar 6 | composer.lock 7 | phpunit.xml 8 | .phpunit.result.cache 9 | .DS_Store 10 | Thumbs.db 11 | dist/js/tool.js.LICENSE.txt 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resource Tool for Laravel Auditing 2 | 3 | A Laravel Nova Resource Tool that allows you to easily display the audit log that is created by the [Laravel Auditing](http://www.laravel-auditing.com/) package (owen-it/laravel-auditing). 4 | 5 | ## Installation 6 | 7 | Simply install using composer. 8 | 9 | ```bash 10 | 11 | composer require devpartners/auditable-log 12 | 13 | ``` 14 | 15 | Then add the resource tool to a resource whose related model uses and implements the Laravel Auditable package. 16 | 17 | ```php 18 | 19 | public function fields(Request $request) 20 | { 21 | 22 | return [ 23 | Text::make('Name'), 24 | Text::make('E-mail'), 25 | 26 | // Shows audit log button on detail view, which expands audit trail 27 | AuditableLog::make() 28 | ]; 29 | 30 | } 31 | 32 | ``` 33 | 34 | ## Laravel Nova 3.x 35 | 36 | The latest version of this package (^2.0) is compatible with Laravel Nova 4. If you require support for Laravel Nova 3, please use version ^1.0. 37 | 38 | ## Policies 39 | 40 | There are two policy gates available that you can implement on your resources' policy. 41 | 42 | 43 | ```php 44 | // Is the user able to access the audit log for this resource? 45 | public function audit($loggedInUser, $resource) { 46 | return true; 47 | } 48 | ``` 49 | 50 | 51 | ```php 52 | // Is the user able to restore values based on audits for this resource? 53 | public function audit_restore($loggedInUser, $resource) { 54 | return true; 55 | } 56 | ``` 57 | 58 | ![Audit Log within Laravel Nova Resource](https://raw.githubusercontent.com/dev-partners/laravel-nova-auditable-log/master/dist/audit-log-button.gif) 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devpartners/auditable-log", 3 | "description": "A Laravel Nova resource tool for displaying the Laravel Auditing audit log.", 4 | "keywords": [ 5 | "laravel", 6 | "nova" 7 | ], 8 | "license": "MIT", 9 | "require": { 10 | "php": ">=7.1.0" 11 | }, 12 | "autoload": { 13 | "psr-4": { 14 | "Devpartners\\AuditableLog\\": "src/" 15 | } 16 | }, 17 | "extra": { 18 | "laravel": { 19 | "providers": [ 20 | "Devpartners\\AuditableLog\\ToolServiceProvider" 21 | ] 22 | } 23 | }, 24 | "suggest": { 25 | "owen-it/laravel-auditing": "Provides the necessary functionality to audit your Eloquent models" 26 | }, 27 | "config": { 28 | "sort-packages": true 29 | }, 30 | "minimum-stability": "dev", 31 | "prefer-stable": true 32 | } 33 | -------------------------------------------------------------------------------- /dist/audit-log-button.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-partners/laravel-nova-auditable-log/ce5fc70001cc7fd5a28f123d8a618d2fd329804f/dist/audit-log-button.gif -------------------------------------------------------------------------------- /dist/css/tool.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dist/js/tool.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see tool.js.LICENSE.txt */ 2 | (()=>{"use strict";var e,t={712:(e,t,r)=>{const n=Vue;var o={key:0},a={class:"flex flex-row items-center justify-between"},i={class:"mb-3 text-90 font-normal text-2xl"},l={class:"card"},c={"data-testid":"resource-table",class:"table w-full rounded-lg overflow-hidden shadow"},s={class:"bg-gray-50 dark:bg-gray-700"},u=(0,n.createElementVNode)("th",null,null,-1),d={class:"text-left text-gray-500 dark:text-gray-400 py-2"},f={class:"text-left text-gray-500 dark:text-gray-400 py-2"},p={class:"text-left text-gray-500 dark:text-gray-400 py-2"},h={class:"text-left text-gray-500 dark:text-gray-400 py-2"},m={class:"text-left text-gray-500 dark:text-gray-400 py-2"},v={key:0,class:"text-gray-500 py-2"},y={class:"group bg-white dark:bg-gray-800"},g={class:"py-2 border-t border-gray-100 dark:border-gray-800"},w={class:"px-4"},b={key:0,"aria-hidden":"true",focusable:"false","data-prefix":"fas","data-icon":"save",class:"h-4 text-60 svg-inline--fa fa-save fa-w-14",role:"img",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 448 512"},x=[(0,n.createElementVNode)("path",{fill:"currentColor",d:"M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"},null,-1)],k={key:1,"aria-hidden":"true",focusable:"false","data-prefix":"fas","data-icon":"save",class:"h-4 text-60 svg-inline--fa fa-save fa-w-14",role:"img",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 448 512"},E=[(0,n.createElementVNode)("path",{fill:"currentColor",d:"M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"},null,-1)],N={key:2,"aria-hidden":"true",focusable:"false","data-prefix":"fas","data-icon":"trash-alt",class:"h-4 text-60 svg-inline--fa fa-trash-alt fa-w-14",role:"img",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 448 512"},_=[(0,n.createElementVNode)("path",{fill:"currentColor",d:"M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"},null,-1)],V={class:"py-2 border-t border-gray-100 dark:border-gray-800"},L={class:"py-2 border-t border-gray-100 dark:border-gray-800"},S={class:"py-2 border-t border-gray-100 dark:border-gray-800"},C={class:"py-2 border-t border-gray-100 dark:border-gray-800"},B={class:"my-2"},O={class:"inline-block bg-30 p-1 rounded-sm mr-2 font-bold"},D={class:"py-2 border-t border-gray-100 dark:border-gray-800"},A={class:"my-2"},j={class:"inline-block bg-30 p-1 rounded-sm mr-2 font-bold"},I={key:0,class:"py-2 text-center border-t border-gray-100 dark:border-gray-800"},F=["onClick"],P=(0,n.createElementVNode)("defs",{id:"defs3455"},null,-1),T=(0,n.createElementVNode)("g",{transform:"matrix(1,0,0,-1,113.89831,1262.6441)",id:"g3449"},[(0,n.createElementVNode)("path",{d:"M 1536,640 Q 1536,484 1475,342 1414,200 1311,97 1208,-6 1066,-67 924,-128 768,-128 589,-128 431.5,-52 274,24 165.5,161 57,298 18,473 q -3,14 7,27 9,12 25,12 h 199 q 23,0 30,-23 Q 329,327 464,227.5 599,128 768,128 872,128 966.5,168.5 1061,209 1130,278 q 69,69 109.5,163.5 40.5,94.5 40.5,198.5 0,104 -40.5,198.5 Q 1199,933 1130,1002 1061,1071 966.5,1111.5 872,1152 768,1152 670,1152 580,1116.5 490,1081 420,1015 L 557,877 q 31,-30 14,-69 -17,-40 -59,-40 H 64 Q 38,768 19,787 0,806 0,832 v 448 q 0,42 40,59 39,17 69,-14 l 130,-129 q 107,101 244.5,156.5 137.5,55.5 284.5,55.5 156,0 298,-61 142,-61 245,-164 103,-103 164,-245 61,-142 61,-298 z",id:"path3451","inkscape:connector-curvature":"0",style:{fill:"currentColor"}})],-1),z={class:"bg-20 rounded-b","per-page":"5","resource-count-label":"1-3 of 3","current-resource-count":"3","all-matching-resource-count":"3"},G={class:"flex justify-between items-center"},M=["disabled"],R={class:"text-sm text-80 px-4"},H=["disabled"];var q={class:"p-8"},$={class:"table w-full mt-4"},Q={style:{"max-width":"20px"}},Y={style:{"max-width":"20px"},class:"text-center"},Z=["value"],U={class:"text-center"},J={class:"text-center"},K={key:0},W={colspan:"4",class:"text-center"},X={class:"bg-30 px-6 py-3 flex"},ee={class:"ml-auto"};function te(e){return te="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},te(e)}function re(){re=function(){return e};var e={},t=Object.prototype,r=t.hasOwnProperty,n="function"==typeof Symbol?Symbol:{},o=n.iterator||"@@iterator",a=n.asyncIterator||"@@asyncIterator",i=n.toStringTag||"@@toStringTag";function l(e,t,r){return Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{l({},"")}catch(e){l=function(e,t,r){return e[t]=r}}function c(e,t,r,n){var o=t&&t.prototype instanceof d?t:d,a=Object.create(o.prototype),i=new E(n||[]);return a._invoke=function(e,t,r){var n="suspendedStart";return function(o,a){if("executing"===n)throw new Error("Generator is already running");if("completed"===n){if("throw"===o)throw a;return _()}for(r.method=o,r.arg=a;;){var i=r.delegate;if(i){var l=b(i,r);if(l){if(l===u)continue;return l}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if("suspendedStart"===n)throw n="completed",r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n="executing";var c=s(e,t,r);if("normal"===c.type){if(n=r.done?"completed":"suspendedYield",c.arg===u)continue;return{value:c.arg,done:r.done}}"throw"===c.type&&(n="completed",r.method="throw",r.arg=c.arg)}}}(e,r,i),a}function s(e,t,r){try{return{type:"normal",arg:e.call(t,r)}}catch(e){return{type:"throw",arg:e}}}e.wrap=c;var u={};function d(){}function f(){}function p(){}var h={};l(h,o,(function(){return this}));var m=Object.getPrototypeOf,v=m&&m(m(N([])));v&&v!==t&&r.call(v,o)&&(h=v);var y=p.prototype=d.prototype=Object.create(h);function g(e){["next","throw","return"].forEach((function(t){l(e,t,(function(e){return this._invoke(t,e)}))}))}function w(e,t){function n(o,a,i,l){var c=s(e[o],e,a);if("throw"!==c.type){var u=c.arg,d=u.value;return d&&"object"==te(d)&&r.call(d,"__await")?t.resolve(d.__await).then((function(e){n("next",e,i,l)}),(function(e){n("throw",e,i,l)})):t.resolve(d).then((function(e){u.value=e,i(u)}),(function(e){return n("throw",e,i,l)}))}l(c.arg)}var o;this._invoke=function(e,r){function a(){return new t((function(t,o){n(e,r,t,o)}))}return o=o?o.then(a,a):a()}}function b(e,t){var r=e.iterator[t.method];if(void 0===r){if(t.delegate=null,"throw"===t.method){if(e.iterator.return&&(t.method="return",t.arg=void 0,b(e,t),"throw"===t.method))return u;t.method="throw",t.arg=new TypeError("The iterator does not provide a 'throw' method")}return u}var n=s(r,e.iterator,t.arg);if("throw"===n.type)return t.method="throw",t.arg=n.arg,t.delegate=null,u;var o=n.arg;return o?o.done?(t[e.resultName]=o.value,t.next=e.nextLoc,"return"!==t.method&&(t.method="next",t.arg=void 0),t.delegate=null,u):o:(t.method="throw",t.arg=new TypeError("iterator result is not an object"),t.delegate=null,u)}function x(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function k(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function E(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(x,this),this.reset(!0)}function N(e){if(e){var t=e[o];if(t)return t.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var n=-1,a=function t(){for(;++n=0;--o){var a=this.tryEntries[o],i=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var l=r.call(a,"catchLoc"),c=r.call(a,"finallyLoc");if(l&&c){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),k(r),u}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;k(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:N(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),u}},e}function ne(e,t,r,n,o,a,i){try{var l=e[a](i),c=l.value}catch(e){return void r(e)}l.done?t(c):Promise.resolve(c).then(n,o)}const oe={props:["fields","audit","resourceName","resourceId"],data:function(){return{restoreIds:[],selectAll:!1}},methods:{handleClose:function(){this.$emit("close")},handleConfirm:function(){var e,t=this;return(e=re().mark((function e(){var r,n;return re().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!(t.restoreIds.length>0)){e.next=6;break}return e.next=3,Nova.request().post("/nova-vendor/auditable-log/audits/".concat(t.resourceName,"/").concat(t.resourceId,"/").concat(t.audit.id),{restore:t.restoreIds});case 3:r=e.sent,n=t.restoreIds.map((function(e){return{key:e,value:r.data.record[e]}})),t.$emit("restored",n);case 6:t.$emit("close");case 7:case"end":return e.stop()}}),e)})),function(){var t=this,r=arguments;return new Promise((function(n,o){var a=e.apply(t,r);function i(e){ne(a,n,o,i,l,"next",e)}function l(e){ne(a,n,o,i,l,"throw",e)}i(void 0)}))})()},toggleSelectAll:function(){this.allSelected?this.restoreIds=[]:this.restoreIds=this.comparison.map((function(e){return e.key}))}},computed:{allSelected:function(){return this.comparison.length===this.restoreIds.length},comparison:function(){var e=this;return Object.keys(this.audit.new_values).map((function(t){return void 0===e.fields[t]||e.fields[t].value==e.audit.new_values[t]?null:{key:t,label:e.fields[t].label,current:e.fields[t].value,restore:e.audit.new_values[t]}})).filter((function(e){return null!==e}))}}};var ae=r(744);function ie(e){return ie="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},ie(e)}function le(){le=function(){return e};var e={},t=Object.prototype,r=t.hasOwnProperty,n="function"==typeof Symbol?Symbol:{},o=n.iterator||"@@iterator",a=n.asyncIterator||"@@asyncIterator",i=n.toStringTag||"@@toStringTag";function l(e,t,r){return Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{l({},"")}catch(e){l=function(e,t,r){return e[t]=r}}function c(e,t,r,n){var o=t&&t.prototype instanceof d?t:d,a=Object.create(o.prototype),i=new E(n||[]);return a._invoke=function(e,t,r){var n="suspendedStart";return function(o,a){if("executing"===n)throw new Error("Generator is already running");if("completed"===n){if("throw"===o)throw a;return _()}for(r.method=o,r.arg=a;;){var i=r.delegate;if(i){var l=b(i,r);if(l){if(l===u)continue;return l}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if("suspendedStart"===n)throw n="completed",r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n="executing";var c=s(e,t,r);if("normal"===c.type){if(n=r.done?"completed":"suspendedYield",c.arg===u)continue;return{value:c.arg,done:r.done}}"throw"===c.type&&(n="completed",r.method="throw",r.arg=c.arg)}}}(e,r,i),a}function s(e,t,r){try{return{type:"normal",arg:e.call(t,r)}}catch(e){return{type:"throw",arg:e}}}e.wrap=c;var u={};function d(){}function f(){}function p(){}var h={};l(h,o,(function(){return this}));var m=Object.getPrototypeOf,v=m&&m(m(N([])));v&&v!==t&&r.call(v,o)&&(h=v);var y=p.prototype=d.prototype=Object.create(h);function g(e){["next","throw","return"].forEach((function(t){l(e,t,(function(e){return this._invoke(t,e)}))}))}function w(e,t){function n(o,a,i,l){var c=s(e[o],e,a);if("throw"!==c.type){var u=c.arg,d=u.value;return d&&"object"==ie(d)&&r.call(d,"__await")?t.resolve(d.__await).then((function(e){n("next",e,i,l)}),(function(e){n("throw",e,i,l)})):t.resolve(d).then((function(e){u.value=e,i(u)}),(function(e){return n("throw",e,i,l)}))}l(c.arg)}var o;this._invoke=function(e,r){function a(){return new t((function(t,o){n(e,r,t,o)}))}return o=o?o.then(a,a):a()}}function b(e,t){var r=e.iterator[t.method];if(void 0===r){if(t.delegate=null,"throw"===t.method){if(e.iterator.return&&(t.method="return",t.arg=void 0,b(e,t),"throw"===t.method))return u;t.method="throw",t.arg=new TypeError("The iterator does not provide a 'throw' method")}return u}var n=s(r,e.iterator,t.arg);if("throw"===n.type)return t.method="throw",t.arg=n.arg,t.delegate=null,u;var o=n.arg;return o?o.done?(t[e.resultName]=o.value,t.next=e.nextLoc,"return"!==t.method&&(t.method="next",t.arg=void 0),t.delegate=null,u):o:(t.method="throw",t.arg=new TypeError("iterator result is not an object"),t.delegate=null,u)}function x(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function k(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function E(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(x,this),this.reset(!0)}function N(e){if(e){var t=e[o];if(t)return t.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var n=-1,a=function t(){for(;++n=0;--o){var a=this.tryEntries[o],i=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var l=r.call(a,"catchLoc"),c=r.call(a,"finallyLoc");if(l&&c){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),k(r),u}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;k(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,r){return this.delegate={iterator:N(e),resultName:t,nextLoc:r},"next"===this.method&&(this.arg=void 0),u}},e}function ce(e,t,r,n,o,a,i){try{var l=e[a](i),c=l.value}catch(e){return void r(e)}l.done?t(c):Promise.resolve(c).then(n,o)}function se(e){return function(){var t=this,r=arguments;return new Promise((function(n,o){var a=e.apply(t,r);function i(e){ce(a,n,o,i,l,"next",e)}function l(e){ce(a,n,o,i,l,"throw",e)}i(void 0)}))}}const ue={props:["resourceName","resourceId","field"],components:{RestoreAuditModal:(0,ae.Z)(oe,[["render",function(e,t,r,o,a,i){var l=(0,n.resolveComponent)("heading"),c=(0,n.resolveComponent)("checkbox"),s=(0,n.resolveComponent)("modal");return(0,n.openBlock)(),(0,n.createBlock)(s,{onModalClose:i.handleClose},{default:(0,n.withCtx)((function(){return[(0,n.createElementVNode)("form",{onSubmit:t[2]||(t[2]=(0,n.withModifiers)((function(){return i.handleConfirm&&i.handleConfirm.apply(i,arguments)}),["prevent"])),"slot-scope":"props",class:"bg-white rounded-lg shadow-lg overflow-hidden",style:{width:"750px"}},[(0,n.renderSlot)(e.$slots,"default",{},(function(){return[(0,n.createElementVNode)("div",q,[(0,n.createVNode)(l,{level:2,class:"mb-6"},{default:(0,n.withCtx)((function(){return[(0,n.createTextVNode)((0,n.toDisplayString)(e.__("Restore audit")),1)]})),_:1}),(0,n.createElementVNode)("table",$,[(0,n.createElementVNode)("thead",null,[(0,n.createElementVNode)("th",Q,[(0,n.createVNode)(c,{onInput:i.toggleSelectAll,checked:i.allSelected},null,8,["onInput","checked"])]),(0,n.createElementVNode)("th",null,(0,n.toDisplayString)(e.__("Field")),1),(0,n.createElementVNode)("th",null,(0,n.toDisplayString)(e.__("Current")),1),(0,n.createElementVNode)("th",null,(0,n.toDisplayString)(e.__("Restore to")),1)]),(0,n.createElementVNode)("tbody",null,[((0,n.openBlock)(!0),(0,n.createElementBlock)(n.Fragment,null,(0,n.renderList)(i.comparison,(function(e){return(0,n.openBlock)(),(0,n.createElementBlock)("tr",null,[(0,n.createElementVNode)("td",Y,[(0,n.withDirectives)((0,n.createElementVNode)("input",{type:"checkbox",class:"checkbox","onUpdate:modelValue":t[0]||(t[0]=function(e){return a.restoreIds=e}),value:e.key},null,8,Z),[[n.vModelCheckbox,a.restoreIds]])]),(0,n.createElementVNode)("td",null,(0,n.toDisplayString)(e.label),1),(0,n.createElementVNode)("td",U,(0,n.toDisplayString)(e.current),1),(0,n.createElementVNode)("td",J,(0,n.toDisplayString)(e.restore),1)])})),256)),0==i.comparison.length?((0,n.openBlock)(),(0,n.createElementBlock)("tr",K,[(0,n.createElementVNode)("td",W,(0,n.toDisplayString)(e.__("No changes")),1)])):(0,n.createCommentVNode)("",!0)])])])]})),(0,n.createElementVNode)("div",X,[(0,n.createElementVNode)("div",ee,[(0,n.createElementVNode)("button",{type:"button","data-testid":"cancel-button",dusk:"cancel-delete-button",onClick:t[1]||(t[1]=(0,n.withModifiers)((function(){return i.handleClose&&i.handleClose.apply(i,arguments)}),["prevent"])),class:"btn text-80 font-normal h-9 px-3 mr-3 btn-link"},(0,n.toDisplayString)(e.__("Cancel")),1),(0,n.createElementVNode)("button",{id:"confirm-delete-button",ref:"confirmButton","data-testid":"confirm-button",type:"submit",class:"btn btn-default btn-danger"},(0,n.toDisplayString)(e.__("Restore")),513)])])],32)]})),_:3},8,["onModalClose"])}]])},data:function(){return{audits:[],displayAudits:!1,pagination:{},restore:null,parentFields:[],canRestore:!1}},mounted:function(){var e,t;!0===this.displayAudits&&this.fetchAudits(),this.parentFields=(e=this.$parent.$parent.$.vnode.component.data.panels[0].fields,t={},e.filter((function(e){return""!==e.attribute})).forEach((function(e){if(""!==e.attribute){var r=e.attribute,n=e.value,o=e.name;t[r]={value:n,label:o}}})),t.length!==e.length&&e.filter((function(e){return""===e.attribute&&void 0!==e.dependencies&&e.dependencies.length===e.dependencies.filter((function(e){return e.satisfied})).length})).forEach((function(e){e.fields.forEach((function(e){var r=e.attribute,n=e.value,o=e.name;t[r]={value:n,label:o}}))})),t)},methods:{showAndFetch:function(){this.displayAudits=!0,this.fetchAudits()},close:function(){this.displayAudits=!1},fetchAudits:function(){var e=arguments,t=this;return se(le().mark((function r(){var n,o,a;return le().wrap((function(r){for(;;)switch(r.prev=r.next){case 0:return(n=e.length>0&&void 0!==e[0]?e[0]:null)||(n="/nova-vendor/auditable-log/audits/".concat(t.resourceName,"/").concat(t.resourceId)),r.prev=2,r.next=5,Nova.request().get(n);case 5:o=r.sent,a=o.data,t.audits=a.audits.data,t.pagination=a.audits,t.canRestore=a.restore,r.next=14;break;case 12:r.prev=12,r.t0=r.catch(2);case 14:case"end":return r.stop()}}),r,null,[[2,12]])})))()},formatData:function(e){var t=[];for(var r in e)e.hasOwnProperty(r)&&t.push({name:r,value:e[r]});return t},showRestoreAudit:function(e){this.restore=e},restored:function(e){var t=this;e.forEach((function(e){t.parentFields[e.key].value=e.value})),this.fetchAudits()}}},de=(0,ae.Z)(ue,[["render",function(e,t,r,q,$,Q){var Y=(0,n.resolveComponent)("DefaultButton"),Z=(0,n.resolveComponent)("sodipodi:namedview"),U=(0,n.resolveComponent)("restore-audit-modal");return(0,n.openBlock)(),(0,n.createElementBlock)("div",null,[$.displayAudits?((0,n.openBlock)(),(0,n.createElementBlock)("div",o,[(0,n.createElementVNode)("div",a,[(0,n.createElementVNode)("h2",i,(0,n.toDisplayString)(e.__("Audit Log")),1),$.displayAudits?((0,n.openBlock)(),(0,n.createBlock)(Y,{key:0,class:"btn btn-default btn-primary ml-4 mb-2",onClick:(0,n.withModifiers)(Q.close,["prevent"])},{default:(0,n.withCtx)((function(){return[(0,n.createTextVNode)((0,n.toDisplayString)(e.__("Close Audit Log")),1)]})),_:1},8,["onClick"])):(0,n.createCommentVNode)("",!0)]),(0,n.createElementVNode)("div",l,[(0,n.createElementVNode)("table",c,[(0,n.createElementVNode)("thead",s,[(0,n.createElementVNode)("tr",null,[u,(0,n.createElementVNode)("th",d,[(0,n.createElementVNode)("span",null,(0,n.toDisplayString)(e.__("User")),1)]),(0,n.createElementVNode)("th",f,[(0,n.createElementVNode)("span",null,(0,n.toDisplayString)(e.__("Event")),1)]),(0,n.createElementVNode)("th",p,[(0,n.createElementVNode)("span",null,(0,n.toDisplayString)(e.__("Date/Time")),1)]),(0,n.createElementVNode)("th",h,[(0,n.createElementVNode)("span",null,(0,n.toDisplayString)(e.__("Old Values")),1)]),(0,n.createElementVNode)("th",m,[(0,n.createElementVNode)("span",null,(0,n.toDisplayString)(e.__("New Values")),1)]),$.canRestore?((0,n.openBlock)(),(0,n.createElementBlock)("th",v)):(0,n.createCommentVNode)("",!0)])]),(0,n.createElementVNode)("tbody",null,[((0,n.openBlock)(!0),(0,n.createElementBlock)(n.Fragment,null,(0,n.renderList)($.audits,(function(t){return(0,n.openBlock)(),(0,n.createElementBlock)("tr",y,[(0,n.createElementVNode)("td",g,[(0,n.createElementVNode)("div",w,["created"===t.event?((0,n.openBlock)(),(0,n.createElementBlock)("svg",b,x)):(0,n.createCommentVNode)("",!0),"updated"===t.event?((0,n.openBlock)(),(0,n.createElementBlock)("svg",k,E)):(0,n.createCommentVNode)("",!0),"deleted"===t.event?((0,n.openBlock)(),(0,n.createElementBlock)("svg",N,_)):(0,n.createCommentVNode)("",!0)])]),(0,n.createElementVNode)("td",V,(0,n.toDisplayString)(t.user?t.user.name:e.__("console")),1),(0,n.createElementVNode)("td",L,(0,n.toDisplayString)(t.event),1),(0,n.createElementVNode)("td",S,(0,n.toDisplayString)(t.created_at),1),(0,n.createElementVNode)("td",C,[((0,n.openBlock)(!0),(0,n.createElementBlock)(n.Fragment,null,(0,n.renderList)(Q.formatData(t.old_values),(function(e){return(0,n.openBlock)(),(0,n.createElementBlock)("div",B,[(0,n.createElementVNode)("span",O,(0,n.toDisplayString)(e.name),1),(0,n.createTextVNode)(" "+(0,n.toDisplayString)(e.value),1)])})),256))]),(0,n.createElementVNode)("td",D,[((0,n.openBlock)(!0),(0,n.createElementBlock)(n.Fragment,null,(0,n.renderList)(Q.formatData(t.new_values),(function(e){return(0,n.openBlock)(),(0,n.createElementBlock)("div",A,[(0,n.createElementVNode)("span",j,(0,n.toDisplayString)(e.name),1),(0,n.createTextVNode)(" "+(0,n.toDisplayString)(e.value),1)])})),256))]),$.canRestore?((0,n.openBlock)(),(0,n.createElementBlock)("td",I,[((0,n.openBlock)(),(0,n.createElementBlock)("svg",{onClick:function(e){return Q.showRestoreAudit(t)},style:{"max-width":"20px"},"xmlns:dc":"http://purl.org/dc/elements/1.1/","xmlns:cc":"http://creativecommons.org/ns#","xmlns:rdf":"http://www.w3.org/1999/02/22-rdf-syntax-ns#","xmlns:svg":"http://www.w3.org/2000/svg",xmlns:"http://www.w3.org/2000/svg","xmlns:sodipodi":"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd","xmlns:inkscape":"http://www.inkscape.org/namespaces/inkscape",viewBox:"0 -256 1792 1792",id:"svg3447",version:"1.1","inkscape:version":"0.48.3.1 r9886",width:"100%",height:"100%","sodipodi:docname":"undo_font_awesome.svg"},[P,(0,n.createVNode)(Z,{pagecolor:"#ffffff",bordercolor:"#666666",borderopacity:"1",objecttolerance:"10",gridtolerance:"10",guidetolerance:"10","inkscape:pageopacity":"0","inkscape:pageshadow":"2","inkscape:window-width":"640","inkscape:window-height":"480",id:"namedview3453",showgrid:"false","inkscape:zoom":"0.13169643","inkscape:cx":"896","inkscape:cy":"896","inkscape:window-x":"0","inkscape:window-y":"25","inkscape:window-maximized":"0","inkscape:current-layer":"svg3447"}),T],8,F))])):(0,n.createCommentVNode)("",!0)])})),256))])]),(0,n.createElementVNode)("div",z,[(0,n.createElementVNode)("nav",G,[(0,n.createElementVNode)("button",{disabled:null===$.pagination.prev_page_url,rel:"prev",dusk:"previous",class:(0,n.normalizeClass)(["btn btn-link py-3 px-4 text-80",{"opacity-50":null===$.pagination.prev_page_url,"text-primary":null!==$.pagination.prev_page_url}]),onClick:t[0]||(t[0]=function(e){return Q.fetchAudits($.pagination.prev_page_url)})},(0,n.toDisplayString)(e.__("Previous")),11,M),(0,n.createElementVNode)("span",R,(0,n.toDisplayString)($.pagination.from)+"-"+(0,n.toDisplayString)($.pagination.to)+" of "+(0,n.toDisplayString)($.pagination.total),1),(0,n.createElementVNode)("button",{disabled:null===$.pagination.next_page_url,rel:"next",dusk:"next",class:(0,n.normalizeClass)([{"opacity-50":null===$.pagination.next_page_url,"text-primary":null!==$.pagination.next_page_url},"btn btn-link py-3 px-4 text-80"]),onClick:t[1]||(t[1]=function(e){return Q.fetchAudits($.pagination.next_page_url)})},(0,n.toDisplayString)(e.__("Next")),11,H)])])])])):(0,n.createCommentVNode)("",!0),!1===$.displayAudits?((0,n.openBlock)(),(0,n.createBlock)(Y,{key:1,onClick:(0,n.withModifiers)(Q.showAndFetch,["prevent"])},{default:(0,n.withCtx)((function(){return[(0,n.createTextVNode)((0,n.toDisplayString)(e.__("View Audit Log")),1)]})),_:1},8,["onClick"])):(0,n.createCommentVNode)("",!0),null!==$.restore?((0,n.openBlock)(),(0,n.createBlock)(U,{key:2,fields:$.parentFields,resourceName:r.resourceName,resourceId:r.resourceId,audit:$.restore,onClose:t[2]||(t[2]=function(e){return $.restore=null}),onRestored:Q.restored},null,8,["fields","resourceName","resourceId","audit","onRestored"])):(0,n.createCommentVNode)("",!0)])}]]);Nova.booting((function(e,t,r){e.component("auditable-log",de)}))},288:()=>{},744:(e,t)=>{t.Z=(e,t)=>{const r=e.__vccOpts||e;for(const[e,n]of t)r[e]=n;return r}}},r={};function n(e){var o=r[e];if(void 0!==o)return o.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,n),a.exports}n.m=t,e=[],n.O=(t,r,o,a)=>{if(!r){var i=1/0;for(u=0;u=a)&&Object.keys(n.O).every((e=>n.O[e](r[c])))?r.splice(c--,1):(l=!1,a0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[r,o,a]},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={103:0,990:0};n.O.j=t=>0===e[t];var t=(t,r)=>{var o,a,[i,l,c]=r,s=0;if(i.some((t=>0!==e[t]))){for(o in l)n.o(l,o)&&(n.m[o]=l[o]);if(c)var u=c(n)}for(t&&t(r);sn(712)));var o=n.O(void 0,[990],(()=>n(288)));o=n.O(o)})(); -------------------------------------------------------------------------------- /dist/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/tool.js": "/js/tool.js", 3 | "/css/tool.css": "/css/tool.css" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "mix", 6 | "watch": "mix watch", 7 | "watch-poll": "mix watch -- --watch-options-poll=1000", 8 | "hot": "mix watch --hot", 9 | "prod": "npm run production", 10 | "production": "mix --production" 11 | }, 12 | "devDependencies": { 13 | "@vue/compiler-sfc": "^3.2.22", 14 | "laravel-mix": "^6.0.41", 15 | "laravel-mix-purgecss": "^6.0", 16 | "postcss": "^8.3.11", 17 | "sass": "^1.52.2", 18 | "sass-loader": "^12.6.0", 19 | "vue-loader": "^16.8.3", 20 | "webpack-cli": "^4.9.2" 21 | }, 22 | "dependencies": { 23 | "tailwindcss": "^3.0", 24 | "vue": "^3.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /resources/js/components/RestoreAuditModal.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 148 | -------------------------------------------------------------------------------- /resources/js/components/Tool.vue: -------------------------------------------------------------------------------- 1 | 130 | 131 | 215 | -------------------------------------------------------------------------------- /resources/js/fields.js: -------------------------------------------------------------------------------- 1 | export const normaliseFields = fields => 2 | { 3 | let indexedFields = {}; 4 | 5 | fields.filter(field => field.attribute !== "") 6 | .forEach(field => { 7 | if (field.attribute !== "") { 8 | const {attribute, value, name} = field; 9 | indexedFields[attribute] = { 10 | value, 11 | label: name 12 | }; 13 | } 14 | }); 15 | 16 | // Support nova dependency container 17 | if (indexedFields.length !== fields.length) { 18 | const dependencies = fields.filter(field => { 19 | // dependencies don't have an attribute set 20 | return field.attribute === "" && 21 | // they should have a "dependencies" prop 22 | typeof field.dependencies !== 'undefined' && 23 | // all dependencies should be satisfied 24 | field.dependencies.length === field.dependencies.filter(dep => dep.satisfied).length 25 | }); 26 | 27 | dependencies.forEach(field => { 28 | field.fields.forEach(f => { 29 | const {attribute, value, name} = f; 30 | indexedFields[attribute] = { 31 | value, 32 | label: name 33 | }; 34 | }) 35 | }); 36 | } 37 | 38 | return indexedFields; 39 | } 40 | -------------------------------------------------------------------------------- /resources/js/tool.js: -------------------------------------------------------------------------------- 1 | import Tool from './components/Tool'; 2 | 3 | Nova.booting((Vue, router, store) => { 4 | Vue.component('auditable-log', Tool) 5 | }) 6 | -------------------------------------------------------------------------------- /resources/sass/tool.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-partners/laravel-nova-auditable-log/ce5fc70001cc7fd5a28f123d8a618d2fd329804f/resources/sass/tool.scss -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | canSee(function (NovaRequest $request) { 16 | if (!$request instanceof \Laravel\Nova\Http\Requests\ResourceDetailRequest) { 17 | return false; 18 | } 19 | 20 | return $request->user()->can('audit', $request->findModelOrFail()); 21 | }); 22 | } 23 | 24 | /** 25 | * Get the displayable name of the resource tool. 26 | * 27 | * @return string 28 | */ 29 | public function name() 30 | { 31 | return __('Auditable Log'); 32 | } 33 | 34 | /** 35 | * Get the component name for the resource tool. 36 | * 37 | * @return string 38 | */ 39 | public function component() 40 | { 41 | return 'auditable-log'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Http/Controllers/AuditController.php: -------------------------------------------------------------------------------- 1 | loadRecord($resourceName, $resourceId); 18 | $user = $request->user(config('nova.guard')); 19 | 20 | abort_if($user->cant('audit', $record), 403, 'Unable to retrieve audits'); 21 | 22 | $audits = $record->audits() 23 | ->with('user') 24 | ->orderBy('created_at', 'desc') 25 | ->paginate(); 26 | 27 | return response()->json(['status' => 'OK', 'audits' => $audits, 'restore' => $user->can('audit_restore', $record)]); 28 | } 29 | 30 | public function restore(Request $request, $resourceName, $resourceId, $auditId) 31 | { 32 | $record = $this->loadRecord($resourceName, $resourceId); 33 | $user = $request->user(config('nova.guard')); 34 | 35 | abort_if($user->cant('audit_restore', $record), 403, 'Unable to restore audits'); 36 | 37 | /** 38 | * @var Audit $auditor 39 | * @var Audit $audit 40 | */ 41 | $auditableClass = Config::get('audit.implementation', Audit::class); 42 | $auditor = new $auditableClass(); 43 | 44 | $audit = $record->audits()->where($auditor->getTable() . '.' . $auditor->getKeyName(), $auditId)->firstOrFail(); 45 | 46 | $record->fill(Arr::only($audit->new_values, $request->input('restore', []))); 47 | $record->save(); 48 | 49 | return response()->json(['status' => 'OK', 'record' => $record]); 50 | } 51 | 52 | /** 53 | * @param $resourceName 54 | * @param $resourceId 55 | * 56 | * @return Auditable|Model 57 | */ 58 | protected function loadRecord($resourceName, $resourceId) 59 | { 60 | $model = Nova::modelInstanceForKey($resourceName); 61 | return method_exists($model, "trashed") 62 | ? $model::withTrashed()->find($resourceId) 63 | : $model->find($resourceId); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ToolServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->booted(function () { 19 | $this->routes(); 20 | 21 | // By default logged in users can audit and restore 22 | Gate::define('audit', function ($user, $resource) { 23 | return true; 24 | }); 25 | Gate::define('audit_restore', function ($user, $resource) { 26 | return true; 27 | }); 28 | }); 29 | 30 | Nova::serving(function (ServingNova $event) { 31 | Nova::script('auditable-log', __DIR__ . '/../dist/js/tool.js'); 32 | Nova::style('auditable-log', __DIR__ . '/../dist/css/tool.css'); 33 | 34 | Nova::translations([ 35 | 'Audit Log' => __('Audit Log'), 36 | 'User' => __('User'), 37 | 'Event' => __('Event'), 38 | 'Date/Time' => __('Date/Time'), 39 | 'Old Values' => __('Old Values'), 40 | 'New Values' => __('New Values'), 41 | 'console' => __('console'), 42 | 'Previous' => __('Previous'), 43 | 'Next' => __('Next'), 44 | 'View Audit Log' => __('View Audit Log'), 45 | 'Restore audit' => __('Restore audit'), 46 | 'Field' => __('Field'), 47 | 'Current' => __('Current'), 48 | 'Restore to' => __('Restore to'), 49 | 'No changes' => __('No changes'), 50 | 'Cancel' => __('Cancel'), 51 | 'Restore' => __('Restore'), 52 | ]); 53 | }); 54 | } 55 | 56 | /** 57 | * Register any application services. 58 | */ 59 | public function register() 60 | { 61 | } 62 | 63 | /** 64 | * Register the tool's routes. 65 | */ 66 | protected function routes() 67 | { 68 | if ($this->app->routesAreCached()) { 69 | return; 70 | } 71 | 72 | Route::middleware(['nova']) 73 | ->prefix('nova-vendor/auditable-log') 74 | ->group(__DIR__ . '/../routes/api.php'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: {} 4 | }, 5 | variants: {}, 6 | plugins: [] 7 | } 8 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix') 2 | let path = require('path') 3 | const tailwindcss = require('tailwindcss') 4 | 5 | mix.alias({ 6 | 'laravel-nova': path.join(__dirname, '../../vendor/laravel/nova/resources/js/mixins/packages.js'), 7 | }) 8 | 9 | mix.js('resources/js/tool.js', 'js').vue({ version: 3 }) 10 | .webpackConfig({ 11 | externals: { 12 | vue: 'Vue', 13 | }, 14 | output: { 15 | uniqueName: 'vendor/auditable-log', 16 | } 17 | }) 18 | .setPublicPath('dist') 19 | .sass('resources/sass/tool.scss', 'css') 20 | .options({ 21 | processCssUrls: false, 22 | postCss: [ tailwindcss('tailwind.config.js') ], 23 | }) 24 | --------------------------------------------------------------------------------