diff --git a/apps/web/public/assets/changelog/status-page-colors-and-more.png b/apps/web/public/assets/changelog/status-page-colors-and-more.png new file mode 100644 index 0000000000000000000000000000000000000000..e57ef2ab85c88796f8f1aefce47a317c6ed6d1e3 GIT binary patch literal 9616 zcmeAS@N?(olHy`uVBq!ia0y~yV1B^Bz<8d6je&vTveSm`3=9mM1s;*b3=G`DAk4@x zYmNj10|R4mkh>GZx^prw85kH?(j9#r85lP9bN@+XWnfTM^>lFzsbGA2mwQ5N>D&h& z>nELSR#9Q;{KT<-X5`bS67D6Ay0Y@t3h#bsC9rWXV_TTzqPzUWm9C}JPVdUn4COWr z-ux)6(Ql<{c<%b+r=G3#7iVBt)_=5E%q(7A zrlw@3y~+9mDMrcF?v0)ar?;(rI#0bWI?VoSf@RLZ&!;XiPn)uHa~fF7+30zCv#pv9 zvgcJTk=gly_2~~)_vtr{d$o-$ISSNTJKn#y|FPMqJY$8--WMB;Ob%ZaW?;B{^IGot zsavJxU$Xq%{M594ze|g;<_Zb5R*9eU?5p#B9zN>ckcW}Gr!&qSICCe``E0%R zp_BzFH{KsDy_aqFZ=H_yho7tr47d8?N-`q)!_b1cMrn0^(;AA+!G0(o<^K*;Ljr*Jh->P5FzZ)TT zM(hCt!xwS?V;?J`)R=LV`ArL%N^);)+FA4e|Gy1u>K^_3bX)z}lu~K=?0w>5XXeFE z)2oSl{(3^ur%ltR3CDdnw5jBvA}a$!f)0yC$(_Huf1mogf5*l%s0x<$cwZo51eV3>~{b9vG3_e|DT-|`s~cC;Ahhx z7X1HsYyQ;fy49XPS5N=jDp<(Ckl?~JC-e2P>iw4`zy5PePpOWMw8;;=DJ}TuW9jw7 z->31{{kETO{Qt-K*&x*oae0T1Z7KPCK3^yP&&jQ+8PC=jZ8!d88@RD2;+lAUO|J1& zSq6p$(;3olb^iEwYwmq5_H(fjY3C&F{AWpu+;}Ep8wWcBLltMivZ*u5>JHDnufzRZ zPa>lxeB+tcVzDXGbsx_bU9P>a$-uxcl_C9BhxEFmruUoWe!dHJKeYb;PxD!&ObiSx zjOVs|Yz>Xi&D*g}|MBjw(j&Vb@x4CqeI7+%j+;O1o!S&t3f>y}EY$ z=e>gFY2C+XAKmr+YVo1)d*2ps`owPk>R z{{61c_uduS?ftxO?x$7ZK0hDVTut8}q4)Q~^4Ycf-W@w%Zom5XWDCcF)7$fN|J6O* zeg17u_Pzh{j0|e#jOVrpT(6TiFj=3{o!)(%_om&irvCxoT2Jf$_7TnwJ@R4Y`nsQa z>!YjxGvB=tca3w$JqG*VbGE-VJh1-Po9VNz=SS>Moc{j)r?t_u-~V6kU$bG(*K^ya z@oYTTaoOOKXZuO5N5)E9!ry$J=q?+m_xFfuvQ^|J`)u|%k#o<-{a$kDmaOvq!bghx zzPW$Ax%&U@&bQ(Z7!E{m7A)JdgRlB3bNQ6!XS#aLU;ePRPvMUHcCP64`h4yGo4%>n z#tWb4*#CTD{50S5d3xN>&geIbEWY*j2cMwnU#6NxNmqVf;;&uxv--%fo6q-bm0Z{p z(KX*yBlgcee)Gcp`q%DjfBSq(R{2-3=7HNEWsh6-J$-bpFz&KtqtN@(Ka~e7-|L6} zd8~fPuj~!?+lJoBZf_a-zZ=^{yUWe|9LePN?TE1SYP&Mq-@m*6chi}fmb-a^j(k7<|1SG%d*uV} zM#;x|((c~>|CRsr?7r$`$t6?me_VfeKiS3FO>}m^zbC4x7uM~5R$ubJ?)3L{_f==; zy<@q&^7Q|lAL+keupB56cpuyQzQTU}uSfQ8ismG<{CV`fcFD}f5AybZR-V)N@p{>l z6WK?;hs?gp@PB{p+wAS7$ySG6m9CH8cl>^*LD&82UoX>7C$`JR-23+}eEP>9XY=Ls zw8QVk_Syei@#)g^e;4&m?|s;IdzQWOtK^*<|8#bw9^Iw%!s*T9`Tt&i+LUtg(6jp+ z`9FT1|L;rq(*@09X0O-ScfUUxes^BS!EfKfN`Jnuy8beC%`3x>zuEGicddy?&+%Vl z+;_R+$ou0(w|)yLEl93UicgxET)F=CZRz;8_g5ZYR`-5kyPUrH-7o9U7G~71yOmUb z|I4!QH=E{`#y|f3^>5$zm&WsVZnV7T_rYN94o>Odd#gyZ#t=?`;H?xoac(1=?zxZvr#f#KsexKU(=E3Ay>w~Ayzb326 zyK&8ry|c@G;`#nwyPO}n@m!T`P4wTtH=bEs45(ke_npPg!fVmz-s@@qyO+QJ(btV@ ze(j35*5zl?|FkVByyn-bp#H-A8uR0C&aT(jwkrrM|G)0m$A9fJ<_f&-ecLrZw&33V zcSStE-m3(R2`S;ECKNI`peZ)>r{~stEAZRJ*lq|SWWa3uy z#gpg%zH`=>!|lhnb?;Bd@7v0`{`;Qix@R}p|Nl9A_I2MQ@9Y2PY|ei^zrM=y`J9K| zB}#m&Z%pGj9+B>nSv}WO?{Rr@^`q}fPPZSK3NBh5Zg2A}x9jPVs>kKk{Ra>8+uw^# z&pB^;{f^bTofD5re$ap7^<{kPseaen{pc-ghD*UpE^Z2uAW z$9CVN-->;aIl*_Y|Jb~D{>OPo(-q^`FLw$SEZb8mEw8&d?W|Vw1?l^Xd+WkCr=8K- zUz_ABxum1LV`9NI%Y5DC^FOu~y?k_f*2RlzW^6WGd)ElJZag=GNw%@%;qLRdpFS6V zXTcb7)c0=I0~dy^@$wUcx7QwJ_sjd&Q_&p}eVO;~;nTmqO3$u$Kd?6TbHdI)^7i-c ze&2Sl{=44luF|+a_a1)8>fdnf{{G+RexKSJ|F7!xZ|n85`)}mN|Cak*P^Nw+>|UYw z_v*cukBjTa-O#a~Cb@q9)9UNJ7D=)Fxi98Ux?A?p_4(A(`nxyXoc8VPcQcdYTHli0 zc78cL|6R|t`ts?2JO0Mqx72(4T`^An_{#6I{z=^qFZW~Gvfgz4am{J>t0$?~m6Zt} z;xGAbf5fnV{f%oo7DjbUwtQQ9?Ed5IzkRRwKK}b!{J7!nM@_}|_uu~h`r9|Z)9X)1 z{A><=x=Ooz@3MXS)ZczzU;gu>+?CTdk5+&GaWExlxv+b})}t>{dKU;Nmb_TV?qA`u ztFLIAvV}{HYt6pLvg`A%NpB2yZ~onD)gQWM_1?3B4T3jAU0&Y4XSe5ERggl{o^rDT zlT;7i7QTIBUen*7=Qvw`{QYM1U1gfIx?xqnW5#*4j*7i|&l`4B^xuD99`h*uw*8e_3etM3x53A@!dX+uP4;j&GbU`^0mjm9VuLK|NZvZ!bu9(vZbt`Q9oP$b1(yvLiSHHwpBmsZg-!b|BfX;Kg9P+7ylccYaC}KLS^~( zM*qI2q3y?TMSAbuOHyn3uGgkMFZmW6_qXev+4YKU%`e*Tf6RaS?*5+*(x(J{1d9Y+ zKW@J9T^nN;+?s-!ZpE0(bHWw)(c9mxBuI7-SmF{ z%kQQ8lVrcTIJHFezK?LrsjjW9eY5Nw=r|x>Ut!iz>k-Rsb zub%bzC3yF5M!nv-Jh#*4CieZ;j~903uKO*1{mb{?GDklp%Res93OW9KW9^yb|BqL* zW%u4Fc-#B`%9$A}*8RJ5-~6@h-mmlC?ThC(o4@tU-tC-?Ry>Xf9>1$ zepOt5=+(HozdpS6>-uhN?2p$nxO%7h{oma#F)aZ{p1bsld?@`={O-=&%e}{qwd}(- z?m5h7yJPn`^)jFOb;>u7%a&j1`2Ex8`PX*2UB$nZm8O>`mG23Zel7ia+p%w7`@*iT ze6sh|i<)~&XhDSW(2V7}!H9~O(? zxH}BG>yFBu-5>i+{b`r6-L)g7_SMq&6|c$OHt;abQdY~VIOb8&p|EeKdX>7?qV=-H zXFV#^a~`!NsLNDm-PhdvamQwXiI=UbV(R0z*XF)1PiEQCl_A&naq8WIuKS(RlS4ng z-!5wxh3=g_7mnq-|q2r=>T@S3VI;HR%?Y-ErIM_9MNQ8}&EJZv0>R?7#=*CvE;r zSEP5fuHN_K``s1&wQB-xFFn_IH$(ooX5I3Caeu788SlHb_}TKUeTjFEM;u>t)cQYj z{>4SE*89p6Bt7rEUyzsYb9nvh-p9A||KCcIe)ap-p5ya(EZKTzE@RyG{k8qgKQ_N} ze&55D@JGo@c}L#CvY@4Oqjb!u&S+lGimXx6aP;9{}oc1a(VGxd0&-%-&@OUuD;;g zd-;O$6gJI6TKkf&SRYzi>G!oS@P^gW@3;K+T&?JQuqJqi@{^?McfXF--p$Qt&$Hf@ z996PLeV=&n&Ev*%_t|R2toMAkTsH2i$HUvzcYiN(W&c)r_Tkx%N}1njEqgcrc{Z_N zz0^ETgQ%EqT>-XRd-j}vZQk=QW8FRXDO=|+t}TE5qdoS}=Fj)O%$u;{_oi+4zy40& zv+j%V>&|4k`TaNAcjP1ld{^G_cU$P){qw(`i?zRgSY5hp`}V$b-}A0#{9o6WK9MKh zs_^snebHtmeZ_AMXBdk&eC)RPbINqPS)#O~+~SMPLLVh3)_SRDRJ+IBR=#OybjR9B zR&mcRW3HOI*J|0NtIFr@vdh@`r`=lly7=Dby1W0bxqtTZqp#Icz4lM1Je;vU_;zwi z^&ayI>6VShYGU^DU+!f{Kh=G_KKK5<+ADz{>LWi-R$F(!{#EO4mmdm`tgqRgaG$Vs zd4YSz_QxyxOs)q$uPvPZM(UAb($g-#f@89Kl-Gpsd4Kq>;1!Np9gEYvzVrz*nf`f{ z{&qsw^`j?>_Fl76(%a|6@}bLHFKgcRJ?hd?g;j4g%+E=E`Mfl_e%rUDEg>TEMpKQI}sV-~(EO+(gtG`=Q<}SRjJ9oE!a(YC6$)rW!ORcAG|DC>S=em8L z@4fGqW-vP)zxlIT!`~xczn$IsMEcvGeSIZHD%)x+)Q&UTuK(VXFR_HYOV-ShUx+5C4I zhqmwf^Xc{M>%Jwc%5;p^`P$dF>|XWVnys)&v1ymBsqH%N{QHr6_Qn<;FZ}c9d-Bi1 zoNpy}j(^&BB>Rzen#GZ=UzIcC^%yR^~HIg zpL5Qw?XX_ZUp9&9Te@Jj-~!WAkE|Yk+w$z&*Xtjj|FXNDbo~8sjkgP2BCfq%+5hMj z?+5L5JNE5Vt6J0lrz@jp;`_&L6>q|H?5{)qbFTNuun!IP9tzXNLjlEBgnH~ErqI~b|W0~t~+dZeEBD@-S9>mcwtI?c@MP=T!OClGOZTn1K4H82jn~NwXYD_}<((}{MRkb9wVS_x zMYzY<@6%_pxwgITxYc8Ax4pN^rQVyr-P`rHGg;KUsJpiB!E5o?hNoh_TDN^QEP4Ak z@B9Rd#N)lcUd<1EU)Xg*K66!>{nvG`_s;Kk?Co&pn4bOXeSFNdfb_a8`l%BAyWj8G zzPo3K+P=N-4S8kNR&&hT!Dpeg%5Pzw|IfbNt;b6~NVLjjc9kB9V7a7j|MRKZrDH-1 z1ZTVreEg&ONb-?-mheBj{a*LEr)_P{$zN9GZgTAF`HxrQ-XFL7cKz)9!s}l-zT7y^ zaaqDJ&z{|8pZLM(tH(2aU39kBvVZweZp&8wB{Kc*O_x8Z&m)fg-f7`C>rKU#n-y-a zlK&Rg)>$8TefQeqC(gEwf*0+@zHUr@dp_a!jrJMe{$Edyk@FD+bu(wEx=+1bzkAgg z&E(l#SNlFV#(duUbm!WqPg3=Dn=j0(t1DE$@XL1Ht8emavX;g3XGvA<{TbmB$&yrD zyxutO{@%E4wd`;1F=Y8W@89!v&g***x-TB@*irFU_x|tqcmLk{`Yh$I{MoC0ufF|C zicY>&6&qHwzOSgSL@6=<=#`%q7X_;1+fBm+?-*t;jB}TJSbi;j-&V=ieY=a_=-Icf7wkSHkw~*?fuDdT)$a z_f&@ctbbkq{KL_;=bz}B-`#awSi7)j(Y>R4_ttOhi}m+HzcT+naK|Eg!P#>oga9-d?ul$zRcl)Qx93@vZ0X;~_nVTts@`|D&TXu% z`24F%{6~b}-^l%`yiP1h)pNh9<$SEWS}T3}?w#cSuZybE^^TYww>!ScZMKuVx`I-} zYrEq`kG@y+hudVW{hP7#;bay5vUvMjTOa%nY-55=26QL7eAA$ljYwg3&s~FrEdAZma$n}d0DdTRJIvCX*vDT zCWq_Zw8hu2pHp?CGgax`zq_?hAK(A0pZamn9c!ue^;OP4Pe}h;XUz0(kv z-pxB@HYpf11|iuiJ+*gPUyZxS(bwNg_I}H_UfN$C*(~sS&EmhpvFWy)f)g!o6~=+a zFw$Ap)c!g5?7Y;2{HqL54V_p8wX#QDW=rWAN?!;)GPIO`o-|`O~-Vfoj>@JcRGf%1hM$| zG_QA*y4>-7N!)SUu*Ix5DyExqe676HY%uX@kGc6~wAmes-+nyvQTJk7>nz&bbc z@uKi~@oDGg+_VwPiSI5w^J2~G^?v`ljbe|MK03{3x3ao;)4kU_7#R|F2|nl*?XLZKC%0_Xc?EvIA_jxloosVX z9x#32W1ar*V}Zo|g6HO^8;iyHbPl~f@!dM^&-|*G;f3mfncL32X!&%yKPGP9hF7bf9_RnF zM#jKo{Tazw#?S8euQq>VdEdYMk@dR`a^{t@&t0!!W?)z>{JzM@F0RaFDfcn1&%0fm`g+^ir#Ew>Pkr=`ztOYI z|EPN}BLhRT>Vbtk_UbnKuH{y35q`X1XZ5u|Th`qEkrBE5=f+JhZ=OAyEp}#R{IqlR zahAUxD4#a;?$cWT^Kkey*3Hkw85kNi3Ou+Vr2J_8-{v1xsf-K^5-R5z*%=rbSeRrO z7#M^&3K$p|JQNNvGB7AKGNkV}ug}PVZQ;`w5_uoS%28P`1`BCA0F+cto)n~*t{+-K^9%NB>((vic=i8=Tm6kIT;$v|5 ze7|LX)u+Zym8Xrt(=-Oi(?((D!9CMT&lTy-uwDO?i6P-vMcV6WpH94=ufv~Tw?(G- zL85QX`FWNh2}S}Be5__~x4YZ&WAgbNo!9g4hA=S9uzSR5SvdLL537`Wj;w0JkU5zc u=Fxfj(G?1#Ycn9ZVRX65uwAJ0Pu@prNAYt`2TleC1_n=8KbLh*2~7arZ8od` literal 0 HcmV?d00001 diff --git a/apps/web/src/app/(content)/features/mock.ts b/apps/web/src/app/(content)/features/mock.ts index 4514c3de..ac706270 100644 --- a/apps/web/src/app/(content)/features/mock.ts +++ b/apps/web/src/app/(content)/features/mock.ts @@ -1014,17 +1014,20 @@ export const mockChartData = { }; export const maintenanceData = { - id: 0, - from: new Date("2024-07-09T21:02:43.000Z"), - to: new Date("2024-07-09T21:05:43.000Z"), - title: "Maintenance", - message: - "We are performing maintenance on our environment. Services and projects may be unavailable for a few minutes.", - createdAt: null, - updatedAt: null, - workspaceId: null, - pageId: null, - monitors: undefined, + maintenance: { + id: 0, + from: new Date("2024-07-09T21:02:43.000Z"), + to: new Date("2024-07-09T21:05:43.000Z"), + title: "Maintenance", + message: + "We are performing maintenance on our environment. Services and projects may be unavailable for a few minutes.", + createdAt: null, + updatedAt: null, + workspaceId: null, + pageId: null, + monitors: undefined, + }, + monitors: [], }; export const statusReportData = { diff --git a/apps/web/src/app/(content)/features/status-page/page.tsx b/apps/web/src/app/(content)/features/status-page/page.tsx index 9544d6bd..ada838f3 100644 --- a/apps/web/src/app/(content)/features/status-page/page.tsx +++ b/apps/web/src/app/(content)/features/status-page/page.tsx @@ -5,7 +5,7 @@ import { } from "@/app/shared-metadata"; import { PasswordFormSuspense } from "@/app/status-page/[domain]/_components/password-form"; import { SubscribeButton } from "@/app/status-page/[domain]/_components/subscribe-button"; -import { MaintenanceBanner } from "@/components/status-page/maintenance-banner"; +import { MaintenanceContainer } from "@/components/status-page/maintenance"; import { StatusCheck } from "@/components/status-page/status-check"; import { StatusReport } from "@/components/status-page/status-report"; import { Tracker } from "@/components/tracker/tracker"; @@ -93,7 +93,7 @@ export default function FeaturePage() { subTitle="Down't let your users in the dark and show what's wrong." component={
- +
} col={1} @@ -118,11 +118,7 @@ export default function FeaturePage() { iconText="Keep it simple" title="Build trust." subTitle="Showcase your reliability to your users, and reduce the number of customer service tickets." - component={ -
- -
- } + component={} action={
- +

diff --git a/apps/web/src/app/status-page/[domain]/events/page.tsx b/apps/web/src/app/status-page/[domain]/events/page.tsx new file mode 100644 index 00000000..8d58880b --- /dev/null +++ b/apps/web/src/app/status-page/[domain]/events/page.tsx @@ -0,0 +1,55 @@ +import { Header } from "@/components/dashboard/header"; +import { SearchParamsPreset } from "@/components/monitor-dashboard/search-params-preset"; // TOO: move to shared components +import { Feed } from "@/components/status-page/feed"; +import { api } from "@/trpc/server"; +import { notFound } from "next/navigation"; +import { z } from "zod"; +import { formatter } from "./utils"; + +const searchParamsSchema = z.object({ + filter: z.enum(["all", "maintenances", "reports"]).optional().default("all"), +}); + +type Props = { + params: { domain: string }; + searchParams: { [key: string]: string | string[] | undefined }; +}; + +export const revalidate = 120; + +export default async function Page({ params, searchParams }: Props) { + const page = await api.page.getPageBySlug.query({ slug: params.domain }); + const search = searchParamsSchema.safeParse(searchParams); + + if (!page) return notFound(); + + const filter = search.success ? search.data.filter : "all"; + + return ( +
+
+ } + className="text-left" + /> + +
+ ); +} diff --git a/apps/web/src/app/status-page/[domain]/incidents/[id]/_components/copy-link-button.tsx b/apps/web/src/app/status-page/[domain]/events/report/[id]/_components/copy-link-button.tsx similarity index 100% rename from apps/web/src/app/status-page/[domain]/incidents/[id]/_components/copy-link-button.tsx rename to apps/web/src/app/status-page/[domain]/events/report/[id]/_components/copy-link-button.tsx diff --git a/apps/web/src/app/status-page/[domain]/events/report/[id]/page.tsx b/apps/web/src/app/status-page/[domain]/events/report/[id]/page.tsx new file mode 100644 index 00000000..a9c9b4ab --- /dev/null +++ b/apps/web/src/app/status-page/[domain]/events/report/[id]/page.tsx @@ -0,0 +1,58 @@ +import { notFound } from "next/navigation"; + +import { Header } from "@/components/dashboard/header"; +import { DateTimeTooltip } from "@/components/status-page/datetime-tooltip"; +import { StatusReportUpdates } from "@/components/status-page/status-report"; +import { api } from "@/trpc/server"; +import { Badge } from "@openstatus/ui"; +import { CopyLinkButton } from "./_components/copy-link-button"; + +export default async function IncidentPage({ + params, +}: { + params: { domain: string; id: string }; +}) { + const report = await api.statusReport.getPublicStatusReportById.query({ + slug: params.domain, + id: Number(params.id), + }); + + if (!report) return notFound(); + + const affectedMonitors = report.monitorsToStatusReports.map( + ({ monitor }) => monitor, + ); + + const firstUpdate = + report.statusReportUpdates[report.statusReportUpdates.length - 1] || + undefined; + + return ( +
+
+

+ Started at +

+ {affectedMonitors.length > 0 ? ( + <> + • +
+ {affectedMonitors.map((monitor) => ( + + {monitor.name} + + ))} +
+ + ) : null} +
+ } + actions={} + /> + +

+ ); +} diff --git a/apps/web/src/app/status-page/[domain]/events/utils.tsx b/apps/web/src/app/status-page/[domain]/events/utils.tsx new file mode 100644 index 00000000..5355a84b --- /dev/null +++ b/apps/web/src/app/status-page/[domain]/events/utils.tsx @@ -0,0 +1,5 @@ +"use client"; + +export function formatter(value: string) { + return {value}; +} diff --git a/apps/web/src/app/status-page/[domain]/incidents/[id]/page.tsx b/apps/web/src/app/status-page/[domain]/incidents/[id]/page.tsx index 9916358c..a990e429 100644 --- a/apps/web/src/app/status-page/[domain]/incidents/[id]/page.tsx +++ b/apps/web/src/app/status-page/[domain]/incidents/[id]/page.tsx @@ -1,43 +1,9 @@ -import { notFound } from "next/navigation"; - -import { Header } from "@/components/dashboard/header"; -import { - StatusReportDescription, - StatusReportUpdates, -} from "@/components/status-page/status-report"; -import { api } from "@/trpc/server"; -import { CopyLinkButton } from "./_components/copy-link-button"; +import { redirect } from "next/navigation"; export default async function IncidentPage({ params, }: { params: { domain: string; id: string }; }) { - const report = await api.statusReport.getPublicStatusReportById.query({ - slug: params.domain, - id: Number(params.id), - }); - - if (!report) return notFound(); - - const affectedMonitors = report.monitorsToStatusReports.map( - ({ monitor }) => monitor, - ); - - return ( -
-
- } - actions={} - /> - -
- ); + redirect(`../events/report/${params.id}`); } diff --git a/apps/web/src/app/status-page/[domain]/incidents/page.tsx b/apps/web/src/app/status-page/[domain]/incidents/page.tsx index a288a937..c1f98cf1 100644 --- a/apps/web/src/app/status-page/[domain]/incidents/page.tsx +++ b/apps/web/src/app/status-page/[domain]/incidents/page.tsx @@ -1,31 +1,5 @@ -import { notFound } from "next/navigation"; +import { redirect } from "next/navigation"; -import { Header } from "@/components/dashboard/header"; -import { StatusReportList } from "@/components/status-page/status-report-list"; -import { api } from "@/trpc/server"; - -type Props = { - params: { domain: string }; - searchParams: { [key: string]: string | string[] | undefined }; -}; - -export default async function Page({ params }: Props) { - if (!params.domain) return notFound(); - - const page = await api.page.getPageBySlug.query({ slug: params.domain }); - if (!page) return notFound(); - - return ( -
-
- -
- ); +export default async function Page() { + redirect("./events?filter=reports"); } diff --git a/apps/web/src/app/status-page/[domain]/layout.tsx b/apps/web/src/app/status-page/[domain]/layout.tsx index eb62e833..18dfe57b 100644 --- a/apps/web/src/app/status-page/[domain]/layout.tsx +++ b/apps/web/src/app/status-page/[domain]/layout.tsx @@ -8,6 +8,7 @@ import { twitterMetadata, } from "@/app/shared-metadata"; import { Shell } from "@/components/dashboard/shell"; +import { getRequestHeaderTimezone } from "@/lib/timezone"; import { api } from "@/trpc/server"; import { Footer } from "./_components/footer"; import { Header } from "./_components/header"; @@ -21,6 +22,7 @@ type Props = { export default async function StatusPageLayout({ children, params }: Props) { const page = await api.page.getPageBySlug.query({ slug: params.domain }); + const timeZone = getRequestHeaderTimezone(); if (!page) return notFound(); @@ -33,22 +35,14 @@ export default async function StatusPageLayout({ children, params }: Props) { href: setPrefixUrl("/", params), }, { - label: "Maintenances", - segment: "maintenances", - href: setPrefixUrl("/maintenances", params), - disabled: page.maintenances.length === 0, - }, - { - label: "Incidents", - segment: "incidents", - href: setPrefixUrl("/incidents", params), + label: "Events", + segment: "events", + href: setPrefixUrl("/events", params), }, { label: "Monitors", segment: "monitors", href: setPrefixUrl("/monitors", params), - disabled: - page.monitors.filter((monitor) => Boolean(monitor.public)).length === 0, }, ]; @@ -64,12 +58,14 @@ export default async function StatusPageLayout({ children, params }: Props) { } return ( -
+
- {children} + + {children} +
-
+
); } diff --git a/apps/web/src/app/status-page/[domain]/maintenances/page.tsx b/apps/web/src/app/status-page/[domain]/maintenances/page.tsx index 4751742e..83c50ade 100644 --- a/apps/web/src/app/status-page/[domain]/maintenances/page.tsx +++ b/apps/web/src/app/status-page/[domain]/maintenances/page.tsx @@ -1,31 +1,5 @@ -import { notFound } from "next/navigation"; +import { redirect } from "next/navigation"; -import { Header } from "@/components/dashboard/header"; -import { MaintenanceList } from "@/components/status-page/maintenance-list"; -import { api } from "@/trpc/server"; - -type Props = { - params: { domain: string }; - searchParams: { [key: string]: string | string[] | undefined }; -}; - -export default async function Page({ params }: Props) { - if (!params.domain) return notFound(); - - const page = await api.page.getPageBySlug.query({ slug: params.domain }); - if (!page) return notFound(); - - return ( -
-
- -
- ); +export default async function Page() { + redirect("./events?filter=maintenances"); } diff --git a/apps/web/src/app/status-page/[domain]/monitors/page.tsx b/apps/web/src/app/status-page/[domain]/monitors/page.tsx index fe82e48a..6820ed34 100644 --- a/apps/web/src/app/status-page/[domain]/monitors/page.tsx +++ b/apps/web/src/app/status-page/[domain]/monitors/page.tsx @@ -6,6 +6,7 @@ import { z } from "zod"; import { OSTinybird } from "@openstatus/tinybird"; import { Button } from "@openstatus/ui"; +import { EmptyState } from "@/components/dashboard/empty-state"; import { Header } from "@/components/dashboard/header"; import { SimpleChart } from "@/components/monitor-charts/simple-chart"; import { groupDataByTimestamp } from "@/components/monitor-charts/utils"; @@ -111,9 +112,11 @@ export default async function Page({
) : ( -

- No public monitor. -

+ )}
); diff --git a/apps/web/src/app/status-page/[domain]/page.tsx b/apps/web/src/app/status-page/[domain]/page.tsx index e5ffac60..a48d604b 100644 --- a/apps/web/src/app/status-page/[domain]/page.tsx +++ b/apps/web/src/app/status-page/[domain]/page.tsx @@ -1,14 +1,13 @@ import { subDays } from "date-fns"; import { notFound } from "next/navigation"; -import { Separator } from "@openstatus/ui"; - +import { EmptyState } from "@/components/dashboard/empty-state"; import { Header } from "@/components/dashboard/header"; -import { MaintenanceBanner } from "@/components/status-page/maintenance-banner"; +import { Feed } from "@/components/status-page/feed"; import { MonitorList } from "@/components/status-page/monitor-list"; import { StatusCheck } from "@/components/status-page/status-check"; -import { StatusReportList } from "@/components/status-page/status-report-list"; import { api } from "@/trpc/server"; +import { Separator } from "@openstatus/ui"; type Props = { params: { domain: string }; @@ -21,14 +20,18 @@ export default async function Page({ params }: Props) { const page = await api.page.getPageBySlug.query({ slug: params.domain }); if (!page) return notFound(); - const currentMaintenances = page.maintenances.filter( - (maintenance) => - maintenance.to.getTime() > Date.now() && - maintenance.from.getTime() < Date.now(), - ); + const lastMaintenances = page.maintenances.filter((maintenance) => { + return maintenance.from.getTime() > subDays(new Date(), 7).getTime(); + }); + + const lastStatusReports = page.statusReports.filter((report) => { + return report.statusReportUpdates.some( + (update) => update.date.getTime() > subDays(new Date(), 7).getTime(), + ); + }); return ( -
+
- {currentMaintenances.length ? ( -
- {currentMaintenances.map((maintenance) => ( - - ))} -
- ) : null} - - -
-
-

Last updates

-

- Reports of the last 7 days or incidents that have not been resolved - yet. -

-
- + ) : ( + + )} + + {lastStatusReports.length || lastMaintenances.length ? ( + { + return ( + maintenance.from.getTime() > subDays(new Date(), 7).getTime() + ); + })} + statusReports={lastStatusReports.filter((report) => { + return report.statusReportUpdates.some( + (update) => + update.date.getTime() > subDays(new Date(), 7).getTime(), + ); + })} + /> + ) : ( + -
+ )}
); } diff --git a/apps/web/src/app/status/utils.ts b/apps/web/src/app/status/utils.ts index dfc998be..c33c6140 100644 --- a/apps/web/src/app/status/utils.ts +++ b/apps/web/src/app/status/utils.ts @@ -34,19 +34,19 @@ export type AtlassianDescriptionEnum = z.infer; export function getClassname(status: ExternalStatus) { switch (status.status_description) { case "All Systems Operational": - return "text-green-500"; + return "text-status-operational"; case "Major System Outage": - return "text-rose-500"; + return "text-status-down"; case "Partial System Outage": - return "text-orange-500"; + return "text-status-degraded"; case "Minor Service Outage": - return "text-amber-500"; + return "text-status-degraded"; case "Degraded System Service": - return "text-amber-500"; + return "text-status-degraded"; case "Partially Degraded Service": - return "text-amber-500"; + return "text-status-degraded"; case "Service Under Maintenance": - return "text-blue-500"; + return "text-status-monitoring"; default: return "text-gray-500"; } diff --git a/apps/web/src/components/data-table/data-table-status-badge.tsx b/apps/web/src/components/data-table/data-table-status-badge.tsx index c69dffc0..c11d68fd 100644 --- a/apps/web/src/components/data-table/data-table-status-badge.tsx +++ b/apps/web/src/components/data-table/data-table-status-badge.tsx @@ -9,14 +9,7 @@ export function DataTableStatusBadge({ statusCode: Ping["statusCode"]; }) { if (!statusCode) { - return ( - - Error - - ); + return Error; } return ; } diff --git a/apps/web/src/components/data-table/status-page/columns.tsx b/apps/web/src/components/data-table/status-page/columns.tsx index 560a5169..62cd0308 100644 --- a/apps/web/src/components/data-table/status-page/columns.tsx +++ b/apps/web/src/components/data-table/status-page/columns.tsx @@ -29,7 +29,7 @@ export const columns: ColumnDef< cell: ({ row }) => { return ( diff --git a/apps/web/src/components/forms/monitor/section-limits.tsx b/apps/web/src/components/forms/monitor/section-limits.tsx index 17611fc5..bc7edb6d 100644 --- a/apps/web/src/components/forms/monitor/section-limits.tsx +++ b/apps/web/src/components/forms/monitor/section-limits.tsx @@ -60,7 +60,8 @@ export function SectionLimits({ form }: Props) { When the response time exceeds this limit, the monitor is will be - considered as degraded. + considered as{" "} + degraded. @@ -91,7 +92,7 @@ export function SectionLimits({ form }: Props) { When the response time exceeds this limit, the monitor is will be - considered as failed. + considered as failed. diff --git a/apps/web/src/components/layout/header/user-nav.tsx b/apps/web/src/components/layout/header/user-nav.tsx index a2651ea6..08677c61 100644 --- a/apps/web/src/components/layout/header/user-nav.tsx +++ b/apps/web/src/components/layout/header/user-nav.tsx @@ -1,4 +1,5 @@ import { signOut, useSession } from "next-auth/react"; +import { useTheme } from "next-themes"; import Link from "next/link"; import { useParams } from "next/navigation"; @@ -8,11 +9,16 @@ import { AvatarImage, Button, DropdownMenu, + DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, + DropdownMenuPortal, DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, DropdownMenuTrigger, Skeleton, } from "@openstatus/ui"; @@ -20,6 +26,7 @@ import { export function UserNav() { const session = useSession(); const { workspaceSlug } = useParams<{ workspaceSlug: string }>(); + const { setTheme, theme } = useTheme(); if (session.status !== "authenticated") { return ; @@ -41,7 +48,7 @@ export function UserNav() { - +

@@ -61,6 +68,27 @@ export function UserNav() { Profile + {" "} + + + + Switch theme + + + Appearance + {["light", "dark", "system"].map((option) => ( + setTheme(option)} + className="capitalize" + > + {option} + + ))} + + + signOut()}>Log out diff --git a/apps/web/src/components/marketing/monitor/card.tsx b/apps/web/src/components/marketing/monitor/card.tsx index ed645c39..c39e52d7 100644 --- a/apps/web/src/components/marketing/monitor/card.tsx +++ b/apps/web/src/components/marketing/monitor/card.tsx @@ -29,10 +29,13 @@ export function MonitoringCard() { // biome-ignore lint/suspicious/noArrayIndexKey: ))} -

- +
diff --git a/apps/web/src/components/marketing/pricing/enterprice-plan.tsx b/apps/web/src/components/marketing/pricing/enterprice-plan.tsx index da2a72a2..7f1352c7 100644 --- a/apps/web/src/components/marketing/pricing/enterprice-plan.tsx +++ b/apps/web/src/components/marketing/pricing/enterprice-plan.tsx @@ -6,7 +6,7 @@ export function EnterpricePlan() {
); diff --git a/apps/web/src/components/monitor-dashboard/search-params-preset.tsx b/apps/web/src/components/monitor-dashboard/search-params-preset.tsx index 5d4caad9..732faecb 100644 --- a/apps/web/src/components/monitor-dashboard/search-params-preset.tsx +++ b/apps/web/src/components/monitor-dashboard/search-params-preset.tsx @@ -13,6 +13,7 @@ import type { ReactNode } from "react"; import { Icons } from "@/components/icons"; import type { ValidIcon } from "@/components/icons"; import useUpdateSearchParams from "@/hooks/use-update-search-params"; +import { cn } from "@/lib/utils"; export function SearchParamsPreset({ disabled, @@ -22,6 +23,7 @@ export function SearchParamsPreset({ icon, placeholder, formatter, + className, }: { disabled?: boolean; defaultValue?: T; @@ -30,6 +32,7 @@ export function SearchParamsPreset({ icon?: ValidIcon; placeholder?: string; formatter?(value: T): ReactNode; + className?: string; }) { const router = useRouter(); const pathname = usePathname(); @@ -48,7 +51,9 @@ export function SearchParamsPreset({ onValueChange={onSelect} disabled={disabled} > - + {Icon ? : null} diff --git a/apps/web/src/components/monitor/status-dot.tsx b/apps/web/src/components/monitor/status-dot.tsx index f8ffc4ce..a27c620f 100644 --- a/apps/web/src/components/monitor/status-dot.tsx +++ b/apps/web/src/components/monitor/status-dot.tsx @@ -10,39 +10,39 @@ export function StatusDot({ active, status, maintenance }: StatusDotProps) { if (!active) { return ( - + ); } if (maintenance) { return ( - - + + ); } if (status === "error") { return ( - - + + ); } if (status === "degraded") { return ( - - + + ); } return ( - - + + ); } diff --git a/apps/web/src/components/status-page/datetime-tooltip.tsx b/apps/web/src/components/status-page/datetime-tooltip.tsx index 4dce774e..64c8a06a 100644 --- a/apps/web/src/components/status-page/datetime-tooltip.tsx +++ b/apps/web/src/components/status-page/datetime-tooltip.tsx @@ -17,6 +17,7 @@ export function DateTimeTooltip({ }: { date?: Date; className?: string; + // formatter?: (date: Date) => string; }) { const [open, setOpen] = useState(false); return ( @@ -25,15 +26,14 @@ export function DateTimeTooltip({ setOpen(false)} className={cn( - "text-muted-foreground underline decoration-muted-foreground/30 decoration-dashed underline-offset-4", + "font-mono text-muted-foreground underline decoration-muted-foreground/30 decoration-dashed underline-offset-4", className, )} - asChild > - {formatInTimeZone(date, "UTC", "LLL dd, y HH:mm (z)")} + {formatInTimeZone(date, "UTC", "LLL dd, y HH:mm (z)")} -

+

{format(date, "LLL dd, y HH:mm (z)")}

diff --git a/apps/web/src/components/status-page/day-header.tsx b/apps/web/src/components/status-page/day-header.tsx new file mode 100644 index 00000000..2dabbc13 --- /dev/null +++ b/apps/web/src/components/status-page/day-header.tsx @@ -0,0 +1,17 @@ +import { Badge, Separator } from "@openstatus/ui"; +import { format } from "date-fns"; + +export function DayHeader({ date }: { date: Date }) { + const isInFuture = date.getTime() > new Date().getTime(); + return ( +
+
+

+ {format(date, "LLL dd, y")} +

+ {isInFuture ? Coming up : null} +
+ +
+ ); +} diff --git a/apps/web/src/components/status-page/feed.tsx b/apps/web/src/components/status-page/feed.tsx new file mode 100644 index 00000000..4cdbd4fb --- /dev/null +++ b/apps/web/src/components/status-page/feed.tsx @@ -0,0 +1,149 @@ +import { notEmpty } from "@/lib/utils"; +import type { + Maintenance, + PublicMonitor, + StatusReportWithUpdates, +} from "@openstatus/db/src/schema"; +import { EmptyState } from "../dashboard/empty-state"; +import { DayHeader } from "./day-header"; +import { MaintenanceContainer } from "./maintenance"; +import { StatusReport } from "./status-report"; + +function isMaintenanceType(value: unknown): value is Maintenance { + return (value as Maintenance).from !== undefined; +} + +function isStatusReportType(value: unknown): value is StatusReportWithUpdates { + return (value as StatusReportWithUpdates).statusReportUpdates !== undefined; +} + +export function Feed({ + maintenances, + monitors, + statusReports, +}: { + maintenances: Maintenance[]; + statusReports: StatusReportWithUpdates[]; + monitors: PublicMonitor[]; +}) { + if ([...maintenances, ...statusReports].length === 0) { + return ( + + ); + } + + function groupByDay( + maintenances: Maintenance[], + statusReports: StatusReportWithUpdates[], + ) { + const grouped = [...maintenances, ...statusReports].reduce( + (acc, element) => { + const isMaintenance = isMaintenanceType(element); + const isStatusReport = isStatusReportType(element); + + if (isMaintenance) { + const date = new Date(element.from.toDateString()).getTime(); + + const exists = acc.find((item) => item.timestamp === date); + + if (exists) { + exists.list.push({ type: "maintenance", value: element }); + } else { + acc.push({ + timestamp: date, + list: [{ type: "maintenance", value: element }], + }); + } + } + + if (isStatusReport) { + const firstUpdate = element.statusReportUpdates[0]; // make sure we get the correct order from backend query! + const date = ( + firstUpdate + ? new Date(firstUpdate?.date.toDateString()) + : new Date(new Date().toDateString()) + ).getTime(); + + const exists = acc.find((item) => item.timestamp === date); + + if (exists) { + exists.list.push({ type: "report", value: element }); + } else { + acc.push({ + timestamp: date, + list: [{ type: "report", value: element }], + }); + } + } + + return acc; + }, + [] as { + timestamp: number; + list: ( + | { + type: "maintenance"; + value: Maintenance; + } + | { + type: "report"; + value: StatusReportWithUpdates; + } + )[]; + }[], + ); + + grouped.sort((a, b) => b.timestamp - a.timestamp); + + return grouped; + } + + return ( +
+ {groupByDay(maintenances, statusReports).map((group) => { + return ( +
+ + {group.list.map((item, _i) => { + if (item.type === "maintenance") { + const affectedMonitors = item.value.monitors + ?.map((monitorId) => { + const monitor = monitors.find(({ id }) => monitorId === id); + return monitor || undefined; + }) + .filter(notEmpty); + + return ( + + ); + } + if (item.type === "report") { + const affectedMonitors = item.value.monitorsToStatusReports + .map(({ monitorId }) => { + const monitor = monitors.find(({ id }) => monitorId === id); + return monitor || undefined; + }) + .filter(notEmpty); + + return ( +
+ +
+ ); + } + return null; + })} +
+ ); + })} +
+ ); +} diff --git a/apps/web/src/components/status-page/maintenance-banner.tsx b/apps/web/src/components/status-page/maintenance-banner.tsx deleted file mode 100644 index 68a31baa..00000000 --- a/apps/web/src/components/status-page/maintenance-banner.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { format, isSameDay } from "date-fns"; -import { Hammer } from "lucide-react"; - -import type { Maintenance } from "@openstatus/db/src/schema"; -import { Alert, AlertDescription, AlertTitle } from "@openstatus/ui"; - -export function MaintenanceBanner(props: Maintenance) { - return ( - - - {props.title} - {props.message} - - {format(props.from, "LLL dd, y HH:mm")} -{" "} - {isSameDay(props.from, props.to) - ? format(props.to, "HH:mm") - : format(props.to, "LLL dd, y HH:mm")} - - - ); -} diff --git a/apps/web/src/components/status-page/maintenance-list.tsx b/apps/web/src/components/status-page/maintenance-list.tsx deleted file mode 100644 index 940b14be..00000000 --- a/apps/web/src/components/status-page/maintenance-list.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { notEmpty } from "@/lib/utils"; -import type { Maintenance, PublicMonitor } from "@openstatus/db/src/schema"; -import { Badge } from "@openstatus/ui"; -import { format, isSameDay } from "date-fns"; - -export function MaintenanceList({ - maintenances, - monitors, -}: { - maintenances: Maintenance[]; - monitors: PublicMonitor[]; -}) { - if (!maintenances.length) { - return ; - } - - return ( -
    - {maintenances.map((maintenance) => { - const maintenanceMonitors = maintenance.monitors - ?.map((id) => { - return monitors.find((monitor) => monitor.id === id); - }) - .filter(notEmpty); - return ( -
  • -
    -

    {maintenance.title}

    -
    - - {format(maintenance.from, "LLL dd, y HH:mm")} -{" "} - {isSameDay(maintenance.from, maintenance.to) - ? format(maintenance.to, "HH:mm") - : format(maintenance.to, "LLL dd, y HH:mm")} - - {maintenanceMonitors?.length ? ( - <> - • - {maintenanceMonitors.map((monitor) => ( - - {monitor.name} - - ))} - - ) : null} -
    -
    -

    {maintenance.message}

    -
  • - ); - })} -
- ); -} - -function EmptyState() { - return ( -

- No maintenances reported. -

- ); -} diff --git a/apps/web/src/components/status-page/maintenance.tsx b/apps/web/src/components/status-page/maintenance.tsx new file mode 100644 index 00000000..95b6f836 --- /dev/null +++ b/apps/web/src/components/status-page/maintenance.tsx @@ -0,0 +1,38 @@ +import { cn } from "@/lib/utils"; +import type { Maintenance, PublicMonitor } from "@openstatus/db/src/schema"; +import { DateTimeTooltip } from "./datetime-tooltip"; +import { StatusReportHeader, StatusReportUpdates } from "./status-report"; + +export function MaintenanceContainer({ + maintenance, + monitors, + className, +}: { + maintenance: Maintenance; + monitors: PublicMonitor[]; + className?: string; +}) { + return ( +
+ + -{" "} + +

+ } + /> + +
+ ); +} diff --git a/apps/web/src/components/status-page/status-check.tsx b/apps/web/src/components/status-page/status-check.tsx index 145d1a4a..0870de34 100644 --- a/apps/web/src/components/status-page/status-check.tsx +++ b/apps/web/src/components/status-page/status-check.tsx @@ -24,16 +24,17 @@ export async function StatusCheck({ const details = tracker.currentDetails; return ( -
-
+
+ + + +

{details.long}

- - - -
-
-

Status Check

- •{" "}

@@ -42,6 +43,16 @@ export async function StatusCheck({ ); } +function containerClassName(variant: StatusVariant) { + if (variant === "incident") return "bg-status-down/10 border-status-down/20"; + if (variant === "maintenance") + return "bg-status-monitoring/10 border-status-monitoring/20"; + if (variant === "degraded") + return "bg-status-degraded/10 border-status-degraded/20"; + if (variant === "down") return "bg-status-down/10 border-status-down/20"; + return "bg-status-operational/10 border-status-operational/20"; +} + export function StatusIcon({ variant }: { variant: StatusVariant }) { if (variant === "incident") { const AlertTriangleIcon = Icons["alert-triangle"]; diff --git a/apps/web/src/components/status-page/status-report-list.tsx b/apps/web/src/components/status-page/status-report-list.tsx deleted file mode 100644 index fc8612c3..00000000 --- a/apps/web/src/components/status-page/status-report-list.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; - -import type { - PublicMonitor, - StatusReportWithUpdates, -} from "@openstatus/db/src/schema"; -import { Separator } from "@openstatus/ui"; - -import { notEmpty } from "@/lib/utils"; -import { StatusReport } from "./status-report"; - -export const StatusReportList = ({ - statusReports, - monitors, - filter, -}: { - statusReports: StatusReportWithUpdates[]; - monitors: PublicMonitor[]; - filter?: { date: Date; open?: boolean }; -}) => { - function getFilteredReports() { - if (!filter?.date) return statusReports; - return statusReports.filter((report) => { - if (filter.open && report.status !== "resolved") return true; - return report.statusReportUpdates.some( - (update) => update.date.getTime() > filter?.date?.getTime(), - ); - }); - } - - const reports = getFilteredReports().sort((a, b) => { - if (a.updatedAt === undefined || a.updatedAt === null) return 1; - if (b?.updatedAt === undefined || b.updatedAt === null) return -1; - return b.updatedAt.getTime() - a.updatedAt.getTime(); - }); - - if (!reports.length) { - return ; - } - - return ( -
- {reports.map((report, i) => { - const affectedMonitors = report.monitorsToStatusReports - .map(({ monitorId }) => { - const monitor = monitors.find(({ id }) => monitorId === id); - return monitor || undefined; - }) - .filter(notEmpty); - const isLast = reports.length - 1 === i; - - return ( -
- - {!isLast ? : null} -
- ); - })} -
- ); -}; - -function EmptyState() { - return ( -

- No incident reported. -

- ); -} diff --git a/apps/web/src/components/status-page/status-report.tsx b/apps/web/src/components/status-page/status-report.tsx index 85056b22..b835492e 100644 --- a/apps/web/src/components/status-page/status-report.tsx +++ b/apps/web/src/components/status-page/status-report.tsx @@ -3,109 +3,140 @@ import { ChevronRight } from "lucide-react"; import Link from "next/link"; import { useParams } from "next/navigation"; -import { Fragment } from "react"; import type { PublicMonitor, StatusReportWithUpdates, } from "@openstatus/db/src/schema"; -import { Badge, Button } from "@openstatus/ui"; +import { Badge } from "@openstatus/ui"; import { setPrefixUrl } from "@/app/status-page/[domain]/utils"; +import { statusDict } from "@/data/incidents-dictionary"; import { cn } from "@/lib/utils"; -import { StatusBadge } from "../status-update/status-badge"; +import { Icons } from "../icons"; import { DateTimeTooltip } from "./datetime-tooltip"; import { ProcessMessage } from "./process-message"; function StatusReport({ report, monitors, + actions, + isDemo, }: { report: StatusReportWithUpdates; monitors: PublicMonitor[]; + actions?: React.ReactNode; + isDemo?: boolean; }) { - return ( -
-
- - + const params = useParams<{ domain: string }>(); + + if (isDemo) { + return ( +
+ +
- -
- ); -} + ); + } -function StatusReportHeader({ report }: { report: StatusReportWithUpdates }) { - const params = useParams<{ domain: string }>(); return ( -
-

{report.title}

- {report.id ? ( - - ) : null} -
+ +
+ + +
+ ); } -function StatusReportDescription({ - report, +// REMINDER: we had the report?.id in the link href to features page to be clickable +function StatusReportHeader({ + title, monitors, - className, + actions, }: { - report: StatusReportWithUpdates; + title: StatusReportWithUpdates["title"]; monitors: PublicMonitor[]; - className?: string; + actions?: React.ReactNode; }) { - const firstReport = - report.statusReportUpdates[report.statusReportUpdates.length - 1]; - return ( -
-

- Started at -

- • - - {monitors.length > 0 ? ( - <> - • +
+
+

{title}

+
    {monitors.map((monitor) => ( - - {monitor.name} - +
  • + {monitor.name} +
  • ))} - - ) : null} +
+
+
+ {/* NOT IDEAL BUT IT WORKS */} + {actions ? ( + actions + ) : ( + + )} +
); } -// reports are already `orderBy: desc(report.date)` within the query itself -function StatusReportUpdates({ report }: { report: StatusReportWithUpdates }) { +interface StatusReportUpdatesProps { + updates: { + date?: Date; + id: number; + status: + | "investigating" + | "identified" + | "monitoring" + | "resolved" + | "maintenance"; + message: string; + }[]; +} + +function StatusReportUpdates({ updates }: StatusReportUpdatesProps) { return ( -
- {report.statusReportUpdates.map((update) => { +
+ {updates.map((update, i) => { + const { icon, label, color } = statusDict[update.status]; + const StatusIcon = Icons[icon]; return ( - -
-

{update.status}

-

- -

+
+
+
+ +
+ {i !== updates.length - 1 ? ( +
+ ) : null}
-
- +
+
+

{label}

+ {update.date ? ( +

+ +

+ ) : null} +
+
+ +
- +
); })}
@@ -113,12 +144,6 @@ function StatusReportUpdates({ report }: { report: StatusReportWithUpdates }) { } StatusReport.Header = StatusReportHeader; -StatusReport.Description = StatusReportDescription; StatusReport.Updates = StatusReportUpdates; -export { - StatusReport, - StatusReportDescription, - StatusReportHeader, - StatusReportUpdates, -}; +export { StatusReport, StatusReportHeader, StatusReportUpdates }; diff --git a/apps/web/src/components/status-update/status-badge.tsx b/apps/web/src/components/status-update/status-badge.tsx index 34b9bad2..36d08d19 100644 --- a/apps/web/src/components/status-update/status-badge.tsx +++ b/apps/web/src/components/status-update/status-badge.tsx @@ -5,13 +5,6 @@ import { statusDict } from "@/data/incidents-dictionary"; import { cn } from "@/lib/utils"; import { Icons } from "../icons"; -const variant = { - investigating: "border-rose-500/20 bg-rose-500/10 text-rose-500", - identified: "border-amber-500/20 bg-amber-500/10 text-amber-500", - monitoring: "border-blue-500/20 bg-blue-500/10 text-blue-500", - resolved: "border-green-500/20 bg-green-500/10 text-green-500", -} satisfies Record; - export function StatusBadge({ status, className, @@ -19,13 +12,10 @@ export function StatusBadge({ status: StatusReport["status"]; className?: string; }) { - const { label, icon } = statusDict[status]; + const { label, icon, color } = statusDict[status]; const Icon = Icons[icon]; return ( - + {label} diff --git a/apps/web/src/components/tracker/tracker.tsx b/apps/web/src/components/tracker/tracker.tsx index 6905ed5c..1f3bcee2 100644 --- a/apps/web/src/components/tracker/tracker.tsx +++ b/apps/web/src/components/tracker/tracker.tsx @@ -35,18 +35,18 @@ import { cn } from "@/lib/utils"; const tracker = cva("h-10 rounded-full flex-1", { variants: { variant: { - blacklist: "bg-green-500/80 data-[state=open]:bg-green-500", + blacklist: + "bg-status-operational/80 data-[state=open]:bg-status-operational", ...classNames, }, report: { - 0: "", - // IDEA: data-[state=open]:from-40% data-[state=open]:to-40% - 30: "bg-gradient-to-t from-blue-500/90 hover:from-blue-500 from-30% to-transparent to-30%", + false: "", + true: classNames.degraded, }, }, defaultVariants: { variant: "empty", - report: 0, + report: false, }, }); @@ -132,7 +132,7 @@ export const Bar = ({ const [open, setOpen] = React.useState(false); const rootClassName = tracker({ - report: statusReports && statusReports.length > 0 ? 30 : undefined, + report: statusReports.length > 0, variant: blacklist ? "blacklist" : variant, }); @@ -167,10 +167,12 @@ export const Bar = ({

- {count} requests + {count}{" "} + requests

- {count - ok} failed + {count - ok}{" "} + failed

@@ -201,11 +203,11 @@ export function StatusReportList({ reports }: { reports: StatusReport[] }) {
  • {report.title} - +
  • ))} @@ -247,7 +249,7 @@ export function DowntimeText({ return (

    - Down for{" "} + Downtime for{" "} {formatDuration( { minutes, hours, days }, { format: ["days", "hours", "minutes", "seconds"], zero: false }, diff --git a/apps/web/src/content/changelog/status-page-colors-and-more.mdx b/apps/web/src/content/changelog/status-page-colors-and-more.mdx new file mode 100644 index 00000000..e090df78 --- /dev/null +++ b/apps/web/src/content/changelog/status-page-colors-and-more.mdx @@ -0,0 +1,12 @@ +--- +title: Status Page rework +description: New status specific colors, improved navigation and more. +publishedAt: 2024-07-21 +image: /assets/changelog/status-page-colors-and-more.png +--- + +We have reworked our status page! + +- **less flashy colors** for _operational_, _degraded_, _downtime_ and _maintenance_ +- **new sticky navigation** bar, merging _maintenances_ and _incidents_ into an _events_ page (your old links will still work) +- redesign of **status reports** \ No newline at end of file diff --git a/apps/web/src/data/incidents-dictionary.ts b/apps/web/src/data/incidents-dictionary.ts index c6d3ceb0..d51ef8e2 100644 --- a/apps/web/src/data/incidents-dictionary.ts +++ b/apps/web/src/data/incidents-dictionary.ts @@ -3,24 +3,40 @@ export const statusDict = { value: "investigating", label: "Investigating", icon: "search", + color: "border-status-down/20 bg-status-down/10 text-status-down", order: 1, }, identified: { value: "identified", label: "Identified", icon: "fingerprint", + color: + "border-status-degraded/20 bg-status-degraded/10 text-status-degraded", order: 2, }, monitoring: { value: "monitoring", label: "Monitoring", icon: "activity", + color: + "border-status-monitoring/20 bg-status-monitoring/10 text-status-monitoring", order: 3, }, resolved: { value: "resolved", label: "Resolved", icon: "search-check", + color: + "border-status-operational/20 bg-status-operational/10 text-status-operational", order: 4, }, + // FIXME: check source of thruth + maintenance: { + value: "maintenance", + label: "Maintenance", + icon: "hammer", + color: + "border-status-monitoring/20 bg-status-monitoring/10 text-status-monitoring", + order: 0, + }, } as const; diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css index 31bd5203..6b884836 100644 --- a/apps/web/src/styles/globals.css +++ b/apps/web/src/styles/globals.css @@ -34,6 +34,12 @@ --ring: 215 20.2% 65.1%; --radius: 0.5rem; + + /* Status Tracker Colors - Radix Color */ + --status-degraded: 26 100% 56%; /* Orange 10 */ + --status-operational: 131 39% 51%; /* Grass 10 */ + --status-down: 11 82% 59%; /* Tomato 10 */ + --status-monitoring: 210 100% 62%; /* Blue 10 */ } .dark { @@ -65,6 +71,12 @@ --destructive-foreground: 0 85.7% 97.3%; --ring: 217.2 32.6% 17.5%; + + /* Status Tracker Colors - Radix Color */ + --status-degraded: 26 100% 56%; /* Orange 10 */ + --status-operational: 131 39% 51%; /* Grass 10 */ + --status-down: 11 82% 59%; /* Tomato 10 */ + --status-monitoring: 210 100% 62%; /* Blue 10 */ } } diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index fceea32e..a9cf6491 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -69,6 +69,21 @@ module.exports = { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, + /* Status Tracker */ + status: { + operational: { + DEFAULT: "hsl(var(--status-operational))", + }, + degraded: { + DEFAULT: "hsl(var(--status-degraded))", + }, + down: { + DEFAULT: "hsl(var(--status-down))", + }, + monitoring: { + DEFAULT: "hsl(var(--status-monitoring))", + }, + }, /* Tremor */ // light mode tremor: { diff --git a/packages/api/src/router/page.ts b/packages/api/src/router/page.ts index 0265ba85..48623e04 100644 --- a/packages/api/src/router/page.ts +++ b/packages/api/src/router/page.ts @@ -269,33 +269,15 @@ export const pageRouter = createTRPCRouter({ ({ monitors_to_pages }) => monitors_to_pages.monitorId, ); - const monitorsToStatusReportResult = - monitorsId.length > 0 - ? await opts.ctx.db - .select() - .from(monitorsToStatusReport) - .where(inArray(monitorsToStatusReport.monitorId, monitorsId)) - .all() - : []; - - const monitorStatusReportIds = monitorsToStatusReportResult.map( - ({ statusReportId }) => statusReportId, - ); - - const statusReportIds = Array.from(new Set([...monitorStatusReportIds])); - - const statusReports = - statusReportIds.length > 0 - ? await opts.ctx.db.query.statusReport.findMany({ - where: eq(statusReport.pageId, result.id), - with: { - statusReportUpdates: { - orderBy: (reports, { desc }) => desc(reports.date), - }, - monitorsToStatusReports: { with: { monitor: true } }, - }, - }) - : []; + const statusReports = await opts.ctx.db.query.statusReport.findMany({ + where: eq(statusReport.pageId, result.id), + with: { + statusReportUpdates: { + orderBy: (reports, { desc }) => desc(reports.date), + }, + monitorsToStatusReports: { with: { monitor: true } }, + }, + }); // TODO: monitorsToPagesResult has the result already, no need to query again const monitors = diff --git a/packages/tracker/src/config.ts b/packages/tracker/src/config.ts index 2b3f8685..9e6a887c 100644 --- a/packages/tracker/src/config.ts +++ b/packages/tracker/src/config.ts @@ -41,10 +41,13 @@ export const statusDetails: Record = { // REMINDER: add `@openstatus/tracker/src/**/*.ts into tailwindcss content prop */ export const classNames: Record = { - up: "bg-green-500/90 data-[state=open]:bg-green-500 border-green-500", - degraded: "bg-amber-500/90 data-[state=open]:bg-amber-500 border-amber-500", - down: "bg-red-500/90 data-[state=open]:bg-red-500 border-red-500", + up: "bg-status-operational/90 data-[state=open]:bg-status-operational border-status-operational", + degraded: + "bg-status-degraded/90 data-[state=open]:bg-status-degraded border-status-degraded", + down: "bg-status-down/90 data-[state=open]:bg-status-down border-status-down", empty: "bg-muted-foreground/20 data-[state=open]:bg-muted-foreground/30", - incident: "bg-red-500/90 data-[state=open]:bg-red-500 border-red-500", - maintenance: "bg-blue-500/90 data-[state=open]:bg-blue-500 border-blue-500", + incident: + "bg-status-down/90 data-[state=open]:bg-status-down border-status-down", + maintenance: + "bg-status-monitoring/90 data-[state=open]:bg-status-monitoring border-status-monitoring", }; diff --git a/packages/tracker/src/tracker.ts b/packages/tracker/src/tracker.ts index f3ccc1f6..6384bd7f 100644 --- a/packages/tracker/src/tracker.ts +++ b/packages/tracker/src/tracker.ts @@ -187,14 +187,21 @@ export class Tracker { const isMissingData = props.count === 0; - // FIXME: + /** + * 1. Maintenance + * 2. Status Reports (Degraded Performance) + * 3. Incidents + * 4. Uptime Status (Operational, Degraded Performance, Partial Outage, Major Outage) + */ const status = maintenances.length ? Status.UnderMaintenance - : incidents.length - ? Status.Incident - : isMissingData - ? Status.Unknown - : this.calculateUptimeStatus([props]); + : statusReports.length + ? Status.DegradedPerformance + : incidents.length + ? Status.Incident + : isMissingData + ? Status.Unknown + : this.calculateUptimeStatus([props]); const variant = statusDetails[status].variant; const label = statusDetails[status].short;