From 7edd604c040e9df69c94d597e21942e74c31f6a1 Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Mon, 22 Jan 2024 20:15:49 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20improve=20incidents=20(#580)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔥 improve incidents * 🧑‍🎨 improve incident * 🚨 incident * ⌛ add queue * remove queue * 🛂 update --- apps/checker/cmd/main.go | 9 +- apps/checker/go.mod | 26 +- apps/checker/go.sum | 125 ++ apps/server/src/checker/index.ts | 176 ++- apps/web/src/app/api/checker/cron/_cron.ts | 3 +- .../incidents/(overview)/layout.tsx | 24 + .../incidents/(overview)/loading.tsx | 5 + .../(dashboard)/incidents/(overview)/page.tsx | 26 + .../(dashboard)/incidents/[id]/layout.tsx | 23 + .../(dashboard)/incidents/[id]/page.tsx | 3 + .../data-table/incident/columns.tsx | 85 + .../incident/data-table-row-actions.tsx | 107 ++ .../data-table/incident/data-table.tsx | 84 + apps/web/src/config/pages.ts | 6 + packages/api/src/edge.ts | 2 + packages/api/src/router/incident.ts | 114 ++ packages/api/src/router/statusReport.ts | 4 +- .../db/drizzle/0015_bent_sister_grimm.sql | 19 + packages/db/drizzle/meta/0015_snapshot.json | 1408 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema/incidents/incident.ts | 44 + packages/db/src/schema/incidents/index.ts | 2 + .../db/src/schema/incidents/validation.ts | 10 + packages/db/src/schema/index.ts | 1 + packages/db/src/schema/monitors/validation.ts | 6 +- 25 files changed, 2237 insertions(+), 82 deletions(-) create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/layout.tsx create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/loading.tsx create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/page.tsx create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/[id]/layout.tsx create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/[id]/page.tsx create mode 100644 apps/web/src/components/data-table/incident/columns.tsx create mode 100644 apps/web/src/components/data-table/incident/data-table-row-actions.tsx create mode 100644 apps/web/src/components/data-table/incident/data-table.tsx create mode 100644 packages/api/src/router/incident.ts create mode 100644 packages/db/drizzle/0015_bent_sister_grimm.sql create mode 100644 packages/db/drizzle/meta/0015_snapshot.json create mode 100644 packages/db/src/schema/incidents/incident.ts create mode 100644 packages/db/src/schema/incidents/index.ts create mode 100644 packages/db/src/schema/incidents/validation.ts diff --git a/apps/checker/cmd/main.go b/apps/checker/cmd/main.go index 4f587805..d324e44f 100644 --- a/apps/checker/cmd/main.go +++ b/apps/checker/cmd/main.go @@ -92,7 +92,6 @@ func main() { if err != nil { return fmt.Errorf("unable to ping: %w", err) } - statusCode := statusCode(res.StatusCode) // let's retry at least once if the status code is not successful. if !statusCode.IsSuccessful() && called < 2 { @@ -109,7 +108,9 @@ func main() { Message: res.Message, CronTimestamp: req.CronTimestamp, }) - } else if req.Status == "error" && statusCode.IsSuccessful() { + } + + if req.Status == "error" && statusCode.IsSuccessful() { // Q: Why here we check the data before updating the status in this scenario? checker.UpdateStatus(ctx, checker.UpdateData{ MonitorId: req.MonitorID, @@ -120,7 +121,6 @@ func main() { }) } - if err := tinybirdClient.SendEvent(ctx, res); err != nil { log.Ctx(ctx).Error().Err(err).Msg("failed to send event to tinybird") } @@ -141,8 +141,6 @@ func main() { log.Ctx(ctx).Error().Err(err).Msg("failed to send event to tinybird") } - // If the status was previously active, we update it to error. - // Q: Why not always updating the status? My idea is that the checker should be dumb and only check the status and return it. if req.Status == "active" { checker.UpdateStatus(ctx, checker.UpdateData{ MonitorId: req.MonitorID, @@ -151,6 +149,7 @@ func main() { Region: flyRegion, }) } + } c.JSON(http.StatusOK, gin.H{"message": "ok"}) diff --git a/apps/checker/go.mod b/apps/checker/go.mod index d3767120..ef21dbf1 100644 --- a/apps/checker/go.mod +++ b/apps/checker/go.mod @@ -10,6 +10,10 @@ require ( ) require ( + cloud.google.com/go/cloudtasks v1.12.4 // indirect + cloud.google.com/go/compute v1.23.1 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/iam v1.1.3 // indirect github.com/WilfredAlmeida/unkey-go v0.2.0 // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect @@ -20,6 +24,11 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect @@ -32,11 +41,20 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect + go.opencensus.io v0.24.0 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.9.0 // indirect - golang.org/x/net v0.10.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect + golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.16.0 // indirect - golang.org/x/text v0.9.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/api v0.149.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/apps/checker/go.sum b/apps/checker/go.sum index 41d86795..32b9d648 100644 --- a/apps/checker/go.sum +++ b/apps/checker/go.sum @@ -1,3 +1,13 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/cloudtasks v1.12.4 h1:5xXuFfAjg0Z5Wb81j2GAbB3e0bwroCeSF+5jBn/L650= +cloud.google.com/go/cloudtasks v1.12.4/go.mod h1:BEPu0Gtt2dU6FxZHNqqNdGqIG86qyWKBPGnsb7udGY0= +cloud.google.com/go/compute v1.23.1 h1:V97tBoDaZHb6leicZ1G6DLK2BAaZLJ/7+9BB/En3hR0= +cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/iam v1.1.3 h1:18tKG7DzydKWUnLjonWcJO6wjSCAtzh4GcRKlH/Hrzc= +cloud.google.com/go/iam v1.1.3/go.mod h1:3khUlaBXfPKKe7huYgEpDn6FtgRyMEqbkvBxrQyY5SE= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/WilfredAlmeida/unkey-go v0.2.0 h1:WnPDbfrwgiI2uZQmFhSATY5Q1FU8NCGtQ+h1VjJWfoI= github.com/WilfredAlmeida/unkey-go v0.2.0/go.mod h1:KZZ52al64FQUnWx0Zf2fIFmZvznwUiHqIg0nak/BGMw= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= @@ -5,13 +15,20 @@ github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -29,10 +46,40 @@ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QX github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -58,6 +105,7 @@ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNc github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -76,29 +124,106 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= +google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/apps/server/src/checker/index.ts b/apps/server/src/checker/index.ts index e529cc87..24fc47e3 100644 --- a/apps/server/src/checker/index.ts +++ b/apps/server/src/checker/index.ts @@ -1,8 +1,8 @@ import { Hono } from "hono"; import { z } from "zod"; -import { eq, schema } from "@openstatus/db"; -import { db } from "@openstatus/db/src/db"; +import { and, db, eq, isNotNull, schema } from "@openstatus/db"; +import { incidentTable } from "@openstatus/db/src/schema"; import { flyRegions } from "@openstatus/db/src/schema/monitors/constants"; import { selectMonitorSchema } from "@openstatus/db/src/schema/monitors/validation"; import { Redis } from "@openstatus/upstash"; @@ -24,63 +24,68 @@ checkerRoute.post("/updateStatus", async (c) => { const json = await c.req.json(); const payloadSchema = z.object({ monitorId: z.string(), - status: z.enum(["active", "error"]), // that's the new status message: z.string().optional(), statusCode: z.number().optional(), region: z.enum(flyRegions), cronTimestamp: z.number().optional(), + // status: z.enum(["active", "error"]), }); const result = payloadSchema.safeParse(json); if (!result.success) { - // console.error(result.error); return c.text("Unprocessable Entity", 422); } - const { monitorId, status, message, region, statusCode, cronTimestamp } = - result.data; + const { monitorId, message, region, statusCode, cronTimestamp } = result.data; console.log(`📝 update monitor status ${JSON.stringify(result.data)}`); - switch (status) { - case "active": - if (!statusCode) { - return; - } - await upsertMonitorStatus({ - monitorId: monitorId, - status: "active", - region: region, - }); - - await checkerAudit.publishAuditLog({ - id: `monitor:${monitorId}`, - action: "monitor.recovered", - targets: [{ id: monitorId, type: "monitor" }], - metadata: { - region: region, - statusCode: statusCode, - cronTimestamp: cronTimestamp, - }, - }); - break; - case "error": { - await upsertMonitorStatus({ - monitorId: monitorId, - status: "error", + // we check if it's an error + // If status not in 200> and <300 + // if there's no incident create one and notify + // publish event to TB + + // if status is ok checked if there's an open incident + // if open incident publish incident recovered + const incident = await db + .select() + .from(incidentTable) + .where( + and( + eq(incidentTable.monitorId, Number(monitorId)), + isNotNull(incidentTable.resolvedAt), + isNotNull(incidentTable.acknowledgedAt), + ), + ); + + // if we are in error + if (statusCode && statusCode < 200 && statusCode > 300) { + // create incident + // trigger alerting + await checkerAudit.publishAuditLog({ + id: `monitor:${monitorId}`, + action: "monitor.failed", + targets: [{ id: monitorId, type: "monitor" }], + metadata: { region: region, - }); - // ALPHA - await checkerAudit.publishAuditLog({ - id: `monitor:${monitorId}`, - action: "monitor.failed", - targets: [{ id: monitorId, type: "monitor" }], - metadata: { - region: region, - statusCode: statusCode, - message, - cronTimestamp, - }, - }); + statusCode: statusCode, + message, + }, + }); + // We upsert the status of the monitor + await upsertMonitorStatus({ + monitorId: monitorId, + status: "error", + region: region, + }); + + if (!incident) { + const redisKey = `${monitorId}-${cronTimestamp}-error`; + // We add the new region to the set + await redis.sadd(redisKey, region); + // let's add an expire to the set + await redis.expire(redisKey, 60 * 60 * 24); + // We get the number of regions affected + const nbAffectedRegion = await redis.scard(redisKey); const currentMonitor = await db .select() @@ -94,30 +99,71 @@ checkerRoute.post("/updateStatus", async (c) => { console.log("cronTimestamp is undefined"); } - const redisKey = `${monitorId}-${cronTimestamp}`; - // We add the new region to the set - await redis.sadd(redisKey, region); - // let's add an expire to the set - await redis.expire(redisKey, 60 * 60 * 24); - // We get the number of regions affected - const nbAffectedRegion = await redis.scard(redisKey); + const numberOfRegions = monitor.regions.length; // If the number of affected regions is greater than half of the total region, we trigger the alerting - if (nbAffectedRegion < monitor.regions.length / 2) { - console.log( - `Not enough affected regions (${nbAffectedRegion}/${flyRegions.length})`, - ); - break; + // 4 of 6 monitor need to fail to trigger an alerting + if (nbAffectedRegion > numberOfRegions / 2) { + await triggerAlerting({ monitorId, statusCode, message, region }); + // create the incident and trigger the alerting + await db.insert(incidentTable).values({ + monitorId: Number(monitorId), + workspaceId: monitor.workspaceId, + startedAt: new Date(), + }); } - // Add a - await triggerAlerting({ - monitorId: monitorId, - region: env.FLY_REGION, - statusCode, - message, - }); - break; } } + + if (statusCode && statusCode >= 200 && statusCode < 300) { + await upsertMonitorStatus({ + monitorId: monitorId, + status: "active", + region: region, + }); + + await checkerAudit.publishAuditLog({ + id: `monitor:${monitorId}`, + action: "monitor.recovered", + targets: [{ id: monitorId, type: "monitor" }], + metadata: { region: region, statusCode: Number(statusCode) }, + }); + + // FIX: TO BE IMPROVED + // if (incident) { + // const redisKey = `${monitorId}-${cronTimestamp}-resolved`; + // // We add the new region to the set + // await redis.sadd(redisKey, region); + // // let's add an expire to the set + // await redis.expire(redisKey, 60 * 60 * 24); + // // We get the number of regions affected + // const nbAffectedRegion = await redis.scard(redisKey); + + // const currentMonitor = await db + // .select() + // .from(schema.monitor) + // .where(eq(schema.monitor.id, Number(monitorId))) + // .get(); + + // const monitor = selectMonitorSchema.parse(currentMonitor); + + // if (!cronTimestamp) { + // console.log("cronTimestamp is undefined"); + // } + + // const numberOfRegions = monitor.regions.length; + + // // If the number of affected regions is greater than half of the total region, we trigger the alerting + // // 4 of 6 monitor need to fail to trigger an alerting + // if (nbAffectedRegion > numberOfRegions / 2) { + // // Trigger recovery notification + // // await triggerRecovery({ monitorId, statusCode, message, region }); + // await db.update(incidentTable).set({ + // resolvedAt: new Date(), + // }); + // } + // } + } + return c.text("Ok", 200); }); diff --git a/apps/web/src/app/api/checker/cron/_cron.ts b/apps/web/src/app/api/checker/cron/_cron.ts index 4c7e2978..cc69fe17 100644 --- a/apps/web/src/app/api/checker/cron/_cron.ts +++ b/apps/web/src/app/api/checker/cron/_cron.ts @@ -64,8 +64,7 @@ export const cron = async ({ for (const region of selectedRegions) { const status = - monitorStatus.find(({ region }) => region === region)?.status || - "active"; + monitorStatus.find((m) => region === m.region)?.status || "active"; const response = await createCronTask({ row, timestamp, diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/layout.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/layout.tsx new file mode 100644 index 00000000..e7b30568 --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/layout.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; + +import { Header } from "@/components/dashboard/header"; +import { HelpCallout } from "@/components/dashboard/help-callout"; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
+
{children}
+
+ +
+
+ ); +} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/loading.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/loading.tsx new file mode 100644 index 00000000..ae51d6a5 --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/loading.tsx @@ -0,0 +1,5 @@ +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; + +export default function Loading() { + return ; +} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/page.tsx new file mode 100644 index 00000000..a5dd2d63 --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/(overview)/page.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; + +import { EmptyState } from "@/components/dashboard/empty-state"; +import { columns } from "@/components/data-table/incident/columns"; +import { DataTable } from "@/components/data-table/incident/data-table"; +import { api } from "@/trpc/server"; + +export default async function IncidentPage() { + const incidents = await api.incident.getAllIncidents.query(); + + if (incidents?.length === 0) + return ( + + ); + + return ( + <> + + + ); +} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/[id]/layout.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/[id]/layout.tsx new file mode 100644 index 00000000..487a8396 --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/[id]/layout.tsx @@ -0,0 +1,23 @@ +import { notFound } from "next/navigation"; + +import { api } from "@/trpc/server"; + +export default async function Layout({ + children, + params, +}: { + children: React.ReactNode; + params: { workspaceSlug: string; id: string }; +}) { + const id = params.id; + + const monitor = await api.incident.getIncidentById.query({ + id: Number(id), + }); + + if (!monitor) { + return notFound(); + } + + return
{children}
; +} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/[id]/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/[id]/page.tsx new file mode 100644 index 00000000..cf2eed2f --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/incidents/[id]/page.tsx @@ -0,0 +1,3 @@ +export default async function IncidentPage() { + return <>; +} diff --git a/apps/web/src/components/data-table/incident/columns.tsx b/apps/web/src/components/data-table/incident/columns.tsx new file mode 100644 index 00000000..96e8e5e2 --- /dev/null +++ b/apps/web/src/components/data-table/incident/columns.tsx @@ -0,0 +1,85 @@ +"use client"; + +import Link from "next/link"; +import type { ColumnDef } from "@tanstack/react-table"; + +import type { Incident } from "@openstatus/db/src/schema"; + +import { DataTableRowActions } from "./data-table-row-actions"; + +export const columns: ColumnDef[] = [ + { + accessorKey: "monitorId", + header: "Monitor", + cell: ({ row }) => { + return ( + + + {row.original.monitorName} + + + ); + }, + }, + { + accessorKey: "startedAt", + header: "Started At", + cell: ({ row }) => { + const { startedAt } = row.original; + const date = startedAt ? new Date(startedAt).toLocaleString() : "-"; + return ( +
+ + {date} + +
+ ); + }, + }, + { + accessorKey: "acknowledgetAt", + header: "Acknowledged At", + cell: ({ row }) => { + const { acknowledgedAt } = row.original; + const date = acknowledgedAt + ? new Date(acknowledgedAt).toLocaleString() + : "-"; + return ( +
+ + {date} + +
+ ); + }, + }, + { + accessorKey: "resolvedAt", + header: "Resolved At", + cell: ({ row }) => { + const { resolvedAt } = row.original; + const date = resolvedAt ? new Date(resolvedAt).toLocaleString() : "-"; + return ( +
+ + {date} + +
+ ); + }, + }, + + { + id: "actions", + cell: ({ row }) => { + return ( +
+ +
+ ); + }, + }, +]; diff --git a/apps/web/src/components/data-table/incident/data-table-row-actions.tsx b/apps/web/src/components/data-table/incident/data-table-row-actions.tsx new file mode 100644 index 00000000..ecd5158d --- /dev/null +++ b/apps/web/src/components/data-table/incident/data-table-row-actions.tsx @@ -0,0 +1,107 @@ +"use client"; + +import * as React from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import type { Row } from "@tanstack/react-table"; +import { MoreHorizontal } from "lucide-react"; + +import { selectIncidentSchema } from "@openstatus/db/src/schema"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@openstatus/ui"; + +import { LoadingAnimation } from "@/components/loading-animation"; +import { useToastAction } from "@/hooks/use-toast-action"; +import { api } from "@/trpc/client"; + +interface DataTableRowActionsProps { + row: Row; +} + +export function DataTableRowActions({ + row, +}: DataTableRowActionsProps) { + const incident = selectIncidentSchema.parse(row.original); + const router = useRouter(); + const { toast } = useToastAction(); + const [isPending, startTransition] = React.useTransition(); + + async function resolved() { + startTransition(async () => { + try { + if (!incident.id) return; + await api.incident.resolvedIncident.mutate({ id: incident.id }); + toast("success"); + router.refresh(); + } catch { + toast("error"); + } + }); + } + + async function acknowledge() { + startTransition(async () => { + try { + // const { jobType, ...rest } = monitor; + // if (!monitor.id) return; + // await api.monitor.update.mutate({ + // ...rest, + // active: !monitor.active, + // }); + if (!incident.id) return; + await api.incident.acknowledgeIncident.mutate({ id: incident.id }); + toast("success"); + router.refresh(); + } catch { + toast("error"); + } + }); + } + + return ( + + + + + + + Acknowledge + + + + + Resolved + + + + ); +} diff --git a/apps/web/src/components/data-table/incident/data-table.tsx b/apps/web/src/components/data-table/incident/data-table.tsx new file mode 100644 index 00000000..fbfabd02 --- /dev/null +++ b/apps/web/src/components/data-table/incident/data-table.tsx @@ -0,0 +1,84 @@ +"use client"; + +import * as React from "react"; +import type { ColumnDef } from "@tanstack/react-table"; +import { + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@openstatus/ui"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; +} + +// FIXME: right now, the mobile layout is messed up +// https://github.com/TanStack/table/discussions/3192#discussioncomment-6458134 + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ); +} diff --git a/apps/web/src/config/pages.ts b/apps/web/src/config/pages.ts index 244cea17..c7f009ca 100644 --- a/apps/web/src/config/pages.ts +++ b/apps/web/src/config/pages.ts @@ -16,6 +16,12 @@ export const pagesConfig: Page[] = [ href: "/monitors", icon: "activity", }, + { + title: "Incidents", + description: "All your incidents.", + href: "/incidents", + icon: "siren", + }, { title: "Status Pages", description: "Where you can see all the pages.", diff --git a/packages/api/src/edge.ts b/packages/api/src/edge.ts index 59646bb7..f52cfb1f 100644 --- a/packages/api/src/edge.ts +++ b/packages/api/src/edge.ts @@ -1,4 +1,5 @@ import { domainRouter } from "./router/domain"; +import { incidentRouter } from "./router/incident"; import { integrationRouter } from "./router/integration"; import { invitationRouter } from "./router/invitation"; import { monitorRouter } from "./router/monitor"; @@ -20,4 +21,5 @@ export const edgeRouter = createTRPCRouter({ user: userRouter, notification: notificationRouter, invitation: invitationRouter, + incident: incidentRouter, }); diff --git a/packages/api/src/router/incident.ts b/packages/api/src/router/incident.ts new file mode 100644 index 00000000..10006635 --- /dev/null +++ b/packages/api/src/router/incident.ts @@ -0,0 +1,114 @@ +import { z } from "zod"; + +import { and, eq, schema } from "@openstatus/db"; +import { selectIncidentSchema } from "@openstatus/db/src/schema"; + +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const incidentRouter = createTRPCRouter({ + getAllIncidents: protectedProcedure + .output(z.array(selectIncidentSchema)) + .query(async (opts) => { + const result = await opts.ctx.db + .select() + .from(schema.incidentTable) + .where(eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id)) + .leftJoin( + schema.monitor, + eq(schema.incidentTable.monitorId, schema.monitor.id), + ) + .all(); + return z + .array(selectIncidentSchema) + .parse( + result.map((r) => ({ ...r.incident, monitorName: r.monitor?.name })), + ); + }), + + getIncidentById: protectedProcedure + .input(z.object({ id: z.number() })) + .output(selectIncidentSchema) + .query(async (opts) => { + const currentIncident = await opts.ctx.db + .select() + .from(schema.incidentTable) + .where( + and( + eq(schema.incidentTable.id, opts.input.id), + eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id), + ), + ) + .get(); + return selectIncidentSchema.parse(currentIncident); + }), + + acknowledgeIncident: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async (opts) => { + const currentIncident = await opts.ctx.db + .select() + .from(schema.incidentTable) + .where( + and( + eq(schema.incidentTable.id, opts.input.id), + eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id), + ), + ) + .get(); + if (!currentIncident) { + throw new Error("Incident not found"); + } + if (currentIncident.acknowledgedAt) { + throw new Error("Incident already acknowledged"); + } + await opts.ctx.db + .update(schema.incidentTable) + .set({ + acknowledgedAt: new Date(), + acknowledgedBy: opts.ctx.user.id, + }) + .where( + and( + eq(schema.incidentTable.id, opts.input.id), + eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id), + ), + ); + return true; + }), + resolvedIncident: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async (opts) => { + const currentIncident = await opts.ctx.db + .select() + .from(schema.incidentTable) + .where( + and( + eq(schema.incidentTable.id, opts.input.id), + eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id), + ), + ) + .get(); + if (!currentIncident) { + throw new Error("Incident not found"); + } + if (!currentIncident.acknowledgedAt) { + throw new Error("Incident not acknowledged"); + } + if (currentIncident.resolvedAt) { + throw new Error("Incident already resolved"); + } + await opts.ctx.db + .update(schema.incidentTable) + .set({ + resolvedAt: new Date(), + resolvedBy: opts.ctx.user.id, + }) + .where( + and( + eq(schema.incidentTable.id, opts.input.id), + eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id), + ), + ); + return true; + }), +}); diff --git a/packages/api/src/router/statusReport.ts b/packages/api/src/router/statusReport.ts index ae482ca8..7ce2efac 100644 --- a/packages/api/src/router/statusReport.ts +++ b/packages/api/src/router/statusReport.ts @@ -37,7 +37,7 @@ export const statusReportRouter = createTRPCRouter({ .returning() .get(); - if (Boolean(monitors.length)) { + if (monitors.length > 0) { await opts.ctx.db .insert(monitorsToStatusReport) .values( @@ -50,7 +50,7 @@ export const statusReportRouter = createTRPCRouter({ .get(); } - if (Boolean(pages.length)) { + if (pages.length > 0) { await opts.ctx.db .insert(pagesToStatusReports) .values( diff --git a/packages/db/drizzle/0015_bent_sister_grimm.sql b/packages/db/drizzle/0015_bent_sister_grimm.sql new file mode 100644 index 00000000..fdf48ec0 --- /dev/null +++ b/packages/db/drizzle/0015_bent_sister_grimm.sql @@ -0,0 +1,19 @@ +CREATE TABLE `incident` ( + `id` integer PRIMARY KEY NOT NULL, + `title` text DEFAULT '' NOT NULL, + `summary` text DEFAULT '' NOT NULL, + `status` text DEFAULT 'triage' NOT NULL, + `monitor_id` integer, + `workspace_id` integer, + `started_at` integer, + `acknowledged_at` integer, + `acknowledged_by` integer, + `resolved_at` integer, + `resolved_by` integer, + `created_at` integer DEFAULT (strftime('%s', 'now')), + `updated_at` integer DEFAULT (strftime('%s', 'now')), + FOREIGN KEY (`monitor_id`) REFERENCES `monitor`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`acknowledged_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`resolved_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action +); diff --git a/packages/db/drizzle/meta/0015_snapshot.json b/packages/db/drizzle/meta/0015_snapshot.json new file mode 100644 index 00000000..626b8af5 --- /dev/null +++ b/packages/db/drizzle/meta/0015_snapshot.json @@ -0,0 +1,1408 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "f5a77857-319d-4575-b570-f0aacb9eab35", + "prevId": "4cc8ce9b-ed70-47c1-8218-2cbc02743563", + "tables": { + "status_report_to_monitors": { + "name": "status_report_to_monitors", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_report_id": { + "name": "status_report_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "status_report_to_monitors_monitor_id_monitor_id_fk": { + "name": "status_report_to_monitors_monitor_id_monitor_id_fk", + "tableFrom": "status_report_to_monitors", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_report_to_monitors_status_report_id_status_report_id_fk": { + "name": "status_report_to_monitors_status_report_id_status_report_id_fk", + "tableFrom": "status_report_to_monitors", + "tableTo": "status_report", + "columnsFrom": [ + "status_report_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "status_report_to_monitors_monitor_id_status_report_id_pk": { + "columns": [ + "monitor_id", + "status_report_id" + ] + } + }, + "uniqueConstraints": {} + }, + "status_reports_to_pages": { + "name": "status_reports_to_pages", + "columns": { + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_report_id": { + "name": "status_report_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "status_reports_to_pages_page_id_page_id_fk": { + "name": "status_reports_to_pages_page_id_page_id_fk", + "tableFrom": "status_reports_to_pages", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_reports_to_pages_status_report_id_status_report_id_fk": { + "name": "status_reports_to_pages_status_report_id_status_report_id_fk", + "tableFrom": "status_reports_to_pages", + "tableTo": "status_report", + "columnsFrom": [ + "status_report_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "status_reports_to_pages_page_id_status_report_id_pk": { + "columns": [ + "page_id", + "status_report_id" + ] + } + }, + "uniqueConstraints": {} + }, + "status_report": { + "name": "status_report", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "status_report_workspace_id_workspace_id_fk": { + "name": "status_report_workspace_id_workspace_id_fk", + "tableFrom": "status_report", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "status_report_update": { + "name": "status_report_update", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text(4)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_report_id": { + "name": "status_report_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "status_report_update_status_report_id_status_report_id_fk": { + "name": "status_report_update_status_report_id_status_report_id_fk", + "tableFrom": "status_report_update", + "tableTo": "status_report", + "columnsFrom": [ + "status_report_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential": { + "name": "credential", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_workspace_id_workspace_id_fk": { + "name": "integration_workspace_id_workspace_id_fk", + "tableFrom": "integration", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "page": { + "name": "page", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "slug": { + "name": "slug", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "custom_domain": { + "name": "custom_domain", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published": { + "name": "published", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "page_slug_unique": { + "name": "page_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "page_workspace_id_workspace_id_fk": { + "name": "page_workspace_id_workspace_id_fk", + "tableFrom": "page", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "monitor": { + "name": "monitor", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "job_type": { + "name": "job_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'other'" + }, + "periodicity": { + "name": "periodicity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'other'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "regions": { + "name": "regions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "url": { + "name": "url", + "type": "text(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'GET'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "monitor_workspace_id_workspace_id_fk": { + "name": "monitor_workspace_id_workspace_id_fk", + "tableFrom": "monitor", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "monitors_to_pages": { + "name": "monitors_to_pages", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "monitors_to_pages_monitor_id_monitor_id_fk": { + "name": "monitors_to_pages_monitor_id_monitor_id_fk", + "tableFrom": "monitors_to_pages", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "monitors_to_pages_page_id_page_id_fk": { + "name": "monitors_to_pages_page_id_page_id_fk", + "tableFrom": "monitors_to_pages", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "monitors_to_pages_monitor_id_page_id_pk": { + "columns": [ + "monitor_id", + "page_id" + ] + } + }, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "user_tenant_id_unique": { + "name": "user_tenant_id_unique", + "columns": [ + "tenant_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users_to_workspaces": { + "name": "users_to_workspaces", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_workspaces_user_id_user_id_fk": { + "name": "users_to_workspaces_user_id_user_id_fk", + "tableFrom": "users_to_workspaces", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_workspaces_workspace_id_workspace_id_fk": { + "name": "users_to_workspaces_workspace_id_workspace_id_fk", + "tableFrom": "users_to_workspaces", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_workspaces_user_id_workspace_id_pk": { + "columns": [ + "user_id", + "workspace_id" + ] + } + }, + "uniqueConstraints": {} + }, + "page_subscriber": { + "name": "page_subscriber", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "page_subscriber_page_id_page_id_fk": { + "name": "page_subscriber_page_id_page_id_fk", + "tableFrom": "page_subscriber", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stripe_id": { + "name": "stripe_id", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ends_at": { + "name": "ends_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "paid_until": { + "name": "paid_until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "workspace_slug_unique": { + "name": "workspace_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "workspace_stripe_id_unique": { + "name": "workspace_stripe_id_unique", + "columns": [ + "stripe_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "notification": { + "name": "notification", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_workspace_id_workspace_id_fk": { + "name": "notification_workspace_id_workspace_id_fk", + "tableFrom": "notification", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "notifications_to_monitors": { + "name": "notifications_to_monitors", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_to_monitors_monitor_id_monitor_id_fk": { + "name": "notifications_to_monitors_monitor_id_monitor_id_fk", + "tableFrom": "notifications_to_monitors", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_to_monitors_notification_id_notification_id_fk": { + "name": "notifications_to_monitors_notification_id_notification_id_fk", + "tableFrom": "notifications_to_monitors", + "tableTo": "notification", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notifications_to_monitors_monitor_id_notification_id_pk": { + "columns": [ + "monitor_id", + "notification_id" + ] + } + }, + "uniqueConstraints": {} + }, + "monitor_status": { + "name": "monitor_status", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "monitor_status_idx": { + "name": "monitor_status_idx", + "columns": [ + "monitor_id", + "region" + ], + "isUnique": false + } + }, + "foreignKeys": { + "monitor_status_monitor_id_monitor_id_fk": { + "name": "monitor_status_monitor_id_monitor_id_fk", + "tableFrom": "monitor_status", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "monitor_status_monitor_id_region_pk": { + "columns": [ + "monitor_id", + "region" + ] + } + }, + "uniqueConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "incident": { + "name": "incident", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'triage'" + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acknowledged_by": { + "name": "acknowledged_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "incident_monitor_id_monitor_id_fk": { + "name": "incident_monitor_id_monitor_id_fk", + "tableFrom": "incident", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "incident_workspace_id_workspace_id_fk": { + "name": "incident_workspace_id_workspace_id_fk", + "tableFrom": "incident", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "incident_acknowledged_by_user_id_fk": { + "name": "incident_acknowledged_by_user_id_fk", + "tableFrom": "incident", + "tableTo": "user", + "columnsFrom": [ + "acknowledged_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "incident_resolved_by_user_id_fk": { + "name": "incident_resolved_by_user_id_fk", + "tableFrom": "incident", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 61f0d24a..3c0cda53 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -106,6 +106,13 @@ "when": 1702227904130, "tag": "0014_adorable_skaar", "breakpoints": true + }, + { + "idx": 15, + "version": "5", + "when": 1705856545397, + "tag": "0015_bent_sister_grimm", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/incidents/incident.ts b/packages/db/src/schema/incidents/incident.ts new file mode 100644 index 00000000..730e683c --- /dev/null +++ b/packages/db/src/schema/incidents/incident.ts @@ -0,0 +1,44 @@ +import { sql } from "drizzle-orm"; +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +import { monitor } from "../monitors"; +import { user } from "../users/user"; +import { workspace } from "../workspaces"; + +export const statusIncident = [ + "triage", + "investigating", + "identified", + "monitoring", + "resolved", + "duplicated", +] as const; + +export const incidentTable = sqliteTable("incident", { + id: integer("id").primaryKey(), + title: text("title").default("").notNull(), + summary: text("summary").default("").notNull(), + status: text("status", { enum: statusIncident }).default("triage").notNull(), + + // Service affected by incident + monitorId: integer("monitor_id").references(() => monitor.id), + + // Workspace where the incident happened + workspaceId: integer("workspace_id").references(() => workspace.id), + // Data related to incident timeline + startedAt: integer("started_at", { mode: "timestamp" }), + // Who has acknoledge the incident + acknowledgedAt: integer("acknowledged_at", { mode: "timestamp" }), + acknowledgedBy: integer("acknowledged_by").references(() => user.id), + + // Who has resolved it + resolvedAt: integer("resolved_at", { mode: "timestamp" }), + resolvedBy: integer("resolved_by").references(() => user.id), + + createdAt: integer("created_at", { mode: "timestamp" }).default( + sql`(strftime('%s', 'now'))`, + ), + updatedAt: integer("updated_at", { mode: "timestamp" }).default( + sql`(strftime('%s', 'now'))`, + ), +}); diff --git a/packages/db/src/schema/incidents/index.ts b/packages/db/src/schema/incidents/index.ts new file mode 100644 index 00000000..eff8f2af --- /dev/null +++ b/packages/db/src/schema/incidents/index.ts @@ -0,0 +1,2 @@ +export * from "./incident"; +export * from "./validation"; diff --git a/packages/db/src/schema/incidents/validation.ts b/packages/db/src/schema/incidents/validation.ts new file mode 100644 index 00000000..72655c06 --- /dev/null +++ b/packages/db/src/schema/incidents/validation.ts @@ -0,0 +1,10 @@ +import { createSelectSchema } from "drizzle-zod"; +import { z } from "zod"; + +import { incidentTable } from "./incident"; + +export const selectIncidentSchema = createSelectSchema(incidentTable).extend({ + monitorName: z.string().optional(), +}); + +export type Incident = z.infer; diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index e189a4a8..bdd284b9 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -9,3 +9,4 @@ export * from "./shared"; export * from "./notifications"; export * from "./monitor_status"; export * from "./invitations"; +export * from "./incidents"; diff --git a/packages/db/src/schema/monitors/validation.ts b/packages/db/src/schema/monitors/validation.ts index 954bffbe..b5260e2d 100644 --- a/packages/db/src/schema/monitors/validation.ts +++ b/packages/db/src/schema/monitors/validation.ts @@ -24,9 +24,8 @@ function stringToArrayProcess(string: T) {} const regionsToArraySchema = z.preprocess((val) => { if (String(val).length > 0) { return String(val).split(","); - } else { - return []; } + return []; }, z.array(monitorRegionSchema)); const bodyToStringSchema = z.preprocess((val) => { @@ -41,9 +40,8 @@ const headersToArraySchema = z.preprocess( } if (String(val).length > 0) { return JSON.parse(String(val)); - } else { - return []; } + return []; }, z.array(z.object({ key: z.string(), value: z.string() })).default([]), ); -- 2.51.2