File manager - Edit - /var/www/payraty/helpdesk/public/storage/branding_media/images/resources.zip
Back
PK ! �@�� ( ( views/layout.blade.phpnu ȯ�� <!DOCTYPE html> <html lang="en"> <head> <!-- Meta Information --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="csrf-token" content="{{ csrf_token() }}"> <link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAipJREFUeNrEV8txwjAQtQ2HHCmB3JKbSQOYCoA0gD0pgFBBwpEToQAGKmDglpwgFdg5kZtNB1BBsuusZ4RY2ZZjYGd2jGWh97Q/rUwjpziPT3V4dECboDZoXZoSka5Al5vFNMqzrpkD2IFHn8B1ZAM6BCKbQgQAuAaPWQFgjoinsoipAEcTr0FrRjmyJxLLTAI5wXFXAehBGMPYcDKIIIm5kkAGOJpwAjqHRfYpbkOXvTBBypIwpT+HCvA3Cqi9Rta8EhHOHS1YCy1oWMKHmQIcGQ90wGMfLaZIoEGAoiDGOHmxhFTr5PGZJgncZYszEGC6ogX6nNn/Ay6RGDCfYveYVOFCJuAaumbPiIk1kyUNS2H6SZngyZrMWM+i/JVlXjK4QUVI3pRTpYPlaG6yeyGvm0Jef1ItiArwQBKu8G5bTMEIhKLkU3q65D+HgieE7+MCBHbygMVMOlCK+CnVDOUZ5s00ghCt2T45C+DDD2MBW/O066YFLYGvuXU5C9i6GYaLUzqr+olQtS5aIMwwtW6QfQnv7awNVanolEWgo9nABBb1cNeSmMDyigRWZkqdPrdEkDm3SRYMr7D7odwRXdIK8e7lOuAxh8W5pHtSiOhw8S4A7iX9IErlyC5b/7t+/7Ar4TKiEuyyRuJA5cQ5Wz8gEhgPNyXvfCQPVtgI+SPxAT/vSqiSEbXh70Uvp27GRSMNeJjV2Jp5V6MGpUeuUR0wAemKuwdy8ivAAJcc0R2NFxWtAAAAAElFTkSuQmCC"> <title>Horizon{{ config('app.name') ? ' - ' . config('app.name') : '' }}</title> <!-- Style sheets--> <link rel="preconnect" href="https://fonts.bunny.net"> <link href="https://fonts.bunny.net/css?family=figtree:300,400,500,600" rel="stylesheet" /> {{ Laravel\Horizon\Horizon::css() }} {{ Laravel\Horizon\Horizon::js() }} </head> <body> <div id="horizon" v-cloak> <alert :message="alert.message" :type="alert.type" :auto-close="alert.autoClose" :confirmation-proceed="alert.confirmationProceed" :confirmation-cancel="alert.confirmationCancel" v-if="alert.type"></alert> <div class="container mb-5"> <div class="d-flex align-items-center py-4 header"> <router-link to="/" class="logo d-flex align-items-center"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"> <path class="fill-primary" d="M5.26176342 26.4094389C2.04147988 23.6582233 0 19.5675182 0 15c0-4.1421356 1.67893219-7.89213562 4.39339828-10.60660172C7.10786438 1.67893219 10.8578644 0 15 0c8.2842712 0 15 6.71572875 15 15 0 8.2842712-6.7157288 15-15 15-3.716753 0-7.11777662-1.3517984-9.73823658-3.5905611zM4.03811305 15.9222506C5.70084247 14.4569342 6.87195416 12.5 10 12.5c5 0 5 5 10 5 3.1280454 0 4.2991572-1.9569336 5.961887-3.4222502C25.4934253 8.43417206 20.7645408 4 15 4 8.92486775 4 4 8.92486775 4 15c0 .3105915.01287248.6181765.03811305.9222506z"/> </svg> <h1 class="h4 mb-0 ms-2"> <strong>Laravel</strong> Horizon{{ config('app.name') ? ' - ' . config('app.name') : '' }} </h1> </router-link> <div class="ms-auto"> <scheme-toggler></scheme-toggler> <button class="btn btn-muted ms-2" :class="{active: autoLoadsNewEntries}" v-on:click.prevent="autoLoadNewEntries" title="Auto Load Entries"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="icon" fill="currentColor"> <path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" clip-rule="evenodd" /> </svg> </button> </div> </div> <div class="row mt-4"> <div class="col-2 sidebar"> <ul class="nav flex-column"> <li class="nav-item"> <router-link active-class="active" to="/dashboard" class="nav-link d-flex align-items-center"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M4.25 2A2.25 2.25 0 002 4.25v2.5A2.25 2.25 0 004.25 9h2.5A2.25 2.25 0 009 6.75v-2.5A2.25 2.25 0 006.75 2h-2.5zm0 9A2.25 2.25 0 002 13.25v2.5A2.25 2.25 0 004.25 18h2.5A2.25 2.25 0 009 15.75v-2.5A2.25 2.25 0 006.75 11h-2.5zm9-9A2.25 2.25 0 0011 4.25v2.5A2.25 2.25 0 0013.25 9h2.5A2.25 2.25 0 0018 6.75v-2.5A2.25 2.25 0 0015.75 2h-2.5zm0 9A2.25 2.25 0 0011 13.25v2.5A2.25 2.25 0 0013.25 18h2.5A2.25 2.25 0 0018 15.75v-2.5A2.25 2.25 0 0015.75 11h-2.5z" clip-rule="evenodd" /> </svg> <span>Dashboard</span> </router-link> </li> <li class="nav-item"> <router-link active-class="active" to="/monitoring" class="nav-link d-flex align-items-center"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" /> </svg> <span>Monitoring</span> </router-link> </li> <li class="nav-item"> <router-link active-class="active" to="/metrics" class="nav-link d-flex align-items-center"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <path d="M15.5 2A1.5 1.5 0 0014 3.5v13a1.5 1.5 0 001.5 1.5h1a1.5 1.5 0 001.5-1.5v-13A1.5 1.5 0 0016.5 2h-1zM9.5 6A1.5 1.5 0 008 7.5v9A1.5 1.5 0 009.5 18h1a1.5 1.5 0 001.5-1.5v-9A1.5 1.5 0 0010.5 6h-1zM3.5 10A1.5 1.5 0 002 11.5v5A1.5 1.5 0 003.5 18h1A1.5 1.5 0 006 16.5v-5A1.5 1.5 0 004.5 10h-1z" /> </svg> <span>Metrics</span> </router-link> </li> <li class="nav-item"> <router-link active-class="active" to="/batches" class="nav-link d-flex align-items-center"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M2 3.75A.75.75 0 012.75 3h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.166a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75zm0 4.167a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z" clip-rule="evenodd" /> </svg> <span>Batches</span> </router-link> </li> <li class="nav-item"> <router-link active-class="active" to="/jobs/pending" class="nav-link d-flex align-items-center"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M2 10a8 8 0 1116 0 8 8 0 01-16 0zm5-2.25A.75.75 0 017.75 7h.5a.75.75 0 01.75.75v4.5a.75.75 0 01-.75.75h-.5a.75.75 0 01-.75-.75v-4.5zm4 0a.75.75 0 01.75-.75h.5a.75.75 0 01.75.75v4.5a.75.75 0 01-.75.75h-.5a.75.75 0 01-.75-.75v-4.5z" clip-rule="evenodd" /> </svg> <span>Pending Jobs</span> </router-link> </li> <li class="nav-item"> <router-link active-class="active" to="/jobs/completed" class="nav-link d-flex align-items-center"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> </svg> <span>Completed Jobs</span> </router-link> </li> <li class="nav-item"> <router-link active-class="active" to="/jobs/silenced" class="nav-link d-flex align-items-center"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <path d="M4 8c0-.26.017-.517.049-.77l7.722 7.723a33.56 33.56 0 01-3.722-.01 2 2 0 003.862.15l1.134 1.134a3.5 3.5 0 01-6.53-1.409 32.91 32.91 0 01-3.257-.508.75.75 0 01-.515-1.076A11.448 11.448 0 004 8zM17.266 13.9a.756.756 0 01-.068.116L6.389 3.207A6 6 0 0116 8c.001 1.887.455 3.665 1.258 5.234a.75.75 0 01.01.666zM3.28 2.22a.75.75 0 00-1.06 1.06l14.5 14.5a.75.75 0 101.06-1.06L3.28 2.22z" /> </svg> <span>Silenced Jobs</span> </router-link> </li> <li class="nav-item"> <router-link active-class="active" to="/failed" class="nav-link d-flex align-items-center"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /> </svg> <span>Failed Jobs</span> </router-link> </li> </ul> </div> <div class="col-10"> @if ($isDownForMaintenance) <div class="alert alert-warning"> This application is in "maintenance mode". Queued jobs may not be processed unless your worker is using the "force" flag. </div> @endif <router-view></router-view> </div> </div> </div> </div> </body> </html> PK ! �� � sass/syntaxhighlight.scssnu ȯ�� .vjs-tree { font-family: 'Monaco', 'Menlo', 'Consolas', 'Bitstream Vera Sans Mono', monospace !important; &.is-root { position: relative; } .vjs-tree-node { display: flex; position: relative; &:hover { background-color: unset; } .vjs-indent-unit { &.has-line { border-left: 1px dotted rgba(204, 204, 204, 0.28) !important; } } &.has-carets { padding-left: 15px; } .has-carets.has-selector, .has-selector { padding-left: 30px; } } .vjs-indent { display: flex; position: relative; } .vjs-indent-unit { width: 1em; } .vjs-tree-brackets { cursor: pointer; &:hover { color: #20a0ff; } } .vjs-key { color: #c3cbd3 !important; padding-right: 10px; } .vjs-value { @extend .text-break; } .vjs-value-string { color: #c3e88d !important; } .vjs-value-null, .vjs-value-number, .vjs-value-boolean, .vjs-value-undefined { color: #a291f5 !important; } } PK ! ��� sass/_colors.scssnu ȯ�� $white: #ffffff; $black: #000000; $gray-50: #f9fafb; $gray-100: #f3f4f6; $gray-200: #e5e7eb; $gray-300: #d1d5db; $gray-400: #9ca3af; $gray-500: #6b7280; $gray-600: #4b5563; $gray-700: #374151; $gray-800: #1f2937; $gray-900: #111827; $red-50: #fef2f2; $red-100: #fee2e2; $red-200: #fecaca; $red-300: #fca5a5; $red-400: #f87171; $red-500: #ef4444; $red-600: #dc2626; $red-700: #b91c1c; $red-800: #991b1b; $red-900: #7f1d1d; $amber-50: #fffbeb; $amber-100: #fef3c7; $amber-200: #fde68a; $amber-300: #fcd34d; $amber-400: #fbbf24; $amber-500: #f59e0b; $amber-600: #d97706; $amber-700: #b45309; $amber-800: #92400e; $amber-900: #78350f; $emerald-50: #ecfdf5; $emerald-100: #d1fae5; $emerald-200: #a7f3d0; $emerald-300: #6ee7b7; $emerald-400: #34d399; $emerald-500: #10b981; $emerald-600: #059669; $emerald-700: #047857; $emerald-800: #065f46; $emerald-900: #064e3b; $blue-50: #eff6ff; $blue-100: #dbeafe; $blue-200: #bfdbfe; $blue-300: #93c5fd; $blue-400: #60a5fa; $blue-500: #3b82f6; $blue-600: #2563eb; $blue-700: #1d4ed8; $blue-800: #1e40af; $blue-900: #1e3a8a; $violet-50: #f5f3ff; $violet-100: #ede9fe; $violet-200: #ddd6fe; $violet-300: #c4b5fd; $violet-400: #a78bfa; $violet-500: #8b5cf6; $violet-600: #7c3aed; $violet-700: #6d28d9; $violet-800: #5b21b6; $violet-900: #4c1d95; PK ! {Xr�b b sass/styles-dark.scssnu ȯ�� @import 'colors'; $font-family-base: Figtree, sans-serif; $font-weight-bold: 600; $font-size-base: 1rem; $badge-font-size: 0.875rem; $link-decoration: none; $link-hover-decoration: underline; $primary: $violet-500; $secondary: $gray-500; $success: $emerald-500; $info: $blue-500; $warning: $amber-500; $danger: $red-500; $body-bg: $gray-900; $body-color: $gray-100; $text-muted: $gray-400; $border-radius-lg: 6px; $logo-color: $gray-200; $link-color: $violet-400; $link-hover-color: $violet-300; $sidebar-nav-color: $gray-400; $sidebar-nav-hover-color: $gray-300; $sidebar-nav-hover-bg: $gray-800; $sidebar-nav-icon-color: $gray-500; $sidebar-nav-active-bg: $gray-800; $sidebar-nav-active-color: $violet-400; $sidebar-nav-active-icon-color: $violet-500; $pill-link: $gray-400; $pill-link-active: $violet-400; $pill-link-hover: $gray-200; $border-color: $gray-600; $table-border-color: $gray-700; $table-headers-color: $gray-800; $table-hover-bg: $gray-700; $header-border-color: $table-border-color; $input-bg: $gray-800; $input-color: $gray-200; $input-border-color: $border-color; $card-cap-bg: $gray-700; $card-bg-secondary: $gray-800; $card-bg: $gray-800; $card-border-radius: $border-radius-lg; $code-bg: #292d3e; $modal-content-bg: $table-headers-color; $modal-backdrop-bg: $gray-600; $modal-footer-border-color: $input-border-color; $modal-header-border-color: $input-border-color; $new-entries-bg: $violet-900; $new-code-entries-bg: $gray-600; $control-action-icon-color: $gray-500; $control-action-icon-hover: $violet-400; $nav-pills-link-active-bg: $gray-800; $dropdown-bg: $gray-700; $dropdown-link-color: $white; $btn-muted-color: $gray-400; $btn-muted-bg: $gray-800; $btn-muted-hover-color: $gray-300; $btn-muted-hover-bg: $gray-700; $btn-muted-active-color: $white; $btn-muted-active-bg: $primary; $badge-secondary-bg: $gray-300; $badge-secondary-color: $gray-700; $badge-success-bg: $emerald-500; $badge-success-color: $white; $badge-info-bg: $blue-500; $badge-info-color: $white; $badge-warning-bg: $amber-500; $badge-warning-color: $white; $badge-danger-bg: $red-500; $badge-danger-color: $white; $grid-breakpoints: ( xs: 0, sm: 2px, md: 8px, lg: 9px, xl: 10px, ) !default; $container-max-widths: ( sm: 1137px, md: 1138px, lg: 1139px, xl: 1140px, ) !default; @import 'base'; .btn-primary { color: rgb(255, 255, 255); } PK ! |D8�� � sass/base.scssnu ȯ�� @import 'syntaxhighlight'; @import 'bootstrap'; body { padding-bottom: 20px; } .container { max-width: 1440px; } html { min-width: 1140px; } [v-cloak] { display: none; } svg.icon { width: 1rem; height: 1rem; } .header { border-bottom: solid 1px $header-border-color; .logo { text-decoration: none; color: $logo-color; svg { width: 2rem; height: 2rem; } } } .sidebar .nav-item { a { color: $sidebar-nav-color; padding: 0.5rem 0.75rem; margin-bottom: 4px; border-radius: $border-radius-lg; svg { width: 1.25rem; height: 1.25rem; margin-right: 15px; fill: $sidebar-nav-icon-color; } &:hover { background-color: $sidebar-nav-hover-bg; color: $sidebar-nav-hover-color; } &.active { background-color: $sidebar-nav-active-bg; color: $sidebar-nav-active-color; svg { fill: $sidebar-nav-active-icon-color; } } } } .card { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); border: none; .bottom-radius { border-bottom-left-radius: $card-border-radius; border-bottom-right-radius: $card-border-radius; } .card-header { padding-top: 0.7rem; padding-bottom: 0.7rem; background-color: $card-cap-bg; border-bottom: none; min-height: 60px; .btn-group { .btn { padding: 0.2rem 0.5rem; } } .form-control-with-icon { position: relative; .icon-wrapper { display: flex; align-items: center; justify-content: center; position: absolute; top: 0; left: 0.75rem; bottom: 0; .icon { fill: $text-muted; } } .form-control { padding-left: 2.25rem; font-size: 0.875rem; border-radius: 9999px; } } } .table { th, td { padding: 0.75rem 1.25rem; } &.table-sm { th, td { padding: 1rem 1.25rem; } } th { background-color: $table-headers-color; font-size: 0.875rem; padding: 0.5rem 1.25rem; border-bottom: 0; } &:not(.table-borderless) { td { border-top: 1px solid $table-border-color; } } &.penultimate-column-right { th:nth-last-child(2), td:nth-last-child(2) { text-align: right; } } th.table-fit, td.table-fit { width: 1%; white-space: nowrap; } } } .fill-text-color { fill: $body-color; } .fill-danger { fill: $danger; } .fill-warning { fill: $warning; } .fill-info { fill: $info; } .fill-success { fill: $success; } .fill-primary { fill: $primary; } button:hover { .fill-primary { fill: #fff; } } .btn-outline-primary.active { .fill-primary { fill: $body-bg; } } .btn-outline-primary:not(:disabled):not(.disabled).active:focus { box-shadow: none !important; } .btn-muted { color: $btn-muted-color; background: $btn-muted-bg; &:hover, &:focus { color: $btn-muted-hover-color; background: $btn-muted-hover-bg; } &.active { color: $btn-muted-active-color; background: $btn-muted-active-bg; } } .badge-secondary { background: $badge-secondary-bg; color: $badge-secondary-color; } .badge-success { background: $badge-success-bg; color: $badge-success-color; } .badge-info { background: $badge-info-bg; color: $badge-info-color; } .badge-warning { background: $badge-warning-bg; color: $badge-warning-color; } .badge-danger { background: $badge-danger-bg; color: $badge-danger-color; } .control-action { svg { fill: $control-action-icon-color; width: 1.2rem; height: 1.2rem; &:hover { fill: $control-action-icon-hover; } } } .info-icon { fill: $control-action-icon-color; } @-webkit-keyframes spin { from { -ms-transform: rotate(0deg); -moz-transform: rotate(0deg); -webkit-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); } to { -ms-transform: rotate(360deg); -moz-transform: rotate(360deg); -webkit-transform: rotate(360deg); -o-transform: rotate(360deg); transform: rotate(360deg); } } @keyframes spin { from { -ms-transform: rotate(0deg); -moz-transform: rotate(0deg); -webkit-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); } to { -ms-transform: rotate(360deg); -moz-transform: rotate(360deg); -webkit-transform: rotate(360deg); -o-transform: rotate(360deg); transform: rotate(360deg); } } .spin { -webkit-animation: spin 2s linear infinite; -moz-animation: spin 2s linear infinite; -ms-animation: spin 2s linear infinite; -o-animation: spin 2s linear infinite; animation: spin 2s linear infinite; } .card { .nav-pills { background: $card-cap-bg; .nav-link { font-size: 0.9rem; border-radius: 0; padding: 0.75rem 1.25rem; color: $pill-link; &:hover, &:focus { color: $pill-link-hover; } &.active { background: none; color: $pill-link-active; border-bottom: solid 2px $pill-link-active; } } } } .list-enter-active:not(.dontanimate) { transition: background 1s linear; } .list-enter:not(.dontanimate), .list-leave-to:not(.dontanimate) { background: $new-entries-bg; } .code-bg .list-enter:not(.dontanimate), .code-bg .list-leave-to:not(.dontanimate) { background: $new-code-entries-bg; } .card table { td { vertical-align: middle !important; } } .card-bg-secondary { background: $card-bg-secondary; } .code-bg { background: $code-bg; } .disabled-watcher { padding: 0.75rem; color: #fff; background: $danger; } .badge-sm { font-size: 0.75rem; } .table > :not(:first-child) { border-top: none; } PK ! ��Y Y sass/styles.scssnu ȯ�� @import 'colors'; $font-family-base: Figtree, sans-serif; $font-weight-bold: 600; $font-size-base: 1rem; $badge-font-size: 0.875rem; $link-decoration: none; $link-hover-decoration: underline; $primary: #7746ec; $secondary: $gray-500; $success: $emerald-500; $info: $blue-500; $warning: $amber-500; $danger: $red-500; $body-bg: $gray-100; $body-color: $gray-900; $text-muted: $gray-500; $border-radius-lg: 6px; $btn-focus-width: 0; $logo-color: $gray-700; $sidebar-nav-color: $gray-600; $sidebar-nav-hover-color: $primary; $sidebar-nav-hover-bg: $gray-200; $sidebar-nav-icon-color: $gray-400; $sidebar-nav-active-bg: $gray-200; $sidebar-nav-active-color: $primary; $sidebar-nav-active-icon-color: $primary; $pill-link: $gray-600; $pill-link-active: $violet-600; $pill-link-hover: $gray-800; $border-color: $gray-300; $table-headers-color: $gray-100; $table-border-color: $gray-200; $table-hover-bg: $gray-100; $header-border-color: $table-border-color; $input-bg: $white; $input-color: $gray-800; $input-border-color: $border-color; $card-cap-bg: $white; $card-bg-secondary: $gray-100; $card-bg: $white; $card-border-radius: $border-radius-lg; $code-bg: #292d3e; $new-entries-bg: $violet-50; $new-code-entries-bg: $gray-600; $control-action-icon-color: $gray-300; $control-action-icon-hover: $violet-600; $nav-pills-link-active-bg: $gray-200; $dropdown-bg: $white; $dropdown-link-color: $gray-700; $btn-muted-color: $gray-600; $btn-muted-bg: $gray-200; $btn-muted-hover-color: $gray-900; $btn-muted-hover-bg: $gray-300; $btn-muted-active-color: $white; $btn-muted-active-bg: $primary; $badge-secondary-bg: $gray-200; $badge-secondary-color: $gray-600; $badge-success-bg: $emerald-100; $badge-success-color: $emerald-600; $badge-info-bg: $blue-100; $badge-info-color: $blue-600; $badge-warning-bg: $amber-100; $badge-warning-color: $amber-600; $badge-danger-bg: $red-100; $badge-danger-color: $red-600; $grid-breakpoints: ( xs: 0, sm: 2px, md: 8px, lg: 9px, xl: 10px ) !default; $container-max-widths: ( sm: 1137px, md: 1138px, lg: 1139px, xl: 1140px ) !default; @import 'base'; PK ! �L�� � js/routes.jsnu ȯ�� import dashboard from './screens/dashboard.vue'; import monitoring from './screens/monitoring/index.vue'; import monitoringTag from './screens/monitoring/tag.vue'; import monitoringTagJobs from './screens/monitoring/tag-jobs.vue'; import metrics from './screens/metrics/index.vue'; import metricsJobs from './screens/metrics/jobs.vue'; import metricsQueues from './screens/metrics/queues.vue'; import metricsPreview from './screens/metrics/preview.vue'; import recentJobs from './screens/recentJobs/index.vue'; import recentJobsJob from './screens/recentJobs/job.vue'; import failedJobs from './screens/failedJobs/index.vue'; import failedJobsJob from './screens/failedJobs/job.vue'; import batches from './screens/batches/index.vue'; import batchesPreview from './screens/batches/preview.vue'; export default [ { path: '/', redirect: '/dashboard' }, { path: '/dashboard', name: 'dashboard', component: dashboard, }, { path: '/monitoring', name: 'monitoring', component: monitoring, }, { path: '/monitoring/:tag', component: monitoringTag, children: [ { path: 'jobs', name: 'monitoring-jobs', component: monitoringTagJobs, props: { type: 'jobs' }, }, { path: 'failed', name: 'monitoring-failed', component: monitoringTagJobs, props: { type: 'failed' }, }, ], }, { path: '/metrics', redirect: '/metrics/jobs' }, { path: '/metrics/', component: metrics, children: [ { path: 'jobs', name: 'metrics-jobs', component: metricsJobs, }, { path: 'queues', name: 'metrics-queues', component: metricsQueues, }, ], }, { path: '/metrics/:type/:slug', name: 'metrics-preview', component: metricsPreview, }, { path: '/jobs/:type', name: 'jobs', component: recentJobs, }, { path: '/jobs/pending/:jobId', name: 'pending-jobs-preview', component: recentJobsJob, }, { path: '/jobs/completed/:jobId', name: 'completed-jobs-preview', component: recentJobsJob, }, { path: '/jobs/silenced/:jobId', name: 'silenced-jobs-preview', component: recentJobsJob, }, { path: '/failed', name: 'failed-jobs', component: failedJobs, }, { path: '/failed/:jobId', name: 'failed-jobs-preview', component: failedJobsJob, }, { path: '/batches', name: 'batches', component: batches, }, { path: '/batches/:batchId', name: 'batches-preview', component: batchesPreview, }, ]; PK ! -�α� � js/app.jsnu ȯ�� import axios from 'axios'; import Vue from 'vue/dist/vue.esm.js'; import VueRouter from 'vue-router'; import VueJsonPretty from 'vue-json-pretty'; import 'vue-json-pretty/lib/styles.css'; import Base from './base'; import Routes from './routes'; import Alert from './components/Alert.vue'; import SchemeToggler from './components/SchemeToggler.vue'; let token = document.head.querySelector("meta[name='csrf-token']"); axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; if (token) { axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; } Vue.use(VueRouter); Vue.prototype.$http = axios.create(); window.Horizon.basePath = '/' + window.Horizon.path; let routerBasePath = window.Horizon.basePath + '/'; if (window.Horizon.path === '' || window.Horizon.path === '/') { routerBasePath = '/'; window.Horizon.basePath = ''; } const router = new VueRouter({ routes: Routes, mode: 'history', base: routerBasePath, }); Vue.component('vue-json-pretty', VueJsonPretty); Vue.component('alert', Alert); Vue.component('scheme-toggler', SchemeToggler); Vue.mixin(Base); new Vue({ router, data() { return { alert: { type: null, autoClose: 0, message: '', confirmationProceed: null, confirmationCancel: null, }, autoLoadsNewEntries: localStorage.autoLoadsNewEntries === '1', }; }, }).$mount('#horizon'); PK ! �ťȤ � js/base.jsnu ȯ�� import moment from 'moment-timezone'; export default { computed: { Horizon() { return Horizon; }, }, methods: { /** * Format the given date with respect to timezone. */ formatDate(unixTime) { return moment(unixTime * 1000).add(new Date().getTimezoneOffset() / 60); }, /** * Format the given date with respect to timezone. */ formatDateIso(date) { return moment(date).add(new Date().getTimezoneOffset() / 60); }, /** * Extract the job base name. */ jobBaseName(name) { if (!name.includes('\\')) return name; var parts = name.split('\\'); return parts[parts.length - 1]; }, /** * Autoload new entries in listing screens. */ autoLoadNewEntries() { if (!this.autoLoadsNewEntries) { this.autoLoadsNewEntries = true; localStorage.autoLoadsNewEntries = 1; } else { this.autoLoadsNewEntries = false; localStorage.autoLoadsNewEntries = 0; } }, /** * Convert to human readable timestamp. */ readableTimestamp(timestamp) { return this.formatDate(timestamp).format('YYYY-MM-DD HH:mm:ss'); }, /** * Uppercase the first character of the string. */ upperFirst(string) { return string.charAt(0).toUpperCase() + string.slice(1); }, /** * Group array entries by a given key. */ groupBy(array, key) { return array.reduce( (grouped, entry) => ({ ...grouped, [entry[key]]: [...(grouped[entry[key]] || []), entry], }), {} ); }, }, }; PK ! ��0" " js/components/Alert.vuenu ȯ�� <script type="text/ecmascript-6"> import { Modal } from 'bootstrap'; export default { props: ['type', 'message', 'autoClose', 'confirmationProceed', 'confirmationCancel'], data(){ return { timeout: null, alertModal: null, anotherModalOpened: document.body.classList.contains('modal-open') } }, mounted() { const alertModalElement = document.getElementById('alertModal'); this.alertModal = Modal.getOrCreateInstance(alertModalElement, { backdrop: 'static', }) this.alertModal.show(); alertModalElement.addEventListener('hidden.bs.modal', e => { this.$root.alert.type = null; this.$root.alert.autoClose = false; this.$root.alert.message = ''; this.$root.alert.confirmationProceed = null; this.$root.alert.confirmationCancel = null; if (this.anotherModalOpened) { document.body.classList.add('modal-open'); } }, this); if (this.autoClose) { this.timeout = setTimeout(() => { this.close(); }, this.autoClose); } }, methods: { /** * Close the modal. */ close(){ clearTimeout(this.timeout); this.alertModal.hide(); }, /** * Confirm and close the modal. */ confirm(){ this.confirmationProceed(); this.close(); }, /** * Cancel and close the modal. */ cancel(){ if (this.confirmationCancel) { this.confirmationCancel(); } this.close(); } } } </script> <template> <div class="modal" id="alertModal" tabindex="-1" role="dialog" aria-labelledby="alertModalLabel" aria-hidden="true"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-body"> <p class="m-0 py-4">{{message}}</p> </div> <div class="modal-footer justify-content-start flex-row-reverse"> <button v-if="type == 'error'" class="btn btn-primary" @click="close"> Close </button> <button v-if="type == 'success'" class="btn btn-primary" @click="close"> Okay </button> <button v-if="type == 'confirmation'" class="btn btn-danger" @click="confirm"> Yes </button> <button v-if="type == 'confirmation'" class="btn" @click="cancel"> Cancel </button> </div> </div> </div> </div> </template> <style> #alertModal { z-index: 99999; background: rgba(0, 0, 0, 0.5); } #alertModal svg { display: block; margin: 0 auto; width: 4rem; height: 4rem; } </style> PK ! �؝Ә � js/components/Stacktrace.vuenu ȯ�� <script type="text/ecmascript-6"> export default { props: ['trace'], /** * The component's data. */ data() { return { minimumLines: 5, showAll: false, }; }, computed: { lines() { return this.trace.slice(0, this.showAll ? 1000 : this.minimumLines); } } } </script> <template> <div class="table-responsive"> <table class="table mb-0"> <tbody> <tr v-for="line in lines"> <td class="card-bg-secondary"><code>{{line}}</code></td> </tr> <tr v-if="! showAll"> <td class="card-bg-secondary"><a href="*" v-on:click.prevent="showAll = true">Show All</a></td> </tr> </tbody> </table> </div> </template> <style scoped> </style> PK ! ���S S js/components/LineChart.vuenu ȯ�� <script type="text/ecmascript-6"> import Chart from 'chart.js'; export default { props: ['data'], data(){ return { context: null, chart:null } }, mounted(){ this.context = this.$refs.canvas.getContext('2d'); this.chart = new Chart(this.context, { type: 'line', options: { tooltips: { intersect: false, }, legend: { display: false, }, scales: { yAxes: [ { ticks: { beginAtZero: true, callback: (value, index, values) => { return this.data.datasets[0].label === "Seconds" ? `${value} secs` : value; }, }, gridLines: { display: true }, beforeBuildTicks: function (scale) { var max = scale.chart.data.datasets[0].data.reduce((max, value) => value > max ? value : max) scale.max = parseFloat(max) + parseFloat(max * 0.25); }, } ], xAxes: [ { gridLines: { display: true }, afterTickToLabelConversion: function (data) { var xLabels = data.ticks; xLabels.forEach(function (labels, i) { if (i % 6 != 0 && (i + 1) != xLabels.length) { xLabels[i] = ''; } }); } }, ] } }, data: this.data }); }, } </script> <template> <div style="position: relative;"> <canvas ref="canvas" height="120"></canvas> </div> </template> PK ! ld�� � js/components/SchemeToggler.vuenu ȯ�� <script type="text/ecmascript-6"> export default { data () { return { scheme: 'system' } }, watch: { scheme (value) { localStorage.setItem('scheme', value); } }, mounted () { this.scheme = localStorage.getItem('scheme') ?? 'system'; window .matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', () => this.calculateScheme()) this.calculateScheme() }, methods: { toggleScheme () { if (this.scheme == 'system') { this.scheme = 'dark' } else if (this.scheme == 'dark') { this.scheme = 'light' } else { this.scheme = 'system' } this.calculateScheme() }, calculateScheme () { const dark = document.querySelector('style[data-scheme="dark"]'); if (this.scheme == 'system') { const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)'); dark.media = prefersDarkMode.matches ? "" : "max-width: 1px"; } else { dark.media = this.scheme == 'dark' ? "" : "max-width: 1px"; } } } } </script> <template> <button class="btn btn-muted" title="Switch Theme" v-on:click.prevent="toggleScheme"> <svg v-if="scheme == 'system'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="icon" fill="currentColor"> <path fill-rule="evenodd" d="M2 4.25A2.25 2.25 0 014.25 2h11.5A2.25 2.25 0 0118 4.25v8.5A2.25 2.25 0 0115.75 15h-3.105a3.501 3.501 0 001.1 1.677A.75.75 0 0113.26 18H6.74a.75.75 0 01-.484-1.323A3.501 3.501 0 007.355 15H4.25A2.25 2.25 0 012 12.75v-8.5zm1.5 0a.75.75 0 01.75-.75h11.5a.75.75 0 01.75.75v7.5a.75.75 0 01-.75.75H4.25a.75.75 0 01-.75-.75v-7.5z" clip-rule="evenodd" /> </svg> <svg v-if="scheme == 'dark'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="icon" fill="currentColor"> <path fill-rule="evenodd" d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z" clip-rule="evenodd" /> </svg> <svg v-if="scheme == 'light'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="icon" fill="currentColor"> <path d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z" /> </svg> </button> </template> PK ! ��(b; b; js/screens/dashboard.vuenu ȯ�� <script type="text/ecmascript-6"> import moment from 'moment'; export default { components: {}, /** * The component's data. */ data() { return { stats: {}, workers: [], workload: [], ready: false, }; }, /** * Prepare the component. */ mounted() { document.title = "Horizon - Dashboard"; this.refreshStatsPeriodically(); }, /** * Clean after the component is destroyed. */ destroyed() { clearTimeout(this.timeout); }, computed: { /** * Determine the recent job period label. */ recentJobsPeriod() { return !this.ready ? 'Jobs Past Hour' : `Jobs Past ${this.determinePeriod(this.stats.periods.recentJobs)}`; }, /** * Determine the recently failed job period label. */ failedJobsPeriod() { return !this.ready ? 'Failed Jobs Past 7 Days' : `Failed Jobs Past ${this.determinePeriod(this.stats.periods.failedJobs)}`; }, }, methods: { /** * Load the general stats. */ loadStats() { return this.$http.get(Horizon.basePath + '/api/stats') .then(response => { this.stats = response.data; if (Object.values(response.data.wait)[0]) { this.stats.max_wait_time = Object.values(response.data.wait)[0]; this.stats.max_wait_queue = Object.keys(response.data.wait)[0].split(':')[1]; } }); }, /** * Load the workers stats. */ loadWorkers() { return this.$http.get(Horizon.basePath + '/api/masters') .then(response => { this.workers = response.data; }); }, /** * Load the workload stats. */ loadWorkload() { return this.$http.get(Horizon.basePath + '/api/workload') .then(response => { this.workload = response.data; }); }, /** * Refresh the stats every period of time. */ refreshStatsPeriodically() { Promise.all([ this.loadStats(), this.loadWorkers(), this.loadWorkload(), ]).then(() => { this.ready = true; this.timeout = setTimeout(() => { this.refreshStatsPeriodically(); }, 5000); }); }, /** * Count processes for the given supervisor. */ countProcesses(processes) { return Object.values(processes).reduce((total, value) => total + value, 0).toLocaleString(); }, /** * Format the Supervisor display name. */ superVisorDisplayName(supervisor, worker) { return supervisor.replace(worker + ':', ''); }, /** * * @returns {string} */ humanTime(time) { return moment.duration(time, "seconds").humanize().replace(/^(.)/g, function ($1) { return $1.toUpperCase(); }); }, /** * Determine the unit for the given timeframe. */ determinePeriod(minutes) { return moment.duration(moment().diff(moment().subtract(minutes, "minutes"))).humanize().replace(/^An?\s/i, '').replace(/^(.)|\s(.)/g, function ($1) { return $1.toUpperCase(); }); } } } </script> <template> <div> <div class="card overflow-hidden"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0">Overview</h2> </div> <div class="card-bg-secondary"> <div class="d-flex"> <div class="w-25"> <div class="p-4"> <small class="text-muted fw-bold">Jobs Per Minute</small> <p class="h4 mt-2 mb-0"> {{ stats.jobsPerMinute ? stats.jobsPerMinute.toLocaleString() : 0 }} </p> </div> </div> <div class="w-25"> <div class="p-4"> <small class="text-muted fw-bold" v-text="recentJobsPeriod"></small> <p class="h4 mt-2 mb-0"> {{ stats.recentJobs ? stats.recentJobs.toLocaleString() : 0 }} </p> </div> </div> <div class="w-25"> <div class="p-4"> <small class="text-muted fw-bold" v-text="failedJobsPeriod"></small> <p class="h4 mt-2 mb-0"> {{ stats.failedJobs ? stats.failedJobs.toLocaleString() : 0 }} </p> </div> </div> <div class="w-25"> <div class="p-4"> <small class="text-muted fw-bold">Status</small> <div class="d-flex align-items-center mt-2"> <svg v-if="stats.status == 'running'" xmlns="http://www.w3.org/2000/svg" class="text-success" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width: 1.5rem; height: 1.5rem;"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> <svg v-if="stats.status == 'paused'" xmlns="http://www.w3.org/2000/svg" class="text-warning" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width: 1.5rem; height: 1.5rem;"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9v6m-4.5 0V9M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> <svg v-if="stats.status == 'inactive'" xmlns="http://www.w3.org/2000/svg" class="text-danger" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width: 1.5rem; height: 1.5rem;"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" /> </svg> <p class="h4 mb-0 ms-2">{{ {running: 'Active', paused: 'Paused', inactive: 'Inactive'}[stats.status] }}</p> <small v-if="stats.status == 'running' && stats.pausedMasters > 0" class="mb-0 ms-2">({{ stats.pausedMasters }} paused)</small> </div> </div> </div> </div> <div class="d-flex"> <div class="w-25"> <div class="p-4 mb-0"> <small class="text-muted fw-bold">Total Processes</small> <p class="h4 mt-2"> {{ stats.processes ? stats.processes.toLocaleString() : 0 }} </p> </div> </div> <div class="w-25"> <div class="p-4 mb-0"> <small class="text-muted fw-bold">Max Wait Time</small> <p class="mt-2 mb-0"> {{ stats.max_wait_time ? humanTime(stats.max_wait_time) : '-' }} </p> <small class="mt-1" v-if="stats.max_wait_queue">({{ stats.max_wait_queue }})</small> </div> </div> <div class="w-25"> <div class="p-4 mb-0"> <small class="text-muted fw-bold">Max Runtime</small> <p class="h4 mt-2"> {{ stats.queueWithMaxRuntime ? stats.queueWithMaxRuntime : '-' }} </p> </div> </div> <div class="w-25"> <div class="p-4 mb-0"> <small class="text-muted fw-bold">Max Throughput</small> <p class="h4 mt-2"> {{ stats.queueWithMaxThroughput ? stats.queueWithMaxThroughput : '-' }} </p> </div> </div> </div> </div> </div> <div class="card overflow-hidden mt-4" v-if="workload.length"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0">Current Workload</h2> </div> <table class="table table-hover mb-0"> <thead> <tr> <th>Queue</th> <th class="text-end" style="width: 120px;">Jobs</th> <th class="text-end" style="width: 120px;">Processes</th> <th class="text-end" style="width: 180px;">Wait</th> </tr> </thead> <tbody> <template v-for="queue in workload"> <tr> <td :class="{ 'fw-bold': queue.split_queues }"> <span>{{ queue.name.replace(/,/g, ', ') }}</span> </td> <td class="text-end text-muted" :class="{ 'fw-bold': queue.split_queues }">{{ queue.length ? queue.length.toLocaleString() : 0 }}</td> <td class="text-end text-muted" :class="{ 'fw-bold': queue.split_queues }">{{ queue.processes ? queue.processes.toLocaleString() : 0 }}</td> <td class="text-end text-muted" :class="{ 'fw-bold': queue.split_queues }">{{ humanTime(queue.wait) }}</td> </tr> <tr v-for="split_queue in queue.split_queues"> <td> <svg class="icon info-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> </svg> <span>{{ split_queue.name.replace(/,/g, ', ') }}</span> </td> <td class="text-end text-muted">{{ split_queue.length ? split_queue.length.toLocaleString() : 0 }}</td> <td class="text-end text-muted">-</td> <td class="text-end text-muted">{{ humanTime(split_queue.wait) }}</td> </tr> </template> </tbody> </table> </div> <div class="card overflow-hidden mt-4" v-for="worker in workers" :key="worker.name"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0">{{ worker.name }}</h2> <svg v-if="worker.status == 'running'" xmlns="http://www.w3.org/2000/svg" class="text-success" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width: 1.5rem; height: 1.5rem;"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> <svg v-if="worker.status == 'paused'" xmlns="http://www.w3.org/2000/svg" class="text-warning" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width: 1.5rem; height: 1.5rem;"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9v6m-4.5 0V9M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> </div> <table class="table table-hover mb-0"> <thead> <tr> <th>Supervisor</th> <th>Queues</th> <th class="text-end" style="width: 120px;">Processes</th> <th class="text-end" style="width: 180px;">Balancing</th> </tr> </thead> <tbody> <tr v-for="supervisor in worker.supervisors"> <td> <svg v-if="supervisor.status == 'paused'" class="fill-warning me-1" viewBox="0 0 20 20" style="width: 1rem; height: 1rem;"> <path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM7 6h2v8H7V6zm4 0h2v8h-2V6z" /> </svg> <svg v-if="supervisor.status == 'inactive'" class="fill-danger me-1" viewBox="0 0 20 20" style="width: 1rem; height: 1rem;"> <path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm1.41-1.41A8 8 0 1 0 15.66 4.34 8 8 0 0 0 4.34 15.66zm9.9-8.49L11.41 10l2.83 2.83-1.41 1.41L10 11.41l-2.83 2.83-1.41-1.41L8.59 10 5.76 7.17l1.41-1.41L10 8.59l2.83-2.83 1.41 1.41z" /> </svg> {{ superVisorDisplayName(supervisor.name, worker.name) }} </td> <td class="text-muted">{{ supervisor.options.queue.replace(/,/g, ', ') }}</td> <td class="text-end text-muted">{{ countProcesses(supervisor.processes) }}</td> <td class="text-end text-muted" v-if="supervisor.options.balance"> {{ supervisor.options.balance.charAt(0).toUpperCase() + supervisor.options.balance.slice(1) }} </td> <td class="text-end text-muted" v-else> Disabled </td> </tr> </tbody> </table> </div> </div> </template> PK ! �Al�* �* js/screens/failedJobs/index.vuenu ȯ�� <script type="text/ecmascript-6"> export default { /** * The component's data. */ data() { return { tagSearchPhrase: '', searchTimeout: null, ready: false, loadingNewEntries: false, hasNewEntries: false, page: 1, perPage: 50, totalPages: 1, jobs: [], retryingJobs: [], }; }, /** * Prepare the component. */ mounted() { document.title = "Horizon - Failed Jobs"; this.loadJobs(); this.refreshJobsPeriodically(); }, /** * Clean after the component is destroyed. */ destroyed() { clearInterval(this.interval); }, /** * Watch these properties for changes. */ watch: { '$route'() { this.page = 1; this.loadJobs(); }, tagSearchPhrase() { clearTimeout(this.searchTimeout); clearInterval(this.interval); this.searchTimeout = setTimeout(() => { this.loadJobs(); this.refreshJobsPeriodically(); }, 500); } }, methods: { /** * Load the jobs of the given tag. */ loadJobs(starting = 0, refreshing = false) { if (!refreshing) { this.ready = false; } var tagQuery = this.tagSearchPhrase ? 'tag=' + this.tagSearchPhrase + '&' : ''; this.$http.get(Horizon.basePath + '/api/jobs/failed?' + tagQuery + 'starting_at=' + starting) .then(response => { if (!this.$root.autoLoadsNewEntries && refreshing && !response.data.jobs.length) { return; } if (!this.$root.autoLoadsNewEntries && refreshing && this.jobs.length && response.data.jobs[0]?.id !== this.jobs[0]?.id) { this.hasNewEntries = true; } else { this.jobs = response.data.jobs; this.totalPages = Math.ceil(response.data.total / this.perPage); } this.ready = true; }); }, loadNewEntries() { this.jobs = []; this.loadJobs(0, false); this.hasNewEntries = false; }, /** * Retry the given failed job. */ retry(id) { if (this.isRetrying(id)) { return; } this.retryingJobs.push(id); this.$http.post(Horizon.basePath + '/api/jobs/retry/' + id) .then((response) => { setTimeout(() => { this.retryingJobs = this.retryingJobs.filter(job => job != id); }, 5000); }).catch(error => { this.retryingJobs = this.retryingJobs.filter(job => job != id); }); }, /** * Determine if the given job is currently retrying. */ isRetrying(id) { return this.retryingJobs.includes(id); }, /** * Determine if the given job has completed. */ hasCompleted(job) { return job.retried_by.find(retry => retry.status === 'completed'); }, /** * Determine if the given job was retried. */ wasRetried(job) { return job.retried_by && job.retried_by.length; }, /** * Determine if the given job is a retry. */ isRetry(job) { return job.payload.retry_of; }, /** * Construct the tooltip label for a retried job. */ retriedJobTooltip(job) { let lastRetry = job.retried_by[job.retried_by.length - 1]; return `Total retries: ${job.retried_by.length}, Last retry status: ${this.upperFirst(lastRetry.status)}`; }, /** * Refresh the jobs every period of time. */ refreshJobsPeriodically() { this.interval = setInterval(() => { this.loadJobs((this.page - 1) * this.perPage, true); }, 3000); }, /** * Load the jobs for the previous page. */ previous() { this.loadJobs( (this.page - 2) * this.perPage ); this.page -= 1; this.hasNewEntries = false; }, /** * Load the jobs for the next page. */ next() { this.loadJobs( this.page * this.perPage ); this.page += 1; this.hasNewEntries = false; } } } </script> <template> <div> <div class="card overflow-hidden"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0">Failed Jobs</h2> <div class="form-control-with-icon"> <div class="icon-wrapper"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="icon"> <path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" /> </svg> </div> <input type="text" class="form-control w-100" v-model="tagSearchPhrase" placeholder="Search Tags"> </div> </div> <div v-if="!ready" class="d-flex align-items-center justify-content-center card-bg-secondary p-5 bottom-radius"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="icon spin me-2 fill-text-color"> <path d="M12 10a2 2 0 0 1-3.41 1.41A2 2 0 0 1 10 8V0a9.97 9.97 0 0 1 10 10h-8zm7.9 1.41A10 10 0 1 1 8.59.1v2.03a8 8 0 1 0 9.29 9.29h2.02zm-4.07 0a6 6 0 1 1-7.25-7.25v2.1a3.99 3.99 0 0 0-1.4 6.57 4 4 0 0 0 6.56-1.42h2.1z"></path> </svg> <span>Loading...</span> </div> <div v-if="ready && jobs.length == 0" class="d-flex flex-column align-items-center justify-content-center card-bg-secondary p-5 bottom-radius"> <span>There aren't any failed jobs.</span> </div> <table v-if="ready && jobs.length > 0" class="table table-hover mb-0"> <thead> <tr> <th>Job</th> <th class="text-end">Runtime</th> <th>Failed</th> <th class="text-end">Retry</th> </tr> </thead> <tbody> <tr v-if="hasNewEntries" key="newEntries" class="dontanimate"> <td colspan="100" class="text-center card-bg-secondary py-2"> <small><a href="#" v-on:click.prevent="loadNewEntries" v-if="!loadingNewEntries">Load New Entries</a></small> <small v-if="loadingNewEntries">Loading...</small> </td> </tr> <tr v-for="job in jobs" :key="job.id"> <td> <router-link :title="job.name" :to="{ name: 'failed-jobs-preview', params: { jobId: job.id }}">{{ jobBaseName(job.name) }}</router-link> <small class="ms-1 badge bg-secondary badge-sm" :title="retriedJobTooltip(job)" v-if="wasRetried(job)"> Retried </small> <br> <small class="text-muted"> Queue: {{job.queue}} | Attempts: {{ job.payload.attempts }} <span v-if="isRetry(job)"> | Retry of <router-link :title="job.name" :to="{ name: 'failed-jobs-preview', params: { jobId: job.payload.retry_of }}"> {{ job.payload.retry_of.split('-')[0] }} </router-link> </span> <span v-if="job.payload.tags && job.payload.tags.length" class="text-break"> | Tags: {{ job.payload.tags && job.payload.tags.length ? job.payload.tags.join(', ') : '' }} </span> </small> </td> <td class="table-fit text-muted text-end"> <span>{{ job.failed_at ? String((job.failed_at - job.reserved_at).toFixed(2))+'s' : '-' }}</span> </td> <td class="table-fit text-muted"> {{ readableTimestamp(job.failed_at) }} </td> <td class="text-end table-fit"> <a href="#" title="Retry Job" @click.prevent="retry(job.id)" v-if="!hasCompleted(job)"> <svg class="fill-primary" viewBox="0 0 20 20" style="width: 1.25rem; height: 1.25rem;" :class="{spin: isRetrying(job.id)}"> <path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" clip-rule="evenodd" /> </svg> </a> </td> </tr> </tbody> </table> <div v-if="ready && jobs.length" class="p-3 d-flex justify-content-between border-top"> <button @click="previous" class="btn btn-secondary btn-sm" :disabled="page==1">Previous</button> <button @click="next" class="btn btn-secondary btn-sm" :disabled="page>=totalPages">Next</button> </div> </div> </div> </template> PK ! B-vO�) �) js/screens/failedJobs/job.vuenu ȯ�� <script type="text/ecmascript-6"> import phpunserialize from 'phpunserialize' import StackTrace from '@/components/Stacktrace.vue' export default { components: { 'stack-trace': StackTrace, }, /** * The component's data. */ data() { return { ready: false, retrying: false, job: {} }; }, /** * Prepare the component. */ mounted() { this.loadFailedJob(this.$route.params.jobId); document.title = "Horizon - Failed Jobs"; this.interval = setInterval(() => { this.reloadRetries(); }, 3000); }, /** * Clean after the component is destroyed. */ destroyed() { clearInterval(this.interval); }, methods: { loadFailedJob(id) { this.ready = false; this.$http.get(Horizon.basePath + '/api/jobs/failed/' + id) .then(response => { this.job = response.data; this.ready = true; }); }, /** * Reload the job retries. */ reloadRetries() { this.$http.get(Horizon.basePath + '/api/jobs/failed/' + this.$route.params.jobId) .then(response => { this.job.retried_by = response.data.retried_by; }); }, /** * Retry the given failed job. */ retry(id) { if (this.retrying) { return; } this.retrying = true; this.$http.post(Horizon.basePath + '/api/jobs/retry/' + id) .then(() => { setTimeout(() => { this.reloadRetries(); this.retrying = false; }, 3000); }); }, /** * Pretty print serialized job. * * @param data * @returns {string} */ prettyPrintJob(data) { try { return data.command && !data.command.includes('CallQueuedClosure') ? phpunserialize(data.command) : data; } catch (err) { return data; } } } } </script> <template> <div> <div class="card overflow-hidden"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0" v-if="!ready">Job Preview</h2> <h2 class="h6 m-0" v-if="ready">{{job.name}}</h2> <button class="btn btn-primary" v-on:click.prevent="retry(job.id)"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="icon" fill="currentColor" :class="{spin: retrying}"> <path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" clip-rule="evenodd" /> </svg> Retry </button> </div> <div v-if="!ready" class="d-flex align-items-center justify-content-center card-bg-secondary p-5 bottom-radius"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="icon spin me-2 fill-text-color"> <path d="M12 10a2 2 0 0 1-3.41 1.41A2 2 0 0 1 10 8V0a9.97 9.97 0 0 1 10 10h-8zm7.9 1.41A10 10 0 1 1 8.59.1v2.03a8 8 0 1 0 9.29 9.29h2.02zm-4.07 0a6 6 0 1 1-7.25-7.25v2.1a3.99 3.99 0 0 0-1.4 6.57 4 4 0 0 0 6.56-1.42h2.1z"></path> </svg> <span>Loading...</span> </div> <div class="card-body card-bg-secondary" v-if="ready"> <div class="row mb-2"> <div class="col-md-2 text-muted">ID</div> <div class="col">{{job.id}}</div> </div> <div class="row mb-2"> <div class="col-md-2 text-muted">Queue</div> <div class="col">{{job.queue}}</div> </div> <div class="row mb-2"> <div class="col-md-2 text-muted">Attempts</div> <div class="col">{{job.payload.attempts}}</div> </div> <div class="row mb-2"> <div class="col-md-2 text-muted">Retries</div> <div class="col">{{job.retried_by.length}}</div> </div> <div class="row mb-2" v-if="job.payload.retry_of"> <div class="col-md-2 text-muted">Retry of ID</div> <div class="col"> <a :href="Horizon.basePath + '/failed/' + job.payload.retry_of"> {{ job.payload.retry_of }} </a> </div> </div> <div class="row mb-2"> <div class="col-md-2 text-muted">Tags</div> <div class="col">{{ job.payload.tags && job.payload.tags.length ? job.payload.tags.join(', ') : '' }}</div> </div> <div class="row mb-2" v-if="prettyPrintJob(job.payload.data).batchId"> <div class="col-md-2 text-muted">Batch</div> <div class="col"> <router-link :to="{ name: 'batches-preview', params: { batchId: prettyPrintJob(job.payload.data).batchId }}"> {{ prettyPrintJob(job.payload.data).batchId }} </router-link> </div> </div> <div class="row mb-2"> <div class="col-md-2 text-muted">Pushed</div> <div class="col">{{ readableTimestamp(job.payload.pushedAt) }}</div> </div> <div class="row"> <div class="col-md-2 text-muted">Failed</div> <div class="col">{{readableTimestamp(job.failed_at)}}</div> </div> </div> </div> <div class="card overflow-hidden mt-4" v-if="ready"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0">Exception</h2> </div> <div> <stack-trace :trace="job.exception.split('\n')"></stack-trace> </div> </div> <div class="card overflow-hidden mt-4" v-if="ready"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0">Exception Context</h2> </div> <div class="card-body code-bg text-white"> <vue-json-pretty :data="prettyPrintJob(job.context)"></vue-json-pretty> </div> </div> <div class="card overflow-hidden mt-4" v-if="ready"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0">Data</h2> </div> <div class="card-body code-bg text-white"> <vue-json-pretty :data="prettyPrintJob(job.payload.data)"></vue-json-pretty> </div> </div> <div class="card overflow-hidden mt-4" v-if="ready && job.retried_by.length"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0">Recent Retries</h2> </div> <table class="table table-hover mb-0"> <thead> <tr> <th>Job</th> <th>ID</th> <th class="text-end">Retry Time</th> </tr> </thead> <tbody> <tr v-for="retry in job.retried_by"> <td> <svg v-if="retry.status == 'completed'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="fill-success" style="width: 1.5rem; height: 1.5rem;"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> </svg> <svg v-if="retry.status == 'reserved' || retry.status == 'pending'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="fill-warning" style="width: 1.5rem; height: 1.5rem;"> <path fill-rule="evenodd" d="M2 10a8 8 0 1116 0 8 8 0 01-16 0zm5-2.25A.75.75 0 017.75 7h.5a.75.75 0 01.75.75v4.5a.75.75 0 01-.75.75h-.5a.75.75 0 01-.75-.75v-4.5zm4 0a.75.75 0 01.75-.75h.5a.75.75 0 01.75.75v4.5a.75.75 0 01-.75.75h-.5a.75.75 0 01-.75-.75v-4.5z" clip-rule="evenodd" /> </svg> <svg v-if="retry.status == 'failed'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="fill-danger" style="width: 1.5rem; height: 1.5rem;"> <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /> </svg> <span class="ms-2">{{ retry.status.charAt(0).toUpperCase() + retry.status.slice(1) }}</span> </td> <td class="table-fit"> <a v-if="retry.status == 'failed'" :href="Horizon.basePath + '/failed/'+retry.id"> {{ retry.id }} </a> <span v-else>{{ retry.id }}</span> </td> <td class="text-end table-fit text-muted"> {{readableTimestamp(retry.retried_at)}} </td> </tr> </tbody> </table> </div> </div> </template> PK ! �UV�6 6 js/screens/recentJobs/index.vuenu ȯ�� <script type="text/ecmascript-6"> import JobRow from './job-row.vue'; export default { /** * The component's data. */ data() { return { ready: false, loadingNewEntries: false, hasNewEntries: false, page: 1, perPage: 50, totalPages: 1, jobs: [] }; }, /** * Components */ components: { JobRow, }, /** * Prepare the component. */ mounted() { this.updatePageTitle(); this.loadJobs(); this.refreshJobsPeriodically(); }, /** * Clean after the component is destroyed. */ destroyed() { clearInterval(this.interval); }, /** * Watch these properties for changes. */ watch: { '$route'() { this.updatePageTitle(); this.page = 1; this.loadJobs(); } }, methods: { /** * Load the jobs of the given tag. */ loadJobs(starting = -1, refreshing = false) { if (!refreshing) { this.ready = false; } this.$http.get(Horizon.basePath + '/api/jobs/' + this.$route.params.type + '?starting_at=' + starting + '&limit=' + this.perPage) .then(response => { if (!this.$root.autoLoadsNewEntries && refreshing && this.jobs.length && response.data.jobs[0]?.id !== this.jobs[0]?.id) { this.hasNewEntries = true; } else { this.jobs = response.data.jobs; this.totalPages = Math.ceil(response.data.total / this.perPage); } this.ready = true; }); }, loadNewEntries() { this.jobs = []; this.loadJobs(-1, false); this.hasNewEntries = false; }, /** * Refresh the jobs every period of time. */ refreshJobsPeriodically() { this.interval = setInterval(() => { if (this.page != 1) { return; } this.loadJobs(-1, true); }, 3000); }, /** * Load the jobs for the previous page. */ previous() { this.loadJobs( (this.page - 2) * this.perPage - 1 ); this.page -= 1; this.hasNewEntries = false; }, /** * Load the jobs for the next page. */ next() { this.loadJobs( this.page * this.perPage - 1 ); this.page += 1; this.hasNewEntries = false; }, /** * Update the page title. */ updatePageTitle() { document.title = this.$route.params.type == 'pending' ? 'Horizon - Pending Jobs' : ( this.$route.params.type == 'silenced' ? 'Horizon - Silenced Jobs' : 'Horizon - Completed Jobs' ); } } } </script> <template> <div> <div class="card overflow-hidden"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0" v-if="$route.params.type == 'pending'">Pending Jobs</h2> <h2 class="h6 m-0" v-if="$route.params.type == 'completed'">Completed Jobs</h2> <h2 class="h6 m-0" v-if="$route.params.type == 'silenced'">Silenced Jobs</h2> </div> <div v-if="!ready" class="d-flex align-items-center justify-content-center card-bg-secondary p-5 bottom-radius"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="icon spin me-2 fill-text-color"> <path d="M12 10a2 2 0 0 1-3.41 1.41A2 2 0 0 1 10 8V0a9.97 9.97 0 0 1 10 10h-8zm7.9 1.41A10 10 0 1 1 8.59.1v2.03a8 8 0 1 0 9.29 9.29h2.02zm-4.07 0a6 6 0 1 1-7.25-7.25v2.1a3.99 3.99 0 0 0-1.4 6.57 4 4 0 0 0 6.56-1.42h2.1z"></path> </svg> <span>Loading...</span> </div> <div v-if="ready && jobs.length == 0" class="d-flex flex-column align-items-center justify-content-center card-bg-secondary p-5 bottom-radius"> <span>There aren't any jobs.</span> </div> <table v-if="ready && jobs.length > 0" class="table table-hover mb-0"> <thead> <tr> <th>Job</th> <th v-if="$route.params.type=='pending'" class="text-end">Queued</th> <th v-if="$route.params.type=='completed' || $route.params.type=='silenced'">Queued</th> <th v-if="$route.params.type=='completed' || $route.params.type=='silenced'">Completed</th> <th v-if="$route.params.type=='completed' || $route.params.type=='silenced'" class="text-end">Runtime</th> </tr> </thead> <tbody> <tr v-if="hasNewEntries" key="newEntries" class="dontanimate"> <td colspan="100" class="text-center card-bg-secondary py-1"> <small><a href="#" v-on:click.prevent="loadNewEntries" v-if="!loadingNewEntries">Load New Entries</a></small> <small v-if="loadingNewEntries">Loading...</small> </td> </tr> <tr v-for="job in jobs" :key="job.id" :job="job" is="job-row"> </tr> </tbody> </table> <div v-if="ready && jobs.length" class="p-3 d-flex justify-content-between border-top"> <button @click="previous" class="btn btn-secondary btn-sm" :disabled="page==1">Previous</button> <button @click="next" class="btn btn-secondary btn-sm" :disabled="page>=totalPages">Next</button> </div> </div> </div> </template> PK ! Z#��' ' js/screens/recentJobs/job.vuenu ȯ�� <template> <div> <div class="card overflow-hidden"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0" v-if="!ready">Job Preview</h2> <h2 class="h6 m-0" v-if="ready">{{job.name}}</h2> <a data-bs-toggle="collapse" href="#collapseDetails" role="button"> Collapse </a> </div> <div v-if="!ready" class="d-flex align-items-center justify-content-center card-bg-secondary p-5 bottom-radius"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="icon spin me-2 fill-text-color"> <path d="M12 10a2 2 0 0 1-3.41 1.41A2 2 0 0 1 10 8V0a9.97 9.97 0 0 1 10 10h-8zm7.9 1.41A10 10 0 1 1 8.59.1v2.03a8 8 0 1 0 9.29 9.29h2.02zm-4.07 0a6 6 0 1 1-7.25-7.25v2.1a3.99 3.99 0 0 0-1.4 6.57 4 4 0 0 0 6.56-1.42h2.1z"></path> </svg> <span>Loading...</span> </div> <div class="card-body card-bg-secondary collapse show" id="collapseDetails" v-if="ready"> <div class="row mb-2"> <div class="col-md-2 text-muted">ID</div> <div class="col">{{job.id}}</div> </div> <div class="row mb-2"> <div class="col-md-2 text-muted">Queue</div> <div class="col">{{job.queue}}</div> </div> <div class="row mb-2"> <div class="col-md-2 text-muted">Pushed</div> <div class="col">{{ readableTimestamp(job.payload.pushedAt) }}</div> </div> <div class="row mb-2" v-if="prettyPrintJob(job.payload.data).batchId"> <div class="col-md-2 text-muted">Batch</div> <div class="col"> <router-link :to="{ name: 'batches-preview', params: { batchId: prettyPrintJob(job.payload.data).batchId }}"> {{ prettyPrintJob(job.payload.data).batchId }} </router-link> </div> </div> <div class="row mb-2" v-if="delayed"> <div class="col-md-2 text-muted">Delayed Until</div> <div class="col">{{delayed}}</div> </div> <div class="row"> <div class="col-md-2 text-muted">Completed</div> <div class="col" v-if="job.completed_at">{{readableTimestamp(job.completed_at)}}</div> <div class="col" v-else>-</div> </div> </div> </div> <div class="card overflow-hidden mt-4" v-if="ready"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0">Data</h2> <a data-bs-toggle="collapse" href="#collapseData" role="button"> Collapse </a> </div> <div class="card-body code-bg text-white collapse show" id="collapseData"> <vue-json-pretty :data="prettyPrintJob(job.payload.data)"></vue-json-pretty> </div> </div> <div class="card overflow-hidden mt-4" v-if="ready && job.payload.tags.length"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0">Tags</h2> <a data-bs-toggle="collapse" href="#collapseTags" role="button"> Collapse </a> </div> <div class="card-body code-bg text-white collapse show" id="collapseTags"> <vue-json-pretty :data="job.payload.tags"></vue-json-pretty> </div> </div> </div> </template> <script type="text/ecmascript-6"> import phpunserialize from 'phpunserialize'; import moment from 'moment-timezone'; import StackTrace from './../../components/Stacktrace.vue'; export default { components: { 'stack-trace': StackTrace, }, data() { return { ready: false, job: {} }; }, computed: { unserialized() { return phpunserialize(this.job.payload.data.command); }, delayed() { let unserialized; try { unserialized = phpunserialize(this.job.payload.data.command); }catch(err){ // } if (unserialized && unserialized.delay && unserialized.delay.date) { return moment.tz(unserialized.delay.date, unserialized.delay.timezone) .local() .format('YYYY-MM-DD HH:mm:ss'); } else if (unserialized && unserialized.delay) { return this.formatDate(this.job.payload.pushedAt).add(unserialized.delay, 'seconds') .local() .format('YYYY-MM-DD HH:mm:ss'); } return null; }, }, mounted() { this.loadJob(this.$route.params.jobId); document.title = "Horizon - Job Detail"; }, methods: { /** * Load a job by the given ID. */ loadJob(id) { this.ready = false; this.$http.get(Horizon.basePath + '/api/jobs/' + id) .then(response => { this.job = response.data; this.ready = true; }); }, /** * Pretty print serialized job. */ prettyPrintJob(data) { try { return data.command && !data.command.includes('CallQueuedClosure') ? phpunserialize(data.command) : data; } catch (err) { return data; } } } } </script> PK ! ���7 ! js/screens/recentJobs/job-row.vuenu ȯ�� <template> <tr> <td> <router-link :title="job.name" :to="{ name: $route.params.type+'-jobs-preview', params: { jobId: job.id }}"> {{ jobBaseName(job.name) }} </router-link> <small class="ms-1 badge bg-secondary badge-sm" :title="`Delayed for ${delayed}`" v-if="delayed && (job.status == 'reserved' || job.status == 'pending')"> Delayed </small> <br> <small class="text-muted"> Queue: {{job.queue}} <span v-if="job.payload.tags && job.payload.tags.length" class="text-break"> | Tags: {{ job.payload.tags && job.payload.tags.length ? job.payload.tags.slice(0,3).join(', ') : '' }}<span v-if="job.payload.tags.length > 3"> ({{ job.payload.tags.length - 3 }} more)</span> </span> </small> </td> <td class="table-fit text-muted"> {{ readableTimestamp(job.payload.pushedAt) }} </td> <td v-if="$route.params.type=='completed' || $route.params.type=='silenced'" class="table-fit text-muted"> {{ readableTimestamp(job.completed_at) }} </td> <td v-if="$route.params.type=='completed' || $route.params.type=='silenced'" class="table-fit text-end text-muted"> <span>{{ job.completed_at ? (job.completed_at - job.reserved_at).toFixed(2)+'s' : '-' }}</span> </td> </tr> </template> <script type="text/ecmascript-6"> import phpunserialize from 'phpunserialize' import moment from 'moment-timezone'; export default { props: { job: { type: Object, required: true } }, computed: { unserialized() { try { return phpunserialize(this.job.payload.data.command); }catch(err){ // } }, delayed() { if (this.unserialized && this.unserialized.delay && this.unserialized.delay.date) { return moment.tz(this.unserialized.delay.date, this.unserialized.delay.timezone) .fromNow(true); } else if (this.unserialized && this.unserialized.delay) { return this.formatDate(this.job.payload.pushedAt).add(this.unserialized.delay, 'seconds') .fromNow(true); } return null; }, }, } </script> PK ! �� js/screens/monitoring/tag.vuenu ȯ�� <script type="text/ecmascript-6"> export default {} </script> <template> <div> <div class="card overflow-hidden"> <div class="card-header d-flex align-items-center justify-content-between"> <h2 class="h6 m-0">Recent Jobs for "{{ $route.params.tag }}"</h2> </div> <ul class="nav nav-pills card-bg-secondary"> <li class="nav-item"> <router-link class="nav-link" active-class="active" :to="{ name: 'monitoring-jobs', params: { tag:$route.params.tag }}" href="#"> Recent Jobs </router-link> </li> <li class="nav-item"> <router-link class="nav-link" active-class="active" :to="{ name: 'monitoring-failed', params: { tag:$route.params.tag }}" href="#"> Failed Jobs </router-link> </li> </ul> <router-view/> </div> </div> </template> PK ! ����# # "