diff --git a/README.md b/README.md index 8baac073..bb5f2f36 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,7 @@ Cachet 3.x is currently in development and is not yet completely ready for produ - [x] Incident Management - [x] Incident Updates - [x] Scheduled Maintenance -- [ ] Scheduled Maintenance Updates - - WIP - https://github.com/cachethq/core/pull/109 +- [x] Scheduled Maintenance Updates - [x] Components - [ ] Metrics - API and dashboard are working. diff --git a/database/factories/IncidentUpdateFactory.php b/database/factories/IncidentUpdateFactory.php deleted file mode 100644 index 14064ba6..00000000 --- a/database/factories/IncidentUpdateFactory.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * @method IncidentUpdateFactory forIncident(...$sequence) - */ -class IncidentUpdateFactory extends Factory -{ - protected $model = IncidentUpdate::class; - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - 'incident_id' => Incident::factory(), - 'status' => IncidentStatusEnum::identified->value, - 'message' => fake()->paragraph, - 'user_id' => 1, // @todo decide how to handle storing of users... nullable? - ]; - } -} diff --git a/database/factories/UpdateFactory.php b/database/factories/UpdateFactory.php new file mode 100644 index 00000000..5aa4066c --- /dev/null +++ b/database/factories/UpdateFactory.php @@ -0,0 +1,61 @@ + + */ +class UpdateFactory extends Factory +{ + protected $model = Update::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'updateable_id' => Incident::factory(), + 'updateable_type' => Relation::getMorphAlias(Incident::class), + 'status' => IncidentStatusEnum::identified->value, + 'message' => fake()->paragraph, + 'user_id' => 1, + ]; + } + + /** + * Create an update for an incident. + */ + public function forIncident(?Incident $incident = null): self + { + return $this->state([ + 'updateable_id' => $component->id ?? Incident::factory(), + 'updateable_type' => Relation::getMorphAlias(Incident::class), + 'status' => IncidentStatusEnum::identified->value, + ]); + } + + /** + * Create an update for a schedule. + */ + public function forSchedule(?Schedule $schedule = null): self + { + return $this->state([ + 'updateable_id' => $schedule->id ?? Schedule::factory(), + 'updateable_type' => Relation::getMorphAlias(Schedule::class), + 'status' => null, + 'user_id' => null, + ]); + } +} diff --git a/database/migrations/2024_10_11_150422_add_morphable_columns_to_incident_updates.php b/database/migrations/2024_10_11_150422_add_morphable_columns_to_incident_updates.php new file mode 100644 index 00000000..b0666184 --- /dev/null +++ b/database/migrations/2024_10_11_150422_add_morphable_columns_to_incident_updates.php @@ -0,0 +1,19 @@ +morphs('updateable'); + $table->unsignedTinyInteger('status')->nullable()->change(); + }); + } +}; diff --git a/database/migrations/2024_10_11_150451_migrate_incident_updates_to_morphables.php b/database/migrations/2024_10_11_150451_migrate_incident_updates_to_morphables.php new file mode 100644 index 00000000..637236a3 --- /dev/null +++ b/database/migrations/2024_10_11_150451_migrate_incident_updates_to_morphables.php @@ -0,0 +1,20 @@ +update([ + 'updateable_type' => 'incident', + 'updateable_id' => DB::raw('incident_id') + ]); + } +}; diff --git a/database/migrations/2024_10_11_150534_drop_incident_id_from_incident_updates_table.php b/database/migrations/2024_10_11_150534_drop_incident_id_from_incident_updates_table.php new file mode 100644 index 00000000..5666ff8c --- /dev/null +++ b/database/migrations/2024_10_11_150534_drop_incident_id_from_incident_updates_table.php @@ -0,0 +1,19 @@ +dropIndex('incident_updates_incident_id_index'); + $table->dropColumn('incident_id'); + }); + } +}; diff --git a/database/migrations/2024_10_11_150552_rename_incident_updates_to_updates.php b/database/migrations/2024_10_11_150552_rename_incident_updates_to_updates.php new file mode 100644 index 00000000..9bdca9af --- /dev/null +++ b/database/migrations/2024_10_11_150552_rename_incident_updates_to_updates.php @@ -0,0 +1,16 @@ +truncate(); DB::table('incidents')->truncate(); - DB::table('incident_updates')->truncate(); DB::table('components')->truncate(); DB::table('component_groups')->truncate(); DB::table('schedules')->truncate(); DB::table('metrics')->truncate(); DB::table('metric_points')->truncate(); + DB::table('updates')->truncate(); /** @var \Illuminate\Foundation\Auth\User $userModel */ $userModel = config('cachet.user_model'); @@ -56,12 +57,24 @@ public function run(): void 'completed_at' => now()->subHours(12), ]); - Schedule::create([ + tap(Schedule::create([ 'name' => 'Documentation Maintenance', 'message' => 'We will be conducting maintenance on our documentation servers. You may experience degraded performance during this time.', 'scheduled_at' => now()->addHours(24), 'completed_at' => null, - ]); + ]), function (Schedule $schedule) use ($user) { + $update = new Update([ + 'message' => <<<'EOF' +This scheduled maintenance period has been pushed back by one hour. +EOF + , + 'user_id' => $user->id, + 'created_at' => $timestamp = $schedule->created_at->addMinutes(45), + 'updated_at' => $timestamp, + ]); + + $schedule->updates()->save($update); + }); $componentGroup = ComponentGroup::create([ 'name' => 'Cachet', @@ -122,7 +135,7 @@ public function run(): void 'updated_at' => $timestamp, 'occurred_at' => $timestamp, ]), function (Incident $incident) use ($user) { - $incident->incidentUpdates()->create([ + $update = new Update([ 'status' => IncidentStatusEnum::identified, 'message' => 'We\'ve confirmed the issue is with our DNS provider. We\'re waiting on them to provide an ETA.', 'user_id' => $user->id, @@ -130,7 +143,9 @@ public function run(): void 'updated_at' => $timestamp, ]); - $incident->incidentUpdates()->create([ + $incident->updates()->save($update); + + $update = new Update([ 'status' => IncidentStatusEnum::fixed, 'message' => <<<'EOF' Our DNS provider has fixed the issue. We will continue to monitor the situation. @@ -142,6 +157,8 @@ public function run(): void 'created_at' => $timestamp = $incident->created_at->addMinutes(45), 'updated_at' => $timestamp, ]); + + $incident->updates()->save($update); }); $incident = Incident::create([ @@ -155,20 +172,22 @@ public function run(): void 'occurred_at' => $timestamp, ]); - $incident->incidentUpdates()->create([ + $update = new Update([ 'status' => IncidentStatusEnum::identified, 'message' => 'We\'ve identified the issue and are working on a fix.', 'created_at' => $timestamp = $incident->created_at->addMinutes(15), 'updated_at' => $timestamp, ]); - $incident->incidentUpdates()->create([ + $incident->updates()->create([ 'status' => IncidentStatusEnum::fixed, 'message' => 'The documentation is now back online. Happy reading!', 'created_at' => $timestamp = $incident->created_at->addMinutes(25), 'updated_at' => $timestamp, ]); + $incident->updates()->save($update); + IncidentTemplate::create([ 'name' => 'Third-Party Service Outage', 'slug' => 'third-party-service-outage', diff --git a/public/build/assets/cachet-DCZQ8JcZ.js b/public/build/assets/cachet-DCZQ8JcZ.js deleted file mode 100644 index 8a73d659..00000000 --- a/public/build/assets/cachet-DCZQ8JcZ.js +++ /dev/null @@ -1,18 +0,0 @@ -var ye=!1,xe=!1,rt=[],we=-1;function yr(t){xr(t)}function xr(t){rt.includes(t)||rt.push(t),wr()}function Mi(t){let e=rt.indexOf(t);e!==-1&&e>we&&rt.splice(e,1)}function wr(){!xe&&!ye&&(ye=!0,queueMicrotask($r))}function $r(){ye=!1,xe=!0;for(let t=0;tt.effect(e,{scheduler:i=>{$e?yr(i):i()}}),Li=t.raw}function ui(t){ft=t}function Sr(t){let e=()=>{};return[n=>{let r=ft(n);return t._x_effects||(t._x_effects=new Set,t._x_runEffects=()=>{t._x_effects.forEach(s=>s())}),t._x_effects.add(r),e=()=>{r!==void 0&&(t._x_effects.delete(r),gt(r))},r},()=>{e()}]}function Bi(t,e){let i=!0,n,r=ft(()=>{let s=t();JSON.stringify(s),i?n=s:queueMicrotask(()=>{e(s,n),n=s}),i=!1});return()=>gt(r)}var ji=[],Vi=[],Wi=[];function Tr(t){Wi.push(t)}function Be(t,e){typeof e=="function"?(t._x_cleanups||(t._x_cleanups=[]),t._x_cleanups.push(e)):(e=t,Vi.push(e))}function qi(t){ji.push(t)}function Hi(t,e,i){t._x_attributeCleanups||(t._x_attributeCleanups={}),t._x_attributeCleanups[e]||(t._x_attributeCleanups[e]=[]),t._x_attributeCleanups[e].push(i)}function zi(t,e){t._x_attributeCleanups&&Object.entries(t._x_attributeCleanups).forEach(([i,n])=>{(e===void 0||e.includes(i))&&(n.forEach(r=>r()),delete t._x_attributeCleanups[i])})}function Cr(t){if(t._x_cleanups)for(;t._x_cleanups.length;)t._x_cleanups.pop()()}var je=new MutationObserver(He),Ve=!1;function We(){je.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),Ve=!0}function Ui(){kr(),je.disconnect(),Ve=!1}var xt=[];function kr(){let t=je.takeRecords();xt.push(()=>t.length>0&&He(t));let e=xt.length;queueMicrotask(()=>{if(xt.length===e)for(;xt.length>0;)xt.shift()()})}function k(t){if(!Ve)return t();Ui();let e=t();return We(),e}var qe=!1,Ut=[];function Ir(){qe=!0}function Dr(){qe=!1,He(Ut),Ut=[]}function He(t){if(qe){Ut=Ut.concat(t);return}let e=new Set,i=new Set,n=new Map,r=new Map;for(let s=0;sa.nodeType===1&&e.add(a)),t[s].removedNodes.forEach(a=>a.nodeType===1&&i.add(a))),t[s].type==="attributes")){let a=t[s].target,o=t[s].attributeName,l=t[s].oldValue,u=()=>{n.has(a)||n.set(a,[]),n.get(a).push({name:o,value:a.getAttribute(o)})},c=()=>{r.has(a)||r.set(a,[]),r.get(a).push(o)};a.hasAttribute(o)&&l===null?u():a.hasAttribute(o)?(c(),u()):c()}r.forEach((s,a)=>{zi(a,s)}),n.forEach((s,a)=>{ji.forEach(o=>o(a,s))});for(let s of i)e.has(s)||Vi.forEach(a=>a(s));e.forEach(s=>{s._x_ignoreSelf=!0,s._x_ignore=!0});for(let s of e)i.has(s)||s.isConnected&&(delete s._x_ignoreSelf,delete s._x_ignore,Wi.forEach(a=>a(s)),s._x_ignore=!0,s._x_ignoreSelf=!0);e.forEach(s=>{delete s._x_ignoreSelf,delete s._x_ignore}),e=null,i=null,n=null,r=null}function Gi(t){return Dt(ht(t))}function It(t,e,i){return t._x_dataStack=[e,...ht(i||t)],()=>{t._x_dataStack=t._x_dataStack.filter(n=>n!==e)}}function ht(t){return t._x_dataStack?t._x_dataStack:typeof ShadowRoot=="function"&&t instanceof ShadowRoot?ht(t.host):t.parentNode?ht(t.parentNode):[]}function Dt(t){return new Proxy({objects:t},Nr)}var Nr={ownKeys({objects:t}){return Array.from(new Set(t.flatMap(e=>Object.keys(e))))},has({objects:t},e){return e==Symbol.unscopables?!1:t.some(i=>Object.prototype.hasOwnProperty.call(i,e)||Reflect.has(i,e))},get({objects:t},e,i){return e=="toJSON"?Rr:Reflect.get(t.find(n=>Reflect.has(n,e))||{},e,i)},set({objects:t},e,i,n){const r=t.find(a=>Object.prototype.hasOwnProperty.call(a,e))||t[t.length-1],s=Object.getOwnPropertyDescriptor(r,e);return s!=null&&s.set&&(s!=null&&s.get)?s.set.call(n,i)||!0:Reflect.set(r,e,i)}};function Rr(){return Reflect.ownKeys(this).reduce((e,i)=>(e[i]=Reflect.get(this,i),e),{})}function Qi(t){let e=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,i=(n,r="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([s,{value:a,enumerable:o}])=>{if(o===!1||a===void 0||typeof a=="object"&&a!==null&&a.__v_skip)return;let l=r===""?s:`${r}.${s}`;typeof a=="object"&&a!==null&&a._x_interceptor?n[s]=a.initialize(t,l,s):e(a)&&a!==n&&!(a instanceof Element)&&i(a,l)})};return i(t)}function Ji(t,e=()=>{}){let i={initialValue:void 0,_x_interceptor:!0,initialize(n,r,s){return t(this.initialValue,()=>Pr(n,r),a=>Ee(n,r,a),r,s)}};return e(i),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let r=i.initialize.bind(i);i.initialize=(s,a,o)=>{let l=n.initialize(s,a,o);return i.initialValue=l,r(s,a,o)}}else i.initialValue=n;return i}}function Pr(t,e){return e.split(".").reduce((i,n)=>i[n],t)}function Ee(t,e,i){if(typeof e=="string"&&(e=e.split(".")),e.length===1)t[e[0]]=i;else{if(e.length===0)throw error;return t[e[0]]||(t[e[0]]={}),Ee(t[e[0]],e.slice(1),i)}}var Yi={};function L(t,e){Yi[t]=e}function Oe(t,e){return Object.entries(Yi).forEach(([i,n])=>{let r=null;function s(){if(r)return r;{let[a,o]=rn(e);return r={interceptor:Ji,...a},Be(e,o),r}}Object.defineProperty(t,`$${i}`,{get(){return n(e,s())},enumerable:!1})}),t}function Fr(t,e,i,...n){try{return i(...n)}catch(r){Ct(r,t,e)}}function Ct(t,e,i=void 0){t=Object.assign(t??{message:"No error message given."},{el:e,expression:i}),console.warn(`Alpine Expression Error: ${t.message} - -${i?'Expression: "'+i+`" - -`:""}`,e),setTimeout(()=>{throw t},0)}var qt=!0;function Xi(t){let e=qt;qt=!1;let i=t();return qt=e,i}function st(t,e,i={}){let n;return N(t,e)(r=>n=r,i),n}function N(...t){return Zi(...t)}var Zi=tn;function Ar(t){Zi=t}function tn(t,e){let i={};Oe(i,t);let n=[i,...ht(t)],r=typeof e=="function"?Kr(n,e):Lr(n,e,t);return Fr.bind(null,t,e,r)}function Kr(t,e){return(i=()=>{},{scope:n={},params:r=[]}={})=>{let s=e.apply(Dt([n,...t]),r);Gt(i,s)}}var pe={};function Mr(t,e){if(pe[t])return pe[t];let i=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(t.trim())||/^(let|const)\s/.test(t.trim())?`(async()=>{ ${t} })()`:t,s=(()=>{try{let a=new i(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(a,"name",{value:`[Alpine] ${t}`}),a}catch(a){return Ct(a,e,t),Promise.resolve()}})();return pe[t]=s,s}function Lr(t,e,i){let n=Mr(e,i);return(r=()=>{},{scope:s={},params:a=[]}={})=>{n.result=void 0,n.finished=!1;let o=Dt([s,...t]);if(typeof n=="function"){let l=n(n,o).catch(u=>Ct(u,i,e));n.finished?(Gt(r,n.result,o,a,i),n.result=void 0):l.then(u=>{Gt(r,u,o,a,i)}).catch(u=>Ct(u,i,e)).finally(()=>n.result=void 0)}}}function Gt(t,e,i,n,r){if(qt&&typeof e=="function"){let s=e.apply(i,n);s instanceof Promise?s.then(a=>Gt(t,a,i,n)).catch(a=>Ct(a,r,e)):t(s)}else typeof e=="object"&&e instanceof Promise?e.then(s=>t(s)):t(e)}var ze="x-";function mt(t=""){return ze+t}function Br(t){ze=t}var Qt={};function C(t,e){return Qt[t]=e,{before(i){if(!Qt[i]){console.warn(String.raw`Cannot find directive \`${i}\`. \`${t}\` will use the default order of execution`);return}const n=nt.indexOf(i);nt.splice(n>=0?n:nt.indexOf("DEFAULT"),0,t)}}}function jr(t){return Object.keys(Qt).includes(t)}function Ue(t,e,i){if(e=Array.from(e),t._x_virtualDirectives){let s=Object.entries(t._x_virtualDirectives).map(([o,l])=>({name:o,value:l})),a=en(s);s=s.map(o=>a.find(l=>l.name===o.name)?{name:`x-bind:${o.name}`,value:`"${o.value}"`}:o),e=e.concat(s)}let n={};return e.map(on((s,a)=>n[s]=a)).filter(un).map(qr(n,i)).sort(Hr).map(s=>Wr(t,s))}function en(t){return Array.from(t).map(on()).filter(e=>!un(e))}var Se=!1,Ot=new Map,nn=Symbol();function Vr(t){Se=!0;let e=Symbol();nn=e,Ot.set(e,[]);let i=()=>{for(;Ot.get(e).length;)Ot.get(e).shift()();Ot.delete(e)},n=()=>{Se=!1,i()};t(i),n()}function rn(t){let e=[],i=o=>e.push(o),[n,r]=Sr(t);return e.push(r),[{Alpine:Rt,effect:n,cleanup:i,evaluateLater:N.bind(N,t),evaluate:st.bind(st,t)},()=>e.forEach(o=>o())]}function Wr(t,e){let i=()=>{},n=Qt[e.type]||i,[r,s]=rn(t);Hi(t,e.original,s);let a=()=>{t._x_ignore||t._x_ignoreSelf||(n.inline&&n.inline(t,e,r),n=n.bind(n,t,e,r),Se?Ot.get(nn).push(n):n())};return a.runCleanups=s,a}var sn=(t,e)=>({name:i,value:n})=>(i.startsWith(t)&&(i=i.replace(t,e)),{name:i,value:n}),an=t=>t;function on(t=()=>{}){return({name:e,value:i})=>{let{name:n,value:r}=ln.reduce((s,a)=>a(s),{name:e,value:i});return n!==e&&t(n,e),{name:n,value:r}}}var ln=[];function Ge(t){ln.push(t)}function un({name:t}){return cn().test(t)}var cn=()=>new RegExp(`^${ze}([^:^.]+)\\b`);function qr(t,e){return({name:i,value:n})=>{let r=i.match(cn()),s=i.match(/:([a-zA-Z0-9\-_:]+)/),a=i.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],o=e||t[i]||i;return{type:r?r[1]:null,value:s?s[1]:null,modifiers:a.map(l=>l.replace(".","")),expression:n,original:o}}}var Te="DEFAULT",nt=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",Te,"teleport"];function Hr(t,e){let i=nt.indexOf(t.type)===-1?Te:t.type,n=nt.indexOf(e.type)===-1?Te:e.type;return nt.indexOf(i)-nt.indexOf(n)}function St(t,e,i={}){t.dispatchEvent(new CustomEvent(e,{detail:i,bubbles:!0,composed:!0,cancelable:!0}))}function J(t,e){if(typeof ShadowRoot=="function"&&t instanceof ShadowRoot){Array.from(t.children).forEach(r=>J(r,e));return}let i=!1;if(e(t,()=>i=!0),i)return;let n=t.firstElementChild;for(;n;)J(n,e),n=n.nextElementSibling}function P(t,...e){console.warn(`Alpine Warning: ${t}`,...e)}var ci=!1;function zr(){ci&&P("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),ci=!0,document.body||P("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `