Dashboard (beta)
diff --git a/composer.json b/composer.json
index c01c313..76ee4cc 100644
--- a/composer.json
+++ b/composer.json
@@ -36,5 +36,23 @@
"Stackkit\\LaravelGoogleCloudTasksQueue\\CloudTasksServiceProvider"
]
}
+ },
+ "scripts": {
+ "l9": [
+ "composer require laravel/framework:9.* orchestra/testbench:7.* --no-interaction --no-update",
+ "composer update --prefer-stable --prefer-dist --no-interaction --no-suggest"
+ ],
+ "l8": [
+ "composer require laravel/framework:8.* orchestra/testbench:6.* --no-interaction --no-update",
+ "composer update --prefer-stable --prefer-dist --no-interaction --no-suggest"
+ ],
+ "l7": [
+ "composer require laravel/framework:7.* orchestra/testbench:5.* --no-interaction --no-update",
+ "composer update --prefer-stable --prefer-dist --no-interaction --no-suggest"
+ ],
+ "l6": [
+ "composer require laravel/framework:6.* orchestra/testbench:4.* --no-interaction --no-update",
+ "composer update --prefer-stable --prefer-dist --no-interaction --no-suggest"
+ ]
}
}
diff --git a/dashboard/dist/assets/index.1002db9a.css b/dashboard/dist/assets/index.d8eef428.css
similarity index 90%
rename from dashboard/dist/assets/index.1002db9a.css
rename to dashboard/dist/assets/index.d8eef428.css
index 9c56a8b..6721fae 100644
--- a/dashboard/dist/assets/index.1002db9a.css
+++ b/dashboard/dist/assets/index.d8eef428.css
@@ -1 +1 @@
-.router-link-active{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity));font-weight:700;--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}:root{--popper-theme-background-color: #333333;--popper-theme-background-color-hover: #333333;--popper-theme-text-color: #ffffff;--popper-theme-border-width: 0px;--popper-theme-border-style: solid;--popper-theme-border-radius: 6px;--popper-theme-padding: 4px;--popper-theme-box-shadow: 0 6px 30px -6px rgba(0, 0, 0, .25)}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.right-0{right:0}.top-0{top:0}.-left-1{left:-.25rem}.mb-4{margin-bottom:1rem}.mt-8{margin-top:2rem}.mb-2{margin-bottom:.5rem}.mt-6{margin-top:1.5rem}.mt-4{margin-top:1rem}.mt-3{margin-top:.75rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.-ml-1{margin-left:-.25rem}.mr-3{margin-right:.75rem}.ml-4{margin-left:1rem}.mt-12{margin-top:3rem}.ml-10{margin-left:2.5rem}.mb-6{margin-bottom:1.5rem}.mb-1{margin-bottom:.25rem}.mt-2{margin-top:.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-screen{height:100vh}.h-2{height:.5rem}.h-full{height:100%}.h-4{height:1rem}.h-5{height:1.25rem}.min-h-screen{min-height:100vh}.w-\[250px\]{width:250px}.w-\[300px\]{width:300px}.w-full{width:100%}.w-2{width:.5rem}.w-\[50px\]{width:50px}.w-\[100px\]{width:100px}.w-\[150px\]{width:150px}.w-\[200px\]{width:200px}.w-4{width:1rem}.w-5{width:1.25rem}.w-2\/12{width:16.666667%}.max-w-\[calc\(100\%-250px\)\]{max-width:calc(100% - 250px)}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-initial{flex:0 1 auto}.shrink-0{flex-shrink:0}.basis-auto{flex-basis:auto}.basis-\[400px\]{flex-basis:400px}.basis-\[250px\]{flex-basis:250px}.table-fixed{table-layout:fixed}.translate-x-\[300px\]{--tw-translate-x: 300px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes spin{to{transform:rotate(360deg)}}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-indigo-100{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-blue-300\/30{background-color:#93c5fd4d}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity))}.bg-red-100\/50{background-color:#fee2e280}.bg-white\/90{background-color:#ffffffe6}.p-6{padding:1.5rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-0{padding-top:0;padding-bottom:0}.px-4{padding-left:1rem;padding-right:1rem}.pr-6{padding-right:1.5rem}.pt-1{padding-top:.25rem}.pr-12{padding-right:3rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.text-sm{font-size:.875rem;line-height:1.25rem}.font-medium{font-weight:500}.font-light{font-weight:300}.font-semibold{font-weight:600}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.tracking-wider{letter-spacing:.05em}.text-indigo-900{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-indigo-100{--tw-text-opacity: 1;color:rgb(224 231 255 / var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.text-red-600\/50{color:#dc262680}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.text-black\/70{color:#000000b3}.text-black\/20{color:#0003}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-white{--tw-ring-opacity: 1;--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.\!filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:scale-\[1\.1\]:hover{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-indigo-100\/10:hover{background-color:#e0e7ff1a}.hover\:text-indigo-900:hover{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}@media (prefers-color-scheme: dark){.dark\:border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.dark\:bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.dark\:text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}}@media (min-width: 640px){.sm\:rounded-lg{border-radius:.5rem}}@-webkit-keyframes shake-59cd799c{8%,41%{transform:translate(-10px)}25%,58%{transform:translate(10px)}75%{transform:translate(-5px)}92%{transform:translate(5px)}0%,to{transform:translate(0)}}@keyframes shake-59cd799c{8%,41%{transform:translate(-10px)}25%,58%{transform:translate(10px)}75%{transform:translate(-5px)}92%{transform:translate(5px)}0%,to{transform:translate(0)}}.shake[data-v-59cd799c]{-webkit-animation:shake-59cd799c .3s linear;animation:shake-59cd799c .3s linear}input[type=password][data-v-59cd799c]{font:small-caption;font-size:36px}.task-error{background-color:#fee2e280;color:#dc262680}.task-queued,.task-scheduled{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.task-successful{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.task-failed{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.task-queued{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.task-running{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.task-successful[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.task-failed[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity))}.task-error[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity))}.task-queued[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity))}.task-running[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.no-scroll[data-v-d792a216]::-webkit-scrollbar{display:none}
+.router-link-active{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity));font-weight:700;--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}:root{--popper-theme-background-color: #333333;--popper-theme-background-color-hover: #333333;--popper-theme-text-color: #ffffff;--popper-theme-border-width: 0px;--popper-theme-border-style: solid;--popper-theme-border-radius: 6px;--popper-theme-padding: 4px;--popper-theme-box-shadow: 0 6px 30px -6px rgba(0, 0, 0, .25)}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.right-0{right:0}.top-0{top:0}.-left-1{left:-.25rem}.mb-4{margin-bottom:1rem}.mt-8{margin-top:2rem}.mb-2{margin-bottom:.5rem}.mt-6{margin-top:1.5rem}.mt-4{margin-top:1rem}.mt-3{margin-top:.75rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.-ml-1{margin-left:-.25rem}.mr-3{margin-right:.75rem}.ml-4{margin-left:1rem}.mt-12{margin-top:3rem}.ml-10{margin-left:2.5rem}.mb-6{margin-bottom:1.5rem}.mb-1{margin-bottom:.25rem}.mt-2{margin-top:.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-screen{height:100vh}.h-2{height:.5rem}.h-full{height:100%}.h-4{height:1rem}.h-5{height:1.25rem}.min-h-screen{min-height:100vh}.w-\[250px\]{width:250px}.w-\[300px\]{width:300px}.w-full{width:100%}.w-2{width:.5rem}.w-\[50px\]{width:50px}.w-\[100px\]{width:100px}.w-\[150px\]{width:150px}.w-\[200px\]{width:200px}.w-4{width:1rem}.w-5{width:1.25rem}.w-2\/12{width:16.666667%}.max-w-\[calc\(100\%-250px\)\]{max-width:calc(100% - 250px)}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-initial{flex:0 1 auto}.shrink-0{flex-shrink:0}.basis-auto{flex-basis:auto}.basis-\[400px\]{flex-basis:400px}.basis-\[250px\]{flex-basis:250px}.table-fixed{table-layout:fixed}.translate-x-\[300px\]{--tw-translate-x: 300px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes spin{to{transform:rotate(360deg)}}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-indigo-100{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-blue-300\/30{background-color:#93c5fd4d}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity))}.bg-red-100\/50{background-color:#fee2e280}.bg-white\/90{background-color:#ffffffe6}.p-6{padding:1.5rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-0{padding-top:0;padding-bottom:0}.px-4{padding-left:1rem;padding-right:1rem}.pr-6{padding-right:1.5rem}.pt-1{padding-top:.25rem}.pr-12{padding-right:3rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.text-sm{font-size:.875rem;line-height:1.25rem}.font-medium{font-weight:500}.font-light{font-weight:300}.font-semibold{font-weight:600}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.tracking-wider{letter-spacing:.05em}.text-indigo-900{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-indigo-100{--tw-text-opacity: 1;color:rgb(224 231 255 / var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.text-red-600\/50{color:#dc262680}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.text-black\/70{color:#000000b3}.text-black\/20{color:#0003}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-white{--tw-ring-opacity: 1;--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.\!filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:scale-\[1\.1\]:hover{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-indigo-100\/10:hover{background-color:#e0e7ff1a}.hover\:text-indigo-900:hover{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}@media (prefers-color-scheme: dark){.dark\:border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.dark\:bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.dark\:text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}}@media (min-width: 640px){.sm\:rounded-lg{border-radius:.5rem}}@-webkit-keyframes shake-59cd799c{8%,41%{transform:translate(-10px)}25%,58%{transform:translate(10px)}75%{transform:translate(-5px)}92%{transform:translate(5px)}0%,to{transform:translate(0)}}@keyframes shake-59cd799c{8%,41%{transform:translate(-10px)}25%,58%{transform:translate(10px)}75%{transform:translate(-5px)}92%{transform:translate(5px)}0%,to{transform:translate(0)}}.shake[data-v-59cd799c]{-webkit-animation:shake-59cd799c .3s linear;animation:shake-59cd799c .3s linear}input[type=password][data-v-59cd799c]{font:small-caption;font-size:36px}.task-error{background-color:#fee2e280;color:#dc262680}.task-queued,.task-scheduled{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.task-running,.task-released{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.task-successful{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.task-failed{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.task-queued{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.task-running{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.task-successful[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.task-failed[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity))}.task-error[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity))}.task-queued[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity))}.task-running[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.no-scroll[data-v-5df67d4c]::-webkit-scrollbar{display:none}
diff --git a/dashboard/dist/assets/index.5a46c6a0.js b/dashboard/dist/assets/index.ea68d73f.js
similarity index 52%
rename from dashboard/dist/assets/index.5a46c6a0.js
rename to dashboard/dist/assets/index.ea68d73f.js
index 5a57c92..e61a7bc 100644
--- a/dashboard/dist/assets/index.5a46c6a0.js
+++ b/dashboard/dist/assets/index.ea68d73f.js
@@ -1 +1 @@
-var W=Object.defineProperty,J=Object.defineProperties;var X=Object.getOwnPropertyDescriptors;var D=Object.getOwnPropertySymbols;var Y=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable;var E=(s,a,t)=>a in s?W(s,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[a]=t,C=(s,a)=>{for(var t in a||(a={}))Y.call(a,t)&&E(s,t,a[t]);if(D)for(var t of D(a))Z.call(a,t)&&E(s,t,a[t]);return s},V=(s,a)=>J(s,X(a));var b=(s,a,t)=>new Promise((r,o)=>{var l=c=>{try{i(t.next(c))}catch(u){o(u)}},n=c=>{try{i(t.throw(c))}catch(u){o(u)}},i=c=>c.done?r(c.value):Promise.resolve(c.value).then(l,n);i((t=t.apply(s,a)).next())});import{r as S,o as d,c as _,a as h,w as x,n as g,F as w,b as y,d as e,e as ee,f as te,u as k,g as m,h as A,i as O,v as se,j as B,p as F,k as N,t as p,l as M,m as oe,q as ae,s as ne,x as q,y as $,z as j,A as le,B as re,C as ie,D as ce}from"./vendor.433de25e.js";const ue=function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const l of o)if(l.type==="childList")for(const n of l.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&r(n)}).observe(document,{childList:!0,subtree:!0});function t(o){const l={};return o.integrity&&(l.integrity=o.integrity),o.referrerpolicy&&(l.referrerPolicy=o.referrerpolicy),o.crossorigin==="use-credentials"?l.credentials="include":o.crossorigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function r(o){if(o.ep)return;o.ep=!0;const l=t(o);fetch(o.href,l)}};ue();var T=(s,a)=>{const t=s.__vccOpts||s;for(const[r,o]of a)t[r]=o;return t};const de={},pe=y("Dashboard "),_e=y("Recent "),he=y("Queued "),me=y("Failed ");function fe(s,a){var r,o,l,n,i,c,u,f,v;const t=S("router-link");return d(),_(w,null,[h(t,{to:{name:"home"},class:"block p-4 rounded mb-2 cursor-pointer"},{default:x(()=>[pe]),_:1}),h(t,{to:{name:"recent"},class:g(["block p-4 rounded mb-2 cursor-pointer",{"router-link-active":((l=(o=(r=s.$route)==null?void 0:r.matched[0])==null?void 0:o.meta)==null?void 0:l.route)==="recent"}])},{default:x(()=>[_e]),_:1},8,["class"]),h(t,{to:{name:"queued"},class:g(["block p-4 rounded mb-2 cursor-pointer",{"router-link-active":((c=(i=(n=s.$route)==null?void 0:n.matched[0])==null?void 0:i.meta)==null?void 0:c.route)==="queued"}])},{default:x(()=>[he]),_:1},8,["class"]),h(t,{to:{name:"failed"},class:g(["block p-4 rounded mb-2",{"router-link-active":((v=(f=(u=s.$route)==null?void 0:u.matched[0])==null?void 0:f.meta)==null?void 0:v.route)==="failed"}])},{default:x(()=>[me]),_:1},8,["class"])],64)}var xe=T(de,[["render",fe]]);const ve={class:"flex"},ge={class:"basis-auto w-[250px] shrink-0 bg-white p-6 min-h-screen"},ye={class:"flex-1 max-w-[calc(100%-250px)] p-6"},be={setup(s){return(a,t)=>{const r=S("router-view");return d(),_("div",ve,[e("aside",ge,[h(xe)]),e("div",ye,[h(r)])])}}};function I(){return b(this,arguments,function*({endpoint:s,router:a,body:t=null,method:r="GET",login:o=!1}={}){const l=yield fetch(`/cloud-tasks-api/${s}`,V(C({method:r},t?{body:t}:{}),{headers:C({},o?{}:{Authorization:`Bearer ${localStorage.getItem("cloud-tasks-token")}`})}));return l.status===403&&!o&&(localStorage.removeItem("cloud-tasks-token"),a.push({name:"login"})),o?yield l.text():yield l.json()})}function L(r){return b(this,arguments,function*(s,a={},t){let o=!1;const l=function(c){return b(this,null,function*(){if(o)return;const u=new URL(window.location.href),f=new URLSearchParams(u.search);for(const[v,G]of Object.entries(a))f.append(v,G);o=!0,c.value=yield I({endpoint:`tasks?${f.toString()}`,router:t}),o=!1})};l(s);let n=setInterval(()=>l(s),3e3);ee(function(){setTimeout(()=>l(s))});const i=function(){document.visibilityState==="visible"?(l(s),clearInterval(n),n=setInterval(()=>l(s),3e3)):document.visibilityState==="hidden"&&clearInterval(n)};document.addEventListener("visibilitychange",i),te(()=>{clearInterval(n),document.removeEventListener("visibilitychange",i),o=!1})})}const z=s=>(F("data-v-59cd799c"),s=s(),N(),s),we={class:"block w-full h-full flex items-center justify-center"},ke=z(()=>e("h3",{class:"text-4xl"},"This application is password protected.",-1)),$e=["onKeyup","disabled"],Ce=z(()=>e("div",{class:"text-center mt-6 text-xl"},[y(" Press "),e("span",{class:"bg-blue-200 py-1 px-2 ml-2 mr-2 rounded text-blue-800"},"Enter"),y(" to log in. ")],-1)),qe={setup(s){const a=k(),t=m(null),r=m(""),o=m(""),l=m(!1),n=m(!1);A(()=>{t.value.focus()});function i(){return b(this,null,function*(){if(r.value===""||r.value===o.value)return;o.value=r.value;const c=new FormData;c.append("password",r.value),n.value=!0;const u=yield I({endpoint:"login",method:"POST",body:c,login:!0});n.value=!1,u?(localStorage.setItem("cloud-tasks-token",u),a.push({name:"home"})):(l.value=!0,setTimeout(()=>{l.value=!1},820),setTimeout(()=>t.value.focus(),50))})}return(c,u)=>(d(),_("div",we,[e("div",null,[ke,O(e("input",{type:"password",class:g(["w-full p-2 px-6 text-2xl font-light mt-8 text-center outline-none shadow rounded-full",{shake:l.value}]),onKeyup:B(i,["enter"]),"onUpdate:modelValue":u[0]||(u[0]=f=>r.value=f),disabled:n.value,ref_key:"inputRef",ref:t},null,42,$e),[[se,r.value]]),Ce])]))}};var Te=T(qe,[["__scopeId","data-v-59cd799c"]]);const Se=e("h3",{class:"text-3xl mb-4"},"All tasks",-1),Ae={class:"grid grid-cols-3 gap-4"},Ie=["textContent"],Le=e("span",{class:"text-gray-600"},"this minute",-1),Re=["textContent"],Pe=e("span",{class:"text-gray-600"},"this hour",-1),Ue=["textContent"],De=e("span",{class:"text-gray-600"},"today",-1),Ee=e("h3",{class:"text-3xl mb-4 mt-8"},"Failed tasks",-1),Ve={class:"grid grid-cols-3 gap-4"},Oe=["textContent"],Be=e("span",{class:"text-gray-600"},"this minute",-1),Fe=["textContent"],Ne=e("span",{class:"text-gray-600"},"this hour",-1),Me=["textContent"],je=e("span",{class:"text-gray-600"},"today",-1),ze={setup(s){const a=k(),t=m({recent:{this_minute:"...",this_hour:"...",today:"..."},failed:{this_minute:"...",this_hour:"...",today:"..."}});return A(()=>b(this,null,function*(){t.value=yield I({endpoint:"dashboard",router:a})})),(r,o)=>{const l=S("router-link");return d(),_(w,null,[Se,e("div",Ae,[h(l,{to:{name:"recent",query:{time:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.recent)==null?void 0:i.this_minute)},null,8,Ie),Le]}),_:1},8,["to"]),h(l,{to:{name:"recent",query:{hour:new Date().getUTCHours()}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.recent)==null?void 0:i.this_hour)},null,8,Re),Pe]}),_:1},8,["to"]),h(l,{to:{name:"recent"},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.recent)==null?void 0:i.this_day)},null,8,Ue),De]}),_:1})]),Ee,e("div",Ve,[h(l,{to:{name:"failed",query:{time:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.failed)==null?void 0:i.this_minute)},null,8,Oe),Be]}),_:1},8,["to"]),h(l,{to:{name:"failed",query:{hour:new Date().getUTCHours()}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.failed)==null?void 0:i.this_hour)},null,8,Fe),Ne]}),_:1},8,["to"]),h(l,{to:{name:"failed"},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.failed)==null?void 0:i.this_day)},null,8,Me),je]}),_:1})])],64)}}};const H={props:{status:String,classes:{type:Array,default:[]}},setup(s){function a(t){return t.charAt(0).toUpperCase()+t.slice(1)}return(t,r)=>(d(),_("span",{class:g(["px-2 inline-flex text-xs leading-5 font-semibold rounded-full",[`task-${s.status}`,...s.classes]])},p(a(s.status)),3))}},He={},Ke=e("tr",null,[e("td",{colspan:"7",class:"px-6 py-4 bg-white"},[e("svg",{class:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},[e("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"indigo","stroke-width":"4"}),e("path",{class:"opacity-75",fill:"indigo",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})])])],-1),Qe=[Ke];function Ge(s,a){return d(),_("tbody",null,Qe)}var We=T(He,[["render",Ge]]);const Je=e("label",{for:"queue",class:"block mb-2 font-medium"},"Queue",-1),Xe=["onKeyup"],Ye=e("label",{for:"status",class:"block mb-2 mt-6 font-medium"},"Status",-1),Ze=ae('',6),et=[Ze],tt={props:{focus:String},setup(s){const a=s,t=k(),r=M(),o=m(!1),l=m(null),n=m(null);function i(){t.push({name:r.name,query:C(C({},l.value.value?{queue:l.value.value}:{}),n.value?{status:n.value}:{})})}function c(u){u===""&&i()}return A(()=>{setTimeout(()=>o.value=!0),a.focus==="queue"&&l.value.focus()}),(u,f)=>(d(),_("div",{class:g(["w-[300px] fixed transition-transform right-0 top-0 p-6 px-6 shadow-2xl h-screen bg-white",{"translate-x-[300px]":o.value===!1}])},[Je,e("input",{type:"text",name:"queue",id:"queue",ref_key:"queue",ref:l,class:"bg-white py-2 px-3 w-full rounded border",onKeyup:[B(i,["enter"]),f[0]||(f[0]=v=>c(v.target.value))]},null,40,Xe),Ye,O(e("select",{name:"status",id:"status","onUpdate:modelValue":f[1]||(f[1]=v=>n.value=v),class:"bg-white py-2 px-3 w-full rounded border"},et,512),[[oe,n.value]]),e("button",{class:"bg-indigo-500 w-full mt-4 text-indigo-100 rounded py-2",onClick:i}," Apply Filter (or Press Enter) ")],2))}};const st={class:"text-4xl mb-2"},ot={class:"text-lg"},at={class:"flex flex-row mt-6"},nt={class:"flex-1"},lt={class:"align-middle"},rt={class:"shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"},it={class:"table-fixed divide-y divide-gray-200 w-full"},ct={class:"bg-gray-50"},ut=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[50px]"}," # ",-1),dt=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider max-w-xl w-[300px]"}," Name ",-1),pt=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[100px]"}," Status ",-1),_t=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[150px] text-center"}," Attempts ",-1),ht=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[200px]"}," Created ",-1),mt={scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"},ft=y(" Queue "),xt={class:"inline relative"},vt=e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"},null,-1),gt=[vt],yt=e("th",{scope:"col",class:"relative px-6 py-3"},[e("span",{class:"sr-only"},"Edit")],-1),bt={key:1},wt=e("tr",null,[e("td",{colspan:"7",class:"px-6 py-4 bg-white"},"No results.")],-1),kt=[wt],$t={class:"bg-white divide-y divide-gray-200"},Ct=["onClick"],qt={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-900"},Tt={class:"px-6 py-4 whitespace-nowrap text-ellipsis text-sm text-gray-900"},St={class:"px-6 py-4 whitespace-nowrap"},At={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center"},It={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Lt={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Rt=e("td",{class:"px-6 py-4 whitespace-nowrap text-right text-sm font-medium"},[e("a",{href:"#",class:"text-indigo-600 hover:text-indigo-900"},"View")],-1),R={props:{title:String,description:String,tasks:Array},setup(s){const a=s,t=m([]),r=m([]),o=m({visible:!1,focus:null});function l(n){t.value.push(n.id),setTimeout(()=>{t.value.splice(t.value.indexOf(n.id),1)},1e3)}return ne(()=>a.tasks,(n,i)=>{var c;if(!!i){r.value=[],i.map((u,f)=>{r[u.id]=f});for(const u of n)(r[u.id]===void 0||((c=i[r[u.id]])==null?void 0:c.status)!==u.status)&&l(u)}}),(n,i)=>(d(),_(w,null,[e("h1",st,p(s.title),1),e("p",ot,p(s.description),1),e("div",at,[e("div",nt,[e("div",lt,[e("div",rt,[e("table",it,[e("thead",ct,[e("tr",null,[ut,dt,pt,_t,ht,e("th",mt,[ft,e("div",xt,[(d(),_("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-4 w-4 inline transition-transform hover:scale-[1.1] cursor-pointer",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",onClick:i[0]||(i[0]=()=>{o.value.visible=!o.value.visible,o.value.focus=o.value.visible?"queue":null})},gt))])]),yt])]),s.tasks===null?(d(),q(We,{key:0})):$("",!0),s.tasks&&s.tasks.length===0?(d(),_("tbody",bt,kt)):$("",!0),e("tbody",$t,[(d(!0),_(w,null,j(s.tasks,c=>(d(),_("tr",{class:g(["cursor-pointer hover:bg-indigo-100/10 transition-colors",{"bg-blue-300/30":t.value.includes(c.id)}]),onClick:u=>n.$router.push({name:`${n.$route.name}-task`,params:{uuid:c.uuid}})},[e("td",qt,p(c.id),1),e("td",Tt,p(c.name.substring(0,30))+p(c.name.length>30?"...":""),1),e("td",St,[h(H,{status:c.status},null,8,["status"])]),e("td",At,p(c.attempts),1),e("td",It,p(c.created),1),e("td",Lt,p(c.queue),1),Rt],10,Ct))),256))])])])])])]),o.value.visible?(d(),q(tt,{key:0,visible:o.value.visible,focus:o.value.focus},null,8,["visible","focus"])):$("",!0)],64))}},Pt={props:{tasks:Array},setup(s){const a=m(null),t=k();return L(a,{filter:"recent"},t),(r,o)=>(d(),q(R,{title:"Recent tasks",description:"Tasks that have been added or processed in the queue recently.",tasks:a.value},null,8,["tasks"]))}},Ut={props:{tasks:Array},setup(s){const a=m(null),t=k();return L(a,{status:"queued"},t),(r,o)=>(d(),q(R,{title:"Queued tasks",description:"Tasks that have been added to the queue recently.",tasks:a.value},null,8,["tasks"]))}},Dt={props:{tasks:Array},setup(s){const a=m(null),t=k();return L(a,{filter:"failed"},t),(r,o)=>(d(),q(R,{title:"Failed tasks",description:"Tasks that permanently failed after they have reached their max number of attempts.",tasks:a.value},null,8,["tasks"]))}};const Et={class:"absolute flex items-center justify-center w-2 h-2 bg-gray-200 rounded-full -left-1 ring-1 mt-3 ring-white"},Vt={props:{status:String,classes:{type:Array,default:[]}},setup(s){return(a,t)=>(d(),_("span",Et))}};var Ot=T(Vt,[["__scopeId","data-v-35155177"]]);const P=s=>(F("data-v-d792a216"),s=s(),N(),s),Bt={class:"text-4xl mb-2"},Ft={class:"flex"},Nt={class:"basis-[400px] shrink-0 pr-6 w-2/12"},Mt={class:"flex-initial sticky ml-4 mt-12"},jt={class:"relative border-l border-gray-200 dark:border-gray-700"},zt={class:"text-gray-900"},Ht={key:0,class:"bg-blue-100 text-blue-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Kt={key:0},Qt={class:"bg-gray-200 text-gray-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Gt={class:"block mb-2 mt-2 text-xs text-black/70 font-normal leading-none"},Wt={class:"cursor-default"},Jt={class:"basis-auto overflow-x-auto pr-12"},Xt=P(()=>e("h2",{class:"text-2xl"},"Task Exception",-1)),Yt={class:"text-xs p-8 border border-[#ccc/80] bg-white/90 mt-4 rounded overflow-auto no-scroll"},Zt={key:1,class:"mt-12"},es=P(()=>e("h2",{class:"text-2xl"},"Task Payload",-1)),ts={class:"text-xs p-8 border border-[#ccc/80] bg-white/90 mt-4 rounded overflow-auto no-scroll"},ss=P(()=>e("div",{class:"basis-[250px] shrink-0 px-6"},[e("h2",{class:"text-3xl"},"Actions"),e("button",{class:"bg-gray-200 text-black/20 cursor-not-allowed mt-4 w-full rounded px-4 py-2"}," Retry "),e("span",{class:"text-xs text-gray-800 mt-2 inline-block"},"Retrying tasks is not available yet.")],-1)),os={setup(s){const a=M(),t=k(),r=m({id:null,status:"loading"});A(()=>b(this,null,function*(){r.value=yield I({endpoint:`task/${a.params.uuid}`,router:t})}));const o={scheduled:"Scheduled",queued:"Added to the queue",running:"Running",successful:"Successful",error:"An error occurred",failed:"Failed permanently"};return(l,n)=>{const i=S("Popper");return d(),_(w,null,[e("h1",Bt,"Task #"+p(r.value.id),1),h(H,{status:r.value.status,classes:["text-sm"]},null,8,["status"]),e("div",Ft,[e("div",Nt,[e("div",Mt,[e("ol",jt,[(d(!0),_(w,null,j(r.value.events,(c,u)=>(d(),_("li",{class:g(["ml-10 pt-1 mb-6",[`event-${c.status}`]])},[h(Ot,{status:c.status},null,8,["status"]),e("h3",zt,[y(p(o[c.status]||c.status)+" ",1),e("div",null,[c.queue?(d(),_("span",Ht,p(r.value.queue),1)):$("",!0)]),c.scheduled_at?(d(),_("div",Kt,[e("span",Qt," Scheduled: "+p(c.scheduled_at)+" (UTC) ",1)])):$("",!0)]),h(i,{content:c.datetime,hover:!0,arrow:!0,placement:"right"},{default:x(()=>[e("time",Gt,[e("span",Wt,p(c.diff),1)])]),_:2},1032,["content"])],2))),256))])])]),e("div",Jt,[r.value.exception?(d(),_(w,{key:0},[Xt,e("pre",Yt,p(r.value.exception),1)],64)):$("",!0),r.value.payload?(d(),_("div",Zt,[es,e("pre",ts,p(r.value.payload),1)])):$("",!0)]),ss])],64)}}};var U=T(os,[["__scopeId","data-v-d792a216"]]);const as=[{name:"home",path:"/",component:ze},{name:"login",path:"/login",component:Te},{name:"recent",path:"/recent",component:Pt,meta:{route:"recent"}},{name:"recent-task",path:"/recent/:uuid",component:U,meta:{route:"recent"}},{name:"queued",path:"/queued",component:Ut,meta:{route:"queued"}},{name:"queued-task",path:"/queued/:uuid",component:U,meta:{route:"queued"}},{name:"failed",path:"/failed",component:Dt,meta:{route:"failed"}},{name:"failed-task",path:"/failed/:uuid",component:U,meta:{route:"failed"}}];let K=null;"CloudTasks"in window&&(K=`/${window.CloudTasks.path}`);const Q=le({history:re(K),routes:as});Q.beforeEach((s,a,t)=>!localStorage.hasOwnProperty("cloud-tasks-token")&&s.name!=="login"?t({name:"login"}):t());ie(be).use(Q).component("Popper",ce).mount("#app");
+var W=Object.defineProperty,J=Object.defineProperties;var X=Object.getOwnPropertyDescriptors;var D=Object.getOwnPropertySymbols;var Y=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable;var E=(s,a,t)=>a in s?W(s,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[a]=t,C=(s,a)=>{for(var t in a||(a={}))Y.call(a,t)&&E(s,t,a[t]);if(D)for(var t of D(a))Z.call(a,t)&&E(s,t,a[t]);return s},V=(s,a)=>J(s,X(a));var w=(s,a,t)=>new Promise((r,o)=>{var l=i=>{try{c(t.next(i))}catch(u){o(u)}},n=i=>{try{c(t.throw(i))}catch(u){o(u)}},c=i=>i.done?r(i.value):Promise.resolve(i.value).then(l,n);c((t=t.apply(s,a)).next())});import{r as S,o as d,c as _,a as h,w as x,n as y,F as k,b as g,d as e,e as ee,f as te,u as $,g as m,h as A,i as O,v as se,j as B,p as F,k as N,t as p,l as M,m as oe,q as ae,s as ne,x as q,y as b,z as j,A as le,B as re,C as ie,D as ce}from"./vendor.433de25e.js";const ue=function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const l of o)if(l.type==="childList")for(const n of l.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&r(n)}).observe(document,{childList:!0,subtree:!0});function t(o){const l={};return o.integrity&&(l.integrity=o.integrity),o.referrerpolicy&&(l.referrerPolicy=o.referrerpolicy),o.crossorigin==="use-credentials"?l.credentials="include":o.crossorigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function r(o){if(o.ep)return;o.ep=!0;const l=t(o);fetch(o.href,l)}};ue();var T=(s,a)=>{const t=s.__vccOpts||s;for(const[r,o]of a)t[r]=o;return t};const de={},pe=g("Dashboard "),_e=g("Recent "),he=g("Queued "),me=g("Failed ");function fe(s,a){var r,o,l,n,c,i,u,f,v;const t=S("router-link");return d(),_(k,null,[h(t,{to:{name:"home"},class:"block p-4 rounded mb-2 cursor-pointer"},{default:x(()=>[pe]),_:1}),h(t,{to:{name:"recent"},class:y(["block p-4 rounded mb-2 cursor-pointer",{"router-link-active":((l=(o=(r=s.$route)==null?void 0:r.matched[0])==null?void 0:o.meta)==null?void 0:l.route)==="recent"}])},{default:x(()=>[_e]),_:1},8,["class"]),h(t,{to:{name:"queued"},class:y(["block p-4 rounded mb-2 cursor-pointer",{"router-link-active":((i=(c=(n=s.$route)==null?void 0:n.matched[0])==null?void 0:c.meta)==null?void 0:i.route)==="queued"}])},{default:x(()=>[he]),_:1},8,["class"]),h(t,{to:{name:"failed"},class:y(["block p-4 rounded mb-2",{"router-link-active":((v=(f=(u=s.$route)==null?void 0:u.matched[0])==null?void 0:f.meta)==null?void 0:v.route)==="failed"}])},{default:x(()=>[me]),_:1},8,["class"])],64)}var xe=T(de,[["render",fe]]);const ve={class:"flex"},ye={class:"basis-auto w-[250px] shrink-0 bg-white p-6 min-h-screen"},ge={class:"flex-1 max-w-[calc(100%-250px)] p-6"},be={setup(s){return(a,t)=>{const r=S("router-view");return d(),_("div",ve,[e("aside",ye,[h(xe)]),e("div",ge,[h(r)])])}}};function I(){return w(this,arguments,function*({endpoint:s,router:a,body:t=null,method:r="GET",login:o=!1}={}){const l=yield fetch(`/cloud-tasks-api/${s}`,V(C({method:r},t?{body:t}:{}),{headers:C({},o?{}:{Authorization:`Bearer ${localStorage.getItem("cloud-tasks-token")}`})}));return l.status===403&&!o&&(localStorage.removeItem("cloud-tasks-token"),a.push({name:"login"})),o?yield l.text():yield l.json()})}function R(r){return w(this,arguments,function*(s,a={},t){let o=!1;const l=function(i){return w(this,null,function*(){if(o)return;const u=new URL(window.location.href),f=new URLSearchParams(u.search);for(const[v,G]of Object.entries(a))f.append(v,G);o=!0,i.value=yield I({endpoint:`tasks?${f.toString()}`,router:t}),o=!1})};l(s);let n=setInterval(()=>l(s),3e3);ee(function(){setTimeout(()=>l(s))});const c=function(){document.visibilityState==="visible"?(l(s),clearInterval(n),n=setInterval(()=>l(s),3e3)):document.visibilityState==="hidden"&&clearInterval(n)};document.addEventListener("visibilitychange",c),te(()=>{clearInterval(n),document.removeEventListener("visibilitychange",c),o=!1})})}const z=s=>(F("data-v-59cd799c"),s=s(),N(),s),we={class:"block w-full h-full flex items-center justify-center"},ke=z(()=>e("h3",{class:"text-4xl"},"This application is password protected.",-1)),$e=["onKeyup","disabled"],Ce=z(()=>e("div",{class:"text-center mt-6 text-xl"},[g(" Press "),e("span",{class:"bg-blue-200 py-1 px-2 ml-2 mr-2 rounded text-blue-800"},"Enter"),g(" to log in. ")],-1)),qe={setup(s){const a=$(),t=m(null),r=m(""),o=m(""),l=m(!1),n=m(!1);A(()=>{t.value.focus()});function c(){return w(this,null,function*(){if(r.value===""||r.value===o.value)return;o.value=r.value;const i=new FormData;i.append("password",r.value),n.value=!0;const u=yield I({endpoint:"login",method:"POST",body:i,login:!0});n.value=!1,u?(localStorage.setItem("cloud-tasks-token",u),a.push({name:"home"})):(l.value=!0,setTimeout(()=>{l.value=!1},820),setTimeout(()=>t.value.focus(),50))})}return(i,u)=>(d(),_("div",we,[e("div",null,[ke,O(e("input",{type:"password",class:y(["w-full p-2 px-6 text-2xl font-light mt-8 text-center outline-none shadow rounded-full",{shake:l.value}]),onKeyup:B(c,["enter"]),"onUpdate:modelValue":u[0]||(u[0]=f=>r.value=f),disabled:n.value,ref_key:"inputRef",ref:t},null,42,$e),[[se,r.value]]),Ce])]))}};var Te=T(qe,[["__scopeId","data-v-59cd799c"]]);const Se=e("h3",{class:"text-3xl mb-4"},"All tasks",-1),Ae={class:"grid grid-cols-3 gap-4"},Ie=["textContent"],Re=e("span",{class:"text-gray-600"},"this minute",-1),Le=["textContent"],Pe=e("span",{class:"text-gray-600"},"this hour",-1),Ue=["textContent"],De=e("span",{class:"text-gray-600"},"today",-1),Ee=e("h3",{class:"text-3xl mb-4 mt-8"},"Failed tasks",-1),Ve={class:"grid grid-cols-3 gap-4"},Oe=["textContent"],Be=e("span",{class:"text-gray-600"},"this minute",-1),Fe=["textContent"],Ne=e("span",{class:"text-gray-600"},"this hour",-1),Me=["textContent"],je=e("span",{class:"text-gray-600"},"today",-1),ze={setup(s){const a=$(),t=m({recent:{this_minute:"...",this_hour:"...",today:"..."},failed:{this_minute:"...",this_hour:"...",today:"..."}});return A(()=>w(this,null,function*(){t.value=yield I({endpoint:"dashboard",router:a})})),(r,o)=>{const l=S("router-link");return d(),_(k,null,[Se,e("div",Ae,[h(l,{to:{name:"recent",query:{time:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.recent)==null?void 0:c.this_minute)},null,8,Ie),Re]}),_:1},8,["to"]),h(l,{to:{name:"recent",query:{hour:new Date().getUTCHours()}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.recent)==null?void 0:c.this_hour)},null,8,Le),Pe]}),_:1},8,["to"]),h(l,{to:{name:"recent"},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.recent)==null?void 0:c.this_day)},null,8,Ue),De]}),_:1})]),Ee,e("div",Ve,[h(l,{to:{name:"failed",query:{time:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.failed)==null?void 0:c.this_minute)},null,8,Oe),Be]}),_:1},8,["to"]),h(l,{to:{name:"failed",query:{hour:new Date().getUTCHours()}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.failed)==null?void 0:c.this_hour)},null,8,Fe),Ne]}),_:1},8,["to"]),h(l,{to:{name:"failed"},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.failed)==null?void 0:c.this_day)},null,8,Me),je]}),_:1})])],64)}}};const H={props:{status:String,classes:{type:Array,default:[]}},setup(s){function a(t){return t.charAt(0).toUpperCase()+t.slice(1)}return(t,r)=>(d(),_("span",{class:y(["px-2 inline-flex text-xs leading-5 font-semibold rounded-full",[`task-${s.status}`,...s.classes]])},p(a(s.status)),3))}},He={},Ke=e("tr",null,[e("td",{colspan:"7",class:"px-6 py-4 bg-white"},[e("svg",{class:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},[e("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"indigo","stroke-width":"4"}),e("path",{class:"opacity-75",fill:"indigo",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})])])],-1),Qe=[Ke];function Ge(s,a){return d(),_("tbody",null,Qe)}var We=T(He,[["render",Ge]]);const Je=e("label",{for:"queue",class:"block mb-2 font-medium"},"Queue",-1),Xe=["onKeyup"],Ye=e("label",{for:"status",class:"block mb-2 mt-6 font-medium"},"Status",-1),Ze=ae('',6),et=[Ze],tt={props:{focus:String},setup(s){const a=s,t=$(),r=M(),o=m(!1),l=m(null),n=m(null);function c(){t.push({name:r.name,query:C(C({},l.value.value?{queue:l.value.value}:{}),n.value?{status:n.value}:{})})}function i(u){u===""&&c()}return A(()=>{setTimeout(()=>o.value=!0),a.focus==="queue"&&l.value.focus()}),(u,f)=>(d(),_("div",{class:y(["w-[300px] fixed transition-transform right-0 top-0 p-6 px-6 shadow-2xl h-screen bg-white",{"translate-x-[300px]":o.value===!1}])},[Je,e("input",{type:"text",name:"queue",id:"queue",ref_key:"queue",ref:l,class:"bg-white py-2 px-3 w-full rounded border",onKeyup:[B(c,["enter"]),f[0]||(f[0]=v=>i(v.target.value))]},null,40,Xe),Ye,O(e("select",{name:"status",id:"status","onUpdate:modelValue":f[1]||(f[1]=v=>n.value=v),class:"bg-white py-2 px-3 w-full rounded border"},et,512),[[oe,n.value]]),e("button",{class:"bg-indigo-500 w-full mt-4 text-indigo-100 rounded py-2",onClick:c}," Apply Filter (or Press Enter) ")],2))}};const st={class:"text-4xl mb-2"},ot={class:"text-lg"},at={class:"flex flex-row mt-6"},nt={class:"flex-1"},lt={class:"align-middle"},rt={class:"shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"},it={class:"table-fixed divide-y divide-gray-200 w-full"},ct={class:"bg-gray-50"},ut=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[50px]"}," # ",-1),dt=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider max-w-xl w-[300px]"}," Name ",-1),pt=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[100px]"}," Status ",-1),_t=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[150px] text-center"}," Attempts ",-1),ht=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[200px]"}," Created ",-1),mt={scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"},ft=g(" Queue "),xt={class:"inline relative"},vt=e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"},null,-1),yt=[vt],gt=e("th",{scope:"col",class:"relative px-6 py-3"},[e("span",{class:"sr-only"},"Edit")],-1),bt={key:1},wt=e("tr",null,[e("td",{colspan:"7",class:"px-6 py-4 bg-white"},"No results.")],-1),kt=[wt],$t={class:"bg-white divide-y divide-gray-200"},Ct=["onClick"],qt={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-900"},Tt={class:"px-6 py-4 whitespace-nowrap text-ellipsis text-sm text-gray-900"},St={class:"px-6 py-4 whitespace-nowrap"},At={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center"},It={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Rt={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Lt=e("td",{class:"px-6 py-4 whitespace-nowrap text-right text-sm font-medium"},[e("a",{href:"#",class:"text-indigo-600 hover:text-indigo-900"},"View")],-1),L={props:{title:String,description:String,tasks:Array},setup(s){const a=s,t=m([]),r=m([]),o=m({visible:!1,focus:null});function l(n){t.value.push(n.id),setTimeout(()=>{t.value.splice(t.value.indexOf(n.id),1)},1e3)}return ne(()=>a.tasks,(n,c)=>{var i;if(!!c){r.value=[],c.map((u,f)=>{r[u.id]=f});for(const u of n)(r[u.id]===void 0||((i=c[r[u.id]])==null?void 0:i.status)!==u.status)&&l(u)}}),(n,c)=>(d(),_(k,null,[e("h1",st,p(s.title),1),e("p",ot,p(s.description),1),e("div",at,[e("div",nt,[e("div",lt,[e("div",rt,[e("table",it,[e("thead",ct,[e("tr",null,[ut,dt,pt,_t,ht,e("th",mt,[ft,e("div",xt,[(d(),_("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-4 w-4 inline transition-transform hover:scale-[1.1] cursor-pointer",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",onClick:c[0]||(c[0]=()=>{o.value.visible=!o.value.visible,o.value.focus=o.value.visible?"queue":null})},yt))])]),gt])]),s.tasks===null?(d(),q(We,{key:0})):b("",!0),s.tasks&&s.tasks.length===0?(d(),_("tbody",bt,kt)):b("",!0),e("tbody",$t,[(d(!0),_(k,null,j(s.tasks,i=>(d(),_("tr",{class:y(["cursor-pointer hover:bg-indigo-100/10 transition-colors",{"bg-blue-300/30":t.value.includes(i.id)}]),onClick:u=>n.$router.push({name:`${n.$route.name}-task`,params:{uuid:i.uuid}})},[e("td",qt,p(i.id),1),e("td",Tt,p(i.name.substring(0,30))+p(i.name.length>30?"...":""),1),e("td",St,[h(H,{status:i.status},null,8,["status"])]),e("td",At,p(i.attempts),1),e("td",It,p(i.created),1),e("td",Rt,p(i.queue),1),Lt],10,Ct))),256))])])])])])]),o.value.visible?(d(),q(tt,{key:0,visible:o.value.visible,focus:o.value.focus},null,8,["visible","focus"])):b("",!0)],64))}},Pt={props:{tasks:Array},setup(s){const a=m(null),t=$();return R(a,{filter:"recent"},t),(r,o)=>(d(),q(L,{title:"Recent tasks",description:"Tasks that have been added or processed in the queue recently.",tasks:a.value},null,8,["tasks"]))}},Ut={props:{tasks:Array},setup(s){const a=m(null),t=$();return R(a,{status:"queued"},t),(r,o)=>(d(),q(L,{title:"Queued tasks",description:"Tasks that have been added to the queue recently.",tasks:a.value},null,8,["tasks"]))}},Dt={props:{tasks:Array},setup(s){const a=m(null),t=$();return R(a,{filter:"failed"},t),(r,o)=>(d(),q(L,{title:"Failed tasks",description:"Tasks that permanently failed after they have reached their max number of attempts.",tasks:a.value},null,8,["tasks"]))}};const Et={class:"absolute flex items-center justify-center w-2 h-2 bg-gray-200 rounded-full -left-1 ring-1 mt-3 ring-white"},Vt={props:{status:String,classes:{type:Array,default:[]}},setup(s){return(a,t)=>(d(),_("span",Et))}};var Ot=T(Vt,[["__scopeId","data-v-35155177"]]);const P=s=>(F("data-v-5df67d4c"),s=s(),N(),s),Bt={class:"text-4xl mb-2"},Ft={class:"flex"},Nt={class:"basis-[400px] shrink-0 pr-6 w-2/12"},Mt={class:"flex-initial sticky ml-4 mt-12"},jt={class:"relative border-l border-gray-200 dark:border-gray-700"},zt={class:"text-gray-900"},Ht={key:0,class:"bg-blue-100 text-blue-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Kt={key:0},Qt={class:"bg-gray-200 text-gray-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Gt={key:1},Wt={class:"bg-gray-200 text-gray-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Jt={class:"block mb-2 mt-2 text-xs text-black/70 font-normal leading-none"},Xt={class:"cursor-default"},Yt={class:"basis-auto overflow-x-auto pr-12"},Zt=P(()=>e("h2",{class:"text-2xl"},"Task Exception",-1)),es={class:"text-xs p-8 border border-[#ccc/80] bg-white/90 mt-4 rounded overflow-auto no-scroll"},ts={key:1,class:"mt-12"},ss=P(()=>e("h2",{class:"text-2xl"},"Task Payload",-1)),os={class:"text-xs p-8 border border-[#ccc/80] bg-white/90 mt-4 rounded overflow-auto no-scroll"},as=P(()=>e("div",{class:"basis-[250px] shrink-0 px-6"},[e("h2",{class:"text-3xl"},"Actions"),e("button",{class:"bg-gray-200 text-black/20 cursor-not-allowed mt-4 w-full rounded px-4 py-2"}," Retry "),e("span",{class:"text-xs text-gray-800 mt-2 inline-block"},"Retrying tasks is not available yet.")],-1)),ns={setup(s){const a=M(),t=$(),r=m({id:null,status:"loading"});A(()=>w(this,null,function*(){r.value=yield I({endpoint:`task/${a.params.uuid}`,router:t})}));const o={scheduled:"Scheduled",queued:"Added to the queue",running:"Running",successful:"Successful",error:"An error occurred",failed:"Failed permanently",released:"Released"};return(l,n)=>{const c=S("Popper");return d(),_(k,null,[e("h1",Bt,"Task #"+p(r.value.id),1),h(H,{status:r.value.status,classes:["text-sm"]},null,8,["status"]),e("div",Ft,[e("div",Nt,[e("div",Mt,[e("ol",jt,[(d(!0),_(k,null,j(r.value.events,(i,u)=>(d(),_("li",{class:y(["ml-10 pt-1 mb-6",[`event-${i.status}`]])},[h(Ot,{status:i.status},null,8,["status"]),e("h3",zt,[g(p(o[i.status]||i.status)+" ",1),e("div",null,[i.queue?(d(),_("span",Ht,p(r.value.queue),1)):b("",!0)]),i.scheduled_at?(d(),_("div",Kt,[e("span",Qt," Scheduled: "+p(i.scheduled_at)+" (UTC) ",1)])):b("",!0),i.delay?(d(),_("div",Gt,[e("span",Wt," Delay: "+p(i.delay)+" seconds ",1)])):b("",!0)]),h(c,{content:i.datetime,hover:!0,arrow:!0,placement:"right"},{default:x(()=>[e("time",Jt,[e("span",Xt,p(i.diff),1)])]),_:2},1032,["content"])],2))),256))])])]),e("div",Yt,[r.value.exception?(d(),_(k,{key:0},[Zt,e("pre",es,p(r.value.exception),1)],64)):b("",!0),r.value.payload?(d(),_("div",ts,[ss,e("pre",os,p(r.value.payload),1)])):b("",!0)]),as])],64)}}};var U=T(ns,[["__scopeId","data-v-5df67d4c"]]);const ls=[{name:"home",path:"/",component:ze},{name:"login",path:"/login",component:Te},{name:"recent",path:"/recent",component:Pt,meta:{route:"recent"}},{name:"recent-task",path:"/recent/:uuid",component:U,meta:{route:"recent"}},{name:"queued",path:"/queued",component:Ut,meta:{route:"queued"}},{name:"queued-task",path:"/queued/:uuid",component:U,meta:{route:"queued"}},{name:"failed",path:"/failed",component:Dt,meta:{route:"failed"}},{name:"failed-task",path:"/failed/:uuid",component:U,meta:{route:"failed"}}];let K=null;"CloudTasks"in window&&(K=`/${window.CloudTasks.path}`);const Q=le({history:re(K),routes:ls});Q.beforeEach((s,a,t)=>!localStorage.hasOwnProperty("cloud-tasks-token")&&s.name!=="login"?t({name:"login"}):t());ie(be).use(Q).component("Popper",ce).mount("#app");
diff --git a/dashboard/dist/crossword.png b/dashboard/dist/crossword.png
deleted file mode 100644
index 2f9f1ad..0000000
Binary files a/dashboard/dist/crossword.png and /dev/null differ
diff --git a/dashboard/dist/dot-grid.png b/dashboard/dist/dot-grid.png
deleted file mode 100644
index ebcefe9..0000000
Binary files a/dashboard/dist/dot-grid.png and /dev/null differ
diff --git a/dashboard/dist/index.html b/dashboard/dist/index.html
index 5b5fcee..440a7b8 100644
--- a/dashboard/dist/index.html
+++ b/dashboard/dist/index.html
@@ -5,9 +5,9 @@
Vite App
-
+
-
+
diff --git a/dashboard/dist/manifest.json b/dashboard/dist/manifest.json
index fedb792..53f0594 100644
--- a/dashboard/dist/manifest.json
+++ b/dashboard/dist/manifest.json
@@ -1,13 +1,13 @@
{
"index.html": {
- "file": "assets/index.5a46c6a0.js",
+ "file": "assets/index.ea68d73f.js",
"src": "index.html",
"isEntry": true,
"imports": [
"_vendor.433de25e.js"
],
"css": [
- "assets/index.1002db9a.css"
+ "assets/index.d8eef428.css"
]
},
"_vendor.433de25e.js": {
diff --git a/dashboard/dist/pw_maze_white.png b/dashboard/dist/pw_maze_white.png
deleted file mode 100644
index 6646483..0000000
Binary files a/dashboard/dist/pw_maze_white.png and /dev/null differ
diff --git a/dashboard/src/components/Status.vue b/dashboard/src/components/Status.vue
index e542088..b1beb73 100644
--- a/dashboard/src/components/Status.vue
+++ b/dashboard/src/components/Status.vue
@@ -31,7 +31,7 @@ function ucfirst(input) {
.task-queued, .task-scheduled {
@apply bg-gray-100 text-gray-500
}
-.task-running {
+.task-running, .task-released {
@apply bg-blue-100 text-blue-800
}
diff --git a/dashboard/src/components/Task.vue b/dashboard/src/components/Task.vue
index b8297ea..031dd73 100644
--- a/dashboard/src/components/Task.vue
+++ b/dashboard/src/components/Task.vue
@@ -27,6 +27,7 @@ const titles = {
successful: 'Successful',
error: 'An error occurred',
failed: 'Failed permanently',
+ released: 'Released',
}
@@ -60,6 +61,13 @@ const titles = {
Scheduled: {{ event['scheduled_at'] }} (UTC)
+
+
+ Delay: {{ event['delay'] }} seconds
+
+
- ./tests/ConfigTest.php
- ./tests/TaskHandlerTest.php
- ./tests/CloudTasksApiTest.php
- ./tests/CloudTasksDashboardTest.php
+ ./tests
diff --git a/src/CloudTasksApiFake.php b/src/CloudTasksApiFake.php
index da3b56c..59a046a 100644
--- a/src/CloudTasksApiFake.php
+++ b/src/CloudTasksApiFake.php
@@ -91,4 +91,9 @@ public function assertTaskCreated(Closure $closure): void
Assert::assertTrue($count > 0, 'Task was not created.');
}
+
+ public function assertCreatedTaskCount(int $count): void
+ {
+ Assert::assertCount($count, $this->createdTasks);
+ }
}
diff --git a/src/CloudTasksConnector.php b/src/CloudTasksConnector.php
index 79465fe..db81cd6 100644
--- a/src/CloudTasksConnector.php
+++ b/src/CloudTasksConnector.php
@@ -23,6 +23,6 @@ public function connect(array $config)
};
}
- return new CloudTasksQueue($config, app(CloudTasksClient::class));
+ return new CloudTasksQueue($config, app(CloudTasksClient::class), $config['after_commit'] ?? null);
}
}
diff --git a/src/CloudTasksJob.php b/src/CloudTasksJob.php
index fc83cbe..7dcea27 100644
--- a/src/CloudTasksJob.php
+++ b/src/CloudTasksJob.php
@@ -3,14 +3,21 @@
namespace Stackkit\LaravelGoogleCloudTasksQueue;
use Illuminate\Container\Container;
-use Illuminate\Queue\Jobs\Job as LaravelJob;
use Illuminate\Contracts\Queue\Job as JobContract;
+use Illuminate\Queue\Jobs\Job as LaravelJob;
+use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased;
+use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleasedAfterException;
use function Safe\json_encode;
class CloudTasksJob extends LaravelJob implements JobContract
{
- private array $job;
- private ?int $attempts;
+ /**
+ * The Cloud Tasks raw job payload (request payload).
+ *
+ * @var array
+ */
+ public array $job;
+
private ?int $maxTries;
public ?int $retryUntil = null;
@@ -24,9 +31,14 @@ public function __construct(array $job, CloudTasksQueue $cloudTasksQueue)
$this->job = $job;
$this->container = Container::getInstance();
$this->cloudTasksQueue = $cloudTasksQueue;
- /** @var \stdClass $command */
- $command = unserialize($job['data']['command']);
- $this->queue = $command->queue;
+
+ $command = TaskHandler::getCommandProperties($job['data']['command']);
+ $this->queue = $command['queue'] ?? config('queue.connections.' .config('queue.default') . '.queue');
+ }
+
+ public function job()
+ {
+ return $this->job;
}
public function getJobId(): string
@@ -46,12 +58,12 @@ public function getRawBody(): string
public function attempts(): ?int
{
- return $this->attempts;
+ return $this->job['internal']['attempts'];
}
public function setAttempts(int $attempts): void
{
- $this->attempts = $attempts;
+ $this->job['internal']['attempts'] = $attempts;
}
public function setMaxTries(int $maxTries): void
@@ -95,4 +107,28 @@ public function delete(): void
$this->cloudTasksQueue->delete($this);
}
+
+ public function release($delay = 0)
+ {
+ parent::release();
+
+ $this->cloudTasksQueue->release($this, $delay);
+
+ $properties = TaskHandler::getCommandProperties($this->job['data']['command']);
+ $connection = $properties['connection'] ?? config('queue.default');
+
+ // The package uses the JobReleasedAfterException provided by Laravel to grab
+ // the payload of the released job in tests to easily run and test a released
+ // job. Because the event is only accessible in Laravel 9.x, we create an
+ // identical event to hook into for Laravel versions older than 9.x
+ if (version_compare(app()->version(), '9.0.0', '<')) {
+ if (data_get($this->job, 'internal.errored')) {
+ app('events')->dispatch(new JobReleasedAfterException($connection, $this));
+ }
+ }
+
+ if (! data_get($this->job, 'internal.errored')) {
+ app('events')->dispatch(new JobReleased($connection, $this, $delay));
+ }
+ }
}
diff --git a/src/CloudTasksQueue.php b/src/CloudTasksQueue.php
index cdcbcf4..a77e09c 100644
--- a/src/CloudTasksQueue.php
+++ b/src/CloudTasksQueue.php
@@ -12,8 +12,9 @@
use Illuminate\Contracts\Queue\Queue as QueueContract;
use Illuminate\Queue\Queue as LaravelQueue;
use Illuminate\Support\Str;
-use function Safe\json_encode;
+use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated;
use function Safe\json_decode;
+use function Safe\json_encode;
class CloudTasksQueue extends LaravelQueue implements QueueContract
{
@@ -24,10 +25,11 @@ class CloudTasksQueue extends LaravelQueue implements QueueContract
public array $config;
- public function __construct(array $config, CloudTasksClient $client)
+ public function __construct(array $config, CloudTasksClient $client, $dispatchAfterCommit = false)
{
$this->client = $client;
$this->config = $config;
+ $this->dispatchAfterCommit = $dispatchAfterCommit;
}
/**
@@ -42,6 +44,25 @@ public function size($queue = null)
return 0;
}
+ /**
+ * Fallback method for Laravel 6x and 7x
+ *
+ * @param \Closure|string|object $job
+ * @param string $payload
+ * @param string $queue
+ * @param \DateTimeInterface|\DateInterval|int|null $delay
+ * @param callable $callback
+ * @return mixed
+ */
+ protected function enqueueUsing($job, $payload, $queue, $delay, $callback)
+ {
+ if (method_exists(parent::class, 'enqueueUsing')) {
+ return parent::enqueueUsing($job, $payload, $queue, $delay, $callback);
+ }
+
+ return $callback($payload, $queue, $delay);
+ }
+
/**
* Push a new job onto the queue.
*
@@ -52,9 +73,15 @@ public function size($queue = null)
*/
public function push($job, $data = '', $queue = null)
{
- $this->pushToCloudTasks($queue, $this->createPayload(
- $job, $this->getQueue($queue), $data
- ));
+ return $this->enqueueUsing(
+ $job,
+ $this->createPayload($job, $this->getQueue($queue), $data),
+ $queue,
+ null,
+ function ($payload, $queue) {
+ return $this->pushRaw($payload, $queue);
+ }
+ );
}
/**
@@ -63,11 +90,13 @@ public function push($job, $data = '', $queue = null)
* @param string $payload
* @param string|null $queue
* @param array $options
- * @return void
+ * @return string
*/
public function pushRaw($payload, $queue = null, array $options = [])
{
- $this->pushToCloudTasks($queue, $payload);
+ $delay = ! empty($options['delay']) ? $options['delay'] : 0;
+
+ $this->pushToCloudTasks($queue, $payload, $delay);
}
/**
@@ -81,9 +110,15 @@ public function pushRaw($payload, $queue = null, array $options = [])
*/
public function later($delay, $job, $data = '', $queue = null)
{
- $this->pushToCloudTasks($queue, $this->createPayload(
- $job, $this->getQueue($queue), $data
- ), $delay);
+ return $this->enqueueUsing(
+ $job,
+ $this->createPayload($job, $this->getQueue($queue), $data),
+ $queue,
+ $delay,
+ function ($payload, $queue, $delay) {
+ return $this->pushToCloudTasks($queue, $payload, $delay);
+ }
+ );
}
/**
@@ -92,7 +127,7 @@ public function later($delay, $job, $data = '', $queue = null)
* @param string|null $queue
* @param string $payload
* @param \DateTimeInterface|\DateInterval|int $delay
- * @return void
+ * @return string
*/
protected function pushToCloudTasks($queue, $payload, $delay = 0)
{
@@ -103,16 +138,25 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0)
$httpRequest = $this->createHttpRequest();
$httpRequest->setUrl($this->getHandler());
$httpRequest->setHttpMethod(HttpMethod::POST);
- $httpRequest->setBody(
- // Laravel 7+ jobs have a uuid, but Laravel 6 doesn't have it.
- // Since we are using and expecting the uuid in some places
- // we will add it manually here if it's not present yet.
- $this->withUuid($payload)
- );
+
+ // Laravel 7+ jobs have a uuid, but Laravel 6 doesn't have it.
+ // Since we are using and expecting the uuid in some places
+ // we will add it manually here if it's not present yet.
+ [$payload, $uuid] = $this->withUuid($payload);
+
+ // Since 3.x tasks are released back onto the queue after an exception has
+ // been thrown. This means we lose the native [X-CloudTasks-TaskRetryCount] header
+ // value and need to manually set and update the number of times a task has been attempted.
+ $payload = $this->withAttempts($payload);
+
+ $httpRequest->setBody($payload);
$task = $this->createTask();
$task->setHttpRequest($httpRequest);
+ // The deadline for requests sent to the app. If the app does not respond by
+ // this deadline then the request is cancelled and the attempt is marked as
+ // a failure. Cloud Tasks will retry the task according to the RetryConfig.
if (!empty($this->config['dispatch_deadline'])) {
$task->setDispatchDeadline(new Duration(['seconds' => $this->config['dispatch_deadline']]));
}
@@ -128,9 +172,11 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0)
$createdTask = CloudTasksApi::createTask($queueName, $task);
event((new TaskCreated)->queue($queue)->task($task));
+
+ return $uuid;
}
- private function withUuid(string $payload): string
+ private function withUuid(string $payload): array
{
/** @var array $decoded */
$decoded = json_decode($payload, true);
@@ -139,6 +185,21 @@ private function withUuid(string $payload): string
$decoded['uuid'] = (string) Str::uuid();
}
+ return [
+ json_encode($decoded),
+ $decoded['uuid'],
+ ];
+ }
+
+ private function withAttempts(string $payload): string
+ {
+ /** @var array $decoded */
+ $decoded = json_decode($payload, true);
+
+ if (!isset($decoded['internal']['attempts'])) {
+ $decoded['internal']['attempts'] = 0;
+ }
+
return json_encode($decoded);
}
@@ -179,6 +240,17 @@ public function delete(CloudTasksJob $job): void
CloudTasksApi::deleteTask($taskName);
}
+ public function release(CloudTasksJob $job, int $delay = 0): void
+ {
+ $job->delete();
+
+ $payload = $job->getRawBody();
+
+ $options = ['delay' => $delay];
+
+ $this->pushRaw($payload, $job->getQueue(), $options);
+ }
+
private function createTask(): Task
{
return app(Task::class);
diff --git a/src/CloudTasksServiceProvider.php b/src/CloudTasksServiceProvider.php
index 07b3c6f..d22a281 100644
--- a/src/CloudTasksServiceProvider.php
+++ b/src/CloudTasksServiceProvider.php
@@ -8,6 +8,8 @@
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Support\ServiceProvider as LaravelServiceProvider;
+use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased;
+use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated;
use function Safe\file_get_contents;
use function Safe\json_decode;
@@ -126,7 +128,9 @@ private function registerRoutes(): void
private function registerDashboard(): void
{
- app('events')->listen(TaskCreated::class, function (TaskCreated $event) {
+ $events = $this->app['events'];
+
+ $events->listen(TaskCreated::class, function (TaskCreated $event) {
if (CloudTasks::dashboardDisabled()) {
return;
}
@@ -134,7 +138,7 @@ private function registerDashboard(): void
DashboardService::make()->add($event->queue, $event->task);
});
- app('events')->listen(JobFailed::class, function (JobFailed $event) {
+ $events->listen(JobFailed::class, function (JobFailed $event) {
if (!$event->job instanceof CloudTasksJob) {
return;
}
@@ -147,40 +151,58 @@ private function registerDashboard(): void
);
});
- app('events')->listen(JobProcessing::class, function (JobProcessing $event) {
- if (!CloudTasks::dashboardEnabled()) {
+ $events->listen(JobProcessing::class, function (JobProcessing $event) {
+ if (!$event->job instanceof CloudTasksJob) {
return;
}
- if ($event->job instanceof CloudTasksJob) {
+ if (CloudTasks::dashboardEnabled()) {
DashboardService::make()->markAsRunning($event->job->uuid());
}
});
- app('events')->listen(JobProcessed::class, function (JobProcessed $event) {
- if (!CloudTasks::dashboardEnabled()) {
+ $events->listen(JobProcessed::class, function (JobProcessed $event) {
+ if (!$event->job instanceof CloudTasksJob) {
return;
}
- if ($event->job instanceof CloudTasksJob) {
+ data_set($event->job->job, 'internal.processed', true);
+
+ if (CloudTasks::dashboardEnabled()) {
DashboardService::make()->markAsSuccessful($event->job->uuid());
}
});
- app('events')->listen(JobExceptionOccurred::class, function (JobExceptionOccurred $event) {
- if (!CloudTasks::dashboardEnabled()) {
+ $events->listen(JobExceptionOccurred::class, function (JobExceptionOccurred $event) {
+ if (!$event->job instanceof CloudTasksJob) {
+ return;
+ }
+
+ data_set($event->job->job, 'internal.errored', true);
+
+ if (CloudTasks::dashboardEnabled()) {
+ DashboardService::make()->markAsError($event);
+ }
+ });
+
+ $events->listen(JobFailed::class, function ($event) {
+ if (!$event->job instanceof CloudTasksJob) {
return;
}
- DashboardService::make()->markAsError($event);
+ if (CloudTasks::dashboardEnabled()) {
+ DashboardService::make()->markAsFailed($event);
+ }
});
- app('events')->listen(JobFailed::class, function ($event) {
- if (!CloudTasks::dashboardEnabled()) {
+ $events->listen(JobReleased::class, function (JobReleased $event) {
+ if (!$event->job instanceof CloudTasksJob) {
return;
}
- DashboardService::make()->markAsFailed($event);
+ if (CloudTasks::dashboardEnabled()) {
+ DashboardService::make()->markAsReleased($event);
+ }
});
}
}
diff --git a/src/DashboardService.php b/src/DashboardService.php
index 49e34d2..8800e3d 100644
--- a/src/DashboardService.php
+++ b/src/DashboardService.php
@@ -9,6 +9,7 @@
use Illuminate\Queue\Events\JobExceptionOccurred;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Support\Facades\DB;
+use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased;
use function Safe\json_decode;
class DashboardService
@@ -31,6 +32,12 @@ private function getTaskBody(Task $task): string
public function add(string $queue, Task $task): void
{
+ $uuid = $this->getTaskUuid($task);
+
+ if (StackkitCloudTask::whereTaskUuid($uuid)->exists()) {
+ return;
+ }
+
$metadata = new TaskMetadata();
$metadata->payload = $this->getTaskBody($task);
@@ -51,7 +58,7 @@ public function add(string $queue, Task $task): void
DB::table('stackkit_cloud_tasks')
->insert([
- 'task_uuid' => $this->getTaskUuid($task),
+ 'task_uuid' => $uuid,
'name' => $this->getTaskName($task),
'queue' => $queue,
'payload' => $this->getTaskBody($task),
@@ -79,6 +86,10 @@ public function markAsSuccessful(string $uuid): void
{
$task = StackkitCloudTask::findByUuid($uuid);
+ if ($task->status === 'released') {
+ return;
+ }
+
$task->status = 'successful';
$task->addMetadataEvent([
'status' => $task->status,
@@ -129,6 +140,23 @@ public function markAsFailed(JobFailed $event): void
$task->save();
}
+ public function markAsReleased(JobReleased $event): void
+ {
+ /** @var CloudTasksJob $job */
+ $job = $event->job;
+
+ $task = StackkitCloudTask::findByUuid($job->uuid());
+
+ $task->status = 'released';
+ $task->addMetadataEvent([
+ 'status' => $task->status,
+ 'datetime' => now()->utc()->toDateTimeString(),
+ 'delay' => $event->delay,
+ ]);
+
+ $task->save();
+ }
+
private function getTaskName(Task $task): string
{
/** @var array $decode */
diff --git a/src/Events/JobReleased.php b/src/Events/JobReleased.php
new file mode 100644
index 0000000..614e45b
--- /dev/null
+++ b/src/Events/JobReleased.php
@@ -0,0 +1,46 @@
+job = $job;
+ $this->connectionName = $connectionName;
+ $this->delay = $delay;
+ }
+}
diff --git a/src/Events/JobReleasedAfterException.php b/src/Events/JobReleasedAfterException.php
new file mode 100644
index 0000000..603fbe3
--- /dev/null
+++ b/src/Events/JobReleasedAfterException.php
@@ -0,0 +1,37 @@
+job = $job;
+ $this->connectionName = $connectionName;
+ }
+}
diff --git a/src/TaskCreated.php b/src/Events/TaskCreated.php
similarity index 86%
rename from src/TaskCreated.php
rename to src/Events/TaskCreated.php
index 96f0f45..a05f415 100644
--- a/src/TaskCreated.php
+++ b/src/Events/TaskCreated.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Stackkit\LaravelGoogleCloudTasksQueue;
+namespace Stackkit\LaravelGoogleCloudTasksQueue\Events;
use Google\Cloud\Tasks\V2\Task;
diff --git a/src/LogFake.php b/src/LogFake.php
index d9a32c6..e4e86ff 100644
--- a/src/LogFake.php
+++ b/src/LogFake.php
@@ -68,4 +68,12 @@ public function assertLogged(string $message): void
{
PHPUnit::assertTrue(in_array($message, $this->loggedMessages), 'The message [' . $message . '] was not logged.');
}
+
+ public function assertNotLogged(string $message): void
+ {
+ PHPUnit::assertTrue(
+ ! in_array($message, $this->loggedMessages),
+ 'The message [' . $message . '] was logged.'
+ );
+ }
}
diff --git a/src/TaskHandler.php b/src/TaskHandler.php
index 9c1ba50..a535f30 100644
--- a/src/TaskHandler.php
+++ b/src/TaskHandler.php
@@ -5,8 +5,13 @@
use Google\Cloud\Tasks\V2\CloudTasksClient;
use Google\Cloud\Tasks\V2\RetryConfig;
use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Queue\Jobs\Job;
+use Illuminate\Queue\QueueManager;
use Illuminate\Queue\WorkerOptions;
+use Illuminate\Support\Str;
+use Illuminate\Validation\ValidationException;
+use Safe\Exceptions\JsonException;
use stdClass;
use UnexpectedValueException;
use function Safe\json_decode;
@@ -38,9 +43,9 @@ public function __construct(CloudTasksClient $client)
$this->client = $client;
}
- public function handle(?array $task = null): void
+ public function handle(?string $task = null): void
{
- $task = $task ?: $this->captureTask();
+ $task = $this->captureTask($task);
$this->loadQueueConnectionConfiguration($task);
@@ -51,17 +56,59 @@ public function handle(?array $task = null): void
$this->handleTask($task);
}
+ /**
+ * @param string|array|null $task
+ * @return array
+ * @throws JsonException
+ */
+ private function captureTask($task): array
+ {
+ $task = $task ?: (string) (request()->getContent());
+
+ try {
+ $array = json_decode($task, true);
+ } catch (JsonException $e) {
+ $array = [];
+ }
+
+ $validator = validator([
+ 'json' => $task,
+ 'task' => $array,
+ 'name_header' => request()->header('X-CloudTasks-Taskname'),
+ ], [
+ 'json' => 'required|json',
+ 'task' => 'required|array',
+ 'task.data' => 'required|array',
+ 'name_header' => 'required|string',
+ ]);
+
+ try {
+ $validator->validate();
+ } catch (ValidationException $e) {
+ if (config('app.debug')) {
+ throw $e;
+ } else {
+ abort(404);
+ }
+ }
+
+ return json_decode($task, true);
+ }
+
private function loadQueueConnectionConfiguration(array $task): void
{
/**
* @var stdClass $command
*/
- $command = unserialize($task['data']['command']);
+ $command = self::getCommandProperties($task['data']['command']);
$connection = $command->connection ?? config('queue.default');
- $this->config = array_merge(
- (array) config("queue.connections.{$connection}"),
- ['connection' => $connection]
- );
+ $baseConfig = config('queue.connections.' . $connection);
+ $config = (new CloudTasksConnector())->connect($baseConfig)->config;
+
+ // The connection name from the config may not be the actual connection name
+ $config['connection'] = $connection;
+
+ $this->config = $config;
}
private function setQueue(): void
@@ -69,33 +116,22 @@ private function setQueue(): void
$this->queue = new CloudTasksQueue($this->config, $this->client);
}
- /**
- * @throws CloudTasksException
- */
- private function captureTask(): array
- {
- $input = (string) (request()->getContent());
-
- if (!$input) {
- throw new CloudTasksException('Could not read incoming task');
- }
-
- $task = json_decode($input, true);
-
- if (!is_array($task)) {
- throw new CloudTasksException('Could not decode incoming task');
- }
-
- return $task;
- }
-
private function handleTask(array $task): void
{
$job = new CloudTasksJob($task, $this->queue);
$this->loadQueueRetryConfig($job);
- $job->setAttempts((int) request()->header('X-CloudTasks-TaskRetryCount'));
+ // If the task has a [X-CloudTasks-TaskRetryCount] header higher than 0, then
+ // we know the job was created using an earlier version of the package. This
+ // job does not have the attempts tracked internally yet.
+ $taskRetryCountHeader = request()->header('X-CloudTasks-TaskRetryCount');
+ if ($taskRetryCountHeader && (int) $taskRetryCountHeader > 0) {
+ $job->setAttempts((int) $taskRetryCountHeader);
+ } else {
+ $job->setAttempts($task['internal']['attempts']);
+ }
+
$job->setMaxTries($this->retryConfig->getMaxAttempts());
// If the job is being attempted again we also check if a
@@ -120,7 +156,7 @@ private function handleTask(array $task): void
$job->setAttempts($job->attempts() + 1);
- app('queue.worker')->process($this->config['connection'], $job, new WorkerOptions());
+ app('queue.worker')->process($this->config['connection'], $job, $this->getWorkerOptions());
}
private function loadQueueRetryConfig(CloudTasksJob $job): void
@@ -131,4 +167,28 @@ private function loadQueueRetryConfig(CloudTasksJob $job): void
$this->retryConfig = CloudTasksApi::getRetryConfig($queueName);
}
+
+ public static function getCommandProperties(string $command): array
+ {
+ if (Str::startsWith($command, 'O:')) {
+ return (array) unserialize($command, ['allowed_classes' => false]);
+ }
+
+ if (app()->bound(Encrypter::class)) {
+ return (array) unserialize(app(Encrypter::class)->decrypt($command), ['allowed_classes' => ['Illuminate\Support\Carbon']]);
+ }
+
+ return [];
+ }
+
+ public function getWorkerOptions(): WorkerOptions
+ {
+ $options = new WorkerOptions();
+
+ $prop = version_compare(app()->version(), '8.0.0', '<') ? 'delay' : 'backoff';
+
+ $options->$prop = $this->config['backoff'] ?? 0;
+
+ return $options;
+ }
}
diff --git a/tests/CloudTasksDashboardTest.php b/tests/CloudTasksDashboardTest.php
index 4ca8578..6cb7b99 100644
--- a/tests/CloudTasksDashboardTest.php
+++ b/tests/CloudTasksDashboardTest.php
@@ -10,6 +10,7 @@
use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator;
use Stackkit\LaravelGoogleCloudTasksQueue\StackkitCloudTask;
use Tests\Support\FailingJob;
+use Tests\Support\JobThatWillBeReleased;
use Tests\Support\SimpleJob;
class CloudTasksDashboardTest extends TestCase
@@ -258,8 +259,7 @@ public function when_a_job_is_dispatched_it_will_be_added_to_the_dashboard()
'status' => 'queued',
'name' => SimpleJob::class,
]);
- $payload = \Safe\json_decode($task->getMetadata()['payload'], true);
- $this->assertSame($payload, $job->payload);
+ $this->assertSame($task->getMetadata()['payload'], $job->payload);
}
/**
@@ -396,9 +396,9 @@ public function when_a_job_fails_it_will_be_updated_in_the_dashboard()
);
$job = $this->dispatch(new FailingJob());
- $job->run();
- $job->run();
- $job->run();
+ $releasedJob = $job->runAndGetReleasedJob();
+ $releasedJob = $releasedJob->runAndGetReleasedJob();
+ $releasedJob->run();
// Assert
$task = StackkitCloudTask::firstOrFail();
@@ -414,6 +414,68 @@ public function when_a_job_fails_it_will_be_updated_in_the_dashboard()
);
}
+ /**
+ * @test
+ */
+ public function when_a_job_is_released_it_will_be_updated_in_the_dashboard()
+ {
+ // Arrange
+ \Illuminate\Support\Carbon::setTestNow(now());
+ CloudTasksApi::fake();
+ OpenIdVerificator::fake();
+ CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn(
+ (new RetryConfig())->setMaxAttempts(3)
+ );
+
+ $this->dispatch(new JobThatWillBeReleased())->run();
+
+ // Assert
+ $task = StackkitCloudTask::firstOrFail();
+ $events = $task->getEvents();
+
+ $this->assertCount(3, $events);
+ $this->assertEquals(
+ [
+ 'status' => 'released',
+ 'datetime' => now()->toDateTimeString(),
+ 'diff' => '1 second ago',
+ 'delay' => 0,
+ ],
+ $events[2]
+ );
+ }
+
+ /**
+ * @test
+ */
+ public function job_release_delay_is_added_to_the_metadata()
+ {
+ // Arrange
+ \Illuminate\Support\Carbon::setTestNow(now());
+ CloudTasksApi::fake();
+ OpenIdVerificator::fake();
+ CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn(
+ (new RetryConfig())->setMaxAttempts(3)
+ );
+
+ $this->dispatch(new JobThatWillBeReleased(15))->run();
+
+ // Assert
+ $task = StackkitCloudTask::firstOrFail();
+ $events = $task->getEvents();
+
+ $this->assertCount(3, $events);
+ $this->assertEquals(
+ [
+ 'status' => 'released',
+ 'datetime' => now()->toDateTimeString(),
+ 'diff' => '1 second ago',
+ 'delay' => 15,
+ ],
+ $events[2]
+ );
+ }
+
/**
* @test
*/
@@ -425,10 +487,17 @@ public function test_publish()
// Act & Assert
$expectedPublishBase = dirname(__DIR__);
- $this->artisan('vendor:publish --tag=cloud-tasks --force')
- ->expectsOutput('Copied File [' . $expectedPublishBase . '/config/cloud-tasks.php] To [/config/cloud-tasks.php]')
- ->expectsOutput('Copied Directory [' . $expectedPublishBase . '/dashboard/dist] To [/public/vendor/cloud-tasks]')
- ->expectsOutput('Publishing complete.');
+ if (version_compare(app()->version(), '9.0.0', '>=')) {
+ $this->artisan('vendor:publish --tag=cloud-tasks --force')
+ ->expectsOutputToContain('Publishing [cloud-tasks] assets.')
+ ->expectsOutputToContain('Copying file [' . $expectedPublishBase . '/config/cloud-tasks.php] to [config/cloud-tasks.php]')
+ ->expectsOutputToContain('Copying directory [' . $expectedPublishBase . '/dashboard/dist] to [public/vendor/cloud-tasks]');
+ } else {
+ $this->artisan('vendor:publish --tag=cloud-tasks --force')
+ ->expectsOutput('Copied File [' . $expectedPublishBase . '/config/cloud-tasks.php] To [/config/cloud-tasks.php]')
+ ->expectsOutput('Copied Directory [' . $expectedPublishBase . '/dashboard/dist] To [/public/vendor/cloud-tasks]')
+ ->expectsOutput('Publishing complete.');
+ }
}
/**
diff --git a/tests/QueueTest.php b/tests/QueueTest.php
index 0a89bda..4a019b9 100644
--- a/tests/QueueTest.php
+++ b/tests/QueueTest.php
@@ -5,10 +5,27 @@
namespace Tests;
use Google\Cloud\Tasks\V2\HttpMethod;
+use Google\Cloud\Tasks\V2\RetryConfig;
use Google\Cloud\Tasks\V2\Task;
+use Illuminate\Queue\Events\JobProcessed;
+use Illuminate\Queue\Events\JobProcessing;
+use Illuminate\Queue\Events\JobQueued;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Queue;
use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksApi;
+use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased;
+use Stackkit\LaravelGoogleCloudTasksQueue\LogFake;
+use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator;
+use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler;
use Tests\Support\FailingJob;
+use Tests\Support\FailingJobWithExponentialBackoff;
+use Tests\Support\JobThatWillBeReleased;
use Tests\Support\SimpleJob;
+use Tests\Support\User;
+use Tests\Support\UserJob;
class QueueTest extends TestCase
{
@@ -137,20 +154,328 @@ public function it_posts_the_task_the_correct_queue()
// Assert
CloudTasksApi::assertTaskCreated(function (Task $task, string $queueName): bool {
$decoded = json_decode($task->getHttpRequest()->getBody(), true);
- $command = unserialize($decoded['data']['command']);
+ $command = TaskHandler::getCommandProperties($decoded['data']['command']);
return $decoded['displayName'] === SimpleJob::class
- && $command->queue === null
+ && ($command['queue'] ?? null) === null
&& $queueName === 'projects/my-test-project/locations/europe-west6/queues/barbequeue';
});
CloudTasksApi::assertTaskCreated(function (Task $task, string $queueName): bool {
$decoded = json_decode($task->getHttpRequest()->getBody(), true);
- $command = unserialize($decoded['data']['command']);
+ $command = TaskHandler::getCommandProperties($decoded['data']['command']);
return $decoded['displayName'] === FailingJob::class
- && $command->queue === 'my-special-queue'
+ && $command['queue'] === 'my-special-queue'
&& $queueName === 'projects/my-test-project/locations/europe-west6/queues/my-special-queue';
});
}
+
+ /**
+ * @test
+ */
+ public function it_can_dispatch_after_commit_inline()
+ {
+ if (version_compare(app()->version(), '8.0.0', '<')) {
+ $this->markTestSkipped('Not supported by Laravel 7.x and below.');
+ }
+
+ // Arrange
+ CloudTasksApi::fake();
+ Event::fake();
+
+ // Act & Assert
+ Event::assertNotDispatched(JobQueued::class);
+ DB::beginTransaction();
+ SimpleJob::dispatch()->afterCommit();
+ Event::assertNotDispatched(JobQueued::class);
+ while (DB::transactionLevel() !== 0) {
+ DB::commit();
+ }
+ Event::assertDispatched(JobQueued::class, function (JobQueued $event) {
+ return $event->job instanceof SimpleJob;
+ });
+ }
+
+ /**
+ * @test
+ */
+ public function it_can_dispatch_after_commit_through_config()
+ {
+ if (version_compare(app()->version(), '8.0.0', '<')) {
+ $this->markTestSkipped('Not supported by Laravel 7.x and below.');
+ }
+
+ // Arrange
+ CloudTasksApi::fake();
+ Event::fake();
+ $this->setConfigValue('after_commit', true);
+
+ // Act & Assert
+ Event::assertNotDispatched(JobQueued::class);
+ DB::beginTransaction();
+ SimpleJob::dispatch();
+ Event::assertNotDispatched(JobQueued::class);
+ while (DB::transactionLevel() !== 0) {
+ DB::commit();
+ }
+ Event::assertDispatched(JobQueued::class, function (JobQueued $event) {
+ return $event->job instanceof SimpleJob;
+ });
+ }
+
+ /**
+ * @test
+ */
+ public function jobs_can_be_released()
+ {
+ // Arrange
+ CloudTasksApi::fake();
+ OpenIdVerificator::fake();
+ Event::fake([
+ $this->getJobReleasedAfterExceptionEvent(),
+ JobReleased::class,
+ ]);
+
+ // Act
+ $this->dispatch(new JobThatWillBeReleased())->run();
+
+ // Assert
+ Event::assertNotDispatched($this->getJobReleasedAfterExceptionEvent());
+ CloudTasksApi::assertDeletedTaskCount(1);
+ $releasedJob = null;
+ Event::assertDispatched(JobReleased::class, function (JobReleased $event) use (&$releasedJob) {
+ $releasedJob = $event->job;
+ return true;
+ });
+ CloudTasksApi::assertTaskCreated(function (Task $task) {
+ $body = $task->getHttpRequest()->getBody();
+ $decoded = json_decode($body, true);
+ return $decoded['data']['commandName'] === 'Tests\\Support\\JobThatWillBeReleased'
+ && $decoded['internal']['attempts'] === 1;
+ });
+
+ $this->runFromPayload($releasedJob->getRawBody());
+
+ CloudTasksApi::assertDeletedTaskCount(2);
+ CloudTasksApi::assertTaskCreated(function (Task $task) {
+ $body = $task->getHttpRequest()->getBody();
+ $decoded = json_decode($body, true);
+ return $decoded['data']['commandName'] === 'Tests\\Support\\JobThatWillBeReleased'
+ && $decoded['internal']['attempts'] === 2;
+ });
+ }
+
+ /**
+ * @test
+ */
+ public function jobs_can_be_released_with_a_delay()
+ {
+ // Arrange
+ CloudTasksApi::fake();
+ OpenIdVerificator::fake();
+ Event::fake([
+ $this->getJobReleasedAfterExceptionEvent(),
+ JobReleased::class,
+ ]);
+ Carbon::setTestNow(now()->addDay());
+
+ // Act
+ $this->dispatch(new JobThatWillBeReleased(15))->run();
+
+ // Assert
+ CloudTasksApi::assertTaskCreated(function (Task $task) {
+ $body = $task->getHttpRequest()->getBody();
+ $decoded = json_decode($body, true);
+
+ $scheduleTime = $task->getScheduleTime() ? $task->getScheduleTime()->getSeconds() : null;
+
+ return $decoded['data']['commandName'] === 'Tests\\Support\\JobThatWillBeReleased'
+ && $decoded['internal']['attempts'] === 1
+ && $scheduleTime === now()->getTimestamp() + 15;
+ });
+ }
+
+ /** @test */
+ public function test_default_backoff()
+ {
+ // Arrange
+ CloudTasksApi::fake();
+ OpenIdVerificator::fake();
+ Event::fake($this->getJobReleasedAfterExceptionEvent());
+
+ // Act
+ $this->dispatch(new FailingJob())->run();
+
+ // Assert
+ CloudTasksApi::assertTaskCreated(function (Task $task) {
+ return is_null($task->getScheduleTime());
+ });
+ }
+
+ /** @test */
+ public function test_backoff_from_queue_config()
+ {
+ // Arrange
+ Carbon::setTestNow(now()->addDay());
+ $this->setConfigValue('backoff', 123);
+ CloudTasksApi::fake();
+ OpenIdVerificator::fake();
+ Event::fake($this->getJobReleasedAfterExceptionEvent());
+
+ // Act
+ $this->dispatch(new FailingJob())->run();
+
+ // Assert
+ CloudTasksApi::assertTaskCreated(function (Task $task) {
+ return $task->getScheduleTime()
+ && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 123;
+ });
+ }
+
+ /** @test */
+ public function test_backoff_from_job()
+ {
+ // Arrange
+ Carbon::setTestNow(now()->addDay());
+ CloudTasksApi::fake();
+ OpenIdVerificator::fake();
+ Event::fake($this->getJobReleasedAfterExceptionEvent());
+
+ // Act
+ $failingJob = new FailingJob();
+ $prop = version_compare(app()->version(), '8.0.0', '<') ? 'delay' : 'backoff';
+ $failingJob->$prop = 123;
+ $this->dispatch($failingJob)->run();
+
+ // Assert
+ CloudTasksApi::assertTaskCreated(function (Task $task) {
+ return $task->getScheduleTime()
+ && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 123;
+ });
+ }
+
+ /** @test */
+ public function test_exponential_backoff_from_job_method()
+ {
+ if (version_compare(app()->version(), '8.0.0', '<')) {
+ $this->markTestSkipped('Not supported by Laravel 7.x and below.');
+ }
+
+ // Arrange
+ Carbon::setTestNow(now()->addDay());
+ CloudTasksApi::fake();
+ OpenIdVerificator::fake();
+
+ // Act
+ $releasedJob = $this->dispatch(new FailingJobWithExponentialBackoff())
+ ->runAndGetReleasedJob();
+ $releasedJob = $releasedJob->runAndGetReleasedJob();
+ $releasedJob->run();
+
+ // Assert
+ CloudTasksApi::assertTaskCreated(function (Task $task) {
+ return $task->getScheduleTime()
+ && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 50;
+ });
+ CloudTasksApi::assertTaskCreated(function (Task $task) {
+ return $task->getScheduleTime()
+ && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 60;
+ });
+ CloudTasksApi::assertTaskCreated(function (Task $task) {
+ return $task->getScheduleTime()
+ && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 70;
+ });
+ }
+
+ /** @test */
+ public function test_failing_method_on_job()
+ {
+ // Arrange
+ CloudTasksApi::fake();
+ CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn(
+ (new RetryConfig())->setMaxAttempts(1)
+ );
+
+ OpenIdVerificator::fake();
+ Log::swap(new LogFake());
+
+ // Act
+ $this->dispatch(new FailingJob())->run();
+
+ // Assert
+ Log::assertLogged('FailingJob:failed');
+ }
+
+ /** @test */
+ public function test_queue_before_and_after_hooks()
+ {
+ // Arrange
+ CloudTasksApi::fake();
+ OpenIdVerificator::fake();
+ Log::swap(new LogFake());
+
+ // Act
+ Queue::before(function (JobProcessing $event) {
+ logger('Queue::before:' . $event->job->payload()['data']['commandName']);
+ });
+ Queue::after(function (JobProcessed $event) {
+ logger('Queue::after:' . $event->job->payload()['data']['commandName']);
+ });
+ $this->dispatch(new SimpleJob())->run();
+
+ // Assert
+ Log::assertLogged('Queue::before:Tests\Support\SimpleJob');
+ Log::assertLogged('Queue::after:Tests\Support\SimpleJob');
+ }
+
+ /** @test */
+ public function test_queue_looping_hook_not_supported_with_this_package()
+ {
+ // Arrange
+ CloudTasksApi::fake();
+ OpenIdVerificator::fake();
+ Log::swap(new LogFake());
+
+ // Act
+ Queue::looping(function () {
+ logger('Queue::looping');
+ });
+ $this->dispatch(new SimpleJob())->run();
+
+ // Assert
+ Log::assertNotLogged('Queue::looping');
+ }
+
+ /** @test */
+ public function test_ignoring_jobs_with_deleted_models()
+ {
+ // Arrange
+ CloudTasksApi::fake();
+ OpenIdVerificator::fake();
+ Log::swap(new LogFake());
+
+ $user1 = User::create([
+ 'name' => 'John',
+ 'email' => 'johndoe@example.com',
+ 'password' => bcrypt('test'),
+ ]);
+
+ $user2 = User::create([
+ 'name' => 'Jane',
+ 'email' => 'janedoe@example.com',
+ 'password' => bcrypt('test'),
+ ]);
+
+ // Act
+ $this->dispatch(new UserJob($user1))->runWithoutExceptionHandler();
+
+ $job = $this->dispatch(new UserJob($user2));
+ $user2->delete();
+ $job->runWithoutExceptionHandler();
+
+ // Act
+ Log::assertLogged('UserJob:John');
+ CloudTasksApi::assertTaskDeleted($job->task->getName());
+ }
}
diff --git a/tests/Support/EncryptedJob.php b/tests/Support/EncryptedJob.php
new file mode 100644
index 0000000..8f8e4ff
--- /dev/null
+++ b/tests/Support/EncryptedJob.php
@@ -0,0 +1,20 @@
+releaseDelay = $releaseDelay;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ logger('JobThatWillBeReleased:beforeRelease');
+ $this->release($this->releaseDelay);
+ logger('JobThatWillBeReleased:afterRelease');
+ }
+}
diff --git a/tests/Support/User.php b/tests/Support/User.php
new file mode 100644
index 0000000..7ffec22
--- /dev/null
+++ b/tests/Support/User.php
@@ -0,0 +1,12 @@
+user = $user;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ logger('UserJob:' . $this->user->name);
+ }
+}
diff --git a/tests/TaskHandlerTest.php b/tests/TaskHandlerTest.php
index b70331f..36a2d13 100644
--- a/tests/TaskHandlerTest.php
+++ b/tests/TaskHandlerTest.php
@@ -5,21 +5,17 @@
use Firebase\JWT\ExpiredException;
use Google\Cloud\Tasks\V2\RetryConfig;
use Google\Protobuf\Duration;
-use Illuminate\Database\Eloquent\Model;
-use Illuminate\Queue\Events\JobExceptionOccurred;
-use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
-use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
-use Illuminate\Support\Facades\Queue;
use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksApi;
use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksException;
-use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksJob;
use Stackkit\LaravelGoogleCloudTasksQueue\LogFake;
use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator;
use Stackkit\LaravelGoogleCloudTasksQueue\StackkitCloudTask;
+use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler;
+use Tests\Support\EncryptedJob;
use Tests\Support\FailingJob;
use Tests\Support\SimpleJob;
use UnexpectedValueException;
@@ -33,6 +29,115 @@ protected function setUp(): void
CloudTasksApi::fake();
}
+ /**
+ * @test
+ * @testWith [true]
+ * [false]
+ */
+ public function it_returns_responses_for_empty_payloads($debug)
+ {
+ // Arrange
+ config()->set('app.debug', $debug);
+
+ // Act
+ $response = $this->postJson(action([TaskHandler::class, 'handle']));
+
+ // Assert
+ if ($debug) {
+ $response->assertJsonValidationErrors('task');
+ } else {
+ $response->assertNotFound();
+ }
+ }
+
+ /**
+ * @test
+ * @testWith [true]
+ * [false]
+ */
+ public function it_returns_responses_for_invalid_json($debug)
+ {
+ // Arrange
+ config()->set('app.debug', $debug);
+
+ // Act
+ $response = $this->call(
+ 'POST',
+ action([TaskHandler::class, 'handle']),
+ [],
+ [],
+ [],
+ [
+ 'HTTP_ACCEPT' => 'application/json',
+ ],
+ 'test',
+ );
+
+ // Assert
+ if ($debug) {
+ $response->assertJsonValidationErrors('task');
+ $this->assertEquals('The json must be a valid JSON string.', $response->json('errors.json.0'));
+ } else {
+ $response->assertNotFound();
+ }
+ }
+
+ /**
+ * @test
+ * @testWith ["{\"invalid\": \"data\"}", "The task.data field is required."]
+ * ["{\"data\": \"\"}", "The task.data field is required."]
+ * ["{\"data\": \"test\"}", "The task.data must be an array."]
+ */
+ public function it_returns_responses_for_invalid_payloads(string $payload, string $expectedMessage)
+ {
+ // Arrange
+
+ // Act
+ $response = $this->call(
+ 'POST',
+ action([TaskHandler::class, 'handle']),
+ [],
+ [],
+ [],
+ [
+ 'HTTP_ACCEPT' => 'application/json',
+ ],
+ $payload,
+ );
+
+ // Assert
+ $response->assertJsonValidationErrors('task.data');
+ $this->assertEquals($expectedMessage, $response->json(['errors', 'task.data', 0]));
+ }
+
+ /**
+ * @test
+ * @testWith [true]
+ * [false]
+ */
+ public function it_validates_headers(bool $withHeaders)
+ {
+ // Arrange
+ $this->withExceptionHandling();
+
+ // Act
+ $response = $this->postJson(
+ action([TaskHandler::class, 'handle']),
+ [],
+ $withHeaders
+ ? [
+ 'X-CloudTasks-Taskname' => 'MyTask',
+ ] : []
+ );
+
+ // Assert
+ if ($withHeaders) {
+ $response->assertJsonMissingValidationErrors('name_header');
+ } else {
+ $response->assertJsonValidationErrors('name_header');
+ }
+ }
+
/**
* @test
*/
@@ -132,13 +237,13 @@ public function after_max_attempts_it_will_log_to_failed_table()
// Act & Assert
$this->assertDatabaseCount('failed_jobs', 0);
- $job->run();
+ $releasedJob = $job->runAndGetReleasedJob();
$this->assertDatabaseCount('failed_jobs', 0);
- $job->run();
+ $releasedJob = $releasedJob->runAndGetReleasedJob();
$this->assertDatabaseCount('failed_jobs', 0);
- $job->run();
+ $releasedJob->run();
$this->assertDatabaseCount('failed_jobs', 1);
}
@@ -157,18 +262,18 @@ public function after_max_attempts_it_will_delete_the_task()
$job = $this->dispatch(new FailingJob());
// Act & Assert
- $job->run();
- CloudTasksApi::assertDeletedTaskCount(0);
- CloudTasksApi::assertTaskNotDeleted($job->task->getName());
+ $releasedJob = $job->runAndGetReleasedJob();
+ CloudTasksApi::assertDeletedTaskCount(1);
+ CloudTasksApi::assertTaskDeleted($job->task->getName());
$this->assertDatabaseCount('failed_jobs', 0);
- $job->run();
- CloudTasksApi::assertDeletedTaskCount(0);
- CloudTasksApi::assertTaskNotDeleted($job->task->getName());
+ $releasedJob = $releasedJob->runAndGetReleasedJob();
+ CloudTasksApi::assertDeletedTaskCount(2);
+ CloudTasksApi::assertTaskDeleted($job->task->getName());
$this->assertDatabaseCount('failed_jobs', 0);
- $job->run();
- CloudTasksApi::assertDeletedTaskCount(1);
+ $releasedJob->run();
+ CloudTasksApi::assertDeletedTaskCount(3);
CloudTasksApi::assertTaskDeleted($job->task->getName());
$this->assertDatabaseCount('failed_jobs', 1);
}
@@ -187,19 +292,19 @@ public function after_max_retry_until_it_will_log_to_failed_table_and_delete_the
$job = $this->dispatch(new FailingJob());
// Act
- $job->run();
+ $releasedJob = $job->runAndGetReleasedJob();
// Assert
- CloudTasksApi::assertDeletedTaskCount(0);
- CloudTasksApi::assertTaskNotDeleted($job->task->getName());
+ CloudTasksApi::assertDeletedTaskCount(1);
+ CloudTasksApi::assertTaskDeleted($job->task->getName());
$this->assertDatabaseCount('failed_jobs', 0);
// Act
CloudTasksApi::partialMock()->shouldReceive('getRetryUntilTimestamp')->andReturn(1);
- $job->run();
+ $releasedJob->run();
// Assert
- CloudTasksApi::assertDeletedTaskCount(1);
+ CloudTasksApi::assertDeletedTaskCount(2);
CloudTasksApi::assertTaskDeleted($job->task->getName());
$this->assertDatabaseCount('failed_jobs', 1);
}
@@ -220,8 +325,8 @@ public function test_unlimited_max_attempts()
$job = $this->dispatch(new FailingJob());
foreach (range(1, 50) as $attempt) {
$job->run();
- CloudTasksApi::assertDeletedTaskCount(0);
- CloudTasksApi::assertTaskNotDeleted($job->task->getName());
+ CloudTasksApi::assertDeletedTaskCount($attempt);
+ CloudTasksApi::assertTaskDeleted($job->task->getName());
$this->assertDatabaseCount('failed_jobs', 0);
}
}
@@ -246,15 +351,15 @@ public function test_max_attempts_in_combination_with_retry_until()
$job = $this->dispatch(new FailingJob());
// Act & Assert
- $job->run();
- $job->run();
+ $releasedJob = $job->runAndGetReleasedJob();
+ $releasedJob = $releasedJob->runAndGetReleasedJob();
# After 2 attempts both Laravel versions should report the same: 2 errors and 0 failures.
- $task = StackkitCloudTask::whereTaskUuid($job->payload['uuid'])->firstOrFail();
+ $task = StackkitCloudTask::whereTaskUuid($job->payloadAsArray('uuid'))->firstOrFail();
$this->assertEquals(2, $task->getNumberOfAttempts());
$this->assertEquals('error', $task->status);
- $job->run();
+ $releasedJob->run();
# Max attempts was reached
# Laravel 5, 6, 7: fail because max attempts was reached
@@ -268,8 +373,108 @@ public function test_max_attempts_in_combination_with_retry_until()
}
CloudTasksApi::shouldReceive('getRetryUntilTimestamp')->andReturn(time() - 1);
- $job->run();
+ $releasedJob->run();
$this->assertEquals('failed', $task->fresh()->status);
}
+
+ /**
+ * @test
+ */
+ public function it_can_handle_encrypted_jobs()
+ {
+ if (version_compare(app()->version(), '8.0.0', '<')) {
+ $this->markTestSkipped('Not supported by Laravel 7.x and below.');
+ }
+
+ // Arrange
+ OpenIdVerificator::fake();
+ Log::swap(new LogFake());
+
+ // Act
+ $job = $this->dispatch(new EncryptedJob());
+ $job->run();
+
+ // Assert
+ $this->assertStringContainsString(
+ 'O:26:"Tests\Support\EncryptedJob"',
+ decrypt($job->payloadAsArray('data.command')),
+ );
+
+ Log::assertLogged('EncryptedJob:success');
+ }
+
+ /**
+ * @test
+ */
+ public function failing_jobs_are_released()
+ {
+ // Arrange
+ OpenIdVerificator::fake();
+ CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn(
+ (new RetryConfig())->setMaxAttempts(3)
+ );
+ Event::fake($this->getJobReleasedAfterExceptionEvent());
+
+ // Act
+ $job = $this->dispatch(new FailingJob());
+
+ CloudTasksApi::assertDeletedTaskCount(0);
+ CloudTasksApi::assertCreatedTaskCount(1);
+ CloudTasksApi::assertTaskNotDeleted($job->task->getName());
+
+ $job->run();
+
+ CloudTasksApi::assertDeletedTaskCount(1);
+ CloudTasksApi::assertCreatedTaskCount(2);
+ CloudTasksApi::assertTaskDeleted($job->task->getName());
+ Event::assertDispatched($this->getJobReleasedAfterExceptionEvent(), function ($event) {
+ return $event->job->attempts() === 1;
+ });
+ }
+
+ /**
+ * @test
+ */
+ public function attempts_are_tracked_internally()
+ {
+ // Arrange
+ OpenIdVerificator::fake();
+ Event::fake($this->getJobReleasedAfterExceptionEvent());
+
+ // Act & Assert
+ $job = $this->dispatch(new FailingJob());
+ $job->run();
+ $releasedJob = null;
+
+ Event::assertDispatched($this->getJobReleasedAfterExceptionEvent(), function ($event) use (&$releasedJob) {
+ $releasedJob = $event->job->getRawBody();
+ return $event->job->attempts() === 1;
+ });
+
+ $this->runFromPayload($releasedJob);
+
+ Event::assertDispatched($this->getJobReleasedAfterExceptionEvent(), function ($event) {
+ return $event->job->attempts() === 2;
+ });
+ }
+
+ /**
+ * @test
+ */
+ public function attempts_are_copied_from_x_header()
+ {
+ // Arrange
+ OpenIdVerificator::fake();
+ Event::fake($this->getJobReleasedAfterExceptionEvent());
+
+ // Act & Assert
+ $job = $this->dispatch(new FailingJob());
+ request()->headers->set('X-CloudTasks-TaskRetryCount', 6);
+ $job->run();
+
+ Event::assertDispatched($this->getJobReleasedAfterExceptionEvent(), function ($event) {
+ return $event->job->attempts() === 7;
+ });
+ }
}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 17e70f3..9110869 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -5,15 +5,14 @@
use Closure;
use Firebase\JWT\JWT;
use Google\ApiCore\ApiException;
-use Google\Cloud\Tasks\V2\Queue;
-use Google\Cloud\Tasks\V2\RetryConfig;
+use Google\Cloud\Tasks\V2\CloudTasksClient;
use Google\Cloud\Tasks\V2\Task;
use Illuminate\Foundation\Testing\DatabaseTransactions;
+use Illuminate\Queue\Events\JobReleasedAfterException;
use Illuminate\Support\Facades\DB;
-use Google\Cloud\Tasks\V2\CloudTasksClient;
use Illuminate\Support\Facades\Event;
-use Mockery;
-use Stackkit\LaravelGoogleCloudTasksQueue\TaskCreated;
+use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleasedAfterException as PackageJobReleasedAfterException;
+use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated;
use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler;
class TestCase extends \Orchestra\Testbench\TestCase
@@ -25,6 +24,8 @@ class TestCase extends \Orchestra\Testbench\TestCase
*/
public $client;
+ public string $releasedJobPayload;
+
protected function setUp(): void
{
parent::setUp();
@@ -32,6 +33,13 @@ protected function setUp(): void
$this->withFactories(__DIR__ . '/../factories');
$this->defaultHeaders['Authorization'] = 'Bearer ' . encrypt(time() + 10);
+
+ Event::listen(
+ $this->getJobReleasedAfterExceptionEvent(),
+ function ($event) {
+ $this->releasedJobPayload = $event->job->getRawBody();
+ }
+ );
}
/**
@@ -116,10 +124,12 @@ protected function setConfigValue($key, $value)
public function dispatch($job)
{
$payload = null;
+ $payloadAsArray = [];
$task = null;
- Event::listen(TaskCreated::class, function (TaskCreated $event) use (&$payload, &$task) {
- $payload = json_decode($event->task->getHttpRequest()->getBody(), true);
+ Event::listen(TaskCreated::class, function (TaskCreated $event) use (&$payload, &$payloadAsArray, &$task) {
+ $payload = $event->task->getHttpRequest()->getBody();
+ $payloadAsArray = json_decode($payload, true);
$task = $event->task;
request()->headers->set('X-Cloudtasks-Taskname', $task->getName());
@@ -127,14 +137,16 @@ public function dispatch($job)
dispatch($job);
- return new class($payload, $task) {
- public array $payload = [];
+ return new class($payload, $task, $this) {
+ public string $payload;
public Task $task;
+ public TestCase $testCase;
- public function __construct(array $payload, Task $task)
+ public function __construct(string $payload, Task $task, TestCase $testCase)
{
$this->payload = $payload;
$this->task = $task;
+ $this->testCase = $testCase;
}
public function run(): void
@@ -142,33 +154,42 @@ public function run(): void
rescue(function (): void {
app(TaskHandler::class)->handle($this->payload);
});
-
- $taskRetryCount = request()->header('X-CloudTasks-TaskRetryCount', 0);
- request()->headers->set('X-CloudTasks-TaskRetryCount', $taskRetryCount + 1);
}
public function runWithoutExceptionHandler(): void
{
app(TaskHandler::class)->handle($this->payload);
+ }
+
+ public function runAndGetReleasedJob(): self
+ {
+ rescue(function (): void {
+ app(TaskHandler::class)->handle($this->payload);
+ });
+
+ return new self(
+ $this->testCase->releasedJobPayload,
+ $this->task,
+ $this->testCase
+ );
+ }
- $taskRetryCount = request()->header('X-CloudTasks-TaskRetryCount', 0);
- request()->headers->set('X-CloudTasks-TaskRetryCount', $taskRetryCount + 1);
+ public function payloadAsArray(string $key = '')
+ {
+ $decoded = json_decode($this->payload, true);
+
+ return data_get($decoded, $key ?: null);
}
};
}
- public function runFromPayload(array $payload): void
+ public function runFromPayload(string $payload): void
{
rescue(function () use ($payload) {
app(TaskHandler::class)->handle($payload);
});
}
- public function dispatchAndRun($job): void
- {
- $this->runFromPayload($this->dispatch($job));
- }
-
public function assertTaskDeleted(string $taskId): void
{
try {
@@ -214,4 +235,14 @@ protected function assertDatabaseCount($table, int $count, $connection = null)
{
$this->assertEquals($count, DB::connection($connection)->table($table)->count());
}
+
+ public function getJobReleasedAfterExceptionEvent(): string
+ {
+ // The JobReleasedAfterException event is not available in Laravel versions
+ // below 9.x so instead for those versions we throw our own event which
+ // is identical to the Laravel one.
+ return version_compare(app()->version(), '9.0.0', '<')
+ ? PackageJobReleasedAfterException::class
+ : JobReleasedAfterException::class;
+ }
}